Merge "Add aria-labels to odlux tables"
[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 { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles';
23
24 import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect";
25 import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
26 import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table";
27 import { Loader } from "../../../../framework/src/components/material-ui/loader";
28 import { renderObject } from '../../../../framework/src/components/objectDump';
29
30 import { DisplayModeType } from '../handlers/viewDescriptionHandler';
31 import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator, updateViewActionAsyncCreator, removeElementActionAsyncCreator, executeRpcActionAsyncCreator } from "../actions/deviceActions";
32 import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection, isViewElementChoise, ViewElement, ViewElementChoise, isViewElementUnion, isViewElementRpc, ViewElementRpc, isViewElementEmpty, isViewElementDate } from "../models/uiModels";
33
34 import { getAccessPolicyByUrl } from "../../../../framework/src/services/restService";
35
36 import Fab from '@material-ui/core/Fab';
37 import AddIcon from '@material-ui/icons/Add';
38 import PostAdd from '@material-ui/icons/PostAdd';
39 import ArrowBack from '@material-ui/icons/ArrowBack';
40 import RemoveIcon from '@material-ui/icons/RemoveCircleOutline';
41 import SaveIcon from '@material-ui/icons/Save';
42 import EditIcon from '@material-ui/icons/Edit';
43 import Tooltip from "@material-ui/core/Tooltip";
44 import FormControl from "@material-ui/core/FormControl";
45 import IconButton from "@material-ui/core/IconButton";
46
47 import InputLabel from "@material-ui/core/InputLabel";
48 import Select from "@material-ui/core/Select";
49 import MenuItem from "@material-ui/core/MenuItem";
50 import Breadcrumbs from "@material-ui/core/Breadcrumbs";
51 import Button from '@material-ui/core/Button';
52 import Link from "@material-ui/core/Link";
53 import Accordion from '@material-ui/core/Accordion';
54 import AccordionSummary from '@material-ui/core/AccordionSummary';
55 import AccordionDetails from '@material-ui/core/AccordionDetails';
56 import Typography from '@material-ui/core/Typography';
57 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
58
59
60 import { BaseProps } from '../components/baseProps';
61 import { UIElementReference } from '../components/uiElementReference';
62 import { UiElementNumber } from '../components/uiElementNumber';
63 import { UiElementString } from '../components/uiElementString';
64 import { UiElementBoolean } from '../components/uiElementBoolean';
65 import { UiElementSelection } from '../components/uiElementSelection';
66 import { UIElementUnion } from '../components/uiElementUnion';
67 import { UiElementLeafList } from '../components/uiElementLeafList';
68
69 import { useConfirm } from 'material-ui-confirm';
70 import restService from '../services/restServices';
71
72 const styles = (theme: Theme) => createStyles({
73   header: {
74     "display": "flex",
75     "justifyContent": "space-between",
76   },
77   leftButton: {
78     "justifyContent": "left"
79   },
80   outer: {
81     "flex": "1",
82     "height": "100%",
83     "display": "flex",
84     "alignItems": "center",
85     "justifyContent": "center",
86   },
87   inner: {
88
89   },
90   container: {
91     "height": "100%",
92     "display": "flex",
93     "flexDirection": "column",
94   },
95   "icon": {
96     "marginRight": theme.spacing(0.5),
97     "width": 20,
98     "height": 20,
99   },
100   "fab": {
101     "margin": theme.spacing(1),
102   },
103   button: {
104     margin: 0,
105     padding: "6px 6px",
106     minWidth: 'unset'
107   },
108   readOnly: {
109     '& label.Mui-focused': {
110       color: 'green',
111     },
112     '& .MuiInput-underline:after': {
113       borderBottomColor: 'green',
114     },
115     '& .MuiOutlinedInput-root': {
116       '& fieldset': {
117         borderColor: 'red',
118       },
119       '&:hover fieldset': {
120         borderColor: 'yellow',
121       },
122       '&.Mui-focused fieldset': {
123         borderColor: 'green',
124       },
125     },
126   },
127   uiView: {
128     overflowY: "auto",
129   },
130   section: {
131     padding: "15px",
132     borderBottom: `2px solid ${theme.palette.divider}`,
133   },
134   viewElements: {
135     width: 485, marginLeft: 20, marginRight: 20
136   },
137   verificationElements: {
138     width: 485, marginLeft: 20, marginRight: 20
139   },
140   heading: {
141     fontSize: theme.typography.pxToRem(15),
142     fontWeight: theme.typography.fontWeightRegular,
143   },
144   moduleCollection: {
145     marginTop: "16px",
146     overflow: "auto",
147   },
148   objectReult: {
149     overflow: "auto"
150   }
151 });
152
153 const mapProps = (state: IApplicationStoreState) => ({
154   collectingData: state.configuration.valueSelector.collectingData,
155   listKeyProperty: state.configuration.valueSelector.keyProperty,
156   listSpecification: state.configuration.valueSelector.listSpecification,
157   listData: state.configuration.valueSelector.listData,
158   vPath: state.configuration.viewDescription.vPath,
159   nodeId: state.configuration.deviceDescription.nodeId,
160   viewData: state.configuration.viewDescription.viewData,
161   outputData: state.configuration.viewDescription.outputData,
162   displaySpecification: state.configuration.viewDescription.displaySpecification,
163 });
164
165 const mapDispatch = (dispatcher: IDispatcher) => ({
166   onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
167   onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)),
168   reloadView: (vPath: string) => dispatcher.dispatch(updateViewActionAsyncCreator(vPath)),
169   removeElement: (vPath: string) => dispatcher.dispatch(removeElementActionAsyncCreator(vPath)),
170   executeRpc: (vPath: string, data: any) => dispatcher.dispatch(executeRpcActionAsyncCreator(vPath, data)),
171 });
172
173 const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
174
175 type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>;
176
177 type ConfigurationApplicationComponentState = {
178   isNew: boolean;
179   editMode: boolean;
180   canEdit: boolean;
181   viewData: { [key: string]: any } | null;
182   choises: { [path: string]: { selectedCase: string, data: { [property: string]: any } } };
183 }
184
185 type GetStatelessComponentProps<T> = T extends (props: infer P & { children?: React.ReactNode }) => any ? P : any
186 const AccordionSummaryExt: React.FC<GetStatelessComponentProps<typeof AccordionSummary>> = (props) => {
187   const [disabled, setDisabled] = useState(true);
188   const onMouseDown = (ev: React.MouseEvent<HTMLElement>) => {
189     if (ev.button === 1) {
190       setDisabled(!disabled);
191       ev.preventDefault();
192     }
193   };
194   return (
195     <div onMouseDown={onMouseDown} >
196       <AccordionSummary {...{ ...props, disabled: props.disabled && disabled }} />
197     </div>
198   );
199 };
200
201 const OldProps = Symbol("OldProps");
202 class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> {
203
204   /**
205    *
206    */
207   constructor(props: ConfigurationApplicationComponentProps) {
208     super(props);
209
210     this.state = {
211       isNew: false,
212       canEdit: false,
213       editMode: false,
214       viewData: null,
215       choises: {},
216     }
217   }
218
219   private static getChoisesFromElements = (elements: { [name: string]: ViewElement }, viewData: any) => {
220     return Object.keys(elements).reduce((acc, cur) => {
221       const elm = elements[cur];
222       if (isViewElementChoise(elm)) {
223         const caseKeys = Object.keys(elm.cases);
224
225         // find the right case for this choise, use the first one with data, at least use index 0
226         const selectedCase = caseKeys.find(key => {
227           const caseElm = elm.cases[key];
228           return Object.keys(caseElm.elements).some(caseElmKey => {
229             const caseElmElm = caseElm.elements[caseElmKey];
230             return viewData[caseElmElm.label] !== undefined || viewData[caseElmElm.id] != undefined;
231           });
232         }) || caseKeys[0];
233
234         // extract all data of the active case
235         const caseElements = elm.cases[selectedCase].elements;
236         const data = Object.keys(caseElements).reduce((dataAcc, dataCur) => {
237           const dataElm = caseElements[dataCur];
238           if (isViewElementEmpty(dataElm)) {
239             dataAcc[dataElm.label] = null;
240           } else if (viewData[dataElm.label] !== undefined) {
241             dataAcc[dataElm.label] = viewData[dataElm.label];
242           } else if (viewData[dataElm.id] !== undefined) {
243             dataAcc[dataElm.id] = viewData[dataElm.id];
244           }
245           return dataAcc;
246         }, {} as { [name: string]: any });
247
248         acc[elm.id] = {
249           selectedCase,
250           data,
251         };
252       }
253       return acc;
254     }, {} as { [path: string]: { selectedCase: string, data: { [property: string]: any } } }) || {}
255   }
256
257   static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps }) {
258
259     if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
260       const isNew: boolean = nextProps.vPath?.endsWith("[]") || false;
261       const state = {
262         ...prevState,
263         isNew: isNew,
264         editMode: isNew,
265         viewData: nextProps.viewData || null,
266         [OldProps]: nextProps,
267         choises: nextProps.displaySpecification.displayMode === DisplayModeType.doNotDisplay
268           ? null
269           : nextProps.displaySpecification.displayMode === DisplayModeType.displayAsRPC
270             ? nextProps.displaySpecification.inputViewSpecification && ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.inputViewSpecification.elements, nextProps.viewData) || []
271             : ConfigurationApplicationComponent.getChoisesFromElements(nextProps.displaySpecification.viewSpecification.elements, nextProps.viewData)
272       }
273       return state;
274     }
275     return null;
276   }
277
278   private navigate = (path: string) => {
279     this.props.history.push(`${this.props.match.url}${path}`);
280   }
281
282   private changeValueFor = (property: string, value: any) => {
283     this.setState({
284       viewData: {
285         ...this.state.viewData,
286         [property]: value
287       }
288     });
289   }
290
291   private collectData = (elements: { [name: string]: ViewElement }) => {
292     // ensure only active choises will be contained
293     const viewData: { [key: string]: any } = { ...this.state.viewData };
294     const choiseKeys = Object.keys(elements).filter(elmKey => isViewElementChoise(elements[elmKey]));
295     const elementsToRemove = choiseKeys.reduce((acc, curChoiceKey) => {
296       const currentChoice = elements[curChoiceKey] as ViewElementChoise;
297       const selectedCase = this.state.choises[curChoiceKey].selectedCase;
298       Object.keys(currentChoice.cases).forEach(caseKey => {
299         const caseElements = currentChoice.cases[caseKey].elements;
300         if (caseKey === selectedCase) {
301           Object.keys(caseElements).forEach(caseElementKey => {
302             const elm = caseElements[caseElementKey];
303             if (isViewElementEmpty(elm)) {
304               // insert null for all empty elements
305               viewData[elm.id] = null;
306             }
307           });
308           return;
309         };
310         Object.keys(caseElements).forEach(caseElementKey => {
311           acc.push(caseElements[caseElementKey]);
312         });
313       });
314       return acc;
315     }, [] as ViewElement[]);
316
317     return viewData && Object.keys(viewData).reduce((acc, cur) => {
318       if (!elementsToRemove.some(elm => elm.label === cur || elm.id === cur)) {
319         acc[cur] = viewData[cur];
320       }
321       return acc;
322     }, {} as { [key: string]: any });
323   }
324
325   private isPolicyViewElementForbidden = (element: ViewElement, dataPath: string): boolean => {
326     const policy = getAccessPolicyByUrl(`${dataPath}/${element.id}`);
327     return !(policy.GET && policy.POST);
328   }
329
330   private isPolicyModuleForbidden = (moduleName: string, dataPath: string): boolean => {
331     const policy = getAccessPolicyByUrl(`${dataPath}/${moduleName}`);
332     return !(policy.GET && policy.POST);
333   }
334
335   private getEditorForViewElement = (uiElement: ViewElement): (null | React.ComponentType<BaseProps<any>>) => {
336     if (isViewElementEmpty(uiElement)) {
337       return null;
338     } else if (isViewElementSelection(uiElement)) {
339       return UiElementSelection;
340     } else if (isViewElementBoolean(uiElement)) {
341       return UiElementBoolean;
342     } else if (isViewElementString(uiElement)) {
343       return UiElementString;
344     } else if (isViewElementDate(uiElement)) {
345       return UiElementString;
346     } else if (isViewElementNumber(uiElement)) {
347       return UiElementNumber;
348     } else if (isViewElementUnion(uiElement)) {
349       return UIElementUnion;
350     } else {
351       if (process.env.NODE_ENV !== "production") {
352         console.error(`Unknown element type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
353       }
354       return null;
355     }
356   }
357
358   private renderUIElement = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
359     const isKey = (uiElement.label === keyProperty);
360     const canEdit = editMode && (isNew || (uiElement.config && !isKey));
361
362     // do not show elements w/o any value from the backend
363     if (viewData[uiElement.id] == null && !editMode) {
364       return null;
365     } else if (isViewElementEmpty(uiElement)) {
366       return null;
367     } else if (uiElement.isList) {
368       /* element is a leaf-list */
369       return <UiElementLeafList
370         key={uiElement.id}
371         inputValue={viewData[uiElement.id] == null ? [] : viewData[uiElement.id]}
372         value={uiElement}
373         readOnly={!canEdit}
374         disabled={editMode && !canEdit}
375         onChange={(e) => { this.changeValueFor(uiElement.id, e) }}
376         getEditorForViewElement={this.getEditorForViewElement}
377       />;
378     } else {
379       const Element = this.getEditorForViewElement(uiElement);
380       return Element != null
381         ? (
382           <Element
383             key={uiElement.id}
384             isKey={isKey}
385             inputValue={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
386             value={uiElement}
387             readOnly={!canEdit}
388             disabled={editMode && !canEdit}
389             onChange={(e) => { this.changeValueFor(uiElement.id, e) }}
390           />)
391         : null;
392     }
393   };
394
395   // private renderUIReference = (uiElement: ViewElement, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
396   //   const isKey = (uiElement.label === keyProperty);
397   //   const canEdit = editMode && (isNew || (uiElement.config && !isKey));
398   //   if (isViewElementObjectOrList(uiElement)) {
399   //     return (
400   //       <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
401   //         <Tooltip title={uiElement.description || ''}>
402   //           <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => {
403   //             this.navigate(`/${uiElement.id}`);
404   //           }}>{uiElement.label}</Button>
405   //         </Tooltip>
406   //       </FormControl>
407   //     );
408   //   } else {
409   //     if (process.env.NODE_ENV !== "production") {
410   //       console.error(`Unknown reference type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
411   //     }
412   //     return null;
413   //   }
414   // };
415
416   private renderUIChoise = (uiElement: ViewElementChoise, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
417     const isKey = (uiElement.label === keyProperty);
418
419     const currentChoise = this.state.choises[uiElement.id];
420     const currentCase = currentChoise && uiElement.cases[currentChoise.selectedCase];
421
422     const canEdit = editMode && (isNew || (uiElement.config && !isKey));
423     if (isViewElementChoise(uiElement)) {
424       const subElements = currentCase?.elements;
425       return (
426         <>
427           <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
428             <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
429             <Select
430               aria-label={uiElement.label + '-selection'}
431               required={!!uiElement.mandatory}
432               onChange={(e) => {
433                 if (currentChoise.selectedCase === e.target.value) {
434                   return; // nothing changed
435                 }
436                 this.setState({ choises: { ...this.state.choises, [uiElement.id]: { ...this.state.choises[uiElement.id], selectedCase: e.target.value as string } } });
437               }}
438               readOnly={!canEdit}
439               disabled={editMode && !canEdit}
440               value={this.state.choises[uiElement.id].selectedCase}
441               inputProps={{
442                 name: uiElement.id,
443                 id: `select-${uiElement.id}`,
444               }}
445             >
446               {
447                 Object.keys(uiElement.cases).map(caseKey => {
448                   const caseElm = uiElement.cases[caseKey];
449                   return (
450                     <MenuItem key={caseElm.id} value={caseKey} aria-label={caseKey}><Tooltip title={caseElm.description || ''}><div style={{ width: "100%" }}>{caseElm.label}</div></Tooltip></MenuItem>
451                   );
452                 })
453               }
454             </Select>
455           </FormControl>
456           {subElements
457             ? Object.keys(subElements).map(elmKey => {
458               const elm = subElements[elmKey];
459               return this.renderUIElement(elm, viewData, keyProperty, editMode, isNew);
460             })
461             : <h3>Invalid Choise</h3>
462           }
463         </>
464       );
465     } else {
466       if (process.env.NODE_ENV !== "production") {
467         console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
468       }
469       return null;
470     }
471   };
472
473   private renderUIView = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
474     const { classes } = this.props;
475
476
477
478     const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
479       if (keyProperty) {
480         // if (vsA.label === vsB.label) return 0;
481         if (vsA.label === keyProperty) return -1;
482         if (vsB.label === keyProperty) return +1;
483       }
484
485       // if (vsA.uiType === vsB.uiType) return 0;
486       // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
487       // if (vsA.uiType === "object") return +1;
488       return -1;
489     };
490
491     const sections = Object.keys(viewSpecification.elements).reduce((acc, cur) => {
492       const elm = viewSpecification.elements[cur];
493       if (isViewElementObjectOrList(elm)) {
494         acc.references.push(elm);
495       } else if (isViewElementChoise(elm)) {
496         acc.choises.push(elm);
497       } else if (isViewElementRpc(elm)) {
498         acc.rpcs.push(elm);
499       } else {
500         acc.elements.push(elm);
501       }
502       return acc;
503     }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] });
504
505     sections.elements = sections.elements.sort(orderFunc);
506
507     return (
508       <div className={classes.uiView}>
509         <div className={classes.section} />
510         {sections.elements.length > 0
511           ? (
512             <div className={classes.section}>
513               {sections.elements.map(element => this.renderUIElement(element, viewData, keyProperty, editMode, isNew))}
514             </div>
515           ) : null
516         }
517         {sections.references.length > 0
518           ? (
519             <div className={classes.section}>
520               {sections.references.map(element => (
521                 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} />
522               ))}
523             </div>
524           ) : null
525         }
526         {sections.choises.length > 0
527           ? (
528             <div className={classes.section}>
529               {sections.choises.map(element => this.renderUIChoise(element, viewData, keyProperty, editMode, isNew))}
530             </div>
531           ) : null
532         }
533         {sections.rpcs.length > 0
534           ? (
535             <div className={classes.section}>
536               {sections.rpcs.map(element => (
537                 <UIElementReference key={element.id} element={element} disabled={editMode || this.isPolicyViewElementForbidden(element, dataPath)} onOpenReference={(elm) => { this.navigate(`/${elm.id}`) }} />
538               ))}
539             </div>
540           ) : null
541         }
542       </div>
543     );
544   };
545
546   private renderUIViewSelector = (viewSpecification: ViewSpecification, dataPath: string, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
547     const { classes } = this.props;
548     // group by module name
549     const modules = Object.keys(viewSpecification.elements).reduce<{ [key: string]: ViewSpecification }>((acc, cur) => {
550       const elm = viewSpecification.elements[cur];
551       const moduleView = (acc[elm.module] = acc[elm.module] || { ...viewSpecification, elements: {} });
552       moduleView.elements[cur] = elm;
553       return acc;
554     }, {});
555
556     const moduleKeys = Object.keys(modules).sort();
557
558     return (
559       <div className={classes.moduleCollection}>
560         {
561           moduleKeys.map(key => {
562             const moduleView = modules[key];
563             return (
564               <Accordion key={key} defaultExpanded={moduleKeys.length < 4} aria-label={key + '-panel'} >
565                 <AccordionSummaryExt expandIcon={<ExpandMoreIcon />} aria-controls={`content-${key}`} id={`header-${key}`} disabled={this.isPolicyModuleForbidden(`${key}:`, dataPath)} >
566                   <Typography className={classes.heading}>{key}</Typography>
567                 </AccordionSummaryExt>
568                 <AccordionDetails>
569                   {this.renderUIView(moduleView, dataPath, viewData, keyProperty, editMode, isNew)}
570                 </AccordionDetails>
571               </Accordion>
572             );
573           })
574         }
575       </div>
576     );
577   };
578
579   private renderUIViewList(listSpecification: ViewSpecification, dataPath: string, listKeyProperty: string, apiDocPath: string, listData: { [key: string]: any }[]) {
580     const listElements = listSpecification.elements;
581     const apiDocPathCreate = apiDocPath ? `${location.origin}${apiDocPath
582       .replace("$$$standard$$$", "topology-netconfnode%20resources%20-%20RestConf%20RFC%208040")
583       .replace("$$$action$$$", "put")}_${listKeyProperty.replace(/[\/=\-\:]/g, '_')}_` : undefined;
584
585     const navigate = (path: string) => {
586       this.props.history.push(`${this.props.match.url}${path}`);
587     };
588
589     const addNewElementAction = {
590       icon: AddIcon, 
591       tooltip: 'Add',
592       ariaLabel:'add-element',
593       onClick: () => {
594         navigate("[]"); // empty key means new element
595       },
596       disabled: !listSpecification.config,
597     };
598
599     const addWithApiDocElementAction = {
600       icon: PostAdd, 
601       tooltip: 'Add',
602       ariaLabel:'add-element-via-api-doc',
603       onClick: () => {
604         window.open(apiDocPathCreate, '_blank');
605       },
606       disabled: !listSpecification.config,
607     };
608
609     const { classes, removeElement } = this.props;
610
611     const DeleteIconWithConfirmation: React.FC<{disabled?: boolean, rowData: { [key: string]: any }, onReload: () => void }> = (props) => {
612       const confirm = useConfirm();
613
614       return (
615         <Tooltip title={"Remove"} >
616           <IconButton disabled={props.disabled} className={classes.button} aria-label="remove-element-button"
617             onClick={async (e) => {
618               e.stopPropagation();
619               e.preventDefault();
620               confirm({ title: "Do you really want to delete this element ?", description: "This action is permanent!", confirmationButtonProps: { color: "secondary" } })
621                 .then(() => removeElement(`${this.props.vPath}[${props.rowData[listKeyProperty]}]`))
622                 .then(props.onReload);
623             }} >
624             <RemoveIcon />
625           </IconButton>
626         </Tooltip>
627       );
628     }
629
630     return (
631       <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} customActionButtons={apiDocPathCreate ? [addNewElementAction, addWithApiDocElementAction] : [addNewElementAction]} columns={
632         Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
633           const elm = listElements[cur];
634           if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
635             if (elm.label !== listKeyProperty) {
636               acc.push(elm.uiType === "boolean"
637                 ? { property: elm.label, type: ColumnType.boolean }
638                 : elm.uiType === "date"
639                   ? { property: elm.label, type: ColumnType.date }
640                   : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
641             } else {
642               acc.unshift(elm.uiType === "boolean"
643                 ? { property: elm.label, type: ColumnType.boolean }
644                 : elm.uiType === "date"
645                   ? { property: elm.label, type: ColumnType.date }
646                   : { property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
647             }
648           }
649           return acc;
650         }, []).concat([{
651           property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (({ rowData }) => {
652             return (
653               <DeleteIconWithConfirmation disabled={!listSpecification.config} rowData={rowData} onReload={() => this.props.vPath && this.props.reloadView(this.props.vPath)} />
654             );
655           })
656         }])
657       } onHandleClick={(ev, row) => {
658         ev.preventDefault();
659         navigate(`[${encodeURIComponent(row[listKeyProperty])}]`);
660       }} ></SelectElementTable>
661     );
662   }
663
664   private renderUIViewRPC(inputViewSpecification: ViewSpecification | undefined, dataPath: string, inputViewData: { [key: string]: any }, outputViewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) {
665     const { classes } = this.props;
666
667     const orderFunc = (vsA: ViewElement, vsB: ViewElement) => {
668       if (keyProperty) {
669         // if (vsA.label === vsB.label) return 0;
670         if (vsA.label === keyProperty) return -1;
671         if (vsB.label === keyProperty) return +1;
672       }
673
674       // if (vsA.uiType === vsB.uiType) return 0;
675       // if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
676       // if (vsA.uiType === "object") return +1;
677       return -1;
678     };
679
680     const sections = inputViewSpecification && Object.keys(inputViewSpecification.elements).reduce((acc, cur) => {
681       const elm = inputViewSpecification.elements[cur];
682       if (isViewElementObjectOrList(elm)) {
683         console.error("Object should not appear in RPC view !");
684       } else if (isViewElementChoise(elm)) {
685         acc.choises.push(elm);
686       } else if (isViewElementRpc(elm)) {
687         console.error("RPC should not appear in RPC view !");
688       } else {
689         acc.elements.push(elm);
690       }
691       return acc;
692     }, { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] })
693       || { elements: [] as ViewElement[], references: [] as ViewElement[], choises: [] as ViewElementChoise[], rpcs: [] as ViewElementRpc[] };
694
695     sections.elements = sections.elements.sort(orderFunc);
696
697     return (
698       <>
699         <div className={classes.section} />
700         { sections.elements.length > 0
701           ? (
702             <div className={classes.section}>
703               {sections.elements.map(element => this.renderUIElement(element, inputViewData, keyProperty, editMode, isNew))}
704             </div>
705           ) : null
706         }
707         { sections.choises.length > 0
708           ? (
709             <div className={classes.section}>
710               {sections.choises.map(element => this.renderUIChoise(element, inputViewData, keyProperty, editMode, isNew))}
711             </div>
712           ) : null
713         }
714         <Button onClick={() => {
715           const resultingViewData = inputViewSpecification && this.collectData(inputViewSpecification.elements);
716           this.props.executeRpc(this.props.vPath!, resultingViewData);
717         }} >Exec</Button>
718         <div className={classes.objectReult}>
719           {outputViewData !== undefined
720             ? renderObject(outputViewData)
721             : null
722           }
723         </div>
724       </>
725     );
726   };
727
728   private renderBreadCrumps() {
729     const { editMode } = this.state;
730     const { displaySpecification, vPath, nodeId } = this.props;
731     const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
732     let lastPath = `/configuration`;
733     let basePath = `/configuration/${nodeId}`;
734     return (
735       <div className={this.props.classes.header}>
736         <div>
737           <Breadcrumbs aria-label="breadcrumbs">
738             <Link color="inherit" href="#" aria-label="back-breadcrumb"
739               onClick={(ev: React.MouseEvent<HTMLElement>) => {
740                 ev.preventDefault();
741                 this.props.history.push(lastPath);
742               }}>Back</Link>
743             <Link color="inherit" href="#"
744               aria-label={nodeId + '-breadcrumb'}
745               onClick={(ev: React.MouseEvent<HTMLElement>) => {
746                 ev.preventDefault();
747                 this.props.history.push(`/configuration/${nodeId}`);
748               }}><span>{nodeId}</span></Link>
749             {
750               pathParts.map(([prop, key], ind) => {
751                 const path = `${basePath}/${prop}`;
752                 const keyPath = key && `${basePath}/${prop}[${key}]`;
753                 const propTitle = prop.replace(/^[^:]+:/, "");
754                 const ret = (
755                   <span key={ind}>
756                     <Link color="inherit" href="#"
757                       aria-label={propTitle + '-breadcrumb'}
758                       onClick={(ev: React.MouseEvent<HTMLElement>) => {
759                         ev.preventDefault();
760                         this.props.history.push(path);
761                       }}><span>{propTitle}</span></Link>
762                     {
763                       keyPath && <Link color="inherit" href="#"
764                         aria-label={key + '-breadcrumb'}
765                         onClick={(ev: React.MouseEvent<HTMLElement>) => {
766                           ev.preventDefault();
767                           this.props.history.push(keyPath);
768                         }}>{`[${key}]`}</Link> || null
769                     }
770                   </span>
771                 );
772                 lastPath = basePath;
773                 basePath = keyPath || path;
774                 return ret;
775               })
776             }
777           </Breadcrumbs>
778         </div>
779         {this.state.editMode && (
780           <Fab color="secondary" aria-label="back-button" className={this.props.classes.fab} onClick={async () => {
781             this.props.vPath && await this.props.reloadView(this.props.vPath);
782             this.setState({ editMode: false });
783           }} ><ArrowBack /></Fab>
784         ) || null}
785         { /* do not show edit if this is a list or it can't be edited */
786           displaySpecification.displayMode === DisplayModeType.displayAsObject && displaySpecification.viewSpecification.canEdit && (<div>
787             <Fab color="secondary" aria-label={editMode ? 'save-button' : 'edit-button'} className={this.props.classes.fab} onClick={() => {
788               if (this.state.editMode) {
789                 // ensure only active choises will be contained
790                 const resultingViewData = this.collectData(displaySpecification.viewSpecification.elements);
791                 this.props.onUpdateData(this.props.vPath!, resultingViewData);
792               }
793               this.setState({ editMode: !editMode });
794             }}>
795               {editMode
796                 ? <SaveIcon />
797                 : <EditIcon />
798               }
799             </Fab>
800           </div> || null)
801         }
802       </div>
803     );
804   }
805
806   private renderValueSelector() {
807     const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
808     if (!listKeyProperty || !listSpecification) {
809       throw new Error("ListKex ot view not specified.");
810     }
811
812     return (
813       <div className={this.props.classes.container}>
814         <SelectElementTable stickyHeader idProperty={listKeyProperty} rows={listData} columns={
815           Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
816             const elm = listSpecification.elements[cur];
817             if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
818               if (elm.label !== listKeyProperty) {
819                 acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
820               } else {
821                 acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
822               }
823             }
824             return acc;
825           }, [])
826         } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
827       </div>
828     );
829   }
830
831   private renderValueEditor() {
832     const { displaySpecification: ds, outputData } = this.props;
833     const { viewData, editMode, isNew } = this.state;
834
835     return (
836       <div className={this.props.classes.container}>
837         {this.renderBreadCrumps()}
838         {ds.displayMode === DisplayModeType.doNotDisplay
839           ? null
840           : ds.displayMode === DisplayModeType.displayAsList && viewData instanceof Array
841             ? this.renderUIViewList(ds.viewSpecification, ds.dataPath!, ds.keyProperty!, ds.apidocPath!, viewData)
842             : ds.displayMode === DisplayModeType.displayAsRPC
843               ? this.renderUIViewRPC(ds.inputViewSpecification, ds.dataPath!, viewData!, outputData, undefined, true, false)
844               : this.renderUIViewSelector(ds.viewSpecification, ds.dataPath!, viewData!, ds.keyProperty, editMode, isNew)
845         }
846       </div >
847     );
848   }
849
850   private renderCollectingData() {
851     return (
852       <div className={this.props.classes.outer}>
853         <div className={this.props.classes.inner}>
854           <Loader />
855           <h3>Processing ...</h3>
856         </div>
857       </div>
858     );
859   }
860
861   render() {
862     return this.props.collectingData || !this.state.viewData
863       ? this.renderCollectingData()
864       : this.props.listSpecification
865         ? this.renderValueSelector()
866         : this.renderValueEditor();
867   }
868 }
869
870 export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
871 export default ConfigurationApplication;