You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2019/11/12 17:42:45 UTC

[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Added schema, entity operations, notifications

This is an automated email from the ASF dual-hosted git repository.

eallen pushed a commit to branch eallen-DISPATCH-1385
in repository https://gitbox.apache.org/repos/asf/qpid-dispatch.git


The following commit(s) were added to refs/heads/eallen-DISPATCH-1385 by this push:
     new 632aacf  Added schema, entity operations, notifications
632aacf is described below

commit 632aacf045c8e4d9595ca8c7302aa01b43a4218d
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Tue Nov 12 12:42:25 2019 -0500

    Added schema, entity operations, notifications
---
 console/react/public/config.json                   |   3 +
 console/react/src/App.css                          | 139 ++++++++-
 console/react/src/App.js                           |   2 +-
 console/react/src/alertList.js                     |  71 +++++
 console/react/src/config.json                      |   3 -
 console/react/src/connect-form.js                  |  45 ++-
 console/react/src/connectPage.js                   |   3 +-
 console/react/src/connecting.js                    |  30 ++
 console/react/src/connectionClose.js               |   7 +-
 console/react/src/contextMenuComponent.js          |  53 +---
 .../connectionData.js => createEntity.js}          |  31 +-
 console/react/src/details/createTablePage.js       | 336 ++++++++++++++++++++
 .../dataSources/{connectionData.js => autoLink.js} |  19 +-
 .../src/details/dataSources/connectionData.js      |  11 +
 .../react/src/details/dataSources/defaultData.js   |  82 ++++-
 console/react/src/details/dataSources/linkData.js  |   8 +-
 console/react/src/details/dataSources/logsData.js  | 161 +---------
 .../react/src/details/dataSources/routerData.js    |   3 -
 console/react/src/details/deleteEntity.js          | 143 +++++++++
 console/react/src/details/enitiesPage.js           |  83 ++++-
 console/react/src/details/entityData.js            |   6 +-
 console/react/src/details/entityListTable.js       |  83 ++++-
 console/react/src/details/schema/schemaPage.js     | 226 ++++++++++++++
 .../connectionData.js => updateEntity.js}          |  33 +-
 console/react/src/details/updateTablePage.js       | 337 +++++++++++++++++++++
 console/react/src/detailsTablePage.js              |  34 ++-
 console/react/src/index.js                         |  14 +-
 console/react/src/layout.js                        |  91 +++---
 console/react/src/notificationDrawer.js            |  18 +-
 .../react/src/overview/dashboard/dashboardPage.js  |   5 +-
 .../overview/dashboard/delayedDeliveriesCard.js    |   6 +-
 console/react/src/pleaseWait.js                    |  51 ++++
 console/react/src/qdrGlobals.js                    |  10 -
 console/react/src/tableToolbar.jsx                 |  34 +--
 console/react/yarn.lock                            |  36 +--
 python/qpid_dispatch_internal/dispatch.py          |   1 +
 36 files changed, 1793 insertions(+), 425 deletions(-)

diff --git a/console/react/public/config.json b/console/react/public/config.json
new file mode 100644
index 0000000..6b19668
--- /dev/null
+++ b/console/react/public/config.json
@@ -0,0 +1,3 @@
+{
+  "title": "Apache Qpid Dispach Console"
+}
diff --git a/console/react/src/App.css b/console/react/src/App.css
index b1eff31..4e4240b 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -1001,7 +1001,7 @@ div.qdrChord .legend-text {
   background-color: transparent;
   color: blue;
   font-weight: bold;
-  white-space: normal;
+  white-space: nowrap;
   padding-left: 0;
 }
 
@@ -1140,19 +1140,20 @@ div.details-table ul.entities-list {
 span.entity-type i {
   padding-right: 1em;
   font-style: normal;
+  font-family: FontAwesome;
 }
 span.entity-type i.address-local:before {
-  font-family: FontAwesome;
   content: "\f0ac";
 }
 span.entity-type i.address-mobile:before {
-  font-family: FontAwesome;
   content: "\f109";
 }
 span.entity-type i.address-router:before {
-  font-family: FontAwesome;
   content: "\f047";
 }
+span.entity-type i.address-topo:before {
+  content: "\f126";
+}
 
 span.entity-type i.link-type-endpoint:before {
   content: "\f109";
@@ -1297,3 +1298,133 @@ span.entity-type i.link-type-router-control:before {
   font-weight: bold;
   color: black;
 }
+
+.details-table-header {
+  display: flex;
+  justify-content: space-between;
+}
+
+.detail-action-button {
+  margin-right: 1em;
+}
+
+#update-form .pf-c-form__helper-text {
+  text-align: left;
+}
+
+#alert-list-container {
+  position: absolute;
+  right: 6em;
+  top: 3em;
+}
+
+/* login form */
+.spinning-clockwise {
+  -webkit-animation: spinc 4s linear infinite;
+  -moz-animation: spinc 4s linear infinite;
+  animation: spinc 4s linear infinite;
+}
+@-moz-keyframes spinc {
+  100% {
+    -moz-transform: rotate(360deg);
+  }
+}
+@-webkit-keyframes spinc {
+  100% {
+    -webkit-transform: rotate(360deg);
+  }
+}
+@keyframes spinc {
+  100% {
+    -webkit-transform: rotate(360deg);
+    transform: rotate(360deg);
+  }
+}
+.spinning-cclockwise {
+  -webkit-animation: spincc 4s linear infinite;
+  -moz-animation: spincc 4s linear infinite;
+  animation: spincc 4s linear infinite;
+}
+@-moz-keyframes spincc {
+  100% {
+    -moz-transform: rotate(-360deg);
+  }
+}
+@-webkit-keyframes spincc {
+  100% {
+    -webkit-transform: rotate(-360deg);
+  }
+}
+@keyframes spincc {
+  100% {
+    -webkit-transform: rotate(-360deg);
+    transform: rotate(-360deg);
+  }
+}
+
+#topicCogWrapper {
+  position: relative;
+  margin: auto;
+  width: 5.5em;
+  height: 5em;
+}
+#topicCogMain {
+  width: 4em;
+  height: 4em;
+  position: absolute;
+  top: 0.5em;
+  left: 0;
+}
+
+#topicCogUpper {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 2em;
+  height: 2em;
+}
+#topicCogLower {
+  position: absolute;
+  bottom: 0;
+  right: 0;
+  width: 2em;
+  height: 2em;
+}
+
+.topic-creating-wrapper {
+  text-align: center;
+  position: absolute;
+  top: 10em;
+  right: 10em;
+}
+
+.topic-creating-message {
+  margin-top: 2em;
+}
+
+div.connecting {
+  opacity: 0.2;
+}
+
+/* schema page */
+
+.list-group-item-heading {
+  text-align: left;
+}
+
+.list-group-item-text {
+  text-align: left;
+}
+
+.list-view-pf-description {
+  display: block;
+}
+
+.list-group-item-fqt {
+  font-weight: bold;
+  font-size: 14px;
+}
+
+#schema-page .pficon.list-view-pf-icon-sm {
+  border: 0;
+}
diff --git a/console/react/src/App.js b/console/react/src/App.js
index 6f66bb7..ff8b629 100644
--- a/console/react/src/App.js
+++ b/console/react/src/App.js
@@ -13,7 +13,7 @@ class App extends Component {
   render() {
     return (
       <div className="App pf-m-redhat-font">
-        <PageLayout />
+        <PageLayout config={this.props.config} />
       </div>
     );
   }
diff --git a/console/react/src/alertList.js b/console/react/src/alertList.js
new file mode 100644
index 0000000..36c8ecf
--- /dev/null
+++ b/console/react/src/alertList.js
@@ -0,0 +1,71 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+*/
+
+import React from "react";
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core";
+
+class AlertList extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      alerts: []
+    };
+    this.nextIndex = 0;
+  }
+
+  hideAlert = alert => {
+    const { alerts } = this.state;
+    const index = alerts.findIndex(a => a.key === alert.key);
+    if (index >= 0) alerts.splice(index, 1);
+    this.setState({ alerts });
+  };
+
+  addAlert = (severity, message) => {
+    const { alerts } = this.state;
+    const alert = { key: this.nextIndex++, type: severity, message };
+    const self = this;
+    setTimeout(() => self.hideAlert(alert), 5000);
+    alerts.unshift(alert);
+    this.setState({ alerts });
+  };
+
+  render() {
+    return (
+      <div id="alert-list-container">
+        {this.state.alerts.map((alert, i) => (
+          <Alert
+            key={`alert-${i}`}
+            variant={alert.type}
+            title={alert.type}
+            isInline
+            action={
+              <AlertActionCloseButton onClose={() => this.hideAlert(alert)} />
+            }
+          >
+            {alert.message.length > 40
+              ? `${alert.message.substr(0, 40)}...`
+              : alert.message}
+          </Alert>
+        ))}
+      </div>
+    );
+  }
+}
+
+export default AlertList;
diff --git a/console/react/src/config.json b/console/react/src/config.json
deleted file mode 100644
index 6f7d7a7..0000000
--- a/console/react/src/config.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "title": "The Apache Qpid Dispach Console"
-}
diff --git a/console/react/src/connect-form.js b/console/react/src/connect-form.js
index a7a9871..37f4f2e 100644
--- a/console/react/src/connect-form.js
+++ b/console/react/src/connect-form.js
@@ -24,7 +24,7 @@ import {
   Text,
   TextVariants
 } from "@patternfly/react-core";
-
+import PleaseWait from "./pleaseWait";
 const CONNECT_KEY = "QDRSettings";
 
 class ConnectForm extends React.Component {
@@ -36,7 +36,9 @@ class ConnectForm extends React.Component {
       port: "",
       username: "",
       password: "",
-      isShown: this.props.isConnectFormOpen
+      isShown: this.props.isConnectFormOpen,
+      connecting: false,
+      connectError: null
     };
   }
 
