diff options
Diffstat (limited to 'sdnr/wt/odlux/apps/configurationApp/src')
29 files changed, 2184 insertions, 1640 deletions
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts index 37583787f..52137135a 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/actions/deviceActions.ts @@ -1,15 +1,30 @@ import { Action } from '../../../../framework/src/flux/action'; import { Dispatch } from '../../../../framework/src/flux/store'; -import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import { PushAction, ReplaceAction } from "../../../../framework/src/actions/navigationActions"; -import { AddErrorInfoAction } from "../../../../framework/src/actions/errorActions"; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { PushAction, ReplaceAction } from '../../../../framework/src/actions/navigationActions'; +import { AddErrorInfoAction } from '../../../../framework/src/actions/errorActions'; import { DisplayModeType, DisplaySpecification } from '../handlers/viewDescriptionHandler'; -import { restService } from "../services/restServices"; -import { YangParser } from "../yang/yangParser"; -import { Module } from "../models/yang"; -import { ViewSpecification, ViewElement, isViewElementReference, isViewElementList, isViewElementObjectOrList, isViewElementRpc, isViewElementChoise, ViewElementChoiseCase, ViewElementString } from "../models/uiModels"; +import { restService } from '../services/restServices'; +import { YangParser } from '../yang/yangParser'; +import { Module } from '../models/yang'; +import { + ViewSpecification, + ViewElement, + isViewElementReference, + isViewElementList, + ViewElementString, +} from '../models/uiModels'; + +import { + checkResponseCode, + splitVPath, + filterViewElements, + flattenViewElements, + getReferencedDataList, + resolveViewDescription, +} from '../utilities/viewEngineHelper'; export class EnableValueSelector extends Action { constructor(public listSpecification: ViewSpecification, public listData: any[], public keyProperty: string, public onValueSelected : (value: any) => void ) { @@ -30,42 +45,26 @@ export class SetSelectedValue extends Action { } export class UpdateDeviceDescription extends Action { - constructor( public nodeId: string, public modules: { [name:string]: Module}, public views: ViewSpecification[]) { + constructor( public nodeId: string, public modules: { [name:string]: Module }, public views: ViewSpecification[]) { super(); } } -export class UpdatViewDescription extends Action { - constructor (public vPath: string, public viewData: any, public displaySpecification: DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay }) { +export class UpdateViewDescription extends Action { + constructor(public vPath: string, public viewData: any, public displaySpecification: DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay }) { super(); } } -export class UpdatOutputData extends Action { - constructor (public outputData: any) { +export class UpdateOutputData extends Action { + constructor(public outputData: any) { super(); } } -type HttpResult = { - status: number; - message?: string | undefined; - data: { - [key: string]: any; - } | null | undefined; -}; - -const checkResponseCode = (restResult: HttpResult) =>{ - - //403 gets handled by the framework from now on - - return restResult.status !== 403 && ( restResult.status < 200 || restResult.status > 299); +export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatch: Dispatch, _getState: () => IApplicationStoreState ) => { -} - -export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState ) => { - - dispatch(new UpdateDeviceDescription("", {}, [])); + dispatch(new UpdateDeviceDescription('', {}, [])); dispatch(new SetCollectingSelectionData(true)); const { availableCapabilities, unavailableCapabilities, importOnlyModules } = await restService.getCapabilitiesByMountId(nodeId); @@ -73,16 +72,24 @@ export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatc if (!availableCapabilities || availableCapabilities.length <= 0) { dispatch(new SetCollectingSelectionData(false)); dispatch(new UpdateDeviceDescription(nodeId, {}, [])); - dispatch(new UpdatViewDescription("", [], { + dispatch(new UpdateViewDescription('', [], { displayMode: DisplayModeType.displayAsMessage, - renderMessage: `NetworkElement : "${nodeId}" has no capabilities.` + renderMessage: `NetworkElement : "${nodeId}" has no capabilities.`, })); throw new Error(`NetworkElement : [${nodeId}] has no capabilities.`); } - const parser = new YangParser(unavailableCapabilities || undefined, importOnlyModules || undefined, nodeId); + const parser = new YangParser( + nodeId, + availableCapabilities.reduce((acc, cur) => { + acc[cur.capability] = cur.version; + return acc; + }, {} as { [key: string]: string }), + unavailableCapabilities || undefined, + importOnlyModules || undefined, + ); - for (let i = 0; i < availableCapabilities.length; ++i){ + for (let i = 0; i < availableCapabilities.length; ++i) { const capRaw = availableCapabilities[i]; try { await parser.addCapability(capRaw.capability, capRaw.version); @@ -95,184 +102,28 @@ export const updateNodeIdAsyncActionCreator = (nodeId: string) => async (dispatc dispatch(new SetCollectingSelectionData(false)); - if (process.env.NODE_ENV === "development" ) { - console.log(parser, parser.modules, parser.views); + if (process.env.NODE_ENV === 'development' ) { + console.log(parser, parser.modules, parser.views); } return dispatch(new UpdateDeviceDescription(nodeId, parser.modules, parser.views)); -} - -export const splitVPath = (vPath: string, vPathParser : RegExp): [string, string?][] => { - const pathParts: [string, string?][] = []; - let partMatch: RegExpExecArray | null; - if (vPath) do { - partMatch = vPathParser.exec(vPath); - if (partMatch) { - pathParts.push([partMatch[1], partMatch[2] || undefined]); - } - } while (partMatch) - return pathParts; -} - -const getReferencedDataList = async (refPath: string, dataPath: string, modules: { [name: string]: Module }, views: ViewSpecification[]) => { - const pathParts = splitVPath(refPath, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property - const defaultNS = pathParts[0][0]; - let referencedModule = modules[defaultNS]; - - let dataMember: string; - let view: ViewSpecification; - let currentNS: string | null = null; - let dataUrls = [dataPath]; - let data: any; - - for (let i = 0; i < pathParts.length; ++i) { - const [pathPartNS, pathPart] = pathParts[i]; - const namespace = pathPartNS != null ? (currentNS = pathPartNS) : currentNS; - - const viewElement = i === 0 - ? views[0].elements[`${referencedModule.name}:${pathPart}`] - : view!.elements[`${pathPart}`] || view!.elements[`${namespace}:${pathPart}`]; - - if (!viewElement) throw new Error(`Could not find ${pathPart} in ${refPath}`); - if (i < pathParts.length - 1) { - if (!isViewElementObjectOrList(viewElement)) { - throw Error(`Module: [${referencedModule.name}].[${viewElement.label}]. Viewelement is not list or object.`); - } - view = views[+viewElement.viewId]; - const resultingDataUrls : string[] = []; - if (isViewElementList(viewElement)) { - for (let j = 0; j < dataUrls.length; ++j) { - const dataUrl = dataUrls[j]; - const restResult = (await restService.getConfigData(dataUrl)); - if (restResult.data == null || checkResponseCode(restResult)) { - const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]["error-message"] || ""; - throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); - } - - let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; - if (dataRaw === undefined) { - dataRaw = restResult.data[dataMember!]; - } - dataRaw = dataRaw instanceof Array - ? dataRaw[0] - : dataRaw; +}; - data = dataRaw && dataRaw[viewElement.label] || []; - const keys: string[] = data.map((entry: { [key: string]: any } )=> entry[viewElement.key!]); - resultingDataUrls.push(...keys.map(key => `${dataUrl}/${viewElement.label.replace(/\//ig, "%2F")}=${key.replace(/\//ig, "%2F")}`)); - } - dataMember = viewElement.label; - } else { - // just a member, not a list - const pathSegment = (i === 0 - ? `/${referencedModule.name}:${viewElement.label.replace(/\//ig, "%2F")}` - : `/${viewElement.label.replace(/\//ig, "%2F")}`); - resultingDataUrls.push(...dataUrls.map(dataUrl => dataUrl + pathSegment)); - dataMember = viewElement.label; - } - dataUrls = resultingDataUrls; - } else { - data = []; - for (let j = 0; j < dataUrls.length; ++j) { - const dataUrl = dataUrls[j]; - const restResult = (await restService.getConfigData(dataUrl)); - if (restResult.data == null || checkResponseCode(restResult)) { - const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]["error-message"] || ""; - throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); - } - let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; - if (dataRaw === undefined) { - dataRaw = restResult.data[dataMember!]; - } - dataRaw = dataRaw instanceof Array - ? dataRaw[0] - : dataRaw; - data.push(dataRaw); - } - // BUG UUID ist nicht in den elements enthalten !!!!!! - const key = viewElement && viewElement.label || pathPart; - return { - view: view!, - data: data, - key: key, - }; - } +export const postProcessDisplaySpecificationActionCreator = (vPath: string, viewData: any, displaySpecification: DisplaySpecification) => async (dispatch: Dispatch, _getState: () => IApplicationStoreState) => { + + if (displaySpecification.displayMode === DisplayModeType.displayAsObject) { + displaySpecification = { + ...displaySpecification, + viewSpecification: await filterViewElements(vPath, viewData, displaySpecification.viewSpecification), + }; } - return null; -} - -const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification): ViewSpecification =>{ - - // check if-feature | when | and resolve all references. - view = { ...view }; - view.elements = Object.keys(view.elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { - const resolveHistory : ViewElement[] = []; - let elm = view.elements[cur]; - const key = defaultNS && cur.replace(new RegExp(`^${defaultNS}:`, "i"),"") || cur; - while (isViewElementReference(elm)) { - const result = (elm.ref(vPath)); - if (result) { - const [referencedElement, referencedPath] = result; - if (resolveHistory.some(hist => hist === referencedElement)) { - console.error(`Circle reference found at: ${vPath}`, resolveHistory); - break; - } - elm = referencedElement; - vPath = referencedPath; - resolveHistory.push(elm); - } - } - - acc[key] = { ...elm, id: key }; - - return acc; - }, {}); - return view; -} - -const flatenViewElements = (defaultNS: string | null, parentPath: string, elements: { [name: string]: ViewElement }, views: ViewSpecification[], currentPath: string ): { [name: string]: ViewElement } => { - if (!elements) return {}; - return Object.keys(elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { - const elm = elements[cur]; - // remove the detault namespace, and only the default namespace, sine it seems that this is also not in the restconf response - const elmKey = defaultNS && elm.id.replace(new RegExp(`^${defaultNS}:`, "i"), "") || elm.id; - const key = parentPath ? `${parentPath}.${elmKey}` : elmKey; - - if (isViewElementRpc(elm)) { - console.warn(`Flaten of RFC not supported ! [${currentPath}][${elm.label}]`); - return acc; - } else if (isViewElementObjectOrList(elm)) { - const view = views[+elm.viewId]; - const inner = view && flatenViewElements(defaultNS, key, view.elements, views, `${currentPath}/${view.name}`); - inner && Object.keys(inner).forEach(k => (acc[k] = inner[k])); - } else if (isViewElementChoise(elm)) { - acc[key] = { - ...elm, - id: key, - cases: Object.keys(elm.cases).reduce<{ [name: string]: ViewElementChoiseCase }>((accCases, curCases) => { - const caseElement = elm.cases[curCases]; - accCases[curCases] = { - ...caseElement, - // Hint: do not use key it contains elmKey, which shell be omitted for cases. - elements: flatenViewElements(defaultNS, /*key*/ parentPath, caseElement.elements, views, `${currentPath}/${elm.label}`) - }; - return accCases; - }, {}), - }; - } else { - acc[key] = { - ...elm, - id: key, - }; - } - return acc; - }, {}); + dispatch(new UpdateViewDescription(vPath, viewData, displaySpecification)); }; export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - const { configuration: { deviceDescription: { nodeId, modules, views } }, framework: { navigationState } } = getState(); + const { configuration: { deviceDescription: { nodeId, modules, views } } } = getState(); let dataPath = `/rests/data/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; let inputViewSpecification: ViewSpecification | undefined = undefined; @@ -291,26 +142,26 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: try { for (let ind = 0; ind < pathParts.length; ++ind) { const [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; + if (ind === 0) { defaultNS = namespace; } viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (viewElement.isList && !key) { if (pathParts.length - 1 > ind) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No key for list [" + property + "]"); - } else if (vPath.endsWith("[]") && pathParts.length - 1 === ind) { + throw new Error('No key for list [' + property + ']'); + } else if (vPath.endsWith('[]') && pathParts.length - 1 === ind) { // empty key is used for new element - if (viewElement && "viewId" in viewElement) viewSpecification = views[+viewElement.viewId]; + if (viewElement && 'viewId' in viewElement) viewSpecification = views[+viewElement.viewId]; const data = Object.keys(viewSpecification.elements).reduce<{ [name: string]: any }>((acc, cur) => { const elm = viewSpecification.elements[cur]; if (elm.default) { - acc[elm.id] = elm.default || "" + acc[elm.id] = elm.default || ''; } return acc; }, {}); @@ -319,13 +170,13 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: const ds: DisplaySpecification = { displayMode: DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), - keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined + keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, }; // update display specification - return dispatch(new UpdatViewDescription(vPath, data, ds)); + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, data, ds)); } - if (viewElement && isViewElementList(viewElement) && viewSpecification.parentView === "0") { + if (viewElement && isViewElementList(viewElement) && viewSpecification.parentView === '0') { // check if there is a reference as key const listSpecification = views[+viewElement.viewId]; const keyElement = viewElement.key && listSpecification.elements[viewElement.key]; @@ -338,35 +189,35 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: throw new Error(`Key property not found for [${keyElement.referencePath}].`); } dispatch(new EnableValueSelector(refList.view, refList.data, refList.key, (refKey) => { - window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, "%2F")}]`))); + window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, '%2F')}]`))); })); } else { - // Found a list at root level of a module w/o a refenrece key. + // Found a list at root level of a module w/o a reference key. dataPath += `?content=config&fields=${encodeURIComponent(viewElement.id)}(${encodeURIComponent(viewElement.key || '')})`; const restResult = (await restService.getConfigData(dataPath)); - if (restResult && restResult.status === 200 && restResult.data && restResult.data[viewElement.id] ){ - // spoof the not existing view here - const refData = restResult.data[viewElement.id]; - const refView : ViewSpecification = { - id: "-1", - canEdit: false, + if (restResult && restResult.status === 200 && restResult.data && restResult.data[viewElement.id] ) { + // spoof the not existing view here + const refData = restResult.data[viewElement.id]; + const refView : ViewSpecification = { + id: '-1', + canEdit: false, + config: false, + language: 'en-US', + elements: { + [viewElement.key!] : { + uiType: 'string', config: false, - language: "en-US", - elements: { - [viewElement.key!] : { - uiType: "string", - config: false, - id: viewElement.key, - label: viewElement.key, - isList: true, - } as ViewElementString - } - }; - dispatch(new EnableValueSelector(refView, refData, viewElement.key!, (refKey) => { - window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, "%2F")}]`))); - })); + id: viewElement.key, + label: viewElement.key, + isList: true, + } as ViewElementString, + }, + }; + dispatch(new EnableValueSelector(refView, refData, viewElement.key!, (refKey) => { + window.setTimeout(() => dispatch(new PushAction(`${vPath}[${refKey.replace(/\//ig, '%2F')}]`))); + })); } else { - throw new Error("Found a list at root level of a module and could not determine the keys."); + throw new Error('Found a list at root level of a module and could not determine the keys.'); } dispatch(new SetCollectingSelectionData(false)); } @@ -374,31 +225,30 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: } extractList = true; } else { - // normal case + // normal case & replaces unicode %2C if present + dataPath += `/${property}${key ? `=${key.replace(/\%2C/g, ',').replace(/\//ig, '%2F')}` : ''}`; + // in case of the root element the required namespace will be added later, // while extracting the data - - dataPath += `/${property}${key ? `=${key.replace(/\%2C/g, ",").replace(/\//ig, "%2F")}` : ""}`; - dataMember = namespace === defaultNS ? viewElement.label : `${namespace}:${viewElement.label}`; extractList = false; } - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; - } else if (viewElement.uiType === "rpc") { + } else if (viewElement.uiType === 'rpc') { viewSpecification = views[+(viewElement.inputViewId || 0)]; // create new instance & flaten inputViewSpecification = viewElement.inputViewId != null && { ...views[+(viewElement.inputViewId || 0)], - elements: flatenViewElements(defaultNS, "", views[+(viewElement.inputViewId || 0)].elements, views, viewElement.label), + elements: flattenViewElements(defaultNS, '', views[+(viewElement.inputViewId || 0)].elements, views, viewElement.label), } || undefined; outputViewSpecification = viewElement.outputViewId != null && { ...views[+(viewElement.outputViewId || 0)], - elements: flatenViewElements(defaultNS, "", views[+(viewElement.outputViewId || 0)].elements, views, viewElement.label), + elements: flattenViewElements(defaultNS, '', views[+(viewElement.outputViewId || 0)].elements, views, viewElement.label), } || undefined; } @@ -406,7 +256,7 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: let data: any = {}; // do not get any data from netconf if there is no view specified || this is the root element [0] || this is an rpc - if (viewSpecification && !(viewSpecification.id === "0" || viewElement!.uiType === "rpc")) { + if (viewSpecification && !(viewSpecification.id === '0' || viewElement!.uiType === 'rpc')) { const restResult = (await restService.getConfigData(dataPath)); if (!restResult.data) { // special case: if this is a list without any response @@ -418,21 +268,21 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: const ds: DisplaySpecification = { displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), - keyProperty: viewElement.key + keyProperty: viewElement.key, }; // update display specification - return dispatch(new UpdatViewDescription(vPath, [], ds)); + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, [], ds)); } throw new Error(`Did not get response from Server. Status: [${restResult.status}]`); } else if (checkResponseCode(restResult)) { - const message = restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]["error-message"] || ""; + const message = restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${restResult.status}]\n${message}`); } else { - // https://tools.ietf.org/html/rfc7951#section-4 the root element may countain a namesapce or not ! + // https://tools.ietf.org/html/rfc7951#section-4 the root element may contain a namespace or not ! data = restResult.data[`${defaultNS}:${dataMember!}`]; if (data === undefined) { - data = restResult.data[dataMember!]; // extract dataMember w/o namespace + data = restResult.data[dataMember!]; // extract dataMember w/o namespace } } @@ -446,19 +296,21 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: ? data[viewElement!.id] || data[viewElement!.label] || [] // if the list is empty, it does not exist : data; - } else if (viewElement! && viewElement!.uiType === "rpc") { + } else if (viewElement! && viewElement!.uiType === 'rpc') { // set data to defaults data = {}; - inputViewSpecification && Object.keys(inputViewSpecification.elements).forEach(key => { - const elm = inputViewSpecification && inputViewSpecification.elements[key]; - if (elm && elm.default != undefined) { - data[elm.id] = elm.default; - } - }); + if (inputViewSpecification) { + Object.keys(inputViewSpecification.elements).forEach(key => { + const elm = inputViewSpecification && inputViewSpecification.elements[key]; + if (elm && elm.default != undefined) { + data[elm.id] = elm.default; + } + }); + } } // create display specification - const ds: DisplaySpecification = viewElement! && viewElement!.uiType === "rpc" + const ds: DisplaySpecification = viewElement! && viewElement!.uiType === 'rpc' ? { dataPath, displayMode: DisplayModeType.displayAsRPC, @@ -470,16 +322,18 @@ export const updateViewActionAsyncCreator = (vPath: string) => async (dispatch: displayMode: extractList ? DisplayModeType.displayAsList : DisplayModeType.displayAsObject, viewSpecification: resolveViewDescription(defaultNS, vPath, viewSpecification), keyProperty: isViewElementList(viewElement!) && viewElement.key || undefined, - apidocPath: isViewElementList(viewElement!) && `/apidoc/explorer/index.html?urls.primaryName=$$$standard$$$#/mounted%20${nodeId}%20${viewElement!.module || 'MODULE_NOT_DEFINED'}/$$$action$$$_${dataPath.replace(/^\//,'').replace(/[\/=\-\:]/g,'_')}_${viewElement! != null ? `${viewElement.id.replace(/[\/=\-\:]/g,'_')}_` : '' }` || undefined, + + // eslint-disable-next-line max-len + apidocPath: isViewElementList(viewElement!) && `/apidoc/explorer/index.html?urls.primaryName=$$$standard$$$#/mounted%20${nodeId}%20${viewElement!.module || 'MODULE_NOT_DEFINED'}/$$$action$$$_${dataPath.replace(/^\//, '').replace(/[\/=\-\:]/g, '_')}_${viewElement! != null ? `${viewElement.id.replace(/[\/=\-\:]/g, '_')}_` : '' }` || undefined, }; // update display specification - return dispatch(new UpdatViewDescription(vPath, data, ds)); - // https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01] - // https://beta.just-run.it/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]/lp + return dispatch(postProcessDisplaySpecificationActionCreator(vPath, data, ds)); + // https://server.com/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01] + // https://server.com/#/configuration/Sim12600/core-model:network-element/ltp[LTP-MWPS-TTP-01]/lp } catch (error) { history.back(); - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not process ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not process ${dataPath}` })); dispatch(new SetCollectingSelectionData(false)); } finally { return; @@ -503,63 +357,63 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async try { for (let ind = 0; ind < pathParts.length; ++ind) { let [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; + if (ind === 0) { defaultNS = namespace; } viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (isViewElementList(viewElement) && !key) { embedList = true; - if (viewElement && viewElement.isList && viewSpecification.parentView === "0") { - throw new Error("Found a list at root level of a module w/o a refenrece key."); + if (viewElement && viewElement.isList && viewSpecification.parentView === '0') { + throw new Error('Found a list at root level of a module w/o a refenrece key.'); } if (pathParts.length - 1 > ind) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No key for list [" + property + "]"); - } else if (vPath.endsWith("[]") && pathParts.length - 1 === ind) { + throw new Error('No key for list [' + property + ']'); + } else if (vPath.endsWith('[]') && pathParts.length - 1 === ind) { // handle new element with any number of arguments - let keyList = viewElement.key?.split(" "); - let dataPathParam = keyList?.map(id => data[id]).join(","); - key = viewElement.key && String(dataPathParam) || ""; + let keyList = viewElement.key?.split(' '); + let dataPathParam = keyList?.map(id => data[id]).join(','); + key = viewElement.key && String(dataPathParam) || ''; isNew = key; if (!key) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No value for key [" + viewElement.key + "] in list [" + property + "]"); + throw new Error('No value for key [' + viewElement.key + '] in list [' + property + ']'); } } } - dataPath += `/${property}${key ? `=${key.replace(/\//ig, "%2F")}` : ""}`; + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; dataMember = viewElement.label; embedList = false; - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; } } // remove read-only elements - const removeReadOnlyElements = (viewSpecification: ViewSpecification, isList: boolean, data: any) => { + const removeReadOnlyElements = (pViewSpecification: ViewSpecification, isList: boolean, pData: any) => { if (isList) { - return data.map((elm : any) => removeReadOnlyElements(viewSpecification, false, elm)); + return pData.map((elm : any) => removeReadOnlyElements(pViewSpecification, false, elm)); } else { - return Object.keys(data).reduce<{[key: string]: any}>((acc, cur)=>{ - const [nsOrName, name] = cur.split(':',1); - const element = viewSpecification.elements[cur] || viewSpecification.elements[nsOrName] || viewSpecification.elements[name]; - if (!element && process.env.NODE_ENV === "development" ) { - throw new Error("removeReadOnlyElements: Could not determine elment for data."); + return Object.keys(pData).reduce<{ [key: string]: any }>((acc, cur)=>{ + const [nsOrName, name] = cur.split(':', 1); + const element = pViewSpecification.elements[cur] || pViewSpecification.elements[nsOrName] || pViewSpecification.elements[name]; + if (!element && process.env.NODE_ENV === 'development' ) { + throw new Error('removeReadOnlyElements: Could not determine elment for data.'); } if (element && element.config) { - if (element.uiType==="object") { + if (element.uiType === 'object') { const view = views[+element.viewId]; if (!view) { - throw new Error("removeReadOnlyElements: Internal Error could not determine viewId: "+element.viewId); + throw new Error('removeReadOnlyElements: Internal Error could not determine viewId: ' + element.viewId); } - acc[cur] = removeReadOnlyElements(view, element.isList != null && element.isList, data[cur]); + acc[cur] = removeReadOnlyElements(view, element.isList != null && element.isList, pData[cur]); } else { - acc[cur] = data[cur]; + acc[cur] = pData[cur]; } } return acc; @@ -580,10 +434,10 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async : data; // do not extract root member (0) - if (viewSpecification && viewSpecification.id !== "0") { + if (viewSpecification && viewSpecification.id !== '0') { const updateResult = await restService.setConfigData(dataPath, { [`${currentNS}:${dataMember!}`]: data }); // addDataMember using currentNS if (checkResponseCode(updateResult)) { - const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]["error-message"] || ""; + const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); } } @@ -600,10 +454,10 @@ export const updateDataActionAsyncCreator = (vPath: string, data: any) => async }; // update display specification - return dispatch(new UpdatViewDescription(vPath, data, ds)); + return dispatch(new UpdateViewDescription(vPath, data, ds)); } catch (error) { history.back(); - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not change ${dataPath}` })); } finally { dispatch(new SetCollectingSelectionData(false)); @@ -619,57 +473,53 @@ export const removeElementActionAsyncCreator = (vPath: string) => async (dispatc let viewElement: ViewElement; let currentNS: string | null = null; - let defaultNS: string | null = null; - + dispatch(new SetCollectingSelectionData(true)); try { for (let ind = 0; ind < pathParts.length; ++ind) { let [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (isViewElementList(viewElement) && !key) { - if (viewElement && viewElement.isList && viewSpecification.parentView === "0") { - throw new Error("Found a list at root level of a module w/o a refenrece key."); + if (viewElement && viewElement.isList && viewSpecification.parentView === '0') { + throw new Error('Found a list at root level of a module w/o a reference key.'); } if (pathParts.length - 1 > ind) { dispatch(new SetCollectingSelectionData(false)); - throw new Error("No key for list [" + property + "]"); - } else if (vPath.endsWith("[]") && pathParts.length - 1 === ind) { + throw new Error('No key for list [' + property + ']'); + } else if (vPath.endsWith('[]') && pathParts.length - 1 === ind) { // remove the whole table } } - dataPath += `/${property}${key ? `=${key.replace(/\//ig, "%2F")}` : ""}`; + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; - } else if (viewElement.uiType === "rpc") { + } else if (viewElement.uiType === 'rpc') { viewSpecification = views[+(viewElement.inputViewId || 0)]; } } const updateResult = await restService.removeConfigElement(dataPath); if (checkResponseCode(updateResult)) { - const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]["error-message"] || ""; + const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); } } catch (error) { - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not remove ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not remove ${dataPath}` })); } finally { dispatch(new SetCollectingSelectionData(false)); } - - }; export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async (dispatch: Dispatch, getState: () => IApplicationStoreState) => { const pathParts = splitVPath(vPath, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - const { configuration: { deviceDescription: { nodeId, views }, viewDescription: oldViewDescription } } = getState(); + const { configuration: { deviceDescription: { nodeId, views } } } = getState(); let dataPath = `/rests/operations/network-topology:network-topology/topology=topology-netconf/node=${nodeId}/yang-ext:mount`; let viewSpecification: ViewSpecification = views[0]; let viewElement: ViewElement; @@ -684,17 +534,17 @@ export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async try { for (let ind = 0; ind < pathParts.length; ++ind) { let [property, key] = pathParts[ind]; - const namespaceInd = property && property.indexOf(":") || -1; + const namespaceInd = property && property.indexOf(':') || -1; const namespace: string | null = namespaceInd > -1 ? (currentNS = property.slice(0, namespaceInd)) : currentNS; - if (ind === 0) { defaultNS = namespace }; + if (ind === 0) { defaultNS = namespace; } viewElement = viewSpecification.elements[property] || viewSpecification.elements[`${namespace}:${property}`]; - if (!viewElement) throw Error("Property [" + property + "] does not exist."); + if (!viewElement) throw Error('Property [' + property + '] does not exist.'); if (isViewElementList(viewElement) && !key) { embedList = true; // if (viewElement && viewElement.isList && viewSpecification.parentView === "0") { - // throw new Error("Found a list at root level of a module w/o a refenrece key."); + // throw new Error("Found a list at root level of a module w/o a reference key."); // } // if (pathParts.length - 1 > ind) { // dispatch(new SetCollectingSelectionData(false)); @@ -710,28 +560,28 @@ export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async // } } - dataPath += `/${property}${key ? `=${key.replace(/\//ig, "%2F")}` : ""}`; + dataPath += `/${property}${key ? `=${key.replace(/\//ig, '%2F')}` : ''}`; dataMember = viewElement.label; embedList = false; - if (viewElement && "viewId" in viewElement) { + if (viewElement && 'viewId' in viewElement) { viewSpecification = views[+viewElement.viewId]; - } else if (viewElement.uiType === "rpc") { + } else if (viewElement.uiType === 'rpc') { viewSpecification = views[+(viewElement.inputViewId || 0)]; } } // re-inflate formerly flatten rpc data data = data && Object.keys(data).reduce < { [name: string ]: any }>((acc, cur) => { - const pathParts = cur.split("."); + const innerPathParts = cur.split('.'); let pos = 0; const updatePath = (obj: any, key: string) => { - obj[key] = (pos >= pathParts.length) + obj[key] = (pos >= innerPathParts.length) ? data[cur] - : updatePath(obj[key] || {}, pathParts[pos++]); + : updatePath(obj[key] || {}, innerPathParts[pos++]); return obj; - } - updatePath(acc, pathParts[pos++]); + }; + updatePath(acc, innerPathParts[pos++]); return acc; }, {}) || null; @@ -746,22 +596,22 @@ export const executeRpcActionAsyncCreator = (vPath: string, data: any) => async : data; // do not post root member (0) - if ((viewSpecification && viewSpecification.id !== "0") || (dataMember! && !data)) { + if ((viewSpecification && viewSpecification.id !== '0') || (dataMember! && !data)) { const updateResult = await restService.executeRpc(dataPath, { [`${defaultNS}:input`]: data || {} }); if (checkResponseCode(updateResult)) { - const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]["error-message"] || ""; + const message = updateResult.data && updateResult.data.errors && updateResult.data.errors.error && updateResult.data.errors.error[0] && updateResult.data.errors.error[0]['error-message'] || ''; throw new Error(`Server Error. Status: [${updateResult.status}]\n${message || updateResult.message || ''}`); } - dispatch(new UpdatOutputData(updateResult.data)); + dispatch(new UpdateOutputData(updateResult.data)); } else { - throw new Error(`There is NO RPC specified.`); + throw new Error('There is NO RPC specified.'); } // // update display specification // return } catch (error) { - dispatch(new AddErrorInfoAction({ title: "Problem", message: error.message || `Could not change ${dataPath}` })); + dispatch(new AddErrorInfoAction({ title: 'Problem', message: error.message || `Could not change ${dataPath}` })); } finally { dispatch(new SetCollectingSelectionData(false)); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg b/sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg new file mode 100644 index 000000000..1b74cc479 --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/assets/icons/configurationAppIcon.svg @@ -0,0 +1,20 @@ +<!-- highstreet technologies GmbH colour scheme
+ Grey #565656
+ LBlue #36A9E1
+ DBlue #246DA2
+ Green #003F2C / #006C4B
+ Yellw #C8D400
+ Red #D81036
+-->
+
+
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 142 140" >
+<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="scale(10,10)">
+
+
+ <path fill="#565656" d="M7.887,9.025 C7.799,8.449 7.569,7.92 7.229,7.475 L7.995,6.71 L7.307,6.023 L6.536,6.794 C6.093,6.467 5.566,6.245 4.994,6.161 L4.994,5.066 L4.021,5.066 L4.021,6.155 C3.444,6.232 2.913,6.452 2.461,6.777 L1.709,6.024 L1.021,6.712 L1.761,7.452 C1.411,7.901 1.175,8.437 1.087,9.024 L0.062,9.024 L0.062,9.025 L0.062,9.998 L1.08,9.998 C1.162,10.589 1.396,11.132 1.744,11.587 L1.02,12.31 L1.708,12.997 L2.437,12.268 C2.892,12.604 3.432,12.83 4.02,12.91 L4.02,13.958 L4.993,13.958 L4.993,12.904 C5.576,12.818 6.11,12.589 6.56,12.252 L7.306,12.999 L7.994,12.311 L7.248,11.564 C7.586,11.115 7.812,10.581 7.893,10 L8.952,10 L8.952,9.998 L8.952,9.026 L7.887,9.026 L7.887,9.025 Z M4.496,11.295 C3.512,11.295 2.715,10.497 2.715,9.512 C2.715,8.528 3.512,7.73 4.496,7.73 C5.481,7.73 6.28,8.528 6.28,9.512 C6.28,10.497 5.481,11.295 4.496,11.295 L4.496,11.295 Z" ></path>
+
+ <path fill="#C8D400" d="m 12.231 4.17 l 1.09 -0.281 l -0.252 -0.979 l -1.091 0.282 c -0.118 -0.24 -0.265 -0.47 -0.461 -0.672 c -0.192 -0.196 -0.415 -0.344 -0.647 -0.464 l 0.301 -1.079 l -0.973 -0.271 l -0.299 1.072 c -0.541 -0.043 -1.091 0.078 -1.566 0.382 l -0.76 -0.776 l -0.721 0.707 l 0.756 0.77 c -0.326 0.47 -0.469 1.024 -0.441 1.575 l -1.04 0.268 l 0.252 0.977 l 1.038 -0.268 c 0.117 0.243 0.266 0.475 0.465 0.678 c 0.203 0.208 0.439 0.362 0.686 0.485 l -0.289 1.039 l 0.971 0.271 l 0.293 -1.048 c 0.542 0.033 1.092 -0.1 1.563 -0.415 l 0.771 0.786 l 0.72 -0.707 l -0.776 -0.791 c 0.307 -0.465 0.439 -1.006 0.41 -1.541 l 0 0 z m -2.517 1.617 c -0.823 0 -1.491 -0.669 -1.491 -1.493 c 0 -0.822 0.668 -1.489 1.491 -1.489 c 0.822 0 1.49 0.667 1.49 1.489 c 0 0.824 -0.668 1.493 -1.49 1.493 l 0 0 z" ></path>
+
+</g>
+</svg>
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts b/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts index 26c3944c9..7187c0a4e 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/baseProps.ts @@ -16,13 +16,13 @@ * ============LICENSE_END========================================================================== */ -import { ViewElement } from "../models/uiModels"; +import { ViewElement } from '../models/uiModels'; export type BaseProps<TValue = string> = { - value: ViewElement, - inputValue: TValue, - readOnly: boolean, - disabled: boolean, - onChange(newValue: TValue): void; - isKey?: boolean + value: ViewElement; + inputValue: TValue; + readOnly: boolean; + disabled: boolean; + onChange(newValue: TValue): void; + isKey?: boolean; };
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx index 8ce3106a6..b176e5db5 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/ifWhenTextInput.tsx @@ -16,82 +16,86 @@ * ============LICENSE_END========================================================================== */ -import { ViewElementBase } from "models/uiModels"; -import { - TextField, - InputAdornment, - Input, - Tooltip, - Divider, - IconButton, - InputBase, - Paper, - Theme, - FormControl, - InputLabel, - FormHelperText, -} from "@mui/material"; +import React from 'react'; +import InputAdornment from '@mui/material/InputAdornment'; +import Input, { InputProps } from '@mui/material/Input'; +import Tooltip from '@mui/material/Tooltip'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import FormHelperText from '@mui/material/FormHelperText'; + import makeStyles from '@mui/styles/makeStyles'; import createStyles from '@mui/styles/createStyles'; -import * as React from 'react'; -import { faAdjust } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { InputProps } from "@mui/material/Input"; -const useStyles = makeStyles((theme: Theme) => +import { faAdjust } from '@fortawesome/free-solid-svg-icons/faAdjust'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { ViewElementBase } from '../models/uiModels'; + +const useStyles = makeStyles(() => createStyles({ iconDark: { - color: '#ff8800' + color: '#ff8800', }, iconLight: { - color: 'orange' + color: 'orange', }, padding: { paddingLeft: 10, - paddingRight: 10 + paddingRight: 10, }, }), ); -type IfwhenProps = InputProps & { +type IfWhenProps = InputProps & { label: string; element: ViewElementBase; helperText: string; error: boolean; - onChangeTooltipVisuability(value: boolean): void; + onChangeTooltipVisibility(value: boolean): void; }; -export const IfWhenTextInput = (props: IfwhenProps) => { +export const IfWhenTextInput = (props: IfWhenProps) => { - const { element, onChangeTooltipVisuability: toogleTooltip, id, label, helperText: errorText, error, style, ...otherProps } = props; + const { element, id, label, helperText: errorText, error, style, ...otherProps } = props; const classes = useStyles(); - const ifFeature = element.ifFeature ? ( - <Tooltip disableInteractive onMouseMove={e => props.onChangeTooltipVisuability(false)} onMouseOut={e => props.onChangeTooltipVisuability(true)} title={element.ifFeature}> + <Tooltip + title={element.ifFeature} + disableInteractive + onMouseMove={() => props.onChangeTooltipVisibility(false)} + onMouseOut={() => props.onChangeTooltipVisibility(true)} + > <InputAdornment position="start"> <FontAwesomeIcon icon={faAdjust} className={classes.iconDark} /> </InputAdornment> </Tooltip> - ) + ) : null; const whenFeature = element.when ? ( - <Tooltip disableInteractive className={classes.padding} onMouseMove={() => props.onChangeTooltipVisuability(false)} onMouseOut={() => props.onChangeTooltipVisuability(true)} title={element.when}> + <Tooltip + title={element.when} + disableInteractive + className={classes.padding} + onMouseMove={() => props.onChangeTooltipVisibility(false)} + onMouseOut={() => props.onChangeTooltipVisibility(true)} + > <InputAdornment className={classes.padding} position="end"> <FontAwesomeIcon icon={faAdjust} className={classes.iconLight}/> </InputAdornment> </Tooltip> - ) + ) : null; return ( <FormControl variant="standard" error={error} style={style}> <InputLabel htmlFor={id} >{label}</InputLabel> - <Input id={id} inputProps={{'aria-label': label+'-input'}} endAdornment={<div>{ifFeature}{whenFeature}</div>} {...otherProps} /> + <Input id={id} inputProps={{ 'aria-label': label + '-input' }} endAdornment={<div>{ifFeature}{whenFeature}</div>} {...otherProps} /> <FormHelperText>{errorText}</FormHelperText> </FormControl> ); -}
\ No newline at end of file +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx index 81c9d6dcd..56fb93cea 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementBoolean.tsx @@ -16,43 +16,48 @@ * ============LICENSE_END========================================================================== */ -import * as React from "react" -import { MenuItem, FormHelperText, Select, FormControl, InputLabel } from "@mui/material"; +import React from 'react'; -import { ViewElementBoolean } from "../models/uiModels"; -import { BaseProps } from "./baseProps"; +import MenuItem from '@mui/material/MenuItem'; +import FormHelperText from '@mui/material/FormHelperText'; +import Select from '@mui/material/Select'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; + +import { ViewElementBoolean } from '../models/uiModels'; +import { BaseProps } from './baseProps'; type BooleanInputProps = BaseProps<boolean>; export const UiElementBoolean = (props: BooleanInputProps) => { - const element = props.value as ViewElementBoolean; + const element = props.value as ViewElementBoolean; - const value = String(props.inputValue).toLowerCase(); - const mandetoryError = element.mandatory && value !== 'true' && value !== 'false'; + const value = String(props.inputValue).toLowerCase(); + const mandatoryError = element.mandatory && value !== 'true' && value !== 'false'; - return (!props.readOnly || element.id != null - ? (<FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + return (!props.readOnly || element.id != null + ? (<FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> <InputLabel htmlFor={`select-${element.id}`} >{element.label}</InputLabel> <Select variant="standard" - aria-label={element.label+'-selection'} + aria-label={element.label + '-selection'} required={!!element.mandatory} - error={mandetoryError} - onChange={(e) => { props.onChange(e.target.value === 'true') }} + error={mandatoryError} + onChange={(e) => { props.onChange(e.target.value === 'true'); }} readOnly={props.readOnly} disabled={props.disabled} value={value} inputProps={{ - name: element.id, - id: `select-${element.id}`, + name: element.id, + id: `select-${element.id}`, }} > <MenuItem value={'true'} aria-label="true">{element.trueValue || 'True'}</MenuItem> <MenuItem value={'false'} aria-label="false">{element.falseValue || 'False'}</MenuItem> </Select> - <FormHelperText>{mandetoryError ? "Value is mandetory" : ""}</FormHelperText> + <FormHelperText>{mandatoryError ? 'Value is mandatory' : ''}</FormHelperText> </FormControl>) - : null - ); -}
\ No newline at end of file + : null + ); +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx index 5937ed7b3..669ddff63 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementLeafList.tsx @@ -16,19 +16,23 @@ * ============LICENSE_END========================================================================== */ -import * as React from "react" -import { FormControl, InputLabel, Paper, Chip, FormHelperText, Dialog, DialogTitle, DialogContentText, DialogActions, Button, DialogContent } from "@mui/material"; +import React from 'react'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import Chip from '@mui/material/Chip'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; + import makeStyles from '@mui/styles/makeStyles'; import AddIcon from '@mui/icons-material/Add'; import { Theme } from '@mui/material/styles'; -import { ViewElement } from "../models/uiModels"; - -import { BaseProps } from "./baseProps"; +import { ViewElement } from '../models/uiModels'; -type LeafListProps = BaseProps<any []> & { - getEditorForViewElement: (uiElement: ViewElement) => (null | React.ComponentType<BaseProps<any>>) -}; +import { BaseProps } from './baseProps'; const useStyles = makeStyles((theme: Theme) => { const light = theme.palette.mode === 'light'; @@ -50,93 +54,93 @@ const useStyles = makeStyles((theme: Theme) => { margin: theme.spacing(0.5), }, underline: { - '&:after': { - borderBottom: `2px solid ${theme.palette.primary.main}`, - left: 0, - bottom: 0, - // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 - content: '""', - position: 'absolute', - right: 0, - transform: 'scaleX(0)', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shorter, - easing: theme.transitions.easing.easeOut, - }), - pointerEvents: 'none', // Transparent to the hover style. - }, - '&.Mui-focused:after': { - transform: 'scaleX(1)', - }, - '&.Mui-error:after': { - borderBottomColor: theme.palette.error.main, - transform: 'scaleX(1)', // error is always underlined in red - }, - '&:before': { + '&:after': { + borderBottom: `2px solid ${theme.palette.primary.main}`, + left: 0, + bottom: 0, + // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 + content: '""', + position: 'absolute', + right: 0, + transform: 'scaleX(0)', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shorter, + easing: theme.transitions.easing.easeOut, + }), + pointerEvents: 'none', // Transparent to the hover style. + }, + '&.Mui-focused:after': { + transform: 'scaleX(1)', + }, + '&.Mui-error:after': { + borderBottomColor: theme.palette.error.main, + transform: 'scaleX(1)', // error is always underlined in red + }, + '&:before': { + borderBottom: `1px solid ${bottomLineColor}`, + left: 0, + bottom: 0, + // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 + content: '"\\00a0"', + position: 'absolute', + right: 0, + transition: theme.transitions.create('border-bottom-color', { + duration: theme.transitions.duration.shorter, + }), + pointerEvents: 'none', // Transparent to the hover style. + }, + '&:hover:not($disabled):before': { + borderBottom: `2px solid ${theme.palette.text.primary}`, + // Reset on touch devices, it doesn't add specificity + // eslint-disable-next-line @typescript-eslint/naming-convention + '@media (hover: none)': { borderBottom: `1px solid ${bottomLineColor}`, - left: 0, - bottom: 0, - // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 - content: '"\\00a0"', - position: 'absolute', - right: 0, - transition: theme.transitions.create('border-bottom-color', { - duration: theme.transitions.duration.shorter, - }), - pointerEvents: 'none', // Transparent to the hover style. - }, - '&:hover:not($disabled):before': { - borderBottom: `2px solid ${theme.palette.text.primary}`, - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - borderBottom: `1px solid ${bottomLineColor}`, - }, - }, - '&.Mui-disabled:before': { - borderBottomStyle: 'dotted', }, }, - }) + '&.Mui-disabled:before': { + borderBottomStyle: 'dotted', + }, + }, + }); }); +type LeafListProps = BaseProps<any []> & { + getEditorForViewElement: (uiElement: ViewElement) => (null | React.ComponentType<BaseProps<any>>); +}; + export const UiElementLeafList = (props: LeafListProps) => { const { value: element, inputValue, onChange } = props; const classes = useStyles(); const [open, setOpen] = React.useState(false); - const [editorValue, setEditorValue] = React.useState(""); + const [editorValue, setEditorValue] = React.useState(''); const [editorValueIndex, setEditorValueIndex] = React.useState(-1); - - const handleClickOpen = () => { - setOpen(true); - }; - const handleClose = () => { setOpen(false); }; const onApplyButton = () => { - if (editorValue != null && editorValue != "" && editorValueIndex < 0) { - props.onChange([ - ...inputValue, - editorValue, - ]); - } else if (editorValue != null && editorValue != "") { - props.onChange([ - ...inputValue.slice(0, editorValueIndex), - editorValue, - ...inputValue.slice(editorValueIndex+1), - ]); - } - setOpen(false); + if (editorValue != null && editorValue != '' && editorValueIndex < 0) { + props.onChange([ + ...inputValue, + editorValue, + ]); + } else if (editorValue != null && editorValue != '') { + props.onChange([ + ...inputValue.slice(0, editorValueIndex), + editorValue, + ...inputValue.slice(editorValueIndex + 1), + ]); + } + setOpen(false); }; const onDelete = (index : number) => { const newValue : any[] = [ ...inputValue.slice(0, index), - ...inputValue.slice(index+1), + ...inputValue.slice(index + 1), ]; onChange(newValue); }; @@ -151,15 +155,15 @@ export const UiElementLeafList = (props: LeafListProps) => { { !props.readOnly ? <li> <Chip icon={<AddIcon />} - label={"Add"} + label={'Add'} className={classes.chip} size="small" color="secondary" onClick={ () => { setOpen(true); - setEditorValue(""); + setEditorValue(''); setEditorValueIndex(-1); - } + } } /> </li> : null } @@ -172,24 +176,24 @@ export const UiElementLeafList = (props: LeafListProps) => { label={String(val)} onDelete={ !props.readOnly ? () => { onDelete(ind); } : undefined } onClick={ !props.readOnly ? () => { - setOpen(true); - setEditorValue(val); - setEditorValueIndex(ind); - } : undefined + setOpen(true); + setEditorValue(val); + setEditorValueIndex(ind); + } : undefined } /> </li> - )) + )) } </ul> {/* <FormHelperText>{ "Value is mandetory"}</FormHelperText> */} </FormControl> <Dialog open={open} onClose={handleClose} aria-labelledby="form-dialog-title"> - <DialogTitle id="form-dialog-title">{editorValueIndex < 0 ? "Add new value" : "Edit value" } </DialogTitle> + <DialogTitle id="form-dialog-title">{editorValueIndex < 0 ? 'Add new value' : 'Edit value' } </DialogTitle> <DialogContent> { ValueEditor && <ValueEditor inputValue={ editorValue } - value={{ ...element, isList: false}} + value={{ ...element, isList: false }} disabled={false} readOnly={props.readOnly} onChange={ setEditorValue } @@ -197,7 +201,7 @@ export const UiElementLeafList = (props: LeafListProps) => { </DialogContent> <DialogActions> <Button color="inherit" onClick={ handleClose }> Cancel </Button> - <Button disabled={editorValue == null || editorValue === "" } onClick={ onApplyButton } color="secondary"> {editorValueIndex < 0 ? "Add" : "Apply"} </Button> + <Button disabled={editorValue == null || editorValue === '' } onClick={ onApplyButton } color="secondary"> {editorValueIndex < 0 ? 'Add' : 'Apply'} </Button> </DialogActions> </Dialog> </> diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx index 76c11f6e5..b0342788f 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementNumber.tsx @@ -16,14 +16,14 @@ * ============LICENSE_END========================================================================== */ -import { ViewElementNumber } from "models/uiModels"; +import React from 'react'; +import { ViewElementNumber } from "../models/uiModels"; import { Tooltip, InputAdornment } from "@mui/material"; -import * as React from 'react'; import { BaseProps } from "./baseProps"; import { IfWhenTextInput } from "./ifWhenTextInput"; -import { checkRange } from "./verifyer"; +import { checkRange } from "../utilities/verifyer"; -type numberInputProps = BaseProps<any>; +type numberInputProps = BaseProps<number>; export const UiElementNumber = (props: numberInputProps) => { @@ -49,12 +49,12 @@ export const UiElementNumber = (props: numberInputProps) => { setError(true); setHelperText("Input is not a number."); } - props.onChange(data); + props.onChange(num); } return ( <Tooltip disableInteractive title={isTooltipVisible ? element.description || '' : ''}> - <IfWhenTextInput element={element} onChangeTooltipVisuability={setTooltipVisibility} + <IfWhenTextInput element={element} onChangeTooltipVisibility={setTooltipVisibility} spellCheck={false} autoFocus margin="dense" id={element.id} label={element.label} type="text" value={props.inputValue} style={{ width: 485, marginLeft: 20, marginRight: 20 }} diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx index 9e863f0d0..e3bb8f048 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementReference.tsx @@ -17,16 +17,16 @@ */ import React, { useState } from 'react'; -import { Tooltip, Button, FormControl, Theme } from '@mui/material'; +import { Tooltip, Button, FormControl } from '@mui/material'; import createStyles from '@mui/styles/createStyles'; import makeStyles from '@mui/styles/makeStyles'; import { ViewElement } from '../models/uiModels'; -const useStyles = makeStyles((theme: Theme) => createStyles({ +const useStyles = makeStyles(() => createStyles({ button: { - "justifyContent": "left" + 'justifyContent': 'left', }, })); @@ -37,16 +37,31 @@ type UIElementReferenceProps = { }; export const UIElementReference: React.FC<UIElementReferenceProps> = (props) => { - const classes = useStyles(); - const [disabled, setDisabled] = useState(true); const { element } = props; + const [disabled, setDisabled] = useState(true); + const classes = useStyles(); return ( - <FormControl variant="standard" key={element.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }} onMouseDown={(ev) => { ev.preventDefault(); ev.stopPropagation(); ev.button === 1 && setDisabled(!disabled) }}> + <FormControl + variant="standard" + key={element.id} + style={{ width: 485, marginLeft: 20, marginRight: 20 }} + onMouseDown={(ev) => { + ev.preventDefault(); + ev.stopPropagation(); + if (ev.button === 1) { + setDisabled(!disabled); + } + }}> <Tooltip disableInteractive title={element.description || element.path || ''}> - <Button className={classes.button} aria-label={element.label+'-button'} color="secondary" disabled={props.disabled && disabled} onClick={() => { - props.onOpenReference(element); - }} >{`${element.label}`}</Button> + <Button + className={classes.button} + aria-label={element.label + '-button'} + color="secondary" + disabled={props.disabled && disabled} + onClick={() => { + props.onOpenReference(element); + }} >{`${element.label}`}</Button> </Tooltip> </FormControl> ); -}
\ No newline at end of file +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx index fdf803419..ebd04dab4 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementSelection.tsx @@ -16,45 +16,54 @@ * ============LICENSE_END========================================================================== */ -import * as React from 'react'; +import React from 'react'; import { BaseProps } from './baseProps'; -import { ViewElementSelection } from '../models/uiModels' +import { ViewElementSelection } from '../models/uiModels'; import { FormControl, InputLabel, Select, FormHelperText, MenuItem, Tooltip } from '@mui/material'; type selectionProps = BaseProps; export const UiElementSelection = (props: selectionProps) => { - const element = props.value as ViewElementSelection; + const element = props.value as ViewElementSelection; - let error = ""; - const value = String(props.inputValue); - if (element.mandatory && Boolean(!value)) { - error = "Error"; - } + let error = ''; + const value = String(props.inputValue); + if (element.mandatory && Boolean(!value)) { + error = 'Error'; + } - return (props.readOnly || props.inputValue != null - ? (<FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> - <InputLabel htmlFor={`select-${element.id}`} >{element.label}</InputLabel> + return (props.readOnly || props.inputValue != null + ? (<FormControl variant="standard" style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`select-${element.id}`} >{element.label}</InputLabel> <Select variant="standard" required={!!element.mandatory} error={!!error} - onChange={(e) => { props.onChange(e.target.value as string) }} + onChange={(e) => { props.onChange(e.target.value as string); }} readOnly={props.readOnly} disabled={props.disabled} value={value.toString()} - aria-label={element.label+'-selection'} + aria-label={element.label + '-selection'} inputProps={{ - name: element.id, - id: `select-${element.id}`, + name: element.id, + id: `select-${element.id}`, }} > - {element.options.map(option => ( - <MenuItem key={option.key} value={option.key} aria-label={option.key}><Tooltip disableInteractive title={option.description || '' }><div style={{width:"100%"}}>{option.key}</div></Tooltip></MenuItem> - ))} + {element.options.map(option => ( + <MenuItem + key={option.key} + value={option.key} + aria-label={option.key}> + <Tooltip disableInteractive title={option.description || ''}> + <div style={{ width: '100%' }}> + {option.key} + </div> + </Tooltip> + </MenuItem> + ))} </Select> <FormHelperText>{error}</FormHelperText> </FormControl>) - : null - ); -}
\ No newline at end of file + : null + ); +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx index 4908c41aa..8381d99a4 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementString.tsx @@ -21,7 +21,7 @@ import { Tooltip, TextField } from "@mui/material"; import { ViewElementString } from "../models/uiModels"; import { BaseProps } from "./baseProps"; import { IfWhenTextInput } from "./ifWhenTextInput"; -import { checkRange, checkPattern } from "./verifyer"; +import { checkRange, checkPattern } from "../utilities/verifyer"; type stringEntryProps = BaseProps ; @@ -69,7 +69,7 @@ export const UiElementString = (props: stringEntryProps) => { return ( <Tooltip disableInteractive title={isTooltipVisible ? element.description || '' : ''}> - <IfWhenTextInput element={element} onChangeTooltipVisuability={setTooltipVisibility} + <IfWhenTextInput element={element} onChangeTooltipVisibility={setTooltipVisibility} spellCheck={false} autoFocus margin="dense" id={element.id} label={props?.isKey ? "🔑 " + element.label : element.label} type="text" value={props.inputValue} style={{ width: 485, marginLeft: 20, marginRight: 20 }} diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementUnion.tsx b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementUnion.tsx index 67cd998d7..8d232f5ee 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementUnion.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/components/uiElementUnion.tsx @@ -21,7 +21,7 @@ import { BaseProps } from './baseProps'; import { Tooltip } from '@mui/material'; import { IfWhenTextInput } from './ifWhenTextInput'; import { ViewElementUnion, isViewElementString, isViewElementNumber, isViewElementObject, ViewElementNumber } from '../models/uiModels'; -import { checkRange, checkPattern } from './verifyer'; +import { checkRange, checkPattern } from '../utilities/verifyer'; type UiElementUnionProps = { isKey: boolean } & BaseProps; @@ -77,7 +77,7 @@ export const UIElementUnion = (props: UiElementUnionProps) => { }; return <Tooltip disableInteractive title={isTooltipVisible ? element.description || '' : ''}> - <IfWhenTextInput element={element} onChangeTooltipVisuability={setTooltipVisibility} + <IfWhenTextInput element={element} onChangeTooltipVisibility={setTooltipVisibility} spellCheck={false} autoFocus margin="dense" id={element.id} label={props.isKey ? "🔑 " + element.label : element.label} type="text" value={props.inputValue} onChange={(e: any) => { verifyValues(e.target.value) }} diff --git a/sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts b/sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts deleted file mode 100644 index 0a95cd8ca..000000000 --- a/sdnr/wt/odlux/apps/configurationApp/src/components/verifyer.ts +++ /dev/null @@ -1,280 +0,0 @@ -/** - * ============LICENSE_START======================================================================== - * ONAP : ccsdk feature sdnr wt odlux - * ================================================================================================= - * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. - * ================================================================================================= - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except - * in compliance with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - * ============LICENSE_END========================================================================== - */ - -import { Expression, YangRange, Operator, ViewElementNumber, ViewElementString, isViewElementNumber, isViewElementString } from '../models/uiModels'; - -export type validated = { isValid: boolean, error?: string } - -export type validatedRange = { isValid: boolean, error?: string }; - - -const rangeErrorStartNumber = "The entered number must be"; -const rangeErrorinnerMinTextNumber = "greater or equals than"; -const rangeErrorinnerMaxTextNumber = "less or equals than"; -const rangeErrorEndTextNumber = "."; - -const rangeErrorStartString = "The entered text must have"; -const rangeErrorinnerMinTextString = "no more than"; -const rangeErrorinnerMaxTextString = "less than"; -const rangeErrorEndTextString = " characters."; - -let errorMessageStart = ""; -let errorMessageMiddleMinPart = ""; -let errorMessageMiddleMaxPart = ""; -let errorMessageEnd = ""; - - -export function checkRange(element: ViewElementNumber | ViewElementString, data: number): string { - - //let test1: Operator<YangRange> = { operation: "AND", arguments: [{ operation: "OR", arguments: [{ operation: "AND", arguments: [new RegExp("^z", "g"), new RegExp("z$", "g")] }, new RegExp("^abc", "g"), new RegExp("^123", "g")] }, new RegExp("^def", "g"), new RegExp("^ppp", "g"), new RegExp("^aaa", "g")] }; - //let test1: Operator<YangRange> = { operation: "AND", arguments: [{ operation: "OR", arguments: [{ operation: "AND", arguments: [{ min: -5, max: 10 }, { min: -30, max: -20 }] }, { min: 8, max: 15 }] }] }; - //let test1: Operator<YangRange> = { operation: "OR", arguments: [{ operation: "OR", arguments: [{ min: -50, max: -40 }] }, { min: -30, max: -20 }, { min: 8, max: 15 }] }; - //let test1: Operator<YangRange> = { operation: "AND", arguments: [{ operation: "OR", arguments: [{ min: -5, max: 10 }, { min: 17, max: 23 }] }] }; - - const number = data; - - var expression = undefined; - - if (isViewElementString(element)) { - expression = element.length; - - errorMessageStart = rangeErrorStartString; - errorMessageMiddleMaxPart = rangeErrorinnerMaxTextString; - errorMessageMiddleMinPart = rangeErrorinnerMinTextString; - errorMessageEnd = rangeErrorEndTextString; - - } else if (isViewElementNumber(element)) { - expression = element.range; - - errorMessageStart = rangeErrorStartNumber; - errorMessageMiddleMaxPart = rangeErrorinnerMaxTextNumber; - errorMessageMiddleMinPart = rangeErrorinnerMinTextNumber; - errorMessageEnd = rangeErrorEndTextNumber; - } - - if (expression) { - if (isYangOperator(expression)) { - - const errorMessage = getRangeErrorMessages(expression, data); - return errorMessage; - - } else - if (isYangRange(expression)) { - - if (!isNaN(expression.min)) { - if (number < expression.min) { - return `${errorMessageStart} ${errorMessageMiddleMinPart} ${expression.min}${errorMessageEnd}`; - } - } - - if (!isNaN(expression.max)) { - if (number > expression.max) { - return `${errorMessageStart} ${errorMessageMiddleMaxPart} ${expression.max}${errorMessageEnd}`; - } - } - } - } - - - return ""; -} - -function isYangRange(val: YangRange | Operator<YangRange>): val is YangRange { - return (val as YangRange).min !== undefined; -} - -function isYangOperator(val: YangRange | Operator<YangRange>): val is Operator<YangRange> { - return (val as Operator<YangRange>).operation !== undefined; -} - -function getRangeErrorMessagesRecursively(value: Operator<YangRange>, data: number): string[] { - let currentItteration: string[] = []; - console.log(value); - - // itterate over all elements - for (let i = 0; i < value.arguments.length; i++) { - const element = value.arguments[i]; - - let min = undefined; - let max = undefined; - - let isNumberCorrect = false; - - if (isYangRange(element)) { - - //check found min values - if (!isNaN(element.min)) { - if (data < element.min) { - min = element.min; - } else { - isNumberCorrect = true; - } - } - - // check found max values - if (!isNaN(element.max)) { - if (data > element.max) { - max = element.max; - } else { - isNumberCorrect = true; - } - } - - // construct error messages - if (min != undefined) { - currentItteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMinPart} ${min}`); - } else if (max != undefined) { - currentItteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMaxPart} ${max}`); - - } - - } else if (isYangOperator(element)) { - - //get errormessages from expression - const result = getRangeErrorMessagesRecursively(element, data); - if (result.length === 0) { - isNumberCorrect = true; - } - currentItteration = currentItteration.concat(result); - } - - // if its an OR operation, the number has been checked and min/max are empty (thus not violated) - // delete everything found (because at least one found is correct, therefore all are correct) and break from loop - if (min === undefined && max === undefined && isNumberCorrect && value.operation === "OR") { - - currentItteration.splice(0, currentItteration.length); - break; - } - } - - return currentItteration; -} - -function getRangeErrorMessages(value: Operator<YangRange>, data: number): string { - - const currentItteration = getRangeErrorMessagesRecursively(value, data); - - // build complete error message from found parts - let errormessage = ""; - if (currentItteration.length > 1) { - - currentItteration.forEach((element, index) => { - if (index === 0) { - errormessage = createStartMessage(element); - } else if (index === currentItteration.length - 1) { - errormessage += ` ${element}${errorMessageEnd}`; - } else { - errormessage += `, ${element}` - } - }); - } else if (currentItteration.length == 1) { - errormessage = `${createStartMessage(currentItteration[0])}${errorMessageEnd}`; - } - - return errormessage; -} - -function createStartMessage(element: string) { - - //remove leading or or and from text - if (element.startsWith("and")) - element = element.replace("and", ""); - else if (element.startsWith("or")) - element = element.replace("or", ""); - - return `${errorMessageStart} ${element}`; -} - -export const checkPattern = (expression: RegExp | Operator<RegExp> | undefined, data: string): validated => { - - if (expression) { - if (isRegExp(expression)) { - const isValid = expression.test(data); - if (!isValid) - return { isValid: isValid, error: "The input is in a wrong format." }; - - } else if (isRegExpOperator(expression)) { - const result = isPatternValid(expression, data); - - if (!result) { - return { isValid: false, error: "The input is in a wrong format." }; - } - } - } - - return { isValid: true } -} - -function getRegexRecursively(value: Operator<RegExp>, data: string): boolean[] { - let currentItteration: boolean[] = []; - for (let i = 0; i < value.arguments.length; i++) { - const element = value.arguments[i]; - if (isRegExp(element)) { - // if regex is found, add it to list - currentItteration.push(element.test(data)) - } else if (isRegExpOperator(element)) { - //if RegexExpression is found, try to get regex from it - currentItteration = currentItteration.concat(getRegexRecursively(element, data)); - } - } - - if (value.operation === "OR") { - // if one is true, all are true, all found items can be discarded - let result = currentItteration.find(element => element); - if (result) { - return []; - } - } - return currentItteration; -} - -function isPatternValid(value: Operator<RegExp>, data: string): boolean { - - - // get all regex - const result = getRegexRecursively(value, data); - console.log(value); - - - if (value.operation === "AND") { - // if AND operation is executed... - // no element can be false - const check = result.find(element => element !== true); - if (check) - return false; - else - return true; - } else { - // if OR operation is executed... - // ... just one element must be true - const check = result.find(element => element === true); - if (check) - return true; - else - return false; - - } -} - -function isRegExp(val: RegExp | Operator<RegExp>): val is RegExp { - return (val as RegExp).source !== undefined; -} - -function isRegExpOperator(val: RegExp | Operator<RegExp>): val is Operator<RegExp> { - return (val as Operator<RegExp>).operation !== undefined; -}
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts index 1af699a6b..9cbd9163e 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/configurationAppRootHandler.ts @@ -19,9 +19,9 @@ import { combineActionHandler } from '../../../../framework/src/flux/middleware'; import { IConnectedNetworkElementsState, connectedNetworkElementsActionHandler } from './connectedNetworkElementsHandler'; -import { IDeviceDescriptionState, deviceDescriptionHandler } from "./deviceDescriptionHandler"; -import { IViewDescriptionState, viewDescriptionHandler } from "./viewDescriptionHandler"; -import { IValueSelectorState, valueSelectorHandler } from "./valueSelectorHandler"; +import { IDeviceDescriptionState, deviceDescriptionHandler } from './deviceDescriptionHandler'; +import { IViewDescriptionState, viewDescriptionHandler } from './viewDescriptionHandler'; +import { IValueSelectorState, valueSelectorHandler } from './valueSelectorHandler'; interface IConfigurationAppStoreState { connectedNetworkElements: IConnectedNetworkElementsState; // used for ne selection @@ -32,7 +32,7 @@ interface IConfigurationAppStoreState { declare module '../../../../framework/src/store/applicationStore' { interface IApplicationStoreState { - configuration: IConfigurationAppStoreState, + configuration: IConfigurationAppStoreState; } } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts index 8ca8fdf27..d2863dd2e 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/connectedNetworkElementsHandler.ts @@ -25,8 +25,8 @@ import { restService } from '../services/restServices'; export interface IConnectedNetworkElementsState extends IExternalTableState<NetworkElementConnection> { } -// create eleactic search material data fetch handler -const connectedNetworkElementsSearchHandler = createSearchDataHandler<NetworkElementConnection>('network-element-connection', false, { status: "Connected" }); +// create elastic search material data fetch handler +const connectedNetworkElementsSearchHandler = createSearchDataHandler<NetworkElementConnection>('network-element-connection', false, { status: 'Connected' }); export const { actionHandler: connectedNetworkElementsActionHandler, @@ -41,5 +41,5 @@ export const { const neUrl = restService.getNetworkElementUri(ne.id); const policy = getAccessPolicyByUrl(neUrl); return !(policy.GET && policy.POST); - } + }, ); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts index 408399da4..cd01b0988 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/deviceDescriptionHandler.ts @@ -16,23 +16,23 @@ * ============LICENSE_END========================================================================== */ -import { Module } from "../models/yang"; -import { ViewSpecification } from "../models/uiModels"; -import { IActionHandler } from "../../../../framework/src/flux/action"; -import { UpdateDeviceDescription } from "../actions/deviceActions"; +import { Module } from '../models/yang'; +import { ViewSpecification } from '../models/uiModels'; +import { IActionHandler } from '../../../../framework/src/flux/action'; +import { UpdateDeviceDescription } from '../actions/deviceActions'; export interface IDeviceDescriptionState { - nodeId: string, + nodeId: string; modules: { - [name: string]: Module - }, - views: ViewSpecification[], + [name: string]: Module; + }; + views: ViewSpecification[]; } const deviceDescriptionStateInit: IDeviceDescriptionState = { - nodeId: "", + nodeId: '', modules: {}, - views: [] + views: [], }; export const deviceDescriptionHandler: IActionHandler<IDeviceDescriptionState> = (state = deviceDescriptionStateInit, action) => { @@ -41,7 +41,7 @@ export const deviceDescriptionHandler: IActionHandler<IDeviceDescriptionState> = ...state, nodeId: action.nodeId, modules: action.modules, - views: action.views + views: action.views, }; } return state; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts index 5b2d55ee2..70d5eb253 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/valueSelectorHandler.ts @@ -16,9 +16,9 @@ * ============LICENSE_END========================================================================== */ -import { IActionHandler } from "../../../../framework/src/flux/action"; -import { ViewSpecification } from "../models/uiModels"; -import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdatViewDescription, UpdatOutputData } from "../actions/deviceActions"; +import { IActionHandler } from '../../../../framework/src/flux/action'; +import { ViewSpecification } from '../models/uiModels'; +import { EnableValueSelector, SetSelectedValue, UpdateDeviceDescription, SetCollectingSelectionData, UpdateViewDescription, UpdateOutputData } from '../actions/deviceActions'; export interface IValueSelectorState { collectingData: boolean; @@ -28,13 +28,13 @@ export interface IValueSelectorState { onValueSelected: (value: any) => void; } -const nc = (val: React.SyntheticEvent) => { }; +const dummyFunc = () => { }; const valueSelectorStateInit: IValueSelectorState = { collectingData: false, keyProperty: undefined, listSpecification: null, listData: [], - onValueSelected: nc, + onValueSelected: dummyFunc, }; export const valueSelectorHandler: IActionHandler<IValueSelectorState> = (state = valueSelectorStateInit, action) => { @@ -53,22 +53,24 @@ export const valueSelectorHandler: IActionHandler<IValueSelectorState> = (state listData: action.listData, }; } else if (action instanceof SetSelectedValue) { - state.keyProperty && state.onValueSelected(action.value[state.keyProperty]); + if (state.keyProperty) { + state.onValueSelected(action.value[state.keyProperty]); + } state = { ...state, collectingData: false, keyProperty: undefined, listSpecification: null, - onValueSelected: nc, + onValueSelected: dummyFunc, listData: [], }; - } else if (action instanceof UpdateDeviceDescription || action instanceof UpdatViewDescription || action instanceof UpdatOutputData) { + } else if (action instanceof UpdateDeviceDescription || action instanceof UpdateViewDescription || action instanceof UpdateOutputData) { state = { ...state, collectingData: false, keyProperty: undefined, listSpecification: null, - onValueSelected: nc, + onValueSelected: dummyFunc, listData: [], }; } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts index ff85a97ea..39b47be84 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/handlers/viewDescriptionHandler.ts @@ -16,18 +16,18 @@ * ============LICENSE_END========================================================================== */ -import { IActionHandler } from "../../../../framework/src/flux/action"; +import { IActionHandler } from '../../../../framework/src/flux/action'; -import { UpdatViewDescription, UpdatOutputData } from "../actions/deviceActions"; -import { ViewSpecification } from "../models/uiModels"; +import { UpdateViewDescription, UpdateOutputData } from '../actions/deviceActions'; +import { ViewSpecification } from '../models/uiModels'; export enum DisplayModeType { doNotDisplay = 0, displayAsObject = 1, displayAsList = 2, displayAsRPC = 3, - displayAsMessage = 4 -}; + displayAsMessage = 4, +} export type DisplaySpecification = { displayMode: DisplayModeType.doNotDisplay; @@ -45,13 +45,13 @@ export type DisplaySpecification = { } | { displayMode: DisplayModeType.displayAsMessage; renderMessage: string; -} +}; export interface IViewDescriptionState { vPath: string | null; displaySpecification: DisplaySpecification; - viewData: any, - outputData?: any, + viewData: any; + outputData?: any; } const viewDescriptionStateInit: IViewDescriptionState = { @@ -64,7 +64,7 @@ const viewDescriptionStateInit: IViewDescriptionState = { }; export const viewDescriptionHandler: IActionHandler<IViewDescriptionState> = (state = viewDescriptionStateInit, action) => { - if (action instanceof UpdatViewDescription) { + if (action instanceof UpdateViewDescription) { state = { ...state, vPath: action.vPath, @@ -72,7 +72,7 @@ export const viewDescriptionHandler: IActionHandler<IViewDescriptionState> = (st outputData: undefined, displaySpecification: action.displaySpecification, }; - } else if (action instanceof UpdatOutputData) { + } else if (action instanceof UpdateOutputData) { state = { ...state, outputData: action.outputData, diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts index 88f70181c..e1ef1ea2d 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/networkElementConnection.ts @@ -24,7 +24,7 @@ export type NetworkElementConnection = { username?: string; password?: string; isRequired?: boolean; - status?: "connected" | "mounted" | "unmounted" | "connecting" | "disconnected" | "idle"; + status?: 'connected' | 'mounted' | 'unmounted' | 'connecting' | 'disconnected' | 'idle'; coreModelCapability?: string; deviceType?: string; nodeDetails?: { @@ -33,5 +33,5 @@ export type NetworkElementConnection = { failureReason: string; capability: string; }[]; - } -} + }; +}; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts index 29484d812..7d9e63caf 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/uiModels.ts @@ -16,128 +16,130 @@ * ============LICENSE_END========================================================================== */ +import type { WhenAST } from '../yang/whenParser'; + export type ViewElementBase = { - "id": string; - "label": string; - "module": string; - "path": string; - "config": boolean; - "ifFeature"?: string; - "when"?: string; - "mandatory"?: boolean; - "description"?: string; - "isList"?: boolean; - "default"?: string; - "status"?: "current" | "deprecated" | "obsolete", - "reference"?: string, // https://tools.ietf.org/html/rfc7950#section-7.21.4 -} + 'id': string; + 'label': string; + 'module': string; + 'path': string; + 'config': boolean; + 'ifFeature'?: string; + 'when'?: WhenAST; + 'mandatory'?: boolean; + 'description'?: string; + 'isList'?: boolean; + 'default'?: string; + 'status'?: 'current' | 'deprecated' | 'obsolete'; + 'reference'?: string; // https://tools.ietf.org/html/rfc7950#section-7.21.4 +}; // https://tools.ietf.org/html/rfc7950#section-9.8 export type ViewElementBinary = ViewElementBase & { - "uiType": "binary"; - "length"?: Expression<YangRange>; // number of octets -} + 'uiType': 'binary'; + 'length'?: Expression<YangRange>; // number of octets +}; // https://tools.ietf.org/html/rfc7950#section-9.7.4 export type ViewElementBits = ViewElementBase & { - "uiType": "bits"; - "flags": { + 'uiType': 'bits'; + 'flags': { [name: string]: number | undefined; // 0 - 4294967295 - } -} + }; +}; // https://tools.ietf.org/html/rfc7950#section-9 export type ViewElementString = ViewElementBase & { - "uiType": "string"; - "pattern"?: Expression<RegExp>; - "length"?: Expression<YangRange>; - "invertMatch"?: true; -} + 'uiType': 'string'; + 'pattern'?: Expression<RegExp>; + 'length'?: Expression<YangRange>; + 'invertMatch'?: true; +}; // special case derived from export type ViewElementDate = ViewElementBase & { - "uiType": "date"; - "pattern"?: Expression<RegExp>; - "length"?: Expression<YangRange>; - "invertMatch"?: true; -} + 'uiType': 'date'; + 'pattern'?: Expression<RegExp>; + 'length'?: Expression<YangRange>; + 'invertMatch'?: true; +}; // https://tools.ietf.org/html/rfc7950#section-9.3 export type ViewElementNumber = ViewElementBase & { - "uiType": "number"; - "min": number; - "max": number; - "range"?: Expression<YangRange>; - "units"?: string; - "format"?: string; - "fDigits"?: number; -} + 'uiType': 'number'; + 'min': number; + 'max': number; + 'range'?: Expression<YangRange>; + 'units'?: string; + 'format'?: string; + 'fDigits'?: number; +}; // https://tools.ietf.org/html/rfc7950#section-9.5 export type ViewElementBoolean = ViewElementBase & { - "uiType": "boolean"; - "trueValue"?: string; - "falseValue"?: string; -} + 'uiType': 'boolean'; + 'trueValue'?: string; + 'falseValue'?: string; +}; // https://tools.ietf.org/html/rfc7950#section-9.6.4 export type ViewElementSelection = ViewElementBase & { - "uiType": "selection"; - "multiSelect"?: boolean - "options": { - "key": string; - "value": string; - "description"?: string, - "status"?: "current" | "deprecated" | "obsolete", - "reference"?: string, + 'uiType': 'selection'; + 'multiSelect'?: boolean; + 'options': { + 'key': string; + 'value': string; + 'description'?: string; + 'status'?: 'current' | 'deprecated' | 'obsolete'; + 'reference'?: string; }[]; -} +}; // is a list if isList is true ;-) export type ViewElementObject = ViewElementBase & { - "uiType": "object"; - "isList"?: false; - "viewId": string; -} + 'uiType': 'object'; + 'isList'?: false; + 'viewId': string; +}; // Hint: read only lists do not need a key export type ViewElementList = (ViewElementBase & { - "uiType": "object"; - "isList": true; - "viewId": string; - "key"?: string; + 'uiType': 'object'; + 'isList': true; + 'viewId': string; + 'key'?: string; }); export type ViewElementReference = ViewElementBase & { - "uiType": "reference"; - "referencePath": string; - "ref": (currentPath: string) => [ViewElement , string] | undefined; -} + 'uiType': 'reference'; + 'referencePath': string; + 'ref': (currentPath: string) => [ViewElement, string] | undefined; +}; export type ViewElementUnion = ViewElementBase & { - "uiType": "union"; - "elements": ViewElement[]; -} + 'uiType': 'union'; + 'elements': ViewElement[]; +}; -export type ViewElementChoiseCase = { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }; +export type ViewElementChoiceCase = { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }; -export type ViewElementChoise = ViewElementBase & { - "uiType": "choise"; - "cases": { - [name: string]: ViewElementChoiseCase; - } -} +export type ViewElementChoice = ViewElementBase & { + 'uiType': 'choice'; + 'cases': { + [name: string]: ViewElementChoiceCase; + }; +}; // https://tools.ietf.org/html/rfc7950#section-7.14.1 export type ViewElementRpc = ViewElementBase & { - "uiType": "rpc"; - "inputViewId"?: string; - "outputViewId"?: string; -} + 'uiType': 'rpc'; + 'inputViewId'?: string; + 'outputViewId'?: string; +}; export type ViewElementEmpty = ViewElementBase & { - "uiType": "empty"; -} + 'uiType': 'empty'; +}; export type ViewElement = | ViewElementEmpty @@ -152,88 +154,88 @@ export type ViewElement = | ViewElementSelection | ViewElementReference | ViewElementUnion - | ViewElementChoise + | ViewElementChoice | ViewElementRpc; export const isViewElementString = (viewElement: ViewElement): viewElement is ViewElementString => { - return viewElement && (viewElement.uiType === "string" || viewElement.uiType === "date"); -} + return viewElement && (viewElement.uiType === 'string' || viewElement.uiType === 'date'); +}; export const isViewElementDate = (viewElement: ViewElement): viewElement is ViewElementDate => { - return viewElement && (viewElement.uiType === "date"); -} + return viewElement && (viewElement.uiType === 'date'); +}; export const isViewElementNumber = (viewElement: ViewElement): viewElement is ViewElementNumber => { - return viewElement && viewElement.uiType === "number"; -} + return viewElement && viewElement.uiType === 'number'; +}; export const isViewElementBoolean = (viewElement: ViewElement): viewElement is ViewElementBoolean => { - return viewElement && viewElement.uiType === "boolean"; -} + return viewElement && viewElement.uiType === 'boolean'; +}; export const isViewElementObject = (viewElement: ViewElement): viewElement is ViewElementObject => { - return viewElement && viewElement.uiType === "object" && viewElement.isList === false; -} + return viewElement && viewElement.uiType === 'object' && viewElement.isList === false; +}; export const isViewElementList = (viewElement: ViewElement): viewElement is ViewElementList => { - return viewElement && viewElement.uiType === "object" && viewElement.isList === true; -} + return viewElement && viewElement.uiType === 'object' && viewElement.isList === true; +}; export const isViewElementObjectOrList = (viewElement: ViewElement): viewElement is ViewElementObject | ViewElementList => { - return viewElement && viewElement.uiType === "object"; -} + return viewElement && viewElement.uiType === 'object'; +}; export const isViewElementSelection = (viewElement: ViewElement): viewElement is ViewElementSelection => { - return viewElement && viewElement.uiType === "selection"; -} + return viewElement && viewElement.uiType === 'selection'; +}; export const isViewElementReference = (viewElement: ViewElement): viewElement is ViewElementReference => { - return viewElement && viewElement.uiType === "reference"; -} + return viewElement && viewElement.uiType === 'reference'; +}; export const isViewElementUnion = (viewElement: ViewElement): viewElement is ViewElementUnion => { - return viewElement && viewElement.uiType === "union"; -} + return viewElement && viewElement.uiType === 'union'; +}; -export const isViewElementChoise = (viewElement: ViewElement): viewElement is ViewElementChoise => { - return viewElement && viewElement.uiType === "choise"; -} +export const isViewElementChoice = (viewElement: ViewElement): viewElement is ViewElementChoice => { + return viewElement && viewElement.uiType === 'choice'; +}; export const isViewElementRpc = (viewElement: ViewElement): viewElement is ViewElementRpc => { - return viewElement && viewElement.uiType === "rpc"; -} + return viewElement && viewElement.uiType === 'rpc'; +}; export const isViewElementEmpty = (viewElement: ViewElement): viewElement is ViewElementRpc => { - return viewElement && viewElement.uiType === "empty"; -} + return viewElement && viewElement.uiType === 'empty'; +}; -export const ResolveFunction = Symbol("IsResolved"); +export const ResolveFunction = Symbol('IsResolved'); export type ViewSpecification = { - "id": string; - "ns"?: string; - "name"?: string; - "title"?: string; - "parentView"?: string; - "language": string; - "ifFeature"?: string; - "when"?: string; - "uses"?: (string[]) & { [ResolveFunction]?: (parent: string) => void }; - "elements": { [name: string]: ViewElement }; - "config": boolean; - readonly "canEdit": boolean; -} + id: string; + ns?: string; + name?: string; + title?: string; + parentView?: string; + language: string; + ifFeature?: string; + when?: WhenAST; + uses?: (string[]) & { [ResolveFunction]?: (parent: string) => void }; + elements: { [name: string]: ViewElement }; + config: boolean; + readonly canEdit: boolean; +}; export type YangRange = { - min: number, - max: number, -} + min: number; + max: number; +}; export type Expression<T> = | T | Operator<T>; export type Operator<T> = { - operation: "AND" | "OR"; + operation: 'AND' | 'OR'; arguments: Expression<T>[]; -}
\ No newline at end of file +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts index 79704ae34..e4e59fb96 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/models/yang.ts @@ -16,7 +16,7 @@ * ============LICENSE_END========================================================================== */ -import { ViewElement, ViewSpecification } from "./uiModels"; +import { ViewElement, ViewSpecification } from './uiModels'; export enum ModuleState { stable, @@ -30,27 +30,27 @@ export type Token = { value: string; start: number; end: number; -} +}; export type Statement = { key: string; arg?: string; sub?: Statement[]; -} +}; export type Identity = { - id: string, - label: string, - base?: string, - description?: string, - reference?: string, - children?: Identity[], - values?: Identity[], -} + id: string; + label: string; + base?: string; + description?: string; + reference?: string; + children?: Identity[]; + values?: Identity[]; +}; export type Revision = { - description?: string, - reference?: string + description?: string; + reference?: string; }; export type Module = { @@ -68,4 +68,4 @@ export type Module = { views: { [view: string]: ViewSpecification }; elements: { [view: string]: ViewElement }; executionOrder?: number; -}
\ No newline at end of file +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx index e37879102..7dd2d6ae4 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/pluginConfiguration.tsx @@ -16,71 +16,74 @@ * ============LICENSE_END========================================================================== */ -import * as React from "react"; +import React from 'react'; import { withRouter, RouteComponentProps, Route, Switch, Redirect } from 'react-router-dom'; -import { faAdjust } from '@fortawesome/free-solid-svg-icons'; // select app icon - -import connect, { Connect, IDispatcher } from '../../../framework/src/flux/connect'; +import { connect, Connect, IDispatcher } from '../../../framework/src/flux/connect'; import applicationManager from '../../../framework/src/services/applicationManager'; -import { IApplicationStoreState } from "../../../framework/src/store/applicationStore"; -import { configurationAppRootHandler } from "./handlers/configurationAppRootHandler"; -import { NetworkElementSelector } from "./views/networkElementSelector"; -import ConfigurationApplication from "./views/configurationApplication"; -import { updateNodeIdAsyncActionCreator, updateViewActionAsyncCreator } from "./actions/deviceActions"; -import { DisplayModeType } from "./handlers/viewDescriptionHandler"; -import { ViewSpecification } from "./models/uiModels"; +import { configurationAppRootHandler } from './handlers/configurationAppRootHandler'; +import { NetworkElementSelector } from './views/networkElementSelector'; + +import ConfigurationApplication from './views/configurationApplication'; +import { updateNodeIdAsyncActionCreator, updateViewActionAsyncCreator } from './actions/deviceActions'; +import { DisplayModeType } from './handlers/viewDescriptionHandler'; +import { ViewSpecification } from './models/uiModels'; + +const appIcon = require('./assets/icons/configurationAppIcon.svg'); // select app icon let currentNodeId: string | null | undefined = undefined; let currentVirtualPath: string | null | undefined = undefined; let lastUrl: string | undefined = undefined; -const mapDisp = (dispatcher: IDispatcher) => ({ +const mapDispatch = (dispatcher: IDispatcher) => ({ updateNodeId: (nodeId: string) => dispatcher.dispatch(updateNodeIdAsyncActionCreator(nodeId)), updateView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), }); -const ConfigurationApplicationRouteAdapter = connect(undefined, mapDisp)((props: RouteComponentProps<{ nodeId?: string, 0: string }> & Connect<undefined, typeof mapDisp>) => { +// eslint-disable-next-line @typescript-eslint/naming-convention +const ConfigurationApplicationRouteAdapter = connect(undefined, mapDispatch)((props: RouteComponentProps<{ nodeId?: string; 0: string }> & Connect<undefined, typeof mapDispatch>) => { React.useEffect(() => { return () => { lastUrl = undefined; currentNodeId = undefined; currentVirtualPath = undefined; - } + }; }, []); if (props.location.pathname !== lastUrl) { - // ensure the asynchronus update will only be called once per path + // ensure the asynchronous update will only be called once per path lastUrl = props.location.pathname; window.setTimeout(async () => { // check if the nodeId has changed - let dump = false; + let enableDump = false; if (currentNodeId !== props.match.params.nodeId) { currentNodeId = props.match.params.nodeId || undefined; - if (currentNodeId && currentNodeId.endsWith("|dump")) { - dump = true; + if (currentNodeId && currentNodeId.endsWith('|dump')) { + enableDump = true; currentNodeId = currentNodeId.replace(/\|dump$/i, ''); } currentVirtualPath = null; - currentNodeId && (await props.updateNodeId(currentNodeId)); + if (currentNodeId) { + await props.updateNodeId(currentNodeId); + } } if (currentVirtualPath !== props.match.params[0]) { currentVirtualPath = props.match.params[0]; - if (currentVirtualPath && currentVirtualPath.endsWith("|dump")) { - dump = true; + if (currentVirtualPath && currentVirtualPath.endsWith('|dump')) { + enableDump = true; currentVirtualPath = currentVirtualPath.replace(/\|dump$/i, ''); } await props.updateView(currentVirtualPath); } - if (dump) { + if (enableDump) { const device = props.state.configuration.deviceDescription; const ds = props.state.configuration.viewDescription.displaySpecification; const createDump = (view: ViewSpecification | null, level: number = 0) => { - if (view === null) return "Empty"; + if (view === null) return 'Empty'; const indention = Array(level * 4).fill(' ').join(''); let result = ''; @@ -88,24 +91,24 @@ const ConfigurationApplicationRouteAdapter = connect(undefined, mapDisp)((props: // result += `${indention} [${view.canEdit ? 'rw' : 'ro'}] ${view.ns}:${view.name} ${ds.displayMode === DisplayModeType.displayAsList ? '[LIST]' : ''}\r\n`; result += Object.keys(view.elements).reduce((acc, cur) => { const elm = view.elements[cur]; - acc += `${indention} [${elm.uiType === "rpc" ? "x" : elm.config ? 'rw' : 'ro'}:${elm.id}] (${elm.module}:${elm.label}) {${elm.uiType}} ${elm.uiType === "object" && elm.isList ? `as LIST with KEY [${elm.key}]` : ""}\r\n`; - // acc += `${indention} +${elm.mandatory ? "mandetory" : "none"} - ${elm.path} \r\n`; + acc += `${indention} [${elm.uiType === 'rpc' ? 'x' : elm.config ? 'rw' : 'ro'}:${elm.id}] (${elm.module}:${elm.label}) {${elm.uiType}} ${elm.uiType === 'object' && elm.isList ? `as LIST with KEY [${elm.key}]` : ''}\r\n`; + // acc += `${indention} +${elm.mandatory ? "mandatory" : "none"} - ${elm.path} \r\n`; switch (elm.uiType) { - case "object": + case 'object': acc += createDump(device.views[(elm as any).viewId], level + 1); break; default: } return acc; - }, ""); + }, ''); return `${result}`; - } + }; const dump = createDump(ds.displayMode === DisplayModeType.displayAsObject || ds.displayMode === DisplayModeType.displayAsList ? ds.viewSpecification : null, 0); - var element = document.createElement('a'); + const element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(dump)); - element.setAttribute('download', currentNodeId + ".txt"); + element.setAttribute('download', currentNodeId + '.txt'); element.style.display = 'none'; document.body.appendChild(element); @@ -133,10 +136,10 @@ const App = withRouter((props: RouteComponentProps) => ( export function register() { applicationManager.registerApplication({ - name: "configuration", - icon: faAdjust, + name: 'configuration', + icon: appIcon, rootComponent: App, rootActionHandler: configurationAppRootHandler, - menuEntry: "Configuration" + menuEntry: 'Configuration', }); } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts index 02060ef12..07e263559 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/services/restServices.ts @@ -16,79 +16,79 @@ * ============LICENSE_END========================================================================== */ -import { requestRest, requestRestExt } from "../../../../framework/src/services/restService"; -import { convertPropertyNames, replaceHyphen } from "../../../../framework/src/utilities/yangHelper"; +import { requestRest, requestRestExt } from '../../../../framework/src/services/restService'; +import { convertPropertyNames, replaceHyphen } from '../../../../framework/src/utilities/yangHelper'; -import { NetworkElementConnection } from "../models/networkElementConnection"; +import { NetworkElementConnection } from '../models/networkElementConnection'; type ImportOnlyResponse = { - "ietf-yang-library:yang-library": { - "module-set": { - "import-only-module": { - "name": string, - "revision": string, - }[], - }[], - }, -} + 'ietf-yang-library:yang-library': { + 'module-set': { + 'import-only-module': { + 'name': string; + 'revision': string; + }[]; + }[]; + }; +}; type CapabilityResponse = { - "network-topology:node": { - "node-id": string, - "netconf-node-topology:available-capabilities": { - "available-capability": { - "capability-origin": string, - "capability": string, - }[] - }, - "netconf-node-topology:unavailable-capabilities": { - "unavailable-capability": { - "capability": string, - "failure-reason": string, - }[] - } - }[] -} + 'network-topology:node': { + 'node-id': string; + 'netconf-node-topology:available-capabilities': { + 'available-capability': { + 'capability-origin': string; + 'capability': string; + }[]; + }; + 'netconf-node-topology:unavailable-capabilities': { + 'unavailable-capability': { + 'capability': string; + 'failure-reason': string; + }[]; + }; + }[]; +}; type CapabilityAnswer = { availableCapabilities: { - capabilityOrigin: string, - capability: string, - version: string, - }[] | null, + capabilityOrigin: string; + capability: string; + version: string; + }[] | null; unavailableCapabilities: { - failureReason: string, - capability: string, - version: string, - }[] | null, + failureReason: string; + capability: string; + version: string; + }[] | null; importOnlyModules: { - name: string, - revision: string, - }[] | null -} + name: string; + revision: string; + }[] | null; +}; const capParser = /^\(.*\?revision=(\d{4}-\d{2}-\d{2})\)(\S+)$/i; class RestService { public getNetworkElementUri = (nodeId: string) => '/rests/data/network-topology:network-topology/topology=topology-netconf/node=' + nodeId; - public async getImportOnlyModules(nodeId: string): Promise<{ name: string, revision: string }[]> { + public async getImportOnlyModules(nodeId: string): Promise<{ name: string; revision: string }[]> { const path = `${this.getNetworkElementUri(nodeId)}/yang-ext:mount/ietf-yang-library:yang-library?content=nonconfig&fields=module-set(import-only-module(name;revision))`; - const importOnlyResult = await requestRest<ImportOnlyResponse>(path, { method: "GET" }); + const importOnlyResult = await requestRest<ImportOnlyResponse>(path, { method: 'GET' }); const importOnlyModules = importOnlyResult - ? importOnlyResult["ietf-yang-library:yang-library"]["module-set"][0]["import-only-module"] + ? importOnlyResult['ietf-yang-library:yang-library']['module-set'][0]['import-only-module'] : []; return importOnlyModules; } public async getCapabilitiesByMountId(nodeId: string): Promise<CapabilityAnswer> { const path = this.getNetworkElementUri(nodeId); - const capabilitiesResult = await requestRest<CapabilityResponse>(path, { method: "GET" }); - const availableCapabilities = capabilitiesResult && capabilitiesResult["network-topology:node"] && capabilitiesResult["network-topology:node"].length > 0 && - (capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"] && - capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"]["available-capability"] && - capabilitiesResult["network-topology:node"][0]["netconf-node-topology:available-capabilities"]["available-capability"].map<any>(obj => convertPropertyNames(obj, replaceHyphen)) || []) + const capabilitiesResult = await requestRest<CapabilityResponse>(path, { method: 'GET' }); + const availableCapabilities = capabilitiesResult && capabilitiesResult['network-topology:node'] && capabilitiesResult['network-topology:node'].length > 0 && + (capabilitiesResult['network-topology:node'][0]['netconf-node-topology:available-capabilities'] && + capabilitiesResult['network-topology:node'][0]['netconf-node-topology:available-capabilities']['available-capability'] && + capabilitiesResult['network-topology:node'][0]['netconf-node-topology:available-capabilities']['available-capability'].map<any>(obj => convertPropertyNames(obj, replaceHyphen)) || []) .map(cap => { const capMatch = cap && capParser.exec(cap.capability); return capMatch ? { @@ -98,20 +98,20 @@ class RestService { } : null ; }).filter((cap) => cap != null) || [] as any; - const unavailableCapabilities = capabilitiesResult && capabilitiesResult["network-topology:node"] && capabilitiesResult["network-topology:node"].length > 0 && - (capabilitiesResult["network-topology:node"][0]["netconf-node-topology:unavailable-capabilities"] && - capabilitiesResult["network-topology:node"][0]["netconf-node-topology:unavailable-capabilities"]["unavailable-capability"] && - capabilitiesResult["network-topology:node"][0]["netconf-node-topology:unavailable-capabilities"]["unavailable-capability"].map<any>(obj => convertPropertyNames(obj, replaceHyphen)) || []) - .map(cap => { - const capMatch = cap && capParser.exec(cap.capability); - return capMatch ? { - failureReason: cap.failureReason, - capability: capMatch && capMatch[2] || '', - version: capMatch && capMatch[1] || '', - } : null ; - }).filter((cap) => cap != null) || [] as any; - - const importOnlyModules = availableCapabilities && availableCapabilities.findIndex((ac: {capability: string }) => ac.capability && ac.capability.toLowerCase() === "ietf-yang-library") > -1 + const unavailableCapabilities = capabilitiesResult && capabilitiesResult['network-topology:node'] && capabilitiesResult['network-topology:node'].length > 0 && + (capabilitiesResult['network-topology:node'][0]['netconf-node-topology:unavailable-capabilities'] && + capabilitiesResult['network-topology:node'][0]['netconf-node-topology:unavailable-capabilities']['unavailable-capability'] && + capabilitiesResult['network-topology:node'][0]['netconf-node-topology:unavailable-capabilities']['unavailable-capability'].map<any>(obj => convertPropertyNames(obj, replaceHyphen)) || []) + .map(cap => { + const capMatch = cap && capParser.exec(cap.capability); + return capMatch ? { + failureReason: cap.failureReason, + capability: capMatch && capMatch[2] || '', + version: capMatch && capMatch[1] || '', + } : null ; + }).filter((cap) => cap != null) || [] as any; + + const importOnlyModules = availableCapabilities && availableCapabilities.findIndex((ac: { capability: string }) => ac.capability && ac.capability.toLowerCase() === 'ietf-yang-library') > -1 ? await this.getImportOnlyModules(nodeId) : null; @@ -123,11 +123,11 @@ class RestService { // const connectedNetworkElement = await requestRest<NetworkElementConnection>(path, { method: "GET" }); // return connectedNetworkElement || null; - const path = "/rests/operations/data-provider:read-network-element-connection-list"; - const body = { "data-provider:input": { "filter": [{ "property": "node-id", "filtervalue": nodeId }], "sortorder": [], "pagination": { "size": 1, "page": 1 } } }; - const networkElementResult = await requestRest<{ "data-provider:output": { data: NetworkElementConnection[] } }>(path, { method: "POST", body: JSON.stringify(body) }); - return networkElementResult && networkElementResult["data-provider:output"] && networkElementResult["data-provider:output"].data && - networkElementResult["data-provider:output"].data.map(obj => convertPropertyNames(obj, replaceHyphen))[0] || null; + const path = '/rests/operations/data-provider:read-network-element-connection-list'; + const body = { 'data-provider:input': { 'filter': [{ 'property': 'node-id', 'filtervalue': nodeId }], 'sortorder': [], 'pagination': { 'size': 1, 'page': 1 } } }; + const networkElementResult = await requestRest<{ 'data-provider:output': { data: NetworkElementConnection[] } }>(path, { method: 'POST', body: JSON.stringify(body) }); + return networkElementResult && networkElementResult['data-provider:output'] && networkElementResult['data-provider:output'].data && + networkElementResult['data-provider:output'].data.map(obj => convertPropertyNames(obj, replaceHyphen))[0] || null; } /** Reads the config data by restconf path. @@ -135,7 +135,7 @@ class RestService { * @returns The data. */ public getConfigData(path: string) { - return requestRestExt<{ [key: string]: any }>(path, { method: "GET" }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'GET' }); } /** Updates or creates the config data by restconf path using data. @@ -144,11 +144,11 @@ class RestService { * @returns The written data. */ public setConfigData(path: string, data: any) { - return requestRestExt<{ [key: string]: any }>(path, { method: "PUT", body: JSON.stringify(data) }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'PUT', body: JSON.stringify(data) }); } public executeRpc(path: string, data: any) { - return requestRestExt<{ [key: string]: any }>(path, { method: "POST", body: JSON.stringify(data) }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'POST', body: JSON.stringify(data) }); } /** Removes the element by restconf path. @@ -156,7 +156,7 @@ class RestService { * @returns The restconf result. */ public removeConfigElement(path: string) { - return requestRestExt<{ [key: string]: any }>(path, { method: "DELETE" }); + return requestRestExt<{ [key: string]: any }>(path, { method: 'DELETE' }); } } diff --git a/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts b/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts index b81a92c14..bbd051aeb 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/services/yangService.ts @@ -16,28 +16,22 @@ * ============LICENSE_END========================================================================== */ -type YangInfo = [string, (string | null | undefined)]; +const cache: { [path: string]: string } = { }; +const getCapability = async (capability: string, nodeId: string, version?: string) => { + const url = `/yang-schema/${capability}${version ? `/${version}` : ''}?node=${nodeId}`; -const cache: { [path: string]: string } = { + const cacheHit = cache[url]; + if (cacheHit) return cacheHit; -}; - -class YangService { - - public async getCapability(capability: string, nodeId: string, version?: string) { - const url = `/yang-schema/${capability}${version ? `/${version}` : ""}?node=${nodeId}`; - - const cacheHit = cache[url]; - if (cacheHit) return cacheHit; - - const res = await fetch(url); - const yangFile = res.ok && (await res.text()); - if (yangFile !== false && yangFile !== null) { - cache[url] = yangFile; - } - return yangFile; + const res = await fetch(url); + const yangFile = res.ok && (await res.text()); + if (yangFile !== false && yangFile !== null) { + cache[url] = yangFile; } -} + return yangFile; +}; -export const yangService = new YangService(); +export const yangService = { + getCapability, +}; export default yangService;
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts b/sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts new file mode 100644 index 000000000..9dd12031f --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/utilities/verifyer.ts @@ -0,0 +1,261 @@ +/** + * ============LICENSE_START======================================================================== + * ONAP : ccsdk feature sdnr wt odlux + * ================================================================================================= + * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved. + * ================================================================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + * ============LICENSE_END========================================================================== + */ + +import { YangRange, Operator, ViewElementNumber, ViewElementString, isViewElementNumber, isViewElementString } from '../models/uiModels'; + +export type validated = { isValid: boolean; error?: string }; + +export type validatedRange = { isValid: boolean; error?: string }; + +const rangeErrorStartNumber = 'The entered number must be'; +const rangeErrorInnerMinTextNumber = 'greater or equals than'; +const rangeErrorInnerMaxTextNumber = 'less or equals than'; +const rangeErrorEndTextNumber = '.'; + +const rangeErrorStartString = 'The entered text must have'; +const rangeErrorInnerMinTextString = 'no more than'; +const rangeErrorInnerMaxTextString = 'less than'; +const rangeErrorEndTextString = ' characters.'; + +let errorMessageStart = ''; +let errorMessageMiddleMinPart = ''; +let errorMessageMiddleMaxPart = ''; +let errorMessageEnd = ''; + +const isYangRange = (val: YangRange | Operator<YangRange>): val is YangRange => (val as YangRange).min !== undefined; + +const isYangOperator = (val: YangRange | Operator<YangRange>): val is Operator<YangRange> => (val as Operator<YangRange>).operation !== undefined; + +const isRegExp = (val: RegExp | Operator<RegExp>): val is RegExp => (val as RegExp).source !== undefined; + +const isRegExpOperator = (val: RegExp | Operator<RegExp>): val is Operator<RegExp> => (val as Operator<RegExp>).operation !== undefined; + +const getRangeErrorMessagesRecursively = (value: Operator<YangRange>, data: number): string[] => { + let currentIteration: string[] = []; + + // iterate over all elements + for (let i = 0; i < value.arguments.length; i++) { + const element = value.arguments[i]; + + let min = undefined; + let max = undefined; + + let isNumberCorrect = false; + + if (isYangRange(element)) { + + //check found min values + if (!isNaN(element.min)) { + if (data < element.min) { + min = element.min; + } else { + isNumberCorrect = true; + } + } + + // check found max values + if (!isNaN(element.max)) { + if (data > element.max) { + max = element.max; + } else { + isNumberCorrect = true; + } + } + + // construct error messages + if (min != undefined) { + currentIteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMinPart} ${min}`); + } else if (max != undefined) { + currentIteration.push(`${value.operation.toLocaleLowerCase()} ${errorMessageMiddleMaxPart} ${max}`); + + } + + } else if (isYangOperator(element)) { + + //get error_message from expression + const result = getRangeErrorMessagesRecursively(element, data); + if (result.length === 0) { + isNumberCorrect = true; + } + currentIteration = currentIteration.concat(result); + } + + // if its an OR operation, the number has been checked and min/max are empty (thus not violated) + // delete everything found (because at least one found is correct, therefore all are correct) and break from loop + if (min === undefined && max === undefined && isNumberCorrect && value.operation === 'OR') { + + currentIteration.splice(0, currentIteration.length); + break; + } + } + + return currentIteration; +}; + +const createStartMessage = (element: string) => { + //remove leading or or and from text + if (element.startsWith('and')) { + element = element.replace('and', ''); + } else if (element.startsWith('or')) { + element = element.replace('or', ''); + } + return `${errorMessageStart} ${element}`; +}; + +const getRangeErrorMessages = (value: Operator<YangRange>, data: number): string => { + + const currentIteration = getRangeErrorMessagesRecursively(value, data); + + // build complete error message from found parts + let errorMessage = ''; + if (currentIteration.length > 1) { + + currentIteration.forEach((element, index) => { + if (index === 0) { + errorMessage = createStartMessage(element); + } else if (index === currentIteration.length - 1) { + errorMessage += ` ${element}${errorMessageEnd}`; + } else { + errorMessage += `, ${element}`; + } + }); + } else if (currentIteration.length == 1) { + errorMessage = `${createStartMessage(currentIteration[0])}${errorMessageEnd}`; + } + + return errorMessage; +}; + +export const checkRange = (element: ViewElementNumber | ViewElementString, data: number): string => { + const number = data; + + let expression = undefined; + + if (isViewElementString(element)) { + expression = element.length; + + errorMessageStart = rangeErrorStartString; + errorMessageMiddleMaxPart = rangeErrorInnerMaxTextString; + errorMessageMiddleMinPart = rangeErrorInnerMinTextString; + errorMessageEnd = rangeErrorEndTextString; + + } else if (isViewElementNumber(element)) { + expression = element.range; + + errorMessageStart = rangeErrorStartNumber; + errorMessageMiddleMaxPart = rangeErrorInnerMaxTextNumber; + errorMessageMiddleMinPart = rangeErrorInnerMinTextNumber; + errorMessageEnd = rangeErrorEndTextNumber; + } + + if (expression) { + if (isYangOperator(expression)) { + + const errorMessage = getRangeErrorMessages(expression, data); + return errorMessage; + + } else + if (isYangRange(expression)) { + + if (!isNaN(expression.min)) { + if (number < expression.min) { + return `${errorMessageStart} ${errorMessageMiddleMinPart} ${expression.min}${errorMessageEnd}`; + } + } + + if (!isNaN(expression.max)) { + if (number > expression.max) { + return `${errorMessageStart} ${errorMessageMiddleMaxPart} ${expression.max}${errorMessageEnd}`; + } + } + } + } + + return ''; +}; + +const getRegexRecursively = (value: Operator<RegExp>, data: string): boolean[] => { + let currentItteration: boolean[] = []; + for (let i = 0; i < value.arguments.length; i++) { + const element = value.arguments[i]; + if (isRegExp(element)) { + // if regex is found, add it to list + currentItteration.push(element.test(data)); + } else if (isRegExpOperator(element)) { + //if RegexExpression is found, try to get regex from it + currentItteration = currentItteration.concat(getRegexRecursively(element, data)); + } + } + + if (value.operation === 'OR') { + // if one is true, all are true, all found items can be discarded + let result = currentItteration.find(element => element); + if (result) { + return []; + } + } + return currentItteration; +}; + +const isPatternValid = (value: Operator<RegExp>, data: string): boolean => { + // get all regex + const result = getRegexRecursively(value, data); + + if (value.operation === 'AND') { + // if AND operation is executed... + // no element can be false + const check = result.find(element => element !== true); + if (check) + return false; + else + return true; + } else { + // if OR operation is executed... + // ... just one element must be true + const check = result.find(element => element === true); + if (check) + return true; + else + return false; + + } +}; + +export const checkPattern = (expression: RegExp | Operator<RegExp> | undefined, data: string): validated => { + + if (expression) { + if (isRegExp(expression)) { + const isValid = expression.test(data); + if (!isValid) + return { isValid: isValid, error: 'The input is in a wrong format.' }; + + } else if (isRegExpOperator(expression)) { + const result = isPatternValid(expression, data); + + if (!result) { + return { isValid: false, error: 'The input is in a wrong format.' }; + } + } + } + + return { isValid: true }; +}; + + + + diff --git a/sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts b/sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts new file mode 100644 index 000000000..ad34c83b9 --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/utilities/viewEngineHelper.ts @@ -0,0 +1,324 @@ +import { storeService } from '../../../../framework/src/services/storeService'; +import { WhenAST, WhenTokenType } from '../yang/whenParser'; + +import { + ViewSpecification, + ViewElement, + isViewElementReference, + isViewElementList, + isViewElementObjectOrList, + isViewElementRpc, + isViewElementChoice, + ViewElementChoiceCase, +} from '../models/uiModels'; + +import { Module } from '../models/yang'; + +import { restService } from '../services/restServices'; + +export type HttpResult = { + status: number; + message?: string | undefined; + data: { + [key: string]: any; + } | null | undefined; +}; + +export const checkResponseCode = (restResult: HttpResult) =>{ + //403 gets handled by the framework from now on + return restResult.status !== 403 && ( restResult.status < 200 || restResult.status > 299); +}; + +export const resolveVPath = (current: string, vPath: string): string => { + if (vPath.startsWith('/')) { + return vPath; + } + const parts = current.split('/'); + const vPathParts = vPath.split('/'); + for (const part of vPathParts) { + if (part === '.') { + continue; + } else if (part === '..') { + parts.pop(); + } else { + parts.push(part); + } + } + return parts.join('/'); +}; + +export const splitVPath = (vPath: string, vPathParser : RegExp): [string, string?][] => { + const pathParts: [string, string?][] = []; + let partMatch: RegExpExecArray | null; + if (vPath) do { + partMatch = vPathParser.exec(vPath); + if (partMatch) { + pathParts.push([partMatch[1], partMatch[2] || undefined]); + } + } while (partMatch); + return pathParts; +}; + +const derivedFrom = (vPath: string, when: WhenAST, viewData: any, includeSelf = false) => { + if (when.args?.length !== 2) { + throw new Error('derived-from or derived-from-or-self requires 2 arguments.'); + } + const [arg1, arg2] = when.args; + if (arg1.type !== WhenTokenType.IDENTIFIER || arg2.type !== WhenTokenType.STRING) { + throw new Error('derived-from or derived-from-or-self requires first argument IDENTIFIER and second argument STRING.'); + } + + if (!storeService.applicationStore) { + throw new Error('storeService.applicationStore is not defined.'); + } + + const pathParts = splitVPath(arg1.value as string || '', /(?:(?:([^\/\:]+):)?([^\/]+))/g); + const referenceValueParts = /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(arg2.value as string || ''); + + if (!pathParts || !referenceValueParts || pathParts.length === 0 || referenceValueParts.length === 0) { + throw new Error('derived-from or derived-from-or-self requires first argument PATH and second argument IDENTITY.'); + } + + if (pathParts[0][1]?.startsWith('..') || pathParts[0][1]?.startsWith('/')) { + throw new Error('derived-from or derived-from-or-self currently only supports relative paths.'); + } + + const { configuration: { deviceDescription: { modules } } } = storeService.applicationStore.state; + const dataValue = pathParts.reduce((acc, [ns, prop]) => { + if (prop === '.') { + return acc; + } + if (acc && prop) { + const moduleName = ns && (Object.values(modules).find((m: Module) => m.prefix === ns) || Object.values(modules).find((m: Module) => m.name === ns))?.name; + return (moduleName) ? acc[`${moduleName}:${prop}`] || acc[prop] : acc[prop]; + } + return undefined; + }, viewData); + + let dataValueParts = dataValue && /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(dataValue); + if (!dataValueParts || dataValueParts.length < 2) { + throw new Error(`derived-from or derived-from-or-self value referenced by first argument [${arg1.value}] not found.`); + } + let [, dataValueNs, dataValueProp] = dataValueParts; + let dataValueModule: Module = dataValueNs && (Object.values(modules).find((m: Module) => m.name === dataValueNs)); + let dataValueIdentity = dataValueModule && dataValueModule.identities && (Object.values(dataValueModule.identities).find((i) => i.label === dataValueProp)); + + if (!dataValueIdentity) { + throw new Error(`derived-from or derived-from-or-self identity [${dataValue}] referenced by first argument [${arg1.value}] not found.`); + } + + const [, referenceValueNs, referenceValueProp] = referenceValueParts; + const referenceValueModule = referenceValueNs && (Object.values(modules).find((m: Module) => m.prefix === referenceValueNs)); + const referenceValueIdentity = referenceValueModule && referenceValueModule.identities && (Object.values(referenceValueModule.identities).find((i) => i.label === referenceValueProp)); + + if (!referenceValueIdentity) { + throw new Error(`derived-from or derived-from-or-self identity [${arg2.value}] referenced by second argument not found.`); + } + + let result = includeSelf && (referenceValueIdentity === dataValueIdentity); + while (dataValueIdentity && dataValueIdentity.base && !result) { + dataValueParts = dataValue && /(?:(?:([^\/\:]+):)?([^\/]+))/g.exec(dataValueIdentity.base); + const [, innerDataValueNs, innerDataValueProp] = dataValueParts; + dataValueModule = innerDataValueNs && (Object.values(modules).find((m: Module) => m.prefix === innerDataValueNs)) || dataValueModule; + dataValueIdentity = dataValueModule && dataValueModule.identities && (Object.values(dataValueModule.identities).find((i) => i.label === innerDataValueProp)) ; + result = (referenceValueIdentity === dataValueIdentity); + } + + return result; +}; + +const evaluateWhen = async (vPath: string, when: WhenAST, viewData: any): Promise<boolean> => { + switch (when.type) { + case WhenTokenType.FUNCTION: + switch (when.name) { + case 'derived-from-or-self': + return derivedFrom(vPath, when, viewData, true); + case 'derived-from': + return derivedFrom(vPath, when, viewData, false); + default: + throw new Error(`Unknown function ${when.name}`); + } + case WhenTokenType.AND: + return !when.left || !when.right || (await evaluateWhen(vPath, when.left, viewData) && await evaluateWhen(vPath, when.right, viewData)); + case WhenTokenType.OR: + return !when.left || !when.right || (await evaluateWhen(vPath, when.left, viewData) || await evaluateWhen(vPath, when.right, viewData)); + case WhenTokenType.NOT: + return !when.right || ! await evaluateWhen(vPath, when.right, viewData); + case WhenTokenType.EXPRESSION: + return !(when.value && typeof when.value !== 'string') || await evaluateWhen(vPath, when.value, viewData); + } + return true; +}; + +export const getReferencedDataList = async (refPath: string, dataPath: string, modules: { [name: string]: Module }, views: ViewSpecification[]) => { + const pathParts = splitVPath(refPath, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property + const defaultNS = pathParts[0][0]; + let referencedModule = modules[defaultNS]; + + let dataMember: string; + let view: ViewSpecification; + let currentNS: string | null = null; + let dataUrls = [dataPath]; + let data: any; + + for (let i = 0; i < pathParts.length; ++i) { + const [pathPartNS, pathPart] = pathParts[i]; + const namespace = pathPartNS != null ? (currentNS = pathPartNS) : currentNS; + + const viewElement = i === 0 + ? views[0].elements[`${referencedModule.name}:${pathPart}`] + : view!.elements[`${pathPart}`] || view!.elements[`${namespace}:${pathPart}`]; + + if (!viewElement) throw new Error(`Could not find ${pathPart} in ${refPath}`); + if (i < pathParts.length - 1) { + if (!isViewElementObjectOrList(viewElement)) { + throw Error(`Module: [${referencedModule.name}].[${viewElement.label}]. View element is not list or object.`); + } + view = views[+viewElement.viewId]; + const resultingDataUrls : string[] = []; + if (isViewElementList(viewElement)) { + for (let j = 0; j < dataUrls.length; ++j) { + const dataUrl = dataUrls[j]; + const restResult = (await restService.getConfigData(dataUrl)); + if (restResult.data == null || checkResponseCode(restResult)) { + const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || ''; + throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); + } + + let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; + if (dataRaw === undefined) { + dataRaw = restResult.data[dataMember!]; + } + dataRaw = dataRaw instanceof Array + ? dataRaw[0] + : dataRaw; + + data = dataRaw && dataRaw[viewElement.label] || []; + const keys: string[] = data.map((entry: { [key: string]: any } )=> entry[viewElement.key!]); + resultingDataUrls.push(...keys.map(key => `${dataUrl}/${viewElement.label.replace(/\//ig, '%2F')}=${key.replace(/\//ig, '%2F')}`)); + } + dataMember = viewElement.label; + } else { + // just a member, not a list + const pathSegment = (i === 0 + ? `/${referencedModule.name}:${viewElement.label.replace(/\//ig, '%2F')}` + : `/${viewElement.label.replace(/\//ig, '%2F')}`); + resultingDataUrls.push(...dataUrls.map(dataUrl => dataUrl + pathSegment)); + dataMember = viewElement.label; + } + dataUrls = resultingDataUrls; + } else { + data = []; + for (let j = 0; j < dataUrls.length; ++j) { + const dataUrl = dataUrls[j]; + const restResult = (await restService.getConfigData(dataUrl)); + if (restResult.data == null || checkResponseCode(restResult)) { + const message = restResult.data && restResult.data.errors && restResult.data.errors.error && restResult.data.errors.error[0] && restResult.data.errors.error[0]['error-message'] || ''; + throw new Error(`Server Error. Status: [${restResult.status}]\n${message || restResult.message || ''}`); + } + let dataRaw = restResult.data[`${defaultNS}:${dataMember!}`]; + if (dataRaw === undefined) { + dataRaw = restResult.data[dataMember!]; + } + dataRaw = dataRaw instanceof Array + ? dataRaw[0] + : dataRaw; + data.push(dataRaw); + } + // BUG UUID ist nicht in den elements enthalten !!!!!! + const key = viewElement && viewElement.label || pathPart; + return { + view: view!, + data: data, + key: key, + }; + } + } + return null; +}; + +export const resolveViewDescription = (defaultNS: string | null, vPath: string, view: ViewSpecification): ViewSpecification =>{ + + // resolve all references. + view = { ...view }; + view.elements = Object.keys(view.elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { + const resolveHistory : ViewElement[] = []; + let elm = view.elements[cur]; + const key = defaultNS && cur.replace(new RegExp(`^${defaultNS}:`, 'i'), '') || cur; + while (isViewElementReference(elm)) { + const result = (elm.ref(vPath)); + if (result) { + const [referencedElement, referencedPath] = result; + if (resolveHistory.some(hist => hist === referencedElement)) { + console.error(`Circle reference found at: ${vPath}`, resolveHistory); + break; + } + elm = referencedElement; + vPath = referencedPath; + resolveHistory.push(elm); + } + } + + acc[key] = { ...elm, id: key }; + + return acc; + }, {}); + return view; +}; + +export const flattenViewElements = (defaultNS: string | null, parentPath: string, elements: { [name: string]: ViewElement }, views: ViewSpecification[], currentPath: string ): { [name: string]: ViewElement } => { + if (!elements) return {}; + return Object.keys(elements).reduce<{ [name: string]: ViewElement }>((acc, cur) => { + const elm = elements[cur]; + + // remove the default namespace, and only the default namespace, sine it seems that this is also not in the restconf response + const elmKey = defaultNS && elm.id.replace(new RegExp(`^${defaultNS}:`, 'i'), '') || elm.id; + const key = parentPath ? `${parentPath}.${elmKey}` : elmKey; + + if (isViewElementRpc(elm)) { + console.warn(`Flatten of RFC not supported ! [${currentPath}][${elm.label}]`); + return acc; + } else if (isViewElementObjectOrList(elm)) { + const view = views[+elm.viewId]; + const inner = view && flattenViewElements(defaultNS, key, view.elements, views, `${currentPath}/${view.name}`); + if (inner) { + Object.keys(inner).forEach(k => (acc[k] = inner[k])); + } + } else if (isViewElementChoice(elm)) { + acc[key] = { + ...elm, + id: key, + cases: Object.keys(elm.cases).reduce<{ [name: string]: ViewElementChoiceCase }>((accCases, curCases) => { + const caseElement = elm.cases[curCases]; + accCases[curCases] = { + ...caseElement, + // Hint: do not use key it contains elmKey, which shell be omitted for cases. + elements: flattenViewElements(defaultNS, /*key*/ parentPath, caseElement.elements, views, `${currentPath}/${elm.label}`), + }; + return accCases; + }, {}), + }; + } else { + acc[key] = { + ...elm, + id: key, + }; + } + return acc; + }, {}); +}; + +export const filterViewElements = async (vPath: string, viewData: any, viewSpecification: ViewSpecification) => { + // filter elements of viewSpecification by evaluating when property + return Object.keys(viewSpecification.elements).reduce(async (accPromise, cur) => { + const acc = await accPromise; + const elm = viewSpecification.elements[cur]; + if (!elm.when || await evaluateWhen(vPath || '', elm.when, viewData).catch((ex) => { + console.warn(`Error evaluating when clause at: ${viewSpecification.name} for element: ${cur}`, ex); + return true; + })) { + acc.elements[cur] = elm; + } + return acc; + }, Promise.resolve({ ...viewSpecification, elements: {} as { [key: string]: ViewElement } })); +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx index 0e2ddb395..0f143d818 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -25,17 +25,41 @@ import { WithStyles } from '@mui/styles'; import withStyles from '@mui/styles/withStyles'; import createStyles from '@mui/styles/createStyles'; -import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect"; -import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table"; -import { Loader } from "../../../../framework/src/components/material-ui/loader"; +import { useConfirm } from 'material-ui-confirm'; + +import { connect, IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table'; +import { Loader } from '../../../../framework/src/components/material-ui/loader'; import { renderObject } from '../../../../framework/src/components/objectDump'; import { DisplayModeType } from '../handlers/viewDescriptionHandler'; -import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator, updateViewActionAsyncCreator, removeElementActionAsyncCreator, executeRpcActionAsyncCreator } from "../actions/deviceActions"; -import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion, isViewElementRpc, ViewElementRpc, isViewElementEmpty, isViewElementDate } from "../models/uiModels"; - -import { getAccessPolicyByUrl } from "../../../../framework/src/services/restService"; +import { + SetSelectedValue, + updateDataActionAsyncCreator, + updateViewActionAsyncCreator, + removeElementActionAsyncCreator, + executeRpcActionAsyncCreator, +} from '../actions/deviceActions'; + +import { + ViewElement, + ViewSpecification, + ViewElementChoice, + ViewElementRpc, + isViewElementString, + isViewElementNumber, + isViewElementBoolean, + isViewElementObjectOrList, + isViewElementSelection, + isViewElementChoice, + isViewElementUnion, + isViewElementRpc, + isViewElementEmpty, + isViewElementDate, +} from '../models/uiModels'; + +import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService'; import Fab from '@mui/material/Fab'; import AddIcon from '@mui/icons-material/Add'; @@ -44,23 +68,22 @@ import ArrowBack from '@mui/icons-material/ArrowBack'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import SaveIcon from '@mui/icons-material/Save'; import EditIcon from '@mui/icons-material/Edit'; -import Tooltip from "@mui/material/Tooltip"; -import FormControl from "@mui/material/FormControl"; -import IconButton from "@mui/material/IconButton"; - -import InputLabel from "@mui/material/InputLabel"; -import Select from "@mui/material/Select"; -import MenuItem from "@mui/material/MenuItem"; -import Breadcrumbs from "@mui/material/Breadcrumbs"; +import Tooltip from '@mui/material/Tooltip'; +import FormControl from '@mui/material/FormControl'; +import IconButton from '@mui/material/IconButton'; + +import InputLabel from '@mui/material/InputLabel'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; import Button from '@mui/material/Button'; -import Link from "@mui/material/Link"; +import Link from '@mui/material/Link'; import Accordion from '@mui/material/Accordion'; import AccordionSummary from '@mui/material/AccordionSummary'; import AccordionDetails from '@mui/material/AccordionDetails'; import Typography from '@mui/material/Typography'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; - import { BaseProps } from '../components/baseProps'; import { UIElementReference } from '../components/uiElementReference'; import { UiElementNumber } from '../components/uiElementNumber'; @@ -70,44 +93,43 @@ import { UiElementSelection } from '../components/uiElementSelection'; import { UIElementUnion } from '../components/uiElementUnion'; import { UiElementLeafList } from '../components/uiElementLeafList'; -import { useConfirm } from 'material-ui-confirm'; -import restService from '../services/restServices'; +import { splitVPath } from '../utilities/viewEngineHelper'; const styles = (theme: Theme) => createStyles({ header: { - "display": "flex", - "justifyContent": "space-between", + 'display': 'flex', + 'justifyContent': 'space-between', }, leftButton: { - "justifyContent": "left" + 'justifyContent': 'left', }, outer: { - "flex": "1", - "height": "100%", - "display": "flex", - "alignItems": "center", - "justifyContent": "center", + 'flex': '1', + 'height': '100%', + 'display': 'flex', + 'alignItems': 'center', + 'justifyContent': 'center', }, inner: { }, container: { - "height": "100%", - "display": "flex", - "flexDirection": "column", + 'height': '100%', + 'display': 'flex', + 'flexDirection': 'column', }, - "icon": { - "marginRight": theme.spacing(0.5), - "width": 20, - "height": 20, + 'icon': { + 'marginRight': theme.spacing(0.5), + 'width': 20, + 'height': 20, }, - "fab": { - "margin": theme.spacing(1), + 'fab': { + 'margin': theme.spacing(1), }, button: { margin: 0, - padding: "6px 6px", - minWidth: 'unset' + padding: '6px 6px', + minWidth: 'unset', }, readOnly: { '& label.Mui-focused': { @@ -129,29 +151,29 @@ const styles = (theme: Theme) => createStyles({ }, }, uiView: { - overflowY: "auto", + overflowY: 'auto', }, section: { - padding: "15px", + padding: '15px', borderBottom: `2px solid ${theme.palette.divider}`, }, viewElements: { - width: 485, marginLeft: 20, marginRight: 20 + width: 485, marginLeft: 20, marginRight: 20, }, verificationElements: { - width: 485, marginLeft: 20, marginRight: 20 + width: 485, marginLeft: 20, marginRight: 20, }, heading: { fontSize: theme.typography.pxToRem(15), fontWeight: theme.typography.fontWeightRegular, }, moduleCollection: { - marginTop: "16px", - overflow: "auto", + marginTop: '16px', + overflow: 'auto', }, objectReult: { - overflow: "auto" - } + overflow: 'auto', + }, }); const mapProps = (state: IApplicationStoreState) => ({ @@ -183,10 +205,10 @@ type ConfigurationApplicationComponentState = { editMode: boolean; canEdit: boolean; viewData: { [key: string]: any } | null; - choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; -} + choices: { [path: string]: { selectedCase: string; data: { [property: string]: any } } }; +}; -type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any +type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any; const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => { const [disabled, setDisabled] = useState(true); const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => { @@ -202,7 +224,7 @@ const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionS ); }; -const OldProps = Symbol("OldProps"); +const OldProps = Symbol('OldProps'); class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> { /** @@ -216,17 +238,17 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp canEdit: false, editMode: false, viewData: null, - choises: {}, - } + choices: {}, + }; } - private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { + private static getChoicesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { return Object.keys(elements).reduce((acc, cur) => { const elm = elements[cur]; - if (isViewElementChoise(elm)) { + if (isViewElementChoice(elm)) { const caseKeys = Object.keys(elm.cases); - // find the right case for this choise, use the first one with data, at least use index 0 + // find the right case for this choice, use the first one with data, at least use index 0 const selectedCase = caseKeys.find(key => { const caseElm = elm.cases[key]; return Object.keys(caseElm.elements).some(caseElmKey => { @@ -255,26 +277,26 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp }; } return acc; - }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {} - } + }, {} 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.displaySpecification.displayMode === DisplayModeType.doNotDisplay + choices: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay || nextProps.displaySpecification.displayMode === DisplayModeType.displayAsMessage ? 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) - } + ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || [] + : ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData), + }; return state; } return null; @@ -282,24 +304,24 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp private navigate = (path: string) => { this.props.history.push(`${this.props.match.url}${path}`); - } + }; private changeValueFor = (property: string, value: any) => { this.setState({ viewData: { ...this.state.viewData, - [property]: value - } + [property]: value, + }, }); - } + }; private collectData = (elements: { [name: string]: ViewElement }) => { - // ensure only active choises will be contained + // ensure only active choices will be contained const viewData: { [key: string]: any } = { ...this.state.viewData }; - const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey])); - const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => { - const currentChoice = elements[curChoiceKey] as ViewElementChoise; - const selectedCase = this.state.choises[curChoiceKey].selectedCase; + const choiceKeys = Object.keys(elements).filter(elmKey => isViewElementChoice(elements[elmKey])); + const elementsToRemove = choiceKeys.reduce((acc, curChoiceKey) => { + const currentChoice = elements[curChoiceKey] as ViewElementChoice; + const selectedCase = this.state.choices[curChoiceKey].selectedCase; Object.keys(currentChoice.cases).forEach(caseKey => { const caseElements = currentChoice.cases[caseKey].elements; if (caseKey === selectedCase) { @@ -311,7 +333,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp } }); return; - }; + } Object.keys(caseElements).forEach(caseElementKey => { acc.push(caseElements[caseElementKey]); }); @@ -325,17 +347,17 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp } return acc; }, {} as { [key: string]: any }); - } + }; private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => { const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`); return !(policy.GET && policy.POST); - } + }; private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => { const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`); return !(policy.GET && policy.POST); - } + }; private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => { if (isViewElementEmpty(uiElement)) { @@ -353,12 +375,12 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp } else if (isViewElementUnion(uiElement)) { return UIElementUnion; } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + if (process.env.NODE_ENV !== 'production') { + console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`); } return null; } - } + }; private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { const isKey = (uiElement.label === keyProperty); @@ -377,7 +399,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp value={uiElement} readOnly={!canEdit} disabled={editMode && !canEdit} - onChange={(e) => { this.changeValueFor(uiElement.id, e) }} + onChange={(e) => { this.changeValueFor(uiElement.id, e); }} getEditorForViewElement={this.getEditorForViewElement} />; } else { @@ -391,7 +413,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp value={uiElement} readOnly={!canEdit} disabled={editMode && !canEdit} - onChange={(e) => { this.changeValueFor(uiElement.id, e) }} + onChange={(e) => { this.changeValueFor(uiElement.id, e); }} />) : null; } @@ -418,14 +440,14 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp // } // }; - private renderUIChoise = (uiElement: ViewElementChoise, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + private renderUIChoice = (uiElement: ViewElementChoice, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { const isKey = (uiElement.label === keyProperty); - const currentChoise = this.state.choises[uiElement.id]; - const currentCase = currentChoise && uiElement.cases[currentChoise.selectedCase]; + const currentChoice = this.state.choices[uiElement.id]; + const currentCase = currentChoice && uiElement.cases[currentChoice.selectedCase]; const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - if (isViewElementChoise(uiElement)) { + if (isViewElementChoice(uiElement)) { const subElements = currentCase?.elements; return ( <> @@ -435,14 +457,14 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp aria-label={uiElement.label + '-selection'} required={!!uiElement.mandatory} onChange={(e) => { - if (currentChoise.selectedCase === e.target.value) { + if (currentChoice.selectedCase === e.target.value) { return; // nothing changed } - this.setState({ choises: { ...this.state.choises, [uiElement.id]: { ...this.state.choises[uiElement.id], selectedCase: e.target.value as string } } }); + this.setState({ choices: { ...this.state.choices, [uiElement.id]: { ...this.state.choices[uiElement.id], selectedCase: e.target.value as string } } }); }} readOnly={!canEdit} disabled={editMode && !canEdit} - value={this.state.choises[uiElement.id].selectedCase} + value={this.state.choices[uiElement.id].selectedCase} inputProps={{ name: uiElement.id, id: `select-${uiElement.id}`, @@ -452,7 +474,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp Object.keys(uiElement.cases).map(caseKey => { const caseElm = uiElement.cases[caseKey]; return ( - <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem> + <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: '100%' }}>{caseElm.label}</div></Tooltip></MenuItem> ); }) } @@ -463,13 +485,13 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp const elm = subElements[elmKey]; return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); }) - : <h3>Invalid Choise</h3> + : <h3>Invalid Choice</h3> } </> ); } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + if (process.env.NODE_ENV !== 'production') { + console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`); } return null; } @@ -478,8 +500,6 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { const { classes } = this.props; - - const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { if (keyProperty) { // if (vsA.label === vsB.label) return 0; @@ -497,15 +517,15 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp const elm = viewSpecification.elements[cur]; if (isViewElementObjectOrList(elm)) { acc.references.push(elm); - } else if (isViewElementChoise(elm)) { - acc.choises.push(elm); + } else if (isViewElementChoice(elm)) { + acc.choices.push(elm); } else if (isViewElementRpc(elm)) { acc.rpcs.push(elm); } else { acc.elements.push(elm); } return acc; - }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }); + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }); sections.elements = sections.elements.sort(orderFunc); @@ -523,15 +543,15 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp ? ( <div className={classes.section}> {sections.references.map(element => ( - <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} /> ))} </div> ) : null } - {sections.choises.length > 0 + {sections.choices.length > 0 ? ( <div className={classes.section}> - {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} + {sections.choices.map(element => this.renderUIChoice(element, viewData, keyProperty, editMode, isNew))} </div> ) : null } @@ -539,7 +559,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp ? ( <div className={classes.section}> {sections.rpcs.map(element => ( - <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} /> ))} </div> ) : null @@ -550,6 +570,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { const { classes } = this.props; + // group by module name const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => { const elm = viewSpecification.elements[cur]; @@ -565,6 +586,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp { moduleKeys.map(key => { const moduleView = modules[key]; + return ( <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} > <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} > @@ -584,8 +606,8 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) { const listElements = listSpecification.elements; const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath - .replace("$$$standard$$$", "topology-netconfnode%20resources%20-%20RestConf%20RFC%208040") - .replace("$$$action$$$", "put")}${listKeyProperty ? `_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : '' }` : undefined; + .replace('$$$standard$$$', 'topology-netconfnode%20resources%20-%20RestConf%20RFC%208040') + .replace('$$$action$$$', 'put')}${listKeyProperty ? `_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : '' }` : undefined; const config = listSpecification.config && listKeyProperty; // We can not configure a list with no key. @@ -598,7 +620,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp tooltip: 'Add', ariaLabel:'add-element', onClick: () => { - navigate("[]"); // empty key means new element + navigate('[]'); // empty key means new element }, disabled: !config, }; @@ -615,11 +637,11 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp const { classes, removeElement } = this.props; - const DeleteIconWithConfirmation: React.FC<{disabled?: boolean, rowData: { [key: string]: any }, onReload: () => void }> = (props) => { + const DeleteIconWithConfirmation: React.FC<{ disabled?: boolean; rowData: { [key: string]: any }; onReload: () => void }> = (props) => { const confirm = useConfirm(); return ( - <Tooltip disableInteractive title={"Remove"} > + <Tooltip disableInteractive title={'Remove'} > <IconButton disabled={props.disabled} className={classes.button} @@ -627,15 +649,15 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp onClick={async (e) => { e.stopPropagation(); e.preventDefault(); - confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" }, cancellationButtonProps: { color:"inherit" } }) - .then(() => { - let keyId = ""; - if (listKeyProperty && listKeyProperty.split(" ").length > 1) { - keyId += listKeyProperty.split(" ").map(id => props.rowData[id]).join(","); + confirm({ title: 'Do you really want to delete this element ?', description: 'This action is permanent!', confirmationButtonProps: { color: 'secondary' }, cancellationButtonProps: { color:'inherit' } }) + .then(() => { + let keyId = ''; + if (listKeyProperty && listKeyProperty.split(' ').length > 1) { + keyId += listKeyProperty.split(' ').map(id => props.rowData[id]).join(','); } else { - keyId = props.rowData[listKeyProperty]; - } - return removeElement(`${this.props.vPath}[${keyId}]`) + keyId = props.rowData[listKeyProperty]; + } + return removeElement(`${this.props.vPath}[${keyId}]`); }).then(props.onReload); }} size="large"> @@ -643,44 +665,46 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp </IconButton> </Tooltip> ); - } + }; return ( <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={ Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { const elm = listElements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) { if (elm.label !== listKeyProperty) { - acc.push(elm.uiType === "boolean" + acc.push(elm.uiType === 'boolean' ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" + : elm.uiType === 'date' ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); } else { - acc.unshift(elm.uiType === "boolean" + acc.unshift(elm.uiType === 'boolean' ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" + : elm.uiType === 'date' ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); } } return acc; }, []).concat([{ - property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { + property: 'Actions', disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { return ( <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} /> ); - }) + }), }]) } onHandleClick={(ev, row) => { ev.preventDefault(); - let keyId = "" - if (listKeyProperty && listKeyProperty.split(" ").length > 1) { - keyId += listKeyProperty.split(" ").map(id => row[id]).join(","); + let keyId = ''; + if (listKeyProperty && listKeyProperty.split(' ').length > 1) { + keyId += listKeyProperty.split(' ').map(id => row[id]).join(','); } else { keyId = row[listKeyProperty]; } - listKeyProperty && navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key. + if (listKeyProperty) { + navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key. + } }} ></SelectElementTable> ); } @@ -704,17 +728,17 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp 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); + console.error('Object should not appear in RPC view !'); + } else if (isViewElementChoice(elm)) { + acc.choices.push(elm); } else if (isViewElementRpc(elm)) { - console.error("RPC should not appear in RPC view !"); + console.error('RPC should not appear in RPC view !'); } else { acc.elements.push(elm); } return acc; - }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }) - || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }) + || { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] }; sections.elements = sections.elements.sort(orderFunc); @@ -728,10 +752,10 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp </div> ) : null } - { sections.choises.length > 0 + { sections.choices.length > 0 ? ( <div className={classes.section}> - {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} + {sections.choices.map(element => this.renderUIChoice(element, inputViewData, keyProperty, editMode, isNew))} </div> ) : null } @@ -747,13 +771,13 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp </div> </> ); - }; + } private renderBreadCrumps() { const { editMode } = this.state; const { displaySpecification, vPath, nodeId } = this.props; const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - let lastPath = `/configuration`; + let lastPath = '/configuration'; let basePath = `/configuration/${nodeId}`; return ( <div className={this.props.classes.header}> @@ -774,7 +798,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp pathParts.map(([prop, key], ind) => { const path = `${basePath}/${prop}`; const keyPath = key && `${basePath}/${prop}[${key}]`; - const propTitle = prop.replace(/^[^:]+:/, ""); + const propTitle = prop.replace(/^[^:]+:/, ''); const ret = ( <span key={ind}> <Link underline="hover" color="inherit" href="#" @@ -789,8 +813,8 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp onClick={(ev: React.MouseEvent<HTMLElement>) => { ev.preventDefault(); this.props.history.push(keyPath); - }}>{`[${key && key.replace(/\%2C/g, ",")}]`}</Link> || null - } + }}>{`[${key && key.replace(/\%2C/g, ',')}]`}</Link> || null + } </span> ); lastPath = basePath; @@ -802,7 +826,9 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp </div> {this.state.editMode && ( <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => { - this.props.vPath && (await this.props.reloadView(this.props.vPath)); + if (this.props.vPath) { + await this.props.reloadView(this.props.vPath); + } this.setState({ editMode: false }); }} ><ArrowBack /></Fab> ) || null} @@ -810,7 +836,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div> <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => { if (this.state.editMode) { - // ensure only active choises will be contained + // ensure only active choices will be contained const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); this.props.onUpdateData(this.props.vPath!, resultingViewData); } @@ -830,7 +856,7 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp private renderValueSelector() { const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; if (!listKeyProperty || !listSpecification) { - throw new Error("ListKex ot view not specified."); + throw new Error('ListKex ot view not specified.'); } return ( @@ -838,11 +864,11 @@ class ConfigurationApplicationComponent extends React.Component<ConfigurationApp <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} columns={ Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { const elm = listSpecification.elements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) { if (elm.label !== listKeyProperty) { - acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + acc.push({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); } else { - acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + acc.unshift({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text }); } } return acc; diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx index 1a1008dad..e96f40d61 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx @@ -16,15 +16,15 @@ * ============LICENSE_END========================================================================== */ -import * as React from 'react'; +import React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; -import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect"; -import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; -import { MaterialTable, MaterialTableCtorType, ColumnType } from "../../../../framework/src/components/material-table"; -import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from "../../../configurationApp/src/handlers/connectedNetworkElementsHandler"; +import { connect, IDispatcher, Connect } from '../../../../framework/src/flux/connect'; +import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore'; +import { MaterialTable, MaterialTableCtorType, ColumnType } from '../../../../framework/src/components/material-table'; -import { NetworkElementConnection } from "../models/networkElementConnection"; +import { NetworkElementConnection } from '../models/networkElementConnection'; +import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from '../../../configurationApp/src/handlers/connectedNetworkElementsHandler'; const mapProps = (state: IApplicationStoreState) => ({ @@ -47,20 +47,20 @@ class NetworkElementSelectorComponent extends React.Component<NetworkElementSele if (!initialSorted) { initialSorted = true; - this.props.connectedNetworkElementsActions.onHandleRequestSort("node-id"); + this.props.connectedNetworkElementsActions.onHandleRequestSort('node-id'); } else this.props.connectedNetworkElementsActions.onRefresh(); } render() { return ( - <ConnectedElementTable stickyHeader tableId="configurable-elements-table" onHandleClick={(e, row) => { this.props.history.push(`${this.props.match.path}/${row.nodeId}`) }} columns={[ - { property: "nodeId", title: "Node Name", type: ColumnType.text }, - { property: "isRequired", title: "Required", type: ColumnType.boolean }, - { property: "host", title: "Host", type: ColumnType.text }, - { property: "port", title: "Port", type: ColumnType.numeric }, - { property: "coreModelCapability", title: "Core Model", type: ColumnType.text }, - { property: "deviceType", title: "Type", type: ColumnType.text }, + <ConnectedElementTable stickyHeader tableId="configurable-elements-table" onHandleClick={(e, row) => { this.props.history.push(`${this.props.match.path}/${row.nodeId}`); }} columns={[ + { property: 'nodeId', title: 'Node Name', type: ColumnType.text }, + { property: 'isRequired', title: 'Required', type: ColumnType.boolean }, + { property: 'host', title: 'Host', type: ColumnType.text }, + { property: 'port', title: 'Port', type: ColumnType.numeric }, + { property: 'coreModelCapability', title: 'Core Model', type: ColumnType.text }, + { property: 'deviceType', title: 'Type', type: ColumnType.text }, ]} idProperty="id" {...this.props.connectedNetworkElementsActions} {...this.props.connectedNetworkElementsProperties} asynchronus > </ConnectedElementTable> ); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts new file mode 100644 index 000000000..fa2968c9c --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/whenParser.ts @@ -0,0 +1,235 @@ +enum WhenTokenType { + AND = 'AND', + OR = 'OR', + NOT = 'NOT', + EQUALS = 'EQUALS', + COMMA = 'COMMA', + STRING = 'STRING', + FUNCTION = 'FUNCTION', + IDENTIFIER = 'IDENTIFIER', + OPEN_PAREN = 'OPEN_PAREN', + CLOSE_PAREN = 'CLOSE_PAREN', + EXPRESSION = 'EXPRESSION', +} + +type Token = { + type: WhenTokenType; + value: string; +}; + +const isAlpha = (char: string) => /[a-z]/i.test(char); + +const isAlphaNumeric = (char: string) => /[A-Za-z0-9_\-/:\.]/i.test(char); + +const lex = (input: string) : Token[] => { + let tokens = [] as any[]; + let current = 0; + + while (current < input.length) { + let char = input[current]; + + if (char === ' ') { + current++; + continue; + } + + if (char === '(') { + tokens.push({ type: WhenTokenType.OPEN_PAREN, value: char }); + current++; + continue; + } + + if (char === ')') { + tokens.push({ type: WhenTokenType.CLOSE_PAREN, value: char }); + current++; + continue; + } + + if (char === '=') { + tokens.push({ type: WhenTokenType.EQUALS, value: char }); + current++; + continue; + } + + if (char === ',') { + tokens.push({ type: WhenTokenType.COMMA, value: char }); + current++; + continue; + } + + if (char === '\"' || char === '\'') { + let value = ''; + let start = current; + current++; + + while (current < input.length) { + let innerChar = input[current]; + if (innerChar === '\\') { + value += input[current] + input[current + 1]; + current += 2; + } else if (innerChar === input[start]) { + current++; + break; + } else { + value += innerChar; + current++; + } + } + + tokens.push({ type: WhenTokenType.STRING, value }); + continue; + } + + if (isAlpha(char)) { + let value = ''; + while (isAlpha(char)) { + value += char; + char = input[++current]; + } + + switch (value) { + case 'and': + tokens.push({ type: WhenTokenType.AND }); + break; + case 'or': + tokens.push({ type: WhenTokenType.OR }); + break; + case 'not': + tokens.push({ type: WhenTokenType.NOT }); + break; + case 'eq': + tokens.push({ type: WhenTokenType.EQUALS }); + break; + default: + while (isAlphaNumeric(char)) { + value += char; + char = input[++current]; + } + tokens.push({ type: WhenTokenType.IDENTIFIER, value }); + } + + continue; + } + if (isAlphaNumeric(char)) { + let value = ''; + while (isAlphaNumeric(char)) { + value += char; + char = input[++current]; + } + + tokens.push({ type: WhenTokenType.IDENTIFIER, value }); + continue; + } + throw new TypeError(`I don't know what this character is: ${char}`); + } + return tokens; +}; + +type WhenAST = { + type: WhenTokenType; + left?: WhenAST; + right?: WhenAST; + value?: string | WhenAST; + name?: string; + args?: WhenAST[]; +}; + +const precedence : { [index: string] : number } = { + 'EQUALS': 4, + 'NOT': 3, + 'AND': 2, + 'OR': 1, +}; + +const parseWhen = (whenExpression: string) => { + const tokens = lex(whenExpression); + let current = 0; + + const walk = (precedenceLevel = 0) : WhenAST => { + let token = tokens[current]; + let node: WhenAST | null = null; + + if (token.type === WhenTokenType.OPEN_PAREN) { + token = tokens[++current]; + let innerNode: WhenAST = { type: WhenTokenType.EXPRESSION, value: walk() }; + token = tokens[current]; + + while (token.type !== WhenTokenType.CLOSE_PAREN) { + innerNode = { + type: token.type, + value: token.value, + left: innerNode, + right: walk(), + }; + token = tokens[current]; + } + current++; + return innerNode; + } + + if (token.type === WhenTokenType.STRING ) { + current++; + node = { type: token.type, value: token.value }; + } + + if (token.type === WhenTokenType.NOT) { + token = tokens[++current]; + node = { type: WhenTokenType.NOT, value: token.value, right: walk() }; + } + + if (token.type === WhenTokenType.IDENTIFIER) { + const nextToken = tokens[current + 1]; + if (nextToken.type === WhenTokenType.OPEN_PAREN) { + let name = token.value; + token = tokens[++current]; + + let args = []; + token = tokens[++current]; + + while (token.type !== WhenTokenType.CLOSE_PAREN) { + if (token.type === WhenTokenType.COMMA) { + current++; + } else { + args.push(walk()); + } + token = tokens[current]; + } + + current++; + node = { type: WhenTokenType.FUNCTION, name, args }; + } else { + current++; + node = { type: WhenTokenType.IDENTIFIER, value: token.value }; + } + } + + if (!node) throw new TypeError('Unexpected token: ' + token.type); + + token = tokens[current]; + while (current < tokens.length && precedence[token.type] >= precedenceLevel) { + console.log(current, tokens[current], tokens[current].type, precedenceLevel, precedence[token.type]); + token = tokens[current]; + if (token.type === WhenTokenType.EQUALS || token.type === WhenTokenType.AND || token.type === WhenTokenType.OR) { + current++; + node = { + type: token.type, + left: node, + right: walk(precedence[token.type]), + }; + } else { + break; + } + } + + return node; + + }; + + return walk(); +}; + +export { + parseWhen, + WhenAST, + WhenTokenType, +};
\ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts index e8e636f9b..cc2520100 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts +++ b/sdnr/wt/odlux/apps/configurationApp/src/yang/yangParser.ts @@ -1,3 +1,6 @@ +/* eslint-disable @typescript-eslint/no-loss-of-precision */ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +/* eslint-disable @typescript-eslint/naming-convention */ /** * ============LICENSE_START======================================================================== * ONAP : ccsdk feature sdnr wt odlux @@ -15,14 +18,30 @@ * the License. * ============LICENSE_END========================================================================== */ -import { Token, Statement, Module, Identity, ModuleState } from "../models/yang"; +import { Token, Statement, Module, Identity, ModuleState } from '../models/yang'; import { - ViewSpecification, ViewElement, isViewElementObjectOrList, ViewElementBase, - isViewElementReference, ViewElementChoise, ViewElementBinary, ViewElementString, isViewElementString, - isViewElementNumber, ViewElementNumber, Expression, YangRange, ViewElementUnion, ViewElementRpc, isViewElementRpc, ResolveFunction, ViewElementDate -} from "../models/uiModels"; -import { yangService } from "../services/yangService"; - + Expression, + ViewElement, + ViewElementBase, + ViewSpecification, + ViewElementNumber, + ViewElementString, + ViewElementChoice, + ViewElementUnion, + ViewElementRpc, + isViewElementObjectOrList, + isViewElementNumber, + isViewElementString, + isViewElementRpc, + ResolveFunction, + YangRange, +} from '../models/uiModels'; +import { yangService } from '../services/yangService'; + +const LOGLEVEL = +(localStorage.getItem('log.odlux.app.configuration.yang.yangParser') || 0); + +import { LogLevel } from '../../../../framework/src/utilities/logLevel'; +import { parseWhen, WhenAST, WhenTokenType } from './whenParser'; export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray[] => { const pathParts: RegExpMatchArray[] = []; @@ -32,21 +51,22 @@ export const splitVPath = (vPath: string, vPathParser: RegExp): RegExpMatchArray if (partMatch) { pathParts.push(partMatch); } - } while (partMatch) + } while (partMatch); return pathParts; -} +}; class YangLexer { private pos: number = 0; - private buf: string = ""; + + private buf: string = ''; constructor(input: string) { this.pos = 0; this.buf = input; } - private _optable: { [key: string]: string } = { + private _opTable: { [key: string]: string } = { ';': 'SEMI', '{': 'L_BRACE', '}': 'R_BRACE', @@ -66,7 +86,7 @@ class YangLexer { private _isAlpha(char: string): boolean { return (char >= 'a' && char <= 'z') || - (char >= 'A' && char <= 'Z') + (char >= 'A' && char <= 'Z'); } private _isAlphanum(char: string): boolean { @@ -74,7 +94,7 @@ class YangLexer { char === '_' || char === '-' || char === '.'; } - private _skipNontokens() { + private _skipNonTokens() { while (this.pos < this.buf.length) { const char = this.buf.charAt(this.pos); if (this._isWhitespace(char)) { @@ -90,11 +110,11 @@ class YangLexer { let end_index = this.pos + 1; while (end_index < this.buf.length) { const char = this.buf.charAt(end_index); - if (char === "\\") { + if (char === '\\') { end_index += 2; continue; - }; - if (terminator === null && (this._isWhitespace(char) || this._optable[char] !== undefined) || char === terminator) { + } + if (terminator === null && (this._isWhitespace(char) || this._opTable[char] !== undefined) || char === terminator) { break; } end_index++; @@ -109,7 +129,7 @@ class YangLexer { name: 'STRING', value: this.buf.substring(start, end), start, - end + end, }; this.pos = terminator ? end + 1 : end; return tok; @@ -122,8 +142,8 @@ class YangLexer { ++endpos; } - let name = 'IDENTIFIER' - if (this.buf.charAt(endpos) === ":") { + let name = 'IDENTIFIER'; + if (this.buf.charAt(endpos) === ':') { name = 'IDENTIFIERREF'; ++endpos; while (endpos < this.buf.length && this._isAlphanum(this.buf.charAt(endpos))) { @@ -135,7 +155,7 @@ class YangLexer { name: name, value: this.buf.substring(this.pos, endpos), start: this.pos, - end: endpos + end: endpos, }; this.pos = endpos; @@ -153,7 +173,7 @@ class YangLexer { name: 'NUMBER', value: this.buf.substring(this.pos, endpos), start: this.pos, - end: endpos + end: endpos, }; this.pos = endpos; return tok; @@ -171,7 +191,7 @@ class YangLexer { private _processBlockComment() { var endpos = this.pos + 2; // Skip until the end of the line - while (endpos < this.buf.length && !((this.buf.charAt(endpos) === "/" && this.buf.charAt(endpos - 1) === "*"))) { + while (endpos < this.buf.length && !((this.buf.charAt(endpos) === '/' && this.buf.charAt(endpos - 1) === '*'))) { endpos++; } this.pos = endpos + 1; @@ -179,87 +199,87 @@ class YangLexer { public tokenize(): Token[] { const result: Token[] = []; - this._skipNontokens(); + this._skipNonTokens(); while (this.pos < this.buf.length) { const char = this.buf.charAt(this.pos); - const op = this._optable[char]; + const op = this._opTable[char]; if (op !== undefined) { result.push({ name: op, value: char, start: this.pos, end: ++this.pos }); } else if (this._isAlpha(char)) { result.push(this._processIdentifier()); - this._skipNontokens(); + this._skipNonTokens(); const peekChar = this.buf.charAt(this.pos); - if (this._optable[peekChar] === undefined) { - result.push((peekChar !== "'" && peekChar !== '"') + if (this._opTable[peekChar] === undefined) { + result.push((peekChar !== '\'' && peekChar !== '"') ? this._processString(null) : this._processString(peekChar)); } - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') { this._processLineComment(); - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') { this._processBlockComment(); } else { - throw Error('Token error at ' + this.pos + " " + this.buf[this.pos]); + throw Error('Token error at ' + this.pos + ' ' + this.buf[this.pos]); } - this._skipNontokens(); + this._skipNonTokens(); } return result; } public tokenize2(): Statement { - let stack: Statement[] = [{ key: "ROOT", sub: [] }]; + let stack: Statement[] = [{ key: 'ROOT', sub: [] }]; let current: Statement | null = null; - this._skipNontokens(); + this._skipNonTokens(); while (this.pos < this.buf.length) { const char = this.buf.charAt(this.pos); - const op = this._optable[char]; + const op = this._opTable[char]; if (op !== undefined) { - if (op === "L_BRACE") { + if (op === 'L_BRACE') { current && stack.unshift(current); current = null; - } else if (op === "R_BRACE") { + } else if (op === 'R_BRACE') { current = stack.shift() || null; } this.pos++; - } else if (this._isAlpha(char) || char === "_") { + } else if (this._isAlpha(char) || char === '_') { const key = this._processIdentifier().value; - this._skipNontokens(); + this._skipNonTokens(); let peekChar = this.buf.charAt(this.pos); let arg = undefined; - if (this._optable[peekChar] === undefined) { - arg = (peekChar === '"' || peekChar === "'") + if (this._opTable[peekChar] === undefined) { + arg = (peekChar === '"' || peekChar === '\'') ? this._processString(peekChar).value : this._processString(null).value; } do { - this._skipNontokens(); + this._skipNonTokens(); peekChar = this.buf.charAt(this.pos); - if (peekChar !== "+") break; + if (peekChar !== '+') break; this.pos++; - this._skipNontokens(); + this._skipNonTokens(); peekChar = this.buf.charAt(this.pos); - arg += (peekChar === '"' || peekChar === "'") + arg += (peekChar === '"' || peekChar === '\'') ? this._processString(peekChar).value : this._processString(null).value; } while (true); current = { key, arg, sub: [] }; stack[0].sub!.push(current); - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "/") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '/') { this._processLineComment(); - } else if (char === '/' && this.buf.charAt(this.pos + 1) === "*") { + } else if (char === '/' && this.buf.charAt(this.pos + 1) === '*') { this._processBlockComment(); } else { - throw Error('Token error at ' + this.pos + " " + this.buf.slice(this.pos - 10, this.pos + 10)); + throw Error('Token error at ' + this.pos + ' ' + this.buf.slice(this.pos - 10, this.pos + 10)); } - this._skipNontokens(); + this._skipNonTokens(); } - if (stack[0].key !== "ROOT" || !stack[0].sub![0]) { - throw new Error("Internal Perser Error"); + if (stack[0].key !== 'ROOT' || !stack[0].sub![0]) { + throw new Error('Internal Perser Error'); } return stack[0].sub![0]; } @@ -269,25 +289,33 @@ export class YangParser { private _groupingsToResolve: ViewSpecification[] = []; private _identityToResolve: (() => void)[] = []; + private _unionsToResolve: (() => void)[] = []; + private _modulesToResolve: (() => void)[] = []; private _modules: { [name: string]: Module } = {}; + private _views: ViewSpecification[] = [{ - id: "0", - name: "root", - language: "en-US", + id: '0', + name: 'root', + language: 'en-US', canEdit: false, config: true, - parentView: "0", - title: "root", + parentView: '0', + title: 'root', elements: {}, }]; - public static ResolveStack = Symbol("ResolveStack"); + public static ResolveStack = Symbol('ResolveStack'); + + constructor( + private nodeId: string, + private _capabilityRevisionMap: { [capability: string]: string } = {}, + private _unavailableCapabilities: { failureReason: string; capability: string }[] = [], + private _importOnlyModules: { name: string; revision: string }[] = [], + ) { - constructor(private _unavailableCapabilities: { failureReason: string; capability: string; }[] = [], private _importOnlyModules: { name: string; revision: string; }[] = [], private nodeId: string) { - } public get modules() { @@ -300,8 +328,12 @@ export class YangParser { public async addCapability(capability: string, version?: string, parentImportOnlyModule?: boolean) { // do not add twice - if (this._modules[capability]) { - // console.warn(`Skipped capability: ${capability} since already contained.` ); + const existingCapability = this._modules[capability]; + const latestVersionExisting = existingCapability && Object.keys(existingCapability.revisions).sort().reverse()[0]; + if ((latestVersionExisting && version) && (version <= latestVersionExisting)) { + if (LOGLEVEL == LogLevel.Warning) { + console.warn(`Skipped capability: ${capability}:${version || ''} since already contained.`); + } return; } @@ -310,14 +342,15 @@ export class YangParser { // // console.warn(`Skipped capability: ${capability} since it is marked as unavailable.` ); // return; // } + const data = await yangService.getCapability(capability, this.nodeId, version); if (!data) { - throw new Error(`Could not load yang file for ${capability}.`); + throw new Error(`Could not load yang file for ${capability}:${version || ''}.`); } const rootStatement = new YangLexer(data).tokenize2(); - if (rootStatement.key !== "module") { + if (rootStatement.key !== 'module') { throw new Error(`Root element of ${capability} is not a module.`); } if (rootStatement.arg !== capability) { @@ -326,10 +359,32 @@ export class YangParser { const isUnavailable = this._unavailableCapabilities.some(c => c.capability === capability); const isImportOnly = parentImportOnlyModule === true || this._importOnlyModules.some(c => c.name === capability); + + // extract revisions + const revisions = this.extractNodes(rootStatement, 'revision').reduce<{ [version: string]: {} }>((acc, revision) => { + if (!revision.arg) { + throw new Error(`Module [${rootStatement.arg}] has a version w/o version number.`); + } + const description = this.extractValue(revision, 'description'); + const reference = this.extractValue(revision, 'reference'); + acc[revision.arg] = { + description, + reference, + }; + return acc; + }, {}); + + const latestVersionLoaded = Object.keys(revisions).sort().reverse()[0]; + if (existingCapability && latestVersionExisting >= latestVersionLoaded) { + if (LOGLEVEL == LogLevel.Warning) { + console.warn(`Skipped capability: ${capability}:${latestVersionLoaded} since ${capability}:${latestVersionExisting} already contained.`); + } + return; + } const module = this._modules[capability] = { name: rootStatement.arg, - revisions: {}, + revisions, imports: {}, features: {}, identities: {}, @@ -339,10 +394,10 @@ export class YangParser { views: {}, elements: {}, state: isUnavailable - ? ModuleState.unavailable - : isImportOnly - ? ModuleState.importOnly - : ModuleState.stable, + ? ModuleState.unavailable + : isImportOnly + ? ModuleState.importOnly + : ModuleState.stable, }; await this.handleModule(module, rootStatement, capability); @@ -351,84 +406,66 @@ export class YangParser { private async handleModule(module: Module, rootStatement: Statement, capability: string) { // extract namespace && prefix - module.namespace = this.extractValue(rootStatement, "namespace"); - module.prefix = this.extractValue(rootStatement, "prefix"); + module.namespace = this.extractValue(rootStatement, 'namespace'); + module.prefix = this.extractValue(rootStatement, 'prefix'); if (module.prefix) { module.imports[module.prefix] = capability; } - // extract revisions - const revisions = this.extractNodes(rootStatement, "revision"); - module.revisions = { - ...module.revisions, - ...revisions.reduce<{ [version: string]: {} }>((acc, version) => { - if (!version.arg) { - throw new Error(`Module [${module.name}] has a version w/o version number.`); - } - const description = this.extractValue(version, "description"); - const reference = this.extractValue(version, "reference"); - acc[version.arg] = { - description, - reference, - }; - return acc; - }, {}) - }; - // extract features - const features = this.extractNodes(rootStatement, "feature"); + const features = this.extractNodes(rootStatement, 'feature'); module.features = { ...module.features, ...features.reduce<{ [version: string]: {} }>((acc, feature) => { if (!feature.arg) { throw new Error(`Module [${module.name}] has a feature w/o name.`); } - const description = this.extractValue(feature, "description"); + const description = this.extractValue(feature, 'description'); acc[feature.arg] = { description, }; return acc; - }, {}) + }, {}), }; // extract imports - const imports = this.extractNodes(rootStatement, "import"); + const imports = this.extractNodes(rootStatement, 'import'); module.imports = { ...module.imports, ...imports.reduce<{ [key: string]: string }>((acc, imp) => { - const prefix = imp.sub && imp.sub.filter(s => s.key === "prefix"); + const prefix = imp.sub && imp.sub.filter(s => s.key === 'prefix'); if (!imp.arg) { throw new Error(`Module [${module.name}] has an import with neither name nor prefix.`); } acc[prefix && prefix.length === 1 && prefix[0].arg || imp.arg] = imp.arg; return acc; - }, {}) + }, {}), }; // import all required files and set module state if (imports) for (let ind = 0; ind < imports.length; ++ind) { - const moduleName = imports[ind].arg!; + const moduleName = imports[ind].arg!; - //TODO: Fix imports getting loaded without revision - await this.addCapability(moduleName, undefined, module.state === ModuleState.importOnly); + const revision = this._capabilityRevisionMap[moduleName] || undefined; + await this.addCapability(moduleName, revision, module.state === ModuleState.importOnly); const importedModule = this._modules[imports[ind].arg!]; if (importedModule && importedModule.state > ModuleState.stable) { - module.state = Math.max(module.state, ModuleState.instable); + module.state = Math.max(module.state, ModuleState.instable); } } - this.extractTypeDefinitions(rootStatement, module, ""); + this.extractTypeDefinitions(rootStatement, module, ''); - this.extractIdentities(rootStatement, 0, module, ""); + this.extractIdentities(rootStatement, 0, module, ''); - const groupings = this.extractGroupings(rootStatement, 0, module, ""); + const groupings = this.extractGroupings(rootStatement, 0, module, ''); this._views.push(...groupings); - const augments = this.extractAugments(rootStatement, 0, module, ""); + const augments = this.extractAugments(rootStatement, 0, module, ''); this._views.push(...augments); // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, ""); + const [currentView, subViews] = this.extractSubViews(rootStatement, 0, module, ''); this._views.push(currentView, ...subViews); // create the root elements for this module @@ -443,7 +480,7 @@ export class YangParser { const viewIdIndex = Number(viewElement.viewId); module.views[key] = this._views[viewIdIndex]; } - + // add only the UI View if the module is available if (module.state === ModuleState.stable || module.state === ModuleState.instable) this._views[0].elements[key] = module.elements[key]; }); @@ -462,7 +499,7 @@ export class YangParser { // process all groupings this._groupingsToResolve.filter(vs => vs.uses && vs.uses[ResolveFunction]).forEach(vs => { - try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!("|"); } catch (error) { + try { vs.uses![ResolveFunction] !== undefined && vs.uses![ResolveFunction]!('|'); } catch (error) { console.warn(`Error resolving: [${vs.name}] [${error.message}]`); } }); @@ -471,16 +508,16 @@ export class YangParser { * This is to fix the issue for sequential execution of modules based on their child and parent relationship * We are sorting the module object based on their augment status */ - Object.keys(this.modules) + Object.keys(this.modules) .map(elem => { - if(this.modules[elem].augments && Object.keys(this.modules[elem].augments).length > 0) { - const {augments, ...rest} = this.modules[elem]; - const partsOfKeys = Object.keys(augments).map((key) => (key.split("/").length - 1)) - this.modules[elem].executionOrder= Math.max(...partsOfKeys) - } else { - this.modules[elem].executionOrder=0; - } - }) + if (this.modules[elem].augments && Object.keys(this.modules[elem].augments).length > 0) { + const { augments, ..._rest } = this.modules[elem]; + const partsOfKeys = Object.keys(augments).map((key) => (key.split('/').length - 1)); + this.modules[elem].executionOrder = Math.max(...partsOfKeys); + } else { + this.modules[elem].executionOrder = 0; + } + }); // process all augmentations / sort by namespace changes to ensure proper order Object.keys(this.modules).sort((a, b) => this.modules[a].executionOrder! - this.modules[b].executionOrder!).forEach(modKey => { @@ -489,8 +526,8 @@ export class YangParser { const pathParts = splitVPath(key, /(?:(?:([^\/\:]+):)?([^\/]+))/g); // 1 = opt: namespace / 2 = property let nameSpaceChangeCounter = 0; let currentNS = module.name; // init namespace - pathParts.forEach(([ns, _])=> { - if (ns === currentNS){ + pathParts.forEach(([ns, _]) => { + if (ns === currentNS) { currentNS = ns; nameSpaceChangeCounter++; } @@ -498,11 +535,11 @@ export class YangParser { return { key, nameSpaceChangeCounter, - } + }; }); - + const augmentKeys = augmentKeysWithCounter - .sort((a,b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1 ) + .sort((a, b) => a.nameSpaceChangeCounter > b.nameSpaceChangeCounter ? 1 : a.nameSpaceChangeCounter === b.nameSpaceChangeCounter ? 0 : -1) .map((a) => a.key); augmentKeys.forEach(augKey => { @@ -512,11 +549,23 @@ export class YangParser { if (augments && viewSpec) { augments.forEach(augment => Object.keys(augment.elements).forEach(key => { const elm = augment.elements[key]; + + const when = elm.when && augment.when + ? { + type: WhenTokenType.AND, + left: elm.when, + right: augment.when, + } + : elm.when || augment.when; + + const ifFeature = elm.ifFeature + ? `(${augment.ifFeature}) and (${elm.ifFeature})` + : augment.ifFeature; + viewSpec.elements[key] = { ...augment.elements[key], - - when: elm.when ? `(${augment.when}) and (${elm.when})` : augment.when, - ifFeature: elm.ifFeature ? `(${augment.ifFeature}) and (${elm.ifFeature})` : augment.ifFeature, + when, + ifFeature, }; })); } @@ -534,7 +583,7 @@ export class YangParser { } } return result; - } + }; const baseIdentities: Identity[] = []; Object.keys(this.modules).forEach(modKey => { @@ -565,30 +614,31 @@ export class YangParser { } }); - // resolve readOnly - const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => { - - // update view config - view.config = view.config && parentConfig; - - Object.keys(view.elements).forEach((key) => { - const elm = view.elements[key]; - - // update element config - elm.config = elm.config && view.config; - - // update all sub-elements of objects - if (elm.uiType === "object") { - resolveReadOnly(this.views[+elm.viewId], elm.config); - } + // // resolve readOnly + // const resolveReadOnly = (view: ViewSpecification, parentConfig: boolean) => { - }) - } + // // update view config + // view.config = view.config && parentConfig; - const dump = resolveReadOnly(this.views[0], true); - }; + // Object.keys(view.elements).forEach((key) => { + // const elm = view.elements[key]; + + // // update element config + // elm.config = elm.config && view.config; + + // // update all sub-elements of objects + // if (elm.uiType === 'object') { + // resolveReadOnly(this.views[+elm.viewId], elm.config); + // } + + // }); + // }; + + // const dump = resolveReadOnly(this.views[0], true); + } private _nextId = 1; + private get nextId() { return this._nextId++; } @@ -608,7 +658,7 @@ export class YangParser { } private extractTypeDefinitions(statement: Statement, module: Module, currentPath: string): void { - const typedefs = this.extractNodes(statement, "typedef"); + const typedefs = this.extractNodes(statement, 'typedef'); typedefs && typedefs.forEach(def => { if (!def.arg) { throw new Error(`Module: [${module.name}]. Found typefed without name.`); @@ -620,7 +670,7 @@ export class YangParser { /** Handles groupings like named Container */ private extractGroupings(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] { const subViews: ViewSpecification[] = []; - const groupings = this.extractNodes(statement, "grouping"); + const groupings = this.extractNodes(statement, 'grouping'); if (groupings && groupings.length > 0) { subViews.push(...groupings.reduce<ViewSpecification[]>((acc, cur) => { if (!cur.arg) { @@ -629,9 +679,9 @@ export class YangParser { const grouping = cur.arg; // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath); + const [currentView, currentSubViews] = this.extractSubViews(cur, /* parentId */ -1, module, currentPath); grouping && (module.groupings[grouping] = currentView); - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } @@ -642,7 +692,7 @@ export class YangParser { /** Handles augments also like named container */ private extractAugments(statement: Statement, parentId: number, module: Module, currentPath: string): ViewSpecification[] { const subViews: ViewSpecification[] = []; - const augments = this.extractNodes(statement, "augment"); + const augments = this.extractNodes(statement, 'augment'); if (augments && augments.length > 0) { subViews.push(...augments.reduce<ViewSpecification[]>((acc, cur) => { if (!cur.arg) { @@ -651,12 +701,12 @@ export class YangParser { const augment = this.resolveReferencePath(cur.arg, module); // the default for config on module level is config = true; - const [currentView, subViews] = this.extractSubViews(cur, parentId, module, currentPath); + const [currentView, currentSubViews] = this.extractSubViews(cur, parentId, module, currentPath); if (augment) { module.augments[augment] = module.augments[augment] || []; module.augments[augment].push(currentView); } - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } @@ -666,109 +716,109 @@ export class YangParser { /** Handles identities */ private extractIdentities(statement: Statement, parentId: number, module: Module, currentPath: string) { - const identities = this.extractNodes(statement, "identity"); + const identities = this.extractNodes(statement, 'identity'); module.identities = identities.reduce<{ [name: string]: Identity }>((acc, cur) => { if (!cur.arg) { - throw new Error(`Module: [${module.name}][${currentPath}]. Found identiy without name.`); + throw new Error(`Module: [${module.name}][${currentPath}]. Found identity without name.`); } acc[cur.arg] = { id: `${module.name}:${cur.arg}`, label: cur.arg, - base: this.extractValue(cur, "base"), - description: this.extractValue(cur, "description"), - reference: this.extractValue(cur, "reference"), - children: [] - } + base: this.extractValue(cur, 'base'), + description: this.extractValue(cur, 'description'), + reference: this.extractValue(cur, 'reference'), + children: [], + }; return acc; }, {}); } - // Hint: use 0 as parentId for rootElements and -1 for rootGroupings. + // Hint: use 0 as parentId for rootElements and -1 for rootGroupings. private extractSubViews(statement: Statement, parentId: number, module: Module, currentPath: string): [ViewSpecification, ViewSpecification[]] { // used for scoped definitions const context: Module = { ...module, typedefs: { - ...module.typedefs - } + ...module.typedefs, + }, }; const currentId = this.nextId; const subViews: ViewSpecification[] = []; let elements: ViewElement[] = []; - const configValue = this.extractValue(statement, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const configValue = this.extractValue(statement, 'config'); + const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false'; // extract conditions - const ifFeature = this.extractValue(statement, "if-feature"); - const whenCondition = this.extractValue(statement, "when"); - if (whenCondition) console.warn("Found in [" + context.name + "]" + currentPath + " when: " + whenCondition); + const ifFeature = this.extractValue(statement, 'if-feature'); + const whenCondition = this.extractValue(statement, 'when'); + if (whenCondition) console.warn('Found in [' + context.name + ']' + currentPath + ' when: ' + whenCondition); // extract all scoped typedefs this.extractTypeDefinitions(statement, context, currentPath); // extract all scoped groupings subViews.push( - ...this.extractGroupings(statement, parentId, context, currentPath) + ...this.extractGroupings(statement, parentId, context, currentPath), ); // extract all container - const container = this.extractNodes(statement, "container"); + const container = this.extractNodes(statement, 'container'); if (container && container.length > 0) { subViews.push(...container.reduce<ViewSpecification[]>((acc, cur) => { if (!cur.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found container without name.`); } - const [currentView, subViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); + const [currentView, currentSubViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); elements.push({ id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg, label: cur.arg, path: currentPath, module: context.name || module.name || '', - uiType: "object", + uiType: 'object', viewId: currentView.id, config: currentView.config, }); - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } // process all lists // a list is a list of containers with the leafs contained in the list - const lists = this.extractNodes(statement, "list"); + const lists = this.extractNodes(statement, 'list'); if (lists && lists.length > 0) { subViews.push(...lists.reduce<ViewSpecification[]>((acc, cur) => { let elmConfig = config; if (!cur.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found list without name.`); } - const key = this.extractValue(cur, "key") || undefined; + const key = this.extractValue(cur, 'key') || undefined; if (elmConfig && !key) { console.warn(`Module: [${context.name}]${currentPath}. Found configurable list without key. Assume config shell be false.`); elmConfig = false; } - const [currentView, subViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); + const [currentView, currentSubViews] = this.extractSubViews(cur, currentId, context, `${currentPath}/${context.name}:${cur.arg}`); elements.push({ id: parentId === 0 ? `${context.name}:${cur.arg}` : cur.arg, label: cur.arg, path: currentPath, module: context.name || module.name || '', isList: true, - uiType: "object", + uiType: 'object', viewId: currentView.id, key: key, config: elmConfig && currentView.config, }); - acc.push(currentView, ...subViews); + acc.push(currentView, ...currentSubViews); return acc; }, [])); } // process all leaf-lists // a leaf-list is a list of some type - const leafLists = this.extractNodes(statement, "leaf-list"); + const leafLists = this.extractNodes(statement, 'leaf-list'); if (leafLists && leafLists.length > 0) { elements.push(...leafLists.reduce<ViewElement[]>((acc, cur) => { const element = this.getViewElement(cur, context, parentId, currentPath, true); @@ -779,7 +829,7 @@ export class YangParser { // process all leafs // a leaf is mainly a property of an object - const leafs = this.extractNodes(statement, "leaf"); + const leafs = this.extractNodes(statement, 'leaf'); if (leafs && leafs.length > 0) { elements.push(...leafs.reduce<ViewElement[]>((acc, cur) => { const element = this.getViewElement(cur, context, parentId, currentPath, false); @@ -789,92 +839,92 @@ export class YangParser { } - const choiceStms = this.extractNodes(statement, "choice"); + const choiceStms = this.extractNodes(statement, 'choice'); if (choiceStms && choiceStms.length > 0) { - elements.push(...choiceStms.reduce<ViewElementChoise[]>((accChoise, curChoise) => { - if (!curChoise.arg) { + elements.push(...choiceStms.reduce<ViewElementChoice[]>((accChoice, curChoice) => { + if (!curChoice.arg) { throw new Error(`Module: [${context.name}]${currentPath}. Found choise without name.`); } // extract all cases like containers - const cases: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[] = []; - const caseStms = this.extractNodes(curChoise, "case"); + const cases: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[] = []; + const caseStms = this.extractNodes(curChoice, 'case'); if (caseStms && caseStms.length > 0) { cases.push(...caseStms.reduce((accCase, curCase) => { if (!curCase.arg) { - throw new Error(`Module: [${context.name}]${currentPath}/${curChoise.arg}. Found case without name.`); + throw new Error(`Module: [${context.name}]${currentPath}/${curChoice.arg}. Found case without name.`); } - const description = this.extractValue(curCase, "description") || undefined; - const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, context, `${currentPath}/${context.name}:${curChoise.arg}`); + const description = this.extractValue(curCase, 'description') || undefined; + const [caseView, caseSubViews] = this.extractSubViews(curCase, parentId, context, `${currentPath}/${context.name}:${curChoice.arg}`); subViews.push(caseView, ...caseSubViews); - const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = { + const caseDef: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } = { id: parentId === 0 ? `${context.name}:${curCase.arg}` : curCase.arg, label: curCase.arg, description: description, - elements: caseView.elements + elements: caseView.elements, }; accCase.push(caseDef); return accCase; - }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[])); + }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[])); } // extract all simple cases (one case per leaf, container, etc.) - const [choiseView, choiseSubViews] = this.extractSubViews(curChoise, parentId, context, `${currentPath}/${context.name}:${curChoise.arg}`); - subViews.push(choiseView, ...choiseSubViews); - cases.push(...Object.keys(choiseView.elements).reduce((accElm, curElm) => { - const elm = choiseView.elements[curElm]; - const caseDef: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } = { + const [choiceView, choiceSubViews] = this.extractSubViews(curChoice, parentId, context, `${currentPath}/${context.name}:${curChoice.arg}`); + subViews.push(choiceView, ...choiceSubViews); + cases.push(...Object.keys(choiceView.elements).reduce((accElm, curElm) => { + const elm = choiceView.elements[curElm]; + const caseDef: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } = { id: elm.id, label: elm.label, description: elm.description, - elements: { [elm.id]: elm } + elements: { [elm.id]: elm }, }; accElm.push(caseDef); return accElm; - }, [] as { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } }[])); + }, [] as { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } }[])); - const description = this.extractValue(curChoise, "description") || undefined; - const configValue = this.extractValue(curChoise, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const choiceDescription = this.extractValue(curChoice, 'description') || undefined; + const choiceConfigValue = this.extractValue(curChoice, 'config'); + const choiceConfig = choiceConfigValue == null ? true : choiceConfigValue.toLocaleLowerCase() !== 'false'; - const mandatory = this.extractValue(curChoise, "mandatory") === "true" || false; + const mandatory = this.extractValue(curChoice, 'mandatory') === 'true' || false; - const element: ViewElementChoise = { - uiType: "choise", - id: parentId === 0 ? `${context.name}:${curChoise.arg}` : curChoise.arg, - label: curChoise.arg, + const element: ViewElementChoice = { + uiType: 'choice', + id: parentId === 0 ? `${context.name}:${curChoice.arg}` : curChoice.arg, + label: curChoice.arg, path: currentPath, module: context.name || module.name || '', - config: config, + config: choiceConfig, mandatory: mandatory, - description: description, + description: choiceDescription, cases: cases.reduce((acc, cur) => { acc[cur.id] = cur; return acc; - }, {} as { [name: string]: { id: string, label: string, description?: string, elements: { [name: string]: ViewElement } } }) + }, {} as { [name: string]: { id: string; label: string; description?: string; elements: { [name: string]: ViewElement } } }), }; - accChoise.push(element); - return accChoise; + accChoice.push(element); + return accChoice; }, [])); } - const rpcStms = this.extractNodes(statement, "rpc"); + const rpcStms = this.extractNodes(statement, 'rpc'); if (rpcStms && rpcStms.length > 0) { elements.push(...rpcStms.reduce<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"; + const rpcDescription = this.extractValue(curRpc, 'description') || undefined; + const rpcConfigValue = this.extractValue(curRpc, 'config'); + const rpcConfig = rpcConfigValue == null ? true : rpcConfigValue.toLocaleLowerCase() !== 'false'; let inputViewId: string | undefined = undefined; let outputViewId: string | undefined = undefined; - const input = this.extractNodes(curRpc, "input") || undefined; - const output = this.extractNodes(curRpc, "output") || undefined; + const input = this.extractNodes(curRpc, 'input') || undefined; + const output = this.extractNodes(curRpc, 'output') || undefined; if (input && input.length > 0) { const [inputView, inputSubViews] = this.extractSubViews(input[0], parentId, context, `${currentPath}/${context.name}:${curRpc.arg}`); @@ -889,13 +939,13 @@ export class YangParser { } const element: ViewElementRpc = { - uiType: "rpc", + uiType: 'rpc', id: parentId === 0 ? `${context.name}:${curRpc.arg}` : curRpc.arg, label: curRpc.arg, path: currentPath, module: context.name || module.name || '', - config: config, - description: description, + config: rpcConfig, + description: rpcDescription, inputViewId: inputViewId, outputViewId: outputViewId, }; @@ -906,9 +956,16 @@ export class YangParser { }, [])); } - // if (!statement.arg) { - // throw new Error(`Module: [${context.name}]. Found statement without name.`); - // } + if (!statement.arg) { + console.error(new Error(`Module: [${context.name}]. Found statement without name.`)); + } + + let whenParsed: WhenAST | undefined = undefined; + try { + whenParsed = whenCondition && parseWhen(whenCondition) || undefined; + } catch (e) { + console.error(new Error(`Module: [${context.name}]. Found invalid when condition: ${whenCondition}`)); + } const viewSpec: ViewSpecification = { id: String(currentId), @@ -916,11 +973,11 @@ export class YangParser { ns: context.name, name: statement.arg != null ? statement.arg : undefined, title: statement.arg != null ? statement.arg : undefined, - language: "en-us", + language: 'en-us', canEdit: false, config: config, ifFeature: ifFeature, - when: whenCondition, + when: whenParsed, elements: elements.reduce<{ [name: string]: ViewElement }>((acc, cur) => { acc[cur.id] = cur; return acc; @@ -928,21 +985,21 @@ export class YangParser { }; // evaluate canEdit depending on all conditions - Object.defineProperty(viewSpec, "canEdit", { + Object.defineProperty(viewSpec, 'canEdit', { get: () => { return Object.keys(viewSpec.elements).some(key => { const elm = viewSpec.elements[key]; return (!isViewElementObjectOrList(elm) && elm.config); }); - } + }, }); // merge in all uses references and resolve groupings - const usesRefs = this.extractNodes(statement, "uses"); + const usesRefs = this.extractNodes(statement, 'uses'); if (usesRefs && usesRefs.length > 0) { viewSpec.uses = (viewSpec.uses || []); - const resolveFunctions : ((parentElementPath: string)=>void)[] = []; + const resolveFunctions: ((parentElementPath: string) => void)[] = []; for (let i = 0; i < usesRefs.length; ++i) { const groupingName = usesRefs[i].arg; @@ -951,7 +1008,7 @@ export class YangParser { } viewSpec.uses.push(this.resolveReferencePath(groupingName, context)); - + resolveFunctions.push((parentElementPath: string) => { const groupingViewSpec = this.resolveGrouping(groupingName, context); if (groupingViewSpec) { @@ -963,10 +1020,22 @@ export class YangParser { Object.keys(groupingViewSpec.elements).forEach(key => { const elm = groupingViewSpec.elements[key]; // a useRef on root level need a namespace + const resolvedWhen = elm.when && groupingViewSpec.when + ? { + type: WhenTokenType.AND, + left: elm.when, + right: groupingViewSpec.when, + } + : elm.when || groupingViewSpec.when; + + const resolvedIfFeature = elm.ifFeature + ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` + : groupingViewSpec.ifFeature; + viewSpec.elements[parentId === 0 ? `${module.name}:${key}` : key] = { ...elm, - when: elm.when ? `(${groupingViewSpec.when}) and (${elm.when})` : groupingViewSpec.when, - ifFeature: elm.ifFeature ? `(${groupingViewSpec.ifFeature}) and (${elm.ifFeature})` : groupingViewSpec.ifFeature, + when: resolvedWhen, + ifFeature: resolvedIfFeature, }; }); } @@ -974,19 +1043,19 @@ export class YangParser { } viewSpec.uses[ResolveFunction] = (parentElementPath: string) => { - const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`; + const currentElementPath = `${parentElementPath} -> ${viewSpec.ns}:${viewSpec.name}`; resolveFunctions.forEach(resolve => { - try { - resolve(currentElementPath); - } catch (error) { - console.error(error); - } + try { + resolve(currentElementPath); + } catch (error) { + console.error(error); + } }); // console.log("Resolved "+currentElementPath, viewSpec); if (viewSpec?.uses) { viewSpec.uses[ResolveFunction] = undefined; } - } + }; this._groupingsToResolve.push(viewSpec); } @@ -1020,28 +1089,28 @@ export class YangParser { /** Extracts the UI View from the type in the cur statement. */ private getViewElement(cur: Statement, module: Module, parentId: number, currentPath: string, isList: boolean): ViewElement { - const type = this.extractValue(cur, "type"); - const defaultVal = this.extractValue(cur, "default") || undefined; - const description = this.extractValue(cur, "description") || undefined; + const type = this.extractValue(cur, 'type'); + const defaultVal = this.extractValue(cur, 'default') || undefined; + const description = this.extractValue(cur, 'description') || undefined; - const configValue = this.extractValue(cur, "config"); - const config = configValue == null ? true : configValue.toLocaleLowerCase() !== "false"; + const configValue = this.extractValue(cur, 'config'); + const config = configValue == null ? true : configValue.toLocaleLowerCase() !== 'false'; - const extractRange = (min: number, max: number, property: string = "range"): { expression: Expression<YangRange> | undefined, min: number, max: number } => { - const ranges = this.extractValue(this.extractNodes(cur, "type")[0]!, property) || undefined; - const range = ranges ?.replace(/min/i, String(min)).replace(/max/i, String(max)).split("|").map(r => { + const extractRange = (min: number, max: number, property: string = 'range'): { expression: Expression<YangRange> | undefined; min: number; max: number } => { + const ranges = this.extractValue(this.extractNodes(cur, 'type')[0]!, property) || undefined; + const range = ranges?.replace(/min/i, String(min)).replace(/max/i, String(max)).split('|').map(r => { let minValue: number; let maxValue: number; - + if (r.indexOf('..') > -1) { - const [minStr, maxStr] = r.split('..'); - minValue = Number(minStr); - maxValue = Number(maxStr); - } else if (!isNaN(maxValue = Number(r && r.trim() )) ) { - minValue = maxValue; + const [minStr, maxStr] = r.split('..'); + minValue = Number(minStr); + maxValue = Number(maxStr); + } else if (!isNaN(maxValue = Number(r && r.trim()))) { + minValue = maxValue; } else { - minValue = min, - maxValue = max; + minValue = min, + maxValue = max; } if (minValue > min) min = minValue; @@ -1049,7 +1118,7 @@ export class YangParser { return { min: minValue, - max: maxValue + max: maxValue, }; }); return { @@ -1058,21 +1127,22 @@ export class YangParser { expression: range && range.length === 1 ? range[0] : range && range.length > 1 - ? { operation: "OR", arguments: range } - : undefined - } + ? { operation: 'OR', arguments: range } + : undefined, + }; }; const extractPattern = (): Expression<RegExp> | undefined => { - const pattern = this.extractNodes(this.extractNodes(cur, "type")[0]!, "pattern").map(p => p.arg!).filter(p => !!p).map(p => `^${p.replace(/(?:\\(.))/g, '$1')}$`); + // 2023.01.26 decision MF & SKO: we will no longer remove the backslashes from the pattern, seems to be a bug in the original code + const pattern = this.extractNodes(this.extractNodes(cur, 'type')[0]!, 'pattern').map(p => p.arg!).filter(p => !!p).map(p => `^${p/*.replace(/(?:\\(.))/g, '$1')*/}$`); return pattern && pattern.length == 1 ? new RegExp(pattern[0]) : pattern && pattern.length > 1 - ? { operation: "AND", arguments: pattern.map(p => new RegExp(p)) } + ? { operation: 'AND', arguments: pattern.map(p => new RegExp(p)) } : undefined; - } + }; - const mandatory = this.extractValue(cur, "mandatory") === "true" || false; + const mandatory = this.extractValue(cur, 'mandatory') === 'true' || false; if (!cur.arg) { throw new Error(`Module: [${module.name}]. Found element without name.`); @@ -1084,159 +1154,159 @@ export class YangParser { const element: ViewElementBase = { id: parentId === 0 ? `${module.name}:${cur.arg}` : cur.arg, - label: cur.arg, + label: cur.arg, path: currentPath, - module: module.name || "", + module: module.name || '', config: config, mandatory: mandatory, isList: isList, default: defaultVal, - description: description + description: description, }; - if (type === "string") { - const length = extractRange(0, +18446744073709551615, "length"); + if (type === 'string') { + const length = extractRange(0, +18446744073709551615, 'length'); return ({ ...element, - uiType: "string", + uiType: 'string', length: length.expression, pattern: extractPattern(), }); - } else if (type === "boolean") { + } else if (type === 'boolean') { return ({ ...element, - uiType: "boolean" + uiType: 'boolean', }); - } else if (type === "uint8") { + } else if (type === 'uint8') { const range = extractRange(0, +255); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "uint16") { + } else if (type === 'uint16') { const range = extractRange(0, +65535); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "uint32") { + } else if (type === 'uint32') { const range = extractRange(0, +4294967295); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "uint64") { + } else if (type === 'uint64') { const range = extractRange(0, +18446744073709551615); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int8") { + } else if (type === 'int8') { const range = extractRange(-128, +127); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int16") { + } else if (type === 'int16') { const range = extractRange(-32768, +32767); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int32") { + } else if (type === 'int32') { const range = extractRange(-2147483648, +2147483647); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "int64") { + } else if (type === 'int64') { const range = extractRange(-9223372036854775808, +9223372036854775807); return ({ ...element, - uiType: "number", + uiType: 'number', range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "decimal64") { + } else if (type === 'decimal64') { // decimalRange - const fDigits = Number(this.extractValue(this.extractNodes(cur, "type")[0]!, "fraction-digits")) || -1; + const fDigits = Number(this.extractValue(this.extractNodes(cur, 'type')[0]!, 'fraction-digits')) || -1; if (fDigits === -1) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found decimal64 with invalid fraction-digits.`); } const range = extractRange(YangParser.decimalRange[fDigits].min, YangParser.decimalRange[fDigits].max); return ({ ...element, - uiType: "number", + uiType: 'number', fDigits: fDigits, range: range.expression, min: range.min, max: range.max, - units: this.extractValue(cur, "units") || undefined, - format: this.extractValue(cur, "format") || undefined, + units: this.extractValue(cur, 'units') || undefined, + format: this.extractValue(cur, 'format') || undefined, }); - } else if (type === "enumeration") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const enumNodes = this.extractNodes(typeNode, "enum"); + } else if (type === 'enumeration') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const enumNodes = this.extractNodes(typeNode, 'enum'); return ({ ...element, - uiType: "selection", + uiType: 'selection', options: enumNodes.reduce<{ key: string; value: string; description?: string }[]>((acc, enumNode) => { if (!enumNode.arg) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found option without name.`); } - const ifClause = this.extractValue(enumNode, "if-feature"); - const value = this.extractValue(enumNode, "value"); + // const ifClause = this.extractValue(enumNode, 'if-feature'); + const value = this.extractValue(enumNode, 'value'); const enumOption = { key: enumNode.arg, value: value != null ? value : enumNode.arg, - description: this.extractValue(enumNode, "description") || undefined + description: this.extractValue(enumNode, 'description') || undefined, }; // todo: ❗ handle the if clause ⚡ acc.push(enumOption); return acc; - }, []) + }, []), }); - } else if (type === "leafref") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const vPath = this.extractValue(typeNode, "path"); + } else if (type === 'leafref') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const vPath = this.extractValue(typeNode, 'path'); if (!vPath) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found leafref without path.`); } @@ -1244,11 +1314,11 @@ export class YangParser { const resolve = this.resolveReference.bind(this); const res: ViewElement = { ...element, - uiType: "reference", + uiType: 'reference', referencePath: refPath, - ref(this: ViewElement, currentPath: string) { - const elementPath = `${currentPath}/${cur.arg}`; - + ref(this: ViewElement, basePath: string) { + const elementPath = `${basePath}/${cur.arg}`; + const result = resolve(refPath, elementPath); if (!result) return undefined; @@ -1262,20 +1332,20 @@ export class YangParser { isList: this.isList, default: this.default, description: this.description, - } as ViewElement , resolvedPath] || undefined; - } + } as ViewElement, resolvedPath] || undefined; + }, }; return res; - } else if (type === "identityref") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const base = this.extractValue(typeNode, "base"); + } else if (type === 'identityref') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const base = this.extractValue(typeNode, 'base'); if (!base) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found identityref without base.`); } const res: ViewElement = { ...element, - uiType: "selection", - options: [] + uiType: 'selection', + options: [], }; this._identityToResolve.push(() => { const identity: Identity = this.resolveIdentity(base, module); @@ -1288,29 +1358,29 @@ export class YangParser { res.options = identity.values.map(val => ({ key: val.id, value: val.id, - description: val.description + description: val.description, })); }); return res; - } else if (type === "empty") { + } else if (type === 'empty') { // todo: ❗ handle empty ⚡ /* 9.11. The empty Built-In Type The empty built-in type represents a leaf that does not have any value, it conveys information by its presence or absence. */ return { ...element, - uiType: "empty", + uiType: 'empty', }; - } else if (type === "union") { + } else if (type === 'union') { // todo: ❗ handle union ⚡ /* 9.12. The union Built-In Type */ - const typeNode = this.extractNodes(cur, "type")[0]!; - const typeNodes = this.extractNodes(typeNode, "type"); + const typeNode = this.extractNodes(cur, 'type')[0]!; + const typeNodes = this.extractNodes(typeNode, 'type'); const resultingElement = { ...element, - uiType: "union", - elements: [] + uiType: 'union', + elements: [], } as ViewElementUnion; const resolveUnion = () => { @@ -1318,13 +1388,13 @@ export class YangParser { const stm: Statement = { ...cur, sub: [ - ...(cur.sub ?.filter(s => s.key !== "type") || []), - node - ] + ...(cur.sub?.filter(s => s.key !== 'type') || []), + node, + ], }; return { ...this.getViewElement(stm, module, parentId, currentPath, isList), - id: node.arg! + id: node.arg!, }; })); }; @@ -1332,34 +1402,34 @@ export class YangParser { this._unionsToResolve.push(resolveUnion); return resultingElement; - } else if (type === "bits") { - const typeNode = this.extractNodes(cur, "type")[0]!; - const bitNodes = this.extractNodes(typeNode, "bit"); + } else if (type === 'bits') { + const typeNode = this.extractNodes(cur, 'type')[0]!; + const bitNodes = this.extractNodes(typeNode, 'bit'); return { ...element, - uiType: "bits", - flags: bitNodes.reduce<{ [name: string]: number | undefined; }>((acc, bitNode) => { + uiType: 'bits', + flags: bitNodes.reduce<{ [name: string]: number | undefined }>((acc, bitNode) => { if (!bitNode.arg) { throw new Error(`Module: [${module.name}][${currentPath}][${cur.arg}]. Found bit without name.`); } - const ifClause = this.extractValue(bitNode, "if-feature"); - const pos = Number(this.extractValue(bitNode, "position")); + // const ifClause = this.extractValue(bitNode, 'if-feature'); + const pos = Number(this.extractValue(bitNode, 'position')); acc[bitNode.arg] = pos === pos ? pos : undefined; return acc; - }, {}) + }, {}), }; - } else if (type === "binary") { + } else if (type === 'binary') { return { ...element, - uiType: "binary", - length: extractRange(0, +18446744073709551615, "length"), + uiType: 'binary', + length: extractRange(0, +18446744073709551615, 'length'), }; - } else if (type === "instance-identifier") { + } else if (type === 'instance-identifier') { // https://tools.ietf.org/html/rfc7950#page-168 return { ...element, - uiType: "string", - length: extractRange(0, +18446744073709551615, "length"), + uiType: 'string', + length: extractRange(0, +18446744073709551615, 'length'), }; } else { // not a build in type, need to resolve type @@ -1374,13 +1444,13 @@ export class YangParser { } // spoof date type here from special string type - if ((type === 'date-and-time' || type.endsWith(':date-and-time') ) && typeRef.module === "ietf-yang-types") { - return { - ...typeRef, - ...element, - description: description, - uiType: "date", - }; + if ((type === 'date-and-time' || type.endsWith(':date-and-time')) && typeRef.module === 'ietf-yang-types') { + return { + ...typeRef, + ...element, + description: description, + uiType: 'date', + }; } return ({ @@ -1391,27 +1461,27 @@ export class YangParser { } } - private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined, min: number, max: number }) { + private resolveStringType(parentElement: ViewElementString, pattern: Expression<RegExp> | undefined, length: { expression: Expression<YangRange> | undefined; min: number; max: number }) { return { ...parentElement, pattern: pattern != null && parentElement.pattern - ? { operation: "AND", arguments: [pattern, parentElement.pattern] } + ? { operation: 'AND', arguments: [pattern, parentElement.pattern] } : parentElement.pattern ? parentElement.pattern : pattern, length: length.expression != null && parentElement.length - ? { operation: "AND", arguments: [length.expression, parentElement.length] } + ? { operation: 'AND', arguments: [length.expression, parentElement.length] } : parentElement.length ? parentElement.length - : length ?.expression, + : length?.expression, } as ViewElementString; } - private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined, min: number, max: number }) { + private resolveNumberType(parentElement: ViewElementNumber, range: { expression: Expression<YangRange> | undefined; min: number; max: number }) { return { ...parentElement, range: range.expression != null && parentElement.range - ? { operation: "AND", arguments: [range.expression, parentElement.range] } + ? { operation: 'AND', arguments: [range.expression, parentElement.range] } : parentElement.range ? parentElement.range : range, @@ -1421,7 +1491,7 @@ export class YangParser { } private resolveReferencePath(vPath: string, module: Module) { - const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g // 1 = opt: namespace / 2 = property + const vPathParser = /(?:(?:([^\/\:]+):)?([^\/]+))/g; // 1 = opt: namespace / 2 = property return vPath.replace(vPathParser, (_, ns, property) => { const nameSpace = ns && module.imports[ns] || module.name; return `${nameSpace}:${property}`; @@ -1429,20 +1499,20 @@ export class YangParser { } private resolveReference(vPath: string, currentPath: string) { - const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath + const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath let element: ViewElement | null = null; - let moduleName = ""; + let moduleName = ''; const vPathParts = splitVPath(vPath, vPathParser).map(p => ({ ns: p[1], property: p[2], ind: p[3] })); - const resultPathParts = !vPath.startsWith("/") - ? splitVPath(currentPath, vPathParser).map(p => { moduleName = p[1] || moduleName ; return { ns: moduleName, property: p[2], ind: p[3] } }) + const resultPathParts = !vPath.startsWith('/') + ? splitVPath(currentPath, vPathParser).map(p => { moduleName = p[1] || moduleName; return { ns: moduleName, property: p[2], ind: p[3] }; }) : []; for (let i = 0; i < vPathParts.length; ++i) { const vPathPart = vPathParts[i]; - if (vPathPart.property === "..") { + if (vPathPart.property === '..') { resultPathParts.pop(); - } else if (vPathPart.property !== ".") { + } else if (vPathPart.property !== '.') { resultPathParts.push(vPathPart); } } @@ -1453,30 +1523,30 @@ export class YangParser { if (j === 0) { moduleName = pathPart.ns; const rootModule = this._modules[moduleName]; - if (!rootModule) throw new Error("Could not resolve module [" + moduleName + "].\r\n" + vPath); + if (!rootModule) throw new Error('Could not resolve module [' + moduleName + '].\r\n' + vPath); element = rootModule.elements[`${pathPart.ns}:${pathPart.property}`]; } else if (element && isViewElementObjectOrList(element)) { const view: ViewSpecification = this._views[+element.viewId]; if (moduleName !== pathPart.ns) { moduleName = pathPart.ns; - } + } element = view.elements[pathPart.property] || view.elements[`${moduleName}:${pathPart.property}`]; } else { - throw new Error("Could not resolve reference.\r\n" + vPath); + throw new Error('Could not resolve reference.\r\n' + vPath); } - if (!element) throw new Error("Could not resolve path [" + pathPart.property + "] in [" + currentPath + "] \r\n" + vPath); + if (!element) throw new Error('Could not resolve path [' + pathPart.property + '] in [' + currentPath + '] \r\n' + vPath); } - moduleName = ""; // create the vPath for the resolved element, do not add the element itself this will be done later in the res(...) function - return [element, resultPathParts.slice(0,-1).map(p => `${moduleName !== p.ns ? `${moduleName=p.ns}:` : ""}${p.property}${p.ind || ''}`).join("/")]; + moduleName = ''; // create the vPath for the resolved element, do not add the element itself this will be done later in the res(...) function + return [element, resultPathParts.slice(0, -1).map(p => `${moduleName !== p.ns ? `${moduleName = p.ns}:` : ''}${p.property}${p.ind || ''}`).join('/')]; } private resolveView(vPath: string) { - const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g // 1 = opt: namespace / 2 = property / 3 = opt: indexPath + const vPathParser = /(?:(?:([^\/\[\]\:]+):)?([^\/\[\]]+)(\[[^\]]+\])?)/g; // 1 = opt: namespace / 2 = property / 3 = opt: indexPath let element: ViewElement | null = null; let partMatch: RegExpExecArray | null; let view: ViewSpecification | null = null; - let moduleName = ""; + let moduleName = ''; if (vPath) do { partMatch = vPathParser.exec(vPath); if (partMatch) { @@ -1498,13 +1568,13 @@ export class YangParser { } if (!element) return null; } - } while (partMatch) + } while (partMatch); return element && isViewElementObjectOrList(element) && this._views[+element.viewId] || null; } private resolveType(type: string, module: Module) { - const collonInd = type.indexOf(":"); - const preFix = collonInd > -1 ? type.slice(0, collonInd) : ""; + const collonInd = type.indexOf(':'); + const preFix = collonInd > -1 ? type.slice(0, collonInd) : ''; const typeName = collonInd > -1 ? type.slice(collonInd + 1) : type; const res = preFix @@ -1514,8 +1584,8 @@ export class YangParser { } private resolveGrouping(grouping: string, module: Module) { - const collonInd = grouping.indexOf(":"); - const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : ""; + const collonInd = grouping.indexOf(':'); + const preFix = collonInd > -1 ? grouping.slice(0, collonInd) : ''; const groupingName = collonInd > -1 ? grouping.slice(collonInd + 1) : grouping; return preFix @@ -1525,8 +1595,8 @@ export class YangParser { } private resolveIdentity(identity: string, module: Module) { - const collonInd = identity.indexOf(":"); - const preFix = collonInd > -1 ? identity.slice(0, collonInd) : ""; + const collonInd = identity.indexOf(':'); + const preFix = collonInd > -1 ? identity.slice(0, collonInd) : ''; const identityName = collonInd > -1 ? identity.slice(collonInd + 1) : identity; return preFix |