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