diff options
author | Michael DÜrre <michael.duerre@highstreet-technologies.com> | 2021-04-08 07:27:18 +0200 |
---|---|---|
committer | Michael DÜrre <michael.duerre@highstreet-technologies.com> | 2021-04-08 07:27:28 +0200 |
commit | 21e4a946cd24b8a03ea577352f0271ebf7669ffa (patch) | |
tree | 4227d8566770b75c2c25b67c764038288cacfe3d /sdnr/wt/odlux/apps/configurationApp/src | |
parent | a252be83694ae33260d99d5371ed48c1558aa2e8 (diff) |
update odlux for notification change
update due new notification protocol
Issue-ID: CCSDK-3253
Signed-off-by: Michael DÜrre <michael.duerre@highstreet-technologies.com>
Change-Id: Iad65459fdc18603cd1ddbd97bb2211308744bd8b
Diffstat (limited to 'sdnr/wt/odlux/apps/configurationApp/src')
10 files changed, 878 insertions, 723 deletions
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, }; |