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 CheckIcon from '@mui/icons-material/Check';
70 import SaveIcon from '@mui/icons-material/Save';
71 import EditIcon from '@mui/icons-material/Edit';
72 import Tooltip from '@mui/material/Tooltip';
73 import FormControl from '@mui/material/FormControl';
74 import IconButton from '@mui/material/IconButton';
76 import InputLabel from '@mui/material/InputLabel';
77 import Select from '@mui/material/Select';
78 import MenuItem from '@mui/material/MenuItem';
79 import Breadcrumbs from '@mui/material/Breadcrumbs';
80 import Button from '@mui/material/Button';
81 import Link from '@mui/material/Link';
82 import Accordion from '@mui/material/Accordion';
83 import AccordionSummary from '@mui/material/AccordionSummary';
84 import AccordionDetails from '@mui/material/AccordionDetails';
85 import Typography from '@mui/material/Typography';
86 import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
88 import { BaseProps } from '../components/baseProps';
89 import { UIElementReference } from '../components/uiElementReference';
90 import { UiElementNumber } from '../components/uiElementNumber';
91 import { UiElementString } from '../components/uiElementString';
92 import { UiElementBoolean } from '../components/uiElementBoolean';
93 import { UiElementSelection } from '../components/uiElementSelection';
94 import { UIElementUnion } from '../components/uiElementUnion';
95 import { UiElementLeafList } from '../components/uiElementLeafList';
97 import { splitVPath } from '../utilities/viewEngineHelper';
99 const styles = (theme: Theme) => createStyles({
102 'justifyContent': 'space-between',
105 'justifyContent': 'left',
111 'alignItems': 'center',
112 'justifyContent': 'center',
120 'flexDirection': 'column',
123 'marginRight': theme.spacing(0.5),
128 'margin': theme.spacing(1),
136 '& label.Mui-focused': {
139 '& .MuiInput-underline:after': {
140 borderBottomColor: 'green',
142 '& .MuiOutlinedInput-root': {
146 '&:hover fieldset': {
147 borderColor: 'yellow',
149 '&.Mui-focused fieldset': {
150 borderColor: 'green',
159 borderBottom: `2px solid ${theme.palette.divider}`,
162 width: 485, marginLeft: 20, marginRight: 20,
164 verificationElements: {
165 width: 485, marginLeft: 20, marginRight: 20,
168 fontSize: theme.typography.pxToRem(15),
169 fontWeight: theme.typography.fontWeightRegular,
180 const mapProps = (state: IApplicationStoreState) => ({
181 collectingData: state.configuration.valueSelector.collectingData,
182 listKeyProperty: state.configuration.valueSelector.keyProperty,
183 listSpecification: state.configuration.valueSelector.listSpecification,
184 listData: state.configuration.valueSelector.listData,
185 vPath: state.configuration.viewDescription.vPath,
186 nodeId: state.configuration.deviceDescription.nodeId,
187 viewData: state.configuration.viewDescription.viewData,
188 outputData: state.configuration.viewDescription.outputData,
189 displaySpecification: state.configuration.viewDescription.displaySpecification,
192 const mapDispatch = (dispatcher: IDispatcher) => ({
193 onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
194 onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)),
195 reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)),
196 removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)),
197 executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)),
200 const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
202 type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>;
204 type ConfigurationApplicationComponentState = {
206 isNewSubElement: boolean;
209 viewData: { [key: string]: any } | null;
210 choices: { [path: string]: { selectedCase: string; data: { [property: string]: any } } };
213 type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any;
214 const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => {
215 const [disabled, setDisabled] = useState(true);
216 const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => {
217 if (ev.button === 1) {
218 setDisabled(!disabled);
223 <div onMouseDown={onMouseDown} >
224 <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} />
229 const OldProps = Symbol('OldProps');
230 class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> {
235 constructor(props: ConfigurationApplicationComponentProps) {
240 isNewSubElement: false,
248 private static getChoicesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => {
249 return Object.keys(elements).reduce((acc, cur) => {
250 const elm = elements[cur];
251 if (isViewElementChoice(elm)) {
252 const caseKeys = Object.keys(elm.cases);
254 // find the right case for this choice, use the first one with data, at least use index 0
255 const selectedCase = caseKeys.find(key => {
256 const caseElm = elm.cases[key];
257 return Object.keys(caseElm.elements).some(caseElmKey => {
258 const caseElmElm = caseElm.elements[caseElmKey];
259 return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined;
263 // extract all data of the active case
264 const caseElements = elm.cases[selectedCase].elements;
265 const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => {
266 const dataElm = caseElements[dataCur];
267 if (isViewElementEmpty(dataElm)) {
268 dataAcc[dataElm.label] = null;
269 } else if (viewData[dataElm.label] !== undefined) {
270 dataAcc[dataElm.label] = viewData[dataElm.label];
271 } else if (viewData[dataElm.id] !== undefined) {
272 dataAcc[dataElm.id] = viewData[dataElm.id];
275 }, {} as { [name: string]: any });
283 }, {} as { [path: string]: { selectedCase: string; data: { [property: string]: any } } }) || {};
286 static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) {
288 if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
289 const isNew: boolean = nextProps.vPath?.includes('[]') || false;
290 const isNewSubElement: boolean = nextProps.vPath?.includes('[]') && !nextProps.vPath?.endsWith('[]') || false;
296 isNewSubElement: isNewSubElement,
297 viewData: nextProps.viewData || null,
298 [OldProps]: nextProps,
299 choices: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay
300 || nextProps.displaySpecification.displayMode === DisplayModeType.displayAsMessage
302 : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC
303 ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || []
304 : ConfigurationApplicationComponent.getChoicesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData),
311 private navigate = (path: string) => {
312 this.props.history.push(`${this.props.match.url}${path}`);
315 private changeValueFor = (property: string, value: any) => {
318 ...this.state.viewData,
324 private collectData = (elements: { [name: string]: ViewElement }) => {
325 // ensure only active choices will be contained
326 const viewData: { [key: string]: any } = { ...this.state.viewData };
327 const choiceKeys = Object.keys(elements).filter(elmKey => isViewElementChoice(elements[elmKey]));
328 const elementsToRemove = choiceKeys.reduce((acc, curChoiceKey) => {
329 const currentChoice = elements[curChoiceKey] as ViewElementChoice;
330 const selectedCase = this.state.choices[curChoiceKey].selectedCase;
331 Object.keys(currentChoice.cases).forEach(caseKey => {
332 const caseElements = currentChoice.cases[caseKey].elements;
333 if (caseKey === selectedCase) {
334 Object.keys(caseElements).forEach(caseElementKey => {
335 const elm = caseElements[caseElementKey];
336 if (isViewElementEmpty(elm)) {
337 // insert null for all empty elements
338 viewData[elm.id] = null;
343 Object.keys(caseElements).forEach(caseElementKey => {
344 acc.push(caseElements[caseElementKey]);
348 }, [] as ViewElement[]);
350 return viewData && Object.keys(viewData).reduce((acc, cur) => {
351 if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) {
352 acc[cur] = viewData[cur];
355 }, {} as { [key: string]: any });
358 private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => {
359 const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`);
360 return !(policy.GET && policy.POST);
363 private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => {
364 const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`);
365 return !(policy.GET && policy.POST);
368 private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => {
369 if (isViewElementEmpty(uiElement)) {
371 } else if (isViewElementSelection(uiElement)) {
372 return UiElementSelection;
373 } else if (isViewElementBoolean(uiElement)) {
374 return UiElementBoolean;
375 } else if (isViewElementString(uiElement)) {
376 return UiElementString;
377 } else if (isViewElementDate(uiElement)) {
378 return UiElementString;
379 } else if (isViewElementNumber(uiElement)) {
380 return UiElementNumber;
381 } else if (isViewElementUnion(uiElement)) {
382 return UIElementUnion;
384 if (process.env.NODE_ENV !== 'production') {
385 console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`);
391 private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
392 const isKey = (uiElement.label === keyProperty);
393 const canEdit = editMode && (isNew || (uiElement.config && !isKey));
395 // do not show elements w/o any value from the backend
396 if (viewData[uiElement.id] == null && !editMode) {
398 } else if (isViewElementEmpty(uiElement)) {
400 } else if (uiElement.isList) {
401 /* element is a leaf-list */
402 return <UiElementLeafList
404 inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]}
407 disabled={editMode && !canEdit}
408 onChange={(e) => { this.changeValueFor(uiElement.id, e); }}
409 getEditorForViewElement={this.getEditorForViewElement}
412 const Element = this.getEditorForViewElement(uiElement);
413 return Element != null
418 inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
421 disabled={editMode && !canEdit}
422 onChange={(e) => { this.changeValueFor(uiElement.id, e); }}
428 // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
429 // const isKey = (uiElement.label === keyProperty);
430 // const canEdit = editMode && (isNew || (uiElement.config && !isKey));
431 // if (isViewElementObjectOrList(uiElement)) {
433 // <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
434 // <Tooltip title={uiElement.description || ''}>
435 // <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => {
436 // this.navigate(`/${uiElement.id}`);
437 // }}>{uiElement.label}</Button>
442 // if (process.env.NODE_ENV !== "production") {
443 // console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
449 private renderUIChoice = (uiElement: ViewElementChoice, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
450 const isKey = (uiElement.label === keyProperty);
452 const currentChoice = this.state.choices[uiElement.id];
453 const currentCase = currentChoice && uiElement.cases[currentChoice.selectedCase];
455 const canEdit = editMode && (isNew || (uiElement.config && !isKey));
456 if (isViewElementChoice(uiElement)) {
457 const subElements = currentCase?.elements;
460 <FormControl variant="standard" key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
461 <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
462 <Select variant="standard"
463 aria-label={uiElement.label + '-selection'}
464 required={!!uiElement.mandatory}
466 if (currentChoice.selectedCase === e.target.value) {
467 return; // nothing changed
469 this.setState({ choices: { ...this.state.choices, [uiElement.id]: { ...this.state.choices[uiElement.id], selectedCase: e.target.value as string } } });
472 disabled={editMode && !canEdit}
473 value={this.state.choices[uiElement.id].selectedCase}
476 id: `select-${uiElement.id}`,
480 Object.keys(uiElement.cases).map(caseKey => {
481 const caseElm = uiElement.cases[caseKey];
483 <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: '100%' }}>{caseElm.label}</div></Tooltip></MenuItem>
490 ? Object.keys(subElements).map(elmKey => {
491 const elm = subElements[elmKey];
492 return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew);
494 : <h3>Invalid Choice</h3>
499 if (process.env.NODE_ENV !== 'production') {
500 console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`);
506 private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
507 const { classes } = this.props;
509 const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
511 // if (vsA.label === vsB.label) return 0;
512 if (vsA.label === keyProperty) return -1;
513 if (vsB.label === keyProperty) return +1;
516 // if (vsA.uiType === vsB.uiType) return 0;
517 // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
518 // if (vsA.uiType === "object") return +1;
522 const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => {
523 const elm = viewSpecification.elements[cur];
524 if (isViewElementObjectOrList(elm)) {
525 acc.references.push(elm);
526 } else if (isViewElementChoice(elm)) {
527 acc.choices.push(elm);
528 } else if (isViewElementRpc(elm)) {
531 acc.elements.push(elm);
534 }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] });
536 sections.elements = sections.elements.sort(orderFunc);
539 <div className={classes.uiView}>
540 <div className={classes.section} />
541 {sections.elements.length > 0
543 <div className={classes.section}>
544 {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))}
548 {sections.references.length > 0
550 <div className={classes.section}>
551 {sections.references.map(element => (
552 <UIElementReference key={element.id} element={element} disabled={!isNew && (editMode || this.isPolicyViewElementForbidden(element, dataPath))} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} />
557 {sections.choices.length > 0
559 <div className={classes.section}>
560 {sections.choices.map(element => this.renderUIChoice(element, viewData, keyProperty, editMode, isNew))}
564 {sections.rpcs.length > 0
566 <div className={classes.section}>
567 {sections.rpcs.map(element => (
568 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`); }} />
577 private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
578 const { classes } = this.props;
580 // group by module name
581 const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => {
582 const elm = viewSpecification.elements[cur];
583 const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} });
584 moduleView.elements[cur] = elm;
588 const moduleKeys = Object.keys(modules).sort();
591 <div className={classes.moduleCollection}>
593 moduleKeys.map(key => {
594 const moduleView = modules[key];
597 <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} >
598 <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} >
599 <Typography className={classes.heading}>{key}</Typography>
600 </AccordionSummaryExt>
602 {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)}
612 private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) {
613 const listElements = listSpecification.elements;
614 const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath
615 .replace('$$$standard$$$', 'topology-netconfnode%20resources%20-%20RestConf%20RFC%208040')
616 .replace('$$$action$$$', 'put')}${listKeyProperty ? `_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : '' }` : undefined;
618 const config = listSpecification.config && listKeyProperty; // We can not configure a list with no key.
620 const navigate = (path: string) => {
621 this.props.history.push(`${this.props.match.url}${path}`);
624 const addNewElementAction = {
627 ariaLabel:'add-element',
629 navigate('[]'); // empty key means new element
634 const addWithApiDocElementAction = {
637 ariaLabel:'add-element-via-api-doc',
639 window.open(apiDocPathCreate, '_blank');
644 const { classes, removeElement } = this.props;
646 const DeleteIconWithConfirmation: React.FC<{ disabled?: boolean; rowData: { [key: string]: any }; onReload: () => void }> = (props) => {
647 const confirm = useConfirm();
650 <Tooltip disableInteractive title={'Remove'} >
652 disabled={props.disabled}
653 className={classes.button}
654 aria-label="remove-element-button"
655 onClick={async (e) => {
658 confirm({ title: 'Do you really want to delete this element ?', description: 'This action is permanent!', confirmationButtonProps: { color: 'secondary' }, cancellationButtonProps: { color:'inherit' } })
661 if (listKeyProperty && listKeyProperty.split(' ').length > 1) {
662 keyId += listKeyProperty.split(' ').map(id => props.rowData[id]).join(',');
664 keyId = props.rowData[listKeyProperty];
666 return removeElement(`${this.props.vPath}[${keyId}]`);
667 }).then(props.onReload);
677 <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={
678 Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
679 const elm = listElements[cur];
680 if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) {
681 if (elm.label !== listKeyProperty) {
682 acc.push(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 });
688 acc.unshift(elm.uiType === 'boolean'
689 ? { property: elm.label, type: ColumnType.boolean }
690 : elm.uiType === 'date'
691 ? { property: elm.label, type: ColumnType.date }
692 : { property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
697 property: 'Actions', disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => {
699 <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} />
703 } onHandleClick={(ev, row) => {
706 if (listKeyProperty && listKeyProperty.split(' ').length > 1) {
707 keyId += listKeyProperty.split(' ').map(id => encodeURIComponent(String(row[id]))).join(',');
709 keyId = encodeURIComponent(String(row[listKeyProperty]));
711 if (listKeyProperty) {
712 navigate(`[${keyId}]`); // Do not navigate without key.
714 }} ></SelectElementTable>
718 private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
719 const { classes } = this.props;
721 const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
723 // if (vsA.label === vsB.label) return 0;
724 if (vsA.label === keyProperty) return -1;
725 if (vsB.label === keyProperty) return +1;
728 // if (vsA.uiType === vsB.uiType) return 0;
729 // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
730 // if (vsA.uiType === "object") return +1;
734 const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
735 const elm = inputViewSpecification.elements[cur];
736 if (isViewElementObjectOrList(elm)) {
737 console.error('Object should not appear in RPC view !');
738 } else if (isViewElementChoice(elm)) {
739 acc.choices.push(elm);
740 } else if (isViewElementRpc(elm)) {
741 console.error('RPC should not appear in RPC view !');
743 acc.elements.push(elm);
746 }, { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] })
747 || { elements: [] as ViewElement[], references: [] as ViewElement[], choices: [] as ViewElementChoice[], rpcs: [] as ViewElementRpc[] };
749 sections.elements = sections.elements.sort(orderFunc);
753 <div className={classes.section} />
754 { sections.elements.length > 0
756 <div className={classes.section}>
757 {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
761 { sections.choices.length > 0
763 <div className={classes.section}>
764 {sections.choices.map(element => this.renderUIChoice(element, inputViewData, keyProperty, editMode, isNew))}
768 <Button color="inherit" onClick={() => {
769 const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
770 this.props.executeRpc(this.props.vPath!, resultingViewData);
772 <div className={classes.objectReult}>
773 {outputViewData !== undefined
774 ? renderObject(outputViewData)
782 private renderBreadCrumps() {
783 const { editMode, isNew, isNewSubElement } = this.state;
784 const { displaySpecification, vPath, nodeId } = this.props;
785 const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
787 let lastPath = '/configuration';
788 let basePath = `/configuration/${nodeId}`;
791 <div className={this.props.classes.header}>
793 <Breadcrumbs aria-label="breadcrumbs">
794 <Link underline="hover" color="inherit" href="#" aria-label="back-breadcrumb"
795 onClick={(ev: React.MouseEvent<HTMLElement>) => {
797 this.props.history.push(lastPath);
799 <Link underline="hover" color="inherit" href="#"
800 aria-label={nodeId + '-breadcrumb'}
801 onClick={(ev: React.MouseEvent<HTMLElement>) => {
803 this.props.history.push(`/configuration/${nodeId}`);
804 }}><span>{nodeId}</span></Link>
806 pathParts.map(([prop, key], ind) => {
807 const path = `${basePath}/${prop}`;
808 const keyPath = key && `${basePath}/${prop}[${key}]`;
809 const propTitle = prop.replace(/^[^:]+:/, '');
812 <Link underline="hover" color="inherit" href="#"
813 aria-label={propTitle + '-breadcrumb'}
814 onClick={(ev: React.MouseEvent<HTMLElement>) => {
816 this.props.history.push(path);
817 }}><span>{propTitle}</span></Link>
819 keyPath && <Link underline="hover" color="inherit" href="#"
820 aria-label={key + '-breadcrumb'}
821 onClick={(ev: React.MouseEvent<HTMLElement>) => {
823 this.props.history.push(keyPath);
824 }}>{`[${key && key.replace(/\%2C/g, ',')}]`}</Link> || null
829 basePath = keyPath || path;
835 {this.state.editMode && (
836 <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => {
837 if (this.props.vPath) {
838 if (isNewSubElement || isNew) {
839 const index = this.props.vPath.lastIndexOf('[]');
840 const newVPath = this.props.vPath.substring(0, index + ( isNewSubElement ? 2 : 0 ));
841 this.props.history.replace(`/configuration/${nodeId}/${newVPath}`);
843 await this.props.reloadView(this.props.vPath);
846 this.setState({ editMode: false });
847 }} ><ArrowBack /></Fab>
849 { /* do not show edit if this is a list or it can't be edited */
850 displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
851 <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => {
852 if (this.state.editMode && this.props.vPath) {
853 // ensure only active choices will be contained
854 const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
855 this.props.onUpdateData(this.props.vPath!, resultingViewData);
857 const index = this.props.vPath.lastIndexOf('[]');
858 const newVPath = this.props.vPath.substring(0, index + ( isNewSubElement ? 2 : 0 ));
859 this.props.history.replace(`/configuration/${nodeId}/${newVPath}`);
861 this.setState({ editMode: !editMode });
876 private renderValueSelector() {
877 const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
878 if (!listKeyProperty || !listSpecification) {
879 throw new Error('ListKex ot view not specified.');
883 <div className={this.props.classes.container}>
884 <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} columns={
885 Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
886 const elm = listSpecification.elements[cur];
887 if (elm.uiType !== 'object' && listData.every(entry => entry[elm.label] != null)) {
888 if (elm.label !== listKeyProperty) {
889 acc.push({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
891 acc.unshift({ property: elm.label, type: elm.uiType === 'number' ? ColumnType.numeric : ColumnType.text });
896 } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
901 private renderValueEditor() {
902 const { displaySpecification: ds, outputData } = this.props;
903 const { viewData, editMode, isNew } = this.state;
906 <div className={this.props.classes.container}>
907 {this.renderBreadCrumps()}
908 {ds.displayMode === DisplayModeType.doNotDisplay
910 : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
911 ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData)
912 : ds.displayMode === DisplayModeType.displayAsRPC
913 ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false)
914 : ds.displayMode === DisplayModeType.displayAsMessage
915 ? this.renderMessage(ds.renderMessage)
916 : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew)
922 private renderMessage(renderMessage: string) {
924 <div className={this.props.classes.container}>
925 <h4>{renderMessage}</h4>
930 private renderCollectingData() {
932 <div className={this.props.classes.outer}>
933 <div className={this.props.classes.inner}>
935 <h3>Processing ...</h3>
942 console.log('ConfigurationApplication.render()', this.props);
943 return this.props.collectingData || !this.state.viewData
944 ? this.renderCollectingData()
945 : this.props.listSpecification
946 ? this.renderValueSelector()
947 : this.renderValueEditor();
951 export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
952 export default ConfigurationApplication;