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