diff options
Diffstat (limited to 'sdnr/wt-odlux/odlux/apps/configurationApp')
35 files changed, 6274 insertions, 0 deletions
diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/.babelrc b/sdnr/wt-odlux/odlux/apps/configurationApp/.babelrc new file mode 100644 index 000000000..3d8cd1260 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/.babelrc @@ -0,0 +1,17 @@ +{ + "presets": [ + ["@babel/preset-react"], + ["@babel/preset-env", { + "targets": { + "chrome": "66" + }, + "spec": true, + "loose": false, + "modules": false, + "debug": false, + "useBuiltIns": "usage", + "forceAllTransforms": true + }] + ], + "plugins": [] +} diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/package.json b/sdnr/wt-odlux/odlux/apps/configurationApp/package.json new file mode 100644 index 000000000..b1d7d95d5 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/package.json @@ -0,0 +1,47 @@ +{ + "name": "@odlux/configuration-app", + "version": "0.1.0", + "description": "A react based modular UI for the configuration app.", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --env debug", + "build": "webpack --env release --config webpack.config.js", + "build:dev": "webpack --env debug --config webpack.config.js" + }, + "repository": { + "type": "git", + "url": "https://git.mfico.de/highstreet-technologies/odlux.git" + }, + "keywords": [ + "reactjs", + "redux", + "ui", + "framework" + ], + "author": "Matthias Fischer", + "license": "Apache-2.0", + "dependencies": { + "@emotion/react": "^11.7.0", + "@emotion/styled": "^11.6.0", + "@mui/icons-material": "^5.2.0", + "@mui/material": "^5.2.2", + "@mui/styles": "^5.2.2", + "@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", + "@types/flux": "3.1.8", + "@types/jquery": "3.3.10", + "@types/react": "17.0.37", + "@types/react-dom": "17.0.11", + "@types/react-router-dom": "5.1.7", + "jquery": "3.3.1", + "react": "17.0.2", + "react-dom": "17.0.2", + "react-router-dom": "5.2.0" + } +} diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/policies.json b/sdnr/wt-odlux/odlux/apps/configurationApp/policies.json new file mode 100644 index 000000000..91a0abf96 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/policies.json @@ -0,0 +1,12 @@ +[ + { + "path": "/rests/**/node=Sim2230**", + "methods": { + "get": false, + "post": false, + "put": false, + "delete": false, + "patch": false + } + } +]
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/pom.xml b/sdnr/wt-odlux/odlux/apps/configurationApp/pom.xml new file mode 100644 index 000000000..ae60d2811 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/pom.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ ============LICENSE_START======================================================= + ~ ONAP : SDNR ODLUX + ~ ================================================================================ + ~ Copyright (C) 2020 AT&T 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======================================================= + ~ + --> + +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>org.onap.ccsdk.features.sdnr.odlux</groupId> + <artifactId>sdnr-odlux-app-configurationApp</artifactId> + <version>1.7.0-SNAPSHOT</version> + <packaging>jar</packaging> + + <name>SDNR ODLUX :: ${project.artifactId}</name> + <licenses> + <license> + <name>Apache License, Version 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0</url> + </license> + </licenses> + + <properties> + <maven.javadoc.skip>true</maven.javadoc.skip> + </properties> + + <build> + <resources> + <resource> + <directory>dist</directory> + <targetPath>odlux</targetPath> + </resource> + </resources> + <plugins> + <plugin> + <artifactId>maven-clean-plugin</artifactId> + <configuration> + <filesets> + <fileset> + <directory>dist</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <fileset> + <directory>node</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <fileset> + <directory>node_modules</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <fileset> + <directory>../node_modules</directory> + <followSymlinks>false</followSymlinks> + </fileset> + <!-- eclipse bug build bin folder in basedir --> + <fileset> + <directory>bin</directory> + <followSymlinks>false</followSymlinks> + </fileset> + </filesets> + </configuration> + </plugin> + <plugin> + <groupId>de.jacks-it-lab</groupId> + <artifactId>frontend-maven-plugin</artifactId> + <version>1.7.2</version> + <executions> + <execution> + <id>install node and yarn</id> + <goals> + <goal>install-node-and-yarn</goal> + </goals> + <!-- optional: default phase is "generate-resources" --> + <phase>initialize</phase> + <configuration> + <nodeVersion>v16.17.0</nodeVersion> + <yarnVersion>v1.22.19</yarnVersion> + </configuration> + </execution> + <execution> + <id>yarn build</id> + <goals> + <goal>yarn</goal> + </goals> + <configuration> + <arguments>run build</arguments> + </configuration> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/actions/deviceActions.ts new file mode 100644 index 000000000..d8ec4bfd9 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/actions/deviceActions.ts @@ -0,0 +1,626 @@ +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 { 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, + 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 ) { + super(); + } +} + +export class SetCollectingSelectionData extends Action { + constructor(public busy: boolean) { + super(); + } +} + +export class SetSelectedValue extends Action { + constructor(public value: any) { + super(); + } +} + +export class UpdateDeviceDescription extends Action { + constructor( public nodeId: string, public modules: { [name:string]: Module }, public views: ViewSpecification[]) { + super(); + } +} + +export class UpdateViewDescription extends Action { + constructor(public vPath: string, public viewData: any, public displaySpecification: DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay }) { + super(); + } +} + +export class UpdateOutputData extends Action { + constructor(public outputData: any) { + super(); + } +} + +export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatch: Dispatch, _getState: () => IApplicationStoreState ) => { + + dispatch(new UpdateDeviceDescription('', {}, [])); + dispatch(new SetCollectingSelectionData(true)); + + const { availableCapabilities, unavailableCapabilities, importOnlyModules } = await restService.getCapabilitiesByMountId(nodeId); + + if (!availableCapabilities || availableCapabilities.length <= 0) { + dispatch(new SetCollectingSelectionData(false)); + dispatch(new UpdateDeviceDescription(nodeId, {}, [])); + dispatch(new UpdateViewDescription('', [], { + displayMode: DisplayModeType.displayAsMessage, + renderMessage: `NetworkElement : "${nodeId}" has no capabilities.`, + })); + throw new Error(`NetworkElement : [${nodeId}] has no capabilities.`); + } + + 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) { + const capRaw = availableCapabilities[i]; + try { + await parser.addCapability(capRaw.capability, capRaw.version); + } catch (err) { + console.error(`Error in ${capRaw.capability} ${capRaw.version}`, err); + } + } + + parser.postProcess(); + + dispatch(new SetCollectingSelectionData(false)); + + if (process.env.NODE_ENV === 'development' ) { + console.log(parser, parser.modules, parser.views); + } + + return dispatch(new UpdateDeviceDescription(nodeId, parser.modules, parser.views)); +}; + +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), + }; + } + + 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 } } } = getState(); + let dataPath = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; + + let inputViewSpecification: ViewSpecification | undefined = undefined; + let outputViewSpecification: ViewSpecification | undefined = undefined; + + let viewSpecification: ViewSpecification = views[0]; + let viewElement: ViewElement; + + let dataMember: string; + let extractList: boolean = false; + + let currentNS: string | null = null; + let defaultNS: string | null = null; + + dispatch(new SetCollectingSelectionData(true)); + try { + for (let ind = 0; ind < pathParts.length; ++ind) { + const [property, key] = pathParts[ind]; + 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.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) { + + // empty key is used for new element + 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 || ''; + } + return acc; + }, {}); + + // create display specification + const ds: DisplaySpecification = { + displayMode: DisplayModeType.displayAsObject, + viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), + keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, + }; + + // update display specification + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, data, ds)); + } + 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]; + if (keyElement && isViewElementReference(keyElement)) { + const refList = await getReferencedDataList(keyElement.referencePath, dataPath, modules, views); + if (!refList) { + throw new Error(`Could not find refList for [${keyElement.referencePath}].`); + } + if (!refList.key) { + 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')}]`))); + })); + } else { + // Found a list at root level of a module w/o a reference key. + dataPath += `?&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]; + if (!Array.isArray(refData) || !refData.length) { + throw new Error('Found a list at root level of a module containing no keys.'); + } + if (refData.length > 1) { + const refView : ViewSpecification = { + id: '-1', + canEdit: false, + 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')}]`))); + })); + } else { + window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refData[0]?.id.replace(/\//ig, '%2F')}]`))); + } + } else { + throw new Error('Found a list at root level of a module and could not determine the keys.'); + } + dispatch(new SetCollectingSelectionData(false)); + } + return; + } + extractList = true; + } else { + // 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 + dataMember = namespace === defaultNS + ? viewElement.label + : `${namespace}:${viewElement.label}`; + extractList = false; + } + + if (viewElement && 'viewId' in viewElement) { + viewSpecification = views[+viewElement.viewId]; + } else if (viewElement.uiType === 'rpc') { + viewSpecification = views[+(viewElement.inputViewId || 0)]; + + // create new instance & flaten + inputViewSpecification = viewElement.inputViewId != null && { + ...views[+(viewElement.inputViewId || 0)], + elements: flattenViewElements(defaultNS, '', views[+(viewElement.inputViewId || 0)].elements, views, viewElement.label), + } || undefined; + outputViewSpecification = viewElement.outputViewId != null && { + ...views[+(viewElement.outputViewId || 0)], + elements: flattenViewElements(defaultNS, '', views[+(viewElement.outputViewId || 0)].elements, views, viewElement.label), + } || undefined; + + } + } + + 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')) { + const restResult = (await restService.getConfigData(dataPath)); + if (!restResult.data) { + // special case: if this is a list without any response + if (extractList && restResult.status === 404) { + if (!isViewElementList(viewElement!)) { + throw new Error(`vPath: [${vPath}]. ViewElement has no key.`); + } + // create display specification + const ds: DisplaySpecification = { + displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, + viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), + keyProperty: viewElement.key, + }; + + // update display specification + 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'] || ''; + throw new Error(`Server Error. Status: [${restResult.status}]\n${message}`); + } else { + // 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 + } + } + + // extract the first element list[key] + data = data instanceof Array + ? data[0] + : data; + + // extract the list -> key: list + data = extractList + ? data[viewElement!.id] || data[viewElement!.label] || [] // if the list is empty, it does not exist + : data; + + } else if (viewElement! && viewElement!.uiType === 'rpc') { + // set data to defaults + data = {}; + 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' + ? { + dataPath, + displayMode: DisplayModeType.displayAsRPC, + inputViewSpecification: inputViewSpecification && resolveViewDescription(defaultNS, vPath, inputViewSpecification), + outputViewSpecification: outputViewSpecification && resolveViewDescription(defaultNS, vPath, outputViewSpecification), + } + : { + dataPath, + displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, + viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), + keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, + + // 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(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 SetCollectingSelectionData(false)); + } finally { + return; + } +}; + +export const updateDataActionAsyncCreator = (vPath: string, data: any) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { + const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key + const { configuration: { deviceDescription: { nodeId, views } } } = getState(); + let dataPath = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; + let viewSpecification: ViewSpecification = views[0]; + let viewElement: ViewElement; + let dataMember: string; + let embedList: boolean = false; + let isNew: string | false = false; + + 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 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 (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 (pathParts.length - 1 > ind) { + dispatch(new SetCollectingSelectionData(false)); + 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) || ''; + isNew = key; + if (!key) { + dispatch(new SetCollectingSelectionData(false)); + throw new Error('No value for key [' + viewElement.key + '] in list [' + property + ']'); + } + } + } + + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; + dataMember = viewElement.label; + embedList = false; + + if (viewElement && 'viewId' in viewElement) { + viewSpecification = views[+viewElement.viewId]; + } + } + + // remove read-only elements + const removeReadOnlyElements = (pViewSpecification: ViewSpecification, isList: boolean, pData: any) => { + if (isList) { + return pData.map((elm : any) => removeReadOnlyElements(pViewSpecification, false, elm)); + } else { + 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') { + const view = views[+element.viewId]; + if (!view) { + throw new Error('removeReadOnlyElements: Internal Error could not determine viewId: ' + element.viewId); + } + acc[cur] = removeReadOnlyElements(view, element.isList != null && element.isList, pData[cur]); + } else { + acc[cur] = pData[cur]; + } + } + return acc; + }, {}); + } + }; + data = removeReadOnlyElements(viewSpecification, embedList, data); + + + // embed the list -> key: list + data = embedList + ? { [viewElement!.label]: data } + : data; + + // embed the first element list[key] + data = isNew + ? [data] + : data; + + // do not extract root member (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'] || ''; + throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); + } + } + + if (isNew) { + return dispatch(new ReplaceAction(`/configuration/${nodeId}/${vPath.replace(/\[\]$/i, `[${isNew}]`)}`)); // navigate to new element + } + + // create display specification + const ds: DisplaySpecification = { + displayMode: embedList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, + viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), + keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, + }; + + // update display specification + return dispatch(new UpdateViewDescription(vPath, data, ds)); + } catch (error) { + history.back(); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not change ${dataPath}` })); + + } finally { + dispatch(new SetCollectingSelectionData(false)); + return; + } +}; + +export const removeElementActionAsyncCreator = (vPath: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { + const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key + const { configuration: { deviceDescription: { nodeId, views } } } = getState(); + let dataPath = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; + let viewSpecification: ViewSpecification = views[0]; + let viewElement: ViewElement; + + let currentNS: 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 namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; + + viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; + 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 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) { + // remove the whole table + } + } + + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; + + if (viewElement && 'viewId' in viewElement) { + viewSpecification = views[+viewElement.viewId]; + } 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'] || ''; + 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}` })); + } 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 } } } = 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; + let dataMember: string; + let embedList: boolean = false; + let isNew: string | false = false; + + 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 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 (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 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) { + // // handle new element + // key = viewElement.key && String(data[viewElement.key]) || ""; + // isNew = key; + // if (!key) { + // dispatch(new SetCollectingSelectionData(false)); + // throw new Error("No value for key [" + viewElement.key + "] in list [" + property + "]"); + // } + // } + } + + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; + dataMember = viewElement.label; + embedList = false; + + if (viewElement && 'viewId' in viewElement) { + viewSpecification = views[+viewElement.viewId]; + } 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 innerPathParts = cur.split('.'); + let pos = 0; + const updatePath = (obj: any, key: string) => { + obj[key] = (pos >= innerPathParts.length) + ? data[cur] + : updatePath(obj[key] || {}, innerPathParts[pos++]); + return obj; + }; + updatePath(acc, innerPathParts[pos++]); + return acc; + }, {}) || null; + + // embed the list -> key: list + data = embedList + ? { [viewElement!.label]: data } + : data; + + // embed the first element list[key] + data = isNew + ? [data] + : data; + + // do not post root member (0) + 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'] || ''; + throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); + } + dispatch(new UpdateOutputData(updateResult.data)); + } else { + 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}` })); + + } finally { + dispatch(new SetCollectingSelectionData(false)); + } +}; diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg b/sdnr/wt-odlux/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg new file mode 100644 index 000000000..1b74cc479 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg @@ -0,0 +1,20 @@ +<!-- highstreet technologies GmbH colour scheme
+ Grey #565656
+ LBlue #36A9E1
+ DBlue #246DA2
+ Green #003F2C / #006C4B
+ Yellw #C8D400
+ Red #D81036
+-->
+
+
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 142 140" >
+<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="scale(10,10)">
+
+
+ <path fill="#565656" d="M7.887,9.025 C7.799,8.449 7.569,7.92 7.229,7.475 L7.995,6.71 L7.307,6.023 L6.536,6.794 C6.093,6.467 5.566,6.245 4.994,6.161 L4.994,5.066 L4.021,5.066 L4.021,6.155 C3.444,6.232 2.913,6.452 2.461,6.777 L1.709,6.024 L1.021,6.712 L1.761,7.452 C1.411,7.901 1.175,8.437 1.087,9.024 L0.062,9.024 L0.062,9.025 L0.062,9.998 L1.08,9.998 C1.162,10.589 1.396,11.132 1.744,11.587 L1.02,12.31 L1.708,12.997 L2.437,12.268 C2.892,12.604 3.432,12.83 4.02,12.91 L4.02,13.958 L4.993,13.958 L4.993,12.904 C5.576,12.818 6.11,12.589 6.56,12.252 L7.306,12.999 L7.994,12.311 L7.248,11.564 C7.586,11.115 7.812,10.581 7.893,10 L8.952,10 L8.952,9.998 L8.952,9.026 L7.887,9.026 L7.887,9.025 Z M4.496,11.295 C3.512,11.295 2.715,10.497 2.715,9.512 C2.715,8.528 3.512,7.73 4.496,7.73 C5.481,7.73 6.28,8.528 6.28,9.512 C6.28,10.497 5.481,11.295 4.496,11.295 L4.496,11.295 Z" ></path>
+
+ <path fill="#C8D400" d="m 12.231 4.17 l 1.09 -0.281 l -0.252 -0.979 l -1.091 0.282 c -0.118 -0.24 -0.265 -0.47 -0.461 -0.672 c -0.192 -0.196 -0.415 -0.344 -0.647 -0.464 l 0.301 -1.079 l -0.973 -0.271 l -0.299 1.072 c -0.541 -0.043 -1.091 0.078 -1.566 0.382 l -0.76 -0.776 l -0.721 0.707 l 0.756 0.77 c -0.326 0.47 -0.469 1.024 -0.441 1.575 l -1.04 0.268 l 0.252 0.977 l 1.038 -0.268 c 0.117 0.243 0.266 0.475 0.465 0.678 c 0.203 0.208 0.439 0.362 0.686 0.485 l -0.289 1.039 l 0.971 0.271 l 0.293 -1.048 c 0.542 0.033 1.092 -0.1 1.563 -0.415 l 0.771 0.786 l 0.72 -0.707 l -0.776 -0.791 c 0.307 -0.465 0.439 -1.006 0.41 -1.541 l 0 0 z m -2.517 1.617 c -0.823 0 -1.491 -0.669 -1.491 -1.493 c 0 -0.822 0.668 -1.489 1.491 -1.489 c 0.822 0 1.49 0.667 1.49 1.489 c 0 0.824 -0.668 1.493 -1.49 1.493 l 0 0 z" ></path>
+
+</g>
+</svg>
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/baseProps.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/baseProps.ts new file mode 100644 index 000000000..7187c0a4e --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/baseProps.ts @@ -0,0 +1,28 @@ +/** + * ============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 { ViewElement } from '../models/uiModels'; + +export type BaseProps<TValue = string> = { + 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/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx new file mode 100644 index 000000000..b176e5db5 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx @@ -0,0 +1,101 @@ +/** + * ============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 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 { 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', + }, + iconLight: { + color: 'orange', + }, + padding: { + paddingLeft: 10, + paddingRight: 10, + }, + }), +); + +type IfWhenProps = InputProps & { + label: string; + element: ViewElementBase; + helperText: string; + error: boolean; + onChangeTooltipVisibility(value: boolean): void; +}; + +export const IfWhenTextInput = (props: IfWhenProps) => { + + const { element, id, label, helperText: errorText, error, style, ...otherProps } = props; + const classes = useStyles(); + + const ifFeature = element.ifFeature + ? ( + <Tooltip + title={element.ifFeature} + disableInteractive + onMouseMove={() => props.onChangeTooltipVisibility(false)} + onMouseOut={() => props.onChangeTooltipVisibility(true)} + > + <InputAdornment position="start"> + <FontAwesomeIcon icon={faAdjust} className={classes.iconDark} /> + </InputAdornment> + </Tooltip> + ) + : null; + + const whenFeature = element.when + ? ( + <Tooltip + title={element.when} + disableInteractive + className={classes.padding} + onMouseMove={() => props.onChangeTooltipVisibility(false)} + onMouseOut={() => props.onChangeTooltipVisibility(true)} + > + <InputAdornment className={classes.padding} position="end"> + <FontAwesomeIcon icon={faAdjust} className={classes.iconLight}/> + </InputAdornment> + </Tooltip> + ) + : null; + + return ( + <FormControl variant="standard" error={error} style={style}> + <InputLabel htmlFor={id} >{label}</InputLabel> + <Input id={id} inputProps={{ 'aria-label': label + '-input' }} endAdornment={<div>{ifFeature}{whenFeature}</div>} {...otherProps} /> + <FormHelperText>{errorText}</FormHelperText> + </FormControl> + ); +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx new file mode 100644 index 000000000..56fb93cea --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx @@ -0,0 +1,63 @@ +/** + * ============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 React from 'react'; + +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<boolean>; + +export const UiElementBoolean = (props: BooleanInputProps) => { + + const element = props.value as ViewElementBoolean; + + const value = String(props.inputValue).toLowerCase(); + const mandatoryError = element.mandatory && value !== 'true' && value !== 'false'; + + return (!props.readOnly || element.id != null + ? (<FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`select-${element.id}`} >{element.label}</InputLabel> + <Select variant="standard" + aria-label={element.label + '-selection'} + required={!!element.mandatory} + error={mandatoryError} + onChange={(e) => { props.onChange(e.target.value === 'true'); }} + readOnly={props.readOnly} + disabled={props.disabled} + value={value} + inputProps={{ + name: element.id, + id: `select-${element.id}`, + }} + > + <MenuItem value={'true'} aria-label="true">{element.trueValue || 'True'}</MenuItem> + <MenuItem value={'false'} aria-label="false">{element.falseValue || 'False'}</MenuItem> + + </Select> + <FormHelperText>{mandatoryError ? 'Value is mandatory' : ''}</FormHelperText> + </FormControl>) + : null + ); +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx new file mode 100644 index 000000000..669ddff63 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx @@ -0,0 +1,209 @@ +/** + * ============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 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'; + +const useStyles = makeStyles((theme: Theme) => { + const light = theme.palette.mode === 'light'; + const bottomLineColor = light ? 'rgba(0, 0, 0, 0.42)' : 'rgba(255, 255, 255, 0.7)'; + + return ({ + root: { + display: 'flex', + justifyContent: 'left', + verticalAlign: 'bottom', + flexWrap: 'wrap', + listStyle: 'none', + margin: 0, + padding: 0, + paddingTop: theme.spacing(0.5), + marginTop: theme.spacing(1), + }, + chip: { + 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': { + 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}`, + }, + }, + '&.Mui-disabled:before': { + borderBottomStyle: 'dotted', + }, + }, + }); +}); + +type LeafListProps = BaseProps<any []> & { + getEditorForViewElement: (uiElement: ViewElement) => (null | React.ComponentType<BaseProps<any>>); +}; + +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 [editorValueIndex, setEditorValueIndex] = React.useState(-1); + + 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); + }; + + const onDelete = (index : number) => { + const newValue : any[] = [ + ...inputValue.slice(0, index), + ...inputValue.slice(index + 1), + ]; + onChange(newValue); + }; + + const ValueEditor = props.getEditorForViewElement(props.value); + + return ( + <> + <FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`list-${element.id}`} shrink={!props.readOnly || !!(inputValue && inputValue.length)} >{element.label}</InputLabel> + <ul className={`${classes.root} ${classes.underline}`} id={`list-${element.id}`}> + { !props.readOnly ? <li> + <Chip + icon={<AddIcon />} + label={'Add'} + className={classes.chip} + size="small" + color="secondary" + onClick={ () => { + setOpen(true); + setEditorValue(''); + setEditorValueIndex(-1); + } + } + /> + </li> : null } + { inputValue.map((val, ind) => ( + <li key={ind}> + <Chip + className={classes.chip} + size="small" + variant="outlined" + label={String(val)} + onDelete={ !props.readOnly ? () => { onDelete(ind); } : undefined } + onClick={ !props.readOnly ? () => { + setOpen(true); + setEditorValue(val); + setEditorValueIndex(ind); + } : undefined + } + /> + </li> + )) + } + </ul> + {/* <FormHelperText>{ "Value is mandetory"}</FormHelperText> */} + </FormControl> + <Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title"> + <DialogTitle id="form-dialog-title">{editorValueIndex < 0 ? 'Add new value' : 'Edit value' } </DialogTitle> + <DialogContent> + { ValueEditor && <ValueEditor + inputValue={ editorValue } + value={{ ...element, isList: false }} + disabled={false} + readOnly={props.readOnly} + onChange={ setEditorValue } + /> || null } + </DialogContent> + <DialogActions> + <Button color="inherit" onClick={ handleClose }> Cancel </Button> + <Button disabled={editorValue == null || editorValue === '' } onClick={ onApplyButton } color="secondary"> {editorValueIndex < 0 ? 'Add' : 'Apply'} </Button> + </DialogActions> + </Dialog> + </> + ); +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementNumber.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementNumber.tsx new file mode 100644 index 000000000..b0342788f --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementNumber.tsx @@ -0,0 +1,70 @@ +/** + * ============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 React from 'react'; +import { ViewElementNumber } from "../models/uiModels"; +import { Tooltip, InputAdornment } from "@mui/material"; +import { BaseProps } from "./baseProps"; +import { IfWhenTextInput } from "./ifWhenTextInput"; +import { checkRange } from "../utilities/verifyer"; + +type numberInputProps = BaseProps<number>; + +export const UiElementNumber = (props: numberInputProps) => { + + + const [error, setError] = React.useState(false); + const [helperText, setHelperText] = React.useState(""); + const [isTooltipVisible, setTooltipVisibility] = React.useState(true); + + const element = props.value as ViewElementNumber; + + const verifyValue = (data: string) => { + const num = Number(data); + if (!isNaN(num)) { + const result = checkRange(element, num); + if (result.length > 0) { + setError(true); + setHelperText(result); + } else { + setError(false); + setHelperText(""); + } + } else { + setError(true); + setHelperText("Input is not a number."); + } + props.onChange(num); + } + + return ( + <Tooltip disableInteractive title={isTooltipVisible ? element.description || '' : ''}> + <IfWhenTextInput element={element} onChangeTooltipVisibility={setTooltipVisibility} + spellCheck={false} autoFocus margin="dense" + id={element.id} label={element.label} type="text" value={props.inputValue} + style={{ width: 485, marginLeft: 20, marginRight: 20 }} + onChange={(e) => { verifyValue(e.target.value) }} + error={error} + readOnly={props.readOnly} + disabled={props.disabled} + helperText={helperText} + startAdornment={element.units != null ? <InputAdornment position="start">{element.units}</InputAdornment> : undefined} + /> + </Tooltip> + ); +}
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementReference.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementReference.tsx new file mode 100644 index 000000000..e3bb8f048 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementReference.tsx @@ -0,0 +1,67 @@ +/** + * ============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 React, { useState } from 'react'; +import { Tooltip, Button, FormControl } from '@mui/material'; + +import createStyles from '@mui/styles/createStyles'; +import makeStyles from '@mui/styles/makeStyles'; + +import { ViewElement } from '../models/uiModels'; + +const useStyles = makeStyles(() => createStyles({ + button: { + 'justifyContent': 'left', + }, +})); + +type UIElementReferenceProps = { + element: ViewElement; + disabled: boolean; + onOpenReference(element: ViewElement): void; +}; + +export const UIElementReference: React.FC<UIElementReferenceProps> = (props) => { + const { element } = props; + const [disabled, setDisabled] = useState(true); + const classes = useStyles(); + return ( + <FormControl + variant="standard" + key={element.id} + style={{ width: 485, marginLeft: 20, marginRight: 20 }} + onMouseDown={(ev) => { + ev.preventDefault(); + ev.stopPropagation(); + if (ev.button === 1) { + setDisabled(!disabled); + } + }}> + <Tooltip disableInteractive title={element.description || element.path || ''}> + <Button + className={classes.button} + aria-label={element.label + '-button'} + color="secondary" + disabled={props.disabled && disabled} + onClick={() => { + props.onOpenReference(element); + }} >{`${element.label}`}</Button> + </Tooltip> + </FormControl> + ); +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementSelection.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementSelection.tsx new file mode 100644 index 000000000..ebd04dab4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementSelection.tsx @@ -0,0 +1,69 @@ +/** + * ============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 React from 'react'; +import { BaseProps } from './baseProps'; +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; + + let error = ''; + const value = String(props.inputValue); + if (element.mandatory && Boolean(!value)) { + error = 'Error'; + } + + return (props.readOnly || props.inputValue != null + ? (<FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`select-${element.id}`} >{element.label}</InputLabel> + <Select variant="standard" + required={!!element.mandatory} + error={!!error} + onChange={(e) => { props.onChange(e.target.value as string); }} + readOnly={props.readOnly} + disabled={props.disabled} + value={value.toString()} + aria-label={element.label + '-selection'} + inputProps={{ + name: element.id, + id: `select-${element.id}`, + }} + > + {element.options.map(option => ( + <MenuItem + key={option.key} + value={option.key} + aria-label={option.key}> + <Tooltip disableInteractive title={option.description || ''}> + <div style={{ width: '100%' }}> + {option.key} + </div> + </Tooltip> + </MenuItem> + ))} + </Select> + <FormHelperText>{error}</FormHelperText> + </FormControl>) + : null + ); +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementString.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementString.tsx new file mode 100644 index 000000000..8381d99a4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementString.tsx @@ -0,0 +1,84 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from "react" +import { Tooltip, TextField } from "@mui/material"; +import { ViewElementString } from "../models/uiModels"; +import { BaseProps } from "./baseProps"; +import { IfWhenTextInput } from "./ifWhenTextInput"; +import { checkRange, checkPattern } from "../utilities/verifyer"; + +type stringEntryProps = BaseProps ; + +export const UiElementString = (props: stringEntryProps) => { + + const [isError, setError] = React.useState(false); + const [helperText, setHelperText] = React.useState(""); + const [isTooltipVisible, setTooltipVisibility] = React.useState(true); + + const element = props.value as ViewElementString; + + const verifyValues = (data: string) => { + + if (data.trim().length > 0) { + + let errorMessage = ""; + const result = checkRange(element, data.length); + + if (result.length > 0) { + errorMessage += result; + } + + const patternResult = checkPattern(element.pattern, data) + + if (patternResult.error) { + errorMessage += patternResult.error; + } + + if (errorMessage.length > 0) { + setError(true); + setHelperText(errorMessage); + } else { + setError(false); + setHelperText(""); + } + } else { + setError(false); + setHelperText(""); + } + + + props.onChange(data); + + } + + return ( + <Tooltip disableInteractive title={isTooltipVisible ? element.description || '' : ''}> + <IfWhenTextInput element={element} onChangeTooltipVisibility={setTooltipVisibility} + spellCheck={false} autoFocus margin="dense" + id={element.id} label={props?.isKey ? "🔑 " + element.label : element.label} type="text" value={props.inputValue} + style={{ width: 485, marginLeft: 20, marginRight: 20 }} + onChange={(e: any) => { verifyValues(e.target.value) }} + error={isError} + readOnly={props.readOnly} + disabled={props.disabled} + helperText={helperText} + /> + </Tooltip> + ); +}
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementUnion.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementUnion.tsx new file mode 100644 index 000000000..8d232f5ee --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/components/uiElementUnion.tsx @@ -0,0 +1,91 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import * as React from 'react' +import { BaseProps } from './baseProps'; +import { Tooltip } from '@mui/material'; +import { IfWhenTextInput } from './ifWhenTextInput'; +import { ViewElementUnion, isViewElementString, isViewElementNumber, isViewElementObject, ViewElementNumber } from '../models/uiModels'; +import { checkRange, checkPattern } from '../utilities/verifyer'; + +type UiElementUnionProps = { isKey: boolean } & BaseProps; + +export const UIElementUnion = (props: UiElementUnionProps) => { + + const [isError, setError] = React.useState(false); + const [helperText, setHelperText] = React.useState(""); + const [isTooltipVisible, setTooltipVisibility] = React.useState(true); + + const element = props.value as ViewElementUnion; + + const verifyValues = (data: string) => { + + let foundObjectElements = 0; + let errorMessage = ""; + let isPatternCorrect = null; + + for (let i = 0; i < element.elements.length; i++) { + const unionElement = element.elements[i]; + + if (isViewElementNumber(unionElement)) { + + errorMessage = checkRange(unionElement, Number(data)); + + } else if (isViewElementString(unionElement)) { + errorMessage += checkRange(unionElement, data.length); + isPatternCorrect = checkPattern(unionElement.pattern, data).isValid; + + + } else if (isViewElementObject(unionElement)) { + foundObjectElements++; + } + + if (isPatternCorrect || errorMessage.length === 0) { + break; + } + } + + if (errorMessage.length > 0 || isPatternCorrect !== null && !isPatternCorrect) { + setError(true); + setHelperText("Input is wrong."); + } else { + setError(false); + setHelperText(""); + } + + if (foundObjectElements > 0 && foundObjectElements != element.elements.length) { + throw new Error(`The union element ${element.id} can't be changed.`); + + } else { + props.onChange(data); + } + }; + + return <Tooltip disableInteractive title={isTooltipVisible ? element.description || '' : ''}> + <IfWhenTextInput element={element} onChangeTooltipVisibility={setTooltipVisibility} + spellCheck={false} autoFocus margin="dense" + id={element.id} label={props.isKey ? "🔑 " + element.label : element.label} type="text" value={props.inputValue} + onChange={(e: any) => { verifyValues(e.target.value) }} + error={isError} + style={{ width: 485, marginLeft: 20, marginRight: 20 }} + readOnly={props.readOnly} + disabled={props.disabled} + helperText={helperText} + /> + </Tooltip>; +}
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts new file mode 100644 index 000000000..9cbd9163e --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts @@ -0,0 +1,47 @@ +/** + * ============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 { 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'; + +interface IConfigurationAppStoreState { + connectedNetworkElements: IConnectedNetworkElementsState; // used for ne selection + deviceDescription: IDeviceDescriptionState; // contains ui and device descriptions + viewDescription: IViewDescriptionState; // contains current ui description + valueSelector: IValueSelectorState; +} + +declare module '../../../../framework/src/store/applicationStore' { + interface IApplicationStoreState { + configuration: IConfigurationAppStoreState; + } +} + +const actionHandlers = { + connectedNetworkElements: connectedNetworkElementsActionHandler, + deviceDescription: deviceDescriptionHandler, + viewDescription: viewDescriptionHandler, + valueSelector: valueSelectorHandler, +}; + +export const configurationAppRootHandler = combineActionHandler<IConfigurationAppStoreState>(actionHandlers); +export default configurationAppRootHandler; diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts new file mode 100644 index 000000000..d2863dd2e --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts @@ -0,0 +1,45 @@ +/** + * ============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 { createExternal, IExternalTableState } from '../../../../framework/src/components/material-table/utilities'; +import { createSearchDataHandler } from '../../../../framework/src/utilities/elasticSearch'; +import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; + +import { NetworkElementConnection } from '../models/networkElementConnection'; +import { restService } from '../services/restServices'; + +export interface IConnectedNetworkElementsState extends IExternalTableState<NetworkElementConnection> { } + +// create elastic search material data fetch handler +const connectedNetworkElementsSearchHandler = createSearchDataHandler<NetworkElementConnection>('network-element-connection', false, { status: 'Connected' }); + +export const { + actionHandler: connectedNetworkElementsActionHandler, + createActions: createConnectedNetworkElementsActions, + createProperties: createConnectedNetworkElementsProperties, + reloadAction: connectedNetworkElementsReloadAction, + + // set value action, to change a value +} = createExternal<NetworkElementConnection>(connectedNetworkElementsSearchHandler, appState => appState.configuration.connectedNetworkElements, + (ne) => { + if (!ne || !ne.id) return true; + const neUrl = restService.getNetworkElementUri(ne.id); + const policy = getAccessPolicyByUrl(neUrl); + return !(policy.GET && policy.POST); + }, +); diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts new file mode 100644 index 000000000..cd01b0988 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts @@ -0,0 +1,48 @@ +/** + * ============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 { 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; + modules: { + [name: string]: Module; + }; + views: ViewSpecification[]; +} + +const deviceDescriptionStateInit: IDeviceDescriptionState = { + nodeId: '', + modules: {}, + views: [], +}; + +export const deviceDescriptionHandler: IActionHandler<IDeviceDescriptionState> = (state = deviceDescriptionStateInit, action) => { + if (action instanceof UpdateDeviceDescription) { + state = { + ...state, + nodeId: action.nodeId, + modules: action.modules, + views: action.views, + }; + } + return state; +}; diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts new file mode 100644 index 000000000..70d5eb253 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts @@ -0,0 +1,78 @@ +/** + * ============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 { 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; + keyProperty: string | undefined; + listSpecification: ViewSpecification | null; + listData: any[]; + onValueSelected: (value: any) => void; +} + +const dummyFunc = () => { }; +const valueSelectorStateInit: IValueSelectorState = { + collectingData: false, + keyProperty: undefined, + listSpecification: null, + listData: [], + onValueSelected: dummyFunc, +}; + +export const valueSelectorHandler: IActionHandler<IValueSelectorState> = (state = valueSelectorStateInit, action) => { + if (action instanceof SetCollectingSelectionData) { + state = { + ...state, + collectingData: action.busy, + }; + } else if (action instanceof EnableValueSelector) { + state = { + ...state, + collectingData: false, + keyProperty: action.keyProperty, + listSpecification: action.listSpecification, + onValueSelected: action.onValueSelected, + listData: action.listData, + }; + } else if (action instanceof SetSelectedValue) { + if (state.keyProperty) { + state.onValueSelected(action.value[state.keyProperty]); + } + state = { + ...state, + collectingData: false, + keyProperty: undefined, + listSpecification: null, + onValueSelected: dummyFunc, + listData: [], + }; + } else if (action instanceof UpdateDeviceDescription || action instanceof UpdateViewDescription || action instanceof UpdateOutputData) { + state = { + ...state, + collectingData: false, + keyProperty: undefined, + listSpecification: null, + onValueSelected: dummyFunc, + listData: [], + }; + } + return state; +}; diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts new file mode 100644 index 000000000..39b47be84 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts @@ -0,0 +1,82 @@ +/** + * ============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 { IActionHandler } from '../../../../framework/src/flux/action'; + +import { UpdateViewDescription, UpdateOutputData } from '../actions/deviceActions'; +import { ViewSpecification } from '../models/uiModels'; + +export enum DisplayModeType { + doNotDisplay = 0, + displayAsObject = 1, + displayAsList = 2, + displayAsRPC = 3, + displayAsMessage = 4, +} + +export type DisplaySpecification = { + displayMode: DisplayModeType.doNotDisplay; +} | { + displayMode: DisplayModeType.displayAsObject | DisplayModeType.displayAsList ; + viewSpecification: ViewSpecification; + keyProperty?: string; + apidocPath?: string; + dataPath?: string; +} | { + displayMode: DisplayModeType.displayAsRPC; + inputViewSpecification?: ViewSpecification; + outputViewSpecification?: ViewSpecification; + dataPath?: string; +} | { + displayMode: DisplayModeType.displayAsMessage; + renderMessage: string; +}; + +export interface IViewDescriptionState { + vPath: string | null; + displaySpecification: DisplaySpecification; + viewData: any; + outputData?: any; +} + +const viewDescriptionStateInit: IViewDescriptionState = { + vPath: null, + displaySpecification: { + displayMode: DisplayModeType.doNotDisplay, + }, + viewData: null, + outputData: undefined, +}; + +export const viewDescriptionHandler: IActionHandler<IViewDescriptionState> = (state = viewDescriptionStateInit, action) => { + if (action instanceof UpdateViewDescription) { + state = { + ...state, + vPath: action.vPath, + viewData: action.viewData, + outputData: undefined, + displaySpecification: action.displaySpecification, + }; + } else if (action instanceof UpdateOutputData) { + state = { + ...state, + outputData: action.outputData, + }; + } + return state; +}; diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/index.html b/sdnr/wt-odlux/odlux/apps/configurationApp/src/index.html new file mode 100644 index 000000000..4a0496bff --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/index.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <meta http-equiv="X-UA-Compatible" content="ie=edge"> + <!-- <link rel="stylesheet" href="./vendor.css"> --> + <title>Configuration App</title> +</head> + +<body> + <div id="app"></div> + <script type="text/javascript" src="./require.js"></script> + <script type="text/javascript" src="./config.js"></script> + <script> + // run the application + require(["app", "connectApp", "maintenanceApp", "configurationApp", "faultApp"], function (app, connectApp, maintenanceApp, configurationApp, faultApp) { + connectApp.register(); + configurationApp.register(); + maintenanceApp.register(); + faultApp.register(); + // app("./app.tsx").configureApplication({ authentication:"oauth", enablePolicy: true,}); + app("./app.tsx").configureApplication({ authentication:"basic", enablePolicy: false,}); + app("./app.tsx").runApplication(); + }); + </script> +</body> + +</html>
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/networkElementConnection.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/networkElementConnection.ts new file mode 100644 index 000000000..e1ef1ea2d --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/networkElementConnection.ts @@ -0,0 +1,37 @@ +/** + * ============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========================================================================== + */ + +export type NetworkElementConnection = { + id?: string; + nodeId: string; + host: string; + port: number; + username?: string; + password?: string; + isRequired?: boolean; + status?: 'connected' | 'mounted' | 'unmounted' | 'connecting' | 'disconnected' | 'idle'; + coreModelCapability?: string; + deviceType?: string; + nodeDetails?: { + availableCapabilities: string[]; + unavailableCapabilities: { + failureReason: string; + capability: string; + }[]; + }; +}; diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/uiModels.ts new file mode 100644 index 000000000..7d9e63caf --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/uiModels.ts @@ -0,0 +1,241 @@ +/** + * ============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 type { WhenAST } from '../yang/whenParser'; + +export type ViewElementBase = { + '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<YangRange>; // number of octets +}; + +// https://tools.ietf.org/html/rfc7950#section-9.7.4 +export type ViewElementBits = ViewElementBase & { + '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<RegExp>; + 'length'?: Expression<YangRange>; + 'invertMatch'?: true; +}; + +// special case derived from +export type ViewElementDate = ViewElementBase & { + 'uiType': 'date'; + 'pattern'?: Expression<RegExp>; + 'length'?: Expression<YangRange>; + 'invertMatch'?: true; +}; + +// https://tools.ietf.org/html/rfc7950#section-9.3 +export type ViewElementNumber = ViewElementBase & { + 'uiType': 'number'; + 'min': number; + 'max': number; + 'range'?: Expression<YangRange>; + '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; +}; + +// 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; + }[]; +}; + +// is a list if isList is true ;-) +export type ViewElementObject = ViewElementBase & { + '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; +}); + +export type ViewElementReference = ViewElementBase & { + 'uiType': 'reference'; + 'referencePath': string; + 'ref': (currentPath: string) => [ViewElement, string] | undefined; +}; + +export type ViewElementUnion = ViewElementBase & { + 'uiType': 'union'; + 'elements': ViewElement[]; +}; + +export type ViewElementChoiceCase = { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }; + +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; +}; + +export type ViewElementEmpty = ViewElementBase & { + 'uiType': 'empty'; +}; + +export type ViewElement = + | ViewElementEmpty + | ViewElementBits + | ViewElementBinary + | ViewElementString + | ViewElementDate + | ViewElementNumber + | ViewElementBoolean + | ViewElementObject + | ViewElementList + | ViewElementSelection + | ViewElementReference + | ViewElementUnion + | ViewElementChoice + | ViewElementRpc; + +export const isViewElementString = (viewElement: ViewElement): viewElement is ViewElementString => { + return viewElement && (viewElement.uiType === 'string' || viewElement.uiType === 'date'); +}; + +export const isViewElementDate = (viewElement: ViewElement): viewElement is ViewElementDate => { + return viewElement && (viewElement.uiType === 'date'); +}; + +export const isViewElementNumber = (viewElement: ViewElement): viewElement is ViewElementNumber => { + return viewElement && viewElement.uiType === 'number'; +}; + +export const isViewElementBoolean = (viewElement: ViewElement): viewElement is ViewElementBoolean => { + return viewElement && viewElement.uiType === 'boolean'; +}; + +export const isViewElementObject = (viewElement: ViewElement): viewElement is ViewElementObject => { + return viewElement && viewElement.uiType === 'object' && viewElement.isList === false; +}; + +export const isViewElementList = (viewElement: ViewElement): viewElement is ViewElementList => { + return viewElement && viewElement.uiType === 'object' && viewElement.isList === true; +}; + +export const isViewElementObjectOrList = (viewElement: ViewElement): viewElement is ViewElementObject | ViewElementList => { + return viewElement && viewElement.uiType === 'object'; +}; + +export const isViewElementSelection = (viewElement: ViewElement): viewElement is ViewElementSelection => { + return viewElement && viewElement.uiType === 'selection'; +}; + +export const isViewElementReference = (viewElement: ViewElement): viewElement is ViewElementReference => { + return viewElement && viewElement.uiType === 'reference'; +}; + +export const isViewElementUnion = (viewElement: ViewElement): viewElement is ViewElementUnion => { + return viewElement && viewElement.uiType === 'union'; +}; + +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'; +}; + +export const isViewElementEmpty = (viewElement: ViewElement): viewElement is ViewElementRpc => { + return viewElement && viewElement.uiType === 'empty'; +}; + +export const ResolveFunction = Symbol('IsResolved'); + +export type ViewSpecification = { + 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; +}; + +export type Expression<T> = + | T + | Operator<T>; + +export type Operator<T> = { + operation: 'AND' | 'OR'; + arguments: Expression<T>[]; +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/yang.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/yang.ts new file mode 100644 index 000000000..e4e59fb96 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/models/yang.ts @@ -0,0 +1,71 @@ +/** + * ============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 { ViewElement, ViewSpecification } from './uiModels'; + +export enum ModuleState { + stable, + instable, + importOnly, + unavailable, +} + +export type Token = { + name: string; + 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[]; +}; + +export type Revision = { + description?: string; + reference?: string; +}; + +export type Module = { + name: string; + namespace?: string; + prefix?: string; + state: ModuleState; + identities: { [name: string]: Identity }; + revisions: { [version: string]: Revision }; + imports: { [prefix: string]: string }; + features: { [feature: string]: { description?: string } }; + typedefs: { [type: string]: ViewElement }; + augments: { [path: string]: ViewSpecification[] }; + groupings: { [group: string]: ViewSpecification }; + views: { [view: string]: ViewSpecification }; + elements: { [view: string]: ViewElement }; + executionOrder?: number; +};
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/pluginConfiguration.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/pluginConfiguration.tsx new file mode 100644 index 000000000..7dd2d6ae4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/pluginConfiguration.tsx @@ -0,0 +1,145 @@ +/** + * ============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 React from 'react'; +import { withRouter, RouteComponentProps, Route, Switch, Redirect } from 'react-router-dom'; + +import { connect, Connect, IDispatcher } from '../../../framework/src/flux/connect'; +import applicationManager from '../../../framework/src/services/applicationManager'; + +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 mapDispatch = (dispatcher: IDispatcher) => ({ + updateNodeId: (nodeId: string) => dispatcher.dispatch(updateNodeIdAsyncActionCreator(nodeId)), + updateView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), +}); + +// eslint-disable-next-line @typescript-eslint/naming-convention +const ConfigurationApplicationRouteAdapter = connect(undefined, mapDispatch)((props: RouteComponentProps<{ nodeId?: string; 0: string }> & Connect<undefined, typeof mapDispatch>) => { + React.useEffect(() => { + return () => { + lastUrl = undefined; + currentNodeId = undefined; + currentVirtualPath = undefined; + }; + }, []); + if (props.location.pathname !== lastUrl) { + // 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 enableDump = false; + if (currentNodeId !== props.match.params.nodeId) { + currentNodeId = props.match.params.nodeId || undefined; + if (currentNodeId && currentNodeId.endsWith('|dump')) { + enableDump = true; + currentNodeId = currentNodeId.replace(/\|dump$/i, ''); + } + currentVirtualPath = null; + if (currentNodeId) { + await props.updateNodeId(currentNodeId); + } + } + + if (currentVirtualPath !== props.match.params[0]) { + currentVirtualPath = props.match.params[0]; + if (currentVirtualPath && currentVirtualPath.endsWith('|dump')) { + enableDump = true; + currentVirtualPath = currentVirtualPath.replace(/\|dump$/i, ''); + } + await props.updateView(currentVirtualPath); + } + + 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'; + const indention = Array(level * 4).fill(' ').join(''); + let result = ''; + + if (!view) debugger; + // result += `${indention} [${view.canEdit ? 'rw' : 'ro'}] ${view.ns}:${view.name} ${ds.displayMode === DisplayModeType.displayAsList ? '[LIST]' : ''}\r\n`; + result += Object.keys(view.elements).reduce((acc, cur) => { + const elm = view.elements[cur]; + acc += `${indention} [${elm.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': + 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); + const element = document.createElement('a'); + element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(dump)); + element.setAttribute('download', currentNodeId + '.txt'); + + element.style.display = 'none'; + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + } + + }); + } + return ( + <ConfigurationApplication /> + ); +}); + +const App = withRouter((props: RouteComponentProps) => ( + <Switch> + <Route path={`${props.match.url}/:nodeId/*`} component={ConfigurationApplicationRouteAdapter} /> + <Route path={`${props.match.url}/:nodeId`} component={ConfigurationApplicationRouteAdapter} /> + <Route path={`${props.match.url}`} component={NetworkElementSelector} /> + <Redirect to={`${props.match.url}`} /> + </Switch> +)); + +export function register() { + applicationManager.registerApplication({ + name: 'configuration', + icon: appIcon, + rootComponent: App, + rootActionHandler: configurationAppRootHandler, + menuEntry: 'Configuration', + }); +} diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/services/restServices.ts new file mode 100644 index 000000000..07e263559 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/services/restServices.ts @@ -0,0 +1,164 @@ +/** + * ============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 { requestRest, requestRestExt } from '../../../../framework/src/services/restService'; +import { convertPropertyNames, replaceHyphen } from '../../../../framework/src/utilities/yangHelper'; + +import { NetworkElementConnection } from '../models/networkElementConnection'; + +type ImportOnlyResponse = { + '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; + }[]; + }; + }[]; +}; + +type CapabilityAnswer = { + availableCapabilities: { + capabilityOrigin: string; + capability: string; + version: string; + }[] | null; + unavailableCapabilities: { + failureReason: string; + capability: string; + version: string; + }[] | null; + importOnlyModules: { + 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 }[]> { + 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<ImportOnlyResponse>(path, { method: 'GET' }); + const importOnlyModules = importOnlyResult + ? importOnlyResult['ietf-yang-library:yang-library']['module-set'][0]['import-only-module'] + : []; + return importOnlyModules; + } + + public async getCapabilitiesByMountId(nodeId: string): Promise<CapabilityAnswer> { + const path = this.getNetworkElementUri(nodeId); + const capabilitiesResult = await requestRest<CapabilityResponse>(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<any>(obj => convertPropertyNames(obj, replaceHyphen)) || []) + .map(cap => { + const capMatch = cap && capParser.exec(cap.capability); + return capMatch ? { + capabilityOrigin: cap.capabilityOrigin, + capability: capMatch && capMatch[2] || '', + version: capMatch && capMatch[1] || '', + } : 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<any>(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; + + return { availableCapabilities, unavailableCapabilities, importOnlyModules }; + } + + public async getMountedNetworkElementByMountId(nodeId: string): Promise<NetworkElementConnection | null> { + // const path = 'restconf/operational/network-topology:network-topology/topology/topology-netconf/node/' + nodeId; + // const connectedNetworkElement = await requestRest<NetworkElementConnection>(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; + } + + /** Reads the config data by restconf path. + * @param path The restconf path to be used for read. + * @returns The data. + */ + public getConfigData(path: string) { + return requestRestExt<{ [key: string]: any }>(path, { method: 'GET' }); + } + + /** Updates or creates the config data by restconf path using data. + * @param path The restconf path to identify the note to update. + * @param data The data to be updated. + * @returns The written data. + */ + public setConfigData(path: string, data: any) { + 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) }); + } + + /** Removes the element by restconf path. + * @param path The restconf path to identify the note to update. + * @returns The restconf result. + */ + public removeConfigElement(path: string) { + return requestRestExt<{ [key: string]: any }>(path, { method: 'DELETE' }); + } +} + +export const restService = new RestService(); +export default restService;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/services/yangService.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/services/yangService.ts new file mode 100644 index 000000000..bbd051aeb --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/services/yangService.ts @@ -0,0 +1,37 @@ +/** + * ============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========================================================================== + */ + +const cache: { [path: string]: string } = { }; +const getCapability = async (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; +}; + +export const yangService = { + getCapability, +}; +export default yangService;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/utilities/verifyer.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/utilities/verifyer.ts new file mode 100644 index 000000000..9dd12031f --- /dev/null +++ b/sdnr/wt-odlux/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<YangRange>): val is YangRange => (val as YangRange).min !== undefined; + +const isYangOperator = (val: YangRange | Operator<YangRange>): val is Operator<YangRange> => (val as Operator<YangRange>).operation !== undefined; + +const isRegExp = (val: RegExp | Operator<RegExp>): val is RegExp => (val as RegExp).source !== undefined; + +const isRegExpOperator = (val: RegExp | Operator<RegExp>): val is Operator<RegExp> => (val as Operator<RegExp>).operation !== undefined; + +const getRangeErrorMessagesRecursively = (value: Operator<YangRange>, 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<YangRange>, 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<RegExp>, 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<RegExp>, 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<RegExp> | 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/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts new file mode 100644 index 000000000..ad34c83b9 --- /dev/null +++ b/sdnr/wt-odlux/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<boolean> => { + 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/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/views/configurationApplication.tsx new file mode 100644 index 000000000..0f143d818 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -0,0 +1,931 @@ +/** + * ============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 React, { useState } from 'react'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; + +import { Theme } from '@mui/material/styles'; + +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; + +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, + 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'; +import PostAdd from '@mui/icons-material/PostAdd'; +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 Button from '@mui/material/Button'; +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'; +import { UiElementString } from '../components/uiElementString'; +import { UiElementBoolean } from '../components/uiElementBoolean'; +import { UiElementSelection } from '../components/uiElementSelection'; +import { UIElementUnion } from '../components/uiElementUnion'; +import { UiElementLeafList } from '../components/uiElementLeafList'; + +import { splitVPath } from '../utilities/viewEngineHelper'; + +const styles = (theme: Theme) => createStyles({ + header: { + 'display': 'flex', + 'justifyContent': 'space-between', + }, + leftButton: { + 'justifyContent': 'left', + }, + outer: { + 'flex': '1', + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center', + 'justifyContent': 'center', + }, + inner: { + + }, + container: { + 'height': '100%', + 'display': 'flex', + 'flexDirection': 'column', + }, + 'icon': { + 'marginRight': theme.spacing(0.5), + 'width': 20, + 'height': 20, + }, + 'fab': { + 'margin': theme.spacing(1), + }, + button: { + margin: 0, + padding: '6px 6px', + minWidth: 'unset', + }, + readOnly: { + '& label.Mui-focused': { + color: 'green', + }, + '& .MuiInput-underline:after': { + borderBottomColor: 'green', + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'red', + }, + '&:hover fieldset': { + borderColor: 'yellow', + }, + '&.Mui-focused fieldset': { + borderColor: 'green', + }, + }, + }, + uiView: { + overflowY: 'auto', + }, + section: { + padding: '15px', + borderBottom: `2px solid ${theme.palette.divider}`, + }, + viewElements: { + width: 485, marginLeft: 20, marginRight: 20, + }, + verificationElements: { + width: 485, marginLeft: 20, marginRight: 20, + }, + heading: { + fontSize: theme.typography.pxToRem(15), + fontWeight: theme.typography.fontWeightRegular, + }, + moduleCollection: { + marginTop: '16px', + overflow: 'auto', + }, + objectReult: { + overflow: 'auto', + }, +}); + +const mapProps = (state: IApplicationStoreState) => ({ + collectingData: state.configuration.valueSelector.collectingData, + listKeyProperty: state.configuration.valueSelector.keyProperty, + listSpecification: state.configuration.valueSelector.listSpecification, + listData: state.configuration.valueSelector.listData, + vPath: state.configuration.viewDescription.vPath, + nodeId: state.configuration.deviceDescription.nodeId, + viewData: state.configuration.viewDescription.viewData, + outputData: state.configuration.viewDescription.outputData, + displaySpecification: state.configuration.viewDescription.displaySpecification, +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), + onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)), + reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), + removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)), + executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)), +}); + +const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>; + +type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>; + +type ConfigurationApplicationComponentState = { + isNew: boolean; + editMode: boolean; + canEdit: boolean; + viewData: { [key: string]: any } | null; + choices: { [path: string]: { selectedCase: string; data: { [property: string]: any } } }; +}; + +type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any; +const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => { + const [disabled, setDisabled] = useState(true); + const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => { + if (ev.button === 1) { + setDisabled(!disabled); + ev.preventDefault(); + } + }; + return ( + <div onMouseDown={onMouseDown} > + <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} /> + </div> + ); +}; + +const OldProps = Symbol('OldProps'); +class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> { + + /** + * + */ + constructor(props: ConfigurationApplicationComponentProps) { + super(props); + + this.state = { + isNew: false, + canEdit: false, + editMode: false, + viewData: null, + choices: {}, + }; + } + + private static getChoicesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { + return Object.keys(elements).reduce((acc, cur) => { + const elm = elements[cur]; + if (isViewElementChoice(elm)) { + const caseKeys = Object.keys(elm.cases); + + // 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 => { + const caseElmElm = caseElm.elements[caseElmKey]; + return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined; + }); + }) || caseKeys[0]; + + // extract all data of the active case + const caseElements = elm.cases[selectedCase].elements; + const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => { + const dataElm = caseElements[dataCur]; + if (isViewElementEmpty(dataElm)) { + dataAcc[dataElm.label] = null; + } else if (viewData[dataElm.label] !== undefined) { + dataAcc[dataElm.label] = viewData[dataElm.label]; + } else if (viewData[dataElm.id] !== undefined) { + dataAcc[dataElm.id] = viewData[dataElm.id]; + } + return dataAcc; + }, {} as { [name: string]: any }); + + acc[elm.id] = { + selectedCase, + data, + }; + } + return acc; + }, {} as { [path: string]: { selectedCase: string; data: { [property: string]: any } } }) || {}; + }; + + static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) { + + if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) { + const isNew: boolean = nextProps.vPath?.endsWith('[]') || false; + const state = { + ...prevState, + isNew: isNew, + editMode: isNew, + viewData: nextProps.viewData || null, + [OldProps]: nextProps, + choices: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay + || nextProps.displaySpecification.displayMode === DisplayModeType.displayAsMessage + ? null + : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC + ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || [] + : ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData), + }; + return state; + } + return null; + } + + private navigate = (path: string) => { + this.props.history.push(`${this.props.match.url}${path}`); + }; + + private changeValueFor = (property: string, value: any) => { + this.setState({ + viewData: { + ...this.state.viewData, + [property]: value, + }, + }); + }; + + private collectData = (elements: { [name: string]: ViewElement }) => { + // ensure only active choices will be contained + const viewData: { [key: string]: any } = { ...this.state.viewData }; + 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) { + Object.keys(caseElements).forEach(caseElementKey => { + const elm = caseElements[caseElementKey]; + if (isViewElementEmpty(elm)) { + // insert null for all empty elements + viewData[elm.id] = null; + } + }); + return; + } + Object.keys(caseElements).forEach(caseElementKey => { + acc.push(caseElements[caseElementKey]); + }); + }); + return acc; + }, [] as ViewElement[]); + + return viewData && Object.keys(viewData).reduce((acc, cur) => { + if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) { + acc[cur] = viewData[cur]; + } + return acc; + }, {} as { [key: string]: any }); + }; + + private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => { + const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`); + return !(policy.GET && policy.POST); + }; + + private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => { + const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`); + return !(policy.GET && policy.POST); + }; + + private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => { + if (isViewElementEmpty(uiElement)) { + return null; + } else if (isViewElementSelection(uiElement)) { + return UiElementSelection; + } else if (isViewElementBoolean(uiElement)) { + return UiElementBoolean; + } else if (isViewElementString(uiElement)) { + return UiElementString; + } else if (isViewElementDate(uiElement)) { + return UiElementString; + } else if (isViewElementNumber(uiElement)) { + return UiElementNumber; + } else if (isViewElementUnion(uiElement)) { + return UIElementUnion; + } else { + if (process.env.NODE_ENV !== 'production') { + console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`); + } + return null; + } + }; + + private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const isKey = (uiElement.label === keyProperty); + const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + + // do not show elements w/o any value from the backend + if (viewData[uiElement.id] == null && !editMode) { + return null; + } else if (isViewElementEmpty(uiElement)) { + return null; + } else if (uiElement.isList) { + /* element is a leaf-list */ + return <UiElementLeafList + key={uiElement.id} + inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]} + value={uiElement} + readOnly={!canEdit} + disabled={editMode && !canEdit} + onChange={(e) => { this.changeValueFor(uiElement.id, e); }} + getEditorForViewElement={this.getEditorForViewElement} + />; + } else { + const Element = this.getEditorForViewElement(uiElement); + return Element != null + ? ( + <Element + key={uiElement.id} + isKey={isKey} + inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]} + value={uiElement} + readOnly={!canEdit} + disabled={editMode && !canEdit} + onChange={(e) => { this.changeValueFor(uiElement.id, e); }} + />) + : null; + } + }; + + // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + // const isKey = (uiElement.label === keyProperty); + // const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + // if (isViewElementObjectOrList(uiElement)) { + // return ( + // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + // <Tooltip title={uiElement.description || ''}> + // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => { + // this.navigate(`/${uiElement.id}`); + // }}>{uiElement.label}</Button> + // </Tooltip> + // </FormControl> + // ); + // } else { + // if (process.env.NODE_ENV !== "production") { + // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + // } + // return null; + // } + // }; + + private renderUIChoice = (uiElement: ViewElementChoice, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const isKey = (uiElement.label === keyProperty); + + const currentChoice = this.state.choices[uiElement.id]; + const currentCase = currentChoice && uiElement.cases[currentChoice.selectedCase]; + + const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + if (isViewElementChoice(uiElement)) { + const subElements = currentCase?.elements; + return ( + <> + <FormControl variant="standard" key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel> + <Select variant="standard" + aria-label={uiElement.label + '-selection'} + required={!!uiElement.mandatory} + onChange={(e) => { + if (currentChoice.selectedCase === e.target.value) { + return; // nothing changed + } + 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.choices[uiElement.id].selectedCase} + inputProps={{ + name: uiElement.id, + id: `select-${uiElement.id}`, + }} + > + { + Object.keys(uiElement.cases).map(caseKey => { + const caseElm = uiElement.cases[caseKey]; + return ( + <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: '100%' }}>{caseElm.label}</div></Tooltip></MenuItem> + ); + }) + } + </Select> + </FormControl> + {subElements + ? Object.keys(subElements).map(elmKey => { + const elm = subElements[elmKey]; + return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); + }) + : <h3>Invalid Choice</h3> + } + </> + ); + } else { + if (process.env.NODE_ENV !== 'production') { + console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`); + } + return null; + } + }; + + private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const { classes } = this.props; + + const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { + if (keyProperty) { + // if (vsA.label === vsB.label) return 0; + if (vsA.label === keyProperty) return -1; + if (vsB.label === keyProperty) return +1; + } + + // if (vsA.uiType === vsB.uiType) return 0; + // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; + // if (vsA.uiType === "object") return +1; + return -1; + }; + + const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => { + const elm = viewSpecification.elements[cur]; + if (isViewElementObjectOrList(elm)) { + acc.references.push(elm); + } else if (isViewElementChoice(elm)) { + acc.choices.push(elm); + } else if (isViewElementRpc(elm)) { + acc.rpcs.push(elm); + } else { + acc.elements.push(elm); + } + return acc; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }); + + sections.elements = sections.elements.sort(orderFunc); + + return ( + <div className={classes.uiView}> + <div className={classes.section} /> + {sections.elements.length > 0 + ? ( + <div className={classes.section}> + {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + {sections.references.length > 0 + ? ( + <div className={classes.section}> + {sections.references.map(element => ( + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} /> + ))} + </div> + ) : null + } + {sections.choices.length > 0 + ? ( + <div className={classes.section}> + {sections.choices.map(element => this.renderUIChoice(element, viewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + {sections.rpcs.length > 0 + ? ( + <div className={classes.section}> + {sections.rpcs.map(element => ( + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} /> + ))} + </div> + ) : null + } + </div> + ); + }; + + private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const { classes } = this.props; + + // group by module name + const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => { + const elm = viewSpecification.elements[cur]; + const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} }); + moduleView.elements[cur] = elm; + return acc; + }, {}); + + const moduleKeys = Object.keys(modules).sort(); + + return ( + <div className={classes.moduleCollection}> + { + moduleKeys.map(key => { + const moduleView = modules[key]; + + return ( + <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} > + <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} > + <Typography className={classes.heading}>{key}</Typography> + </AccordionSummaryExt> + <AccordionDetails> + {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)} + </AccordionDetails> + </Accordion> + ); + }) + } + </div> + ); + }; + + private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) { + const listElements = listSpecification.elements; + const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath + .replace('$$$standard$$$', 'topology-netconfnode%20resources%20-%20RestConf%20RFC%208040') + .replace('$$$action$$$', 'put')}${listKeyProperty ? `_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : '' }` : undefined; + + const config = listSpecification.config && listKeyProperty; // We can not configure a list with no key. + + const navigate = (path: string) => { + this.props.history.push(`${this.props.match.url}${path}`); + }; + + const addNewElementAction = { + icon: AddIcon, + tooltip: 'Add', + ariaLabel:'add-element', + onClick: () => { + navigate('[]'); // empty key means new element + }, + disabled: !config, + }; + + const addWithApiDocElementAction = { + icon: PostAdd, + tooltip: 'Add', + ariaLabel:'add-element-via-api-doc', + onClick: () => { + window.open(apiDocPathCreate, '_blank'); + }, + disabled: !config, + }; + + const { classes, removeElement } = this.props; + + const DeleteIconWithConfirmation: React.FC<{ disabled?: boolean; rowData: { [key: string]: any }; onReload: () => void }> = (props) => { + const confirm = useConfirm(); + + return ( + <Tooltip disableInteractive title={'Remove'} > + <IconButton + disabled={props.disabled} + className={classes.button} + aria-label="remove-element-button" + onClick={async (e) => { + e.stopPropagation(); + e.preventDefault(); + confirm({ title: 'Do you really want to delete this element ?', description: 'This action is permanent!', confirmationButtonProps: { color: 'secondary' }, 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}]`); + }).then(props.onReload); + }} + size="large"> + <RemoveIcon /> + </IconButton> + </Tooltip> + ); + }; + + return ( + <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={ + Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { + const elm = listElements[cur]; + if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) { + if (elm.label !== listKeyProperty) { + acc.push(elm.uiType === 'boolean' + ? { property: elm.label, type: ColumnType.boolean } + : elm.uiType === 'date' + ? { property: elm.label, type: ColumnType.date } + : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); + } else { + acc.unshift(elm.uiType === 'boolean' + ? { property: elm.label, type: ColumnType.boolean } + : elm.uiType === 'date' + ? { property: elm.label, type: ColumnType.date } + : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); + } + } + return acc; + }, []).concat([{ + property: 'Actions', disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { + return ( + <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => 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(','); + } else { + keyId = row[listKeyProperty]; + } + if (listKeyProperty) { + navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key. + } + }} ></SelectElementTable> + ); + } + + private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) { + const { classes } = this.props; + + const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { + if (keyProperty) { + // if (vsA.label === vsB.label) return 0; + if (vsA.label === keyProperty) return -1; + if (vsB.label === keyProperty) return +1; + } + + // if (vsA.uiType === vsB.uiType) return 0; + // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; + // if (vsA.uiType === "object") return +1; + return -1; + }; + + const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => { + const elm = inputViewSpecification.elements[cur]; + if (isViewElementObjectOrList(elm)) { + console.error('Object should not appear in RPC view !'); + } else if (isViewElementChoice(elm)) { + acc.choices.push(elm); + } else if (isViewElementRpc(elm)) { + console.error('RPC should not appear in RPC view !'); + } else { + acc.elements.push(elm); + } + return acc; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }) + || { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }; + + sections.elements = sections.elements.sort(orderFunc); + + return ( + <> + <div className={classes.section} /> + { sections.elements.length > 0 + ? ( + <div className={classes.section}> + {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + { sections.choices.length > 0 + ? ( + <div className={classes.section}> + {sections.choices.map(element => this.renderUIChoice(element, inputViewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + <Button color="inherit" onClick={() => { + const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements); + this.props.executeRpc(this.props.vPath!, resultingViewData); + }} >Exec</Button> + <div className={classes.objectReult}> + {outputViewData !== undefined + ? renderObject(outputViewData) + : null + } + </div> + </> + ); + } + + private renderBreadCrumps() { + const { editMode } = this.state; + const { displaySpecification, vPath, nodeId } = this.props; + const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key + let lastPath = '/configuration'; + let basePath = `/configuration/${nodeId}`; + return ( + <div className={this.props.classes.header}> + <div> + <Breadcrumbs aria-label="breadcrumbs"> + <Link underline="hover" color="inherit" href="#" aria-label="back-breadcrumb" + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(lastPath); + }}>Back</Link> + <Link underline="hover" color="inherit" href="#" + aria-label={nodeId + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(`/configuration/${nodeId}`); + }}><span>{nodeId}</span></Link> + { + pathParts.map(([prop, key], ind) => { + const path = `${basePath}/${prop}`; + const keyPath = key && `${basePath}/${prop}[${key}]`; + const propTitle = prop.replace(/^[^:]+:/, ''); + const ret = ( + <span key={ind}> + <Link underline="hover" color="inherit" href="#" + aria-label={propTitle + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(path); + }}><span>{propTitle}</span></Link> + { + keyPath && <Link underline="hover" color="inherit" href="#" + aria-label={key + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(keyPath); + }}>{`[${key && key.replace(/\%2C/g, ',')}]`}</Link> || null + } + </span> + ); + lastPath = basePath; + basePath = keyPath || path; + return ret; + }) + } + </Breadcrumbs> + </div> + {this.state.editMode && ( + <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => { + if (this.props.vPath) { + await this.props.reloadView(this.props.vPath); + } + this.setState({ editMode: false }); + }} ><ArrowBack /></Fab> + ) || null} + { /* do not show edit if this is a list or it can't be edited */ + displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div> + <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => { + if (this.state.editMode) { + // ensure only active choices will be contained + const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); + this.props.onUpdateData(this.props.vPath!, resultingViewData); + } + this.setState({ editMode: !editMode }); + }}> + {editMode + ? <SaveIcon /> + : <EditIcon /> + } + </Fab> + </div> || null) + } + </div> + ); + } + + private renderValueSelector() { + const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; + if (!listKeyProperty || !listSpecification) { + throw new Error('ListKex ot view not specified.'); + } + + return ( + <div className={this.props.classes.container}> + <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} columns={ + Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { + const elm = listSpecification.elements[cur]; + if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) { + if (elm.label !== listKeyProperty) { + acc.push({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); + } else { + acc.unshift({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); + } + } + return acc; + }, []) + } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable> + </div> + ); + } + + private renderValueEditor() { + const { displaySpecification: ds, outputData } = this.props; + const { viewData, editMode, isNew } = this.state; + + return ( + <div className={this.props.classes.container}> + {this.renderBreadCrumps()} + {ds.displayMode === DisplayModeType.doNotDisplay + ? null + : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array + ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData) + : ds.displayMode === DisplayModeType.displayAsRPC + ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false) + : ds.displayMode === DisplayModeType.displayAsMessage + ? this.renderMessage(ds.renderMessage) + : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew) + } + </div > + ); + } + + private renderMessage(renderMessage: string) { + return ( + <div className={this.props.classes.container}> + <h4>{renderMessage}</h4> + </div> + ); + } + + private renderCollectingData() { + return ( + <div className={this.props.classes.outer}> + <div className={this.props.classes.inner}> + <Loader /> + <h3>Processing ...</h3> + </div> + </div> + ); + } + + render() { + return this.props.collectingData || !this.state.viewData + ? this.renderCollectingData() + : this.props.listSpecification + ? this.renderValueSelector() + : this.renderValueEditor(); + } +} + +export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent))); +export default ConfigurationApplication;
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt-odlux/odlux/apps/configurationApp/src/views/networkElementSelector.tsx new file mode 100644 index 000000000..e96f40d61 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/views/networkElementSelector.tsx @@ -0,0 +1,72 @@ +/** + * ============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 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 { NetworkElementConnection } from '../models/networkElementConnection'; +import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from '../../../configurationApp/src/handlers/connectedNetworkElementsHandler'; + + +const mapProps = (state: IApplicationStoreState) => ({ + connectedNetworkElementsProperties: createConnectedNetworkElementsProperties(state), +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + connectedNetworkElementsActions: createConnectedNetworkElementsActions(dispatcher.dispatch), +}); + +const ConnectedElementTable = MaterialTable as MaterialTableCtorType<NetworkElementConnection>; + +type NetworkElementSelectorComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch>; + +let initialSorted = false; + +class NetworkElementSelectorComponent extends React.Component<NetworkElementSelectorComponentProps> { + + componentDidMount() { + + if (!initialSorted) { + initialSorted = true; + this.props.connectedNetworkElementsActions.onHandleRequestSort('node-id'); + } else + this.props.connectedNetworkElementsActions.onRefresh(); + } + + render() { + return ( + <ConnectedElementTable stickyHeader tableId="configurable-elements-table" onHandleClick={(e, row) => { 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 > + </ConnectedElementTable> + ); + } +} + +export const NetworkElementSelector = withRouter(connect(mapProps, mapDispatch)(NetworkElementSelectorComponent)); +export default NetworkElementSelector; + diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/src/yang/whenParser.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/yang/whenParser.ts new file mode 100644 index 000000000..fa2968c9c --- /dev/null +++ b/sdnr/wt-odlux/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/odlux/apps/configurationApp/src/yang/yangParser.ts b/sdnr/wt-odlux/odlux/apps/configurationApp/src/yang/yangParser.ts new file mode 100644 index 000000000..2dbbae274 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/src/yang/yangParser.ts @@ -0,0 +1,1625 @@ +/* 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 + * ================================================================================================= + * 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 { Token, Statement, Module, Identity, ModuleState } from '../models/yang'; +import { + 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[] = []; + let partMatch: RegExpExecArray | null; + if (vPath) do { + partMatch = vPathParser.exec(vPath); + if (partMatch) { + pathParts.push(partMatch); + } + } while (partMatch); + return pathParts; +}; + +class YangLexer { + + private pos: number = 0; + + private buf: string = ''; + + constructor(input: string) { + this.pos = 0; + this.buf = input; + } + + private _opTable: { [key: string]: string } = { + ';': 'SEMI', + '{': 'L_BRACE', + '}': 'R_BRACE', + }; + + private _isNewline(char: string): boolean { + return char === '\r' || char === '\n'; + } + + private _isWhitespace(char: string): boolean { + return char === ' ' || char === '\t' || this._isNewline(char); + } + + private _isDigit(char: string): boolean { + return char >= '0' && char <= '9'; + } + + private _isAlpha(char: string): boolean { + return (char >= 'a' && char <= 'z') || + (char >= 'A' && char <= 'Z'); + } + + private _isAlphanum(char: string): boolean { + return this._isAlpha(char) || this._isDigit(char) || + char === '_' || char === '-' || char === '.'; + } + + private _skipNonTokens() { + while (this.pos < this.buf.length) { + const char = this.buf.charAt(this.pos); + if (this._isWhitespace(char)) { + this.pos++; + } else { + break; + } + } + } + + private _processString(terminator: string | null): Token { + // this.pos points at the opening quote. Find the ending quote. + let end_index = this.pos + 1; + while (end_index < this.buf.length) { + const char = this.buf.charAt(end_index); + if (char === '\\') { + end_index += 2; + continue; + } + if (terminator === null && (this._isWhitespace(char) || this._opTable[char] !== undefined) || char === terminator) { + break; + } + end_index++; + } + + if (end_index >= this.buf.length) { + throw Error('Unterminated quote at ' + this.pos); + } else { + const start = this.pos + (terminator ? 1 : 0); + const end = end_index; + const tok = { + name: 'STRING', + value: this.buf.substring(start, end), + start, + end, + }; + this.pos = terminator ? end + 1 : end; + return tok; + } + } + + private _processIdentifier(): Token { + let endpos = this.pos + 1; + while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) { + ++endpos; + } + + let name = 'IDENTIFIER'; + if (this.buf.charAt(endpos) === ':') { + name = 'IDENTIFIERREF'; + ++endpos; + while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) { + ++endpos; + } + } + + const tok = { + name: name, + value: this.buf.substring(this.pos, endpos), + start: this.pos, + end: endpos, + }; + + this.pos = endpos; + return tok; + } + + private _processNumber(): Token { + let endpos = this.pos + 1; + while (endpos < this.buf.length && + this._isDigit(this.buf.charAt(endpos))) { + endpos++; + } + + const tok = { + name: 'NUMBER', + value: this.buf.substring(this.pos, endpos), + start: this.pos, + end: endpos, + }; + this.pos = endpos; + return tok; + } + + private _processLineComment() { + var endpos = this.pos + 2; + // Skip until the end of the line + while (endpos < this.buf.length && !this._isNewline(this.buf.charAt(endpos))) { + endpos++; + } + this.pos = endpos + 1; + } + + 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) === '*'))) { + endpos++; + } + this.pos = endpos + 1; + } + + public tokenize(): Token[] { + const result: Token[] = []; + this._skipNonTokens(); + while (this.pos < this.buf.length) { + + const char = this.buf.charAt(this.pos); + 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(); + const peekChar = this.buf.charAt(this.pos); + if (this._opTable[peekChar] === undefined) { + result.push((peekChar !== '\'' && peekChar !== '"') + ? this._processString(null) + : this._processString(peekChar)); + } + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') { + this._processLineComment(); + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') { + this._processBlockComment(); + } else { + throw Error('Token error at ' + this.pos + ' ' + this.buf[this.pos]); + } + this._skipNonTokens(); + } + return result; + } + + public tokenize2(): Statement { + let stack: Statement[] = [{ key: 'ROOT', sub: [] }]; + let current: Statement | null = null; + + this._skipNonTokens(); + while (this.pos < this.buf.length) { + + const char = this.buf.charAt(this.pos); + const op = this._opTable[char]; + + if (op !== undefined) { + if (op === 'L_BRACE') { + current && stack.unshift(current); + current = null; + } else if (op === 'R_BRACE') { + current = stack.shift() || null; + } + this.pos++; + } else if (this._isAlpha(char) || char === '_') { + const key = this._processIdentifier().value; + this._skipNonTokens(); + let peekChar = this.buf.charAt(this.pos); + let arg = undefined; + if (this._opTable[peekChar] === undefined) { + arg = (peekChar === '"' || peekChar === '\'') + ? this._processString(peekChar).value + : this._processString(null).value; + } + do { + this._skipNonTokens(); + peekChar = this.buf.charAt(this.pos); + if (peekChar !== '+') break; + this.pos++; + this._skipNonTokens(); + peekChar = this.buf.charAt(this.pos); + 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) === '/') { + this._processLineComment(); + } 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)); + } + this._skipNonTokens(); + } + if (stack[0].key !== 'ROOT' || !stack[0].sub![0]) { + throw new Error('Internal Perser Error'); + } + return stack[0].sub![0]; + } +} + +export class YangParser { + private _groupingsToResolve: ViewSpecification[] = []; + + private _typeRefToResolve: (() => void)[] = []; + + private _identityToResolve: (() => void)[] = []; + + private _unionsToResolve: (() => void)[] = []; + + private _modulesToResolve: (() => void)[] = []; + + private _modules: { [name: string]: Module } = {}; + + private _views: ViewSpecification[] = [{ + id: '0', + name: 'root', + language: 'en-US', + canEdit: false, + config: true, + parentView: '0', + title: 'root', + elements: {}, + }]; + + 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 }[] = [], + ) { + + } + + public get modules() { + return this._modules; + } + + public get views() { + return this._views; + } + + public async addCapability(capability: string, version?: string, parentImportOnlyModule?: boolean) { + // do not add twice + 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; + } + + // // do not add unavailable capabilities + // if (this._unavailableCapabilities.some(c => c.capability === capability)) { + // // 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}:${version || ''}.`); + } + + const rootStatement = new YangLexer(data).tokenize2(); + + if (rootStatement.key !== 'module') { + throw new Error(`Root element of ${capability} is not a module.`); + } + if (rootStatement.arg !== capability) { + throw new Error(`Root element capability ${rootStatement.arg} does not requested ${capability}.`); + } + + 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, + imports: {}, + features: {}, + identities: {}, + augments: {}, + groupings: {}, + typedefs: {}, + views: {}, + elements: {}, + state: isUnavailable + ? ModuleState.unavailable + : isImportOnly + ? ModuleState.importOnly + : ModuleState.stable, + }; + + await this.handleModule(module, rootStatement, capability); + } + + private async handleModule(module: Module, rootStatement: Statement, capability: string) { + + // extract namespace && prefix + module.namespace = this.extractValue(rootStatement, 'namespace'); + module.prefix = this.extractValue(rootStatement, 'prefix'); + if (module.prefix) { + module.imports[module.prefix] = capability; + } + + // extract features + 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'); + acc[feature.arg] = { + description, + }; + return acc; + }, {}), + }; + + // extract imports + 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'); + 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 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); + } + } + + this.extractTypeDefinitions(rootStatement, module, ''); + + this.extractIdentities(rootStatement, 0, module, ''); + + const groupings = this.extractGroupings(rootStatement, 0, module, ''); + this._views.push(...groupings); + + 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, ''); + this._views.push(currentView, ...subViews); + + // create the root elements for this module + module.elements = currentView.elements; + this._modulesToResolve.push(() => { + Object.keys(module.elements).forEach(key => { + const viewElement = module.elements[key]; + if (!(isViewElementObjectOrList(viewElement) || isViewElementRpc(viewElement))) { + console.error(new Error(`Module: [${module}]. Only Object, List or RPC are allowed on root level.`)); + } + if (isViewElementObjectOrList(viewElement)) { + 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]; + }); + }); + return module; + } + + public postProcess() { + + // execute all post processes like resolving in proper order + this._unionsToResolve.forEach(cb => { + try { cb(); } catch (error) { + console.warn(error.message); + } + }); + + // process all groupings + this._groupingsToResolve.filter(vs => vs.uses && vs.uses[ResolveFunction]).forEach(vs => { + try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!('|'); } catch (error) { + console.warn(`Error resolving: [${vs.name}] [${error.message}]`); + } + }); + + /** + * 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) + .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; + } + }); + + // 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 => { + const module = this.modules[modKey]; + const augmentKeysWithCounter = Object.keys(module.augments).map((key) => { + const pathParts = splitVPath(key, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property + let nameSpaceChangeCounter = 0; + let currentNS = module.name; // init namespace + pathParts.forEach(([ns, _]) => { + if (ns === currentNS) { + currentNS = ns; + nameSpaceChangeCounter++; + } + }); + return { + key, + nameSpaceChangeCounter, + }; + }); + + const augmentKeys = augmentKeysWithCounter + .sort((a, b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1) + .map((a) => a.key); + + augmentKeys.forEach(augKey => { + const augments = module.augments[augKey]; + const viewSpec = this.resolveView(augKey); + if (!viewSpec) console.warn(`Could not find view to augment [${augKey}] in [${module.name}].`); + 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, + ifFeature, + }; + })); + } + }); + }); + + // process Identities + const traverseIdentity = (identities: Identity[]) => { + const result: Identity[] = []; + for (let identity of identities) { + if (identity.children && identity.children.length > 0) { + result.push(...traverseIdentity(identity.children)); + } else { + result.push(identity); + } + } + return result; + }; + + const baseIdentities: Identity[] = []; + Object.keys(this.modules).forEach(modKey => { + const module = this.modules[modKey]; + Object.keys(module.identities).forEach(idKey => { + const identity = module.identities[idKey]; + if (identity.base != null) { + const base = this.resolveIdentity(identity.base, module); + base.children?.push(identity); + } else { + baseIdentities.push(identity); + } + }); + }); + baseIdentities.forEach(identity => { + identity.values = identity.children && traverseIdentity(identity.children) || []; + }); + + this._identityToResolve.forEach(cb => { + try { cb(); } catch (error) { + console.warn(error.message); + } + }); + + this._typeRefToResolve.forEach(cb => { + try { cb(); } catch (error) { + console.warn(error.message); + } + }); + + this._modulesToResolve.forEach(cb => { + try { cb(); } catch (error) { + console.warn(error.message); + } + }); + + // resolve readOnly + const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => { + + // update view config + view.config = view.config && parentConfig; + + Object.keys(view.elements).forEach((key) => { + const elm = view.elements[key]; + + // update element config + elm.config = elm.config && view.config; + + // update all sub-elements of objects + if (elm.uiType === 'object') { + resolveReadOnly(this.views[+elm.viewId], elm.config); + } + + }); + }; + + const dump = resolveReadOnly(this.views[0], true); + if (LOGLEVEL > 2) { + console.log('Resolved views:', dump); + } + } + + private _nextId = 1; + + private get nextId() { + return this._nextId++; + } + + private extractNodes(statement: Statement, key: string): Statement[] { + return statement.sub && statement.sub.filter(s => s.key === key) || []; + } + + private extractValue(statement: Statement, key: string): string | undefined; + private extractValue(statement: Statement, key: string, parser: RegExp): RegExpExecArray | undefined; + private extractValue(statement: Statement, key: string, parser?: RegExp): string | RegExpExecArray | undefined { + const typeNodes = this.extractNodes(statement, key); + const rawValue = typeNodes.length > 0 && typeNodes[0].arg || undefined; + return parser + ? rawValue && parser.exec(rawValue) || undefined + : rawValue; + } + + private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void { + const typedefs = this.extractNodes(statement, 'typedef'); + typedefs && typedefs.forEach(def => { + if (!def.arg) { + throw new Error(`Module: [${module.name}]. Found typedef without name.`); + } + module.typedefs[def.arg] = this.getViewElement(def, module, 0, currentPath, false); + }); + } + + /** 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'); + if (groupings && groupings.length > 0) { + subViews.push(...groupings.reduce<ViewSpecification[]>((acc, cur) => { + if (!cur.arg) { + throw new Error(`Module: [${module.name}][${currentPath}]. Found grouping without name.`); + } + const grouping = cur.arg; + + // the default for config on module level is config = true; + const [currentView, currentSubViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath); + grouping && (module.groupings[grouping] = currentView); + acc.push(currentView, ...currentSubViews); + return acc; + }, [])); + } + + return subViews; + } + + /** 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'); + if (augments && augments.length > 0) { + subViews.push(...augments.reduce<ViewSpecification[]>((acc, cur) => { + if (!cur.arg) { + throw new Error(`Module: [${module.name}][${currentPath}]. Found augment without path.`); + } + const augment = this.resolveReferencePath(cur.arg, module); + + // the default for config on module level is config = true; + 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, ...currentSubViews); + return acc; + }, [])); + } + + return subViews; + } + + /** Handles identities */ + private extractIdentities(statement: Statement, parentId: number, module: Module, currentPath: string) { + 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 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: [], + }; + return acc; + }, {}); + } + + // 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, + }, + }; + + const currentId = this.nextId; + const subViews: ViewSpecification[] = []; + let elements: ViewElement[] = []; + + 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); + + // extract all scoped typedefs + this.extractTypeDefinitions(statement, context, currentPath); + + // extract all scoped groupings + subViews.push( + ...this.extractGroupings(statement, parentId, context, currentPath), + ); + + // extract all container + const container = this.extractNodes(statement, 'container'); + if (container && container.length > 0) { + subViews.push(...container.reduce<ViewSpecification[]>((acc, cur) => { + if (!cur.arg) { + throw new Error(`Module: [${context.name}]${currentPath}. Found container without name.`); + } + 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', + viewId: currentView.id, + config: currentView.config, + }); + 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'); + if (lists && lists.length > 0) { + subViews.push(...lists.reduce<ViewSpecification[]>((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; + if (elmConfig && !key) { + console.warn(`Module: [${context.name}]${currentPath}. Found configurable list without key. Assume config shell be false.`); + elmConfig = false; + } + 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', + viewId: currentView.id, + key: key, + config: elmConfig && currentView.config, + }); + 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'); + if (leafLists && leafLists.length > 0) { + elements.push(...leafLists.reduce<ViewElement[]>((acc, cur) => { + const element = this.getViewElement(cur, context, parentId, currentPath, true); + element && acc.push(element); + return acc; + }, [])); + } + + // process all leafs + // a leaf is mainly a property of an object + const leafs = this.extractNodes(statement, 'leaf'); + if (leafs && leafs.length > 0) { + elements.push(...leafs.reduce<ViewElement[]>((acc, cur) => { + const element = this.getViewElement(cur, context, parentId, currentPath, false); + element && acc.push(element); + return acc; + }, [])); + } + + + const choiceStms = this.extractNodes(statement, 'choice'); + if (choiceStms && choiceStms.length > 0) { + elements.push(...choiceStms.reduce<ViewElementChoice[]>((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(curChoice, 'case'); + if (caseStms && caseStms.length > 0) { + cases.push(...caseStms.reduce((accCase, curCase) => { + if (!curCase.arg) { + 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}:${curChoice.arg}`); + subViews.push(caseView, ...caseSubViews); + + 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, + }; + accCase.push(caseDef); + return accCase; + }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[])); + } + + // extract all simple cases (one case per leaf, container, etc.) + 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 }, + }; + accElm.push(caseDef); + return accElm; + }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[])); + + 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(curChoice, 'mandatory') === 'true' || false; + + 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: choiceConfig, + mandatory: mandatory, + 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 } } }), + }; + + accChoice.push(element); + return accChoice; + }, [])); + } + + const rpcStms = this.extractNodes(statement, 'rpc'); + if (rpcStms && rpcStms.length > 0) { + elements.push(...rpcStms.reduce<ViewElementRpc[]>((accRpc, curRpc) => { + if (!curRpc.arg) { + throw new Error(`Module: [${context.name}]${currentPath}. Found rpc without name.`); + } + + 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; + + if (input && input.length > 0) { + const [inputView, inputSubViews] = this.extractSubViews(input[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`); + subViews.push(inputView, ...inputSubViews); + inputViewId = inputView.id; + } + + if (output && output.length > 0) { + const [outputView, outputSubViews] = this.extractSubViews(output[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`); + subViews.push(outputView, ...outputSubViews); + outputViewId = outputView.id; + } + + const element: ViewElementRpc = { + uiType: 'rpc', + id: parentId === 0 ? `${context.name}:${curRpc.arg}` : curRpc.arg, + label: curRpc.arg, + path: currentPath, + module: context.name || module.name || '', + config: rpcConfig, + description: rpcDescription, + inputViewId: inputViewId, + outputViewId: outputViewId, + }; + + accRpc.push(element); + + return accRpc; + }, [])); + } + + 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), + parentView: String(parentId), + ns: context.name, + name: statement.arg != null ? statement.arg : undefined, + title: statement.arg != null ? statement.arg : undefined, + language: 'en-us', + canEdit: false, + config: config, + ifFeature: ifFeature, + when: whenParsed, + elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => { + acc[cur.id] = cur; + return acc; + }, {}), + }; + + // evaluate canEdit depending on all conditions + 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'); + if (usesRefs && usesRefs.length > 0) { + + viewSpec.uses = (viewSpec.uses || []); + const resolveFunctions: ((parentElementPath: string) => void)[] = []; + + for (let i = 0; i < usesRefs.length; ++i) { + const groupingName = usesRefs[i].arg; + if (!groupingName) { + throw new Error(`Module: [${context.name}]. Found an uses statement without a grouping name.`); + } + + viewSpec.uses.push(this.resolveReferencePath(groupingName, context)); + + resolveFunctions.push((parentElementPath: string) => { + const groupingViewSpec = this.resolveGrouping(groupingName, context); + if (groupingViewSpec) { + + // resolve recursive + const resolveFunc = groupingViewSpec.uses && groupingViewSpec.uses[ResolveFunction]; + resolveFunc && resolveFunc(parentElementPath); + + 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: resolvedWhen, + ifFeature: resolvedIfFeature, + }; + }); + } + }); + } + + viewSpec.uses[ResolveFunction] = (parentElementPath: string) => { + const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`; + resolveFunctions.forEach(resolve => { + try { + resolve(currentElementPath); + } catch (error) { + console.error(error); + } + }); + // console.log("Resolved "+currentElementPath, viewSpec); + if (viewSpec?.uses) { + viewSpec.uses[ResolveFunction] = undefined; + } + }; + + this._groupingsToResolve.push(viewSpec); + } + + return [viewSpec, subViews]; + } + + // https://tools.ietf.org/html/rfc7950#section-9.3.4 + private static decimalRange = [ + { min: -9223372036854775808, max: 9223372036854775807 }, + { min: -922337203685477580.8, max: 922337203685477580.7 }, + { min: -92233720368547758.08, max: 92233720368547758.07 }, + { min: -9223372036854775.808, max: 9223372036854775.807 }, + { min: -922337203685477.5808, max: 922337203685477.5807 }, + { min: -92233720368547.75808, max: 92233720368547.75807 }, + { min: -9223372036854.775808, max: 9223372036854.775807 }, + { min: -922337203685.4775808, max: 922337203685.4775807 }, + { min: -92233720368.54775808, max: 92233720368.54775807 }, + { min: -9223372036.854775808, max: 9223372036.854775807 }, + { min: -922337203.6854775808, max: 922337203.6854775807 }, + { min: -92233720.36854775808, max: 92233720.36854775807 }, + { min: -9223372.036854775808, max: 9223372.036854775807 }, + { min: -922337.2036854775808, max: 922337.2036854775807 }, + { min: -92233.72036854775808, max: 92233.72036854775807 }, + { min: -9223.372036854775808, max: 9223.372036854775807 }, + { min: -922.3372036854775808, max: 922.3372036854775807 }, + { min: -92.23372036854775808, max: 92.23372036854775807 }, + { min: -9.223372036854775808, max: 9.223372036854775807 }, + ]; + + /** 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 configValue = this.extractValue(cur, 'config'); + const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false'; + + const extractRange = (min: number, max: number, property: string = 'range'): { expression: Expression<YangRange> | 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; + } else { + minValue = min, + maxValue = max; + } + + if (minValue > min) min = minValue; + if (maxValue < max) max = maxValue; + + return { + min: minValue, + max: maxValue, + }; + }); + return { + min: min, + max: max, + expression: range && range.length === 1 + ? range[0] + : range && range.length > 1 + ? { operation: 'OR', arguments: range } + : undefined, + }; + }; + + const extractPattern = (): Expression<RegExp> | undefined => { + // 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)) } + : undefined; + }; + + const mandatory = this.extractValue(cur, 'mandatory') === 'true' || false; + + if (!cur.arg) { + throw new Error(`Module: [${module.name}]. Found element without name.`); + } + + if (!type) { + throw new Error(`Module: [${module.name}].[${cur.arg}]. Found element without type.`); + } + + const element: ViewElementBase = { + id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg, + label: cur.arg, + path: currentPath, + module: module.name || '', + config: config, + mandatory: mandatory, + isList: isList, + default: defaultVal, + description: description, + }; + + if (type === 'string') { + const length = extractRange(0, +18446744073709551615, 'length'); + return ({ + ...element, + uiType: 'string', + length: length.expression, + pattern: extractPattern(), + }); + } else if (type === 'boolean') { + return ({ + ...element, + uiType: 'boolean', + }); + } else if (type === 'uint8') { + const range = extractRange(0, +255); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'uint16') { + const range = extractRange(0, +65535); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'uint32') { + const range = extractRange(0, +4294967295); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'uint64') { + const range = extractRange(0, +18446744073709551615); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'int8') { + const range = extractRange(-128, +127); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'int16') { + const range = extractRange(-32768, +32767); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'int32') { + const range = extractRange(-2147483648, +2147483647); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'int64') { + const range = extractRange(-9223372036854775808, +9223372036854775807); + return ({ + ...element, + uiType: 'number', + range: range.expression, + min: range.min, + max: range.max, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, + }); + } else if (type === 'decimal64') { + // decimalRange + 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', + fDigits: fDigits, + range: range.expression, + min: range.min, + max: range.max, + 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'); + return ({ + ...element, + 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 enumOption = { + key: enumNode.arg, + value: value != null ? value : enumNode.arg, + 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'); + if (!vPath) { + throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`); + } + const refPath = this.resolveReferencePath(vPath, module); + const resolve = this.resolveReference.bind(this); + const res: ViewElement = { + ...element, + uiType: 'reference', + referencePath: refPath, + ref(this: ViewElement, basePath: string) { + const elementPath = `${basePath}/${cur.arg}`; + + const result = resolve(refPath, elementPath); + if (!result) return undefined; + + const [resolvedElement, resolvedPath] = result; + return resolvedElement && [{ + ...resolvedElement, + id: this.id, + label: this.label, + config: this.config, + mandatory: this.mandatory, + isList: this.isList, + default: this.default, + description: this.description, + } as ViewElement, resolvedPath] || undefined; + }, + }; + return res; + } 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: [], + }; + this._identityToResolve.push(() => { + const identity: Identity = this.resolveIdentity(base, module); + if (!identity) { + throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Could not resolve identity [${base}].`); + } + if (!identity.values || identity.values.length === 0) { + throw new Error(`Identity: [${base}] has no values.`); + } + res.options = identity.values.map(val => ({ + key: val.id, + value: val.id, + description: val.description, + })); + }); + return res; + } 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', + }; + } 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 resultingElement = { + ...element, + uiType: 'union', + elements: [], + } as ViewElementUnion; + + const resolveUnion = () => { + resultingElement.elements.push(...typeNodes.map(node => { + const stm: Statement = { + ...cur, + sub: [ + ...(cur.sub?.filter(s => s.key !== 'type') || []), + node, + ], + }; + return { + ...this.getViewElement(stm, module, parentId, currentPath, isList), + id: node.arg!, + }; + })); + }; + + this._unionsToResolve.push(resolveUnion); + + return resultingElement; + } 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) => { + 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')); + acc[bitNode.arg] = pos === pos ? pos : undefined; + return acc; + }, {}), + }; + } else if (type === 'binary') { + return { + ...element, + uiType: 'binary', + length: extractRange(0, +18446744073709551615, 'length'), + }; + } else if (type === 'instance-identifier') { + // https://tools.ietf.org/html/rfc7950#page-168 + return { + ...element, + uiType: 'string', + length: extractRange(0, +18446744073709551615, 'length'), + }; + } else { + // not a build in type, need to resolve type + let typeRef = this.resolveType(type, module); + if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`)); + + if (isViewElementString(typeRef)) { + typeRef = this.resolveStringType(typeRef, extractPattern(), extractRange(0, +18446744073709551615)); + } else if (isViewElementNumber(typeRef)) { + typeRef = this.resolveNumberType(typeRef, extractRange(typeRef.min, typeRef.max)); + } + + const res = { + id: element.id, + } as ViewElement; + + this._typeRefToResolve.push(() => { + // spoof date type here from special string type + if ((type === 'date-and-time' || type.endsWith(':date-and-time')) && typeRef.module === 'ietf-yang-types') { + Object.assign(res, { + ...typeRef, + ...element, + description: description, + uiType: 'date', + }); + } else { + Object.assign(res, { + ...typeRef, + ...element, + description: description, + }); + } + }); + + return res; + } + } + + private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined; min: number; max: number }) { + return { + ...parentElement, + pattern: pattern != null && 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] } + : parentElement.length + ? parentElement.length + : length?.expression, + } as ViewElementString; + } + + private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined; min: number; max: number }) { + return { + ...parentElement, + range: range.expression != null && parentElement.range + ? { operation: 'AND', arguments: [range.expression, parentElement.range] } + : parentElement.range + ? parentElement.range + : range, + min: range.min, + max: range.max, + } as ViewElementNumber; + } + + private resolveReferencePath(vPath: string, module: Module) { + 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}`; + }); + } + + private resolveReference(vPath: string, currentPath: string) { + const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath + let element: ViewElement | null = null; + 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] }; }) + : []; + + for (let i = 0; i < vPathParts.length; ++i) { + const vPathPart = vPathParts[i]; + if (vPathPart.property === '..') { + resultPathParts.pop(); + } else if (vPathPart.property !== '.') { + resultPathParts.push(vPathPart); + } + } + + // resolve element by path + for (let j = 0; j < resultPathParts.length; ++j) { + const pathPart = resultPathParts[j]; + if (j === 0) { + moduleName = pathPart.ns; + const rootModule = this._modules[moduleName]; + 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); + } + 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('/')]; + } + + private resolveView(vPath: string) { + 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 = ''; + if (vPath) do { + partMatch = vPathParser.exec(vPath); + if (partMatch) { + if (element === null) { + moduleName = partMatch[1]!; + const rootModule = this._modules[moduleName]; + if (!rootModule) return null; + element = rootModule.elements[`${moduleName}:${partMatch[2]!}`]; + } else if (isViewElementObjectOrList(element)) { + view = this._views[+element.viewId]; + if (moduleName !== partMatch[1]) { + moduleName = partMatch[1]; + element = view.elements[`${moduleName}:${partMatch[2]}`]; + } else { + element = view.elements[partMatch[2]]; + } + } else { + return null; + } + if (!element) return null; + } + } while (partMatch); + return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null; + } + + private resolveType(type: string, module: Module) { + const colonInd = type.indexOf(':'); + const preFix = colonInd > -1 ? type.slice(0, colonInd) : ''; + const typeName = colonInd > -1 ? type.slice(colonInd + 1) : type; + + const res = preFix + ? this._modules[module.imports[preFix]].typedefs[typeName] + : module.typedefs[typeName]; + return res; + } + + private resolveGrouping(grouping: string, module: Module) { + const collonInd = grouping.indexOf(':'); + const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : ''; + const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping; + + return preFix + ? this._modules[module.imports[preFix]].groupings[groupingName] + : module.groupings[groupingName]; + + } + + private resolveIdentity(identity: string, module: Module) { + const collonInd = identity.indexOf(':'); + const preFix = collonInd > -1 ? identity.slice(0, collonInd) : ''; + const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity; + + return preFix + ? this._modules[module.imports[preFix]].identities[identityName] + : module.identities[identityName]; + + } +}
\ No newline at end of file diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/tsconfig.json b/sdnr/wt-odlux/odlux/apps/configurationApp/tsconfig.json new file mode 100644 index 000000000..c95005660 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "baseUrl": "./src", + "outDir": "./dist", + "sourceMap": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "strictNullChecks": true, + "pretty": true, + "newLine": "LF", + "module": "es2015", + "target": "es2016", + "moduleResolution": "node", + "experimentalDecorators": true, + "jsx": "preserve", + "lib": [ + "dom", + "es2015", + "es2016" + ], + "types": [ + "node", + "prop-types", + "react", + "react-dom" + ] + }, + "exclude": [ + "dist", + "node_modules" + ] +} diff --git a/sdnr/wt-odlux/odlux/apps/configurationApp/webpack.config.js b/sdnr/wt-odlux/odlux/apps/configurationApp/webpack.config.js new file mode 100644 index 000000000..2f7ab11b4 --- /dev/null +++ b/sdnr/wt-odlux/odlux/apps/configurationApp/webpack.config.js @@ -0,0 +1,150 @@ +/** + * Webpack 4 configuration file + * see https://webpack.js.org/configuration/ + * see https://webpack.js.org/configuration/dev-server/ + */ + +"use strict"; + +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'); + +// const __dirname = (path => path.replace(/^([a-z]\:)/, c => c.toUpperCase()))(process.__dirname()); + +module.exports = (env) => { + const distPath = path.resolve(__dirname, env === "release" ? "." : "../..", "dist"); + const frameworkPath = path.resolve(__dirname, env === "release" ? "../../framework" : "../..", "dist"); + return [{ + name: "App", + + mode: "none", //disable default behavior + + target: "web", + + context: path.resolve(__dirname, "src"), + + entry: { + configurationApp: ["./pluginConfiguration.tsx"] + }, + + devtool: env === "release" ? false : "source-map", + + resolve: { + extensions: [".ts", ".tsx", ".js", ".jsx"] + }, + + output: { + path: distPath, + filename: "[name].js", + library: "configurationApp", + libraryTarget: "umd2", + chunkFilename: "[name].js" + }, + module: { + rules: [{ + test: /\.tsx?$/, + exclude: /node_modules/, + use: [{ + loader: "babel-loader" + }, { + loader: "ts-loader" + }] + }, { + test: /\.jsx?$/, + exclude: /node_modules/, + use: [{ + loader: "babel-loader" + }] + },{ + //don't minify images + test: /\.(png|gif|jpg|svg)$/, + use: [{ + loader: 'url-loader', + options: { + limit: 10, + name: './images/[name].[ext]' + } + }] + }] + }, + + optimization: { + noEmitOnErrors: true, + namedModules: env !== "release", + minimize: env === "release", + minimizer: env !== "release" ? [] : [new TerserPlugin({ + terserOptions: { + warnings: false, // false, true, "verbose" + compress: { + drop_console: true, + drop_debugger: true, + } + } + })], + }, + + plugins: [ + new webpack.DllReferencePlugin({ + context: path.resolve(__dirname, "../../framework/src"), + manifest: require(path.resolve(frameworkPath, "vendor-manifest.json")), + sourceType: "umd2" + }), + new webpack.DllReferencePlugin({ + context: path.resolve(__dirname, "../../framework/src"), + manifest: require(path.resolve(frameworkPath, "app-manifest.json")), + sourceType: "umd2" + }), + ...(env === "release" ? [ + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: "'production'", + VERSION: JSON.stringify(require("./package.json").version) + } + }), + ] : [ + new webpack.DefinePlugin({ + "process.env": { + NODE_ENV: "'development'", + VERSION: JSON.stringify(require("./package.json").version) + } + }), + new CopyWebpackPlugin([{ + from: 'index.html', + to: distPath + }]), + ]) + ], + + watchOptions: { + ignored: /node_modules/ + }, + + devServer: { + contentBase: frameworkPath, + + compress: true, + headers: { + "Access-Control-Allow-Origin": "*" + }, + host: "0.0.0.0", + port: 3100, + disableHostCheck: true, + historyApiFallback: true, + inline: true, + hot: false, + quiet: false, + stats: { + colors: true + }, + before: function(app, server, compiler) { + app.get('/oauth/policies',(_, res) => res.json(policies)); + }, + proxy: proxyConf, + } + }]; +} |