From 3d202a04b99f0e61b6ccf8b7a5610e1a15ca58e7 Mon Sep 17 00:00:00 2001 From: Herbert Eiselt Date: Mon, 11 Feb 2019 14:54:12 +0100 Subject: Add sdnr wt odlux Add complete sdnr wireless transport app odlux core and apps Change-Id: I5dcbfb8f3b790e3bda7c8df67bd69d81958f65e5 Issue-ID: SDNC-576 Signed-off-by: Herbert Eiselt --- .../odlux/framework/src/actions/authentication.ts | 8 + .../wt/odlux/framework/src/actions/errorActions.ts | 25 + .../framework/src/actions/navigationActions.ts | 47 ++ .../odlux/framework/src/actions/snackbarActions.ts | 20 + .../wt/odlux/framework/src/actions/titleActions.ts | 10 + sdnr/wt/odlux/framework/src/app.css | 5 + sdnr/wt/odlux/framework/src/app.tsx | 82 ++ .../framework/src/assets/images/defaultLogo.svg | 179 +++++ .../src/assets/images/defaultLogo.svg.d.ts | 2 + sdnr/wt/odlux/framework/src/common/event.ts | 62 ++ .../framework/src/components/errorDisplay.tsx | 105 +++ sdnr/wt/odlux/framework/src/components/logo.tsx | 81 ++ .../src/components/material-table/columnModel.ts | 27 + .../src/components/material-table/index.tsx | 423 ++++++++++ .../src/components/material-table/tableFilter.tsx | 67 ++ .../src/components/material-table/tableHead.tsx | 84 ++ .../src/components/material-table/tableToolbar.tsx | 131 ++++ .../src/components/material-table/utilities.ts | 207 +++++ .../framework/src/components/material-ui/index.ts | 3 + .../src/components/material-ui/listItemLink.tsx | 50 ++ .../framework/src/components/material-ui/panel.tsx | 56 ++ .../src/components/material-ui/snackDisplay.tsx | 57 ++ .../src/components/material-ui/toggleButton.tsx | 159 ++++ .../components/material-ui/toggleButtonGroup.tsx | 19 + .../src/components/material-ui/treeView.tsx | 251 ++++++ .../framework/src/components/navigationMenu.tsx | 59 ++ .../framework/src/components/routing/appFrame.tsx | 37 + .../wt/odlux/framework/src/components/titleBar.tsx | 125 +++ sdnr/wt/odlux/framework/src/design/default.ts | 53 ++ sdnr/wt/odlux/framework/src/favicon.ico | Bin 0 -> 1150 bytes sdnr/wt/odlux/framework/src/flux/action.ts | 8 + sdnr/wt/odlux/framework/src/flux/connect.ts | 148 ++++ sdnr/wt/odlux/framework/src/flux/middleware.ts | 90 +++ sdnr/wt/odlux/framework/src/flux/store.ts | 81 ++ .../src/handlers/applicationRegistryHandler.ts | 14 + .../src/handlers/applicationStateHandler.ts | 70 ++ .../src/handlers/authenticationHandler.ts | 33 + .../src/handlers/navigationStateHandler.ts | 28 + sdnr/wt/odlux/framework/src/index.dev.html | 28 + sdnr/wt/odlux/framework/src/index.html | 24 + sdnr/wt/odlux/framework/src/middleware/api.ts | 55 ++ sdnr/wt/odlux/framework/src/middleware/logger.ts | 17 + .../odlux/framework/src/middleware/navigation.ts | 53 ++ sdnr/wt/odlux/framework/src/middleware/thunk.ts | 18 + .../odlux/framework/src/models/applicationInfo.ts | 31 + .../odlux/framework/src/models/authentication.ts | 50 ++ .../wt/odlux/framework/src/models/elasticSearch.ts | 22 + sdnr/wt/odlux/framework/src/models/errorInfo.ts | 11 + .../odlux/framework/src/models/iconDefinition.ts | 4 + sdnr/wt/odlux/framework/src/models/index.ts | 1 + sdnr/wt/odlux/framework/src/models/restService.ts | 30 + sdnr/wt/odlux/framework/src/models/snackbarItem.ts | 3 + sdnr/wt/odlux/framework/src/run.ts | 1 + .../odlux/framework/src/services/applicationApi.ts | 25 + .../framework/src/services/applicationManager.ts | 36 + .../src/services/authenticationService.ts | 16 + sdnr/wt/odlux/framework/src/services/index.ts | 4 + .../framework/src/services/notificationService.ts | 137 ++++ .../framework/src/services/restAccessorService.ts | 76 ++ .../wt/odlux/framework/src/services/restService.ts | 29 + .../framework/src/services/snackbarService.ts | 5 + .../odlux/framework/src/store/applicationStore.ts | 56 ++ sdnr/wt/odlux/framework/src/styles/att.ts | 29 + .../odlux/framework/src/utilities/elasticSearch.ts | 70 ++ .../framework/src/utilities/withComponents.ts | 20 + sdnr/wt/odlux/framework/src/views/about.tsx | 859 +++++++++++++++++++++ sdnr/wt/odlux/framework/src/views/frame.tsx | 85 ++ sdnr/wt/odlux/framework/src/views/home.tsx | 38 + sdnr/wt/odlux/framework/src/views/login.tsx | 145 ++++ 69 files changed, 4884 insertions(+) create mode 100644 sdnr/wt/odlux/framework/src/actions/authentication.ts create mode 100644 sdnr/wt/odlux/framework/src/actions/errorActions.ts create mode 100644 sdnr/wt/odlux/framework/src/actions/navigationActions.ts create mode 100644 sdnr/wt/odlux/framework/src/actions/snackbarActions.ts create mode 100644 sdnr/wt/odlux/framework/src/actions/titleActions.ts create mode 100644 sdnr/wt/odlux/framework/src/app.css create mode 100644 sdnr/wt/odlux/framework/src/app.tsx create mode 100644 sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg create mode 100644 sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg.d.ts create mode 100644 sdnr/wt/odlux/framework/src/common/event.ts create mode 100644 sdnr/wt/odlux/framework/src/components/errorDisplay.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/logo.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/index.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-table/utilities.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/index.ts create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/navigationMenu.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx create mode 100644 sdnr/wt/odlux/framework/src/components/titleBar.tsx create mode 100644 sdnr/wt/odlux/framework/src/design/default.ts create mode 100644 sdnr/wt/odlux/framework/src/favicon.ico create mode 100644 sdnr/wt/odlux/framework/src/flux/action.ts create mode 100644 sdnr/wt/odlux/framework/src/flux/connect.ts create mode 100644 sdnr/wt/odlux/framework/src/flux/middleware.ts create mode 100644 sdnr/wt/odlux/framework/src/flux/store.ts create mode 100644 sdnr/wt/odlux/framework/src/handlers/applicationRegistryHandler.ts create mode 100644 sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts create mode 100644 sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts create mode 100644 sdnr/wt/odlux/framework/src/handlers/navigationStateHandler.ts create mode 100644 sdnr/wt/odlux/framework/src/index.dev.html create mode 100644 sdnr/wt/odlux/framework/src/index.html create mode 100644 sdnr/wt/odlux/framework/src/middleware/api.ts create mode 100644 sdnr/wt/odlux/framework/src/middleware/logger.ts create mode 100644 sdnr/wt/odlux/framework/src/middleware/navigation.ts create mode 100644 sdnr/wt/odlux/framework/src/middleware/thunk.ts create mode 100644 sdnr/wt/odlux/framework/src/models/applicationInfo.ts create mode 100644 sdnr/wt/odlux/framework/src/models/authentication.ts create mode 100644 sdnr/wt/odlux/framework/src/models/elasticSearch.ts create mode 100644 sdnr/wt/odlux/framework/src/models/errorInfo.ts create mode 100644 sdnr/wt/odlux/framework/src/models/iconDefinition.ts create mode 100644 sdnr/wt/odlux/framework/src/models/index.ts create mode 100644 sdnr/wt/odlux/framework/src/models/restService.ts create mode 100644 sdnr/wt/odlux/framework/src/models/snackbarItem.ts create mode 100644 sdnr/wt/odlux/framework/src/run.ts create mode 100644 sdnr/wt/odlux/framework/src/services/applicationApi.ts create mode 100644 sdnr/wt/odlux/framework/src/services/applicationManager.ts create mode 100644 sdnr/wt/odlux/framework/src/services/authenticationService.ts create mode 100644 sdnr/wt/odlux/framework/src/services/index.ts create mode 100644 sdnr/wt/odlux/framework/src/services/notificationService.ts create mode 100644 sdnr/wt/odlux/framework/src/services/restAccessorService.ts create mode 100644 sdnr/wt/odlux/framework/src/services/restService.ts create mode 100644 sdnr/wt/odlux/framework/src/services/snackbarService.ts create mode 100644 sdnr/wt/odlux/framework/src/store/applicationStore.ts create mode 100644 sdnr/wt/odlux/framework/src/styles/att.ts create mode 100644 sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts create mode 100644 sdnr/wt/odlux/framework/src/utilities/withComponents.ts create mode 100644 sdnr/wt/odlux/framework/src/views/about.tsx create mode 100644 sdnr/wt/odlux/framework/src/views/frame.tsx create mode 100644 sdnr/wt/odlux/framework/src/views/home.tsx create mode 100644 sdnr/wt/odlux/framework/src/views/login.tsx (limited to 'sdnr/wt/odlux/framework/src') diff --git a/sdnr/wt/odlux/framework/src/actions/authentication.ts b/sdnr/wt/odlux/framework/src/actions/authentication.ts new file mode 100644 index 000000000..8cbc22271 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/actions/authentication.ts @@ -0,0 +1,8 @@ +import { Action } from '../flux/action'; + +export class UpdateAuthentication extends Action { + + constructor(public bearerToken: string | null) { + super(); + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/actions/errorActions.ts b/sdnr/wt/odlux/framework/src/actions/errorActions.ts new file mode 100644 index 000000000..05d1868b1 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/actions/errorActions.ts @@ -0,0 +1,25 @@ +import { Action } from '../flux/action'; + +import { ErrorInfo } from '../models/errorInfo'; +export { ErrorInfo } from '../models/errorInfo'; + +export class AddErrorInfoAction extends Action { + + constructor(public errorInfo: ErrorInfo) { + super(); + } +} + +export class RemoveErrorInfoAction extends Action { + + constructor(public errorInfo: ErrorInfo) { + super(); + } +} + +export class ClearErrorInfoAction extends Action { + + constructor() { + super(); + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/actions/navigationActions.ts b/sdnr/wt/odlux/framework/src/actions/navigationActions.ts new file mode 100644 index 000000000..3aee9e455 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/actions/navigationActions.ts @@ -0,0 +1,47 @@ +import { Action } from "../flux/action"; + +export abstract class NavigationAction extends Action { } + +export class NavigateToApplication extends NavigationAction { + + constructor(public applicationName: string, public href?: string, public state?: TState, public replace: boolean = false ) { + super(); + + } +} + +export class PushAction extends NavigationAction { + constructor(public href: string, public state?: TState) { + super(); + + } +} + +export class ReplaceAction extends NavigationAction { + constructor(public href: string, public state?: TState) { + super(); + + } +} + +export class GoAction extends NavigationAction { + constructor(public index: number) { + super(); + + } +} + +export class GoBackAction extends NavigationAction { + +} + +export class GoForwardeAction extends NavigationAction { + +} + +export class LocationChanged extends NavigationAction { + constructor(public pathname: string, public search: string, public hash: string ) { + super(); + + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/actions/snackbarActions.ts b/sdnr/wt/odlux/framework/src/actions/snackbarActions.ts new file mode 100644 index 000000000..c5bf9278b --- /dev/null +++ b/sdnr/wt/odlux/framework/src/actions/snackbarActions.ts @@ -0,0 +1,20 @@ +import { Action } from '../flux/action'; +import { SnackbarItem } from '../models/snackbarItem'; +import { Omit } from '@material-ui/core'; + +export class AddSnackbarNotification extends Action { + + constructor(notification: Omit) { + super(); + + this.notification = { ...notification, key: (new Date().getTime() + Math.random()) } + } + + public notification: SnackbarItem +} + +export class RemoveSnackbarNotification extends Action { + constructor(public key: number) { + super(); + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/actions/titleActions.ts b/sdnr/wt/odlux/framework/src/actions/titleActions.ts new file mode 100644 index 000000000..b641bab95 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/actions/titleActions.ts @@ -0,0 +1,10 @@ +import { Action } from '../flux/action'; + +import { IconType } from '../models/iconDefinition'; + +export class SetTitleAction extends Action { + + constructor(public title: string, public icon?: IconType) { + super(); + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/app.css b/sdnr/wt/odlux/framework/src/app.css new file mode 100644 index 000000000..f3d3b7aab --- /dev/null +++ b/sdnr/wt/odlux/framework/src/app.css @@ -0,0 +1,5 @@ +html, body, #app { + height: 100%; + padding: 0px; + margin: 0px; +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/app.tsx b/sdnr/wt/odlux/framework/src/app.tsx new file mode 100644 index 000000000..1879c7bc6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/app.tsx @@ -0,0 +1,82 @@ +/****************************************************************************** + * Copyright 2018 highstreet technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { MuiThemeProvider } from '@material-ui/core/styles'; + +import { Frame } from './views/frame'; + +import { AddErrorInfoAction } from './actions/errorActions'; + +import { applicationStoreCreator } from './store/applicationStore'; +import { ApplicationStoreProvider } from './flux/connect'; + +import { startHistoryListener } from './middleware/navigation'; + +import theme from './design/default'; +import '!style-loader!css-loader!./app.css'; + +declare module '@material-ui/core/styles/createMuiTheme' { + + interface IDesign { + id: string, + name: string, + url: string, // image url of a company logo, which will be presented in the ui header + height: number, // image height [px] as delivered by the url + width: number, // image width [px] as delivered by the url + logoHeight: number // height in [px] of the logo (see url) within the ui header + } + + interface Theme { + design?: IDesign + } + interface ThemeOptions { + design?: IDesign + } +} + +export const runApplication = () => { + const applicationStore = applicationStoreCreator(); + + window.onerror = function (msg: string, url: string, line: number, col: number, error: Error) { + // Note that col & error are new to the HTML 5 spec and may not be + // supported in every browser. It worked for me in Chrome. + var extra = !col ? '' : '\ncolumn: ' + col; + extra += !error ? '' : '\nerror: ' + error; + + // You can view the information in an alert to see things working like this: + applicationStore.dispatch(new AddErrorInfoAction({ error, message: msg, url, line, col, info: { extra } })); + + var suppressErrorAlert = true; + // If you return true, then error alerts (like in older versions of + // Internet Explorer) will be suppressed. + return suppressErrorAlert; + }; + + startHistoryListener(applicationStore); + + const App = (): JSX.Element => ( + + + + + + ); + + ReactDOM.render(, document.getElementById('app')); +}; diff --git a/sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg b/sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg new file mode 100644 index 000000000..bd9ddf583 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg @@ -0,0 +1,179 @@ + +image/svg+xml \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg.d.ts b/sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg.d.ts new file mode 100644 index 000000000..f03ed0182 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/assets/images/defaultLogo.svg.d.ts @@ -0,0 +1,2 @@ +declare const path: string; +export default path; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/common/event.ts b/sdnr/wt/odlux/framework/src/common/event.ts new file mode 100644 index 000000000..f71b0164a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/common/event.ts @@ -0,0 +1,62 @@ +/** + * Represents an event. + * Events enable a class or object to notify other classes or objects when something of interest occurs. + * The class that sends (or invokes) the event is called the publisher and the classes that receive (or handle) the event are called subscribers. + * + * Objects can create an instances of an Events and offer that Events for other objects to attach to. + * Objects who want to be informed about an Event can attach a function (an event handler) to the event which is then called when the event is fired. + * + * @template TEventArg Type of the event argument. Use void if the event does not has an argument. + */ +export class Event { + + /** + * Creates a new instance of the Event class. + */ + constructor() { + this.eventHandlers = new Array<(arg: TEventArg) => void>(); + } + + /** + * Adds an event handler to this event, so that when the event is fired the given event handler function is called. + * + * @param eventHandler The event handler function to add to this event. + * @throws {Error} Thrown if the given event handler function has already been added to this event. + */ + public addHandler = (eventHandler: (arg: TEventArg) => void): void => { + if (this.eventHandlers.indexOf(eventHandler) > -1) { + throw new Error("The given event handler is already added to this event."); + } + + this.eventHandlers.push(eventHandler); + } + + /** + * Removes an event handler from this event, so that the given event handler function will not be called anymore when the event is fired. + * + * @param eventHandler: The event handler function to remove. + * @throws {Error} Thrown if the given event handler function has not been added to this event before. + */ + public removeHandler = (eventHandler: (arg: TEventArg) => void): void => { + const index = this.eventHandlers.indexOf(eventHandler); + if (!(index > -1)) { + throw new Error("The given event handler has not been added to this event yet."); + } + + this.eventHandlers.splice(index, 1); + } + + /** + * Invokes the event and calls all event handler functions currently registered on the event. + * + * @param argument The argument for the event. The argument will be passed to all registered event handler functions. + */ + public invoke = (argument?: TEventArg): void => { + this.eventHandlers.forEach((eventHandler: (arg?: TEventArg) => void, index: number, array: Array<(arg: TEventArg) => void>): void => { + eventHandler(argument); + }); + } + + private eventHandlers: Array<(arg?: TEventArg) => void>; + +} diff --git a/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx b/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx new file mode 100644 index 000000000..b5f8f385d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/errorDisplay.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import Modal from '@material-ui/core/Modal'; +import Button from '@material-ui/core/Button'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import Typography from '@material-ui/core/Typography'; + +import { ClearErrorInfoAction, RemoveErrorInfoAction } from '../actions/errorActions'; + +import connect, { Connect } from '../flux/connect'; + +const styles = (theme: Theme) => createStyles({ + modal: { + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + paper: { + width: theme.spacing.unit * 50, + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[5], + padding: theme.spacing.unit * 4, + }, + card: { + minWidth: 275, + }, + bullet: { + display: 'inline-block', + margin: '0 2px', + transform: 'scale(0.8)', + }, + title: { + marginBottom: 16, + fontSize: 14, + }, + pos: { + marginBottom: 12, + }, +}); + +type ErrorDisplayProps = WithStyles & Connect; + +// function getModalStyle() { +// const top = 50 + rand(); +// const left = 50 + rand(); + +// return { +// top: `${ top }%`, +// left: `${ left }%`, +// transform: `translate(-${ top }%, -${ left }%)`, +// }; +// } + +/** + * Represents a compnent for formaing and displaying errors. + */ +class ErrorDisplayComponent extends React.Component { + render(): JSX.Element { + const { classes, state } = this.props; + const errorInfo = state.framework.applicationState.errors.length && state.framework.applicationState.errors[state.framework.applicationState.errors.length - 1]; + return ( + 0 } + onClose={ () => this.props.dispatch(new ClearErrorInfoAction()) } + > + { errorInfo && +
+ + + + Something went wrong. + + + { errorInfo.error && errorInfo.error.toString() } + + + { errorInfo.message && errorInfo.message .toString() } + + + { errorInfo.info && errorInfo.info.componentStack && errorInfo.info.componentStack.split('\n').map(line => { + return [line,
]; + }) } + { errorInfo.info && errorInfo.info.extra && errorInfo.info.extra.split('\n').map(line => { + return [line,
]; + }) } +
+
+ + + +
+
|| null + } +
+ ); + } +} + +export const ErrorDisplay = withStyles(styles)(connect()(ErrorDisplayComponent)); +export default ErrorDisplay; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/logo.tsx b/sdnr/wt/odlux/framework/src/components/logo.tsx new file mode 100644 index 000000000..95c06a30c --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/logo.tsx @@ -0,0 +1,81 @@ +/****************************************************************************** + * Copyright 2018 highstreet technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; // infra for styling + + +import defaultLogo from '../assets/images/defaultLogo.svg'; + +const styles = (theme: Theme) => createStyles({ + headerLogo: { + backgroundImage: "url(" + (theme.design && theme.design.url || defaultLogo) + ")", + backgroundColor: theme.palette.primary.main, + backgroundRepeat: "no-repeat", + backgroundSize: "auto " + (theme.design && theme.design.logoHeight || 70) + "px", + height: theme.design && theme.design.logoHeight || 70, + width: theme.design ? theme.design.width / theme.design.height * theme.design.logoHeight : 220 + } +}); + +type LogoProps = RouteComponentProps<{ id: string }> & WithStyles; +interface ILogoState { + windowWidth: number +} + +class LogoComponent extends React.Component { + + private hideLogoWhenWindowWidthIsLower: number = 800; + + constructor(props: LogoProps) { + super(props); + this.state = { + windowWidth: 0 + }; + this.updateWindowDimensions = this.updateWindowDimensions.bind(this); + } + + componentDidMount(): void { + this.updateWindowDimensions(); + window.addEventListener('resize', this.updateWindowDimensions); + }; + componentWillUnmount(): void { + window.removeEventListener('resize', this.updateWindowDimensions); + }; + updateWindowDimensions(): void { + this.setState({ windowWidth: window.innerWidth }); + } + + render(): JSX.Element { + let div: JSX.Element =
; + if (this.state.windowWidth >= this.hideLogoWhenWindowWidthIsLower) { + div =
; + } else { + console.info([ + "Logo hidden, because browser window width (", + this.state.windowWidth, + "px) is lower thershold (", + this.hideLogoWhenWindowWidthIsLower, + "px)."].join('')); + } + return div; + } +} + +export const Logo = withStyles(styles)(withRouter(LogoComponent)); +export default Logo; diff --git a/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts b/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts new file mode 100644 index 000000000..6acea01d5 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/columnModel.ts @@ -0,0 +1,27 @@ + +import * as React from 'react'; + +export enum ColumnType { + text, + numeric, + custom +} + +type CustomControl = { + rowData: TData +} + +export type ColumnModel = { + title?: string; + disablePadding?: boolean; + width?: string | number; + disableSorting?: boolean; + disableFilter?: boolean; +} & ({ + property: string; + type: ColumnType.custom; + customControl: React.ComponentType>; +} | { + property: keyof TData; + type?: ColumnType.numeric | ColumnType.text; +}); \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/index.tsx b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx new file mode 100644 index 000000000..3b906cfbb --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/index.tsx @@ -0,0 +1,423 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; +import Paper from '@material-ui/core/Paper'; +import Checkbox from '@material-ui/core/Checkbox'; + +import { TableToolbar } from './tableToolbar'; +import { EnhancedTableHead } from './tableHead'; +import { EnhancedTableFilter } from './tableFilter'; + +import { ColumnModel, ColumnType } from './columnModel'; +import { Omit } from '@material-ui/core'; +import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; +export { ColumnModel, ColumnType } from './columnModel'; + +type propType = string | number | null | undefined | (string|number)[]; +type dataType = { [prop: string]: propType }; +type resultType = { page: number, rowCount: number, rows: TData[] }; + +export type DataCallback = (page?: number, rowsPerPage?: number, orderBy?: string | null, order?: 'asc' | 'desc' | null, filter?: { [property: string]: string }) =>resultType | Promise>; + +function desc(a: dataType, b: dataType, orderBy: string) { + if ((b[orderBy] || "") < (a[orderBy] || "") ) { + return -1; + } + if ((b[orderBy] || "") > (a[orderBy] || "") ) { + return 1; + } + return 0; +} + +function stableSort(array: dataType[], cmp: (a: dataType, b: dataType) => number) { + const stabilizedThis = array.map((el, index) => [el, index]) as [dataType, number][]; + stabilizedThis.sort((a, b) => { + const order = cmp(a[0], b[0]); + if (order !== 0) return order; + return a[1] - b[1]; + }); + return stabilizedThis.map(el => el[0]); +} + +function getSorting(order: 'asc' | 'desc' | null, orderBy: string) { + return order === 'desc' ? (a: dataType, b: dataType) => desc(a, b, orderBy) : (a: dataType, b: dataType) => -desc(a, b, orderBy); +} + +const styles = (theme: Theme) => createStyles({ + root: { + width: '100%', + marginTop: theme.spacing.unit * 3, + }, + table: { + minWidth: 1020, + }, + tableWrapper: { + overflowX: 'auto', + }, +}); + +export type MaterialTableComponentState = { + order: 'asc' | 'desc'; + orderBy: string | null; + selected: any[] | null; + rows: TData[]; + rowCount: number; + page: number; + rowsPerPage: number; + loading: boolean; + showFilter: boolean; + filter: { [property: string]: string }; +}; + +export type TableApi = { forceRefresh?: () => Promise }; + +type MaterialTableComponentBaseProps = WithStyles & { + columns: ColumnModel[]; + idProperty: keyof TData | ((data: TData) => React.Key ); + title?: string; + enableSelection?: boolean; + disableSorting?: boolean; + disableFilter?: boolean; + customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void }[]; + onHandleClick?(event: React.MouseEvent, rowData: TData): void; +}; + +type MaterialTableComponentPropsWithRows = MaterialTableComponentBaseProps & { rows: TData[]; asynchronus?: boolean; }; +type MaterialTableComponentPropsWithRequestData = MaterialTableComponentBaseProps & { onRequestData: DataCallback; tableApi?: TableApi; }; +type MaterialTableComponentPropsWithExternalState = MaterialTableComponentBaseProps & MaterialTableComponentState & { + onToggleFilter: () => void; + onFilterChanged: (property: string, filterTerm: string) => void; + onHandleChangePage: (page: number) => void; + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => void; + onHandleRequestSort: (property: string) => void; +}; + +type MaterialTableComponentProps = + MaterialTableComponentPropsWithRows | + MaterialTableComponentPropsWithRequestData | + MaterialTableComponentPropsWithExternalState; + +function isMaterialTableComponentPropsWithRows(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRows { + return (props as MaterialTableComponentPropsWithRows).rows !== undefined && (props as MaterialTableComponentPropsWithRows).rows instanceof Array; +} + +function isMaterialTableComponentPropsWithRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithRequestData { + return (props as MaterialTableComponentPropsWithRequestData).onRequestData !== undefined && (props as MaterialTableComponentPropsWithRequestData).onRequestData instanceof Function; +} + +function isMaterialTableComponentPropsWithRowsAndRequestData(props: MaterialTableComponentProps): props is MaterialTableComponentPropsWithExternalState { + const propsWithExternalState = (props as MaterialTableComponentPropsWithExternalState) + return propsWithExternalState.onFilterChanged instanceof Function || + propsWithExternalState.onHandleChangePage instanceof Function || + propsWithExternalState.onHandleChangeRowsPerPage instanceof Function || + propsWithExternalState.onToggleFilter instanceof Function || + propsWithExternalState.onHandleRequestSort instanceof Function +} + +class MaterialTableComponent extends React.Component { + + constructor(props: MaterialTableComponentProps) { + super(props); + + const page = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.page : 0; + const rowsPerPage = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.rowsPerPage || 10 : 10; + + this.state = { + filter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.filter || {} : {}, + showFilter: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.showFilter : false, + loading: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.loading : false, + order: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.order : 'asc', + orderBy: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.orderBy : null, + selected: isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.selected : null, + rows: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) || [], + rowCount: isMaterialTableComponentPropsWithRows(this.props) && this.props.rows.length || 0, + page, + rowsPerPage, + }; + + if (isMaterialTableComponentPropsWithRequestData(this.props)) { + this.update(); + + if (this.props.tableApi) { + this.props.tableApi.forceRefresh = () => this.update(); + } + } + } + render(): JSX.Element { + const { classes, columns } = this.props; + const { rows, rowCount, order, orderBy, selected, rowsPerPage, page, showFilter, filter } = this.state; + const emptyRows = rowsPerPage - Math.min(rowsPerPage, rowCount - page * rowsPerPage); + const getId = typeof this.props.idProperty !== "function" ? (data: TData) => ((data as {[key:string]: any })[this.props.idProperty as any as string] as string | number) : this.props.idProperty; + const toggleFilter = isMaterialTableComponentPropsWithRowsAndRequestData(this.props) ? this.props.onToggleFilter : () => { !this.props.disableFilter && this.setState({ showFilter: !showFilter }, this.update) } + return ( + + +
+ + + + { showFilter && || null } + { rows // may need ordering here + .map((entry: TData & { [key: string]: any }) => { + const entryId = getId(entry); + const isSelected = this.isSelected(entryId); + return ( + this.handleClick(event, entry, entryId) } + role="checkbox" + aria-checked={ isSelected } + tabIndex={ -1 } + key={ entryId } + selected={ isSelected } + > + { this.props.enableSelection + ? + + + : null + } + { + this.props.columns.map( + col => { + const style = col.width ? { width: col.width } : {}; + return ( + + { col.type === ColumnType.custom && col.customControl + ? + : entry[col.property] + } + + ); + } + ) + } + + ); + }) } + { emptyRows > 0 && ( + + + + ) } + +
+
+ +
+ ); + } + + static getDerivedStateFromProps(props: MaterialTableComponentProps, state: MaterialTableComponentState & { _rawRows: {}[] }): MaterialTableComponentState & { _rawRows: {}[] } { + if (isMaterialTableComponentPropsWithRowsAndRequestData(props)) { + return { + ...state, + rows: props.rows, + rowCount: props.rowCount, + orderBy: props.orderBy, + order: props.order, + filter: props.filter, + loading: props.loading, + showFilter: props.showFilter, + page: props.page, + rowsPerPage: props.rowsPerPage + } + } else if (isMaterialTableComponentPropsWithRows(props) && props.asynchronus && state._rawRows !== props.rows) { + const newState = MaterialTableComponent.updateRows(props, state); + return { + ...state, + ...newState, + _rawRows: props.rows || [] + }; + } + return state; + } + + private static updateRows(props: MaterialTableComponentPropsWithRows, state: MaterialTableComponentState): { rows: {}[], rowCount: number } { + try { + const { page, rowsPerPage, order, orderBy, filter } = state; + let data: dataType[] = props.rows || []; + let filtered = false; + if (state.showFilter) { + Object.keys(filter).forEach(prop => { + const exp = filter[prop]; + filtered = filtered || !!exp; + data = exp ? data.filter((val) => { + const value = val[prop]; + return value && value.toString().indexOf(exp) > -1; + }) : data; + }); + } + + const rowCount = data.length; + + data = (orderBy && order + ? stableSort(data, getSorting(order, orderBy)) + : data).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage); + + return { + rows: data, + rowCount + }; + } catch{ + return { + rows: [], + rowCount: 0 + } + } + } + + private async update() { + if (isMaterialTableComponentPropsWithRequestData(this.props)) { + const response = await Promise.resolve( + this.props.onRequestData( + this.state.page, this.state.rowsPerPage, this.state.orderBy, this.state.order, this.state.showFilter && this.state.filter || {}) + ); + this.setState(response); + } else { + this.setState(MaterialTableComponent.updateRows(this.props, this.state)); + } + } + + private onFilterChanged = (property: string, filterTerm: string) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onFilterChanged(property, filterTerm); + return; + } + if (this.props.disableFilter) return; + const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property); + if (colDefinition && colDefinition.disableFilter) return; + + const filter = { ...this.state.filter, [property]: filterTerm }; + this.setState({ + filter + }, this.update); + }; + + private onHandleRequestSort = (event: React.SyntheticEvent, property: string) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleRequestSort(property); + return; + } + if (this.props.disableSorting) return; + const colDefinition = this.props.columns && this.props.columns.find(col => col.property === property); + if (colDefinition && colDefinition.disableSorting) return; + + const orderBy = this.state.orderBy === property && this.state.order === 'desc' ? null : property; + const order = this.state.orderBy === property && this.state.order === 'asc' ? 'desc' : 'asc'; + this.setState({ + order, + orderBy + }, this.update); + }; + + handleSelectAllClick: () => {}; + + private onHandleChangePage = (event: React.MouseEvent | null, page: number) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleChangePage(page); + return; + } + this.setState({ + page + }, this.update); + }; + + private onHandleChangeRowsPerPage = (event: React.ChangeEvent) => { + if (isMaterialTableComponentPropsWithRowsAndRequestData(this.props)) { + this.props.onHandleChangeRowsPerPage(+(event && event.target.value)); + return; + } + const rowsPerPage = +(event && event.target.value); + if (rowsPerPage && rowsPerPage > 0) { + this.setState({ + rowsPerPage + }, this.update); + } + }; + + private isSelected(id: string | number): boolean { + let selected = this.state.selected || []; + const selectedIndex = selected.indexOf(id); + return (selectedIndex > -1); + } + + private handleClick(event: React.MouseEvent, rowData: TData, id: string | number): void { + if (this.props.onHandleClick instanceof Function) { + this.props.onHandleClick(event, rowData); + return; + } + if (!this.props.enableSelection){ + return; + } + let selected = this.state.selected || []; + const selectedIndex = selected.indexOf(id); + if (selectedIndex > -1) { + selected = [ + ...selected.slice(0, selectedIndex), + ...selected.slice(selectedIndex + 1) + ]; + } else { + selected = [ + ...selected, + id + ]; + } + this.setState({ + selected + }); + } + + private exportToCsv = () => { + let file; + const data: string[] = []; + data.push(this.props.columns.map(col => col.title || col.property).join(',')+"\r\n"); + this.state.rows && this.state.rows.forEach((row : any)=> { + data.push(this.props.columns.map(col => row[col.property]).join(',') + "\r\n"); + }); + const properties = { type: 'text/csv' }; // Specify the file's mime-type. + try { + // Specify the filename using the File constructor, but ... + file = new File(data, "export.csv", properties); + } catch (e) { + // ... fall back to the Blob constructor if that isn't supported. + file = new Blob(data, properties); + } + const url = URL.createObjectURL(file); + window.location.replace(url); + } +} + +export type MaterialTableCtorType = new () => React.Component, 'classes'>>; + +export const MaterialTable = withStyles(styles)(MaterialTableComponent); +export default MaterialTable; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx new file mode 100644 index 000000000..68e47d7ee --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableFilter.tsx @@ -0,0 +1,67 @@ + +import * as React from 'react'; +import { ColumnModel, ColumnType } from './columnModel'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + + +import TableCell from '@material-ui/core/TableCell'; +import TableRow from '@material-ui/core/TableRow'; +import Input from '@material-ui/core/Input'; + + +const styles = (theme: Theme) => createStyles({ + container: { + display: 'flex', + flexWrap: 'wrap', + }, + input: { + margin: theme.spacing.unit, + }, +}); + +interface IEnhancedTableFilterComponentProps extends WithStyles { + onFilterChanged: (property: string, filterTerm: string) => void; + filter: { [property: string]: string }; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableFilterComponent extends React.Component { + createFilterHandler = (property: string) => (event: React.ChangeEvent) => { + this.props.onFilterChanged && this.props.onFilterChanged(property, event.target.value); + }; + + render() { + const { columns, filter, classes } = this.props; + return ( + + { this.props.enableSelection + ? + + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + + { col.disableFilter || (col.type === ColumnType.custom) ? null : } + + ); + }, this) } + + ); + } +} + +export const EnhancedTableFilter = withStyles(styles)(EnhancedTableFilterComponent); \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx new file mode 100644 index 000000000..5846e5e51 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableHead.tsx @@ -0,0 +1,84 @@ + +import * as React from 'react'; +import { ColumnModel, ColumnType } from './columnModel'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import TableCell from '@material-ui/core/TableCell'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import Checkbox from '@material-ui/core/Checkbox'; +import Tooltip from '@material-ui/core/Tooltip'; + +interface IEnhancedTableHeadComponentProps { + numSelected: number | null; + onRequestSort: (event: React.SyntheticEvent, property: string) => void; + onSelectAllClick: () => void; + order: 'asc' | 'desc'; + orderBy: string | null; + rowCount: number; + columns: ColumnModel<{}>[]; + enableSelection?: boolean; +} + +class EnhancedTableHeadComponent extends React.Component { + createSortHandler = (property: string) => (event: React.SyntheticEvent) => { + this.props.onRequestSort(event, property); + }; + + render() { + const { onSelectAllClick, order, orderBy, numSelected, rowCount, columns } = this.props; + + return ( + + + { this.props.enableSelection + ? + 0 && numSelected < rowCount || undefined } + checked={ numSelected === rowCount } + onChange={ onSelectAllClick } + /> + + : null + } + { columns.map(col => { + const style = col.width ? { width: col.width } : {}; + return ( + + { col.disableSorting || (col.type === ColumnType.custom) + ? + { col.title || col.property } + + : + + { col.title || col.property } + + } + + ); + }, this) } + + + ); + } +} + +export const EnhancedTableHead = EnhancedTableHeadComponent; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx new file mode 100644 index 000000000..9ee2e13c7 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/tableToolbar.tsx @@ -0,0 +1,131 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import IconButton from '@material-ui/core/IconButton'; +import Tooltip from '@material-ui/core/Tooltip'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import DeleteIcon from '@material-ui/icons/Delete'; +import MoreIcon from '@material-ui/icons/MoreVert'; +import FilterListIcon from '@material-ui/icons/FilterList'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; +import { lighten } from '@material-ui/core/styles/colorManipulator'; +import { SvgIconProps } from '@material-ui/core/SvgIcon/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + root: { + paddingRight: theme.spacing.unit, + }, + highlight: + theme.palette.type === 'light' + ? { + color: theme.palette.secondary.main, + backgroundColor: lighten(theme.palette.secondary.light, 0.85), + } + : { + color: theme.palette.text.primary, + backgroundColor: theme.palette.secondary.dark, + }, + spacer: { + flex: '1 1 100%', + }, + actions: { + color: theme.palette.text.secondary, + display: "flex", + flex: "auto", + flexDirection: "row" + }, + title: { + flex: '0 0 auto', + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, +}); + +interface ITableToolbarComponentProps extends WithStyles { + numSelected: number | null; + title?: string; + customActionButtons?: { icon: React.ComponentType, tooltip?: string, onClick: () => void }[]; + onToggleFilter: () => void; + onExportToCsv: () => void; +} + +class TableToolbarComponent extends React.Component { + constructor(props: ITableToolbarComponentProps) { + super(props); + + this.state = { + anchorEl: null + }; + } + + private handleMenu = (event: React.MouseEvent) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private handleClose = () => { + this.setState({ anchorEl: null }); + }; + render() { + const { numSelected, classes } = this.props; + const open = !!this.state.anchorEl; + + return ( + 0 ? classes.highlight : '' } ` } > +
+ { numSelected && numSelected > 0 ? ( + + { numSelected } selected + + ) : ( + + { this.props.title || null } + + ) } +
+
+
+ { this.props.customActionButtons + ? this.props.customActionButtons.map((action, ind) =>( + + action.onClick() }> + + + + )) + : null } + { numSelected && numSelected > 0 ? ( + + + + + + ) : ( + + { this.props.onToggleFilter && this.props.onToggleFilter() } }> + + + + ) } + + + + + + + Export as CSV + +
+ + ); + } +}; + +export const TableToolbar = withStyles(styles)(TableToolbarComponent); \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts new file mode 100644 index 000000000..e52fdb731 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-table/utilities.ts @@ -0,0 +1,207 @@ +import { Action, IActionHandler } from '../../flux/action'; +import { Dispatch } from '../../flux/store'; + +import { AddErrorInfoAction } from '../../actions/errorActions'; +import { IApplicationStoreState } from '../../store/applicationStore'; + +import { DataCallback } from "."; +export interface IExternalTableState { + order: 'asc' | 'desc'; + orderBy: string | null; + selected: any[] | null; + rows: TData[]; + rowCount: number; + page: number; + rowsPerPage: number; + loading: boolean; + showFilter: boolean; + filter: { [property: string]: string }; +} + +/** Create an actionHandler and actions for external table states. */ +export function createExternal(callback: DataCallback, selectState: (appState: IApplicationStoreState) => IExternalTableState) { + + //#region Actions + abstract class TableAction extends Action { } + + + class RequestSortAction extends TableAction { + constructor(public orderBy: string) { + super(); + } + } + + class SetSelectedAction extends TableAction { + constructor(public selected: TData[] | null) { + super(); + } + } + + class SetPageAction extends TableAction { + constructor(public page: number) { + super(); + } + } + + class SetRowsPerPageAction extends TableAction { + constructor(public rowsPerPage: number) { + super(); + } + } + + class SetFilterChangedAction extends TableAction { + constructor(public filter: {[key: string]: string}) { + super(); + } + } + + class SetShowFilterAction extends TableAction { + constructor(public show: boolean) { + super(); + } + } + + class RefreshAction extends TableAction { + constructor() { + super(); + } + } + + class SetResultAction extends TableAction { + constructor(public result: { page: number, rowCount: number, rows: TData[] }) { + super(); + } + } + + // #endregion + + //#region Action Handler + const externalTableStateInit: IExternalTableState = { + order: 'asc', + orderBy: null, + selected: null, + rows: [], + rowCount: 0, + page: 0, + rowsPerPage: 10, + loading: false, + showFilter: false, + filter: {} + }; + + const externalTableStateActionHandler: IActionHandler> = (state = externalTableStateInit, action) => { + if (!(action instanceof TableAction)) return state; + if (action instanceof RefreshAction) { + state = { + ...state, + loading: true + } + } else if (action instanceof SetResultAction) { + state = { + ...state, + loading: false, + rows: action.result.rows, + rowCount: action.result.rowCount, + page: action.result.page, + } + } else if (action instanceof RequestSortAction) { + state = { + ...state, + loading: true, + orderBy : state.orderBy === action.orderBy && state.order === 'desc' ? null : action.orderBy , + order: state.orderBy === action.orderBy && state.order === 'asc' ? 'desc' : 'asc', + } + } else if (action instanceof SetShowFilterAction) { + state = { + ...state, + loading: true, + showFilter: action.show + } + } else if (action instanceof SetFilterChangedAction) { + state = { + ...state, + loading: true, + filter: action.filter + } + } else if (action instanceof SetPageAction) { + state = { + ...state, + loading: true, + page: action.page + } + } else if (action instanceof SetRowsPerPageAction) { + state = { + ...state, + loading: true, + rowsPerPage: action.rowsPerPage + } + } + return state; + } + + //const createTableAction(tableAction) + + //#endregion + const reloadAction = (dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + dispatch(new RefreshAction()); + const ownState = selectState(getAppState()); + Promise.resolve(callback(ownState.page, ownState.rowsPerPage, ownState.orderBy, ownState.order, ownState.showFilter && ownState.filter || {})).then(result => { + dispatch(new SetResultAction(result)); + }).catch(error => new AddErrorInfoAction(error)); + }; + + const createActions = (dispatch: Dispatch, skipRefresh: boolean = false) => { + return { + onRefresh: () => { + dispatch(reloadAction); + }, + onHandleRequestSort: (orderBy: string) => { + dispatch((dispatch: Dispatch) => { + dispatch(new RequestSortAction(orderBy)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onToggleFilter: () => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + const { showFilter } = selectState(getAppState()); + dispatch(new SetShowFilterAction(!showFilter)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onFilterChanged: (property: string, filterTerm: string) => { + dispatch((dispatch: Dispatch, getAppState: () => IApplicationStoreState) => { + let { filter } = selectState(getAppState()); + filter = { ...filter, [property]: filterTerm }; + dispatch(new SetFilterChangedAction(filter)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onHandleChangePage: (page: number) => { + dispatch((dispatch: Dispatch) => { + dispatch(new SetPageAction(page)); + (!skipRefresh) && dispatch(reloadAction); + }); + }, + onHandleChangeRowsPerPage: (rowsPerPage: number | null) => { + dispatch((dispatch: Dispatch) => { + dispatch(new SetRowsPerPageAction(rowsPerPage || 10)); + (!skipRefresh) && dispatch(reloadAction); + }); + } + // selected: + }; + }; + + const createProperties = (state: IApplicationStoreState) => { + return { + ...selectState(state) + } + } + + return { + reloadAction: reloadAction, + createActions: createActions, + createProperties: createProperties, + actionHandler: externalTableStateActionHandler + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/index.ts b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts new file mode 100644 index 000000000..890312ce2 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/index.ts @@ -0,0 +1,3 @@ +export { ListItemLink } from './listItemLink'; +export { Panel } from './panel'; +export { ToggleButton, ToggleButtonClassKey } from './toggleButton'; diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx new file mode 100644 index 000000000..6ace59534 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/listItemLink.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import { NavLink, Link, Route } from 'react-router-dom'; + +import ListItem from '@material-ui/core/ListItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; + +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +const styles = (theme: Theme) => createStyles({ + active: { + backgroundColor: theme.palette.action.selected + } +}); + +export interface IListItemLinkProps extends WithStyles { + icon: JSX.Element | null; + primary: string | React.ComponentType; + secondary?: React.ComponentType; + to: string; + exact?: boolean; +} + +export const ListItemLink = withStyles(styles)((props: IListItemLinkProps) => { + const { icon, primary: Primary, secondary: Secondary, classes, to, exact = false } = props; + const renderLink = (itemProps: any): JSX.Element => (); + + return ( + <> + + { icon + ? { icon } + : null + } + { typeof Primary === 'string' + ? + : + } + + { Secondary + ? + : null + } + + ); + } +); + +export default ListItemLink; + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx new file mode 100644 index 000000000..0b64666c0 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/panel.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; + +import { withStyles, Theme, WithStyles, createStyles } from '@material-ui/core/styles'; + +import { ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Typography, ExpansionPanelActions } from '@material-ui/core'; + +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + accordion: { + // background: theme.palette.secondary.dark, + // color: theme.palette.primary.contrastText + }, + detail: { + // background: theme.palette.background.paper, + // color: theme.palette.text.primary, + position: "relative", + display: 'flex', + flexDirection: 'column' + }, + text: { + // color: theme.palette.common.white, + // fontSize: "1rem" + }, +}); + +type PanalProps = WithStyles & { + activePanel: string | null, + panelId: string, + title: string, + customActionButtons?: JSX.Element[]; + onToggle: (panelId: string | null) => void; +} + +const PanelComponent: React.SFC = (props) => { + const { classes, activePanel, onToggle } = props; + return ( + onToggle(props.panelId) } > + }> + { props.title } + + + { props.children } + + { props.customActionButtons + ? + { props.customActionButtons } + + : null } + + ); +}; + +export const Panel = withStyles(styles)(PanelComponent); +export default Panel; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx new file mode 100644 index 000000000..c02bf93e9 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/snackDisplay.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; + +import { IApplicationStoreState } from '../../store/applicationStore'; +import { Connect, connect, IDispatcher } from '../../flux/connect'; +import { RemoveSnackbarNotification } from '../../actions/snackbarActions'; + +import { InjectedNotistackProps, withSnackbar } from 'notistack'; + +const mapProps = (state: IApplicationStoreState) => ({ + notifications: state.framework.applicationState.snackBars +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + removeSnackbar: (key: number) => { + dispatcher.dispatch(new RemoveSnackbarNotification(key)); + } +}); + +type DisplaySnackbarsComponentProps = Connect & InjectedNotistackProps; + +class DisplaySnackbarsComponent extends React.Component { + private displayed: number[] = []; + + private storeDisplayed = (id: number) => { + this.displayed = [...this.displayed, id]; + }; + + public shouldComponentUpdate({ notifications: newSnacks = [] }: DisplaySnackbarsComponentProps) { + + const { notifications: currentSnacks } = this.props; + let notExists = false; + for (let i = 0; i < newSnacks.length; i++) { + if (notExists) continue; + notExists = notExists || !currentSnacks.filter(({ key }) => newSnacks[i].key === key).length; + } + return notExists; + } + + componentDidUpdate() { + const { notifications = [] } = this.props; + + notifications.forEach(notification => { + if (this.displayed.includes(notification.key)) return; + const options = notification.options || {}; + this.props.enqueueSnackbar(notification.message, options); + this.storeDisplayed(notification.key); + this.props.removeSnackbar(notification.key); + }); + } + + render() { + return null; + } +} + +const DisplayStackbars = withSnackbar(connect(mapProps, mapDispatch)(DisplaySnackbarsComponent)); +export default DisplayStackbars; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx new file mode 100644 index 000000000..522ff12c8 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButton.tsx @@ -0,0 +1,159 @@ + +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; +import { fade } from '@material-ui/core/styles/colorManipulator'; +import ButtonBase from '@material-ui/core/ButtonBase'; + + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + ...theme.typography.button, + height: 32, + minWidth: 48, + margin: 0, + padding: `${theme.spacing.unit - 4}px ${theme.spacing.unit * 1.5}px`, + borderRadius: 2, + willChange: 'opacity', + color: fade(theme.palette.action.active, 0.38), + '&:hover': { + textDecoration: 'none', + // Reset on mouse devices + backgroundColor: fade(theme.palette.text.primary, 0.12), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + '&$disabled': { + backgroundColor: 'transparent', + }, + }, + '&:not(:first-child)': { + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }, + '&:not(:last-child)': { + borderTopRightRadius: 0, + borderBottomRightRadius: 0, + }, + }, + /* Styles applied to the root element if `disabled={true}`. */ + disabled: { + color: fade(theme.palette.action.disabled, 0.12), + }, + /* Styles applied to the root element if `selected={true}`. */ + selected: { + color: theme.palette.action.active, + '&:after': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + borderRadius: 'inherit', + width: '100%', + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.38, + }, + '& + &:before': { + content: '""', + display: 'block', + position: 'absolute', + overflow: 'hidden', + width: 1, + height: '100%', + left: 0, + top: 0, + pointerEvents: 'none', + zIndex: 0, + backgroundColor: 'currentColor', + opacity: 0.12, + }, + }, + /* Styles applied to the `label` wrapper element. */ + label: { + width: '100%', + display: 'inherit', + alignItems: 'inherit', + justifyContent: 'inherit', + }, +}); + +export type ToggleButtonClassKey = 'disabled' | 'root' | 'label' | 'selected'; + +interface IToggleButtonProps extends WithStyles { + className?: string; + component?: React.ReactType; + disabled?: boolean; + disableFocusRipple?: boolean; + disableRipple?: boolean; + selected?: boolean; + type?: string; + value?: any; + onClick?: (event: React.FormEvent, value?: any) => void; + onChange?: (event: React.FormEvent, value?: any) => void; +} + +class ToggleButtonComponent extends React.Component { + handleChange = (event: React.FormEvent) => { + const { onChange, onClick, value } = this.props; + + if (onClick) { + onClick(event, value); + if (event.isDefaultPrevented()) { + return; + } + } + + if (onChange) { + onChange(event, value); + } + }; + + render() { + const { + children, + className: classNameProp, + classes, + disableFocusRipple, + disabled, + selected, + ...other + } = this.props; + + const className = classNames( + classes.root, + { + [classes.disabled]: disabled, + [classes.selected]: selected, + }, + classNameProp, + ); + + return ( + + {children} + + ); + } + public static defaultProps = { + disabled: false, + disableFocusRipple: false, + disableRipple: false, + }; + + public static muiName = 'ToggleButton'; +} + +export const ToggleButton = withStyles(styles, { name: 'MuiToggleButton' })(ToggleButtonComponent); +export default ToggleButton; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx new file mode 100644 index 000000000..8ab7c2b91 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/toggleButtonGroup.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { withStyles, WithStyles, Theme, createStyles } from '@material-ui/core/styles'; + +export const styles = (theme: Theme) => createStyles({ + /* Styles applied to the root element. */ + root: { + transition: theme.transitions.create('background,box-shadow'), + background: 'transparent', + borderRadius: 2, + overflow: 'hidden', + }, + /* Styles applied to the root element if `selected={true}` or `selected="auto" and `value` set. */ + selected: { + background: theme.palette.background.paper, + boxShadow: theme.shadows[2], + }, +}); + diff --git a/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx new file mode 100644 index 000000000..8bcdc8bc6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/material-ui/treeView.tsx @@ -0,0 +1,251 @@ +import * as React from 'react'; + +import { SvgIconProps } from '@material-ui/core/SvgIcon'; +import { List, ListItem, TextField, ListItemText, ListItemIcon, WithTheme, withTheme, Omit } from '@material-ui/core'; + +import FileIcon from '@material-ui/icons/InsertDriveFile'; +import CloseIcon from '@material-ui/icons/ExpandLess'; +import OpenIcon from '@material-ui/icons/ExpandMore'; +import FolderIcon from '@material-ui/icons/Folder'; + +export interface ITreeItem { + disabled?: boolean; + icon?: React.ComponentType; +} + +type TreeViewComponentState = { + /** All indices of all expanded Items */ + expandedItems: TData[]; + /** The index of the active iten or undefined if no item is active. */ + activeItem: undefined | TData; + /** The search term or undefined if search is corrently not active. */ + searchTerm: undefined | string; +} + +type TreeViewComponentBaseProps = WithTheme & { + items: TData[]; + contentProperty: keyof Omit; + childrenProperty: keyof Omit; + useFolderIcons?: boolean; + enableSearchBar?: boolean; + autoExpandFolder?: boolean; + style?: React.CSSProperties; + itemHeight?: number; + depthOffset?: number; +} + +type TreeViewComponentWithInternalStateProps = TreeViewComponentBaseProps & { + onItemClick?: (item: TData) => void; + onFolderClick?: (item: TData) => void; +} + +type TreeViewComponentWithExternalStateProps = TreeViewComponentBaseProps & TreeViewComponentState & { + onSearch: (searchTerm: string) => void; + onItemClick: (item: TData) => void; + onFolderClick: (item: TData) => void; +} + +type TreeViewComponentProps = + TreeViewComponentWithInternalStateProps | + TreeViewComponentWithExternalStateProps; + +function isTreeViewComponentWithExternalStateProps(props: TreeViewComponentProps): props is TreeViewComponentWithExternalStateProps { + const propsWithExternalState = (props as TreeViewComponentWithExternalStateProps) + return ( + propsWithExternalState.onSearch instanceof Function || + propsWithExternalState.expandedItems !== undefined || + propsWithExternalState.activeItem !== undefined || + propsWithExternalState.searchTerm !== undefined + ); +} + +class TreeViewComponent extends React.Component, TreeViewComponentState> { + + /** + * Initializes a new instance. + */ + constructor(props: TreeViewComponentProps) { + super(props); + + this.state = { + expandedItems: [], + activeItem: undefined, + searchTerm: undefined + }; + } + + render(): JSX.Element { + this.itemIndex = 0; + const { searchTerm } = this.state; + const { children, items, enableSearchBar } = this.props; + const styles = { + root: { + padding: 0, + paddingBottom: 8, + paddingTop: children ? 0 : 8, + ...this.props.style + }, + search: { + padding: `0px ${ this.props.theme.spacing.unit }px` + } + }; + return ( +
+ { children } + { enableSearchBar && || null } + + { this.renderItems(items, searchTerm && searchTerm.toLowerCase()) } + +
+ ); + } + + private itemIndex: number = 0; + private renderItems = (items: TData[], searchTerm: string | undefined, depth: number = 1) => { + return items.reduce((acc, item) => { + + const children = this.props.childrenProperty && ((item as any)[this.props.childrenProperty] as TData[]); + const childrenJsx = children && this.renderItems(children, searchTerm, depth + 1); + + const expanded = searchTerm + ? children && childrenJsx.length > 0 + : !children + ? false + : this.state.expandedItems.indexOf(item) > -1; + const isFolder = children !== undefined; + + const itemJsx = this.renderItem(item, searchTerm, depth, isFolder, expanded); + itemJsx && acc.push(itemJsx); + + if (isFolder && expanded) { + acc.push(...childrenJsx); + } + return acc; + + }, [] as JSX.Element[]); + } + private renderItem = (item: TData, searchTerm: string | undefined, depth: number, isFolder: boolean, expanded: boolean): JSX.Element | null => { + const styles = { + item: { + paddingLeft: (((this.props.depthOffset || 0) + depth) * this.props.theme.spacing.unit * 3), + backgroundColor: this.state.activeItem === item ? this.props.theme.palette.action.selected : undefined, + height: this.props.itemHeight || undefined, + cursor: item.disabled ? 'not-allowed' : 'pointer', + color: item.disabled ? this.props.theme.palette.text.disabled : this.props.theme.palette.text.primary, + overflow: 'hidden', + transform: 'translateZ(0)', + } + }; + + const text = (item as any)[this.props.contentProperty] as string || ''; // need to keep track of search + const matchIndex = searchTerm ? text.toLowerCase().indexOf(searchTerm) : -1; + const searchTermLength = searchTerm && searchTerm.length || 0; + + const handleClickCreator = (isIcon: boolean) => (event: React.SyntheticEvent) => { + if (item.disabled) return; + event.preventDefault(); + event.stopPropagation(); + if (isFolder && (this.props.autoExpandFolder || isIcon)) { + this.props.onFolderClick ? this.props.onFolderClick(item) : this.onFolderClick(item); + } else { + this.props.onItemClick ? this.props.onItemClick(item) : this.onItemClick(item); + } + }; + + return ((searchTerm && (matchIndex > -1 || expanded) || !searchTerm) + ? ( + + + { // display the left icon + (this.props.useFolderIcons && { isFolder ? : }) || + (item.icon && ()) } + + + { // highlight search result + matchIndex > -1 + ? ( + { text.substring(0, matchIndex) } + + { text.substring(matchIndex, matchIndex + searchTermLength) } + + { text.substring(matchIndex + searchTermLength) } + ) + : () + } + + { // display the right icon, depending on the state + !isFolder ? null : expanded ? () : () } + + ) + : null + ); + } + + private onFolderClick = (item: TData) => { + // toggle items with children + if (this.state.searchTerm) return; + const indexOfItemToToggle = this.state.expandedItems.indexOf(item); + if (indexOfItemToToggle === -1) { + this.setState({ + expandedItems: [...this.state.expandedItems, item], + }); + } else { + this.setState({ + expandedItems: [ + ...this.state.expandedItems.slice(0, indexOfItemToToggle), + ...this.state.expandedItems.slice(indexOfItemToToggle + 1), + ] + }); + } + }; + + private onItemClick = (item: TData) => { + // activate items without children + this.setState({ + activeItem: item, + }); + }; + + private onChangeSearchText = (event: React.ChangeEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (isTreeViewComponentWithExternalStateProps(this.props)) { + this.props.onSearch(event.target.value) + } else { + this.setState({ + searchTerm: event.target.value + }); + } + }; + + static getDerivedStateFromProps(props: TreeViewComponentProps, state: TreeViewComponentState): TreeViewComponentState { + if (isTreeViewComponentWithExternalStateProps(props)) { + return { + ...state, + expandedItems: props.expandedItems || [], + activeItem: props.activeItem, + searchTerm: props.searchTerm + }; + } + return state; + } + + public static defaultProps = { + useFolderIcons: false, + enableSearchBar: false, + autoExpandFolder: false, + depthOffset: 0 + } +} + +export type TreeViewCtorType = new () => React.Component, 'theme'>>; + +export const TreeView = withTheme()(TreeViewComponent); +export default TreeView; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx new file mode 100644 index 000000000..f6df244a0 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/navigationMenu.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import { faHome, faAddressBook } from '@fortawesome/free-solid-svg-icons'; + +import Drawer from '@material-ui/core/Drawer'; +import List from '@material-ui/core/List'; + +import Divider from '@material-ui/core/Divider'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import ListItemLink from '../components/material-ui/listItemLink'; + +import connect, { Connect } from '../flux/connect'; + +const drawerWidth = 240; + +const styles = (theme: Theme) => createStyles({ + drawerPaper: { + position: 'relative', + width: drawerWidth, + }, + toolbar: theme.mixins.toolbar +}); + +export const NavigationMenu = withStyles(styles)(connect()(({ classes, state }: WithStyles & Connect) => { + return ( + +
+ { /* https://fiffty.github.io/react-treeview-mui/ */} + + { process.env.NODE_ENV === "development" ? } /> : null } + + { + state.framework.applicationRegistraion && Object.keys(state.framework.applicationRegistraion).map(key => { + const reg = state.framework.applicationRegistraion[key]; + return reg && ( + || null} /> + ) || null; + }) || null + } + + { process.env.NODE_ENV === "development" ? } /> : null } + + ) +})); + +export default NavigationMenu; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx b/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx new file mode 100644 index 000000000..55b249246 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/routing/appFrame.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; + +import connect, { Connect } from '../../flux/connect'; + +import { SetTitleAction } from '../../actions/titleActions'; +import { AddErrorInfoAction } from '../../actions/errorActions'; + +import { IconType } from '../../models/iconDefinition'; + +export interface IAppFrameProps { + title: string; + icon?: IconType; +} + +/** + * Represents a component to wich will embed each single app providing the + * functionality to update the title and implement an exeprion border. + */ +export class AppFrame extends React.Component { + + public render(): JSX.Element { + return ( +
+ { this.props.children } +
+ ) + } + + public componentDidMount() { + this.props.dispatch(new SetTitleAction(this.props.title, this.props.icon)); + } + public componentDidCatch(error: Error | null, info: object) { + this.props.dispatch(new AddErrorInfoAction({ error, info })); + } +} + +export default connect()(AppFrame); \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/components/titleBar.tsx b/sdnr/wt/odlux/framework/src/components/titleBar.tsx new file mode 100644 index 000000000..d4d17d22e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/components/titleBar.tsx @@ -0,0 +1,125 @@ +import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import MenuIcon from '@material-ui/icons/Menu'; +import AccountCircle from '@material-ui/icons/AccountCircle'; +import MenuItem from '@material-ui/core/MenuItem'; +import Menu from '@material-ui/core/Menu'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { UpdateAuthentication } from '../actions/authentication'; + +import connect, { Connect, IDispatcher } from '../flux/connect'; +import Logo from './logo'; + +const styles = (theme: Theme) => createStyles({ + appBar: { + zIndex: theme.zIndex.drawer + 1, + }, + grow: { + flexGrow: 1, + }, + menuButton: { + marginLeft: -12, + marginRight: 20, + }, + icon: { + marginRight: 8, + marginLeft: 24, + } +}); + +const mapDispatch = (dispatcher: IDispatcher) => { + return { + logout: () => { dispatcher.dispatch(new UpdateAuthentication(null)); } + } +}; + +type TitleBarProps = RouteComponentProps<{}> & WithStyles & Connect + +class TitleBarComponent extends React.Component { + + constructor(props: TitleBarProps) { + super(props); + + this.state = { + anchorEl: null + } + + } + render(): JSX.Element { + const { classes, state, history, location } = this.props; + const open = !!this.state.anchorEl; + + return ( + + + + + + + + { state.framework.applicationState.icon + ? () + : null } + { state.framework.applicationState.title } + + { state.framework.authenticationState.user + ? (
+ + + Profile + { + this.props.logout(); + this.closeMenu(); + } }>Logout + +
) + : () } +
+
+ ); + }; + + + private openMenu = (event: React.MouseEvent) => { + this.setState({ anchorEl: event.currentTarget }); + }; + + private closeMenu = () => { + this.setState({ anchorEl: null }); + }; +} + +//todo: ggf. https://github.com/acdlite/recompose verwenden zur Vereinfachung + +export const TitleBar = withStyles(styles)(withRouter(connect(undefined, mapDispatch)(TitleBarComponent))); +export default TitleBar; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/design/default.ts b/sdnr/wt/odlux/framework/src/design/default.ts new file mode 100644 index 000000000..ecc4ebcf2 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/design/default.ts @@ -0,0 +1,53 @@ +/****************************************************************************** + * Copyright 2018 highstreet technologies GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *****************************************************************************/ + +import { createMuiTheme } from '@material-ui/core/styles'; + +const theme = createMuiTheme({ + design: { + id: "onap", + name: "Open Networking Automation Plattform (ONAP)", + url: "https://www.onap.org/wp-content/uploads/sites/20/2017/02/logo_onap_2017.png", + height: 49, + width: 229, + logoHeight: 32, + }, + palette: { + type: "light", + common: { + black: "#000", + white: "#fff" + }, + background: { + paper: "#fff", + default: "#fafafa" + }, + primary: { + light: "#eee", + main: "#fff", + dark: "#e0e0e0", + contrastText: "#07819B" + }, + secondary: { + light: "#07819b5e", + main: "#07819bc9", + dark: "#07819B", + contrastText: "#fff" + }, + } + }); + + export default theme; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/favicon.ico b/sdnr/wt/odlux/framework/src/favicon.ico new file mode 100644 index 000000000..a8a5d31ca Binary files /dev/null and b/sdnr/wt/odlux/framework/src/favicon.ico differ diff --git a/sdnr/wt/odlux/framework/src/flux/action.ts b/sdnr/wt/odlux/framework/src/flux/action.ts new file mode 100644 index 000000000..8a90f24b2 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/action.ts @@ -0,0 +1,8 @@ +/** + * Represents an action in the odlux flux architecture. + */ +export abstract class Action { } + +export interface IActionHandler { + (state: TState | undefined, action: TAction): TState; +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/flux/connect.ts b/sdnr/wt/odlux/framework/src/flux/connect.ts new file mode 100644 index 000000000..0c8d36d0a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/connect.ts @@ -0,0 +1,148 @@ +import * as React from 'react'; +import * as PropTypes from 'prop-types'; + +import { Dispatch } from '../flux/store'; + +import { ApplicationStore, IApplicationStoreState } from '../store/applicationStore'; + +interface IApplicationStoreContext { + applicationStore: ApplicationStore; +} + +export interface IDispatcher { + dispatch: Dispatch; +} + +interface IApplicationStoreProps { + state: IApplicationStoreState; +} + +interface IDispatchProps { + dispatch: Dispatch; +} + +type FuncInfer = { + ([...args]: any): T; +}; + +type FunctionResult = T extends FuncInfer ? U : never; + +type ComponentDecoratorInfer = { + (wrappedComponent: React.ComponentType): React.ComponentClass; +}; + +export type Connect = + (TMapProps extends undefined ? IApplicationStoreProps : FunctionResult) & + (TMapDispatch extends undefined ? IDispatchProps : FunctionResult); + +export function connect(): ComponentDecoratorInfer; + +export function connect( + mapStateToProps: (state: IApplicationStoreState) => TStateProps +): ComponentDecoratorInfer; + +export function connect( + mapStateToProps: (state: IApplicationStoreState) => TStateProps, + mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps +): ComponentDecoratorInfer; + + +export function connect( + mapStateToProps: undefined, + mapDispatchToProps: (dispatcher: IDispatcher) => TDispatchProps +): ComponentDecoratorInfer; + + +export function connect( + mapStateToProps?: ((state: IApplicationStoreState) => TStateProps), + mapDispatchToProps?: ((dispatcher: IDispatcher) => TDispatchProps) +) : + ((WrappedComponent: React.ComponentType) => React.ComponentType) { + + const injectApplicationStore = (WrappedComponent: React.ComponentType): React.ComponentType => { + + class StoreAdapter extends React.Component { + public static contextTypes = { ...WrappedComponent.contextTypes, applicationStore: PropTypes.object.isRequired }; + context: IApplicationStoreContext; + + render(): JSX.Element { + + if (isWrappedComponentIsVersion1(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, dispatch: this.store.dispatch.bind(this.store) }); + return element; + } else if (mapStateToProps && isWrappedComponentIsVersion2(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), dispatch: this.store.dispatch.bind(this.store) }); + return element; + } else if (mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion3(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), ...(mapStateToProps(this.store.state) as any), ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store)}) as any) }); + return element; + } else if (!mapStateToProps && mapDispatchToProps && isWrappedComponentIsVersion4(WrappedComponent)) { + const element = React.createElement(WrappedComponent, { ...(this.props as any), state: this.store.state, ...(mapDispatchToProps({ dispatch: this.store.dispatch.bind(this.store)}) as any) }); + return element; + } + throw new Error("Invalid arguments in connect."); + } + + componentDidMount(): void { + this.store && this.store.changed.addHandler(this.handleStoreChanged); + } + + componentWillUnmount(): void { + this.store && this.store.changed.removeHandler(this.handleStoreChanged); + } + + private get store(): ApplicationStore { + return this.context.applicationStore; + } + + private handleStoreChanged = () => { + this.forceUpdate(); + } + } + + return StoreAdapter; + } + + + return injectApplicationStore; + + /* inline methods */ + + function isWrappedComponentIsVersion1(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !mapStateToProps && !mapDispatchToProps; + } + + function isWrappedComponentIsVersion2(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !!mapStateToProps && !mapDispatchToProps; + } + + function isWrappedComponentIsVersion3(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !!mapStateToProps && !!mapDispatchToProps; + } + + function isWrappedComponentIsVersion4(wrappedComponent: any): wrappedComponent is React.ComponentType { + return !mapStateToProps && !!mapDispatchToProps; + } +} + +interface ApplicationStoreProviderProps extends React.Props { + applicationStore: ApplicationStore; +} + +export class ApplicationStoreProvider extends React.Component + implements /* React.ComponentLifecycle, */ React.ChildContextProvider { + + public static childContextTypes = { applicationStore: PropTypes.object.isRequired }; + + getChildContext(): IApplicationStoreContext { + return { + applicationStore: this.props.applicationStore + }; + } + + render(): JSX.Element { + return React.Children.only(this.props.children); + } +} + +export default connect; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/flux/middleware.ts b/sdnr/wt/odlux/framework/src/flux/middleware.ts new file mode 100644 index 000000000..006d94551 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/middleware.ts @@ -0,0 +1,90 @@ +import { Action, IActionHandler } from './action'; +import { Store, Dispatch, Enhancer } from './store'; + +export interface MiddlewareArg { + dispatch: Dispatch; + getState: () => T; +} + +export interface Middleware { + (obj: MiddlewareArg): Function; +} + +class InitialisationAction extends Action { }; +const initialisationAction = new InitialisationAction(); + +export type ActionHandlerMapObject = { + [K in keyof S]: IActionHandler +} + +export const combineActionHandler = (actionHandlers: ActionHandlerMapObject) : IActionHandler => { + const finalActionHandlers: ActionHandlerMapObject = {} as ActionHandlerMapObject; + Object.keys(actionHandlers).forEach(actionHandlerKey => { + const handler = actionHandlers[actionHandlerKey]; + if (typeof handler === 'function') { + finalActionHandlers[actionHandlerKey] = handler; + } + }); + + // ensure initialisation + Object.keys(finalActionHandlers).forEach(key => { + const actionHandler = finalActionHandlers[key]; + const initialState = actionHandler(undefined, initialisationAction); + if (typeof initialState === 'undefined') { + const errorMessage = `Action handler ${ key } returned undefiend during initialization.`; + throw new Error(errorMessage); + } + }); + + return function combination(state: TState = ({} as TState), action: TAction) { + let hasChanged = false; + const nextState : TState = {} as TState; + Object.keys(finalActionHandlers).forEach(key => { + const actionHandler = finalActionHandlers[key]; + const previousState = state[key]; + const nextStateKey = actionHandler(previousState, action); + if (typeof nextStateKey === 'undefined') { + const errorMessage = `Given ${ action.constructor } and action handler ${ key } returned undefiend.`; + throw new Error(errorMessage); + } + nextState[key] = nextStateKey; + hasChanged = hasChanged || nextStateKey !== previousState; + }); + return (hasChanged ? nextState : state) as TState; + }; +}; + +export const chainMiddleware = (...middlewares: Middleware[]): Enhancer => { + return (store: Store) => { + const middlewareAPI = { + getState() { return store.state }, + dispatch: (action: TAction) => store.dispatch(action) // we want to use the combinded dispatch + // we should NOT use the flux dispatcher here, since the action would affect ALL stores + }; + const chain = middlewares.map(middleware => middleware(middlewareAPI)); + return compose(...chain)(store.dispatch) as Dispatch; + } +}; + +/** + * Composes single-argument functions from right to left. The rightmost + * function can take multiple arguments as it provides the signature for + * the resulting composite function. + * + * @param {...Function} funcs The functions to compose. + * @returns {Function} A function obtained by composing the argument functions + * from right to left. For example, compose(f, g, h) is identical to doing + * (...args) => f(g(h(...args))). + */ +const compose = (...funcs: Function[]) => { + if (funcs.length === 0) { + return (arg: any) => arg + } + + if (funcs.length === 1) { + return funcs[0] + } + + return funcs.reduce((a, b) => (...args: any[]) => a(b(...args))); +}; + diff --git a/sdnr/wt/odlux/framework/src/flux/store.ts b/sdnr/wt/odlux/framework/src/flux/store.ts new file mode 100644 index 000000000..7a2e578f9 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/flux/store.ts @@ -0,0 +1,81 @@ +import { Event } from "../common/event" + +import { Action } from './action'; +import { IActionHandler } from './action'; + +export interface Dispatch { + (action: TAction): TAction; +} + +export interface Enhancer { + (store: Store): Dispatch; +} + +class InitialisationAction extends Action { }; +const initialisationAction = new InitialisationAction(); + +export class Store { + + constructor(actionHandler: IActionHandler, enhancer?: Enhancer) + constructor(actionHandler: IActionHandler, initialState: TStoreState, enhancer?: Enhancer) + constructor(actionHandler: IActionHandler, initialState?: TStoreState | Enhancer, enhancer?: Enhancer) { + if (typeof initialState === 'function') { + enhancer = initialState as Enhancer; + initialState = undefined; + } + + this._isDispatching = false; + + this.changed = new Event(); // sollten wir hier eventuell sogar den state mit übergeben ? + + this._actionHandler = actionHandler; + + this._state = initialState as TStoreState; + if (enhancer) this._dispatch = enhancer(this); + + this._dispatch(initialisationAction); + } + + public changed: Event; + + private _dispatch: Dispatch = (payload: TAction): TAction => { + if (payload == null || !(payload instanceof Action)) { + throw new Error( + 'Actions must inherit from type Action. ' + + 'Use a custom middleware for async actions.' + ); + } + + if (this._isDispatching) { + throw new Error('ActionHandler may not dispatch actions.'); + } + + const oldState = this._state; + try { + this._isDispatching = true; + this._state = this._actionHandler(oldState, payload); + } finally { + this._isDispatching = false; + } + + if (this._state !== oldState) { + this.changed.invoke(); + } + + return payload; + } + + public get dispatch(): Dispatch { + return this._dispatch; + } + + public get state() { + return this._state + } + + private _state: TStoreState; + private _isDispatching: boolean; + private _actionHandler: IActionHandler; + +} + diff --git a/sdnr/wt/odlux/framework/src/handlers/applicationRegistryHandler.ts b/sdnr/wt/odlux/framework/src/handlers/applicationRegistryHandler.ts new file mode 100644 index 000000000..b30512cbb --- /dev/null +++ b/sdnr/wt/odlux/framework/src/handlers/applicationRegistryHandler.ts @@ -0,0 +1,14 @@ +import { IActionHandler } from '../flux/action'; + +import { ApplicationInfo } from '../models/applicationInfo'; +import { applicationManager } from '../services/applicationManager'; + +export interface IApplicationRegistration { + [name: string]: ApplicationInfo; +} + +const applicationRegistrationInit: IApplicationRegistration = applicationManager.applications; + +export const applicationRegistryHandler: IActionHandler = (state = applicationRegistrationInit, action) => { + return state; +}; diff --git a/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts b/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts new file mode 100644 index 000000000..2a952b61e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/handlers/applicationStateHandler.ts @@ -0,0 +1,70 @@ +import { IActionHandler } from '../flux/action'; +import { SetTitleAction } from '../actions/titleActions'; + +import { AddSnackbarNotification, RemoveSnackbarNotification } from '../actions/snackbarActions'; +import { AddErrorInfoAction, RemoveErrorInfoAction, ClearErrorInfoAction } from '../actions/errorActions'; + +import { IconType } from '../models/iconDefinition'; + +import { ErrorInfo } from '../models/errorInfo'; +import { SnackbarItem } from '../models/snackbarItem'; + +export interface IApplicationState { + title: string; + icon?: IconType; + + errors: ErrorInfo[]; + snackBars: SnackbarItem[]; +} + +const applicationStateInit: IApplicationState = { title: "Loading ...", errors: [], snackBars:[] }; + +export const applicationStateHandler: IActionHandler = (state = applicationStateInit, action) => { + if (action instanceof SetTitleAction) { + state = { + ...state, + title: action.title, + icon: action.icon + }; + } else if (action instanceof AddErrorInfoAction) { + state = { + ...state, + errors: [ + ...state.errors, + action.errorInfo + ] + }; + } else if (action instanceof RemoveErrorInfoAction) { + const index = state.errors.indexOf(action.errorInfo); + if (index > -1) { + state = { + ...state, + errors: [ + ...state.errors.slice(0, index), + ...state.errors.slice(index + 1) + ] + }; + } + } else if (action instanceof ClearErrorInfoAction) { + if (state.errors && state.errors.length) { + state = { + ...state, + errors: [] + }; + } + } else if (action instanceof AddSnackbarNotification) { + state = { + ...state, + snackBars: [ + ...state.snackBars, + action.notification + ] + }; + } else if (action instanceof RemoveSnackbarNotification) { + state = { + ...state, + snackBars: state.snackBars.filter(s => s.key !== action.key) + }; + } + return state; +}; diff --git a/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts b/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts new file mode 100644 index 000000000..e0ae1aa8d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/handlers/authenticationHandler.ts @@ -0,0 +1,33 @@ +import { IActionHandler } from '../flux/action'; +import { UpdateAuthentication } from '../actions/authentication'; + +import { User } from '../models/authentication'; + +export interface IAuthenticationState { + user?: User; +} + +const initialToken = localStorage.getItem("userToken"); + +const authenticationStateInit: IAuthenticationState = { + user: initialToken && new User(initialToken) || undefined +}; + +export const authenticationStateHandler: IActionHandler = (state = authenticationStateInit, action) => { + if (action instanceof UpdateAuthentication) { + + if (action.bearerToken) { + localStorage.setItem("userToken", action.bearerToken); + } else { + localStorage.removeItem("userToken"); + } + + const user = action.bearerToken && new User(action.bearerToken) || undefined; + state = { + ...state, + user + }; + } + + return state; +}; diff --git a/sdnr/wt/odlux/framework/src/handlers/navigationStateHandler.ts b/sdnr/wt/odlux/framework/src/handlers/navigationStateHandler.ts new file mode 100644 index 000000000..d9036000a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/handlers/navigationStateHandler.ts @@ -0,0 +1,28 @@ +import { IActionHandler } from '../flux/action'; +import { LocationChanged } from '../actions/navigationActions'; + + +export interface INavigationState { + pathname: string; + search: string; + hash: string; +} + +const navigationStateInit: INavigationState = { + pathname: '/', + search: '', + hash: '', +}; + + +export const navigationStateHandler: IActionHandler = (state = navigationStateInit, action) => { + if (action instanceof LocationChanged) { + state = { + ...state, + pathname: action.pathname, + search: action.search, + hash: action.hash + } + } + return state; +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/index.dev.html b/sdnr/wt/odlux/framework/src/index.dev.html new file mode 100644 index 000000000..71cb7408d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/index.dev.html @@ -0,0 +1,28 @@ + + + + + + + + + O D L UX + + + +
+ + + + + + \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/index.html b/sdnr/wt/odlux/framework/src/index.html new file mode 100644 index 000000000..36d937775 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/index.html @@ -0,0 +1,24 @@ + + + + + + + + + O D L UX + + + +
+ + + + + + \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/middleware/api.ts b/sdnr/wt/odlux/framework/src/middleware/api.ts new file mode 100644 index 000000000..190505760 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/middleware/api.ts @@ -0,0 +1,55 @@ +import { Action, IActionHandler } from '../flux/action'; +import { MiddlewareArg } from '../flux/middleware'; +import { Dispatch } from '../flux/store'; + +import { IApplicationStoreState } from '../store/applicationStore'; +import { AddErrorInfoAction, ErrorInfo } from '../actions/errorActions'; + +const baseUrl = `${ window.location.origin }${ window.location.pathname }`; + +export class ApiAction extends Action { + constructor(public endpoint: string, public successAction: { new(result: TResult): TSuccessAction }, public authenticate: boolean = false) { + super(); + } +} + +export const apiMiddleware = (store: MiddlewareArg) => (next: Dispatch) => (action: A) => { + + // So the middleware doesn't get applied to every single action + if (action instanceof ApiAction) { + const user = store && store.getState().framework.authenticationState.user; + const token = user && user.token || null; + let config = { headers: {} }; + + if (action.authenticate) { + if (token) { + config = { + ...config, + headers: { + ...config.headers, + // 'Authorization': `Bearer ${ token }` + authorization: "Basic YWRtaW46YWRtaW4=" + } + } + } else { + return next(new AddErrorInfoAction({ message: 'Please login to continue.' })); + } + } + + fetch(baseUrl + action.endpoint.replace(/\/{2,}/, '/'), config) + .then(response => + response.json().then(data => ({ data, response })) + ) + .then(result => { + next(new action.successAction(result.data)); + }) + .catch((error: any) => { + next(new AddErrorInfoAction((error instanceof Error) ? { error: error } : { message: error.toString() })); + }); + } + + // let all actions pass + return next(action); +} + +export default apiMiddleware; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/middleware/logger.ts b/sdnr/wt/odlux/framework/src/middleware/logger.ts new file mode 100644 index 000000000..e47484fbf --- /dev/null +++ b/sdnr/wt/odlux/framework/src/middleware/logger.ts @@ -0,0 +1,17 @@ +import { Dispatch } from '../flux/store'; +import { MiddlewareApi } from '../store/applicationStore'; + + +function createLoggerMiddleware() { + return function logger({ getState }: MiddlewareApi) { + return (next: Dispatch): Dispatch => action => { + console.log('will dispatch', action); + const returnValue = next(action); + console.log('state after dispatch', getState()); + return returnValue; + }; + } +} + +export const logger = createLoggerMiddleware(); +export default logger; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/middleware/navigation.ts b/sdnr/wt/odlux/framework/src/middleware/navigation.ts new file mode 100644 index 000000000..758b51845 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/middleware/navigation.ts @@ -0,0 +1,53 @@ +import { Location, History, createHashHistory } from "history"; + +import { ApplicationStore } from "../store/applicationStore"; +import { Dispatch } from '../flux/store'; + +import { LocationChanged, NavigateToApplication } from "../actions/navigationActions"; +import { PushAction, ReplaceAction, GoAction, GoBackAction, GoForwardeAction } from '../actions/navigationActions'; + +import applicationManager from "../services/applicationManager"; + +const routerMiddlewareCreator = (history: History) => () => (next: Dispatch): Dispatch => (action) => { + + if (action instanceof NavigateToApplication) { + const application = applicationManager.applications && applicationManager.applications[action.applicationName]; + if (application) { + const href = `/${ application.path || application.name }${ action.href ? '/' + action.href : '' }`.replace(/\/{2,}/i, '/'); + if (action.replace) { + history.replace(href, action.state); + } else { + history.push(href, action.state); + } + } + } else if (action instanceof PushAction) { + history.push(action.href, action.state); + } else if (action instanceof ReplaceAction) { + history.replace(action.href, action.state); + } else if (action instanceof GoAction) { + history.go(action.index); + } else if (action instanceof GoBackAction) { + history.goBack(); + } else if (action instanceof GoForwardeAction) { + history.goForward(); + } else { + return next(action); + } + return action; +}; + +function startListener(history: History, store: ApplicationStore) { + store.dispatch(new LocationChanged(history.location.pathname, history.location.search, history.location.hash)); + history.listen((location: Location) => { + store.dispatch(new LocationChanged(location.pathname, location.search, location.hash)); + }); +} + +const history = createHashHistory(); + +export function startHistoryListener(store: ApplicationStore) { + startListener(history, store); +} + +export const routerMiddleware = routerMiddlewareCreator(history); +export default routerMiddleware; diff --git a/sdnr/wt/odlux/framework/src/middleware/thunk.ts b/sdnr/wt/odlux/framework/src/middleware/thunk.ts new file mode 100644 index 000000000..3844485ec --- /dev/null +++ b/sdnr/wt/odlux/framework/src/middleware/thunk.ts @@ -0,0 +1,18 @@ + +import { Dispatch } from '../flux/store'; +import { MiddlewareApi } from '../store/applicationStore'; + +function createThunkMiddleware() { + return ({ dispatch, getState }: MiddlewareApi) => + (next : Dispatch) : Dispatch => + action => { + if (typeof action === 'function') { + return action(dispatch, getState); + } + + return next(action); + }; +} + +export const thunk = createThunkMiddleware(); +export default thunk; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/models/applicationInfo.ts b/sdnr/wt/odlux/framework/src/models/applicationInfo.ts new file mode 100644 index 000000000..d2076591e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/applicationInfo.ts @@ -0,0 +1,31 @@ +import { ComponentType } from 'react'; +import { IconType } from './iconDefinition'; + +import { IActionHandler } from '../flux/action'; +import { Middleware } from '../flux/middleware'; + +/** Represents the information needed about an application to integrate. */ +export class ApplicationInfo { + /** The name of the application. */ + name: string; + /** Optional: The title of the application, if null ot undefined the name will be used. */ + title?: string; + /** Optional: The icon of the application for the navigation and title bar. */ + icon?: IconType; + /** Optional: The description of the application. */ + description?: string; + /** The root component of the application. */ + rootComponent: ComponentType; + /** Optional: The root action handler of the application. */ + rootActionHandler?: IActionHandler<{ [key: string]: any }>; + /** Optional: Application speciffic middlewares. */ + middlewares?: Middleware<{ [key: string]: any }>[]; + /** Optional: A mapping object with the exported components. */ + exportedComponents?: { [key: string]: ComponentType } + /** Optional: The entry to be shown in the menu. If undefiened the name will be used. */ + menuEntry?: string | React.ComponentType; + /** Optional: A component to be shown in the menu when this app is active below the main entry. If undefiened the name will be used. */ + subMenuEntry?: React.ComponentType; + /** Optional: The pasth for this application. If undefined the name will be use as path. */ + path?: string; +} diff --git a/sdnr/wt/odlux/framework/src/models/authentication.ts b/sdnr/wt/odlux/framework/src/models/authentication.ts new file mode 100644 index 000000000..44b5ae436 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/authentication.ts @@ -0,0 +1,50 @@ +import * as JWT from 'jsonwebtoken'; + +export interface IUserInfo { + iss: string, + iat: number, + exp: number, + aud: string, + sub: string, + firstName: string, + lastName: string, + email: string, + role: string[] +} + + +export class User { + + public _userInfo: IUserInfo | null; + + constructor(private _bearerToken: string) { + //const pem = require('raw-loader!../assets/publicKey.pem'); + const pem = "kFfAgpf806IKa4z88EEk6Lim7NMGicrw99OmIB38myM9CS44nEmMNJxnFu3ImViS248wSwkuZ3HvrhsPrA1ZFRNb1a6CEtGN4DaPJbfuo35qMp50tIEpy8nsSFpayOBE"; + + try { + const dec = (JWT.verify(_bearerToken, pem)) as IUserInfo; + this._userInfo = dec; + } catch (ex) { + this._userInfo = null; + } + } + + public get user(): string | null { + return this._userInfo && this._userInfo.email; + }; + + public get roles(): string[] | null { + return this._userInfo && this._userInfo.role; + } + public get token(): string | null { + return this._userInfo && this._bearerToken; + } + + public isInRole(role: string | string[]): boolean { + return false; + } + +} + +// key:kFfAgpf806IKa4z88EEk6Lim7NMGicrw99OmIB38myM9CS44nEmMNJxnFu3ImViS248wSwkuZ3HvrhsPrA1ZFRNb1a6CEtGN4DaPJbfuo35qMp50tIEpy8nsSFpayOBE +// token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPRExVWCIsImlhdCI6MTUzODQ2NDMyMCwiZXhwIjoxNTcwMDAwMzIwLCJhdWQiOiJsb2NhbGhvc3QiLCJzdWIiOiJsb2NhbGhvc3QiLCJmaXJzdE5hbWUiOiJNYXgiLCJsYXN0TmFtZSI6Ik11c3Rlcm1hbm4iLCJlbWFpbCI6Im1heEBvZGx1eC5jb20iLCJyb2xlIjpbInVzZXIiLCJhZG1pbiJdfQ.9e5hDi2uxmIXNwHkJoScBZsHBk0jQ8CcZ7YIcZhDtuI \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/models/elasticSearch.ts b/sdnr/wt/odlux/framework/src/models/elasticSearch.ts new file mode 100644 index 000000000..504b2cf21 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/elasticSearch.ts @@ -0,0 +1,22 @@ +export type Result = { + took: number; + timed_out: boolean; + _shards: { + total: number; + successful: number; + failed: number; + }; + hits: { + total: number; + max_score: number; + hits?: (HitEntry)[] | null; + }; +} + +export type HitEntry = { + _index: string; + _type: string; + _id: string; + _score: number; + _source: TSource; +} diff --git a/sdnr/wt/odlux/framework/src/models/errorInfo.ts b/sdnr/wt/odlux/framework/src/models/errorInfo.ts new file mode 100644 index 000000000..f07145500 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/errorInfo.ts @@ -0,0 +1,11 @@ +export type ErrorInfo = { + error?: Error | null, + url?: string, + line?: number, + col?: number, + info?: { + extra?: string, + componentStack?: string + }, + message?: string +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/models/iconDefinition.ts b/sdnr/wt/odlux/framework/src/models/iconDefinition.ts new file mode 100644 index 000000000..6fef9dbc7 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/iconDefinition.ts @@ -0,0 +1,4 @@ + +import { IconDefinition } from '@fortawesome/free-solid-svg-icons'; + +export type IconType = IconDefinition; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/models/index.ts b/sdnr/wt/odlux/framework/src/models/index.ts new file mode 100644 index 000000000..a8a7ca032 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/index.ts @@ -0,0 +1 @@ +export * from './elasticSearch'; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/models/restService.ts b/sdnr/wt/odlux/framework/src/models/restService.ts new file mode 100644 index 000000000..053c29b8e --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/restService.ts @@ -0,0 +1,30 @@ +/** + * The PlainObject type is a JavaScript object containing zero or more key-value pairs. + */ +export interface PlainObject { + [key: string]: T; +} + +export interface AjaxParameter { + /** + * The HTTP method to use for the request (e.g. "POST", "GET", "PUT"). + */ + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'PATCH'; + /** + * An object of additional header key/value pairs to send along with requests using the XMLHttpRequest + * transport. The header X-Requested-With: XMLHttpRequest is always added, but its default + * XMLHttpRequest value can be changed here. Values in the headers setting can also be overwritten from + * within the beforeSend function. + */ + headers?: PlainObject; + /** + * Data to be sent to the server. It is converted to a query string, if not already a string. It's + * appended to the url for GET-requests. See processData option to prevent this automatic processing. + * Object must be Key/Value pairs. If value is an Array, jQuery serializes multiple values with same + * key based on the value of the traditional setting (described below). + */ + data?: PlainObject | string; +} + + + diff --git a/sdnr/wt/odlux/framework/src/models/snackbarItem.ts b/sdnr/wt/odlux/framework/src/models/snackbarItem.ts new file mode 100644 index 000000000..5aa4dd78a --- /dev/null +++ b/sdnr/wt/odlux/framework/src/models/snackbarItem.ts @@ -0,0 +1,3 @@ +import { OptionsObject } from "notistack"; + +export type SnackbarItem = { key: number, message: string, options?: OptionsObject }; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/run.ts b/sdnr/wt/odlux/framework/src/run.ts new file mode 100644 index 000000000..68eff37d6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/run.ts @@ -0,0 +1 @@ +export { runApplication } from './app'; diff --git a/sdnr/wt/odlux/framework/src/services/applicationApi.ts b/sdnr/wt/odlux/framework/src/services/applicationApi.ts new file mode 100644 index 000000000..bddfb24c6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/applicationApi.ts @@ -0,0 +1,25 @@ +import { ApplicationStore } from '../store/applicationStore'; + + +let resolveApplicationStoreInitialized: (store: ApplicationStore) => void; +let applicationStore: ApplicationStore | null = null; +const applicationStoreInitialized: Promise = new Promise((resolve) => resolveApplicationStoreInitialized = resolve); + +export const setApplicationStore = (store: ApplicationStore) => { + if (!applicationStore && store) { + applicationStore = store; + resolveApplicationStoreInitialized(store); + } +} + +export const applicationApi = { + get applicationStore(): ApplicationStore | null { + return applicationStore; + }, + + get applicationStoreInitialized(): Promise { + return applicationStoreInitialized; + } +}; + +export default applicationApi; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/services/applicationManager.ts b/sdnr/wt/odlux/framework/src/services/applicationManager.ts new file mode 100644 index 000000000..b7a6f2efc --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/applicationManager.ts @@ -0,0 +1,36 @@ +import { ApplicationInfo } from '../models/applicationInfo'; +import { Event } from '../common/event'; + +import { applicationApi } from './applicationApi'; + +/** Represents registry to manage all applications. */ +class ApplicationManager { + + /** Stores all registerd applications. */ + private _applications: { [key: string]: ApplicationInfo }; + + /** Initializes a new instance of this class. */ + constructor() { + this._applications = {}; + this.changed = new Event(); + } + + /** The chaged event will fire if the registration has changed. */ + public changed: Event; + + /** Registers a new application. */ + public registerApplication(applicationInfo: ApplicationInfo) { + this._applications[applicationInfo.name] = applicationInfo; + this.changed.invoke(); + return applicationApi; + } + + /** Gets all registered applications. */ + public get applications() { + return this._applications; + } +} + +/** A singleton instance of the application manager. */ +export const applicationManager = new ApplicationManager(); +export default applicationManager; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/services/authenticationService.ts b/sdnr/wt/odlux/framework/src/services/authenticationService.ts new file mode 100644 index 000000000..5e6fc81a8 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/authenticationService.ts @@ -0,0 +1,16 @@ +function timeout(ms:number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +class AuthenticationService { + public async authenticateUser(email: string, password: string) : Promise { + await timeout(650); + if (email === "max@odlux.com" && password === "geheim") { + return "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPRExVWCIsImlhdCI6MTUzODQ2NDMyMCwiZXhwIjoxNTcwMDAwMzIwLCJhdWQiOiJsb2NhbGhvc3QiLCJzdWIiOiJsb2NhbGhvc3QiLCJmaXJzdE5hbWUiOiJNYXgiLCJsYXN0TmFtZSI6Ik11c3Rlcm1hbm4iLCJlbWFpbCI6Im1heEBvZGx1eC5jb20iLCJyb2xlIjpbInVzZXIiLCJhZG1pbiJdfQ.9e5hDi2uxmIXNwHkJoScBZsHBk0jQ8CcZ7YIcZhDtuI" + } + return null; + } +} + +export const authenticationService = new AuthenticationService(); +export default authenticationService; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/services/index.ts b/sdnr/wt/odlux/framework/src/services/index.ts new file mode 100644 index 000000000..2bff79ac6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/index.ts @@ -0,0 +1,4 @@ +export { applicationManager } from './applicationManager'; +export { subscribe, unsubscribe } from './notificationService'; +export { requestRest } from './restService'; + diff --git a/sdnr/wt/odlux/framework/src/services/notificationService.ts b/sdnr/wt/odlux/framework/src/services/notificationService.ts new file mode 100644 index 000000000..242a6c03b --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/notificationService.ts @@ -0,0 +1,137 @@ +import * as X2JS from 'x2js'; + +const socketUrl = [ location.protocol === 'https:' ? 'wss://' : 'ws://', 'admin', ':', 'admin', '@', location.hostname, ':',location.port,'/websocket'].join(''); +const subscriptions: { [scope: string]: SubscriptionCallback[] } = { }; + +export interface IFormatedMessage { + notifType: string | null; + time: string; +} + +export type SubscriptionCallback = (msg: TMessage) => void; + +function formatData(event: MessageEvent) : IFormatedMessage | undefined { + + var x2js = new X2JS(); + var jsonObj: { [key: string]: IFormatedMessage } = x2js.xml2js(event.data); + if (jsonObj && typeof (jsonObj) === 'object') { + + const notifType = Object.keys(jsonObj)[0]; + const formated = jsonObj[notifType]; + formated.notifType = notifType ; + formated.time = new Date().toISOString(); + return formated; + } + return undefined; + +} + +export function subscribe(scope: string | string[], callback: SubscriptionCallback): Promise { + return socketReady.then((notificationSocket) => { + const scopes = scope instanceof Array ? scope : [scope]; + + // send all new scopes to subscribe + const newScopesToSubscribe: string[] = scopes.reduce((acc: string[], cur: string) => { + const currentCallbacks = subscriptions[cur]; + if (currentCallbacks) { + if (!currentCallbacks.some(c => c === callback)) { + currentCallbacks.push(callback); + } + } else { + subscriptions[cur] = [callback]; + acc.push(cur); + } + return acc; + }, []); + + if (newScopesToSubscribe.length === 0) { + return true; + } + + // send a subscription to all active scopes + const scopesToSubscribe = Object.keys(subscriptions); + if (notificationSocket.readyState === notificationSocket.OPEN) { + const data = { + 'data': 'scopes', + 'scopes': scopesToSubscribe + }; + notificationSocket.send(JSON.stringify(data)); + return true; + } + return false; + }); +} + +export function unsubscribe(scope: string | string[], callback: SubscriptionCallback): Promise { + return socketReady.then((notificationSocket) => { + const scopes = scope instanceof Array ? scope : [scope]; + scopes.forEach(s => { + const callbacks = subscriptions[s]; + const index = callbacks && callbacks.indexOf(callback); + if (index > -1) { + callbacks.splice(index, 1); + } + if (callbacks.length === 0) { + subscriptions[s] === undefined; + } + }); + + // send a subscription to all active scopes + const scopesToSubscribe = Object.keys(subscriptions); + if (notificationSocket.readyState === notificationSocket.OPEN) { + const data = { + 'data': 'scopes', + 'scopes': scopesToSubscribe + }; + notificationSocket.send(JSON.stringify(data)); + return true; + } + return false; + }); +} + +const connect = (): Promise => { + return new Promise((resolve, reject) => { + const notificationSocket = new WebSocket(socketUrl); + + notificationSocket.onmessage = (event) => { + // process received event + if (typeof event.data === 'string') { + const formated = formatData(event); + if (formated && formated.notifType) { + const callbacks = subscriptions[formated.notifType]; + if (callbacks) { + callbacks.forEach(cb => { + // ensure all callbacks will be called + try { + return cb(formated); + } catch (reason) { + console.error(reason); + } + }); + } + } + } + }; + + notificationSocket.onerror = function (error) { + console.log("Socket error: " + error); + reject("Socket error: " + error); + }; + + notificationSocket.onopen = function (event) { + console.log("Socket connection opened."); + resolve(notificationSocket); + }; + + notificationSocket.onclose = function (event) { + socketReady = connect(); + }; + }); +} + +let socketReady = connect(); + + + + diff --git a/sdnr/wt/odlux/framework/src/services/restAccessorService.ts b/sdnr/wt/odlux/framework/src/services/restAccessorService.ts new file mode 100644 index 000000000..66bf12b15 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/restAccessorService.ts @@ -0,0 +1,76 @@ +import * as $ from 'jquery'; +import { Action, IActionHandler } from '../flux/action'; +import { MiddlewareArg } from '../flux/middleware'; +import { Dispatch } from '../flux/store'; + +import { IApplicationStoreState } from '../store/applicationStore'; +import { AddErrorInfoAction, ErrorInfo } from '../actions/errorActions'; +import { PlainObject, AjaxParameter } from 'models/restService'; + +export const absoluteUri = /^(https?:\/\/|blob:)/i; +export const baseUrl = `${ window.location.origin }${ window.location.pathname }`; + +class RestBaseAction extends Action { } + +export const createRestApiAccessor = (urlOrPath: string, initialValue: TResult) => { + const isLocalRequest = !absoluteUri.test(urlOrPath); + const uri = isLocalRequest ? `${ baseUrl }/${ urlOrPath }`.replace(/\/{2,}/, '/') : urlOrPath ; + + class RestRequestAction extends RestBaseAction { constructor(public settings?: AjaxParameter) { super(); } } + + class RestResponseAction extends RestBaseAction { constructor(public result: TResult) { super(); } } + + class RestErrorAction extends RestBaseAction { constructor(public error?: Error | string) { super(); } } + + type RestAction = RestRequestAction | RestResponseAction | RestErrorAction; + + /** Represents our middleware to handle rest backend requests */ + const restMiddleware = (api: MiddlewareArg) => + (next: Dispatch) => (action: RestAction): RestAction => { + + // pass all actions through by default + next(action); + // handle the RestRequestAction + if (action instanceof RestRequestAction) { + const state = api.getState(); + const authHeader = isLocalRequest && state && state.framework.authenticationState.user && state.framework.authenticationState.user.token + ? { "Authentication": "Bearer " + state.framework.authenticationState.user.token } : { }; + $.ajax({ + url: uri, + method: (action.settings && action.settings.method) || "GET", + headers: { ...authHeader, ...action.settings && action.settings.headers ? action.settings.headers : { } }, + }).then((data: TResult) => { + next(new RestResponseAction(data)); + }).catch((err: any) => { + next(new RestErrorAction()); + next(new AddErrorInfoAction((err instanceof Error) ? { error: err } : { message: err.toString() })); + }); + } + // allways return action + return action; + }; + + /** Represents our action handler to handle our actions */ + const restActionHandler: IActionHandler = (state = initialValue, action) => { + if (action instanceof RestRequestAction) { + return { + ...(state as any), + busy: true + }; + } else if (action instanceof RestResponseAction) { + return action.result; + } else if (action instanceof RestErrorAction) { + return initialValue; + } + return state; + }; + + return { + requestAction: RestRequestAction, + actionHandler: restActionHandler, + middleware: restMiddleware, + }; +} + + + diff --git a/sdnr/wt/odlux/framework/src/services/restService.ts b/sdnr/wt/odlux/framework/src/services/restService.ts new file mode 100644 index 000000000..83c005c13 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/restService.ts @@ -0,0 +1,29 @@ + +const baseUri = `${ window.location.origin }`; +const absUrlPattern = /^https?:\/\//; + +export async function requestRest(path: string = '', init: RequestInit = {}, authenticate: boolean = false): Promise { + const isAbsUrl = absUrlPattern.test(path); + const uri = isAbsUrl ? path : (baseUri) + ('/' + path).replace(/\/{2,}/i, '/'); + init.headers = { + 'method': 'GET', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...init.headers + }; + if (!isAbsUrl && authenticate) { + init.headers = { + ...init.headers, + 'Authorization': 'Basic YWRtaW46S3A4Yko0U1hzek0wV1hsaGFrM2VIbGNzZTJnQXc4NHZhb0dHbUp2VXkyVQ==' + }; + } + const result = await fetch(uri, init); + const contentType = result.headers.get("Content-Type") || result.headers.get("content-type"); + const isJson = contentType && contentType.toLowerCase().startsWith("application/json"); + try { + const data = result.ok && (isJson ? await result.json() : await result.text()) as TData ; + return data; + } catch { + return null; + } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/services/snackbarService.ts b/sdnr/wt/odlux/framework/src/services/snackbarService.ts new file mode 100644 index 000000000..a8bbf1602 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/services/snackbarService.ts @@ -0,0 +1,5 @@ +import { OptionsObject } from "notistack"; + +export const snackbarService = { + enqueueSnackbar: (message: string, options?: OptionsObject) =>{ } +} \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/store/applicationStore.ts b/sdnr/wt/odlux/framework/src/store/applicationStore.ts new file mode 100644 index 000000000..97c98d120 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/store/applicationStore.ts @@ -0,0 +1,56 @@ + +import { Store } from '../flux/store'; +import { combineActionHandler, MiddlewareArg, Middleware, chainMiddleware } from '../flux/middleware'; + +import applicationService from '../services/applicationManager'; + +import { applicationRegistryHandler, IApplicationRegistration } from '../handlers/applicationRegistryHandler'; +import { authenticationStateHandler, IAuthenticationState } from '../handlers/authenticationHandler'; +import { applicationStateHandler, IApplicationState } from '../handlers/applicationStateHandler'; +import { navigationStateHandler, INavigationState } from '../handlers/navigationStateHandler'; + +import { setApplicationStore } from '../services/applicationApi'; + +import apiMiddleware from '../middleware/api'; +import thunkMiddleware from '../middleware/thunk'; +import loggerMiddleware from '../middleware/logger'; +import routerMiddleware from '../middleware/navigation'; + +export type MiddlewareApi = MiddlewareArg; + +export interface IFrameworkStoreState { + applicationRegistraion: IApplicationRegistration; + applicationState: IApplicationState; + authenticationState: IAuthenticationState; + navigationState: INavigationState; +} + +export interface IApplicationStoreState { + framework: IFrameworkStoreState; +} + +const frameworkHandlers = combineActionHandler({ + applicationRegistraion: applicationRegistryHandler, + applicationState: applicationStateHandler, + authenticationState: authenticationStateHandler, + navigationState: navigationStateHandler +}); + +export class ApplicationStore extends Store { } + +/** This function will create the application store considering the currently registered application ans their middlewares. */ +export const applicationStoreCreator = (): ApplicationStore => { + const middlewares: Middleware[] = []; + const actionHandlers = Object.keys(applicationService.applications).reduce((acc, cur) => { + const reg = applicationService.applications[cur]; + reg && typeof reg.rootActionHandler === 'function' && (acc[cur] = reg.rootActionHandler); + reg && +(reg.middlewares || 0) && middlewares.push(...(reg.middlewares as Middleware[])); + return acc; + }, { framework: frameworkHandlers } as any); + + const applicationStore = new ApplicationStore(combineActionHandler(actionHandlers), chainMiddleware(loggerMiddleware, thunkMiddleware, routerMiddleware, apiMiddleware, ...middlewares)); + setApplicationStore(applicationStore); + return applicationStore; +} + +export default applicationStoreCreator; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/styles/att.ts b/sdnr/wt/odlux/framework/src/styles/att.ts new file mode 100644 index 000000000..e1444cb24 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/styles/att.ts @@ -0,0 +1,29 @@ + +import { createMuiTheme } from '@material-ui/core/styles'; + +const theme = createMuiTheme({ + design: { + id: "att", + name: "AT&T", + url: "https://pmcvariety.files.wordpress.com/2016/04/att_logo.jpg?w=1000&h=563&crop=1", + height: 70, + width: 150, + logoHeight: 60, + }, + palette: { + primary: { + light: "#f2f2f29c", + main: "#f2f2f2", + dark: "#d5d5d5", + contrastText: "#0094d3" + }, + secondary: { + light: "#f2f2f2", + main: "rgba(51, 171, 226, 1)", + dark: "rgba(41, 159, 213, 1)", + contrastText: "#0094d3" + } + }, +}); + +export default theme; diff --git a/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts b/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts new file mode 100644 index 000000000..aeab8a0d6 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/utilities/elasticSearch.ts @@ -0,0 +1,70 @@ + +import { DataCallback } from '../components/material-table'; +import { Result, HitEntry } from '../models'; + +type propType = string | number | null | undefined | (string | number)[]; +type dataType = { [prop: string]: propType }; +type resultType = { page: number, rowCount: number, rows: TData[] }; + +export function createSearchDataHandler(uri: string, additionalParameters?: {}): DataCallback<(TResult & { _id: string })>; +export function createSearchDataHandler(uri: string, additionalParameters: {} | null | undefined, mapResult: (res: HitEntry, index: number, arr: HitEntry[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })> +export function createSearchDataHandler(uri: string, additionalParameters?: {} | null | undefined, mapResult?: (res: HitEntry, index: number, arr: HitEntry[]) => (TData & { _id: string }), mapRequest?: (name?: string | null) => string): DataCallback<(TData & { _id: string })> { + const url = `${ window.location.origin }/database/${uri}/_search`; + const fetchData: DataCallback<(TData & { _id: string }) > = async (page, rowsPerPage, orderBy, order, filter) => { + const from = rowsPerPage && page != null && !isNaN(+page) + ? (+page) * rowsPerPage + : null; + + const filterKeys = filter && Object.keys(filter) || []; + + const query = { + ...filterKeys.length > 0 ? { + query: { + bool: { + must: filterKeys.reduce((acc, cur) => { + if (acc && filter && filter[cur]) { + acc.push({ [filter[cur].indexOf("*") > -1 || filter[cur].indexOf("?") > -1 ? "wildcard" : "prefix"]: { [mapRequest ? mapRequest(cur) : cur]: filter[cur] } }); + } + return acc; + }, [] as any[]) + } + } + } : { "query": { "match_all": {} } }, + ...rowsPerPage ? { "size": rowsPerPage } : {}, + ...from ? { "from": from } : {}, + ...orderBy && order ? { "sort": [{ [mapRequest ? mapRequest(orderBy) : orderBy]: order }] } : {}, + ...additionalParameters ? additionalParameters : {} + }; + const result = await fetch(url, { + method: "POST", // *GET, POST, PUT, DELETE, etc. + mode: "no-cors", // no-cors, cors, *same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + headers: { + "Content-Type": "application/json; charset=utf-8", + // "Content-Type": "application/x-www-form-urlencoded", + }, + body: JSON.stringify(query), // body data type must match "Content-Type" header + }); + + if (result.ok) { + const queryResult: Result = await result.json(); + let rows: (TData & { _id: string })[] = []; + + if (queryResult && queryResult.hits && queryResult.hits.hits) { + rows = queryResult.hits.hits.map( mapResult ? mapResult : h => ( + { ...(h._source as any as TData), _id: h._id } + )) || [] + } + + const data = { + page: Math.min(page || 0, queryResult.hits.total || 0 / (rowsPerPage || 1)), rowCount: queryResult.hits.total, rows: rows + }; + return data; + } + + return { page: 0, rowCount: 0, rows: [] }; + }; + + return fetchData; +} + diff --git a/sdnr/wt/odlux/framework/src/utilities/withComponents.ts b/sdnr/wt/odlux/framework/src/utilities/withComponents.ts new file mode 100644 index 000000000..af7c65b5c --- /dev/null +++ b/sdnr/wt/odlux/framework/src/utilities/withComponents.ts @@ -0,0 +1,20 @@ +import * as React from 'react'; +import applicationService from '../services/applicationManager'; +export type WithComponents = { + components: { [prop in keyof T]: React.ComponentType } +}; + +export function withComponents(mapping: TMap) { + return (component: React.ComponentType>): React.ComponentType => { + const components = {} as any; + Object.keys(mapping).forEach(name => { + const [appKey, componentKey] = mapping[name].split('.'); + const reg = applicationService.applications[appKey]; + components[name] = reg && reg.exportedComponents && reg.exportedComponents[componentKey] || (() => null); + }); + return (props: TProps) => ( + React.createElement(component, Object.assign({ components }, props)) + ); + } +} +export default withComponents; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/views/about.tsx b/sdnr/wt/odlux/framework/src/views/about.tsx new file mode 100644 index 000000000..f905e0e75 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/views/about.tsx @@ -0,0 +1,859 @@ +import * as React from 'react'; + +import { withComponents, WithComponents } from '../utilities/withComponents'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import ExpansionPanel from '@material-ui/core/ExpansionPanel'; +import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary'; +import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; + +import { MaterialTable, MaterialTableCtorType, ColumnType } from '../components/material-table'; +import { TreeView, ITreeItem, TreeViewCtorType } from '../components/material-ui/treeView'; +import { SvgIconProps } from '@material-ui/core/SvgIcon'; + +const styles = (theme: Theme) => createStyles({ + root: { + width: '100%', + }, + heading: { + fontSize: theme.typography.pxToRem(15), + fontWeight: theme.typography.fontWeightRegular, + }, +}); + +class SampleData { + _id: string; + index: number; + guid: string; + isActive: boolean; + balance: string; + age: number; + firstName: string; + lastName: string; + company: string; + email: string; + registered: string; + latitude: string; + longitude: string; +} + +// https://next.json-generator.com/NJ5Bv-v1I +const tableData: SampleData[] = [ + { + "_id": "5c0e18399919a5c43636fdf2", + "index": 0, + "guid": "48728d8e-8300-4d0f-b967-e2166d023066", + "isActive": false, + "balance": "$3,480.16", + "age": 33, + "firstName": "Brooke", + "lastName": "Morris", + "company": "ZORROMOP", + "email": "brooke.morris@zorromop.de", + "registered": "Sunday, February 11, 2018 2:55 PM", + "latitude": "-69.109379", + "longitude": "113.735639" + }, + { + "_id": "5c0e1839b61e3eeaf164259d", + "index": 1, + "guid": "28723570-1507-422e-b78c-924402371fb1", + "isActive": false, + "balance": "$1,305.01", + "age": 28, + "firstName": "Jolene", + "lastName": "Everett", + "company": "ZENCO", + "email": "jolene.everett@zenco.de", + "registered": "Saturday, December 8, 2018 5:17 PM", + "latitude": "13.683025", + "longitude": "85.101421" + }, + { + "_id": "5c0e1839e81f57913c5d2147", + "index": 2, + "guid": "e914dc5d-91a3-405d-ac48-aee6f0cd391a", + "isActive": true, + "balance": "$1,418.37", + "age": 28, + "firstName": "Elva", + "lastName": "Travis", + "company": "ZYTREK", + "email": "elva.travis@zytrek.de", + "registered": "Thursday, March 10, 2016 5:13 PM", + "latitude": "53.75862", + "longitude": "-67.784532" + }, + { + "_id": "5c0e1839bc9224a2b54c0f69", + "index": 3, + "guid": "88cbdce0-0bcc-4d16-83c3-3017690503c4", + "isActive": true, + "balance": "$1,709.60", + "age": 21, + "firstName": "Ellis", + "lastName": "Mcpherson", + "company": "DIGIPRINT", + "email": "ellis.mcpherson@digiprint.de", + "registered": "Sunday, December 21, 2014 5:25 AM", + "latitude": "46.486149", + "longitude": "-66.657067" + }, + { + "_id": "5c0e183951b51475db0f35d1", + "index": 4, + "guid": "c887ac86-7ba1-4eb6-9b47-e88a1bcb3713", + "isActive": true, + "balance": "$3,578.54", + "age": 25, + "firstName": "Marcia", + "lastName": "Rocha", + "company": "ZAPPIX", + "email": "marcia.rocha@zappix.de", + "registered": "Tuesday, June 16, 2015 11:21 AM", + "latitude": "-39.905461", + "longitude": "150.873895" + }, + { + "_id": "5c0e18398c5be8d362a578eb", + "index": 5, + "guid": "0d160697-9b5b-4941-9b5f-4ba3a7f97b49", + "isActive": true, + "balance": "$414.98", + "age": 32, + "firstName": "Lavonne", + "lastName": "Wilkins", + "company": "FARMAGE", + "email": "lavonne.wilkins@farmage.de", + "registered": "Monday, February 1, 2016 5:27 PM", + "latitude": "-16.839256", + "longitude": "-105.824746" + }, + { + "_id": "5c0e18399804086c836d7d56", + "index": 6, + "guid": "715a5f63-35b6-4903-a46e-ba584b005e64", + "isActive": false, + "balance": "$1,755.78", + "age": 32, + "firstName": "Wise", + "lastName": "Berg", + "company": "ZIZZLE", + "email": "wise.berg@zizzle.de", + "registered": "Saturday, March 28, 2015 1:40 AM", + "latitude": "51.15269", + "longitude": "65.795093" + }, + { + "_id": "5c0e18399c4d13538bcaf8c9", + "index": 7, + "guid": "7ee50269-23e8-499e-9a16-09f393d7600c", + "isActive": false, + "balance": "$342.52", + "age": 27, + "firstName": "Isabel", + "lastName": "Battle", + "company": "EZENTIA", + "email": "isabel.battle@ezentia.de", + "registered": "Thursday, June 7, 2018 12:16 AM", + "latitude": "-53.318152", + "longitude": "-153.516824" + }, + { + "_id": "5c0e18398d7fb9a4eceeffa2", + "index": 8, + "guid": "1e30c9ac-2297-4f16-83e6-9559b1ebe92c", + "isActive": true, + "balance": "$3,184.71", + "age": 36, + "firstName": "Lenora", + "lastName": "Crawford", + "company": "KIDGREASE", + "email": "lenora.crawford@kidgrease.de", + "registered": "Saturday, January 7, 2017 6:17 PM", + "latitude": "-72.431496", + "longitude": "9.413359" + }, + { + "_id": "5c0e18395837069ab6b79d00", + "index": 9, + "guid": "d04a02ed-5899-4729-a7e5-2d85b5d03973", + "isActive": true, + "balance": "$1,553.28", + "age": 35, + "firstName": "Sasha", + "lastName": "Bridges", + "company": "IDEALIS", + "email": "sasha.bridges@idealis.de", + "registered": "Sunday, February 4, 2018 7:02 PM", + "latitude": "8.095691", + "longitude": "-105.758195" + }, + { + "_id": "5c0e18390be19bf65acad180", + "index": 10, + "guid": "3a1a77e6-ef15-4598-8274-c68ac3bb922a", + "isActive": false, + "balance": "$3,587.96", + "age": 20, + "firstName": "Wilkins", + "lastName": "Beasley", + "company": "DIGIFAD", + "email": "wilkins.beasley@digifad.de", + "registered": "Monday, March 5, 2018 1:27 PM", + "latitude": "-88.062704", + "longitude": "149.95661" + }, + { + "_id": "5c0e1839ffbbad5c9954e49f", + "index": 11, + "guid": "97a56950-a08c-4e00-8002-ba2d5de4da5d", + "isActive": false, + "balance": "$1,997.80", + "age": 31, + "firstName": "Sullivan", + "lastName": "Mcclain", + "company": "EARTHMARK", + "email": "sullivan.mcclain@earthmark.de", + "registered": "Saturday, October 27, 2018 2:51 PM", + "latitude": "-81.86349", + "longitude": "-79.596991" + }, + { + "_id": "5c0e183914bd464d55e7325f", + "index": 12, + "guid": "294f6485-d0f9-4b25-b998-325ae90fa769", + "isActive": true, + "balance": "$1,405.46", + "age": 24, + "firstName": "Herminia", + "lastName": "Fischer", + "company": "ECOLIGHT", + "email": "herminia.fischer@ecolight.de", + "registered": "Thursday, January 16, 2014 4:48 PM", + "latitude": "48.224363", + "longitude": "11.08339" + }, + { + "_id": "5c0e183968ec2556d8f6566c", + "index": 13, + "guid": "16edfea4-7b37-4e54-868c-c369b413dd78", + "isActive": false, + "balance": "$3,440.67", + "age": 39, + "firstName": "Blanchard", + "lastName": "Blackwell", + "company": "GEOFORMA", + "email": "blanchard.blackwell@geoforma.de", + "registered": "Wednesday, July 30, 2014 4:07 AM", + "latitude": "-52.169297", + "longitude": "10.415879" + }, + { + "_id": "5c0e183939a0fc955f2d94da", + "index": 14, + "guid": "4ed454e2-dde1-4ab5-a434-4a82205ced2d", + "isActive": true, + "balance": "$1,883.27", + "age": 35, + "firstName": "Gayle", + "lastName": "Little", + "company": "AQUAZURE", + "email": "gayle.little@aquazure.de", + "registered": "Tuesday, December 12, 2017 5:08 PM", + "latitude": "-58.473236", + "longitude": "38.022269" + }, + { + "_id": "5c0e1839099f9221ccd968ac", + "index": 15, + "guid": "1d052fd4-7c54-45fb-b0db-7de1acc4262a", + "isActive": false, + "balance": "$2,601.94", + "age": 31, + "firstName": "Jocelyn", + "lastName": "Richards", + "company": "GINK", + "email": "jocelyn.richards@gink.de", + "registered": "Sunday, October 30, 2016 9:12 PM", + "latitude": "-43.489676", + "longitude": "2.557869" + }, + { + "_id": "5c0e183970f320f377321c3f", + "index": 16, + "guid": "45bca125-8831-48c3-b22b-29ae318e7096", + "isActive": false, + "balance": "$3,441.74", + "age": 34, + "firstName": "Berta", + "lastName": "Valentine", + "company": "ISOSPHERE", + "email": "berta.valentine@isosphere.de", + "registered": "Sunday, March 19, 2017 8:22 PM", + "latitude": "-40.188039", + "longitude": "-170.085092" + }, + { + "_id": "5c0e1839ab960bb0a9f4f392", + "index": 17, + "guid": "d7b5122a-94c9-423c-b799-1a8f8314b152", + "isActive": false, + "balance": "$56.39", + "age": 21, + "firstName": "Russell", + "lastName": "Powers", + "company": "TETAK", + "email": "russell.powers@tetak.de", + "registered": "Thursday, November 3, 2016 9:23 PM", + "latitude": "-51.610519", + "longitude": "-133.280363" + }, + { + "_id": "5c0e183998f0195404b9aaa4", + "index": 18, + "guid": "a043ba97-ea7e-48ce-bb15-18ee09fb393d", + "isActive": true, + "balance": "$1,503.57", + "age": 37, + "firstName": "Rosario", + "lastName": "Brennan", + "company": "VIAGRAND", + "email": "rosario.brennan@viagrand.de", + "registered": "Saturday, March 17, 2018 10:32 PM", + "latitude": "-43.773365", + "longitude": "47.58682" + }, + { + "_id": "5c0e1839bcb2a5cc567129ac", + "index": 19, + "guid": "de6d5d36-201e-4f87-9976-ed31f3160e42", + "isActive": false, + "balance": "$1,160.18", + "age": 29, + "firstName": "Anita", + "lastName": "Hodges", + "company": "TUBALUM", + "email": "anita.hodges@tubalum.de", + "registered": "Sunday, November 26, 2017 11:54 AM", + "latitude": "7.080244", + "longitude": "-9.970715" + }, + { + "_id": "5c0e18394b37e854a1ef371c", + "index": 20, + "guid": "9407113b-896a-4699-ac1b-363bc3c6f8ad", + "isActive": false, + "balance": "$34.81", + "age": 31, + "firstName": "Barrett", + "lastName": "Weaver", + "company": "DUOFLEX", + "email": "barrett.weaver@duoflex.de", + "registered": "Tuesday, November 3, 2015 9:31 AM", + "latitude": "40.30558", + "longitude": "-69.986664" + }, + { + "_id": "5c0e1839b5658f90e16a86e0", + "index": 21, + "guid": "81f894c4-c931-422d-a30e-593824d95bf9", + "isActive": true, + "balance": "$2,808.63", + "age": 26, + "firstName": "Baxter", + "lastName": "Chase", + "company": "BUNGA", + "email": "baxter.chase@bunga.de", + "registered": "Friday, October 28, 2016 7:10 AM", + "latitude": "-49.05652", + "longitude": "63.123535" + }, + { + "_id": "5c0e1839cb9462c9ecbb59af", + "index": 22, + "guid": "92e67862-4fdf-43af-a3ef-ef3edb8d6706", + "isActive": true, + "balance": "$3,552.71", + "age": 29, + "firstName": "Olga", + "lastName": "Kemp", + "company": "OHMNET", + "email": "olga.kemp@ohmnet.de", + "registered": "Saturday, March 26, 2016 11:51 AM", + "latitude": "-17.450481", + "longitude": "-13.945794" + }, + { + "_id": "5c0e18396f999c2b8ac731a9", + "index": 23, + "guid": "a682eaae-34f0-4973-b8a0-30972de0732b", + "isActive": false, + "balance": "$1,999.20", + "age": 21, + "firstName": "Ebony", + "lastName": "Le", + "company": "MULTRON", + "email": "ebony.le@multron.de", + "registered": "Friday, March 27, 2015 9:23 AM", + "latitude": "-70.380014", + "longitude": "173.20685" + }, + { + "_id": "5c0e18391cfb28263eb42db7", + "index": 24, + "guid": "f1cddb5f-0b89-453e-b0c9-8193a56cc610", + "isActive": true, + "balance": "$2,950.91", + "age": 30, + "firstName": "Norman", + "lastName": "Price", + "company": "COMVEX", + "email": "norman.price@comvex.de", + "registered": "Tuesday, August 21, 2018 11:17 PM", + "latitude": "86.501469", + "longitude": "159.545352" + }, + { + "_id": "5c0e18394a6be11128c7e5ca", + "index": 25, + "guid": "dadb738a-40fd-45b6-abac-023a803d95c2", + "isActive": true, + "balance": "$2,767.09", + "age": 25, + "firstName": "Sara", + "lastName": "Ruiz", + "company": "AUSTECH", + "email": "sara.ruiz@austech.de", + "registered": "Wednesday, June 20, 2018 6:34 AM", + "latitude": "86.784904", + "longitude": "-120.331325" + }, + { + "_id": "5c0e183974631549eda97cea", + "index": 26, + "guid": "b5c43ee5-14ed-4ab5-b3db-b31a8bb65ceb", + "isActive": true, + "balance": "$3,235.42", + "age": 32, + "firstName": "Holly", + "lastName": "Santos", + "company": "LOVEPAD", + "email": "holly.santos@lovepad.de", + "registered": "Thursday, November 22, 2018 9:26 PM", + "latitude": "-19.640066", + "longitude": "50.410992" + }, + { + "_id": "5c0e1839ab9b933881429d78", + "index": 27, + "guid": "94961092-65ca-41b9-bc69-3e40ce2cafc9", + "isActive": true, + "balance": "$2,106.34", + "age": 39, + "firstName": "Rachel", + "lastName": "Douglas", + "company": "DEMINIMUM", + "email": "rachel.douglas@deminimum.de", + "registered": "Sunday, April 9, 2017 3:55 AM", + "latitude": "31.395281", + "longitude": "-1.899514" + }, + { + "_id": "5c0e183937f743155859c5a9", + "index": 28, + "guid": "07d7ef18-bcef-483d-999e-0b3da4a7098b", + "isActive": true, + "balance": "$2,260.65", + "age": 40, + "firstName": "Reed", + "lastName": "Workman", + "company": "BUZZMAKER", + "email": "reed.workman@buzzmaker.de", + "registered": "Wednesday, May 28, 2014 3:44 PM", + "latitude": "23.789646", + "longitude": "106.938375" + }, + { + "_id": "5c0e1839f8f4b60beb28b7ed", + "index": 29, + "guid": "9b4952e5-aa0e-4919-9e17-7c357a297394", + "isActive": false, + "balance": "$702.99", + "age": 27, + "firstName": "Cochran", + "lastName": "Ware", + "company": "HIVEDOM", + "email": "cochran.ware@hivedom.de", + "registered": "Monday, October 16, 2017 5:51 AM", + "latitude": "85.953108", + "longitude": "124.590037" + }, + { + "_id": "5c0e1839342fbd54a88269df", + "index": 30, + "guid": "30937d5b-9514-4ebd-b628-2cfb5017fe41", + "isActive": false, + "balance": "$385.88", + "age": 35, + "firstName": "Cote", + "lastName": "Hess", + "company": "TERAPRENE", + "email": "cote.hess@teraprene.de", + "registered": "Thursday, March 15, 2018 4:42 PM", + "latitude": "81.38211", + "longitude": "64.516797" + }, + { + "_id": "5c0e18395b6dc85d73ce1fb3", + "index": 31, + "guid": "f34847da-7f96-4cd8-8d8a-b06c0eb0a8f2", + "isActive": true, + "balance": "$3,494.56", + "age": 27, + "firstName": "Daniels", + "lastName": "Ayala", + "company": "BESTO", + "email": "daniels.ayala@besto.de", + "registered": "Sunday, December 18, 2016 10:52 AM", + "latitude": "47.704227", + "longitude": "41.674767" + }, + { + "_id": "5c0e183974587cdccf30b13f", + "index": 32, + "guid": "fdbb6d83-0e47-4453-b8a7-b47f44e4164b", + "isActive": false, + "balance": "$2,087.38", + "age": 26, + "firstName": "Powers", + "lastName": "Drake", + "company": "GENESYNK", + "email": "powers.drake@genesynk.de", + "registered": "Saturday, September 29, 2018 12:24 AM", + "latitude": "40.580432", + "longitude": "110.940759" + }, + { + "_id": "5c0e18397b51245e971c58b8", + "index": 33, + "guid": "6adfe544-238b-4001-b2a6-f50ea3094da3", + "isActive": true, + "balance": "$3,566.22", + "age": 34, + "firstName": "Pacheco", + "lastName": "Ramsey", + "company": "ENVIRE", + "email": "pacheco.ramsey@envire.de", + "registered": "Friday, September 11, 2015 12:14 AM", + "latitude": "-30.691235", + "longitude": "69.343692" + }, + { + "_id": "5c0e18391ede9c0996fd09e7", + "index": 34, + "guid": "d190b32f-d33b-4c17-a18a-bb2f57e79ba7", + "isActive": false, + "balance": "$1,671.63", + "age": 32, + "firstName": "Mcintyre", + "lastName": "Chan", + "company": "ORBAXTER", + "email": "mcintyre.chan@orbaxter.de", + "registered": "Wednesday, May 7, 2014 7:11 PM", + "latitude": "7.380435", + "longitude": "70.955103" + }, + { + "_id": "5c0e1839fe48069c9c260fa9", + "index": 35, + "guid": "a41c064b-6bf4-4ba5-b229-9b657d286936", + "isActive": false, + "balance": "$24.02", + "age": 27, + "firstName": "Genevieve", + "lastName": "Sparks", + "company": "ZBOO", + "email": "genevieve.sparks@zboo.de", + "registered": "Saturday, December 16, 2017 2:51 PM", + "latitude": "-63.406337", + "longitude": "118.662621" + }, + { + "_id": "5c0e1839a7e8e76accf0803e", + "index": 36, + "guid": "3e71864d-4be5-418e-ace8-346c3d7a9c5f", + "isActive": true, + "balance": "$3,261.01", + "age": 30, + "firstName": "Powell", + "lastName": "Patterson", + "company": "GAZAK", + "email": "powell.patterson@gazak.de", + "registered": "Thursday, May 18, 2017 10:10 AM", + "latitude": "-10.428548", + "longitude": "64.979192" + }, + { + "_id": "5c0e183984b0320f1118a8b0", + "index": 37, + "guid": "ec5b292c-6efb-471b-9bf5-a47286e03515", + "isActive": false, + "balance": "$918.71", + "age": 37, + "firstName": "Tara", + "lastName": "Mcmillan", + "company": "GRAINSPOT", + "email": "tara.mcmillan@grainspot.de", + "registered": "Sunday, May 17, 2015 1:01 PM", + "latitude": "-13.519031", + "longitude": "67.931062" + }, + { + "_id": "5c0e183965875876835ccd79", + "index": 38, + "guid": "b7e97ffb-439a-4454-90af-7f5ebd565ebc", + "isActive": true, + "balance": "$574.99", + "age": 28, + "firstName": "Pennington", + "lastName": "Gallegos", + "company": "CEDWARD", + "email": "pennington.gallegos@cedward.de", + "registered": "Wednesday, September 26, 2018 6:01 AM", + "latitude": "-63.693261", + "longitude": "-38.352153" + }, + { + "_id": "5c0e183922505dd21be49009", + "index": 39, + "guid": "5187aa39-4357-462b-9508-3c537d26d70d", + "isActive": false, + "balance": "$2,447.08", + "age": 26, + "firstName": "Meagan", + "lastName": "Irwin", + "company": "SENTIA", + "email": "meagan.irwin@sentia.de", + "registered": "Saturday, April 2, 2016 4:39 PM", + "latitude": "1.051313", + "longitude": "-86.168315" + }, + { + "_id": "5c0e183900a9f7f896e5b3b1", + "index": 40, + "guid": "31889843-79e7-4636-9ca1-4eb5cbcb0ae3", + "isActive": true, + "balance": "$1,992.25", + "age": 22, + "firstName": "Kelly", + "lastName": "Cobb", + "company": "BOVIS", + "email": "kelly.cobb@bovis.de", + "registered": "Tuesday, August 9, 2016 5:36 PM", + "latitude": "-85.547579", + "longitude": "-89.794104" + }, + { + "_id": "5c0e18393b25b8552ff950e2", + "index": 41, + "guid": "0bf02edc-ca1b-4cfe-8356-b65881bdca11", + "isActive": true, + "balance": "$465.96", + "age": 27, + "firstName": "Angela", + "lastName": "Booker", + "company": "EQUICOM", + "email": "angela.booker@equicom.de", + "registered": "Thursday, July 30, 2015 1:39 AM", + "latitude": "-9.345395", + "longitude": "107.070665" + }, + { + "_id": "5c0e183955d747ebbe25437b", + "index": 42, + "guid": "6405e559-5849-4d12-ae4e-520f13b4dffe", + "isActive": true, + "balance": "$15.63", + "age": 28, + "firstName": "Carrie", + "lastName": "Mclean", + "company": "BOINK", + "email": "carrie.mclean@boink.de", + "registered": "Wednesday, February 1, 2017 1:50 PM", + "latitude": "72.287519", + "longitude": "-135.436286" + }, + { + "_id": "5c0e1839e9cfe1b28e31e7e6", + "index": 43, + "guid": "e49e7ca7-a6cc-4cdb-bebe-5a3b6ba931eb", + "isActive": true, + "balance": "$3,127.94", + "age": 33, + "firstName": "Callie", + "lastName": "Cooley", + "company": "MUSIX", + "email": "callie.cooley@musix.de", + "registered": "Wednesday, August 30, 2017 4:58 PM", + "latitude": "-38.954739", + "longitude": "-152.706424" + }, + { + "_id": "5c0e18391bafa0750ff4f280", + "index": 44, + "guid": "c245ffd3-4924-4dce-ae4a-f4cabf057b54", + "isActive": false, + "balance": "$1,320.36", + "age": 35, + "firstName": "Terry", + "lastName": "Bennett", + "company": "EXOTECHNO", + "email": "terry.bennett@exotechno.de", + "registered": "Friday, June 17, 2016 11:54 PM", + "latitude": "-48.946183", + "longitude": "32.53167" + }, + { + "_id": "5c0e1839e91b27fcce34b70f", + "index": 45, + "guid": "0860cb66-de4c-410e-8233-aeef5ee9d64e", + "isActive": false, + "balance": "$1,187.75", + "age": 30, + "firstName": "Phoebe", + "lastName": "Bartlett", + "company": "VORATAK", + "email": "phoebe.bartlett@voratak.de", + "registered": "Tuesday, July 25, 2017 2:57 AM", + "latitude": "-63.208957", + "longitude": "-91.209743" + }, + { + "_id": "5c0e183987e8a4e98415c8dd", + "index": 46, + "guid": "49219833-172c-4659-9192-d1116a5ca833", + "isActive": false, + "balance": "$3,225.24", + "age": 38, + "firstName": "Jordan", + "lastName": "Evans", + "company": "PHARMACON", + "email": "jordan.evans@pharmacon.de", + "registered": "Sunday, April 23, 2017 6:27 PM", + "latitude": "-59.454678", + "longitude": "67.251185" + }, + { + "_id": "5c0e183944979692cc1a3e48", + "index": 47, + "guid": "680c4d15-d539-4db9-8793-a2f6d3f354aa", + "isActive": false, + "balance": "$2,913.14", + "age": 28, + "firstName": "Goodman", + "lastName": "Cain", + "company": "CAXT", + "email": "goodman.cain@caxt.de", + "registered": "Tuesday, November 1, 2016 6:11 PM", + "latitude": "-30.187547", + "longitude": "-164.313273" + }, + { + "_id": "5c0e1839ef5312ac08e3cbc3", + "index": 48, + "guid": "85f5fa5d-b6b3-47c6-ad1b-faee10a4e1bd", + "isActive": true, + "balance": "$544.97", + "age": 27, + "firstName": "Aisha", + "lastName": "Oliver", + "company": "MINGA", + "email": "aisha.oliver@minga.de", + "registered": "Sunday, July 3, 2016 8:18 AM", + "latitude": "-21.527536", + "longitude": "141.029691" + }, + { + "_id": "5c0e1839c2e58f5da04f29fd", + "index": 49, + "guid": "e2ee9b25-5887-49a9-a1c6-17432154d266", + "isActive": true, + "balance": "$3,621.65", + "age": 31, + "firstName": "Erin", + "lastName": "Lester", + "company": "SLOFAST", + "email": "erin.lester@slofast.de", + "registered": "Saturday, February 20, 2016 5:13 AM", + "latitude": "-30.080798", + "longitude": "-1.291093" + } +]; + +const components = { + 'counter': 'demoApp.counter' +}; + +class TreeDemoItem implements ITreeItem { + title: string; + children?: TreeDemoItem[]; + disabled?: boolean; + icon?: React.ComponentType; +} + +const treeData: TreeDemoItem[] = [ + { title: "Erste Ebene", children: [ + { title: "Zweite Ebene", children: [ + { title: "Dritte Ebene" }, + ] + }, + { title: "Zweite Ebene 2" }, + ] + }, + { title: "Erste Ebene 3" }, +]; + +const SampleDataMaterialTable = MaterialTable as MaterialTableCtorType; + +const SampleTree = TreeView as any as TreeViewCtorType; + +const AboutComponent = (props: WithComponents & WithStyles) => { + + return ( +
+

About

+ + }> + Client Side Table Demo + + + (
Button
) }, + ] + } idProperty={ "_id" } title={ "Customers 2018" } > +
+
+
+ + }> + Tree Demo + + + + + +
+ ) +}; + +export const About = withComponents(components)(withStyles(styles)(AboutComponent)); +export default About; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/views/frame.tsx b/sdnr/wt/odlux/framework/src/views/frame.tsx new file mode 100644 index 000000000..fd943319d --- /dev/null +++ b/sdnr/wt/odlux/framework/src/views/frame.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { HashRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; + +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; +import { faHome, faAddressBook, faSignInAlt } from '@fortawesome/free-solid-svg-icons'; + +import AppFrame from '../components/routing/appFrame'; +import TitleBar from '../components/titleBar'; +import Menu from '../components/navigationMenu'; +import ErrorDisplay from '../components/errorDisplay'; +import SnackDisplay from '../components/material-ui/snackDisplay'; + +import Home from '../views/home'; +import Login from '../views/login'; +import About from '../views/about'; + +import applicationService from '../services/applicationManager'; +import { SnackbarProvider } from 'notistack'; + +const styles = (theme: Theme) => createStyles({ + root: { + flexGrow: 1, + height: '100%', + zIndex: 1, + overflow: 'hidden', + position: 'relative', + display: 'flex', + }, + content: { + flexGrow: 1, + display: "flex", + flexDirection: "column", + backgroundColor: theme.palette.background.default, + padding: theme.spacing.unit * 3, + minWidth: 0, // So the Typography noWrap works + }, + toolbar: theme.mixins.toolbar +}); + +export const Frame = withStyles(styles)(({ classes }: WithStyles) => { + const registrations = applicationService.applications; + return ( + + +
+ + + + +
+
+ + ( + + + + ) } /> + ( + + + + ) } /> + ( + + + + ) } /> + { Object.keys(registrations).map(p => { + const application = registrations[p]; + return ( ( + + + + ) } />) + }) } + + +
+
+
+
+ ); +}); + +export default Frame; diff --git a/sdnr/wt/odlux/framework/src/views/home.tsx b/sdnr/wt/odlux/framework/src/views/home.tsx new file mode 100644 index 000000000..3d7497401 --- /dev/null +++ b/sdnr/wt/odlux/framework/src/views/home.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Button from '@material-ui/core/Button'; + +class BuggyCounter extends React.Component<{}, {counter:number}> { + constructor(props: {}) { + super(props); + this.state = { counter: 0 }; + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.setState(({ counter }) => ({ + counter: counter + 1 + })); + } + + render() { + if (this.state.counter === 5) { + // Simulate a JS error + throw new Error('I crashed!'); + } + return

{ this.state.counter }

; + } +} + +export const Home = (props: React.Props) => { + return ( +
+

Welcome to ODLUX.

+ + +
+ ) +} + +export default Home; \ No newline at end of file diff --git a/sdnr/wt/odlux/framework/src/views/login.tsx b/sdnr/wt/odlux/framework/src/views/login.tsx new file mode 100644 index 000000000..6fa24e4ab --- /dev/null +++ b/sdnr/wt/odlux/framework/src/views/login.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; +import { withRouter, RouteComponentProps } from 'react-router-dom'; + +import Avatar from '@material-ui/core/Avatar'; +import Button from '@material-ui/core/Button'; +import CssBaseline from '@material-ui/core/CssBaseline'; +import FormControl from '@material-ui/core/FormControl'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Checkbox from '@material-ui/core/Checkbox'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import LockIcon from '@material-ui/icons/LockOutlined'; +import Paper from '@material-ui/core/Paper'; +import Typography from '@material-ui/core/Typography'; +import { withStyles, WithStyles, createStyles, Theme } from '@material-ui/core/styles'; + +import connect, { Connect } from '../flux/connect'; +import authenticationService from '../services/authenticationService'; + +import { UpdateAuthentication } from '../actions/authentication'; + +const styles = (theme: Theme) => createStyles({ + layout: { + width: 'auto', + display: 'block', // Fix IE11 issue. + marginLeft: theme.spacing.unit * 3, + marginRight: theme.spacing.unit * 3, + [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { + width: 400, + marginLeft: 'auto', + marginRight: 'auto', + }, + }, + paper: { + marginTop: theme.spacing.unit * 8, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: `${ theme.spacing.unit * 2 }px ${ theme.spacing.unit * 3 }px ${ theme.spacing.unit * 3 }px`, + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main, + }, + form: { + width: '100%', // Fix IE11 issue. + marginTop: theme.spacing.unit, + }, + submit: { + marginTop: theme.spacing.unit * 3, + }, +}); + +type LoginProps = RouteComponentProps<{}> & WithStyles & Connect ; + +interface ILoginState { + busy: boolean; + email: string; + password: string; +} + + +// todo: ggf. redirect to einbauen +class LoginComponent extends React.Component { + + constructor(props: LoginProps) { + super(props); + + this.state = { + busy: false, + email: '', + password: '' + }; + } + + render(): JSX.Element { + const { classes } = this.props; + return ( + + +
+ + + + + Sign in +
+ + Email Address + { this.setState({ email: event.target.value }) } }/> + + + Password + { this.setState({ password: event.target.value }) } } + /> + + } + label="Remember me" + /> + + +
+
+
+ ); + } + + private onSignIn = async (event: React.MouseEvent) => { + event.preventDefault(); + + this.setState({ busy: true }); + const token = await authenticationService.authenticateUser(this.state.email, this.state.password); + this.props.dispatch(new UpdateAuthentication(token)); + this.setState({ busy: false }); + + if (token) { + this.props.history.replace("/"); + } + + } +} + +export const Login = withStyles(styles)(withRouter(connect()(LoginComponent))); +export default Login; \ No newline at end of file -- cgit 1.2.3-korg