/** * ============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 React from 'react'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles'; import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect"; import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore"; import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table"; import { Loader } from "../../../../framework/src/components/material-ui/loader"; import { 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 } from "../models/uiModels"; import Fab from '@material-ui/core/Fab'; import AddIcon from '@material-ui/icons/Add'; import ArrowBack from '@material-ui/icons/ArrowBack'; import RemoveIcon from '@material-ui/icons/RemoveCircleOutline'; import SaveIcon from '@material-ui/icons/Save'; import EditIcon from '@material-ui/icons/Edit'; import Tooltip from "@material-ui/core/Tooltip"; import FormControl from "@material-ui/core/FormControl"; import IconButton from "@material-ui/core/IconButton"; import InputLabel from "@material-ui/core/InputLabel"; import Select from "@material-ui/core/Select"; import MenuItem from "@material-ui/core/MenuItem"; import Breadcrumbs from "@material-ui/core/Breadcrumbs"; import Link from "@material-ui/core/Link"; import { UIElementReference } from '../components/uiElementReference'; import { UiElementNumber } from '../components/uiElementNumber'; import { UiElementString } from '../components/uiElementString'; import { UiElementBoolean } from '../components/uiElementBoolean'; import { UiElementSelection } from '../components/uiElementSelection'; import { UIElementUnion } from '../components/uiElementUnion'; import { Button } from '@material-ui/core'; const styles = (theme: Theme) => createStyles({ header: { "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', }, '& .MuiInput-underline:after': { borderBottomColor: '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 } }); 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, }); const mapDispatch = (dispatcher: IDispatcher) => ({ onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)), reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)), removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)), executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)), }); const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>; type ConfigurationApplicationComponentProps = RouteComponentProps & Connect & WithStyles; type ConfigurationApplicationComponentState = { isNew: boolean; editMode: boolean; canEdit: boolean; viewData: { [key: string]: any } | null; choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } }; } const OldProps = Symbol("OldProps"); class ConfigurationApplicationComponent extends React.Component { /** * */ 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 } } }) || {} } 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; } 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; }; Object.keys(caseElements).forEach(caseElementKey => { acc.push(caseElements[caseElementKey]); }); }); return acc; }, [] as ViewElement[]); return viewData && Object.keys(viewData).reduce((acc, cur) => { if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) { acc[cur] = viewData[cur]; } return acc; }, {} as { [key: string]: any }); } private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { const isKey = (uiElement.label === keyProperty); const canEdit = editMode && (isNew || (uiElement.config && !isKey)); if (isViewElementEmpty(uiElement)) { return null; } else if (isViewElementSelection(uiElement)) { return { this.changeValueFor(uiElement.id, e) }} /> } else if (isViewElementBoolean(uiElement)) { return { this.changeValueFor(uiElement.id, e) }} /> } else if (isViewElementString(uiElement)) { return { this.changeValueFor(uiElement.id, e) }} /> } else if (isViewElementNumber(uiElement)) { return { this.changeValueFor(uiElement.id, e) }} /> } else if (isViewElementUnion(uiElement)) { return { this.changeValueFor(uiElement.id, e) }} /> } else { if (process.env.NODE_ENV !== "production") { console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) } return null; } }; // 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 ( // // // // // // ); // } 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 ( <> {uiElement.label} {subElements ? Object.keys(subElements).map(elmKey => { const elm = subElements[elmKey]; return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew); }) :

