Merge "YANG Model update for A1 Adapter"
[ccsdk/features.git] / sdnr / wt / odlux / apps / configurationApp / src / views / configurationApplication.tsx
index 65fbd70..24a4af8 100644 (file)
+/**
+ * ============LICENSE_START========================================================================
+ * ONAP : ccsdk feature sdnr wt odlux
+ * =================================================================================================
+ * Copyright (C) 2019 highstreet technologies GmbH Intellectual Property. All rights reserved.
+ * =================================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ * ============LICENSE_END==========================================================================
+ */
+
 import * as React from 'react';
+import { RouteComponentProps, withRouter } from 'react-router-dom';
+
+import { WithStyles, withStyles, createStyles, Theme } from '@material-ui/core/styles';
 
-import { MaterialTable, MaterialTableCtorType } from '../../../../framework/src/components/material-table';
-import connect, { Connect, IDispatcher } from '../../../../framework/src/flux/connect';
-import { IApplicationStoreState } from '../../../../framework/src/store/applicationStore';
-import { IConnectAppStoreState } from '../../../connectApp/src/handlers/connectAppRootHandler';
-import { MountedNetworkElementType } from '../../../connectApp/src/models/mountedNetworkElements';
-import { NavigateToApplication } from '../../../../framework/src/actions/navigationActions';
-import { Dispatch } from '../../../../framework/src/flux/store';
+import connect, { IDispatcher, Connect } from "../../../../framework/src/flux/connect";
+import { IApplicationStoreState } from "../../../../framework/src/store/applicationStore";
+import MaterialTable, { ColumnModel, ColumnType, MaterialTableCtorType } from "../../../../framework/src/components/material-table";
+import { Loader } from "../../../../framework/src/components/material-ui/loader";
 
-import TextField from '@material-ui/core/TextField';
-import { Tooltip, Button, FormControl, InputLabel, Select, MenuItem, InputAdornment } from '@material-ui/core';
-import Link from '@material-ui/core/Link';
+import { SetSelectedValue, splitVPath, updateDataActionAsyncCreator } from "../actions/deviceActions";
+import { ViewSpecification, isViewElementString, isViewElementNumber, isViewElementBoolean, isViewElementObjectOrList, isViewElementSelection } from "../models/uiModels";
 
-import Table from '@material-ui/core/Table';
-import TableBody from '@material-ui/core/TableBody';
-import TableCell from '@material-ui/core/TableCell';
-import TableHead from '@material-ui/core/TableHead';
-import TableRow from '@material-ui/core/TableRow';
+import Fab from '@material-ui/core/Fab';
+import AddIcon from '@material-ui/icons/Add';
+import RemoveIcon from '@material-ui/icons/RemoveCircleOutline';
+import SaveIcon from '@material-ui/icons/Save';
+import EditIcon from '@material-ui/icons/Edit';
+import Tooltip from "@material-ui/core/Tooltip";
+import TextField from "@material-ui/core/TextField";
+import FormControl from "@material-ui/core/FormControl";
+import IconButton from "@material-ui/core/IconButton";
+import Button from "@material-ui/core/Button";
+import InputAdornment from "@material-ui/core/InputAdornment";
+import InputLabel from "@material-ui/core/InputLabel";
+import Select from "@material-ui/core/Select";
+import MenuItem from "@material-ui/core/MenuItem";
+import Breadcrumbs from "@material-ui/core/Breadcrumbs";
+import Link from "@material-ui/core/Link";
+import FormHelperText from '@material-ui/core/FormHelperText';
 
