diff options
author | 2021-04-08 07:27:18 +0200 | |
---|---|---|
committer | 2021-04-08 07:27:28 +0200 | |
commit | 21e4a946cd24b8a03ea577352f0271ebf7669ffa (patch) | |
tree | 4227d8566770b75c2c25b67c764038288cacfe3d /sdnr/wt/odlux/apps/configurationApp/src/views | |
parent | a252be83694ae33260d99d5371ed48c1558aa2e8 (diff) |
update odlux for notification change
update due new notification protocol
Issue-ID: CCSDK-3253
Signed-off-by: Michael DÜrre <michael.duerre@highstreet-technologies.com>
Change-Id: Iad65459fdc18603cd1ddbd97bb2211308744bd8b
Diffstat (limited to 'sdnr/wt/odlux/apps/configurationApp/src/views')
-rw-r--r-- | sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx | 1452 | ||||
-rw-r--r-- | sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx | 2 |
2 files changed, 745 insertions, 709 deletions
diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx index dbaa77874..e466dbacc 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -16,7 +16,7 @@ * ============LICENSE_END========================================================================== */ -import React from 'react'; +import React, { useState } from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; @@ -31,6 +31,8 @@ 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 Fab from '@material-ui/core/Fab'; import AddIcon from '@material-ui/icons/Add'; import PostAdd from '@material-ui/icons/PostAdd'; @@ -65,106 +67,107 @@ import { UIElementUnion } from '../components/uiElementUnion'; import { UiElementLeafList } from '../components/uiElementLeafList'; import { useConfirm } from 'material-ui-confirm'; +import restService from '../services/restServices'; const styles = (theme: Theme) => createStyles({ - header: { - "display": "flex", - "justifyContent": "space-between", - }, - leftButton: { - "justifyContent": "left" - }, - outer: { - "flex": "1", - "height": "100%", - "display": "flex", - "alignItems": "center", - "justifyContent": "center", - }, - inner: { - - }, - container: { - "height": "100%", - "display": "flex", - "flexDirection": "column", - }, - "icon": { - "marginRight": theme.spacing(0.5), - "width": 20, - "height": 20, - }, - "fab": { - "margin": theme.spacing(1), + header: { + "display": "flex", + "justifyContent": "space-between", + }, + leftButton: { + "justifyContent": "left" + }, + outer: { + "flex": "1", + "height": "100%", + "display": "flex", + "alignItems": "center", + "justifyContent": "center", + }, + inner: { + + }, + container: { + "height": "100%", + "display": "flex", + "flexDirection": "column", + }, + "icon": { + "marginRight": theme.spacing(0.5), + "width": 20, + "height": 20, + }, + "fab": { + "margin": theme.spacing(1), + }, + button: { + margin: 0, + padding: "6px 6px", + minWidth: 'unset' + }, + readOnly: { + '& label.Mui-focused': { + color: 'green', }, - button: { - margin: 0, - padding: "6px 6px", - minWidth: 'unset' + '& .MuiInput-underline:after': { + borderBottomColor: 'green', }, - readOnly: { - '& label.Mui-focused': { - color: 'green', - }, - '& .MuiInput-underline:after': { - borderBottomColor: 'green', - }, - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: 'red', - }, - '&:hover fieldset': { - borderColor: 'yellow', - }, - '&.Mui-focused fieldset': { - borderColor: 'green', - }, - }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'red', + }, + '&:hover fieldset': { + borderColor: 'yellow', + }, + '&.Mui-focused fieldset': { + borderColor: 'green', + }, }, - uiView: { - overflowY: "auto", - }, - section: { - padding: "15px", - borderBottom: `2px solid ${theme.palette.divider}`, - }, - viewElements: { - width: 485, marginLeft: 20, marginRight: 20 - }, - verificationElements: { - width: 485, marginLeft: 20, marginRight: 20 - }, - heading: { - fontSize: theme.typography.pxToRem(15), - fontWeight: theme.typography.fontWeightRegular, - }, - moduleCollection: { - marginTop: "16px", - overflow: "auto", - }, - objectReult: { - overflow: "auto" - } + }, + uiView: { + overflowY: "auto", + }, + section: { + padding: "15px", + borderBottom: `2px solid ${theme.palette.divider}`, + }, + viewElements: { + width: 485, marginLeft: 20, marginRight: 20 + }, + verificationElements: { + width: 485, marginLeft: 20, marginRight: 20 + }, + heading: { + fontSize: theme.typography.pxToRem(15), + fontWeight: theme.typography.fontWeightRegular, + }, + moduleCollection: { + marginTop: "16px", + overflow: "auto", + }, + objectReult: { + overflow: "auto" + } }); const mapProps = (state: IApplicationStoreState) => ({ - collectingData: state.configuration.valueSelector.collectingData, - listKeyProperty: state.configuration.valueSelector.keyProperty, - listSpecification: state.configuration.valueSelector.listSpecification, - listData: state.configuration.valueSelector.listData, - vPath: state.configuration.viewDescription.vPath, - nodeId: state.configuration.deviceDescription.nodeId, - viewData: state.configuration.viewDescription.viewData, - outputData: state.configuration.viewDescription.outputData, - displaySpecification: state.configuration.viewDescription.displaySpecification, + collectingData: state.configuration.valueSelector.collectingData, + listKeyProperty: state.configuration.valueSelector.keyProperty, + listSpecification: state.configuration.valueSelector.listSpecification, + listData: state.configuration.valueSelector.listData, + vPath: state.configuration.viewDescription.vPath, + nodeId: state.configuration.deviceDescription.nodeId, + viewData: state.configuration.viewDescription.viewData, + outputData: state.configuration.viewDescription.outputData, + displaySpecification: state.configuration.viewDescription.displaySpecification, }); const mapDispatch = (dispatcher: IDispatcher) => ({ - onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), - onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)), - reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), - removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)), - executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)), + onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), + onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)), + reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), + removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)), + executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)), }); const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>; @@ -172,661 +175,694 @@ const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>; type ConfigurationApplicationComponentState = { - isNew: boolean; - editMode: boolean; - canEdit: boolean; - viewData: { [key: string]: any } | null; - choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; + isNew: boolean; + editMode: boolean; + canEdit: boolean; + viewData: { [key: string]: any } | null; + choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; } +type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any +const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => { + const [disabled, setDisabled] = useState(true); + const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => { + if (ev.button === 1) { + setDisabled(!disabled); + ev.preventDefault(); + } + }; + return ( + <div onMouseDown={onMouseDown} > + <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} /> + </div> + ); +}; + const OldProps = Symbol("OldProps"); class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> { - /** - * - */ - constructor(props: ConfigurationApplicationComponentProps) { - super(props); - - this.state = { - isNew: false, - canEdit: false, - editMode: false, - viewData: null, - choises: {}, - } + /** + * + */ + constructor(props: ConfigurationApplicationComponentProps) { + super(props); + + this.state = { + isNew: false, + canEdit: false, + editMode: false, + viewData: null, + choises: {}, } - - private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { - return Object.keys(elements).reduce((acc, cur) => { - const elm = elements[cur]; - if (isViewElementChoise(elm)) { - const caseKeys = Object.keys(elm.cases); - - // find the right case for this choise, use the first one with data, at least use index 0 - const selectedCase = caseKeys.find(key => { - const caseElm = elm.cases[key]; - return Object.keys(caseElm.elements).some(caseElmKey => { - const caseElmElm = caseElm.elements[caseElmKey]; - return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined; - }); - }) || caseKeys[0]; - - // extract all data of the active case - const caseElements = elm.cases[selectedCase].elements; - const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => { - const dataElm = caseElements[dataCur]; - if (isViewElementEmpty(dataElm)) { - dataAcc[dataElm.label] = null; - } else if (viewData[dataElm.label] !== undefined) { - dataAcc[dataElm.label] = viewData[dataElm.label]; - } else if (viewData[dataElm.id] !== undefined) { - dataAcc[dataElm.id] = viewData[dataElm.id]; - } - return dataAcc; - }, {} as { [name: string]: any }); - - acc[elm.id] = { - selectedCase, - data, - }; - } - return acc; - }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {} + } + + private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => { + return Object.keys(elements).reduce((acc, cur) => { + const elm = elements[cur]; + if (isViewElementChoise(elm)) { + const caseKeys = Object.keys(elm.cases); + + // find the right case for this choise, use the first one with data, at least use index 0 + const selectedCase = caseKeys.find(key => { + const caseElm = elm.cases[key]; + return Object.keys(caseElm.elements).some(caseElmKey => { + const caseElmElm = caseElm.elements[caseElmKey]; + return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined; + }); + }) || caseKeys[0]; + + // extract all data of the active case + const caseElements = elm.cases[selectedCase].elements; + const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => { + const dataElm = caseElements[dataCur]; + if (isViewElementEmpty(dataElm)) { + dataAcc[dataElm.label] = null; + } else if (viewData[dataElm.label] !== undefined) { + dataAcc[dataElm.label] = viewData[dataElm.label]; + } else if (viewData[dataElm.id] !== undefined) { + dataAcc[dataElm.id] = viewData[dataElm.id]; + } + return dataAcc; + }, {} as { [name: string]: any }); + + acc[elm.id] = { + selectedCase, + data, + }; + } + return acc; + }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {} + } + + static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) { + + if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) { + const isNew: boolean = nextProps.vPath?.endsWith("[]") || false; + const state = { + ...prevState, + isNew: isNew, + editMode: isNew, + viewData: nextProps.viewData || null, + [OldProps]: nextProps, + choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay + ? null + : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC + ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || [] + : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData) + } + return state; } - - static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) { - - if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) { - const isNew: boolean = nextProps.vPath?.endsWith("[]") || false; - const state = { - ...prevState, - isNew: isNew, - editMode: isNew, - viewData: nextProps.viewData || null, - [OldProps]: nextProps, - choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay - ? null - : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC - ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || [] - : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData) + return null; + } + + private navigate = (path: string) => { + this.props.history.push(`${this.props.match.url}${path}`); + } + + private changeValueFor = (property: string, value: any) => { + this.setState({ + viewData: { + ...this.state.viewData, + [property]: value + } + }); + } + + private collectData = (elements: { [name: string]: ViewElement }) => { + // ensure only active choises will be contained + const viewData: { [key: string]: any } = { ...this.state.viewData }; + const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey])); + const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => { + const currentChoice = elements[curChoiceKey] as ViewElementChoise; + const selectedCase = this.state.choises[curChoiceKey].selectedCase; + Object.keys(currentChoice.cases).forEach(caseKey => { + const caseElements = currentChoice.cases[caseKey].elements; + if (caseKey === selectedCase) { + Object.keys(caseElements).forEach(caseElementKey => { + const elm = caseElements[caseElementKey]; + if (isViewElementEmpty(elm)) { + // insert null for all empty elements + viewData[elm.id] = null; } - return state; - } - return null; + }); + return; + }; + Object.keys(caseElements).forEach(caseElementKey => { + acc.push(caseElements[caseElementKey]); + }); + }); + return acc; + }, [] as ViewElement[]); + + return viewData && Object.keys(viewData).reduce((acc, cur) => { + if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) { + acc[cur] = viewData[cur]; + } + return acc; + }, {} as { [key: string]: any }); + } + + private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => { + const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`); + return !(policy.GET && policy.POST); + } + + private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => { + const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`); + return !(policy.GET && policy.POST); + } + + private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => { + if (isViewElementEmpty(uiElement)) { + return null; + } else if (isViewElementSelection(uiElement)) { + return UiElementSelection; + } else if (isViewElementBoolean(uiElement)) { + return UiElementBoolean; + } else if (isViewElementString(uiElement)) { + return UiElementString; + } else if (isViewElementDate(uiElement)) { + return UiElementString; + } else if (isViewElementNumber(uiElement)) { + return UiElementNumber; + } else if (isViewElementUnion(uiElement)) { + return UIElementUnion; + } else { + if (process.env.NODE_ENV !== "production") { + console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + } + return null; } - - private navigate = (path: string) => { - this.props.history.push(`${this.props.match.url}${path}`); + } + + private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const isKey = (uiElement.label === keyProperty); + const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + + // do not show elements w/o any value from the backend + if (viewData[uiElement.id] == null && !editMode) { + return null; + } else if (isViewElementEmpty(uiElement)) { + return null; + } else if (uiElement.isList) { + /* element is a leaf-list */ + return <UiElementLeafList + key={uiElement.id} + inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]} + value={uiElement} + readOnly={!canEdit} + disabled={editMode && !canEdit} + onChange={(e) => { this.changeValueFor(uiElement.id, e) }} + getEditorForViewElement={this.getEditorForViewElement} + />; + } else { + const Element = this.getEditorForViewElement(uiElement); + return Element != null + ? ( + <Element + key={uiElement.id} + isKey={isKey} + inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]} + value={uiElement} + readOnly={!canEdit} + disabled={editMode && !canEdit} + onChange={(e) => { this.changeValueFor(uiElement.id, e) }} + />) + : null; } - - private changeValueFor = (property: string, value: any) => { - this.setState({ - viewData: { - ...this.state.viewData, - [property]: value - } - }); + }; + + // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + // const isKey = (uiElement.label === keyProperty); + // const canEdit = editMode && (isNew || (uiElement.config && !isKey)); + // if (isViewElementObjectOrList(uiElement)) { + // return ( + // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + // <Tooltip title={uiElement.description || ''}> + // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => { + // this.navigate(`/${uiElement.id}`); + // }}>{uiElement.label}</Button> + // </Tooltip> + // </FormControl> + // ); + // } else { + // if (process.env.NODE_ENV !== "production") { + // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + // } + // return null; + // } + // }; + + private renderUIChoise = (uiElement: ViewElementChoise, 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 canEdit = editMode && (isNew || (uiElement.config && !isKey)); + if (isViewElementChoise(uiElement)) { + const subElements = currentCase?.elements; + return ( + <> + <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> + <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel> + <Select + aria-label={uiElement.label + '-selection'} + required={!!uiElement.mandatory} + onChange={(e) => { + if (currentChoise.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 } } }); + }} + readOnly={!canEdit} + disabled={editMode && !canEdit} + value={this.state.choises[uiElement.id].selectedCase} + inputProps={{ + name: uiElement.id, + id: `select-${uiElement.id}`, + }} + > + { + Object.keys(uiElement.cases).map(caseKey => { + const caseElm = uiElement.cases[caseKey]; + return ( + <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem> + ); + }) + } + </Select> + </FormControl> + {subElements + ? Object.keys(subElements).map(elmKey => { + const elm = subElements[elmKey]; + return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); + }) + : <h3>Invalid Choise</h3> + } + </> + ); + } else { + if (process.env.NODE_ENV !== "production") { + console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) + } + return null; } + }; - private collectData = (elements: { [name: string]: ViewElement }) => { - // ensure only active choises will be contained - const viewData: { [key: string]: any } = { ...this.state.viewData }; - const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey])); - const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => { - const currentChoice = elements[curChoiceKey] as ViewElementChoise; - const selectedCase = this.state.choises[curChoiceKey].selectedCase; - Object.keys(currentChoice.cases).forEach(caseKey => { - const caseElements = currentChoice.cases[caseKey].elements; - if (caseKey === selectedCase) { - Object.keys(caseElements).forEach(caseElementKey => { - const elm = caseElements[caseElementKey]; - if (isViewElementEmpty(elm)) { - // insert null for all empty elements - viewData[elm.id] = null; - } - }); - return; - }; - Object.keys(caseElements).forEach(caseElementKey => { - acc.push(caseElements[caseElementKey]); - }); - }); - return acc; - }, [] as ViewElement[]); + private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const { classes } = this.props; - return viewData && Object.keys(viewData).reduce((acc, cur) => { - if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) { - acc[cur] = viewData[cur]; - } - return acc; - }, {} as { [key: string]: any }); - } - private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => { - if (isViewElementEmpty(uiElement)) { - return null; - } else if (isViewElementSelection(uiElement)) { - return UiElementSelection; - } else if (isViewElementBoolean(uiElement)) { - return UiElementBoolean; - } else if (isViewElementString(uiElement)) { - return UiElementString; - } else if (isViewElementDate(uiElement)) { - return UiElementString; - } else if (isViewElementNumber(uiElement)) { - return UiElementNumber; - } else if (isViewElementUnion(uiElement)) { - return UIElementUnion; - } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - } - return null; - } - } - private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const isKey = (uiElement.label === keyProperty); - const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - - // do not show elements w/o any value from the backend - if (viewData[uiElement.id] == null && !editMode) { - return null; - } else if (isViewElementEmpty(uiElement)) { - return null; - } else if (uiElement.isList) { - /* element is a leaf-list */ - return <UiElementLeafList - key={uiElement.id} - inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]} - value={uiElement} - readOnly={!canEdit} - disabled={editMode && !canEdit} - onChange={(e) => { this.changeValueFor(uiElement.id, e) }} - getEditorForViewElement={this.getEditorForViewElement} - />; - } else { - const Element = this.getEditorForViewElement(uiElement); - return Element != null - ? ( - <Element - key={uiElement.id} - isKey={isKey} - inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]} - value={uiElement} - readOnly={!canEdit} - disabled={editMode && !canEdit} - onChange={(e) => { this.changeValueFor(uiElement.id, e) }} - />) - : null; - } + const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { + if (keyProperty) { + // if (vsA.label === vsB.label) return 0; + if (vsA.label === keyProperty) return -1; + if (vsB.label === keyProperty) return +1; + } + + // if (vsA.uiType === vsB.uiType) return 0; + // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; + // if (vsA.uiType === "object") return +1; + return -1; }; - // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - // const isKey = (uiElement.label === keyProperty); - // const canEdit = editMode && (isNew || (uiElement.config && !isKey)); - // if (isViewElementObjectOrList(uiElement)) { - // return ( - // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> - // <Tooltip title={uiElement.description || ''}> - // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => { - // this.navigate(`/${uiElement.id}`); - // }}>{uiElement.label}</Button> - // </Tooltip> - // </FormControl> - // ); - // } else { - // if (process.env.NODE_ENV !== "production") { - // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - // } - // return null; - // } - // }; - - private renderUIChoise = (uiElement: ViewElementChoise, 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 canEdit = editMode && (isNew || (uiElement.config && !isKey)); - if (isViewElementChoise(uiElement)) { - const subElements = currentCase?.elements; + const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => { + const elm = viewSpecification.elements[cur]; + if (isViewElementObjectOrList(elm)) { + acc.references.push(elm); + } else if (isViewElementChoise(elm)) { + acc.choises.push(elm); + } else if (isViewElementRpc(elm)) { + acc.rpcs.push(elm); + } else { + acc.elements.push(elm); + } + return acc; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }); + + sections.elements = sections.elements.sort(orderFunc); + + return ( + <div className={classes.uiView}> + <div className={classes.section} /> + {sections.elements.length > 0 + ? ( + <div className={classes.section}> + {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + {sections.references.length > 0 + ? ( + <div className={classes.section}> + {sections.references.map(element => ( + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> + ))} + </div> + ) : null + } + {sections.choises.length > 0 + ? ( + <div className={classes.section}> + {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} + </div> + ) : null + } + {sections.rpcs.length > 0 + ? ( + <div className={classes.section}> + {sections.rpcs.map(element => ( + <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> + ))} + </div> + ) : null + } + </div> + ); + }; + + private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { + const { classes } = this.props; + // group by module name + const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => { + const elm = viewSpecification.elements[cur]; + const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} }); + moduleView.elements[cur] = elm; + return acc; + }, {}); + + const moduleKeys = Object.keys(modules).sort(); + + return ( + <div className={classes.moduleCollection}> + { + moduleKeys.map(key => { + const moduleView = modules[key]; return ( - <> - <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}> - <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel> - <Select - aria-label={uiElement.label+'-selection'} - required={!!uiElement.mandatory} - onChange={(e) => { - if (currentChoise.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 } } }); - }} - readOnly={!canEdit} - disabled={editMode && !canEdit} - value={this.state.choises[uiElement.id].selectedCase} - inputProps={{ - name: uiElement.id, - id: `select-${uiElement.id}`, - }} - > - { - Object.keys(uiElement.cases).map(caseKey => { - const caseElm = uiElement.cases[caseKey]; - return ( - <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem> - ); - }) - } - </Select> - </FormControl> - {subElements - ? Object.keys(subElements).map(elmKey => { - const elm = subElements[elmKey]; - return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); - }) - : <h3>Invalid Choise</h3> - } - </> + <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} > + <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} > + <Typography className={classes.heading}>{key}</Typography> + </AccordionSummaryExt> + <AccordionDetails> + {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)} + </AccordionDetails> + </Accordion> ); - } else { - if (process.env.NODE_ENV !== "production") { - console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) - } - return null; + }) } + </div> + ); + }; + + private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) { + const listElements = listSpecification.elements; + const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath + .replace("$$$standard$$$", "topology-netconfnode%20resources%20-%20RestConf%20RFC%208040") + .replace("$$$action$$$", "put")}_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : undefined; + + const navigate = (path: string) => { + this.props.history.push(`${this.props.match.url}${path}`); }; - private renderUIView = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const { classes } = this.props; - - const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { - if (keyProperty) { - // if (vsA.label === vsB.label) return 0; - if (vsA.label === keyProperty) return -1; - if (vsB.label === keyProperty) return +1; - } - - // if (vsA.uiType === vsB.uiType) return 0; - // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; - // if (vsA.uiType === "object") return +1; - return -1; - }; - - const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => { - const elm = viewSpecification.elements[cur]; - if (isViewElementObjectOrList(elm)) { - acc.references.push(elm); - } else if (isViewElementChoise(elm)) { - acc.choises.push(elm); - } else if (isViewElementRpc(elm)) { - acc.rpcs.push(elm); - } else { - acc.elements.push(elm); - } - return acc; - }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }); - - sections.elements = sections.elements.sort(orderFunc); - - return ( - <div className={classes.uiView}> - <div className={classes.section} /> - {sections.elements.length > 0 - ? ( - <div className={classes.section}> - {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - {sections.references.length > 0 - ? ( - <div className={classes.section}> - {sections.references.map(element => ( - <UIElementReference key={element.id} element={element} disabled={editMode} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> - ))} - </div> - ) : null - } - {sections.choises.length > 0 - ? ( - <div className={classes.section}> - {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - {sections.rpcs.length > 0 - ? ( - <div className={classes.section}> - {sections.rpcs.map(element => ( - <UIElementReference key={element.id} element={element} disabled={editMode} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} /> - ))} - </div> - ) : null - } - </div> - ); + const addNewElementAction = { + icon: AddIcon, + tooltip: 'Add', + onClick: () => { + navigate("[]"); // empty key means new element + }, + disabled: !listSpecification.config, }; - private renderUIViewSelector = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { - const { classes } = this.props; - - // group by module name - const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => { - const elm = viewSpecification.elements[cur]; - const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} }); - moduleView.elements[cur] = elm; - return acc; - }, {}); - - const moduleKeys = Object.keys(modules).sort(); - - return ( - <div className={classes.moduleCollection}> - { - moduleKeys.map(key => { - const moduleView = modules[key]; - return ( - <Accordion key={key} defaultExpanded={ moduleKeys.length < 4 } aria-label={key+'-panel'} > - <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} > - <Typography className={classes.heading}>{key}</Typography> - </AccordionSummary> - <AccordionDetails> - {this.renderUIView(moduleView, viewData, keyProperty, editMode, isNew)} - </AccordionDetails> - </Accordion> - ); - }) - } - </div> - ); + const addWithApiDocElementAction = { + icon: PostAdd, + tooltip: 'Add', + onClick: () => { + window.open(apiDocPathCreate, '_blank'); + }, + disabled: !listSpecification.config, }; - private renderUIViewList(listSpecification: ViewSpecification, 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.replace(/[\/=\-\:]/g,'_')}_` : undefined; - - const navigate = (path: string) => { - this.props.history.push(`${this.props.match.url}${path}`); - }; - - const addNewElementAction = { - icon: AddIcon, tooltip: 'Add', onClick: () => { - navigate("[]"); // empty key means new element - } - }; - - const addWithApiDocElementAction = { - icon: PostAdd, tooltip: 'Add', onClick: () => { - window.open(apiDocPathCreate, '_blank'); - } - }; - - const { classes, removeElement } = this.props; - - const DeleteIconWithConfirmation: React.FC<{ rowData: { [key: string]: any }, onReload: () => void }> = (props) => { - const confirm = useConfirm(); - - return ( - <Tooltip title={"Remove"} > - <IconButton className={classes.button} aria-label="remove-element-button" - onClick={async (e) => { - e.stopPropagation(); - e.preventDefault(); - confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" } }) - .then(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`)) - .then(props.onReload); - }} > - <RemoveIcon /> - </IconButton> - </Tooltip> - ); - } - - return ( - <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={ - Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { - const elm = listElements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { - if (elm.label !== listKeyProperty) { - acc.push(elm.uiType === "boolean" - ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" - ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } else { - acc.unshift(elm.uiType === "boolean" - ? { property: elm.label, type: ColumnType.boolean } - : elm.uiType === "date" - ? { property: elm.label, type: ColumnType.date } - : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } - } - return acc; - }, []).concat([{ - property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { - return ( - <DeleteIconWithConfirmation rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} /> - ); - }) - }]) - } onHandleClick={(ev, row) => { - ev.preventDefault(); - navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); - }} ></SelectElementTable> - ); + const { classes, removeElement } = this.props; + + const DeleteIconWithConfirmation: React.FC<{disabled?: boolean, rowData: { [key: string]: any }, onReload: () => void }> = (props) => { + const confirm = useConfirm(); + + return ( + <Tooltip title={"Remove"} > + <IconButton disabled={props.disabled} className={classes.button} aria-label="remove-element-button" + onClick={async (e) => { + e.stopPropagation(); + e.preventDefault(); + confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" } }) + .then(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`)) + .then(props.onReload); + }} > + <RemoveIcon /> + </IconButton> + </Tooltip> + ); } - private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) { - const { classes } = this.props; - - const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { - if (keyProperty) { - // if (vsA.label === vsB.label) return 0; - if (vsA.label === keyProperty) return -1; - if (vsB.label === keyProperty) return +1; - } - - // if (vsA.uiType === vsB.uiType) return 0; - // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; - // if (vsA.uiType === "object") return +1; - return -1; - }; - - const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => { - const elm = inputViewSpecification.elements[cur]; - if (isViewElementObjectOrList(elm)) { - console.error("Object should not appear in RPC view !"); - } else if (isViewElementChoise(elm)) { - acc.choises.push(elm); - } else if (isViewElementRpc(elm)) { - console.error("RPC should not appear in RPC view !"); + return ( + <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={ + Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { + const elm = listElements[cur]; + if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.label !== listKeyProperty) { + acc.push(elm.uiType === "boolean" + ? { property: elm.label, type: ColumnType.boolean } + : elm.uiType === "date" + ? { property: elm.label, type: ColumnType.date } + : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); } else { - acc.elements.push(elm); + acc.unshift(elm.uiType === "boolean" + ? { property: elm.label, type: ColumnType.boolean } + : elm.uiType === "date" + ? { property: elm.label, type: ColumnType.date } + : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); } - return acc; - }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }) - || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }; - - sections.elements = sections.elements.sort(orderFunc); - - return ( - <> - <div className={classes.section} /> - { sections.elements.length > 0 - ? ( - <div className={classes.section}> - {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - { sections.choises.length > 0 - ? ( - <div className={classes.section}> - {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} - </div> - ) : null - } - <Button onClick={() => { - const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements); - this.props.executeRpc(this.props.vPath!, resultingViewData); - }} >Exec</Button> - <div className={classes.objectReult}> - { outputViewData !== undefined - ? renderObject(outputViewData) - : null - } - </div> - </> - ); + } + return acc; + }, []).concat([{ + property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => { + return ( + <DeleteIconWithConfirmation disabled={!listSpecification.config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} /> + ); + }) + }]) + } onHandleClick={(ev, row) => { + ev.preventDefault(); + navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); + }} ></SelectElementTable> + ); + } + + private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) { + const { classes } = this.props; + + const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { + if (keyProperty) { + // if (vsA.label === vsB.label) return 0; + if (vsA.label === keyProperty) return -1; + if (vsB.label === keyProperty) return +1; + } + + // if (vsA.uiType === vsB.uiType) return 0; + // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; + // if (vsA.uiType === "object") return +1; + return -1; }; - private renderBreadCrumps() { - const { editMode } = this.state; - const { displaySpecification, vPath, nodeId } = this.props; - const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key - let lastPath = `/configuration`; - let basePath = `/configuration/${nodeId}`; - return ( - <div className={this.props.classes.header}> - <div> - <Breadcrumbs aria-label="breadcrumbs"> - <Link color="inherit" href="#" aria-label="back-breadcrumb" - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(lastPath); - }}>Back</Link> - <Link color="inherit" href="#" - aria-label={nodeId+'-breadcrumb'} - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(`/configuration/${nodeId}`); - }}><span>{nodeId}</span></Link> - { - pathParts.map(([prop, key], ind) => { - const path = `${basePath}/${prop}`; - const keyPath = key && `${basePath}/${prop}[${key}]`; - const propTitle= prop.replace(/^[^:]+:/, ""); - const ret = ( - <span key={ind}> - <Link color="inherit" href="#" - aria-label={propTitle+'-breadcrumb'} - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(path); - }}><span>{propTitle}</span></Link> - { - keyPath && <Link color="inherit" href="#" - aria-label={key+'-breadcrumb'} - onClick={(ev: React.MouseEvent<HTMLElement>) => { - ev.preventDefault(); - this.props.history.push(keyPath); - }}>{`[${key}]`}</Link> || null - } - </span> - ); - lastPath = basePath; - basePath = keyPath || path; - return ret; - }) - } - </Breadcrumbs> - </div> - {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); - this.setState({ editMode: false }); - }} ><ArrowBack /></Fab> - ) || null} - { /* do not show edit if this is a list or it can't be edited */ - displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div> - <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => { - if (this.state.editMode) { - // ensure only active choises will be contained - const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); - this.props.onUpdateData(this.props.vPath!, resultingViewData); - } - this.setState({ editMode: !editMode }); - }}> - {editMode - ? <SaveIcon /> - : <EditIcon /> - } - </Fab> - </div> || null) - } + const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => { + const elm = inputViewSpecification.elements[cur]; + if (isViewElementObjectOrList(elm)) { + console.error("Object should not appear in RPC view !"); + } else if (isViewElementChoise(elm)) { + acc.choises.push(elm); + } else if (isViewElementRpc(elm)) { + console.error("RPC should not appear in RPC view !"); + } else { + acc.elements.push(elm); + } + return acc; + }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }) + || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }; + + sections.elements = sections.elements.sort(orderFunc); + + return ( + <> + <div className={classes.section} /> + { sections.elements.length > 0 + ? ( + <div className={classes.section}> + {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))} </div> - ); - } - - private renderValueSelector() { - const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; - if (!listKeyProperty || !listSpecification) { - throw new Error("ListKex ot view not specified."); + ) : null } - - return ( - <div className={this.props.classes.container}> - <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={ - Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { - const elm = listSpecification.elements[cur]; - if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { - if (elm.label !== listKeyProperty) { - acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } else { - acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); - } - } - return acc; - }, []) - } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable> + { sections.choises.length > 0 + ? ( + <div className={classes.section}> + {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))} </div> - ); - } - - private renderValueEditor() { - const { displaySpecification: ds, outputData } = this.props; - const { viewData, editMode, isNew } = this.state; - - return ( - <div className={this.props.classes.container}> - {this.renderBreadCrumps()} - {ds.displayMode === DisplayModeType.doNotDisplay - ? null - : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array - ? this.renderUIViewList(ds.viewSpecification, ds.keyProperty!, ds.apidocPath!, viewData) - : ds.displayMode === DisplayModeType.displayAsRPC - ? this.renderUIViewRPC(ds.inputViewSpecification, viewData!, outputData, undefined, true, false) - : this.renderUIViewSelector(ds.viewSpecification, viewData!, ds.keyProperty, editMode, isNew) - } - </div > - ); - } - - private renderCollectingData() { - return ( - <div className={this.props.classes.outer}> - <div className={this.props.classes.inner}> - <Loader /> - <h3>Processing ...</h3> - </div> - </div> - ); + ) : null + } + <Button onClick={() => { + const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements); + this.props.executeRpc(this.props.vPath!, resultingViewData); + }} >Exec</Button> + <div className={classes.objectReult}> + {outputViewData !== undefined + ? renderObject(outputViewData) + : null + } + </div> + </> + ); + }; + + private renderBreadCrumps() { + const { editMode } = this.state; + const { displaySpecification, vPath, nodeId } = this.props; + const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key + let lastPath = `/configuration`; + let basePath = `/configuration/${nodeId}`; + return ( + <div className={this.props.classes.header}> + <div> + <Breadcrumbs aria-label="breadcrumbs"> + <Link color="inherit" href="#" aria-label="back-breadcrumb" + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(lastPath); + }}>Back</Link> + <Link color="inherit" href="#" + aria-label={nodeId + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(`/configuration/${nodeId}`); + }}><span>{nodeId}</span></Link> + { + pathParts.map(([prop, key], ind) => { + const path = `${basePath}/${prop}`; + const keyPath = key && `${basePath}/${prop}[${key}]`; + const propTitle = prop.replace(/^[^:]+:/, ""); + const ret = ( + <span key={ind}> + <Link color="inherit" href="#" + aria-label={propTitle + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(path); + }}><span>{propTitle}</span></Link> + { + keyPath && <Link color="inherit" href="#" + aria-label={key + '-breadcrumb'} + onClick={(ev: React.MouseEvent<HTMLElement>) => { + ev.preventDefault(); + this.props.history.push(keyPath); + }}>{`[${key}]`}</Link> || null + } + </span> + ); + lastPath = basePath; + basePath = keyPath || path; + return ret; + }) + } + </Breadcrumbs> + </div> + {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); + this.setState({ editMode: false }); + }} ><ArrowBack /></Fab> + ) || null} + { /* do not show edit if this is a list or it can't be edited */ + displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div> + <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => { + if (this.state.editMode) { + // ensure only active choises will be contained + const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements); + this.props.onUpdateData(this.props.vPath!, resultingViewData); + } + this.setState({ editMode: !editMode }); + }}> + {editMode + ? <SaveIcon /> + : <EditIcon /> + } + </Fab> + </div> || null) + } + </div> + ); + } + + private renderValueSelector() { + const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; + if (!listKeyProperty || !listSpecification) { + throw new Error("ListKex ot view not specified."); } - render() { - return this.props.collectingData || !this.state.viewData - ? this.renderCollectingData() - : this.props.listSpecification - ? this.renderValueSelector() - : this.renderValueEditor(); - } + return ( + <div className={this.props.classes.container}> + <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={ + Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => { + const elm = listSpecification.elements[cur]; + if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { + if (elm.label !== listKeyProperty) { + acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + } else { + acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); + } + } + return acc; + }, []) + } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable> + </div> + ); + } + + private renderValueEditor() { + const { displaySpecification: ds, outputData } = this.props; + const { viewData, editMode, isNew } = this.state; + + return ( + <div className={this.props.classes.container}> + {this.renderBreadCrumps()} + {ds.displayMode === DisplayModeType.doNotDisplay + ? null + : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array + ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData) + : ds.displayMode === DisplayModeType.displayAsRPC + ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false) + : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew) + } + </div > + ); + } + + private renderCollectingData() { + return ( + <div className={this.props.classes.outer}> + <div className={this.props.classes.inner}> + <Loader /> + <h3>Processing ...</h3> + </div> + </div> + ); + } + + render() { + return this.props.collectingData || !this.state.viewData + ? this.renderCollectingData() + : this.props.listSpecification + ? this.renderValueSelector() + : this.renderValueEditor(); + } } export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent))); diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx index 5d849986d..5cac22eba 100644 --- a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx @@ -25,7 +25,7 @@ import { MaterialTable, MaterialTableCtorType, ColumnType } from "../../../../fr import { createConnectedNetworkElementsProperties, createConnectedNetworkElementsActions } from "../../../configurationApp/src/handlers/connectedNetworkElementsHandler"; import { NetworkElementConnection } from "../models/networkElementConnection"; -import { Tooltip, Button, IconButton } from "@material-ui/core"; + const mapProps = (state: IApplicationStoreState) => ({ connectedNetworkElementsProperties: createConnectedNetworkElementsProperties(state), |