@@ -70,7 +72,28 @@ class ConnectForm extends React.Component {
   };
 
   handleConnect = () => {
-    this.props.handleConnect(this.props.fromPath, this.state);
+    if (this.props.isConnected) {
+      // handle disconnects from the main page
+      this.props.handleConnect(this.props.fromPath);
+    } else {
+      const connectOptions = JSON.parse(JSON.stringify(this.state));
+      if (connectOptions.username === "") connectOptions.username = undefined;
+      if (connectOptions.password === "") connectOptions.password = undefined;
+      connectOptions.reconnect = true;
+
+      this.setState({ connecting: true }, () => {
+        this.props.service.connect(connectOptions).then(
+          r => {
+            this.setState({ connecting: false });
+            this.props.handleConnect(this.props.fromPath, r);
+          },
+          e => {
+            console.log(e);
+            this.setState({ connecting: false, connectError: e.msg });
+          }
+        );
+      });
+    }
   };
 
   toggleDrawerHide = () => {
@@ -78,12 +101,19 @@ class ConnectForm extends React.Component {
   };
 
   render() {
-    const { isShown, address, port, username, password } = this.state;
+    const {
+      isShown,
+      address,
+      port,
+      username,
+      password,
+      connecting
+    } = this.state;
 
     return isShown ? (
       <div>
         <div className="connect-modal">
-          <div className="">
+          <div className={connecting ? "connecting" : ""}>
             <Form isHorizontal>
               <TextContent className="connect-title">
                 <Text component={TextVariants.h1}>Connect</Text>
@@ -161,6 +191,11 @@ class ConnectForm extends React.Component {
               </ActionGroup>
             </Form>
           </div>
+          <PleaseWait
+            isOpen={connecting}
+            title="Connecting"
+            message="Connecting to the router, please wait..."
+          />
         </div>
       </div>
     ) : null;
diff --git a/console/react/src/connectPage.js b/console/react/src/connectPage.js
index 5975c4c..32688f6 100644
--- a/console/react/src/connectPage.js
+++ b/console/react/src/connectPage.js
@@ -47,6 +47,7 @@ class ConnectPage extends React.Component {
           {showForm ? (
             <ConnectForm
               prefix="form"
+              service={this.props.service}
               handleConnect={this.props.handleConnect}
               handleConnectCancel={this.handleConnectCancel}
               fromPath={from.pathname}
@@ -58,7 +59,7 @@ class ConnectPage extends React.Component {
           <div className="left-content">
             <TextContent>
               <Text component="h1" className="console-banner">
-                Apache Qpid Dispatch Console
+                {this.props.config.title}
               </Text>
             </TextContent>
             <TextContent>
diff --git a/console/react/src/connecting.js b/console/react/src/connecting.js
new file mode 100644
index 0000000..0d24c3a
--- /dev/null
+++ b/console/react/src/connecting.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Red Hat Inc.
+ *
+ * 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.
+ */
+import React from "react";
+
+class Connecting extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {};
+  }
+
+  render() {
+    return <div id="connecting">Connecting</div>;
+  }
+}
+
+export default Connecting;
diff --git a/console/react/src/connectionClose.js b/console/react/src/connectionClose.js
index 9da3d13..b27ba05 100644
--- a/console/react/src/connectionClose.js
+++ b/console/react/src/connectionClose.js
@@ -57,7 +57,7 @@ class ConnectionClose extends React.Component {
               "action",
               `Close connection ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`,
               new Date(),
-              "error"
+              "danger"
             );
           } else {
             this.props.handleAddNotification(
@@ -85,7 +85,10 @@ class ConnectionClose extends React.Component {
     if (record.role === "normal") {
       return (
         <React.Fragment>
-          <Button className="link-button" onClick={this.handleModalToggle}>
+          <Button
+            className={`${this.props.asButton ? "" : "link-button"}`}
+            onClick={this.handleModalToggle}
+          >
             Close
           </Button>
           <Modal
diff --git a/console/react/src/contextMenuComponent.js b/console/react/src/contextMenuComponent.js
index a5912a7..ffa4fba 100644
--- a/console/react/src/contextMenuComponent.js
+++ b/console/react/src/contextMenuComponent.js
@@ -6,55 +6,25 @@ class ContextMenuComponent extends React.Component {
     this.state = {};
   }
 
-  componentDidMount = () => {
-    this.registerHandlers();
-  };
-
-  componentWillUnmount = () => {
-    this.unregisterHandlers();
-  };
-
-  registerHandlers = () => {
-    document.addEventListener("mousedown", this.handleOutsideClick);
-    document.addEventListener("touchstart", this.handleOutsideClick);
-    document.addEventListener("scroll", this.handleHide);
-    document.addEventListener("contextmenu", this.handleContextMenuEvent);
-    window.addEventListener("resize", this.handleHide);
-  };
-
-  unregisterHandlers = () => {
-    document.removeEventListener("mousedown", this.handleOutsideClick);
-    document.removeEventListener("touchstart", this.handleOutsideClick);
-    document.removeEventListener("scroll", this.handleHide);
-    document.removeEventListener("contextmenu", this.handleContextMenuEvent);
-    window.removeEventListener("resize", this.handleHide);
-  };
-
-  handleHide = e => {
-    this.unregisterHandlers();
-    this.props.handleContextHide();
-  };
+  componentDidMount() {
+    document.addEventListener("mousedown", this.handleClickOutside);
+  }
 
-  handleContextMenuEvent = e => {
-    // if the event happened to an svg circle, don't hide the context menu
-    if (!e.target || e.target.nodeName !== "circle") {
-      this.handleHide(e);
-    }
-  };
+  componentWillUnmount() {
+    document.removeEventListener("mousedown", this.handleClickOutside);
+  }
 
-  handleOutsideClick = e => {
-    if (
-      e.target.nodeName !== "LI" &&
-      !e.target.className.includes(this.props.parentClass)
-    ) {
-      this.handleHide(e);
+  handleClickOutside = event => {
+    if (this.listRef && this.listRef.contains(event.target)) {
+      return;
     }
+    this.props.handleContextHide();
   };
 
   proxyClick = (item, e) => {
     if (item.action && item.enabled(this.props.contextEventData)) {
       item.action(item, this.props.contextEventData, e);
-      this.handleHide(e);
+      this.props.handleContextHide();
     }
   };
 
@@ -90,6 +60,7 @@ class ContextMenuComponent extends React.Component {
       <ul
         className={`context-menu ${this.props.className || ""}`}
         style={style}
+        ref={el => (this.listRef = el)}
       >
         {menuItems}
       </ul>
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/createEntity.js
similarity index 63%
copy from console/react/src/details/dataSources/connectionData.js
copy to console/react/src/details/createEntity.js
index 568ac4c..3f5dfe2 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/createEntity.js
@@ -17,23 +17,22 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import DefaultData from "./defaultData";
-import ConnectionClose from "../../connectionClose";
+import React from "react";
+import { Button } from "@patternfly/react-core";
 
-class ConnectionData extends DefaultData {
-  constructor(service, schema) {
-    super(service, schema);
-    this.extraFields = [
-      {
-        title: "",
-        field: "connection",
-        noSort: true,
-        formatter: ConnectionClose
-      }
-    ];
-    this.detailEntity = "router.link";
-    this.detailName = "Link";
+class CreateEntity extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  handleClick = () => {
+    this.props.handleEntityAction("create");
+  };
+
+  render() {
+    return <Button onClick={this.handleClick}>Create</Button>;
   }
 }
 
-export default ConnectionData;
+export default CreateEntity;
diff --git a/console/react/src/details/createTablePage.js b/console/react/src/details/createTablePage.js
new file mode 100644
index 0000000..7a4ceb5
--- /dev/null
+++ b/console/react/src/details/createTablePage.js
@@ -0,0 +1,336 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+  Button,
+  Stack,
+  StackItem,
+  TextContent,
+  Text,
+  TextVariants,
+  Breadcrumb,
+  BreadcrumbItem
+} from "@patternfly/react-core";
+
+import {
+  Form,
+  FormGroup,
+  TextInput,
+  FormSelectOption,
+  FormSelect,
+  Checkbox,
+  ActionGroup
+} from "@patternfly/react-core";
+
+import { cellWidth } from "@patternfly/react-table";
+import { Card, CardBody } from "@patternfly/react-core";
+import { Redirect } from "react-router-dom";
+import { dataMap as detailsDataMap, defaultData } from "./entityData";
+
+class CreateTablePage extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      columns: [
+        { title: "Attribute", transforms: [cellWidth(20)] },
+        {
+          title: "Value",
+          transforms: [cellWidth("max")],
+          props: { className: "pf-u-text-align-left" }
+        }
+      ],
+      rows: [],
+      redirect: false,
+      redirectState: { page: 1 },
+      redirectPath: "/dashboard",
+      lastUpdated: new Date(),
+      record: {}
+    };
+
+    // if we get to this page and we don't have a props.location.state.entity
+    // then redirect back to the dashboard.
+    // this can happen if we get here from a bookmark or browser refresh
+    this.entity =
+      this.props.entity ||
+      (this.props &&
+        this.props.location &&
+        this.props.location.state &&
+        this.props.location.state.entity);
+
+    if (!this.entity) {
+      this.state.redirect = true;
+    } else {
+      this.dataSource = !detailsDataMap[this.entity]
+        ? new defaultData(this.props.service, this.props.schema)
+        : new detailsDataMap[this.entity](
+            this.props.service,
+            this.props.schema
+          );
+      this.locationState = this.props.locationState;
+
+      const attributes = this.dataSource.schemaAttributes(this.entity);
+      for (let attributeKey in attributes) {
+        this.state.record[attributeKey] = this.getDefault(
+          attributes[attributeKey]
+        );
+      }
+    }
+  }
+
+  handleTextInputChange = (value, key) => {
+    console.log(`handleTextInputChange was passed ${value} ${key}`);
+    const { record } = this.state;
+    record[key] = value;
+    this.setState({ record });
+  };
+
+  getDefault = attribute => {
+    let defaultVal = "";
+    if (attribute.default) defaultVal = attribute.default;
+    if (typeof attribute.default === "undefined") {
+      if (attribute.type === "boolean") {
+        defaultVal = false;
+      } else if (Array.isArray(attribute.type)) {
+        defaultVal = attribute.type[0];
+      } else if (attribute.type === "integer") {
+        defaultVal = 0;
+      }
+    }
+    return defaultVal;
+  };
+
+  schemaToForm = () => {
+    const attributes = this.dataSource.schemaAttributes(this.entity);
+    const formGroups = [];
+    for (let attributeKey in attributes) {
+      if (attributeKey !== "identity") {
+        const attribute = attributes[attributeKey];
+        if (!attribute.graph) {
+          let type = attribute.type;
+          let options = [];
+          let readOnly = attributeKey === "identity";
+          if (type === "list") readOnly = true;
+          if (Array.isArray(attribute.type)) {
+            type = "select";
+            options = attribute.type;
+          }
+          let required = attribute.required;
+          if (
+            this.dataSource.updateMetaData &&
+            this.dataSource.updateMetaData[attributeKey]
+          ) {
+            const override = this.dataSource.updateMetaData[attributeKey];
+            if (override.readOnly) {
+              type = "string";
+              readOnly = true;
+            }
+            if (override.type === "select") {
+              type = "select";
+              options = override.options;
+            }
+          }
+          if (!readOnly) {
+            // no need to display readonly fields on create form
+            const id = `form-${attributeKey}`;
+            const formGroupProps = {
+              label: attributeKey,
+              isRequired: required,
+              fieldId: id,
+              helperText: attribute.description
+            };
+            if (type === "string" || type === "integer") {
+              formGroups.push(
+                <FormGroup {...formGroupProps} key={attributeKey}>
+                  <TextInput
+                    value={this.state.record[attributeKey]}
+                    isRequired={required}
+                    type={type === "string" ? "text" : "number"}
+                    id={id}
+                    aria-describedby="entiy-form-field"
+                    name={attributeKey}
+                    isDisabled={readOnly}
+                    onChange={value =>
+                      this.handleTextInputChange(value, attributeKey)
+                    }
+                  />
+                </FormGroup>
+              );
+            } else if (type === "select") {
+              formGroups.push(
+                <FormGroup {...formGroupProps} key={attributeKey}>
+                  <FormSelect
+                    value={this.state.record[attributeKey]}
+                    onChange={value =>
+                      this.handleTextInputChange(value, attributeKey)
+                    }
+                    id={id}
+                    name={attributeKey}
+                  >
+                    {options.map((option, index) => (
+                      <FormSelectOption
+                        isDisabled={false}
+                        key={`${attributeKey}-${index}`}
+                        value={option}
+                        label={option}
+                      />
+                    ))}
+                  </FormSelect>
+                </FormGroup>
+              );
+            } else if (type === "boolean") {
+              formGroups.push(
+                <FormGroup {...formGroupProps} key={attributeKey}>
+                  <Checkbox
+                    isChecked={this.state.record[attributeKey]}
+                    label={attributeKey}
+                    id={id}
+                    name={attributeKey}
+                    onChange={value =>
+                      this.handleTextInputChange(value, attributeKey)
+                    }
+                  />
+                </FormGroup>
+              );
+            }
+          }
+        }
+      }
+    }
+    return formGroups;
+  };
+
+  toString = val => {
+    return val === null ? "" : String(val);
+  };
+
+  icap = s => s.charAt(0).toUpperCase() + s.slice(1);
+
+  parentItem = () => this.state.record.name;
+
+  breadcrumbSelected = () => {
+    this.props.handleSelectEntity(this.entity);
+  };
+
+  handleCancel = () => {
+    this.props.handleActionCancel(this.props);
+  };
+
+  handleCreate = () => {
+    const { record } = this.state;
+    const attributes = {};
+    const schemaAttributes = this.dataSource.schemaAttributes(this.entity);
+    for (let attr in record) {
+      if (
+        this.getDefault(schemaAttributes[attr]) !== record[attr] ||
+        schemaAttributes[attr].required
+      )
+        attributes[attr] = record[attr];
+    }
+    console.log(`creating ${this.entity}`);
+    console.log(attributes);
+
+    // call update
+    this.props.service.management.connection
+      .sendMethod(this.props.routerId, this.entity, attributes, "CREATE")
+      .then(results => {
+        let statusCode =
+          results.context.message.application_properties.statusCode;
+        if (statusCode < 200 || statusCode >= 300) {
+          let message =
+            results.context.message.application_properties.statusDescription;
+          const msg = `Create failed with message: ${message}`;
+          console.log(
+            `error Create failed ${results.context.message.application_properties.statusDescription}`
+          );
+          this.props.handleAddNotification("action", msg, new Date(), "danger");
+        } else {
+          const msg = `Created ${this.props.entity} ${record.name}`;
+          console.log(`success ${msg}`);
+          this.props.handleAddNotification(
+            "action",
+            msg,
+            new Date(),
+            "success"
+          );
+        }
+        this.handleCancel();
+      });
+  };
+
+  render() {
+    if (this.state.redirect) {
+      return (
+        <Redirect
+          to={{
+            pathname: this.state.redirectPath,
+            state: this.state.redirectState
+          }}
+        />
+      );
+    }
+
+    return (
+      <React.Fragment>
+        <PageSection
+          variant={PageSectionVariants.light}
+          className="overview-table-page"
+        >
+          <Stack>
+            <StackItem className="overview-header details">
+              <Breadcrumb>
+                <BreadcrumbItem
+                  className="link-button"
+                  onClick={this.breadcrumbSelected}
+                >
+                  {this.icap(this.entity)}
+                </BreadcrumbItem>
+              </Breadcrumb>
+
+              <TextContent className="details-table-header">
+                <Text className="overview-title" component={TextVariants.h1}>
+                  {this.parentItem()}
+                </Text>
+                <ActionGroup>
+                  <Button
+                    className="detail-action-button link-button"
+                    onClick={this.handleCancel}
+                  >
+                    Cancel
+                  </Button>
+                  <Button onClick={this.handleCreate}>Create</Button>
+                </ActionGroup>
+              </TextContent>
+            </StackItem>
+            <StackItem id="update-form">
+              <Card>
+                <CardBody>
+                  <Form isHorizontal>{this.schemaToForm()}</Form>
+                </CardBody>
+              </Card>
+            </StackItem>
+          </Stack>
+        </PageSection>
+      </React.Fragment>
+    );
+  }
+}
+
+export default CreateTablePage;
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/dataSources/autoLink.js
similarity index 71%
copy from console/react/src/details/dataSources/connectionData.js
copy to console/react/src/details/dataSources/autoLink.js
index 568ac4c..d5dc74c 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/dataSources/autoLink.js
@@ -18,22 +18,15 @@ under the License.
 */
 
 import DefaultData from "./defaultData";
-import ConnectionClose from "../../connectionClose";
 
-class ConnectionData extends DefaultData {
+class AutoLinkData extends DefaultData {
   constructor(service, schema) {
     super(service, schema);
-    this.extraFields = [
-      {
-        title: "",
-        field: "connection",
-        noSort: true,
-        formatter: ConnectionClose
-      }
-    ];
-    this.detailEntity = "router.link";
-    this.detailName = "Link";
+
+    this.updateMetaData = {
+      operStatus: { readOnly: true }
+    };
   }
 }
 
-export default ConnectionData;
+export default AutoLinkData;
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/dataSources/connectionData.js
index 568ac4c..2271434 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/dataSources/connectionData.js
@@ -17,6 +17,7 @@ specific language governing permissions and limitations
 under the License.
 */
 
+import React from "react";
 import DefaultData from "./defaultData";
 import ConnectionClose from "../../connectionClose";
 
@@ -34,6 +35,16 @@ class ConnectionData extends DefaultData {
     this.detailEntity = "router.link";
     this.detailName = "Link";
   }
+
+  detailActions = (entity, props, record) => {
+    return (
+      <ConnectionClose
+        asButton={true}
+        extraInfo={{ rowData: { data: record } }}
+        {...props}
+      />
+    );
+  };
 }
 
 export default ConnectionData;
diff --git a/console/react/src/details/dataSources/defaultData.js b/console/react/src/details/dataSources/defaultData.js
index 0ed5235..6e4b401 100644
--- a/console/react/src/details/dataSources/defaultData.js
+++ b/console/react/src/details/dataSources/defaultData.js
@@ -17,23 +17,61 @@ specific language governing permissions and limitations
 under the License.
 */
 
+import React from "react";
 import { utils } from "../../amqp/utilities";
+import DeleteEntity from "../deleteEntity";
+import UpdateEntity from "../updateEntity";
+import CreateEntity from "../createEntity";
 
 class DefaultData {
   constructor(service, schema) {
     this.service = service;
     this.schema = schema;
+    this.actionMap = {
+      DELETE: DeleteEntity,
+      UPDATE: UpdateEntity,
+      CREATE: CreateEntity
+    };
   }
 
-  hasType = () => {
-    return false;
-  };
+  schemaAttributes = entity => this.schema.entityTypes[entity].attributes;
+  schemaOperations = entity => this.schema.entityTypes[entity].operations;
 
-  actions = entity => {
-    return this.schema.entityTypes[entity].operations.filter(
-      action => action !== "READ" && action !== "UPDATE"
-    );
-  };
+  // emit a single button/component
+  entityAction = ({
+    component: Component,
+    props,
+    record,
+    click,
+    i,
+    asButton
+  }) => (
+    <Component
+      key={`action-${i}`}
+      record={record}
+      notifyClick={click}
+      {...props}
+      asButton={asButton}
+    />
+  );
+
+  // action buttons for the detailsTablePage for a single record
+  detailActions = (entity, props, record, click) => (
+    <>
+      {this.actions(entity)
+        .filter(action => action !== "CREATE")
+        .map((action, i) =>
+          this.entityAction({
+            component: this.actionMap[action],
+            props,
+            record,
+            click,
+            i,
+            asButton: true
+          })
+        )}
+    </>
+  );
 
   // called by detailsTablePage to display a single record
   fetchRecord = (currentRecord, schema, entity) => {
@@ -61,6 +99,34 @@ class DefaultData {
     });
   };
 
+  // return a list of operations allowed for this entity
+  actions = entity =>
+    this.schema.entityTypes[entity].operations.filter(
+      action => action !== "READ"
+    );
+
+  // action button for the entityListTable
+  actionButton = ({ action, props, click, record, i, asButton }) =>
+    this.entityAction({
+      component: this.actionMap[action],
+      props,
+      click,
+      record,
+      i,
+      asButton
+    });
+
+  // actions menu on entityListTable for each record
+  actionMenuItems = (entity, click) => {
+    const actions = this.actions(entity).filter(action => action !== "CREATE");
+    return actions.map(action => ({
+      title: action,
+      onClick: (event, rowId, rowData, extra) => {
+        click({ action, entity, event, rowId, rowData, extra });
+      }
+    }));
+  };
+
   // called by entityListTable to get the list of records
   doFetch = (page, perPage, routerId, entity) => {
     return new Promise(resolve => {
diff --git a/console/react/src/details/dataSources/linkData.js b/console/react/src/details/dataSources/linkData.js
index 47bee54..69636b3 100644
--- a/console/react/src/details/dataSources/linkData.js
+++ b/console/react/src/details/dataSources/linkData.js
@@ -47,7 +47,13 @@ const LinkType = ({ value, extraInfo }) => {
 class LinkData extends DefaultData {
   constructor(service, schema) {
     super(service, schema);
-    this.service = service;
+
+    this.updateMetaData = {
+      peer: { readOnly: true },
+      linkName: { readOnly: true },
+      owningAddr: { readOnly: true }
+    };
+
     this.extraFields = [{ title: "Dir", field: "linkDir", formatter: LinkDir }];
     this.detailEntity = "router.link";
     this.detailName = "Link";
diff --git a/console/react/src/details/dataSources/logsData.js b/console/react/src/details/dataSources/logsData.js
index 4d2128c..daa2553 100644
--- a/console/react/src/details/dataSources/logsData.js
+++ b/console/react/src/details/dataSources/logsData.js
@@ -17,160 +17,15 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import React from "react";
-import { Button } from "@patternfly/react-core";
-class LogRecords extends React.Component {
-  detailClick = () => {
-    this.props.detailClick(this.props.value, this.props.extraInfo);
-  };
-  render() {
-    if (
-      this.props.extraInfo.rowData.enable.title !== "" &&
-      this.props.value !== "0"
-    ) {
-      return (
-        <Button className="link-button" onClick={this.detailClick}>
-          {this.props.value}
-        </Button>
-      );
-    } else {
-      return this.props.value;
-    }
+import DefaultData from "./defaultData";
+
+class LogsData extends DefaultData {
+  constructor(service, schema) {
+    super(service, schema);
+    this.updateMetaData = {
+      module: { readOnly: true }
+    };
   }
 }
 
-class LogsData {
-  constructor(service) {
-    this.service = service;
-    this.fields = [
-      { title: "Router", field: "node" },
-      { title: "Enable", field: "enable" },
-      { title: "Module", field: "name" },
-      {
-        title: "Info",
-        field: "infoCount",
-        numeric: true,
-        formatter: LogRecords
-      },
-      {
-        title: "Trace",
-        field: "traceCount",
-        numeric: true,
-        formatter: LogRecords
-      },
-      {
-        title: "Debug",
-        field: "debugCount",
-        numeric: true,
-        formatter: LogRecords
-      },
-      {
-        title: "Notice",
-        field: "noticeCount",
-        numeric: true,
-        formatter: LogRecords
-      },
-      {
-        title: "Warning",
-        field: "warningCount",
-        numeric: true,
-        formatter: LogRecords
-      },
-      {
-        title: "Error",
-        field: "errorCount",
-        numeric: true,
-        formatter: LogRecords
-      },
-      {
-        title: "Critical",
-        field: "criticalCount",
-        numeric: true,
-        formatter: LogRecords
-      }
-    ];
-    this.detailEntity = "log";
-    this.detailName = "Log";
-    this.detailPath = "/logs";
-    this.detailFormatter = true;
-  }
-  hasType = () => {
-    return true;
-  };
-
-  fetchRecord = (currentRecord, schema) => {
-    return new Promise(resolve => {
-      this.service.management.topology.fetchEntities(
-        currentRecord.nodeId,
-        { entity: "logStats" },
-        data => {
-          const record = data[currentRecord.nodeId]["logStats"];
-          const identityIndex = record.attributeNames.indexOf("name");
-          const result = record.results.find(
-            r => r[identityIndex] === currentRecord.name
-          );
-          let obj = this.service.utilities.flatten(
-            record.attributeNames,
-            result
-          );
-          obj = this.service.utilities.formatAttributes(
-            obj,
-            schema.entityTypes["logStats"]
-          );
-          resolve(obj);
-        }
-      );
-    });
-  };
-
-  doFetch = (page, perPage) => {
-    return new Promise(resolve => {
-      // an array of logStat records that have router name and log.enable added
-      let logModules = [];
-      const insertEnable = (record, logData) => {
-        // find the logData result for this record
-        const moduleIndex = logData.attributeNames.indexOf("module");
-        const enableIndex = logData.attributeNames.indexOf("enable");
-        const logRec = logData.results.find(
-          r => r[moduleIndex] === record.name
-        );
-        if (logRec) {
-          record.enable =
-            logRec[enableIndex] === null ? "" : String(logRec[enableIndex]);
-        } else {
-          record.enable = "";
-        }
-      };
-      this.service.management.topology.fetchAllEntities(
-        [{ entity: "log" }, { entity: "logStats" }],
-        nodes => {
-          // each router is a node in nodes
-          for (let node in nodes) {
-            const nodeName = this.service.utilities.nameFromId(node);
-            let response = nodes[node]["logStats"];
-            // response is an array of records for this node/router
-            response.results.forEach(result => {
-              // result is a single log record for this router
-              let logStat = this.service.utilities.flatten(
-                response.attributeNames,
-                result
-              );
-
-              logStat.node = nodeName;
-              logStat.nodeId = node;
-              insertEnable(logStat, nodes[node]["log"]);
-              logModules.push(logStat);
-            });
-          }
-          resolve({
-            data: logModules,
-            page,
-            perPage
-          });
-        }
-      );
-    });
-  };
-}
-
 export default LogsData;
diff --git a/console/react/src/details/dataSources/routerData.js b/console/react/src/details/dataSources/routerData.js
index eb4c39e..67c015e 100644
--- a/console/react/src/details/dataSources/routerData.js
+++ b/console/react/src/details/dataSources/routerData.js
@@ -43,9 +43,6 @@ class RouterData {
     this.detailEntity = "router";
     this.detailName = "Router";
   }
-  hasType = () => {
-    return true;
-  };
 
   fetchRecord = (currentRecord, schema) => {
     return new Promise(resolve => {
diff --git a/console/react/src/details/deleteEntity.js b/console/react/src/details/deleteEntity.js
new file mode 100644
index 0000000..01e1912
--- /dev/null
+++ b/console/react/src/details/deleteEntity.js
@@ -0,0 +1,143 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+*/
+
+import React from "react";
+import { Button, Modal } from "@patternfly/react-core";
+
+class DeleteEntity extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isModalOpen: false,
+      closing: false,
+      closed: false
+    };
+  }
+
+  handleModalShow = () => {
+    console.log("handleModalShow DELETE");
+    this.setState({ isModalOpen: true, closed: false });
+  };
+
+  handleModalHide = () => {
+    this.setState({ isModalOpen: false, closed: true }, () => {
+      if (this.props.cancelledAction) {
+        this.props.cancelledAction("DELETE");
+      }
+    });
+  };
+
+  getName = record => {
+    return record.name !== null
+      ? record.name
+      : `${this.props.entity}/${record.identity}`;
+  };
+
+  delete = () => {
+    this.setState({ closing: true }, () => {
+      const record = this.props.record;
+      const name = this.getName(record);
+      this.props.service.management.connection
+        .sendMethod(
+          record.nodeId || record.routerId,
+          this.props.entity,
+          { identity: record.identity },
+          "DELETE"
+        )
+        .then(results => {
+          let statusCode =
+            results.context.message.application_properties.statusCode;
+          if (statusCode < 200 || statusCode >= 300) {
+            const msg = `Deleted ${name} failed with message: ${results.context.message.application_properties.statusDescription}`;
+            console.log(`error ${msg}`);
+            this.props.handleAddNotification(
+              "action",
+              msg,
+              new Date(),
+              "danger"
+            );
+          } else {
+            const msg = `Deleted ${this.props.entity} ${name}`;
+            console.log(`success ${msg}`);
+            this.props.handleAddNotification(
+              "action",
+              msg,
+              new Date(),
+              "success"
+            );
+          }
+          this.setState(
+            { isModalOpen: false, closing: false, closed: true },
+            () => {
+              if (this.props.notifyClick) {
+                this.props.notifyClick("Done");
+              }
+            }
+          );
+        });
+    });
+  };
+
+  render() {
+    const { isModalOpen, closing } = this.state;
+    const record = this.props.record;
+    const name = this.getName(record);
+    return (
+      <React.Fragment>
+        {!this.props.showNow && (
+          <Button
+            className={`${this.props.asButton ? "" : "link-button"}`}
+            onClick={this.handleModalShow}
+          >
+            Delete
+          </Button>
+        )}
+        <Modal
+          isSmall
+          title={`Delete this ${this.props.entity}?`}
+          isOpen={isModalOpen || (this.props.showNow && !this.state.closed)}
+          onClose={this.handleModalHide}
+          actions={[
+            <Button
+              key="confirm"
+              variant="primary"
+              onClick={this.delete}
+              isDisabled={closing}
+            >
+              Delete
+            </Button>,
+            <Button
+              key="cancel"
+              variant="link"
+              onClick={this.handleModalHide}
+              isDisabled={closing}
+            >
+              Cancel
+            </Button>
+          ]}
+          isFooterLeftAligned
+        >
+          {closing ? `Deleting ${name}` : `${name}`}
+        </Modal>
+      </React.Fragment>
+    );
+  }
+}
+
+export default DeleteEntity;
diff --git a/console/react/src/details/enitiesPage.js b/console/react/src/details/enitiesPage.js
index 51529b9..f80b02f 100644
--- a/console/react/src/details/enitiesPage.js
+++ b/console/react/src/details/enitiesPage.js
@@ -23,6 +23,8 @@ import { Stack, StackItem } from "@patternfly/react-core";
 import { Split, SplitItem } from "@patternfly/react-core";
 
 import DetailsTablePage from "../detailsTablePage";
+import UpdateTablePage from "./updateTablePage";
+import CreateTablePage from "./createTablePage";
 import EntityListTable from "./entityListTable";
 import EntityList from "./entityList";
 import RouterSelect from "./routerSelect";
@@ -35,7 +37,8 @@ class EntitiesPage extends React.Component {
       loading: false,
       lastUpdated: new Date(),
       entity: null,
-      routerId: null
+      routerId: null,
+      showTable: "entities"
     };
     this.schema = this.props.service.management.schema();
   }
@@ -47,22 +50,57 @@ class EntitiesPage extends React.Component {
   // called from entityList to change entity summary
   handleSwitchEntity = entity => {
     if (this.listTableRef) this.listTableRef.reset();
-    this.setState({ entity, showDetails: false, detailsState: {} });
+    this.setState({ entity, showTable: "entities", detailsState: {} });
   };
 
-  // called from breadcrumb on entityListTable to return to current entity summary
+  // called from breadcrumb on detailsTablePage to return to current entity summary
   handleSelectEntity = entity => {
-    this.setState({ entity, showDetails: false });
+    this.setState({ entity, showTable: "entities" });
+  };
+
+  handleEntityAction = (action, record) => {
+    if (action === "Done") action = "entities";
+    this.setState({
+      actionState: {
+        currentRecord: record,
+        entity: this.props.entity
+      },
+      showTable: action
+    });
+  };
+
+  handleActionCancel = props => {
+    const { detailsState } = this.state;
+    const { page, sortBy, filterBy, perPage } = detailsState;
+    const extraInfo = { rowData: { data: props.locationState.currentRecord } };
+    if (!props.locationState.currentRecord) {
+      this.handleSwitchEntity(this.state.entity);
+    } else {
+      this.handleDetailClick(
+        props.locationState.currentRecord.name,
+        extraInfo,
+        {
+          page,
+          sortBy,
+          filterBy,
+          perPage
+        }
+      );
+    }
   };
 
   handleRouterSelected = routerId => {
-    this.setState({ routerId, showDetails: false });
+    this.setState({ routerId, showTable: "entities" });
   };
 
+  // clicked on 1st column in the entityTable
+  // show the details page for this record
   handleDetailClick = (value, extraInfo, stateInfo) => {
+    // pass along the current state of the entity table
+    // so we can restore it if the breadcrumb on the details page is clicked
     this.setState({
       detailsState: {
-        value: extraInfo.rowData.cells[extraInfo.columnIndex],
+        value: value,
         currentRecord: extraInfo.rowData.data,
         entity: this.props.entity,
         page: stateInfo.page,
@@ -71,14 +109,15 @@ class EntitiesPage extends React.Component {
         perPage: stateInfo.perPage,
         property: extraInfo.property
       },
-      showDetails: true
+      showTable: "details"
     });
   };
 
   render() {
+    const TABLE = this.state.showTable.toUpperCase();
     const entityTable = () => {
       if (this.state.entity) {
-        if (!this.state.showDetails) {
+        if (TABLE === "ENTITIES") {
           return (
             <EntityListTable
               ref={el => (this.listTableRef = el)}
@@ -89,9 +128,10 @@ class EntitiesPage extends React.Component {
               lastUpdated={this.lastUpdated}
               handleDetailClick={this.handleDetailClick}
               detailsState={this.state.detailsState}
+              handleEntityAction={this.handleEntityAction}
             />
           );
-        } else {
+        } else if (TABLE === "DETAILS") {
           return (
             <DetailsTablePage
               details={true}
@@ -101,6 +141,31 @@ class EntitiesPage extends React.Component {
               lastUpdated={this.lastUpdated}
               schema={this.schema}
               handleSelectEntity={this.handleSelectEntity}
+              handleEntityAction={this.handleEntityAction}
+            />
+          );
+        } else if (TABLE === "UPDATE") {
+          return (
+            <UpdateTablePage
+              entity={this.state.entity}
+              {...this.props}
+              schema={this.schema}
+              locationState={this.state.actionState}
+              handleSelectEntity={this.handleSelectEntity}
+              handleActionCancel={this.handleActionCancel}
+              handleEntityAction={this.handleEntityAction}
+            />
+          );
+        } else if (TABLE === "CREATE") {
+          return (
+            <CreateTablePage
+              entity={this.state.entity}
+              routerId={this.state.routerId}
+              {...this.props}
+              schema={this.schema}
+              locationState={this.state.actionState}
+              handleSelectEntity={this.handleSelectEntity}
+              handleActionCancel={this.handleActionCancel}
             />
           );
         }
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/entityData.js
index 351072d..53e6366 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/entityData.js
@@ -19,8 +19,10 @@ under the License.
 
 import AddressData from "./dataSources/addressData";
 import LinkData from "./dataSources/linkData";
+import AutoLinkData from "./dataSources/autoLink";
 import ListenerData from "./dataSources/listenerData";
 import ConnectionData from "./dataSources/connectionData";
+import LogsData from "./dataSources/logsData";
 
 import DefaultData from "./dataSources/defaultData";
 
@@ -28,7 +30,9 @@ const dataMap = {
   "router.address": AddressData,
   "router.link": LinkData,
   listener: ListenerData,
-  connection: ConnectionData
+  connection: ConnectionData,
+  log: LogsData,
+  autoLink: AutoLinkData
 };
 
 const defaultData = DefaultData;
diff --git a/console/react/src/details/entityListTable.js b/console/react/src/details/entityListTable.js
index 1aa3703..2ff0924 100644
--- a/console/react/src/details/entityListTable.js
+++ b/console/react/src/details/entityListTable.js
@@ -57,7 +57,8 @@ class EntityListTable extends React.Component {
       rows: [],
       redirect: false,
       redirectState: {},
-      hasChecked: false
+      action: null,
+      data: null
     };
     this.initDataSource();
     this.columns = [];
@@ -97,6 +98,9 @@ class EntityListTable extends React.Component {
     if (this.dataSource.extraFields) {
       this.dataSource.fields.push(...this.dataSource.extraFields);
     }
+    if (this.dataSource.actionColumn) {
+      this.dataSource.fields.push(this.dataSource.actionColumn);
+    }
   };
 
   setupFields = () => {
@@ -164,6 +168,9 @@ class EntityListTable extends React.Component {
   };
 
   detailLink = (value, extraInfo) => {
+    if (value === null) {
+      value = `${this.props.entity}/${extraInfo.rowData.data.identity}`;
+    }
     return (
       <Button
         className="link-button"
@@ -327,10 +334,8 @@ class EntityListTable extends React.Component {
       rows = [...this.state.rows];
       rows[rowId].selected = isSelected;
     }
-    const hasChecked = this.state.rows.some(row => row.selected);
     this.setState({
-      rows,
-      hasChecked
+      rows
     });
   };
 
@@ -351,21 +356,57 @@ class EntityListTable extends React.Component {
     );
   };
 
-  handleAction = action => {};
+  // an action was clicked on a row's kebab menu
+  handleAction = ({ action, rowData }) => {
+    console.log(`handleActions ${action}`);
+    console.log(rowData);
+
+    if (action === "UPDATE") {
+      this.props.handleEntityAction(action, rowData.data);
+    } else {
+      this.setState({ action, data: rowData.data });
+    }
+  };
+
+  cancelledAction = () => {
+    this.setState({ action: null });
+  };
+
+  // show the confirmation modal for an action
+  doAction = () => {
+    const props = {
+      showNow: true,
+      cancelledAction: this.cancelledAction,
+      ...this.props
+    };
+    return this.dataSource.actionButton({
+      action: this.state.action,
+      props: props,
+      click: this.didAction,
+      record: this.state.data,
+      i: 0,
+      asButton: false
+    });
+  };
+
+  // called by action modal after action is performed or cancelled
+  didAction = () => {
+    this.setState({ action: null, data: null }, this.update);
+  };
 
   render() {
     const tableProps = {
       cells: this.columns,
       rows: this.state.rows,
+      actions: this.dataSource.actionMenuItems(
+        this.props.entity,
+        this.handleAction
+      ),
       "aria-label": this.props.entity,
       sortBy: this.state.sortBy,
       onSort: this.onSort,
       variant: TableVariant.compact
     };
-    if (this.dataSource.actions(this.props.entity).includes("DELETE")) {
-      tableProps.onSelect = this.onSelect;
-      tableProps.canSelectAll = true;
-    }
 
     if (this.state.redirect) {
       return (
@@ -378,6 +419,25 @@ class EntityListTable extends React.Component {
       );
     }
 
+    // map of actions to buttons for the table toolbar
+    const actionButtons = () => {
+      // don't show UPDATE or DELETE for the entire list of records
+      const actions = this.dataSource
+        .actions(this.props.entity)
+        .filter(action => action !== "UPDATE" && action !== "DELETE");
+      const buttons = {};
+      actions.forEach((action, i) => {
+        buttons[action] = this.dataSource.actionButton({
+          action,
+          props: this.props,
+          click: this.handleAction,
+          i,
+          asButton: true
+        });
+      });
+      return buttons;
+    };
+
     return (
       <React.Fragment>
         <TableToolbar
@@ -391,15 +451,14 @@ class EntityListTable extends React.Component {
           filterBy={this.state.filterBy}
           handleChangeFilterValue={this.handleChangeFilterValue}
           hidePagination={true}
-          actions={this.dataSource.actions(this.props.entity)}
-          hasChecked={this.state.hasChecked}
-          handleAction={this.handleAction}
+          actionButtons={actionButtons()}
         />
         <Table {...tableProps}>
           <TableHeader />
           <TableBody />
         </Table>
         {this.renderPagination("bottom")}
+        {this.state.action && this.doAction()}
       </React.Fragment>
     );
   }
diff --git a/console/react/src/details/schema/schemaPage.js b/console/react/src/details/schema/schemaPage.js
new file mode 100644
index 0000000..8cd5e6c
--- /dev/null
+++ b/console/react/src/details/schema/schemaPage.js
@@ -0,0 +1,226 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+  Stack,
+  StackItem,
+  TextContent,
+  Text,
+  TextVariants
+} from "@patternfly/react-core";
+import { Card, CardBody } from "@patternfly/react-core";
+
+class SchemaPage extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      root: {
+        key: "entities",
+        title: "Schema entities",
+        description:
+          "List of management entities. Click on an entity to view its attributes.",
+        hidden: false
+      }
+    };
+    this.initRoot(this.state.root, this.props.schema.entityTypes);
+  }
+
+  initRoot = (root, schema) => {
+    root.attributes = [
+      {
+        key: Object.keys(schema).length,
+        value: "Entities"
+      }
+    ];
+    root.children = [];
+    const entities = Object.keys(schema).sort();
+    for (let i = 0; i < entities.length; i++) {
+      const entity = entities[i];
+      const child = { title: entity, key: entity };
+      this.initChild(child, schema[entity]);
+      root.children.push(child);
+    }
+  };
+
+  initChild = (child, obj) => {
+    child.hidden = true;
+    child.description = obj.description;
+    child.attributes = [];
+    if (obj.attributes) {
+      child.hidden = false;
+      child.attributes.push({
+        key: Object.keys(obj.attributes).length,
+        value: "Attributes"
+      });
+      child.children = [];
+      for (const attr in obj.attributes) {
+        const sub = { title: attr, key: `${child.key}-${attr}` };
+        this.initChild(sub, obj.attributes[attr]);
+        child.children.push(sub);
+      }
+    }
+    if (obj.operations) {
+      child.attributes.push({
+        key: "Operations",
+        value: `[${obj.operations.join(", ")}]`
+      });
+    }
+    if (obj.fullyQualifiedType) {
+      child.fqt = obj.fullyQualifiedType;
+    }
+    if (obj.type) {
+      child.attributes.push({
+        key: "type",
+        value:
+          obj.type.constructor === Array ? `[${obj.type.join(", ")}]` : obj.type
+      });
+    }
+    if (obj.default) {
+      child.attributes.push({ key: "default", value: obj.default });
+    }
+    if (obj.required) {
+      child.attributes.push({ key: "required", value: "" });
+    }
+    if (obj.unique) {
+      child.attributes.push({ key: "unique", value: "" });
+    }
+    if (obj.graph) {
+      child.attributes.push({ key: "statistic", value: "" });
+    }
+  };
+
+  toggleChildren = (event, parent) => {
+    event.stopPropagation();
+    if (parent.children && parent.children.length > 0) {
+      parent.children.forEach(child => {
+        child.hidden = !child.hidden;
+      });
+      this.setState({ root: this.state.root });
+    }
+  };
+
+  folderIconClass = item => {
+    if (item.children) {
+      return item.children.some(child => !child.hidden)
+        ? "pficon-folder-open"
+        : "pficon-folder-close";
+    }
+    return "pficon-catalog";
+  };
+
+  attrIconClass = attr => {
+    const attrMap = {
+      type: "pficon-builder-image",
+      Operations: "pficon-maintenance",
+      default: "pficon-info",
+      unique: "pficon-locked",
+      statistic: "fa fa-icon fa-tachometer"
+    };
+    if (attrMap[attr.key]) return `pficon ${attrMap[attr.key]}`;
+    return "pficon pficon-repository";
+  };
+
+  render() {
+    const TreeItem = itemInfo => {
+      return (
+        !itemInfo.hidden && (
+          <div
+            key={itemInfo.key}
+            className={`list-group-item-container container-fluid`}
+            onClick={event => this.toggleChildren(event, itemInfo)}
+          >
+            <div className="list-group-item">
+              <div className="list-group-item-header">
+                <div className="list-view-pf-main-info">
+                  <div className="list-view-pf-left">
+                    <span
+                      className={`pficon ${this.folderIconClass(
+                        itemInfo
+                      )} list-view-pf-icon-sm`}
+                    ></span>
+                  </div>
+                  <div className="list-view-pf-body">
+                    <div className="list-view-pf-description">
+                      <div className="list-group-item-heading">
+                        {itemInfo.title}
+                      </div>
+                      <div className="list-group-item-text">
+                        {itemInfo.description}
+                        {itemInfo.fqt && (
+                          <div className="list-group-item-fqt">
+                            {itemInfo.fqt}
+                          </div>
+                        )}
+                      </div>
+                    </div>
+                    <div className="list-view-pf-additional-info">
+                      {itemInfo.attributes &&
+                        itemInfo.attributes.map((attr, i) => (
+                          <div
+                            className="list-view-pf-additional-info-item"
+                            key={`${itemInfo.key}-${i}`}
+                          >
+                            <span className={this.attrIconClass(attr)}></span>
+                            <strong>{attr.key}</strong>
+                            {attr.value}
+                          </div>
+                        ))}
+                    </div>
+                  </div>
+                </div>
+              </div>
+              {itemInfo.children &&
+                itemInfo.children.map(childInfo => TreeItem(childInfo))}
+            </div>
+          </div>
+        )
+      );
+    };
+
+    return (
+      <PageSection variant={PageSectionVariants.light} id="schema-page">
+        <Stack>
+          <StackItem>
+            <TextContent>
+              <Text className="overview-title" component={TextVariants.h1}>
+                Schema
+              </Text>
+            </TextContent>
+          </StackItem>
+          <StackItem>
+            <Card>
+              <CardBody>
+                <div className="container-fluid">
+                  <div className="list-group tree-list-view-pf">
+                    {TreeItem(this.state.root)}
+                  </div>
+                </div>
+              </CardBody>
+            </Card>
+          </StackItem>
+        </Stack>
+      </PageSection>
+    );
+  }
+}
+
+export default SchemaPage;
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/updateEntity.js
similarity index 62%
copy from console/react/src/details/dataSources/connectionData.js
copy to console/react/src/details/updateEntity.js
index 568ac4c..01c4abb 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/updateEntity.js
@@ -17,23 +17,24 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import DefaultData from "./defaultData";
-import ConnectionClose from "../../connectionClose";
+import React from "react";
+import { Button } from "@patternfly/react-core";
 
-class ConnectionData extends DefaultData {
-  constructor(service, schema) {
-    super(service, schema);
-    this.extraFields = [
-      {
-        title: "",
-        field: "connection",
-        noSort: true,
-        formatter: ConnectionClose
-      }
-    ];
-    this.detailEntity = "router.link";
-    this.detailName = "Link";
+class UpdateEntity extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  handleClick = () => {
+    this.props.handleEntityAction("update", this.props.record);
+  };
+
+  render() {
+    console.log("rendering update button");
+    console.log(this.props);
+    return <Button onClick={this.handleClick}>Update</Button>;
   }
 }
 
-export default ConnectionData;
+export default UpdateEntity;
diff --git a/console/react/src/details/updateTablePage.js b/console/react/src/details/updateTablePage.js
new file mode 100644
index 0000000..d738fef
--- /dev/null
+++ b/console/react/src/details/updateTablePage.js
@@ -0,0 +1,337 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+  Button,
+  Stack,
+  StackItem,
+  TextContent,
+  Text,
+  TextVariants,
+  Breadcrumb,
+  BreadcrumbItem
+} from "@patternfly/react-core";
+
+import {
+  Form,
+  FormGroup,
+  TextInput,
+  FormSelectOption,
+  FormSelect,
+  Checkbox,
+  ActionGroup
+} from "@patternfly/react-core";
+
+import { cellWidth } from "@patternfly/react-table";
+import { Card, CardBody } from "@patternfly/react-core";
+import { Redirect } from "react-router-dom";
+import { dataMap as detailsDataMap, defaultData } from "./entityData";
+import { utils } from "../amqp/utilities";
+
+class UpdateTablePage extends React.Component {
+  constructor(props) {
+    super(props);
+    // if we get to this page and we don't have a props.location.state.entity
+    // then redirect back to the dashboard.
+    // this can happen if we get here from a bookmark or browser refresh
+    this.entity =
+      this.props.entity ||
+      (this.props &&
+        this.props.location &&
+        this.props.location.state &&
+        this.props.location.state.entity);
+
+    if (!this.entity) {
+      this.state.redirect = true;
+    } else {
+      this.dataSource = !detailsDataMap[this.entity]
+        ? new defaultData(this.props.service, this.props.schema)
+        : new detailsDataMap[this.entity](
+            this.props.service,
+            this.props.schema
+          );
+    }
+
+    this.state = {
+      columns: [
+        { title: "Attribute", transforms: [cellWidth(20)] },
+        {
+          title: "Value",
+          transforms: [cellWidth("max")],
+          props: { className: "pf-u-text-align-left" }
+        }
+      ],
+      rows: [],
+      redirect: false,
+      redirectState: { page: 1 },
+      redirectPath: "/dashboard",
+      lastUpdated: new Date(),
+      changes: false,
+      record: this.fixNull(this.props.locationState.currentRecord)
+    };
+    this.originalRecord = utils.copy(this.state.record);
+  }
+
+  fixNull = rec => {
+    const record = utils.copy(rec);
+    const attributes = this.dataSource.schemaAttributes(this.entity);
+    for (const attr in record) {
+      if (record[attr] === null) {
+        if (attributes[attr].type === "string") {
+          record[attr] = "";
+        } else if (attributes[attr].type === "integer") {
+          record[attr] = 0;
+        }
+      }
+    }
+    return record;
+  };
+
+  handleTextInputChange = (value, key) => {
+    const { record } = this.state;
+    record[key] = value;
+    let changes = false;
+    for (let attr in record) {
+      changes = changes || record[attr] !== this.originalRecord[attr];
+    }
+    this.setState({ record, changes });
+  };
+
+  schemaToForm = () => {
+    const record = this.state.record;
+    const attributes = this.dataSource.schemaAttributes(this.entity);
+    const formGroups = [];
+    for (let attributeKey in attributes) {
+      const attribute = attributes[attributeKey];
+      let type = attribute.type;
+      let options = [];
+      let readOnly = attributeKey === "identity";
+      if (type === "list") readOnly = true;
+      if (type === "integer" && attribute.graph) readOnly = true;
+      let required = attribute.required;
+      const value = record[attributeKey];
+      if (
+        this.dataSource.updateMetaData &&
+        this.dataSource.updateMetaData[attributeKey]
+      ) {
+        const override = this.dataSource.updateMetaData[attributeKey];
+        if (override.readOnly) {
+          type = "string";
+          readOnly = true;
+        }
+        if (override.type === "select") {
+          type = "select";
+          options = override.options;
+        }
+      }
+      const id = `form-${attributeKey}`;
+      const formGroupProps = {
+        label: attributeKey,
+        isRequired: required,
+        fieldId: id,
+        helperText: attribute.description
+      };
+      if (!readOnly) {
+        if (type === "string" || type === "integer") {
+          formGroups.push(
+            <FormGroup {...formGroupProps} key={attributeKey}>
+              <TextInput
+                value={record[attributeKey]}
+                isRequired={required}
+                type={type === "string" ? "text" : "number"}
+                id={id}
+                aria-describedby="entiy-form-field"
+                name={attributeKey}
+                isDisabled={readOnly}
+                onChange={value =>
+                  this.handleTextInputChange(value, attributeKey)
+                }
+              />
+            </FormGroup>
+          );
+        } else if (type === "select") {
+          formGroups.push(
+            <FormGroup {...formGroupProps} key={attributeKey}>
+              <FormSelect
+                value={value}
+                onChange={value =>
+                  this.handleTextInputChange(value, attributeKey)
+                }
+                id={id}
+                name={attributeKey}
+              >
+                {options.map((option, index) => (
+                  <FormSelectOption
+                    isDisabled={false}
+                    key={`${attributeKey}-${index}`}
+                    value={option}
+                    label={option}
+                  />
+                ))}
+              </FormSelect>
+            </FormGroup>
+          );
+        } else if (type === "boolean") {
+          formGroups.push(
+            <FormGroup {...formGroupProps} key={attributeKey}>
+              <Checkbox
+                isChecked={
+                  record[attributeKey] === null ? false : record[attributeKey]
+                }
+                onChange={value =>
+                  this.handleTextInputChange(value, attributeKey)
+                }
+                label={attributeKey}
+                id={id}
+                name={attributeKey}
+              />
+            </FormGroup>
+          );
+        }
+      }
+    }
+    return formGroups;
+  };
+
+  toString = val => {
+    return val === null ? "" : String(val);
+  };
+
+  icap = s => s.charAt(0).toUpperCase() + s.slice(1);
+
+  parentItem = () => this.state.record.name;
+
+  breadcrumbSelected = () => {
+    this.props.handleSelectEntity(this.entity);
+  };
+
+  handleCancel = () => {
+    this.props.handleActionCancel(this.props);
+  };
+
+  handleUpdate = () => {
+    const record = this.state.record;
+    const attributes = {};
+    // identity is needed to update the record
+    attributes["identity"] = record.identity;
+    // pass any other attributes that have changed
+    for (const attr in record) {
+      if (record[attr] !== this.originalRecord[attr]) {
+        attributes[attr] = record[attr];
+      } else if (attr === "outputFile")
+        attributes["outputFile"] =
+          record.outputFile === "" ? null : record.outputFile;
+    }
+    // call update
+    this.props.service.management.connection
+      .sendMethod(
+        record.routerId || record.nodeId,
+        this.entity,
+        attributes,
+        "UPDATE"
+      )
+      .then(results => {
+        let statusCode =
+          results.context.message.application_properties.statusCode;
+        if (statusCode < 200 || statusCode >= 300) {
+          const msg = `Updated ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`;
+          console.log(`error ${msg}`);
+          this.props.handleAddNotification("action", msg, new Date(), "danger");
+        } else {
+          const msg = `Updated ${this.props.entity} ${record.name}`;
+          console.log(`success ${msg}`);
+          this.props.handleAddNotification(
+            "action",
+            msg,
+            new Date(),
+            "success"
+          );
+        }
+        const props = this.props;
+        props.locationState.currentRecord = record;
+        this.props.handleActionCancel(props);
+      });
+  };
+
+  render() {
+    if (this.state.redirect) {
+      return (
+        <Redirect
+          to={{
+            pathname: this.state.redirectPath,
+            state: this.state.redirectState
+          }}
+        />
+      );
+    }
+
+    return (
+      <React.Fragment>
+        <PageSection
+          variant={PageSectionVariants.light}
+          className="overview-table-page"
+        >
+          <Stack>
+            <StackItem className="overview-header details">
+              <Breadcrumb>
+                <BreadcrumbItem
+                  className="link-button"
+                  onClick={this.breadcrumbSelected}
+                >
+                  {this.icap(this.entity)}
+                </BreadcrumbItem>
+              </Breadcrumb>
+
+              <TextContent className="details-table-header">
+                <Text className="overview-title" component={TextVariants.h1}>
+                  {this.parentItem()}
+                </Text>
+                <ActionGroup>
+                  <Button
+                    className="detail-action-button link-button"
+                    onClick={this.handleCancel}
+                  >
+                    Cancel
+                  </Button>
+                  <Button
+                    onClick={this.handleUpdate}
+                    isDisabled={!this.state.changes}
+                  >
+                    Update
+                  </Button>
+                </ActionGroup>
+              </TextContent>
+            </StackItem>
+            <StackItem id="update-form">
+              <Card>
+                <CardBody>
+                  <Form isHorizontal>{this.schemaToForm()}</Form>
+                </CardBody>
+              </Card>
+            </StackItem>
+          </Stack>
+        </PageSection>
+      </React.Fragment>
+    );
+  }
+}
+
+export default UpdateTablePage;
diff --git a/console/react/src/detailsTablePage.js b/console/react/src/detailsTablePage.js
index c253ca6..44d726d 100644
--- a/console/react/src/detailsTablePage.js
+++ b/console/react/src/detailsTablePage.js
@@ -80,13 +80,11 @@ class DetailTablesPage extends React.Component {
               this.props.service,
               this.props.schema
             );
-        this.locationState = this.props.locationState;
       } else {
         this.dataSource = new dataMap[this.entity](
           this.props.service,
           this.props.schema
         );
-        this.locationState = this.props.location.state;
       }
     }
   }
@@ -102,6 +100,12 @@ class DetailTablesPage extends React.Component {
     }
   };
 
+  locationState = () => {
+    return this.props.details
+      ? this.props.locationState
+      : this.props.location.state;
+  };
+
   update = () => {
     this.mapRows().then(
       rows => {
@@ -130,7 +134,7 @@ class DetailTablesPage extends React.Component {
       }
       this.dataSource
         .fetchRecord(
-          this.locationState.currentRecord,
+          this.locationState().currentRecord,
           this.props.schema,
           this.entity
         )
@@ -152,8 +156,7 @@ class DetailTablesPage extends React.Component {
 
   icap = s => s.charAt(0).toUpperCase() + s.slice(1);
 
-  parentItem = () =>
-    this.locationState.currentRecord[this.locationState.property];
+  parentItem = () => this.locationState().currentRecord.name;
 
   breadcrumbSelected = () => {
     if (this.props.details) {
@@ -162,11 +165,15 @@ class DetailTablesPage extends React.Component {
       this.setState({
         redirect: true,
         redirectPath: `/overview/${this.entity}`,
-        redirectState: this.locationState
+        redirectState: this.locationState()
       });
     }
   };
 
+  handleActionClicked = (action, record) => {
+    this.props.handleEntityAction(action, record);
+  };
+
   render() {
     if (this.state.redirect) {
       return (
@@ -179,6 +186,18 @@ class DetailTablesPage extends React.Component {
       );
     }
 
+    const actionsButtons = () => {
+      console.log("generating actionButtons for detailsTablePage");
+      console.log(this.locationState().currentRecord);
+      return this.dataSource.detailActions(
+        this.entity,
+        this.props,
+        this.locationState().currentRecord,
+        event =>
+          this.handleActionClicked(event, this.locationState().currentRecord)
+      );
+    };
+
     return (
       <React.Fragment>
         <PageSection
@@ -196,7 +215,7 @@ class DetailTablesPage extends React.Component {
                 </BreadcrumbItem>
               </Breadcrumb>
 
-              <TextContent>
+              <TextContent className="details-table-header">
                 <Text className="overview-title" component={TextVariants.h1}>
                   {this.parentItem()}
                 </Text>
@@ -206,6 +225,7 @@ class DetailTablesPage extends React.Component {
                     lastUpdated={this.state.lastUpdated}
                   />
                 )}
+                {this.props.details && actionsButtons()}
               </TextContent>
             </StackItem>
             <StackItem className="overview-table">
diff --git a/console/react/src/index.js b/console/react/src/index.js
index c1684e8..7af6900 100644
--- a/console/react/src/index.js
+++ b/console/react/src/index.js
@@ -3,7 +3,19 @@ import ReactDOM from "react-dom";
 import App from "./App";
 import * as serviceWorker from "./serviceWorker";
 
-ReactDOM.render(<App />, document.getElementById("root"));
+let config = { title: "Apache Qpid Dispatch Console" };
+fetch("/config.json")
+  .then(res => res.json())
+  .then(cfg => {
+    config = cfg;
+    console.log("successfully loaded console title from /config.json");
+  })
+  .catch(error => {
+    console.log("/config.json not found. Using default console title");
+  })
+  .finally(() =>
+    ReactDOM.render(<App config={config} />, document.getElementById("root"))
+  );
 
 // If you want your app to work offline and load faster, you can change
 // unregister() to register() below. Note this comes with some pitfalls.
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index 242d1d8..ebc6f43 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -55,6 +55,7 @@ import DetailsTablePage from "./detailsTablePage";
 import EntitiesPage from "./details/enitiesPage";
 import TopologyPage from "./topology/topologyPage";
 import MessageFlowPage from "./chord/chordPage";
+import SchemaPage from "./details/schema/schemaPage";
 import LogDetails from "./overview/logDetails";
 import { QDRService } from "./qdrService";
 import ConnectForm from "./connect-form";
@@ -125,7 +126,7 @@ class PageLayout extends React.Component {
     this.isDropdownOpen = false;
   };
 
-  handleConnect = (connectPath, connectInfo) => {
+  handleConnect = (connectPath, result) => {
     if (this.state.connected) {
       this.setState({ connectPath: "", connected: false }, () => {
         this.handleConnectCancel();
@@ -138,38 +139,31 @@ class PageLayout extends React.Component {
         );
       });
     } else {
-      const connectOptions = JSON.parse(JSON.stringify(connectInfo));
-      if (connectOptions.username === "") connectOptions.username = undefined;
-      if (connectOptions.password === "") connectOptions.password = undefined;
-      connectOptions.reconnect = true;
-
-      this.service.connect(connectOptions).then(
-        r => {
-          this.schema = this.service.schema;
-          if (connectPath === "/") connectPath = "/dashboard";
-          const activeItem = connectPath.split("/").pop();
-          // find the active group for this item
-          let activeGroup = "overview";
-          for (const group in this.nav) {
-            if (this.nav[group].some(item => item.name === activeItem)) {
-              activeGroup = group;
-              break;
-            }
-          }
-          this.handleConnectCancel();
-          this.handleAddNotification("event", "Connected", new Date(), "info");
-
-          this.setState({
-            activeItem,
-            activeGroup,
-            connected: true,
-            connectPath
-          });
-        },
-        e => {
-          console.log(e);
+      this.schema = this.service.schema;
+      if (connectPath === "/") connectPath = "/dashboard";
+      const activeItem = connectPath.split("/").pop();
+      // find the active group for this item
+      let activeGroup = "overview";
+      for (const group in this.nav) {
+        if (this.nav[group].some(item => item.name === activeItem)) {
+          activeGroup = group;
+          break;
         }
+      }
+      this.handleConnectCancel();
+      this.handleAddNotification(
+        "event",
+        `Console connected to router`,
+        new Date(),
+        "success"
       );
+
+      this.setState({
+        activeItem,
+        activeGroup,
+        connected: true,
+        connectPath
+      });
     }
   };
 
@@ -318,7 +312,7 @@ class PageLayout extends React.Component {
     const Header = (
       <PageHeader
         className="topology-header"
-        logo={<span className="logo-text">Apache Qpid Dispatch Console</span>}
+        logo={<span className="logo-text">{this.props.config.title}</span>}
         toolbar={PageToolbar}
         avatar={<Avatar src={avatarImg} alt="Avatar image" />}
         showNavToggle
@@ -384,6 +378,7 @@ class PageLayout extends React.Component {
       return (
         <ConnectForm
           ref={el => (this.connectFormRef = el)}
+          service={this.service}
           isConnectFormOpen={this.isConnectFormOpen}
           fromPath={"/"}
           handleConnect={this.handleConnect}
@@ -420,10 +415,20 @@ class PageLayout extends React.Component {
             <PrivateRoute path="/flow" component={MessageFlowPage} />
             <PrivateRoute path="/logs" component={LogDetails} />
             <PrivateRoute path="/entities" component={EntitiesPage} />
+            <PrivateRoute
+              path="/schema"
+              schema={this.schema}
+              component={SchemaPage}
+            />
             <Route
               path="/login"
               render={props => (
-                <ConnectPage {...props} handleConnect={this.handleConnect} />
+                <ConnectPage
+                  {...props}
+                  service={this.service}
+                  config={this.props.config}
+                  handleConnect={this.handleConnect}
+                />
               )}
             />
           </Switch>
@@ -434,23 +439,3 @@ class PageLayout extends React.Component {
 }
 
 export default PageLayout;
-
-/*          <ToolbarItem>
-            <ConnectForm prefix="toolbar" handleConnect={this.handleConnect} />
-          </ToolbarItem>
-
-
-
-            <Dropdown
-              isPlain
-              position="right"
-              onSelect={this.onDropdownSelect}
-              isOpen={isDropdownOpen}
-              toggle={
-                <DropdownToggle onToggle={this.onDropdownToggle}>
-                  anonymous
-                </DropdownToggle>
-              }
-              dropdownItems={userDropdownItems}
-            />
-          */
diff --git a/console/react/src/notificationDrawer.js b/console/react/src/notificationDrawer.js
index 0ade746..583d678 100644
--- a/console/react/src/notificationDrawer.js
+++ b/console/react/src/notificationDrawer.js
@@ -18,13 +18,13 @@ under the License.
 */
 
 import React from "react";
-import { NotificationBadge } from "@patternfly/react-core";
-import { Button } from "@patternfly/react-core";
 import {
   Accordion,
   AccordionItem,
   AccordionContent,
-  AccordionToggle
+  AccordionToggle,
+  Button,
+  NotificationBadge
 } from "@patternfly/react-core";
 
 import {
@@ -33,7 +33,7 @@ import {
   BellIcon,
   TimesIcon
 } from "@patternfly/react-icons";
-
+import AlertList from "./alertList";
 import { safePlural } from "./qdrGlobals";
 
 class NotificationDrawer extends React.Component {
@@ -55,6 +55,7 @@ class NotificationDrawer extends React.Component {
     this.severityToIcon = {
       info: { icon: "pficon-info", color: "#313131" },
       error: { icon: "pficon-error-circle-o", color: "red" },
+      danger: { icon: "pficon-error-circle-o", color: "red" },
       warning: { icon: "pficon-warning-triangle-o", color: "yellow" },
       success: { icon: "pficon-ok", color: "green" }
     };
@@ -92,7 +93,13 @@ class NotificationDrawer extends React.Component {
     });
     event.isRead = false;
     accordionSections[section].events.unshift(event);
-    this.setState({ accordionSections, isAnyUnread: true });
+    if (this.alertListRef) {
+      this.alertListRef.addAlert(severity, message);
+    }
+    this.setState({
+      accordionSections,
+      isAnyUnread: true
+    });
   };
 
   close = () => {
@@ -183,6 +190,7 @@ class NotificationDrawer extends React.Component {
             <BellIcon />
           </NotificationBadge>
         </div>
+        {<AlertList ref={el => (this.alertListRef = el)} />}
         {this.state.isShown && (
           <div
             ref={el => (this.notificationRef = el)}
diff --git a/console/react/src/overview/dashboard/dashboardPage.js b/console/react/src/overview/dashboard/dashboardPage.js
index 9feefcd..a7fca39 100644
--- a/console/react/src/overview/dashboard/dashboardPage.js
+++ b/console/react/src/overview/dashboard/dashboardPage.js
@@ -98,7 +98,10 @@ class DashboardPage extends React.Component {
                 <ActiveAddressesCard service={this.props.service} />
               </SplitItem>
               <SplitItem className="fill-card">
-                <DelayedDeliveriesCard service={this.props.service} />
+                <DelayedDeliveriesCard
+                  {...this.props}
+                  service={this.props.service}
+                />
               </SplitItem>
             </Split>
           </StackItem>
diff --git a/console/react/src/overview/dashboard/delayedDeliveriesCard.js b/console/react/src/overview/dashboard/delayedDeliveriesCard.js
index 72220b4..e6aaf4d 100644
--- a/console/react/src/overview/dashboard/delayedDeliveriesCard.js
+++ b/console/react/src/overview/dashboard/delayedDeliveriesCard.js
@@ -26,7 +26,11 @@ class DelayedDeliveriesCard extends React.Component {
 
   closeButton = (value, extraInfo) => {
     return (
-      <ConnectionClose extraInfo={extraInfo} service={this.props.service} />
+      <ConnectionClose
+        extraInfo={extraInfo}
+        {...this.props}
+        service={this.props.service}
+      />
     );
   };
 
diff --git a/console/react/src/pleaseWait.js b/console/react/src/pleaseWait.js
new file mode 100644
index 0000000..228538b
--- /dev/null
+++ b/console/react/src/pleaseWait.js
@@ -0,0 +1,51 @@
+import React from "react";
+import { TextContent, Text, TextVariants } from "@patternfly/react-core";
+import PropTypes from "prop-types";
+
+import { CogIcon } from "@patternfly/react-icons";
+
+class PleaseWait extends React.Component {
+  static propTypes = {
+    isOpen: PropTypes.bool.isRequired,
+    title: PropTypes.string.isRequired,
+    message: PropTypes.string.isRequired
+  };
+
+  state = {};
+
+  render() {
+    return (
+      this.props.isOpen && (
+        <div className="topic-creating-wrapper">
+          <div id="topicCogWrapper">
+            <CogIcon
+              id="topicCogMain"
+              className="spinning-clockwise"
+              color="#AAAAAA"
+            />
+            <CogIcon
+              id="topicCogUpper"
+              className="spinning-cclockwise"
+              color="#AAAAAA"
+            />
+            <CogIcon
+              id="topicCogLower"
+              className="spinning-cclockwise"
+              color="#AAAAAA"
+            />
+          </div>
+          <TextContent>
+            <Text component={TextVariants.h3}>{this.props.title}</Text>
+          </TextContent>
+          <TextContent>
+            <Text className="topic-creating-message" component={TextVariants.p}>
+              {this.props.message}
+            </Text>
+          </TextContent>
+        </div>
+      )
+    );
+  }
+}
+
+export default PleaseWait;
diff --git a/console/react/src/qdrGlobals.js b/console/react/src/qdrGlobals.js
index 8dc2b47..67076ad 100644
--- a/console/react/src/qdrGlobals.js
+++ b/console/react/src/qdrGlobals.js
@@ -17,8 +17,6 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import config from "./config.json";
-/* globals Promise */
 export var QDRFolder = (function() {
   function Folder(title) {
     this.title = title;
@@ -56,14 +54,6 @@ export const QDRTemplatePath = "html/";
 export const QDR_LAST_LOCATION = "QDRLastLocation";
 export const QDR_INTERVAL = "QDRInterval";
 
-export var getConfigVars = () =>
-  new Promise(resolve => {
-    const s = {};
-    s.QDR_CONSOLE_TITLE = config.title;
-    document.title = s.QDR_CONSOLE_TITLE;
-    resolve(s);
-  });
-
 export const safePlural = (count, str) => {
   if (count === 1) return str;
   var es = ["x", "ch", "ss", "sh"];
diff --git a/console/react/src/tableToolbar.jsx b/console/react/src/tableToolbar.jsx
index 69824d5..b8748bb 100644
--- a/console/react/src/tableToolbar.jsx
+++ b/console/react/src/tableToolbar.jsx
@@ -19,7 +19,6 @@ under the License.
 
 import React from "react";
 import {
-  Button,
   Dropdown,
   DropdownPosition,
   DropdownToggle,
@@ -116,28 +115,13 @@ class TableToolbar extends React.Component {
   };
 
   render() {
-    const actions =
-      this.props.actions &&
-      this.props.actions.map(action => {
-        let variant = "primary";
-        let isDisabled = false;
-        if (action === "DELETE" && !this.props.hasChecked) {
-          variant = "tertiary";
-          isDisabled = true;
-        }
-        return (
-          <ToolbarItem className="pf-u-mx-md" key={action}>
-            <Button
-              aria-label={action}
-              onClick={() => this.props.handleAction(action)}
-              variant={variant}
-              isDisabled={isDisabled}
-            >
-              {action}
-            </Button>
-          </ToolbarItem>
-        );
-      });
+    const actionsButtons =
+      this.props.actionButtons &&
+      Object.keys(this.props.actionButtons).map(action => (
+        <ToolbarItem className="pf-u-mx-md" key={`toolbar-item-${action}`}>
+          {this.props.actionButtons[action]}
+        </ToolbarItem>
+      ));
 
     return (
       <Toolbar className="pf-l-toolbar pf-u-mx-xl pf-u-my-md table-toolbar">
@@ -149,7 +133,9 @@ class TableToolbar extends React.Component {
             {this.buildSearchBox()}
           </ToolbarItem>
         </ToolbarGroup>
-        {this.props.actions && <ToolbarGroup>{actions}</ToolbarGroup>}
+        {this.props.actionButtons && (
+          <ToolbarGroup>{actionsButtons}</ToolbarGroup>
+        )}
         {!this.props.hidePagination && (
           <ToolbarGroup className="toolbar-pagination">
             <ToolbarItem>
diff --git a/console/react/yarn.lock b/console/react/yarn.lock
index 78edc2e..59b7b90 100644
--- a/console/react/yarn.lock
+++ b/console/react/yarn.lock
@@ -4830,7 +4830,7 @@ debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
   dependencies:
     ms "^2.1.1"
 
-debuglog@*, debuglog@^1.0.1:
+debuglog@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
   integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -7108,7 +7108,7 @@ import-local@^2.0.0:
     pkg-dir "^3.0.0"
     resolve-cwd "^2.0.0"
 
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
   integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -8677,11 +8677,6 @@ lodash-es@^4.17.11:
   resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
   integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
 
-lodash._baseindexof@*:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
-  integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
-
 lodash._baseuniq@~4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -8690,33 +8685,11 @@ lodash._baseuniq@~4.6.0:
     lodash._createset "~4.0.0"
     lodash._root "~3.0.0"
 
-lodash._bindcallback@*:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
-  integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
-
-lodash._cacheindexof@*:
-  version "3.0.2"
-  resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
-  integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
-
-lodash._createcache@*:
-  version "3.1.2"
-  resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
-  integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
-  dependencies:
-    lodash._getnative "^3.0.0"
-
 lodash._createset@~4.0.0:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
   integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
 
-lodash._getnative@*, lodash._getnative@^3.0.0:
-  version "3.9.1"
-  resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
-  integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
-
 lodash._reinterpolate@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -8777,11 +8750,6 @@ lodash.memoize@^4.1.2:
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
-lodash.restparam@*:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
-  integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
-
 lodash.set@^4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
diff --git a/python/qpid_dispatch_internal/dispatch.py b/python/qpid_dispatch_internal/dispatch.py
index 8b86dae..b6e5d5f 100644
--- a/python/qpid_dispatch_internal/dispatch.py
+++ b/python/qpid_dispatch_internal/dispatch.py
@@ -71,6 +71,7 @@ class QdDll(ctypes.PyDLL):
         self._prototype(self.qd_connection_manager_delete_listener, None, [self.qd_dispatch_p, ctypes.c_void_p])
         self._prototype(self.qd_connection_manager_delete_connector, None, [self.qd_dispatch_p, ctypes.c_void_p])
         self._prototype(self.qd_connection_manager_delete_ssl_profile, ctypes.c_bool, [self.qd_dispatch_p, ctypes.c_void_p])
+        self._prototype(self.qd_connection_manager_delete_sasl_plugin, ctypes.c_bool, [self.qd_dispatch_p, ctypes.c_void_p])
 
         self._prototype(self.qd_dispatch_configure_address, None, [self.qd_dispatch_p, py_object])
         self._prototype(self.qd_dispatch_configure_link_route, None, [self.qd_dispatch_p, py_object])


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org