aboutsummaryrefslogtreecommitdiffstats
path: root/sdnr/wt/odlux/apps/configurationApp/src
diff options
context:
space:
mode:
authorMichael Dürre <michael.duerre@highstreet-technologies.com>2020-07-16 05:55:07 +0200
committerMichael Dürre <michael.duerre@highstreet-technologies.com>2020-07-16 05:55:21 +0200
commit7dbe38ba0522b346a0fcd9851e797f0fd71ecd5e (patch)
treecc19db7e0637c8e392d40cdf3a53bb5e5f3e0d30 /sdnr/wt/odlux/apps/configurationApp/src
parent25b3759a0907d06e0d8e391f751c6fcf067087f5 (diff)
switch to rfc8040 restconf
change rest interface and some small code cleanups Issue-ID: CCSDK-2572 Signed-off-by: Michael Dürre <michael.duerre@highstreet-technologies.com> Change-Id: I3475bd2574b32950c4bf84fbd1c2a9dac9af208a
Diffstat (limited to 'sdnr/wt/odlux/apps/configurationApp/src')
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts321
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx5
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts4
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts55
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts34
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts12
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx271
-rw-r--r--sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts109
8 files changed, 658 insertions, 153 deletions
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts
index 45cdfe64d..dbe95e0d1 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts
+++ b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts
@@ -2,12 +2,14 @@ import { Action } from '../../../../framework/src/flux/action';
import { Dispatch } from '../../../../framework/src/flux/store';
import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
import { PushAction, ReplaceAction } from "../../../../framework/src/actions/navigationActions";
+import { AddErrorInfoAction } from "../../../../framework/src/actions/errorActions";
+import { DisplayModeType, DisplaySpecification } from '../handlers/viewDescriptionHandler';
import { restService } from "../services/restServices";
import { YangParser } from "../yang/yangParser";
import { Module } from "../models/yang";
-import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList } from "../models/uiModels";
-import { AddErrorInfoAction } from "../../../../framework/src/actions/errorActions";
+import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList, isViewElementRpc, isViewElementChoise, ViewElementChoiseCase } from "../models/uiModels";
+import { element } from 'prop-types';
export class EnableValueSelector extends Action {
constructor(public listSpecification: ViewSpecification, public listData: any[], public keyProperty: string, public onValueSelected : (value: any) => void ) {
@@ -34,7 +36,13 @@ export class UpdateDeviceDescription extends Action {
}
export class UpdatViewDescription extends Action {
- constructor(public vPath: string, public view: ViewSpecification, public viewData: any, public displayAsList: boolean = false, public key?: string ) {
+ constructor (public vPath: string, public viewData: any, public displaySpecification: DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay } ) {
+ super();
+ }
+}
+
+export class UpdatOutputData extends Action {
+ constructor (public outputData: any) {
super();
}
}
@@ -159,7 +167,7 @@ const getReferencedDataList = async (refPath: string, dataPath: string, modules:
return null;
}
-const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification, viewData: any, displayAsList: boolean = false, key?: string): UpdatViewDescription =>{
+const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification): ViewSpecification =>{
// check if-feature | when | and resolve all references.
view = { ...view };
@@ -173,28 +181,72 @@ const resolveViewDescription = (defaultNS: string | null, vPath: string, view: V
}
return acc;
}, {});
- return new UpdatViewDescription(vPath, view, viewData, displayAsList, key);
+ return view;
}
+const flatenViewElements = (defaultNS: string | null, parentPath: string, elements: { [name: string]: ViewElement }, views: ViewSpecification[], currentPath: string ): { [name: string]: ViewElement } => {
+ if (!elements) return {};
+ return Object.keys(elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => {
+ const elm = elements[cur];
+
+ // remove the detault namespace, and only the default namespace, sine it seems that this is also not in the restconf response
+ const elmKey = defaultNS && elm.id.replace(new RegExp(`^${defaultNS}:`, "i"), "") || elm.id;
+ const key = parentPath ? `${parentPath}.${elmKey}` : elmKey;
+
+ if (isViewElementRpc(elm)) {
+ console.warn(`Flaten of RFC not supported ! [${currentPath}][${elm.label}]`);
+ return acc;
+ } else if (isViewElementObjectOrList(elm)) {
+ const view = views[+elm.viewId];
+ const inner = view && flatenViewElements(defaultNS, key, view.elements, views, `${currentPath}/${view.name}`);
+ inner && Object.keys(inner).forEach(k => (acc[k] = inner[k]));
+ } else if (isViewElementChoise(elm)) {
+ acc[key] = {
+ ...elm,
+ id: key,
+ cases: Object.keys(elm.cases).reduce<{ [name: string]: ViewElementChoiseCase }>((accCases, curCases) => {
+ const caseElement = elm.cases[curCases];
+ accCases[curCases] = {
+ ...caseElement,
+ // Hint: do not use key it contains elmKey, which shell be omitted for cases.
+ elements: flatenViewElements(defaultNS, /*key*/ parentPath, caseElement.elements, views, `${currentPath}/${elm.label}`)
+ };
+ return accCases;
+ }, {}),
+ };
+ } else {
+ acc[key] = {
+ ...elm,
+ id: key,
+ };
+ }
+ return acc;
+ }, {});
+};
+
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 inputViewSpecification: ViewSpecification | undefined = undefined;
+ let outputViewSpecification: ViewSpecification | undefined = undefined;
+
let viewSpecification: ViewSpecification = views[0];
let viewElement: ViewElement;
let dataMember: string;
let extractList: boolean = false;
- let currentNS : string | null = null;
- let defaultNS : string | null = null;
+ 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;
+ const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS;
if (ind === 0) { defaultNS = namespace };
@@ -206,6 +258,7 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch:
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) => {
@@ -215,7 +268,16 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch:
}
return acc;
}, {});
- return dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, data, false, isViewElementList(viewElement!) && viewElement.key || undefined));
+
+ // create display specification
+ const ds: DisplaySpecification = {
+ displayMode: DisplayModeType.displayAsObject,
+ viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification),
+ keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined
+ };
+
+ // update display specification
+ return dispatch(new UpdatViewDescription(vPath, data, ds));
}
if (viewElement && isViewElementList(viewElement) && viewSpecification.parentView === "0") {
// check if there is a reference as key
@@ -247,11 +309,27 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch:
extractList = false;
}
- if (viewElement && "viewId" in viewElement) viewSpecification = views[+viewElement.viewId];
+ if (viewElement && "viewId" in viewElement) {
+ viewSpecification = views[+viewElement.viewId];
+ } else if (viewElement.uiType === "rpc") {
+ viewSpecification = views[+(viewElement.inputViewId || 0)];
+
+ // create new instance & flaten
+ inputViewSpecification = viewElement.inputViewId != null && {
+ ...views[+(viewElement.inputViewId || 0)],
+ elements: flatenViewElements(defaultNS, "", views[+(viewElement.inputViewId || 0)].elements, views, viewElement.label),
+ } || undefined;
+ outputViewSpecification = viewElement.outputViewId != null && {
+ ...views[+(viewElement.outputViewId || 0)],
+ elements: flatenViewElements(defaultNS, "", views[+(viewElement.outputViewId || 0)].elements, views, viewElement.label),
+ } || undefined;
+
+ }
}
let data: any = {};
- if (viewSpecification && viewSpecification.id !== "0") {
+ // do not get any data from netconf if there is no view specified || this is the root element [0] || this is an rpc
+ if (viewSpecification && !(viewSpecification.id === "0" || viewElement!.uiType === "rpc")) {
const restResult = (await restService.getConfigData(dataPath));
if (!restResult.data) {
// special case: if this is a list without any response
@@ -259,7 +337,15 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch:
if (!isViewElementList(viewElement!)) {
throw new Error(`vPath: [${vPath}]. ViewElement has no key.`);
}
- return dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, [], extractList, viewElement.key));
+ // create display specification
+ const ds: DisplaySpecification = {
+ displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject,
+ viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification),
+ keyProperty: viewElement.key
+ };
+
+ // update display specification
+ return dispatch(new UpdatViewDescription(vPath, [], ds));
}
throw new Error(`Did not get response from Server. Status: [${restResult.status}]`);
} else if (restResult.status < 200 || restResult.status > 299) {
@@ -278,9 +364,33 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch:
data = extractList
? data[viewElement!.label] || [] // if the list is empty, it does not exist
: data;
+
+ } else if (viewElement! && viewElement!.uiType === "rpc") {
+ // set data to defaults
+ data = {};
+ inputViewSpecification && Object.keys(inputViewSpecification.elements).forEach(key => {
+ const elm = inputViewSpecification && inputViewSpecification.elements[key];
+ if (elm && elm.default != undefined) {
+ data[elm.id] = elm.default;
+ }
+ });
}
- return dispatch(resolveViewDescription(defaultNS, vPath, viewSpecification, data, extractList, isViewElementList(viewElement!) && viewElement.key || undefined));
+ // create display specification
+ const ds: DisplaySpecification = viewElement! && viewElement!.uiType === "rpc"
+ ? {
+ displayMode: DisplayModeType.displayAsRPC,
+ inputViewSpecification: inputViewSpecification && resolveViewDescription(defaultNS, vPath, inputViewSpecification),
+ outputViewSpecification: outputViewSpecification && resolveViewDescription(defaultNS, vPath, outputViewSpecification),
+ }
+ : {
+ displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject,
+ viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification),
+ keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined,
+ };
+
+ // update display specification
+ return dispatch(new UpdatViewDescription(vPath, data, ds));
// https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]
// https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]/lp
} catch (error) {
@@ -290,7 +400,7 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch:
} finally {
return;
}
-}
+};
export const updateDataActionAsyncCreator = (vPath: string, data: any) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => {
const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
@@ -313,7 +423,7 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async
const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS;
if (ind === 0) { defaultNS = namespace };
- viewElement = viewSpecification.elements[property];
+ viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`];
if (!viewElement) throw Error("Property [" + property + "] does not exist.");
if (isViewElementList(viewElement) && !key) {
@@ -330,7 +440,7 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async
isNew = key;
if (!key) {
dispatch(new SetCollectingSelectionData(false));
- throw new Error("No value for key [" + viewElement.key +"] in list [" + property + "]");
+ throw new Error("No value for key [" + viewElement.key + "] in list [" + property + "]");
}
}
}
@@ -363,14 +473,183 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async
}
}
- 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));
+ if (isNew) {
+ return dispatch(new ReplaceAction(`/configuration/${nodeId}/${vPath.replace(/\[\]$/i, `[${isNew}]`)}`)) // navigate to new element
+ }
+
+ // create display specification
+ const ds: DisplaySpecification = {
+ displayMode: embedList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject,
+ viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification),
+ keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined,
+ };
+
+ // update display specification
+ return dispatch(new UpdatViewDescription(vPath, data, ds));
} catch (error) {
history.back();
dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` }));
- dispatch(new SetCollectingSelectionData(false));
+
} finally {
+ dispatch(new SetCollectingSelectionData(false));
return;
}
-} \ No newline at end of file
+};
+
+export const removeElementActionAsyncCreator = (vPath: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => {
+ const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
+ const { configuration: { deviceDescription: { nodeId, views } } } = getState();
+ let dataPath = `/restconf/config/network-topology:network-topology/topology/topology-netconf/node/${nodeId}/yang-ext:mount`;
+ let viewSpecification: ViewSpecification = views[0];
+ let viewElement: ViewElement;
+
+ let currentNS: string | null = null;
+ let defaultNS: string | null = null;
+
+ dispatch(new SetCollectingSelectionData(true));
+ try {
+ for (let ind = 0; ind < pathParts.length; ++ind) {
+ let [property, key] = pathParts[ind];
+ const namespaceInd = property && property.indexOf(":") || -1;
+ const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS;
+
+ if (ind === 0) { defaultNS = namespace };
+ viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`];
+ if (!viewElement) throw Error("Property [" + property + "] does not exist.");
+
+ if (isViewElementList(viewElement) && !key) {
+ 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) {
+ // remove the whole table
+ }
+ }
+
+ dataPath += `/${property}${key ? `/${key.replace(/\//ig, "%2F")}` : ""}`;
+
+ if (viewElement && "viewId" in viewElement) {
+ viewSpecification = views[+viewElement.viewId];
+ } else if (viewElement.uiType === "rpc") {
+ viewSpecification = views[+(viewElement.inputViewId || 0)];
+ }
+ }
+
+ const updateResult = await restService.removeConfigElement(dataPath);
+ if (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 || ''}`);
+ }
+ } catch (error) {
+ dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not remove ${dataPath}` }));
+ } finally {
+ dispatch(new SetCollectingSelectionData(false));
+ }
+
+
+};
+
+export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => {
+ const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
+ const { configuration: { deviceDescription: { nodeId, views }, viewDescription: oldViewDescription } } = getState();
+ let dataPath = `/restconf/operations/network-topology:network-topology/topology/topology-netconf/node/${nodeId}/yang-ext:mount`;
+ let viewSpecification: ViewSpecification = views[0];
+ let viewElement: ViewElement;
+ let dataMember: string;
+ let embedList: boolean = false;
+ let isNew: string | false = false;
+
+ let currentNS: string | null = null;
+ let defaultNS: string | null = null;
+
+ dispatch(new SetCollectingSelectionData(true));
+ try {
+ for (let ind = 0; ind < pathParts.length; ++ind) {
+ let [property, key] = pathParts[ind];
+ const namespaceInd = property && property.indexOf(":") || -1;
+ const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS;
+
+ if (ind === 0) { defaultNS = namespace };
+ viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`];
+ if (!viewElement) throw Error("Property [" + property + "] does not exist.");
+
+ if (isViewElementList(viewElement) && !key) {
+ embedList = true;
+ // if (viewElement && viewElement.isList && viewSpecification.parentView === "0") {
+ // throw new Error("Found a list at root level of a module w/o a 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];
+ } else if (viewElement.uiType === "rpc") {
+ viewSpecification = views[+(viewElement.inputViewId || 0)];
+ }
+ }
+
+ // re-inflate formerly flatten rpc data
+ data = data && Object.keys(data).reduce < { [name: string ]: any }>((acc, cur) => {
+ const pathParts = cur.split(".");
+ let pos = 0;
+ const updatePath = (obj: any, key: string) => {
+ obj[key] = (pos >= pathParts.length)
+ ? data[cur]
+ : updatePath(obj[key] || {}, pathParts[pos++]);
+ return obj;
+ }
+ updatePath(acc, pathParts[pos++]);
+ return acc;
+ }, {}) || null;
+
+ // embed the list -> key: list
+ data = embedList
+ ? { [viewElement!.label]: data }
+ : data;
+
+ // embed the first element list[key]
+ data = isNew
+ ? [data]
+ : data;
+
+ // do not post root member (0)
+ if ((viewSpecification && viewSpecification.id !== "0") || (dataMember! && !data)) {
+ const updateResult = await restService.executeRpc(dataPath, { [`${defaultNS}:input`]: data || {} });
+ if (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 || ''}`);
+ }
+ const viewData = { ...oldViewDescription.viewData, output: updateResult.data || null};
+ dispatch(new UpdatOutputData(viewData));
+ } else {
+ throw new Error(`There is NO RPC specified.`);
+ }
+
+
+ // // update display specification
+ // return
+ } catch (error) {
+ dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` }));
+
+ } finally {
+ dispatch(new SetCollectingSelectionData(false));
+ }
+};
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx
index d46076338..8b0b890d0 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx
+++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx
@@ -39,6 +39,7 @@ export const UiElementSelection = (props: selectionProps) => {
? (<FormControl style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
<InputLabel htmlFor={`select-${element.id}`} >{element.label}</InputLabel>
<Select
+ title={(element.options.find(o => o.key === value.toString().toLowerCase()) || { description: undefined }).description}
required={!!element.mandatory}
error={!!error}
onChange={(e) => { props.onChange(e.target.value as string) }}
@@ -50,7 +51,9 @@ export const UiElementSelection = (props: selectionProps) => {
id: `select-${element.id}`,
}}
>
- {element.options.map(option => (<MenuItem key={option.key} title={option.description} value={option.key}>{option.key}</MenuItem>))}
+ {element.options.map(option => (
+ <MenuItem key={option.key} title={option.description} value={option.key}>{option.key}</MenuItem>
+ ))}
</Select>
<FormHelperText>{error}</FormHelperText>
</FormControl>)
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts
index 9af640f16..5b2d55ee2 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts
@@ -18,7 +18,7 @@
import { IActionHandler } from "../../../../framework/src/flux/action";
import { ViewSpecification } from "../models/uiModels";
-import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdatViewDescription } from "../actions/deviceActions";
+import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdatViewDescription, UpdatOutputData } from "../actions/deviceActions";
export interface IValueSelectorState {
collectingData: boolean;
@@ -62,7 +62,7 @@ export const valueSelectorHandler: IActionHandler<IValueSelectorState> = (state
onValueSelected: nc,
listData: [],
};
- } else if (action instanceof UpdateDeviceDescription || action instanceof UpdatViewDescription) {
+ } else if (action instanceof UpdateDeviceDescription || action instanceof UpdatViewDescription || action instanceof UpdatOutputData) {
state = {
...state,
collectingData: false,
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts
index c1b3350b2..69710b9e9 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts
+++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts
@@ -18,31 +18,42 @@
import { IActionHandler } from "../../../../framework/src/flux/action";
-import { UpdatViewDescription } from "../actions/deviceActions";
+import { UpdatViewDescription, UpdatOutputData } from "../actions/deviceActions";
import { ViewSpecification } from "../models/uiModels";
+export enum DisplayModeType {
+ doNotDisplay = 0,
+ displayAsObject = 1,
+ displayAsList = 2,
+ displayAsRPC = 3
+};
+
+export type DisplaySpecification = {
+ displayMode: DisplayModeType.doNotDisplay;
+} | {
+ displayMode: DisplayModeType.displayAsObject | DisplayModeType.displayAsList ;
+ viewSpecification: ViewSpecification;
+ keyProperty: string | undefined;
+} | {
+ displayMode: DisplayModeType.displayAsRPC;
+ inputViewSpecification?: ViewSpecification;
+ outputViewSpecification?: ViewSpecification;
+}
+
export interface IViewDescriptionState {
vPath: string | null;
- keyProperty: string | undefined;
- displayAsList: boolean;
- viewSpecification: ViewSpecification;
- viewData: any
+ displaySpecification: DisplaySpecification;
+ viewData: any,
+ outputData?: any,
}
const viewDescriptionStateInit: IViewDescriptionState = {
vPath: null,
- keyProperty: undefined,
- displayAsList: false,
- viewSpecification: {
- id: "empty",
- canEdit: false,
- parentView: "",
- name: "emplty",
- language: "en-US",
- title: "empty",
- elements: {}
+ displaySpecification: {
+ displayMode: DisplayModeType.doNotDisplay,
},
- viewData: null
+ viewData: null,
+ outputData: undefined,
};
export const viewDescriptionHandler: IActionHandler<IViewDescriptionState> = (state = viewDescriptionStateInit, action) => {
@@ -50,11 +61,15 @@ export const viewDescriptionHandler: IActionHandler<IViewDescriptionState> = (st
state = {
...state,
vPath: action.vPath,
- keyProperty: action.key,
- displayAsList: action.displayAsList,
- viewSpecification: action.view,
viewData: action.viewData,
- }
+ outputData: undefined,
+ displaySpecification: action.displaySpecification,
+ };
+ } else if (action instanceof UpdatOutputData) {
+ state = {
+ ...state,
+ outputData: action.outputData,
+ };
}
return state;
};
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts
index 7b41c3845..f0391eebf 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts
+++ b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts
@@ -109,12 +109,28 @@ export type ViewElementUnion = ViewElementBase & {
"elements": ViewElement[];
}
+export type ViewElementChoiseCase = { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } };
+
export type ViewElementChoise = ViewElementBase & {
"uiType": "choise";
- "cases": { [name: string]: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } };
+ "cases": {
+ [name: string]: ViewElementChoiseCase;
+ }
+}
+
+// https://tools.ietf.org/html/rfc7950#section-7.14.1
+export type ViewElementRpc = ViewElementBase & {
+ "uiType": "rpc";
+ "inputViewId"?: string;
+ "outputViewId"?: string;
+}
+
+export type ViewElementEmpty = ViewElementBase & {
+ "uiType": "empty";
}
export type ViewElement =
+ | ViewElementEmpty
| ViewElementBits
| ViewElementBinary
| ViewElementString
@@ -125,7 +141,8 @@ export type ViewElement =
| ViewElementSelection
| ViewElementReference
| ViewElementUnion
- | ViewElementChoise;
+ | ViewElementChoise
+ | ViewElementRpc;
export const isViewElementString = (viewElement: ViewElement): viewElement is ViewElementString => {
return viewElement && viewElement.uiType === "string";
@@ -167,16 +184,25 @@ export const isViewElementChoise = (viewElement: ViewElement): viewElement is Vi
return viewElement && viewElement.uiType === "choise";
}
+export const isViewElementRpc = (viewElement: ViewElement): viewElement is ViewElementRpc => {
+ return viewElement && viewElement.uiType === "rpc";
+}
+
+export const isViewElementEmpty = (viewElement: ViewElement): viewElement is ViewElementRpc => {
+ return viewElement && viewElement.uiType === "empty";
+}
+
+export const ResolveFunction = Symbol("IsResolved");
export type ViewSpecification = {
"id": string;
- "name": string;
+ "name"?: string;
"title"?: string;
"parentView"?: string;
"language": string;
"ifFeature"?: string;
"when"?: string;
- "uses"?: string[];
+ "uses"?: (string[]) & { [ResolveFunction]?: () => void };
"elements": { [name: string]: ViewElement };
readonly "canEdit": boolean;
}
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts
index 0d28e6653..b260f1ffb 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts
+++ b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts
@@ -57,6 +57,18 @@ class RestService {
public setConfigData(path: string, data: any) {
return requestRestExt<{ [key: string]: any }>(path, { method: "PUT", body: JSON.stringify(data) });
}
+
+ public executeRpc(path: string, data: any) {
+ return requestRestExt<{ [key: string]: any }>(path, { method: "POST", body: JSON.stringify(data) });
+ }
+
+ /** Removes the element by restconf path.
+ * @param path The restconf path to identify the note to update.
+ * @returns The restconf result.
+ */
+ public removeConfigElement(path: string) {
+ return requestRestExt<{ [key: string]: any }>(path, { method: "DELETE" });
+ }
}
export const restService = new RestService();
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx
index 06de39b9d..385f3b52f 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx
+++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx
@@ -25,9 +25,11 @@ import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/co
import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table";
import { Loader } from "../../../../framework/src/components/material-ui/loader";
+import { renderObject } from '../../../../framework/src/components/objectDump';
-import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator, updateViewActionAsyncCreator } from "../actions/deviceActions";
-import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion } from "../models/uiModels";
+import { DisplayModeType } from '../handlers/viewDescriptionHandler';
+import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator, updateViewActionAsyncCreator, removeElementActionAsyncCreator, executeRpcActionAsyncCreator } from "../actions/deviceActions";
+import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion, isViewElementRpc, ViewElementRpc, isViewElementEmpty } from "../models/uiModels";
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
@@ -36,17 +38,14 @@ 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 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';
import { UIElementReference } from '../components/uiElementReference';
import { UiElementNumber } from '../components/uiElementNumber';
@@ -54,6 +53,7 @@ import { UiElementString } from '../components/uiElementString';
import { UiElementBoolean } from '../components/uiElementBoolean';
import { UiElementSelection } from '../components/uiElementSelection';
import { UIElementUnion } from '../components/uiElementUnion';
+import { Button } from '@material-ui/core';
const styles = (theme: Theme) => createStyles({
header: {
@@ -110,6 +110,9 @@ const styles = (theme: Theme) => createStyles({
},
},
},
+ uiView: {
+ overflowY: "auto",
+ },
section: {
padding: "15px",
borderBottom: `2px solid ${theme.palette.divider}`,
@@ -130,15 +133,16 @@ const mapProps = (state: IApplicationStoreState) => ({
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,
+ outputData: state.configuration.viewDescription.outputData,
+ displaySpecification: state.configuration.viewDescription.displaySpecification,
});
const mapDispatch = (dispatcher: IDispatcher) => ({
onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)),
reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)),
+ removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)),
+ executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)),
});
const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
@@ -171,49 +175,59 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
}
}
+ private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => {
+ return Object.keys(elements).reduce((acc, cur) => {
+ const elm = elements[cur];
+ if (isViewElementChoise(elm)) {
+ const caseKeys = Object.keys(elm.cases);
+
+ // find the right case for this choise, use the first one with data, at least use index 0
+ const selectedCase = caseKeys.find(key => {
+ const caseElm = elm.cases[key];
+ return Object.keys(caseElm.elements).some(caseElmKey => {
+ const caseElmElm = caseElm.elements[caseElmKey];
+ return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined;
+ });
+ }) || caseKeys[0];
+
+ // extract all data of the active case
+ const caseElements = elm.cases[selectedCase].elements;
+ const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => {
+ const dataElm = caseElements[dataCur];
+ if (isViewElementEmpty(dataElm)) {
+ dataAcc[dataElm.label] = null;
+ } else if (viewData[dataElm.label] !== undefined) {
+ dataAcc[dataElm.label] = viewData[dataElm.label];
+ } else if (viewData[dataElm.id] !== undefined) {
+ dataAcc[dataElm.id] = viewData[dataElm.id];
+ }
+ return dataAcc;
+ }, {} as { [name: string]: any });
+
+ acc[elm.id] = {
+ selectedCase,
+ data,
+ };
+ }
+ return acc;
+ }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {}
+ }
+
static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) {
if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
- const isNew: boolean = nextProps.vPath ?.endsWith("[]") || false;
+ const isNew: boolean = nextProps.vPath?.endsWith("[]") || false;
const state = {
...prevState,
isNew: isNew,
editMode: isNew,
viewData: nextProps.viewData || null,
[OldProps]: nextProps,
- choises: nextProps.viewSpecification && Object.keys(nextProps.viewSpecification.elements).reduce((acc, cur) => {
- const elm = nextProps.viewSpecification.elements[cur];
- if (isViewElementChoise(elm)) {
- const caseKeys = Object.keys(elm.cases);
-
- // find the right case for this choise, use the first one with data, at least use index 0
- const selectedCase = caseKeys.find(key => {
- const caseElm = elm.cases[key];
- return Object.keys(caseElm.elements).some(caseElmKey => {
- const caseElmElm = caseElm.elements[caseElmKey];
- return nextProps.viewData[caseElmElm.label] != null || nextProps.viewData[caseElmElm.id] != null;
- });
- }) || caseKeys[0];
-
- // extract all data of the active case
- const caseElements = elm.cases[selectedCase].elements;
- const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => {
- const dataElm = caseElements[dataCur];
- if (nextProps.viewData[dataElm.label] !== undefined) {
- dataAcc[dataElm.label] = nextProps.viewData[dataElm.label];
- } else if (nextProps.viewData[dataElm.id] !== undefined) {
- dataAcc[dataElm.id] = nextProps.viewData[dataElm.id];
- }
- return dataAcc;
- }, {} as { [name: string]: any });
-
- acc[elm.id] = {
- selectedCase,
- data,
- };
- }
- return acc;
- }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {}
+ choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay
+ ? null
+ : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC
+ ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || []
+ : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData)
}
return state;
}
@@ -233,10 +247,46 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
});
}
+ private collectData = (elements: { [name: string]: ViewElement }) => {
+ // ensure only active choises will be contained
+ const viewData : { [key: string]: any }= { ...this.state.viewData };
+ const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey]));
+ const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => {
+ const currentChoice = elements[curChoiceKey] as ViewElementChoise;
+ const selectedCase = this.state.choises[curChoiceKey].selectedCase;
+ Object.keys(currentChoice.cases).forEach(caseKey => {
+ const caseElements = currentChoice.cases[caseKey].elements;
+ if (caseKey === selectedCase) {
+ Object.keys(caseElements).forEach(caseElementKey => {
+ const elm = caseElements[caseElementKey];
+ if (isViewElementEmpty(elm)) {
+ // insert null for all empty elements
+ viewData[elm.id] = null;
+ }
+ });
+ return;
+ };
+ Object.keys(caseElements).forEach(caseElementKey => {
+ acc.push(caseElements[caseElementKey]);
+ });
+ });
+ return acc;
+ }, [] as ViewElement[]);
+
+ return viewData && Object.keys(viewData).reduce((acc, cur) => {
+ if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) {
+ acc[cur] = viewData[cur];
+ }
+ return acc;
+ }, {} as { [key: string]: any });
+ }
+
private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
const isKey = (uiElement.label === keyProperty);
const canEdit = editMode && (isNew || (uiElement.config && !isKey));
- if (isViewElementSelection(uiElement)) {
+ if (isViewElementEmpty(uiElement)) {
+ return null;
+ } else if (isViewElementSelection(uiElement)) {
return <UiElementSelection
key={uiElement.id}
@@ -283,8 +333,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
readOnly={!canEdit}
disabled={editMode && !canEdit}
onChange={(e) => { this.changeValueFor(uiElement.id, e) }} />
- }
- else {
+ } else {
if (process.env.NODE_ENV !== "production") {
console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
}
@@ -391,16 +440,18 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
acc.references.push(elm);
} else if (isViewElementChoise(elm)) {
acc.choises.push(elm);
+ } else if (isViewElementRpc(elm)) {
+ acc.rpcs.push(elm);
} else {
acc.elements.push(elm);
}
return acc;
- }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[] });
+ }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] });
sections.elements = sections.elements.sort(orderFunc);
return (
- <>
+ <div className={classes.uiView}>
<div className={classes.section} />
{sections.elements.length > 0
? (
@@ -425,7 +476,16 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
</div>
) : null
}
- </>
+ {sections.rpcs.length > 0
+ ? (
+ <div className={classes.section}>
+ {sections.rpcs.map(element => (
+ <UIElementReference key={element.id} element={element} disabled={editMode} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} />
+ ))}
+ </div>
+ ) : null
+ }
+ </div>
);
};
@@ -442,7 +502,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
}
};
- const { classes } = this.props;
+ const { classes, removeElement } = this.props;
return (
<SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={[addNewElementAction]} columns={
@@ -457,11 +517,13 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
}
return acc;
}, []).concat([{
- property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (row => {
+ property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: ( ({ rowData })=> {
return (
<Tooltip title={"Remove"} >
- <IconButton className={classes.button} onClick={event => {
-
+ <IconButton className={classes.button} onClick={(e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ removeElement(`${this.props.vPath}[${rowData[listKeyProperty]}]`)
}} >
<RemoveIcon />
</IconButton>
@@ -476,10 +538,73 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
);
}
+ private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
+ const { classes } = this.props;
+
+ const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
+ if (keyProperty) {
+ // if (vsA.label === vsB.label) return 0;
+ if (vsA.label === keyProperty) return -1;
+ if (vsB.label === keyProperty) return +1;
+ }
+
+ // if (vsA.uiType === vsB.uiType) return 0;
+ // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
+ // if (vsA.uiType === "object") return +1;
+ return -1;
+ };
+
+ const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
+ const elm = inputViewSpecification.elements[cur];
+ if (isViewElementObjectOrList(elm)) {
+ console.error("Object should not appear in RPC view !");
+ } else if (isViewElementChoise(elm)) {
+ acc.choises.push(elm);
+ } else if (isViewElementRpc(elm)) {
+ console.error("RPC should not appear in RPC view !");
+ } else {
+ acc.elements.push(elm);
+ }
+ return acc;
+ }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] })
+ || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] };
+
+ sections.elements = sections.elements.sort(orderFunc);
+
+ return (
+ <>
+ <div className={classes.section} />
+ {sections.elements.length > 0
+ ? (
+ <div className={classes.section}>
+ {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
+ </div>
+ ) : null
+ }
+ {sections.choises.length > 0
+ ? (
+ <div className={classes.section}>
+ {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))}
+ </div>
+ ) : null
+ }
+ <Button onClick={() => {
+ const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
+ this.props.executeRpc(this.props.vPath!, resultingViewData);
+ }} >Exec</Button>
+ {outputViewData !== undefined
+ ? (
+ renderObject(outputViewData) )
+ : null
+ }
+ </>
+ );
+ };
+
private renderBreadCrumps() {
const { editMode } = this.state;
- const { viewSpecification, displayAsList } = this.props;
- const { vPath, match: { url, path }, nodeId } = this.props;
+ const { displaySpecification } = this.props;
+ const { vPath, nodeId } = this.props;
const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
let lastPath = `/configuration`;
let basePath = `/configuration/${nodeId}`;
@@ -527,33 +652,11 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
}} ><ArrowBack /></Fab>
) || null}
{ /* do not show edit if this is a list or it can't be edited */
- !displayAsList && viewSpecification.canEdit && (<div>
+ displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
<Fab color="secondary" aria-label="edit" className={this.props.classes.fab} onClick={() => {
if (this.state.editMode) {
-
// ensure only active choises will be contained
- const choiseKeys = Object.keys(viewSpecification.elements).filter(elmKey => isViewElementChoise(viewSpecification.elements[elmKey]));
- const elementsToRemove = choiseKeys.reduce((acc, cur) => {
- const choise = viewSpecification.elements[cur] as ViewElementChoise;
- const selectedCase = this.state.choises[cur].selectedCase;
- Object.keys(choise.cases).forEach(caseKey => {
- if (caseKey === selectedCase) return;
- const caseElements = choise.cases[caseKey].elements;
- Object.keys(caseElements).forEach(caseElementKey => {
- acc.push(caseElements[caseElementKey]);
- });
- });
- return acc;
- }, [] as ViewElement[]);
-
- const viewData = this.state.viewData;
- const resultingViewData = viewData && Object.keys(viewData).reduce((acc, cur) => {
- if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) {
- acc[cur] = viewData[cur];
- }
- return acc;
- }, {} as { [key: string]: any });
-
+ const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
this.props.onUpdateData(this.props.vPath!, resultingViewData);
}
this.setState({ editMode: !editMode });
@@ -595,15 +698,19 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp
}
private renderValueEditor() {
- const { keyProperty, displayAsList, viewSpecification } = this.props;
+ const { displaySpecification: ds, outputData } = this.props;
const { viewData, editMode, isNew } = this.state;
return (
<div className={this.props.classes.container}>
{this.renderBreadCrumps()}
- {displayAsList && viewData instanceof Array
- ? this.renderUIViewList(viewSpecification, keyProperty!, viewData)
- : this.renderUIView(viewSpecification, viewData!, keyProperty, editMode, isNew)
+ {ds.displayMode === DisplayModeType.doNotDisplay
+ ? null
+ : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
+ ? this.renderUIViewList(ds.viewSpecification, ds.keyProperty!, viewData)
+ : ds.displayMode === DisplayModeType.displayAsRPC
+ ? this.renderUIViewRPC(ds.inputViewSpecification, viewData!, outputData, undefined, true, false)
+ : this.renderUIView(ds.viewSpecification, viewData!, ds.keyProperty, editMode, isNew)
}
</div >
);
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts
index 27859f7b6..b1c1e7430 100644
--- a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts
+++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts
@@ -15,10 +15,12 @@
* the License.
* ============LICENSE_END==========================================================================
*/
-
-
import { Token, Statement, Module, Identity } from "../models/yang";
-import { ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase, isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString, isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion } from "../models/uiModels";
+import {
+ ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase,
+ isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString,
+ isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion, ViewElementRpc, isViewElementRpc, ResolveFunction
+} from "../models/uiModels";
import { yangService } from "../services/yangService";
export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => {
@@ -263,7 +265,8 @@ class YangLexer {
}
export class YangParser {
- private _groupingsToResolve: (() => void)[] = [];
+ private _groupingsToResolve: ViewSpecification[] = [];
+
private _identityToResolve: (() => void)[] = [];
private _unionsToResolve: (() => void)[] = [];
private _modulesToResolve: (() => void)[] = [];
@@ -410,11 +413,13 @@ export class YangParser {
this._modulesToResolve.push(() => {
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.`);
+ if (!(isViewElementObjectOrList(viewElement) || isViewElementRpc(viewElement))) {
+ console.error(new Error(`Module: [${module}]. Only Object, List or RPC are allowed on root level.`));
+ }
+ if (isViewElementObjectOrList(viewElement)) {
+ const viewIdIndex = Number(viewElement.viewId);
+ module.views[key] = this._views[viewIdIndex];
}
- const viewIdIndex = Number(viewElement.viewId);
- module.views[key] = this._views[viewIdIndex];
this._views[0].elements[key] = module.elements[key];
});
});
@@ -431,8 +436,8 @@ export class YangParser {
});
// process all groupings
- this._groupingsToResolve.forEach(cb => {
- try { cb(); } catch (error) {
+ this._groupingsToResolve.filter(vs => vs.uses && vs.uses[ResolveFunction]).forEach(vs => {
+ try { vs.uses![ResolveFunction]!(); } catch (error) {
console.warn(`Error resolving: [${error.message}]`);
}
});
@@ -470,7 +475,6 @@ export class YangParser {
return result;
}
-
const baseIdentites: Identity[] = [];
Object.keys(this.modules).forEach(modKey => {
const module = this.modules[modKey];
@@ -478,7 +482,7 @@ export class YangParser {
const identity = module.identities[idKey];
if (identity.base != null) {
const base = this.resolveIdentity(identity.base, module);
- base.children ?.push(identity);
+ base.children?.push(identity);
} else {
baseIdentites.push(identity);
}
@@ -765,20 +769,60 @@ export class YangParser {
}, []));
}
- const rpcs = this.extractNodes(statement, "rpc");
- if (rpcs && rpcs.length > 0) {
- // todo:
- }
+ const rpcStms = this.extractNodes(statement, "rpc");
+ if (rpcStms && rpcStms.length > 0) {
+ elements.push(...rpcStms.reduce<ViewElementRpc[]>((accRpc, curRpc) => {
+ if (!curRpc.arg) {
+ throw new Error(`Module: [${context.name}]${currentPath}. Found rpc without name.`);
+ }
+
+ const description = this.extractValue(curRpc, "description") || undefined;
+ const configValue = this.extractValue(curRpc, "config");
+ const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false";
+
+ let inputViewId: string | undefined = undefined;
+ let outputViewId: string | undefined = undefined;
+
+ const input = this.extractNodes(curRpc, "input") || undefined;
+ const output = this.extractNodes(curRpc, "output") || undefined;
+
+ if (input && input.length > 0) {
+ const [inputView, inputSubViews] = this.extractSubViews(input[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`);
+ subViews.push(inputView, ...inputSubViews);
+ inputViewId = inputView.id;
+ }
+
+ if (output && output.length > 0) {
+ const [outputView, outputSubViews] = this.extractSubViews(output[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`);
+ subViews.push(outputView, ...outputSubViews);
+ outputViewId = outputView.id;
+ }
- if (!statement.arg) {
- throw new Error(`Module: [${context.name}]. Found statement without name.`);
+ const element: ViewElementRpc = {
+ uiType: "rpc",
+ id: parentId === 0 ? `${context.name}:${curRpc.arg}` : curRpc.arg,
+ label: curRpc.arg,
+ config: config,
+ description: description,
+ inputViewId: inputViewId,
+ outputViewId: outputViewId,
+ };
+
+ accRpc.push(element);
+
+ return accRpc;
+ }, []));
}
+ // if (!statement.arg) {
+ // throw new Error(`Module: [${context.name}]. Found statement without name.`);
+ // }
+
const viewSpec: ViewSpecification = {
id: String(currentId),
parentView: String(parentId),
- name: statement.arg,
- title: statement.arg,
+ name: statement.arg != null ? statement.arg : undefined,
+ title: statement.arg != null ? statement.arg : undefined,
language: "en-us",
canEdit: false,
ifFeature: ifFeature,
@@ -804,6 +848,8 @@ export class YangParser {
if (usesRefs && usesRefs.length > 0) {
viewSpec.uses = (viewSpec.uses || []);
+ const resolveFunctions : (()=>void)[] = [];
+
for (let i = 0; i < usesRefs.length; ++i) {
const groupingName = usesRefs[i].arg;
if (!groupingName) {
@@ -812,9 +858,14 @@ export class YangParser {
viewSpec.uses.push(this.resolveReferencePath(groupingName, context));
- this._groupingsToResolve.push(() => {
+ resolveFunctions.push(() => {
const groupingViewSpec = this.resolveGrouping(groupingName, context);
if (groupingViewSpec) {
+
+ // resolve recursive
+ const resolveFunc = groupingViewSpec.uses && groupingViewSpec.uses[ResolveFunction];
+ resolveFunc && resolveFunc();
+
Object.keys(groupingViewSpec.elements).forEach(key => {
const elm = groupingViewSpec.elements[key];
viewSpec.elements[key] = {
@@ -826,6 +877,19 @@ export class YangParser {
}
});
}
+
+ viewSpec.uses[ResolveFunction] = () => {
+ resolveFunctions.forEach(res => {
+ try {
+ res();
+ } catch (error) {
+ console.error(error);
+ }
+ });
+ viewSpec?.uses![ResolveFunction] = undefined;
+ }
+
+ this._groupingsToResolve.push(viewSpec);
}
return [viewSpec, subViews];
@@ -1117,10 +1181,9 @@ export class YangParser {
/* 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",
+ uiType: "empty",
};
} else if (type === "union") {
// todo: ❗ handle union ⚡