From e3ad1d3884cb4c801679e3390088ce17c997f9d1 Mon Sep 17 00:00:00 2001 From: Aijana Schumann Date: Fri, 4 Dec 2020 17:40:42 +0100 Subject: Update ConfigurationApp Add grouping per yang module Issue-ID: CCSDK-3023 Signed-off-by: Aijana Schumann Change-Id: I7fd15d0a7dc982c6d824e679b5a0d1eeaaa2e7a8 --- .../configurationApp/src/actions/deviceActions.ts | 56 +- .../configurationApp/src/components/baseProps.ts | 2 +- .../src/components/uiElementReference.tsx | 2 +- .../src/components/uiElementString.tsx | 4 +- .../apps/configurationApp/src/models/uiModels.ts | 17 +- .../odlux/apps/configurationApp/src/models/yang.ts | 8 + .../configurationApp/src/services/restServices.ts | 46 +- .../src/views/configurationApplication.tsx | 1339 +++++++++++--------- .../apps/configurationApp/src/yang/yangParser.ts | 113 +- .../odlux/apps/configurationApp/webpack.config.js | 26 +- sdnr/wt/odlux/framework/pom.xml | 2 +- sdnr/wt/odlux/framework/src/assets/version.json | 5 +- 12 files changed, 935 insertions(+), 685 deletions(-) (limited to 'sdnr') diff --git a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts index 83134fc92..d7babc156 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts @@ -8,8 +8,7 @@ import { DisplayModeType, DisplaySpecification } from '../handlers/viewDescripti import { restService } from "../services/restServices"; import { YangParser } from "../yang/yangParser"; import { Module } from "../models/yang"; -import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList, isViewElementRpc, isViewElementChoise, ViewElementChoiseCase } from "../models/uiModels"; -import { element } from 'prop-types'; +import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList, isViewElementRpc, isViewElementChoise, ViewElementChoiseCase, ViewElementString } from "../models/uiModels"; export class EnableValueSelector extends Action { constructor(public listSpecification: ViewSpecification, public listData: any[], public keyProperty: string, public onValueSelected : (value: any) => void ) { @@ -52,22 +51,26 @@ export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatc dispatch(new UpdateDeviceDescription("", {}, [])); dispatch(new SetCollectingSelectionData(true)); - const availableCapabilities = await restService.getCapabilitiesByMoutId(nodeId); + const { avaliableCapabilities, unavaliableCapabilities } = await restService.getCapabilitiesByMoutId(nodeId); - if (!availableCapabilities || availableCapabilities.length <= 0) { + if (!avaliableCapabilities || avaliableCapabilities.length <= 0) { throw new Error(`NetworkElement : [${nodeId}] has no capabilities.`); } - - const parser = new YangParser(); - + const capParser = /^\(.*\?revision=(\d{4}-\d{2}-\d{2})\)(\S+)$/i; - for (let i = 0; i < availableCapabilities.length; ++i){ - const capRaw = availableCapabilities[i]; + + const parser = new YangParser(unavaliableCapabilities?.map(cap => { + const capMatch = cap && capParser.exec(cap.capability); + return { capability:capMatch && capMatch[2] || '', failureReason: cap.failureReason }; + }) || undefined); + + for (let i = 0; i < avaliableCapabilities.length; ++i){ + const capRaw = avaliableCapabilities[i]; const capMatch = capRaw && capParser.exec(capRaw.capability); try { capMatch && await parser.addCapability(capMatch[2], capMatch[1]); } catch (err) { - console.error(err); + console.error(`Error in ${capMatch && capMatch[2]} ${capMatch && capMatch[1]}`, err); } } @@ -75,6 +78,10 @@ export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatc dispatch(new SetCollectingSelectionData(false)); + if (process.env.NODE_ENV === "development" ) { + console.log(parser, parser.modules, parser.views); + } + return dispatch(new UpdateDeviceDescription(nodeId, parser.modules, parser.views)); } @@ -317,8 +324,33 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, "%2F")}]`))); })); } else { + // Found a list at root level of a module w/o a refenrece key. + dataPath += `?content=config&fields=${encodeURIComponent(viewElement.id)}(${encodeURIComponent(viewElement.key || '')})`; + const restResult = (await restService.getConfigData(dataPath)); + if (restResult && restResult.status === 200 && restResult.data && restResult.data[viewElement.id] ){ + // spoof the not existing view here + const refData = restResult.data[viewElement.id]; + const refView : ViewSpecification = { + id: "-1", + canEdit: false, + language: "en-US", + elements: { + [viewElement.key!] : { + uiType: "string", + config: false, + id: viewElement.key, + label: viewElement.key, + isList: true, + } as ViewElementString + } + }; + dispatch(new EnableValueSelector(refView, refData, viewElement.key!, (refKey) => { + window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, "%2F")}]`))); + })); + } else { + throw new Error("Found a list at root level of a module and could not determine the keys."); + } dispatch(new SetCollectingSelectionData(false)); - throw new Error("Found a list at root level of a module w/o a refenrece key."); } return; } @@ -392,7 +424,7 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: // extract the list -> key: list data = extractList - ? data[viewElement!.label] || [] // if the list is empty, it does not exist + ? data[viewElement!.id] || data[viewElement!.label] || [] // if the list is empty, it does not exist : data; } else if (viewElement! && viewElement!.uiType === "rpc") { diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts b/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts index c08f5c9bc..26c3944c9 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts @@ -23,6 +23,6 @@ export type BaseProps = { inputValue: TValue, readOnly: boolean, disabled: boolean, - onChange(newValue: TValue): void, + onChange(newValue: TValue): void; isKey?: boolean }; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx index b95df1f51..223c4cb25 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx @@ -38,7 +38,7 @@ export const UIElementReference: React.FC = (props) => const { element } = props; return ( - + diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx index 122f7150a..f87b94f1d 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx @@ -23,7 +23,7 @@ import { BaseProps } from "./baseProps"; import { IfWhenTextInput } from "./ifWhenTextInput"; import { checkRange, checkPattern } from "./verifyer"; -type stringEntryProps = BaseProps; +type stringEntryProps = BaseProps ; export const UiElementString = (props: stringEntryProps) => { @@ -81,4 +81,4 @@ export const UiElementString = (props: stringEntryProps) => { /> ); -} +} \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts index a5a52fc2e..9c03bdf9b 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts @@ -19,6 +19,8 @@ export type ViewElementBase = { "id": string; "label": string; + "module": string; + "path": string; "config": boolean; "ifFeature"?: string; "when"?: string; @@ -52,6 +54,14 @@ export type ViewElementString = ViewElementBase & { "invertMatch"?: true; } +// special case derived from +export type ViewElementDate = ViewElementBase & { + "uiType": "date"; + "pattern"?: Expression; + "length"?: Expression; + "invertMatch"?: true; +} + // https://tools.ietf.org/html/rfc7950#section-9.3 export type ViewElementNumber = ViewElementBase & { "uiType": "number"; @@ -134,6 +144,7 @@ export type ViewElement = | ViewElementBits | ViewElementBinary | ViewElementString + | ViewElementDate | ViewElementNumber | ViewElementBoolean | ViewElementObject @@ -145,7 +156,11 @@ export type ViewElement = | ViewElementRpc; export const isViewElementString = (viewElement: ViewElement): viewElement is ViewElementString => { - return viewElement && viewElement.uiType === "string"; + return viewElement && (viewElement.uiType === "string" || viewElement.uiType === "date"); +} + +export const isViewElementDate = (viewElement: ViewElement): viewElement is ViewElementDate => { + return viewElement && (viewElement.uiType === "date"); } export const isViewElementNumber = (viewElement: ViewElement): viewElement is ViewElementNumber => { diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts index 11eb44d92..e4ab6f59f 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts @@ -17,6 +17,13 @@ */ import { ViewElement, ViewSpecification } from "./uiModels"; +import { StepLabel } from "@material-ui/core"; + +export enum ModuleState { + stable, + instable, + unabaliabe, +} export type Token = { name: string; @@ -50,6 +57,7 @@ export type Module = { name: string; namespace?: string; prefix?: string; + state: ModuleState; identities: { [name: string]: Identity }; revisions: { [version: string]: Revision }; imports: { [prefix: string]: string }; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts index eb2c67c26..239a8e448 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts @@ -21,12 +21,50 @@ import { convertPropertyNames, replaceHyphen } from "../../../../framework/src/u import { NetworkElementConnection } from "../models/networkElementConnection"; +type CapabilityResponse = { + "network-topology:node": { + "node-id": string, + "netconf-node-topology:available-capabilities": { + "available-capability": { + "capability-origin": string, + "capability": string, + }[] + }, + "netconf-node-topology:unavailable-capabilities": { + "unavailable-capability": { + "capability": string, + "failure-reason": string, + }[] + } + }[] +} + +type CapabilityAnswer = { + avaliableCapabilities: { + capabilityOrigin: string, + capability: string + }[] | null , + unavaliableCapabilities: { + failureReason: string, + capability: string + }[] | null , +} + class RestService { - public async getCapabilitiesByMoutId(nodeId: string): Promise<{ "capabilityOrigin": string, "capability": string }[] | null> { + public async getCapabilitiesByMoutId(nodeId: string): Promise { const path = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}`; - const capabilitiesResult = await requestRest<{"network-topology:node": {"node-id": string, "netconf-node-topology:available-capabilities": { "available-capability": { "capability-origin": string, "capability": string }[] }}[] }>(path, { method: "GET" }); - return capabilitiesResult && capabilitiesResult["network-topology:node"] && capabilitiesResult["network-topology:node"].length > 0 && - capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"]["available-capability"].map(obj => convertPropertyNames(obj, replaceHyphen)) || null; + const capabilitiesResult = await requestRest(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"] && + capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"]["available-capability"] && + capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"]["available-capability"].map(obj => convertPropertyNames(obj, replaceHyphen)) || []; + + const unavaliableCapabilities = capabilitiesResult && capabilitiesResult["network-topology:node"] && capabilitiesResult["network-topology:node"].length > 0 && + capabilitiesResult["network-topology:node"][0]["netconf-node-topology:unavailable-capabilities"] && + capabilitiesResult["network-topology:node"][0]["netconf-node-topology:unavailable-capabilities"]["unavailable-capability"] && + capabilitiesResult["network-topology:node"][0]["netconf-node-topology:unavailable-capabilities"]["unavailable-capability"].map(obj => convertPropertyNames(obj, replaceHyphen)) || [] + + return { avaliableCapabilities, unavaliableCapabilities }; } public async getMountedNetworkElementByMountId(nodeId: string): Promise { diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx index 3b1df6f87..45b3081c2 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -29,7 +29,7 @@ import { renderObject } from '../../../../framework/src/components/objectDump'; 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 } from "../models/uiModels"; +import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion, isViewElementRpc, ViewElementRpc, isViewElementEmpty, isViewElementDate } from "../models/uiModels"; import Fab from '@material-ui/core/Fab'; import AddIcon from '@material-ui/icons/Add'; @@ -45,8 +45,14 @@ import InputLabel from "@material-ui/core/InputLabel"; import Select from "@material-ui/core/Select"; import MenuItem from "@material-ui/core/MenuItem"; import Breadcrumbs from "@material-ui/core/Breadcrumbs"; -import { Button } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; import Link from "@material-ui/core/Link"; +import Accordion from '@material-ui/core/Accordion'; +import AccordionSummary from '@material-ui/core/AccordionSummary'; +import AccordionDetails from '@material-ui/core/AccordionDetails'; +import Typography from '@material-ui/core/Typography'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; + import { BaseProps } from '../components/baseProps'; import { UIElementReference } from '../components/uiElementReference'; @@ -60,93 +66,104 @@ import { UiElementLeafList } from '../components/uiElementLeafList'; import { useConfirm } from 'material-ui-confirm'; 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), - }, - button: { - margin: 0, - padding: "6px 6px", - minWidth: 'unset' - }, - readOnly: { - '& label.Mui-focused': { - color: 'green', + 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' }, - '& .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", }, - }, - 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 - } + 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 }>; @@ -154,599 +171,653 @@ const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string type ConfigurationApplicationComponentProps = RouteComponentProps & Connect & WithStyles; 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 } } }; } const OldProps = Symbol("OldProps"); class ConfigurationApplicationComponent extends React.Component { - /** - * - */ - 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 } } }) || {} - } - - 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; + + 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 } } }) || {} } - 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; + + 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; + } + 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 } - }); - 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 getEditorForViewElement = (uiElement: ViewElement) : (null | React.ComponentType>) => { - 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 (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 { this.changeValueFor(uiElement.id, e) }} - getEditorForViewElement = { this.getEditorForViewElement } - />; - } else { - const Element = this.getEditorForViewElement(uiElement); - return Element != null - ? ( - { + // 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[]); + + 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>) => { + 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 { this.changeValueFor(uiElement.id, e) }} - /> ) - : null ; - } - }; - - // 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 ( - // - // - // - // - // - // ); - // } 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 ( - <> - - {uiElement.label} - - - {subElements - ? Object.keys(subElements).map(elmKey => { - const elm = subElements[elmKey]; - return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); - }) - :

