/** * ============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 * as 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 { SetSelectedValue, splitVPath, updateDataActionAsyncCreator } from "../actions/deviceActions"; import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection } from "../models/uiModels"; import Fab from '@material-ui/core/Fab'; import AddIcon from '@material-ui/icons/Add'; import RemoveIcon from '@material-ui/icons/RemoveCircleOutline'; import SaveIcon from '@material-ui/icons/Save'; import EditIcon from '@material-ui/icons/Edit'; import Tooltip from "@material-ui/core/Tooltip"; import TextField from "@material-ui/core/TextField"; import FormControl from "@material-ui/core/FormControl"; import IconButton from "@material-ui/core/IconButton"; import Button from "@material-ui/core/Button"; import InputAdornment from "@material-ui/core/InputAdornment"; import InputLabel from "@material-ui/core/InputLabel"; import Select from "@material-ui/core/Select"; import MenuItem from "@material-ui/core/MenuItem"; import Breadcrumbs from "@material-ui/core/Breadcrumbs"; import Link from "@material-ui/core/Link"; import FormHelperText from '@material-ui/core/FormHelperText'; const styles = (theme: Theme) => createStyles({ header: { "display": "flex", "justifyContent": "space-between", }, leftButton: { "justifyContent": "left" }, outer: { "flex": "1", "heigh": "100%", "display": "flex", "alignItems": "center", "justifyContent": "center", }, inner: { }, "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', }, }, }, }); 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, viewSpecification: state.configuration.viewDescription.viewSpecification, displayAsList: state.configuration.viewDescription.displayAsList, keyProperty: state.configuration.viewDescription.keyProperty, }); const mapDispatch = (dispatcher: IDispatcher) => ({ onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)), onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(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; } const OldProps = Symbol("OldProps"); class ConfigurationApplicationComponent extends React.Component { /** * */ constructor (props: ConfigurationApplicationComponentProps) { super(props); this.state = { isNew: false, canEdit: false, editMode: false, viewData: null } } 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, } 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 renderUIElement = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => { const elements = viewSpecification.elements; return ( Object.keys(elements).sort((a, b) => { const vsA = elements[a]; const vsB = elements[b]; 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; }).map(key => { const uiElement = elements[key]; const isKey = (uiElement.label === keyProperty); const canEdit = editMode && (isNew || (uiElement.config && !isKey)); if (isViewElementSelection(uiElement)) { let error = "" const value = String(viewData[uiElement.id]).toLowerCase(); if (uiElement.mandatory && !!value) { error = "Error"; } return (canEdit || viewData[uiElement.id] != null ? ( {uiElement.label} {error} ) : null ); } else if (isViewElementBoolean(uiElement)) { let error = "" const value = String(viewData[uiElement.id]).toLowerCase(); if (uiElement.mandatory && value !== "true" && value !== "false") { error = "Error"; } return (canEdit || viewData[uiElement.id] != null ? ( {uiElement.label} {error} ) : null ); } else if (isViewElementString(uiElement)) { return ( { this.changeValueFor(uiElement.id, e.target.value) }} /> ); } else if (isViewElementNumber(uiElement)) { return ( {uiElement.units} : undefined }} spellCheck={false} autoFocus margin="dense" id={uiElement.id} label={uiElement.label} type="text" value={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]} style={{ width: 485, marginLeft: 20, marginRight: 20 }} onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }} /> ); } else if (isViewElementObjectOrList(uiElement)) { return ( ); } else { if (process.env.NODE_ENV !== "production") { console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`) } return null; } }) ); }; private renderUIElementList(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 } = 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: (row => { return ( { }} > ) }) }]) } onHandleClick={(ev, row) => { ev.preventDefault(); navigate(`[${row[listKeyProperty]}]`); }} > ); } private renderBreadCrumps() { const { editMode } = this.state; const { viewSpecification, displayAsList } = this.props; const { vPath, match: { url, path }, 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; }) }
{ /* do not show edit if this is a list or it can't be edited */ !displayAsList && viewSpecification.canEdit && (
{ if (this.state.editMode) { this.props.onUpdateData(this.props.vPath!, this.state.viewData); } 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 { keyProperty, displayAsList, viewSpecification } = this.props; const { viewData, editMode, isNew } = this.state; return (
{ this.renderBreadCrumps() } { displayAsList && viewData instanceof Array ? this.renderUIElementList(viewSpecification, keyProperty!, viewData) : this.renderUIElement(viewSpecification, viewData!, 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;