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/03 18:51:47 UTC

[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Removed unused files. Added notification drawer

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 8c5e99d  Removed unused files. Added notification drawer
8c5e99d is described below

commit 8c5e99dc44abec23a24007810e139861b5d48921
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Sun Nov 3 13:51:28 2019 -0500

    Removed unused files. Added notification drawer
---
 console/react/src/App.css                          | 121 +++++++
 console/react/src/App.js                           |   1 +
 console/react/src/REST.js                          | 169 ----------
 console/react/src/connectionClose.js               | 120 +++++--
 .../connectionData.js}                             |  30 +-
 console/react/src/details/enitiesPage.js           |   4 +-
 console/react/src/details/entityData.js            |   4 +-
 console/react/src/details/entityListTable.js       |   9 +-
 console/react/src/edge-table-pagination.js         |  24 --
 console/react/src/edge-table-toolbar.js            | 133 --------
 console/react/src/edge-table.js                    | 289 -----------------
 console/react/src/empty-edge-class-table.js        |  43 ---
 console/react/src/empty-selection.js               |  33 --
 console/react/src/field-details.js                 | 225 -------------
 console/react/src/graph.js                         | 358 ---------------------
 console/react/src/layout.js                        |  38 ++-
 console/react/src/network-name.js                  |  67 ----
 console/react/src/nodes.js                         | 260 ---------------
 console/react/src/nodeslinks.js                    | 144 ---------
 console/react/src/notificationDrawer.js            | 293 +++++++++++++++++
 console/react/src/qdrGlobals.js                    |  11 +
 console/react/src/show-d3-svg.js                   | 236 --------------
 console/react/src/topology-context.js              | 139 --------
 23 files changed, 577 insertions(+), 2174 deletions(-)

diff --git a/console/react/src/App.css b/console/react/src/App.css
index 758ca39..b1eff31 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -1176,3 +1176,124 @@ span.entity-type i.link-type-router-control:before {
 .details-header {
   border-bottom: 1px solid #eaeaea;
 }
+
+#NotificationDrawer {
+  position: absolute;
+  top: 4.8em;
+  color: black;
+  right: 0em;
+}
+
+.drawer-pf-title {
+  display: flex;
+  border: 1px solid lightgray;
+  background-color: #f3f3f3;
+  height: 2em;
+  justify-content: space-between;
+  position: unset;
+}
+
+#NotificationDrawer.compact {
+  transition: width 0.25s ease;
+  width: 20em;
+}
+
+#NotificationDrawer.expanded {
+  transition: width 0.25s ease;
+  width: 50em;
+}
+
+.drawer-pf-title h3 {
+  margin: 0.5em 0;
+}
+
+.drawer-pf-title svg {
+  fill: #333333;
+}
+
+.notification-button dl.pf-c-accordion {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.notification-button dl.pf-c-accordion * {
+  border-left-color: transparent;
+}
+
+.notification-button dl.pf-c-accordion dt {
+  background-image: linear-gradient(to bottom, #fafafa 0, #ededed 100%);
+  background-repeat: repeat-x;
+}
+
+.notification-button dl.pf-c-accordion h3 {
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+.notification-button button.pf-c-accordion__toggle span {
+  font-weight: normal;
+  display: block;
+  font-size: 14px;
+  line-height: normal;
+}
+
+.notification-button button.pf-c-accordion__toggle {
+  display: flex;
+}
+
+.notification-button .pf-c-accordion__toggle svg {
+  position: absolute;
+  left: 0.5em;
+}
+
+.notification-button button.pf-c-accordion__toggle {
+  text-align: center;
+}
+
+.notification-button .pf-c-accordion__toggle-text {
+  margin: auto;
+}
+
+.drawer-pf-notification {
+  display: flex;
+  justify-content: space-between;
+}
+
+.drawer-pf-notification-info,
+.drawer-pf-notification-message {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+.drawer-pf-notification-content {
+  display: flex;
+  flex-direction: column;
+  margin: auto;
+}
+
+#NotificationDrawer .pf-c-accordion__expanded-content-body {
+  padding: 0;
+}
+
+#NotificationDrawer .drawer-pf-action-link {
+  border-left: 1px solid #d1d1d1;
+}
+
+#NotificationDrawer button.pf-m-expanded,
+#NotificationDrawer dd.pf-m-expanded {
+  border-left-color: blue;
+}
+
+#NotificationDrawer .panel-body {
+  padding-left: 0;
+}
+
+#NotificationDrawer .drawer-pf-notification {
+  padding-left: 15px;
+  font-size: 16px;
+}
+
+#NotificationDrawer .drawer-pf-notification.unread {
+  font-weight: bold;
+  color: black;
+}
diff --git a/console/react/src/App.js b/console/react/src/App.js
index cb56d14..6f66bb7 100644
--- a/console/react/src/App.js
+++ b/console/react/src/App.js
@@ -3,6 +3,7 @@ import "@patternfly/patternfly/patternfly.css";
 import "@patternfly/patternfly/patternfly-addons.css";
 
 import "patternfly/dist/css/patternfly.css";
+import "patternfly/dist/css/patternfly-additions.css";
 import "@patternfly/patternfly/components/Nav/nav.css";
 import "./App.css";
 import PageLayout from "./layout";
diff --git a/console/react/src/REST.js b/console/react/src/REST.js
deleted file mode 100644
index 427ac22..0000000
--- a/console/react/src/REST.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2019 Red Hat Inc. A division of IBM
- *
- * 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.
- */
-
-/* The web front-end will use this class to talk to the backend (/public/app.js)
- */
-class REST {
-  constructor() {
-    this.url = `${window.location.protocol}//${window.location.host}`;
-  }
-
-  getRouterState = router => {
-    const body = {
-      what: "getState",
-      router: router
-    };
-    return this.doPost(body);
-  };
-
-  saveNetwork = networkInfo => {
-    const body = {
-      what: "saveNetwork",
-      network: networkInfo
-    };
-    return this.doPost(body);
-  };
-
-  doPost = body =>
-    new Promise((resolve, reject) => {
-      fetch(`${this.url}/api`, {
-        headers: {
-          Accept: "application/json",
-          "Content-Type": "application/json"
-        },
-        method: "POST",
-        body: JSON.stringify(body)
-      })
-        .then(response => {
-          if (response.status < 200 || response.status > 299) {
-            reject(response.statusText);
-            return {};
-          }
-          return response.json();
-        })
-        .then(myJson => {
-          resolve(myJson);
-        })
-        // network error?
-        .catch(error => reject(error));
-    });
-
-  // example of how to periodically do a GET request
-  examplePoll = () =>
-    new Promise((resolve, reject) => {
-      // strategy defines how we want to handle various return codes
-      // 200 means OK, in this example we want to resolve
-      // 404 means NOT_FOUND, in this example we want to resolve so caller knows it wasn't found
-      // 500 means there was a communication error, in this example we want to reject
-      const strategy = { "200": "resolve", "404": "resolve", "500": "reject" };
-      // call GET until the return code is what you want or the timeout is reached
-      poll(`${this.url}/api`, strategy).then(
-        res => {
-          resolve(res);
-        },
-        e => {
-          reject(e);
-        }
-      );
-    });
-
-  exampleDelete = name =>
-    new Promise((resolve, reject) => {
-      // console.log(` *** deleting ${name} ***`);
-      fetch(`${this.url}/api/${name}`, {
-        method: "DELETE"
-      }).then(() => {
-        // status 200 means the thing we want to delete is still there, so keep waiting
-        // status 404 means the thing we want to delete is now gone, so resolve
-        // status 500 is an error
-        const strategy = { "200": "wait", "404": "resolve", "500": "reject" };
-        // call GET for name until it returns that the name is gone or times out
-        poll(`${this.url}/api/${name}`, strategy).then(
-          res => {
-            resolve(res);
-          },
-          e => {
-            reject(e);
-          }
-        );
-      });
-    });
-
-  exampleBatch(names) {
-    return new Promise((resolve, reject) => {
-      Promise.all(names.map(name => this.exampleDelete(name))).then(
-        () => {
-          resolve();
-        },
-        firstError => {
-          reject(firstError);
-        }
-      );
-    });
-  }
-}
-
-// poll for a condition
-const poll = (url, strategy, timeout, interval) => {
-  const endTime = Number(new Date()) + (timeout || 10000);
-  interval = interval || 1000;
-  const s200 = strategy["200"];
-  const s404 = strategy["404"];
-  const s500 = strategy["500"];
-  let lastStatus = 0;
-
-  const checkCondition = (resolve, reject) => {
-    // If the condition is met, we're done!
-    fetch(url)
-      .then(res => {
-        lastStatus = res.status;
-        const ret = {};
-        // decide whether to resolve, reject, or wait
-        if (res.status >= 200 && res.status <= 299) {
-          ret[s200] = res.json();
-          return ret;
-        } else if (res.status === 404) {
-          ret[s404] = [];
-          return ret;
-        }
-        ret[s500] = res.status;
-        return ret;
-      })
-      .then(json => {
-        if (json.resolve) {
-          resolve(json.resolve);
-        } else if (json.reject) {
-          reject(json.reject);
-        }
-        // If the condition isn't met but the timeout hasn't elapsed, go again
-        else if (Number(new Date()) < endTime) {
-          setTimeout(checkCondition, interval, resolve, reject);
-        }
-        // Didn't match and too much time, reject!
-        else {
-          const msg = { message: "timeout", status: lastStatus };
-          reject(new Error(JSON.stringify(msg)));
-        }
-      })
-      .catch(e => {
-        console.log(`poll caught error ${e}`);
-        reject(e);
-      });
-  };
-  return new Promise(checkCondition);
-};
-
-export default REST;
diff --git a/console/react/src/connectionClose.js b/console/react/src/connectionClose.js
index 7b64f1f..9da3d13 100644
--- a/console/react/src/connectionClose.js
+++ b/console/react/src/connectionClose.js
@@ -18,40 +18,106 @@ under the License.
 */
 
 import React from "react";
-import { Button } from "@patternfly/react-core";
+import { Button, Modal } from "@patternfly/react-core";
 
 class ConnectionClose extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isModalOpen: false,
