summaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/apps/configurationApp
diff options
context:
space:
mode:
Diffstat (limited to 'sdnr/wt/odlux/apps/configurationApp')
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/.babelrc17
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/package.json40
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/pom.xml147
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts380
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts29
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts19
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts30
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts58
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts42
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/index.html27
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts19
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts142
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts44
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx77
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts38
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts25
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx464
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx47
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts1099
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java68
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml9
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java45
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src2/test/resources/test.js5
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/tsconfig.json38
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/webpack.config.js158
25 files changed, 3067 insertions, 0 deletions
diff --git a/sdnr/wt/odlux/apps/configurationApp/.babelrc b/sdnr/wt/odlux/apps/configurationApp/.babelrc
new file mode 100644
index 000000000..3d8cd1260
--- /dev/null
+++ b/sdnr/wt/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/apps/configurationApp/package.json b/sdnr/wt/odlux/apps/configurationApp/package.json
new file mode 100644
index 000000000..2985ba4fb
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/package.json
@@ -0,0 +1,40 @@
+{
+ "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": {
+ "@odlux/framework": "*"
+ },
+ "peerDependencies": {
+ "@types/react": "16.9.11",
+ "@types/react-dom": "16.9.4",
+ "@types/react-router-dom": "4.3.1",
+ "@material-ui/core": "4.6.1",
+ "@material-ui/icons": "4.5.1",
+ "@types/classnames": "2.2.6",
+ "@types/flux": "3.1.8",
+ "@types/jquery": "3.3.10",
+ "jquery": "3.3.1",
+ "react": "16.11.0",
+ "react-dom": "16.11.0",
+ "react-router-dom": "4.3.1"
+ }
+} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/pom.xml b/sdnr/wt/odlux/apps/configurationApp/pom.xml
new file mode 100644
index 000000000..a4e067873
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/pom.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<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">
+
+ <parent>
+ <groupId>org.onap.ccsdk.parent</groupId>
+ <artifactId>odlparent</artifactId>
+ <version>1.5.1-SNAPSHOT</version>
+ <relativePath/>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.onap.ccsdk.features.sdnr.wt</groupId>
+ <artifactId>sdnr-wt-odlux-app-configurationApp</artifactId>
+ <version>0.7.0-SNAPSHOT</version>
+ <packaging>bundle</packaging>
+ <name>sdnr-wt-odlux-app-configurationApp</name>
+ <licenses>
+ <license>
+ <name>Apache License, Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0</url>
+ </license>
+ </licenses>
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>sdnr-wt-odlux-core-model</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>sdnr-wt-odlux-core-provider</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <sourceDirectory>src2/main/java</sourceDirectory>
+ <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>org.codehaus.mojo</groupId>
+ <artifactId>build-helper-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>add-test-source</id>
+ <phase>generate-test-sources</phase>
+ <goals>
+ <goal>add-test-source</goal>
+ </goals>
+ <configuration>
+ <sources>
+ <source>src2/test/java</source>
+ </sources>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>de.jacks-it-lab</groupId>
+ <artifactId>frontend-maven-plugin</artifactId>
+ <version>1.7.1</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>v10.16.3</nodeVersion>
+ <yarnVersion>v1.19.0</yarnVersion>
+ </configuration>
+ </execution>
+ <execution>
+ <id>yarn build</id>
+ <goals>
+ <goal>yarn</goal>
+ </goals>
+ <configuration>
+ <arguments>run build</arguments>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Import-Package>org.onap.ccsdk.features.sdnr.wt.odlux.model.*,com.opensymphony.*</Import-Package>
+ <Private-Package/>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ <resources>
+ <resource>
+ <directory>dist</directory>
+ <targetPath>odlux</targetPath>
+ </resource>
+ <resource>
+ <directory>src2/main/resources</directory>
+ </resource>
+ <resource>
+ <directory>src2/test/resources</directory>
+ </resource>
+ </resources>
+ </build>
+</project>
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts
new file mode 100644
index 000000000..fc0665325
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts
@@ -0,0 +1,380 @@
+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 { restService } from "../services/restServices";
+import { YangParser } from "../yang/yangParser";
+import { Module } from "../models/yang";
+import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList } from "../models/uiModels";
+import { AddErrorInfoAction } from "../../../../framework/src/actions/errorActions";
+
+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 UpdatViewDescription extends Action {
+ constructor(public vPath: string, public view: ViewSpecification, public viewData: any, public displayAsList: boolean = false, public key?: string ) {
+ super();
+ }
+}
+
+export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState ) => {
+ const { configuration: { connectedNetworkElements : { rows }} } = getState();
+ dispatch(new SetCollectingSelectionData(true));
+ const networkElement = rows.find(r => r.nodeId === nodeId) || await restService.getMountedNetworkElementByMountId(nodeId);
+ if (!networkElement) {
+ console.error(new Error(`NetworkElement : [${nodeId}] does not exist.`));
+ return dispatch(new UpdateDeviceDescription("", { }, [ ]));
+ }
+
+ if (!networkElement.nodeDetails || !networkElement.nodeDetails.availableCapabilities) {
+ throw new Error(`NetworkElement : [${nodeId}] has no capabilities.`);
+ }
+ const parser = new YangParser();
+
+ const capParser = /^\(.*\?revision=(\d{4}-\d{2}-\d{2})\)(\S+)$/i;
+ for (let i = 0; i < networkElement.nodeDetails.availableCapabilities.length; ++i){
+ const capRaw = networkElement.nodeDetails.availableCapabilities[i];
+ const capMatch = capRaw && capParser.exec(capRaw);
+ try {
+ capMatch && await parser.addCapability(capMatch[2], capMatch[1]);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ parser.postProcess();
+
+ console.log(parser.modules, parser.views)
+
+ return dispatch(new UpdateDeviceDescription(nodeId, parser.modules, parser.views));
+}
+
+export const splitVPath = (vPath: string, vPathParser : RegExp): [string, string?][] => {
+ const pathParts: [string, string?][] = [];
+ let partMatch: RegExpExecArray | null;
+ if (vPath) do {
+ partMatch = vPathParser.exec(vPath);
+ if (partMatch) {
+ pathParts.push([partMatch[1], partMatch[2] || undefined]);
+ }
+ } while (partMatch)
+ return pathParts;
+}
+
+const getReferencedDataList = async (refPath: string, dataPath: string, modules: { [name: string]: Module }, views: ViewSpecification[]) => {
+ const pathParts = splitVPath(refPath, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property
+ let referencedModule = modules[pathParts[0][0]];
+
+ let dataMember: string;
+ let view: ViewSpecification;
+ let currentNS: string | null = null;
+ let dataUrls = [dataPath];
+ let data: any;
+
+ for (let i = 0; i < pathParts.length; ++i) {
+ const [pathPartNS, pathPart] = pathParts[i];
+ const namespace = pathPartNS != null ? (currentNS = pathPartNS) : currentNS;
+
+ const viewElement = i === 0
+ ? views[0].elements[`${referencedModule.name}:${pathPart}`]
+ : view!.elements[`${pathPart}`] || view!.elements[`${namespace}:${pathPart}`];
+
+ if (!viewElement) throw new Error(`Could not find ${pathPart} in ${refPath}`);
+ if (i < pathParts.length - 1) {
+ if (!isViewElementObjectOrList(viewElement)) {
+ throw Error(`Module: [${referencedModule.name}].[${viewElement.label}]. Viewelement is not list or object.`);
+ }
+ view = views[+viewElement.viewId];
+ const resultingDataUrls : string[] = [];
+ if (isViewElementList(viewElement)) {
+ for (let j = 0; j < dataUrls.length; ++j) {
+ const dataUrl = dataUrls[j];
+ const restResult = (await restService.getConfigData(dataUrl));
+ if (restResult.data == null || restResult.status < 200 || restResult.status > 299) {
+ 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[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 || restResult.status < 200 || restResult.status > 299) {
+ 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[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;
+}
+
+const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification, viewData: any, displayAsList: boolean = false, key?: string): UpdatViewDescription =>{
+
+ // check if-feature | when | and resolve all references.
+ view = { ...view };
+ view.elements = Object.keys(view.elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => {
+ const elm = view.elements[cur];
+ const key = defaultNS && cur.replace(new RegExp(`^${defaultNS}:`, "i"),"") || cur;
+ if (isViewElementReference(elm)) {
+ acc[key] = { ...(elm.ref(vPath) || elm), id: key };
+ } else {
+ acc[key] = { ...elm, id: key };
+ }
+ return acc;
+ }, {});
+ return new UpdatViewDescription(vPath, view, viewData, displayAsList, key);
+}
+
+export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => {
+ const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
+ const { configuration: { deviceDescription: { nodeId, modules, views } }, framework: { navigationState } } = getState();
+ let dataPath = `/restconf/config/network-topology:network-topology/topology/topology-netconf/node/${nodeId}/yang-ext:mount`;
+ 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];
+ 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;
+ }, {});
+ return dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, data, false, isViewElementList(viewElement!) && viewElement.key || undefined));
+ }
+ 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 {
+ dispatch(new SetCollectingSelectionData(false));
+ throw new Error("Found a list at root level of a module w/o a refenrece key.");
+ }
+ return;
+ }
+ extractList = true;
+ } else {
+ dataPath += `/${property}${key ? `/${key.replace(/\//ig, "%2F")}` : ""}`;
+ dataMember = namespace === defaultNS
+ ? viewElement.label
+ : `${namespace}:${viewElement.label}`;
+ extractList = false;
+ }
+
+ if (viewElement && "viewId" in viewElement) viewSpecification = views[+viewElement.viewId];
+ }
+
+ let data: any = {};
+ if (viewSpecification && viewSpecification.id !== "0") {
+ 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.`);
+ }
+ return dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, [], extractList, viewElement.key));
+ }
+ throw new Error(`Did not get response from Server. Status: [${restResult.status}]`);
+ } else if (restResult.status < 200 || restResult.status > 299) {
+ 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 {
+ data = restResult.data[dataMember!]; // extract dataMember
+ }
+
+ // extract the first element list[key]
+ data = data instanceof Array
+ ? data[0]
+ : data;
+
+ // extract the list -> key: list
+ data = extractList
+ ? data[viewElement!.label] || [] // if the list is empty, it does not exist
+ : data;
+ }
+
+ return dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, data, extractList, isViewElementList(viewElement!) && viewElement.key || undefined));
+ // https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]
+ // https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]/lp
+ } 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 = `/restconf/config/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];
+ 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
+ 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];
+ }
+ }
+
+ // 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, { [dataMember!]: data }); // extractDataMember
+ if (updateResult.status < 200 || updateResult.status > 299) {
+ 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 || ''}`);
+ }
+ }
+
+ return isNew
+ ? dispatch(new ReplaceAction(`/configuration/${nodeId}/${vPath.replace(/\[\]$/i,`[${isNew}]`)}`)) // navigate to new element
+ : dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, data, embedList, isViewElementList(viewElement!) && viewElement.key || undefined));
+ } catch (error) {
+ history.back();
+ dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` }));
+ dispatch(new SetCollectingSelectionData(false));
+ } finally {
+ return;
+ }
+} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts
new file mode 100644
index 000000000..04b63d39b
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts
@@ -0,0 +1,29 @@
+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/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts
new file mode 100644
index 000000000..6a68242eb
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts
@@ -0,0 +1,19 @@
+import { createExternal, IExternalTableState } from '../../../../framework/src/components/material-table/utilities';
+import { createSearchDataHandler } from '../../../../framework/src/utilities/elasticSearch';
+
+import { NetworkElementConnection } from '../models/networkElementConnection';
+
+export interface IConnectedNetworkElementsState extends IExternalTableState<NetworkElementConnection> { }
+
+// create eleactic search material data fetch handler
+const connectedNetworkElementsSearchHandler = createSearchDataHandler<NetworkElementConnection>('network-element-connection', { 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);
+ \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts
new file mode 100644
index 000000000..3cc27aa95
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts
@@ -0,0 +1,30 @@
+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/apps/configurationApp/src/handlers/valueSelectorHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts
new file mode 100644
index 000000000..c9e7dd2cc
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts
@@ -0,0 +1,58 @@
+import { IActionHandler } from "../../../../framework/src/flux/action";
+import { ViewSpecification } from "../models/uiModels";
+import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdatViewDescription } from "../actions/deviceActions";
+
+export interface IValueSelectorState {
+ collectingData: boolean;
+ keyProperty: string | undefined;
+ listSpecification: ViewSpecification | null;
+ listData: any[];
+ onValueSelected: (value: any) => void;
+}
+
+const nc = (val: React.SyntheticEvent) => { };
+const valueSelectorStateInit: IValueSelectorState = {
+ collectingData: false,
+ keyProperty: undefined,
+ listSpecification: null,
+ listData: [],
+ onValueSelected: nc,
+};
+
+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) {
+ state.keyProperty && state.onValueSelected(action.value[state.keyProperty]);
+ state = {
+ ...state,
+ collectingData: false,
+ keyProperty: undefined,
+ listSpecification: null,
+ onValueSelected: nc,
+ listData: [],
+ };
+ } else if (action instanceof UpdateDeviceDescription || action instanceof UpdatViewDescription) {
+ state = {
+ ...state,
+ collectingData: false,
+ keyProperty: undefined,
+ listSpecification: null,
+ onValueSelected: nc,
+ listData: [],
+ };
+ }
+ return state;
+};
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts
new file mode 100644
index 000000000..48155ee1e
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts
@@ -0,0 +1,42 @@
+import { IActionHandler } from "../../../../framework/src/flux/action";
+
+import { UpdatViewDescription } from "../actions/deviceActions";
+import { ViewSpecification } from "../models/uiModels";
+
+export interface IViewDescriptionState {
+ vPath: string | null;
+ keyProperty: string | undefined;
+ displayAsList: boolean;
+ viewSpecification: ViewSpecification;
+ viewData: any
+}
+
+const viewDescriptionStateInit: IViewDescriptionState = {
+ vPath: null,
+ keyProperty: undefined,
+ displayAsList: false,
+ viewSpecification: {
+ id: "empty",
+ canEdit: false,
+ parentView: "",
+ name: "emplty",
+ language: "en-US",
+ title: "empty",
+ elements: {}
+ },
+ viewData: null
+};
+
+export const viewDescriptionHandler: IActionHandler<IViewDescriptionState> = (state = viewDescriptionStateInit, action) => {
+ if (action instanceof UpdatViewDescription) {
+ state = {
+ ...state,
+ vPath: action.vPath,
+ keyProperty: action.key,
+ displayAsList: action.displayAsList,
+ viewSpecification: action.view,
+ viewData: action.viewData,
+ }
+ }
+ return state;
+};
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/index.html b/sdnr/wt/odlux/apps/configurationApp/src/index.html
new file mode 100644
index 000000000..759b7b535
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/index.html
@@ -0,0 +1,27 @@
+<!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"], function (app, connectApp, maintenanceApp, configurationApp) {
+ connectApp.register();
+ configurationApp.register();
+ maintenanceApp.register();
+ app("./app.tsx").runApplication();
+ });
+ </script>
+</body>
+
+</html> \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts
new file mode 100644
index 000000000..2575500a3
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts
@@ -0,0 +1,19 @@
+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/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts
new file mode 100644
index 000000000..441d1281d
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts
@@ -0,0 +1,142 @@
+export type ViewElementBase = {
+ "id": string;
+ "label": string;
+ "config": boolean;
+ "ifFeature"?: string;
+ "when"?: string;
+ "mandatory"?: boolean;
+ "description"?: string;
+ "isList"?: boolean;
+ "default"?: string;
+ "status"?: "current" | "deprecated" | "obsolete",
+ "reference"?: string, // https://tools.ietf.org/html/rfc7950#section-7.21.4
+}
+
+// https://tools.ietf.org/html/rfc7950#section-9.8
+export type ViewElementBinary = ViewElementBase & {
+ "uiType": "binary";
+ "length"?: number; // 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"?: string[];
+ "length"?: string;
+ "invertMatch"?: true;
+}
+
+// https://tools.ietf.org/html/rfc7950#section-9.3
+export type ViewElementNumber = ViewElementBase & {
+ "uiType": "number";
+ "min"?: number;
+ "max"?: number;
+ "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 | null;
+}
+
+export type ViewElement =
+ | ViewElementBits
+ | ViewElementBinary
+ | ViewElementString
+ | ViewElementNumber
+ | ViewElementBoolean
+ | ViewElementObject
+ | ViewElementList
+ | ViewElementSelection
+ | ViewElementReference;
+
+export const isViewElementString = (viewElement: ViewElement): viewElement is ViewElementString => {
+ return viewElement && viewElement.uiType === "string";
+}
+
+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 type ViewSpecification = {
+ "id": string;
+ "name": string;
+ "title"?: string;
+ "parentView"?: string;
+ "language": string;
+ "ifFeature"?: string;
+ "when"?: string;
+ "uses"?: string[];
+ "elements": { [name: string]: ViewElement };
+ readonly "canEdit": boolean;
+}
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts
new file mode 100644
index 000000000..57edf803f
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts
@@ -0,0 +1,44 @@
+import { ViewElement, ViewSpecification } from "./uiModels";
+
+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;
+ 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 };
+} \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx
new file mode 100644
index 000000000..7fd3a97f9
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx
@@ -0,0 +1,77 @@
+import * as React from "react";
+import { withRouter, RouteComponentProps, Route, Switch, Redirect } from 'react-router-dom';
+
+import { faAdjust } from '@fortawesome/free-solid-svg-icons'; // select app icon
+
+import connect, { Connect, IDispatcher } from '../../../framework/src/flux/connect';
+import applicationManager from '../../../framework/src/services/applicationManager';
+import { IApplicationStoreState } from "../../../framework/src/store/applicationStore";
+import { configurationAppRootHandler } from "./handlers/configurationAppRootHandler";
+import { NetworkElementSelector } from "./views/networkElementSelector";
+
+import ConfigurationApplication from "./views/configurationApplication";
+import { updateNodeIdAsyncActionCreator, updateViewActionAsyncCreator } from "./actions/deviceActions";
+
+let currentNodeId: string | null | undefined = undefined;
+let currentVirtualPath: string | null | undefined = undefined;
+let lastUrl: string | undefined = undefined;
+
+const mapProps = (state: IApplicationStoreState) => ({
+ // currentProblemsProperties: createCurrentProblemsProperties(state),
+});
+
+const mapDisp = (dispatcher: IDispatcher) => ({
+ updateNodeId: (nodeId: string) => dispatcher.dispatch(updateNodeIdAsyncActionCreator(nodeId)),
+ updateView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)),
+});
+
+const ConfigurationApplicationRouteAdapter = connect(mapProps, mapDisp)((props: RouteComponentProps<{ nodeId?: string, 0: string }> & Connect<typeof mapProps, typeof mapDisp>) => {
+ React.useEffect(() => {
+ return () => {
+ lastUrl = undefined;
+ currentNodeId = undefined;
+ currentVirtualPath = undefined;
+ }
+ },[]);
+ if (props.location.pathname !== lastUrl) {
+ // ensure the asynchronus update will only be called once per path
+ lastUrl = props.location.pathname;
+ window.setTimeout(async () => {
+
+ // check if the nodeId has changed
+ if (currentNodeId !== props.match.params.nodeId) {
+ currentNodeId = props.match.params.nodeId || undefined;
+ currentVirtualPath = null;
+ currentNodeId && await props.updateNodeId(currentNodeId);
+ }
+
+ if (currentVirtualPath !== props.match.params[0]) {
+ currentVirtualPath = props.match.params[0];
+ await props.updateView(currentVirtualPath);
+ }
+
+ });
+ }
+ 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: faAdjust,
+ rootComponent: App,
+ rootActionHandler: configurationAppRootHandler,
+ menuEntry: "Configuration"
+ });
+}
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts
new file mode 100644
index 000000000..061be05ec
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts
@@ -0,0 +1,38 @@
+import { requestRest, requestRestExt } from "../../../../framework/src/services/restService";
+import { convertPropertyNames, replaceHyphen } from "../../../../framework/src/utilities/yangHelper";
+
+import { NetworkElementConnection } from "../models/networkElementConnection";
+
+class RestService {
+ 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 = "/restconf/operations/data-provider:read-network-element-connection-list";
+ const body = { "input": { "filter": [{ "property": "node-id", "filtervalue": nodeId }], "sortorder": [], "pagination": { "size": 1, "page": 1 } } };
+ const networkElementResult = await requestRest<{ output: { data: NetworkElementConnection[] } }>(path, { method: "POST", body: JSON.stringify(body) });
+ return networkElementResult && networkElementResult.output && networkElementResult.output.data &&
+ networkElementResult.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) });
+ }
+ }
+
+export const restService = new RestService();
+export default restService; \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts
new file mode 100644
index 000000000..17a4e43a7
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts
@@ -0,0 +1,25 @@
+type YangInfo = [string, (string | null | undefined)];
+
+const cache: { [path: string]: string } = {
+
+};
+
+class YangService {
+
+ public async getCapability(capability: string, version?: string) {
+ const url = `/yang-schema/${capability}${version ? `/${version}` : ""}`;
+
+ 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 = new YangService();
+export default yangService; \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx
new file mode 100644
index 000000000..24a4af8b2
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx
@@ -0,0 +1,464 @@
+/**
+ * ============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 { RouteComponentProps, withRouter } from 'react-router-dom';
+
+import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles';
+
+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 { SetSelectedValue, splitVPath, updateDataActionAsyncCreator } from "../actions/deviceActions";
+import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection } from "../models/uiModels";
+
+import Fab from '@material-ui/core/Fab';
+import AddIcon from '@material-ui/icons/Add';
+import RemoveIcon from '@material-ui/icons/RemoveCircleOutline';
+import SaveIcon from '@material-ui/icons/Save';
+import EditIcon from '@material-ui/icons/Edit';
+import Tooltip from "@material-ui/core/Tooltip";
+import TextField from "@material-ui/core/TextField";
+import FormControl from "@material-ui/core/FormControl";
+import IconButton from "@material-ui/core/IconButton";
+import Button from "@material-ui/core/Button";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import InputLabel from "@material-ui/core/InputLabel";
+import Select from "@material-ui/core/Select";
+import MenuItem from "@material-ui/core/MenuItem";
+import Breadcrumbs from "@material-ui/core/Breadcrumbs";
+import Link from "@material-ui/core/Link";
+import FormHelperText from '@material-ui/core/FormHelperText';
+
+const styles = (theme: Theme) => createStyles({
+ header: {
+ "display": "flex",
+ "justifyContent": "space-between",
+ },
+ leftButton: {
+ "justifyContent": "left"
+ },
+ outer: {
+ "flex": "1",
+ "heigh": "100%",
+ "display": "flex",
+ "alignItems": "center",
+ "justifyContent": "center",
+ },
+ inner: {
+
+ },
+ "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',
+ },
+ },
+ },
+});
+
+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,
+ viewSpecification: state.configuration.viewDescription.viewSpecification,
+ displayAsList: state.configuration.viewDescription.displayAsList,
+ keyProperty: state.configuration.viewDescription.keyProperty,
+});
+
+const mapDispatch = (dispatcher: IDispatcher) => ({
+ onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
+ onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(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;
+}
+
+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
+ }
+ }
+
+ 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,
+ }
+ 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 renderUIElement = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
+ const elements = viewSpecification.elements;
+ return (
+ Object.keys(elements).sort((a, b) => {
+ const vsA = elements[a];
+ const vsB = elements[b];
+ 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;
+ }).map(key => {
+ const uiElement = elements[key];
+ const isKey = (uiElement.label === keyProperty);
+ const canEdit = editMode && (isNew || (uiElement.config && !isKey));
+ if (isViewElementSelection(uiElement)) {
+ let error = ""
+ const value = String(viewData[uiElement.id]).toLowerCase();
+ if (uiElement.mandatory && !!value) {
+ error = "Error";
+ }
+ return (canEdit || viewData[uiElement.id] != null
+ ? (<FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
+ <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
+ <Select
+ required={!!uiElement.mandatory}
+ error={!!error}
+ onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+ readOnly={!canEdit}
+ disabled={editMode && !canEdit}
+ value={(viewData[uiElement.id] || '').toString().toLowerCase()}
+ inputProps={{
+ name: uiElement.id,
+ id: `select-${uiElement.id}`,
+ }}
+ >
+ {uiElement.options.map(option => (<MenuItem key={option.key} title={option.description} value={option.value}>{option.key}</MenuItem>))}
+ </Select>
+ <FormHelperText>{error}</FormHelperText>
+ </FormControl>)
+ : null
+ );
+ } else if (isViewElementBoolean(uiElement)) {
+ let error = ""
+ const value = String(viewData[uiElement.id]).toLowerCase();
+ if (uiElement.mandatory && value !== "true" && value !== "false") {
+ error = "Error";
+ }
+ return (canEdit || viewData[uiElement.id] != null
+ ? (<FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
+ <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
+ <Select
+ required={!!uiElement.mandatory}
+ error={!!error}
+ onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+ readOnly={!canEdit}
+ disabled={editMode && !canEdit}
+ value={value}
+ inputProps={{
+ name: uiElement.id,
+ id: `select-${uiElement.id}`,
+ }}
+ >
+ <MenuItem value={'true'}>{uiElement.trueValue || 'True'}</MenuItem>
+ <MenuItem value={'false'}>{uiElement.falseValue || 'False'}</MenuItem>
+
+ </Select>
+ <FormHelperText>{error}</FormHelperText>
+ </FormControl>)
+ : null
+ );
+ } else if (isViewElementString(uiElement)) {
+ return (
+ <Tooltip key={uiElement.id} title={uiElement.description || ''}>
+ <TextField InputProps={{ readOnly: !canEdit, disabled: editMode && !canEdit }} spellCheck={false} autoFocus margin="dense"
+ id={uiElement.id} label={isKey ? "🔑 " + uiElement.label : uiElement.label} type="text" value={viewData[uiElement.id] || ''}
+ style={{ width: 485, marginLeft: 20, marginRight: 20 }}
+ onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+ />
+ </Tooltip>
+ );
+ } else if (isViewElementNumber(uiElement)) {
+ return (
+ <Tooltip key={uiElement.id} title={uiElement.description || ''}>
+ <TextField InputProps={{ readOnly: !canEdit, disabled: editMode && !canEdit, startAdornment: uiElement.units != null ? <InputAdornment position="start">{uiElement.units}</InputAdornment> : undefined }} spellCheck={false} autoFocus margin="dense"
+ id={uiElement.id} label={uiElement.label} type="text" value={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
+ style={{ width: 485, marginLeft: 20, marginRight: 20 }}
+ onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+ />
+ </Tooltip>
+ );
+ } else 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 type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
+ }
+ return null;
+ }
+ })
+ );
+ };
+
+ private renderUIElementList(listSpecification: ViewSpecification, listKeyProperty: string, listData: { [key: string]: any }[]) {
+ const listElements = listSpecification.elements;
+
+ const navigate = (path: string) => {
+ this.props.history.push(`${this.props.match.url}${path}`);
+ };
+
+ const addNewElementAction = {
+ icon: AddIcon, tooltip: 'Add', onClick: () => {
+ navigate("[]"); // empty key means new element
+ }
+ };
+
+ const { classes } = this.props;
+
+ return (
+ <SelectElementTable idProperty={listKeyProperty} rows={listData} customActionButtons={[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({ 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;
+ }, []).concat([{
+ property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (row => {
+ return (
+ <Tooltip title={"Remove"} >
+ <IconButton className={classes.button} onClick={event => {
+
+ }} >
+ <RemoveIcon />
+ </IconButton>
+ </Tooltip>
+ )
+ })
+ }])
+ } onHandleClick={(ev, row) => {
+ ev.preventDefault();
+ navigate(`[${row[listKeyProperty]}]`);
+ }} ></SelectElementTable>
+ );
+ }
+
+ private renderBreadCrumps() {
+ const { editMode } = this.state;
+ const { viewSpecification, displayAsList } = this.props;
+ const { vPath, match: { url, path }, 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="breadcrumb">
+ <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+ ev.preventDefault();
+ this.props.history.push(lastPath);
+ }}>Back</Link>
+ <Link color="inherit" href="#" 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 ret = (
+ <span key={ind}>
+ <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+ ev.preventDefault();
+ this.props.history.push(path);
+ }}><span>{prop.replace(/^[^:]+:/, "")}</span></Link>
+ {
+ keyPath && <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+ ev.preventDefault();
+ this.props.history.push(keyPath);
+ }}>{`[${key}]`}</Link> || null
+ }
+ </span>
+ );
+ lastPath = basePath;
+ basePath = keyPath || path;
+ return ret;
+ })
+ }
+ </Breadcrumbs>
+ </div>
+ { /* do not show edit if this is a list or it can't be edited */
+ !displayAsList && viewSpecification.canEdit && (<div>
+ <Fab color="secondary" aria-label="edit" className={this.props.classes.fab} onClick={() => {
+ if (this.state.editMode) {
+ this.props.onUpdateData(this.props.vPath!, this.state.viewData);
+ }
+ 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>
+ <SelectElementTable idProperty={listKeyProperty} rows={listData} columns={
+ Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
+ const elm = listSpecification.elements[cur];
+ if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
+ if (elm.label !== listKeyProperty) {
+ acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
+ } else {
+ acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
+ }
+ }
+ return acc;
+ }, [])
+ } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
+ </div>
+ );
+ }
+
+ private renderValueEditor() {
+ const { keyProperty, displayAsList, viewSpecification } = this.props;
+ const { viewData, editMode, isNew } = this.state;
+
+ return (
+ <div>
+ { this.renderBreadCrumps() }
+ { displayAsList && viewData instanceof Array
+ ? this.renderUIElementList(viewSpecification, keyProperty!, viewData)
+ : this.renderUIElement(viewSpecification, viewData!, keyProperty, editMode, isNew)
+ }
+ </div >
+ );
+ }
+
+ private renderCollectingData() {
+ return (
+ <div className={this.props.classes.outer}>
+ <div className={this.props.classes.inner}>
+ <Loader />
+ <h3>Collecting Data ...</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/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx
new file mode 100644
index 000000000..6fd5c8cf0
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx
@@ -0,0 +1,47 @@
+import * as React from 'react';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+
+import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect";
+import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
+import { MaterialTable, MaterialTableCtorType, ColumnType } from "../../../../framework/src/components/material-table";
+import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from "../../../configurationApp/src/handlers/connectedNetworkElementsHandler";
+
+import { NetworkElementConnection } from "../models/networkElementConnection";
+import { Tooltip, Button, IconButton } from "@material-ui/core";
+
+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>;
+
+class NetworkElementSelectorComponent extends React.Component<NetworkElementSelectorComponentProps> {
+
+ componentDidMount() {
+ this.props.connectedNetworkElementsActions.onRefresh();
+ }
+
+ render() {
+ return (
+ <ConnectedElementTable onHandleClick={(e, row) => { this.props.history.push(`${ this.props.match.path }/${row.nodeId}`) }} columns={[
+ { property: "nodeId", title: "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/apps/configurationApp/src/yang/yangParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts
new file mode 100644
index 000000000..c7ab5e4a3
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts
@@ -0,0 +1,1099 @@
+
+import { Token, Statement, Module, Identity } from "../models/yang";
+import { ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase, isViewElementReference } from "../models/uiModels";
+import { yangService } from "../services/yangService";
+
+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++;
+ }
+
+ const tok = {
+ name: 'IDENTIFIER',
+ 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)) {
+ 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: (() => void)[] = [];
+ private _identityToResolve: (() => void)[] = [];
+
+ private _modules: { [name: string]: Module } = {};
+ private _views: ViewSpecification[] = [{
+ id: "0",
+ name: "root",
+ language: "en-US",
+ canEdit: false,
+ parentView: "0",
+ title: "root",
+ elements: {},
+ }];
+
+ constructor() {
+
+ }
+
+ public get modules() {
+ return this._modules;
+ }
+
+ public get views() {
+ return this._views;
+ }
+
+ public async addCapability(capability: string, version?: string) {
+ // do not add twice
+ if (this._modules[capability]) {
+ return;
+ }
+
+ const data = await yangService.getCapability(capability, version);
+ if (!data) {
+ throw new Error(`Could not load yang file for ${capability}.`);
+ }
+
+ 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 module = this._modules[capability] = {
+ name: rootStatement.arg,
+ revisions: {},
+ imports: {},
+ features: {},
+ identities: {},
+ augments: {},
+ groupings: {},
+ typedefs: {},
+ views: {},
+ elements: {}
+ };
+
+ 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 revisions
+ const revisions = this.extractNodes(rootStatement, "revision");
+ module.revisions = {
+ ...module.revisions,
+ ...revisions.reduce<{ [version: string]: { }}>((acc, version) => {
+ if (!version.arg) {
+ throw new Error(`Module [${module.name}] has a version w/o version number.`);
+ }
+ const description = this.extractValue(version, "description");
+ const reference = this.extractValue(version,"reference");
+ acc[version.arg] = {
+ description,
+ reference,
+ };
+ return acc;
+ }, {})
+ };
+
+ // extract features
+ const features = this.extractNodes(rootStatement, "feature");
+ 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
+ if (imports) for (let ind = 0; ind < imports.length; ++ind) {
+ await this.addCapability(imports[ind].arg!);
+ }
+
+ this.extractTypeDefinitions(rootStatement, module, "");
+
+ this.extractIdentites(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;
+ Object.keys(module.elements).forEach(key => {
+ const viewElement = module.elements[key];
+ if (!isViewElementObjectOrList(viewElement)) {
+ throw new Error(`Module: [${module}]. Only List or Object allowed on root level.`);
+ }
+ const viewIdIndex = Number(viewElement.viewId);
+ module.views[key] = this._views[viewIdIndex];
+ this._views[0].elements[key] = module.elements[key];
+ });
+ return module;
+ }
+
+ public postProcess() {
+ // process all groupings
+ // execute all post processes like resolving in propper order
+ this._groupingsToResolve.forEach(cb => {
+ try { cb(); } catch (error) {
+ console.warn(`Error resolving: [${error.message}]`);
+ }
+ });
+
+ // process all augmentations
+ Object.keys(this.modules).forEach(modKey => {
+ const module = this.modules[modKey];
+ Object.keys(module.augments).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];
+ viewSpec.elements[key] = {
+ ...augment.elements[key],
+ when: elm.when ? `(${augment.when}) and (${elm.when})` : augment.when,
+ ifFeature: elm.ifFeature ? `(${augment.ifFeature}) and (${elm.ifFeature})` : augment.ifFeature,
+ };
+ }));
+ }
+ });
+ });
+
+ // 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 baseIdentites: 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 {
+ baseIdentites.push(identity);
+ }
+ });
+ });
+ baseIdentites.forEach(identity => {
+ identity.values = identity.children && traverseIdentity(identity.children) || [];
+ });
+
+ this._identityToResolve.forEach(cb => {
+ try { cb(); } catch (error) {
+ console.warn(error.message);
+ }
+ });
+ };
+
+
+ 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 typefed without name.`);
+ }
+ module.typedefs[def.arg] = this.getViewElement(def, module, 0, currentPath, false);
+ });
+ }
+
+ /** Handles Goupings 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, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
+ grouping && (module.groupings[grouping] = currentView);
+ acc.push(currentView, ...subViews);
+ return acc;
+ }, []));
+ }
+
+ return subViews;
+ }
+
+ /** Handles Augmants 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, subViews] = this.extractSubViews(cur, parentId, module, currentPath);
+ if (augment) {
+ module.augments[augment] = module.augments[augment] || [];
+ module.augments[augment].push(currentView);
+ }
+ acc.push(currentView, ...subViews);
+ return acc;
+ }, []));
+ }
+
+ return subViews;
+ }
+
+ /** Handles Identities */
+ private extractIdentites(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 identiy 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;
+ }, {});
+ }
+
+ private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] {
+ const subViews: ViewSpecification[] = [];
+ const currentId = this.nextId;
+ 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");
+
+ // 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: [${module.name}]. Found container without name.`);
+ }
+ const [currentView, subViews] = this.extractSubViews(cur, currentId, module, `${currentPath}/${module.name}:${cur.arg}`);
+ elements.push({
+ id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
+ label: cur.arg,
+ uiType: "object",
+ viewId: currentView.id,
+ config: config
+ });
+ acc.push(currentView, ...subViews);
+ 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) => {
+ if (!cur.arg) {
+ throw new Error(`Module: [${module.name}]. Found list without name.`);
+ }
+ const key = this.extractValue(cur, "key") || undefined;
+ if (config && !key) {
+ throw new Error(`Module: [${module.name}]. Found configurable list without key.`);
+ }
+ const [currentView, subViews] = this.extractSubViews(cur, currentId, module, `${currentPath}/${module.name}:${cur.arg}`);
+ elements.push({
+ id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg,
+ label: cur.arg,
+ isList: true,
+ uiType: "object",
+ viewId: currentView.id,
+ key: key,
+ config: config
+ });
+ acc.push(currentView, ...subViews);
+ 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, module, 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, module, parentId, currentPath, false);
+ element && acc.push(element);
+ return acc;
+ }, []));
+ }
+
+
+ const choiceStms = this.extractNodes(statement, "choice");
+ if (choiceStms && choiceStms.length > 0) {
+ for (let i = 0; i < choiceStms.length; ++i) {
+ const cases = this.extractNodes(choiceStms[i], "case");
+ console.warn(`Choice found ${choiceStms[i].arg}::${cases.map(c => c.arg).join(";")}`, choiceStms[i]);
+ }
+ }
+
+ const rpcs = this.extractNodes(statement, "rpc");
+ if (rpcs && rpcs.length > 0) {
+ // todo:
+ }
+
+ if (!statement.arg) {
+ throw new Error(`Module: [${module.name}]. Found statement without name.`);
+ }
+
+ const viewSpec: ViewSpecification = {
+ id: String(currentId),
+ parentView: String(parentId),
+ name: statement.arg,
+ title: statement.arg,
+ language: "en-us",
+ canEdit: false,
+ ifFeature: ifFeature,
+ when: whenCondition,
+ 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 || []);
+ for (let i = 0; i < usesRefs.length; ++i) {
+ const groupingName = usesRefs[i].arg;
+ if (!groupingName) {
+ throw new Error(`Module: [${module.name}]. Found an uses statement without a grouping name.`);
+ }
+
+ viewSpec.uses.push(this.resolveReferencePath(groupingName, module));
+
+ this._groupingsToResolve.push(() => {
+ const groupingViewSpec = this.resolveGrouping(groupingName, module);
+ if (groupingViewSpec) {
+ Object.keys(groupingViewSpec.elements).forEach(key => {
+ const elm = groupingViewSpec.elements[key];
+ viewSpec.elements[key] = {
+ ...groupingViewSpec.elements[key],
+ when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when,
+ ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature,
+ };
+ });
+ }
+ });
+ }
+ }
+
+ return [viewSpec, subViews];
+ }
+
+ /** 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 rangeMatch = this.extractValue(cur, "range", /^(\d+)\.\.(\d+)/) || undefined;
+
+ const configValue = this.extractValue(cur, "config");
+ const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
+
+ 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,
+ config: config,
+ mandatory: mandatory,
+ isList: isList,
+ default: defaultVal,
+ description: description
+ };
+
+ if (type === "string") {
+ return ({
+ ...element,
+ uiType: "string",
+ pattern: this.extractNodes(this.extractNodes(cur, "type")[0]!, "pattern").map(p => p.arg!).filter(p => !!p),
+ });
+ } else if (type === "boolean") {
+ return ({
+ ...element,
+ uiType: "boolean"
+ });
+ } else if (type === "uint8") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +255,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "uint16") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +65535,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "uint32") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +4294967295,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "uint64") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +18446744073709551615,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "int8") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : -128,
+ max: rangeMatch ? Number(rangeMatch[1]) : +127,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "int16") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : -32768,
+ max: rangeMatch ? Number(rangeMatch[1]) : +32767,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "int32") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : -2147483648,
+ max: rangeMatch ? Number(rangeMatch[1]) : +2147483647,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "int64") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +18446744073709551615,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ });
+ } else if (type === "decimal16") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +18446744073709551615,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ fDigits: Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1
+ });
+ } else if (type === "decimal32") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +18446744073709551615,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ fDigits: Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1
+ });
+ } else if (type === "decimal64") {
+ return ({
+ ...element,
+ uiType: "number",
+ min: rangeMatch ? Number(rangeMatch[0]) : 0,
+ max: rangeMatch ? Number(rangeMatch[1]) : +18446744073709551615,
+ units: this.extractValue(cur, "units") || undefined,
+ format: this.extractValue(cur, "format") || undefined,
+ fDigits: Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1
+ });
+ } 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, currentPath: string) {
+ const resolved = resolve(refPath, currentPath);
+ return resolved && {
+ ...resolved,
+ id: this.id,
+ label: this.label,
+ config: this.config,
+ mandatory: this.mandatory,
+ isList: this.isList,
+ default: this.default,
+ description: this.description,
+ } as ViewElement;
+ }
+ };
+ 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. */
+ console.warn(`found type: empty in [${module.name}][${currentPath}][${element.label}]`);
+ return {
+ ...element,
+ uiType: "string",
+ };
+ } else if (type === "union") {
+ // todo: ❗ handle union ⚡
+ /* 9.12. The union Built-In Type */
+ console.warn(`found type: union in [${module.name}][${currentPath}][${element.label}]`);
+ return {
+ ...element,
+ uiType: "string",
+ };
+ } 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") {
+ const typeNode = this.extractNodes(cur, "type")[0]!;
+ const length = Number(this.extractValue(typeNode, "length"));
+ return {
+ ...element,
+ uiType: "binary",
+ length: length === length ? length : undefined
+ };
+ } else {
+ // not a build in type, have to resolve type
+ const typeRef = this.resolveType(type, module);
+ if (typeRef == null) console.error(new Error(`Could not resolve type ${type} in [${module.name}][${currentPath}].`));
+ return ({
+ ...typeRef,
+ ...element,
+ description: description
+ }) as ViewElement;
+ }
+ }
+
+ 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 => ({ ns: p[1], 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[`${moduleName}:${pathPart.property}`];
+ } else {
+ 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);
+ }
+
+ return element;
+ }
+
+ 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 collonInd = type.indexOf(":");
+ const preFix = collonInd > -1 ? type.slice(0, collonInd) : "";
+ const typeName = collonInd > -1 ? type.slice(collonInd + 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/apps/configurationApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java b/sdnr/wt/odlux/apps/configurationApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java
new file mode 100644
index 000000000..1e882fc69
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src2/main/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/MyOdluxBundle.java
@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt
+ * =================================================================================================
+ * 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==========================================================================
+ ******************************************************************************/
+package org.onap.ccsdk.features.sdnr.wt.odlux.bundles;
+
+import org.onap.ccsdk.features.sdnr.wt.odlux.model.bundles.OdluxBundle;
+import org.onap.ccsdk.features.sdnr.wt.odlux.model.bundles.OdluxBundleLoader;
+
+public class MyOdluxBundle extends OdluxBundle {
+
+ @Override
+ public void initialize() {
+ super.initialize();
+ }
+
+ @Override
+ public void clean() {
+ super.clean();
+ }
+
+ @Override
+ public String getResourceFileContent(String filename) {
+ return super.getResourceFileContent(filename);
+ }
+
+ @Override
+ public boolean hasResource(String filename) {
+ return super.hasResource(filename);
+ }
+
+ @Override
+ public void setBundleName(String bundleName) {
+ super.setBundleName(bundleName);
+ }
+
+ @Override
+ public void setLoader(OdluxBundleLoader loader) {
+ super.setLoader(loader);
+ }
+
+ @Override
+ public String getBundleName() {
+ return super.getBundleName();
+ }
+
+ @Override
+ public OdluxBundleLoader getLoader() {
+ return super.getLoader();
+ }
+
+ public MyOdluxBundle() {
+ super();
+ }
+}
diff --git a/sdnr/wt/odlux/apps/configurationApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml b/sdnr/wt/odlux/apps/configurationApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml
new file mode 100644
index 000000000..48f7dde3d
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src2/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -0,0 +1,9 @@
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0">
+ <reference id="loadersvc" availability="mandatory" activation="eager" interface="org.onap.ccsdk.features.sdnr.wt.odlux.model.bundles.OdluxBundleLoader"/>
+
+ <bean id="bundle" init-method="initialize" destroy-method="clean" class="org.onap.ccsdk.features.sdnr.wt.odlux.bundles.MyOdluxBundle">
+ <property name="loader" ref="loadersvc"/>
+ <property name="bundleName" value="configurationApp"/>
+ <property name="index" value="30"/>
+ </bean>
+</blueprint>
diff --git a/sdnr/wt/odlux/apps/configurationApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java b/sdnr/wt/odlux/apps/configurationApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java
new file mode 100644
index 000000000..edf68e096
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src2/test/java/org/onap/ccsdk/features/sdnr/wt/odlux/bundles/test/TestBundleRes.java
@@ -0,0 +1,45 @@
+/*******************************************************************************
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt
+ * =================================================================================================
+ * 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==========================================================================
+ ******************************************************************************/
+package org.onap.ccsdk.features.sdnr.wt.odlux.bundles.test;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.onap.ccsdk.features.sdnr.wt.odlux.OdluxBundleLoaderImpl;
+import org.onap.ccsdk.features.sdnr.wt.odlux.bundles.MyOdluxBundle;
+
+public class TestBundleRes {
+
+ @Test
+ public void test() {
+ OdluxBundleLoaderImpl loader = OdluxBundleLoaderImpl.getInstance();
+ MyOdluxBundle b = new MyOdluxBundle();
+ b.setLoader(loader);
+ b.setIndex(0);
+ b.setBundleName("abc");
+ b.initialize();
+ assertTrue(loader.getNumberOfBundles()==1);
+ assertNotNull(b.getLoader());
+ assertEquals("abc",b.getBundleName());
+ assertTrue(b.hasResource("test.js"));
+ assertNotNull(b.getResourceFileContent("test.js"));
+ b.clean();
+ assertTrue(loader.getNumberOfBundles()==0);
+ }
+
+}
diff --git a/sdnr/wt/odlux/apps/configurationApp/src2/test/resources/test.js b/sdnr/wt/odlux/apps/configurationApp/src2/test/resources/test.js
new file mode 100644
index 000000000..b47fdc39f
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/src2/test/resources/test.js
@@ -0,0 +1,5 @@
+asdac sad
+as
+d
+sad
+ sadfa \ No newline at end of file
diff --git a/sdnr/wt/odlux/apps/configurationApp/tsconfig.json b/sdnr/wt/odlux/apps/configurationApp/tsconfig.json
new file mode 100644
index 000000000..b0c9b424d
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/tsconfig.json
@@ -0,0 +1,38 @@
+{
+ "compilerOptions": {
+ "baseUrl": "./src",
+ "outDir": "./dist",
+ "sourceMap": true,
+ "forceConsistentCasingInFileNames": true,
+ "allowSyntheticDefaultImports": false,
+ "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/apps/configurationApp/webpack.config.js b/sdnr/wt/odlux/apps/configurationApp/webpack.config.js
new file mode 100644
index 000000000..6a5c5d55e
--- /dev/null
+++ b/sdnr/wt/odlux/apps/configurationApp/webpack.config.js
@@ -0,0 +1,158 @@
+/**
+ * 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 __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: "[name]",
+ 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"
+ }]
+ }]
+ },
+
+ 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
+ }]),
+ ]
+ ],
+
+ devServer: {
+ public: "http://localhost:3100",
+ 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
+ },
+ proxy: {
+ "/oauth2/": {
+ target: "http://10.20.6.29:48181",
+ secure: false
+ },
+ "/yang-schema/": {
+ target: "http://10.20.6.29:48181",
+ secure: false
+ },
+ "/database/": {
+ target: "http://10.20.6.29:48181",
+ secure: false
+ },
+ "/restconf/": {
+ target: "http://10.20.6.29:48181",
+ secure: false
+ },
+ "/help/": {
+ target: "http://10.20.6.29:48181",
+ secure: false
+ },
+ "/websocket": {
+ target: "http://10.20.6.29:48181",
+ ws: true,
+ changeOrigin: false,
+ secure: false
+ }
+ }
+ }
+ }];
+}