diff options
author | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2021-07-28 11:19:09 +0200 |
---|---|---|
committer | Aijana Schumann <aijana.schumann@highstreet-technologies.com> | 2021-07-28 11:19:09 +0200 |
commit | d70d7624795a9305eef1f0a6d3842d47ecd27160 (patch) | |
tree | 5324c4484be5a94d2a8f2cebb8035792a6073d7b | |
parent | 2aa3a5dd6c190bf0a6c398bf7bf69e359eff2f9d (diff) |
ODLUX Connect App Info enhancement
Present yang capabilities in a table view instead of list
Issue-ID: SDNC-1121
Signed-off-by: Aijana Schumann <aijana.schumann@highstreet-technologies.com>
Change-Id: I4904fb132351199b57a851faf07d371fa5e575ab
9 files changed, 577 insertions, 462 deletions
diff --git a/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts b/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts index 0c3266216..26aa8d2d7 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts @@ -64,6 +64,9 @@ export const findWebUrisForGuiCutThroughAsyncAction = (networkElementIds: string // keep method from executing simultanously; state not used because change of iu isn't needed + if (isBusy) + return; + isBusy = true; const { connect: { guiCutThrough, networkElements } } = getState(); @@ -78,16 +81,16 @@ export const findWebUrisForGuiCutThroughAsyncAction = (networkElementIds: string if (item.status === "Connected") { // if (item.coreModelCapability !== "Unsupported") { - // element is connected and is added to search list, if it doesn't exist already - const exists = guiCutThrough.searchedElements.filter(element => element.id === id).length > 0; - if (!exists) { - elementsToSearch.push(id); - - //element was found previously, but wasn't connected - if (guiCutThrough.notSearchedElements.length > 0 && guiCutThrough.notSearchedElements.includes(id)) { - prevFoundElements.push(id); - } + // element is connected and is added to search list, if it doesn't exist already + const exists = guiCutThrough.searchedElements.filter(element => element.id === id).length > 0; + if (!exists) { + elementsToSearch.push(id); + + //element was found previously, but wasn't connected + if (guiCutThrough.notSearchedElements.length > 0 && guiCutThrough.notSearchedElements.includes(id)) { + prevFoundElements.push(id); } + } // } else { // // element does not support core model and must not be searched for a weburi // const id = item.id as string; @@ -113,9 +116,10 @@ export const findWebUrisForGuiCutThroughAsyncAction = (networkElementIds: string if (elementsToSearch.length > 0 || notConnectedElements.length > 0 || unsupportedElements.length > 0) { - const result = await connectService.getAllWebUriExtensionsForNetworkElementListAsync(elementsToSearch); - dispatcher(new AddWebUriList(result, notConnectedElements, unsupportedElements, prevFoundElements)); + const result = await connectService.getAllWebUriExtensionsForNetworkElementListAsync(elementsToSearch); + dispatcher(new AddWebUriList(result, notConnectedElements, unsupportedElements, prevFoundElements)); } + isBusy = false; } diff --git a/sdnr/wt/odlux/apps/connectApp/src/actions/infoNetworkElementActions.ts b/sdnr/wt/odlux/apps/connectApp/src/actions/infoNetworkElementActions.ts index 4ae28aab2..bb744e236 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/actions/infoNetworkElementActions.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/actions/infoNetworkElementActions.ts @@ -15,43 +15,68 @@ * the License. * ============LICENSE_END========================================================================== */ -import { Action } from '../../../../framework/src/flux/action'; -import { Dispatch } from '../../../../framework/src/flux/store'; - -import { TopologyNode } from '../models/topologyNetconf'; -import { connectService } from '../services/connectService'; - -/** - * Represents the base action. - */ -export class BaseAction extends Action { } - -/** - * Represents an action causing the store to load all element Yang capabilities. - */ -export class LoadAllElementInfoAction extends BaseAction { } - -/** - * Represents an action causing the store to update element Yang capabilities. - */ -export class AllElementInfoLoadedAction extends BaseAction { - /** - * Initialize this instance. - * @param elementInfo The information of the element which is returned. - */ - constructor(public elementInfo: TopologyNode | null, public error?: string) { - super(); - } -} - -/** - * Represents an asynchronous thunk action to load all yang capabilities. - */ -export const loadAllInfoElementAsync = (nodeId: string) => (dispatch: Dispatch) => { - dispatch(new LoadAllElementInfoAction()); - connectService.infoNetworkElement(nodeId).then(info => { - dispatch(new AllElementInfoLoadedAction(info)); - }, error => { - dispatch(new AllElementInfoLoadedAction(null, error)); - }); -}
\ No newline at end of file + import { Action } from '../../../../framework/src/flux/action'; + import { Dispatch } from '../../../../framework/src/flux/store'; + + import { Module, TopologyNode } from '../models/topologyNetconf'; + import { connectService } from '../services/connectService'; + + /** + * Represents the base action. + */ + export class BaseAction extends Action { } + + /** + * Represents an action causing the store to load all element Yang capabilities. + */ + export class LoadAllElementInfoAction extends BaseAction { } + + /** + * Represents an action causing the store to update element Yang capabilities. + */ + export class AllElementInfoLoadedAction extends BaseAction { + /** + * Initialize this instance. + * @param elementInfo The information of the element which is returned. + */ + constructor(public elementInfo: TopologyNode | null, public error?: string) { + super(); + } + } + + /** + * Represents an action causing the store to update element Yang capabilities Module Features. + */ + export class AllElementInfoFeatureLoadedAction extends BaseAction { + /** + * Initialize this instance. + * @param elementFeatureInfo The information of the element which is returned. + */ + constructor(public elementFeatureInfo: Module[] | null | undefined, public error?: string) { + super(); + } + } + + /** + * Represents an asynchronous thunk action to load all yang capabilities. + */ + export const loadAllInfoElementAsync = (nodeId: string) => (dispatch: Dispatch) => { + dispatch(new LoadAllElementInfoAction()); + connectService.infoNetworkElement(nodeId).then(info => { + dispatch(new AllElementInfoLoadedAction(info)); + }, error => { + dispatch(new AllElementInfoLoadedAction(null, error)); + }); + } + + /** + * Represents an asynchronous thunk action to load all yang features. + */ + export const loadAllInfoElementFeaturesAsync = (nodeId: string) => (dispatch: Dispatch) => { + dispatch(new LoadAllElementInfoAction()); + connectService.infoNetworkElementFeatures(nodeId).then(infoFeatures => { + dispatch(new AllElementInfoFeatureLoadedAction(infoFeatures)); + }, error => { + dispatch(new AllElementInfoFeatureLoadedAction(null, error)); + }); + }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/infoNetworkElementDialog.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/infoNetworkElementDialog.tsx index df8515e58..9b71eb354 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/components/infoNetworkElementDialog.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/components/infoNetworkElementDialog.tsx @@ -15,142 +15,145 @@ * the License. * ============LICENSE_END========================================================================== */ -import * as React from 'react'; - -import Button from '@material-ui/core/Button'; -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 Table from '@material-ui/core/Table'; -import TableBody from '@material-ui/core/TableBody'; -import TableCell from '@material-ui/core/TableCell'; -import TableHead from '@material-ui/core/TableHead'; -import TableRow from '@material-ui/core/TableRow'; -import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; - -import { NetworkElementConnection } from '../models/networkElementConnection'; -import { AvailableCapabilities } from '../models/yangCapabilitiesType' - -export enum InfoNetworkElementDialogMode { - None = "none", - InfoNetworkElement = "infoNetworkElement" -} - -const mapDispatch = (dispatcher: IDispatcher) => ({ -}); - -type DialogSettings = { - dialogTitle: string, - dialogDescription: string, - cancelButtonText: string, -} - -const settings: { [key: string]: DialogSettings } = { - [InfoNetworkElementDialogMode.None]: { - dialogTitle: "", - dialogDescription: "", - cancelButtonText: "", - }, - [InfoNetworkElementDialogMode.InfoNetworkElement]: { - dialogTitle: "Yang capabilities of the network element", - dialogDescription: "Available capabilities of the network element", - cancelButtonText: "OK", - } -} - -type InfoNetworkElementDialogComponentProps = Connect<undefined, typeof mapDispatch> & { - mode: InfoNetworkElementDialogMode; - initialNetworkElement: NetworkElementConnection; - onClose: () => void; -}; - -type InfoNetworkElementDialogComponentState = NetworkElementConnection; - -class InfoNetworkElementDialogComponent extends React.Component<InfoNetworkElementDialogComponentProps, InfoNetworkElementDialogComponentState> { - constructor(props: InfoNetworkElementDialogComponentProps) { - super(props); - - this.state = { - nodeId: this.props.initialNetworkElement.nodeId, - isRequired: false, - host: this.props.initialNetworkElement.host, - port: this.props.initialNetworkElement.port, - }; - } - - render(): JSX.Element { - const setting = settings[this.props.mode]; - const availableCapabilities = this.props.state.connect.elementInfo.elementInfo["netconf-node-topology:available-capabilities"]["available-capability"]; - let yangCapabilities: AvailableCapabilities[] = []; - - availableCapabilities.forEach(value => { - const capabilty = value.capability; - const indexRevision = capabilty.indexOf("revision="); - const indexModule = capabilty.indexOf(")", indexRevision); - if (indexRevision > 0 && indexModule > 0) { - yangCapabilities.push({ - module: capabilty.substr(indexModule + 1), - revision: capabilty.substr(indexRevision + 9, 10) - }); - } - }); - - yangCapabilities = yangCapabilities.sort((a,b) => a.module === b.module ? 0 : a.module > b.module ? 1 : -1); - - return ( - <Dialog open={this.props.mode !== InfoNetworkElementDialogMode.None}> - <DialogTitle id="form-dialog-title">{setting.dialogTitle}</DialogTitle> - <DialogContent> - <DialogContentText> - {setting.dialogDescription + " " + this.state.nodeId} - </DialogContentText> - <Table aria-label="yang-capabilities-table"> - <TableHead> - <TableRow> - <TableCell align="right">S.No</TableCell> - <TableCell >Module</TableCell> - <TableCell >Revision</TableCell> - </TableRow> - </TableHead> - <TableBody> - {yangCapabilities.map((yang, index) => ( - <TableRow aria-label="yang-capabilities-row"> - <TableCell>{index + 1}</TableCell> - <TableCell aria-label="yang-module"><a href={`/yang-schema/${yang.module}`} target={"_blank"}> {yang.module} </a></TableCell> - <TableCell aria-label="yang-revision">{yang.revision}</TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </DialogContent> - <DialogActions> - <Button aria-label="ok-button" onClick={(event) => { - this.onCancel(); - event.preventDefault(); - event.stopPropagation(); - }} color="secondary"> {setting.cancelButtonText} </Button> - </DialogActions> - </Dialog> - ) - } - - private onCancel = () => { - this.props.onClose(); - } - - static getDerivedStateFromProps(props: InfoNetworkElementDialogComponentProps, state: InfoNetworkElementDialogComponentState & { _initialNetworkElement: NetworkElementConnection }): InfoNetworkElementDialogComponentState & { _initialNetworkElement: NetworkElementConnection } { - if (props.initialNetworkElement !== state._initialNetworkElement) { - state = { - ...state, - ...props.initialNetworkElement, - _initialNetworkElement: props.initialNetworkElement, - }; - } - return state; - } -} - -export const InfoNetworkElementDialog = connect(undefined, mapDispatch)(InfoNetworkElementDialogComponent); -export default InfoNetworkElementDialog;
\ No newline at end of file + import * as React from 'react'; + + import Button from '@material-ui/core/Button'; + import Dialog from '@material-ui/core/Dialog'; + import DialogActions from '@material-ui/core/DialogActions'; + import DialogTitle from '@material-ui/core/DialogTitle'; + import { MaterialTable, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; + import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + + import { NetworkElementConnection } from '../models/networkElementConnection'; + import { AvailableCapabilities } from '../models/yangCapabilitiesType' + + export enum InfoNetworkElementDialogMode { + None = "none", + InfoNetworkElement = "infoNetworkElement" + } + + const mapDispatch = (dispatcher: IDispatcher) => ({ + }); + + + const InfoElementTable = MaterialTable as MaterialTableCtorType<AvailableCapabilities>; + + type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + cancelButtonText: string, + } + + const settings: { [key: string]: DialogSettings } = { + [InfoNetworkElementDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + cancelButtonText: "", + }, + [InfoNetworkElementDialogMode.InfoNetworkElement]: { + dialogTitle: "Yang capabilities of the network element", + dialogDescription: "", + cancelButtonText: "OK", + } + } + + type InfoNetworkElementDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: InfoNetworkElementDialogMode; + initialNetworkElement: NetworkElementConnection; + onClose: () => void; + }; + + type InfoNetworkElementDialogComponentState = NetworkElementConnection; + + class InfoNetworkElementDialogComponent extends React.Component<InfoNetworkElementDialogComponentProps, InfoNetworkElementDialogComponentState> { + constructor(props: InfoNetworkElementDialogComponentProps) { + super(props); + + this.state = { + nodeId: this.props.initialNetworkElement.nodeId, + isRequired: false, + host: this.props.initialNetworkElement.host, + port: this.props.initialNetworkElement.port, + }; + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + const availableCapabilities = this.props.state.connect.elementInfo.elementInfo["netconf-node-topology:available-capabilities"]["available-capability"]; + let yangFeatures = this.props.state.connect.elementFeatureInfo.elementFeatureInfo; + let yangCapabilities: AvailableCapabilities[] = []; + + availableCapabilities.forEach(value => { + const capabilty = value.capability; + const indexRevision = capabilty.indexOf("revision="); + const indexModule = capabilty.indexOf(")", indexRevision); + if (indexRevision > 0 && indexModule > 0) { + let moduleName = capabilty.substr(indexModule + 1); + let ModuleFeaturesList; + for(let index = 0; index < yangFeatures.length; index++) { + if(yangFeatures[index].name == moduleName) { + ModuleFeaturesList = yangFeatures[index].feature? yangFeatures[index].feature : null; + break; + } + } + const featuresListCommaSeparated= ModuleFeaturesList? ModuleFeaturesList.toString() : "" + let featuresList = featuresListCommaSeparated.replace(',',', '); + + yangCapabilities.push({ + module: moduleName, + revision: capabilty.substr(indexRevision + 9, 10), + features: featuresList + }); + } + }); + + yangCapabilities = yangCapabilities.sort((a,b) => a.module === b.module ? 0 : a.module > b.module ? 1 : -1); + + return ( + <> + <Dialog open={this.props.mode !== InfoNetworkElementDialogMode.None} > + <DialogTitle id="form-dialog-title">{setting.dialogTitle + ' - ' + this.state.nodeId}</DialogTitle> + <InfoElementTable stickyHeader tableId="info-element-table" asynchronus columns={[ + { property: "module", title: "Module", type: ColumnType.text, width:900 }, + { + property: "revision", title: "Revision", type: ColumnType.custom, customControl: ({ rowData }) => { + return ( + <div> + <a href={'/yang-schema/' + rowData.module + '/' + rowData.revision} target="_blank" > {rowData.revision} </a> + </div> + ) + } + }, + { property: "features", title: "Features", type: ColumnType.text, width:500 }, + ]} idProperty="id" rows={yangCapabilities} > + </InfoElementTable> + <DialogActions> + <Button aria-label="ok-button" onClick={(event) => { + this.onCancel(); + event.preventDefault(); + event.stopPropagation(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + </> + ) + } + + private onCancel = () => { + this.props.onClose(); + } + + static getDerivedStateFromProps(props: InfoNetworkElementDialogComponentProps, state: InfoNetworkElementDialogComponentState & { _initialNetworkElement: NetworkElementConnection }): InfoNetworkElementDialogComponentState & { _initialNetworkElement: NetworkElementConnection } { + if (props.initialNetworkElement !== state._initialNetworkElement) { + state = { + ...state, + ...props.initialNetworkElement, + _initialNetworkElement: props.initialNetworkElement, + }; + } + return state; + } + } + + export const InfoNetworkElementDialog = connect(undefined, mapDispatch)(InfoNetworkElementDialogComponent); + export default InfoNetworkElementDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx index 84a22a99a..73706f678 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx @@ -36,12 +36,12 @@ import { NavigateToApplication } from '../../../../framework/src/actions/navigat import { createNetworkElementsActions, createNetworkElementsProperties } from '../handlers/networkElementsHandler'; import { NetworkElementConnection } from '../models/networkElementConnection'; -import { TopologyNode } from '../models/topologyNetconf'; +import { ModuleSet, TopologyNode } from '../models/topologyNetconf'; import EditNetworkElementDialog, { EditNetworkElementDialogMode } from './editNetworkElementDialog'; import RefreshNetworkElementsDialog, { RefreshNetworkElementsDialogMode } from './refreshNetworkElementsDialog'; import InfoNetworkElementDialog, { InfoNetworkElementDialogMode } from './infoNetworkElementDialog'; -import { loadAllInfoElementAsync } from '../actions/infoNetworkElementActions'; +import { loadAllInfoElementAsync, loadAllInfoElementFeaturesAsync } from '../actions/infoNetworkElementActions'; import { connectService } from '../services/connectService'; import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; @@ -92,6 +92,7 @@ const mapDispatch = (dispatcher: IDispatcher) => ({ networkElementsActions: createNetworkElementsActions(dispatcher.dispatch), navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path)), networkElementInfo: async (nodeId: string) => await dispatcher.dispatch(loadAllInfoElementAsync(nodeId)), + networkElementFeaturesInfo: async (nodeId: string) => await dispatcher.dispatch(loadAllInfoElementFeaturesAsync(nodeId)) }); type NetworkElementsListComponentProps = WithStyles<typeof styles> & Connect<typeof mapProps, typeof mapDispatch>; @@ -100,7 +101,8 @@ type NetworkElementsListComponentState = { networkElementEditorMode: EditNetworkElementDialogMode, refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode, infoNetworkElementEditorMode: InfoNetworkElementDialogMode, - elementInfo: TopologyNode | null + elementInfo: TopologyNode | null, + elementInfoFeature: ModuleSet | null } const emptyRequireNetworkElement: NetworkElementConnection = { id: "", nodeId: "", host: "", port: 0, status: "Disconnected", isRequired: false }; @@ -117,6 +119,7 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement networkElementEditorMode: EditNetworkElementDialogMode.None, refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode.None, elementInfo: null, + elementInfoFeature:null, infoNetworkElementEditorMode: InfoNetworkElementDialogMode.None }; } @@ -164,7 +167,7 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement const canAdd = true; const addRequireNetworkElementAction = { - icon: AddIcon, tooltip: 'Add', onClick: () => { + icon: AddIcon, tooltip: 'Add', ariaLabel:"add-element", onClick: () => { this.setState({ networkElementEditorMode: EditNetworkElementDialogMode.AddNewNetworkElement, networkElementToEdit: emptyRequireNetworkElement, @@ -173,7 +176,7 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement }; const refreshNetworkElementsAction = { - icon: Refresh, tooltip: 'Refresh Network Elements table', onClick: () => { + icon: Refresh, tooltip: 'Refresh Network Elements table', ariaLabel:'refresh', onClick: () => { this.setState({ refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode.RefreshNetworkElementsTable }); @@ -266,6 +269,7 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement private onOpenInfoNetworkElementDialog = (event: React.MouseEvent<HTMLElement>, element: NetworkElementConnection) => { this.props.networkElementInfo(element.nodeId); + this.props.networkElementFeaturesInfo(element.nodeId); this.setState({ networkElementToEdit: element, infoNetworkElementEditorMode: InfoNetworkElementDialogMode.InfoNetworkElement, diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts index 70b64c976..81ee97a0a 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts @@ -20,7 +20,7 @@ import { IActionHandler } from '../../../../framework/src/flux/action'; import { combineActionHandler } from '../../../../framework/src/flux/middleware'; import { INetworkElementsState, networkElementsActionHandler } from './networkElementsHandler'; import { IConnectionStatusLogState, connectionStatusLogActionHandler } from './connectionStatusLogHandler'; -import { IInfoNetworkElementsState, infoNetworkElementsActionHandler } from './infoNetworkElementHandler'; +import { IInfoNetworkElementsState, infoNetworkElementsActionHandler, IInfoNetworkElementFeaturesState, infoNetworkElementFeaturesActionHandler } from './infoNetworkElementHandler'; import { SetPanelAction, AddWebUriList, RemoveWebUri, SetWeburiSearchBusy } from '../actions/commonNetworkElementsActions'; import { PanelId } from '../models/panelId'; import { guiCutThrough } from '../models/guiCutTrough'; @@ -31,6 +31,7 @@ export interface IConnectAppStoreState { connectionStatusLog: IConnectionStatusLogState; currentOpenPanel: PanelId; elementInfo: IInfoNetworkElementsState; + elementFeatureInfo: IInfoNetworkElementFeaturesState; guiCutThrough: guiCutThroughState; connectionStatusCount: IConnectionStatusCount; } @@ -48,7 +49,7 @@ interface guiCutThroughState { unsupportedElements: string[]; } -const guiCutThroughHandler: IActionHandler<guiCutThroughState> = (state = { searchedElements: [], notSearchedElements: [], unsupportedElements:[] }, action) => { +const guiCutThroughHandler: IActionHandler<guiCutThroughState> = (state = { searchedElements: [], notSearchedElements: [], unsupportedElements: [] }, action) => { if (action instanceof AddWebUriList) { let notSearchedElements: string[]; let searchedElements: guiCutThrough[]; @@ -73,7 +74,7 @@ const guiCutThroughHandler: IActionHandler<guiCutThroughState> = (state = { sear const webUris = state.searchedElements.filter(item => item.id !== nodeId); const knownElements = state.notSearchedElements.filter(item => item !== nodeId); const unsupportedElement = state.unsupportedElements.filter(item => item != nodeId); - state = { notSearchedElements: knownElements, searchedElements: webUris, unsupportedElements: unsupportedElement }; + state = { notSearchedElements: knownElements, searchedElements: webUris, unsupportedElements: unsupportedElement }; } return state; } @@ -89,6 +90,7 @@ const actionHandlers = { connectionStatusLog: connectionStatusLogActionHandler, currentOpenPanel: currentOpenPanelHandler, elementInfo: infoNetworkElementsActionHandler, + elementFeatureInfo: infoNetworkElementFeaturesActionHandler, guiCutThrough: guiCutThroughHandler, connectionStatusCount: connectionStatusCountHandler }; diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/infoNetworkElementHandler.ts b/sdnr/wt/odlux/apps/connectApp/src/handlers/infoNetworkElementHandler.ts index d67a81ec4..3e2d1cec1 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/handlers/infoNetworkElementHandler.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/infoNetworkElementHandler.ts @@ -15,46 +15,79 @@ * the License. * ============LICENSE_END========================================================================== */ -import { IActionHandler } from '../../../../framework/src/flux/action'; + import { IActionHandler } from '../../../../framework/src/flux/action'; -import { AllElementInfoLoadedAction, LoadAllElementInfoAction } from '../actions/infoNetworkElementActions'; - -import { TopologyNode } from '../models/topologyNetconf'; - -export interface IInfoNetworkElementsState { - elementInfo: TopologyNode; - busy: boolean; -} - -const infoNetworkElementsStateInit: IInfoNetworkElementsState = { - elementInfo: { - "node-id": "", - "netconf-node-topology:available-capabilities": { - "available-capability": [] - } - }, - busy: false -}; - -export const infoNetworkElementsActionHandler: IActionHandler<IInfoNetworkElementsState> = (state = infoNetworkElementsStateInit, action) => { - if (action instanceof LoadAllElementInfoAction) { - state = { - ...state, - busy: true - }; - } else if (action instanceof AllElementInfoLoadedAction) { - if (!action.error && action.elementInfo) { - state = { - ...state, - elementInfo: action.elementInfo, - busy: false - }; - } else { - state = { - ...state, - busy: false - }; - } - } - return state; -};
\ No newline at end of file + import { AllElementInfoLoadedAction, AllElementInfoFeatureLoadedAction, LoadAllElementInfoAction } from '../actions/infoNetworkElementActions'; + + import { Module, TopologyNode } from '../models/topologyNetconf'; + + export interface IInfoNetworkElementsState { + elementInfo: TopologyNode; + busy: boolean; + } + + export interface IInfoNetworkElementFeaturesState { + elementFeatureInfo: Module[]; + busy: boolean; + } + + const infoNetworkElementsStateInit: IInfoNetworkElementsState = { + elementInfo: { + "node-id": "", + "netconf-node-topology:available-capabilities": { + "available-capability": [] + } + }, + busy: false + }; + + const infoNetworkElementFeaturesStateInit: IInfoNetworkElementFeaturesState = { + elementFeatureInfo: [], + busy: false + }; + + export const infoNetworkElementsActionHandler: IActionHandler<IInfoNetworkElementsState> = (state = infoNetworkElementsStateInit, action) => { + if (action instanceof LoadAllElementInfoAction) { + state = { + ...state, + busy: true + }; + } else if (action instanceof AllElementInfoLoadedAction) { + if (!action.error && action.elementInfo) { + state = { + ...state, + elementInfo: action.elementInfo, + busy: false + }; + } else { + state = { + ...state, + busy: false + }; + } + } + return state; + }; + + export const infoNetworkElementFeaturesActionHandler: IActionHandler<IInfoNetworkElementFeaturesState> = (state = infoNetworkElementFeaturesStateInit, action) => { + if (action instanceof LoadAllElementInfoAction) { + state = { + ...state, + busy: true + }; + } else if (action instanceof AllElementInfoFeatureLoadedAction) { + if (!action.error && action.elementFeatureInfo) { + state = { + ...state, + elementFeatureInfo: action.elementFeatureInfo, + busy: false + }; + } else { + state = { + ...state, + busy: false + }; + } + } + return state; + };
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts b/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts index ef22aab43..936e20bc2 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/models/topologyNetconf.ts @@ -16,7 +16,7 @@ * ============LICENSE_END========================================================================== */ -export interface AvailableCapability { + export interface AvailableCapability { "capability-origin": string; capability: string; } @@ -34,3 +34,26 @@ export interface Topology { "topology-id": string; "network-topology:node": TopologyNode[]; } + +/** + * Represents the type of the features of the Module. + */ +export interface Module { + feature?: string[]; + location?: string[]; + name: string; + namespace?: string; + revision?: string; +} + +export interface ModuleFeatures { + module: Module[]; +} + +export interface ModuleSet { + "module-set": ModuleFeatures[]; +} + +export interface FeatureTopology { + "ietf-yang-library:yang-library" : ModuleSet +} diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/yangCapabilitiesType.ts b/sdnr/wt/odlux/apps/connectApp/src/models/yangCapabilitiesType.ts index 230468287..c8cf7049c 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/models/yangCapabilitiesType.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/models/yangCapabilitiesType.ts @@ -16,7 +16,9 @@ * ============LICENSE_END========================================================================== */ -export type AvailableCapabilities = { + export type AvailableCapabilities = { + id?: string, module: string, - revision: string + revision: string, + features: string }
\ 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 index 5d7667a7f..ed8b0f67d 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts @@ -16,224 +16,243 @@ * ============LICENSE_END========================================================================== */ -import { requestRest } from '../../../../framework/src/services/restService'; -import { NetworkElementConnection, ConnectionStatus, UpdateNetworkElement } from '../models/networkElementConnection'; -import { convertPropertyNames, replaceUpperCase } from '../../../../framework/src/utilities/yangHelper'; -import { Result } from '../../../../framework/src/models/elasticSearch'; - -import { Topology, TopologyNode } from '../models/topologyNetconf'; -import { guiCutThrough } from '../models/guiCutTrough'; - -/** -* Represents a web api accessor service for all Network Elements actions. -*/ -class ConnectService { - public getNetworkElementUri = (nodeId: string) => '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; - public getNetworkElementConnectDataProviderUri = (operation: "create" | "update" | "delete" ) => `/rests/operations/data-provider:${operation}-network-element-connection`; - public getAllWebUriExtensionsForNetworkElementListUri = (nodeId: string) => this.getNetworkElementUri(nodeId) + '/yang-ext:mount/core-model:network-element'; - - /** - * Inserts a network elements. - */ - public async createNetworkElement(element: NetworkElementConnection): Promise<NetworkElementConnection | null> { - const path = this.getNetworkElementConnectDataProviderUri("create") ; - const result = await requestRest<NetworkElementConnection>(path, { - method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": element }, replaceUpperCase)) - }); - return result || null; - } - - /** - * Updates a network element. - */ - public async updateNetworkElement(element: UpdateNetworkElement): Promise<NetworkElementConnection | null> { - const path = this.getNetworkElementConnectDataProviderUri("update"); - const result = await requestRest<NetworkElementConnection>(path, { - method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": element }, replaceUpperCase)) - }); - return result || null; - } - - /** - * Deletes a network element. + import { requestRest } from '../../../../framework/src/services/restService'; + import { NetworkElementConnection, ConnectionStatus, UpdateNetworkElement } from '../models/networkElementConnection'; + import { convertPropertyNames, replaceUpperCase } from '../../../../framework/src/utilities/yangHelper'; + import { Result } from '../../../../framework/src/models/elasticSearch'; + + import { FeatureTopology, Topology, TopologyNode, Module } from '../models/topologyNetconf'; + import { guiCutThrough } from '../models/guiCutTrough'; + + /** + * Represents a web api accessor service for all Network Elements actions. + */ + class ConnectService { + public getNetworkElementUri = (nodeId: string) => '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; + public getNetworkElementConnectDataProviderUri = (operation: "create" | "update" | "delete" ) => `/rests/operations/data-provider:${operation}-network-element-connection`; + public getAllWebUriExtensionsForNetworkElementListUri = (nodeId: string) => this.getNetworkElementUri(nodeId) + '/yang-ext:mount/core-model:network-element'; + public getNetworkElementYangLibraryFeature = (nodeId: string) => '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId + '/yang-ext:mount/ietf-yang-library:yang-library?content=nonconfig' + + /** + * Inserts a network elements. */ - public async deleteNetworkElement(element: UpdateNetworkElement): Promise<NetworkElementConnection | null> { - const query = { - "id": element.id - }; - const path = this.getNetworkElementConnectDataProviderUri("delete"); - const result = await requestRest<NetworkElementConnection>(path, { - method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": query }, replaceUpperCase)) - }); - return result || null; - } - - /** Mounts network element. */ - public async mountNetworkElement(networkElement: NetworkElementConnection): Promise<boolean> { - const path = this.getNetworkElementUri(networkElement.nodeId); - const mountXml = [ - '<node xmlns="urn:TBD:params:xml:ns:yang:network-topology">', - `<node-id>${networkElement.nodeId}</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<string>(path, { - method: 'PUT', - headers: { - 'Content-Type': 'application/xml', - 'Accept': 'application/xml' - }, - body: mountXml - }); - // expect an empty answer - return result !== null; - } catch { - return false; - } - }; - - /** Unmounts a network element by its id. */ - public async unmountNetworkElement(nodeId: string): Promise<boolean> { - const path = this.getNetworkElementUri(nodeId); - - try { - const result = await requestRest<string>(path, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/xml', - 'Accept': 'application/xml' - }, - }); - // expect an empty answer - return result !== null; - - } catch { - return false; - } - }; - - /** Yang capabilities of the selected network elements. */ - public async infoNetworkElement(nodeId: string): Promise<TopologyNode | null> { - const path = this.getNetworkElementUri(nodeId); - const topologyRequestPomise = requestRest<Topology>(path, { method: "GET" }); - - return topologyRequestPomise && topologyRequestPomise.then(result => { - return result && result["network-topology:node"] && result["network-topology:node"][0] || null; - }); - } - - /** - * Get the connection state of the network element. + public async createNetworkElement(element: NetworkElementConnection): Promise<NetworkElementConnection | null> { + const path = this.getNetworkElementConnectDataProviderUri("create") ; + const result = await requestRest<NetworkElementConnection>(path, { + method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": element }, replaceUpperCase)) + }); + return result || null; + } + + /** + * Updates a network element. */ - public async getNetworkElementConnectionStatus(element: string): Promise<(ConnectionStatus)[] | null> { - const path = `/rests/operations/data-provider:read-network-element-connection-list`; - const query = { - "data-provider:input": { - "filter": [{ - "property": "node-id", - "filtervalue": element - }], - "pagination": { - "size": 20, - "page": 1 - } - } - } - const result = await requestRest<Result<ConnectionStatus>>(path, { method: "POST", body: JSON.stringify(query) }); - return result && result["data-provider:output"] && result["data-provider:output"].data && result["data-provider:output"].data.map(ne => ({ - status: ne.status - })) || null; - } - - public async getAllWebUriExtensionsForNetworkElementListAsync(neList: string[]): Promise<(guiCutThrough)[]> { - const path = `/rests/operations/data-provider:read-gui-cut-through-entry`; - let webUriList: guiCutThrough[] = [] - const query = { - "data-provider:input": { - "filter": [{ - "property": "id", - "filtervalues": neList - }], - "pagination": { - "size": 20, - "page": 1 - } - } - } - - const result = await requestRest<Result<guiCutThrough>>(path, { method: "POST", body: JSON.stringify(query) }); - const resultData = result && result["data-provider:output"] && result["data-provider:output"].data; - neList.forEach(nodeId => { - let entryNotFound = true; - if (resultData) { - const BreakException = {}; - try { - resultData.forEach(entry => { - if (entry.id == nodeId) { - entryNotFound = false; - if (entry.weburi) { - webUriList.push({ id: nodeId, weburi: entry.weburi }); - } else { - webUriList.push({ id: nodeId, weburi: undefined }); - } - throw BreakException; - } - }); - } catch (e) {} - } - if (entryNotFound) - webUriList.push({ id: nodeId, weburi: undefined }); - }); - return webUriList; - } - - // public async getAllWebUriExtensionsForNetworkElementListAsync(ne: string[]): Promise<(guiCutThrough)[] | null> { - - // let promises: any[] = []; - // let webUris: guiCutThrough[] = [] - - // ne.forEach(nodeId => { - // const path = this.getAllWebUriExtensionsForNetworkElementListUri(nodeId); - - // // add search request to array - // promises.push(requestRest<any>(path, { method: "GET" }) - // .then(result => { - // if (result != null && result['core-model:network-element'] && result['core-model:network-element'].extension) { - // const webUri = result['core-model:network-element'].extension.find((item: any) => item['value-name'] === "webUri") - // if (webUri) { - // webUris.push({ weburi: webUri.value, id: nodeId }); - // } else { - // webUris.push({ weburi: undefined, id: nodeId }); - // } - // } else { - // webUris.push({ weburi: undefined, id: nodeId }); - // } - // }) - // .catch(error => { - // webUris.push({ weburi: undefined, id: nodeId }); - // })) - // }) - // // wait until all promises are done and return weburis - // return Promise.all(promises).then(result => { return webUris }); - // } - -} - - - -export const connectService = new ConnectService(); + public async updateNetworkElement(element: UpdateNetworkElement): Promise<NetworkElementConnection | null> { + const path = this.getNetworkElementConnectDataProviderUri("update"); + const result = await requestRest<NetworkElementConnection>(path, { + method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": element }, replaceUpperCase)) + }); + return result || null; + } + + /** + * Deletes a network element. + */ + public async deleteNetworkElement(element: UpdateNetworkElement): Promise<NetworkElementConnection | null> { + const query = { + "id": element.id + }; + const path = this.getNetworkElementConnectDataProviderUri("delete"); + const result = await requestRest<NetworkElementConnection>(path, { + method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": query }, replaceUpperCase)) + }); + return result || null; + } + + /** Mounts network element. */ + public async mountNetworkElement(networkElement: NetworkElementConnection): Promise<boolean> { + const path = this.getNetworkElementUri(networkElement.nodeId); + const mountXml = [ + '<node xmlns="urn:TBD:params:xml:ns:yang:network-topology">', + `<node-id>${networkElement.nodeId}</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<string>(path, { + method: 'PUT', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }, + body: mountXml + }); + // expect an empty answer + return result !== null; + } catch { + return false; + } + }; + + /** Unmounts a network element by its id. */ + public async unmountNetworkElement(nodeId: string): Promise<boolean> { + const path = this.getNetworkElementUri(nodeId); + + try { + const result = await requestRest<string>(path, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/xml', + 'Accept': 'application/xml' + }, + }); + // expect an empty answer + return result !== null; + + } catch { + return false; + } + }; + + /** Yang capabilities of the selected network elements. */ + public async infoNetworkElement(nodeId: string): Promise<TopologyNode | null> { + const path = this.getNetworkElementUri(nodeId); + const topologyRequestPomise = requestRest<Topology>(path, { method: "GET" }); + + return topologyRequestPomise && topologyRequestPomise.then(result => { + return result && result["network-topology:node"] && result["network-topology:node"][0] || null; + }); + } + + + /** Yang features of the selected network element module. */ + public async infoNetworkElementFeatures(nodeId: string): Promise<Module[] | null | undefined> { + const path = this.getNetworkElementYangLibraryFeature(nodeId); + const topologyRequestPomise = requestRest<FeatureTopology>(path, { method: "GET" }); + + return topologyRequestPomise && topologyRequestPomise.then(result => { + const resultFinal = result && result['ietf-yang-library:yang-library'] + && result["ietf-yang-library:yang-library"]["module-set"] && + result["ietf-yang-library:yang-library"]["module-set"][0] && + result["ietf-yang-library:yang-library"]["module-set"][0]['module'] || null; + return resultFinal; + }); + } + + + + /** + * Get the connection state of the network element. + */ + public async getNetworkElementConnectionStatus(element: string): Promise<(ConnectionStatus)[] | null> { + const path = `/rests/operations/data-provider:read-network-element-connection-list`; + const query = { + "data-provider:input": { + "filter": [{ + "property": "node-id", + "filtervalue": element + }], + "pagination": { + "size": 20, + "page": 1 + } + } + } + const result = await requestRest<Result<ConnectionStatus>>(path, { method: "POST", body: JSON.stringify(query) }); + return result && result["data-provider:output"] && result["data-provider:output"].data && result["data-provider:output"].data.map(ne => ({ + status: ne.status + })) || null; + } + + public async getAllWebUriExtensionsForNetworkElementListAsync(neList: string[]): Promise<(guiCutThrough)[]> { + const path = `/rests/operations/data-provider:read-gui-cut-through-entry`; + let webUriList: guiCutThrough[] = [] + const query = { + "data-provider:input": { + "filter": [{ + "property": "id", + "filtervalues": neList + }], + "pagination": { + "size": 20, + "page": 1 + } + } + } + + const result = await requestRest<Result<guiCutThrough>>(path, { method: "POST", body: JSON.stringify(query) }); + const resultData = result && result["data-provider:output"] && result["data-provider:output"].data; + neList.forEach(nodeId => { + let entryNotFound = true; + if (resultData) { + const BreakException = {}; + try { + resultData.forEach(entry => { + if (entry.id == nodeId) { + entryNotFound = false; + if (entry.weburi) { + webUriList.push({ id: nodeId, weburi: entry.weburi }); + } else { + webUriList.push({ id: nodeId, weburi: undefined }); + } + throw BreakException; + } + }); + } catch (e) {} + } + if (entryNotFound) + webUriList.push({ id: nodeId, weburi: undefined }); + }); + return webUriList; + } + + // public async getAllWebUriExtensionsForNetworkElementListAsync(ne: string[]): Promise<(guiCutThrough)[] | null> { + + // let promises: any[] = []; + // let webUris: guiCutThrough[] = [] + + // ne.forEach(nodeId => { + // const path = this.getAllWebUriExtensionsForNetworkElementListUri(nodeId); + + // // add search request to array + // promises.push(requestRest<any>(path, { method: "GET" }) + // .then(result => { + // if (result != null && result['core-model:network-element'] && result['core-model:network-element'].extension) { + // const webUri = result['core-model:network-element'].extension.find((item: any) => item['value-name'] === "webUri") + // if (webUri) { + // webUris.push({ weburi: webUri.value, id: nodeId }); + // } else { + // webUris.push({ weburi: undefined, id: nodeId }); + // } + // } else { + // webUris.push({ weburi: undefined, id: nodeId }); + // } + // }) + // .catch(error => { + // webUris.push({ weburi: undefined, id: nodeId }); + // })) + // }) + // // wait until all promises are done and return weburis + // return Promise.all(promises).then(result => { return webUris }); + // } + + } + + + + export const connectService = new ConnectService(); +
\ No newline at end of file |