From e6d0d67fdbe3fc70c996c8df33bd65d3b151dfad Mon Sep 17 00:00:00 2001 From: herbert Date: Sat, 14 Dec 2019 01:05:47 +0100 Subject: update odlux and featureaggregator v2 update odlux and featureaggregator bundles Issue-ID: SDNC-1008 Signed-off-by: herbert Change-Id: I0018d7bfa3a0e6896c1b210b539a574af9808e22 Signed-off-by: herbert --- .../src/views/configurationApplication.tsx | 464 +++++++++++++++++++++ .../src/views/networkElementSelector.tsx | 47 +++ 2 files changed, 511 insertions(+) create mode 100644 sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx create mode 100644 sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx (limited to 'sdnr/wt/odlux/apps/configurationApp/src/views') diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx new file mode 100644 index 000000000..24a4af8b2 --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/configurationApplication.tsx @@ -0,0 +1,464 @@ +/** + * ============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; \ No newline at end of file diff --git a/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx new file mode 100644 index 000000000..6fd5c8cf0 --- /dev/null +++ b/sdnr/wt/odlux/apps/configurationApp/src/views/networkElementSelector.tsx @@ -0,0 +1,47 @@ +import * as 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 { NetworkElementConnection } from "../models/networkElementConnection"; +import { Tooltip, Button, IconButton } from "@material-ui/core"; + +const mapProps = (state: IApplicationStoreState) => ({ + connectedNetworkElementsProperties: createConnectedNetworkElementsProperties(state), +}); + +const mapDispatch = (dispatcher: IDispatcher) => ({ + connectedNetworkElementsActions: createConnectedNetworkElementsActions(dispatcher.dispatch), +}); + +const ConnectedElementTable = MaterialTable as MaterialTableCtorType; + +type NetworkElementSelectorComponentProps = RouteComponentProps & Connect; + +class NetworkElementSelectorComponent extends React.Component { + + componentDidMount() { + this.props.connectedNetworkElementsActions.onRefresh(); + } + + render() { + return ( + { this.props.history.push(`${ this.props.match.path }/${row.nodeId}`) }} columns={[ + { property: "nodeId", title: "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 > + + ); + } +} + +export const NetworkElementSelector = withRouter(connect(mapProps, mapDispatch)(NetworkElementSelectorComponent)); +export default NetworkElementSelector; + -- cgit 1.2.3-korg