From 15e2d3a29b0d1a304965e34f114a911e5a7abdb3 Mon Sep 17 00:00:00 2001 From: sai-neetha Date: Mon, 20 Mar 2023 08:05:47 +0100 Subject: Odlux Update Add eslint and custom icons update Issue-ID: CCSDK-3871 Signed-off-by: sai-neetha Change-Id: If6b676128cc9cff0437a5dc54f85eaafd3b8c586 Signed-off-by: highstreetherbert --- sdnr/wt/odlux/apps/configurationApp/package.json | 6 +- sdnr/wt/odlux/apps/configurationApp/pom.xml | 3 +- .../configurationApp/src/actions/deviceActions.ts | 498 +++++------- .../src/assets/icons/configurationAppIcon.svg | 20 + .../configurationApp/src/components/baseProps.ts | 14 +- .../src/components/ifWhenTextInput.tsx | 72 +- .../src/components/uiElementBoolean.tsx | 41 +- .../src/components/uiElementLeafList.tsx | 170 +++-- .../src/components/uiElementNumber.tsx | 12 +- .../src/components/uiElementReference.tsx | 35 +- .../src/components/uiElementSelection.tsx | 51 +- .../src/components/uiElementString.tsx | 4 +- .../src/components/uiElementUnion.tsx | 4 +- .../configurationApp/src/components/verifyer.ts | 280 ------- .../src/handlers/configurationAppRootHandler.ts | 8 +- .../handlers/connectedNetworkElementsHandler.ts | 6 +- .../src/handlers/deviceDescriptionHandler.ts | 22 +- .../src/handlers/valueSelectorHandler.ts | 20 +- .../src/handlers/viewDescriptionHandler.ts | 20 +- .../src/models/networkElementConnection.ts | 6 +- .../apps/configurationApp/src/models/uiModels.ts | 256 +++---- .../odlux/apps/configurationApp/src/models/yang.ts | 28 +- .../configurationApp/src/pluginConfiguration.tsx | 69 +- .../configurationApp/src/services/restServices.ts | 142 ++-- .../configurationApp/src/services/yangService.ts | 34 +- .../configurationApp/src/utilities/verifyer.ts | 261 +++++++ .../src/utilities/viewEngineHelper.ts | 324 ++++++++ .../src/views/configurationApplication.tsx | 320 ++++---- .../src/views/networkElementSelector.tsx | 28 +- .../apps/configurationApp/src/yang/whenParser.ts | 235 ++++++ .../apps/configurationApp/src/yang/yangParser.ts | 844 +++++++++++---------- .../odlux/apps/configurationApp/webpack.config.js | 62 +- 32 files changed, 2203 insertions(+), 1692 deletions(-) create mode 100644 sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg delete mode 100644 sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts create mode 100644 sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts create mode 100644 sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts create mode 100644 sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts (limited to 'sdnr/wt/odlux/apps/configurationApp') diff --git a/sdnr/wt/odlux/apps/configurationApp/package.json b/sdnr/wt/odlux/apps/configurationApp/package.json index afd5456ec..b1d7d95d5 100644 --- a/sdnr/wt/odlux/apps/configurationApp/package.json +++ b/sdnr/wt/odlux/apps/configurationApp/package.json @@ -26,7 +26,11 @@ "@mui/icons-material": "^5.2.0", "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", - "@odlux/framework": "*" + "@odlux/framework": "*", + "@fortawesome/fontawesome-svg-core": "1.2.35", + "@fortawesome/free-solid-svg-icons": "5.6.3", + "@fortawesome/react-fontawesome": "0.1.14", + "material-ui-confirm": "3.0.2" }, "peerDependencies": { "@types/classnames": "2.2.6", diff --git a/sdnr/wt/odlux/apps/configurationApp/pom.xml b/sdnr/wt/odlux/apps/configurationApp/pom.xml index 63475625d..9703e2924 100644 --- a/sdnr/wt/odlux/apps/configurationApp/pom.xml +++ b/sdnr/wt/odlux/apps/configurationApp/pom.xml @@ -19,6 +19,7 @@ ~ ============LICENSE_END======================================================= ~ --> + 4.0.0 @@ -139,7 +140,7 @@ initialize - v12.13.0 + v12.22.0 v1.22.10 diff --git a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts index 37583787f..52137135a 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts @@ -1,15 +1,30 @@ import { Action } from '../../../../framework/src/flux/action'; import { Dispatch } from '../../../../framework/src/flux/store'; -import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import { PushAction, ReplaceAction } from "../../../../framework/src/actions/navigationActions"; -import { AddErrorInfoAction } from "../../../../framework/src/actions/errorActions"; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { PushAction, ReplaceAction } from '../../../../framework/src/actions/navigationActions'; +import { AddErrorInfoAction } from '../../../../framework/src/actions/errorActions'; import { DisplayModeType, DisplaySpecification } from '../handlers/viewDescriptionHandler'; -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 { restService } from '../services/restServices'; +import { YangParser } from '../yang/yangParser'; +import { Module } from '../models/yang'; +import { + ViewSpecification, + ViewElement, + isViewElementReference, + isViewElementList, + ViewElementString, +} from '../models/uiModels'; + +import { + checkResponseCode, + splitVPath, + filterViewElements, + flattenViewElements, + getReferencedDataList, + resolveViewDescription, +} from '../utilities/viewEngineHelper'; export class EnableValueSelector extends Action { constructor(public listSpecification: ViewSpecification, public listData: any[], public keyProperty: string, public onValueSelected : (value: any) => void ) { @@ -30,42 +45,26 @@ export class SetSelectedValue extends Action { } export class UpdateDeviceDescription extends Action { - constructor( public nodeId: string, public modules: { [name:string]: Module}, public views: ViewSpecification[]) { + constructor( public nodeId: string, public modules: { [name:string]: Module }, public views: ViewSpecification[]) { super(); } } -export class UpdatViewDescription extends Action { - constructor (public vPath: string, public viewData: any, public displaySpecification: DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay }) { +export class UpdateViewDescription extends Action { + constructor(public vPath: string, public viewData: any, public displaySpecification: DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay }) { super(); } } -export class UpdatOutputData extends Action { - constructor (public outputData: any) { +export class UpdateOutputData extends Action { + constructor(public outputData: any) { super(); } } -type HttpResult = { - status: number; - message?: string | undefined; - data: { - [key: string]: any; - } | null | undefined; -}; - -const checkResponseCode = (restResult: HttpResult) =>{ - - //403 gets handled by the framework from now on - - return restResult.status !== 403 && ( restResult.status < 200 || restResult.status > 299); +export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatch: Dispatch, _getState: () => IApplicationStoreState ) => { -} - -export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState ) => { - - dispatch(new UpdateDeviceDescription("", {}, [])); + dispatch(new UpdateDeviceDescription('', {}, [])); dispatch(new SetCollectingSelectionData(true)); const { availableCapabilities, unavailableCapabilities, importOnlyModules } = await restService.getCapabilitiesByMountId(nodeId); @@ -73,16 +72,24 @@ export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatc if (!availableCapabilities || availableCapabilities.length <= 0) { dispatch(new SetCollectingSelectionData(false)); dispatch(new UpdateDeviceDescription(nodeId, {}, [])); - dispatch(new UpdatViewDescription("", [], { + dispatch(new UpdateViewDescription('', [], { displayMode: DisplayModeType.displayAsMessage, - renderMessage: `NetworkElement : "${nodeId}" has no capabilities.` + renderMessage: `NetworkElement : "${nodeId}" has no capabilities.`, })); throw new Error(`NetworkElement : [${nodeId}] has no capabilities.`); } - const parser = new YangParser(unavailableCapabilities || undefined, importOnlyModules || undefined, nodeId); + const parser = new YangParser( + nodeId, + availableCapabilities.reduce((acc, cur) => { + acc[cur.capability] = cur.version; + return acc; + }, {} as { [key: string]: string }), + unavailableCapabilities || undefined, + importOnlyModules || undefined, + ); - for (let i = 0; i < availableCapabilities.length; ++i){ + for (let i = 0; i < availableCapabilities.length; ++i) { const capRaw = availableCapabilities[i]; try { await parser.addCapability(capRaw.capability, capRaw.version); @@ -95,184 +102,28 @@ export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatc dispatch(new SetCollectingSelectionData(false)); - if (process.env.NODE_ENV === "development" ) { - console.log(parser, parser.modules, parser.views); + if (process.env.NODE_ENV === 'development' ) { + console.log(parser, parser.modules, parser.views); } return dispatch(new UpdateDeviceDescription(nodeId, parser.modules, parser.views)); -} - -export const splitVPath = (vPath: string, vPathParser : RegExp): [string, string?][] => { - const pathParts: [string, string?][] = []; - let partMatch: RegExpExecArray | null; - if (vPath) do { - partMatch = vPathParser.exec(vPath); - if (partMatch) { - pathParts.push([partMatch[1], partMatch[2] || undefined]); - } - } while (partMatch) - return pathParts; -} - -const getReferencedDataList = async (refPath: string, dataPath: string, modules: { [name: string]: Module }, views: ViewSpecification[]) => { - const pathParts = splitVPath(refPath, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property - const defaultNS = pathParts[0][0]; - let referencedModule = modules[defaultNS]; - - let dataMember: string; - let view: ViewSpecification; - let currentNS: string | null = null; - let dataUrls = [dataPath]; - let data: any; - - for (let i = 0; i < pathParts.length; ++i) { - const [pathPartNS, pathPart] = pathParts[i]; - const namespace = pathPartNS != null ? (currentNS = pathPartNS) : currentNS; - - const viewElement = i === 0 - ? views[0].elements[`${referencedModule.name}:${pathPart}`] - : view!.elements[`${pathPart}`] || view!.elements[`${namespace}:${pathPart}`]; - - if (!viewElement) throw new Error(`Could not find ${pathPart} in ${refPath}`); - if (i < pathParts.length - 1) { - if (!isViewElementObjectOrList(viewElement)) { - throw Error(`Module: [${referencedModule.name}].[${viewElement.label}]. Viewelement is not list or object.`); - } - view = views[+viewElement.viewId]; - const resultingDataUrls : string[] = []; - if (isViewElementList(viewElement)) { - for (let j = 0; j < dataUrls.length; ++j) { - const dataUrl = dataUrls[j]; - const restResult = (await restService.getConfigData(dataUrl)); - if (restResult.data == null || checkResponseCode(restResult)) { - const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]["error-message"] || ""; - throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); - } - - let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; - if (dataRaw === undefined) { - dataRaw = restResult.data[dataMember!]; - } - dataRaw = dataRaw instanceof Array - ? dataRaw[0] - : dataRaw; +}; - data = dataRaw && dataRaw[viewElement.label] || []; - const keys: string[] = data.map((entry: { [key: string]: any } )=> entry[viewElement.key!]); - resultingDataUrls.push(...keys.map(key => `${dataUrl}/${viewElement.label.replace(/\//ig, "%2F")}=${key.replace(/\//ig, "%2F")}`)); - } - dataMember = viewElement.label; - } else { - // just a member, not a list - const pathSegment = (i === 0 - ? `/${referencedModule.name}:${viewElement.label.replace(/\//ig, "%2F")}` - : `/${viewElement.label.replace(/\//ig, "%2F")}`); - resultingDataUrls.push(...dataUrls.map(dataUrl => dataUrl + pathSegment)); - dataMember = viewElement.label; - } - dataUrls = resultingDataUrls; - } else { - data = []; - for (let j = 0; j < dataUrls.length; ++j) { - const dataUrl = dataUrls[j]; - const restResult = (await restService.getConfigData(dataUrl)); - if (restResult.data == null || checkResponseCode(restResult)) { - const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]["error-message"] || ""; - throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); - } - let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; - if (dataRaw === undefined) { - dataRaw = restResult.data[dataMember!]; - } - dataRaw = dataRaw instanceof Array - ? dataRaw[0] - : dataRaw; - data.push(dataRaw); - } - // BUG UUID ist nicht in den elements enthalten !!!!!! - const key = viewElement && viewElement.label || pathPart; - return { - view: view!, - data: data, - key: key, - }; - } +export const postProcessDisplaySpecificationActionCreator = (vPath: string, viewData: any, displaySpecification: DisplaySpecification) => async (dispatch: Dispatch, _getState: () => IApplicationStoreState) => { + + if (displaySpecification.displayMode === DisplayModeType.displayAsObject) { + displaySpecification = { + ...displaySpecification, + viewSpecification: await filterViewElements(vPath, viewData, displaySpecification.viewSpecification), + }; } - return null; -} - -const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification): ViewSpecification =>{ - - // check if-feature | when | and resolve all references. - view = { ...view }; - view.elements = Object.keys(view.elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { - const resolveHistory : ViewElement[] = []; - let elm = view.elements[cur]; - const key = defaultNS && cur.replace(new RegExp(`^${defaultNS}:`, "i"),"") || cur; - while (isViewElementReference(elm)) { - const result = (elm.ref(vPath)); - if (result) { - const [referencedElement, referencedPath] = result; - if (resolveHistory.some(hist => hist === referencedElement)) { - console.error(`Circle reference found at: ${vPath}`, resolveHistory); - break; - } - elm = referencedElement; - vPath = referencedPath; - resolveHistory.push(elm); - } - } - - acc[key] = { ...elm, id: key }; - - return acc; - }, {}); - return view; -} - -const flatenViewElements = (defaultNS: string | null, parentPath: string, elements: { [name: string]: ViewElement }, views: ViewSpecification[], currentPath: string ): { [name: string]: ViewElement } => { - if (!elements) return {}; - return Object.keys(elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { - const elm = elements[cur]; - // remove the detault namespace, and only the default namespace, sine it seems that this is also not in the restconf response - const elmKey = defaultNS && elm.id.replace(new RegExp(`^${defaultNS}:`, "i"), "") || elm.id; - const key = parentPath ? `${parentPath}.${elmKey}` : elmKey; - - if (isViewElementRpc(elm)) { - console.warn(`Flaten of RFC not supported ! [${currentPath}][${elm.label}]`); - return acc; - } else if (isViewElementObjectOrList(elm)) { - const view = views[+elm.viewId]; - const inner = view && flatenViewElements(defaultNS, key, view.elements, views, `${currentPath}/${view.name}`); - inner && Object.keys(inner).forEach(k => (acc[k] = inner[k])); - } else if (isViewElementChoise(elm)) { - acc[key] = { - ...elm, - id: key, - cases: Object.keys(elm.cases).reduce<{ [name: string]: ViewElementChoiseCase }>((accCases, curCases) => { - const caseElement = elm.cases[curCases]; - accCases[curCases] = { - ...caseElement, - // Hint: do not use key it contains elmKey, which shell be omitted for cases. - elements: flatenViewElements(defaultNS, /*key*/ parentPath, caseElement.elements, views, `${currentPath}/${elm.label}`) - }; - return accCases; - }, {}), - }; - } else { - acc[key] = { - ...elm, - id: key, - }; - } - return acc; - }, {}); + dispatch(new UpdateViewDescription(vPath, viewData, displaySpecification)); }; export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - const { configuration: { deviceDescription: { nodeId, modules, views } }, framework: { navigationState } } = getState(); + const { configuration: { deviceDescription: { nodeId, modules, views } } } = getState(); let dataPath = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; let inputViewSpecification: ViewSpecification | undefined = undefined; @@ -291,26 +142,26 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: try { for (let ind = 0; ind < pathParts.length; ++ind) { const [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; + if (ind === 0) { defaultNS = namespace; } viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (viewElement.isList && !key) { if (pathParts.length - 1 > ind) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No key for list [" + property + "]"); - } else if (vPath.endsWith("[]") && pathParts.length - 1 === ind) { + throw new Error('No key for list [' + property + ']'); + } else if (vPath.endsWith('[]') && pathParts.length - 1 === ind) { // empty key is used for new element - if (viewElement && "viewId" in viewElement) viewSpecification = views[+viewElement.viewId]; + if (viewElement && 'viewId' in viewElement) viewSpecification = views[+viewElement.viewId]; const data = Object.keys(viewSpecification.elements).reduce<{ [name: string]: any }>((acc, cur) => { const elm = viewSpecification.elements[cur]; if (elm.default) { - acc[elm.id] = elm.default || "" + acc[elm.id] = elm.default || ''; } return acc; }, {}); @@ -319,13 +170,13 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: const ds: DisplaySpecification = { displayMode: DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), - keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined + keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, }; // update display specification - return dispatch(new UpdatViewDescription(vPath, data, ds)); + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, data, ds)); } - if (viewElement && isViewElementList(viewElement) && viewSpecification.parentView === "0") { + if (viewElement && isViewElementList(viewElement) && viewSpecification.parentView === '0') { // check if there is a reference as key const listSpecification = views[+viewElement.viewId]; const keyElement = viewElement.key && listSpecification.elements[viewElement.key]; @@ -338,35 +189,35 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: throw new Error(`Key property not found for [${keyElement.referencePath}].`); } dispatch(new EnableValueSelector(refList.view, refList.data, refList.key, (refKey) => { - window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, "%2F")}]`))); + 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. + // Found a list at root level of a module w/o a reference 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, + 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, + config: false, + language: 'en-US', + elements: { + [viewElement.key!] : { + uiType: 'string', config: 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")}]`))); - })); + 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."); + throw new Error('Found a list at root level of a module and could not determine the keys.'); } dispatch(new SetCollectingSelectionData(false)); } @@ -374,31 +225,30 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: } extractList = true; } else { - // normal case + // normal case & replaces unicode %2C if present + dataPath += `/${property}${key ? `=${key.replace(/\%2C/g, ',').replace(/\//ig, '%2F')}` : ''}`; + // in case of the root element the required namespace will be added later, // while extracting the data - - dataPath += `/${property}${key ? `=${key.replace(/\%2C/g, ",").replace(/\//ig, "%2F")}` : ""}`; - dataMember = namespace === defaultNS ? viewElement.label : `${namespace}:${viewElement.label}`; extractList = false; } - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; - } else if (viewElement.uiType === "rpc") { + } else if (viewElement.uiType === 'rpc') { viewSpecification = views[+(viewElement.inputViewId || 0)]; // create new instance & flaten inputViewSpecification = viewElement.inputViewId != null && { ...views[+(viewElement.inputViewId || 0)], - elements: flatenViewElements(defaultNS, "", views[+(viewElement.inputViewId || 0)].elements, views, viewElement.label), + elements: flattenViewElements(defaultNS, '', views[+(viewElement.inputViewId || 0)].elements, views, viewElement.label), } || undefined; outputViewSpecification = viewElement.outputViewId != null && { ...views[+(viewElement.outputViewId || 0)], - elements: flatenViewElements(defaultNS, "", views[+(viewElement.outputViewId || 0)].elements, views, viewElement.label), + elements: flattenViewElements(defaultNS, '', views[+(viewElement.outputViewId || 0)].elements, views, viewElement.label), } || undefined; } @@ -406,7 +256,7 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: let data: any = {}; // do not get any data from netconf if there is no view specified || this is the root element [0] || this is an rpc - if (viewSpecification && !(viewSpecification.id === "0" || viewElement!.uiType === "rpc")) { + if (viewSpecification && !(viewSpecification.id === '0' || viewElement!.uiType === 'rpc')) { const restResult = (await restService.getConfigData(dataPath)); if (!restResult.data) { // special case: if this is a list without any response @@ -418,21 +268,21 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: const ds: DisplaySpecification = { displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), - keyProperty: viewElement.key + keyProperty: viewElement.key, }; // update display specification - return dispatch(new UpdatViewDescription(vPath, [], ds)); + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, [], ds)); } throw new Error(`Did not get response from Server. Status: [${restResult.status}]`); } else if (checkResponseCode(restResult)) { - const message = restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]["error-message"] || ""; + const message = restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${restResult.status}]\n${message}`); } else { - // https://tools.ietf.org/html/rfc7951#section-4 the root element may countain a namesapce or not ! + // https://tools.ietf.org/html/rfc7951#section-4 the root element may contain a namespace or not ! data = restResult.data[`${defaultNS}:${dataMember!}`]; if (data === undefined) { - data = restResult.data[dataMember!]; // extract dataMember w/o namespace + data = restResult.data[dataMember!]; // extract dataMember w/o namespace } } @@ -446,19 +296,21 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: ? data[viewElement!.id] || data[viewElement!.label] || [] // if the list is empty, it does not exist : data; - } else if (viewElement! && viewElement!.uiType === "rpc") { + } else if (viewElement! && viewElement!.uiType === 'rpc') { // set data to defaults data = {}; - inputViewSpecification && Object.keys(inputViewSpecification.elements).forEach(key => { - const elm = inputViewSpecification && inputViewSpecification.elements[key]; - if (elm && elm.default != undefined) { - data[elm.id] = elm.default; - } - }); + if (inputViewSpecification) { + Object.keys(inputViewSpecification.elements).forEach(key => { + const elm = inputViewSpecification && inputViewSpecification.elements[key]; + if (elm && elm.default != undefined) { + data[elm.id] = elm.default; + } + }); + } } // create display specification - const ds: DisplaySpecification = viewElement! && viewElement!.uiType === "rpc" + const ds: DisplaySpecification = viewElement! && viewElement!.uiType === 'rpc' ? { dataPath, displayMode: DisplayModeType.displayAsRPC, @@ -470,16 +322,18 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, - apidocPath: isViewElementList(viewElement!) && `/apidoc/explorer/index.html?urls.primaryName=$$$standard$$$#/mounted%20${nodeId}%20${viewElement!.module || 'MODULE_NOT_DEFINED'}/$$$action$$$_${dataPath.replace(/^\//,'').replace(/[\/=\-\:]/g,'_')}_${viewElement! != null ? `${viewElement.id.replace(/[\/=\-\:]/g,'_')}_` : '' }` || undefined, + + // eslint-disable-next-line max-len + apidocPath: isViewElementList(viewElement!) && `/apidoc/explorer/index.html?urls.primaryName=$$$standard$$$#/mounted%20${nodeId}%20${viewElement!.module || 'MODULE_NOT_DEFINED'}/$$$action$$$_${dataPath.replace(/^\//, '').replace(/[\/=\-\:]/g, '_')}_${viewElement! != null ? `${viewElement.id.replace(/[\/=\-\:]/g, '_')}_` : '' }` || undefined, }; // update display specification - return dispatch(new UpdatViewDescription(vPath, data, ds)); - // https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01] - // https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]/lp + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, data, ds)); + // https://server.com/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01] + // https://server.com/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]/lp } catch (error) { history.back(); - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not process ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not process ${dataPath}` })); dispatch(new SetCollectingSelectionData(false)); } finally { return; @@ -503,63 +357,63 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async try { for (let ind = 0; ind < pathParts.length; ++ind) { let [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; + if (ind === 0) { defaultNS = namespace; } viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (isViewElementList(viewElement) && !key) { embedList = true; - if (viewElement && viewElement.isList && viewSpecification.parentView === "0") { - throw new Error("Found a list at root level of a module w/o a refenrece key."); + if (viewElement && viewElement.isList && viewSpecification.parentView === '0') { + throw new Error('Found a list at root level of a module w/o a refenrece key.'); } if (pathParts.length - 1 > ind) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No key for list [" + property + "]"); - } else if (vPath.endsWith("[]") && pathParts.length - 1 === ind) { + throw new Error('No key for list [' + property + ']'); + } else if (vPath.endsWith('[]') && pathParts.length - 1 === ind) { // handle new element with any number of arguments - let keyList = viewElement.key?.split(" "); - let dataPathParam = keyList?.map(id => data[id]).join(","); - key = viewElement.key && String(dataPathParam) || ""; + let keyList = viewElement.key?.split(' '); + let dataPathParam = keyList?.map(id => data[id]).join(','); + key = viewElement.key && String(dataPathParam) || ''; isNew = key; if (!key) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No value for key [" + viewElement.key + "] in list [" + property + "]"); + throw new Error('No value for key [' + viewElement.key + '] in list [' + property + ']'); } } } - dataPath += `/${property}${key ? `=${key.replace(/\//ig, "%2F")}` : ""}`; + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; dataMember = viewElement.label; embedList = false; - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; } } // remove read-only elements - const removeReadOnlyElements = (viewSpecification: ViewSpecification, isList: boolean, data: any) => { + const removeReadOnlyElements = (pViewSpecification: ViewSpecification, isList: boolean, pData: any) => { if (isList) { - return data.map((elm : any) => removeReadOnlyElements(viewSpecification, false, elm)); + return pData.map((elm : any) => removeReadOnlyElements(pViewSpecification, 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."); + return Object.keys(pData).reduce<{ [key: string]: any }>((acc, cur)=>{ + const [nsOrName, name] = cur.split(':', 1); + const element = pViewSpecification.elements[cur] || pViewSpecification.elements[nsOrName] || pViewSpecification.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") { + if (element.uiType === 'object') { const view = views[+element.viewId]; if (!view) { - throw new Error("removeReadOnlyElements: Internal Error could not determine viewId: "+element.viewId); + throw new Error('removeReadOnlyElements: Internal Error could not determine viewId: ' + element.viewId); } - acc[cur] = removeReadOnlyElements(view, element.isList != null && element.isList, data[cur]); + acc[cur] = removeReadOnlyElements(view, element.isList != null && element.isList, pData[cur]); } else { - acc[cur] = data[cur]; + acc[cur] = pData[cur]; } } return acc; @@ -580,10 +434,10 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async : data; // do not extract root member (0) - if (viewSpecification && viewSpecification.id !== "0") { + if (viewSpecification && viewSpecification.id !== '0') { const updateResult = await restService.setConfigData(dataPath, { [`${currentNS}:${dataMember!}`]: data }); // addDataMember using currentNS if (checkResponseCode(updateResult)) { - const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]["error-message"] || ""; + const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); } } @@ -600,10 +454,10 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async }; // update display specification - return dispatch(new UpdatViewDescription(vPath, data, ds)); + return dispatch(new UpdateViewDescription(vPath, data, ds)); } catch (error) { history.back(); - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not change ${dataPath}` })); } finally { dispatch(new SetCollectingSelectionData(false)); @@ -619,57 +473,53 @@ export const removeElementActionAsyncCreator = (vPath: string) => async (dispatc let viewElement: ViewElement; let currentNS: string | null = null; - let defaultNS: string | null = null; - + dispatch(new SetCollectingSelectionData(true)); try { for (let ind = 0; ind < pathParts.length; ++ind) { let [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (isViewElementList(viewElement) && !key) { - if (viewElement && viewElement.isList && viewSpecification.parentView === "0") { - throw new Error("Found a list at root level of a module w/o a refenrece key."); + if (viewElement && viewElement.isList && viewSpecification.parentView === '0') { + throw new Error('Found a list at root level of a module w/o a reference key.'); } if (pathParts.length - 1 > ind) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No key for list [" + property + "]"); - } else if (vPath.endsWith("[]") && pathParts.length - 1 === ind) { + throw new Error('No key for list [' + property + ']'); + } else if (vPath.endsWith('[]') && pathParts.length - 1 === ind) { // remove the whole table } } - dataPath += `/${property}${key ? `=${key.replace(/\//ig, "%2F")}` : ""}`; + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; - } else if (viewElement.uiType === "rpc") { + } else if (viewElement.uiType === 'rpc') { viewSpecification = views[+(viewElement.inputViewId || 0)]; } } const updateResult = await restService.removeConfigElement(dataPath); if (checkResponseCode(updateResult)) { - const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]["error-message"] || ""; + const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); } } catch (error) { - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not remove ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not remove ${dataPath}` })); } finally { dispatch(new SetCollectingSelectionData(false)); } - - }; export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - const { configuration: { deviceDescription: { nodeId, views }, viewDescription: oldViewDescription } } = getState(); + const { configuration: { deviceDescription: { nodeId, views } } } = getState(); let dataPath = `/rests/operations/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; let viewSpecification: ViewSpecification = views[0]; let viewElement: ViewElement; @@ -684,17 +534,17 @@ export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async try { for (let ind = 0; ind < pathParts.length; ++ind) { let [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; + if (ind === 0) { defaultNS = namespace; } viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (isViewElementList(viewElement) && !key) { embedList = true; // if (viewElement && viewElement.isList && viewSpecification.parentView === "0") { - // throw new Error("Found a list at root level of a module w/o a refenrece key."); + // throw new Error("Found a list at root level of a module w/o a reference key."); // } // if (pathParts.length - 1 > ind) { // dispatch(new SetCollectingSelectionData(false)); @@ -710,28 +560,28 @@ export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async // } } - dataPath += `/${property}${key ? `=${key.replace(/\//ig, "%2F")}` : ""}`; + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; dataMember = viewElement.label; embedList = false; - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; - } else if (viewElement.uiType === "rpc") { + } else if (viewElement.uiType === 'rpc') { viewSpecification = views[+(viewElement.inputViewId || 0)]; } } // re-inflate formerly flatten rpc data data = data && Object.keys(data).reduce < { [name: string ]: any }>((acc, cur) => { - const pathParts = cur.split("."); + const innerPathParts = cur.split('.'); let pos = 0; const updatePath = (obj: any, key: string) => { - obj[key] = (pos >= pathParts.length) + obj[key] = (pos >= innerPathParts.length) ? data[cur] - : updatePath(obj[key] || {}, pathParts[pos++]); + : updatePath(obj[key] || {}, innerPathParts[pos++]); return obj; - } - updatePath(acc, pathParts[pos++]); + }; + updatePath(acc, innerPathParts[pos++]); return acc; }, {}) || null; @@ -746,22 +596,22 @@ export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async : data; // do not post root member (0) - if ((viewSpecification && viewSpecification.id !== "0") || (dataMember! && !data)) { + if ((viewSpecification && viewSpecification.id !== '0') || (dataMember! && !data)) { const updateResult = await restService.executeRpc(dataPath, { [`${defaultNS}:input`]: data || {} }); if (checkResponseCode(updateResult)) { - const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]["error-message"] || ""; + const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); } - dispatch(new UpdatOutputData(updateResult.data)); + dispatch(new UpdateOutputData(updateResult.data)); } else { - throw new Error(`There is NO RPC specified.`); + throw new Error('There is NO RPC specified.'); } // // update display specification // return } catch (error) { - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not change ${dataPath}` })); } finally { dispatch(new SetCollectingSelectionData(false)); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg b/sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg new file mode 100644 index 000000000..1b74cc479 --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts b/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts index 26c3944c9..7187c0a4e 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts @@ -16,13 +16,13 @@ * ============LICENSE_END========================================================================== */ -import { ViewElement } from "../models/uiModels"; +import { ViewElement } from '../models/uiModels'; export type BaseProps = { - value: ViewElement, - inputValue: TValue, - readOnly: boolean, - disabled: boolean, - onChange(newValue: TValue): void; - isKey?: boolean + value: ViewElement; + inputValue: TValue; + readOnly: boolean; + disabled: boolean; + onChange(newValue: TValue): void; + isKey?: boolean; }; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx index 8ce3106a6..b176e5db5 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx @@ -16,82 +16,86 @@ * ============LICENSE_END========================================================================== */ -import { ViewElementBase } from "models/uiModels"; -import { - TextField, - InputAdornment, - Input, - Tooltip, - Divider, - IconButton, - InputBase, - Paper, - Theme, - FormControl, - InputLabel, - FormHelperText, -} from "@mui/material"; +import React from 'react'; +import InputAdornment from '@mui/material/InputAdornment'; +import Input, { InputProps } from '@mui/material/Input'; +import Tooltip from '@mui/material/Tooltip'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import FormHelperText from '@mui/material/FormHelperText'; + import makeStyles from '@mui/styles/makeStyles'; import createStyles from '@mui/styles/createStyles'; -import * as React from 'react'; -import { faAdjust } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { InputProps } from "@mui/material/Input"; -const useStyles = makeStyles((theme: Theme) => +import { faAdjust } from '@fortawesome/free-solid-svg-icons/faAdjust'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { ViewElementBase } from '../models/uiModels'; + +const useStyles = makeStyles(() => createStyles({ iconDark: { - color: '#ff8800' + color: '#ff8800', }, iconLight: { - color: 'orange' + color: 'orange', }, padding: { paddingLeft: 10, - paddingRight: 10 + paddingRight: 10, }, }), ); -type IfwhenProps = InputProps & { +type IfWhenProps = InputProps & { label: string; element: ViewElementBase; helperText: string; error: boolean; - onChangeTooltipVisuability(value: boolean): void; + onChangeTooltipVisibility(value: boolean): void; }; -export const IfWhenTextInput = (props: IfwhenProps) => { +export const IfWhenTextInput = (props: IfWhenProps) => { - const { element, onChangeTooltipVisuability: toogleTooltip, id, label, helperText: errorText, error, style, ...otherProps } = props; + const { element, id, label, helperText: errorText, error, style, ...otherProps } = props; const classes = useStyles(); - const ifFeature = element.ifFeature ? ( - props.onChangeTooltipVisuability(false)} onMouseOut={e => props.onChangeTooltipVisuability(true)} title={element.ifFeature}> + props.onChangeTooltipVisibility(false)} + onMouseOut={() => props.onChangeTooltipVisibility(true)} + > - ) + ) : null; const whenFeature = element.when ? ( - props.onChangeTooltipVisuability(false)} onMouseOut={() => props.onChangeTooltipVisuability(true)} title={element.when}> + props.onChangeTooltipVisibility(false)} + onMouseOut={() => props.onChangeTooltipVisibility(true)} + > - ) + ) : null; return ( {label} - {ifFeature}{whenFeature}} {...otherProps} /> + {ifFeature}{whenFeature}} {...otherProps} /> {errorText} ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx index 81c9d6dcd..56fb93cea 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx @@ -16,43 +16,48 @@ * ============LICENSE_END========================================================================== */ -import * as React from "react" -import { MenuItem, FormHelperText, Select, FormControl, InputLabel } from "@mui/material"; +import React from 'react'; -import { ViewElementBoolean } from "../models/uiModels"; -import { BaseProps } from "./baseProps"; +import MenuItem from '@mui/material/MenuItem'; +import FormHelperText from '@mui/material/FormHelperText'; +import Select from '@mui/material/Select'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; + +import { ViewElementBoolean } from '../models/uiModels'; +import { BaseProps } from './baseProps'; type BooleanInputProps = BaseProps; export const UiElementBoolean = (props: BooleanInputProps) => { - const element = props.value as ViewElementBoolean; + const element = props.value as ViewElementBoolean; - const value = String(props.inputValue).toLowerCase(); - const mandetoryError = element.mandatory && value !== 'true' && value !== 'false'; + const value = String(props.inputValue).toLowerCase(); + const mandatoryError = element.mandatory && value !== 'true' && value !== 'false'; - return (!props.readOnly || element.id != null - ? ( + return (!props.readOnly || element.id != null + ? ( {element.label} - {mandetoryError ? "Value is mandetory" : ""} + {mandatoryError ? 'Value is mandatory' : ''} ) - : null - ); -} \ No newline at end of file + : null + ); +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx index 5937ed7b3..669ddff63 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx @@ -16,19 +16,23 @@ * ============LICENSE_END========================================================================== */ -import * as React from "react" -import { FormControl, InputLabel, Paper, Chip, FormHelperText, Dialog, DialogTitle, DialogContentText, DialogActions, Button, DialogContent } from "@mui/material"; +import React from 'react'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Chip from '@mui/material/Chip'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; + import makeStyles from '@mui/styles/makeStyles'; import AddIcon from '@mui/icons-material/Add'; import { Theme } from '@mui/material/styles'; -import { ViewElement } from "../models/uiModels"; - -import { BaseProps } from "./baseProps"; +import { ViewElement } from '../models/uiModels'; -type LeafListProps = BaseProps & { - getEditorForViewElement: (uiElement: ViewElement) => (null | React.ComponentType>) -}; +import { BaseProps } from './baseProps'; const useStyles = makeStyles((theme: Theme) => { const light = theme.palette.mode === 'light'; @@ -50,93 +54,93 @@ const useStyles = makeStyles((theme: Theme) => { margin: theme.spacing(0.5), }, underline: { - '&:after': { - borderBottom: `2px solid ${theme.palette.primary.main}`, - left: 0, - bottom: 0, - // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 - content: '""', - position: 'absolute', - right: 0, - transform: 'scaleX(0)', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shorter, - easing: theme.transitions.easing.easeOut, - }), - pointerEvents: 'none', // Transparent to the hover style. - }, - '&.Mui-focused:after': { - transform: 'scaleX(1)', - }, - '&.Mui-error:after': { - borderBottomColor: theme.palette.error.main, - transform: 'scaleX(1)', // error is always underlined in red - }, - '&:before': { + '&:after': { + borderBottom: `2px solid ${theme.palette.primary.main}`, + left: 0, + bottom: 0, + // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 + content: '""', + position: 'absolute', + right: 0, + transform: 'scaleX(0)', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shorter, + easing: theme.transitions.easing.easeOut, + }), + pointerEvents: 'none', // Transparent to the hover style. + }, + '&.Mui-focused:after': { + transform: 'scaleX(1)', + }, + '&.Mui-error:after': { + borderBottomColor: theme.palette.error.main, + transform: 'scaleX(1)', // error is always underlined in red + }, + '&:before': { + borderBottom: `1px solid ${bottomLineColor}`, + left: 0, + bottom: 0, + // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 + content: '"\\00a0"', + position: 'absolute', + right: 0, + transition: theme.transitions.create('border-bottom-color', { + duration: theme.transitions.duration.shorter, + }), + pointerEvents: 'none', // Transparent to the hover style. + }, + '&:hover:not($disabled):before': { + borderBottom: `2px solid ${theme.palette.text.primary}`, + // Reset on touch devices, it doesn't add specificity + // eslint-disable-next-line @typescript-eslint/naming-convention + '@media (hover: none)': { borderBottom: `1px solid ${bottomLineColor}`, - left: 0, - bottom: 0, - // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 - content: '"\\00a0"', - position: 'absolute', - right: 0, - transition: theme.transitions.create('border-bottom-color', { - duration: theme.transitions.duration.shorter, - }), - pointerEvents: 'none', // Transparent to the hover style. - }, - '&:hover:not($disabled):before': { - borderBottom: `2px solid ${theme.palette.text.primary}`, - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - borderBottom: `1px solid ${bottomLineColor}`, - }, - }, - '&.Mui-disabled:before': { - borderBottomStyle: 'dotted', }, }, - }) + '&.Mui-disabled:before': { + borderBottomStyle: 'dotted', + }, + }, + }); }); +type LeafListProps = BaseProps & { + getEditorForViewElement: (uiElement: ViewElement) => (null | React.ComponentType>); +}; + export const UiElementLeafList = (props: LeafListProps) => { const { value: element, inputValue, onChange } = props; const classes = useStyles(); const [open, setOpen] = React.useState(false); - const [editorValue, setEditorValue] = React.useState(""); + const [editorValue, setEditorValue] = React.useState(''); const [editorValueIndex, setEditorValueIndex] = React.useState(-1); - - const handleClickOpen = () => { - setOpen(true); - }; - const handleClose = () => { setOpen(false); }; const onApplyButton = () => { - if (editorValue != null && editorValue != "" && editorValueIndex < 0) { - props.onChange([ - ...inputValue, - editorValue, - ]); - } else if (editorValue != null && editorValue != "") { - props.onChange([ - ...inputValue.slice(0, editorValueIndex), - editorValue, - ...inputValue.slice(editorValueIndex+1), - ]); - } - setOpen(false); + if (editorValue != null && editorValue != '' && editorValueIndex < 0) { + props.onChange([ + ...inputValue, + editorValue, + ]); + } else if (editorValue != null && editorValue != '') { + props.onChange([ + ...inputValue.slice(0, editorValueIndex), + editorValue, + ...inputValue.slice(editorValueIndex + 1), + ]); + } + setOpen(false); }; const onDelete = (index : number) => { const newValue : any[] = [ ...inputValue.slice(0, index), - ...inputValue.slice(index+1), + ...inputValue.slice(index + 1), ]; onChange(newValue); }; @@ -151,15 +155,15 @@ export const UiElementLeafList = (props: LeafListProps) => { { !props.readOnly ?
  • } - label={"Add"} + label={'Add'} className={classes.chip} size="small" color="secondary" onClick={ () => { setOpen(true); - setEditorValue(""); + setEditorValue(''); setEditorValueIndex(-1); - } + } } />
  • : null } @@ -172,24 +176,24 @@ export const UiElementLeafList = (props: LeafListProps) => { label={String(val)} onDelete={ !props.readOnly ? () => { onDelete(ind); } : undefined } onClick={ !props.readOnly ? () => { - setOpen(true); - setEditorValue(val); - setEditorValueIndex(ind); - } : undefined + setOpen(true); + setEditorValue(val); + setEditorValueIndex(ind); + } : undefined } /> - )) + )) } {/* { "Value is mandetory"} */}
    - {editorValueIndex < 0 ? "Add new value" : "Edit value" } + {editorValueIndex < 0 ? 'Add new value' : 'Edit value' } { ValueEditor && { - + diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx index 76c11f6e5..b0342788f 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx @@ -16,14 +16,14 @@ * ============LICENSE_END========================================================================== */ -import { ViewElementNumber } from "models/uiModels"; +import React from 'react'; +import { ViewElementNumber } from "../models/uiModels"; import { Tooltip, InputAdornment } from "@mui/material"; -import * as React from 'react'; import { BaseProps } from "./baseProps"; import { IfWhenTextInput } from "./ifWhenTextInput"; -import { checkRange } from "./verifyer"; +import { checkRange } from "../utilities/verifyer"; -type numberInputProps = BaseProps; +type numberInputProps = BaseProps; export const UiElementNumber = (props: numberInputProps) => { @@ -49,12 +49,12 @@ export const UiElementNumber = (props: numberInputProps) => { setError(true); setHelperText("Input is not a number."); } - props.onChange(data); + props.onChange(num); } return ( - createStyles({ +const useStyles = makeStyles(() => createStyles({ button: { - "justifyContent": "left" + 'justifyContent': 'left', }, })); @@ -37,16 +37,31 @@ type UIElementReferenceProps = { }; export const UIElementReference: React.FC = (props) => { - const classes = useStyles(); - const [disabled, setDisabled] = useState(true); const { element } = props; + const [disabled, setDisabled] = useState(true); + const classes = useStyles(); return ( - { ev.preventDefault(); ev.stopPropagation(); ev.button === 1 && setDisabled(!disabled) }}> + { + ev.preventDefault(); + ev.stopPropagation(); + if (ev.button === 1) { + setDisabled(!disabled); + } + }}> - + ); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx index fdf803419..ebd04dab4 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx @@ -16,45 +16,54 @@ * ============LICENSE_END========================================================================== */ -import * as React from 'react'; +import React from 'react'; import { BaseProps } from './baseProps'; -import { ViewElementSelection } from '../models/uiModels' +import { ViewElementSelection } from '../models/uiModels'; import { FormControl, InputLabel, Select, FormHelperText, MenuItem, Tooltip } from '@mui/material'; type selectionProps = BaseProps; export const UiElementSelection = (props: selectionProps) => { - const element = props.value as ViewElementSelection; + const element = props.value as ViewElementSelection; - let error = ""; - const value = String(props.inputValue); - if (element.mandatory && Boolean(!value)) { - error = "Error"; - } + let error = ''; + const value = String(props.inputValue); + if (element.mandatory && Boolean(!value)) { + error = 'Error'; + } - return (props.readOnly || props.inputValue != null - ? ( - {element.label} + return (props.readOnly || props.inputValue != null + ? ( + {element.label} {error} ) - : null - ); -} \ No newline at end of file + : null + ); +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx index 4908c41aa..8381d99a4 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx @@ -21,7 +21,7 @@ import { Tooltip, TextField } from "@mui/material"; import { ViewElementString } from "../models/uiModels"; import { BaseProps } from "./baseProps"; import { IfWhenTextInput } from "./ifWhenTextInput"; -import { checkRange, checkPattern } from "./verifyer"; +import { checkRange, checkPattern } from "../utilities/verifyer"; type stringEntryProps = BaseProps ; @@ -69,7 +69,7 @@ export const UiElementString = (props: stringEntryProps) => { return ( - { }; return - { verifyValues(e.target.value) }} diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts b/sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts deleted file mode 100644 index 0a95cd8ca..000000000 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * ============LICENSE_START======================================================================== - * ONAP : ccsdk feature sdnr wt odlux - * ================================================================================================= - * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. - * ================================================================================================= - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - * ============LICENSE_END========================================================================== - */ - -import { Expression, YangRange, Operator, ViewElementNumber, ViewElementString, isViewElementNumber, isViewElementString } from '../models/uiModels'; - -export type validated = { isValid: boolean, error?: string } - -export type validatedRange = { isValid: boolean, error?: string }; - - -const rangeErrorStartNumber = "The entered number must be"; -const rangeErrorinnerMinTextNumber = "greater or equals than"; -const rangeErrorinnerMaxTextNumber = "less or equals than"; -const rangeErrorEndTextNumber = "."; - -const rangeErrorStartString = "The entered text must have"; -const rangeErrorinnerMinTextString = "no more than"; -const rangeErrorinnerMaxTextString = "less than"; -const rangeErrorEndTextString = " characters."; - -let errorMessageStart = ""; -let errorMessageMiddleMinPart = ""; -let errorMessageMiddleMaxPart = ""; -let errorMessageEnd = ""; - - -export function checkRange(element: ViewElementNumber | ViewElementString, data: number): string { - - //let test1: Operator = { operation: "AND", arguments: [{ operation: "OR", arguments: [{ operation: "AND", arguments: [new RegExp("^z", "g"), new RegExp("z$", "g")] }, new RegExp("^abc", "g"), new RegExp("^123", "g")] }, new RegExp("^def", "g"), new RegExp("^ppp", "g"), new RegExp("^aaa", "g")] }; - //let test1: Operator = { operation: "AND", arguments: [{ operation: "OR", arguments: [{ operation: "AND", arguments: [{ min: -5, max: 10 }, { min: -30, max: -20 }] }, { min: 8, max: 15 }] }] }; - //let test1: Operator = { operation: "OR", arguments: [{ operation: "OR", arguments: [{ min: -50, max: -40 }] }, { min: -30, max: -20 }, { min: 8, max: 15 }] }; - //let test1: Operator = { operation: "AND", arguments: [{ operation: "OR", arguments: [{ min: -5, max: 10 }, { min: 17, max: 23 }] }] }; - - const number = data; - - var expression = undefined; - - if (isViewElementString(element)) { - expression = element.length; - - errorMessageStart = rangeErrorStartString; - errorMessageMiddleMaxPart = rangeErrorinnerMaxTextString; - errorMessageMiddleMinPart = rangeErrorinnerMinTextString; - errorMessageEnd = rangeErrorEndTextString; - - } else if (isViewElementNumber(element)) { - expression = element.range; - - errorMessageStart = rangeErrorStartNumber; - errorMessageMiddleMaxPart = rangeErrorinnerMaxTextNumber; - errorMessageMiddleMinPart = rangeErrorinnerMinTextNumber; - errorMessageEnd = rangeErrorEndTextNumber; - } - - if (expression) { - if (isYangOperator(expression)) { - - const errorMessage = getRangeErrorMessages(expression, data); - return errorMessage; - - } else - if (isYangRange(expression)) { - - if (!isNaN(expression.min)) { - if (number < expression.min) { - return `${errorMessageStart} ${errorMessageMiddleMinPart} ${expression.min}${errorMessageEnd}`; - } - } - - if (!isNaN(expression.max)) { - if (number > expression.max) { - return `${errorMessageStart} ${errorMessageMiddleMaxPart} ${expression.max}${errorMessageEnd}`; - } - } - } - } - - - return ""; -} - -function isYangRange(val: YangRange | Operator): val is YangRange { - return (val as YangRange).min !== undefined; -} - -function isYangOperator(val: YangRange | Operator): val is Operator { - return (val as Operator).operation !== undefined; -} - -function getRangeErrorMessagesRecursively(value: Operator, data: number): string[] { - let currentItteration: string[] = []; - console.log(value); - - // itterate over all elements - for (let i = 0; i < value.arguments.length; i++) { - const element = value.arguments[i]; - - let min = undefined; - let max = undefined; - - let isNumberCorrect = false; - - if (isYangRange(element)) { - - //check found min values - if (!isNaN(element.min)) { - if (data < element.min) { - min = element.min; - } else { - isNumberCorrect = true; - } - } - - // check found max values - if (!isNaN(element.max)) { - if (data > element.max) { - max = element.max; - } else { - isNumberCorrect = true; - } - } - - // construct error messages - if (min != undefined) { - currentItteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMinPart} ${min}`); - } else if (max != undefined) { - currentItteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMaxPart} ${max}`); - - } - - } else if (isYangOperator(element)) { - - //get errormessages from expression - const result = getRangeErrorMessagesRecursively(element, data); - if (result.length === 0) { - isNumberCorrect = true; - } - currentItteration = currentItteration.concat(result); - } - - // if its an OR operation, the number has been checked and min/max are empty (thus not violated) - // delete everything found (because at least one found is correct, therefore all are correct) and break from loop - if (min === undefined && max === undefined && isNumberCorrect && value.operation === "OR") { - - currentItteration.splice(0, currentItteration.length); - break; - } - } - - return currentItteration; -} - -function getRangeErrorMessages(value: Operator, data: number): string { - - const currentItteration = getRangeErrorMessagesRecursively(value, data); - - // build complete error message from found parts - let errormessage = ""; - if (currentItteration.length > 1) { - - currentItteration.forEach((element, index) => { - if (index === 0) { - errormessage = createStartMessage(element); - } else if (index === currentItteration.length - 1) { - errormessage += ` ${element}${errorMessageEnd}`; - } else { - errormessage += `, ${element}` - } - }); - } else if (currentItteration.length == 1) { - errormessage = `${createStartMessage(currentItteration[0])}${errorMessageEnd}`; - } - - return errormessage; -} - -function createStartMessage(element: string) { - - //remove leading or or and from text - if (element.startsWith("and")) - element = element.replace("and", ""); - else if (element.startsWith("or")) - element = element.replace("or", ""); - - return `${errorMessageStart} ${element}`; -} - -export const checkPattern = (expression: RegExp | Operator | undefined, data: string): validated => { - - if (expression) { - if (isRegExp(expression)) { - const isValid = expression.test(data); - if (!isValid) - return { isValid: isValid, error: "The input is in a wrong format." }; - - } else if (isRegExpOperator(expression)) { - const result = isPatternValid(expression, data); - - if (!result) { - return { isValid: false, error: "The input is in a wrong format." }; - } - } - } - - return { isValid: true } -} - -function getRegexRecursively(value: Operator, data: string): boolean[] { - let currentItteration: boolean[] = []; - for (let i = 0; i < value.arguments.length; i++) { - const element = value.arguments[i]; - if (isRegExp(element)) { - // if regex is found, add it to list - currentItteration.push(element.test(data)) - } else if (isRegExpOperator(element)) { - //if RegexExpression is found, try to get regex from it - currentItteration = currentItteration.concat(getRegexRecursively(element, data)); - } - } - - if (value.operation === "OR") { - // if one is true, all are true, all found items can be discarded - let result = currentItteration.find(element => element); - if (result) { - return []; - } - } - return currentItteration; -} - -function isPatternValid(value: Operator, data: string): boolean { - - - // get all regex - const result = getRegexRecursively(value, data); - console.log(value); - - - if (value.operation === "AND") { - // if AND operation is executed... - // no element can be false - const check = result.find(element => element !== true); - if (check) - return false; - else - return true; - } else { - // if OR operation is executed... - // ... just one element must be true - const check = result.find(element => element === true); - if (check) - return true; - else - return false; - - } -} - -function isRegExp(val: RegExp | Operator): val is RegExp { - return (val as RegExp).source !== undefined; -} - -function isRegExpOperator(val: RegExp | Operator): val is Operator { - return (val as Operator).operation !== undefined; -} \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts index 1af699a6b..9cbd9163e 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts @@ -19,9 +19,9 @@ import { combineActionHandler } from '../../../../framework/src/flux/middleware'; import { IConnectedNetworkElementsState, connectedNetworkElementsActionHandler } from './connectedNetworkElementsHandler'; -import { IDeviceDescriptionState, deviceDescriptionHandler } from "./deviceDescriptionHandler"; -import { IViewDescriptionState, viewDescriptionHandler } from "./viewDescriptionHandler"; -import { IValueSelectorState, valueSelectorHandler } from "./valueSelectorHandler"; +import { IDeviceDescriptionState, deviceDescriptionHandler } from './deviceDescriptionHandler'; +import { IViewDescriptionState, viewDescriptionHandler } from './viewDescriptionHandler'; +import { IValueSelectorState, valueSelectorHandler } from './valueSelectorHandler'; interface IConfigurationAppStoreState { connectedNetworkElements: IConnectedNetworkElementsState; // used for ne selection @@ -32,7 +32,7 @@ interface IConfigurationAppStoreState { declare module '../../../../framework/src/store/applicationStore' { interface IApplicationStoreState { - configuration: IConfigurationAppStoreState, + configuration: IConfigurationAppStoreState; } } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts index 8ca8fdf27..d2863dd2e 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts @@ -25,8 +25,8 @@ import { restService } from '../services/restServices'; export interface IConnectedNetworkElementsState extends IExternalTableState { } -// create eleactic search material data fetch handler -const connectedNetworkElementsSearchHandler = createSearchDataHandler('network-element-connection', false, { status: "Connected" }); +// create elastic search material data fetch handler +const connectedNetworkElementsSearchHandler = createSearchDataHandler('network-element-connection', false, { status: 'Connected' }); export const { actionHandler: connectedNetworkElementsActionHandler, @@ -41,5 +41,5 @@ export const { 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/deviceDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts index 408399da4..cd01b0988 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts @@ -16,23 +16,23 @@ * ============LICENSE_END========================================================================== */ -import { Module } from "../models/yang"; -import { ViewSpecification } from "../models/uiModels"; -import { IActionHandler } from "../../../../framework/src/flux/action"; -import { UpdateDeviceDescription } from "../actions/deviceActions"; +import { Module } from '../models/yang'; +import { ViewSpecification } from '../models/uiModels'; +import { IActionHandler } from '../../../../framework/src/flux/action'; +import { UpdateDeviceDescription } from '../actions/deviceActions'; export interface IDeviceDescriptionState { - nodeId: string, + nodeId: string; modules: { - [name: string]: Module - }, - views: ViewSpecification[], + [name: string]: Module; + }; + views: ViewSpecification[]; } const deviceDescriptionStateInit: IDeviceDescriptionState = { - nodeId: "", + nodeId: '', modules: {}, - views: [] + views: [], }; export const deviceDescriptionHandler: IActionHandler = (state = deviceDescriptionStateInit, action) => { @@ -41,7 +41,7 @@ export const deviceDescriptionHandler: IActionHandler = ...state, nodeId: action.nodeId, modules: action.modules, - views: action.views + views: action.views, }; } return state; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts index 5b2d55ee2..70d5eb253 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts @@ -16,9 +16,9 @@ * ============LICENSE_END========================================================================== */ -import { IActionHandler } from "../../../../framework/src/flux/action"; -import { ViewSpecification } from "../models/uiModels"; -import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdatViewDescription, UpdatOutputData } from "../actions/deviceActions"; +import { IActionHandler } from '../../../../framework/src/flux/action'; +import { ViewSpecification } from '../models/uiModels'; +import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdateViewDescription, UpdateOutputData } from '../actions/deviceActions'; export interface IValueSelectorState { collectingData: boolean; @@ -28,13 +28,13 @@ export interface IValueSelectorState { onValueSelected: (value: any) => void; } -const nc = (val: React.SyntheticEvent) => { }; +const dummyFunc = () => { }; const valueSelectorStateInit: IValueSelectorState = { collectingData: false, keyProperty: undefined, listSpecification: null, listData: [], - onValueSelected: nc, + onValueSelected: dummyFunc, }; export const valueSelectorHandler: IActionHandler = (state = valueSelectorStateInit, action) => { @@ -53,22 +53,24 @@ export const valueSelectorHandler: IActionHandler = (state listData: action.listData, }; } else if (action instanceof SetSelectedValue) { - state.keyProperty && state.onValueSelected(action.value[state.keyProperty]); + if (state.keyProperty) { + state.onValueSelected(action.value[state.keyProperty]); + } state = { ...state, collectingData: false, keyProperty: undefined, listSpecification: null, - onValueSelected: nc, + onValueSelected: dummyFunc, listData: [], }; - } else if (action instanceof UpdateDeviceDescription || action instanceof UpdatViewDescription || action instanceof UpdatOutputData) { + } else if (action instanceof UpdateDeviceDescription || action instanceof UpdateViewDescription || action instanceof UpdateOutputData) { state = { ...state, collectingData: false, keyProperty: undefined, listSpecification: null, - onValueSelected: nc, + onValueSelected: dummyFunc, listData: [], }; } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts index ff85a97ea..39b47be84 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts @@ -16,18 +16,18 @@ * ============LICENSE_END========================================================================== */ -import { IActionHandler } from "../../../../framework/src/flux/action"; +import { IActionHandler } from '../../../../framework/src/flux/action'; -import { UpdatViewDescription, UpdatOutputData } from "../actions/deviceActions"; -import { ViewSpecification } from "../models/uiModels"; +import { UpdateViewDescription, UpdateOutputData } from '../actions/deviceActions'; +import { ViewSpecification } from '../models/uiModels'; export enum DisplayModeType { doNotDisplay = 0, displayAsObject = 1, displayAsList = 2, displayAsRPC = 3, - displayAsMessage = 4 -}; + displayAsMessage = 4, +} export type DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay; @@ -45,13 +45,13 @@ export type DisplaySpecification = { } | { displayMode: DisplayModeType.displayAsMessage; renderMessage: string; -} +}; export interface IViewDescriptionState { vPath: string | null; displaySpecification: DisplaySpecification; - viewData: any, - outputData?: any, + viewData: any; + outputData?: any; } const viewDescriptionStateInit: IViewDescriptionState = { @@ -64,7 +64,7 @@ const viewDescriptionStateInit: IViewDescriptionState = { }; export const viewDescriptionHandler: IActionHandler = (state = viewDescriptionStateInit, action) => { - if (action instanceof UpdatViewDescription) { + if (action instanceof UpdateViewDescription) { state = { ...state, vPath: action.vPath, @@ -72,7 +72,7 @@ export const viewDescriptionHandler: IActionHandler = (st outputData: undefined, displaySpecification: action.displaySpecification, }; - } else if (action instanceof UpdatOutputData) { + } else if (action instanceof UpdateOutputData) { state = { ...state, outputData: action.outputData, diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts index 88f70181c..e1ef1ea2d 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts @@ -24,7 +24,7 @@ export type NetworkElementConnection = { username?: string; password?: string; isRequired?: boolean; - status?: "connected" | "mounted" | "unmounted" | "connecting" | "disconnected" | "idle"; + status?: 'connected' | 'mounted' | 'unmounted' | 'connecting' | 'disconnected' | 'idle'; coreModelCapability?: string; deviceType?: string; nodeDetails?: { @@ -33,5 +33,5 @@ export type NetworkElementConnection = { failureReason: string; capability: string; }[]; - } -} + }; +}; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts index 29484d812..7d9e63caf 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts @@ -16,128 +16,130 @@ * ============LICENSE_END========================================================================== */ +import type { WhenAST } from '../yang/whenParser'; + export type ViewElementBase = { - "id": string; - "label": string; - "module": string; - "path": string; - "config": boolean; - "ifFeature"?: string; - "when"?: string; - "mandatory"?: boolean; - "description"?: string; - "isList"?: boolean; - "default"?: string; - "status"?: "current" | "deprecated" | "obsolete", - "reference"?: string, // https://tools.ietf.org/html/rfc7950#section-7.21.4 -} + 'id': string; + 'label': string; + 'module': string; + 'path': string; + 'config': boolean; + 'ifFeature'?: string; + 'when'?: WhenAST; + 'mandatory'?: boolean; + 'description'?: string; + 'isList'?: boolean; + 'default'?: string; + 'status'?: 'current' | 'deprecated' | 'obsolete'; + 'reference'?: string; // https://tools.ietf.org/html/rfc7950#section-7.21.4 +}; // https://tools.ietf.org/html/rfc7950#section-9.8 export type ViewElementBinary = ViewElementBase & { - "uiType": "binary"; - "length"?: Expression; // number of octets -} + 'uiType': 'binary'; + 'length'?: Expression; // number of octets +}; // https://tools.ietf.org/html/rfc7950#section-9.7.4 export type ViewElementBits = ViewElementBase & { - "uiType": "bits"; - "flags": { + 'uiType': 'bits'; + 'flags': { [name: string]: number | undefined; // 0 - 4294967295 - } -} + }; +}; // https://tools.ietf.org/html/rfc7950#section-9 export type ViewElementString = ViewElementBase & { - "uiType": "string"; - "pattern"?: Expression; - "length"?: Expression; - "invertMatch"?: true; -} + 'uiType': 'string'; + 'pattern'?: Expression; + 'length'?: Expression; + 'invertMatch'?: true; +}; // special case derived from export type ViewElementDate = ViewElementBase & { - "uiType": "date"; - "pattern"?: Expression; - "length"?: Expression; - "invertMatch"?: true; -} + 'uiType': 'date'; + 'pattern'?: Expression; + 'length'?: Expression; + 'invertMatch'?: true; +}; // https://tools.ietf.org/html/rfc7950#section-9.3 export type ViewElementNumber = ViewElementBase & { - "uiType": "number"; - "min": number; - "max": number; - "range"?: Expression; - "units"?: string; - "format"?: string; - "fDigits"?: number; -} + 'uiType': 'number'; + 'min': number; + 'max': number; + 'range'?: Expression; + 'units'?: string; + 'format'?: string; + 'fDigits'?: number; +}; // https://tools.ietf.org/html/rfc7950#section-9.5 export type ViewElementBoolean = ViewElementBase & { - "uiType": "boolean"; - "trueValue"?: string; - "falseValue"?: string; -} + 'uiType': 'boolean'; + 'trueValue'?: string; + 'falseValue'?: string; +}; // https://tools.ietf.org/html/rfc7950#section-9.6.4 export type ViewElementSelection = ViewElementBase & { - "uiType": "selection"; - "multiSelect"?: boolean - "options": { - "key": string; - "value": string; - "description"?: string, - "status"?: "current" | "deprecated" | "obsolete", - "reference"?: string, + 'uiType': 'selection'; + 'multiSelect'?: boolean; + 'options': { + 'key': string; + 'value': string; + 'description'?: string; + 'status'?: 'current' | 'deprecated' | 'obsolete'; + 'reference'?: string; }[]; -} +}; // is a list if isList is true ;-) export type ViewElementObject = ViewElementBase & { - "uiType": "object"; - "isList"?: false; - "viewId": string; -} + 'uiType': 'object'; + 'isList'?: false; + 'viewId': string; +}; // Hint: read only lists do not need a key export type ViewElementList = (ViewElementBase & { - "uiType": "object"; - "isList": true; - "viewId": string; - "key"?: string; + 'uiType': 'object'; + 'isList': true; + 'viewId': string; + 'key'?: string; }); export type ViewElementReference = ViewElementBase & { - "uiType": "reference"; - "referencePath": string; - "ref": (currentPath: string) => [ViewElement , string] | undefined; -} + 'uiType': 'reference'; + 'referencePath': string; + 'ref': (currentPath: string) => [ViewElement, string] | undefined; +}; export type ViewElementUnion = ViewElementBase & { - "uiType": "union"; - "elements": ViewElement[]; -} + 'uiType': 'union'; + 'elements': ViewElement[]; +}; -export type ViewElementChoiseCase = { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }; +export type ViewElementChoiceCase = { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }; -export type ViewElementChoise = ViewElementBase & { - "uiType": "choise"; - "cases": { - [name: string]: ViewElementChoiseCase; - } -} +export type ViewElementChoice = ViewElementBase & { + 'uiType': 'choice'; + 'cases': { + [name: string]: ViewElementChoiceCase; + }; +}; // https://tools.ietf.org/html/rfc7950#section-7.14.1 export type ViewElementRpc = ViewElementBase & { - "uiType": "rpc"; - "inputViewId"?: string; - "outputViewId"?: string; -} + 'uiType': 'rpc'; + 'inputViewId'?: string; + 'outputViewId'?: string; +}; export type ViewElementEmpty = ViewElementBase & { - "uiType": "empty"; -} + 'uiType': 'empty'; +}; export type ViewElement = | ViewElementEmpty @@ -152,88 +154,88 @@ export type ViewElement = | ViewElementSelection | ViewElementReference | ViewElementUnion - | ViewElementChoise + | ViewElementChoice | ViewElementRpc; export const isViewElementString = (viewElement: ViewElement): viewElement is ViewElementString => { - return viewElement && (viewElement.uiType === "string" || viewElement.uiType === "date"); -} + return viewElement && (viewElement.uiType === 'string' || viewElement.uiType === 'date'); +}; export const isViewElementDate = (viewElement: ViewElement): viewElement is ViewElementDate => { - return viewElement && (viewElement.uiType === "date"); -} + return viewElement && (viewElement.uiType === 'date'); +}; export const isViewElementNumber = (viewElement: ViewElement): viewElement is ViewElementNumber => { - return viewElement && viewElement.uiType === "number"; -} + return viewElement && viewElement.uiType === 'number'; +}; export const isViewElementBoolean = (viewElement: ViewElement): viewElement is ViewElementBoolean => { - return viewElement && viewElement.uiType === "boolean"; -} + return viewElement && viewElement.uiType === 'boolean'; +}; export const isViewElementObject = (viewElement: ViewElement): viewElement is ViewElementObject => { - return viewElement && viewElement.uiType === "object" && viewElement.isList === false; -} + return viewElement && viewElement.uiType === 'object' && viewElement.isList === false; +}; export const isViewElementList = (viewElement: ViewElement): viewElement is ViewElementList => { - return viewElement && viewElement.uiType === "object" && viewElement.isList === true; -} + return viewElement && viewElement.uiType === 'object' && viewElement.isList === true; +}; export const isViewElementObjectOrList = (viewElement: ViewElement): viewElement is ViewElementObject | ViewElementList => { - return viewElement && viewElement.uiType === "object"; -} + return viewElement && viewElement.uiType === 'object'; +}; export const isViewElementSelection = (viewElement: ViewElement): viewElement is ViewElementSelection => { - return viewElement && viewElement.uiType === "selection"; -} + return viewElement && viewElement.uiType === 'selection'; +}; export const isViewElementReference = (viewElement: ViewElement): viewElement is ViewElementReference => { - return viewElement && viewElement.uiType === "reference"; -} + return viewElement && viewElement.uiType === 'reference'; +}; export const isViewElementUnion = (viewElement: ViewElement): viewElement is ViewElementUnion => { - return viewElement && viewElement.uiType === "union"; -} + return viewElement && viewElement.uiType === 'union'; +}; -export const isViewElementChoise = (viewElement: ViewElement): viewElement is ViewElementChoise => { - return viewElement && viewElement.uiType === "choise"; -} +export const isViewElementChoice = (viewElement: ViewElement): viewElement is ViewElementChoice => { + return viewElement && viewElement.uiType === 'choice'; +}; export const isViewElementRpc = (viewElement: ViewElement): viewElement is ViewElementRpc => { - return viewElement && viewElement.uiType === "rpc"; -} + return viewElement && viewElement.uiType === 'rpc'; +}; export const isViewElementEmpty = (viewElement: ViewElement): viewElement is ViewElementRpc => { - return viewElement && viewElement.uiType === "empty"; -} + return viewElement && viewElement.uiType === 'empty'; +}; -export const ResolveFunction = Symbol("IsResolved"); +export const ResolveFunction = Symbol('IsResolved'); export type ViewSpecification = { - "id": string; - "ns"?: string; - "name"?: string; - "title"?: string; - "parentView"?: string; - "language": string; - "ifFeature"?: string; - "when"?: string; - "uses"?: (string[]) & { [ResolveFunction]?: (parent: string) => void }; - "elements": { [name: string]: ViewElement }; - "config": boolean; - readonly "canEdit": boolean; -} + id: string; + ns?: string; + name?: string; + title?: string; + parentView?: string; + language: string; + ifFeature?: string; + when?: WhenAST; + uses?: (string[]) & { [ResolveFunction]?: (parent: string) => void }; + elements: { [name: string]: ViewElement }; + config: boolean; + readonly canEdit: boolean; +}; export type YangRange = { - min: number, - max: number, -} + min: number; + max: number; +}; export type Expression = | T | Operator; export type Operator = { - operation: "AND" | "OR"; + operation: 'AND' | 'OR'; arguments: Expression[]; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts index 79704ae34..e4e59fb96 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts @@ -16,7 +16,7 @@ * ============LICENSE_END========================================================================== */ -import { ViewElement, ViewSpecification } from "./uiModels"; +import { ViewElement, ViewSpecification } from './uiModels'; export enum ModuleState { stable, @@ -30,27 +30,27 @@ export type Token = { value: string; start: number; end: number; -} +}; export type Statement = { key: string; arg?: string; sub?: Statement[]; -} +}; export type Identity = { - id: string, - label: string, - base?: string, - description?: string, - reference?: string, - children?: Identity[], - values?: Identity[], -} + id: string; + label: string; + base?: string; + description?: string; + reference?: string; + children?: Identity[]; + values?: Identity[]; +}; export type Revision = { - description?: string, - reference?: string + description?: string; + reference?: string; }; export type Module = { @@ -68,4 +68,4 @@ export type Module = { views: { [view: string]: ViewSpecification }; elements: { [view: string]: ViewElement }; executionOrder?: number; -} \ No newline at end of file +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx index e37879102..7dd2d6ae4 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx @@ -16,71 +16,74 @@ * ============LICENSE_END========================================================================== */ -import * as React from "react"; +import React from 'react'; import { withRouter, RouteComponentProps, Route, Switch, Redirect } from 'react-router-dom'; -import { faAdjust } from '@fortawesome/free-solid-svg-icons'; // select app icon - -import connect, { Connect, IDispatcher } from '../../../framework/src/flux/connect'; +import { connect, Connect, IDispatcher } from '../../../framework/src/flux/connect'; import applicationManager from '../../../framework/src/services/applicationManager'; -import { IApplicationStoreState } from "../../../framework/src/store/applicationStore"; -import { configurationAppRootHandler } from "./handlers/configurationAppRootHandler"; -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"; +import { configurationAppRootHandler } from './handlers/configurationAppRootHandler'; +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'; + +const appIcon = require('./assets/icons/configurationAppIcon.svg'); // select app icon let currentNodeId: string | null | undefined = undefined; let currentVirtualPath: string | null | undefined = undefined; let lastUrl: string | undefined = undefined; -const mapDisp = (dispatcher: IDispatcher) => ({ +const mapDispatch = (dispatcher: IDispatcher) => ({ updateNodeId: (nodeId: string) => dispatcher.dispatch(updateNodeIdAsyncActionCreator(nodeId)), updateView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), }); -const ConfigurationApplicationRouteAdapter = connect(undefined, mapDisp)((props: RouteComponentProps<{ nodeId?: string, 0: string }> & Connect) => { +// eslint-disable-next-line @typescript-eslint/naming-convention +const ConfigurationApplicationRouteAdapter = connect(undefined, mapDispatch)((props: RouteComponentProps<{ nodeId?: string; 0: string }> & Connect) => { React.useEffect(() => { return () => { lastUrl = undefined; currentNodeId = undefined; currentVirtualPath = undefined; - } + }; }, []); if (props.location.pathname !== lastUrl) { - // ensure the asynchronus update will only be called once per path + // ensure the asynchronous update will only be called once per path lastUrl = props.location.pathname; window.setTimeout(async () => { // check if the nodeId has changed - let dump = false; + let enableDump = false; if (currentNodeId !== props.match.params.nodeId) { currentNodeId = props.match.params.nodeId || undefined; - if (currentNodeId && currentNodeId.endsWith("|dump")) { - dump = true; + if (currentNodeId && currentNodeId.endsWith('|dump')) { + enableDump = true; currentNodeId = currentNodeId.replace(/\|dump$/i, ''); } currentVirtualPath = null; - currentNodeId && (await props.updateNodeId(currentNodeId)); + if (currentNodeId) { + await props.updateNodeId(currentNodeId); + } } if (currentVirtualPath !== props.match.params[0]) { currentVirtualPath = props.match.params[0]; - if (currentVirtualPath && currentVirtualPath.endsWith("|dump")) { - dump = true; + if (currentVirtualPath && currentVirtualPath.endsWith('|dump')) { + enableDump = true; currentVirtualPath = currentVirtualPath.replace(/\|dump$/i, ''); } await props.updateView(currentVirtualPath); } - if (dump) { + if (enableDump) { 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"; + if (view === null) return 'Empty'; const indention = Array(level * 4).fill(' ').join(''); let result = ''; @@ -88,24 +91,24 @@ const ConfigurationApplicationRouteAdapter = connect(undefined, mapDisp)((props: // 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.uiType === "rpc" ? "x" : 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`; + acc += `${indention} [${elm.uiType === 'rpc' ? 'x' : 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 ? "mandatory" : "none"} - ${elm.path} \r\n`; switch (elm.uiType) { - case "object": + 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'); + const element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(dump)); - element.setAttribute('download', currentNodeId + ".txt"); + element.setAttribute('download', currentNodeId + '.txt'); element.style.display = 'none'; document.body.appendChild(element); @@ -133,10 +136,10 @@ const App = withRouter((props: RouteComponentProps) => ( export function register() { applicationManager.registerApplication({ - name: "configuration", - icon: faAdjust, + name: 'configuration', + icon: appIcon, rootComponent: App, rootActionHandler: configurationAppRootHandler, - menuEntry: "Configuration" + menuEntry: 'Configuration', }); } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts index 02060ef12..07e263559 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts @@ -16,79 +16,79 @@ * ============LICENSE_END========================================================================== */ -import { requestRest, requestRestExt } from "../../../../framework/src/services/restService"; -import { convertPropertyNames, replaceHyphen } from "../../../../framework/src/utilities/yangHelper"; +import { requestRest, requestRestExt } from '../../../../framework/src/services/restService'; +import { convertPropertyNames, replaceHyphen } from '../../../../framework/src/utilities/yangHelper'; -import { NetworkElementConnection } from "../models/networkElementConnection"; +import { NetworkElementConnection } from '../models/networkElementConnection'; type ImportOnlyResponse = { - "ietf-yang-library:yang-library": { - "module-set": { - "import-only-module": { - "name": string, - "revision": string, - }[], - }[], - }, -} + 'ietf-yang-library:yang-library': { + 'module-set': { + 'import-only-module': { + 'name': string; + 'revision': string; + }[]; + }[]; + }; +}; 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, - }[] - } - }[] -} + '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 = { availableCapabilities: { - capabilityOrigin: string, - capability: string, - version: string, - }[] | null, + capabilityOrigin: string; + capability: string; + version: string; + }[] | null; unavailableCapabilities: { - failureReason: string, - capability: string, - version: string, - }[] | null, + failureReason: string; + capability: string; + version: string; + }[] | null; importOnlyModules: { - name: string, - revision: string, - }[] | null -} + name: string; + revision: string; + }[] | null; +}; const capParser = /^\(.*\?revision=(\d{4}-\d{2}-\d{2})\)(\S+)$/i; class RestService { public getNetworkElementUri = (nodeId: string) => '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; - public async getImportOnlyModules(nodeId: string): Promise<{ name: string, revision: string }[]> { + public async getImportOnlyModules(nodeId: string): Promise<{ name: string; revision: string }[]> { const path = `${this.getNetworkElementUri(nodeId)}/yang-ext:mount/ietf-yang-library:yang-library?content=nonconfig&fields=module-set(import-only-module(name;revision))`; - const importOnlyResult = await requestRest(path, { method: "GET" }); + const importOnlyResult = await requestRest(path, { method: 'GET' }); const importOnlyModules = importOnlyResult - ? importOnlyResult["ietf-yang-library:yang-library"]["module-set"][0]["import-only-module"] + ? importOnlyResult['ietf-yang-library:yang-library']['module-set'][0]['import-only-module'] : []; return importOnlyModules; } public async getCapabilitiesByMountId(nodeId: string): Promise { const path = this.getNetworkElementUri(nodeId); - const capabilitiesResult = await requestRest(path, { method: "GET" }); - const availableCapabilities = 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 capabilitiesResult = await requestRest(path, { method: 'GET' }); + const availableCapabilities = 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)) || []) .map(cap => { const capMatch = cap && capParser.exec(cap.capability); return capMatch ? { @@ -98,20 +98,20 @@ class RestService { } : null ; }).filter((cap) => cap != null) || [] as any; - const unavailableCapabilities = 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)) || []) - .map(cap => { - const capMatch = cap && capParser.exec(cap.capability); - return capMatch ? { - failureReason: cap.failureReason, - capability: capMatch && capMatch[2] || '', - version: capMatch && capMatch[1] || '', - } : null ; - }).filter((cap) => cap != null) || [] as any; - - const importOnlyModules = availableCapabilities && availableCapabilities.findIndex((ac: {capability: string }) => ac.capability && ac.capability.toLowerCase() === "ietf-yang-library") > -1 + const unavailableCapabilities = 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)) || []) + .map(cap => { + const capMatch = cap && capParser.exec(cap.capability); + return capMatch ? { + failureReason: cap.failureReason, + capability: capMatch && capMatch[2] || '', + version: capMatch && capMatch[1] || '', + } : null ; + }).filter((cap) => cap != null) || [] as any; + + const importOnlyModules = availableCapabilities && availableCapabilities.findIndex((ac: { capability: string }) => ac.capability && ac.capability.toLowerCase() === 'ietf-yang-library') > -1 ? await this.getImportOnlyModules(nodeId) : null; @@ -123,11 +123,11 @@ class RestService { // const connectedNetworkElement = await requestRest(path, { method: "GET" }); // return connectedNetworkElement || null; - const path = "/rests/operations/data-provider:read-network-element-connection-list"; - const body = { "data-provider:input": { "filter": [{ "property": "node-id", "filtervalue": nodeId }], "sortorder": [], "pagination": { "size": 1, "page": 1 } } }; - const networkElementResult = await requestRest<{ "data-provider:output": { data: NetworkElementConnection[] } }>(path, { method: "POST", body: JSON.stringify(body) }); - return networkElementResult && networkElementResult["data-provider:output"] && networkElementResult["data-provider:output"].data && - networkElementResult["data-provider:output"].data.map(obj => convertPropertyNames(obj, replaceHyphen))[0] || null; + const path = '/rests/operations/data-provider:read-network-element-connection-list'; + const body = { 'data-provider:input': { 'filter': [{ 'property': 'node-id', 'filtervalue': nodeId }], 'sortorder': [], 'pagination': { 'size': 1, 'page': 1 } } }; + const networkElementResult = await requestRest<{ 'data-provider:output': { data: NetworkElementConnection[] } }>(path, { method: 'POST', body: JSON.stringify(body) }); + return networkElementResult && networkElementResult['data-provider:output'] && networkElementResult['data-provider:output'].data && + networkElementResult['data-provider:output'].data.map(obj => convertPropertyNames(obj, replaceHyphen))[0] || null; } /** Reads the config data by restconf path. @@ -135,7 +135,7 @@ class RestService { * @returns The data. */ public getConfigData(path: string) { - return requestRestExt<{ [key: string]: any }>(path, { method: "GET" }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'GET' }); } /** Updates or creates the config data by restconf path using data. @@ -144,11 +144,11 @@ class RestService { * @returns The written data. */ public setConfigData(path: string, data: any) { - return requestRestExt<{ [key: string]: any }>(path, { method: "PUT", body: JSON.stringify(data) }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'PUT', body: JSON.stringify(data) }); } public executeRpc(path: string, data: any) { - return requestRestExt<{ [key: string]: any }>(path, { method: "POST", body: JSON.stringify(data) }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'POST', body: JSON.stringify(data) }); } /** Removes the element by restconf path. @@ -156,7 +156,7 @@ class RestService { * @returns The restconf result. */ public removeConfigElement(path: string) { - return requestRestExt<{ [key: string]: any }>(path, { method: "DELETE" }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'DELETE' }); } } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts index b81a92c14..bbd051aeb 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts @@ -16,28 +16,22 @@ * ============LICENSE_END========================================================================== */ -type YangInfo = [string, (string | null | undefined)]; +const cache: { [path: string]: string } = { }; +const getCapability = async (capability: string, nodeId: string, version?: string) => { + const url = `/yang-schema/${capability}${version ? `/${version}` : ''}?node=${nodeId}`; -const cache: { [path: string]: string } = { + const cacheHit = cache[url]; + if (cacheHit) return cacheHit; -}; - -class YangService { - - public async getCapability(capability: string, nodeId: string, version?: string) { - const url = `/yang-schema/${capability}${version ? `/${version}` : ""}?node=${nodeId}`; - - const cacheHit = cache[url]; - if (cacheHit) return cacheHit; - - const res = await fetch(url); - const yangFile = res.ok && (await res.text()); - if (yangFile !== false && yangFile !== null) { - cache[url] = yangFile; - } - return yangFile; + const res = await fetch(url); + const yangFile = res.ok && (await res.text()); + if (yangFile !== false && yangFile !== null) { + cache[url] = yangFile; } -} + return yangFile; +}; -export const yangService = new YangService(); +export const yangService = { + getCapability, +}; export default yangService; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts b/sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts new file mode 100644 index 000000000..9dd12031f --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts @@ -0,0 +1,261 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import { YangRange, Operator, ViewElementNumber, ViewElementString, isViewElementNumber, isViewElementString } from '../models/uiModels'; + +export type validated = { isValid: boolean; error?: string }; + +export type validatedRange = { isValid: boolean; error?: string }; + +const rangeErrorStartNumber = 'The entered number must be'; +const rangeErrorInnerMinTextNumber = 'greater or equals than'; +const rangeErrorInnerMaxTextNumber = 'less or equals than'; +const rangeErrorEndTextNumber = '.'; + +const rangeErrorStartString = 'The entered text must have'; +const rangeErrorInnerMinTextString = 'no more than'; +const rangeErrorInnerMaxTextString = 'less than'; +const rangeErrorEndTextString = ' characters.'; + +let errorMessageStart = ''; +let errorMessageMiddleMinPart = ''; +let errorMessageMiddleMaxPart = ''; +let errorMessageEnd = ''; + +const isYangRange = (val: YangRange | Operator): val is YangRange => (val as YangRange).min !== undefined; + +const isYangOperator = (val: YangRange | Operator): val is Operator => (val as Operator).operation !== undefined; + +const isRegExp = (val: RegExp | Operator): val is RegExp => (val as RegExp).source !== undefined; + +const isRegExpOperator = (val: RegExp | Operator): val is Operator => (val as Operator).operation !== undefined; + +const getRangeErrorMessagesRecursively = (value: Operator, data: number): string[] => { + let currentIteration: string[] = []; + + // iterate over all elements + for (let i = 0; i < value.arguments.length; i++) { + const element = value.arguments[i]; + + let min = undefined; + let max = undefined; + + let isNumberCorrect = false; + + if (isYangRange(element)) { + + //check found min values + if (!isNaN(element.min)) { + if (data < element.min) { + min = element.min; + } else { + isNumberCorrect = true; + } + } + + // check found max values + if (!isNaN(element.max)) { + if (data > element.max) { + max = element.max; + } else { + isNumberCorrect = true; + } + } + + // construct error messages + if (min != undefined) { + currentIteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMinPart} ${min}`); + } else if (max != undefined) { + currentIteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMaxPart} ${max}`); + + } + + } else if (isYangOperator(element)) { + + //get error_message from expression + const result = getRangeErrorMessagesRecursively(element, data); + if (result.length === 0) { + isNumberCorrect = true; + } + currentIteration = currentIteration.concat(result); + } + + // if its an OR operation, the number has been checked and min/max are empty (thus not violated) + // delete everything found (because at least one found is correct, therefore all are correct) and break from loop + if (min === undefined && max === undefined && isNumberCorrect && value.operation === 'OR') { + + currentIteration.splice(0, currentIteration.length); + break; + } + } + + return currentIteration; +}; + +const createStartMessage = (element: string) => { + //remove leading or or and from text + if (element.startsWith('and')) { + element = element.replace('and', ''); + } else if (element.startsWith('or')) { + element = element.replace('or', ''); + } + return `${errorMessageStart} ${element}`; +}; + +const getRangeErrorMessages = (value: Operator, data: number): string => { + + const currentIteration = getRangeErrorMessagesRecursively(value, data); + + // build complete error message from found parts + let errorMessage = ''; + if (currentIteration.length > 1) { + + currentIteration.forEach((element, index) => { + if (index === 0) { + errorMessage = createStartMessage(element); + } else if (index === currentIteration.length - 1) { + errorMessage += ` ${element}${errorMessageEnd}`; + } else { + errorMessage += `, ${element}`; + } + }); + } else if (currentIteration.length == 1) { + errorMessage = `${createStartMessage(currentIteration[0])}${errorMessageEnd}`; + } + + return errorMessage; +}; + +export const checkRange = (element: ViewElementNumber | ViewElementString, data: number): string => { + const number = data; + + let expression = undefined; + + if (isViewElementString(element)) { + expression = element.length; + + errorMessageStart = rangeErrorStartString; + errorMessageMiddleMaxPart = rangeErrorInnerMaxTextString; + errorMessageMiddleMinPart = rangeErrorInnerMinTextString; + errorMessageEnd = rangeErrorEndTextString; + + } else if (isViewElementNumber(element)) { + expression = element.range; + + errorMessageStart = rangeErrorStartNumber; + errorMessageMiddleMaxPart = rangeErrorInnerMaxTextNumber; + errorMessageMiddleMinPart = rangeErrorInnerMinTextNumber; + errorMessageEnd = rangeErrorEndTextNumber; + } + + if (expression) { + if (isYangOperator(expression)) { + + const errorMessage = getRangeErrorMessages(expression, data); + return errorMessage; + + } else + if (isYangRange(expression)) { + + if (!isNaN(expression.min)) { + if (number < expression.min) { + return `${errorMessageStart} ${errorMessageMiddleMinPart} ${expression.min}${errorMessageEnd}`; + } + } + + if (!isNaN(expression.max)) { + if (number > expression.max) { + return `${errorMessageStart} ${errorMessageMiddleMaxPart} ${expression.max}${errorMessageEnd}`; + } + } + } + } + + return ''; +}; + +const getRegexRecursively = (value: Operator, data: string): boolean[] => { + let currentItteration: boolean[] = []; + for (let i = 0; i < value.arguments.length; i++) { + const element = value.arguments[i]; + if (isRegExp(element)) { + // if regex is found, add it to list + currentItteration.push(element.test(data)); + } else if (isRegExpOperator(element)) { + //if RegexExpression is found, try to get regex from it + currentItteration = currentItteration.concat(getRegexRecursively(element, data)); + } + } + + if (value.operation === 'OR') { + // if one is true, all are true, all found items can be discarded + let result = currentItteration.find(element => element); + if (result) { + return []; + } + } + return currentItteration; +}; + +const isPatternValid = (value: Operator, data: string): boolean => { + // get all regex + const result = getRegexRecursively(value, data); + + if (value.operation === 'AND') { + // if AND operation is executed... + // no element can be false + const check = result.find(element => element !== true); + if (check) + return false; + else + return true; + } else { + // if OR operation is executed... + // ... just one element must be true + const check = result.find(element => element === true); + if (check) + return true; + else + return false; + + } +}; + +export const checkPattern = (expression: RegExp | Operator | undefined, data: string): validated => { + + if (expression) { + if (isRegExp(expression)) { + const isValid = expression.test(data); + if (!isValid) + return { isValid: isValid, error: 'The input is in a wrong format.' }; + + } else if (isRegExpOperator(expression)) { + const result = isPatternValid(expression, data); + + if (!result) { + return { isValid: false, error: 'The input is in a wrong format.' }; + } + } + } + + return { isValid: true }; +}; + + + + diff --git a/sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts b/sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts new file mode 100644 index 000000000..ad34c83b9 --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts @@ -0,0 +1,324 @@ +import { storeService } from '../../../../framework/src/services/storeService'; +import { WhenAST, WhenTokenType } from '../yang/whenParser'; + +import { + ViewSpecification, + ViewElement, + isViewElementReference, + isViewElementList, + isViewElementObjectOrList, + isViewElementRpc, + isViewElementChoice, + ViewElementChoiceCase, +} from '../models/uiModels'; + +import { Module } from '../models/yang'; + +import { restService } from '../services/restServices'; + +export type HttpResult = { + status: number; + message?: string | undefined; + data: { + [key: string]: any; + } | null | undefined; +}; + +export const checkResponseCode = (restResult: HttpResult) =>{ + //403 gets handled by the framework from now on + return restResult.status !== 403 && ( restResult.status < 200 || restResult.status > 299); +}; + +export const resolveVPath = (current: string, vPath: string): string => { + if (vPath.startsWith('/')) { + return vPath; + } + const parts = current.split('/'); + const vPathParts = vPath.split('/'); + for (const part of vPathParts) { + if (part === '.') { + continue; + } else if (part === '..') { + parts.pop(); + } else { + parts.push(part); + } + } + return parts.join('/'); +}; + +export const splitVPath = (vPath: string, vPathParser : RegExp): [string, string?][] => { + const pathParts: [string, string?][] = []; + let partMatch: RegExpExecArray | null; + if (vPath) do { + partMatch = vPathParser.exec(vPath); + if (partMatch) { + pathParts.push([partMatch[1], partMatch[2] || undefined]); + } + } while (partMatch); + return pathParts; +}; + +const derivedFrom = (vPath: string, when: WhenAST, viewData: any, includeSelf = false) => { + if (when.args?.length !== 2) { + throw new Error('derived-from or derived-from-or-self requires 2 arguments.'); + } + const [arg1, arg2] = when.args; + if (arg1.type !== WhenTokenType.IDENTIFIER || arg2.type !== WhenTokenType.STRING) { + throw new Error('derived-from or derived-from-or-self requires first argument IDENTIFIER and second argument STRING.'); + } + + if (!storeService.applicationStore) { + throw new Error('storeService.applicationStore is not defined.'); + } + + const pathParts = splitVPath(arg1.value as string || '', /(?:(?:([^\/\:]+):)?([^\/]+))/g); + const referenceValueParts = /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(arg2.value as string || ''); + + if (!pathParts || !referenceValueParts || pathParts.length === 0 || referenceValueParts.length === 0) { + throw new Error('derived-from or derived-from-or-self requires first argument PATH and second argument IDENTITY.'); + } + + if (pathParts[0][1]?.startsWith('..') || pathParts[0][1]?.startsWith('/')) { + throw new Error('derived-from or derived-from-or-self currently only supports relative paths.'); + } + + const { configuration: { deviceDescription: { modules } } } = storeService.applicationStore.state; + const dataValue = pathParts.reduce((acc, [ns, prop]) => { + if (prop === '.') { + return acc; + } + if (acc && prop) { + const moduleName = ns && (Object.values(modules).find((m: Module) => m.prefix === ns) || Object.values(modules).find((m: Module) => m.name === ns))?.name; + return (moduleName) ? acc[`${moduleName}:${prop}`] || acc[prop] : acc[prop]; + } + return undefined; + }, viewData); + + let dataValueParts = dataValue && /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(dataValue); + if (!dataValueParts || dataValueParts.length < 2) { + throw new Error(`derived-from or derived-from-or-self value referenced by first argument [${arg1.value}] not found.`); + } + let [, dataValueNs, dataValueProp] = dataValueParts; + let dataValueModule: Module = dataValueNs && (Object.values(modules).find((m: Module) => m.name === dataValueNs)); + let dataValueIdentity = dataValueModule && dataValueModule.identities && (Object.values(dataValueModule.identities).find((i) => i.label === dataValueProp)); + + if (!dataValueIdentity) { + throw new Error(`derived-from or derived-from-or-self identity [${dataValue}] referenced by first argument [${arg1.value}] not found.`); + } + + const [, referenceValueNs, referenceValueProp] = referenceValueParts; + const referenceValueModule = referenceValueNs && (Object.values(modules).find((m: Module) => m.prefix === referenceValueNs)); + const referenceValueIdentity = referenceValueModule && referenceValueModule.identities && (Object.values(referenceValueModule.identities).find((i) => i.label === referenceValueProp)); + + if (!referenceValueIdentity) { + throw new Error(`derived-from or derived-from-or-self identity [${arg2.value}] referenced by second argument not found.`); + } + + let result = includeSelf && (referenceValueIdentity === dataValueIdentity); + while (dataValueIdentity && dataValueIdentity.base && !result) { + dataValueParts = dataValue && /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(dataValueIdentity.base); + const [, innerDataValueNs, innerDataValueProp] = dataValueParts; + dataValueModule = innerDataValueNs && (Object.values(modules).find((m: Module) => m.prefix === innerDataValueNs)) || dataValueModule; + dataValueIdentity = dataValueModule && dataValueModule.identities && (Object.values(dataValueModule.identities).find((i) => i.label === innerDataValueProp)) ; + result = (referenceValueIdentity === dataValueIdentity); + } + + return result; +}; + +const evaluateWhen = async (vPath: string, when: WhenAST, viewData: any): Promise => { + switch (when.type) { + case WhenTokenType.FUNCTION: + switch (when.name) { + case 'derived-from-or-self': + return derivedFrom(vPath, when, viewData, true); + case 'derived-from': + return derivedFrom(vPath, when, viewData, false); + default: + throw new Error(`Unknown function ${when.name}`); + } + case WhenTokenType.AND: + return !when.left || !when.right || (await evaluateWhen(vPath, when.left, viewData) && await evaluateWhen(vPath, when.right, viewData)); + case WhenTokenType.OR: + return !when.left || !when.right || (await evaluateWhen(vPath, when.left, viewData) || await evaluateWhen(vPath, when.right, viewData)); + case WhenTokenType.NOT: + return !when.right || ! await evaluateWhen(vPath, when.right, viewData); + case WhenTokenType.EXPRESSION: + return !(when.value && typeof when.value !== 'string') || await evaluateWhen(vPath, when.value, viewData); + } + return true; +}; + +export const getReferencedDataList = async (refPath: string, dataPath: string, modules: { [name: string]: Module }, views: ViewSpecification[]) => { + const pathParts = splitVPath(refPath, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property + const defaultNS = pathParts[0][0]; + let referencedModule = modules[defaultNS]; + + let dataMember: string; + let view: ViewSpecification; + let currentNS: string | null = null; + let dataUrls = [dataPath]; + let data: any; + + for (let i = 0; i < pathParts.length; ++i) { + const [pathPartNS, pathPart] = pathParts[i]; + const namespace = pathPartNS != null ? (currentNS = pathPartNS) : currentNS; + + const viewElement = i === 0 + ? views[0].elements[`${referencedModule.name}:${pathPart}`] + : view!.elements[`${pathPart}`] || view!.elements[`${namespace}:${pathPart}`]; + + if (!viewElement) throw new Error(`Could not find ${pathPart} in ${refPath}`); + if (i < pathParts.length - 1) { + if (!isViewElementObjectOrList(viewElement)) { + throw Error(`Module: [${referencedModule.name}].[${viewElement.label}]. View element is not list or object.`); + } + view = views[+viewElement.viewId]; + const resultingDataUrls : string[] = []; + if (isViewElementList(viewElement)) { + for (let j = 0; j < dataUrls.length; ++j) { + const dataUrl = dataUrls[j]; + const restResult = (await restService.getConfigData(dataUrl)); + if (restResult.data == null || checkResponseCode(restResult)) { + const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || ''; + throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); + } + + let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; + if (dataRaw === undefined) { + dataRaw = restResult.data[dataMember!]; + } + dataRaw = dataRaw instanceof Array + ? dataRaw[0] + : dataRaw; + + data = dataRaw && dataRaw[viewElement.label] || []; + const keys: string[] = data.map((entry: { [key: string]: any } )=> entry[viewElement.key!]); + resultingDataUrls.push(...keys.map(key => `${dataUrl}/${viewElement.label.replace(/\//ig, '%2F')}=${key.replace(/\//ig, '%2F')}`)); + } + dataMember = viewElement.label; + } else { + // just a member, not a list + const pathSegment = (i === 0 + ? `/${referencedModule.name}:${viewElement.label.replace(/\//ig, '%2F')}` + : `/${viewElement.label.replace(/\//ig, '%2F')}`); + resultingDataUrls.push(...dataUrls.map(dataUrl => dataUrl + pathSegment)); + dataMember = viewElement.label; + } + dataUrls = resultingDataUrls; + } else { + data = []; + for (let j = 0; j < dataUrls.length; ++j) { + const dataUrl = dataUrls[j]; + const restResult = (await restService.getConfigData(dataUrl)); + if (restResult.data == null || checkResponseCode(restResult)) { + const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || ''; + throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); + } + let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; + if (dataRaw === undefined) { + dataRaw = restResult.data[dataMember!]; + } + dataRaw = dataRaw instanceof Array + ? dataRaw[0] + : dataRaw; + data.push(dataRaw); + } + // BUG UUID ist nicht in den elements enthalten !!!!!! + const key = viewElement && viewElement.label || pathPart; + return { + view: view!, + data: data, + key: key, + }; + } + } + return null; +}; + +export const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification): ViewSpecification =>{ + + // resolve all references. + view = { ...view }; + view.elements = Object.keys(view.elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { + const resolveHistory : ViewElement[] = []; + let elm = view.elements[cur]; + const key = defaultNS && cur.replace(new RegExp(`^${defaultNS}:`, 'i'), '') || cur; + while (isViewElementReference(elm)) { + const result = (elm.ref(vPath)); + if (result) { + const [referencedElement, referencedPath] = result; + if (resolveHistory.some(hist => hist === referencedElement)) { + console.error(`Circle reference found at: ${vPath}`, resolveHistory); + break; + } + elm = referencedElement; + vPath = referencedPath; + resolveHistory.push(elm); + } + } + + acc[key] = { ...elm, id: key }; + + return acc; + }, {}); + return view; +}; + +export const flattenViewElements = (defaultNS: string | null, parentPath: string, elements: { [name: string]: ViewElement }, views: ViewSpecification[], currentPath: string ): { [name: string]: ViewElement } => { + if (!elements) return {}; + return Object.keys(elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { + const elm = elements[cur]; + + // remove the default namespace, and only the default namespace, sine it seems that this is also not in the restconf response + const elmKey = defaultNS && elm.id.replace(new RegExp(`^${defaultNS}:`, 'i'), '') || elm.id; + const key = parentPath ? `${parentPath}.${elmKey}` : elmKey; + + if (isViewElementRpc(elm)) { + console.warn(`Flatten of RFC not supported ! [${currentPath}][${elm.label}]`); + return acc; + } else if (isViewElementObjectOrList(elm)) { + const view = views[+elm.viewId]; + const inner = view && flattenViewElements(defaultNS, key, view.elements, views, `${currentPath}/${view.name}`); + if (inner) { + Object.keys(inner).forEach(k => (acc[k] = inner[k])); + } + } else if (isViewElementChoice(elm)) { + acc[key] = { + ...elm, + id: key, + cases: Object.keys(elm.cases).reduce<{ [name: string]: ViewElementChoiceCase }>((accCases, curCases) => { + const caseElement = elm.cases[curCases]; + accCases[curCases] = { + ...caseElement, + // Hint: do not use key it contains elmKey, which shell be omitted for cases. + elements: flattenViewElements(defaultNS, /*key*/ parentPath, caseElement.elements, views, `${currentPath}/${elm.label}`), + }; + return accCases; + }, {}), + }; + } else { + acc[key] = { + ...elm, + id: key, + }; + } + return acc; + }, {}); +}; + +export const filterViewElements = async (vPath: string, viewData: any, viewSpecification: ViewSpecification) => { + // filter elements of viewSpecification by evaluating when property + return Object.keys(viewSpecification.elements).reduce(async (accPromise, cur) => { + const acc = await accPromise; + const elm = viewSpecification.elements[cur]; + if (!elm.when || await evaluateWhen(vPath || '', elm.when, viewData).catch((ex) => { + console.warn(`Error evaluating when clause at: ${viewSpecification.name} for element: ${cur}`, ex); + return true; + })) { + acc.elements[cur] = elm; + } + return acc; + }, Promise.resolve({ ...viewSpecification, elements: {} as { [key: string]: ViewElement } })); +}; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx index 0e2ddb395..0f143d818 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -25,17 +25,41 @@ import { WithStyles } from '@mui/styles'; import withStyles from '@mui/styles/withStyles'; import createStyles from '@mui/styles/createStyles'; -import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect"; -import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table"; -import { Loader } from "../../../../framework/src/components/material-ui/loader"; +import { useConfirm } from 'material-ui-confirm'; + +import { connect, IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; +import { Loader } from '../../../../framework/src/components/material-ui/loader'; 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, isViewElementDate } from "../models/uiModels"; - -import { getAccessPolicyByUrl } from "../../../../framework/src/services/restService"; +import { + SetSelectedValue, + updateDataActionAsyncCreator, + updateViewActionAsyncCreator, + removeElementActionAsyncCreator, + executeRpcActionAsyncCreator, +} from '../actions/deviceActions'; + +import { + ViewElement, + ViewSpecification, + ViewElementChoice, + ViewElementRpc, + isViewElementString, + isViewElementNumber, + isViewElementBoolean, + isViewElementObjectOrList, + isViewElementSelection, + isViewElementChoice, + isViewElementUnion, + isViewElementRpc, + isViewElementEmpty, + isViewElementDate, +} from '../models/uiModels'; + +import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; import Fab from '@mui/material/Fab'; import AddIcon from '@mui/icons-material/Add'; @@ -44,23 +68,22 @@ import ArrowBack from '@mui/icons-material/ArrowBack'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import SaveIcon from '@mui/icons-material/Save'; import EditIcon from '@mui/icons-material/Edit'; -import Tooltip from "@mui/material/Tooltip"; -import FormControl from "@mui/material/FormControl"; -import IconButton from "@mui/material/IconButton"; - -import InputLabel from "@mui/material/InputLabel"; -import Select from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Tooltip from '@mui/material/Tooltip'; +import FormControl from '@mui/material/FormControl'; +import IconButton from '@mui/material/IconButton'; + +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; import Button from '@mui/material/Button'; -import Link from "@mui/material/Link"; +import Link from '@mui/material/Link'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; import Typography from '@mui/material/Typography'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; - import { BaseProps } from '../components/baseProps'; import { UIElementReference } from '../components/uiElementReference'; import { UiElementNumber } from '../components/uiElementNumber'; @@ -70,44 +93,43 @@ import { UiElementSelection } from '../components/uiElementSelection'; import { UIElementUnion } from '../components/uiElementUnion'; import { UiElementLeafList } from '../components/uiElementLeafList'; -import { useConfirm } from 'material-ui-confirm'; -import restService from '../services/restServices'; +import { splitVPath } from '../utilities/viewEngineHelper'; const styles = (theme: Theme) => createStyles({ header: { - "display": "flex", - "justifyContent": "space-between", + 'display': 'flex', + 'justifyContent': 'space-between', }, leftButton: { - "justifyContent": "left" + 'justifyContent': 'left', }, outer: { - "flex": "1", - "height": "100%", - "display": "flex", - "alignItems": "center", - "justifyContent": "center", + 'flex': '1', + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center', + 'justifyContent': 'center', }, inner: { }, container: { - "height": "100%", - "display": "flex", - "flexDirection": "column", + 'height': '100%', + 'display': 'flex', + 'flexDirection': 'column', }, - "icon": { - "marginRight": theme.spacing(0.5), - "width": 20, - "height": 20, + 'icon': { + 'marginRight': theme.spacing(0.5), + 'width': 20, + 'height': 20, }, - "fab": { - "margin": theme.spacing(1), + 'fab': { + 'margin': theme.spacing(1), }, button: { margin: 0, - padding: "6px 6px", - minWidth: 'unset' + padding: '6px 6px', + minWidth: 'unset', }, readOnly: { '& label.Mui-focused': { @@ -129,29 +151,29 @@ const styles = (theme: Theme) => createStyles({ }, }, uiView: { - overflowY: "auto", + overflowY: 'auto', }, section: { - padding: "15px", + padding: '15px', borderBottom: `2px solid ${theme.palette.divider}`, }, viewElements: { - width: 485, marginLeft: 20, marginRight: 20 + width: 485, marginLeft: 20, marginRight: 20, }, verificationElements: { - width: 485, marginLeft: 20, marginRight: 20 + width: 485, marginLeft: 20, marginRight: 20, }, heading: { fontSize: theme.typography.pxToRem(15), fontWeight: theme.typography.fontWeightRegular, }, moduleCollection: { - marginTop: "16px", - overflow: "auto", + marginTop: '16px', + overflow: 'auto', }, objectReult: { - overflow: "auto" - } + overflow: 'auto', + }, }); const mapProps = (state: IApplicationStoreState) => ({ @@ -183,10 +205,10 @@ type ConfigurationApplicationComponentState = { editMode: boolean; canEdit: boolean; viewData: { [key: string]: any } | null; - choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; -} + choices: { [path: string]: { selectedCase: string; data: { [property: string]: any } } }; +}; -type GetStatelessComponentProps = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any +type GetStatelessComponentProps = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any; const AccordionSummaryExt: React.FC> = (props) => { const [disabled, setDisabled] = useState(true); const onMouseDown = (ev: React.MouseEvent) => { @@ -202,7 +224,7 @@ const AccordionSummaryExt: React.FC { /** @@ -216,17 +238,17 @@ class ConfigurationApplicationComponent extends React.Component { + private static getChoicesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { return Object.keys(elements).reduce((acc, cur) => { const elm = elements[cur]; - if (isViewElementChoise(elm)) { + if (isViewElementChoice(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 + // find the right case for this choice, 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 => { @@ -255,26 +277,26 @@ class ConfigurationApplicationComponent extends React.Component { this.props.history.push(`${this.props.match.url}${path}`); - } + }; private changeValueFor = (property: string, value: any) => { this.setState({ viewData: { ...this.state.viewData, - [property]: value - } + [property]: value, + }, }); - } + }; private collectData = (elements: { [name: string]: ViewElement }) => { - // ensure only active choises will be contained + // ensure only active choices 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; + const choiceKeys = Object.keys(elements).filter(elmKey => isViewElementChoice(elements[elmKey])); + const elementsToRemove = choiceKeys.reduce((acc, curChoiceKey) => { + const currentChoice = elements[curChoiceKey] as ViewElementChoice; + const selectedCase = this.state.choices[curChoiceKey].selectedCase; Object.keys(currentChoice.cases).forEach(caseKey => { const caseElements = currentChoice.cases[caseKey].elements; if (caseKey === selectedCase) { @@ -311,7 +333,7 @@ class ConfigurationApplicationComponent extends React.Component { acc.push(caseElements[caseElementKey]); }); @@ -325,17 +347,17 @@ class ConfigurationApplicationComponent extends React.Component { 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>) => { if (isViewElementEmpty(uiElement)) { @@ -353,12 +375,12 @@ class ConfigurationApplicationComponent extends React.Component { const isKey = (uiElement.label === keyProperty); @@ -377,7 +399,7 @@ class ConfigurationApplicationComponent extends React.Component { this.changeValueFor(uiElement.id, e) }} + onChange={(e) => { this.changeValueFor(uiElement.id, e); }} getEditorForViewElement={this.getEditorForViewElement} />; } else { @@ -391,7 +413,7 @@ class ConfigurationApplicationComponent extends React.Component { this.changeValueFor(uiElement.id, e) }} + onChange={(e) => { this.changeValueFor(uiElement.id, e); }} />) : null; } @@ -418,14 +440,14 @@ class ConfigurationApplicationComponent extends React.Component { + private renderUIChoice = (uiElement: ViewElementChoice, 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 currentChoice = this.state.choices[uiElement.id]; + const currentCase = currentChoice && uiElement.cases[currentChoice.selectedCase]; const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - if (isViewElementChoise(uiElement)) { + if (isViewElementChoice(uiElement)) { const subElements = currentCase?.elements; return ( <> @@ -435,14 +457,14 @@ class ConfigurationApplicationComponent extends React.Component { - if (currentChoise.selectedCase === e.target.value) { + if (currentChoice.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 } } }); + this.setState({ choices: { ...this.state.choices, [uiElement.id]: { ...this.state.choices[uiElement.id], selectedCase: e.target.value as string } } }); }} readOnly={!canEdit} disabled={editMode && !canEdit} - value={this.state.choises[uiElement.id].selectedCase} + value={this.state.choices[uiElement.id].selectedCase} inputProps={{ name: uiElement.id, id: `select-${uiElement.id}`, @@ -452,7 +474,7 @@ class ConfigurationApplicationComponent extends React.Component { const caseElm = uiElement.cases[caseKey]; return ( -
    {caseElm.label}
    +
    {caseElm.label}
    ); }) } @@ -463,13 +485,13 @@ class ConfigurationApplicationComponent extends React.ComponentInvalid Choise + :

    Invalid Choice

    } ); } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + if (process.env.NODE_ENV !== 'production') { + console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`); } return null; } @@ -478,8 +500,6 @@ class ConfigurationApplicationComponent extends React.Component { const { classes } = this.props; - - const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { if (keyProperty) { // if (vsA.label === vsB.label) return 0; @@ -497,15 +517,15 @@ class ConfigurationApplicationComponent extends React.Component {sections.references.map(element => ( - { this.navigate(`/${elm.id}`) }} /> + { this.navigate(`/${elm.id}`); }} /> ))} ) : null } - {sections.choises.length > 0 + {sections.choices.length > 0 ? (
    - {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} + {sections.choices.map(element => this.renderUIChoice(element, viewData, keyProperty, editMode, isNew))}
    ) : null } @@ -539,7 +559,7 @@ class ConfigurationApplicationComponent extends React.Component {sections.rpcs.map(element => ( - { this.navigate(`/${elm.id}`) }} /> + { this.navigate(`/${elm.id}`); }} /> ))} ) : null @@ -550,6 +570,7 @@ class ConfigurationApplicationComponent extends React.Component { 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]; @@ -565,6 +586,7 @@ class ConfigurationApplicationComponent extends React.Component { const moduleView = modules[key]; + return ( } aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} > @@ -584,8 +606,8 @@ class ConfigurationApplicationComponent extends React.Component { - navigate("[]"); // empty key means new element + navigate('[]'); // empty key means new element }, disabled: !config, }; @@ -615,11 +637,11 @@ class ConfigurationApplicationComponent extends React.Component void }> = (props) => { + const DeleteIconWithConfirmation: React.FC<{ disabled?: boolean; 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" }, cancellationButtonProps: { color:"inherit" } }) - .then(() => { - let keyId = ""; - if (listKeyProperty && listKeyProperty.split(" ").length > 1) { - keyId += listKeyProperty.split(" ").map(id => props.rowData[id]).join(","); + confirm({ title: 'Do you really want to delete this element ?', description: 'This action is permanent!', confirmationButtonProps: { color: 'secondary' }, cancellationButtonProps: { color:'inherit' } }) + .then(() => { + let keyId = ''; + if (listKeyProperty && listKeyProperty.split(' ').length > 1) { + keyId += listKeyProperty.split(' ').map(id => props.rowData[id]).join(','); } else { - keyId = props.rowData[listKeyProperty]; - } - return removeElement(`${this.props.vPath}[${keyId}]`) + keyId = props.rowData[listKeyProperty]; + } + return removeElement(`${this.props.vPath}[${keyId}]`); }).then(props.onReload); }} size="large"> @@ -643,44 +665,46 @@ class ConfigurationApplicationComponent extends React.Component ); - } + }; return ( []>((acc, cur) => { const elm = listElements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) { if (elm.label !== listKeyProperty) { - acc.push(elm.uiType === "boolean" + acc.push(elm.uiType === 'boolean' ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" + : elm.uiType === 'date' ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); } else { - acc.unshift(elm.uiType === "boolean" + acc.unshift(elm.uiType === 'boolean' ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" + : elm.uiType === 'date' ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + : { 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 }) => { + 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(); - let keyId = "" - if (listKeyProperty && listKeyProperty.split(" ").length > 1) { - keyId += listKeyProperty.split(" ").map(id => row[id]).join(","); + let keyId = ''; + if (listKeyProperty && listKeyProperty.split(' ').length > 1) { + keyId += listKeyProperty.split(' ').map(id => row[id]).join(','); } else { keyId = row[listKeyProperty]; } - listKeyProperty && navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key. + if (listKeyProperty) { + navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key. + } }} > ); } @@ -704,17 +728,17 @@ class ConfigurationApplicationComponent extends React.Component { 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); + console.error('Object should not appear in RPC view !'); + } else if (isViewElementChoice(elm)) { + acc.choices.push(elm); } else if (isViewElementRpc(elm)) { - console.error("RPC should not appear in RPC view !"); + 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[] }; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }) + || { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }; sections.elements = sections.elements.sort(orderFunc); @@ -728,10 +752,10 @@ class ConfigurationApplicationComponent extends React.Component ) : null } - { sections.choises.length > 0 + { sections.choices.length > 0 ? (
    - {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} + {sections.choices.map(element => this.renderUIChoice(element, inputViewData, keyProperty, editMode, isNew))}
    ) : null } @@ -747,13 +771,13 @@ class ConfigurationApplicationComponent extends React.Component ); - }; + } 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 lastPath = '/configuration'; let basePath = `/configuration/${nodeId}`; return (
    @@ -774,7 +798,7 @@ class ConfigurationApplicationComponent extends React.Component { const path = `${basePath}/${prop}`; const keyPath = key && `${basePath}/${prop}[${key}]`; - const propTitle = prop.replace(/^[^:]+:/, ""); + const propTitle = prop.replace(/^[^:]+:/, ''); const ret = ( ) => { ev.preventDefault(); this.props.history.push(keyPath); - }}>{`[${key && key.replace(/\%2C/g, ",")}]`} || null - } + }}>{`[${key && key.replace(/\%2C/g, ',')}]`} || null + } ); lastPath = basePath; @@ -802,7 +826,9 @@ class ConfigurationApplicationComponent extends React.Component {this.state.editMode && ( { - this.props.vPath && (await this.props.reloadView(this.props.vPath)); + if (this.props.vPath) { + await this.props.reloadView(this.props.vPath); + } this.setState({ editMode: false }); }} > ) || null} @@ -810,7 +836,7 @@ class ConfigurationApplicationComponent extends React.Component { if (this.state.editMode) { - // ensure only active choises will be contained + // ensure only active choices will be contained const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); this.props.onUpdateData(this.props.vPath!, resultingViewData); } @@ -830,7 +856,7 @@ class ConfigurationApplicationComponent extends React.Component[]>((acc, cur) => { const elm = listSpecification.elements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + 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 }); + 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 }); + acc.unshift({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); } } return acc; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx index 1a1008dad..e96f40d61 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx @@ -16,15 +16,15 @@ * ============LICENSE_END========================================================================== */ -import * as React from 'react'; +import React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect"; -import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import { MaterialTable, MaterialTableCtorType, ColumnType } from "../../../../framework/src/components/material-table"; -import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from "../../../configurationApp/src/handlers/connectedNetworkElementsHandler"; +import { connect, IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { MaterialTable, MaterialTableCtorType, ColumnType } from '../../../../framework/src/components/material-table'; -import { NetworkElementConnection } from "../models/networkElementConnection"; +import { NetworkElementConnection } from '../models/networkElementConnection'; +import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from '../../../configurationApp/src/handlers/connectedNetworkElementsHandler'; const mapProps = (state: IApplicationStoreState) => ({ @@ -47,20 +47,20 @@ class NetworkElementSelectorComponent extends React.Component { this.props.history.push(`${this.props.match.path}/${row.nodeId}`) }} columns={[ - { property: "nodeId", title: "Node Name", type: ColumnType.text }, - { property: "isRequired", title: "Required", type: ColumnType.boolean }, - { property: "host", title: "Host", type: ColumnType.text }, - { property: "port", title: "Port", type: ColumnType.numeric }, - { property: "coreModelCapability", title: "Core Model", type: ColumnType.text }, - { property: "deviceType", title: "Type", type: ColumnType.text }, + { this.props.history.push(`${this.props.match.path}/${row.nodeId}`); }} columns={[ + { property: 'nodeId', title: 'Node Name', type: ColumnType.text }, + { property: 'isRequired', title: 'Required', type: ColumnType.boolean }, + { property: 'host', title: 'Host', type: ColumnType.text }, + { property: 'port', title: 'Port', type: ColumnType.numeric }, + { property: 'coreModelCapability', title: 'Core Model', type: ColumnType.text }, + { property: 'deviceType', title: 'Type', type: ColumnType.text }, ]} idProperty="id" {...this.props.connectedNetworkElementsActions} {...this.props.connectedNetworkElementsProperties} asynchronus > ); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts new file mode 100644 index 000000000..fa2968c9c --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts @@ -0,0 +1,235 @@ +enum WhenTokenType { + AND = 'AND', + OR = 'OR', + NOT = 'NOT', + EQUALS = 'EQUALS', + COMMA = 'COMMA', + STRING = 'STRING', + FUNCTION = 'FUNCTION', + IDENTIFIER = 'IDENTIFIER', + OPEN_PAREN = 'OPEN_PAREN', + CLOSE_PAREN = 'CLOSE_PAREN', + EXPRESSION = 'EXPRESSION', +} + +type Token = { + type: WhenTokenType; + value: string; +}; + +const isAlpha = (char: string) => /[a-z]/i.test(char); + +const isAlphaNumeric = (char: string) => /[A-Za-z0-9_\-/:\.]/i.test(char); + +const lex = (input: string) : Token[] => { + let tokens = [] as any[]; + let current = 0; + + while (current < input.length) { + let char = input[current]; + + if (char === ' ') { + current++; + continue; + } + + if (char === '(') { + tokens.push({ type: WhenTokenType.OPEN_PAREN, value: char }); + current++; + continue; + } + + if (char === ')') { + tokens.push({ type: WhenTokenType.CLOSE_PAREN, value: char }); + current++; + continue; + } + + if (char === '=') { + tokens.push({ type: WhenTokenType.EQUALS, value: char }); + current++; + continue; + } + + if (char === ',') { + tokens.push({ type: WhenTokenType.COMMA, value: char }); + current++; + continue; + } + + if (char === '\"' || char === '\'') { + let value = ''; + let start = current; + current++; + + while (current < input.length) { + let innerChar = input[current]; + if (innerChar === '\\') { + value += input[current] + input[current + 1]; + current += 2; + } else if (innerChar === input[start]) { + current++; + break; + } else { + value += innerChar; + current++; + } + } + + tokens.push({ type: WhenTokenType.STRING, value }); + continue; + } + + if (isAlpha(char)) { + let value = ''; + while (isAlpha(char)) { + value += char; + char = input[++current]; + } + + switch (value) { + case 'and': + tokens.push({ type: WhenTokenType.AND }); + break; + case 'or': + tokens.push({ type: WhenTokenType.OR }); + break; + case 'not': + tokens.push({ type: WhenTokenType.NOT }); + break; + case 'eq': + tokens.push({ type: WhenTokenType.EQUALS }); + break; + default: + while (isAlphaNumeric(char)) { + value += char; + char = input[++current]; + } + tokens.push({ type: WhenTokenType.IDENTIFIER, value }); + } + + continue; + } + if (isAlphaNumeric(char)) { + let value = ''; + while (isAlphaNumeric(char)) { + value += char; + char = input[++current]; + } + + tokens.push({ type: WhenTokenType.IDENTIFIER, value }); + continue; + } + throw new TypeError(`I don't know what this character is: ${char}`); + } + return tokens; +}; + +type WhenAST = { + type: WhenTokenType; + left?: WhenAST; + right?: WhenAST; + value?: string | WhenAST; + name?: string; + args?: WhenAST[]; +}; + +const precedence : { [index: string] : number } = { + 'EQUALS': 4, + 'NOT': 3, + 'AND': 2, + 'OR': 1, +}; + +const parseWhen = (whenExpression: string) => { + const tokens = lex(whenExpression); + let current = 0; + + const walk = (precedenceLevel = 0) : WhenAST => { + let token = tokens[current]; + let node: WhenAST | null = null; + + if (token.type === WhenTokenType.OPEN_PAREN) { + token = tokens[++current]; + let innerNode: WhenAST = { type: WhenTokenType.EXPRESSION, value: walk() }; + token = tokens[current]; + + while (token.type !== WhenTokenType.CLOSE_PAREN) { + innerNode = { + type: token.type, + value: token.value, + left: innerNode, + right: walk(), + }; + token = tokens[current]; + } + current++; + return innerNode; + } + + if (token.type === WhenTokenType.STRING ) { + current++; + node = { type: token.type, value: token.value }; + } + + if (token.type === WhenTokenType.NOT) { + token = tokens[++current]; + node = { type: WhenTokenType.NOT, value: token.value, right: walk() }; + } + + if (token.type === WhenTokenType.IDENTIFIER) { + const nextToken = tokens[current + 1]; + if (nextToken.type === WhenTokenType.OPEN_PAREN) { + let name = token.value; + token = tokens[++current]; + + let args = []; + token = tokens[++current]; + + while (token.type !== WhenTokenType.CLOSE_PAREN) { + if (token.type === WhenTokenType.COMMA) { + current++; + } else { + args.push(walk()); + } + token = tokens[current]; + } + + current++; + node = { type: WhenTokenType.FUNCTION, name, args }; + } else { + current++; + node = { type: WhenTokenType.IDENTIFIER, value: token.value }; + } + } + + if (!node) throw new TypeError('Unexpected token: ' + token.type); + + token = tokens[current]; + while (current < tokens.length && precedence[token.type] >= precedenceLevel) { + console.log(current, tokens[current], tokens[current].type, precedenceLevel, precedence[token.type]); + token = tokens[current]; + if (token.type === WhenTokenType.EQUALS || token.type === WhenTokenType.AND || token.type === WhenTokenType.OR) { + current++; + node = { + type: token.type, + left: node, + right: walk(precedence[token.type]), + }; + } else { + break; + } + } + + return node; + + }; + + return walk(); +}; + +export { + parseWhen, + WhenAST, + WhenTokenType, +}; \ 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 e8e636f9b..cc2520100 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-loss-of-precision */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable @typescript-eslint/naming-convention */ /** * ============LICENSE_START======================================================================== * ONAP : ccsdk feature sdnr wt odlux @@ -15,14 +18,30 @@ * the License. * ============LICENSE_END========================================================================== */ -import { Token, Statement, Module, Identity, ModuleState } 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, ViewElementDate -} from "../models/uiModels"; -import { yangService } from "../services/yangService"; - + Expression, + ViewElement, + ViewElementBase, + ViewSpecification, + ViewElementNumber, + ViewElementString, + ViewElementChoice, + ViewElementUnion, + ViewElementRpc, + isViewElementObjectOrList, + isViewElementNumber, + isViewElementString, + isViewElementRpc, + ResolveFunction, + YangRange, +} from '../models/uiModels'; +import { yangService } from '../services/yangService'; + +const LOGLEVEL = +(localStorage.getItem('log.odlux.app.configuration.yang.yangParser') || 0); + +import { LogLevel } from '../../../../framework/src/utilities/logLevel'; +import { parseWhen, WhenAST, WhenTokenType } from './whenParser'; export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => { const pathParts: RegExpMatchArray[] = []; @@ -32,21 +51,22 @@ export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray if (partMatch) { pathParts.push(partMatch); } - } while (partMatch) + } while (partMatch); return pathParts; -} +}; class YangLexer { private pos: number = 0; - private buf: string = ""; + + private buf: string = ''; constructor(input: string) { this.pos = 0; this.buf = input; } - private _optable: { [key: string]: string } = { + private _opTable: { [key: string]: string } = { ';': 'SEMI', '{': 'L_BRACE', '}': 'R_BRACE', @@ -66,7 +86,7 @@ class YangLexer { private _isAlpha(char: string): boolean { return (char >= 'a' && char <= 'z') || - (char >= 'A' && char <= 'Z') + (char >= 'A' && char <= 'Z'); } private _isAlphanum(char: string): boolean { @@ -74,7 +94,7 @@ class YangLexer { char === '_' || char === '-' || char === '.'; } - private _skipNontokens() { + private _skipNonTokens() { while (this.pos < this.buf.length) { const char = this.buf.charAt(this.pos); if (this._isWhitespace(char)) { @@ -90,11 +110,11 @@ class YangLexer { let end_index = this.pos + 1; while (end_index < this.buf.length) { const char = this.buf.charAt(end_index); - if (char === "\\") { + if (char === '\\') { end_index += 2; continue; - }; - if (terminator === null && (this._isWhitespace(char) || this._optable[char] !== undefined) || char === terminator) { + } + if (terminator === null && (this._isWhitespace(char) || this._opTable[char] !== undefined) || char === terminator) { break; } end_index++; @@ -109,7 +129,7 @@ class YangLexer { name: 'STRING', value: this.buf.substring(start, end), start, - end + end, }; this.pos = terminator ? end + 1 : end; return tok; @@ -122,8 +142,8 @@ class YangLexer { ++endpos; } - let name = 'IDENTIFIER' - if (this.buf.charAt(endpos) === ":") { + let name = 'IDENTIFIER'; + if (this.buf.charAt(endpos) === ':') { name = 'IDENTIFIERREF'; ++endpos; while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) { @@ -135,7 +155,7 @@ class YangLexer { name: name, value: this.buf.substring(this.pos, endpos), start: this.pos, - end: endpos + end: endpos, }; this.pos = endpos; @@ -153,7 +173,7 @@ class YangLexer { name: 'NUMBER', value: this.buf.substring(this.pos, endpos), start: this.pos, - end: endpos + end: endpos, }; this.pos = endpos; return tok; @@ -171,7 +191,7 @@ class YangLexer { private _processBlockComment() { var endpos = this.pos + 2; // Skip until the end of the line - while (endpos < this.buf.length && !((this.buf.charAt(endpos) === "/" && this.buf.charAt(endpos - 1) === "*"))) { + while (endpos < this.buf.length && !((this.buf.charAt(endpos) === '/' && this.buf.charAt(endpos - 1) === '*'))) { endpos++; } this.pos = endpos + 1; @@ -179,87 +199,87 @@ class YangLexer { public tokenize(): Token[] { const result: Token[] = []; - this._skipNontokens(); + this._skipNonTokens(); while (this.pos < this.buf.length) { const char = this.buf.charAt(this.pos); - const op = this._optable[char]; + const op = this._opTable[char]; if (op !== undefined) { result.push({ name: op, value: char, start: this.pos, end: ++this.pos }); } else if (this._isAlpha(char)) { result.push(this._processIdentifier()); - this._skipNontokens(); + this._skipNonTokens(); const peekChar = this.buf.charAt(this.pos); - if (this._optable[peekChar] === undefined) { - result.push((peekChar !== "'" && peekChar !== '"') + if (this._opTable[peekChar] === undefined) { + result.push((peekChar !== '\'' && peekChar !== '"') ? this._processString(null) : this._processString(peekChar)); } - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') { this._processLineComment(); - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') { this._processBlockComment(); } else { - throw Error('Token error at ' + this.pos + " " + this.buf[this.pos]); + throw Error('Token error at ' + this.pos + ' ' + this.buf[this.pos]); } - this._skipNontokens(); + this._skipNonTokens(); } return result; } public tokenize2(): Statement { - let stack: Statement[] = [{ key: "ROOT", sub: [] }]; + let stack: Statement[] = [{ key: 'ROOT', sub: [] }]; let current: Statement | null = null; - this._skipNontokens(); + this._skipNonTokens(); while (this.pos < this.buf.length) { const char = this.buf.charAt(this.pos); - const op = this._optable[char]; + const op = this._opTable[char]; if (op !== undefined) { - if (op === "L_BRACE") { + if (op === 'L_BRACE') { current && stack.unshift(current); current = null; - } else if (op === "R_BRACE") { + } else if (op === 'R_BRACE') { current = stack.shift() || null; } this.pos++; - } else if (this._isAlpha(char) || char === "_") { + } else if (this._isAlpha(char) || char === '_') { const key = this._processIdentifier().value; - this._skipNontokens(); + this._skipNonTokens(); let peekChar = this.buf.charAt(this.pos); let arg = undefined; - if (this._optable[peekChar] === undefined) { - arg = (peekChar === '"' || peekChar === "'") + if (this._opTable[peekChar] === undefined) { + arg = (peekChar === '"' || peekChar === '\'') ? this._processString(peekChar).value : this._processString(null).value; } do { - this._skipNontokens(); + this._skipNonTokens(); peekChar = this.buf.charAt(this.pos); - if (peekChar !== "+") break; + if (peekChar !== '+') break; this.pos++; - this._skipNontokens(); + this._skipNonTokens(); peekChar = this.buf.charAt(this.pos); - arg += (peekChar === '"' || peekChar === "'") + arg += (peekChar === '"' || peekChar === '\'') ? this._processString(peekChar).value : this._processString(null).value; } while (true); current = { key, arg, sub: [] }; stack[0].sub!.push(current); - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') { this._processLineComment(); - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') { this._processBlockComment(); } else { - throw Error('Token error at ' + this.pos + " " + this.buf.slice(this.pos - 10, this.pos + 10)); + throw Error('Token error at ' + this.pos + ' ' + this.buf.slice(this.pos - 10, this.pos + 10)); } - this._skipNontokens(); + this._skipNonTokens(); } - if (stack[0].key !== "ROOT" || !stack[0].sub![0]) { - throw new Error("Internal Perser Error"); + if (stack[0].key !== 'ROOT' || !stack[0].sub![0]) { + throw new Error('Internal Perser Error'); } return stack[0].sub![0]; } @@ -269,25 +289,33 @@ export class YangParser { private _groupingsToResolve: ViewSpecification[] = []; private _identityToResolve: (() => void)[] = []; + private _unionsToResolve: (() => void)[] = []; + private _modulesToResolve: (() => void)[] = []; private _modules: { [name: string]: Module } = {}; + private _views: ViewSpecification[] = [{ - id: "0", - name: "root", - language: "en-US", + id: '0', + name: 'root', + language: 'en-US', canEdit: false, config: true, - parentView: "0", - title: "root", + parentView: '0', + title: 'root', elements: {}, }]; - public static ResolveStack = Symbol("ResolveStack"); + public static ResolveStack = Symbol('ResolveStack'); + + constructor( + private nodeId: string, + private _capabilityRevisionMap: { [capability: string]: string } = {}, + private _unavailableCapabilities: { failureReason: string; capability: string }[] = [], + private _importOnlyModules: { name: string; revision: string }[] = [], + ) { - constructor(private _unavailableCapabilities: { failureReason: string; capability: string; }[] = [], private _importOnlyModules: { name: string; revision: string; }[] = [], private nodeId: string) { - } public get modules() { @@ -300,8 +328,12 @@ export class YangParser { public async addCapability(capability: string, version?: string, parentImportOnlyModule?: boolean) { // do not add twice - if (this._modules[capability]) { - // console.warn(`Skipped capability: ${capability} since already contained.` ); + const existingCapability = this._modules[capability]; + const latestVersionExisting = existingCapability && Object.keys(existingCapability.revisions).sort().reverse()[0]; + if ((latestVersionExisting && version) && (version <= latestVersionExisting)) { + if (LOGLEVEL == LogLevel.Warning) { + console.warn(`Skipped capability: ${capability}:${version || ''} since already contained.`); + } return; } @@ -310,14 +342,15 @@ export class YangParser { // // console.warn(`Skipped capability: ${capability} since it is marked as unavailable.` ); // return; // } + const data = await yangService.getCapability(capability, this.nodeId, version); if (!data) { - throw new Error(`Could not load yang file for ${capability}.`); + throw new Error(`Could not load yang file for ${capability}:${version || ''}.`); } const rootStatement = new YangLexer(data).tokenize2(); - if (rootStatement.key !== "module") { + if (rootStatement.key !== 'module') { throw new Error(`Root element of ${capability} is not a module.`); } if (rootStatement.arg !== capability) { @@ -326,10 +359,32 @@ export class YangParser { const isUnavailable = this._unavailableCapabilities.some(c => c.capability === capability); const isImportOnly = parentImportOnlyModule === true || this._importOnlyModules.some(c => c.name === capability); + + // extract revisions + const revisions = this.extractNodes(rootStatement, 'revision').reduce<{ [version: string]: {} }>((acc, revision) => { + if (!revision.arg) { + throw new Error(`Module [${rootStatement.arg}] has a version w/o version number.`); + } + const description = this.extractValue(revision, 'description'); + const reference = this.extractValue(revision, 'reference'); + acc[revision.arg] = { + description, + reference, + }; + return acc; + }, {}); + + const latestVersionLoaded = Object.keys(revisions).sort().reverse()[0]; + if (existingCapability && latestVersionExisting >= latestVersionLoaded) { + if (LOGLEVEL == LogLevel.Warning) { + console.warn(`Skipped capability: ${capability}:${latestVersionLoaded} since ${capability}:${latestVersionExisting} already contained.`); + } + return; + } const module = this._modules[capability] = { name: rootStatement.arg, - revisions: {}, + revisions, imports: {}, features: {}, identities: {}, @@ -339,10 +394,10 @@ export class YangParser { views: {}, elements: {}, state: isUnavailable - ? ModuleState.unavailable - : isImportOnly - ? ModuleState.importOnly - : ModuleState.stable, + ? ModuleState.unavailable + : isImportOnly + ? ModuleState.importOnly + : ModuleState.stable, }; await this.handleModule(module, rootStatement, capability); @@ -351,84 +406,66 @@ export class YangParser { private async handleModule(module: Module, rootStatement: Statement, capability: string) { // extract namespace && prefix - module.namespace = this.extractValue(rootStatement, "namespace"); - module.prefix = this.extractValue(rootStatement, "prefix"); + module.namespace = this.extractValue(rootStatement, 'namespace'); + module.prefix = this.extractValue(rootStatement, 'prefix'); if (module.prefix) { module.imports[module.prefix] = capability; } - // extract revisions - const revisions = this.extractNodes(rootStatement, "revision"); - module.revisions = { - ...module.revisions, - ...revisions.reduce<{ [version: string]: {} }>((acc, version) => { - if (!version.arg) { - throw new Error(`Module [${module.name}] has a version w/o version number.`); - } - const description = this.extractValue(version, "description"); - const reference = this.extractValue(version, "reference"); - acc[version.arg] = { - description, - reference, - }; - return acc; - }, {}) - }; - // extract features - const features = this.extractNodes(rootStatement, "feature"); + const features = this.extractNodes(rootStatement, 'feature'); module.features = { ...module.features, ...features.reduce<{ [version: string]: {} }>((acc, feature) => { if (!feature.arg) { throw new Error(`Module [${module.name}] has a feature w/o name.`); } - const description = this.extractValue(feature, "description"); + const description = this.extractValue(feature, 'description'); acc[feature.arg] = { description, }; return acc; - }, {}) + }, {}), }; // extract imports - const imports = this.extractNodes(rootStatement, "import"); + const imports = this.extractNodes(rootStatement, 'import'); module.imports = { ...module.imports, ...imports.reduce<{ [key: string]: string }>((acc, imp) => { - const prefix = imp.sub && imp.sub.filter(s => s.key === "prefix"); + const prefix = imp.sub && imp.sub.filter(s => s.key === 'prefix'); if (!imp.arg) { throw new Error(`Module [${module.name}] has an import with neither name nor prefix.`); } acc[prefix && prefix.length === 1 && prefix[0].arg || imp.arg] = imp.arg; return acc; - }, {}) + }, {}), }; // import all required files and set module state if (imports) for (let ind = 0; ind < imports.length; ++ind) { - const moduleName = imports[ind].arg!; + const moduleName = imports[ind].arg!; - //TODO: Fix imports getting loaded without revision - await this.addCapability(moduleName, undefined, module.state === ModuleState.importOnly); + const revision = this._capabilityRevisionMap[moduleName] || undefined; + await this.addCapability(moduleName, revision, module.state === ModuleState.importOnly); const importedModule = this._modules[imports[ind].arg!]; if (importedModule && importedModule.state > ModuleState.stable) { - module.state = Math.max(module.state, ModuleState.instable); + module.state = Math.max(module.state, ModuleState.instable); } } - this.extractTypeDefinitions(rootStatement, module, ""); + this.extractTypeDefinitions(rootStatement, module, ''); - this.extractIdentities(rootStatement, 0, module, ""); + this.extractIdentities(rootStatement, 0, module, ''); - const groupings = this.extractGroupings(rootStatement, 0, module, ""); + const groupings = this.extractGroupings(rootStatement, 0, module, ''); this._views.push(...groupings); - const augments = this.extractAugments(rootStatement, 0, module, ""); + const augments = this.extractAugments(rootStatement, 0, module, ''); this._views.push(...augments); // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, ""); + const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, ''); this._views.push(currentView, ...subViews); // create the root elements for this module @@ -443,7 +480,7 @@ export class YangParser { const viewIdIndex = Number(viewElement.viewId); module.views[key] = this._views[viewIdIndex]; } - + // add only the UI View if the module is available if (module.state === ModuleState.stable || module.state === ModuleState.instable) this._views[0].elements[key] = module.elements[key]; }); @@ -462,7 +499,7 @@ export class YangParser { // process all groupings this._groupingsToResolve.filter(vs => vs.uses && vs.uses[ResolveFunction]).forEach(vs => { - try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!("|"); } catch (error) { + try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!('|'); } catch (error) { console.warn(`Error resolving: [${vs.name}] [${error.message}]`); } }); @@ -471,16 +508,16 @@ export class YangParser { * This is to fix the issue for sequential execution of modules based on their child and parent relationship * We are sorting the module object based on their augment status */ - Object.keys(this.modules) + Object.keys(this.modules) .map(elem => { - if(this.modules[elem].augments && Object.keys(this.modules[elem].augments).length > 0) { - const {augments, ...rest} = this.modules[elem]; - const partsOfKeys = Object.keys(augments).map((key) => (key.split("/").length - 1)) - this.modules[elem].executionOrder= Math.max(...partsOfKeys) - } else { - this.modules[elem].executionOrder=0; - } - }) + if (this.modules[elem].augments && Object.keys(this.modules[elem].augments).length > 0) { + const { augments, ..._rest } = this.modules[elem]; + const partsOfKeys = Object.keys(augments).map((key) => (key.split('/').length - 1)); + this.modules[elem].executionOrder = Math.max(...partsOfKeys); + } else { + this.modules[elem].executionOrder = 0; + } + }); // process all augmentations / sort by namespace changes to ensure proper order Object.keys(this.modules).sort((a, b) => this.modules[a].executionOrder! - this.modules[b].executionOrder!).forEach(modKey => { @@ -489,8 +526,8 @@ export class YangParser { 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){ + pathParts.forEach(([ns, _]) => { + if (ns === currentNS) { currentNS = ns; nameSpaceChangeCounter++; } @@ -498,11 +535,11 @@ export class YangParser { return { key, nameSpaceChangeCounter, - } + }; }); - + const augmentKeys = augmentKeysWithCounter - .sort((a,b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1 ) + .sort((a, b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1) .map((a) => a.key); augmentKeys.forEach(augKey => { @@ -512,11 +549,23 @@ export class YangParser { if (augments && viewSpec) { augments.forEach(augment => Object.keys(augment.elements).forEach(key => { const elm = augment.elements[key]; + + const when = elm.when && augment.when + ? { + type: WhenTokenType.AND, + left: elm.when, + right: augment.when, + } + : elm.when || augment.when; + + const ifFeature = elm.ifFeature + ? `(${augment.ifFeature}) and (${elm.ifFeature})` + : augment.ifFeature; + 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, + when, + ifFeature, }; })); } @@ -534,7 +583,7 @@ export class YangParser { } } return result; - } + }; const baseIdentities: Identity[] = []; Object.keys(this.modules).forEach(modKey => { @@ -565,30 +614,31 @@ export class YangParser { } }); - // 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); - } + // // resolve readOnly + // const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => { - }) - } + // // update view config + // view.config = view.config && parentConfig; - const dump = resolveReadOnly(this.views[0], true); - }; + // 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; + private get nextId() { return this._nextId++; } @@ -608,7 +658,7 @@ export class YangParser { } private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void { - const typedefs = this.extractNodes(statement, "typedef"); + const typedefs = this.extractNodes(statement, 'typedef'); typedefs && typedefs.forEach(def => { if (!def.arg) { throw new Error(`Module: [${module.name}]. Found typefed without name.`); @@ -620,7 +670,7 @@ export class YangParser { /** Handles groupings like named Container */ private extractGroupings(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] { const subViews: ViewSpecification[] = []; - const groupings = this.extractNodes(statement, "grouping"); + const groupings = this.extractNodes(statement, 'grouping'); if (groupings && groupings.length > 0) { subViews.push(...groupings.reduce((acc, cur) => { if (!cur.arg) { @@ -629,9 +679,9 @@ export class YangParser { const grouping = cur.arg; // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath); + const [currentView, currentSubViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath); grouping && (module.groupings[grouping] = currentView); - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } @@ -642,7 +692,7 @@ export class YangParser { /** Handles augments also like named container */ private extractAugments(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] { const subViews: ViewSpecification[] = []; - const augments = this.extractNodes(statement, "augment"); + const augments = this.extractNodes(statement, 'augment'); if (augments && augments.length > 0) { subViews.push(...augments.reduce((acc, cur) => { if (!cur.arg) { @@ -651,12 +701,12 @@ export class YangParser { const augment = this.resolveReferencePath(cur.arg, module); // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath); + const [currentView, currentSubViews] = this.extractSubViews(cur, parentId, module, currentPath); if (augment) { module.augments[augment] = module.augments[augment] || []; module.augments[augment].push(currentView); } - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } @@ -666,109 +716,109 @@ export class YangParser { /** Handles identities */ private extractIdentities(statement: Statement, parentId: number, module: Module, currentPath: string) { - const identities = this.extractNodes(statement, "identity"); + const identities = this.extractNodes(statement, 'identity'); module.identities = identities.reduce<{ [name: string]: Identity }>((acc, cur) => { if (!cur.arg) { - throw new Error(`Module: [${module.name}][${currentPath}]. Found identiy without name.`); + throw new Error(`Module: [${module.name}][${currentPath}]. Found identity without name.`); } acc[cur.arg] = { id: `${module.name}:${cur.arg}`, label: cur.arg, - base: this.extractValue(cur, "base"), - description: this.extractValue(cur, "description"), - reference: this.extractValue(cur, "reference"), - children: [] - } + base: this.extractValue(cur, 'base'), + description: this.extractValue(cur, 'description'), + reference: this.extractValue(cur, 'reference'), + children: [], + }; return acc; }, {}); } - // Hint: use 0 as parentId for rootElements and -1 for rootGroupings. + // 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 = { ...module, typedefs: { - ...module.typedefs - } + ...module.typedefs, + }, }; const currentId = this.nextId; const subViews: ViewSpecification[] = []; let elements: ViewElement[] = []; - const configValue = this.extractValue(statement, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const configValue = this.extractValue(statement, 'config'); + const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false'; // extract conditions - const ifFeature = this.extractValue(statement, "if-feature"); - const whenCondition = this.extractValue(statement, "when"); - if (whenCondition) console.warn("Found in [" + context.name + "]" + currentPath + " when: " + whenCondition); + const ifFeature = this.extractValue(statement, 'if-feature'); + const whenCondition = this.extractValue(statement, 'when'); + if (whenCondition) console.warn('Found in [' + context.name + ']' + currentPath + ' when: ' + whenCondition); // extract all scoped typedefs this.extractTypeDefinitions(statement, context, currentPath); // extract all scoped groupings subViews.push( - ...this.extractGroupings(statement, parentId, context, currentPath) + ...this.extractGroupings(statement, parentId, context, currentPath), ); // extract all container - const container = this.extractNodes(statement, "container"); + const container = this.extractNodes(statement, 'container'); if (container && container.length > 0) { subViews.push(...container.reduce((acc, cur) => { if (!cur.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found container without name.`); } - const [currentView, subViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); + const [currentView, currentSubViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); elements.push({ id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg, label: cur.arg, path: currentPath, module: context.name || module.name || '', - uiType: "object", + uiType: 'object', viewId: currentView.id, config: currentView.config, }); - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } // process all lists // a list is a list of containers with the leafs contained in the list - const lists = this.extractNodes(statement, "list"); + const lists = this.extractNodes(statement, 'list'); if (lists && lists.length > 0) { subViews.push(...lists.reduce((acc, cur) => { let elmConfig = config; if (!cur.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found list without name.`); } - const key = this.extractValue(cur, "key") || undefined; + const key = this.extractValue(cur, 'key') || undefined; if (elmConfig && !key) { console.warn(`Module: [${context.name}]${currentPath}. Found configurable list without key. Assume config shell be false.`); elmConfig = false; } - const [currentView, subViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); + const [currentView, currentSubViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); 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", + uiType: 'object', viewId: currentView.id, key: key, config: elmConfig && currentView.config, }); - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } // process all leaf-lists // a leaf-list is a list of some type - const leafLists = this.extractNodes(statement, "leaf-list"); + const leafLists = this.extractNodes(statement, 'leaf-list'); if (leafLists && leafLists.length > 0) { elements.push(...leafLists.reduce((acc, cur) => { const element = this.getViewElement(cur, context, parentId, currentPath, true); @@ -779,7 +829,7 @@ export class YangParser { // process all leafs // a leaf is mainly a property of an object - const leafs = this.extractNodes(statement, "leaf"); + const leafs = this.extractNodes(statement, 'leaf'); if (leafs && leafs.length > 0) { elements.push(...leafs.reduce((acc, cur) => { const element = this.getViewElement(cur, context, parentId, currentPath, false); @@ -789,92 +839,92 @@ export class YangParser { } - const choiceStms = this.extractNodes(statement, "choice"); + const choiceStms = this.extractNodes(statement, 'choice'); if (choiceStms && choiceStms.length > 0) { - elements.push(...choiceStms.reduce((accChoise, curChoise) => { - if (!curChoise.arg) { + elements.push(...choiceStms.reduce((accChoice, curChoice) => { + if (!curChoice.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found choise without name.`); } // extract all cases like containers - const cases: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[] = []; - const caseStms = this.extractNodes(curChoise, "case"); + const cases: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[] = []; + const caseStms = this.extractNodes(curChoice, 'case'); if (caseStms && caseStms.length > 0) { cases.push(...caseStms.reduce((accCase, curCase) => { if (!curCase.arg) { - throw new Error(`Module: [${context.name}]${currentPath}/${curChoise.arg}. Found case without name.`); + throw new Error(`Module: [${context.name}]${currentPath}/${curChoice.arg}. Found case without name.`); } - const description = this.extractValue(curCase, "description") || undefined; - const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, context, `${currentPath}/${context.name}:${curChoise.arg}`); + const description = this.extractValue(curCase, 'description') || undefined; + const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, context, `${currentPath}/${context.name}:${curChoice.arg}`); subViews.push(caseView, ...caseSubViews); - const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = { + const caseDef: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } = { id: parentId === 0 ? `${context.name}:${curCase.arg}` : curCase.arg, label: curCase.arg, description: description, - elements: caseView.elements + elements: caseView.elements, }; accCase.push(caseDef); return accCase; - }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[])); + }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[])); } // extract all simple cases (one case per leaf, container, etc.) - const [choiseView, choiseSubViews] = this.extractSubViews(curChoise, parentId, context, `${currentPath}/${context.name}:${curChoise.arg}`); - subViews.push(choiseView, ...choiseSubViews); - cases.push(...Object.keys(choiseView.elements).reduce((accElm, curElm) => { - const elm = choiseView.elements[curElm]; - const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = { + const [choiceView, choiceSubViews] = this.extractSubViews(curChoice, parentId, context, `${currentPath}/${context.name}:${curChoice.arg}`); + subViews.push(choiceView, ...choiceSubViews); + cases.push(...Object.keys(choiceView.elements).reduce((accElm, curElm) => { + const elm = choiceView.elements[curElm]; + const caseDef: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } = { id: elm.id, label: elm.label, description: elm.description, - elements: { [elm.id]: elm } + elements: { [elm.id]: elm }, }; accElm.push(caseDef); return accElm; - }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[])); + }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[])); - const description = this.extractValue(curChoise, "description") || undefined; - const configValue = this.extractValue(curChoise, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const choiceDescription = this.extractValue(curChoice, 'description') || undefined; + const choiceConfigValue = this.extractValue(curChoice, 'config'); + const choiceConfig = choiceConfigValue == null ? true : choiceConfigValue.toLocaleLowerCase() !== 'false'; - const mandatory = this.extractValue(curChoise, "mandatory") === "true" || false; + const mandatory = this.extractValue(curChoice, 'mandatory') === 'true' || false; - const element: ViewElementChoise = { - uiType: "choise", - id: parentId === 0 ? `${context.name}:${curChoise.arg}` : curChoise.arg, - label: curChoise.arg, + const element: ViewElementChoice = { + uiType: 'choice', + id: parentId === 0 ? `${context.name}:${curChoice.arg}` : curChoice.arg, + label: curChoice.arg, path: currentPath, module: context.name || module.name || '', - config: config, + config: choiceConfig, mandatory: mandatory, - description: description, + description: choiceDescription, cases: cases.reduce((acc, cur) => { acc[cur.id] = cur; return acc; - }, {} as { [name: string]: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } }) + }, {} as { [name: string]: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } }), }; - accChoise.push(element); - return accChoise; + accChoice.push(element); + return accChoice; }, [])); } - const rpcStms = this.extractNodes(statement, "rpc"); + const rpcStms = this.extractNodes(statement, 'rpc'); if (rpcStms && rpcStms.length > 0) { elements.push(...rpcStms.reduce((accRpc, curRpc) => { if (!curRpc.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found rpc without name.`); } - const description = this.extractValue(curRpc, "description") || undefined; - const configValue = this.extractValue(curRpc, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const rpcDescription = this.extractValue(curRpc, 'description') || undefined; + const rpcConfigValue = this.extractValue(curRpc, 'config'); + const rpcConfig = rpcConfigValue == null ? true : rpcConfigValue.toLocaleLowerCase() !== 'false'; let inputViewId: string | undefined = undefined; let outputViewId: string | undefined = undefined; - const input = this.extractNodes(curRpc, "input") || undefined; - const output = this.extractNodes(curRpc, "output") || undefined; + const input = this.extractNodes(curRpc, 'input') || undefined; + const output = this.extractNodes(curRpc, 'output') || undefined; if (input && input.length > 0) { const [inputView, inputSubViews] = this.extractSubViews(input[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`); @@ -889,13 +939,13 @@ export class YangParser { } const element: ViewElementRpc = { - uiType: "rpc", + 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, + config: rpcConfig, + description: rpcDescription, inputViewId: inputViewId, outputViewId: outputViewId, }; @@ -906,9 +956,16 @@ export class YangParser { }, [])); } - // if (!statement.arg) { - // throw new Error(`Module: [${context.name}]. Found statement without name.`); - // } + if (!statement.arg) { + console.error(new Error(`Module: [${context.name}]. Found statement without name.`)); + } + + let whenParsed: WhenAST | undefined = undefined; + try { + whenParsed = whenCondition && parseWhen(whenCondition) || undefined; + } catch (e) { + console.error(new Error(`Module: [${context.name}]. Found invalid when condition: ${whenCondition}`)); + } const viewSpec: ViewSpecification = { id: String(currentId), @@ -916,11 +973,11 @@ export class YangParser { ns: context.name, name: statement.arg != null ? statement.arg : undefined, title: statement.arg != null ? statement.arg : undefined, - language: "en-us", + language: 'en-us', canEdit: false, config: config, ifFeature: ifFeature, - when: whenCondition, + when: whenParsed, elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => { acc[cur.id] = cur; return acc; @@ -928,21 +985,21 @@ export class YangParser { }; // evaluate canEdit depending on all conditions - Object.defineProperty(viewSpec, "canEdit", { + Object.defineProperty(viewSpec, 'canEdit', { get: () => { return Object.keys(viewSpec.elements).some(key => { const elm = viewSpec.elements[key]; return (!isViewElementObjectOrList(elm) && elm.config); }); - } + }, }); // merge in all uses references and resolve groupings - const usesRefs = this.extractNodes(statement, "uses"); + const usesRefs = this.extractNodes(statement, 'uses'); if (usesRefs && usesRefs.length > 0) { viewSpec.uses = (viewSpec.uses || []); - const resolveFunctions : ((parentElementPath: string)=>void)[] = []; + const resolveFunctions: ((parentElementPath: string) => void)[] = []; for (let i = 0; i < usesRefs.length; ++i) { const groupingName = usesRefs[i].arg; @@ -951,7 +1008,7 @@ export class YangParser { } viewSpec.uses.push(this.resolveReferencePath(groupingName, context)); - + resolveFunctions.push((parentElementPath: string) => { const groupingViewSpec = this.resolveGrouping(groupingName, context); if (groupingViewSpec) { @@ -963,10 +1020,22 @@ export class YangParser { Object.keys(groupingViewSpec.elements).forEach(key => { const elm = groupingViewSpec.elements[key]; // a useRef on root level need a namespace + const resolvedWhen = elm.when && groupingViewSpec.when + ? { + type: WhenTokenType.AND, + left: elm.when, + right: groupingViewSpec.when, + } + : elm.when || groupingViewSpec.when; + + const resolvedIfFeature = elm.ifFeature + ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` + : groupingViewSpec.ifFeature; + viewSpec.elements[parentId === 0 ? `${module.name}:${key}` : key] = { ...elm, - when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when, - ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature, + when: resolvedWhen, + ifFeature: resolvedIfFeature, }; }); } @@ -974,19 +1043,19 @@ export class YangParser { } viewSpec.uses[ResolveFunction] = (parentElementPath: string) => { - const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`; + const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`; resolveFunctions.forEach(resolve => { - try { - resolve(currentElementPath); - } catch (error) { - console.error(error); - } + try { + resolve(currentElementPath); + } catch (error) { + console.error(error); + } }); // console.log("Resolved "+currentElementPath, viewSpec); if (viewSpec?.uses) { viewSpec.uses[ResolveFunction] = undefined; } - } + }; this._groupingsToResolve.push(viewSpec); } @@ -1020,28 +1089,28 @@ export class YangParser { /** Extracts the UI View from the type in the cur statement. */ private getViewElement(cur: Statement, module: Module, parentId: number, currentPath: string, isList: boolean): ViewElement { - const type = this.extractValue(cur, "type"); - const defaultVal = this.extractValue(cur, "default") || undefined; - const description = this.extractValue(cur, "description") || undefined; + const type = this.extractValue(cur, 'type'); + const defaultVal = this.extractValue(cur, 'default') || undefined; + const description = this.extractValue(cur, 'description') || undefined; - const configValue = this.extractValue(cur, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const configValue = this.extractValue(cur, 'config'); + const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false'; - 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 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 => { 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; + 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; + minValue = min, + maxValue = max; } if (minValue > min) min = minValue; @@ -1049,7 +1118,7 @@ export class YangParser { return { min: minValue, - max: maxValue + max: maxValue, }; }); return { @@ -1058,21 +1127,22 @@ export class YangParser { expression: range && range.length === 1 ? range[0] : range && range.length > 1 - ? { operation: "OR", arguments: range } - : undefined - } + ? { operation: 'OR', arguments: range } + : undefined, + }; }; const extractPattern = (): Expression | undefined => { - const pattern = this.extractNodes(this.extractNodes(cur, "type")[0]!, "pattern").map(p => p.arg!).filter(p => !!p).map(p => `^${p.replace(/(?:\\(.))/g, '$1')}$`); + // 2023.01.26 decision MF & SKO: we will no longer remove the backslashes from the pattern, seems to be a bug in the original code + const pattern = this.extractNodes(this.extractNodes(cur, 'type')[0]!, 'pattern').map(p => p.arg!).filter(p => !!p).map(p => `^${p/*.replace(/(?:\\(.))/g, '$1')*/}$`); return pattern && pattern.length == 1 ? new RegExp(pattern[0]) : pattern && pattern.length > 1 - ? { operation: "AND", arguments: pattern.map(p => new RegExp(p)) } + ? { operation: 'AND', arguments: pattern.map(p => new RegExp(p)) } : undefined; - } + }; - const mandatory = this.extractValue(cur, "mandatory") === "true" || false; + const mandatory = this.extractValue(cur, 'mandatory') === 'true' || false; if (!cur.arg) { throw new Error(`Module: [${module.name}]. Found element without name.`); @@ -1084,159 +1154,159 @@ 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 || "", + module: module.name || '', config: config, mandatory: mandatory, isList: isList, default: defaultVal, - description: description + description: description, }; - if (type === "string") { - const length = extractRange(0, +18446744073709551615, "length"); + if (type === 'string') { + const length = extractRange(0, +18446744073709551615, 'length'); return ({ ...element, - uiType: "string", + uiType: 'string', length: length.expression, pattern: extractPattern(), }); - } else if (type === "boolean") { + } else if (type === 'boolean') { return ({ ...element, - uiType: "boolean" + uiType: 'boolean', }); - } else if (type === "uint8") { + } else if (type === 'uint8') { const range = extractRange(0, +255); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "uint16") { + } else if (type === 'uint16') { const range = extractRange(0, +65535); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "uint32") { + } else if (type === 'uint32') { const range = extractRange(0, +4294967295); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "uint64") { + } else if (type === 'uint64') { const range = extractRange(0, +18446744073709551615); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int8") { + } else if (type === 'int8') { const range = extractRange(-128, +127); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int16") { + } else if (type === 'int16') { const range = extractRange(-32768, +32767); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int32") { + } else if (type === 'int32') { const range = extractRange(-2147483648, +2147483647); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int64") { + } else if (type === 'int64') { const range = extractRange(-9223372036854775808, +9223372036854775807); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "decimal64") { + } else if (type === 'decimal64') { // decimalRange - const fDigits = Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1; + const fDigits = Number(this.extractValue(this.extractNodes(cur, 'type')[0]!, 'fraction-digits')) || -1; if (fDigits === -1) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found decimal64 with invalid fraction-digits.`); } const range = extractRange(YangParser.decimalRange[fDigits].min, YangParser.decimalRange[fDigits].max); return ({ ...element, - uiType: "number", + uiType: 'number', fDigits: fDigits, range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "enumeration") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const enumNodes = this.extractNodes(typeNode, "enum"); + } else if (type === 'enumeration') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const enumNodes = this.extractNodes(typeNode, 'enum'); return ({ ...element, - uiType: "selection", + uiType: 'selection', options: enumNodes.reduce<{ key: string; value: string; description?: string }[]>((acc, enumNode) => { if (!enumNode.arg) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found option without name.`); } - const ifClause = this.extractValue(enumNode, "if-feature"); - const value = this.extractValue(enumNode, "value"); + // const ifClause = this.extractValue(enumNode, 'if-feature'); + const value = this.extractValue(enumNode, 'value'); const enumOption = { key: enumNode.arg, value: value != null ? value : enumNode.arg, - description: this.extractValue(enumNode, "description") || undefined + description: this.extractValue(enumNode, 'description') || undefined, }; // todo: ❗ handle the if clause ⚡ acc.push(enumOption); return acc; - }, []) + }, []), }); - } else if (type === "leafref") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const vPath = this.extractValue(typeNode, "path"); + } else if (type === 'leafref') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const vPath = this.extractValue(typeNode, 'path'); if (!vPath) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`); } @@ -1244,11 +1314,11 @@ export class YangParser { const resolve = this.resolveReference.bind(this); const res: ViewElement = { ...element, - uiType: "reference", + uiType: 'reference', referencePath: refPath, - ref(this: ViewElement, currentPath: string) { - const elementPath = `${currentPath}/${cur.arg}`; - + ref(this: ViewElement, basePath: string) { + const elementPath = `${basePath}/${cur.arg}`; + const result = resolve(refPath, elementPath); if (!result) return undefined; @@ -1262,20 +1332,20 @@ export class YangParser { isList: this.isList, default: this.default, description: this.description, - } as ViewElement , resolvedPath] || undefined; - } + } as ViewElement, resolvedPath] || undefined; + }, }; return res; - } else if (type === "identityref") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const base = this.extractValue(typeNode, "base"); + } else if (type === 'identityref') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const base = this.extractValue(typeNode, 'base'); if (!base) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found identityref without base.`); } const res: ViewElement = { ...element, - uiType: "selection", - options: [] + uiType: 'selection', + options: [], }; this._identityToResolve.push(() => { const identity: Identity = this.resolveIdentity(base, module); @@ -1288,29 +1358,29 @@ export class YangParser { res.options = identity.values.map(val => ({ key: val.id, value: val.id, - description: val.description + description: val.description, })); }); return res; - } else if (type === "empty") { + } else if (type === 'empty') { // todo: ❗ handle empty ⚡ /* 9.11. The empty Built-In Type The empty built-in type represents a leaf that does not have any value, it conveys information by its presence or absence. */ return { ...element, - uiType: "empty", + uiType: 'empty', }; - } else if (type === "union") { + } else if (type === 'union') { // todo: ❗ handle union ⚡ /* 9.12. The union Built-In Type */ - const typeNode = this.extractNodes(cur, "type")[0]!; - const typeNodes = this.extractNodes(typeNode, "type"); + const typeNode = this.extractNodes(cur, 'type')[0]!; + const typeNodes = this.extractNodes(typeNode, 'type'); const resultingElement = { ...element, - uiType: "union", - elements: [] + uiType: 'union', + elements: [], } as ViewElementUnion; const resolveUnion = () => { @@ -1318,13 +1388,13 @@ export class YangParser { const stm: Statement = { ...cur, sub: [ - ...(cur.sub ?.filter(s => s.key !== "type") || []), - node - ] + ...(cur.sub?.filter(s => s.key !== 'type') || []), + node, + ], }; return { ...this.getViewElement(stm, module, parentId, currentPath, isList), - id: node.arg! + id: node.arg!, }; })); }; @@ -1332,34 +1402,34 @@ export class YangParser { this._unionsToResolve.push(resolveUnion); return resultingElement; - } else if (type === "bits") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const bitNodes = this.extractNodes(typeNode, "bit"); + } else if (type === 'bits') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const bitNodes = this.extractNodes(typeNode, 'bit'); return { ...element, - uiType: "bits", - flags: bitNodes.reduce<{ [name: string]: number | undefined; }>((acc, bitNode) => { + uiType: 'bits', + flags: bitNodes.reduce<{ [name: string]: number | undefined }>((acc, bitNode) => { if (!bitNode.arg) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found bit without name.`); } - const ifClause = this.extractValue(bitNode, "if-feature"); - const pos = Number(this.extractValue(bitNode, "position")); + // const ifClause = this.extractValue(bitNode, 'if-feature'); + const pos = Number(this.extractValue(bitNode, 'position')); acc[bitNode.arg] = pos === pos ? pos : undefined; return acc; - }, {}) + }, {}), }; - } else if (type === "binary") { + } else if (type === 'binary') { return { ...element, - uiType: "binary", - length: extractRange(0, +18446744073709551615, "length"), + uiType: 'binary', + length: extractRange(0, +18446744073709551615, 'length'), }; - } else if (type === "instance-identifier") { + } else if (type === 'instance-identifier') { // https://tools.ietf.org/html/rfc7950#page-168 return { ...element, - uiType: "string", - length: extractRange(0, +18446744073709551615, "length"), + uiType: 'string', + length: extractRange(0, +18446744073709551615, 'length'), }; } else { // not a build in type, need to resolve type @@ -1374,13 +1444,13 @@ export class YangParser { } // 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", - }; + if ((type === 'date-and-time' || type.endsWith(':date-and-time')) && typeRef.module === 'ietf-yang-types') { + return { + ...typeRef, + ...element, + description: description, + uiType: 'date', + }; } return ({ @@ -1391,27 +1461,27 @@ export class YangParser { } } - private resolveStringType(parentElement: ViewElementString, pattern: Expression | undefined, length: { expression: Expression | undefined, min: number, max: number }) { + private resolveStringType(parentElement: ViewElementString, pattern: Expression | undefined, length: { expression: Expression | undefined; min: number; max: number }) { return { ...parentElement, pattern: pattern != null && parentElement.pattern - ? { operation: "AND", arguments: [pattern, parentElement.pattern] } + ? { operation: 'AND', arguments: [pattern, parentElement.pattern] } : parentElement.pattern ? parentElement.pattern : pattern, length: length.expression != null && parentElement.length - ? { operation: "AND", arguments: [length.expression, parentElement.length] } + ? { operation: 'AND', arguments: [length.expression, parentElement.length] } : parentElement.length ? parentElement.length - : length ?.expression, + : length?.expression, } as ViewElementString; } - private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression | undefined, min: number, max: number }) { + private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression | undefined; min: number; max: number }) { return { ...parentElement, range: range.expression != null && parentElement.range - ? { operation: "AND", arguments: [range.expression, parentElement.range] } + ? { operation: 'AND', arguments: [range.expression, parentElement.range] } : parentElement.range ? parentElement.range : range, @@ -1421,7 +1491,7 @@ export class YangParser { } private resolveReferencePath(vPath: string, module: Module) { - const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g // 1 = opt: namespace / 2 = property + const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g; // 1 = opt: namespace / 2 = property return vPath.replace(vPathParser, (_, ns, property) => { const nameSpace = ns && module.imports[ns] || module.name; return `${nameSpace}:${property}`; @@ -1429,20 +1499,20 @@ export class YangParser { } private resolveReference(vPath: string, currentPath: string) { - const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath + const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath let element: ViewElement | null = null; - let moduleName = ""; + let moduleName = ''; const vPathParts = splitVPath(vPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] })); - const resultPathParts = !vPath.startsWith("/") - ? splitVPath(currentPath, vPathParser).map(p => { moduleName = p[1] || moduleName ; return { ns: moduleName, property: p[2], ind: p[3] } }) + const resultPathParts = !vPath.startsWith('/') + ? splitVPath(currentPath, vPathParser).map(p => { moduleName = p[1] || moduleName; return { ns: moduleName, property: p[2], ind: p[3] }; }) : []; for (let i = 0; i < vPathParts.length; ++i) { const vPathPart = vPathParts[i]; - if (vPathPart.property === "..") { + if (vPathPart.property === '..') { resultPathParts.pop(); - } else if (vPathPart.property !== ".") { + } else if (vPathPart.property !== '.') { resultPathParts.push(vPathPart); } } @@ -1453,30 +1523,30 @@ export class YangParser { if (j === 0) { moduleName = pathPart.ns; const rootModule = this._modules[moduleName]; - if (!rootModule) throw new Error("Could not resolve module [" + moduleName + "].\r\n" + vPath); + if (!rootModule) throw new Error('Could not resolve module [' + moduleName + '].\r\n' + vPath); element = rootModule.elements[`${pathPart.ns}:${pathPart.property}`]; } else if (element && isViewElementObjectOrList(element)) { const view: ViewSpecification = this._views[+element.viewId]; if (moduleName !== pathPart.ns) { moduleName = pathPart.ns; - } + } element = view.elements[pathPart.property] || view.elements[`${moduleName}:${pathPart.property}`]; } else { - throw new Error("Could not resolve reference.\r\n" + vPath); + throw new Error('Could not resolve reference.\r\n' + vPath); } - if (!element) throw new Error("Could not resolve path [" + pathPart.property + "] in [" + currentPath + "] \r\n" + vPath); + if (!element) throw new Error('Could not resolve path [' + pathPart.property + '] in [' + currentPath + '] \r\n' + vPath); } - moduleName = ""; // create the vPath for the resolved element, do not add the element itself this will be done later in the res(...) function - return [element, resultPathParts.slice(0,-1).map(p => `${moduleName !== p.ns ? `${moduleName=p.ns}:` : ""}${p.property}${p.ind || ''}`).join("/")]; + moduleName = ''; // create the vPath for the resolved element, do not add the element itself this will be done later in the res(...) function + return [element, resultPathParts.slice(0, -1).map(p => `${moduleName !== p.ns ? `${moduleName = p.ns}:` : ''}${p.property}${p.ind || ''}`).join('/')]; } private resolveView(vPath: string) { - const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath + const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath let element: ViewElement | null = null; let partMatch: RegExpExecArray | null; let view: ViewSpecification | null = null; - let moduleName = ""; + let moduleName = ''; if (vPath) do { partMatch = vPathParser.exec(vPath); if (partMatch) { @@ -1498,13 +1568,13 @@ export class YangParser { } if (!element) return null; } - } while (partMatch) + } while (partMatch); return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null; } private resolveType(type: string, module: Module) { - const collonInd = type.indexOf(":"); - const preFix = collonInd > -1 ? type.slice(0, collonInd) : ""; + const collonInd = type.indexOf(':'); + const preFix = collonInd > -1 ? type.slice(0, collonInd) : ''; const typeName = collonInd > -1 ? type.slice(collonInd + 1) : type; const res = preFix @@ -1514,8 +1584,8 @@ export class YangParser { } private resolveGrouping(grouping: string, module: Module) { - const collonInd = grouping.indexOf(":"); - const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : ""; + const collonInd = grouping.indexOf(':'); + const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : ''; const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping; return preFix @@ -1525,8 +1595,8 @@ export class YangParser { } private resolveIdentity(identity: string, module: Module) { - const collonInd = identity.indexOf(":"); - const preFix = collonInd > -1 ? identity.slice(0, collonInd) : ""; + const collonInd = identity.indexOf(':'); + const preFix = collonInd > -1 ? identity.slice(0, collonInd) : ''; const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity; return preFix diff --git a/sdnr/wt/odlux/apps/configurationApp/webpack.config.js b/sdnr/wt/odlux/apps/configurationApp/webpack.config.js index 57caf079f..0d37c7d87 100644 --- a/sdnr/wt/odlux/apps/configurationApp/webpack.config.js +++ b/sdnr/wt/odlux/apps/configurationApp/webpack.config.js @@ -10,6 +10,7 @@ const path = require("path"); const webpack = require("webpack"); const CopyWebpackPlugin = require("copy-webpack-plugin"); const TerserPlugin = require('terser-webpack-plugin'); +const proxyConf = require('../../proxy.conf'); const policies = require('./policies.json'); @@ -59,6 +60,16 @@ module.exports = (env) => { use: [{ loader: "babel-loader" }] + },{ + //don't minify images + test: /\.(png|gif|jpg|svg)$/, + use: [{ + loader: 'url-loader', + options: { + limit: 10, + name: './images/[name].[ext]' + } + }] }] }, @@ -133,56 +144,7 @@ module.exports = (env) => { before: function(app, server, compiler) { app.get('/oauth/policies',(_, res) => res.json(policies)); }, - proxy: { - "/about": { - target: "http://sdnr:8181", - secure: false - }, - "/yang-schema/": { - target: "http://sdnr:8181", - secure: false - }, - "/oauth/": { - target: "http://sdnr:8181", - secure: false - }, - "/database/": { - target: "http://sdnr:8181", - secure: false - }, - "/restconf/": { - target: "http://sdnr:8181", - secure: false - }, - "/rests/": { - target: "http://sdnr:8181", - secure: false - }, - "/help/": { - target: "http://sdnr:8181", - secure: false - }, - "/about/": { - target: "http://sdnr:8181", - secure: false - }, - "/tree/": { - target: "http://sdnr:8181", - secure: false - }, - "/websocket": { - target: "http://sdnr:8181", - ws: true, - changeOrigin: true, - secure: false - }, - "/apidoc": { - target: "http://sdnr:8181", - ws: true, - changeOrigin: true, - secure: false - } - } + proxy: proxyConf, } }]; } -- cgit 1.2.3-korg