Merge "Update ODLUX"
[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(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`))
632                 .then(props.onReload);
633             }}
634             size="large">
635             <RemoveIcon />
636           </IconButton>
637         </Tooltip>
638       );
639     }
640
641     return (
642       <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={
643         Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
644           const elm = listElements[cur];
645           if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
646             if (elm.label !== listKeyProperty) {
647               acc.push(elm.uiType === "boolean"
648                 ? { property: elm.label, type: ColumnType.boolean }
649                 : elm.uiType === "date"
650                   ? { property: elm.label, type: ColumnType.date }
651                   : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
652             } else {
653               acc.unshift(elm.uiType === "boolean"
654                 ? { property: elm.label, type: ColumnType.boolean }
655                 : elm.uiType === "date"
656                   ? { property: elm.label, type: ColumnType.date }
657                   : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
658             }
659           }
660           return acc;
661         }, []).concat([{
662           property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => {
663             return (
664               <DeleteIconWithConfirmation disabled={!config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} />
665             );
666           })
667         }])
668       } onHandleClick={(ev, row) => {
669         ev.preventDefault();
670         listKeyProperty && navigate(`[${encodeURIComponent(row[listKeyProperty])}]`); // Do not navigate without key.
671       }} ></SelectElementTable>
672     );
673   }
674
675   private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
676     const { classes } = this.props;
677
678     const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
679       if (keyProperty) {
680         // if (vsA.label === vsB.label) return 0;
681         if (vsA.label === keyProperty) return -1;
682         if (vsB.label === keyProperty) return +1;
683       }
684
685       // if (vsA.uiType === vsB.uiType) return 0;
686       // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
687       // if (vsA.uiType === "object") return +1;
688       return -1;
689     };
690
691     const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
692       const elm = inputViewSpecification.elements[cur];
693       if (isViewElementObjectOrList(elm)) {
694         console.error("Object should not appear in RPC view !");
695       } else if (isViewElementChoise(elm)) {
696         acc.choises.push(elm);
697       } else if (isViewElementRpc(elm)) {
698         console.error("RPC should not appear in RPC view !");
699       } else {
700         acc.elements.push(elm);
701       }
702       return acc;
703     }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] })
704       || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] };
705
706     sections.elements = sections.elements.sort(orderFunc);
707
708     return (
709       <>
710         <div className={classes.section} />
711         { sections.elements.length > 0
712           ? (
713             <div className={classes.section}>
714               {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
715             </div>
716           ) : null
717         }
718         { sections.choises.length > 0
719           ? (
720             <div className={classes.section}>
721               {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))}
722             </div>
723           ) : null
724         }
725         <Button color="inherit" onClick={() => {
726           const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
727           this.props.executeRpc(this.props.vPath!, resultingViewData);
728         }} >Exec</Button>
729         <div className={classes.objectReult}>
730           {outputViewData !== undefined
731             ? renderObject(outputViewData)
732             : null
733           }
734         </div>
735       </>
736     );
737   };
738
739   private renderBreadCrumps() {
740     const { editMode } = this.state;
741     const { displaySpecification, vPath, nodeId } = this.props;
742     const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
743     let lastPath = `/configuration`;
744     let basePath = `/configuration/${nodeId}`;
745     return (
746       <div className={this.props.classes.header}>
747         <div>
748           <Breadcrumbs aria-label="breadcrumbs">
749             <Link underline="hover" color="inherit" href="#" aria-label="back-breadcrumb"
750               onClick={(ev: React.MouseEvent<HTMLElement>) => {
751                 ev.preventDefault();
752                 this.props.history.push(lastPath);
753               }}>Back</Link>
754             <Link underline="hover" color="inherit" href="#"
755               aria-label={nodeId + '-breadcrumb'}
756               onClick={(ev: React.MouseEvent<HTMLElement>) => {
757                 ev.preventDefault();
758                 this.props.history.push(`/configuration/${nodeId}`);
759               }}><span>{nodeId}</span></Link>
760             {
761               pathParts.map(([prop, key], ind) => {
762                 const path = `${basePath}/${prop}`;
763                 const keyPath = key && `${basePath}/${prop}[${key}]`;
764                 const propTitle = prop.replace(/^[^:]+:/, "");
765                 const ret = (
766                   <span key={ind}>
767                     <Link underline="hover" color="inherit" href="#"
768                       aria-label={propTitle + '-breadcrumb'}
769                       onClick={(ev: React.MouseEvent<HTMLElement>) => {
770                         ev.preventDefault();
771                         this.props.history.push(path);
772                       }}><span>{propTitle}</span></Link>
773                     {
774                       keyPath && <Link underline="hover" color="inherit" href="#"
775                         aria-label={key + '-breadcrumb'}
776                         onClick={(ev: React.MouseEvent<HTMLElement>) => {
777                           ev.preventDefault();
778                           this.props.history.push(keyPath);
779                         }}>{`[${key}]`}</Link> || null
780                     }
781                   </span>
782                 );
783                 lastPath = basePath;
784                 basePath = keyPath || path;
785                 return ret;
786               })
787             }
788           </Breadcrumbs>
789         </div>
790         {this.state.editMode && (
791           <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => {
792             this.props.vPath && (await this.props.reloadView(this.props.vPath));
793             this.setState({ editMode: false });
794           }} ><ArrowBack /></Fab>
795         ) || null}
796         { /* do not show edit if this is a list or it can't be edited */
797           displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
798             <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => {
799               if (this.state.editMode) {
800                 // ensure only active choises will be contained
801                 const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
802                 this.props.onUpdateData(this.props.vPath!, resultingViewData);
803               }
804               this.setState({ editMode: !editMode });
805             }}>
806               {editMode
807                 ? <SaveIcon />
808                 : <EditIcon />
809               }
810             </Fab>
811           </div> || null)
812         }
813       </div>
814     );
815   }
816
817   private renderValueSelector() {
818     const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
819     if (!listKeyProperty || !listSpecification) {
820       throw new Error("ListKex ot view not specified.");
821     }
822
823     return (
824       <div className={this.props.classes.container}>
825         <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={
826           Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
827             const elm = listSpecification.elements[cur];
828             if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
829               if (elm.label !== listKeyProperty) {
830                 acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
831               } else {
832                 acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
833               }
834             }
835             return acc;
836           }, [])
837         } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
838       </div>
839     );
840   }
841
842   private renderValueEditor() {
843     const { displaySpecification: ds, outputData } = this.props;
844     const { viewData, editMode, isNew } = this.state;
845
846     return (
847       <div className={this.props.classes.container}>
848         {this.renderBreadCrumps()}
849         {ds.displayMode === DisplayModeType.doNotDisplay
850           ? null
851           : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
852             ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData)
853             : ds.displayMode === DisplayModeType.displayAsRPC
854               ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false)
855               : ds.displayMode === DisplayModeType.displayAsMessage
856                 ? this.renderMessage(ds.renderMessage)
857                 : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew)
858         }
859       </div >
860     );
861   }
862
863   private renderMessage(renderMessage: string) {
864     return (
865       <div className={this.props.classes.container}>
866         <h4>{renderMessage}</h4>
867       </div>
868     );
869   }
870
871   private renderCollectingData() {
872     return (
873       <div className={this.props.classes.outer}>
874         <div className={this.props.classes.inner}>
875           <Loader />
876           <h3>Processing ...</h3>
877         </div>
878       </div>
879     );
880   }
881
882   render() {
883     return this.props.collectingData || !this.state.viewData
884       ? this.renderCollectingData()
885       : this.props.listSpecification
886         ? this.renderValueSelector()
887         : this.renderValueEditor();
888   }
889 }
890
891 export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
892 export default ConfigurationApplication;