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