diff options
author | Herbert Eiselt <herbert.eiselt@highstreet-technologies.com> | 2019-02-11 14:54:12 +0100 |
---|---|---|
committer | Herbert Eiselt <herbert.eiselt@highstreet-technologies.com> | 2019-02-11 14:54:53 +0100 |
commit | 3d202a04b99f0e61b6ccf8b7a5610e1a15ca58e7 (patch) | |
tree | ab756cfa8de5eced886d3947423d198be8c0ce62 /sdnr/wt/odlux/apps/connectApp/src | |
parent | 12a8c669f52c0e84d580c078cee849b25133b585 (diff) |
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 <herbert.eiselt@highstreet-technologies.com>
Diffstat (limited to 'sdnr/wt/odlux/apps/connectApp/src')
19 files changed, 1370 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/apps/connectApp/src/actions/mountedNetworkElementsActions.ts b/sdnr/wt/odlux/apps/connectApp/src/actions/mountedNetworkElementsActions.ts new file mode 100644 index 000000000..e342f6314 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/actions/mountedNetworkElementsActions.ts @@ -0,0 +1,90 @@ +import { Action } from '../../../../framework/src/flux/action'; +import { Dispatch } from '../../../../framework/src/flux/store'; + +import { MountedNetworkElementType } from '../models/mountedNetworkElements'; +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; + +import { connectService } from '../services/connectService'; +import { AddSnackbarNotification } from '../../../../framework/src/actions/snackbarActions'; + +/** Represents the base action. */ +export class BaseAction extends Action { } + +/** Represents an action causing the store to load all mounted network elements. */ +export class LoadAllMountedNetworkElementsAction extends BaseAction { } + +/** Represents an action causing the store to update all mounted network elements. */ +export class AllMountedNetworkElementsLoadedAction extends BaseAction { + constructor(public mountedNetworkElements: MountedNetworkElementType[] | null, public error?: string) { + super(); + } +} + +/** Represents an action causing the store to update all mounted network elements. */ +export class AddMountedNetworkElement extends BaseAction { + constructor(public mountedNetworkElement: MountedNetworkElementType | null, public error?: string) { + super(); + } +} + +export class RemoveMountedNetworkElement extends BaseAction { + constructor(public mountId: string) { + super(); + } +} + +export class UpdateConnectionStateMountedNetworkElement extends BaseAction { + constructor(public mountId: string, connectionState: string) { + super(); + } +} + + +export class UpdateRequiredMountedNetworkElement extends BaseAction { + constructor(public mountId: string, public required: boolean) { + super(); + } +} + +/** + * An actioncrator for a async thunk action to add an allready mounted element to the state of this app. + * Note: Use this action to add created object notified by the websocket. +*/ +export const addMountedNetworkElementAsyncActionCreator = (mountId: string) => async (dispatch: Dispatch) => { + connectService.getMountedNetworkElementByMountId(mountId).then(mountedNetworkElement => { + mountedNetworkElement && dispatch(new AddMountedNetworkElement(mountedNetworkElement)); + }).catch(error => { + dispatch(new AddMountedNetworkElement(null, error)); + }); +}; + +/** Represents an async thunk action to load all mounted network elements. */ +export const loadAllMountedNetworkElementsAsync = (dispatch: Dispatch) => { + dispatch(new LoadAllMountedNetworkElementsAction()); + connectService.getMountedNetworkElementsList().then(mountedNetworkElements => { + mountedNetworkElements && dispatch(new AllMountedNetworkElementsLoadedAction(mountedNetworkElements)); + }).catch(error => { + dispatch(new AllMountedNetworkElementsLoadedAction(null, error)); + }); +}; + +/** Represents an async thunk action to load all mounted network elements. */ +export const mountNetworkElementActionCreatorAsync = (networkElement: RequiredNetworkElementType) => (dispatch: Dispatch) => { + connectService.mountNetworkElement(networkElement).then((success) => { + success && dispatch(new AddSnackbarNotification({ message: `Requesting mount [${ networkElement.mountId }]`, options: { variant: 'info' } })) + || dispatch(new AddSnackbarNotification({ message: `Failed to mount [${ networkElement.mountId }]`, options: { variant: 'warning' } })); + }).catch(error => { + dispatch(new AddMountedNetworkElement(null, error)); + }); +}; + +export const unmountNetworkElementActionCreatorAsync = (mountId: string) => (dispatch: Dispatch) => { + connectService.unmountNetworkElement(mountId).then((success) => { + success && dispatch(new AddSnackbarNotification({ message: `Requesting unmount [${ mountId }]`, options: { variant: 'info' } })) + || dispatch(new AddSnackbarNotification({ message: `Failed to unmount [${ mountId }]`, options: { variant: 'warning' } })); + }).catch(error => { + dispatch(new AddMountedNetworkElement(null, error)); + }); +}; + + diff --git a/sdnr/wt/odlux/apps/connectApp/src/actions/requiredNetworkElementsActions.ts b/sdnr/wt/odlux/apps/connectApp/src/actions/requiredNetworkElementsActions.ts new file mode 100644 index 000000000..979321957 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/actions/requiredNetworkElementsActions.ts @@ -0,0 +1,38 @@ +import { Action } from '../../../../framework/src/flux/action'; +import { Dispatch } from '../../../../framework/src/flux/store'; +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; +import { requiredNetworkElementsReloadAction } from '../handlers/requiredNetworkElementsHandler'; +import { UpdateRequiredMountedNetworkElement } from '../actions/mountedNetworkElementsActions'; + +import { AddSnackbarNotification } from '../../../../framework/src/actions/snackbarActions'; + +import { connectService } from '../services/connectService'; + +/** Represents the base action. */ +export class BaseAction extends Action { } + + +/** Represents an async thunk action that will add an element to the required network elements. */ +export const addToRequiredNetworkElementsAsyncActionCreator = (element: RequiredNetworkElementType) => (dispatch: Dispatch) => { + connectService.insertRequiredNetworkElement(element).then(_ => { + window.setTimeout(() => { + dispatch(requiredNetworkElementsReloadAction); + dispatch(new UpdateRequiredMountedNetworkElement(element.mountId, true)); + dispatch(new AddSnackbarNotification({ message: `Successfully added [${ element.mountId }]`, options: { variant: 'success' } })); + }, 900); + }); +}; + +/** Represents an async thunk action that will delete an element from the required network elements. */ +export const removeFromRequiredNetworkElementsAsyncActionCreator = (element: RequiredNetworkElementType) => (dispatch: Dispatch) => { + connectService.deleteRequiredNetworkElement(element).then(_ => { + window.setTimeout(() => { + dispatch(requiredNetworkElementsReloadAction); + dispatch(new UpdateRequiredMountedNetworkElement(element.mountId, false)); + dispatch(new AddSnackbarNotification({ message: `Successfully removed [${ element.mountId }]`, options: { variant: 'success' } })); + }, 900); + }); +}; + + + diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx new file mode 100644 index 000000000..b4b08fcf3 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { MaterialTable, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; + +import { createConnectionStatusLogActions, createConnectionStatusLogProperties } from '../handlers/connectionStatusLogHandler'; +import { ConnectionStatusLogType } from '../models/connectionStatusLog'; + +const mapProps = (state: IApplicationStoreState) => ({ + connectionStatusLogProperties: createConnectionStatusLogProperties(state), +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + connectionStatusLogActions: createConnectionStatusLogActions(dispatcher.dispatch), +}); + +const ConnectionStatusTable = MaterialTable as MaterialTableCtorType<ConnectionStatusLogType>; + +type ConnectionStatusLogComponentProps = Connect<typeof mapProps, typeof mapDispatch>; + +class ConnectionStatusLogComponent extends React.Component<ConnectionStatusLogComponentProps> { + render(): JSX.Element { + return ( + <ConnectionStatusTable columns={ [ + { property: "timeStamp", title: "Time", type: ColumnType.text }, + { property: "objectId", title: "Name", type: ColumnType.text }, + { property: "elementStatus", title: "Connection status", type: ColumnType.text, disableFilter: true, disableSorting: true }, + ] } idProperty="_id" { ...this.props.connectionStatusLogActions } {...this.props.connectionStatusLogProperties } > + </ConnectionStatusTable> + ); + }; + +} + +export const ConnectionStatusLog = connect(mapProps, mapDispatch)(ConnectionStatusLogComponent); +export default ConnectionStatusLog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/editNetworkElementDialog.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/editNetworkElementDialog.tsx new file mode 100644 index 000000000..ee876e854 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/components/editNetworkElementDialog.tsx @@ -0,0 +1,228 @@ +import * as React from 'react'; + +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { + addToRequiredNetworkElementsAsyncActionCreator, + removeFromRequiredNetworkElementsAsyncActionCreator +} from '../actions/requiredNetworkElementsActions'; + +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; +import { unmountNetworkElementActionCreatorAsync, mountNetworkElementActionCreatorAsync } from '../actions/mountedNetworkElementsActions'; +export enum EditNetworkElementDialogMode { + None = "none", + UnknownNetworkElementToRequiredNetworkElements = "unknownNetworkElementToRequiredNetworkElements", + RequiredNetworkElementToUnknownNetworkElements = "requiredNetworkElementToUnknownNetworkElements", + MountNetworkElementToRequiredNetworkElements = "mountNetworkElementToRequiredNetworkElements", + MountNetworkElementToUnknonwNetworkElements = "mountNetworkElementToRequiredUnknownElements", + MountNetworkElement = "mountNetworkElement", + UnmountNetworkElement = "unmountNetworkElement", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + addToRequiredNetworkElements: (element: RequiredNetworkElementType) => { + dispatcher.dispatch(addToRequiredNetworkElementsAsyncActionCreator(element)); + }, + removeFromRequiredNetworkElements: (element: RequiredNetworkElementType) => { + dispatcher.dispatch(removeFromRequiredNetworkElementsAsyncActionCreator(element)); + }, + mountNetworkElement: (element: RequiredNetworkElementType) => { + dispatcher.dispatch(mountNetworkElementActionCreatorAsync(element)); + }, + mountAndRquireNetworkElement: (element: RequiredNetworkElementType) => { + dispatcher.dispatch(addToRequiredNetworkElementsAsyncActionCreator(element)); + dispatcher.dispatch(mountNetworkElementActionCreatorAsync(element)); + }, + unmountNetworkElement: (element: RequiredNetworkElementType) => { + dispatcher.dispatch(unmountNetworkElementActionCreatorAsync(element && element.mountId)); + } +} +); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [EditNetworkElementDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [EditNetworkElementDialogMode.UnknownNetworkElementToRequiredNetworkElements] : { + dialogTitle: "Add to required network elements" , + dialogDescription: "Create a new NetworkElement in planning database as clone of existing real NetworkElement." , + applyButtonText: "Add to required network elements" , + cancelButtonText: "Cancel", + enableMountIdEditor: false, + enableUsernameEditor: true, + enableExtendedEditor: false, + }, + [EditNetworkElementDialogMode.RequiredNetworkElementToUnknownNetworkElements]: { + dialogTitle: "Remove from required network elements", + dialogDescription: "Do you really want to remove the required element:", + applyButtonText: "Remove network element", + cancelButtonText: "Cancel", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [EditNetworkElementDialogMode.MountNetworkElementToUnknonwNetworkElements]: { + dialogTitle: "Mount to unknown network elements", + dialogDescription: "Mount this network element:", + applyButtonText: "Mount network element", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + }, + [EditNetworkElementDialogMode.MountNetworkElementToRequiredNetworkElements]: { + dialogTitle: "Mount to required network elements", + dialogDescription: "Mount this network element:", + applyButtonText: "Mount network element", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + }, + [EditNetworkElementDialogMode.MountNetworkElement]: { + dialogTitle: "Mount network element", + dialogDescription: "mount this network element:", + applyButtonText: "mount network element", + cancelButtonText: "Cancel", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [EditNetworkElementDialogMode.UnmountNetworkElement]: { + dialogTitle: "Unmount network element", + dialogDescription: "unmount this network element:", + applyButtonText: "Unmount network element", + cancelButtonText: "Cancel", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, +} + +type EditNetworkElementDialogComponentProps = Connect<undefined,typeof mapDispatch> & { + mode: EditNetworkElementDialogMode; + initialNetworkElement: RequiredNetworkElementType; + onClose: () => void; +}; + +type EditNetworkElementDialogComponentState = RequiredNetworkElementType & { + required: boolean; +}; + +class EditNetworkElementDialogComponent extends React.Component<EditNetworkElementDialogComponentProps, EditNetworkElementDialogComponentState> { + constructor(props: EditNetworkElementDialogComponentProps) { + super(props); + + this.state = { + mountId: this.props.initialNetworkElement.mountId, + host: this.props.initialNetworkElement.host, + port: this.props.initialNetworkElement.port, + password: this.props.initialNetworkElement.password, + username: this.props.initialNetworkElement.username, + required: false + }; + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={ this.props.mode !== EditNetworkElementDialogMode.None }> + <DialogTitle id="form-dialog-title">{ setting.dialogTitle }</DialogTitle> + <DialogContent> + <DialogContentText> + { setting.dialogDescription } + </DialogContentText> + <TextField disabled={ !setting.enableMountIdEditor } spellCheck={false} autoFocus margin="dense" id="name" label="Name" type="text" fullWidth value={ this.state.mountId } onChange={(event)=>{ this.setState({mountId: event.target.value}); } } /> + <TextField disabled={ !setting.enableMountIdEditor } spellCheck={false} margin="dense" id="ipaddress" label="IP address" type="text" fullWidth value={ this.state.host } onChange={(event)=>{ this.setState({host: event.target.value}); } }/> + <TextField disabled={ !setting.enableMountIdEditor } spellCheck={false} margin="dense" id="netconfport" label="NetConf port" type="number" fullWidth value={ this.state.port.toString() } onChange={(event)=>{ this.setState({port: +event.target.value}); } }/> + { setting.enableUsernameEditor && <TextField disabled={ !setting.enableUsernameEditor } spellCheck={ false } margin="dense" id="username" label="Username" type="text" fullWidth value={ this.state.username } onChange={ (event) => { this.setState({ username: event.target.value }); } } /> || null } + { setting.enableUsernameEditor && <TextField disabled={ !setting.enableUsernameEditor } spellCheck={ false } margin="dense" id="password" label="Password" type="password" fullWidth value={ this.state.password } onChange={ (event) => { this.setState({ password: event.target.value }); } } /> || null } + </DialogContent> + <DialogActions> + <Button onClick={ (event) => { + this.onApply({ + mountId: this.state.mountId, + host: this.state.host, + port: this.state.port, + username: this.state.username, + password: this.state.password + }); + event.preventDefault(); + event.stopPropagation(); + } } > { setting.applyButtonText } </Button> + <Button onClick={ (event) => { + this.onCancel(); + event.preventDefault(); + event.stopPropagation(); + } } color="secondary"> { setting.cancelButtonText } </Button> + </DialogActions> + </Dialog> + ) + } + + private onApply = (element: RequiredNetworkElementType) => { + this.props.onClose && this.props.onClose(); + switch (this.props.mode) { + case EditNetworkElementDialogMode.UnknownNetworkElementToRequiredNetworkElements: + element && this.props.addToRequiredNetworkElements(element); + break; + case EditNetworkElementDialogMode.RequiredNetworkElementToUnknownNetworkElements: + element && this.props.removeFromRequiredNetworkElements(element); + break; + case EditNetworkElementDialogMode.MountNetworkElementToUnknonwNetworkElements: + element && this.props.mountNetworkElement(element); + break; + case EditNetworkElementDialogMode.MountNetworkElementToRequiredNetworkElements: + element && this.props.mountAndRquireNetworkElement(element); + break; + case EditNetworkElementDialogMode.MountNetworkElement: + element && this.props.mountNetworkElement(element); + break; + case EditNetworkElementDialogMode.UnmountNetworkElement: + element && this.props.unmountNetworkElement(element); + break; + } + }; + + private onCancel = () => { + this.props.onClose && this.props.onClose(); + } + + static getDerivedStateFromProps(props: EditNetworkElementDialogComponentProps, state: EditNetworkElementDialogComponentState & { _initialNetworkElement: RequiredNetworkElementType }): EditNetworkElementDialogComponentState & { _initialNetworkElement: RequiredNetworkElementType } { + if (props.initialNetworkElement !== state._initialNetworkElement) { + state = { + ...state, + ...props.initialNetworkElement, + _initialNetworkElement: props.initialNetworkElement, + }; + } + return state; + } +} + +export const EditNetworkElementDialog = connect(undefined, mapDispatch)(EditNetworkElementDialogComponent); +export default EditNetworkElementDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/requiredNetworkElements.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/requiredNetworkElements.tsx new file mode 100644 index 000000000..13f5fec20 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/components/requiredNetworkElements.tsx @@ -0,0 +1,194 @@ +import * as React from 'react'; +import { Theme, createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; + +import AddIcon from '@material-ui/icons/Add'; +import LinkIcon from '@material-ui/icons/Link'; +import LinkOffIcon from '@material-ui/icons/LinkOff'; +import RemoveIcon from '@material-ui/icons/RemoveCircleOutline'; + +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; + +import { MaterialTable, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { connect, Connect, IDispatcher } from '../../../../framework/src/flux/connect'; +import { NavigateToApplication } from '../../../../framework/src/actions/navigationActions'; + + +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; +import { createRequiredNetworkElementsActions, createRequiredNetworkElementsProperties } from '../handlers/requiredNetworkElementsHandler'; + +import EditNetworkElementDialog, { EditNetworkElementDialogMode } from './editNetworkElementDialog'; +import { Tooltip } from '@material-ui/core'; +import { NetworkElementBaseType } from 'models/networkElementBase'; + +const styles = (theme: Theme) => createStyles({ + connectionStatusConnected: { + color: 'darkgreen', + }, + connectionStatusConnecting: { + color: theme.palette.primary.main, + }, + connectionStatusDisconnected: { + color: 'red', + }, + button: { + margin: 0, + padding: "6px 6px", + minWidth: 'unset' + }, + spacer: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + display: "inline" + } +}); + +const mapProps = (state: IApplicationStoreState) => ({ + requiredNetworkElementsProperties: createRequiredNetworkElementsProperties(state), + mountedNetworkElements: state.connectApp.mountedNetworkElements +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + requiredNetworkElementsActions: createRequiredNetworkElementsActions(dispatcher.dispatch), + navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path)), +}); + +type RequiredNetworkElementsListComponentProps = WithStyles<typeof styles> & Connect<typeof mapProps, typeof mapDispatch>; +type RequiredNetworkElementsListComponentState = { + networkElementToEdit: RequiredNetworkElementType, + networkElementEditorMode: EditNetworkElementDialogMode +} + +const emptyRequireNetworkElement = { mountId: '', host: '', port: 0 }; + +const RequiredNetworkElementTable = MaterialTable as MaterialTableCtorType<RequiredNetworkElementType>; + +export class RequiredNetworkElementsListComponent extends React.Component<RequiredNetworkElementsListComponentProps, RequiredNetworkElementsListComponentState> { + + constructor(props: RequiredNetworkElementsListComponentProps) { + super(props); + + this.state = { + networkElementToEdit: emptyRequireNetworkElement, + networkElementEditorMode: EditNetworkElementDialogMode.None + }; + } + + // private navigationCreator + + render(): JSX.Element { + const { classes } = this.props; + const { networkElementToEdit } = this.state; + const addRequireNetworkElementAction = { + icon: AddIcon, tooltip: 'Add', onClick: () => { + this.setState({ + networkElementEditorMode: EditNetworkElementDialogMode.MountNetworkElementToRequiredNetworkElements, + networkElementToEdit: emptyRequireNetworkElement, + }); + } + }; + return ( + <> + <RequiredNetworkElementTable customActionButtons={ [addRequireNetworkElementAction] } columns={ [ + { property: "mountId", title: "Name", type: ColumnType.text }, + { + property: "connectionStatus", title: "Connection Status", type: ColumnType.custom, disableFilter: true, disableSorting: true, customControl: ({ rowData }) => { + const unknownNetworkElement = this.props.mountedNetworkElements.elements.find(el => el.mountId === rowData.mountId); + const connectionStatus = unknownNetworkElement && unknownNetworkElement.connectionStatus || 'disconnected'; + const cssClasses = connectionStatus === "connected" + ? classes.connectionStatusConnected + : connectionStatus === "disconnected" + ? classes.connectionStatusDisconnected + : classes.connectionStatusConnecting + return <div className={ cssClasses } >{ connectionStatus } </div> + + } + }, + { property: "host", title: "Host", type: ColumnType.text }, + { property: "port", title: "Port", type: ColumnType.text }, + // { property: "username", title: "Username", type: ColumnType.text }, + // { property: "password", title: "Password", type: ColumnType.text }, + { + property: "actions", title: "Actions", type: ColumnType.custom, customControl: ({ rowData }) => { + const unknownNetworkElement = this.props.mountedNetworkElements.elements.find(el => el.mountId === rowData.mountId); + const connectionStatus = unknownNetworkElement && unknownNetworkElement.connectionStatus || 'disconnected'; + return ( + <> + <div className={ classes.spacer }> + <Tooltip title={ "Mount" } ><IconButton className={ classes.button } onClick={ event => this.onOpenMountdNetworkElementsDialog(event, rowData) }><LinkIcon /></IconButton></Tooltip> + <Tooltip title={ "Unmount" } ><IconButton className={ classes.button } onClick={ event => this.onOpenUnmountdNetworkElementsDialog(event, rowData) }><LinkOffIcon /></IconButton></Tooltip> + <Tooltip title={ "Remove" } ><IconButton className={ classes.button } onClick={ event => this.onOpenRemoveRequiredNetworkElementDialog(event, rowData) } ><RemoveIcon /></IconButton></Tooltip> + </div> + <div className={ classes.spacer }> + <Tooltip title={ "Info" } ><Button className={ classes.button } >I</Button></Tooltip> + </div> + <div className={ classes.spacer }> + <Tooltip title={ "Fault" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("faultApp", rowData) } >F</Button></Tooltip> + <Tooltip title={ "Configure" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("configureApp", rowData)} >C</Button></Tooltip> + <Tooltip title={ "Accounting " } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("accountingApp", rowData) }>A</Button></Tooltip> + <Tooltip title={ "Performance" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("performanceApp", rowData) }>P</Button></Tooltip> + <Tooltip title={ "Security" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("securityApp", rowData) }>S</Button></Tooltip> + </div> + </> + ) + } + }, + ] } idProperty="mountId" { ...this.props.requiredNetworkElementsActions } { ...this.props.requiredNetworkElementsProperties } asynchronus > + </RequiredNetworkElementTable> + <EditNetworkElementDialog + initialNetworkElement={ networkElementToEdit } + mode={ this.state.networkElementEditorMode } + onClose={ this.onCloseEditNetworkElementDialog } + /> + </> + ); + }; + + public componentDidMount() { + this.props.requiredNetworkElementsActions.onRefresh(); + } + + private onOpenRemoveRequiredNetworkElementDialog = (event: React.MouseEvent<HTMLElement>, element: RequiredNetworkElementType) => { + this.setState({ + networkElementToEdit: element, + networkElementEditorMode: EditNetworkElementDialogMode.RequiredNetworkElementToUnknownNetworkElements + }); + event.preventDefault(); + event.stopPropagation(); + } + + private onOpenUnmountdNetworkElementsDialog = (event: React.MouseEvent<HTMLElement>, element: RequiredNetworkElementType) => { + this.setState({ + networkElementToEdit: element, + networkElementEditorMode: EditNetworkElementDialogMode.UnmountNetworkElement + }); + event.preventDefault(); + event.stopPropagation(); + } + + private onOpenMountdNetworkElementsDialog = (event: React.MouseEvent<HTMLElement>, element: RequiredNetworkElementType) => { + this.setState({ + networkElementToEdit: element, + networkElementEditorMode: EditNetworkElementDialogMode.MountNetworkElement + }); + event.preventDefault(); + event.stopPropagation(); + } + + private onCloseEditNetworkElementDialog = () => { + this.setState({ + networkElementEditorMode: EditNetworkElementDialogMode.None, + networkElementToEdit: emptyRequireNetworkElement, + }); + } + + private navigateToApplicationHandlerCreator = (applicationName: string, element: NetworkElementBaseType) => (event: React.MouseEvent<HTMLElement>) => { + this.props.navigateToApplication(applicationName, element.mountId); + event.preventDefault(); + event.stopPropagation(); + } +} + +export const RequiredNetworkElementsList = withStyles(styles)(connect(mapProps, mapDispatch)(RequiredNetworkElementsListComponent)); +export default RequiredNetworkElementsList;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/unknownNetworkElements.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/unknownNetworkElements.tsx new file mode 100644 index 000000000..432103128 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/components/unknownNetworkElements.tsx @@ -0,0 +1,196 @@ +import * as React from 'react'; +import { Theme, createStyles, WithStyles, withStyles, Tooltip } from '@material-ui/core'; + +import AddIcon from '@material-ui/icons/Add'; +import LinkOffIcon from '@material-ui/icons/LinkOff'; +import AddCircleIcon from '@material-ui/icons/AddCircleOutline'; + +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; + +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { MaterialTable, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; +import { Connect, connect, IDispatcher } from '../../../../framework/src/flux/connect'; +import { NavigateToApplication } from '../../../../framework/src/actions/navigationActions'; + +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; +import { IMountedNetworkElementsState } from '../handlers/mountedNetworkElementsHandler'; +import EditNetworkElementDialog, { EditNetworkElementDialogMode } from './editNetworkElementDialog'; +import { NetworkElementBaseType } from 'models/networkElementBase'; + + +const styles = (theme: Theme) => createStyles({ + button: { + margin: 0, + padding: "6px 6px", + minWidth: 'unset' + }, + spacer: { + marginLeft: theme.spacing.unit, + marginRight: theme.spacing.unit, + display: "inline" + } +}); + +const mapProps = ({ connectApp: state }: IApplicationStoreState) => ({ + mountedNetworkElements: state.mountedNetworkElements +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path)), +}); +type UnknownNetworkElementDisplayType = NetworkElementBaseType & { + connectionStatus: string, + coreModelRev: string, + airInterfaceRev: string +} + +type UnknownNetworkElementsListProps = WithStyles<typeof styles> & Connect<typeof mapProps, typeof mapDispatch> & {} + +type UnknownNetworkElementsListState = { + + unknownNetworkElements: UnknownNetworkElementDisplayType[]; + + networkElementToEdit: RequiredNetworkElementType; + networkElementEditorMode: EditNetworkElementDialogMode; +} + + +const emptyRequireNetworkElement = { mountId: '', host: '', port: 0 }; +const UnknownNetworkElementTable = MaterialTable as MaterialTableCtorType<UnknownNetworkElementDisplayType>; +export class UnknownNetworkElementsListComponent extends React.Component<UnknownNetworkElementsListProps, UnknownNetworkElementsListState> { + + constructor(props: UnknownNetworkElementsListProps) { + super(props); + + this.state = { + unknownNetworkElements: [], + networkElementToEdit: emptyRequireNetworkElement, + networkElementEditorMode: EditNetworkElementDialogMode.None, + }; + } + + static getDerivedStateFromProps(props: UnknownNetworkElementsListProps, state: UnknownNetworkElementsListState & { _mountedNetworkElements: IMountedNetworkElementsState }) { + if (props.mountedNetworkElements != state._mountedNetworkElements) { + state.unknownNetworkElements = props.mountedNetworkElements.elements.filter(element => !element.required).map(element => { + + // handle onfCoreModelRevision + const onfCoreModelRevision = element.capabilities.find((cap) => { + return cap.module === 'core-model' || cap.module === 'CoreModel-CoreNetworkModule-ObjectClasses' ; + }); + const onfAirInterfaceRevision = element.capabilities.find((cap) => { + return cap.module === 'microwave-model' || cap.module === 'MicrowaveModel-ObjectClasses-AirInterface' ; + }); + return { + mountId: element.mountId, + host: element.host, + port: element.port, + connectionStatus: element.connectionStatus, + coreModelRev: onfCoreModelRevision && onfCoreModelRevision.revision || 'unknown', + airInterfaceRev: onfAirInterfaceRevision && onfAirInterfaceRevision.revision || 'unknown' + } + } + ); + } + return state; + } + + render(): JSX.Element { + const { classes } = this.props; + const { networkElementToEdit, networkElementEditorMode, unknownNetworkElements } = this.state; + const addRequireNetworkElementAction = { + icon: AddIcon, tooltip: 'Add', onClick: () => { + this.setState({ + networkElementEditorMode: EditNetworkElementDialogMode.MountNetworkElementToUnknonwNetworkElements, + networkElementToEdit: emptyRequireNetworkElement, + }); + } + }; + return ( + <> + <UnknownNetworkElementTable customActionButtons={ [addRequireNetworkElementAction] } asynchronus rows={ unknownNetworkElements } columns={ [ + { property: "mountId", title: "Name", type: ColumnType.text }, + { property: "connectionStatus", title: "Connection Status", type: ColumnType.text }, + { property: "host", title: "Host", type: ColumnType.text }, + { property: "port", title: "Port", type: ColumnType.text }, + { property: "coreModelRev", title: "Core Model", type: ColumnType.text }, + { property: "airInterfaceRev", title: "Air interface", type: ColumnType.text }, + { + property: "actions", title: "Actions", type: ColumnType.custom, customControl: ({ rowData }) => ( + <> + <div className={ classes.spacer }> + <Tooltip title={ "Unmount" } ><IconButton className={ classes.button } onClick={ event => this.onOpenUnmountdNetworkElementsDialog(event, rowData) } ><LinkOffIcon /></IconButton></Tooltip> + <Tooltip title={ "Add to required" } ><IconButton className={ classes.button } onClick={ event => this.onOpenAddToRequiredNetworkElementsDialog(event, rowData) } ><AddCircleIcon /></IconButton></Tooltip> + </div> + <div className={ classes.spacer }> + <Tooltip title={ "Info" } ><Button className={ classes.button } >I</Button></Tooltip> + </div> + <div className={ classes.spacer }> + <div className={ classes.spacer }> + <Tooltip title={ "Fault" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("faultApp", rowData) } >F</Button></Tooltip> + <Tooltip title={ "Configure" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("configureApp", rowData) } >C</Button></Tooltip> + <Tooltip title={ "Accounting " } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("accountingApp", rowData) }>A</Button></Tooltip> + <Tooltip title={ "Performance" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("performanceApp", rowData) }>P</Button></Tooltip> + <Tooltip title={ "Security" } ><Button className={ classes.button } onClick={ this.navigateToApplicationHandlerCreator("securityApp", rowData) }>S</Button></Tooltip> + </div> + </div> + </> + ) + }, + ] } idProperty="mountId" > + </UnknownNetworkElementTable> + + <EditNetworkElementDialog + mode={ networkElementEditorMode } + initialNetworkElement={ networkElementToEdit } + onClose={ this.onCloseEditNetworkElementDialog } + /> + </> + ); + }; + + private onOpenAddToRequiredNetworkElementsDialog = (event: React.MouseEvent<HTMLElement>, element: UnknownNetworkElementDisplayType) => { + this.setState({ + networkElementToEdit: { + mountId: element.mountId, + host: element.host, + port: element.port, + username: 'admin', + password: 'admin', + }, + networkElementEditorMode: EditNetworkElementDialogMode.UnknownNetworkElementToRequiredNetworkElements + }); + event.preventDefault(); + event.stopPropagation(); + } + + private onOpenUnmountdNetworkElementsDialog = (event: React.MouseEvent<HTMLElement>, element: UnknownNetworkElementDisplayType) => { + this.setState({ + networkElementToEdit: { + mountId: element.mountId, + host: element.host, + port: element.port + }, + networkElementEditorMode: EditNetworkElementDialogMode.UnmountNetworkElement + }); + event.preventDefault(); + event.stopPropagation(); + } + + private onCloseEditNetworkElementDialog = () => { + this.setState({ + networkElementEditorMode: EditNetworkElementDialogMode.None, + networkElementToEdit: emptyRequireNetworkElement, + }); + } + + private navigateToApplicationHandlerCreator = (applicationName: string, element: NetworkElementBaseType) => (event: React.MouseEvent<HTMLElement>) => { + this.props.navigateToApplication(applicationName, element.mountId); + event.preventDefault(); + event.stopPropagation(); + } + +} + +export const UnknownNetworkElementsList = withStyles(styles)(connect(mapProps, mapDispatch)(UnknownNetworkElementsListComponent)); +export default UnknownNetworkElementsList; diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.tsx b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.tsx new file mode 100644 index 000000000..26d02c4e9 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.tsx @@ -0,0 +1,26 @@ +import { combineActionHandler } from '../../../../framework/src/flux/middleware'; +import { IRequiredNetworkElementsState, requiredNetworkElementsActionHandler } from './requiredNetworkElementsHandler'; +import { IMountedNetworkElementsState, mountedNetworkElementsActionHandler } from './mountedNetworkElementsHandler'; +import { IConnectionStatusLogState, connectionStatusLogActionHandler } from './connectionStatusLogHandler'; + +export interface IConnectAppStoreState { + + requiredNetworkElements: IRequiredNetworkElementsState; + mountedNetworkElements: IMountedNetworkElementsState; + connectionStatusLog: IConnectionStatusLogState; +} + +declare module '../../../../framework/src/store/applicationStore' { + interface IApplicationStoreState { + connectApp: IConnectAppStoreState + } +} + +const actionHandlers = { + requiredNetworkElements: requiredNetworkElementsActionHandler, + mountedNetworkElements: mountedNetworkElementsActionHandler, + connectionStatusLog: connectionStatusLogActionHandler +}; + +export const connectAppRootHandler = combineActionHandler <IConnectAppStoreState>(actionHandlers); +export default connectAppRootHandler; diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/connectionStatusLogHandler.tsx b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectionStatusLogHandler.tsx new file mode 100644 index 000000000..140020eaa --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectionStatusLogHandler.tsx @@ -0,0 +1,30 @@ +import { createExternal,IExternalTableState } from '../../../../framework/src/components/material-table/utilities'; +import { createSearchDataHandler } from '../../../../framework/src/utilities/elasticSearch'; + +import { ConnectionStatusLogType } from '../models/connectionStatusLog'; +export interface IConnectionStatusLogState extends IExternalTableState<ConnectionStatusLogType> { } + +// create eleactic search material data fetch handler +const connectionStatusLogSearchHandler = createSearchDataHandler<{ event: ConnectionStatusLogType }, ConnectionStatusLogType>('sdnevents_v1/eventlog', null, + (event) => ({ + _id: event._id, + timeStamp: event._source.event.timeStamp, + objectId: event._source.event.objectId, + type: event._source.event.type, + elementStatus: event._source.event.type === 'ObjectCreationNotificationXml' + ? 'connected' + : event._source.event.type === 'ObjectDeletionNotificationXml' + ? 'disconnected' + : 'unknown' + }), + (name) => `event.${ name }`); + +export const { + actionHandler: connectionStatusLogActionHandler, + createActions: createConnectionStatusLogActions, + createProperties: createConnectionStatusLogProperties, + reloadAction: connectionStatusLogReloadAction, + + // set value action, to change a value +} = createExternal<ConnectionStatusLogType>(connectionStatusLogSearchHandler, appState => appState.connectApp.connectionStatusLog); + diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/mountedNetworkElementsHandler.tsx b/sdnr/wt/odlux/apps/connectApp/src/handlers/mountedNetworkElementsHandler.tsx new file mode 100644 index 000000000..70b4d8f2a --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/mountedNetworkElementsHandler.tsx @@ -0,0 +1,81 @@ +import { IActionHandler } from '../../../../framework/src/flux/action'; + +import { + AddMountedNetworkElement, + AllMountedNetworkElementsLoadedAction, + LoadAllMountedNetworkElementsAction, + RemoveMountedNetworkElement, + UpdateConnectionStateMountedNetworkElement, + UpdateRequiredMountedNetworkElement +} from '../actions/mountedNetworkElementsActions'; + +import { MountedNetworkElementType } from '../models/mountedNetworkElements'; + +export interface IMountedNetworkElementsState { + elements: MountedNetworkElementType[]; + busy: boolean; +} + +const mountedNetworkElementsStateInit: IMountedNetworkElementsState = { + elements: [], + busy: false +}; + +export const mountedNetworkElementsActionHandler: IActionHandler<IMountedNetworkElementsState> = (state = mountedNetworkElementsStateInit, action) => { + if (action instanceof LoadAllMountedNetworkElementsAction) { + + state = { + ...state, + busy: true + }; + + } else if (action instanceof AllMountedNetworkElementsLoadedAction) { + if (!action.error && action.mountedNetworkElements) { + state = { + ...state, + elements: action.mountedNetworkElements, + busy: false + }; + } else { + state = { + ...state, + busy: false + }; + } + } else if (action instanceof AddMountedNetworkElement) { + action.mountedNetworkElement && (state = { + ...state, + elements: [...state.elements, action.mountedNetworkElement], + }); + } else if (action instanceof RemoveMountedNetworkElement) { + state = { + ...state, + elements: state.elements.filter(e => e.mountId !== action.mountId), + }; + } else if (action instanceof UpdateConnectionStateMountedNetworkElement) { + const index = state.elements.findIndex(el => el.mountId === action.mountId); + if (index > -1) { + state = { + ...state, + elements: [ + ...state.elements.slice(0, index), + { ...state.elements[index], connectionStatus: action.mountId }, + ...state.elements.slice(index + 1) + ] + } + } + } else if (action instanceof UpdateRequiredMountedNetworkElement) { + const index = state.elements.findIndex(el => el.mountId === action.mountId); + if (index > -1 && (state.elements[index].required !== action.required)) { + state = { + ...state, + elements: [ + ...state.elements.slice(0, index), + { ...state.elements[index], required: action.required }, + ...state.elements.slice(index + 1) + ] + } + } + }; + return state; +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/requiredNetworkElementsHandler.tsx b/sdnr/wt/odlux/apps/connectApp/src/handlers/requiredNetworkElementsHandler.tsx new file mode 100644 index 000000000..b2d547717 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/requiredNetworkElementsHandler.tsx @@ -0,0 +1,18 @@ +import { createExternal,IExternalTableState } from '../../../../framework/src/components/material-table/utilities'; +import { createSearchDataHandler } from '../../../../framework/src/utilities/elasticSearch'; + +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; +export interface IRequiredNetworkElementsState extends IExternalTableState<RequiredNetworkElementType> { } + +// create eleactic search material data fetch handler +const requiredNetworkElementsSearchHandler = createSearchDataHandler<RequiredNetworkElementType>('mwtn/required-networkelement'); + +export const { + actionHandler: requiredNetworkElementsActionHandler, + createActions: createRequiredNetworkElementsActions, + createProperties: createRequiredNetworkElementsProperties, + reloadAction: requiredNetworkElementsReloadAction, + + // set value action, to change a value +} = createExternal<RequiredNetworkElementType>(requiredNetworkElementsSearchHandler, appState => appState.connectApp.requiredNetworkElements); + diff --git a/sdnr/wt/odlux/apps/connectApp/src/index.html b/sdnr/wt/odlux/apps/connectApp/src/index.html new file mode 100644 index 000000000..c28708a83 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/index.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <link rel="stylesheet" href="./vendor.css" > + <title>Document</title> +</head> + +<body> + <div id="app"></div> + <script type="text/javascript" src="./require.js"></script> + <script type="text/javascript" src="./config.js"></script> + <script> + // run the application + require(["app","connectApp", "faultApp"], function (app, connectApp, faultApp) { + connectApp.register(); + faultApp.register(); + app("./app.tsx") + }); + </script> +</body> + +</html>
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/connectionStatusLog.ts b/sdnr/wt/odlux/apps/connectApp/src/models/connectionStatusLog.ts new file mode 100644 index 000000000..d3aa20379 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/models/connectionStatusLog.ts @@ -0,0 +1,9 @@ + +export type ConnectionStatusLogType = { + _id: string; + elementStatus: string; + timeStamp: string; + objectId: string; + type: string; +} + diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/mountedNetworkElements.ts b/sdnr/wt/odlux/apps/connectApp/src/models/mountedNetworkElements.ts new file mode 100644 index 000000000..4ab7c8e20 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/models/mountedNetworkElements.ts @@ -0,0 +1,11 @@ +import { NetworkElementBaseType } from "./networkElementBase"; + +/** +* Represents data of an mounted network elements. +*/ +export type MountedNetworkElementType = NetworkElementBaseType & { + connectionStatus: string; + required: boolean; + capabilities: { module: string, revision: string }[]; +}; + diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/networkElementBase.ts b/sdnr/wt/odlux/apps/connectApp/src/models/networkElementBase.ts new file mode 100644 index 000000000..85390bef9 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/models/networkElementBase.ts @@ -0,0 +1,5 @@ +export type NetworkElementBaseType = { + mountId: string, + host: string, + port: number, +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/requiredNetworkElements.ts b/sdnr/wt/odlux/apps/connectApp/src/models/requiredNetworkElements.ts new file mode 100644 index 000000000..08d1f91ec --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/models/requiredNetworkElements.ts @@ -0,0 +1,10 @@ +import { NetworkElementBaseType } from "./networkElementBase"; + +/** +* Represents data of Required Network Elements. +*/ +export type RequiredNetworkElementType = NetworkElementBaseType & { + username?: string; + password?: string; +} + diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts b/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts new file mode 100644 index 000000000..5cf29c708 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts @@ -0,0 +1,36 @@ +export interface UnavailableCapability { + capability: string; + "failure-reason": string; +} + +export interface NetconfNodeTopologyUnavailableCapabilities { + "unavailable-capability": UnavailableCapability[]; +} + +export interface AvailableCapability { + "capability-origin": string; + capability: string; +} + +export interface NetconfNodeTopologyAvailableCapabilities { + "available-capability": AvailableCapability[]; +} + +export interface NetconfNodeTopologyClusteredConnectionStatus { + "netconf-master-node": string +} + +export interface TopologyNode { + "node-id": string; + "netconf-node-topology:clustered-connection-status": NetconfNodeTopologyClusteredConnectionStatus; + "netconf-node-topology:unavailable-capabilities": NetconfNodeTopologyUnavailableCapabilities; + "netconf-node-topology:available-capabilities": NetconfNodeTopologyAvailableCapabilities; + "netconf-node-topology:host": string; + "netconf-node-topology:connection-status": string; + "netconf-node-topology:port": number; +} + +export interface Topology { + "topology-id": string; + node: TopologyNode[]; +} diff --git a/sdnr/wt/odlux/apps/connectApp/src/plugin.tsx b/sdnr/wt/odlux/apps/connectApp/src/plugin.tsx new file mode 100644 index 000000000..4e61c326b --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/plugin.tsx @@ -0,0 +1,39 @@ + +import { faPlug } from '@fortawesome/free-solid-svg-icons'; + +import applicationManager from '../../../framework/src/services/applicationManager'; +import { subscribe, IFormatedMessage } from '../../../framework/src/services/notificationService'; + +import connectAppRootHandler from './handlers/connectAppRootHandler'; +import ConnectApplication from './views/connectView'; +import { RemoveMountedNetworkElement, addMountedNetworkElementAsyncActionCreator } from './actions/mountedNetworkElementsActions' ; +import { AddSnackbarNotification } from '../../../framework/src/actions/snackbarActions'; + +type ObjectNotification = { + counter: string; + nodeName: string; + objectId: string; + timeStamp: string; +} + +export function register() { + const applicationApi = applicationManager.registerApplication({ + name: "connectApp", + icon: faPlug, + rootComponent: ConnectApplication, + rootActionHandler: connectAppRootHandler, + menuEntry: "Connect App" + }); + + // subscribe to the websocket notifications + subscribe<ObjectNotification & IFormatedMessage>(["ObjectCreationNotification", "ObjectDeletionNotification"], (msg => { + const store = applicationApi && applicationApi.applicationStore; + if (msg && msg.notifType === "ObjectCreationNotification" && store) { + store.dispatch(addMountedNetworkElementAsyncActionCreator(msg.objectId)); + store.dispatch(new AddSnackbarNotification({ message: `Adding network element [${ msg.objectId }]`, options: { variant: 'info' } })); + } else if (msg && msg.notifType === "ObjectDeletionNotification" && store) { + store.dispatch(new AddSnackbarNotification({ message: `Removing network element [${ msg.objectId }]`, options: { variant: 'info' } })); + store.dispatch(new RemoveMountedNetworkElement(msg.objectId)); + } + })); +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts b/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts new file mode 100644 index 000000000..0adcd49c6 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts @@ -0,0 +1,204 @@ +import { RequiredNetworkElementType } from '../models/requiredNetworkElements'; +import { MountedNetworkElementType } from 'models/mountedNetworkElements'; +import { Topology, TopologyNode } from 'models/topologyNetconf'; + +import { requestRest } from '../../../../framework/src/services/restService'; +import { Result, HitEntry } from '../../../../framework/src/models/elasticSearch'; + + +/** + * Represents a web api accessor service for all Network Elements actions. + */ +class ConnectService { + /** + * Gets all known required network elements from the backend. + */ + public async getAllRequiredNetworkElements(): Promise<(RequiredNetworkElementType & { _id: string })[] | null> { + const path = 'database/mwtn/required-networkelement/_search'; + const query = { "query": { "match_all": {} } }; + + const result = await requestRest<Result<RequiredNetworkElementType>>(path, { method: "POST", body: JSON.stringify(query) }); + return result && result.hits && result.hits.hits && result.hits.hits.map(ne => ({ + _id: ne._id, + mountId: ne._source && ne._source.mountId, + host: ne._source && ne._source.host, + port: ne._source && ne._source.port, + username: ne._source && ne._source.username, + password: ne._source && ne._source.password, + })) || null; + } + + public async getRequiredNetworkElementByMountId(mountId:string): Promise<(RequiredNetworkElementType & { _id: string }) | null> { + const path = `database/mwtn/required-networkelement/${mountId}`; + + const result = await requestRest<HitEntry<RequiredNetworkElementType> & { found: boolean }>(path, { method: "GET" }); + return result && result.found && result._source && { + _id: result._id, + mountId: result._source.mountId, + host: result._source.host, + port: result._source.port, + username: result._source.username, + password: result._source.password, + } || null; + + } + + /** + * Inserts data into the required network elements table. + */ + public async insertRequiredNetworkElement(element: RequiredNetworkElementType): Promise<RequiredNetworkElementType | null> { + const path = `database/mwtn/required-networkelement/${ element.mountId }`; + const result = await requestRest<RequiredNetworkElementType>(path, { method: "POST", body: JSON.stringify(element) }); + return result || null; + } + + /** + * Deletes data from the Required Network Elements backend. + */ + public async deleteRequiredNetworkElement(element: RequiredNetworkElementType): Promise<RequiredNetworkElementType | null> { + const path = `database/mwtn/required-networkelement/${ element.mountId }`; + const result = await requestRest<RequiredNetworkElementType>(path, { method: "DELETE", body: JSON.stringify(element) }); + return result || null; + } + + + + private static mapTopologyNode = (mountPoint: TopologyNode, required: boolean ) => { + // handle onfCapabilities + let onfCapabilities: { module: string, revision: string }[] | undefined = undefined; + let onfCoreModelRevision: string[] | undefined = undefined; + let onfAirInterfaceRevision: string[] | undefined = undefined; + + const capId = 'netconf-node-topology:available-capabilities'; + if (mountPoint[capId] && mountPoint[capId]['available-capability']) { + onfCapabilities = mountPoint[capId]['available-capability'].filter((cap) => { + return cap.capability.includes('?revision='); + }).map((cap) => { + return { + module: cap.capability.split(')')[1], + revision: cap.capability.split('?revision=')[1].substring(0, 10) + }; + }).sort((a, b) => { + if (a.module < b.module) return -1; + if (a.module > b.module) return 1; + return 0; + }); + } + + // handle clustered-connection-status + const statusId = 'netconf-node-topology:clustered-connection-status'; + let client = 'localhost'; + + if (mountPoint[statusId] && mountPoint[statusId]['netconf-master-node']) { + let node = mountPoint[statusId]['netconf-master-node']; + node = node.substring(node.indexOf('@')); + client = node.substring(1, node.indexOf(':')); + } + const mountId = mountPoint["node-id"]; + return { + mountId: mountId, + host: mountPoint["netconf-node-topology:host"], + port: mountPoint["netconf-node-topology:port"], + connectionStatus: mountPoint['netconf-node-topology:connection-status'], + capabilities: onfCapabilities || [], + required: required, + client + } + } + + /** Get all mounted network elements and fills the property required according to the database contents. */ + public async getMountedNetworkElementsList(): Promise<MountedNetworkElementType[] | null> { + const path = 'restconf/operational/network-topology:network-topology/topology/topology-netconf'; + + const topologyRequestPomise = requestRest<{ topology: Topology[] | null }>(path, { method: "GET" }, true); + const requiredNetworkElementsPromise = this.getAllRequiredNetworkElements(); + + const [netconfResponse, requiredNetworkElements] = await Promise.all([topologyRequestPomise, requiredNetworkElementsPromise]); + + // process topologyNetconf (get all known network elements) + const topologyNetconf = netconfResponse && netconfResponse.topology && netconfResponse.topology.find(topology => topology["topology-id"] === 'topology-netconf'); + let mountPoints = topologyNetconf && topologyNetconf.node && topologyNetconf.node.filter( + mountPoint => mountPoint['node-id'] !== 'controller-config').map(mountedElement => { + const required = requiredNetworkElements && requiredNetworkElements.some( + requiredElement => requiredElement.mountId === mountedElement["node-id"]); + return ConnectService.mapTopologyNode(mountedElement, !!required); + }); + + return mountPoints || []; + } + + /** Get one mounted network element. */ + public async getMountedNetworkElementByMountId(mountId: string): Promise<MountedNetworkElementType | null> { + const path = 'restconf/operational/network-topology:network-topology/topology/topology-netconf/node/' + mountId; + const getMountedNetworkElementByMountIdPromise = requestRest<{ node: TopologyNode[] | null }>(path, { method: "GET" }, true); + const getRequiredNetworkElementByMountIdPromise = this.getRequiredNetworkElementByMountId(mountId); + + const [mountedNetworkElement, requiredNetworkElement] = await Promise.all([getMountedNetworkElementByMountIdPromise, getRequiredNetworkElementByMountIdPromise]); + return mountedNetworkElement && mountedNetworkElement.node && ConnectService.mapTopologyNode(mountedNetworkElement.node[0], requiredNetworkElement && requiredNetworkElement.mountId === mountedNetworkElement.node[0]["node-id"] || false) || null; + } + + /** Mounts an required network element. */ + public async mountNetworkElement(networkElement: RequiredNetworkElementType): Promise<boolean> { + const path = 'restconf/config/network-topology:network-topology/topology/topology-netconf/node/' + networkElement.mountId; + const mountXml = [ + '<node xmlns="urn:TBD:params:xml:ns:yang:network-topology">', + `<node-id>${ networkElement.mountId }</node-id>`, + `<host xmlns="urn:opendaylight:netconf-node-topology">${ networkElement.host }</host>`, + `<port xmlns="urn:opendaylight:netconf-node-topology">${ networkElement.port }</port>`, + `<username xmlns="urn:opendaylight:netconf-node-topology">${ networkElement.username }</username>`, + `<password xmlns="urn:opendaylight:netconf-node-topology">${ networkElement.password }</password>`, + ' <tcp-only xmlns="urn:opendaylight:netconf-node-topology">false</tcp-only>', + + ' <!-- non-mandatory fields with default values, you can safely remove these if you do not wish to override any of these values-->', + ' <reconnect-on-changed-schema xmlns="urn:opendaylight:netconf-node-topology">false</reconnect-on-changed-schema>', + ' <connection-timeout-millis xmlns="urn:opendaylight:netconf-node-topology">20000</connection-timeout-millis>', + ' <max-connection-attempts xmlns="urn:opendaylight:netconf-node-topology">100</max-connection-attempts>', + ' <between-attempts-timeout-millis xmlns="urn:opendaylight:netconf-node-topology">2000</between-attempts-timeout-millis>', + ' <sleep-factor xmlns="urn:opendaylight:netconf-node-topology">1.5</sleep-factor>', + + ' <!-- keepalive-delay set to 0 turns off keepalives-->', + ' <keepalive-delay xmlns="urn:opendaylight:netconf-node-topology">120</keepalive-delay>', + '</node>'].join(''); + + try { + const result = await requestRest<{}>(path, { + method: 'PUT', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + }, + body: mountXml + }, true); + // expect an empty answer + return result === null; + } catch { + return false; + } + }; + + /** Unmounts a network element by its id. */ + public async unmountNetworkElement(mountId: string): Promise<boolean> { + const path = 'restconf/config/network-topology:network-topology/topology/topology-netconf/node/' + mountId; + + try { + const result = await requestRest<{}>(path, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml', + 'Authorization': 'Basic YWRtaW46YWRtaW4=' + }, + }, true); + // expect an empty answer + return result === null; + + } catch { + return false; + } + }; + + +} +export const connectService = new ConnectService(); +export default connectService; diff --git a/sdnr/wt/odlux/apps/connectApp/src/views/connectView.tsx b/sdnr/wt/odlux/apps/connectApp/src/views/connectView.tsx new file mode 100644 index 000000000..b73eb39d7 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/views/connectView.tsx @@ -0,0 +1,93 @@ +import * as React from 'react'; + +import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { Panel } from '../../../../framework/src/components/material-ui'; + +import { requiredNetworkElementsReloadAction } from '../handlers/requiredNetworkElementsHandler'; +import { loadAllMountedNetworkElementsAsync } from '../actions/mountedNetworkElementsActions'; +import { connectionStatusLogReloadAction } from '../handlers/connectionStatusLogHandler'; + +import { RequiredNetworkElementsList } from '../components/requiredNetworkElements'; +import { ConnectionStatusLog } from '../components/connectionStatusLog'; +import { UnknownNetworkElementsList } from '../components/unknownNetworkElements'; + +const mapDispatcher = (dispatcher: IDispatcher) => ({ + onLoadUnknownNetworkElements: () => { + dispatcher.dispatch(loadAllMountedNetworkElementsAsync); + }, + onLoadRequiredNetworkElements: () => { + dispatcher.dispatch(requiredNetworkElementsReloadAction); + }, + onLoadConnectionStatusLog: () => { + dispatcher.dispatch(connectionStatusLogReloadAction); + } +}); + +type PanelId = null | "RequiredNetworkElements" | "UnknownNetworkElements" | "ConnectionStatusLog"; + +type ConnectApplicationComponentProps = Connect<undefined, typeof mapDispatcher> ; + +type ConnectApplicationComponentState = { + activePanel: PanelId; +}; + +class ConnectApplicationComponent extends React.Component<ConnectApplicationComponentProps, ConnectApplicationComponentState>{ + /** + * Initialises this instance + */ + constructor(props: ConnectApplicationComponentProps) { + super(props); + + this.state = { + activePanel: null + }; + } + private onTogglePanel = (panelId: PanelId) => { + const nextActivePanel = panelId === this.state.activePanel ? null : panelId; + this.setState({ + activePanel: nextActivePanel + }, () => { + switch (nextActivePanel) { + case 'RequiredNetworkElements': + this.props.onLoadRequiredNetworkElements(); + break; + case 'UnknownNetworkElements': + // todo: should we update the application state ? + break; + case 'ConnectionStatusLog': + this.props.onLoadConnectionStatusLog(); + break; + case null: + // do nothing if all panels are closed + break; + default: + console.warn("Unknown nextActivePanel [" + nextActivePanel + "] in connectView"); + break; + } + }); + }; + + render(): JSX.Element { + const { activePanel } = this.state; + + return ( + <> + <Panel activePanel={ activePanel } panelId={ 'RequiredNetworkElements' } onToggle={ this.onTogglePanel } title={ "Required Network Elements" }> + <RequiredNetworkElementsList /> + </Panel> + <Panel activePanel={ activePanel } panelId={ 'UnknownNetworkElements' } onToggle={ this.onTogglePanel } title={ "Unknown Network Elements" }> + <UnknownNetworkElementsList /> + </Panel> + <Panel activePanel={ activePanel } panelId={ 'ConnectionStatusLog' } onToggle={ this.onTogglePanel } title={ "Connection Status Log" }> + <ConnectionStatusLog /> + </Panel> + </> + ); + }; + public componentDidMount() { + this.props.onLoadUnknownNetworkElements(); + } +} + +export const ConnectApplication = (connect(undefined, mapDispatcher)(ConnectApplicationComponent)); +export default ConnectApplication;
\ No newline at end of file |