Merge "SDNR UI don't process list which has more than one key"
[ccsdk/features.git] / sdnr / wt / odlux / apps / configurationApp / src / views / configurationApplication.tsx
1 /**
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
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
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
15  * the License.
16  * ============LICENSE_END==========================================================================
17  */
18
19 import React, { useState } from 'react';
20 import { RouteComponentProps, withRouter } from 'react-router-dom';
21
22 import { Theme } from '@mui/material/styles';
23
24 import { WithStyles } from '@mui/styles';
25 import withStyles from '@mui/styles/withStyles';
26 import createStyles from '@mui/styles/createStyles';
27
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';
33
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";
37
38 import { getAccessPolicyByUrl } from "../../../../framework/src/services/restService";
39
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";
50
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';
62
63
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';
72
73 import { useConfirm } from 'material-ui-confirm';
74 import restService from '../services/restServices';
75
76 const styles = (theme: Theme) => createStyles({
77   header: {
78     "display": "flex",
79     "justifyContent": "space-between",
80   },
81   leftButton: {
82     "justifyContent": "left"
83   },
84   outer: {
85     "flex": "1",
86     "height": "100%",
87     "display": "flex",
88     "alignItems": "center",
89     "justifyContent": "center",
90   },
91   inner: {
92
93   },
94   container: {
95     "height": "100%",
96     "display": "flex",
97     "flexDirection": "column",
98   },
99   "icon": {
100     "marginRight": theme.spacing(0.5),
101     "width": 20,
102     "height": 20,
103   },
104   "fab": {
105     "margin": theme.spacing(1),
106   },
107   button: {
108     margin: 0,
109     padding: "6px 6px",
110     minWidth: 'unset'
111   },
112   readOnly: {
113     '& label.Mui-focused': {
114       color: 'green',
115     },
116     '& .MuiInput-underline:after': {
117       borderBottomColor: 'green',
118     },
119     '& .MuiOutlinedInput-root': {
120       '& fieldset': {
121         borderColor: 'red',
122       },
123       '&:hover fieldset': {
124         borderColor: 'yellow',
125       },
126       '&.Mui-focused fieldset': {
127         borderColor: 'green',
128       },
129     },
130   },
131   uiView: {
132     overflowY: "auto",
133   },
134   section: {
135     padding: "15px",
136     borderBottom: `2px solid ${theme.palette.divider}`,
137   },
138   viewElements: {
139     width: 485, marginLeft: 20, marginRight: 20
140   },
141   verificationElements: {
142     width: 485, marginLeft: 20, marginRight: 20
143   },
144   heading: {
145     fontSize: theme.typography.pxToRem(15),
146     fontWeight: theme.typography.fontWeightRegular,
147   },
148   moduleCollection: {
149     marginTop: "16px",
150     overflow: "auto",
151   },
152   objectReult: {
153     overflow: "auto"
154   }
155 });
156
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,
167 });
168
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)),
175 });
176
177 const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
178
179 type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>;
180
181 type ConfigurationApplicationComponentState = {
182   isNew: boolean;
183   editMode: boolean;
184   canEdit: boolean;
185   viewData: { [key: string]: any } | null;
186   choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } };
187 }
188
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);
195       ev.preventDefault();
196     }
197   };
198   return (
199     <div onMouseDown={onMouseDown} >
200       <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} />
201     </div>
202   );
203 };
204
205 const OldProps = Symbol("OldProps");
206 class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> {
207
208   /**
209    *
210    */
211   constructor(props: ConfigurationApplicationComponentProps) {
212     super(props);
213
214     this.state = {
215       isNew: false,
216       canEdit: false,
217       editMode: false,
218       viewData: null,
219       choises: {},
220     }
221   }
222
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);
228
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;
235           });
236         }) || caseKeys[0];
237
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];
248           }
249           return dataAcc;
250         }, {} as { [name: string]: any });
251
252         acc[elm.id] = {
253           selectedCase,
254           data,
255         };
256       }
257       return acc;
258     }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {}
259   }
260
261   static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) {
262
263     if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
264       const isNew: boolean = nextProps.vPath?.endsWith("[]") || false;
265       const state = {
266         ...prevState,
267         isNew: isNew,
268         editMode: isNew,
269         viewData: nextProps.viewData || null,
270         [OldProps]: nextProps,
271         choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay
272           || nextProps.displaySpecification.displayMode === DisplayModeType.displayAsMessage
273           ? null
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)
277       }
278       return state;
279     }
280     return null;
281   }
282
283   private navigate = (path: string) => {
284     this.props.history.push(`${this.props.match.url}${path}`);
285   }
286
287   private changeValueFor = (property: string, value: any) => {
288     this.setState({
289       viewData: {
290         ...this.state.viewData,
291         [property]: value
292       }
293     });
294   }
295
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;
311             }
312           });
313           return;
314         };
315         Object.keys(caseElements).forEach(caseElementKey => {
316           acc.push(caseElements[caseElementKey]);
317         });
318       });
319       return acc;
320     }, [] as ViewElement[]);
321
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];
325       }
326       return acc;
327     }, {} as { [key: string]: any });
328   }
329
330   private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => {
331     const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`);
332     return !(policy.GET && policy.POST);
333   }
334
335   private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => {
336     const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`);
337     return !(policy.GET && policy.POST);
338   }
339
340   private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => {
341     if (isViewElementEmpty(uiElement)) {
342       return null;
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;
355     } else {
356       if (process.env.NODE_ENV !== "production") {
357         console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
358       }
359       return null;
360     }
361   }
362
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));
366
367     // do not show elements w/o any value from the backend
368     if (viewData[uiElement.id] == null && !editMode) {
369       return null;
370     } else if (isViewElementEmpty(uiElement)) {
371       return null;
372     } else if (uiElement.isList) {
373       /* element is a leaf-list */
374       return <UiElementLeafList
375         key={uiElement.id}
376         inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]}
377         value={uiElement}
378         readOnly={!canEdit}
379         disabled={editMode && !canEdit}
380         onChange={(e) => { this.changeValueFor(uiElement.id, e) }}
381         getEditorForViewElement={this.getEditorForViewElement}
382       />;
383     } else {
384       const Element = this.getEditorForViewElement(uiElement);
385       return Element != null
386         ? (
387           <Element
388             key={uiElement.id}
389             isKey={isKey}
390             inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
391             value={uiElement}
392             readOnly={!canEdit}
393             disabled={editMode && !canEdit}
394             onChange={(e) => { this.changeValueFor(uiElement.id, e) }}
395           />)
396         : null;
397     }
398   };
399
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)) {
404   //     return (
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>
410   //         </Tooltip>
411   //       </FormControl>
412   //     );
413   //   } else {
414   //     if (process.env.NODE_ENV !== "production") {
415   //       console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
416   //     }
417   //     return null;
418   //   }
419   // };
420
421   private renderUIChoise = (uiElement: ViewElementChoise, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
422     const isKey = (uiElement.label === keyProperty);
423
424     const currentChoise = this.state.choises[uiElement.id];
425     const currentCase = currentChoise && uiElement.cases[currentChoise.selectedCase];
426
427     const canEdit = editMode && (isNew || (uiElement.config && !isKey));
428     if (isViewElementChoise(uiElement)) {
429       const subElements = currentCase?.elements;
430       return (
431         <>
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}
437               onChange={(e) => {
438                 if (currentChoise.selectedCase === e.target.value) {
439                   return; // nothing changed
440                 }
441                 this.setState({ choises: { ...this.state.choises, [uiElement.id]: { ...this.state.choises[uiElement.id], selectedCase: e.target.value as string } } });
442               }}
443               readOnly={!canEdit}
444               disabled={editMode && !canEdit}
445               value={this.state.choises[uiElement.id].selectedCase}
446               inputProps={{
447                 name: uiElement.id,
448                 id: `select-${uiElement.id}`,
449               }}
450             >
451               {
452                 Object.keys(uiElement.cases).map(caseKey => {
453                   const caseElm = uiElement.cases[caseKey];
454                   return (
455                     <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem>
456                   );
457                 })
458               }
459             </Select>
460           </FormControl>
461           {subElements
462             ? Object.keys(subElements).map(elmKey => {
463               const elm = subElements[elmKey];
464               return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew);
465             })
466             : <h3>Invalid Choise</h3>
467           }
468         </>
469       );
470     } else {
471       if (process.env.NODE_ENV !== "production") {
472         console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
473       }
474       return null;
475     }
476   };
477
478   private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
479     const { classes } = this.props;
480
481
482
483     const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
484       if (keyProperty) {
485         // if (vsA.label === vsB.label) return 0;
486         if (vsA.label === keyProperty) return -1;
487         if (vsB.label === keyProperty) return +1;
488       }
489
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;
493       return -1;
494     };
495
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)) {
503         acc.rpcs.push(elm);
504       } else {
505         acc.elements.push(elm);
506       }
507       return acc;
508     }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] });
509
510     sections.elements = sections.elements.sort(orderFunc);
511
512     return (
513       <div className={classes.uiView}>
514         <div className={classes.section} />
515         {sections.elements.length > 0
516           ? (
517             <div className={classes.section}>
518               {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))}
519             </div>
520           ) : null
521         }
522         {sections.references.length > 0
523           ? (
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}`) }} />
527               ))}
528             </div>
529           ) : null
530         }
531         {sections.choises.length > 0
532           ? (
533             <div className={classes.section}>
534               {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))}
535             </div>
536           ) : null
537         }
538         {sections.rpcs.length > 0
539           ? (
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}`) }} />
543               ))}
544             </div>
545           ) : null
546         }
547       </div>
548     );
549   };
550
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;
558       return acc;
559     }, {});
560
561     const moduleKeys = Object.keys(modules).sort();
562
563     return (
564       <div className={classes.moduleCollection}>
565         {
566           moduleKeys.map(key => {
567             const moduleView = modules[key];
568             return (
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>
573                 <AccordionDetails>
574                   {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)}
575                 </AccordionDetails>
576               </Accordion>
577             );
578           })
579         }
580       </div>
581     );
582   };
583
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;
589
590     const config = listSpecification.config && listKeyProperty; // We can not configure a list with no key.
591
592     const navigate = (path: string) => {
593       this.props.history.push(`${this.props.match.url}${path}`);
594     };
595
596     const addNewElementAction = {
597       icon: AddIcon, 
598       tooltip: 'Add',
599       ariaLabel:'add-element',
600       onClick: () => {
601         navigate("[]"); // empty key means new element
602       },
603       disabled: !config,
604     };
605
606     const addWithApiDocElementAction = {
607       icon: PostAdd, 
608       tooltip: 'Add',
609       ariaLabel:'add-element-via-api-doc',
610       onClick: () => {
611         window.open(apiDocPathCreate, '_blank');
612       },
613       disabled: !config,
614     };
615
616     const { classes, removeElement } = this.props;
617
618     const DeleteIconWithConfirmation: React.FC<{disabled?: boolean, rowData: { [key: string]: any }, onReload: () => void }> = (props) => {
619       const confirm = useConfirm();
620
621       return (
622         <Tooltip disableInteractive title={"Remove"} >
623           <IconButton
624             disabled={props.disabled}
625             className={classes.button}
626             aria-label="remove-element-button"
627             onClick={async (e) => {
628               e.stopPropagation();
629               e.preventDefault();
630               confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" }, cancellationButtonProps: { color:"inherit" } })
631               .then(() => {
632                  let keyId = "";
633                  if (listKeyProperty && listKeyProperty.split(" ").length > 1) {
634                  keyId += listKeyProperty.split(" ").map(id => props.rowData[id]).join(",");
635                   } else {
636                    keyId = props.rowData[listKeyProperty];
637                     }
638                    return removeElement(`${this.props.vPath}[${keyId}]`)
639                 }).then(props.onReload);
640             }}
641             size="large">
642             <RemoveIcon />
643           </IconButton>
644         </Tooltip>
645       );
646     }
647
648     return (
649       <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={
650         Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
651           const elm = listElements[cur];
652           if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
653             if (elm.label !== listKeyProperty) {
654               acc.push(elm.uiType === "boolean"
655                 ? { property: elm.label, type: ColumnType.boolean }
656                 : elm.uiType === "date"
657                   ? { property: elm.label, type: ColumnType.date }
658                   : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
659             } else {
660               acc.unshift(elm.uiType === "boolean"
661                 ? { property: elm.label, type: ColumnType.boolean }
662                 : elm.uiType === "date"
663                   ? { property: elm.label, type: ColumnType.date }
664                   : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
665             }
666           }
667           return acc;
668         }, []).concat([{
669           property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => {
670             return (
671               <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} />
672             );
673           })
674         }])
675       } onHandleClick={(ev, row) => {
676         ev.preventDefault();
677         let keyId = ""
678         if (listKeyProperty && listKeyProperty.split(" ").length > 1) {
679           keyId += listKeyProperty.split(" ").map(id => row[id]).join(",");
680         } else {
681           keyId = row[listKeyProperty];
682         }
683         listKeyProperty && navigate(`[${encodeURIComponent(keyId)}]`); // Do not navigate without key.
684       }} ></SelectElementTable>
685     );
686   }
687
688   private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
689     const { classes } = this.props;
690
691     const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
692       if (keyProperty) {
693         // if (vsA.label === vsB.label) return 0;
694         if (vsA.label === keyProperty) return -1;
695         if (vsB.label === keyProperty) return +1;
696       }
697
698       // if (vsA.uiType === vsB.uiType) return 0;
699       // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
700       // if (vsA.uiType === "object") return +1;
701       return -1;
702     };
703
704     const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
705       const elm = inputViewSpecification.elements[cur];
706       if (isViewElementObjectOrList(elm)) {
707         console.error("Object should not appear in RPC view !");
708       } else if (isViewElementChoise(elm)) {
709         acc.choises.push(elm);
710       } else if (isViewElementRpc(elm)) {
711         console.error("RPC should not appear in RPC view !");
712       } else {
713         acc.elements.push(elm);
714       }
715       return acc;
716     }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] })
717       || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] };
718
719     sections.elements = sections.elements.sort(orderFunc);
720
721     return (
722       <>
723         <div className={classes.section} />
724         { sections.elements.length > 0
725           ? (
726             <div className={classes.section}>
727               {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
728             </div>
729           ) : null
730         }
731         { sections.choises.length > 0
732           ? (
733             <div className={classes.section}>
734               {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))}
735             </div>
736           ) : null
737         }
738         <Button color="inherit" onClick={() => {
739           const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
740           this.props.executeRpc(this.props.vPath!, resultingViewData);
741         }} >Exec</Button>
742         <div className={classes.objectReult}>
743           {outputViewData !== undefined
744             ? renderObject(outputViewData)
745             : null
746           }
747         </div>
748       </>
749     );
750   };
751
752   private renderBreadCrumps() {
753     const { editMode } = this.state;
754     const { displaySpecification, vPath, nodeId } = this.props;
755     const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
756     let lastPath = `/configuration`;
757     let basePath = `/configuration/${nodeId}`;
758     return (
759       <div className={this.props.classes.header}>
760         <div>
761           <Breadcrumbs aria-label="breadcrumbs">
762             <Link underline="hover" color="inherit" href="#" aria-label="back-breadcrumb"
763               onClick={(ev: React.MouseEvent<HTMLElement>) => {
764                 ev.preventDefault();
765                 this.props.history.push(lastPath);
766               }}>Back</Link>
767             <Link underline="hover" color="inherit" href="#"
768               aria-label={nodeId + '-breadcrumb'}
769               onClick={(ev: React.MouseEvent<HTMLElement>) => {
770                 ev.preventDefault();
771                 this.props.history.push(`/configuration/${nodeId}`);
772               }}><span>{nodeId}</span></Link>
773             {
774               pathParts.map(([prop, key], ind) => {
775                 const path = `${basePath}/${prop}`;
776                 const keyPath = key && `${basePath}/${prop}[${key}]`;
777                 const propTitle = prop.replace(/^[^:]+:/, "");
778                 const ret = (
779                   <span key={ind}>
780                     <Link underline="hover" color="inherit" href="#"
781                       aria-label={propTitle + '-breadcrumb'}
782                       onClick={(ev: React.MouseEvent<HTMLElement>) => {
783                         ev.preventDefault();
784                         this.props.history.push(path);
785                       }}><span>{propTitle}</span></Link>
786                     {
787                       keyPath && <Link underline="hover" color="inherit" href="#"
788                         aria-label={key + '-breadcrumb'}
789                         onClick={(ev: React.MouseEvent<HTMLElement>) => {
790                           ev.preventDefault();
791                           this.props.history.push(keyPath);
792                         }}>{`[${key && key.replace(/\%2C/g, ",")}]`}</Link> || null
793                     }
794                   </span>
795                 );
796                 lastPath = basePath;
797                 basePath = keyPath || path;
798                 return ret;
799               })
800             }
801           </Breadcrumbs>
802         </div>
803         {this.state.editMode && (
804           <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => {
805             this.props.vPath && (await this.props.reloadView(this.props.vPath));
806             this.setState({ editMode: false });
807           }} ><ArrowBack /></Fab>
808         ) || null}
809         { /* do not show edit if this is a list or it can't be edited */
810           displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
811             <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => {
812               if (this.state.editMode) {
813                 // ensure only active choises will be contained
814                 const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
815                 this.props.onUpdateData(this.props.vPath!, resultingViewData);
816               }
817               this.setState({ editMode: !editMode });
818             }}>
819               {editMode
820                 ? <SaveIcon />
821                 : <EditIcon />
822               }
823             </Fab>
824           </div> || null)
825         }
826       </div>
827     );
828   }
829
830   private renderValueSelector() {
831     const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
832     if (!listKeyProperty || !listSpecification) {
833       throw new Error("ListKex ot view not specified.");
834     }
835
836     return (
837       <div className={this.props.classes.container}>
838         <SelectElementTable stickyHeader idProperty={listKeyProperty} tableId={null} rows={listData} columns={
839           Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
840             const elm = listSpecification.elements[cur];
841             if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
842               if (elm.label !== listKeyProperty) {
843                 acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
844               } else {
845                 acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
846               }
847             }
848             return acc;
849           }, [])
850         } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
851       </div>
852     );
853   }
854
855   private renderValueEditor() {
856     const { displaySpecification: ds, outputData } = this.props;
857     const { viewData, editMode, isNew } = this.state;
858
859     return (
860       <div className={this.props.classes.container}>
861         {this.renderBreadCrumps()}
862         {ds.displayMode === DisplayModeType.doNotDisplay
863           ? null
864           : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
865             ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData)
866             : ds.displayMode === DisplayModeType.displayAsRPC
867               ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false)
868               : ds.displayMode === DisplayModeType.displayAsMessage
869                 ? this.renderMessage(ds.renderMessage)
870                 : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew)
871         }
872       </div >
873     );
874   }
875
876   private renderMessage(renderMessage: string) {
877     return (
878       <div className={this.props.classes.container}>
879         <h4>{renderMessage}</h4>
880       </div>
881     );
882   }
883
884   private renderCollectingData() {
885     return (
886       <div className={this.props.classes.outer}>
887         <div className={this.props.classes.inner}>
888           <Loader />
889           <h3>Processing ...</h3>
890         </div>
891       </div>
892     );
893   }
894
895   render() {
896     return this.props.collectingData || !this.state.viewData
897       ? this.renderCollectingData()
898       : this.props.listSpecification
899         ? this.renderValueSelector()
900         : this.renderValueEditor();
901   }
902 }
903
904 export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
905 export default ConfigurationApplication;