2 * ============LICENSE_START========================================================================
3 * ONAP : ccsdk feature sdnr wt odlux
4 * =================================================================================================
5 * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
6 * =================================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
8 * in compliance with the License. You may obtain a copy of the License at
10 * http://www.apache.org/licenses/LICENSE-2.0
12 * Unless required by applicable law or agreed to in writing, software distributed under the License
13 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
14 * or implied. See the License for the specific language governing permissions and limitations under
16 * ============LICENSE_END==========================================================================
19 import React, { useState } from 'react';
20 import { RouteComponentProps, withRouter } from 'react-router-dom';
22 import { Theme } from '@mui/material/styles';
24 import { WithStyles } from '@mui/styles';
25 import withStyles from '@mui/styles/withStyles';
26 import createStyles from '@mui/styles/createStyles';
28 import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect";
29 import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
30 import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table";
31 import { Loader } from "../../../../framework/src/components/material-ui/loader";
32 import { renderObject } from '../../../../framework/src/components/objectDump';
34 import { DisplayModeType } from '../handlers/viewDescriptionHandler';
35 import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator, updateViewActionAsyncCreator, removeElementActionAsyncCreator, executeRpcActionAsyncCreator } from "../actions/deviceActions";
36 import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion, isViewElementRpc, ViewElementRpc, isViewElementEmpty, isViewElementDate } from "../models/uiModels";
38 import { getAccessPolicyByUrl } from "../../../../framework/src/services/restService";
40 import Fab from '@mui/material/Fab';
41 import AddIcon from '@mui/icons-material/Add';
42 import PostAdd from '@mui/icons-material/PostAdd';
43 import ArrowBack from '@mui/icons-material/ArrowBack';
44 import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
45 import SaveIcon from '@mui/icons-material/Save';
46 import EditIcon from '@mui/icons-material/Edit';
47 import Tooltip from "@mui/material/Tooltip";
48 import FormControl from "@mui/material/FormControl";
49 import IconButton from "@mui/material/IconButton";
51 import InputLabel from "@mui/material/InputLabel";
52 import Select from "@mui/material/Select";
53 import MenuItem from "@mui/material/MenuItem";
54 import Breadcrumbs from "@mui/material/Breadcrumbs";
55 import Button from '@mui/material/Button';
56 import Link from "@mui/material/Link";
57 import Accordion from '@mui/material/Accordion';
58 import AccordionSummary from '@mui/material/AccordionSummary';
59 import AccordionDetails from '@mui/material/AccordionDetails';
60 import Typography from '@mui/material/Typography';
61 import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
64 import { BaseProps } from '../components/baseProps';
65 import { UIElementReference } from '../components/uiElementReference';
66 import { UiElementNumber } from '../components/uiElementNumber';
67 import { UiElementString } from '../components/uiElementString';
68 import { UiElementBoolean } from '../components/uiElementBoolean';
69 import { UiElementSelection } from '../components/uiElementSelection';
70 import { UIElementUnion } from '../components/uiElementUnion';
71 import { UiElementLeafList } from '../components/uiElementLeafList';
73 import { useConfirm } from 'material-ui-confirm';
74 import restService from '../services/restServices';
76 const styles = (theme: Theme) => createStyles({
79 "justifyContent": "space-between",
82 "justifyContent": "left"
88 "alignItems": "center",
89 "justifyContent": "center",
97 "flexDirection": "column",
100 "marginRight": theme.spacing(0.5),
105 "margin": theme.spacing(1),
113 '& label.Mui-focused': {
116 '& .MuiInput-underline:after': {
117 borderBottomColor: 'green',
119 '& .MuiOutlinedInput-root': {
123 '&:hover fieldset': {
124 borderColor: 'yellow',
126 '&.Mui-focused fieldset': {
127 borderColor: 'green',
136 borderBottom: `2px solid ${theme.palette.divider}`,
139 width: 485, marginLeft: 20, marginRight: 20
141 verificationElements: {
142 width: 485, marginLeft: 20, marginRight: 20
145 fontSize: theme.typography.pxToRem(15),
146 fontWeight: theme.typography.fontWeightRegular,
157 const mapProps = (state: IApplicationStoreState) => ({
158 collectingData: state.configuration.valueSelector.collectingData,
159 listKeyProperty: state.configuration.valueSelector.keyProperty,
160 listSpecification: state.configuration.valueSelector.listSpecification,
161 listData: state.configuration.valueSelector.listData,
162 vPath: state.configuration.viewDescription.vPath,
163 nodeId: state.configuration.deviceDescription.nodeId,
164 viewData: state.configuration.viewDescription.viewData,
165 outputData: state.configuration.viewDescription.outputData,
166 displaySpecification: state.configuration.viewDescription.displaySpecification,
169 const mapDispatch = (dispatcher: IDispatcher) => ({
170 onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
171 onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)),
172 reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)),
173 removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)),
174 executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)),
177 const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
179 type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>;
181 type ConfigurationApplicationComponentState = {
185 viewData: { [key: string]: any } | null;
186 choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } };
189 type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any
190 const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => {
191 const [disabled, setDisabled] = useState(true);
192 const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => {
193 if (ev.button === 1) {
194 setDisabled(!disabled);
199 <div onMouseDown={onMouseDown} >
200 <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} />
205 const OldProps = Symbol("OldProps");
206 class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> {
211 constructor(props: ConfigurationApplicationComponentProps) {
223 private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => {
224 return Object.keys(elements).reduce((acc, cur) => {
225 const elm = elements[cur];
226 if (isViewElementChoise(elm)) {
227 const caseKeys = Object.keys(elm.cases);
229 // find the right case for this choise, use the first one with data, at least use index 0
230 const selectedCase = caseKeys.find(key => {
231 const caseElm = elm.cases[key];
232 return Object.keys(caseElm.elements).some(caseElmKey => {
233 const caseElmElm = caseElm.elements[caseElmKey];
234 return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined;
238 // extract all data of the active case
239 const caseElements = elm.cases[selectedCase].elements;
240 const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => {
241 const dataElm = caseElements[dataCur];
242 if (isViewElementEmpty(dataElm)) {
243 dataAcc[dataElm.label] = null;
244 } else if (viewData[dataElm.label] !== undefined) {
245 dataAcc[dataElm.label] = viewData[dataElm.label];
246 } else if (viewData[dataElm.id] !== undefined) {
247 dataAcc[dataElm.id] = viewData[dataElm.id];
250 }, {} as { [name: string]: any });
258 }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {}
261 static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) {
263 if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
264 const isNew: boolean = nextProps.vPath?.endsWith("[]") || false;
269 viewData: nextProps.viewData || null,
270 [OldProps]: nextProps,
271 choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay
272 || nextProps.displaySpecification.displayMode === DisplayModeType.displayAsMessage
274 : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC
275 ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || []
276 : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData)
283 private navigate = (path: string) => {
284 this.props.history.push(`${this.props.match.url}${path}`);
287 private changeValueFor = (property: string, value: any) => {
290 ...this.state.viewData,
296 private collectData = (elements: { [name: string]: ViewElement }) => {
297 // ensure only active choises will be contained
298 const viewData: { [key: string]: any } = { ...this.state.viewData };
299 const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey]));
300 const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => {
301 const currentChoice = elements[curChoiceKey] as ViewElementChoise;
302 const selectedCase = this.state.choises[curChoiceKey].selectedCase;
303 Object.keys(currentChoice.cases).forEach(caseKey => {
304 const caseElements = currentChoice.cases[caseKey].elements;
305 if (caseKey === selectedCase) {
306 Object.keys(caseElements).forEach(caseElementKey => {
307 const elm = caseElements[caseElementKey];
308 if (isViewElementEmpty(elm)) {
309 // insert null for all empty elements
310 viewData[elm.id] = null;
315 Object.keys(caseElements).forEach(caseElementKey => {
316 acc.push(caseElements[caseElementKey]);
320 }, [] as ViewElement[]);
322 return viewData && Object.keys(viewData).reduce((acc, cur) => {
323 if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) {
324 acc[cur] = viewData[cur];
327 }, {} as { [key: string]: any });
330 private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => {
331 const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`);
332 return !(policy.GET && policy.POST);
335 private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => {
336 const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`);
337 return !(policy.GET && policy.POST);
340 private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => {
341 if (isViewElementEmpty(uiElement)) {
343 } else if (isViewElementSelection(uiElement)) {
344 return UiElementSelection;
345 } else if (isViewElementBoolean(uiElement)) {
346 return UiElementBoolean;
347 } else if (isViewElementString(uiElement)) {
348 return UiElementString;
349 } else if (isViewElementDate(uiElement)) {
350 return UiElementString;
351 } else if (isViewElementNumber(uiElement)) {
352 return UiElementNumber;
353 } else if (isViewElementUnion(uiElement)) {
354 return UIElementUnion;
356 if (process.env.NODE_ENV !== "production") {
357 console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
363 private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
364 const isKey = (uiElement.label === keyProperty);
365 const canEdit = editMode && (isNew || (uiElement.config && !isKey));
367 // do not show elements w/o any value from the backend
368 if (viewData[uiElement.id] == null && !editMode) {
370 } else if (isViewElementEmpty(uiElement)) {
372 } else if (uiElement.isList) {
373 /* element is a leaf-list */
374 return <UiElementLeafList
376 inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]}
379 disabled={editMode && !canEdit}
380 onChange={(e) => { this.changeValueFor(uiElement.id, e) }}
381 getEditorForViewElement={this.getEditorForViewElement}
384 const Element = this.getEditorForViewElement(uiElement);
385 return Element != null
390 inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
393 disabled={editMode && !canEdit}
394 onChange={(e) => { this.changeValueFor(uiElement.id, e) }}
400 // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
401 // const isKey = (uiElement.label === keyProperty);
402 // const canEdit = editMode && (isNew || (uiElement.config && !isKey));
403 // if (isViewElementObjectOrList(uiElement)) {
405 // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
406 // <Tooltip title={uiElement.description || ''}>
407 // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => {
408 // this.navigate(`/${uiElement.id}`);
409 // }}>{uiElement.label}</Button>
414 // if (process.env.NODE_ENV !== "production") {
415 // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
421 private renderUIChoise = (uiElement: ViewElementChoise, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
422 const isKey = (uiElement.label === keyProperty);
424 const currentChoise = this.state.choises[uiElement.id];
425 const currentCase = currentChoise && uiElement.cases[currentChoise.selectedCase];
427 const canEdit = editMode && (isNew || (uiElement.config && !isKey));
428 if (isViewElementChoise(uiElement)) {
429 const subElements = currentCase?.elements;
432 <FormControl variant="standard" key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
433 <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
434 <Select variant="standard"
435 aria-label={uiElement.label + '-selection'}
436 required={!!uiElement.mandatory}
438 if (currentChoise.selectedCase === e.target.value) {
439 return; // nothing changed
441 this.setState({ choises: { ...this.state.choises, [uiElement.id]: { ...this.state.choises[uiElement.id], selectedCase: e.target.value as string } } });
444 disabled={editMode && !canEdit}
445 value={this.state.choises[uiElement.id].selectedCase}
448 id: `select-${uiElement.id}`,
452 Object.keys(uiElement.cases).map(caseKey => {
453 const caseElm = uiElement.cases[caseKey];
455 <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem>
462 ? Object.keys(subElements).map(elmKey => {
463 const elm = subElements[elmKey];
464 return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew);
466 : <h3>Invalid Choise</h3>
471 if (process.env.NODE_ENV !== "production") {
472 console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
478 private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
479 const { classes } = this.props;
483 const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
485 // if (vsA.label === vsB.label) return 0;
486 if (vsA.label === keyProperty) return -1;
487 if (vsB.label === keyProperty) return +1;
490 // if (vsA.uiType === vsB.uiType) return 0;
491 // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
492 // if (vsA.uiType === "object") return +1;
496 const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => {
497 const elm = viewSpecification.elements[cur];
498 if (isViewElementObjectOrList(elm)) {
499 acc.references.push(elm);
500 } else if (isViewElementChoise(elm)) {
501 acc.choises.push(elm);
502 } else if (isViewElementRpc(elm)) {
505 acc.elements.push(elm);
508 }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] });
510 sections.elements = sections.elements.sort(orderFunc);
513 <div className={classes.uiView}>
514 <div className={classes.section} />
515 {sections.elements.length > 0
517 <div className={classes.section}>
518 {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))}
522 {sections.references.length > 0
524 <div className={classes.section}>
525 {sections.references.map(element => (
526 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} />
531 {sections.choises.length > 0
533 <div className={classes.section}>
534 {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))}
538 {sections.rpcs.length > 0
540 <div className={classes.section}>
541 {sections.rpcs.map(element => (
542 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} />
551 private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
552 const { classes } = this.props;
553 // group by module name
554 const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => {
555 const elm = viewSpecification.elements[cur];
556 const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} });
557 moduleView.elements[cur] = elm;
561 const moduleKeys = Object.keys(modules).sort();
564 <div className={classes.moduleCollection}>
566 moduleKeys.map(key => {
567 const moduleView = modules[key];
569 <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} >
570 <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} >
571 <Typography className={classes.heading}>{key}</Typography>
572 </AccordionSummaryExt>
574 {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)}
584 private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) {
585 const listElements = listSpecification.elements;
586 const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath
587 .replace("$$$standard$$$", "topology-netconfnode%20resources%20-%20RestConf%20RFC%208040")
588 .replace("$$$action$$$", "put")}${listKeyProperty ? `_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : '' }` : undefined;
590 const config = listSpecification.config && listKeyProperty; // We can not configure a list with no key.
592 const navigate = (path: string) => {
593 this.props.history.push(`${this.props.match.url}${path}`);
596 const addNewElementAction = {
599 ariaLabel:'add-element',
601 navigate("[]"); // empty key means new element
606 const addWithApiDocElementAction = {
609 ariaLabel:'add-element-via-api-doc',
611 window.open(apiDocPathCreate, '_blank');
616 const { classes, removeElement } = this.props;
618 const DeleteIconWithConfirmation: React.FC<{disabled?: boolean, rowData: { [key: string]: any }, onReload: () => void }> = (props) => {
619 const confirm = useConfirm();
622 <Tooltip disableInteractive title={"Remove"} >
624 disabled={props.disabled}
625 className={classes.button}
626 aria-label="remove-element-button"
627 onClick={async (e) => {
630 confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" }, cancellationButtonProps: { color:"inherit" } })
631 .then(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`))
632 .then(props.onReload);
642 <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={
643 Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
644 const elm = listElements[cur];
645 if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
646 if (elm.label !== listKeyProperty) {
647 acc.push(elm.uiType === "boolean"
648 ? { property: elm.label, type: ColumnType.boolean }
649 : elm.uiType === "date"
650 ? { property: elm.label, type: ColumnType.date }
651 : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
653 acc.unshift(elm.uiType === "boolean"
654 ? { property: elm.label, type: ColumnType.boolean }
655 : elm.uiType === "date"
656 ? { property: elm.label, type: ColumnType.date }
657 : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
662 property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => {
664 <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} />
668 } onHandleClick={(ev, row) => {
670 listKeyProperty && navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); // Do not navigate without key.
671 }} ></SelectElementTable>
675 private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
676 const { classes } = this.props;
678 const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
680 // if (vsA.label === vsB.label) return 0;
681 if (vsA.label === keyProperty) return -1;
682 if (vsB.label === keyProperty) return +1;
685 // if (vsA.uiType === vsB.uiType) return 0;
686 // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
687 // if (vsA.uiType === "object") return +1;
691 const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
692 const elm = inputViewSpecification.elements[cur];
693 if (isViewElementObjectOrList(elm)) {
694 console.error("Object should not appear in RPC view !");
695 } else if (isViewElementChoise(elm)) {
696 acc.choises.push(elm);
697 } else if (isViewElementRpc(elm)) {
698 console.error("RPC should not appear in RPC view !");
700 acc.elements.push(elm);
703 }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] })
704 || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] };
706 sections.elements = sections.elements.sort(orderFunc);
710 <div className={classes.section} />
711 { sections.elements.length > 0
713 <div className={classes.section}>
714 {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
718 { sections.choises.length > 0
720 <div className={classes.section}>
721 {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))}
725 <Button color="inherit" onClick={() => {
726 const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
727 this.props.executeRpc(this.props.vPath!, resultingViewData);
729 <div className={classes.objectReult}>
730 {outputViewData !== undefined
731 ? renderObject(outputViewData)
739 private renderBreadCrumps() {
740 const { editMode } = this.state;
741 const { displaySpecification, vPath, nodeId } = this.props;
742 const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
743 let lastPath = `/configuration`;
744 let basePath = `/configuration/${nodeId}`;
746 <div className={this.props.classes.header}>
748 <Breadcrumbs aria-label="breadcrumbs">
749 <Link underline="hover" color="inherit" href="#" aria-label="back-breadcrumb"
750 onClick={(ev: React.MouseEvent<HTMLElement>) => {
752 this.props.history.push(lastPath);
754 <Link underline="hover" color="inherit" href="#"
755 aria-label={nodeId + '-breadcrumb'}
756 onClick={(ev: React.MouseEvent<HTMLElement>) => {
758 this.props.history.push(`/configuration/${nodeId}`);
759 }}><span>{nodeId}</span></Link>
761 pathParts.map(([prop, key], ind) => {
762 const path = `${basePath}/${prop}`;
763 const keyPath = key && `${basePath}/${prop}[${key}]`;
764 const propTitle = prop.replace(/^[^:]+:/, "");
767 <Link underline="hover" color="inherit" href="#"
768 aria-label={propTitle + '-breadcrumb'}
769 onClick={(ev: React.MouseEvent<HTMLElement>) => {
771 this.props.history.push(path);
772 }}><span>{propTitle}</span></Link>
774 keyPath && <Link underline="hover" color="inherit" href="#"
775 aria-label={key + '-breadcrumb'}
776 onClick={(ev: React.MouseEvent<HTMLElement>) => {
778 this.props.history.push(keyPath);
779 }}>{`[${key}]`}</Link> || null
784 basePath = keyPath || path;
790 {this.state.editMode && (
791 <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => {
792 this.props.vPath && (await this.props.reloadView(this.props.vPath));
793 this.setState({ editMode: false });
794 }} ><ArrowBack /></Fab>
796 { /* do not show edit if this is a list or it can't be edited */
797 displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
798 <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => {
799 if (this.state.editMode) {
800 // ensure only active choises will be contained
801 const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
802 this.props.onUpdateData(this.props.vPath!, resultingViewData);
804 this.setState({ editMode: !editMode });
817 private renderValueSelector() {
818 const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
819 if (!listKeyProperty || !listSpecification) {
820 throw new Error("ListKex ot view not specified.");
824 <div className={this.props.classes.container}>
825 <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={
826 Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
827 const elm = listSpecification.elements[cur];
828 if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
829 if (elm.label !== listKeyProperty) {
830 acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
832 acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
837 } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
842 private renderValueEditor() {
843 const { displaySpecification: ds, outputData } = this.props;
844 const { viewData, editMode, isNew } = this.state;
847 <div className={this.props.classes.container}>
848 {this.renderBreadCrumps()}
849 {ds.displayMode === DisplayModeType.doNotDisplay
851 : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
852 ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData)
853 : ds.displayMode === DisplayModeType.displayAsRPC
854 ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false)
855 : ds.displayMode === DisplayModeType.displayAsMessage
856 ? this.renderMessage(ds.renderMessage)
857 : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew)
863 private renderMessage(renderMessage: string) {
865 <div className={this.props.classes.container}>
866 <h4>{renderMessage}</h4>
871 private renderCollectingData() {
873 <div className={this.props.classes.outer}>
874 <div className={this.props.classes.inner}>
876 <h3>Processing ...</h3>
883 return this.props.collectingData || !this.state.viewData
884 ? this.renderCollectingData()
885 : this.props.listSpecification
886 ? this.renderValueSelector()
887 : this.renderValueEditor();
891 export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
892 export default ConfigurationApplication;