Invalid Choise

- } - - ); - } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - } - return null; - } - }; - - 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; + getEditorForViewElement={this.getEditorForViewElement} + />; + } else { + const Element = this.getEditorForViewElement(uiElement); + return Element != null + ? ( + { this.changeValueFor(uiElement.id, e) }} + />) + : null; + } }; - 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 ( -
-
- {sections.elements.length > 0 - ? ( -
- {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} -
- ) : null - } - {sections.references.length > 0 - ? ( -
- {sections.references.map(element => ( - { this.navigate(`/${elm.id}`) }} /> - ))} -
- ) : null - } - {sections.choises.length > 0 - ? ( -
- {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} -
- ) : null - } - {sections.rpcs.length > 0 - ? ( -
- {sections.rpcs.map(element => ( - { this.navigate(`/${elm.id}`) }} /> - ))} -
- ) : null + // 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 ( + // + // + // + // + // + // ); + // } 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 ( + <> + + {uiElement.label} + + + {subElements + ? Object.keys(subElements).map(elmKey => { + const elm = subElements[elmKey]; + return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); + }) + :

Invalid Choise

+ } + + ); + } else { + if (process.env.NODE_ENV !== "production") { + console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + } + return null; } -
- ); - }; + }; + + 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[] }); - private renderUIViewList(listSpecification: ViewSpecification, listKeyProperty: string, listData: { [key: string]: any }[]) { - const listElements = listSpecification.elements; + sections.elements = sections.elements.sort(orderFunc); - const navigate = (path: string) => { - this.props.history.push(`${this.props.match.url}${path}`); + return ( +
+
+ {sections.elements.length > 0 + ? ( +
+ {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} +
+ ) : null + } + {sections.references.length > 0 + ? ( +
+ {sections.references.map(element => ( + { this.navigate(`/${elm.id}`) }} /> + ))} +
+ ) : null + } + {sections.choises.length > 0 + ? ( +
+ {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} +
+ ) : null + } + {sections.rpcs.length > 0 + ? ( +
+ {sections.rpcs.map(element => ( + { this.navigate(`/${elm.id}`) }} /> + ))} +
+ ) : null + } +
+ ); }; - const addNewElementAction = { - icon: AddIcon, tooltip: 'Add', onClick: () => { - navigate("[]"); // empty key means new element - } + 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 ( +
+ { + moduleKeys.map(key => { + const moduleView = modules[key]; + return ( + + } aria-controls={`content-${key}`} id={`header-${key}`} > + {key} + + + {this.renderUIView(moduleView, viewData, keyProperty, editMode, isNew)} + + + ); + }) + } +
+ ); }; - const { classes, removeElement } = this.props; + private renderUIViewList(listSpecification: ViewSpecification, listKeyProperty: string, listData: { [key: string]: any }[]) { + const listElements = listSpecification.elements; + + 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 { classes, removeElement } = this.props; - const DeleteIconWithConfirmation : React.FC<{rowData: {[key:string]:any}, onReload: () => void} > = (props) => { - const confirm = useConfirm(); + const DeleteIconWithConfirmation: React.FC<{ rowData: { [key: string]: any }, onReload: () => void }> = (props) => { + const confirm = useConfirm(); + + return ( + + { + 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); + }} > + + + + ); + } return ( - - { - 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 ); - }} > - - - + []>((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 ( + this.props.vPath && this.props.reloadView(this.props.vPath)} /> + ); + }) + }]) + } onHandleClick={(ev, row) => { + ev.preventDefault(); + navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); + }} > ); } - return ( - []>((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 } : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + 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 !"); } else { - acc.unshift(elm.uiType === "boolean" ? { property: elm.label, type: ColumnType.boolean } : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + acc.elements.push(elm); } - } - return acc; - }, []).concat([{ - property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: ( ({ rowData })=> { - return ( - this.props.vPath && this.props.reloadView(this.props.vPath) } /> - ); - }) - }]) - } onHandleClick={(ev, row) => { - ev.preventDefault(); - navigate(`[${row[listKeyProperty]}]`); - }} > - ); - } - - 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; + 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 ( + <> +
+ { sections.elements.length > 0 + ? ( +
+ {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} +
+ ) : null + } + { sections.choises.length > 0 + ? ( +
+ {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} +
+ ) : null + } + +
+ { outputViewData !== undefined + ? renderObject(outputViewData) + : 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 ( - <> -
- {sections.elements.length > 0 - ? ( -
- {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} + 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 ( +
+
+ + ) => { + ev.preventDefault(); + this.props.history.push(lastPath); + }}>Back + ) => { + ev.preventDefault(); + this.props.history.push(`/configuration/${nodeId}`); + }}>{nodeId} + { + pathParts.map(([prop, key], ind) => { + const path = `${basePath}/${prop}`; + const keyPath = key && `${basePath}/${prop}[${key}]`; + const propTitle= prop.replace(/^[^:]+:/, ""); + const ret = ( + + ) => { + ev.preventDefault(); + this.props.history.push(path); + }}>{propTitle} + { + keyPath && ) => { + ev.preventDefault(); + this.props.history.push(keyPath); + }}>{`[${key}]`} || null + } + + ); + lastPath = basePath; + basePath = keyPath || path; + return ret; + }) + } + +
+ {this.state.editMode && ( + { + this.props.vPath && await this.props.reloadView(this.props.vPath); + this.setState({ editMode: false }); + }} > + ) || null} + { /* do not show edit if this is a list or it can't be edited */ + displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (
+ { + 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 + ? + : + } + +
|| null) + }
- ) : null + ); + } + + private renderValueSelector() { + const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; + if (!listKeyProperty || !listSpecification) { + throw new Error("ListKex ot view not specified."); } - {sections.choises.length > 0 - ? ( -
- {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} + + return ( +
+ []>((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); }} >
- ) : null - } - - {outputViewData !== undefined - ? ( - renderObject(outputViewData) ) - : null - } - - ); - }; - - private renderBreadCrumps() { - const { editMode } = this.state; - const { displaySpecification } = this.props; - const { vPath, nodeId } = this.props; - const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - let lastPath = `/configuration`; - let basePath = `/configuration/${nodeId}`; - return ( -
-
- - ) => { - ev.preventDefault(); - this.props.history.push(lastPath); - }}>Back - ) => { - ev.preventDefault(); - this.props.history.push(`/configuration/${nodeId}`); - }}>{nodeId} - { - pathParts.map(([prop, key], ind) => { - const path = `${basePath}/${prop}`; - const keyPath = key && `${basePath}/${prop}[${key}]`; - const ret = ( - - ) => { - ev.preventDefault(); - this.props.history.push(path); - }}>{prop.replace(/^[^:]+:/, "")} - { - keyPath && ) => { - ev.preventDefault(); - this.props.history.push(keyPath); - }}>{`[${key}]`} || null - } - - ); - lastPath = basePath; - basePath = keyPath || path; - return ret; - }) - } - -
- {this.state.editMode && ( - { - this.props.vPath && await this.props.reloadView(this.props.vPath); - this.setState({ editMode: false }); - }} > - ) || null} - { /* do not show edit if this is a list or it can't be edited */ - displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (
- { - 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 - ? - : - } - -
|| null) - } -
- ); - } - - private renderValueSelector() { - const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; - if (!listKeyProperty || !listSpecification) { - throw new Error("ListKex ot view not specified."); + ); } - return ( -
- []>((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); }} > -
- ); - } - - private renderValueEditor() { - const { displaySpecification: ds, outputData } = this.props; - const { viewData, editMode, isNew } = this.state; - - return ( -
- {this.renderBreadCrumps()} - {ds.displayMode === DisplayModeType.doNotDisplay - ? null - : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array - ? this.renderUIViewList(ds.viewSpecification, ds.keyProperty!, viewData) - : ds.displayMode === DisplayModeType.displayAsRPC - ? this.renderUIViewRPC(ds.inputViewSpecification, viewData!, outputData, undefined, true, false) - : this.renderUIView(ds.viewSpecification, viewData!, ds.keyProperty, editMode, isNew) - } -
- ); - } - - private renderCollectingData() { - return ( -
-
- -

Processing ...

-
-
- ); - } - - render() { - return this.props.collectingData || !this.state.viewData - ? this.renderCollectingData() - : this.props.listSpecification - ? this.renderValueSelector() - : this.renderValueEditor(); - } + private renderValueEditor() { + const { displaySpecification: ds, outputData } = this.props; + const { viewData, editMode, isNew } = this.state; + + return ( +
+ {this.renderBreadCrumps()} + {ds.displayMode === DisplayModeType.doNotDisplay + ? null + : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array + ? this.renderUIViewList(ds.viewSpecification, ds.keyProperty!, viewData) + : ds.displayMode === DisplayModeType.displayAsRPC + ? this.renderUIViewRPC(ds.inputViewSpecification, viewData!, outputData, undefined, true, false) + : this.renderUIViewSelector(ds.viewSpecification, viewData!, ds.keyProperty, editMode, isNew) + } +
+ ); + } + + private renderCollectingData() { + return ( +
+
+ +

Processing ...

+
+
+ ); + } + + 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))); -export default ConfigurationApplication; +export default ConfigurationApplication; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts index 0f74297df..c5cb8fb4c 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts @@ -15,14 +15,15 @@ * the License. * ============LICENSE_END========================================================================== */ -import { Token, Statement, Module, Identity } from "../models/yang"; +import { Token, Statement, Module, Identity, ModuleState } from "../models/yang"; import { ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase, isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString, - isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion, ViewElementRpc, isViewElementRpc, ResolveFunction + isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion, ViewElementRpc, isViewElementRpc, ResolveFunction, ViewElementDate } from "../models/uiModels"; import { yangService } from "../services/yangService"; + export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => { const pathParts: RegExpMatchArray[] = []; let partMatch: RegExpExecArray | null; @@ -284,8 +285,8 @@ export class YangParser { public static ResolveStack = Symbol("ResolveStack"); - constructor() { - + constructor(private _unavailableCapabilities: { failureReason: string; capability: string; }[] = []) { + } public get modules() { @@ -299,9 +300,16 @@ export class YangParser { public async addCapability(capability: string, version?: string) { // do not add twice if (this._modules[capability]) { + // console.warn(`Skipped capability: ${capability} since allready contained.` ); return; } + // // do not add unavaliabe capabilities + // if (this._unavailableCapabilities.some(c => c.capability === capability)) { + // // console.warn(`Skipped capability: ${capability} since it is marked as unavaliable.` ); + // return; + // } + const data = await yangService.getCapability(capability, version); if (!data) { throw new Error(`Could not load yang file for ${capability}.`); @@ -316,6 +324,8 @@ export class YangParser { throw new Error(`Root element capability ${rootStatement.arg} does not requested ${capability}.`); } + const isUnavaliabe = this._unavailableCapabilities.some(c => c.capability === capability); + const module = this._modules[capability] = { name: rootStatement.arg, revisions: {}, @@ -326,7 +336,10 @@ export class YangParser { groupings: {}, typedefs: {}, views: {}, - elements: {} + elements: {}, + state: isUnavaliabe + ? ModuleState.unabaliabe + : ModuleState.stable, }; await this.handleModule(module, rootStatement, capability); @@ -389,9 +402,14 @@ export class YangParser { }, {}) }; - // import all required files + // import all required files and set module state if (imports) for (let ind = 0; ind < imports.length; ++ind) { - await this.addCapability(imports[ind].arg!); + const moduleName = imports[ind].arg!; + await this.addCapability(moduleName); + const importedModule = this._modules[imports[ind].arg!]; + if (importedModule && importedModule.state > ModuleState.stable) { + module.state = Math.max(module.state, ModuleState.instable); + } } this.extractTypeDefinitions(rootStatement, module, ""); @@ -420,7 +438,9 @@ export class YangParser { const viewIdIndex = Number(viewElement.viewId); module.views[key] = this._views[viewIdIndex]; } - this._views[0].elements[key] = module.elements[key]; + + // add only the UI View if the module is avliable + if (module.state !== ModuleState.unabaliabe) this._views[0].elements[key] = module.elements[key]; }); }); return module; @@ -442,10 +462,30 @@ export class YangParser { } }); - // process all augmentations + // process all augmentations / sort by namespace changes to ensure propper order Object.keys(this.modules).forEach(modKey => { const module = this.modules[modKey]; - Object.keys(module.augments).forEach(augKey => { + const augmentKeysWithCounter = Object.keys(module.augments).map((key) => { + const pathParts = splitVPath(key, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property + let nameSpaceChangeCounter = 0; + let currentNS = module.name; // init namespace + pathParts.forEach(([ns, _])=> { + if (ns === currentNS){ + currentNS = ns; + nameSpaceChangeCounter++; + } + }); + return { + key, + nameSpaceChangeCounter, + } + }); + + const augmentKeys = augmentKeysWithCounter + .sort((a,b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1 ) + .map((a) => a.key); + + augmentKeys.forEach(augKey => { const augments = module.augments[augKey]; const viewSpec = this.resolveView(augKey); if (!viewSpec) console.warn(`Could not find view to augment [${augKey}] in [${module.name}].`); @@ -454,6 +494,7 @@ export class YangParser { const elm = augment.elements[key]; viewSpec.elements[key] = { ...augment.elements[key], + when: elm.when ? `(${augment.when}) and (${elm.when})` : augment.when, ifFeature: elm.ifFeature ? `(${augment.ifFeature}) and (${elm.ifFeature})` : augment.ifFeature, }; @@ -546,7 +587,7 @@ export class YangParser { const grouping = cur.arg; // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath); + const [currentView, subViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath); grouping && (module.groupings[grouping] = currentView); acc.push(currentView, ...subViews); return acc; @@ -600,6 +641,7 @@ export class YangParser { }, {}); } + // Hint: use 0 as parentId for rootElements and -1 for rootGroupings. private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] { // used for scoped definitions const context: Module = { @@ -640,6 +682,8 @@ export class YangParser { elements.push({ id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg, label: cur.arg, + path: currentPath, + module: context.name || module.name || '', uiType: "object", viewId: currentView.id, config: config @@ -667,6 +711,8 @@ export class YangParser { elements.push({ id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg, label: cur.arg, + path: currentPath, + module: context.name || module.name || '', isList: true, uiType: "object", viewId: currentView.id, @@ -755,6 +801,8 @@ export class YangParser { uiType: "choise", id: parentId === 0 ? `${context.name}:${curChoise.arg}` : curChoise.arg, label: curChoise.arg, + path: currentPath, + module: context.name || module.name || '', config: config, mandatory: mandatory, description: description, @@ -802,6 +850,8 @@ export class YangParser { uiType: "rpc", id: parentId === 0 ? `${context.name}:${curRpc.arg}` : curRpc.arg, label: curRpc.arg, + path: currentPath, + module: context.name || module.name || '', config: config, description: description, inputViewId: inputViewId, @@ -869,7 +919,8 @@ export class YangParser { Object.keys(groupingViewSpec.elements).forEach(key => { const elm = groupingViewSpec.elements[key]; - viewSpec.elements[key] = { + // a useRef on root level need a namespace + viewSpec.elements[parentId === 0 ? `${module.name}:${key}` : key] = { ...groupingViewSpec.elements[key], when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when, ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature, @@ -889,7 +940,9 @@ export class YangParser { } }); // console.log("Resolved "+currentElementPath, viewSpec); - viewSpec?.uses![ResolveFunction] = undefined; + if (viewSpec?.uses) { + viewSpec.uses[ResolveFunction] = undefined; + } } this._groupingsToResolve.push(viewSpec); @@ -934,9 +987,19 @@ export class YangParser { const extractRange = (min: number, max: number, property: string = "range"): { expression: Expression | undefined, min: number, max: number } => { const ranges = this.extractValue(this.extractNodes(cur, "type")[0]!, property) || undefined; const range = ranges ?.replace(/min/i, String(min)).replace(/max/i, String(max)).split("|").map(r => { - const [minStr, maxStr] = r.split('..'); - const minValue = Number(minStr); - const maxValue = Number(maxStr); + let minValue: number; + let maxValue: number; + + if (r.indexOf('..') > -1) { + const [minStr, maxStr] = r.split('..'); + minValue = Number(minStr); + maxValue = Number(maxStr); + } else if (!isNaN(maxValue = Number(r && r.trim() )) ) { + minValue = maxValue; + } else { + minValue = min, + maxValue = max; + } if (minValue > min) min = minValue; if (maxValue < max) max = maxValue; @@ -978,7 +1041,9 @@ export class YangParser { const element: ViewElementBase = { id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg, - label: cur.arg, + label: cur.arg, + path: currentPath, + module: module.name || "", config: config, mandatory: mandatory, isList: isList, @@ -1254,17 +1319,27 @@ export class YangParser { length: extractRange(0, +18446744073709551615, "length"), }; } else { - // not a build in type, have to resolve type + // not a build in type, need to resolve type let typeRef = this.resolveType(type, module); if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`)); + if (isViewElementString(typeRef)) { typeRef = this.resolveStringType(typeRef, extractPattern(), extractRange(0, +18446744073709551615)); - } else if (isViewElementNumber(typeRef)) { typeRef = this.resolveNumberType(typeRef, extractRange(typeRef.min, typeRef.max)); } + // spoof date type here from special string type + if ((type === 'date-and-time' || type.endsWith(':date-and-time') ) && typeRef.module === "ietf-yang-types") { + return { + ...typeRef, + ...element, + description: description, + uiType: "date", + }; + } + return ({ ...typeRef, ...element, diff --git a/sdnr/wt/odlux/apps/configurationApp/webpack.config.js b/sdnr/wt/odlux/apps/configurationApp/webpack.config.js index 329eb00f4..6349305ac 100644 --- a/sdnr/wt/odlux/apps/configurationApp/webpack.config.js +++ b/sdnr/wt/odlux/apps/configurationApp/webpack.config.js @@ -129,36 +129,46 @@ module.exports = (env) => { colors: true }, proxy: { + "/about": { + // target: "http://10.20.6.29:48181", + target: "http://localhost:8181", + secure: false + }, "/yang-schema/": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", secure: false }, "/oauth2/": { - target: "http://10.20.6.29:48181", + // target: "https://10.20.35.188:30205", + target: "http://localhost:8181", secure: false }, "/database/": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", secure: false }, "/restconf/": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", secure: false }, "/rests/": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", secure: false }, "/help/": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", + secure: false + }, + "/about/": { + target: "http://localhost:8181", secure: false }, "/tree/": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", secure: false }, "/websocket": { - target: "http://10.20.6.29:48181", + target: "http://localhost:8181", ws: true, changeOrigin: true, secure: false diff --git a/sdnr/wt/odlux/framework/pom.xml b/sdnr/wt/odlux/framework/pom.xml index 732b4fec5..c849c1968 100644 --- a/sdnr/wt/odlux/framework/pom.xml +++ b/sdnr/wt/odlux/framework/pom.xml @@ -46,7 +46,7 @@ ${maven.build.timestamp} ONAP Frankfurt (Neon, mdsal ${odl.mdsal.version}) - 68.d7886ce(20/09/04) + 86.51a94bd(20/11/17) ONAP SDN-R | ONF Wireless for ${distversion} - Build: ${buildtime} ${buildno} ${project.version} diff --git a/sdnr/wt/odlux/framework/src/assets/version.json b/sdnr/wt/odlux/framework/src/assets/version.json index 6311e1094..7720cb969 100644 --- a/sdnr/wt/odlux/framework/src/assets/version.json +++ b/sdnr/wt/odlux/framework/src/assets/version.json @@ -1,4 +1,5 @@ { - "version":"68.d7886ce(20/09/04)", - "build":"2020-09-04T12:31:19Z" + "version":"86.51a94bd(20/11/17)", + "build":"2020-11-17T11:13:24Z" + } \ No newline at end of file -- cgit 1.2.3-korg