Invalid Choise

} ); } else { if (process.env.NODE_ENV !== "production") { console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) } return null; } }; 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 (
{sections.elements.length > 0 ? (
{sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))}
) : null } {sections.references.length > 0 ? (
{sections.references.map(element => ( { this.navigate(`/${elm.id}`) }} /> ))}
) : null } {sections.choises.length > 0 ? (
{sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))}
) : null } {sections.rpcs.length > 0 ? (
{sections.rpcs.map(element => ( { this.navigate(`/${elm.id}`) }} /> ))}
) : null }
); }; private renderUIViewList(listSpecification: ViewSpecification, listKeyProperty: string, listData: { [key: string]: any }[]) { const listElements = listSpecification.elements; const navigate = (path: string) => { this.props.history.push(`${this.props.match.url}${path}`); }; const addNewElementAction = { icon: AddIcon, tooltip: 'Add', onClick: () => { navigate("[]"); // empty key means new element } }; const { classes, removeElement } = this.props; return ( []>((acc, cur) => { const elm = listElements[cur]; if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) { if (elm.label !== listKeyProperty) { acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); } else { acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text }); } } return acc; }, []).concat([{ property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: ( ({ rowData })=> { return ( { e.stopPropagation(); e.preventDefault(); removeElement(`${this.props.vPath}[${rowData[listKeyProperty]}]`) }} > ) }) }]) } onHandleClick={(ev, row) => { ev.preventDefault(); navigate(`[${row[listKeyProperty]}]`); }} > ); } private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) { const { classes } = this.props; const orderFunc = (vsA: ViewElement, vsB: ViewElement) => { if (keyProperty) { // if (vsA.label === vsB.label) return 0; if (vsA.label === keyProperty) return -1; if (vsB.label === keyProperty) return +1; } // if (vsA.uiType === vsB.uiType) return 0; // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0; // if (vsA.uiType === "object") return +1; return -1; }; const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => { const elm = inputViewSpecification.elements[cur]; if (isViewElementObjectOrList(elm)) { console.error("Object should not appear in RPC view !"); } else if (isViewElementChoise(elm)) { acc.choises.push(elm); } else if (isViewElementRpc(elm)) { console.error("RPC should not appear in RPC view !"); } else { acc.elements.push(elm); } return acc; }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }) || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] }; sections.elements = sections.elements.sort(orderFunc); return ( <>
{sections.elements.length > 0 ? (
{sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
) : null } {sections.choises.length > 0 ? (
{sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))}
) : null } {outputViewData !== undefined ? ( renderObject(outputViewData) ) : null } ); }; private renderBreadCrumps() { const { editMode } = this.state; const { displaySpecification } = this.props; const { vPath, nodeId } = this.props; const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key let lastPath = `/configuration`; let basePath = `/configuration/${nodeId}`; return (
) => { ev.preventDefault(); this.props.history.push(lastPath); }}>Back ) => { ev.preventDefault(); this.props.history.push(`/configuration/${nodeId}`); }}>{nodeId} { pathParts.map(([prop, key], ind) => { const path = `${basePath}/${prop}`; const keyPath = key && `${basePath}/${prop}[${key}]`; const ret = ( ) => { ev.preventDefault(); this.props.history.push(path); }}>{prop.replace(/^[^:]+:/, "")} { keyPath && ) => { ev.preventDefault(); this.props.history.push(keyPath); }}>{`[${key}]`} || null } ); lastPath = basePath; basePath = keyPath || path; return ret; }) }
{this.state.editMode && ( { this.props.vPath && await this.props.reloadView(this.props.vPath); this.setState({ editMode: false }); }} > ) || null} { /* do not show edit if this is a list or it can't be edited */ displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (
{ 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 ? : }
|| null) }
); } private renderValueSelector() { const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props; if (!listKeyProperty || !listSpecification) { throw new Error("ListKex ot view not specified."); } return (
[]>((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); }} >
); } private renderValueEditor() { const { displaySpecification: ds, outputData } = this.props; const { viewData, editMode, isNew } = this.state; return (
{this.renderBreadCrumps()} {ds.displayMode === DisplayModeType.doNotDisplay ? null : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array ? this.renderUIViewList(ds.viewSpecification, ds.keyProperty!, viewData) : ds.displayMode === DisplayModeType.displayAsRPC ? this.renderUIViewRPC(ds.inputViewSpecification, viewData!, outputData, undefined, true, false) : this.renderUIView(ds.viewSpecification, viewData!, ds.keyProperty, editMode, isNew) }
); } private renderCollectingData() { return (

Collecting Data ...

); } render() { return this.props.collectingData || !this.state.viewData ? this.renderCollectingData() : this.props.listSpecification ? this.renderValueSelector() : this.renderValueEditor(); } } export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent))); export default ConfigurationApplication;