+      closing: false
+    };
+  }
+
+  handleModalToggle = () => {
+    this.setState(({ isModalOpen }) => ({
+      isModalOpen: !isModalOpen
+    }));
+  };
+
   closeConnection = () => {
-    const record = this.props.extraInfo.rowData.data;
-    this.props.service.management.connection
-      .sendMethod(
-        record.nodeId || record.routerId,
-        "connection",
-        { adminStatus: "deleted", identity: record.identity },
-        "UPDATE",
-        { adminStatus: "deleted" }
-      )
-      .then(results => {
-        let statusCode =
-          results.context.message.application_properties.statusCode;
-        if (statusCode < 200 || statusCode >= 300) {
-          console.log(
-            `error ${record.name} ${results.context.message.application_properties.statusDescription}`
-          );
-        } else {
-          console.log(
-            `success ${record.name} ${results.context.message.application_properties.statusDescription}`
-          );
-        }
-      });
+    this.setState({ closing: true }, () => {
+      const record = this.props.extraInfo.rowData.data;
+      this.props.service.management.connection
+        .sendMethod(
+          record.nodeId || record.routerId,
+          "connection",
+          { adminStatus: "deleted", identity: record.identity },
+          "UPDATE",
+          { adminStatus: "deleted" }
+        )
+        .then(results => {
+          let statusCode =
+            results.context.message.application_properties.statusCode;
+          if (statusCode < 200 || statusCode >= 300) {
+            console.log(
+              `error ${record.name} ${results.context.message.application_properties.statusDescription}`
+            );
+            this.props.handleAddNotification(
+              "action",
+              `Close connection ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`,
+              new Date(),
+              "error"
+            );
+          } else {
+            this.props.handleAddNotification(
+              "action",
+              `Closed connection ${record.name}`,
+              new Date(),
+              "success"
+            );
+            console.log(
+              `success ${record.name} ${results.context.message.application_properties.statusDescription}`
+            );
+          }
+          this.setState({ isModalOpen: false, closing: false }, () => {
+            if (this.props.notifyClick) {
+              this.props.notifyClick();
+            }
+          });
+        });
+    });
   };
 
   render() {
-    if (this.props.extraInfo.rowData.data.role === "normal") {
+    const { isModalOpen, closing } = this.state;
+    const record = this.props.extraInfo.rowData.data;
+    if (record.role === "normal") {
       return (
-        <Button className="link-button" onClick={this.closeConnection}>
-          Close
-        </Button>
+        <React.Fragment>
+          <Button className="link-button" onClick={this.handleModalToggle}>
+            Close
+          </Button>
+          <Modal
+            isSmall
+            title="Close conection"
+            isOpen={isModalOpen}
+            onClose={this.handleModalToggle}
+            actions={[
+              <Button
+                key="confirm"
+                variant="primary"
+                onClick={this.closeConnection}
+                isDisabled={closing}
+              >
+                Confirm
+              </Button>,
+              <Button
+                key="cancel"
+                variant="link"
+                onClick={this.handleModalToggle}
+                isDisabled={closing}
+              >
+                Cancel
+              </Button>
+            ]}
+            isFooterLeftAligned
+          >
+            {closing
+              ? `Closing connection ${record.name}`
+              : `Close connection ${record.name}?`}
+          </Modal>
+        </React.Fragment>
       );
     } else {
       return <React.Fragment />;
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/dataSources/connectionData.js
similarity index 62%
copy from console/react/src/details/entityData.js
copy to console/react/src/details/dataSources/connectionData.js
index 8ab6fc2..568ac4c 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/dataSources/connectionData.js
@@ -17,17 +17,23 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import AddressData from "./dataSources/addressData";
-import LinkData from "./dataSources/linkData";
-import ListenerData from "./dataSources/listenerData";
+import DefaultData from "./defaultData";
+import ConnectionClose from "../../connectionClose";
 
-import DefaultData from "./dataSources/defaultData";
+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";
+  }
+}
 
-const dataMap = {
-  "router.address": AddressData,
-  "router.link": LinkData,
-  listener: ListenerData
-};
-
-const defaultData = DefaultData;
-export { dataMap, defaultData };
+export default ConnectionData;
diff --git a/console/react/src/details/enitiesPage.js b/console/react/src/details/enitiesPage.js
index 6392968..51529b9 100644
--- a/console/react/src/details/enitiesPage.js
+++ b/console/react/src/details/enitiesPage.js
@@ -82,7 +82,7 @@ class EntitiesPage extends React.Component {
           return (
             <EntityListTable
               ref={el => (this.listTableRef = el)}
-              service={this.props.service}
+              {...this.props}
               entity={this.state.entity}
               schema={this.schema}
               routerId={this.state.routerId}
@@ -97,7 +97,7 @@ class EntitiesPage extends React.Component {
               details={true}
               locationState={this.state.detailsState}
               entity={this.state.entity}
-              service={this.props.service}
+              {...this.props}
               lastUpdated={this.lastUpdated}
               schema={this.schema}
               handleSelectEntity={this.handleSelectEntity}
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/entityData.js
index 8ab6fc2..351072d 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/entityData.js
@@ -20,13 +20,15 @@ under the License.
 import AddressData from "./dataSources/addressData";
 import LinkData from "./dataSources/linkData";
 import ListenerData from "./dataSources/listenerData";
+import ConnectionData from "./dataSources/connectionData";
 
 import DefaultData from "./dataSources/defaultData";
 
 const dataMap = {
   "router.address": AddressData,
   "router.link": LinkData,
-  listener: ListenerData
+  listener: ListenerData,
+  connection: ConnectionData
 };
 
 const defaultData = DefaultData;
diff --git a/console/react/src/details/entityListTable.js b/console/react/src/details/entityListTable.js
index 287d872..1aa3703 100644
--- a/console/react/src/details/entityListTable.js
+++ b/console/react/src/details/entityListTable.js
@@ -31,7 +31,7 @@ import { Redirect } from "react-router-dom";
 import TableToolbar from "../tableToolbar";
 import { dataMap, defaultData } from "./entityData";
 
-// If the breadcrumb on the details page was used to return to this page,
+// If the breadcrumb on the detailsTablePage was used to return to this page,
 // we will have saved state info in props.location.state
 const propFromLocation = (props, which, defaultValue) => {
   return props &&
@@ -203,12 +203,17 @@ class EntityListTable extends React.Component {
       <Component
         value={value}
         extraInfo={extraInfo}
-        service={this.props.service}
         detailClick={this.detailClick}
+        notifyClick={this.notifyClick}
+        {...this.props}
       />
     );
   };
 
+  notifyClick = () => {
+    this.update();
+  };
+
   onSort = (_event, index, direction) => {
     this.setState({ sortBy: { index, direction } }, () => {
       const { allRows, page, perPage } = this.state;
diff --git a/console/react/src/edge-table-pagination.js b/console/react/src/edge-table-pagination.js
deleted file mode 100644
index 9ddac74..0000000
--- a/console/react/src/edge-table-pagination.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from "react";
-import { Pagination } from "@patternfly/react-core";
-
-class EdgeTablePagination extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {};
-  }
-
-  render() {
-    return (
-      <Pagination
-        itemCount={this.props.rows}
-        perPage={this.props.perPage}
-        page={this.props.page}
-        onSetPage={this.props.onSetPage}
-        widgetId="pagination-options-menu-top"
-        onPerPageSelect={this.props.onPerPageSelect}
-      />
-    );
-  }
-}
-
-export default EdgeTablePagination;
diff --git a/console/react/src/edge-table-toolbar.js b/console/react/src/edge-table-toolbar.js
deleted file mode 100644
index 2afec75..0000000
--- a/console/react/src/edge-table-toolbar.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import React from "react";
-import {
-  Button,
-  ButtonVariant,
-  InputGroup,
-  TextInput,
-  Toolbar,
-  ToolbarGroup,
-  ToolbarItem
-} from "@patternfly/react-core";
-import {
-  SearchIcon,
-  SortAlphaDownIcon,
-  SortAlphaUpIcon
-} from "@patternfly/react-icons";
-import Confirm from "./confirm";
-
-class EdgeTableToolbar extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      isDropDownOpen: false,
-      isKebabOpen: false,
-      searchValue: ""
-    };
-    this.handleTextInputChange = searchValue => {
-      this.setState({ searchValue });
-    };
-
-    this.onDropDownToggle = isOpen => {
-      this.setState({
-        isDropDownOpen: isOpen
-      });
-    };
-
-    this.onDropDownSelect = event => {
-      this.setState({
-        isDropDownOpen: !this.state.isDropDownOpen
-      });
-    };
-
-    this.onKebabToggle = isOpen => {
-      this.setState({
-        isKebabOpen: isOpen
-      });
-    };
-
-    this.onKebabSelect = event => {
-      this.setState({
-        isKebabOpen: !this.state.isKebabOpen
-      });
-    };
-
-    this.buildSearchBox = () => {
-      return (
-        <InputGroup>
-          <TextInput
-            value={this.props.filterText}
-            type="search"
-            onChange={this.props.handleChangeFilter}
-            aria-label="search text input"
-            placeholder="search for edge namespaces"
-          />
-          <Button
-            variant={ButtonVariant.tertiary}
-            aria-label="search button for search input"
-          >
-            <SearchIcon />
-          </Button>
-        </InputGroup>
-      );
-    };
-  }
-
-  isDeleteDisabled = () => this.props.rows.every(r => !r.selected);
-  deleteText = () => {
-    return (
-      <React.Fragment>
-        <h2>Are you sure you want to delete:</h2>
-        <ul>
-          {this.props.rows
-            .filter(r => r.selected)
-            .map((r, i) => {
-              return <li key={`key-${i}`}>{r.name}</li>;
-            })}
-        </ul>
-      </React.Fragment>
-    );
-  };
-  render() {
-    return (
-      <Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
-        <ToolbarGroup>
-          <ToolbarItem className="pf-u-mr-md">
-            {this.buildSearchBox()}
-          </ToolbarItem>
-          <ToolbarItem>
-            <Button
-              variant="plain"
-              onClick={this.props.toggleAlphaSort}
-              aria-label="Sort A-Z"
-            >
-              {this.props.sortDown ? (
-                <SortAlphaDownIcon />
-              ) : (
-                <SortAlphaUpIcon />
-              )}
-            </Button>
-          </ToolbarItem>
-        </ToolbarGroup>
-        <ToolbarGroup className="edge-table-actions">
-          <ToolbarItem className="pf-u-mx-sm">
-            <Confirm
-              handleConfirm={this.props.handleDeleteEdge}
-              buttonText="Delete"
-              title="Confirm delete"
-              isDeleteDisabled={this.isDeleteDisabled()}
-            >
-              {this.deleteText()}
-            </Confirm>
-          </ToolbarItem>
-          <ToolbarItem className="pf-u-mx-sm">
-            <Button aria-label="Add" onClick={this.props.handleAddEdge}>
-              Add
-            </Button>
-          </ToolbarItem>
-        </ToolbarGroup>
-      </Toolbar>
-    );
-  }
-}
-
-export default EdgeTableToolbar;
diff --git a/console/react/src/edge-table.js b/console/react/src/edge-table.js
deleted file mode 100644
index ef9d7ed..0000000
--- a/console/react/src/edge-table.js
+++ /dev/null
@@ -1,289 +0,0 @@
-import React from "react";
-import { Button, ClipboardCopy, TextInput } from "@patternfly/react-core";
-import {
-  cellWidth,
-  Table,
-  TableHeader,
-  TableBody,
-  TableVariant
-} from "@patternfly/react-table";
-import EdgeTableToolbar from "./edge-table-toolbar";
-import EdgeTablePagination from "./edge-table-pagination";
-import EmptyEdgeClassTable from "./empty-edge-class-table";
-import Graph from "./graph";
-import { RouterStates } from "./nodes";
-
-const YAML = 0;
-const STATE_ICON = 1;
-const STATE_TEXT = 2;
-const NAME = 3;
-
-class EdgeTable extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      columns: [
-        {
-          title: "yaml",
-          cellFormatters: [this.formatYaml],
-          transforms: [cellWidth(5)]
-        },
-        {
-          title: "State",
-          cellFormatters: [this.formatState],
-          transforms: [cellWidth(5)]
-        },
-        { title: "", cellFormatters: [this.formatStateDescription] },
-        { title: "Name", cellFormatters: [this.formatName] }
-      ],
-      filterText: "",
-      sortDown: true,
-      editingEdgeRow: -1,
-      page: 1,
-      perPage: 5
-    };
-
-    this.rows = [];
-  }
-
-  onSelect = (event, isSelected, rowId) => {
-    // the internal rows array may be different from the props.rows array
-    const realRowIndex =
-      rowId >= 0
-        ? this.props.rows.findIndex(r => r.key === this.rows[rowId].key)
-        : rowId;
-    this.props.handleSelectEdgeRow(realRowIndex, isSelected);
-  };
-
-  handleEdgeNameBlur = () => {
-    this.onSelect("", false, -1);
-    this.setState({ editingEdgeRow: -1 });
-  };
-
-  handleEdgeNameClick = rowIndex => {
-    this.onSelect("", true, rowIndex);
-    this.setState({ editingEdgeRow: rowIndex });
-  };
-
-  handleEdgeKeyPress = event => {
-    if (event.key === "Enter") {
-      this.handleEdgeNameBlur();
-    }
-  };
-
-  formatYaml = (value, _xtraInfo) => {
-    const cells = _xtraInfo.rowData.cells;
-    let yaml = <div className="state-placeholder"></div>;
-    if (cells[0] && cells[1] === 1) {
-      yaml = (
-        <ClipboardCopy
-          className="state-copy"
-          onClick={(event, text) => {
-            const clipboard = event.currentTarget.parentElement;
-            const el = document.createElement("input");
-            el.value = JSON.stringify(cells[0]);
-            clipboard.appendChild(el);
-            el.select();
-            document.execCommand("copy");
-            clipboard.removeChild(el);
-          }}
-        />
-      );
-    }
-    return yaml;
-  };
-
-  formatStateDescription = (value, _xtraInfo) => {
-    return <div className="state-text">{RouterStates[value]}</div>;
-  };
-
-  formatState = (value, _xtraInfo) => {
-    return (
-      <Graph
-        id={`State-${_xtraInfo.rowIndex}`}
-        thumbNail={true}
-        legend={true}
-        dimensions={{ width: 30, height: 30 }}
-        nodes={[
-          {
-            key: `edge-key-${_xtraInfo.rowIndex}-${value}`,
-            r: 10,
-            type: "interior",
-            state: value,
-            x: 0,
-            y: 0
-          }
-        ]}
-        links={[]}
-        notifyCurrentRouter={() => {}}
-      />
-    );
-  };
-  formatName = (value, _xtraInfo) => {
-    const realRowIndex = this.props.rows.findIndex(
-      r => r.key === _xtraInfo.rowData.key
-    );
-    if (this.state.editingEdgeRow === _xtraInfo.rowIndex) {
-      // the internal rows array may be different from the props.rows array
-      return (
-        <TextInput
-          value={this.props.rows[realRowIndex].name}
-          type="text"
-          autoFocus
-          onChange={val => this.props.handleEdgeNameChange(val, realRowIndex)}
-          onBlur={this.handleEdgeNameBlur}
-          onKeyPress={this.handleEdgeKeyPress}
-          aria-label="text input example"
-        />
-      );
-    }
-    return (
-      <Button
-        variant="link"
-        isInline
-        onClick={() => this.handleEdgeNameClick(_xtraInfo.rowIndex)}
-      >
-        {this.rows[_xtraInfo.rowIndex].cells[NAME]}
-      </Button>
-    );
-  };
-
-  onSelect = (event, isSelected, rowId) => {
-    // the internal rows array may be different from the props.rows array
-    const realRowIndex =
-      rowId >= 0
-        ? this.props.rows.findIndex(r => r.key === this.rows[rowId].key)
-        : rowId;
-    this.props.handleSelectEdgeRow(realRowIndex, isSelected);
-  };
-
-  toggleAlphaSort = () => {
-    this.setState({ sortDown: !this.state.sortDown });
-  };
-
-  genTable = () => {
-    const { columns, filterText } = this.state;
-    if (this.props.rows.length > 0) {
-      if (this.state.editingEdgeRow === -1 || this.rows.length === 0) {
-        this.rows = this.props.rows.map(r => ({
-          cells: [r.yaml, r.state, r.state, r.name],
-          selected: r.selected,
-          key: r.key
-        }));
-        // sort the rows
-        this.rows = this.rows.sort((a, b) =>
-          a.cells[NAME] < b.cells[NAME]
-            ? -1
-            : a.cells[NAME] > b.cells[NAME]
-            ? 1
-            : 0
-        );
-        if (!this.state.sortDown) {
-          this.rows = this.rows.reverse();
-        }
-        // filter the rows
-        if (filterText !== "") {
-          this.rows = this.rows.filter(
-            r => r.cells[NAME].indexOf(filterText) >= 0
-          );
-        }
-        // only show rows on current page
-        const start = (this.state.page - 1) * this.state.perPage;
-        const end = Math.min(this.rows.length, start + this.state.perPage);
-        this.rows = this.rows.slice(start, end);
-      } else {
-        // pickup any changed info
-        this.rows.forEach(r => {
-          const rrow = this.props.rows.find(rr => rr.key === r.key);
-          if (rrow) {
-            r.selected = rrow.selected;
-            r.cells[YAML] = rrow.yaml;
-            r.cells[STATE_ICON] = rrow.state;
-            r.cells[STATE_TEXT] = rrow.state;
-            r.cells[NAME] = rrow.name;
-          }
-        });
-      }
-
-      return (
-        <React.Fragment>
-          <Table
-            className="edge-table"
-            variant={TableVariant.compact}
-            onSelect={this.onSelect}
-            cells={columns}
-            rows={this.rows}
-          >
-            <TableHeader />
-            <TableBody />
-          </Table>
-        </React.Fragment>
-      );
-    }
-    return <EmptyEdgeClassTable handleAddEdge={this.props.handleAddEdge} />;
-  };
-
-  handleChangeFilter = filterText => {
-    let { page } = this.state;
-    if (filterText !== "") {
-      page = 1;
-    }
-    this.setState({ filterText, page });
-  };
-
-  genToolbar = () => {
-    if (this.props.rows.length > 0) {
-      return (
-        <React.Fragment>
-          <label>Edge namespaces</label>
-          <EdgeTableToolbar
-            handleAddEdge={this.props.handleAddEdge}
-            handleDeleteEdge={this.props.handleDeleteEdge}
-            handleChangeFilter={this.handleChangeFilter}
-            toggleAlphaSort={this.toggleAlphaSort}
-            filterText={this.state.filterText}
-            sortDown={this.state.sortDown}
-            rows={this.props.rows}
-          />
-        </React.Fragment>
-      );
-    }
-  };
-
-  onSetPage = (_event, pageNumber) => {
-    this.setState({
-      page: pageNumber
-    });
-  };
-
-  onPerPageSelect = (_event, perPage) => {
-    this.setState({
-      perPage
-    });
-  };
-
-  genPagination = () => {
-    if (this.props.rows.length > 0) {
-      return (
-        <EdgeTablePagination
-          rows={this.props.rows.length}
-          perPage={this.state.perPage}
-          page={this.state.page}
-          onPerPageSelect={this.onPerPageSelect}
-          onSetPage={this.onSetPage}
-        />
-      );
-    }
-  };
-  render() {
-    return (
-      <React.Fragment>
-        {this.genToolbar()}
-        {this.genTable()}
-        {this.genPagination()}
-      </React.Fragment>
-    );
-  }
-}
-
-export default EdgeTable;
diff --git a/console/react/src/empty-edge-class-table.js b/console/react/src/empty-edge-class-table.js
deleted file mode 100644
index a4defc1..0000000
--- a/console/react/src/empty-edge-class-table.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from "react";
-import {
-  Title,
-  Button,
-  EmptyState,
-  EmptyStateVariant,
-  EmptyStateIcon,
-  EmptyStateBody,
-  EmptyStateSecondaryActions
-} from "@patternfly/react-core";
-import { CubesIcon } from "@patternfly/react-icons";
-
-class EmptyEdgeClassTable extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {};
-  }
-
-  render() {
-    return (
-      <EmptyState variant={EmptyStateVariant.small}>
-        <EmptyStateIcon icon={CubesIcon} />
-        <Title headingLevel="h5" size="lg">
-          No edge namespaces
-        </Title>
-        <EmptyStateBody>
-          This edge class does not contain any edge namespaces yet.
-        </EmptyStateBody>
-        <EmptyStateSecondaryActions>
-          <Button
-            variant="primary"
-            aria-label="Add"
-            onClick={this.props.handleAddEdge}
-          >
-            Add an edge namespace
-          </Button>
-        </EmptyStateSecondaryActions>
-      </EmptyState>
-    );
-  }
-}
-
-export default EmptyEdgeClassTable;
diff --git a/console/react/src/empty-selection.js b/console/react/src/empty-selection.js
deleted file mode 100644
index 73a6eef..0000000
--- a/console/react/src/empty-selection.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import {
-  Title,
-  EmptyState,
-  EmptyStateVariant,
-  EmptyStateIcon,
-  EmptyStateBody
-} from "@patternfly/react-core";
-import { CubesIcon } from "@patternfly/react-icons";
-
-class EmptySelection extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {};
-  }
-
-  render() {
-    return (
-      <EmptyState variant={EmptyStateVariant.small} className="empty-selection">
-        <EmptyStateIcon icon={CubesIcon} />
-        <Title headingLevel="h5" size="lg">
-          Nothing selected
-        </Title>
-        <EmptyStateBody>
-          Select a cluster, edge class, or line between them to view/edit their
-          information.
-        </EmptyStateBody>
-      </EmptyState>
-    );
-  }
-}
-
-export default EmptySelection;
diff --git a/console/react/src/field-details.js b/console/react/src/field-details.js
deleted file mode 100644
index f2318eb..0000000
--- a/console/react/src/field-details.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import React from "react";
-import {
-  ActionGroup,
-  Button,
-  ClipboardCopy,
-  Form,
-  FormGroup,
-  TextInput,
-  Radio
-} from "@patternfly/react-core";
-import EdgeTable from "./edge-table";
-import Graph from "./graph";
-import Confirm from "./confirm";
-
-class FieldDetails extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {};
-  }
-
-  currentNode = () => {
-    let current = this.props.networkInfo.nodes.find(
-      n => n.key === this.props.selectedKey
-    );
-    return current;
-  };
-
-  formElement = (field, currentNode) => {
-    if (field.type === "text") {
-      let isRequired = field.isRequired;
-      if (typeof field.isRequired === "function")
-        isRequired = field.isRequired(
-          field.title,
-          this.props.networkInfo,
-          this.props.selectedKey
-        );
-      return (
-        <TextInput
-          isRequired={isRequired}
-          type="text"
-          id={field.title}
-          name={field.title}
-          aria-describedby="simple-form-name-helper"
-          value={currentNode[field.title]}
-          onChange={newVal =>
-            this.props.handleEditField(newVal, field.title, currentNode.key)
-          }
-        />
-      );
-    }
-    if (field.type === "radio") {
-      return field.options.map((o, i) => {
-        return (
-          <Radio
-            key={`key-radio-${o}-${i}`}
-            id={o}
-            value={o}
-            name={field.title}
-            label={o}
-            aria-label={o}
-            onChange={() => this.props.handleRadioChange(o, field.title)}
-            isChecked={currentNode[field.title] === o}
-          />
-        );
-      });
-    } else if (field.type === "states") {
-      return field.options.map((o, i) => {
-        let yaml = <div className="state-placeholder"></div>;
-        if (currentNode.yaml && i === 1) {
-          yaml = (
-            <ClipboardCopy
-              className="state-copy"
-              onClick={(event, text) => {
-                const clipboard = event.currentTarget.parentElement;
-                const el = document.createElement("input");
-                el.value = JSON.stringify(currentNode.yaml);
-                clipboard.appendChild(el);
-                el.select();
-                document.execCommand("copy");
-                clipboard.removeChild(el);
-              }}
-            />
-          );
-        }
-        return (
-          <div
-            className="state-container"
-            key={`key-checkbox-${o}-${i}`}
-            id={`${field.title}-${i}`}
-          >
-            {yaml}
-            <Graph
-              id={`State-${i}`}
-              thumbNail={true}
-              legend={true}
-              dimensions={{ width: 30, height: 30 }}
-              nodes={[
-                {
-                  key: `legend-key-${i}`,
-                  r: 10,
-                  type: "interior",
-                  state: i,
-                  x: 0,
-                  y: 0
-                }
-              ]}
-              links={[]}
-              notifyCurrentRouter={() => {}}
-            />
-            <div className="state-text">{o}</div>
-          </div>
-        );
-      });
-    } else if (field.type === "label") {
-      const currentLink = this.props.networkInfo.links.find(
-        n => n.key === this.props.selectedKey
-      );
-      return (
-        <span className="link-label">
-          {typeof currentLink[field.title] === "function"
-            ? currentLink[field.title]()
-            : currentLink[field.title]}
-        </span>
-      );
-    }
-  };
-
-  extra = currentNode => {
-    if (this.props.details.extra) {
-      return (
-        <EdgeTable
-          rows={currentNode.rows}
-          networkInfo={this.props.networkInfo}
-          handleAddEdge={this.props.handleAddEdge}
-          handleDeleteEdge={this.props.handleDeleteEdge}
-          handleEdgeNameChange={this.props.handleEdgeNameChange}
-          handleSelectEdgeRow={this.props.handleSelectEdgeRow}
-        />
-      );
-    }
-  };
-
-  render() {
-    const currentNode = this.currentNode();
-    return (
-      <Form
-        onSubmit={e => {
-          e.preventDefault();
-          return false;
-        }}
-      >
-        <h1>{this.props.details.title}</h1>
-        <ActionGroup>
-          {this.props.details.actions.map(action => {
-            if (action.confirm) {
-              return (
-                <Confirm
-                  key={action.title}
-                  handleConfirm={action.onClick}
-                  variant="secondary"
-                  isDeleteDisabled={
-                    action.isDisabled
-                      ? action.isDisabled(
-                          action.title,
-                          this.props.networkInfo,
-                          this.props.selectedKey
-                        )
-                      : false
-                  }
-                  buttonText={action.title}
-                  title={`Confirm ${action.title}`}
-                >
-                  <h2>Are you sure?</h2>
-                </Confirm>
-              );
-            } else
-              return (
-                <Button
-                  key={action.title}
-                  variant="secondary"
-                  onClick={action.onClick}
-                  isDisabled={
-                    action.isDisabled
-                      ? action.isDisabled(
-                          action.title,
-                          this.props.networkInfo,
-                          this.props.selectedKey
-                        )
-                      : false
-                  }
-                >
-                  {action.title}
-                </Button>
-              );
-          })}
-        </ActionGroup>
-        {this.props.details.fields.map(field => {
-          return (
-            <FormGroup
-              key={field.title}
-              label={field.title}
-              isRequired={
-                typeof field.isRequired === "function"
-                  ? field.isRequired(
-                      field.title,
-                      this.props.networkInfo,
-                      this.props.selectedKey
-                    )
-                  : field.isRequired
-              }
-              fieldId={field.title}
-              helperText={field.help}
-              isInline={field.type === "radio"}
-            >
-              {this.formElement(field, currentNode)}
-            </FormGroup>
-          );
-        })}
-        <FormGroup fieldId="extra">{this.extra(currentNode)}</FormGroup>
-      </Form>
-    );
-  }
-}
-
-export default FieldDetails;
diff --git a/console/react/src/graph.js b/console/react/src/graph.js
deleted file mode 100644
index d629d1d..0000000
--- a/console/react/src/graph.js
+++ /dev/null
@@ -1,358 +0,0 @@
-import React from "react";
-
-import * as d3 from "d3";
-import { addDefs } from "./nodes";
-
-class Graph extends React.Component {
-  constructor(props) {
-    super(props);
-
-    this.state = {};
-    this.force = d3.layout
-      .force()
-      .size([this.props.dimensions.width, this.props.dimensions.height])
-      .linkDistance(l => {
-        if (this.props.thumbNail) return 40;
-        else if (l.type === "router") return 150;
-        else if (l.type === "edge") return 20;
-        return 50;
-      })
-      .charge(-800)
-      .friction(0.1)
-      .gravity(0.001);
-
-    this.mouse_down_position = null;
-  }
-
-  // called only once when the component is initialized
-  componentDidMount() {
-    const svg = d3.select(this.svg);
-    if (!this.props.thumbNail && !this.props.legend) {
-      addDefs(svg);
-    }
-
-    this.force.on("tick", () => {
-      // after force calculation starts, call updateGraph
-      // which uses d3 to manipulate the attributes,
-      // and React doesn't have to go through lifecycle on each tick
-      d3.select(this.svgg).call(this.updateGraph);
-    });
-    // call this manually to create svg circles and lines
-    this.shouldComponentUpdate(this.props);
-  }
-
-  // called each time one of the properties changes
-  shouldComponentUpdate(nextProps) {
-    this.d3Graph = d3.select(this.svgg);
-    this.appendNodes(this.d3Graph, nextProps, nextProps.nodes);
-    this.appendLinks(this.d3Graph, nextProps.links, "connector", ".node");
-    this.d3Graph.call(this.updateGraph);
-
-    // warning: d3's force function modifies the nodes and links arrays
-    this.force.nodes(nextProps.nodes).links(nextProps.links);
-    this.force.start();
-    if (nextProps) this.refresh(nextProps);
-    return false;
-  }
-
-  appendNodes = (selection, nextProps, nodes) => {
-    const subNodes = selection.selectAll(".node").data(nodes, node => node.key);
-    subNodes
-      .enter()
-      .append("g")
-      .attr("class", "node")
-      .attr("id", n => n.key)
-      .call(selection => this.enterNode(selection, nextProps));
-    subNodes.exit().remove();
-    subNodes.call(this.updateNode);
-    subNodes.call(this.force.drag);
-  };
-
-  appendLinks = (selection, clinks, linkClass, before) => {
-    const subLinks = selection
-      .selectAll(`.${linkClass}`)
-      .data(clinks, link => link.key);
-    subLinks
-      .enter()
-      .insert("g", before)
-      .attr("class", linkClass)
-      .call(this.enterLink);
-    subLinks.exit().remove();
-    subLinks.call(this.updateLink);
-  };
-
-  // new node/nodes are present
-  // append all the stuff and set the attributes that don't change
-  enterNode = (selection, props) => {
-    const graph = this;
-    const routers = selection.filter(d => d.type === "interior");
-    const edges = selection.filter(d => d.type === "edgeClass");
-
-    selection.append("circle").attr("r", d => d.r);
-
-    routers.append("path").attr("d", d =>
-      d3.svg
-        .arc()
-        .innerRadius(0)
-        .outerRadius(d.r)({
-        startAngle: 0,
-        endAngle: (d.state * 2.0 * Math.PI) / 3.0
-      })
-    );
-
-    selection
-      .classed("edgeClass", d => d.type === "edgeClass")
-      .classed("interior", d => d.type === "interior");
-
-    if (!props.thumbNail || props.legend) {
-      selection.classed("network", true);
-    }
-    if (!props.thumbNail) {
-      selection
-        .append("text")
-        .attr("x", d => d.r + 5)
-        .attr("dy", ".35em")
-        .text(d => d.Name);
-      edges
-        .append("text")
-        .classed("edge-count", true)
-        .attr("x", -24)
-        .attr("dy", "0.35em")
-        .text(""); // updated in refresh
-    }
-    /* // this creates an octagon
-    const sqr2o2 = Math.sqrt(2.0) / 2.0;
-    const points = `1 0 ${sqr2o2} ${sqr2o2} 0 1 -${sqr2o2} ${sqr2o2} -1 0 -${sqr2o2} -${sqr2o2} 0 -1 ${sqr2o2} -${sqr2o2}`;
-    selection
-      .filter(d => d.type === "edgeClass")
-      .append("polygon")
-      .attr("points", points)
-      .attr("transform", `scale(60) rotate(22.5)`);
-*/
-    selection
-      .on("mouseover", function(n) {
-        if (graph.props.thumbNail) return;
-        n.over = true;
-        graph.updateNode(d3.select(this));
-      })
-      .on("mouseout", function(n) {
-        if (graph.props.thumbNail) return;
-        n.over = false;
-        graph.updateNode(d3.select(this));
-      })
-      .on("click", function(n) {
-        if (graph.props.thumbNail) return;
-        // if there was a selected node and it was not the one we just clicked on:
-        // create a link between the selected node and the clicked on node
-        if (graph.props.selectedKey && graph.props.selectedKey !== n.key) {
-          graph.props.notifyCreateLink(n.key, graph.props.selectedKey);
-        }
-
-        // see if the node was dragged (same === false)
-        const same = graph.samePos(
-          d3.mouse(this.parentNode),
-          graph.mouse_down_position,
-          "node"
-        );
-        if (same) {
-          if (graph.props.selectedKey === n.key) {
-            graph.props.notifyCurrentRouter(null);
-          } else {
-            graph.props.notifyCurrentRouter(n.key);
-          }
-        } else {
-          graph.props.notifyCurrentRouter(n.key);
-        }
-        graph.refresh(graph.props);
-      })
-      .on("mousedown", function(n) {
-        graph.mouse_down_position = d3.mouse(this.parentNode);
-        graph.draggingNode = true;
-      })
-      .on("mouseup", n => {
-        if (graph.props.thumbNail) return;
-        if (n.type !== "edge") n.fixed = true;
-      });
-  };
-
-  samePos = (pos1, pos2, where) => {
-    if (pos1 && pos2) {
-      if (pos1[0] === pos2[0] && pos1[1] === pos2[1]) return true;
-    }
-    return false;
-  };
-
-  arcTween = (oldData, newData, arc) => {
-    const copy = { ...oldData };
-    return function() {
-      const interpolateStartAngle = d3.interpolate(
-          oldData.startAngle,
-          newData.startAngle
-        ),
-        interpolateEndAngle = d3.interpolate(
-          oldData.endAngle,
-          newData.endAngle
-        );
-
-      return function(t) {
-        copy.startAngle = interpolateStartAngle(t);
-        copy.endAngle = interpolateEndAngle(t);
-        return arc(copy);
-      };
-    };
-  };
-  // called each time a property changes
-  // update the classes/text based on the new properties
-  refresh = props => {
-    if (props.thumbNail) return;
-    d3.selectAll("g.node.network").classed(
-      "selected",
-      d => d.key === props.selectedKey
-    );
-
-    // update the interior node state
-    d3.selectAll("g.node.network.interior path").attr("d", d =>
-      d3.svg
-        .arc()
-        .innerRadius(0)
-        .outerRadius(d.r)({
-        startAngle: 0,
-        endAngle: (d.state * 2.0 * Math.PI) / 3.0
-      })
-    );
-
-    d3.selectAll("svg text").each(function(d) {
-      d3.select(this).text(d.Name);
-    });
-    d3.selectAll("g.connector").classed(
-      "selected",
-      d => d.key === props.selectedKey
-    );
-    d3.selectAll("text.edge-count").text(d => {
-      return d.rows.length > 0 ? `Edges: ${d.rows.length}` : "";
-    });
-  };
-
-  // update the node's positions
-  updateNode = selection => {
-    selection.attr("transform", d => {
-      let container = {
-        width: this.props.dimensions.width,
-        height: this.props.dimensions.height
-      };
-      let r = 15;
-      d.x = Math.max(Math.min(d.x, container.width - r), r);
-      d.y = Math.max(Math.min(d.y, container.height - r), r);
-      return `translate(${d.x || 0},${d.y || 0}) ${d.over ? "scale(1.1)" : ""}`;
-    });
-  };
-
-  markerId = (link, end) => {
-    return `--${end === "end" ? link.size : link.size}`;
-  };
-
-  // called with a selection that represents all the new links between nodes
-  // here we add the lines and set their attributes
-  enterLink = selection => {
-    const graph = this;
-
-    // add a visible line with an arrow
-    selection
-      .append("path")
-      .classed("link", true)
-      .attr("stroke-width", d => d.size)
-      .attr("marker-end", d => {
-        return d.right ? `url(#end--20)` : null;
-      })
-      .attr("marker-start", d => {
-        if (d.type === "edge") return null;
-        if (this.props.thumbNail) return null;
-        return d.left || (!d.left && !d.right) ? `url(#start--20)` : null;
-      });
-
-    if (!this.props.thumbNail && !this.props.legend) {
-      // add an invisible wide path to make it easier to mouseover
-      selection
-        .append("path")
-        .classed("hittarget", true)
-        .on("click", function(d) {
-          d3.select(this.parentNode).classed("selected", true);
-          graph.notifyCurrentConnector(d);
-          graph.refresh(graph.props);
-        })
-        .on("mouseover", function(n) {
-          d3.select(this.parentNode).classed("over", true);
-        })
-        .on("mouseout", function(n) {
-          d3.select(this.parentNode).classed("over", false);
-        });
-    }
-    this.refresh(this.props);
-  };
-
-  notifyCurrentConnector = d => {
-    if (this.props.notifyCurrentConnector) this.props.notifyCurrentConnector(d);
-  };
-
-  // update the links' positions
-  updateLink = selection => {
-    const stxy = d => {
-      let sx = d.source.x || this.props.nodes[d.source].x;
-      let tx = d.target.x || this.props.nodes[d.target].x;
-      let sy = d.source.y || this.props.nodes[d.source].y;
-      let ty = d.target.y || this.props.nodes[d.target].y;
-
-      if (d.source.parentKey !== d.target.parentKey) {
-        const snode = this.props.nodes.find(n => n.key === d.source.parentKey);
-        const tnode = this.props.nodes.find(n => n.key === d.target.parentKey);
-        if (snode && tnode) {
-          sx += snode.kx;
-          tx += tnode.kx;
-          sy += snode.ky;
-          ty += tnode.ky;
-        }
-      }
-      const deltaX = tx - sx;
-      const deltaY = ty - sy;
-      const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
-      const normX = deltaX / dist;
-      const normY = deltaY / dist;
-      const sourcePadding = d.source.r || this.props.nodes[d.source].r;
-      const targetPadding = d.target.r || this.props.nodes[d.target].r;
-      const sourceX = sx + sourcePadding * normX;
-      const sourceY = sy + sourcePadding * normY;
-      const targetX = tx - targetPadding * normX;
-      const targetY = ty - targetPadding * normY;
-      return { x1: sourceX, y1: sourceY, x2: targetX, y2: targetY };
-    };
-    selection.attr("d", d => {
-      const endp = stxy(d);
-      return `M${endp.x1},${endp.y1}L${endp.x2},${endp.y2}`;
-    });
-  };
-
-  // called each animation tick to update the positions
-  updateGraph = selection => {
-    selection.selectAll(".node").call(this.updateNode);
-    selection.selectAll(".link").call(this.updateLink);
-    selection.selectAll(".hittarget").call(this.updateLink);
-  };
-
-  render() {
-    const { width, height } = this.props.dimensions;
-    return (
-      <React.Fragment>
-        <svg
-          width={width}
-          height={height}
-          ref={el => (this.svg = el)}
-          xmlns="http://www.w3.org/2000/svg"
-        >
-          <g ref={el => (this.svgg = el)} />
-        </svg>
-      </React.Fragment>
-    );
-  }
-}
-
-export default Graph;
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index 82c9534..242d1d8 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -46,7 +46,7 @@ import {
 
 import accessibleStyles from "@patternfly/patternfly/utilities/Accessibility/accessibility.css";
 import { css } from "@patternfly/react-styles";
-import { BellIcon, PowerOffIcon } from "@patternfly/react-icons";
+import { PowerOffIcon } from "@patternfly/react-icons";
 import DropdownMenu from "./DropdownMenu";
 import ConnectPage from "./connectPage";
 import DashboardPage from "./overview/dashboard/dashboardPage";
@@ -58,6 +58,7 @@ import MessageFlowPage from "./chord/chordPage";
 import LogDetails from "./overview/logDetails";
 import { QDRService } from "./qdrService";
 import ConnectForm from "./connect-form";
+import NotificationDrawer from "./notificationDrawer";
 import { utils } from "./amqp/utilities";
 const avatarImg = require("./assets/img_avatar.svg");
 
@@ -129,6 +130,12 @@ class PageLayout extends React.Component {
       this.setState({ connectPath: "", connected: false }, () => {
         this.handleConnectCancel();
         this.service.disconnect();
+        this.handleAddNotification(
+          "event",
+          "Manually disconnected",
+          new Date(),
+          "info"
+        );
       });
     } else {
       const connectOptions = JSON.parse(JSON.stringify(connectInfo));
@@ -150,6 +157,7 @@ class PageLayout extends React.Component {
             }
           }
           this.handleConnectCancel();
+          this.handleAddNotification("event", "Connected", new Date(), "info");
 
           this.setState({
             activeItem,
@@ -211,6 +219,17 @@ class PageLayout extends React.Component {
     return this.state.connected;
   };
 
+  handleAddNotification = (section, message, timestamp, severity) => {
+    if (this.notificationRef) {
+      this.notificationRef.addNotification({
+        section,
+        message,
+        timestamp,
+        severity
+      });
+    }
+  };
+
   render() {
     const { activeItem, activeGroup } = this.state;
     const { isNavOpenDesktop, isNavOpenMobile, isMobileView } = this.state;
@@ -267,14 +286,8 @@ class PageLayout extends React.Component {
               <PowerOffIcon />
             </Button>
           </ToolbarItem>
-          <ToolbarItem>
-            <Button
-              id="default-example-uid-01"
-              aria-label="Notifications actions"
-              variant={ButtonVariant.plain}
-            >
-              <BellIcon />
-            </Button>
+          <ToolbarItem className="notification-button">
+            <NotificationDrawer ref={el => (this.notificationRef = el)} />
           </ToolbarItem>
         </ToolbarGroup>
         <ToolbarGroup>
@@ -340,7 +353,12 @@ class PageLayout extends React.Component {
         {...(more.exact ? "exact" : "")}
         render={props =>
           this.state.connected ? (
-            <Component service={this.service} {...props} {...more} />
+            <Component
+              service={this.service}
+              handleAddNotification={this.handleAddNotification}
+              {...props}
+              {...more}
+            />
           ) : (
             <Redirect
               to={{ pathname: "/login", state: { from: props.location } }}
diff --git a/console/react/src/network-name.js b/console/react/src/network-name.js
deleted file mode 100644
index c1e817d..0000000
--- a/console/react/src/network-name.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from "react";
-import {
-  FormGroup,
-  Text,
-  TextContent,
-  TextInput,
-  Split,
-  SplitItem
-} from "@patternfly/react-core";
-
-class NetworkName extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      editing: true,
-      editingName: this.props.networkInfo.name
-    };
-  }
-
-  handleSaveClick = () => {
-    let editing = !this.state.editing;
-    this.setState({ editing });
-    if (!editing) this.props.handleNetworkNameChange(this.state.editingName);
-  };
-
-  handleCancelClick = () => {
-    let { editing, editingName } = this.state;
-    editing = false;
-    editingName = this.props.networkInfo.name;
-    this.setState({ editing, editingName });
-  };
-
-  handleLocalEditName = editingName => {
-    this.setState({ editingName });
-  };
-
-  render() {
-    return (
-      <React.Fragment>
-        <Split gutter="md" className="network-name">
-          <SplitItem>
-            <TextContent className="enter-prompt">
-              <Text component="h3">Enter a network name to get started</Text>
-            </TextContent>
-          </SplitItem>
-          <SplitItem isFilled>
-            <FormGroup label="" isRequired fieldId="simple-form-name">
-              <TextInput
-                className={this.state.editing ? "editing" : "not-editing"}
-                isRequired
-                isDisabled={!this.state.editing}
-                type="text"
-                id="network-name"
-                name="network-name"
-                aria-describedby="network-name-helper"
-                value={this.state.editingName}
-                onChange={this.handleLocalEditName}
-              />
-            </FormGroup>
-          </SplitItem>
-        </Split>
-      </React.Fragment>
-    );
-  }
-}
-
-export default NetworkName;
diff --git a/console/react/src/nodes.js b/console/react/src/nodes.js
deleted file mode 100644
index 9127d6a..0000000
--- a/console/react/src/nodes.js
+++ /dev/null
@@ -1,260 +0,0 @@
-/*
-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 * as d3 from "d3";
-
-export const RouterStates = [
-  "NEW",
-  "READY TO DEPLOY",
-  "CLUSTER HEARD FROM",
-  "IN NETWORK"
-];
-
-const nodeProperties = {
-  // router types
-  "inter-router": {
-    radius: 28,
-    refX: {
-      end: 32,
-      start: -19
-    },
-    linkDistance: [150, 70],
-    charge: [-1800, -900]
-  },
-  edge: {
-    radius: 20,
-    refX: {
-      end: 24,
-      start: -14
-    },
-    linkDistance: [110, 55],
-    charge: [-1350, -900]
-  },
-  // generated nodes from connections. key is from connection.role
-  normal: {
-    radius: 15,
-    refX: {
-      end: 20,
-      start: -7
-    },
-    linkDistance: [75, 40],
-    charge: [-900, -900]
-  }
-};
-// aliases
-nodeProperties._topo = nodeProperties["inter-router"];
-nodeProperties._edge = nodeProperties["edge"];
-nodeProperties["on-demand"] = nodeProperties["normal"];
-nodeProperties["route-container"] = nodeProperties["normal"];
-
-export class Nodes {
-  constructor() {
-    this.nodes = [];
-  }
-  static radius(type) {
-    if (nodeProperties[type].radius) return nodeProperties[type].radius;
-    return 15;
-  }
-  static maxRadius() {
-    let max = 0;
-    for (let key in nodeProperties) {
-      max = Math.max(max, nodeProperties[key].radius);
-    }
-    return max;
-  }
-  static refX(end, r) {
-    for (let key in nodeProperties) {
-      if (nodeProperties[key].radius === parseInt(r)) {
-        return nodeProperties[key].refX[end];
-      }
-    }
-    return 0;
-  }
-  // return all possible values of node radii
-  static discrete() {
-    let values = {};
-    for (let key in nodeProperties) {
-      values[nodeProperties[key].radius] = true;
-    }
-    return Object.keys(values);
-  }
-  // vary the following force graph attributes based on nodeCount
-  static forceScale(nodeCount, minmax) {
-    let count = Math.max(Math.min(nodeCount, 80), 6);
-    let x = d3.scale
-      .linear()
-      .domain([6, 80])
-      .range(minmax);
-    return x(count);
-  }
-  linkDistance(d, nodeCount) {
-    let range = nodeProperties[d.target.nodeType].linkDistance;
-    return Nodes.forceScale(nodeCount, range);
-  }
-  charge(d, nodeCount) {
-    let charge = nodeProperties[d.nodeType].charge;
-    return Nodes.forceScale(nodeCount, charge);
-  }
-  gravity(d, nodeCount) {
-    return Nodes.forceScale(nodeCount, [0.0001, 0.1]);
-  }
-  setFixed(d, fixed) {
-    let n = this.find(d.container, d.properties, d.name);
-    if (n) {
-      n.fixed = fixed;
-    }
-    d.setFixed(fixed);
-  }
-  getLength() {
-    return this.nodes.length;
-  }
-  get(index) {
-    if (index < this.getLength()) {
-      return this.nodes[index];
-    }
-    return undefined;
-  }
-  nodeFor(name) {
-    for (let i = 0; i < this.nodes.length; ++i) {
-      if (this.nodes[i].name === name) return this.nodes[i];
-    }
-    return null;
-  }
-  nodeExists(connectionContainer) {
-    return this.nodes.findIndex(function(node) {
-      return node.container === connectionContainer;
-    });
-  }
-
-  find(connectionContainer, properties, name) {
-    properties = properties || {};
-    for (let i = 0; i < this.nodes.length; ++i) {
-      if (
-        this.nodes[i].name === name ||
-        this.nodes[i].container === connectionContainer
-      ) {
-        if (properties.product) this.nodes[i].properties = properties;
-        return this.nodes[i];
-      }
-    }
-    return undefined;
-  }
-  clearHighlighted() {
-    for (let i = 0; i < this.nodes.length; ++i) {
-      this.nodes[i].highlighted = false;
-    }
-  }
-}
-
-// Generate a marker for each combination of:
-//  start|end, ''|selected highlighted, and each possible node radius
-export function addDefs(svg) {
-  let sten = ["start", "end"];
-  let states = [""];
-  let radii = Nodes.discrete();
-  let defs = [];
-  for (let isten = 0; isten < sten.length; isten++) {
-    for (let istate = 0; istate < states.length; istate++) {
-      for (let iradii = 0; iradii < radii.length; iradii++) {
-        defs.push({
-          sten: sten[isten],
-          state: states[istate],
-          r: radii[iradii]
-        });
-      }
-    }
-  }
-
-  svg
-    .insert("svg:defs", "svg g")
-    .attr("class", "marker-defs")
-    .selectAll("marker")
-    .data(defs)
-    .enter()
-    .append("svg:marker")
-    .attr("id", function(d) {
-      return [d.sten, d.state, d.r].join("-");
-    })
-    .attr("viewBox", "0 -5 10 10")
-    .attr("refX", function(d) {
-      return -1;
-      //return Nodes.refX(d.sten, d.r);
-    })
-    .attr("markerWidth", 14)
-    .attr("markerHeight", 14)
-    .attr("markerUnits", "userSpaceOnUse")
-    .attr("orient", "auto")
-    .append("svg:path")
-    .attr("d", function(d) {
-      return d.sten === "end"
-        ? "M 0 -5 L 10 0 L 0 5 z"
-        : "M 10 -5 L 0 0 L 10 5 z";
-    })
-    .attr("fill", "#000000");
-
-  addStyles(
-    sten,
-    {
-      selected: "#33F",
-      highlighted: "#6F6",
-      unknown: "#888"
-    },
-    radii
-  );
-}
-export function addGradient(svg) {
-  // gradient for sender/receiver client
-  let grad = svg
-    .append("svg:defs")
-    .append("linearGradient")
-    .attr("id", "half-circle")
-    .attr("x1", "0%")
-    .attr("x2", "0%")
-    .attr("y1", "100%")
-    .attr("y2", "0%");
-  grad
-    .append("stop")
-    .attr("offset", "50%")
-    .style("stop-color", "#C0F0C0");
-  grad
-    .append("stop")
-    .attr("offset", "50%")
-    .style("stop-color", "#F0F000");
-}
-
-function addStyles(stend, stateColor, radii) {
-  // the <style>
-  let element = document.querySelector("style");
-  // Reference to the stylesheet
-  let sheet = element.sheet;
-
-  let states = Object.keys(stateColor);
-  // create styles for each combo of 'stend-state-radii'
-  for (let istend = 0; istend < stend.length; istend++) {
-    for (let istate = 0; istate < states.length; istate++) {
-      let selectors = [];
-      for (let iradii = 0; iradii < radii.length; iradii++) {
-        selectors.push(`#${stend[istend]}-${states[istate]}-${radii[iradii]}`);
-      }
-      let color = stateColor[states[istate]];
-      let sels = `${selectors.join(",")} {fill: ${color}; stroke: ${color};}`;
-      sheet.insertRule(sels, 0);
-    }
-  }
-}
diff --git a/console/react/src/nodeslinks.js b/console/react/src/nodeslinks.js
deleted file mode 100644
index 0249ecb..0000000
--- a/console/react/src/nodeslinks.js
+++ /dev/null
@@ -1,144 +0,0 @@
-class NodesLinks {
-  static nextNodeIndex = 1;
-  static nextLinkIndex = 1;
-  static nextEdgeIndex = 1;
-  static nextEdgeClassIndex = 1;
-  link = (s, t, i) => ({
-    source: s,
-    target: t,
-    key: `link-${i}`,
-    size: 2,
-    type: "connector",
-    left: true,
-    right: false
-  });
-  setLinks = (start, count) => {
-    let links = [];
-    for (let i = start; i < start + count - 1; i++) {
-      links.push(this.link(i, i + 1, NodesLinks.nextLinkIndex++));
-    }
-    return links;
-  };
-
-  node = (type, i, x, y) => ({
-    key: `key_${type}_${i}`,
-    val: i,
-    r: 20,
-    x: x,
-    y: y,
-    type: type
-  });
-
-  getXY = (start, count, midX, midY, rotate, displace) => {
-    const ang = (start * 2.0 * Math.PI) / count + rotate;
-    const x = midX + Math.cos(ang) * displace;
-    const y = midY + Math.sin(ang) * displace;
-    return { x, y };
-  };
-
-  addNodesInCircle = (nodes, start, count, midX, midY, displace, rotate) => {
-    rotate = rotate || 0;
-    for (let i = start; i < start + count; i++) {
-      const { x, y } = this.getXY(
-        i - start,
-        count,
-        midX,
-        midY,
-        rotate,
-        displace
-      );
-      nodes.push(this.node("R", i, x, y));
-    }
-  };
-
-  addNode = (type, networkInfo, dimensions) => {
-    let i;
-    if (type === "edgeClass") {
-      i = NodesLinks.nextEdgeClassIndex++;
-    } else {
-      i = NodesLinks.nextNodeIndex++;
-    }
-    const x = dimensions.width / 2;
-    const y = dimensions.height / 2;
-    const newNode = this.node(type, i, x, y);
-    newNode.type = type;
-    if (type === "interior") {
-      newNode.Name = `hub-${i}`;
-      newNode["Route-suffix"] = "";
-      newNode.Namespace = "";
-      newNode.state = 0;
-    } else if (type === "edgeClass") {
-      newNode.Name = `EC-${i}`;
-      newNode.rows = [];
-      newNode.r = 60;
-    }
-    networkInfo.nodes.push(newNode);
-    return newNode;
-  };
-
-  addLink = (toIndex, fromIndex, links, nodes) => {
-    if (!this.linkBetween(links, toIndex, fromIndex)) {
-      const link = this.link(fromIndex, toIndex, NodesLinks.nextLinkIndex++);
-      if (
-        nodes[toIndex].type === "interior" &&
-        nodes[fromIndex].type === "interior"
-      ) {
-        link["connector type"] = "inter-router";
-      } else {
-        link["connector type"] = "edge";
-      }
-      link.connector = () => nodes[toIndex].Name;
-      link.listener = () => nodes[fromIndex].Name;
-      links.push(link);
-      return link;
-    }
-  };
-
-  getEdgeName = () => {
-    return `edge-${NodesLinks.nextEdgeIndex++}`;
-  };
-  getEdgeKey = () => {
-    return NodesLinks.nextEdgeIndex;
-  };
-
-  // return true if there are any links between toIndex and fromIndex
-  linkBetween = (links, toIndex, fromIndex) => {
-    return links.some(
-      l =>
-        (l.source.index === toIndex && l.target.index === fromIndex) ||
-        (l.source.index === fromIndex && l.target.index === toIndex)
-    );
-  };
-  linkIndex = (links, nodeIndex) => {
-    return links.findIndex(
-      l => l.source.index === nodeIndex || l.target.index === nodeIndex
-    );
-  };
-
-  setNodesLinks = (networkInfo, dimensions) => {
-    const nodes = [];
-    let links = [];
-    const midX = dimensions.width / 2;
-    const midY = dimensions.height / 2;
-    const displace = dimensions.height / 2;
-    // create the routers
-    // set their starting positions in a circle
-    if (networkInfo.routers.length === 1) {
-      nodes.push(this.node("R", 0, midX, midY));
-    } else {
-      this.addNodesInCircle(
-        nodes,
-        0,
-        networkInfo.routers.length,
-        midX,
-        midY,
-        displace
-      );
-    }
-    if (networkInfo.routers.length > 1)
-      links = this.setLinks(0, networkInfo.routers.length);
-    return { nodes, links };
-  };
-}
-
-export default NodesLinks;
diff --git a/console/react/src/notificationDrawer.js b/console/react/src/notificationDrawer.js
new file mode 100644
index 0000000..0ade746
--- /dev/null
+++ b/console/react/src/notificationDrawer.js
@@ -0,0 +1,293 @@
+/*
+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 { NotificationBadge } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core";
+import {
+  Accordion,
+  AccordionItem,
+  AccordionContent,
+  AccordionToggle
+} from "@patternfly/react-core";
+
+import {
+  AngleDoubleLeftIcon,
+  AngleDoubleRightIcon,
+  BellIcon,
+  TimesIcon
+} from "@patternfly/react-icons";
+
+import { safePlural } from "./qdrGlobals";
+
+class NotificationDrawer extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      isShown: false, // is the drawer shown
+      expanded: false, // is the drawer wide
+      isAnyUnread: false,
+      accordionSections: {
+        action: {
+          title: "Management Actions",
+          isOpen: false,
+          events: []
+        },
+        event: { title: "Events", isOpen: false, events: [] }
+      }
+    };
+    this.severityToIcon = {
+      info: { icon: "pficon-info", color: "#313131" },
+      error: { icon: "pficon-error-circle-o", color: "red" },
+      warning: { icon: "pficon-warning-triangle-o", color: "yellow" },
+      success: { icon: "pficon-ok", color: "green" }
+    };
+  }
+
+  componentDidMount() {
+    document.addEventListener("mousedown", this.handleClickOutside);
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener("mousedown", this.handleClickOutside);
+  }
+
+  handleClickOutside = event => {
+    if (this.notificationRef && !this.notificationRef.contains(event.target)) {
+      // don't close if the click was on the bell icon since
+      // that icon's event handler will toggle the drawer
+      if (this.buttonRef && this.buttonRef.contains(event.target)) return;
+      this.close();
+    }
+  };
+
+  addNotification = ({ section, message, timestamp, severity }) => {
+    const { accordionSections } = this.state;
+    const event = { message, timestamp, severity };
+    event.date = timestamp.toLocaleDateString(undefined, {
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit"
+    });
+    event.time = timestamp.toLocaleTimeString(undefined, {
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit"
+    });
+    event.isRead = false;
+    accordionSections[section].events.unshift(event);
+    this.setState({ accordionSections, isAnyUnread: true });
+  };
+
+  close = () => {
+    this.setState({ isShown: false });
+  };
+
+  toggleDrawer = sectionKey => {
+    const { accordionSections } = this.state;
+    accordionSections[sectionKey].isOpen = !accordionSections[sectionKey]
+      .isOpen;
+    this.setState(accordionSections);
+  };
+
+  toggleExpand = () => {
+    this.setState({ expanded: !this.state.expanded });
+  };
+
+  toggle = () => {
+    this.setState({ isShown: !this.state.isShown });
+  };
+
+  setAnyUnread = accordionSections => {
+    let isAnyUnread = false;
+    for (let sectionKey in accordionSections) {
+      isAnyUnread =
+        isAnyUnread ||
+        accordionSections[sectionKey].events.some(e => !e.isRead);
+    }
+    this.setState({ accordionSections, isAnyUnread });
+  };
+
+  markAsRead = event => {
+    event.isRead = true;
+    const { accordionSections } = this.state;
+    this.setAnyUnread(accordionSections);
+  };
+
+  clearAll = sectionKey => {
+    const { accordionSections } = this.state;
+    accordionSections[sectionKey].events = [];
+    this.setAnyUnread(accordionSections);
+  };
+
+  markAllRead = sectionKey => {
+    const { accordionSections } = this.state;
+    accordionSections[sectionKey].events.forEach(e => (e.isRead = true));
+    this.setAnyUnread(accordionSections);
+  };
+
+  hasUnread = section => {
+    return !section.events.some(e => e.isRead);
+  };
+
+  render() {
+    const DrawerTitle = (
+      <div className="drawer-pf-title">
+        <Button variant="plain" aria-label="expand" onClick={this.toggleExpand}>
+          {this.state.expanded ? (
+            <AngleDoubleRightIcon />
+          ) : (
+            <AngleDoubleLeftIcon />
+          )}
+        </Button>
+        <h3 className="text-center">Notifications Drawer</h3>
+        <Button variant="plain" aria-label="close" onClick={this.close}>
+          <TimesIcon />
+        </Button>
+      </div>
+    );
+
+    const severityIcon = event => {
+      return (
+        <i
+          className={`pf pficon ${this.severityToIcon[event.severity].icon}`}
+          style={{ color: this.severityToIcon[event.severity].color }}
+        ></i>
+      );
+    };
+
+    return (
+      <>
+        <div ref={el => (this.buttonRef = el)}>
+          <NotificationBadge
+            isRead={!this.state.isAnyUnread}
+            onClick={this.toggle}
+            aria-label="Notifications"
+          >
+            <BellIcon />
+          </NotificationBadge>
+        </div>
+        {this.state.isShown && (
+          <div
+            ref={el => (this.notificationRef = el)}
+            id="NotificationDrawer"
+            className={`drawer-pf drawer-pf-notifications-non-clickable 
+            ${this.state.expanded ? "expanded" : "compact"}`}
+          >
+            {DrawerTitle}
+            <Accordion>
+              {Object.keys(this.state.accordionSections).map(sectionKey => {
+                const section = this.state.accordionSections[sectionKey];
+                return (
+                  <AccordionItem key={`${sectionKey}-item`}>
+                    <AccordionToggle
+                      onClick={() => this.toggleDrawer(sectionKey)}
+                      isExpanded={section.isOpen}
+                      key={sectionKey}
+                      id={sectionKey}
+                    >
+                      {section.title}
+                      <span className="panel-counter">{`${
+                        section.events.filter(e => !e.isRead).length
+                      } new ${safePlural(
+                        section.events.filter(e => !e.isRead).length,
+                        "event"
+                      )}`}</span>
+                    </AccordionToggle>
+                    <AccordionContent
+                      key={`${sectionKey}-content`}
+                      isHidden={!section.isOpen}
+                      isFixed
+                    >
+                      <div
+                        className={`panel-body ${
+                          section.events.length === 0 ? "hidden" : ""
+                        }`}
+                      >
+                        {section.events.map((event, i) => {
+                          return (
+                            <div
+                              key={`${sectionKey}-event-${i}`}
+                              className={`drawer-pf-notification ${
+                                event.isRead ? "" : "unread"
+                              }`}
+                              onClick={() => this.markAsRead(event)}
+                            >
+                              {severityIcon(event)}
+                              <div className="drawer-pf-notification-content">
+                                <span className="drawer-pf-notification-message">
+                                  {event.message}
+                                </span>
+                                <div className="drawer-pf-notification-info">
+                                  <span className="date">{event.date}</span>
+                                  <span className="time">{event.time}</span>
+                                </div>
+                              </div>
+                            </div>
+                          );
+                        })}
+                      </div>
+                      <div
+                        className={`blank-slate-pf ${
+                          section.events.length === 0 ? "" : "hidden"
+                        }`}
+                      >
+                        <div className="blank-slate-pf-icon">
+                          <span className="pficon pficon-info"></span>
+                        </div>
+                        <h1>There are no notifications to display.</h1>
+                      </div>
+                      <div
+                        className={`drawer-pf-action ${
+                          section.events.length > 0 ? "" : "hidden"
+                        }`}
+                      >
+                        <div className="drawer-pf-action-link">
+                          <button
+                            className={`btn btn-link ${
+                              this.hasUnread(section) ? "" : "disabled"
+                            }`}
+                            onClick={() => this.markAllRead(sectionKey)}
+                          >
+                            Mark All Read
+                          </button>
+                        </div>
+                        <div className="drawer-pf-action-link">
+                          <button
+                            className="btn btn-link"
+                            onClick={() => this.clearAll(sectionKey)}
+                          >
+                            <span className="pficon pficon-close"></span>
+                            Clear All
+                          </button>
+                        </div>
+                      </div>
+                    </AccordionContent>
+                  </AccordionItem>
+                );
+              })}
+            </Accordion>
+          </div>
+        )}
+      </>
+    );
+  }
+}
+
+export default NotificationDrawer;
diff --git a/console/react/src/qdrGlobals.js b/console/react/src/qdrGlobals.js
index a121bfb..8dc2b47 100644
--- a/console/react/src/qdrGlobals.js
+++ b/console/react/src/qdrGlobals.js
@@ -63,3 +63,14 @@ export var getConfigVars = () =>
     document.title = s.QDR_CONSOLE_TITLE;
     resolve(s);
   });
+
+export const safePlural = (count, str) => {
+  if (count === 1) return str;
+  var es = ["x", "ch", "ss", "sh"];
+  for (var i = 0; i < es.length; ++i) {
+    if (str.endsWith(es[i])) return str + "es";
+  }
+  if (str.endsWith("y")) return str.substr(0, str.length - 2) + "ies";
+  if (str.endsWith("s")) return str;
+  return str + "s";
+};
diff --git a/console/react/src/show-d3-svg.js b/console/react/src/show-d3-svg.js
deleted file mode 100644
index 4144b04..0000000
--- a/console/react/src/show-d3-svg.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import React from "react";
-import Graph from "./graph";
-
-class ShowD3SVG extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = this.setNodesLinks();
-  }
-
-  // if the number of routers has changed
-  // recreate the nodes and links arrays
-  componentDidUpdate(prevProps) {
-    if (this.props.routers !== prevProps.routers) {
-      this.setState(this.addClients(this.setNodesLinks()));
-    }
-  }
-
-  addClients = state => {
-    const { nodes, links } = state;
-    const midX = this.props.dimensions.width / 2;
-    const midY = this.props.dimensions.height / 2;
-    for (const r in this.props.routerInfo) {
-      if (this.props.routerInfo.hasOwnProperty(r)) {
-        const info = this.props.routerInfo[r];
-        const parent = nodes.find(n => n.name === r);
-        if (parent) {
-          // addNodesInCircle = (nodes, start, count, midX, midY, displace, rotate) => {
-          info.forEach((inf, i) => {
-            const node = this.node(nodes.length, midX, midY);
-            node.parent = parent.val;
-            node.key = `${node.key}.C${i}`;
-            node.name = `C${i}`;
-            node.type = inf.client;
-            const { x, y } = this.getXY(
-              i,
-              info.length,
-              parent.x,
-              parent.y,
-              0,
-              40
-            );
-            node.x = x;
-            node.y = y;
-            nodes.push(node);
-            const l = this.link(parent.val, node.val);
-            l.type = "client";
-            links.push(l);
-          });
-        }
-      }
-    }
-    return state;
-  };
-
-  link = (s, t) => ({
-    source: s,
-    target: t,
-    key: `${s}:${t}`,
-    size: 2,
-    type: "router"
-  });
-  setLinks = (topology, start, count) => {
-    let links = [];
-    if (topology === "linear") {
-      for (let i = start; i < start + count - 1; i++) {
-        links.push(this.link(i, i + 1));
-      }
-    } else if (topology === "mesh") {
-      for (let i = start; i < start + count - 1; i++) {
-        for (let j = i + 1; j < start + count; j++) {
-          links.push(this.link(i, j));
-        }
-      }
-    } else if (topology === "star") {
-      for (let i = start; i < start + count; i++) {
-        links.push(this.link(start, i));
-      }
-    } else if (topology === "ring") {
-      for (let i = start; i < start + count - 1; i++) {
-        links.push(this.link(i, i + 1));
-      }
-      if (start + count > 2) links.push(this.link(start + count - 1, start));
-    } else if (topology === "ted") {
-      if (count < 3) {
-        links = this.setLinks("linear", 0, count);
-      } else {
-        links = this.setLinks("mesh", 2, count - 2);
-        links.push(this.link(0, 2));
-        links.push(this.link(0, count - 1));
-        links.push(this.link(1, Math.floor((count - 2) / 2) + 1));
-        links.push(this.link(1, Math.floor((count - 2) / 2) + 2));
-      }
-    } else if (topology === "bar_bell") {
-      links.push(this.link(0, Math.ceil(count / 2)));
-      links = links.concat(this.setLinks("ring", 0, Math.ceil(count / 2)));
-      links = links.concat(
-        this.setLinks("ring", Math.ceil(count / 2), Math.floor(count / 2))
-      );
-    } else if (topology === "random") {
-      // random int from min to max inclusive
-      let randomIntFromInterval = (min, max) =>
-        Math.floor(Math.random() * (max - min + 1) + min);
-      // are two nodes already connected
-      let isConnected = (s, t) => {
-        if (s === t) return true;
-        return links.some(l => {
-          return (
-            (l.source === s && l.target === t) ||
-            (l.target === s && l.source === t)
-          );
-        });
-      };
-
-      // connect all nodes
-      for (let i = 1; i < this.props.routers; i++) {
-        let source = randomIntFromInterval(0, i - 1);
-        links.push(this.link(source, i));
-      }
-      // randomly add n-1 connections
-      for (let i = 0; i < this.props.routers - 1; i++) {
-        let source = randomIntFromInterval(0, this.props.routers - 1);
-        let target = randomIntFromInterval(0, this.props.routers - 1);
-        if (!isConnected(source, target)) {
-          links.push(this.link(source, target));
-        }
-      }
-    }
-    return links;
-  };
-
-  node = (i, x, y) => ({
-    key: `key_${i}`,
-    name: `R${i}`,
-    val: i,
-    size: this.props.radius ? this.props.radius : 15,
-    x: x,
-    y: y,
-    parentKey: "cluster",
-    r: 8
-  });
-
-  getXY = (start, count, midX, midY, rotate, displace) => {
-    const ang = (start * 2.0 * Math.PI) / count + rotate;
-    const x = midX + Math.cos(ang) * displace;
-    const y = midY + Math.sin(ang) * displace;
-    return { x, y };
-  };
-
-  addNodesInCircle = (nodes, start, count, midX, midY, displace, rotate) => {
-    rotate = rotate || 0;
-    for (let i = start; i < start + count; i++) {
-      const { x, y } = this.getXY(
-        i - start,
-        count,
-        midX,
-        midY,
-        rotate,
-        displace
-      );
-      nodes.push(this.node(i, x, y));
-    }
-  };
-  setNodesLinks = () => {
-    const nodes = [];
-    let links = [];
-    const midX = this.props.dimensions.width / 2;
-    const midY = this.props.dimensions.height / 2;
-    const displace = this.props.dimensions.height / 2;
-
-    // create the routers
-    // set their starting positions in a circle
-    if (this.props.topology === "ted" && this.props.routers > 1) {
-      nodes.push(this.node(0, this.props.dimensions.width, midY));
-      nodes.push(this.node(1, 0, midY));
-      this.addNodesInCircle(
-        nodes,
-        2,
-        this.props.routers - 2,
-        midX,
-        midY,
-        displace,
-        Math.PI / (this.props.routers - 2)
-      );
-    } else if (this.props.topology === "bar_bell" && this.props.routers > 1) {
-      this.addNodesInCircle(
-        nodes,
-        0, // start
-        Math.ceil(this.props.routers / 2), // count
-        this.props.dimensions.width / 4, // midX
-        midY, // midY
-        displace / 3 // displace
-      );
-      this.addNodesInCircle(
-        nodes,
-        Math.ceil(this.props.routers / 2),
-        Math.floor(this.props.routers / 2),
-        this.props.dimensions.width * 0.75,
-        midY,
-        displace / 3,
-        Math.PI
-      );
-    } else if (this.props.center && this.props.routers > 1) {
-      nodes.push(this.node(0, midX, midY));
-      this.addNodesInCircle(
-        nodes,
-        1,
-        this.props.routers - 1,
-        midX,
-        midY,
-        displace
-      );
-    } else if (this.props.routers === 1) {
-      nodes.push(this.node(0, midX, midY));
-    } else {
-      this.addNodesInCircle(nodes, 0, this.props.routers, midX, midY, displace);
-    }
-    if (this.props.routers > 1)
-      links = this.setLinks(this.props.topology, 0, this.props.routers);
-    return { nodes: nodes, links: links };
-  };
-
-  render() {
-    const { nodes, links } = this.state;
-    return (
-      <Graph
-        nodes={nodes}
-        links={links}
-        dimensions={this.props.dimensions}
-        thumbNail={this.props.thumbNail}
-        notifyCurrentRouter={this.props.notifyCurrentRouter}
-      />
-    );
-  }
-}
-
-export default ShowD3SVG;
diff --git a/console/react/src/topology-context.js b/console/react/src/topology-context.js
deleted file mode 100644
index 85d7951..0000000
--- a/console/react/src/topology-context.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import React from "react";
-import FieldDetails from "./field-details";
-import { RouterStates } from "./nodes";
-import EmptySelection from "./empty-selection";
-
-class TopologyContext extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {};
-
-    this.contexts = {
-      interior: {
-        title: "Namespace",
-        fields: [
-          { title: "Name", type: "text", isRequired: true },
-          {
-            title: "State",
-            type: "states",
-            options: RouterStates
-          },
-          {
-            title: "Type",
-            type: "radio",
-            options: ["Kube", "okd", "OC 3.11", "OC 4.1", "unknown"]
-          },
-          { title: "Route-suffix", type: "text" },
-          { title: "Namespace", type: "text" }
-        ],
-        actions: [
-          {
-            title: "Delete",
-            onClick: this.props.handleDeleteRouter,
-            confirm: true
-          }
-        ]
-      },
-      edgeClass: {
-        title: "Edge class",
-        fields: [{ title: "Name", type: "text", isRequired: true }],
-        actions: [
-          {
-            title: "Delete",
-            onClick: this.props.handleDeleteRouter,
-            confirm: true
-          }
-        ],
-        extra: { title: "Edge namespaces", type: "edgeTable" }
-      },
-      edge: {
-        title: "Edge namespace",
-        fields: [{ title: "Name", type: "text", isRequired: true }],
-        actions: [
-          {
-            title: "Delete",
-            onClick: this.props.handleDeleteRouter,
-            confirm: true
-          }
-        ]
-      },
-      connector: {
-        title: "Connection",
-        fields: [
-          { title: "connector type", type: "label" },
-          { title: "connector", type: "label" },
-          { title: "listener", type: "label" }
-        ],
-        actions: [
-          {
-            title: "Delete",
-            onClick: this.props.handleDeleteConnection,
-            confirm: true
-          },
-          {
-            title: "Reverse",
-            onClick: this.props.handleReverseConnection,
-            isDisabled: this.isActionDisabled
-          }
-        ]
-      }
-    };
-  }
-
-  isActionDisabled = (title, networkInfo, selectedKey) => {
-    if (title === "Reverse") {
-      const currentLink = networkInfo.links.find(l => l.key === selectedKey);
-      if (currentLink) {
-        if (currentLink["connector type"] === "edge") return true;
-      }
-    }
-    return false;
-  };
-
-  isRequired = (title, networkInfo, selectedKey) => {
-    // if there are any links going to this node, suffix and namespace are required
-    if (title === "Route-suffix" || title === "Namespace")
-      return networkInfo.links.some(l => l.source.key === selectedKey);
-    return false;
-  };
-
-  render() {
-    let currentContext = null;
-    const currentNode = this.props.networkInfo.nodes.find(
-      n => n.key === this.props.selectedKey
-    );
-    if (currentNode) {
-      currentContext = this.contexts[currentNode.type];
-    } else {
-      const currentLink = this.props.networkInfo.links.find(
-        l => l.key === this.props.selectedKey
-      );
-      if (currentLink) {
-        currentContext = this.contexts[currentLink.type];
-      }
-    }
-
-    if (!currentContext) {
-      return (
-        <div>
-          <EmptySelection />
-        </div>
-      );
-    }
-    return (
-      <FieldDetails
-        details={currentContext}
-        networkInfo={this.props.networkInfo}
-        selectedKey={this.props.selectedKey}
-        handleEditField={this.props.handleEditField}
-        handleAddEdge={this.props.handleAddEdge}
-        handleDeleteEdge={this.props.handleDeleteEdge}
-        handleEdgeNameChange={this.props.handleEdgeNameChange}
-        handleSelectEdgeRow={this.props.handleSelectEdgeRow}
-        handleRadioChange={this.props.handleRadioChange}
-      />
-    );
-  }
-}
-
-export default TopologyContext;


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