-import { ViewSpecification } from '../models/uiModels';
+const styles = (theme: Theme) => createStyles({
+  header: {
+    "display": "flex",
+    "justifyContent": "space-between",
+  },
+  leftButton: {
+    "justifyContent": "left"
+  },
+  outer: {
+    "flex": "1",
+    "heigh": "100%",
+    "display": "flex",
+    "alignItems": "center",
+    "justifyContent": "center",
+  },
+  inner: {
 
-const NetworkElementTable = MaterialTable as MaterialTableCtorType<MountedNetworkElementType>;
+  },
+  "icon": {
+    "marginRight": theme.spacing(0.5),
+    "width": 20,
+    "height": 20,
+  },
+  "fab": {
+    "margin": theme.spacing(1),
+  },
+  button: {
+    margin: 0,
+    padding: "6px 6px",
+    minWidth: 'unset'
+  },
+  readOnly: {
+    '& label.Mui-focused': {
+      color: 'green',
+    },
+    '& .MuiInput-underline:after': {
+      borderBottomColor: 'green',
+    },
+    '& .MuiOutlinedInput-root': {
+      '& fieldset': {
+        borderColor: 'red',
+      },
+      '&:hover fieldset': {
+        borderColor: 'yellow',
+      },
+      '&.Mui-focused fieldset': {
+        borderColor: 'green',
+      },
+    },
+  },
+});
 
 const mapProps = (state: IApplicationStoreState) => ({
-  ...state.configuration,
-  avaliableDevices: state.connect.mountedNetworkElements.elements.filter(el => el.connectionStatus === "connected")
+  collectingData: state.configuration.valueSelector.collectingData,
+  listKeyProperty: state.configuration.valueSelector.keyProperty,
+  listSpecification: state.configuration.valueSelector.listSpecification,
+  listData: state.configuration.valueSelector.listData,
+  vPath: state.configuration.viewDescription.vPath,
+  nodeId: state.configuration.deviceDescription.nodeId,
+  viewData: state.configuration.viewDescription.viewData,
+  viewSpecification: state.configuration.viewDescription.viewSpecification,
+  displayAsList: state.configuration.viewDescription.displayAsList,
+  keyProperty: state.configuration.viewDescription.keyProperty,
 });
