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 { useConfirm } from 'material-ui-confirm';
30 import { connect, IDispatcher, Connect } from '../../../../framework/src/flux/connect';
31 import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
32 import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from '../../../../framework/src/components/material-table';
33 import { Loader } from '../../../../framework/src/components/material-ui/loader';
34 import { renderObject } from '../../../../framework/src/components/objectDump';
36 import { DisplayModeType } from '../handlers/viewDescriptionHandler';
39 updateDataActionAsyncCreator,
40 updateViewActionAsyncCreator,
41 removeElementActionAsyncCreator,
42 executeRpcActionAsyncCreator,
43 } from '../actions/deviceActions';
53 isViewElementObjectOrList,
54 isViewElementSelection,
60 } from '../models/uiModels';
62 import { getAccessPolicyByUrl } from '../../../../framework/src/services/restService';
64 import Fab from '@mui/material/Fab';
65 import AddIcon from '@mui/icons-material/Add';
66 import PostAdd from '@mui/icons-material/PostAdd';
67 import ArrowBack from '@mui/icons-material/ArrowBack';
68 import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
69 import SaveIcon from '@mui/icons-material/Save';
70 import EditIcon from '@mui/icons-material/Edit';
71 import Tooltip from '@mui/material/Tooltip';
72 import FormControl from '@mui/material/FormControl';
73 import IconButton from '@mui/material/IconButton';
75 import InputLabel from '@mui/material/InputLabel';
76 import Select from '@mui/material/Select';
77 import MenuItem from '@mui/material/MenuItem';
78 import Breadcrumbs from '@mui/material/Breadcrumbs';
79 import Button from '@mui/material/Button';
80 import Link from '@mui/material/Link';
81 import Accordion from '@mui/material/Accordion';
82 import AccordionSummary from '@mui/material/AccordionSummary';
83 import AccordionDetails from '@mui/material/AccordionDetails';
84 import Typography from '@mui/material/Typography';
85 import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
87 import { BaseProps } from '../components/baseProps';
88 import { UIElementReference } from '../components/uiElementReference';
89 import { UiElementNumber } from '../components/uiElementNumber';
90 import { UiElementString } from '../components/uiElementString';
91 import { UiElementBoolean } from '../components/uiElementBoolean';
92 import { UiElementSelection } from '../components/uiElementSelection';
93 import { UIElementUnion } from '../components/uiElementUnion';
94 import { UiElementLeafList } from '../components/uiElementLeafList';
96 import { splitVPath } from '../utilities/viewEngineHelper';
98 const styles = (theme: Theme) => createStyles({
101 'justifyContent': 'space-between',
104 'justifyContent': 'left',
110 'alignItems': 'center',
111 'justifyContent': 'center',
119 'flexDirection': 'column',
122 'marginRight': theme.spacing(0.5),
127 'margin': theme.spacing(1),
135 '& label.Mui-focused': {
138 '& .MuiInput-underline:after': {
139 borderBottomColor: 'green',
141 '& .MuiOutlinedInput-root': {
145 '&:hover fieldset': {
146 borderColor: 'yellow',
148 '&.Mui-focused fieldset': {
149 borderColor: 'green',
158 borderBottom: `2px solid ${theme.palette.divider}`,
161 width: 485, marginLeft: 20, marginRight: 20,
163 verificationElements: {
164 width: 485, marginLeft: 20, marginRight: 20,
167 fontSize: theme.typography.pxToRem(15),
168 fontWeight: theme.typography.fontWeightRegular,
179 const mapProps = (state: IApplicationStoreState) => ({
180 collectingData: state.configuration.valueSelector.collectingData,
181 listKeyProperty: state.configuration.valueSelector.keyProperty,
182 listSpecification: state.configuration.valueSelector.listSpecification,
183 listData: state.configuration.valueSelector.listData,
184 vPath: state.configuration.viewDescription.vPath,
185 nodeId: state.configuration.deviceDescription.nodeId,
186 viewData: state.configuration.viewDescription.viewData,
187 outputData: state.configuration.viewDescription.outputData,
188 displaySpecification: state.configuration.viewDescription.displaySpecification,
191 const mapDispatch = (dispatcher: IDispatcher) => ({
192 onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
193 onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)),
194 reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)),
195 removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)),
196 executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)),
199 const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
201 type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>;
203 type ConfigurationApplicationComponentState = {
207 viewData: { [key: string]: any } | null;
208 choices: { [path: string]: { selectedCase: string; data: { [property: string]: any } } };
211 type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any;
212 const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => {
213 const [disabled, setDisabled] = useState(true);
214 const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => {
215 if (ev.button === 1) {
216 setDisabled(!disabled);
221 <div onMouseDown={onMouseDown} >
222 <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} />
227 const OldProps = Symbol('OldProps');
228 class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> {
233 constructor(props: ConfigurationApplicationComponentProps) {
245 private static getChoicesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => {
246 return Object.keys(elements).reduce((acc, cur) => {
247 const elm = elements[cur];
248 if (isViewElementChoice(elm)) {
249 const caseKeys = Object.keys(elm.cases);
251 // find the right case for this choice, use the first one with data, at least use index 0
252 const selectedCase = caseKeys.find(key => {
253 const caseElm = elm.cases[key];
254 return Object.keys(caseElm.elements).some(caseElmKey => {
255 const caseElmElm = caseElm.elements[caseElmKey];
256 return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined;
260 // extract all data of the active case
261 const caseElements = elm.cases[selectedCase].elements;
262 const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => {
263 const dataElm = caseElements[dataCur];
264 if (isViewElementEmpty(dataElm)) {
265 dataAcc[dataElm.label] = null;
266 } else if (viewData[dataElm.label] !== undefined) {
267 dataAcc[dataElm.label] = viewData[dataElm.label];
268 } else if (viewData[dataElm.id] !== undefined) {
269 dataAcc[dataElm.id] = viewData[dataElm.id];
272 }, {} as { [name: string]: any });
280 }, {} as { [path: string]: { selectedCase: string; data: { [property: string]: any } } }) || {};
283 static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) {
285 if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
286 const isNew: boolean = nextProps.vPath?.endsWith('[]') || false;
291 viewData: nextProps.viewData || null,
292 [OldProps]: nextProps,
293 choices: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay
294 || nextProps.displaySpecification.displayMode === DisplayModeType.displayAsMessage
296 : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC
297 ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || []
298 : ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData),
305 private navigate = (path: string) => {
306 this.props.history.push(`${this.props.match.url}${path}`);
309 private changeValueFor = (property: string, value: any) => {
312 ...this.state.viewData,
318 private collectData = (elements: { [name: string]: ViewElement }) => {
319 // ensure only active choices will be contained
320 const viewData: { [key: string]: any } = { ...this.state.viewData };
321 const choiceKeys = Object.keys(elements).filter(elmKey => isViewElementChoice(elements[elmKey]));
322 const elementsToRemove = choiceKeys.reduce((acc, curChoiceKey) => {
323 const currentChoice = elements[curChoiceKey] as ViewElementChoice;
324 const selectedCase = this.state.choices[curChoiceKey].selectedCase;
325 Object.keys(currentChoice.cases).forEach(caseKey => {
326 const caseElements = currentChoice.cases[caseKey].elements;
327 if (caseKey === selectedCase) {
328 Object.keys(caseElements).forEach(caseElementKey => {
329 const elm = caseElements[caseElementKey];
330 if (isViewElementEmpty(elm)) {
331 // insert null for all empty elements
332 viewData[elm.id] = null;
337 Object.keys(caseElements).forEach(caseElementKey => {
338 acc.push(caseElements[caseElementKey]);
342 }, [] as ViewElement[]);
344 return viewData && Object.keys(viewData).reduce((acc, cur) => {
345 if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) {
346 acc[cur] = viewData[cur];
349 }, {} as { [key: string]: any });
352 private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => {
353 const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`);
354 return !(policy.GET && policy.POST);
357 private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => {
358 const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`);
359 return !(policy.GET && policy.POST);
362 private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => {
363 if (isViewElementEmpty(uiElement)) {
365 } else if (isViewElementSelection(uiElement)) {
366 return UiElementSelection;
367 } else if (isViewElementBoolean(uiElement)) {
368 return UiElementBoolean;
369 } else if (isViewElementString(uiElement)) {
370 return UiElementString;
371 } else if (isViewElementDate(uiElement)) {
372 return UiElementString;
373 } else if (isViewElementNumber(uiElement)) {
374 return UiElementNumber;
375 } else if (isViewElementUnion(uiElement)) {
376 return UIElementUnion;
378 if (process.env.NODE_ENV !== 'production') {
379 console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`);
385 private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
386 const isKey = (uiElement.label === keyProperty);
387 const canEdit = editMode && (isNew || (uiElement.config && !isKey));
389 // do not show elements w/o any value from the backend
390 if (viewData[uiElement.id] == null && !editMode) {
392 } else if (isViewElementEmpty(uiElement)) {
394 } else if (uiElement.isList) {
395 /* element is a leaf-list */
396 return <UiElementLeafList
398 inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]}
401 disabled={editMode && !canEdit}
402 onChange={(e) => { this.changeValueFor(uiElement.id, e); }}
403 getEditorForViewElement={this.getEditorForViewElement}
406 const Element = this.getEditorForViewElement(uiElement);
407 return Element != null
412 inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
415 disabled={editMode && !canEdit}
416 onChange={(e) => { this.changeValueFor(uiElement.id, e); }}
422 // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
423 // const isKey = (uiElement.label === keyProperty);
424 // const canEdit = editMode && (isNew || (uiElement.config && !isKey));
425 // if (isViewElementObjectOrList(uiElement)) {
427 // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
428 // <Tooltip title={uiElement.description || ''}>
429 // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => {
430 // this.navigate(`/${uiElement.id}`);
431 // }}>{uiElement.label}</Button>
436 // if (process.env.NODE_ENV !== "production") {
437 // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
443 private renderUIChoice = (uiElement: ViewElementChoice, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
444 const isKey = (uiElement.label === keyProperty);
446 const currentChoice = this.state.choices[uiElement.id];
447 const currentCase = currentChoice && uiElement.cases[currentChoice.selectedCase];
449 const canEdit = editMode && (isNew || (uiElement.config && !isKey));
450 if (isViewElementChoice(uiElement)) {
451 const subElements = currentCase?.elements;
454 <FormControl variant="standard" key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
455 <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
456 <Select variant="standard"
457 aria-label={uiElement.label + '-selection'}
458 required={!!uiElement.mandatory}
460 if (currentChoice.selectedCase === e.target.value) {
461 return; // nothing changed
463 this.setState({ choices: { ...this.state.choices, [uiElement.id]: { ...this.state.choices[uiElement.id], selectedCase: e.target.value as string } } });
466 disabled={editMode && !canEdit}
467 value={this.state.choices[uiElement.id].selectedCase}
470 id: `select-${uiElement.id}`,
474 Object.keys(uiElement.cases).map(caseKey => {
475 const caseElm = uiElement.cases[caseKey];
477 <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: '100%' }}>{caseElm.label}</div></Tooltip></MenuItem>
484 ? Object.keys(subElements).map(elmKey => {
485 const elm = subElements[elmKey];
486 return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew);
488 : <h3>Invalid Choice</h3>
493 if (process.env.NODE_ENV !== 'production') {
494 console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`);
500 private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
501 const { classes } = this.props;
503 const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
505 // if (vsA.label === vsB.label) return 0;
506 if (vsA.label === keyProperty) return -1;
507 if (vsB.label === keyProperty) return +1;
510 // if (vsA.uiType === vsB.uiType) return 0;
511 // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
512 // if (vsA.uiType === "object") return +1;
516 const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => {
517 const elm = viewSpecification.elements[cur];
518 if (isViewElementObjectOrList(elm)) {
519 acc.references.push(elm);
520 } else if (isViewElementChoice(elm)) {
521 acc.choices.push(elm);
522 } else if (isViewElementRpc(elm)) {
525 acc.elements.push(elm);
528 }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] });
530 sections.elements = sections.elements.sort(orderFunc);
533 <div className={classes.uiView}>
534 <div className={classes.section} />
535 {sections.elements.length > 0
537 <div className={classes.section}>
538 {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))}
542 {sections.references.length > 0
544 <div className={classes.section}>
545 {sections.references.map(element => (
546 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} />
551 {sections.choices.length > 0
553 <div className={classes.section}>
554 {sections.choices.map(element => this.renderUIChoice(element, viewData, keyProperty, editMode, isNew))}
558 {sections.rpcs.length > 0
560 <div className={classes.section}>
561 {sections.rpcs.map(element => (
562 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} />
571 private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
572 const { classes } = this.props;
574 // group by module name
575 const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => {
576 const elm = viewSpecification.elements[cur];
577 const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} });
578 moduleView.elements[cur] = elm;
582 const moduleKeys = Object.keys(modules).sort();
585 <div className={classes.moduleCollection}>
587 moduleKeys.map(key => {
588 const moduleView = modules[key];
591 <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} >
592 <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} >
593 <Typography className={classes.heading}>{key}</Typography>
594 </AccordionSummaryExt>
596 {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)}
606 private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) {
607 const listElements = listSpecification.elements;
608 const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath
609 .replace('$$$standard$$$', 'topology-netconfnode%20resources%20-%20RestConf%20RFC%208040')
610 .replace('$$$action$$$', 'put')}${listKeyProperty ? `_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : '' }` : undefined;
612 const config = listSpecification.config && listKeyProperty; // We can not configure a list with no key.
614 const navigate = (path: string) => {
615 this.props.history.push(`${this.props.match.url}${path}`);
618 const addNewElementAction = {
621 ariaLabel:'add-element',
623 navigate('[]'); // empty key means new element
628 const addWithApiDocElementAction = {
631 ariaLabel:'add-element-via-api-doc',
633 window.open(apiDocPathCreate, '_blank');
638 const { classes, removeElement } = this.props;
640 const DeleteIconWithConfirmation: React.FC<{ disabled?: boolean; rowData: { [key: string]: any }; onReload: () => void }> = (props) => {
641 const confirm = useConfirm();
644 <Tooltip disableInteractive title={'Remove'} >
646 disabled={props.disabled}
647 className={classes.button}
648 aria-label="remove-element-button"
649 onClick={async (e) => {
652 confirm({ title: 'Do you really want to delete this element ?', description: 'This action is permanent!', confirmationButtonProps: { color: 'secondary' }, cancellationButtonProps: { color:'inherit' } })
655 if (listKeyProperty && listKeyProperty.split(' ').length > 1) {
656 keyId += listKeyProperty.split(' ').map(id => props.rowData[id]).join(',');
658 keyId = props.rowData[listKeyProperty];
660 return removeElement(`${this.props.vPath}[${keyId}]`);
661 }).then(props.onReload);
671 <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={
672 Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
673 const elm = listElements[cur];
674 if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) {
675 if (elm.label !== listKeyProperty) {
676 acc.push(elm.uiType === 'boolean'
677 ? { property: elm.label, type: ColumnType.boolean }
678 : elm.uiType === 'date'
679 ? { property: elm.label, type: ColumnType.date }
680 : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
682 acc.unshift(elm.uiType === 'boolean'
683 ? { property: elm.label, type: ColumnType.boolean }
684 : elm.uiType === 'date'
685 ? { property: elm.label, type: ColumnType.date }
686 : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
691 property: 'Actions', disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => {
693 <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} />
697 } onHandleClick={(ev, row) => {
700 if (listKeyProperty && listKeyProperty.split(' ').length > 1) {
701 keyId += listKeyProperty.split(' ').map(id => row[id]).join(',');
703 keyId = row[listKeyProperty];
705 if (listKeyProperty) {
706 navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key.
708 }} ></SelectElementTable>
712 private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
713 const { classes } = this.props;
715 const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
717 // if (vsA.label === vsB.label) return 0;
718 if (vsA.label === keyProperty) return -1;
719 if (vsB.label === keyProperty) return +1;
722 // if (vsA.uiType === vsB.uiType) return 0;
723 // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
724 // if (vsA.uiType === "object") return +1;
728 const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
729 const elm = inputViewSpecification.elements[cur];
730 if (isViewElementObjectOrList(elm)) {
731 console.error('Object should not appear in RPC view !');
732 } else if (isViewElementChoice(elm)) {
733 acc.choices.push(elm);
734 } else if (isViewElementRpc(elm)) {
735 console.error('RPC should not appear in RPC view !');
737 acc.elements.push(elm);
740 }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] })
741 || { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] };
743 sections.elements = sections.elements.sort(orderFunc);
747 <div className={classes.section} />
748 { sections.elements.length > 0
750 <div className={classes.section}>
751 {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
755 { sections.choices.length > 0
757 <div className={classes.section}>
758 {sections.choices.map(element => this.renderUIChoice(element, inputViewData, keyProperty, editMode, isNew))}
762 <Button color="inherit" onClick={() => {
763 const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
764 this.props.executeRpc(this.props.vPath!, resultingViewData);
766 <div className={classes.objectReult}>
767 {outputViewData !== undefined
768 ? renderObject(outputViewData)
776 private renderBreadCrumps() {
777 const { editMode } = this.state;
778 const { displaySpecification, vPath, nodeId } = this.props;
779 const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
780 let lastPath = '/configuration';
781 let basePath = `/configuration/${nodeId}`;
783 <div className={this.props.classes.header}>
785 <Breadcrumbs aria-label="breadcrumbs">
786 <Link underline="hover" color="inherit" href="#" aria-label="back-breadcrumb"
787 onClick={(ev: React.MouseEvent<HTMLElement>) => {
789 this.props.history.push(lastPath);
791 <Link underline="hover" color="inherit" href="#"
792 aria-label={nodeId + '-breadcrumb'}
793 onClick={(ev: React.MouseEvent<HTMLElement>) => {
795 this.props.history.push(`/configuration/${nodeId}`);
796 }}><span>{nodeId}</span></Link>
798 pathParts.map(([prop, key], ind) => {
799 const path = `${basePath}/${prop}`;
800 const keyPath = key && `${basePath}/${prop}[${key}]`;
801 const propTitle = prop.replace(/^[^:]+:/, '');
804 <Link underline="hover" color="inherit" href="#"
805 aria-label={propTitle + '-breadcrumb'}
806 onClick={(ev: React.MouseEvent<HTMLElement>) => {
808 this.props.history.push(path);
809 }}><span>{propTitle}</span></Link>
811 keyPath && <Link underline="hover" color="inherit" href="#"
812 aria-label={key + '-breadcrumb'}
813 onClick={(ev: React.MouseEvent<HTMLElement>) => {
815 this.props.history.push(keyPath);
816 }}>{`[${key && key.replace(/\%2C/g, ',')}]`}</Link> || null
821 basePath = keyPath || path;
827 {this.state.editMode && (
828 <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => {
829 if (this.props.vPath) {
830 await this.props.reloadView(this.props.vPath);
832 this.setState({ editMode: false });
833 }} ><ArrowBack /></Fab>
835 { /* do not show edit if this is a list or it can't be edited */
836 displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
837 <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => {
838 if (this.state.editMode) {
839 // ensure only active choices will be contained
840 const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
841 this.props.onUpdateData(this.props.vPath!, resultingViewData);
843 this.setState({ editMode: !editMode });
856 private renderValueSelector() {
857 const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
858 if (!listKeyProperty || !listSpecification) {
859 throw new Error('ListKex ot view not specified.');
863 <div className={this.props.classes.container}>
864 <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} columns={
865 Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
866 const elm = listSpecification.elements[cur];
867 if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) {
868 if (elm.label !== listKeyProperty) {
869 acc.push({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
871 acc.unshift({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
876 } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
881 private renderValueEditor() {
882 const { displaySpecification: ds, outputData } = this.props;
883 const { viewData, editMode, isNew } = this.state;
886 <div className={this.props.classes.container}>
887 {this.renderBreadCrumps()}
888 {ds.displayMode === DisplayModeType.doNotDisplay
890 : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
891 ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData)
892 : ds.displayMode === DisplayModeType.displayAsRPC
893 ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false)
894 : ds.displayMode === DisplayModeType.displayAsMessage
895 ? this.renderMessage(ds.renderMessage)
896 : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew)
902 private renderMessage(renderMessage: string) {
904 <div className={this.props.classes.container}>
905 <h4>{renderMessage}</h4>
910 private renderCollectingData() {
912 <div className={this.props.classes.outer}>
913 <div className={this.props.classes.inner}>
915 <h3>Processing ...</h3>
922 return this.props.collectingData || !this.state.viewData
923 ? this.renderCollectingData()
924 : this.props.listSpecification
925 ? this.renderValueSelector()
926 : this.renderValueEditor();
930 export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
931 export default ConfigurationApplication;