diff options
author | Dan Timoney <dtimoney@att.com> | 2021-04-08 12:39:48 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2021-04-08 12:39:48 +0000 |
commit | d702f00b71218c56af766363ce19f2459081d73c (patch) | |
tree | fafd0b1b622473745bebd767ccab55382d79b5df /sdnr/wt/odlux/apps | |
parent | f3969004c6ccac18e742c5fc48c844e315991023 (diff) | |
parent | 21e4a946cd24b8a03ea577352f0271ebf7669ffa (diff) |
Merge "update odlux for notification change"
Diffstat (limited to 'sdnr/wt/odlux/apps')
58 files changed, 2513 insertions, 1094 deletions
diff --git a/sdnr/wt/odlux/apps/apiDemo/package.json b/sdnr/wt/odlux/apps/apiDemo/package.json index 230e86aea..3805c020e 100644 --- a/sdnr/wt/odlux/apps/apiDemo/package.json +++ b/sdnr/wt/odlux/apps/apiDemo/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/package.json b/sdnr/wt/odlux/apps/configurationApp/package.json index d1ab86da7..16588cd18 100644 --- a/sdnr/wt/odlux/apps/configurationApp/package.json +++ b/sdnr/wt/odlux/apps/configurationApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/policies.json b/sdnr/wt/odlux/apps/configurationApp/policies.json index cd9e9fc15..91a0abf96 100644 --- a/sdnr/wt/odlux/apps/configurationApp/policies.json +++ b/sdnr/wt/odlux/apps/configurationApp/policies.json @@ -1,12 +1,12 @@ [ { - "path": "/**/operations/cluster-admin**", - "actions": { - "get": true, - "post": true, - "put": true, - "patch": true, - "delete": true + "path": "/rests/**/node=Sim2230**", + "methods": { + "get": false, + "post": false, + "put": false, + "delete": false, + "patch": false } } ]
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts index d6283852c..f80fbfc4d 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts @@ -9,6 +9,7 @@ import { restService } from "../services/restServices"; import { YangParser } from "../yang/yangParser"; import { Module } from "../models/yang"; import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList, isViewElementRpc, isViewElementChoise, ViewElementChoiseCase, ViewElementString } from "../models/uiModels"; +import { exception } from 'console'; export class EnableValueSelector extends Action { constructor(public listSpecification: ViewSpecification, public listData: any[], public keyProperty: string, public onValueSelected : (value: any) => void ) { @@ -333,6 +334,7 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: const refView : ViewSpecification = { id: "-1", canEdit: false, + config: false, language: "en-US", elements: { [viewElement.key!] : { @@ -441,11 +443,13 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: // create display specification const ds: DisplaySpecification = viewElement! && viewElement!.uiType === "rpc" ? { + dataPath, displayMode: DisplayModeType.displayAsRPC, inputViewSpecification: inputViewSpecification && resolveViewDescription(defaultNS, vPath, inputViewSpecification), outputViewSpecification: outputViewSpecification && resolveViewDescription(defaultNS, vPath, outputViewSpecification), } : { + dataPath, displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, @@ -517,6 +521,35 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async } } + // remove read-only elements + const removeReadOnlyElements = (viewSpecification: ViewSpecification, isList: boolean, data: any) => { + if (isList) { + return data.map((elm : any) => removeReadOnlyElements(viewSpecification, false, elm)); + } else { + return Object.keys(data).reduce<{[key: string]: any}>((acc, cur)=>{ + const [nsOrName, name] = cur.split(':',1); + const element = viewSpecification.elements[cur] || viewSpecification.elements[nsOrName] || viewSpecification.elements[name]; + if (!element && process.env.NODE_ENV === "development" ) { + throw new Error("removeReadOnlyElements: Could not determine elment for data."); + } + if (element && element.config) { + if (element.uiType==="object") { + const view = views[+element.viewId]; + if (!view) { + throw new Error("removeReadOnlyElements: Internal Error could not determine viewId: "+element.viewId); + } + acc[cur] = removeReadOnlyElements(view, element.isList != null && element.isList, data[cur]); + } else { + acc[cur] = data[cur]; + } + } + return acc; + }, {}); + } + }; + data = removeReadOnlyElements(viewSpecification, embedList, data); + + // embed the list -> key: list data = embedList ? { [viewElement!.label]: data } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx index 223c4cb25..b7697c880 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx @@ -16,7 +16,7 @@ * ============LICENSE_END========================================================================== */ -import React from 'react'; +import React, { useState } from 'react'; import { Tooltip, Button, FormControl, Theme, createStyles, makeStyles } from '@material-ui/core'; import { ViewElement } from '../models/uiModels'; @@ -35,13 +35,14 @@ type UIElementReferenceProps = { export const UIElementReference: React.FC<UIElementReferenceProps> = (props) => { const classes = useStyles(); + const [disabled, setDisabled] = useState(true); const { element } = props; return ( - <FormControl key={element.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <FormControl key={element.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }} onMouseDown={(ev) => { ev.preventDefault(); ev.stopPropagation(); ev.button === 1 && setDisabled(!disabled) }}> <Tooltip title={element.description || element.path || ''}> - <Button className={classes.button} aria-label={element.label+'-button'} color="secondary" disabled={props.disabled} onClick={() => { + <Button className={classes.button} aria-label={element.label+'-button'} color="secondary" disabled={props.disabled && disabled} onClick={() => { props.onOpenReference(element); - }}>{`${element.label}`}</Button> + }} >{`${element.label}`}</Button> </Tooltip> </FormControl> ); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts index e6b808b80..02f2929cd 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts @@ -18,8 +18,10 @@ import { createExternal, IExternalTableState } from '../../../../framework/src/components/material-table/utilities'; import { createSearchDataHandler } from '../../../../framework/src/utilities/elasticSearch'; +import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; import { NetworkElementConnection } from '../models/networkElementConnection'; +import { restService } from '../services/restServices'; export interface IConnectedNetworkElementsState extends IExternalTableState<NetworkElementConnection> { } @@ -33,4 +35,11 @@ export const { reloadAction: connectedNetworkElementsReloadAction, // set value action, to change a value -} = createExternal<NetworkElementConnection>(connectedNetworkElementsSearchHandler, appState => appState.configuration.connectedNetworkElements); +} = createExternal<NetworkElementConnection>(connectedNetworkElementsSearchHandler, appState => appState.configuration.connectedNetworkElements, + (ne) => { + if (!ne || !ne.id) return true; + const neUrl = restService.getNetworkElementUri(ne.id); + const policy = getAccessPolicyByUrl(neUrl); + return !(policy.GET && policy.POST); + } +); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts index ea2036415..7a9812bfd 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts @@ -35,10 +35,12 @@ export type DisplaySpecification = { viewSpecification: ViewSpecification; keyProperty?: string; apidocPath?: string; + dataPath?: string; } | { displayMode: DisplayModeType.displayAsRPC; inputViewSpecification?: ViewSpecification; outputViewSpecification?: ViewSpecification; + dataPath?: string; } export interface IViewDescriptionState { diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts index 9c03bdf9b..29484d812 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts @@ -220,6 +220,7 @@ export type ViewSpecification = { "when"?: string; "uses"?: (string[]) & { [ResolveFunction]?: (parent: string) => void }; "elements": { [name: string]: ViewElement }; + "config": boolean; readonly "canEdit": boolean; } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx index 0cab7b793..3bc0e3968 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx @@ -29,21 +29,19 @@ import { NetworkElementSelector } from "./views/networkElementSelector"; import ConfigurationApplication from "./views/configurationApplication"; import { updateNodeIdAsyncActionCreator, updateViewActionAsyncCreator } from "./actions/deviceActions"; +import { DisplayModeType } from "./handlers/viewDescriptionHandler"; +import { ViewSpecification } from "./models/uiModels"; let currentNodeId: string | null | undefined = undefined; let currentVirtualPath: string | null | undefined = undefined; let lastUrl: string | undefined = undefined; -const mapProps = (state: IApplicationStoreState) => ({ - // currentProblemsProperties: createCurrentProblemsProperties(state), -}); - const mapDisp = (dispatcher: IDispatcher) => ({ updateNodeId: (nodeId: string) => dispatcher.dispatch(updateNodeIdAsyncActionCreator(nodeId)), updateView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), }); -const ConfigurationApplicationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComponentProps<{ nodeId?: string, 0: string }> & Connect<typeof mapProps, typeof mapDisp>) => { +const ConfigurationApplicationRouteAdapter = connect(undefined, mapDisp)((props: RouteComponentProps<{ nodeId?: string, 0: string }> & Connect<undefined, typeof mapDisp>) => { React.useEffect(() => { return () => { lastUrl = undefined; @@ -57,17 +55,66 @@ const ConfigurationApplicationRouteAdapter = connect(mapProps, mapDisp)((props: window.setTimeout(async () => { // check if the nodeId has changed + let dump = false; if (currentNodeId !== props.match.params.nodeId) { currentNodeId = props.match.params.nodeId || undefined; + if (currentNodeId && currentNodeId.endsWith("|dump")) { + dump = true; + currentNodeId = currentNodeId.replace(/\|dump$/i, ''); + } currentVirtualPath = null; currentNodeId && await props.updateNodeId(currentNodeId); } if (currentVirtualPath !== props.match.params[0]) { currentVirtualPath = props.match.params[0]; + if (currentVirtualPath && currentVirtualPath.endsWith("|dump")) { + dump = true; + currentVirtualPath = currentVirtualPath.replace(/\|dump$/i, ''); + } await props.updateView(currentVirtualPath); } + if (dump) { + const device = props.state.configuration.deviceDescription; + const ds = props.state.configuration.viewDescription.displaySpecification; + + const createDump = (view: ViewSpecification | null, level: number = 0) => { + if (view === null) return "Empty"; + const indention = Array(level * 4).fill(' ').join(''); + let result = ''; + + if (!view) debugger; + // result += `${indention} [${view.canEdit ? 'rw' : 'ro'}] ${view.ns}:${view.name} ${ds.displayMode === DisplayModeType.displayAsList ? '[LIST]' : ''}\r\n`; + result += Object.keys(view.elements).reduce((acc, cur) => { + const elm = view.elements[cur]; + acc += `${indention} [${elm.config ? 'rw' : 'ro'}:${elm.id}] (${elm.module}:${elm.label}) {${elm.uiType}} ${elm.uiType === "object" && elm.isList ? `as LIST with KEY [${elm.key}]` : ""}\r\n`; + // acc += `${indention} +${elm.mandatory ? "mandetory" : "none"} - ${elm.path} \r\n`; + + switch (elm.uiType) { + case "object": + acc += createDump(device.views[(elm as any).viewId], level + 1); + break; + default: + } + return acc; + }, ""); + return `${result}`; + } + + const dump = createDump(ds.displayMode === DisplayModeType.displayAsObject || ds.displayMode === DisplayModeType.displayAsList ? ds.viewSpecification : null, 0); + var element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(dump)); + element.setAttribute('download', currentNodeId + ".txt"); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + } + }); } return ( diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts index 239a8e448..bdef64cf2 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts @@ -51,8 +51,10 @@ type CapabilityAnswer = { } class RestService { + public getNetworkElementUri = (nodeId: string) => '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; + public async getCapabilitiesByMoutId(nodeId: string): Promise<CapabilityAnswer> { - const path = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}`; + const path = this.getNetworkElementUri(nodeId); const capabilitiesResult = await requestRest<CapabilityResponse>(path, { method: "GET" }); const avaliableCapabilities = capabilitiesResult && capabilitiesResult["network-topology:node"] && capabilitiesResult["network-topology:node"].length > 0 && capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"] && diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx index dbaa77874..e466dbacc 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -16,7 +16,7 @@ * ============LICENSE_END========================================================================== */ -import React from 'react'; +import React, { useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; @@ -31,6 +31,8 @@ import { DisplayModeType } from '../handlers/viewDescriptionHandler'; import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator, updateViewActionAsyncCreator, removeElementActionAsyncCreator, executeRpcActionAsyncCreator } from "../actions/deviceActions"; import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion, isViewElementRpc, ViewElementRpc, isViewElementEmpty, isViewElementDate } from "../models/uiModels"; +import { getAccessPolicyByUrl } from "../../../../framework/src/services/restService"; + import Fab from '@material-ui/core/Fab'; import AddIcon from '@material-ui/icons/Add'; import PostAdd from '@material-ui/icons/PostAdd'; @@ -65,106 +67,107 @@ import { UIElementUnion } from '../components/uiElementUnion'; import { UiElementLeafList } from '../components/uiElementLeafList'; import { useConfirm } from 'material-ui-confirm'; +import restService from '../services/restServices'; const styles = (theme: Theme) => createStyles({ - header: { - "display": "flex", - "justifyContent": "space-between", - }, - leftButton: { - "justifyContent": "left" - }, - outer: { - "flex": "1", - "height": "100%", - "display": "flex", - "alignItems": "center", - "justifyContent": "center", - }, - inner: { - - }, - container: { - "height": "100%", - "display": "flex", - "flexDirection": "column", - }, - "icon": { - "marginRight": theme.spacing(0.5), - "width": 20, - "height": 20, - }, - "fab": { - "margin": theme.spacing(1), + header: { + "display": "flex", + "justifyContent": "space-between", + }, + leftButton: { + "justifyContent": "left" + }, + outer: { + "flex": "1", + "height": "100%", + "display": "flex", + "alignItems": "center", + "justifyContent": "center", + }, + inner: { + + }, + container: { + "height": "100%", + "display": "flex", + "flexDirection": "column", + }, + "icon": { + "marginRight": theme.spacing(0.5), + "width": 20, + "height": 20, + }, + "fab": { + "margin": theme.spacing(1), + }, + button: { + margin: 0, + padding: "6px 6px", + minWidth: 'unset' + }, + readOnly: { + '& label.Mui-focused': { + color: 'green', }, - button: { - margin: 0, - padding: "6px 6px", - minWidth: 'unset' + '& .MuiInput-underline:after': { + borderBottomColor: 'green', }, - readOnly: { - '& label.Mui-focused': { - color: 'green', - }, - '& .MuiInput-underline:after': { - borderBottomColor: 'green', - }, - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: 'red', - }, - '&:hover fieldset': { - borderColor: 'yellow', - }, - '&.Mui-focused fieldset': { - borderColor: 'green', - }, - }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'red', + }, + '&:hover fieldset': { + borderColor: 'yellow', + }, + '&.Mui-focused fieldset': { + borderColor: 'green', + }, }, - uiView: { - overflowY: "auto", - }, - section: { - padding: "15px", - borderBottom: `2px solid ${theme.palette.divider}`, - }, - viewElements: { - width: 485, marginLeft: 20, marginRight: 20 - }, - verificationElements: { - width: 485, marginLeft: 20, marginRight: 20 - }, - heading: { - fontSize: theme.typography.pxToRem(15), - fontWeight: theme.typography.fontWeightRegular, - }, - moduleCollection: { - marginTop: "16px", - overflow: "auto", - }, - objectReult: { - overflow: "auto" - } + }, + uiView: { + overflowY: "auto", + }, + section: { + padding: "15px", + borderBottom: `2px solid ${theme.palette.divider}`, + }, + viewElements: { + width: 485, marginLeft: 20, marginRight: 20 + }, + verificationElements: { + width: 485, marginLeft: 20, marginRight: 20 + }, + heading: { + fontSize: theme.typography.pxToRem(15), + fontWeight: theme.typography.fontWeightRegular, + }, + moduleCollection: { + marginTop: "16px", + overflow: "auto", + }, + objectReult: { + overflow: "auto" + } }); const mapProps = (state: IApplicationStoreState) => ({ - collectingData: state.configuration.valueSelector.collectingData, - listKeyProperty: state.configuration.valueSelector.keyProperty, - listSpecification: state.configuration.valueSelector.listSpecification, - listData: state.configuration.valueSelector.listData, - vPath: state.configuration.viewDescription.vPath, - nodeId: state.configuration.deviceDescription.nodeId, - viewData: state.configuration.viewDescription.viewData, - outputData: state.configuration.viewDescription.outputData, - displaySpecification: state.configuration.viewDescription.displaySpecification, + collectingData: state.configuration.valueSelector.collectingData, + listKeyProperty: state.configuration.valueSelector.keyProperty, + listSpecification: state.configuration.valueSelector.listSpecification, + listData: state.configuration.valueSelector.listData, + vPath: state.configuration.viewDescription.vPath, + nodeId: state.configuration.deviceDescription.nodeId, + viewData: state.configuration.viewDescription.viewData, + outputData: state.configuration.viewDescription.outputData, + displaySpecification: state.configuration.viewDescription.displaySpecification, }); const mapDispatch = (dispatcher: IDispatcher) => ({ - onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), - onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)), - reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), - removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)), - executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)), + onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), + onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)), + reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), + removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)), + executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)), }); const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>; @@ -172,661 +175,694 @@ const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>; type ConfigurationApplicationComponentState = { - isNew: boolean; - editMode: boolean; - canEdit: boolean; - viewData: { [key: string]: any } | null; - choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; + isNew: boolean; + editMode: boolean; + canEdit: boolean; + viewData: { [key: string]: any } | null; + choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; } +type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any +const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => { + const [disabled, setDisabled] = useState(true); + const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => { + if (ev.button === 1) { + setDisabled(!disabled); + ev.preventDefault(); + } + }; + return ( + <div onMouseDown={onMouseDown} > + <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} /> + </div> + ); +}; + const OldProps = Symbol("OldProps"); class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> { - /** - * - */ - constructor(props: ConfigurationApplicationComponentProps) { - super(props); - - this.state = { - isNew: false, - canEdit: false, - editMode: false, - viewData: null, - choises: {}, - } + /** + * + */ + constructor(props: ConfigurationApplicationComponentProps) { + super(props); + + this.state = { + isNew: false, + canEdit: false, + editMode: false, + viewData: null, + choises: {}, } - - private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { - return Object.keys(elements).reduce((acc, cur) => { - const elm = elements[cur]; - if (isViewElementChoise(elm)) { - const caseKeys = Object.keys(elm.cases); - - // find the right case for this choise, use the first one with data, at least use index 0 - const selectedCase = caseKeys.find(key => { - const caseElm = elm.cases[key]; - return Object.keys(caseElm.elements).some(caseElmKey => { - const caseElmElm = caseElm.elements[caseElmKey]; - return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined; - }); - }) || caseKeys[0]; - - // extract all data of the active case - const caseElements = elm.cases[selectedCase].elements; - const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => { - const dataElm = caseElements[dataCur]; - if (isViewElementEmpty(dataElm)) { - dataAcc[dataElm.label] = null; - } else if (viewData[dataElm.label] !== undefined) { - dataAcc[dataElm.label] = viewData[dataElm.label]; - } else if (viewData[dataElm.id] !== undefined) { - dataAcc[dataElm.id] = viewData[dataElm.id]; - } - return dataAcc; - }, {} as { [name: string]: any }); - - acc[elm.id] = { - selectedCase, - data, - }; - } - return acc; - }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {} + } + + private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { + return Object.keys(elements).reduce((acc, cur) => { + const elm = elements[cur]; + if (isViewElementChoise(elm)) { + const caseKeys = Object.keys(elm.cases); + + // find the right case for this choise, use the first one with data, at least use index 0 + const selectedCase = caseKeys.find(key => { + const caseElm = elm.cases[key]; + return Object.keys(caseElm.elements).some(caseElmKey => { + const caseElmElm = caseElm.elements[caseElmKey]; + return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined; + }); + }) || caseKeys[0]; + + // extract all data of the active case + const caseElements = elm.cases[selectedCase].elements; + const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => { + const dataElm = caseElements[dataCur]; + if (isViewElementEmpty(dataElm)) { + dataAcc[dataElm.label] = null; + } else if (viewData[dataElm.label] !== undefined) { + dataAcc[dataElm.label] = viewData[dataElm.label]; + } else if (viewData[dataElm.id] !== undefined) { + dataAcc[dataElm.id] = viewData[dataElm.id]; + } + return dataAcc; + }, {} as { [name: string]: any }); + + acc[elm.id] = { + selectedCase, + data, + }; + } + return acc; + }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {} + } + + static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) { + + if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) { + const isNew: boolean = nextProps.vPath?.endsWith("[]") || false; + const state = { + ...prevState, + isNew: isNew, + editMode: isNew, + viewData: nextProps.viewData || null, + [OldProps]: nextProps, + choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay + ? null + : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC + ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || [] + : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData) + } + return state; } - - static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) { - - if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) { - const isNew: boolean = nextProps.vPath?.endsWith("[]") || false; - const state = { - ...prevState, - isNew: isNew, - editMode: isNew, - viewData: nextProps.viewData || null, - [OldProps]: nextProps, - choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay - ? null - : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC - ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || [] - : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData) + return null; + } + + private navigate = (path: string) => { + this.props.history.push(`${this.props.match.url}${path}`); + } + + private changeValueFor = (property: string, value: any) => { + this.setState({ + viewData: { + ...this.state.viewData, + [property]: value + } + }); + } + + private collectData = (elements: { [name: string]: ViewElement }) => { + // ensure only active choises will be contained + const viewData: { [key: string]: any } = { ...this.state.viewData }; + const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey])); + const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => { + const currentChoice = elements[curChoiceKey] as ViewElementChoise; + const selectedCase = this.state.choises[curChoiceKey].selectedCase; + Object.keys(currentChoice.cases).forEach(caseKey => { + const caseElements = currentChoice.cases[caseKey].elements; + if (caseKey === selectedCase) { + Object.keys(caseElements).forEach(caseElementKey => { + const elm = caseElements[caseElementKey]; + if (isViewElementEmpty(elm)) { + // insert null for all empty elements + viewData[elm.id] = null; } - return state; - } - return null; + }); + return; + }; + Object.keys(caseElements).forEach(caseElementKey => { + acc.push(caseElements[caseElementKey]); + }); + }); + return acc; + }, [] as ViewElement[]); + + return viewData && Object.keys(viewData).reduce((acc, cur) => { + if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) { + acc[cur] = viewData[cur]; + } + return acc; + }, {} as { [key: string]: any }); + } + + private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => { + const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`); + return !(policy.GET && policy.POST); + } + + private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => { + const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`); + return !(policy.GET && policy.POST); + } + + private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => { + if (isViewElementEmpty(uiElement)) { + return null; + } else if (isViewElementSelection(uiElement)) { + return UiElementSelection; + } else if (isViewElementBoolean(uiElement)) { + return UiElementBoolean; + } else if (isViewElementString(uiElement)) { + return UiElementString; + } else if (isViewElementDate(uiElement)) { + return UiElementString; + } else if (isViewElementNumber(uiElement)) { + return UiElementNumber; + } else if (isViewElementUnion(uiElement)) { + return UIElementUnion; + } else { + if (process.env.NODE_ENV !== "production") { + console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + } + return null; } - - private navigate = (path: string) => { - this.props.history.push(`${this.props.match.url}${path}`); + } + + private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const isKey = (uiElement.label === keyProperty); + const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + + // do not show elements w/o any value from the backend + if (viewData[uiElement.id] == null && !editMode) { + return null; + } else if (isViewElementEmpty(uiElement)) { + return null; + } else if (uiElement.isList) { + /* element is a leaf-list */ + return <UiElementLeafList + key={uiElement.id} + inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]} + value={uiElement} + readOnly={!canEdit} + disabled={editMode && !canEdit} + onChange={(e) => { this.changeValueFor(uiElement.id, e) }} + getEditorForViewElement={this.getEditorForViewElement} + />; + } else { + const Element = this.getEditorForViewElement(uiElement); + return Element != null + ? ( + <Element + key={uiElement.id} + isKey={isKey} + inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]} + value={uiElement} + readOnly={!canEdit} + disabled={editMode && !canEdit} + onChange={(e) => { this.changeValueFor(uiElement.id, e) }} + />) + : null; } - - private changeValueFor = (property: string, value: any) => { - this.setState({ - viewData: { - ...this.state.viewData, - [property]: value - } - }); + }; + + // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + // const isKey = (uiElement.label === keyProperty); + // const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + // if (isViewElementObjectOrList(uiElement)) { + // return ( + // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + // <Tooltip title={uiElement.description || ''}> + // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => { + // this.navigate(`/${uiElement.id}`); + // }}>{uiElement.label}</Button> + // </Tooltip> + // </FormControl> + // ); + // } else { + // if (process.env.NODE_ENV !== "production") { + // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + // } + // return null; + // } + // }; + + private renderUIChoise = (uiElement: ViewElementChoise, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const isKey = (uiElement.label === keyProperty); + + const currentChoise = this.state.choises[uiElement.id]; + const currentCase = currentChoise && uiElement.cases[currentChoise.selectedCase]; + + const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + if (isViewElementChoise(uiElement)) { + const subElements = currentCase?.elements; + return ( + <> + <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel> + <Select + aria-label={uiElement.label + '-selection'} + required={!!uiElement.mandatory} + onChange={(e) => { + if (currentChoise.selectedCase === e.target.value) { + return; // nothing changed + } + this.setState({ choises: { ...this.state.choises, [uiElement.id]: { ...this.state.choises[uiElement.id], selectedCase: e.target.value as string } } }); + }} + readOnly={!canEdit} + disabled={editMode && !canEdit} + value={this.state.choises[uiElement.id].selectedCase} + inputProps={{ + name: uiElement.id, + id: `select-${uiElement.id}`, + }} + > + { + Object.keys(uiElement.cases).map(caseKey => { + const caseElm = uiElement.cases[caseKey]; + return ( + <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem> + ); + }) + } + </Select> + </FormControl> + {subElements + ? Object.keys(subElements).map(elmKey => { + const elm = subElements[elmKey]; + return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); + }) + : <h3>Invalid Choise</h3> + } + </> + ); + } else { + if (process.env.NODE_ENV !== "production") { + console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + } + return null; } + }; - private collectData = (elements: { [name: string]: ViewElement }) => { - // ensure only active choises will be contained - const viewData: { [key: string]: any } = { ...this.state.viewData }; - const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey])); - const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => { - const currentChoice = elements[curChoiceKey] as ViewElementChoise; - const selectedCase = this.state.choises[curChoiceKey].selectedCase; - Object.keys(currentChoice.cases).forEach(caseKey => { - const caseElements = currentChoice.cases[caseKey].elements; - if (caseKey === selectedCase) { - Object.keys(caseElements).forEach(caseElementKey => { - const elm = caseElements[caseElementKey]; - if (isViewElementEmpty(elm)) { - // insert null for all empty elements - viewData[elm.id] = null; - } - }); - return; - }; - Object.keys(caseElements).forEach(caseElementKey => { - acc.push(caseElements[caseElementKey]); - }); - }); - return acc; - }, [] as ViewElement[]); + private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const { classes } = this.props; - return viewData && Object.keys(viewData).reduce((acc, cur) => { - if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) { - acc[cur] = viewData[cur]; - } - return acc; - }, {} as { [key: string]: any }); - } - private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => { - if (isViewElementEmpty(uiElement)) { - return null; - } else if (isViewElementSelection(uiElement)) { - return UiElementSelection; - } else if (isViewElementBoolean(uiElement)) { - return UiElementBoolean; - } else if (isViewElementString(uiElement)) { - return UiElementString; - } else if (isViewElementDate(uiElement)) { - return UiElementString; - } else if (isViewElementNumber(uiElement)) { - return UiElementNumber; - } else if (isViewElementUnion(uiElement)) { - return UIElementUnion; - } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - } - return null; - } - } - private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const isKey = (uiElement.label === keyProperty); - const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - - // do not show elements w/o any value from the backend - if (viewData[uiElement.id] == null && !editMode) { - return null; - } else if (isViewElementEmpty(uiElement)) { - return null; - } else if (uiElement.isList) { - /* element is a leaf-list */ - return <UiElementLeafList - key={uiElement.id} - inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]} - value={uiElement} - readOnly={!canEdit} - disabled={editMode && !canEdit} - onChange={(e) => { this.changeValueFor(uiElement.id, e) }} - getEditorForViewElement={this.getEditorForViewElement} - />; - } else { - const Element = this.getEditorForViewElement(uiElement); - return Element != null - ? ( - <Element - key={uiElement.id} - isKey={isKey} - inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]} - value={uiElement} - readOnly={!canEdit} - disabled={editMode && !canEdit} - onChange={(e) => { this.changeValueFor(uiElement.id, e) }} - />) - : null; - } + const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { + if (keyProperty) { + // if (vsA.label === vsB.label) return 0; + if (vsA.label === keyProperty) return -1; + if (vsB.label === keyProperty) return +1; + } + + // if (vsA.uiType === vsB.uiType) return 0; + // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; + // if (vsA.uiType === "object") return +1; + return -1; }; - // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - // const isKey = (uiElement.label === keyProperty); - // const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - // if (isViewElementObjectOrList(uiElement)) { - // return ( - // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> - // <Tooltip title={uiElement.description || ''}> - // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => { - // this.navigate(`/${uiElement.id}`); - // }}>{uiElement.label}</Button> - // </Tooltip> - // </FormControl> - // ); - // } else { - // if (process.env.NODE_ENV !== "production") { - // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - // } - // return null; - // } - // }; - - private renderUIChoise = (uiElement: ViewElementChoise, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const isKey = (uiElement.label === keyProperty); - - const currentChoise = this.state.choises[uiElement.id]; - const currentCase = currentChoise && uiElement.cases[currentChoise.selectedCase]; - - const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - if (isViewElementChoise(uiElement)) { - const subElements = currentCase?.elements; + const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => { + const elm = viewSpecification.elements[cur]; + if (isViewElementObjectOrList(elm)) { + acc.references.push(elm); + } else if (isViewElementChoise(elm)) { + acc.choises.push(elm); + } else if (isViewElementRpc(elm)) { + acc.rpcs.push(elm); + } else { + acc.elements.push(elm); + } + return acc; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }); + + sections.elements = sections.elements.sort(orderFunc); + + return ( + <div className={classes.uiView}> + <div className={classes.section} /> + {sections.elements.length > 0 + ? ( + <div className={classes.section}> + {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + {sections.references.length > 0 + ? ( + <div className={classes.section}> + {sections.references.map(element => ( + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> + ))} + </div> + ) : null + } + {sections.choises.length > 0 + ? ( + <div className={classes.section}> + {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + {sections.rpcs.length > 0 + ? ( + <div className={classes.section}> + {sections.rpcs.map(element => ( + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> + ))} + </div> + ) : null + } + </div> + ); + }; + + private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const { classes } = this.props; + // group by module name + const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => { + const elm = viewSpecification.elements[cur]; + const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} }); + moduleView.elements[cur] = elm; + return acc; + }, {}); + + const moduleKeys = Object.keys(modules).sort(); + + return ( + <div className={classes.moduleCollection}> + { + moduleKeys.map(key => { + const moduleView = modules[key]; return ( - <> - <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> - <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel> - <Select - aria-label={uiElement.label+'-selection'} - required={!!uiElement.mandatory} - onChange={(e) => { - if (currentChoise.selectedCase === e.target.value) { - return; // nothing changed - } - this.setState({ choises: { ...this.state.choises, [uiElement.id]: { ...this.state.choises[uiElement.id], selectedCase: e.target.value as string } } }); - }} - readOnly={!canEdit} - disabled={editMode && !canEdit} - value={this.state.choises[uiElement.id].selectedCase} - inputProps={{ - name: uiElement.id, - id: `select-${uiElement.id}`, - }} - > - { - Object.keys(uiElement.cases).map(caseKey => { - const caseElm = uiElement.cases[caseKey]; - return ( - <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem> - ); - }) - } - </Select> - </FormControl> - {subElements - ? Object.keys(subElements).map(elmKey => { - const elm = subElements[elmKey]; - return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); - }) - : <h3>Invalid Choise</h3> - } - </> + <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} > + <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} > + <Typography className={classes.heading}>{key}</Typography> + </AccordionSummaryExt> + <AccordionDetails> + {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)} + </AccordionDetails> + </Accordion> ); - } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - } - return null; + }) } + </div> + ); + }; + + private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) { + const listElements = listSpecification.elements; + const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath + .replace("$$$standard$$$", "topology-netconfnode%20resources%20-%20RestConf%20RFC%208040") + .replace("$$$action$$$", "put")}_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : undefined; + + const navigate = (path: string) => { + this.props.history.push(`${this.props.match.url}${path}`); }; - private renderUIView = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const { classes } = this.props; - - const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { - if (keyProperty) { - // if (vsA.label === vsB.label) return 0; - if (vsA.label === keyProperty) return -1; - if (vsB.label === keyProperty) return +1; - } - - // if (vsA.uiType === vsB.uiType) return 0; - // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; - // if (vsA.uiType === "object") return +1; - return -1; - }; - - const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => { - const elm = viewSpecification.elements[cur]; - if (isViewElementObjectOrList(elm)) { - acc.references.push(elm); - } else if (isViewElementChoise(elm)) { - acc.choises.push(elm); - } else if (isViewElementRpc(elm)) { - acc.rpcs.push(elm); - } else { - acc.elements.push(elm); - } - return acc; - }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }); - - sections.elements = sections.elements.sort(orderFunc); - - return ( - <div className={classes.uiView}> - <div className={classes.section} /> - {sections.elements.length > 0 - ? ( - <div className={classes.section}> - {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - {sections.references.length > 0 - ? ( - <div className={classes.section}> - {sections.references.map(element => ( - <UIElementReference key={element.id} element={element} disabled={editMode} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> - ))} - </div> - ) : null - } - {sections.choises.length > 0 - ? ( - <div className={classes.section}> - {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - {sections.rpcs.length > 0 - ? ( - <div className={classes.section}> - {sections.rpcs.map(element => ( - <UIElementReference key={element.id} element={element} disabled={editMode} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> - ))} - </div> - ) : null - } - </div> - ); + const addNewElementAction = { + icon: AddIcon, + tooltip: 'Add', + onClick: () => { + navigate("[]"); // empty key means new element + }, + disabled: !listSpecification.config, }; - private renderUIViewSelector = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const { classes } = this.props; - - // group by module name - const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => { - const elm = viewSpecification.elements[cur]; - const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} }); - moduleView.elements[cur] = elm; - return acc; - }, {}); - - const moduleKeys = Object.keys(modules).sort(); - - return ( - <div className={classes.moduleCollection}> - { - moduleKeys.map(key => { - const moduleView = modules[key]; - return ( - <Accordion key={key} defaultExpanded={ moduleKeys.length < 4 } aria-label={key+'-panel'} > - <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} > - <Typography className={classes.heading}>{key}</Typography> - </AccordionSummary> - <AccordionDetails> - {this.renderUIView(moduleView, viewData, keyProperty, editMode, isNew)} - </AccordionDetails> - </Accordion> - ); - }) - } - </div> - ); + const addWithApiDocElementAction = { + icon: PostAdd, + tooltip: 'Add', + onClick: () => { + window.open(apiDocPathCreate, '_blank'); + }, + disabled: !listSpecification.config, }; - private renderUIViewList(listSpecification: ViewSpecification, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) { - const listElements = listSpecification.elements; - const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath - .replace("$$$standard$$$","topology-netconfnode%20resources%20-%20RestConf%20RFC%208040") - .replace("$$$action$$$","put")}_${listKeyProperty.replace(/[\/=\-\:]/g,'_')}_` : undefined; - - const navigate = (path: string) => { - this.props.history.push(`${this.props.match.url}${path}`); - }; - - const addNewElementAction = { - icon: AddIcon, tooltip: 'Add', onClick: () => { - navigate("[]"); // empty key means new element - } - }; - - const addWithApiDocElementAction = { - icon: PostAdd, tooltip: 'Add', onClick: () => { - window.open(apiDocPathCreate, '_blank'); - } - }; - - const { classes, removeElement } = this.props; - - const DeleteIconWithConfirmation: React.FC<{ rowData: { [key: string]: any }, onReload: () => void }> = (props) => { - const confirm = useConfirm(); - - return ( - <Tooltip title={"Remove"} > - <IconButton className={classes.button} aria-label="remove-element-button" - onClick={async (e) => { - e.stopPropagation(); - e.preventDefault(); - confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" } }) - .then(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`)) - .then(props.onReload); - }} > - <RemoveIcon /> - </IconButton> - </Tooltip> - ); - } - - return ( - <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={ - Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { - const elm = listElements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { - if (elm.label !== listKeyProperty) { - acc.push(elm.uiType === "boolean" - ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" - ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } else { - acc.unshift(elm.uiType === "boolean" - ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" - ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } - } - return acc; - }, []).concat([{ - property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { - return ( - <DeleteIconWithConfirmation rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} /> - ); - }) - }]) - } onHandleClick={(ev, row) => { - ev.preventDefault(); - navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); - }} ></SelectElementTable> - ); + const { classes, removeElement } = this.props; + + const DeleteIconWithConfirmation: React.FC<{disabled?: boolean, rowData: { [key: string]: any }, onReload: () => void }> = (props) => { + const confirm = useConfirm(); + + return ( + <Tooltip title={"Remove"} > + <IconButton disabled={props.disabled} className={classes.button} aria-label="remove-element-button" + onClick={async (e) => { + e.stopPropagation(); + e.preventDefault(); + confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" } }) + .then(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`)) + .then(props.onReload); + }} > + <RemoveIcon /> + </IconButton> + </Tooltip> + ); } - private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) { - const { classes } = this.props; - - const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { - if (keyProperty) { - // if (vsA.label === vsB.label) return 0; - if (vsA.label === keyProperty) return -1; - if (vsB.label === keyProperty) return +1; - } - - // if (vsA.uiType === vsB.uiType) return 0; - // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; - // if (vsA.uiType === "object") return +1; - return -1; - }; - - const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => { - const elm = inputViewSpecification.elements[cur]; - if (isViewElementObjectOrList(elm)) { - console.error("Object should not appear in RPC view !"); - } else if (isViewElementChoise(elm)) { - acc.choises.push(elm); - } else if (isViewElementRpc(elm)) { - console.error("RPC should not appear in RPC view !"); + return ( + <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={ + Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { + const elm = listElements[cur]; + if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.label !== listKeyProperty) { + acc.push(elm.uiType === "boolean" + ? { property: elm.label, type: ColumnType.boolean } + : elm.uiType === "date" + ? { property: elm.label, type: ColumnType.date } + : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); } else { - acc.elements.push(elm); + acc.unshift(elm.uiType === "boolean" + ? { property: elm.label, type: ColumnType.boolean } + : elm.uiType === "date" + ? { property: elm.label, type: ColumnType.date } + : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); } - return acc; - }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }) - || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }; - - sections.elements = sections.elements.sort(orderFunc); - - return ( - <> - <div className={classes.section} /> - { sections.elements.length > 0 - ? ( - <div className={classes.section}> - {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - { sections.choises.length > 0 - ? ( - <div className={classes.section}> - {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - <Button onClick={() => { - const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements); - this.props.executeRpc(this.props.vPath!, resultingViewData); - }} >Exec</Button> - <div className={classes.objectReult}> - { outputViewData !== undefined - ? renderObject(outputViewData) - : null - } - </div> - </> - ); + } + return acc; + }, []).concat([{ + property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { + return ( + <DeleteIconWithConfirmation disabled={!listSpecification.config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} /> + ); + }) + }]) + } onHandleClick={(ev, row) => { + ev.preventDefault(); + navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); + }} ></SelectElementTable> + ); + } + + private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) { + const { classes } = this.props; + + const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { + if (keyProperty) { + // if (vsA.label === vsB.label) return 0; + if (vsA.label === keyProperty) return -1; + if (vsB.label === keyProperty) return +1; + } + + // if (vsA.uiType === vsB.uiType) return 0; + // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; + // if (vsA.uiType === "object") return +1; + return -1; }; - private renderBreadCrumps() { - const { editMode } = this.state; - const { displaySpecification, vPath, nodeId } = this.props; - const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - let lastPath = `/configuration`; - let basePath = `/configuration/${nodeId}`; - return ( - <div className={this.props.classes.header}> - <div> - <Breadcrumbs aria-label="breadcrumbs"> - <Link color="inherit" href="#" aria-label="back-breadcrumb" - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(lastPath); - }}>Back</Link> - <Link color="inherit" href="#" - aria-label={nodeId+'-breadcrumb'} - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(`/configuration/${nodeId}`); - }}><span>{nodeId}</span></Link> - { - pathParts.map(([prop, key], ind) => { - const path = `${basePath}/${prop}`; - const keyPath = key && `${basePath}/${prop}[${key}]`; - const propTitle= prop.replace(/^[^:]+:/, ""); - const ret = ( - <span key={ind}> - <Link color="inherit" href="#" - aria-label={propTitle+'-breadcrumb'} - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(path); - }}><span>{propTitle}</span></Link> - { - keyPath && <Link color="inherit" href="#" - aria-label={key+'-breadcrumb'} - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(keyPath); - }}>{`[${key}]`}</Link> || null - } - </span> - ); - lastPath = basePath; - basePath = keyPath || path; - return ret; - }) - } - </Breadcrumbs> - </div> - {this.state.editMode && ( - <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => { - this.props.vPath && await this.props.reloadView(this.props.vPath); - this.setState({ editMode: false }); - }} ><ArrowBack /></Fab> - ) || null} - { /* do not show edit if this is a list or it can't be edited */ - displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div> - <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => { - if (this.state.editMode) { - // ensure only active choises will be contained - const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); - this.props.onUpdateData(this.props.vPath!, resultingViewData); - } - this.setState({ editMode: !editMode }); - }}> - {editMode - ? <SaveIcon /> - : <EditIcon /> - } - </Fab> - </div> || null) - } + const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => { + const elm = inputViewSpecification.elements[cur]; + if (isViewElementObjectOrList(elm)) { + console.error("Object should not appear in RPC view !"); + } else if (isViewElementChoise(elm)) { + acc.choises.push(elm); + } else if (isViewElementRpc(elm)) { + console.error("RPC should not appear in RPC view !"); + } else { + acc.elements.push(elm); + } + return acc; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }) + || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }; + + sections.elements = sections.elements.sort(orderFunc); + + return ( + <> + <div className={classes.section} /> + { sections.elements.length > 0 + ? ( + <div className={classes.section}> + {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} </div> - ); - } - - private renderValueSelector() { - const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; - if (!listKeyProperty || !listSpecification) { - throw new Error("ListKex ot view not specified."); + ) : null } - - return ( - <div className={this.props.classes.container}> - <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={ - Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { - const elm = listSpecification.elements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { - if (elm.label !== listKeyProperty) { - acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } else { - acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } - } - return acc; - }, []) - } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable> + { sections.choises.length > 0 + ? ( + <div className={classes.section}> + {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} </div> - ); - } - - private renderValueEditor() { - const { displaySpecification: ds, outputData } = this.props; - const { viewData, editMode, isNew } = this.state; - - return ( - <div className={this.props.classes.container}> - {this.renderBreadCrumps()} - {ds.displayMode === DisplayModeType.doNotDisplay - ? null - : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array - ? this.renderUIViewList(ds.viewSpecification, ds.keyProperty!, ds.apidocPath!, viewData) - : ds.displayMode === DisplayModeType.displayAsRPC - ? this.renderUIViewRPC(ds.inputViewSpecification, viewData!, outputData, undefined, true, false) - : this.renderUIViewSelector(ds.viewSpecification, viewData!, ds.keyProperty, editMode, isNew) - } - </div > - ); - } - - private renderCollectingData() { - return ( - <div className={this.props.classes.outer}> - <div className={this.props.classes.inner}> - <Loader /> - <h3>Processing ...</h3> - </div> - </div> - ); + ) : null + } + <Button onClick={() => { + const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements); + this.props.executeRpc(this.props.vPath!, resultingViewData); + }} >Exec</Button> + <div className={classes.objectReult}> + {outputViewData !== undefined + ? renderObject(outputViewData) + : null + } + </div> + </> + ); + }; + + private renderBreadCrumps() { + const { editMode } = this.state; + const { displaySpecification, vPath, nodeId } = this.props; + const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key + let lastPath = `/configuration`; + let basePath = `/configuration/${nodeId}`; + return ( + <div className={this.props.classes.header}> + <div> + <Breadcrumbs aria-label="breadcrumbs"> + <Link color="inherit" href="#" aria-label="back-breadcrumb" + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(lastPath); + }}>Back</Link> + <Link color="inherit" href="#" + aria-label={nodeId + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(`/configuration/${nodeId}`); + }}><span>{nodeId}</span></Link> + { + pathParts.map(([prop, key], ind) => { + const path = `${basePath}/${prop}`; + const keyPath = key && `${basePath}/${prop}[${key}]`; + const propTitle = prop.replace(/^[^:]+:/, ""); + const ret = ( + <span key={ind}> + <Link color="inherit" href="#" + aria-label={propTitle + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(path); + }}><span>{propTitle}</span></Link> + { + keyPath && <Link color="inherit" href="#" + aria-label={key + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(keyPath); + }}>{`[${key}]`}</Link> || null + } + </span> + ); + lastPath = basePath; + basePath = keyPath || path; + return ret; + }) + } + </Breadcrumbs> + </div> + {this.state.editMode && ( + <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => { + this.props.vPath && await this.props.reloadView(this.props.vPath); + this.setState({ editMode: false }); + }} ><ArrowBack /></Fab> + ) || null} + { /* do not show edit if this is a list or it can't be edited */ + displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div> + <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => { + if (this.state.editMode) { + // ensure only active choises will be contained + const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); + this.props.onUpdateData(this.props.vPath!, resultingViewData); + } + this.setState({ editMode: !editMode }); + }}> + {editMode + ? <SaveIcon /> + : <EditIcon /> + } + </Fab> + </div> || null) + } + </div> + ); + } + + private renderValueSelector() { + const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; + if (!listKeyProperty || !listSpecification) { + throw new Error("ListKex ot view not specified."); } - render() { - return this.props.collectingData || !this.state.viewData - ? this.renderCollectingData() - : this.props.listSpecification - ? this.renderValueSelector() - : this.renderValueEditor(); - } + return ( + <div className={this.props.classes.container}> + <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={ + Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { + const elm = listSpecification.elements[cur]; + if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.label !== listKeyProperty) { + acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + } else { + acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + } + } + return acc; + }, []) + } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable> + </div> + ); + } + + private renderValueEditor() { + const { displaySpecification: ds, outputData } = this.props; + const { viewData, editMode, isNew } = this.state; + + return ( + <div className={this.props.classes.container}> + {this.renderBreadCrumps()} + {ds.displayMode === DisplayModeType.doNotDisplay + ? null + : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array + ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData) + : ds.displayMode === DisplayModeType.displayAsRPC + ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false) + : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew) + } + </div > + ); + } + + private renderCollectingData() { + return ( + <div className={this.props.classes.outer}> + <div className={this.props.classes.inner}> + <Loader /> + <h3>Processing ...</h3> + </div> + </div> + ); + } + + render() { + return this.props.collectingData || !this.state.viewData + ? this.renderCollectingData() + : this.props.listSpecification + ? this.renderValueSelector() + : this.renderValueEditor(); + } } export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent))); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx index 5d849986d..5cac22eba 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx @@ -25,7 +25,7 @@ import { MaterialTable, MaterialTableCtorType, ColumnType } from "../../../../fr import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from "../../../configurationApp/src/handlers/connectedNetworkElementsHandler"; import { NetworkElementConnection } from "../models/networkElementConnection"; -import { Tooltip, Button, IconButton } from "@material-ui/core"; + const mapProps = (state: IApplicationStoreState) => ({ connectedNetworkElementsProperties: createConnectedNetworkElementsProperties(state), diff --git a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts index c5cb8fb4c..2d38976d5 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts @@ -278,6 +278,7 @@ export class YangParser { name: "root", language: "en-US", canEdit: false, + config: true, parentView: "0", title: "root", elements: {}, @@ -544,6 +545,28 @@ export class YangParser { console.warn(error.message); } }); + + // resolve readOnly + const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => { + + // update view config + view.config = view.config && parentConfig; + + Object.keys(view.elements).forEach((key) => { + const elm = view.elements[key]; + + // update element config + elm.config = elm.config && view.config; + + // update all sub-elements of objects + if (elm.uiType === "object") { + resolveReadOnly(this.views[+elm.viewId], elm.config); + } + + }) + } + + const dump = resolveReadOnly(this.views[0], true); }; private _nextId = 1; @@ -686,7 +709,7 @@ export class YangParser { module: context.name || module.name || '', uiType: "object", viewId: currentView.id, - config: config + config: currentView.config, }); acc.push(currentView, ...subViews); return acc; @@ -717,7 +740,7 @@ export class YangParser { uiType: "object", viewId: currentView.id, key: key, - config: elmConfig + config: elmConfig && currentView.config, }); acc.push(currentView, ...subViews); return acc; @@ -876,6 +899,7 @@ export class YangParser { title: statement.arg != null ? statement.arg : undefined, language: "en-us", canEdit: false, + config: config, ifFeature: ifFeature, when: whenCondition, elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => { @@ -921,7 +945,7 @@ export class YangParser { const elm = groupingViewSpec.elements[key]; // a useRef on root level need a namespace viewSpec.elements[parentId === 0 ? `${module.name}:${key}` : key] = { - ...groupingViewSpec.elements[key], + ...elm, when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when, ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature, }; diff --git a/sdnr/wt/odlux/apps/connectApp/package.json b/sdnr/wt/odlux/apps/connectApp/package.json index 836af66ee..4fae39ccb 100644 --- a/sdnr/wt/odlux/apps/connectApp/package.json +++ b/sdnr/wt/odlux/apps/connectApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/policies.json b/sdnr/wt/odlux/apps/connectApp/policies.json new file mode 100644 index 000000000..2ec73612e --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/policies.json @@ -0,0 +1,12 @@ +[ + { + "path": "/rests/**/node=Sim2230**", + "methods": { + "get": true, + "post": false, + "put": false, + "delete": false, + "patch": false + } + } +]
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts b/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts index a3bdc6828..a83e00239 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/actions/commonNetworkElementsActions.ts @@ -23,13 +23,13 @@ import { Action } from '../../../../framework/src/flux/action'; import { Dispatch } from '../../../../framework/src/flux/store'; import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; -import { networkElementsReloadAction, networkElementsReloadActionAsync } from '../handlers/networkElementsHandler'; +import { networkElementsReloadAction } from '../handlers/networkElementsHandler'; import { connectionStatusLogReloadAction } from '../handlers/connectionStatusLogHandler'; import { PanelId } from '../models/panelId'; import { guiCutThrough } from '../models/guiCutTrough'; -import connectService from '../services/connectService'; -import { NetworkElementConnection } from '../models/networkElementConnection'; +import { connectService} from '../services/connectService'; + export class SetPanelAction extends Action { constructor(public panelId: PanelId) { @@ -38,7 +38,7 @@ export class SetPanelAction extends Action { } export class AddWebUriList extends Action { - constructor(public searchedElements: guiCutThrough[], public notSearchedElements: string[], public unsupportedElements: string[], public newlySearchedElements?: string[] ) { + constructor(public searchedElements: guiCutThrough[], public notSearchedElements: string[], public unsupportedElements: string[], public newlySearchedElements?: string[]) { super(); } } @@ -60,49 +60,47 @@ export class SetWeburiSearchBusy extends Action { } let isBusy = false; -export const findWebUrisForGuiCutThroughAsyncAction = (networkElements: NetworkElementConnection[]) => async (dispatcher: Dispatch, getState: () => IApplicationStoreState) => { +export const findWebUrisForGuiCutThroughAsyncAction = (networkElementIds: string[]) => async (dispatcher: Dispatch, getState: () => IApplicationStoreState) => { // keep method from executing simultanously; state not used because change of iu isn't needed - if (isBusy) - return; - isBusy = true; - const { connect: { guiCutThrough } } = getState(); + + const { connect: { guiCutThrough, networkElements } } = getState(); let notConnectedElements: string[] = []; let elementsToSearch: string[] = []; let prevFoundElements: string[] = []; - let unsupportedElements: string[]= []; + let unsupportedElements: string[] = []; - - networkElements.forEach(item => { - const id = item.id as string; + networkElementIds.forEach(id => { + const item = networkElements.rows.find((ne) => ne.id === id); + if (item) { if (item.status === "Connected") { - if(item.coreModelCapability!== "Unsupported"){ + // 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.nodeId === id).length > 0; + 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; - const exists = guiCutThrough.unsupportedElements.filter(element => element === id).length > 0; - if(!exists){ - unsupportedElements.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; + // const exists = guiCutThrough.unsupportedElements.filter(element => element === id).length > 0; + // if (!exists) { + // unsupportedElements.push(id); + + // //element was found previously, but wasn't connected + // if (guiCutThrough.notSearchedElements.length > 0 && guiCutThrough.notSearchedElements.includes(id)) { + // prevFoundElements.push(id); + // } + // } + // } } else { // element isn't connected and cannot be searched for a weburi @@ -110,25 +108,25 @@ export const findWebUrisForGuiCutThroughAsyncAction = (networkElements: NetworkE notConnectedElements.push(item.id as string); } } + } }); - - if (elementsToSearch.length > 0 || notConnectedElements.length > 0 || unsupportedElements.length>0 ) { - const result = await connectService.getAllWebUriExtensionsForNetworkElementListAsync(elementsToSearch); - dispatcher(new AddWebUriList(result, notConnectedElements, unsupportedElements, prevFoundElements)); + + if (elementsToSearch.length > 0 || notConnectedElements.length > 0 || unsupportedElements.length > 0) { + const result = await connectService.getAllWebUriExtensionsForNetworkElementListAsync(elementsToSearch); + dispatcher(new AddWebUriList(result, notConnectedElements, unsupportedElements, prevFoundElements)); } - isBusy = false; } export const setPanelAction = (panelId: PanelId) => { return new SetPanelAction(panelId); } -export const updateCurrentViewAsyncAction = () => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { +export const updateCurrentViewAsyncAction = () => (dispatch: Dispatch, getState: () => IApplicationStoreState) => { const { connect: { currentOpenPanel } } = getState(); if (currentOpenPanel === "NetworkElements") { - return await dispatch(networkElementsReloadActionAsync); + return dispatch(networkElementsReloadAction); } else { return dispatch(connectionStatusLogReloadAction); diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx index f2fd2937d..94b4872dd 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/components/connectionStatusLog.tsx @@ -19,9 +19,11 @@ 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 Refresh from '@material-ui/icons/Refresh'; import { createConnectionStatusLogActions, createConnectionStatusLogProperties } from '../handlers/connectionStatusLogHandler'; import { NetworkElementConnectionLog } from '../models/networkElementConnectionLog'; +import RefreshConnectionStatusLogDialog, { RefreshConnectionStatusLogDialogMode } from './refreshConnectionStatusLogDialog'; const mapProps = (state: IApplicationStoreState) => ({ connectionStatusLogProperties: createConnectionStatusLogProperties(state), @@ -34,22 +36,52 @@ const mapDispatch = (dispatcher: IDispatcher) => ({ const ConnectionStatusTable = MaterialTable as MaterialTableCtorType<NetworkElementConnectionLog>; type ConnectionStatusLogComponentProps = Connect<typeof mapProps, typeof mapDispatch>; +type ConnectionStatusLogComponentState = { + refreshConnectionStatusLogEditorMode: RefreshConnectionStatusLogDialogMode +} let initialSorted = false; -class ConnectionStatusLogComponent extends React.Component<ConnectionStatusLogComponentProps> { +class ConnectionStatusLogComponent extends React.Component<ConnectionStatusLogComponentProps,ConnectionStatusLogComponentState > { + constructor(props: ConnectionStatusLogComponentProps) { + super(props); + + this.state = { + refreshConnectionStatusLogEditorMode: RefreshConnectionStatusLogDialogMode.None + }; + } + render(): JSX.Element { + const refreshConnectionStatusLogAction = { + icon: Refresh, tooltip: 'Refresh Connection Status Log Table', onClick: () => { + this.setState({ + refreshConnectionStatusLogEditorMode: RefreshConnectionStatusLogDialogMode.RefreshConnectionStatusLogTable + }); + } + }; + return ( - <ConnectionStatusTable stickyHeader tableId="connection-status-table" columns={[ + <> + <ConnectionStatusTable stickyHeader tableId="connection-status-table" customActionButtons={[refreshConnectionStatusLogAction]} columns={[ { property: "timestamp", title: "Timestamp", type: ColumnType.text }, { property: "nodeId", title: "Node Name", type: ColumnType.text }, { property: "status", title: "Connection Status", type: ColumnType.text }, ]} idProperty="id" {...this.props.connectionStatusLogActions} {...this.props.connectionStatusLogProperties} > </ConnectionStatusTable> + <RefreshConnectionStatusLogDialog + mode={ this.state.refreshConnectionStatusLogEditorMode } + onClose={ this.onCloseRefreshConnectionStatusLogDialog } + /> + </> ); }; + private onCloseRefreshConnectionStatusLogDialog = () => { + this.setState({ + refreshConnectionStatusLogEditorMode: RefreshConnectionStatusLogDialogMode.None + }); + } componentDidMount() { if (!initialSorted) { initialSorted = true; diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx index 53e10481a..84a22a99a 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/components/networkElements.tsx @@ -19,12 +19,14 @@ import * as React from 'react'; import { Theme, createStyles, withStyles, WithStyles } from '@material-ui/core/styles'; import AddIcon from '@material-ui/icons/Add'; +import Refresh from '@material-ui/icons/Refresh'; import LinkIcon from '@material-ui/icons/Link'; import LinkOffIcon from '@material-ui/icons/LinkOff'; import RemoveIcon from '@material-ui/icons/RemoveCircleOutline'; import EditIcon from '@material-ui/icons/Edit'; import Info from '@material-ui/icons/Info'; import ComputerIcon from '@material-ui/icons/Computer'; +import { MenuItem, Divider, Typography } from '@material-ui/core'; import { MaterialTable, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; @@ -34,12 +36,14 @@ import { NavigateToApplication } from '../../../../framework/src/actions/navigat import { createNetworkElementsActions, createNetworkElementsProperties } from '../handlers/networkElementsHandler'; import { NetworkElementConnection } from '../models/networkElementConnection'; +import { 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 { TopologyNode } from '../models/topologyNetconf'; -import { MenuItem, Divider, Typography } from '@material-ui/core'; +import { connectService } from '../services/connectService'; +import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; const styles = (theme: Theme) => createStyles({ connectionStatusConnected: { @@ -63,6 +67,22 @@ const styles = (theme: Theme) => createStyles({ } }); +type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any +const MenuItemExt : React.FC<GetStatelessComponentProps<typeof MenuItem>> = (props) => { + const [disabled, setDisabled] = React.useState(true); + const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => { + if (ev.button ===1){ + setDisabled(!disabled); + ev.preventDefault(); + } + }; + return ( + <div onMouseDown={onMouseDown} > + <MenuItem {...{...props, disabled: props.disabled && disabled }} /> + </div> + ); +}; + const mapProps = (state: IApplicationStoreState) => ({ networkElementsProperties: createNetworkElementsProperties(state), applicationState: state, @@ -78,6 +98,7 @@ type NetworkElementsListComponentProps = WithStyles<typeof styles> & Connect<typ type NetworkElementsListComponentState = { networkElementToEdit: NetworkElementConnection, networkElementEditorMode: EditNetworkElementDialogMode, + refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode, infoNetworkElementEditorMode: InfoNetworkElementDialogMode, elementInfo: TopologyNode | null } @@ -94,19 +115,21 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement this.state = { networkElementToEdit: emptyRequireNetworkElement, networkElementEditorMode: EditNetworkElementDialogMode.None, + refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode.None, elementInfo: null, infoNetworkElementEditorMode: InfoNetworkElementDialogMode.None }; } - + getContextMenu(rowData: NetworkElementConnection): JSX.Element[] { - - - - const { configuration, fault, inventory } = this.props.applicationState as any; - let buttonArray = [ - <MenuItem aria-label={"mount-button"} onClick={event => this.onOpenMountdNetworkElementsDialog(event, rowData)} ><LinkIcon /><Typography>Mount</Typography></MenuItem>, - <MenuItem aria-label={"unmount-button"} onClick={event => this.onOpenUnmountdNetworkElementsDialog(event, rowData)}><LinkOffIcon /><Typography>Unmount</Typography></MenuItem>, + const mountUri = rowData.id && connectService.getNetworkElementUri(rowData.id); + const mountPolicy = mountUri && getAccessPolicyByUrl(mountUri); + const canMount = mountPolicy && mountPolicy.POST || false; + + const { configuration} = this.props.applicationState as any; + const buttonArray = [ + <MenuItemExt aria-label={"mount-button"} onClick={event => this.onOpenMountdNetworkElementsDialog(event, rowData)} disabled={!canMount} ><LinkIcon /><Typography>Mount</Typography></MenuItemExt>, + <MenuItemExt aria-label={"unmount-button"} onClick={event => this.onOpenUnmountdNetworkElementsDialog(event, rowData)} disabled={!canMount} ><LinkOffIcon /><Typography>Unmount</Typography></MenuItemExt>, <Divider />, <MenuItem aria-label={"info-button"} onClick={event => this.onOpenInfoNetworkElementDialog(event, rowData)} disabled={rowData.status === "Connecting" || rowData.status === "Disconnected"} ><Info /><Typography>Info</Typography></MenuItem>, <MenuItem aria-label={"edit-button"} onClick={event => this.onOpenEditNetworkElementDialog(event, rowData)}><EditIcon /><Typography>Edit</Typography></MenuItem>, @@ -121,9 +144,9 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement <MenuItem onClick={event => this.props.navigateToApplication("security", rowData.nodeId)} disabled={true} ><Typography>Security</Typography></MenuItem>, ]; - if (rowData.webUri) { + if (rowData.weburi) { // add an icon for gui cuttrough, if weburi is available - return [<MenuItem aria-label={"web-client-button"} onClick={event => window.open(rowData.webUri, "_blank")} ><ComputerIcon /><Typography>Web Client</Typography></MenuItem>].concat(buttonArray) + return [<MenuItem aria-label={"web-client-button"} onClick={event => window.open(rowData.weburi, "_blank")} ><ComputerIcon /><Typography>Web Client</Typography></MenuItem>].concat(buttonArray) } else { return buttonArray; } @@ -134,6 +157,12 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement render(): JSX.Element { const { classes } = this.props; const { networkElementToEdit } = this.state; + + // const mountUri = rowData.id && connectService.getNetworkElementUri(rowData.id); + // const mountPolicy = mountUri && getAccessPolicyByUrl(mountUri); + // const canAdd = mountPolicy && mountPolicy.POST || false; + const canAdd = true; + const addRequireNetworkElementAction = { icon: AddIcon, tooltip: 'Add', onClick: () => { this.setState({ @@ -143,9 +172,17 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement } }; + const refreshNetworkElementsAction = { + icon: Refresh, tooltip: 'Refresh Network Elements table', onClick: () => { + this.setState({ + refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode.RefreshNetworkElementsTable + }); + } + }; + return ( <> - <NetworkElementTable stickyHeader tableId="network-element-table" customActionButtons={[addRequireNetworkElementAction]} columns={[ + <NetworkElementTable stickyHeader tableId="network-element-table" customActionButtons={[refreshNetworkElementsAction, ...canAdd ? [addRequireNetworkElementAction]: []]} columns={[ { property: "nodeId", title: "Node Name", type: ColumnType.text }, { property: "isRequired", title: "Required", type: ColumnType.boolean }, { property: "status", title: "Connection Status", type: ColumnType.text }, @@ -163,6 +200,10 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement mode={this.state.networkElementEditorMode} onClose={this.onCloseEditNetworkElementDialog} /> + <RefreshNetworkElementsDialog + mode={this.state.refreshNetworkElementsEditorMode} + onClose={this.onCloseRefreshNetworkElementsDialog} + /> <InfoNetworkElementDialog initialNetworkElement={networkElementToEdit} mode={this.state.infoNetworkElementEditorMode} @@ -243,7 +284,11 @@ export class NetworkElementsListComponent extends React.Component<NetworkElement networkElementToEdit: emptyRequireNetworkElement, }); } + private onCloseRefreshNetworkElementsDialog = () => { + this.setState({ + refreshNetworkElementsEditorMode: RefreshNetworkElementsDialogMode.None + }); + } } export const NetworkElementsList = withStyles(styles)(connect(mapProps, mapDispatch)(NetworkElementsListComponent)); -export default NetworkElementsList;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/refreshConnectionStatusLogDialog.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/refreshConnectionStatusLogDialog.tsx new file mode 100644 index 000000000..41229eae6 --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/components/refreshConnectionStatusLogDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { connectionStatusLogReloadAction } from '../handlers/connectionStatusLogHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { ConnectionStatusLogType } from '../models/connectionStatusLog'; + +export enum RefreshConnectionStatusLogDialogMode { + None = "none", + RefreshConnectionStatusLogTable = "RefreshConnectionStatusLogTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshConnectionStatusLog: () => dispatcher.dispatch(connectionStatusLogReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshConnectionStatusLogDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshConnectionStatusLogDialogMode.RefreshConnectionStatusLogTable]: { + dialogTitle: "Do you want to refresh the Connection Status Log table?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshConnectionStatusLogDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshConnectionStatusLogDialogMode; + onClose: () => void; +}; + +type RefreshConnectionStatusLogDialogComponentState = ConnectionStatusLogType & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshConnectionStatusLogDialogComponent extends React.Component<RefreshConnectionStatusLogDialogComponentProps, RefreshConnectionStatusLogDialogComponentState> { + constructor(props: RefreshConnectionStatusLogDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshConnectionStatusLogDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshConnectionStatusLog(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshConnectionStatusLogDialog = connect(undefined, mapDispatch)(RefreshConnectionStatusLogDialogComponent); +export default RefreshConnectionStatusLogDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/components/refreshNetworkElementsDialog.tsx b/sdnr/wt/odlux/apps/connectApp/src/components/refreshNetworkElementsDialog.tsx new file mode 100644 index 000000000..a349977ab --- /dev/null +++ b/sdnr/wt/odlux/apps/connectApp/src/components/refreshNetworkElementsDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { networkElementsReloadAction } from '../handlers/networkElementsHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { NetworkElementConnection } from '../models/networkElementConnection'; + +export enum RefreshNetworkElementsDialogMode { + None = "none", + RefreshNetworkElementsTable = "RefreshNetworkElementsTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshNetworkElement: () => dispatcher.dispatch(networkElementsReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshNetworkElementsDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshNetworkElementsDialogMode.RefreshNetworkElementsTable]: { + dialogTitle: "Do you want to refresh the Network Elements table?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshNetworkElementsDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshNetworkElementsDialogMode; + onClose: () => void; +}; + +type RefreshNetworkElementsDialogComponentState = NetworkElementConnection & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshNetworkElementsDialogComponent extends React.Component<RefreshNetworkElementsDialogComponentProps, RefreshNetworkElementsDialogComponentState> { + constructor(props: RefreshNetworkElementsDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshNetworkElementsDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshNetworkElement(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshNetworkElementsDialog = connect(undefined, mapDispatch)(RefreshNetworkElementsDialogComponent); +export default RefreshNetworkElementsDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts index 302a981eb..c23e43924 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/connectAppRootHandler.ts @@ -68,7 +68,7 @@ const guiCutThroughHandler: IActionHandler<guiCutThroughState> = (state = { sear } else if (action instanceof RemoveWebUri) { const nodeId = action.element; - const webUris = state.searchedElements.filter(item => item.nodeId !== nodeId); + 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 }; diff --git a/sdnr/wt/odlux/apps/connectApp/src/handlers/networkElementsHandler.ts b/sdnr/wt/odlux/apps/connectApp/src/handlers/networkElementsHandler.ts index 4fe858c69..b74a39427 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/handlers/networkElementsHandler.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/handlers/networkElementsHandler.ts @@ -17,10 +17,11 @@ */ import { createExternal, IExternalTableState } from '../../../../framework/src/components/material-table/utilities'; import { createSearchDataHandler } from '../../../../framework/src/utilities/elasticSearch'; +import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; import { NetworkElementConnection } from '../models/networkElementConnection'; -import connectService from '../services/connectService'; -import { requestRest } from '../../../../framework/src/services/restService'; +import { connectService } from '../services/connectService'; + export interface INetworkElementsState extends IExternalTableState<NetworkElementConnection> { } // create eleactic search material data fetch handler @@ -31,8 +32,7 @@ export const { createActions: createNetworkElementsActions, createProperties: createNetworkElementsProperties, reloadAction: networkElementsReloadAction, - reloadActionAsync: networkElementsReloadActionAsync - + // set value action, to change a value } = createExternal<NetworkElementConnection>(networkElementsSearchHandler, appState => { @@ -43,9 +43,9 @@ export const { appState.connect.networkElements.rows.forEach(element => { if (element.status === "Connected") { - const webUri = webUris.find(item => item.nodeId === element.id as string); + const webUri = webUris.find(item => item.id === element.id as string); if (webUri) { - element.webUri = webUri.webUri; + element.weburi = webUri.weburi; element.isWebUriUnreachable = false; } else { @@ -56,5 +56,10 @@ export const { } return appState.connect.networkElements +}, (ne) => { + if (!ne || !ne.id) return true; + const neUrl = connectService.getNetworkElementUri(ne.id); + const policy = getAccessPolicyByUrl(neUrl); + return !(policy.GET || policy.POST); }); diff --git a/sdnr/wt/odlux/apps/connectApp/src/index.html b/sdnr/wt/odlux/apps/connectApp/src/index.html index 9c8a2063e..6f44c25f7 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/index.html +++ b/sdnr/wt/odlux/apps/connectApp/src/index.html @@ -19,6 +19,7 @@ connectApp.register(); faultApp.register(); inventoryApp.register(); + app("./app.tsx").configureApplication({ authentication:"basic", enablePolicy: false,}); app("./app.tsx").runApplication(); }); </script> diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/guiCutTrough.ts b/sdnr/wt/odlux/apps/connectApp/src/models/guiCutTrough.ts index d44eea685..b9f515dc8 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/models/guiCutTrough.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/models/guiCutTrough.ts @@ -16,4 +16,7 @@ * ============LICENSE_END========================================================================== */ -export type guiCutThrough = { webUri?: string, nodeId: string }
\ No newline at end of file +export type guiCutThrough = { + id: string, + weburi?: string +}
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts b/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts index b3586d693..71eddc808 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/models/networkElementConnection.ts @@ -24,7 +24,7 @@ export type NetworkElementConnection = { port: number; username?: string; password?: string; - webUri?: string; + weburi?: string; isWebUriUnreachable?: boolean; status?: "Connected" | "mounted" | "unmounted" | "Connecting" | "Disconnected" | "idle"; coreModelCapability?: string; diff --git a/sdnr/wt/odlux/apps/connectApp/src/pluginConnect.tsx b/sdnr/wt/odlux/apps/connectApp/src/pluginConnect.tsx index f711c440c..93bed1aad 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/pluginConnect.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/pluginConnect.tsx @@ -20,19 +20,12 @@ import { faPlug } from '@fortawesome/free-solid-svg-icons'; import applicationManager from '../../../framework/src/services/applicationManager'; import { subscribe, IFormatedMessage } from '../../../framework/src/services/notificationService'; +import { AddSnackbarNotification } from '../../../framework/src/actions/snackbarActions'; import connectAppRootHandler from './handlers/connectAppRootHandler'; import ConnectApplication from './views/connectView'; -import { AddSnackbarNotification } from '../../../framework/src/actions/snackbarActions'; -import { updateCurrentViewAsyncAction } from './actions/commonNetworkElementsActions'; - -type ObjectNotification = { - counter: string; - nodeName: string; - objectId: string; - timeStamp: string; -} +import { findWebUrisForGuiCutThroughAsyncAction, updateCurrentViewAsyncAction } from './actions/commonNetworkElementsActions'; export function register() { const applicationApi = applicationManager.registerApplication({ @@ -44,13 +37,19 @@ export function register() { }); // subscribe to the websocket notifications - subscribe<ObjectNotification & IFormatedMessage>(["ObjectCreationNotification", "ObjectDeletionNotification", "AttributeValueChangedNotification"], (msg => { + subscribe<IFormatedMessage>(["object-creation-notification", "object-deletion-notification", "attribute-value-changed-notification"], (msg => { const store = applicationApi.applicationStore; - if (msg && msg.notifType === "ObjectCreationNotification" && store) { - store.dispatch(new AddSnackbarNotification({ message: `Adding network element [${msg.objectId}]`, options: { variant: 'info' } })); - } else if (msg && (msg.notifType === "ObjectDeletionNotification" || msg.notifType === "AttributeValueChangedNotification") && store) { - store.dispatch(new AddSnackbarNotification({ message: `Updating network element [${msg.objectId}]`, options: { variant: 'info' } })); + if (msg && msg.type.type === "object-creation-notification" && store) { + store.dispatch(new AddSnackbarNotification({ message: `Adding network element [${msg['node-id']}]`, options: { variant: 'info' } })); + } else if (msg && (msg.type.type === "object-deletion-notification" || msg.type.type === "attribute-value-changed-notification") && store) { + store.dispatch(new AddSnackbarNotification({ message: `Updating network element [${msg['node-id']}]`, options: { variant: 'info' } })); + } + if (store) { + store.dispatch(updateCurrentViewAsyncAction() as any).then(() => { + if (msg['node-id']) { + store.dispatch(findWebUrisForGuiCutThroughAsyncAction([msg['node-id']])); + } + }); } - store && store.dispatch(updateCurrentViewAsyncAction()); })); }
\ 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 fbbfa68d9..5d7667a7f 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts +++ b/sdnr/wt/odlux/apps/connectApp/src/services/connectService.ts @@ -28,12 +28,15 @@ 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 = `/rests/operations/data-provider:create-network-element-connection`; + const path = this.getNetworkElementConnectDataProviderUri("create") ; const result = await requestRest<NetworkElementConnection>(path, { method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": element }, replaceUpperCase)) }); @@ -44,7 +47,7 @@ class ConnectService { * Updates a network element. */ public async updateNetworkElement(element: UpdateNetworkElement): Promise<NetworkElementConnection | null> { - const path = `/rests/operations/data-provider:update-network-element-connection`; + const path = this.getNetworkElementConnectDataProviderUri("update"); const result = await requestRest<NetworkElementConnection>(path, { method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": element }, replaceUpperCase)) }); @@ -58,7 +61,7 @@ class ConnectService { const query = { "id": element.id }; - const path = `/rests/operations/data-provider:delete-network-element-connection`; + const path = this.getNetworkElementConnectDataProviderUri("delete"); const result = await requestRest<NetworkElementConnection>(path, { method: "POST", body: JSON.stringify(convertPropertyNames({ "data-provider:input": query }, replaceUpperCase)) }); @@ -67,7 +70,7 @@ class ConnectService { /** Mounts network element. */ public async mountNetworkElement(networkElement: NetworkElementConnection): Promise<boolean> { - const path = '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + networkElement.nodeId; + const path = this.getNetworkElementUri(networkElement.nodeId); const mountXml = [ '<node xmlns="urn:TBD:params:xml:ns:yang:network-topology">', `<node-id>${networkElement.nodeId}</node-id>`, @@ -106,7 +109,7 @@ class ConnectService { /** Unmounts a network element by its id. */ public async unmountNetworkElement(nodeId: string): Promise<boolean> { - const path = '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; + const path = this.getNetworkElementUri(nodeId); try { const result = await requestRest<string>(path, { @@ -126,7 +129,7 @@ class ConnectService { /** Yang capabilities of the selected network elements. */ public async infoNetworkElement(nodeId: string): Promise<TopologyNode | null> { - const path = '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; + const path = this.getNetworkElementUri(nodeId); const topologyRequestPomise = requestRest<Topology>(path, { method: "GET" }); return topologyRequestPomise && topologyRequestPomise.then(result => { @@ -157,38 +160,80 @@ class ConnectService { })) || null; } - public getAllWebUriExtensionsForNetworkElementListAsync(ne: string[]) { - - let promises: any[] = []; - let webUris: guiCutThrough[] = [] - - ne.forEach(nodeId => { - const path = '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId + '/yang-ext:mount/core-model:network-element'; + 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 + } + } + } - // 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, nodeId: nodeId }); - } else { - webUris.push({ webUri: undefined, nodeId: nodeId }); + 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; } - } else { - webUris.push({ webUri: undefined, nodeId: nodeId }); - } - }) - .catch(error => { - webUris.push({ webUri: undefined, nodeId: nodeId }); - })) - - }) - - // wait until all promises are done and return weburis - return Promise.all(promises).then(result => { return webUris }); + }); + } 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(); -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 index 1fa5e1909..34b1b94b6 100644 --- a/sdnr/wt/odlux/apps/connectApp/src/views/connectView.tsx +++ b/sdnr/wt/odlux/apps/connectApp/src/views/connectView.tsx @@ -40,10 +40,9 @@ const mapProps = (state: IApplicationStoreState) => ({ const mapDispatcher = (dispatcher: IDispatcher) => ({ networkElementsActions: createNetworkElementsActions(dispatcher.dispatch), connectionStatusLogActions: createConnectionStatusLogActions(dispatcher.dispatch), - onLoadNetworkElements: () => { - dispatcher.dispatch(networkElementsReloadAction); - }, - loadWebUris: async (networkElements: NetworkElementConnection[]) => { await dispatcher.dispatch(findWebUrisForGuiCutThroughAsyncAction(networkElements)) }, + onLoadNetworkElements: () => dispatcher.dispatch(networkElementsReloadAction), + loadWebUris: (networkElements: NetworkElementConnection[]) => + dispatcher.dispatch(findWebUrisForGuiCutThroughAsyncAction(networkElements.map((ne) => ne.id!))), isBusy: (busy: boolean) => dispatcher.dispatch(new SetWeburiSearchBusy(busy)), onLoadConnectionStatusLog: () => { dispatcher.dispatch(connectionStatusLogReloadAction); @@ -65,12 +64,14 @@ class ConnectApplicationComponent extends React.Component<ConnectApplicationComp //this.props.connectionStatusLogActions.onToggleFilter(); } - public componentDidUpdate = async () => { - // search for guicutthroughs after networkelements were found + public componentDidUpdate = () => { + const networkElements = this.props.netWorkElements; if (networkElements.rows.length > 0) { - await this.props.loadWebUris(networkElements.rows); + // Update all netWorkElements for propper WebUriClient settings in case of table data changes. + // e.G: Pagination of the table data (there is no event) + this.props.loadWebUris(networkElements.rows); } } diff --git a/sdnr/wt/odlux/apps/connectApp/webpack.config.js b/sdnr/wt/odlux/apps/connectApp/webpack.config.js index 7f36e4e17..df88a80a9 100644 --- a/sdnr/wt/odlux/apps/connectApp/webpack.config.js +++ b/sdnr/wt/odlux/apps/connectApp/webpack.config.js @@ -11,6 +11,8 @@ const webpack = require("webpack"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const TerserPlugin = require('terser-webpack-plugin'); +const policies = require('./policies.json'); + // const __dirname = (path => path.replace(/^([a-z]\:)/, c => c.toUpperCase()))(process.__dirname()); module.exports = (env) => { @@ -124,34 +126,54 @@ module.exports = (env) => { stats: { colors: true }, - proxy: { - "/oauth2/": { - target: "http://sdnr:8181", + before: function(app, server, compiler) { + app.get('/oauth/policies',(_, res) => res.json(policies)); + }, + proxy: { + "/about": { + target: "http://localhost:18181", + secure: false + }, + "/yang-schema/": { + target: "http://localhost:18181", + secure: false + }, + "/oauth/": { + target: "http://localhost:18181", secure: false }, - - "/oauth/": { - target: "http://sdnr:8181", - secure: false - }, "/database/": { - target: "http://sdnr:8181", + target: "http://localhost:18181", secure: false }, "/restconf/": { - target: "http://sdnr:8181", + target: "http://localhost:18181", secure: false }, "/rests/": { - target: "http://sdnr:8181", + target: "http://localhost:18181", secure: false }, "/help/": { - target: "http://sdnr:8181", + target: "http://localhost:18181", + secure: false + }, + "/about/": { + target: "http://localhost:18181", + secure: false + }, + "/tree/": { + target: "http://localhost:18181", secure: false }, "/websocket": { - target: "http://sdnr:8181", + target: "http://localhost:18181", + ws: true, + changeOrigin: true, + secure: false + }, + "/apidoc": { + target: "http://localhost:18181", ws: true, changeOrigin: true, secure: false diff --git a/sdnr/wt/odlux/apps/demoApp/package.json b/sdnr/wt/odlux/apps/demoApp/package.json index 9505cfbb0..7756fe328 100644 --- a/sdnr/wt/odlux/apps/demoApp/package.json +++ b/sdnr/wt/odlux/apps/demoApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/demoApp/src/index.html b/sdnr/wt/odlux/apps/demoApp/src/index.html index e85bcbb03..521c8902c 100644 --- a/sdnr/wt/odlux/apps/demoApp/src/index.html +++ b/sdnr/wt/odlux/apps/demoApp/src/index.html @@ -17,6 +17,7 @@ // run the application require(["app","demoApp"], function (app, demoApp) { demoApp.register(); + app("./app.tsx").configureApplication({ authentication:"oauth", enablePolicy: true,}); app("./app.tsx"); }); </script> diff --git a/sdnr/wt/odlux/apps/eventLogApp/package.json b/sdnr/wt/odlux/apps/eventLogApp/package.json index 4e98a1756..dfcebfe63 100644 --- a/sdnr/wt/odlux/apps/eventLogApp/package.json +++ b/sdnr/wt/odlux/apps/eventLogApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/eventLogApp/src/components/refreshEventLogDialog.tsx b/sdnr/wt/odlux/apps/eventLogApp/src/components/refreshEventLogDialog.tsx new file mode 100644 index 000000000..7d86e9f04 --- /dev/null +++ b/sdnr/wt/odlux/apps/eventLogApp/src/components/refreshEventLogDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { eventLogReloadAction } from '../handlers/eventLogHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { EventLogType } from '../models/eventLogType'; + +export enum RefreshEventLogDialogMode { + None = "none", + RefreshEventLogTable = "RefreshEventLogTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshEventLog: () => dispatcher.dispatch(eventLogReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshEventLogDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshEventLogDialogMode.RefreshEventLogTable]: { + dialogTitle: "Do you want to refresh the Event Log?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshEventLogDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshEventLogDialogMode; + onClose: () => void; +}; + +type RefreshEventLogDialogComponentState = EventLogType & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshEventLogDialogComponent extends React.Component<RefreshEventLogDialogComponentProps, RefreshEventLogDialogComponentState> { + constructor(props: RefreshEventLogDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshEventLogDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshEventLog(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshEventLogDialog = connect(undefined, mapDispatch)(RefreshEventLogDialogComponent); +export default RefreshEventLogDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/eventLogApp/src/views/eventLog.tsx b/sdnr/wt/odlux/apps/eventLogApp/src/views/eventLog.tsx index 3d81e4e46..748c66e51 100644 --- a/sdnr/wt/odlux/apps/eventLogApp/src/views/eventLog.tsx +++ b/sdnr/wt/odlux/apps/eventLogApp/src/views/eventLog.tsx @@ -19,10 +19,12 @@ import * as React from "react"; import { Connect, connect, IDispatcher } from '../../../../framework/src/flux/connect'; import { MaterialTable, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; +import Refresh from '@material-ui/icons/Refresh'; import { EventLogType } from '../models/eventLogType'; import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; import { createEventLogProperties, createEventLogActions } from "../handlers/eventLogHandler"; +import RefreshEventLogDialog, { RefreshEventLogDialogMode } from '../components/refreshEventLogDialog'; const EventLogTable = MaterialTable as MaterialTableCtorType<EventLogType & { _id: string }>; @@ -35,22 +37,56 @@ const mapDispatch = (dispatcher: IDispatcher) => ({ eventLogActions: createEventLogActions(dispatcher.dispatch) }); +type EventLogComponentProps = Connect<typeof mapProps, typeof mapDispatch>; +type EventLogComponentState = { + refreshEventLogEditorMode: RefreshEventLogDialogMode +} let initalSorted = false; -class EventLogComponent extends React.Component<Connect<typeof mapProps, typeof mapDispatch>> { - render() { - return <EventLogTable stickyHeader title="Event Log" tableId="event-log-table" idProperty="_id" columns={[ - { property: "nodeId", title: "Node Name" }, - { property: "counter", title: "Counter" }, - { property: "timestamp", title: "Timestamp" }, - { property: "objectId", title: "Object ID" }, - { property: "attributeName", title: "Attribute Name" }, - { property: "newValue", title: "Message" }, - { property: "sourceType", title: "Source" } - ]} {...this.props.eventLogActions} {...this.props.eventLogProperties} > - </EventLogTable> +class EventLogComponent extends React.Component<EventLogComponentProps, EventLogComponentState> { + constructor(props: EventLogComponentProps) { + super(props); + + this.state = { + refreshEventLogEditorMode: RefreshEventLogDialogMode.None + }; + } + + render(): JSX.Element { + + const refreshEventLogAction = { + icon: Refresh, tooltip: 'Refresh Event log', onClick: () => { + this.setState({ + refreshEventLogEditorMode: RefreshEventLogDialogMode.RefreshEventLogTable + }); + } + }; + return ( + <> + <EventLogTable stickyHeader title="Event Log" tableId="event-log-table" idProperty="_id" customActionButtons={[refreshEventLogAction]} + columns={[ + { property: "nodeId", title: "Node Name" }, + { property: "counter", title: "Counter" }, + { property: "timestamp", title: "Timestamp" }, + { property: "objectId", title: "Object ID" }, + { property: "attributeName", title: "Attribute Name" }, + { property: "newValue", title: "Message" }, + { property: "sourceType", title: "Source" } + ]} {...this.props.eventLogActions} {...this.props.eventLogProperties} > + </EventLogTable> + <RefreshEventLogDialog + mode={this.state.refreshEventLogEditorMode} + onClose={this.onCloseRefreshEventLogDialog} + /> + </> + ) } + private onCloseRefreshEventLogDialog = () => { + this.setState({ + refreshEventLogEditorMode: RefreshEventLogDialogMode.None + }); + } componentDidMount() { if (!initalSorted) { diff --git a/sdnr/wt/odlux/apps/faultApp/package.json b/sdnr/wt/odlux/apps/faultApp/package.json index 70952530c..5d1439b85 100644 --- a/sdnr/wt/odlux/apps/faultApp/package.json +++ b/sdnr/wt/odlux/apps/faultApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/faultApp/src/components/refreshAlarmLogDialog.tsx b/sdnr/wt/odlux/apps/faultApp/src/components/refreshAlarmLogDialog.tsx new file mode 100644 index 000000000..1d1b34cf2 --- /dev/null +++ b/sdnr/wt/odlux/apps/faultApp/src/components/refreshAlarmLogDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { alarmLogEntriesReloadAction } from '../handlers/alarmLogEntriesHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { Fault } from '../models/fault'; + +export enum RefreshAlarmLogDialogMode { + None = "none", + RefreshAlarmLogTable = "RefreshAlarmLogTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshAlarmLog: () => dispatcher.dispatch(alarmLogEntriesReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshAlarmLogDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshAlarmLogDialogMode.RefreshAlarmLogTable]: { + dialogTitle: "Do you want to refresh the Alarm Log?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshAlarmLogDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshAlarmLogDialogMode; + onClose: () => void; +}; + +type RefreshAlarmLogDialogComponentState = Fault & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshAlarmLogDialogComponent extends React.Component<RefreshAlarmLogDialogComponentProps, RefreshAlarmLogDialogComponentState> { + constructor(props: RefreshAlarmLogDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshAlarmLogDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshAlarmLog(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshAlarmLogDialog = connect(undefined, mapDispatch)(RefreshAlarmLogDialogComponent); +export default RefreshAlarmLogDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/faultApp/src/components/refreshCurrentProblemsDialog.tsx b/sdnr/wt/odlux/apps/faultApp/src/components/refreshCurrentProblemsDialog.tsx new file mode 100644 index 000000000..c49722077 --- /dev/null +++ b/sdnr/wt/odlux/apps/faultApp/src/components/refreshCurrentProblemsDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { currentProblemsReloadAction } from '../handlers/currentProblemsHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { Fault } from '../models/fault'; + +export enum RefreshCurrentProblemsDialogMode { + None = "none", + RefreshCurrentProblemsTable = "RefreshCurrentProblemsTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshCurrentProblems: () => dispatcher.dispatch(currentProblemsReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshCurrentProblemsDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshCurrentProblemsDialogMode.RefreshCurrentProblemsTable]: { + dialogTitle: "Do you want to refresh the Current Problems List?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshCurrentProblemsDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshCurrentProblemsDialogMode; + onClose: () => void; +}; + +type RefreshCurrentProblemsDialogComponentState = Fault & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshCurrentProblemsDialogComponent extends React.Component<RefreshCurrentProblemsDialogComponentProps, RefreshCurrentProblemsDialogComponentState> { + constructor(props: RefreshCurrentProblemsDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshCurrentProblemsDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshCurrentProblems(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshCurrentProblemsDialog = connect(undefined, mapDispatch)(RefreshCurrentProblemsDialogComponent); +export default RefreshCurrentProblemsDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/faultApp/src/views/faultApplication.tsx b/sdnr/wt/odlux/apps/faultApp/src/views/faultApplication.tsx index 2c167c5b2..d313a2048 100644 --- a/sdnr/wt/odlux/apps/faultApp/src/views/faultApplication.tsx +++ b/sdnr/wt/odlux/apps/faultApp/src/views/faultApplication.tsx @@ -35,8 +35,12 @@ import { createCurrentProblemsProperties, createCurrentProblemsActions, currentP import { createAlarmLogEntriesProperties, createAlarmLogEntriesActions, alarmLogEntriesReloadAction } from '../handlers/alarmLogEntriesHandler'; import { setPanelAction } from '../actions/panelChangeActions'; import { Tooltip, IconButton, AppBar, Tabs, Tab } from '@material-ui/core'; -import RefreshIcon from '@material-ui/icons/Refresh'; +import Sync from '@material-ui/icons/Sync'; +import Refresh from '@material-ui/icons/Refresh'; + import ClearStuckAlarmsDialog, { ClearStuckAlarmsDialogMode } from '../components/clearStuckAlarmsDialog'; +import RefreshAlarmLogDialog, { RefreshAlarmLogDialogMode } from '../components/refreshAlarmLogDialog'; +import RefreshCurrentProblemsDialog, { RefreshCurrentProblemsDialogMode } from '../components/refreshCurrentProblemsDialog'; import { SetPartialUpdatesAction } from '../actions/partialUpdatesAction'; const mapProps = (state: IApplicationStoreState) => ({ @@ -61,7 +65,9 @@ type FaultApplicationComponentProps = RouteComponentProps & Connect<typeof mapPr type FaultApplicationState = { clearAlarmDialogMode: ClearStuckAlarmsDialogMode, - stuckAlarms: string[] + stuckAlarms: string[], + refreshAlarmLogEditorMode: RefreshAlarmLogDialogMode, + refreshCurrentProblemsEditorMode: RefreshCurrentProblemsDialogMode } @@ -78,7 +84,12 @@ class FaultApplicationComponent extends React.Component<FaultApplicationComponen */ constructor(props: FaultApplicationComponentProps) { super(props); - this.state = { clearAlarmDialogMode: ClearStuckAlarmsDialogMode.None, stuckAlarms: [] } + this.state = { + clearAlarmDialogMode: ClearStuckAlarmsDialogMode.None, + stuckAlarms: [], + refreshAlarmLogEditorMode: RefreshAlarmLogDialogMode.None, + refreshCurrentProblemsEditorMode: RefreshCurrentProblemsDialogMode.None + } } onDialogClose = () => { @@ -127,10 +138,27 @@ class FaultApplicationComponent extends React.Component<FaultApplicationComponen render(): JSX.Element { const refreshButton = { - icon: RefreshIcon, tooltip: 'Clear stuck alarms', onClick: this.onDialogOpen + icon: Sync, tooltip: 'Clear stuck alarms', onClick: this.onDialogOpen + }; + + const refreshCurrentProblemsAction = { + icon: Refresh, tooltip: 'Refresh Current Problems List', onClick: () => { + this.setState({ + refreshCurrentProblemsEditorMode: RefreshCurrentProblemsDialogMode.RefreshCurrentProblemsTable + }); + } + }; + + const refreshAlarmLogAction = { + icon: Refresh, tooltip: 'Refresh Alarm log table', onClick: () => { + this.setState({ + refreshAlarmLogEditorMode: RefreshAlarmLogDialogMode.RefreshAlarmLogTable + }); + } }; + const areFaultsAvailable = this.props.currentProblemsProperties.rows && this.props.currentProblemsProperties.rows.length > 0 - const customAction = areFaultsAvailable ? [refreshButton] : []; + const customActions = areFaultsAvailable ? [refreshButton, refreshCurrentProblemsAction] : [refreshCurrentProblemsAction]; const { panelId: activePanelId } = this.props; @@ -144,15 +172,22 @@ class FaultApplicationComponent extends React.Component<FaultApplicationComponen </Tabs> </AppBar> { - activePanelId === 'CurrentProblem' && <FaultTable stickyHeader tableId="current-problems-table" idProperty={'id'} customActionButtons={customAction} columns={[ - { property: "icon", title: "", type: ColumnType.custom, customControl: this.renderIcon }, - { property: "timestamp", type: ColumnType.text, title: "Timestamp" }, - { property: "nodeId", title: "Node Name", type: ColumnType.text }, - { property: "counter", title: "Count", type: ColumnType.numeric, width: "100px" }, - { property: "objectId", title: "Object Id", type: ColumnType.text }, - { property: "problem", title: "Alarm Type", type: ColumnType.text }, - { property: "severity", title: "Severity", type: ColumnType.text, width: "140px" }, - ]} {...this.props.currentProblemsProperties} {...this.props.currentProblemsActions} /> + activePanelId === 'CurrentProblem' && + <> + <FaultTable stickyHeader tableId="current-problems-table" idProperty={'id'} customActionButtons={customActions} columns={[ + { property: "icon", title: "", type: ColumnType.custom, customControl: this.renderIcon }, + { property: "timestamp", type: ColumnType.text, title: "Timestamp" }, + { property: "nodeId", title: "Node Name", type: ColumnType.text }, + { property: "counter", title: "Count", type: ColumnType.numeric, width: "100px" }, + { property: "objectId", title: "Object Id", type: ColumnType.text }, + { property: "problem", title: "Alarm Type", type: ColumnType.text }, + { property: "severity", title: "Severity", type: ColumnType.text, width: "140px" }, + ]} {...this.props.currentProblemsProperties} {...this.props.currentProblemsActions} /> + <RefreshCurrentProblemsDialog + mode={this.state.refreshCurrentProblemsEditorMode} + onClose={this.onCloseRefreshCurrentProblemsDialog} + /> + </> } {activePanelId === 'AlarmNotifications' && @@ -168,18 +203,24 @@ class FaultApplicationComponent extends React.Component<FaultApplicationComponen } - {activePanelId === 'AlarmLog' && - <FaultTable stickyHeader idProperty={'id'} tableId="alarm-log-table" - columns={[ - { property: "icon", title: "", type: ColumnType.custom, customControl: this.renderIcon }, - { property: "timestamp", title: "Timestamp" }, - { property: "nodeId", title: "Node Name" }, - { property: "counter", title: "Count", type: ColumnType.numeric, width: "100px" }, - { property: "objectId", title: "Object Id" }, - { property: "problem", title: "Alarm Type" }, - { property: "severity", title: "Severity", width: "140px" }, - { property: "sourceType", title: "Source", width: "140px" }, - ]} {...this.props.alarmLogEntriesProperties} {...this.props.alarmLogEntriesActions} /> + {activePanelId === 'AlarmLog' && + <> + <FaultTable stickyHeader idProperty={'id'} tableId="alarm-log-table" customActionButtons={[refreshAlarmLogAction]} + columns={[ + { property: "icon", title: "", type: ColumnType.custom, customControl: this.renderIcon }, + { property: "timestamp", title: "Timestamp" }, + { property: "nodeId", title: "Node Name" }, + { property: "counter", title: "Count", type: ColumnType.numeric, width: "100px" }, + { property: "objectId", title: "Object Id" }, + { property: "problem", title: "Alarm Type" }, + { property: "severity", title: "Severity", width: "140px" }, + { property: "sourceType", title: "Source", width: "140px" }, + ]} {...this.props.alarmLogEntriesProperties} {...this.props.alarmLogEntriesActions} /> + <RefreshAlarmLogDialog + mode={this.state.refreshAlarmLogEditorMode} + onClose={this.onCloseRefreshAlarmLogDialog} + /> + </> } { @@ -207,6 +248,16 @@ class FaultApplicationComponent extends React.Component<FaultApplicationComponen ); }; + private onCloseRefreshAlarmLogDialog = () => { + this.setState({ + refreshAlarmLogEditorMode: RefreshAlarmLogDialogMode.None + }); + } + private onCloseRefreshCurrentProblemsDialog = () => { + this.setState({ + refreshCurrentProblemsEditorMode: RefreshCurrentProblemsDialogMode.None + }); + } } export const FaultApplication = withRouter(connect(mapProps, mapDisp)(FaultApplicationComponent)); diff --git a/sdnr/wt/odlux/apps/helpApp/package.json b/sdnr/wt/odlux/apps/helpApp/package.json index 0a7593dcc..58e68dd03 100644 --- a/sdnr/wt/odlux/apps/helpApp/package.json +++ b/sdnr/wt/odlux/apps/helpApp/package.json @@ -29,17 +29,17 @@ "github-markdown-css": "2.10.0" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/inventoryApp/package.json b/sdnr/wt/odlux/apps/inventoryApp/package.json index 1e11f78d0..0f6b40379 100644 --- a/sdnr/wt/odlux/apps/inventoryApp/package.json +++ b/sdnr/wt/odlux/apps/inventoryApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/inventoryApp/src/components/refreshInventoryDialog.tsx b/sdnr/wt/odlux/apps/inventoryApp/src/components/refreshInventoryDialog.tsx new file mode 100644 index 000000000..d2efb4e97 --- /dev/null +++ b/sdnr/wt/odlux/apps/inventoryApp/src/components/refreshInventoryDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { inventoryElementsReloadAction } from '../handlers/inventoryElementsHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { InventoryType } from '../models/inventory'; + +export enum RefreshInventoryDialogMode { + None = "none", + RefreshInventoryTable = "RefreshInventoryTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshInventory: () => dispatcher.dispatch(inventoryElementsReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshInventoryDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshInventoryDialogMode.RefreshInventoryTable]: { + dialogTitle: "Do you want to refresh the Inventory table?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshInventoryDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshInventoryDialogMode; + onClose: () => void; +}; + +type RefreshInventoryDialogComponentState = InventoryType & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshInventoryDialogComponent extends React.Component<RefreshInventoryDialogComponentProps, RefreshInventoryDialogComponentState> { + constructor(props: RefreshInventoryDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshInventoryDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshInventory(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshInventoryDialog = connect(undefined, mapDispatch)(RefreshInventoryDialogComponent); +export default RefreshInventoryDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/inventoryApp/src/index.html b/sdnr/wt/odlux/apps/inventoryApp/src/index.html index 75531ec1b..21f31300f 100644 --- a/sdnr/wt/odlux/apps/inventoryApp/src/index.html +++ b/sdnr/wt/odlux/apps/inventoryApp/src/index.html @@ -15,9 +15,10 @@ <script type="text/javascript" src="./config.js"></script> <script> // run the application - require(["app", "inventoryApp", "connectApp"], function (app, inventoryApp, connectApp) { + require(["app", "inventoryApp", "connectApp", "configurationApp"], function (app, inventoryApp, connectApp, configurationApp) { inventoryApp.register(); connectApp.register(); + configurationApp.register(); app("./app.tsx").runApplication(); }); </script> diff --git a/sdnr/wt/odlux/apps/inventoryApp/src/views/dashboard.tsx b/sdnr/wt/odlux/apps/inventoryApp/src/views/dashboard.tsx index f5ada22ab..11427fba9 100644 --- a/sdnr/wt/odlux/apps/inventoryApp/src/views/dashboard.tsx +++ b/sdnr/wt/odlux/apps/inventoryApp/src/views/dashboard.tsx @@ -23,6 +23,7 @@ import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/co import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; import { MaterialTable, MaterialTableCtorType, ColumnType } from "../../../../framework/src/components/material-table"; import { AppBar, Tabs, Tab, MenuItem, Typography } from "@material-ui/core"; +import Refresh from '@material-ui/icons/Refresh'; import { PanelId } from "../models/panelId"; import { setPanelAction } from "../actions/panelActions"; @@ -36,6 +37,7 @@ import { InventoryType } from '../models/inventory'; import { createInventoryElementsProperties, createInventoryElementsActions } from "../handlers/inventoryElementsHandler"; import { NavigateToApplication } from '../../../../framework/src/actions/navigationActions'; import { updateInventoryTreeAsyncAction } from '../actions/inventoryTreeActions'; +import RefreshInventoryDialog, { RefreshInventoryDialogMode } from '../components/refreshInventoryDialog'; const InventoryTable = MaterialTable as MaterialTableCtorType<InventoryType & { _id: string }>; @@ -49,7 +51,7 @@ const mapProps = (state: IApplicationStoreState) => ({ const mapDispatch = (dispatcher: IDispatcher) => ({ connectedNetworkElementsActions: createConnectedNetworkElementsActions(dispatcher.dispatch), switchActivePanel: (panelId: PanelId) => { - dispatcher.dispatch(setPanelAction(panelId)); + dispatcher.dispatch(setPanelAction(panelId)); }, inventoryElementsActions: createInventoryElementsActions(dispatcher.dispatch), navigateToApplication: (applicationName: string, path?: string) => dispatcher.dispatch(new NavigateToApplication(applicationName, path)), @@ -62,8 +64,18 @@ let inventoryInitialSorted = false; const ConnectedElementTable = MaterialTable as MaterialTableCtorType<NetworkElementConnection>; type DashboardComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch>; +type DashboardComponentState = { + refreshInventoryEditorMode: RefreshInventoryDialogMode +} + +class DashboardSelectorComponent extends React.Component<DashboardComponentProps, DashboardComponentState> { + constructor(props: DashboardComponentProps) { + super(props); -class DashboardSelectorComponent extends React.Component<DashboardComponentProps> { + this.state = { + refreshInventoryEditorMode: RefreshInventoryDialogMode.None + }; + } private onHandleTabChange = (event: React.ChangeEvent<{}>, newValue: PanelId) => { this.onTogglePanel(newValue); @@ -111,6 +123,13 @@ class DashboardSelectorComponent extends React.Component<DashboardComponentProps render() { + const refreshInventoryAction = { + icon: Refresh, tooltip: 'Refresh Inventory', onClick: () => { + this.setState({ + refreshInventoryEditorMode: RefreshInventoryDialogMode.RefreshInventoryTable + }); + } + }; const { panelId: activePanelId } = this.props; return ( <> @@ -124,32 +143,42 @@ class DashboardSelectorComponent extends React.Component<DashboardComponentProps { activePanelId === "InventoryElementsTable" && - - <InventoryTable stickyHeader title="Inventory" idProperty="_id" tableId="inventory-table" columns={[ - { property: "nodeId", title: "Node Name" }, - { property: "manufacturerIdentifier", title: "Manufacturer" }, - { property: "parentUuid", title: "Parent" }, - { property: "uuid", title: "Name" }, - { property: "serial", title: "Serial" }, - { property: "version", title: "Version" }, - { property: "date", title: "Date" }, - { property: "description", title: "Description" }, - { property: "partTypeId", title: "Part Type Id" }, - { property: "modelIdentifier", title: "Model Identifier" }, - { property: "typeName", title: "Type" }, - { property: "treeLevel", title: "Containment Level" }, - ]} {...this.props.inventoryElementsActions} {...this.props.inventoryElementsProperties} - createContextMenu={rowData => { - - return this.getContextMenu(rowData); - }} > - </InventoryTable> + <> + <InventoryTable stickyHeader title="Inventory" idProperty="_id" tableId="inventory-table" customActionButtons={[refreshInventoryAction]} columns={[ + { property: "nodeId", title: "Node Name" }, + { property: "manufacturerIdentifier", title: "Manufacturer" }, + { property: "parentUuid", title: "Parent" }, + { property: "uuid", title: "Name" }, + { property: "serial", title: "Serial" }, + { property: "version", title: "Version" }, + { property: "date", title: "Date" }, + { property: "description", title: "Description" }, + { property: "partTypeId", title: "Part Type Id" }, + { property: "modelIdentifier", title: "Model Identifier" }, + { property: "typeName", title: "Type" }, + { property: "treeLevel", title: "Containment Level" }, + ]} {...this.props.inventoryElementsActions} {...this.props.inventoryElementsProperties} + createContextMenu={rowData => { + + return this.getContextMenu(rowData); + }} > + </InventoryTable> + <RefreshInventoryDialog + mode={this.state.refreshInventoryEditorMode} + onClose={this.onCloseRefreshInventoryDialog} + /> + </> } { activePanelId === "TreeviewTable" && - <ConnectedElementTable stickyHeader tableId="treeview-networkelement-selection-table" onHandleClick={(e, row) => { this.props.history.push(`${this.props.match.path}/${row.nodeId}`) }} columns={[ + <ConnectedElementTable stickyHeader tableId="treeview-networkelement-selection-table" + onHandleClick={(e, row) => { + this.props.history.push(`${this.props.match.path}/${row.nodeId}`); + this.props.updateInventoryTree(row.nodeId, '*'); + }} + columns={[ { property: "nodeId", title: "Node Name", type: ColumnType.text }, { property: "isRequired", title: "Required", type: ColumnType.boolean }, { property: "host", title: "Host", type: ColumnType.text }, @@ -163,6 +192,11 @@ class DashboardSelectorComponent extends React.Component<DashboardComponentProps ); } + private onCloseRefreshInventoryDialog = () => { + this.setState({ + refreshInventoryEditorMode: RefreshInventoryDialogMode.None + }); + } componentDidMount() { if (this.props.panelId === null) { //set default tab if none is set diff --git a/sdnr/wt/odlux/apps/inventoryApp/src/views/treeview.tsx b/sdnr/wt/odlux/apps/inventoryApp/src/views/treeview.tsx index 5ba82abe7..9544861ef 100644 --- a/sdnr/wt/odlux/apps/inventoryApp/src/views/treeview.tsx +++ b/sdnr/wt/odlux/apps/inventoryApp/src/views/treeview.tsx @@ -24,7 +24,7 @@ import { TreeView, TreeViewCtorType, SearchMode } from '../../../../framework/sr import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import { updateInventoryTreeAsyncAction, selectInventoryNodeAsyncAction, UpdateSelectedNodeAction, UpdateExpandedNodesAction, setSearchTermAction} from "../actions/inventoryTreeActions"; +import { updateInventoryTreeAsyncAction, selectInventoryNodeAsyncAction, UpdateSelectedNodeAction, UpdateExpandedNodesAction, setSearchTermAction } from "../actions/inventoryTreeActions"; import { TreeDemoItem } from "../models/inventory"; import { RouteComponentProps } from "react-router-dom"; @@ -66,7 +66,7 @@ const InventoryTree = TreeView as any as TreeViewCtorType<string>; -type TreeviewComponentProps = RouteComponentProps<{ mountId: string}> & WithStyles<typeof styles> & Connect<typeof mapProps, typeof mapDispatch> +type TreeviewComponentProps = RouteComponentProps<{ mountId: string }> & WithStyles<typeof styles> & Connect<typeof mapProps, typeof mapDispatch> type TreeviewComponentState = { [propsChache]: { @@ -78,7 +78,7 @@ type TreeviewComponentState = { class DashboardComponent extends React.Component<TreeviewComponentProps, TreeviewComponentState> { - constructor (props: TreeviewComponentProps) { + constructor(props: TreeviewComponentProps) { super(props); this.state = { @@ -89,15 +89,16 @@ class DashboardComponent extends React.Component<TreeviewComponentProps, Treevie static getDerivedStateFromProps(props: TreeviewComponentProps, state: TreeviewComponentState) { if (state[propsChache].rootNodes != props.rootNodes) { - state = { ...state, rootNodes: props.rootNodes} + state = { ...state, rootNodes: props.rootNodes } } return state; } render() { - const { classes, updateInventoryTree, updateExpendedNodes, expendedItems, selectedNode, selectTreeNode, searchTerm, match: { params: { mountId }} } = this.props; + const { classes, updateInventoryTree, updateExpendedNodes, expendedItems, selectedNode, selectTreeNode, searchTerm, match: { params: { mountId } } } = this.props; + const scrollbar = { overflow: "auto", paddingRight: "20px" } return ( - <div className={classes.root}> + <div style={scrollbar} className={classes.root}> <InventoryTree className={classes.tree} items={this.state.rootNodes} enableSearchBar initialSearchTerm={searchTerm} searchMode={SearchMode.OnEnter} searchTerm={searchTerm} onSearch={(searchTerm) => updateInventoryTree(mountId, searchTerm)} expandedItems={expendedItems} onFolderClick={(item) => { const indexOfItemToToggle = expendedItems.indexOf(item); @@ -119,11 +120,11 @@ class DashboardComponent extends React.Component<TreeviewComponentProps, Treevie } componentDidMount() { - const { updateInventoryTree, searchTerm, match: { params: { mountId } }} = this.props; + const { updateInventoryTree, searchTerm, match: { params: { mountId } } } = this.props; updateInventoryTree(mountId, searchTerm); } - componentWillUnmount(){ + componentWillUnmount() { this.props.setSearchTerm(""); } } diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/package.json b/sdnr/wt/odlux/apps/linkCalculationApp/package.json index 22b2a6c0a..171770603 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/package.json +++ b/sdnr/wt/odlux/apps/linkCalculationApp/package.json @@ -26,18 +26,18 @@ "yup": "^0.29.3" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" }, "devDependencies": { "@types/yup": "^0.29.7", diff --git a/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx b/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx index c498379b7..61a700c71 100644 --- a/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx +++ b/sdnr/wt/odlux/apps/linkCalculationApp/src/views/linkCalculationComponent.tsx @@ -324,16 +324,16 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState <div >Site A <form > - <label>Latitude: <input className={this.state.latitude1Error.length>0 ? 'error' : 'input'} id='Lat1' type='number' onChange={(e: any) => {this.handleChange(e)} }/></label> + <label>Latitude: <input aria-label="site-a-latitude-input" className={this.state.latitude1Error.length>0 ? 'error' : 'input'} id='Lat1' type='number' onChange={(e: any) => {this.handleChange(e)} }/></label> <div style={{fontSize:12, color:'red'}}> {this.state.latitude1Error} </div></form> - <form><label>Longitude: <input className={this.state.longitude1Error.length>0 ? 'error' : 'input'} id='Lon1' type='number' onChange={(e: any) => this.handleChange(e) } /></label><div style={{fontSize:12, color:'red'}}> {this.state.longitude1Error} </div> + <form><label>Longitude: <input aria-label="site-a-longitude-input" className={this.state.longitude1Error.length>0 ? 'error' : 'input'} id='Lon1' type='number' onChange={(e: any) => this.handleChange(e) } /></label><div style={{fontSize:12, color:'red'}}> {this.state.longitude1Error} </div> </form> </div> <div>Site B <form> - <label>Latitude: <input className={this.state.latitude2Error.length>0 ? 'error' : 'input'} id='Lat2' type='number' onChange={(e: any) => {this.handleChange(e) }} /></label><div style={{fontSize:12, color:'red'}}> {this.state.latitude2Error} </div></form> - <form><label>Longitude: <input className={this.state.longitude2Error.length>0 ? 'error' : 'input'} id='Lon2' type='number' onChange={(e: any) => {this.handleChange(e) } }/></label><div style={{fontSize:12, color:'red'}}> {this.state.longitude2Error} </div></form> + <label>Latitude: <input aria-label="site-b-latitude-input" className={this.state.latitude2Error.length>0 ? 'error' : 'input'} id='Lat2' type='number' onChange={(e: any) => {this.handleChange(e) }} /></label><div style={{fontSize:12, color:'red'}}> {this.state.latitude2Error} </div></form> + <form><label>Longitude: <input aria-label="site-b-longitude-input" className={this.state.longitude2Error.length>0 ? 'error' : 'input'} id='Lon2' type='number' onChange={(e: any) => {this.handleChange(e) } }/></label><div style={{fontSize:12, color:'red'}}> {this.state.longitude2Error} </div></form> </div> </div> @@ -341,8 +341,8 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState <div className='container-1'> - <div>{<form><input type='checkbox' id='Annual' value ="Annual" checked= {this.state.worstmonth===false} onClick= {(e: any) => this.setState ({worstmonth: false})}></input>Annual - <input style={{marginLeft:10}} type='checkbox' id='Worst Month' value ="Worst" checked= {this.state.worstmonth===true} onClick= {(e:any)=>this.setState ({worstmonth: true})}></input>WM</form>}</div> + <div>{<form><input aria-label="annual" type='checkbox' id='Annual' value ="Annual" checked= {this.state.worstmonth===false} onClick= {(e: any) => this.setState ({worstmonth: false})}></input>Annual + <input aria-label="worst-month" style={{marginLeft:10}} type='checkbox' id='Worst Month' value ="Worst" checked= {this.state.worstmonth===true} onClick= {(e:any)=>this.setState ({worstmonth: true})}></input>WM</form>}</div> <div className='column1'> @@ -369,55 +369,55 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState <div className='middlecolumn'> <div >Site A</div> {this.props.siteA.length>0 &&<div> {this.props.siteA }</div>} - <div> {this.props.lat1 && this.LatLonToDMS(this.props.lat1)}</div> - <div>{this.props.lon1 && this.LatLonToDMS(this.props.lon1)}</div> + <div aria-label="site-a-latitude-dms"> {this.props.lat1 && this.LatLonToDMS(this.props.lat1)}</div> + <div aria-label="site-a-longitude-dms">{this.props.lon1 && this.LatLonToDMS(this.props.lon1)}</div> <div>0</div> - <div>{this.props.amslA.toFixed(2)} m</div> - <div>{this.props.aglA.toFixed(2)} m</div> + <div aria-label="site-a-amsl">{this.props.amslA.toFixed(2)} m</div> + <div aria-label="site-a-antenna-amsl">{this.props.aglA.toFixed(2)} m</div> <div className='column2'> - <div>{this.props.distance?.toFixed(3)} km</div> - <div>{<form><input type='checkbox' id='Horizontal' value ="Horizontal" checked= {this.props.polarization==='Horizontal'} onClick= {(e: any) => this.props.updatePolarization(e.target.value)}></input>Horizontal - <input style={{marginLeft:10}} type='checkbox' id='Vertical' value ="Vertical" checked= {this.props.polarization==='Vertical'} onClick= {(e:any)=>{this.props.updatePolarization(e.target.value)}}></input>Vertical</form>}</div> + <div aria-label="distance-between-sites">{this.props.distance?.toFixed(3)} km</div> + <div>{<form><input aria-label="polarization-horizontal" type='checkbox' id='Horizontal' value ="Horizontal" checked= {this.props.polarization==='Horizontal'} onClick= {(e: any) => this.props.updatePolarization(e.target.value)}></input>Horizontal + <input aria-label="polarization-vertical" style={{marginLeft:10}} type='checkbox' id='Vertical' value ="Vertical" checked= {this.props.polarization==='Vertical'} onClick= {(e:any)=>{this.props.updatePolarization(e.target.value)}}></input>Vertical</form>}</div> - <div> {<select className={this.state.frequencyError.length>0 ? 'error' : 'input'} onChange={(e) => { this.props.updateFrequency(Number(e.target.value)); e.target.value==='0'? this.setState({frequencyError: 'select a frequency'}): this.setState({frequencyError:''})}}> + <div> {<select aria-label="select-frequency-in-ghz" className={this.state.frequencyError.length>0 ? 'error' : 'input'} onChange={(e) => { this.props.updateFrequency(Number(e.target.value)); e.target.value==='0'? this.setState({frequencyError: 'select a frequency'}): this.setState({frequencyError:''})}}> - <option value='0' >Select Freq</option> - <option value='7' >7 GHz</option> - <option value='11' >11 GHz</option> - <option value='15' >15 GHz</option> - <option value='23' >23 GHz</option> - <option value='26' >26 GHz</option> - <option value='28' >28 GHz</option> - <option value='38' >38 GHz</option> - <option value='42' >42 GHz</option> - <option value='80' >80 GHz</option> + <option value='0' aria-label="none-value" >Select Freq</option> + <option value='7' aria-label="7" >7 GHz</option> + <option value='11' aria-label="11" >11 GHz</option> + <option value='15' aria-label="15" >15 GHz</option> + <option value='23' aria-label="23">23 GHz</option> + <option value='26' aria-label="26">26 GHz</option> + <option value='28' aria-label="28">28 GHz</option> + <option value='38' aria-label="38">38 GHz</option> + <option value='42' aria-label="42">42 GHz</option> + <option value='80' aria-label="80">80 GHz</option> </select>} <div style={{fontSize:12, color:'red'}}> {this.state.frequencyError} </div> </div> - <div>{this.props.fsl.toFixed(3)} dB</div> + <div aria-label="fspl-value">{this.props.fsl.toFixed(3)} dB</div> - <div> {<select className={this.state.rainMethodError.length>0 ? 'error' : 'input'} onChange = {(e) => {e.target.value === 'itu' ? this.setState({ rainMethodDisplay: false}):this.setState({ rainMethodDisplay: true}); e.target.value==='0'? this.setState({rainMethodError: 'select a Rain model'}): this.setState({rainMethodError:''}) }}> - <option value='0' >Select Rain Method</option> - <option value='itu' >ITU-R P.837-7</option> - <option value='manual' >Specific Rain</option> + <div> {<select aria-label="select-rain-method" className={this.state.rainMethodError.length>0 ? 'error' : 'input'} onChange = {(e) => {e.target.value === 'itu' ? this.setState({ rainMethodDisplay: false}):this.setState({ rainMethodDisplay: true}); e.target.value==='0'? this.setState({rainMethodError: 'select a Rain model'}): this.setState({rainMethodError:''}) }}> + <option value='0' aria-label="none-value" >Select Rain Method</option> + <option value='itu' aria-label="itur8377">ITU-R P.837-7</option> + <option value='manual' aria-label="manual-entry">Specific Rain</option> </select>} <div style={{fontSize:12,color:'red'}}>{this.state.rainMethodError}</div> </div> - <div> {<form><input type="number" style={{ width: 70, height: 15, fontSize: 14 }} onChange={(e) => { this.props.updateRainValue(Number(e.target.value)) }} + <div> {<form><input aria-label="rain-value" type="number" style={{ width: 70, height: 15, fontSize: 14 }} onChange={(e) => { this.props.updateRainValue(Number(e.target.value)) }} value={this.props.rainVal} disabled={this.state.rainMethodDisplay === false ? true : false}> </input> mm/hr {this.state.showWM} {this.props.month}</form> } </div> - <div>{this.props.rainAtt.toFixed(3)} dB</div> + <div aria-label="rain-attenuation-value">{this.props.rainAtt.toFixed(3)} dB</div> - <div> {<select className={this.state.attenuationMethodError.length>0 ? 'error' : 'input'} onChange = {(e) => { if (e.target.value!== ''){ this.setState({absorptionMethod : e.target.value}); this.setState({attenuationMethodError:''}) }}}> - <option value='0' >Select Absorption Method</option> - <option value='ITURP67612' >ITU-R P.676-12</option> - <option value='ITURP67611' >ITU-R P.676-11</option> - <option value='ITURP67610' >ITU-R P.676-10</option> + <div> {<select aria-label="select-absorption-method" className={this.state.attenuationMethodError.length>0 ? 'error' : 'input'} onChange = {(e) => { if (e.target.value!== ''){ this.setState({absorptionMethod : e.target.value}); this.setState({attenuationMethodError:''}) }}}> + <option value='0' aria-label="none-value" >Select Absorption Method</option> + <option value='ITURP67612' aria-label="iturp67612" >ITU-R P.676-12</option> + <option value='ITURP67611' aria-label="iturp67611" >ITU-R P.676-11</option> + <option value='ITURP67610' aria-label="iturp67610" >ITU-R P.676-10</option> </select>} <div style={{fontSize:12,color:'red'}}>{this.state.attenuationMethodError}</div> </div> - <div>{this.props.absorptionOxygen.toFixed(3)} dB</div> - <div>{this.props.absorptionWater.toFixed(3)} dB</div> - <div>{<button style={{color: '#222', fontFamily:'Arial', boxAlign: 'center', display:'inline-block', insetInlineStart: '20' , alignSelf:'center' }} + <div aria-label="absorption-oxygen-value">{this.props.absorptionOxygen.toFixed(3)} dB</div> + <div aria-label="absorption-water-value">{this.props.absorptionWater.toFixed(3)} dB</div> + <div>{<button aria-label="calculate-button" style={{color: '#222', fontFamily:'Arial', boxAlign: 'center', display:'inline-block', insetInlineStart: '20' , alignSelf:'center' }} onClick = {(e) => this.buttonHandler()} >Calculate</button>} </div> @@ -426,11 +426,11 @@ class LinkCalculation extends React.Component<linkCalculationProps, initialState <div className= 'middlecolumn'> <div >Site B</div> {this.props.siteB.length>0 &&<div> {this.props.siteB}</div>} - <div> {this.props.lat2 && this.LatLonToDMS(this.props.lat2)}</div> - <div>{this.props.lon2 && this.LatLonToDMS(this.props.lon2)}</div> + <div aria-label="site-b-latitude-dms"> {this.props.lat2 && this.LatLonToDMS(this.props.lat2)}</div> + <div aria-label="site-b-longitude-dms">{this.props.lon2 && this.LatLonToDMS(this.props.lon2)}</div> <div>0</div> - <div>{this.props.amslB.toFixed(2)} m</div> - <div>{this.props.aglB.toFixed(2)} m</div> + <div aria-label="site-b-asml">{this.props.amslB.toFixed(2)} m</div> + <div aria-label="site-b-antenna-asml">{this.props.aglB.toFixed(2)} m</div> </div> diff --git a/sdnr/wt/odlux/apps/maintenanceApp/package.json b/sdnr/wt/odlux/apps/maintenanceApp/package.json index da5b3d288..d2980f1dc 100644 --- a/sdnr/wt/odlux/apps/maintenanceApp/package.json +++ b/sdnr/wt/odlux/apps/maintenanceApp/package.json @@ -25,17 +25,17 @@ "@odlux/connect-app": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/maintenanceApp/src/components/refreshMaintenanceEntries.tsx b/sdnr/wt/odlux/apps/maintenanceApp/src/components/refreshMaintenanceEntries.tsx new file mode 100644 index 000000000..1a00c70c4 --- /dev/null +++ b/sdnr/wt/odlux/apps/maintenanceApp/src/components/refreshMaintenanceEntries.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { maintenanceEntriesReloadAction } from '../handlers/maintenenceEntriesHandler'; +import { MaintenenceEntry } from '../models/maintenenceEntryType'; + +export enum RefreshMaintenanceEntriesDialogMode { + None = "none", + RefreshMaintenanceEntriesTable = "RefreshMaintenanceEntriesTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshMaintenanceEntries: () => dispatcher.dispatch(maintenanceEntriesReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshMaintenanceEntriesDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshMaintenanceEntriesDialogMode.RefreshMaintenanceEntriesTable]: { + dialogTitle: "Do you want to refresh Maintenance Entries?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshMaintenanceEntriesDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshMaintenanceEntriesDialogMode; + onClose: () => void; +}; + +type RefreshMaintenanceEntriesDialogComponentState = MaintenenceEntry & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshMaintenanceEntriesDialogComponent extends React.Component<RefreshMaintenanceEntriesDialogComponentProps, RefreshMaintenanceEntriesDialogComponentState> { + constructor(props: RefreshMaintenanceEntriesDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshMaintenanceEntriesDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshMaintenanceEntries(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshMaintenanceEntriesDialog = connect(undefined, mapDispatch)(RefreshMaintenanceEntriesDialogComponent); +export default RefreshMaintenanceEntriesDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/maintenanceApp/src/views/maintenenceView.tsx b/sdnr/wt/odlux/apps/maintenanceApp/src/views/maintenenceView.tsx index f52d46d81..6b5071d90 100644 --- a/sdnr/wt/odlux/apps/maintenanceApp/src/views/maintenenceView.tsx +++ b/sdnr/wt/odlux/apps/maintenanceApp/src/views/maintenenceView.tsx @@ -25,9 +25,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import AddIcon from '@material-ui/icons/Add'; import EditIcon from '@material-ui/icons/Edit'; import RemoveIcon from '@material-ui/icons/RemoveCircleOutline'; - -import Button from '@material-ui/core/Button'; -import IconButton from '@material-ui/core/IconButton'; +import Refresh from '@material-ui/icons/Refresh'; +import { MenuItem, Divider, Typography } from '@material-ui/core'; import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect'; import MaterialTable, { MaterialTableCtorType, ColumnType } from '../../../../framework/src/components/material-table'; @@ -36,6 +35,7 @@ import { IApplicationStoreState } from '../../../../framework/src/store/applicat import { MaintenenceEntry, spoofSymbol } from '../models/maintenenceEntryType'; import EditMaintenenceEntryDialog, { EditMaintenenceEntryDialogMode } from '../components/editMaintenenceEntryDialog'; +import RefreshMaintenanceEntriesDialog, { RefreshMaintenanceEntriesDialogMode } from '../components/refreshMaintenanceEntries'; import { convertToLocaleString } from '../utils/timeUtils'; import { createmaintenanceEntriesActions, createmaintenanceEntriesProperties, maintenanceEntriesReloadAction } from '../handlers/maintenenceEntriesHandler'; @@ -81,6 +81,7 @@ type MaintenenceViewComponentProps = Connect<typeof mapProps, typeof mapDispatch type MaintenenceViewComponentState = { maintenenceEntryToEdit: MaintenenceEntry; maintenenceEntryEditorMode: EditMaintenenceEntryDialogMode; + refreshMaintenenceEntriesEditorMode: RefreshMaintenanceEntriesDialogMode; }; let initialSorted = false; @@ -93,10 +94,22 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP this.state = { maintenenceEntryToEdit: emptyMaintenenceEntry, maintenenceEntryEditorMode: EditMaintenenceEntryDialogMode.None, + refreshMaintenenceEntriesEditorMode: RefreshMaintenanceEntriesDialogMode.None }; } + getContextMenu(rowData: MaintenenceEntry): JSX.Element[] { + let buttonArray = [ + <MenuItem aria-label={"1hr-from-now"} onClick={event => this.onOpenPlus1hEditMaintenenceEntryDialog(event, rowData)}><Typography>+1h</Typography></MenuItem>, + <MenuItem aria-label={"8hr-from-now"} onClick={event => this.onOpenPlus8hEditMaintenenceEntryDialog(event, rowData)}><Typography>+8h</Typography></MenuItem>, + <Divider />, + <MenuItem aria-label={"edit"} onClick={event => this.onOpenEditMaintenenceEntryDialog(event, rowData)}><EditIcon /><Typography>Edit</Typography></MenuItem>, + <MenuItem aria-label={"remove"} onClick={event => this.onOpenRemoveMaintenenceEntryDialog(event, rowData)}><RemoveIcon /><Typography>Remove</Typography></MenuItem> + ]; + return buttonArray; + } + render() { const { classes } = this.props; const addMaintenenceEntryAction = { @@ -113,10 +126,19 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP }); } }; + + const refreshMaintenanceEntriesAction = { + icon: Refresh, tooltip: 'Refresh Maintenance Entries', onClick: () => { + this.setState({ + refreshMaintenenceEntriesEditorMode: RefreshMaintenanceEntriesDialogMode.RefreshMaintenanceEntriesTable + }); + } + }; + const now = new Date().valueOf(); return ( <> - <MaintenenceEntriesTable stickyHeader tableId="maintenance-table" title={"Maintenance"} customActionButtons={[addMaintenenceEntryAction]} columns={ + <MaintenenceEntriesTable stickyHeader tableId="maintenance-table" title={"Maintenance"} customActionButtons={[refreshMaintenanceEntriesAction, addMaintenenceEntryAction]} columns={ [ { property: "nodeId", title: "Node Name", type: ColumnType.text }, { @@ -126,25 +148,16 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP }, { property: "active", title: "Activation State", type: ColumnType.boolean, labels: { "true": "active", "false": "not active" }, }, { property: "start", title: "Start Date (UTC)", type: ColumnType.text }, - { property: "end", title: "End Date (UTC)", type: ColumnType.text }, - { - property: "actions", title: "Actions", type: ColumnType.custom, customControl: ({ rowData }) => ( - <> - <div className={classes.spacer}> - <Tooltip title={"1h from now"} ><Button className={classes.button} onClick={(event) => this.onOpenPlus1hEditMaintenenceEntryDialog(event, rowData)} >+1h</Button></Tooltip> - <Tooltip title={"8h from now"} ><Button className={classes.button} onClick={(event) => this.onOpenPlus8hEditMaintenenceEntryDialog(event, rowData)} >+8h</Button></Tooltip> - </div> - <div className={classes.spacer}> - <Tooltip title={"Edit"} ><IconButton className={classes.button} onClick={(event) => this.onOpenEditMaintenenceEntryDialog(event, rowData)} ><EditIcon /></IconButton></Tooltip> - <Tooltip title={"Remove"} ><IconButton disabled={!!rowData[spoofSymbol]} className={classes.button} onClick={(event) => this.onOpenRemoveMaintenenceEntryDialog(event, rowData)} ><RemoveIcon /></IconButton></Tooltip> - </div> - </> - ) - }, + { property: "end", title: "End Date (UTC)", type: ColumnType.text } ] - } idProperty={'_id'}{...this.props.maintenanceEntriesActions} {...this.props.maintenanceEntriesProperties} asynchronus > </MaintenenceEntriesTable> + } idProperty={'_id'}{...this.props.maintenanceEntriesActions} {...this.props.maintenanceEntriesProperties} asynchronus createContextMenu={rowData => { + return this.getContextMenu(rowData); + }} > + </MaintenenceEntriesTable> <EditMaintenenceEntryDialog initialMaintenenceEntry={this.state.maintenenceEntryToEdit} mode={this.state.maintenenceEntryEditorMode} onClose={this.onCloseEditMaintenenceEntryDialog} /> + <RefreshMaintenanceEntriesDialog mode={this.state.refreshMaintenenceEntriesEditorMode} + onClose={this.onCloseRefreshMaintenenceEntryDialog} /> </> ); } @@ -162,8 +175,8 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP } private onOpenPlus1hEditMaintenenceEntryDialog = (event: React.MouseEvent<HTMLElement>, entry: MaintenenceEntry) => { - event.preventDefault(); - event.stopPropagation(); + // event.preventDefault(); + // event.stopPropagation(); const startTime = (new Date().valueOf()); const endTime = startTime + (1 * 60 * 60 * 1000); this.setState({ @@ -177,8 +190,8 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP } private onOpenPlus8hEditMaintenenceEntryDialog = (event: React.MouseEvent<HTMLElement>, entry: MaintenenceEntry) => { - event.preventDefault(); - event.stopPropagation(); + // event.preventDefault(); + // event.stopPropagation(); const startTime = (new Date().valueOf()); const endTime = startTime + (8 * 60 * 60 * 1000); this.setState({ @@ -192,8 +205,8 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP } private onOpenEditMaintenenceEntryDialog = (event: React.MouseEvent<HTMLElement>, entry: MaintenenceEntry) => { - event.preventDefault(); - event.stopPropagation(); + // event.preventDefault(); + // event.stopPropagation(); const startTime = (new Date().valueOf()); const endTime = startTime; this.setState({ @@ -208,8 +221,8 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP } private onOpenRemoveMaintenenceEntryDialog = (event: React.MouseEvent<HTMLElement>, entry: MaintenenceEntry) => { - event.preventDefault(); - event.stopPropagation(); + // event.preventDefault(); + // event.stopPropagation(); const startTime = (new Date().valueOf()); const endTime = startTime; this.setState({ @@ -229,7 +242,13 @@ class MaintenenceViewComponent extends React.Component<MaintenenceViewComponentP maintenenceEntryEditorMode: EditMaintenenceEntryDialogMode.None, }); } + + private onCloseRefreshMaintenenceEntryDialog = () => { + this.setState({ + refreshMaintenenceEntriesEditorMode: RefreshMaintenanceEntriesDialogMode.None, + }); + } } export const MaintenenceView = withStyles(styles)(connect(mapProps, mapDispatcher)(MaintenenceViewComponent)); -export default MaintenenceView;
\ No newline at end of file +export default MaintenenceView; diff --git a/sdnr/wt/odlux/apps/mediatorApp/package.json b/sdnr/wt/odlux/apps/mediatorApp/package.json index ddce56757..0f0e74673 100644 --- a/sdnr/wt/odlux/apps/mediatorApp/package.json +++ b/sdnr/wt/odlux/apps/mediatorApp/package.json @@ -25,17 +25,17 @@ }, "peerDependencies": { "@fortawesome/free-solid-svg-icons": "5.6.3", - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/mediatorApp/src/components/refreshMediatorDialog.tsx b/sdnr/wt/odlux/apps/mediatorApp/src/components/refreshMediatorDialog.tsx new file mode 100644 index 000000000..af94f5a32 --- /dev/null +++ b/sdnr/wt/odlux/apps/mediatorApp/src/components/refreshMediatorDialog.tsx @@ -0,0 +1,117 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============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 { avaliableMediatorServersReloadAction } from '../handlers/avaliableMediatorServersHandler'; +import { IDispatcher, connect, Connect } from '../../../../framework/src/flux/connect'; + +import { MediatorServer } from '../models/mediatorServer'; + +export enum RefreshMediatorDialogMode { + None = "none", + RefreshMediatorTable = "RefreshMediatorTable", +} + +const mapDispatch = (dispatcher: IDispatcher) => ({ + refreshMediator: () => dispatcher.dispatch(avaliableMediatorServersReloadAction) +}); + +type DialogSettings = { + dialogTitle: string, + dialogDescription: string, + applyButtonText: string, + cancelButtonText: string, + enableMountIdEditor: boolean, + enableUsernameEditor: boolean, + enableExtendedEditor: boolean, +} + +const settings: { [key: string]: DialogSettings } = { + [RefreshMediatorDialogMode.None]: { + dialogTitle: "", + dialogDescription: "", + applyButtonText: "", + cancelButtonText: "", + enableMountIdEditor: false, + enableUsernameEditor: false, + enableExtendedEditor: false, + }, + [RefreshMediatorDialogMode.RefreshMediatorTable]: { + dialogTitle: "Do you want to refresh the Mediator table?", + dialogDescription: "", + applyButtonText: "Yes", + cancelButtonText: "Cancel", + enableMountIdEditor: true, + enableUsernameEditor: true, + enableExtendedEditor: true, + } +} + +type RefreshMediatorDialogComponentProps = Connect<undefined, typeof mapDispatch> & { + mode: RefreshMediatorDialogMode; + onClose: () => void; +}; + +type RefreshMediatorDialogComponentState = MediatorServer & { isNameValid: boolean, isHostSet: boolean }; + +class RefreshMediatorDialogComponent extends React.Component<RefreshMediatorDialogComponentProps, RefreshMediatorDialogComponentState> { + constructor(props: RefreshMediatorDialogComponentProps) { + super(props); + } + + render(): JSX.Element { + const setting = settings[this.props.mode]; + return ( + <Dialog open={this.props.mode !== RefreshMediatorDialogMode.None}> + <DialogTitle id="form-dialog-title" aria-label={`${setting.dialogTitle.replace(/ /g, "-").toLowerCase()}-dialog`}>{setting.dialogTitle}</DialogTitle> + <DialogContent> + <DialogContentText> + {setting.dialogDescription} + </DialogContentText> + </DialogContent> + <DialogActions> + <Button aria-label="dialog-confirm-button" onClick={(event) => { + this.onRefresh(); + }} > {setting.applyButtonText} </Button> + <Button aria-label="dialog-cancel-button" onClick={(event) => { + this.onCancel(); + }} color="secondary"> {setting.cancelButtonText} </Button> + </DialogActions> + </Dialog> + ) + } + + private onRefresh = () => { + this.props.refreshMediator(); + this.props.onClose(); + }; + + private onCancel = () => { + this.props.onClose(); + } +} + +export const RefreshMediatorDialog = connect(undefined, mapDispatch)(RefreshMediatorDialogComponent); +export default RefreshMediatorDialog;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/mediatorApp/src/views/mediatorServerSelection.tsx b/sdnr/wt/odlux/apps/mediatorApp/src/views/mediatorServerSelection.tsx index c16906ad0..0f4ebbe0b 100644 --- a/sdnr/wt/odlux/apps/mediatorApp/src/views/mediatorServerSelection.tsx +++ b/sdnr/wt/odlux/apps/mediatorApp/src/views/mediatorServerSelection.tsx @@ -22,6 +22,7 @@ import AddIcon from '@material-ui/icons/Add'; import IconButton from '@material-ui/core/IconButton'; import EditIcon from '@material-ui/icons/Edit'; import DeleteIcon from '@material-ui/icons/Delete'; +import Refresh from '@material-ui/icons/Refresh'; import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; import connect, { IDispatcher, Connect } from '../../../../framework/src/flux/connect'; @@ -31,6 +32,7 @@ import { createAvaliableMediatorServersProperties, createAvaliableMediatorServer import { MediatorServer } from '../models/mediatorServer'; import EditMediatorServerDialog, { EditMediatorServerDialogMode } from '../components/editMediatorServerDialog'; +import RefreshMediatorDialog, { RefreshMediatorDialogMode } from '../components/refreshMediatorDialog'; import { NavigateToApplication } from '../../../../framework/src/actions/navigationActions'; const MediatorServersTable = MaterialTable as MaterialTableCtorType<MediatorServer>; @@ -67,7 +69,8 @@ type MediatorServerSelectionComponentProps = Connect<typeof mapProps, typeof map type MediatorServerSelectionComponentState = { mediatorServerToEdit: MediatorServer, - mediatorServerEditorMode: EditMediatorServerDialogMode + mediatorServerEditorMode: EditMediatorServerDialogMode, + refreshMediatorEditorMode: RefreshMediatorDialogMode } let initialSorted = false; @@ -80,11 +83,19 @@ class MediatorServerSelectionComponent extends React.Component<MediatorServerSel this.state = { mediatorServerEditorMode: EditMediatorServerDialogMode.None, mediatorServerToEdit: emptyMediatorServer, + refreshMediatorEditorMode: RefreshMediatorDialogMode.None } } render() { const { classes } = this.props; + const refreshMediatorAction = { + icon: Refresh, tooltip: 'Refresh Mediator Server Table', onClick: () => { + this.setState({ + refreshMediatorEditorMode: RefreshMediatorDialogMode.RefreshMediatorTable + }); + } + }; const addMediatorServerActionButton = { icon: AddIcon, tooltip: 'Add', onClick: () => { @@ -96,7 +107,7 @@ class MediatorServerSelectionComponent extends React.Component<MediatorServerSel }; return ( <> - <MediatorServersTable stickyHeader title={"Mediator"} customActionButtons={[addMediatorServerActionButton]} idProperty={"id"} + <MediatorServersTable stickyHeader title={"Mediator"} customActionButtons={[refreshMediatorAction, addMediatorServerActionButton]} idProperty={"id"} {...this.props.mediatorServersActions} {...this.props.mediatorServersProperties} columns={[ { property: "name", title: "Name", type: ColumnType.text }, { property: "url", title: "Url", type: ColumnType.text }, @@ -113,6 +124,10 @@ class MediatorServerSelectionComponent extends React.Component<MediatorServerSel mediatorServer={this.state.mediatorServerToEdit} mode={this.state.mediatorServerEditorMode} onClose={this.onCloseEditMediatorServerDialog} /> + <RefreshMediatorDialog + mode={this.state.refreshMediatorEditorMode} + onClose={this.onCloseRefreshMediatorDialog} + /> </> ); } @@ -158,6 +173,11 @@ class MediatorServerSelectionComponent extends React.Component<MediatorServerSel mediatorServerToEdit: emptyMediatorServer, }); } + private onCloseRefreshMediatorDialog = () => { + this.setState({ + refreshMediatorEditorMode: RefreshMediatorDialogMode.None + }); + } } diff --git a/sdnr/wt/odlux/apps/minimumApp/package.json b/sdnr/wt/odlux/apps/minimumApp/package.json index 069cb138c..bcda2fb58 100644 --- a/sdnr/wt/odlux/apps/minimumApp/package.json +++ b/sdnr/wt/odlux/apps/minimumApp/package.json @@ -24,17 +24,17 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/networkMapApp/package.json b/sdnr/wt/odlux/apps/networkMapApp/package.json index 7119f1b38..cde312ce3 100644 --- a/sdnr/wt/odlux/apps/networkMapApp/package.json +++ b/sdnr/wt/odlux/apps/networkMapApp/package.json @@ -27,18 +27,18 @@ "object.values": "^1.1.1" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } } diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx index b2c724636..57091aebf 100644 --- a/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx +++ b/sdnr/wt/odlux/apps/networkMapApp/src/components/details/linkDetails.tsx @@ -68,7 +68,9 @@ const LinkDetails: React.FunctionComponent<props> = (props) => { const distance = props.link.length > 0 ? props.link.length : props.link.calculatedLength; const azimuthA = props.link.azimuthA; const azimuthB = props.link.azimuthB; - window.open(`/#/linkCalculation?lat1=${siteA.lat}&lon1=${siteA.lon}&lat2=${siteB.lat}&lon2=${siteB.lon}&siteA=${nameA}&siteB=${nameB}&azimuthA=${azimuthA}&azimuthB=${azimuthB}&distance=${distance}&amslSiteA=${siteA.amsl}&AGLsiteA=${siteA.antennaHeight}&amslSiteB=${siteB.amsl}&AGLsiteB=${siteB.antennaHeight}`) + + const baseUrl = window.location.pathname.split('#')[0]; + window.open(`${baseUrl}#/linkCalculation?lat1=${siteA.lat}&lon1=${siteA.lon}&lat2=${siteB.lat}&lon2=${siteB.lon}&siteA=${nameA}&siteB=${nameB}&azimuthA=${azimuthA}&azimuthB=${azimuthB}&distance=${distance}&amslSiteA=${siteA.amsl}&AGLsiteA=${siteA.antennaHeight}&amslSiteB=${siteB.amsl}&AGLsiteB=${siteB.antennaHeight}`) } diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts index b820746b5..442a5083c 100644 --- a/sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts +++ b/sdnr/wt/odlux/apps/networkMapApp/src/handlers/mapReducer.ts @@ -56,12 +56,12 @@ export const MapReducer: IActionHandler<mapState> = (state=initialState, action: } else if(action instanceof HighlightSiteAction){ - state = Object.assign({}, state, {selectedLink: null, selectedSite:{type: "Feature", properties: {id: action.site.name, type:action.site.type}, geometry:{type:"Point", coordinates:[action.site.location.lon,action.site.location.lat ]}}}) + state = Object.assign({}, state, {selectedLink: null, selectedSite:{type: "Feature", properties: {id: action.site.id, type:action.site.type}, geometry:{type:"Point", coordinates:[action.site.location.lon,action.site.location.lat ]}}}) }else if (action instanceof ZoomToSearchResultAction){ state = Object.assign({}, state, {zoomToElement:{lat: action.lat, lon: action.lon}}); }else if (action instanceof AddAlarmAction){ - state = Object.assign({}, state, {alarmlement:{type: "Feature", properties: {id: action.site.name, type:action.site.type}, geometry:{type:"Point", coordinates:[action.site.location.lon,action.site.location.lat ]}}}); + state = Object.assign({}, state, {alarmlement:{type: "Feature", properties: {id: action.site.id, type:action.site.type}, geometry:{type:"Point", coordinates:[action.site.location.lon,action.site.location.lat ]}}}); }else if(action instanceof SetCoordinatesAction){ state = Object.assign({}, state, {lat:action.lat, lon: action.lon, zoom:action.zoom}); diff --git a/sdnr/wt/odlux/apps/networkMapApp/src/index.html b/sdnr/wt/odlux/apps/networkMapApp/src/index.html index 9a7f77a29..e64c08c1e 100644 --- a/sdnr/wt/odlux/apps/networkMapApp/src/index.html +++ b/sdnr/wt/odlux/apps/networkMapApp/src/index.html @@ -20,8 +20,8 @@ require(["app","connectApp","faultApp", "networkMapApp", "configurationApp", "linkCalculationApp"], function (app, connectApp, faultApp, networkMapApp, configurationApp, linkCalculationApp) { connectApp.register(); //faultApp.register(); - configurationApp.register(); - linkCalculationApp.register(); + //configurationApp.register(); + //linkCalculationApp.register(); networkMapApp.register(); app("./app.tsx").runApplication(); }); diff --git a/sdnr/wt/odlux/apps/performanceHistoryApp/package.json b/sdnr/wt/odlux/apps/performanceHistoryApp/package.json index 2b69cc3c7..305fc07f4 100644 --- a/sdnr/wt/odlux/apps/performanceHistoryApp/package.json +++ b/sdnr/wt/odlux/apps/performanceHistoryApp/package.json @@ -27,17 +27,17 @@ "chart.js": "2.8.0" }, "peerDependencies": { - "@types/react": "16.9.19", - "@types/react-dom": "16.9.5", - "@types/react-router-dom": "4.3.1", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-router-dom": "5.1.7", "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", "@types/classnames": "2.2.6", "@types/flux": "3.1.8", "@types/jquery": "3.3.10", "jquery": "3.3.1", - "react": "16.12.0", - "react-dom": "16.12.0", - "react-router-dom": "4.3.1" + "react": "17.0.1", + "react-dom": "17.0.1", + "react-router-dom": "5.2.0" } }
\ No newline at end of file |