-const mapDisp = (dispatcher: IDispatcher) => ({
-  navigateTo: (viewId: string, index?: string | number) => dispatcher.dispatch((dispatch: Dispatch, getState: () => IApplicationStoreState) => {
-    const { configuration: { nodeId, lpId, indexValues } } = getState();
-    const newIndexValues = typeof index === 'number' && indexValues
-      ? indexValues.split('/').slice(0, index).join("/")
-      : indexValues
-        ? `${indexValues}${index ? `/${index}` : ''}`
-        : index;
-    dispatch(new NavigateToApplication("configuration", `${nodeId}/${lpId}/${viewId}${newIndexValues ? `/${newIndexValues}` : ''}`));
-
-  }),
-  changeNode: (ndoeId: string) => dispatcher.dispatch((dispatch: Dispatch) => {
-    dispatch(new NavigateToApplication("configuration", ndoeId));
-  }),
-  changeLp: (lpId: string) => dispatcher.dispatch((dispatch: Dispatch, getState: () => IApplicationStoreState) => {
-    const { configuration: { nodeId } } = getState();
-    dispatch(new NavigateToApplication("configuration", `${nodeId}/${lpId}`));
-  })
+
+const mapDispatch = (dispatcher: IDispatcher) => ({
+  onValueSelected: (value: any) => dispatcher.dispatch(new SetSelectedValue(value)),
+  onUpdateData: (vPath: string, data: any) => dispatcher.dispatch(updateDataActionAsyncCreator(vPath, data)),
 });
 
-type ConfigurationApplicationProps = Connect<typeof mapProps, typeof mapDisp>;
+const SelectElementTable = MaterialTable as MaterialTableCtorType<{ [key: string]: any }>;
 
-type ConfigurationApplicationState = {
+type ConfigurationApplicationComponentProps = RouteComponentProps & Connect<typeof mapProps, typeof mapDispatch> & WithStyles<typeof styles>;
 
+type ConfigurationApplicationComponentState = {
+  isNew: boolean;
+  editMode: boolean;
+  canEdit: boolean;
+  viewData: { [key: string]: any } | null;
 }
 
-class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationProps, ConfigurationApplicationState> {
-
- render() {
-    if (this.props.loading) {
-      return (
-        <h2>Collecting data from network element. Please wait ...</h2>
-      );
-    } else if (!this.props.nodeId) {
-      return (
-        <>
-          <h2>Please select an network element to configure !</h2>
-          <NetworkElementTable idProperty={"mountId"} rows={this.props.avaliableDevices} asynchronus
-            onHandleClick={(evetm, rowData) => { this.props.changeNode(rowData.mountId) }} columns={
-            [{ property:"mountId" }]
-          } />
-        </>
-      );
-    } else if (!this.props.lpId) {
-      return this.props.coreModel && this.props.coreModel.ltp && this.props.coreModel.ltp.length
-       ? (
-        <>
-          <h2>Please select an existing LP first !</h2>
-          <ul>
-            { this.props.coreModel.ltp.map(ltp => {
-              return <li key={ltp.uuid}>
-                <Link component="a" variant="body2" color="secondary" onClick={() => {
-                  this.props.changeLp(ltp.lp[0].uuid);
-                }}>{ltp.lp[0].label[0].value}</Link>
-              </li>
-            }) || null}
-          </ul>
-        </>
-      ) 
-      : (
-        <>
-         <h2>No LTP / LP found !</h2>
-        </>
-      );
-    } else if (!this.props.capability && !this.props.viewId) {
-      return (
-        <h2>Please select a capability or viewId first !</h2>
-      );
+const OldProps = Symbol("OldProps");
+class ConfigurationApplicationComponent extends React.Component<ConfigurationApplicationComponentProps, ConfigurationApplicationComponentState> {
+
+  /**
+   *
+   */
+  constructor (props: ConfigurationApplicationComponentProps) {
+    super(props);
+
+    this.state = {
+      isNew: false,
+      canEdit: false,
+      editMode: false,
+      viewData: null
     }
-    const viewData = this.props.viewData;
-    const viewSpecification = this.props.viewId
-      ? this.props.viewSpecifications.find(d => d.id === this.props.viewId)
-      : this.props.viewSpecifications.find(d => d.name === this.props.conditionalPackage);
-
-    return viewSpecification
-      ? (
-        <>
-          <hgroup style={{ marginBottom: 15 }}>
-            <h2>{`${this.props.nodeId} - ${this.props.lpId}`}</h2>
-            {this.createBreadCrump(viewSpecification.id, this.props.viewSpecifications)}
-          </hgroup>
-          <div style={{ display: "flex", flexWrap: "wrap", overflow: "auto" }}>
-            {
+  }
 
-            (this.props.viewData && this.props.viewData instanceof Array)
-              ? this.renderUIList(viewSpecification, viewData as { [key: string]: string | number }[])
-              : this.renderUIElement(viewSpecification, viewData as { [key: string]: string | number })
-          }
-            {/* { <pre>{JSON.stringify(this.props.viewData, null, 2)} </pre> } */}
+  static getDerivedStateFromProps(nextProps: ConfigurationApplicationComponentProps, prevState: ConfigurationApplicationComponentState & { [OldProps]: ConfigurationApplicationComponentProps  }) {
 
-        </div>
-        </>
-      )
-      : <h2>View [{this.props.viewId || this.props.conditionalPackage}] Not Found ! {this.props.viewSpecifications.length} </h2>;
+    if (!prevState || !prevState[OldProps] || (prevState[OldProps].viewData !== nextProps.viewData)) {
+      const isNew: boolean = nextProps.vPath?.endsWith("[]") || false;
+      const state = {
+        ...prevState,
+        isNew: isNew,
+        editMode: isNew,
+        viewData: nextProps.viewData || null,
+        [OldProps]: nextProps,
+      }
+      return state;
+    }
+    return null;
   }
 
-  private static keyPropertyParser = /\$\$INDEX:(\d+):?([a-z\-]+)?\$\$$/;
-  private renderUIList = (viewSpecification: ViewSpecification, viewData: { [key: string]: string | number }[]) => {
-    const keyMatch = ConfigurationApplicationComponent.keyPropertyParser.exec(viewSpecification.dataPath);
-    const keyProperty = keyMatch && keyMatch[2];
-    return (
-      <Table>
-        <TableHead>
-          <TableRow>
-            {viewSpecification.elements.map(uiElement => {
-              switch (uiElement.uiType) {
-                case "number":
-                return (
-                  <TableCell key={uiElement.id} align={"right"} >{uiElement.label}</TableCell>
-                );
-                case "selection":
-                case "object":
-                case "list":
-                case "string":
-                case "boolean":
-                  return (
-                    <TableCell key={uiElement.id} align={"left"} >{uiElement.label}</TableCell>
-                  );
-                default:
-                  if (process.env.NODE_ENV !== "production") {
-                    console.error(`Unknown column type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
-                  }
-                  return null;
-              }
-            })
-            }
-            <TableCell align={"right"} >Actions</TableCell>
-          </TableRow>
-        </TableHead>
-        <TableBody>
-          {viewData.map((row, ind) => (
-            <TableRow key={keyProperty && row[keyProperty] || ind}>
-              {viewSpecification.elements.map(uiElement => {
-                switch (uiElement.uiType) {
-                  case "string":
-                  case "number":
-                    return (
-                      <TableCell key={uiElement.id} component="td" scope="row" align={uiElement.uiType === "number" ? "right" : "left"}>{row[uiElement.id] == null ? "---" : row[uiElement.id] } </TableCell>
-                    );
-                  case "boolean":
-                    return (
-                      <TableCell key={uiElement.id} component="td" scope="row" align={"left"} >{row[uiElement.id] == null ? "---" : row[uiElement.id] ? uiElement.trueValue || 'True' : uiElement.falseValue || 'False'} </TableCell>
-                    );
-                  case "list":
-                  case "object":
-                    return (
-                      <TableCell key={uiElement.id} component="td" scope="row" align={"left"} >
-                        <Tooltip title={uiElement.description || ''}>
-                          <Link component="a" variant="body2" color="secondary" onClick={() => {
-                            this.props.navigateTo(uiElement.viewId, String(ind));
-                          }}>{uiElement.label}</Link>
-                        </Tooltip></TableCell>
-                    );
-                  case "selection":
-                    const option = row[uiElement.id] ? uiElement.options.find(opt => opt.key === row[uiElement.id]) : null;
-                    return (
-                      <TableCell key={uiElement.id} component="td" scope="row" align={"left"} >{option ? option.value : row[uiElement.id] == null ? "---" : row[uiElement.id]  } </TableCell>
-                    );
-                  default:
-                    if (process.env.NODE_ENV !== "production") {
-                      console.error(`Unknown column type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
-                    }
-                    return null;
-                }
-              })}
-              <TableCell align={"right"} ><Button onClick={() => {
-                this.props.navigateTo(this.props.viewId || '', String(/*keyProperty && row[keyProperty] || */ ind));
-              }} >Details</Button>
-              </TableCell>
-            </TableRow>
-          ))}
-        </TableBody>
-      </Table>
-    );
+  private navigate = (path: string) => {
+    this.props.history.push(`${this.props.match.url}${path}`);
   }
 
-  private renderUIElement = (viewSpecification: ViewSpecification, viewData: { [key: string]: string | number }) => (
-    viewSpecification.elements.map(uiElement => {
-      if (uiElement.leafrefPath) {
-        return null;
+  private changeValueFor = (property: string, value: any) => {
+    this.setState({
+      viewData: {
+        ...this.state.viewData,
+        [property]: value
       }
-      switch (uiElement.uiType) {
-        case "selection":
-          return (viewData[uiElement.id] != null
+    });
+  }
+
+  private renderUIElement = (viewSpecification: ViewSpecification, viewData: { [key: string]: any }, keyProperty: string | undefined, editMode: boolean, isNew: boolean) => {
+    const elements = viewSpecification.elements;
+    return (
+      Object.keys(elements).sort((a, b) => {
+        const vsA = elements[a];
+        const vsB = elements[b];
+        if (keyProperty) {
+          // if (vsA.label === vsB.label) return 0;
+          if (vsA.label === keyProperty) return -1;
+          if (vsB.label === keyProperty) return +1;
+        }
+
+        if (vsA.uiType === vsB.uiType) return 0;
+        if (vsA.uiType !== "object" && vsB.uiType !== "object") return 0;
+        if (vsA.uiType === "object") return +1;
+        return -1;
+      }).map(key => {
+        const uiElement = elements[key];
+        const isKey = (uiElement.label === keyProperty);
+        const canEdit = editMode && (isNew || (uiElement.config && !isKey));
+        if (isViewElementSelection(uiElement)) {
+          let error = ""
+          const value = String(viewData[uiElement.id]).toLowerCase();
+          if (uiElement.mandatory && !!value) {
+            error = "Error";
+          }
+          return (canEdit || viewData[uiElement.id] != null
             ? (<FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
               <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
               <Select
-                readOnly={ true }
+                required={!!uiElement.mandatory}
+                error={!!error}
+                onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+                readOnly={!canEdit}
+                disabled={editMode && !canEdit}
                 value={(viewData[uiElement.id] || '').toString().toLowerCase()}
                 inputProps={{
                   name: uiElement.id,
                   id: `select-${uiElement.id}`,
                 }}
               >
-                {uiElement.options.map(option => (<MenuItem title={option.description} value={option.value}>{option.key}</MenuItem>))}
+                {uiElement.options.map(option => (<MenuItem key={option.key} title={option.description} value={option.value}>{option.key}</MenuItem>))}
               </Select>
+              <FormHelperText>{error}</FormHelperText>
             </FormControl>)
             : null
           );
-        case "boolean":
-          return (viewData[uiElement.id] != null
+        } else if (isViewElementBoolean(uiElement)) {
+          let error = ""
+          const value = String(viewData[uiElement.id]).toLowerCase();
+          if (uiElement.mandatory && value !== "true" && value !== "false") {
+            error = "Error";
+          }
+          return (canEdit || viewData[uiElement.id] != null
             ? (<FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
               <InputLabel htmlFor={`select-${uiElement.id}`} >{uiElement.label}</InputLabel>
               <Select
-                readOnly={ true }
-
-                  value={String(viewData[uiElement.id]).toLowerCase()}
-                  inputProps={{
-                    name: uiElement.id,
-                    id: `select-${uiElement.id}`,
-                  }}
-                >
-                  <MenuItem value={'true'}>{uiElement.trueValue || 'True'}</MenuItem>
-                  <MenuItem value={'false'}>{uiElement.falseValue || 'False'}</MenuItem>
-
-                </Select>
+                required={!!uiElement.mandatory}
+                error={!!error}
+                onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+                readOnly={!canEdit}
+                disabled={editMode && !canEdit}
+                value={value}
+                inputProps={{
+                  name: uiElement.id,
+                  id: `select-${uiElement.id}`,
+                }}
+              >
+                <MenuItem value={'true'}>{uiElement.trueValue || 'True'}</MenuItem>
+                <MenuItem value={'false'}>{uiElement.falseValue || 'False'}</MenuItem>
+
+              </Select>
+              <FormHelperText>{error}</FormHelperText>
             </FormControl>)
             : null
           );
-        case "string":
+        } else if (isViewElementString(uiElement)) {
           return (
             <Tooltip key={uiElement.id} title={uiElement.description || ''}>
-              <TextField InputProps={{ readOnly: true }} spellCheck={false} autoFocus margin="dense"
-                id={uiElement.id} label={uiElement.label} type="text" value={viewData[uiElement.id] || ''} style={{ width: 485, marginLeft: 20, marginRight: 20 }} />
+              <TextField InputProps={{ readOnly: !canEdit, disabled: editMode && !canEdit }} spellCheck={false} autoFocus margin="dense"
+                id={uiElement.id} label={isKey ? "🔑 " + uiElement.label : uiElement.label} type="text" value={viewData[uiElement.id] || ''}
+                style={{ width: 485, marginLeft: 20, marginRight: 20 }}
+                onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+              />
             </Tooltip>
           );
-        case "number":
+        } else if (isViewElementNumber(uiElement)) {
           return (
             <Tooltip key={uiElement.id} title={uiElement.description || ''}>
-              <TextField InputProps={{ readOnly: true, startAdornment: uiElement.unit != null ? <InputAdornment position="start">{uiElement.unit}</InputAdornment> : undefined }} spellCheck={false} autoFocus margin="dense"
-                id={uiElement.id} label={uiElement.label} type="text" value={viewData[uiElement.id] || ''} style={{ width: 485, marginLeft: 20, marginRight: 20 }} />
+              <TextField InputProps={{ readOnly: !canEdit, disabled: editMode && !canEdit, startAdornment: uiElement.units != null ? <InputAdornment position="start">{uiElement.units}</InputAdornment> : undefined }} spellCheck={false} autoFocus margin="dense"
+                id={uiElement.id} label={uiElement.label} type="text" value={viewData[uiElement.id] == null ? '' : viewData[uiElement.id]}
+                style={{ width: 485, marginLeft: 20, marginRight: 20 }}
+                onChange={(e) => { this.changeValueFor(uiElement.id, e.target.value) }}
+              />
             </Tooltip>
           );
-        case "list":
-        case "object":
+        } else if (isViewElementObjectOrList(uiElement)) {
           return (
-            <Tooltip key={uiElement.id} title={uiElement.description || ''}>
-              <Link component="a" variant="body2" color="secondary" style={{ width: 485, marginLeft: 20, marginRight: 20 }}  onClick={() => {
-                this.props.navigateTo(uiElement.viewId);
-              }}>{uiElement.label}</Link>
-            </Tooltip>
+            <FormControl key={uiElement.id} style={{ width: 485, marginLeft: 20, marginRight: 20 }}>
+              <Tooltip title={uiElement.description || ''}>
+                <Button className={this.props.classes.leftButton} color="secondary" disabled={this.state.editMode} onClick={() => {
+                  this.navigate(`/${uiElement.id}`);
+                }}>{uiElement.label}</Button>
+              </Tooltip>
+            </FormControl>
           );
-        default:
+        } else {
           if (process.env.NODE_ENV !== "production") {
             console.error(`Unknown type - ${(uiElement as any).uiType} in ${(uiElement as any).id}.`)
           }
           return null;
+        }
+      })
+    );
+  };
+
+  private renderUIElementList(listSpecification: ViewSpecification, listKeyProperty: string, listData: { [key: string]: any }[]) {
+    const listElements = listSpecification.elements;
+
+    const navigate = (path: string) => {
+      this.props.history.push(`${this.props.match.url}${path}`);
+    };
+
+    const addNewElementAction = {
+      icon: AddIcon, tooltip: 'Add', onClick: () => {
+        navigate("[]"); // empty key means new element
       }
-    })
-  )
-
-  private createBreadCrump = (viewId: string, viewSpecifications: ViewSpecification[]) => {
-    const result: JSX.Element[] = [];
-    const hasIndex = /\/\$\$INDEX:(\d+):?([a-z\-]+)?\$\$/i;
-    let currentViewSpecification = viewSpecifications.find(s => s.id === viewId);
-    let indexCounter = 0;
-    while (currentViewSpecification != null) {
-      const currentViewId = currentViewSpecification.id;
-      const currentDataPathHasIndex = hasIndex.test(currentViewSpecification.dataPath);
-      result.unshift((
-        <span>
-          <Link component="a" variant="body2" color="secondary" onClick={() => {
-            this.props.navigateTo(currentViewId, currentDataPathHasIndex ? ++indexCounter : indexCounter);
-          }}>{currentViewSpecification.name}</Link>
-          {viewId === currentViewId ? null : " | "}
-        </span>
-      ));
-      currentViewSpecification = viewSpecifications.find(s => s.id === (currentViewSpecification && currentViewSpecification.parentView || ''));
+    };
+
+    const { classes } = this.props;
+
+    return (
+      <SelectElementTable idProperty={listKeyProperty} rows={listData} customActionButtons={[addNewElementAction]} columns={
+        Object.keys(listElements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
+          const elm = listElements[cur];
+          if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
+            if (elm.label !== listKeyProperty) {
+              acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
+            } else {
+              acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
+            }
+          }
+          return acc;
+        }, []).concat([{
+          property: "Actions", disableFilter: true, disableSorting: true, type: ColumnType.custom, customControl: (row => {
+            return (
+              <Tooltip title={"Remove"} >
+                <IconButton className={classes.button} onClick={event => {
+
+                }} >
+                  <RemoveIcon />
+                </IconButton>
+              </Tooltip>
+            )
+          })
+        }])
+      } onHandleClick={(ev, row) => {
+        ev.preventDefault();
+        navigate(`[${row[listKeyProperty]}]`);
+      }} ></SelectElementTable>
+    );
+  }
+
+  private renderBreadCrumps() {
+    const { editMode } = this.state;
+    const { viewSpecification, displayAsList } = this.props;
+    const { vPath, match: { url, path }, nodeId } = this.props;
+    const pathParts = splitVPath(vPath!, /(?:([^\/\["]+)(?:\[([^\]]*)\])?)/g); // 1 = property / 2 = optional key
+    let lastPath = `/configuration`;
+    let basePath = `/configuration/${nodeId}`;
+    return (
+      <div className={this.props.classes.header}>
+        <div>
+          <Breadcrumbs aria-label="breadcrumb">
+            <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+              ev.preventDefault();
+              this.props.history.push(lastPath);
+            }}>Back</Link>
+            <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+              ev.preventDefault();
+              this.props.history.push(`/configuration/${nodeId}`);
+            }}><span>{nodeId}</span></Link>
+            {
+              pathParts.map(([prop, key], ind) => {
+                const path = `${basePath}/${prop}`;
+                const keyPath = key && `${basePath}/${prop}[${key}]`;
+                const ret = (
+                  <span key={ind}>
+                    <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+                      ev.preventDefault();
+                      this.props.history.push(path);
+                    }}><span>{prop.replace(/^[^:]+:/, "")}</span></Link>
+                    {
+                      keyPath && <Link color="inherit" href="#" onClick={(ev: React.MouseEvent<HTMLElement>) => {
+                        ev.preventDefault();
+                        this.props.history.push(keyPath);
+                      }}>{`[${key}]`}</Link> || null
+                    }
+                  </span>
+                );
+                lastPath = basePath;
+                basePath = keyPath || path;
+                return ret;
+              })
+            }
+          </Breadcrumbs>
+        </div>
+        { /* do not show edit if this is a list or it can't be edited */
+          !displayAsList && viewSpecification.canEdit && (<div>
+            <Fab color="secondary" aria-label="edit" className={this.props.classes.fab} onClick={() => {
+              if (this.state.editMode) {
+                this.props.onUpdateData(this.props.vPath!, this.state.viewData);
+              }
+              this.setState({ editMode: !editMode });
+            }}>
+              {editMode
+                ? <SaveIcon />
+                : <EditIcon />
+              }
+            </Fab>
+          </div> || null)
+        }
+      </div>
+    );
+  }
+
+  private renderValueSelector() {
+    const { listKeyProperty, listSpecification, listData, onValueSelected } = this.props;
+    if (!listKeyProperty || !listSpecification) {
+      throw new Error("ListKex ot view not specified.");
     }
-    return result;
+
+    return (
+      <div>
+        <SelectElementTable idProperty={listKeyProperty} rows={listData} columns={
+          Object.keys(listSpecification.elements).reduce<ColumnModel<{ [key: string]: any }>[]>((acc, cur) => {
+            const elm = listSpecification.elements[cur];
+            if (elm.uiType !== "object" && listData.every(entry => entry[elm.label] != null)) {
+              if (elm.label !== listKeyProperty) {
+                acc.push({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
+              } else {
+                acc.unshift({ property: elm.label, type: elm.uiType === "number" ? ColumnType.numeric : ColumnType.text });
+              }
+            }
+            return acc;
+          }, [])
+        } onHandleClick={(ev, row) => { ev.preventDefault(); onValueSelected(row); }} ></SelectElementTable>
+      </div>
+    );
+  }
+
+  private renderValueEditor() {
+    const { keyProperty, displayAsList, viewSpecification } = this.props;
+    const { viewData, editMode, isNew } = this.state;
+
+    return (
+      <div>
+        { this.renderBreadCrumps() }
+        { displayAsList && viewData instanceof Array
+            ? this.renderUIElementList(viewSpecification, keyProperty!, viewData)
+            : this.renderUIElement(viewSpecification, viewData!, keyProperty, editMode, isNew)
+        }
+      </div >
+    );
+  }
+
+  private renderCollectingData() {
+    return (
+      <div className={this.props.classes.outer}>
+        <div className={this.props.classes.inner}>
+          <Loader />
+          <h3>Collecting Data ...</h3>
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    return this.props.collectingData || !this.state.viewData
+      ? this.renderCollectingData()
+      : this.props.listSpecification
+        ? this.renderValueSelector()
+        : this.renderValueEditor();
   }
 }
 
-export const ConfigurationApplication = connect(mapProps, mapDisp)(ConfigurationApplicationComponent);
+export const ConfigurationApplication = withStyles(styles)(withRouter(connect(mapProps, mapDispatch)(ConfigurationApplicationComponent)));
 export default ConfigurationApplication;
\ No newline at end of file