diff options
author | Michael Dürre <michael.duerre@highstreet-technologies.com> | 2020-07-16 05:55:07 +0200 |
---|---|---|
committer | Michael Dürre <michael.duerre@highstreet-technologies.com> | 2020-07-16 05:55:21 +0200 |
commit | 7dbe38ba0522b346a0fcd9851e797f0fd71ecd5e (patch) | |
tree | cc19db7e0637c8e392d40cdf3a53bb5e5f3e0d30 /sdnr/wt/odlux/apps/configurationApp | |
parent | 25b3759a0907d06e0d8e391f751c6fcf067087f5 (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')
9 files changed, 660 insertions, 155 deletions
diff --git a/sdnr/wt/odlux/apps/configurationApp/package.json b/sdnr/wt/odlux/apps/configurationApp/package.json index b18d1ce10..7b9dce688 100644 --- a/sdnr/wt/odlux/apps/configurationApp/package.json +++ b/sdnr/wt/odlux/apps/configurationApp/package.json @@ -24,8 +24,8 @@ "@odlux/framework": "*" }, "peerDependencies": { - "@types/react": "16.9.11", - "@types/react-dom": "16.9.4", + "@types/react": "16.9.19", + "@types/react-dom": "16.9.5", "@types/react-router-dom": "4.3.1", "@material-ui/core": "4.9.0", "@material-ui/icons": "4.5.1", 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 ⚡ |