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/27 13:36:10 UTC

[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Changes requested from reviews

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 4d7ba1e  Changes requested from reviews
4d7ba1e is described below

commit 4d7ba1eece35c3fb6dfb26328fde1c83803f9592
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Wed Nov 27 08:35:54 2019 -0500

    Changes requested from reviews
---
 console/react/public/config.json                   |   2 +-
 console/react/src/App.css                          | 115 +++++++----------
 console/react/src/chord/chordPage.js               |   9 +-
 .../throughputChart.js => chord/chordPage.test.js} |  24 ++--
 console/react/src/chord/chordToolbar.js            |  13 ++
 console/react/src/chord/chordViewer.js             |  11 +-
 console/react/src/chord/legendComponent.js         | 110 ----------------
 console/react/src/chord/optionsComponent.js        |  11 ++
 console/react/src/chord/routersComponent.js        |  48 +++----
 console/react/src/common/DropdownMenu.js           |   8 ++
 console/react/src/common/DropdownMenu.test.js      |   2 +
 console/react/src/common/addressesComponent.js     |   7 +
 .../react/src/common/addressesComponent.test.js    |   4 +-
 console/react/src/common/amqp/connection.js        |  10 +-
 console/react/src/common/amqp/correlator.js        |  19 ++-
 console/react/src/common/connectionClose.js        |  11 +-
 ...xtMenu.test.js => contextMenuComponent.test.js} |   0
 console/react/src/common/dropdownPanel.js          |   5 +
 console/react/src/common/qdrService.js             |   2 +-
 console/react/src/common/tableToolbar.js           |  28 ++--
 console/react/src/common/updated.js                |   4 +-
 console/react/src/details/createTablePage.js       |  66 ++++------
 console/react/src/details/createTablePage.test.js  |   4 +-
 .../react/src/details/dataSources/defaultData.js   |   4 +
 console/react/src/details/dataSources/logsData.js  |  20 +++
 console/react/src/details/deleteEntity.test.js     |  24 +++-
 console/react/src/details/detailsTablePage.js      |   6 +-
 .../updated.js => details/emptyTablePage.js}       |  30 ++++-
 .../logsData.js => emptyTablePage.test.js}         |  19 ++-
 console/react/src/details/entityListTable.js       |  17 ++-
 console/react/src/details/routerSelect.js          |  37 +++---
 console/react/src/details/updateTablePage.js       |  23 +++-
 console/react/src/overview/dashboard/alertList.js  |  37 +++++-
 .../react/src/overview/dashboard/alertList.test.js |   5 +-
 console/react/src/overview/dashboard/chartData.js  |  19 ++-
 .../react/src/overview/dashboard/dashboardPage.js  |   2 +-
 .../overview/dashboard/delayedDeliveriesCard.js    |   2 +-
 .../react/src/overview/dashboard/inflightChart.js  |   5 +-
 console/react/src/overview/dashboard/layout.js     |   3 +-
 .../src/overview/dashboard/notificationDrawer.js   |   2 +-
 .../src/overview/dashboard/throughputChart.js      |   2 +-
 .../react/src/overview/dataSources/routerData.js   |   1 -
 console/react/src/overview/overviewTable.js        |   1 +
 console/react/src/topology/legend.js               |  24 ++--
 console/react/src/topology/links.js                |  33 +++--
 console/react/src/topology/nodes.js                |  83 +++++-------
 console/react/src/topology/topoUtils.js            |  17 +--
 console/react/src/topology/topologyPage.js         |   4 +-
 console/react/src/topology/topologyViewer.js       | 142 ++++++++++++---------
 console/react/src/topology/topologyViewer.test.js  |   2 +-
 console/react/src/topology/traffic.js              |  18 +++
 console/react/src/topology/trafficComponent.js     |  10 ++
 52 files changed, 578 insertions(+), 527 deletions(-)

diff --git a/console/react/public/config.json b/console/react/public/config.json
index 6b19668..666f5bf 100644
--- a/console/react/public/config.json
+++ b/console/react/public/config.json
@@ -1,3 +1,3 @@
 {
-  "title": "Apache Qpid Dispach Console"
+  "title": "APACHE QPID DISAPTCH CONSOLE"
 }
diff --git a/console/react/src/App.css b/console/react/src/App.css
index e4a51a6..bb44280 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -21,84 +21,15 @@ under the License.
   height: 100vh;
 }
 
-/*
-#main-content-page-layout-manual-nav {
-  overflow-y: hidden;
-}
-*/
-
 .App {
   text-align: center;
   height: 100%;
 }
 
-.App-logo {
-  animation: App-logo-spin infinite 20s linear;
-  height: 40vmin;
-  pointer-events: none;
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
 .pf-c-avatar {
   --pf-c-avatar--Width: initial !important;
 }
 
-.donut-chart-sm {
-  width: 8em;
-  height: 8em;
-}
-
-.donut-chart-sm tspan {
-  fill: #000000 !important;
-}
-
-.deployment-donut {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-}
-
-.deployment-donut .deployment-donut-row {
-  display: flex;
-}
-
-.deployment-donut .deployment-donut-column {
-  display: flex;
-  flex-direction: column;
-}
-
-.deployment-donut .scaling-controls {
-  justify-content: center;
-  font-size: 24px;
-}
-
-.deployment-donut .scaling-controls a {
-  color: #bbb;
-}
-
 .topo-middle {
   margin: auto;
 }
@@ -354,6 +285,7 @@ div.state-container button.pf-c-clipboard-copy__group-copy {
   width: 40em !important;
   position: absolute !important;
   right: 0;
+  top: 4.5em;
   padding: 2em;
   background-color: #fafafa;
   border: 1px solid #d1d1d1;
@@ -983,6 +915,7 @@ div.qdrChord .legend-text {
 
 .duration-tabs li.selected {
   border-bottom: 4px solid blue;
+  color: blue;
 }
 
 .overview-charts-page .pf-c-table caption {
@@ -1210,6 +1143,7 @@ span.entity-type i.link-type-router-control:before {
   top: 4.8em;
   color: black;
   right: 0em;
+  max-height: calc(100vh - 80px);
 }
 
 .drawer-pf-title {
@@ -1389,6 +1323,31 @@ span.entity-type i.link-type-router-control:before {
   }
 }
 
+.alert-in {
+  animation: fadeIn 0.25s linear;
+}
+.alert-out {
+  animation: fadeOut 1s linear;
+}
+@keyframes fadeIn {
+  0% {
+    opacity: 0;
+  }
+
+  100% {
+    opacity: 1;
+  }
+}
+@keyframes fadeOut {
+  0% {
+    opacity: 1;
+  }
+
+  100% {
+    opacity: 0;
+  }
+}
+
 #topicCogWrapper {
   position: relative;
   margin: auto;
@@ -1430,7 +1389,7 @@ span.entity-type i.link-type-router-control:before {
 }
 
 div.connecting {
-  opacity: 0.2;
+  opacity: 0;
 }
 
 .connect-error p {
@@ -1501,8 +1460,24 @@ button.dropdown-panel-toggle.pf-c-accordion__toggle span.pf-c-accordion__toggle-
   background-image: none;
 }
 
+#NotificationDrawer .pf-c-accordion__expanded-content.pf-m-fixed {
+  max-height: none;
+}
+
 .dropdown-panel-accordion.pf-c-accordion {
   box-shadow: none;
   border: 1px solid #eaeaea;
   border-bottom: 1px solid black;
 }
+
+.pf-c-modal-box.pf-m-sm * {
+  font-family: RedHatDisplay;
+}
+
+.form-error {
+  color: red;
+}
+
+#emptyResults {
+  height: auto;
+}
diff --git a/console/react/src/chord/chordPage.js b/console/react/src/chord/chordPage.js
index 7005d1b..e43cd1d 100644
--- a/console/react/src/chord/chordPage.js
+++ b/console/react/src/chord/chordPage.js
@@ -20,13 +20,16 @@ under the License.
 import React, { Component } from "react";
 import { PageSection, PageSectionVariants } from "@patternfly/react-core";
 import ChordViewer from "./chordViewer";
+import PropTypes from "prop-types";
 
 class MessageFlowPage extends Component {
+  static propTypes = {
+    service: PropTypes.object.isRequired
+  };
+
   constructor(props) {
     super(props);
-    this.state = {
-      lastUpdated: new Date()
-    };
+    this.state = {};
   }
 
   render() {
diff --git a/console/react/src/overview/dashboard/throughputChart.js b/console/react/src/chord/chordPage.test.js
similarity index 67%
copy from console/react/src/overview/dashboard/throughputChart.js
copy to console/react/src/chord/chordPage.test.js
index 6600df4..71c028d 100644
--- a/console/react/src/overview/dashboard/throughputChart.js
+++ b/console/react/src/chord/chordPage.test.js
@@ -17,16 +17,18 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import ChartBase from "./chartBase";
+import React from "react";
+import { render } from "@testing-library/react";
+import { service, login } from "../serviceTest";
+import ChordPage from "./chordPage";
 
-class ThroughputChart extends ChartBase {
-  constructor(props) {
-    super(props);
-    this.title = "Deliveries per sec";
-    this.color = "#99C2EB"; //ChartThemeColor.blue;
-    this.setStyle(this.color);
-    this.ariaLabel = "throughput-chart";
-  }
-}
+it("renders the ChordPage", async () => {
+  await login();
+  expect(service.management.connection.is_connected()).toBe(true);
 
-export default ThroughputChart;
+  const props = {
+    service
+  };
+
+  render(<ChordPage {...props} />);
+});
diff --git a/console/react/src/chord/chordToolbar.js b/console/react/src/chord/chordToolbar.js
index d2a5226..2144683 100644
--- a/console/react/src/chord/chordToolbar.js
+++ b/console/react/src/chord/chordToolbar.js
@@ -22,8 +22,21 @@ import { Toolbar, ToolbarGroup, ToolbarItem } from "@patternfly/react-core";
 import OptionsComponent from "./optionsComponent";
 import RoutersComponent from "./routersComponent";
 import DropdownPanel from "../common/dropdownPanel";
+import PropTypes from "prop-types";
 
 class ChordToolbar extends React.Component {
+  static propTypes = {
+    isRate: PropTypes.bool.isRequired,
+    byAddress: PropTypes.bool.isRequired,
+    addresses: PropTypes.object.isRequired,
+    chordColors: PropTypes.object.isRequired,
+    arcColors: PropTypes.object.isRequired,
+    handleChangeAddress: PropTypes.func.isRequired,
+    handleChangeOption: PropTypes.func.isRequired,
+    handleHoverAddress: PropTypes.func.isRequired,
+    handleHoverRouter: PropTypes.func.isRequired
+  };
+
   render() {
     return (
       <Toolbar
diff --git a/console/react/src/chord/chordViewer.js b/console/react/src/chord/chordViewer.js
index 0d2c177..34a59e8 100644
--- a/console/react/src/chord/chordViewer.js
+++ b/console/react/src/chord/chordViewer.js
@@ -27,6 +27,7 @@ import { qdrlayoutChord } from "./layout/layout.js";
 import ChordToolbar from "./chordToolbar";
 import QDRPopup from "../common/qdrPopup";
 import * as d3 from "d3";
+import PropTypes from "prop-types";
 
 const CHORDOPTIONSKEY = "chordOptions";
 const CHORDFILTERKEY = "chordFilter";
@@ -38,10 +39,14 @@ const MIN_RADIUS = 200;
 const TRANSITION_DURATION = 1000;
 
 class ChordViewer extends Component {
+  static propTypes = {
+    service: PropTypes.object.isRequired
+  };
+
   constructor(props) {
     super(props);
     this.state = {
-      addresses: [],
+      addresses: {},
       showPopup: false,
       popupContent: "",
       showEmpty: false,
@@ -192,7 +197,7 @@ class ChordViewer extends Component {
 
   // size the diagram based on the browser window size
   getRadius = () => {
-    const { width, height } = getSizes(this.chordRef);
+    const { width, height } = getSizes("chordContainer");
     return Math.max(Math.floor((Math.min(width, height) * 0.9) / 2), MIN_RADIUS);
   };
 
@@ -932,7 +937,7 @@ class ChordViewer extends Component {
         sideBarOpen={false}
         className="qdrTopology"
       >
-        <div ref={el => (this.chordRef = el)} className="qdrChord">
+        <div id="chordContainer" className="qdrChord">
           {this.state.showEmpty ? (
             <div aria-label="chord-no-traffic" id="noTraffic">
               {this.state.emptyText}
diff --git a/console/react/src/chord/legendComponent.js b/console/react/src/chord/legendComponent.js
deleted file mode 100644
index 0736984..0000000
--- a/console/react/src/chord/legendComponent.js
+++ /dev/null
@@ -1,110 +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 React, { Component } from "react";
-import {
-  Accordion,
-  AccordionItem,
-  AccordionContent,
-  AccordionToggle
-} from "@patternfly/react-core";
-import OptionsComponent from "./optionsComponent";
-import RoutersComponent from "./routersComponent";
-import AddressesComponent from "../common/addressesComponent";
-
-class LegendComponent extends Component {
-  constructor(props) {
-    super(props);
-    this.state = {};
-  }
-
-  render() {
-    const toggle = id => {
-      const idOpen = this.props[`${id}Open`];
-      this.props.handleOpenChange(id, !idOpen);
-    };
-
-    return (
-      <Accordion className="legend">
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("options")}
-            isExpanded={this.props.optionsOpen}
-            id="options"
-          >
-            Options
-          </AccordionToggle>
-          <AccordionContent
-            id="options-expand"
-            isHidden={!this.props.optionsOpen}
-            isFixed
-          >
-            <OptionsComponent
-              isRate={this.props.isRate}
-              byAddress={this.props.byAddress}
-              handleChangeOption={this.props.handleChangeOption}
-            />
-          </AccordionContent>
-        </AccordionItem>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("routers")}
-            isExpanded={this.props.routersOpen}
-            id="routers"
-          >
-            Routers
-          </AccordionToggle>
-          <AccordionContent
-            id="routers-expand"
-            isHidden={!this.props.routersOpen}
-            isFixed
-          >
-            <RoutersComponent
-              arcColors={this.props.arcColors}
-              handleHoverRouter={this.props.handleHoverRouter}
-            />
-          </AccordionContent>
-        </AccordionItem>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("addresses")}
-            isExpanded={this.props.addressesOpen}
-            id="addresses"
-          >
-            Addresses
-          </AccordionToggle>
-          <AccordionContent
-            id="addresses-expand"
-            isHidden={!this.props.addressesOpen}
-            isFixed
-          >
-            <AddressesComponent
-              addresses={this.props.addresses}
-              addressColors={this.props.chordColors}
-              handleChangeAddress={this.props.handleChangeAddress}
-              handleHoverAddress={this.props.handleHoverAddress}
-            />
-          </AccordionContent>
-        </AccordionItem>
-      </Accordion>
-    );
-  }
-}
-
-export default LegendComponent;
diff --git a/console/react/src/chord/optionsComponent.js b/console/react/src/chord/optionsComponent.js
index 4e674dc..35b9331 100644
--- a/console/react/src/chord/optionsComponent.js
+++ b/console/react/src/chord/optionsComponent.js
@@ -20,8 +20,19 @@ under the License.
 import React, { Component } from "react";
 import { Checkbox } from "@patternfly/react-core";
 import AddressesComponent from "../common/addressesComponent";
+import PropTypes from "prop-types";
 
 class OptionsComponent extends Component {
+  static propTypes = {
+    isRate: PropTypes.bool.isRequired,
+    handleChangeOption: PropTypes.func.isRequired,
+    byAddress: PropTypes.bool.isRequired,
+    addresses: PropTypes.object.isRequired,
+    addressColors: PropTypes.object.isRequired,
+    handleChangeAddress: PropTypes.func.isRequired,
+    handleHoverAddress: PropTypes.func.isRequired
+  };
+
   constructor(props) {
     super(props);
     this.state = {};
diff --git a/console/react/src/chord/routersComponent.js b/console/react/src/chord/routersComponent.js
index cebdac1..d0e67fc 100644
--- a/console/react/src/chord/routersComponent.js
+++ b/console/react/src/chord/routersComponent.js
@@ -18,8 +18,14 @@ under the License.
 */
 
 import React, { Component } from "react";
+import PropTypes from "prop-types";
 
 class RoutersComponent extends Component {
+  static propTypes = {
+    arcColors: PropTypes.object.isRequired,
+    handleHoverRouter: PropTypes.func.isRequired
+  };
+
   constructor(props) {
     super(props);
     this.state = {};
@@ -32,29 +38,25 @@ class RoutersComponent extends Component {
           {Object.keys(this.props.arcColors).length === 0 ? (
             <li key={`colors-empty`}>There is no traffic</li>
           ) : (
-              Object.keys(this.props.arcColors).map((router, i) => {
-                return (
-                  <li
-                    key={`router-${i}`}
-                    className="legend-line"
-                    onMouseEnter={() =>
-                      this.props.handleHoverRouter(router, true)
-                    }
-                    onMouseLeave={() =>
-                      this.props.handleHoverRouter(router, false)
-                    }
-                  >
-                    <span
-                      className="legend-color"
-                      style={{ backgroundColor: this.props.arcColors[router] }}
-                    ></span>
-                    <span className="legend-router legend-text" title={router}>
-                      {router}
-                    </span>
-                  </li>
-                );
-              })
-            )}
+            Object.keys(this.props.arcColors).map((router, i) => {
+              return (
+                <li
+                  key={`router-${i}`}
+                  className="legend-line"
+                  onMouseEnter={() => this.props.handleHoverRouter(router, true)}
+                  onMouseLeave={() => this.props.handleHoverRouter(router, false)}
+                >
+                  <span
+                    className="legend-color"
+                    style={{ backgroundColor: this.props.arcColors[router] }}
+                  ></span>
+                  <span className="legend-router legend-text" title={router}>
+                    {router}
+                  </span>
+                </li>
+              );
+            })
+          )}
         </ul>
       </React.Fragment>
     );
diff --git a/console/react/src/common/DropdownMenu.js b/console/react/src/common/DropdownMenu.js
index 30b0db7..5f58b9d 100644
--- a/console/react/src/common/DropdownMenu.js
+++ b/console/react/src/common/DropdownMenu.js
@@ -19,8 +19,16 @@ under the License.
 
 import React, { Component } from "react";
 import ContextMenuComponent from "./contextMenuComponent";
+import PropTypes from "prop-types";
 
 class DropdownMenu extends Component {
+  static propTypes = {
+    isVisible: PropTypes.bool,
+    handleDropdownLogout: PropTypes.func.isRequired,
+    isConnected: PropTypes.func.isRequired,
+    handleContextHide: PropTypes.func.isRequired,
+    parentClass: PropTypes.string.isRequired
+  };
   constructor(props) {
     super(props);
     this.state = {
diff --git a/console/react/src/common/DropdownMenu.test.js b/console/react/src/common/DropdownMenu.test.js
index 4442441..29b8940 100644
--- a/console/react/src/common/DropdownMenu.test.js
+++ b/console/react/src/common/DropdownMenu.test.js
@@ -32,7 +32,9 @@ it("the dropdown menu component renders and calls event handlers", () => {
       ref={el => (menuRef = el)}
       isVisible={isVisible}
       isConnected={isConnected}
+      parentClass=""
       handleDropdownLogout={handleDropdownLogout}
+      handleContextHide={() => {}}
     />
   );
   menuRef.show(true);
diff --git a/console/react/src/common/addressesComponent.js b/console/react/src/common/addressesComponent.js
index 154d403..acd6395 100644
--- a/console/react/src/common/addressesComponent.js
+++ b/console/react/src/common/addressesComponent.js
@@ -19,9 +19,16 @@ under the License.
 
 import React, { Component } from "react";
 import * as d3 from "d3";
+import PropTypes from "prop-types";
 const FA = require("react-fontawesome");
 
 class AddressesComponent extends Component {
+  static propTypes = {
+    addresses: PropTypes.object.isRequired,
+    handleChangeAddress: PropTypes.func.isRequired,
+    handleHoverAddress: PropTypes.func.isRequired,
+    addressColors: PropTypes.object.isRequired
+  };
   constructor(props) {
     super(props);
     this.state = {};
diff --git a/console/react/src/common/addressesComponent.test.js b/console/react/src/common/addressesComponent.test.js
index 291b861..bb01880 100644
--- a/console/react/src/common/addressesComponent.test.js
+++ b/console/react/src/common/addressesComponent.test.js
@@ -43,7 +43,9 @@ it("renders the addresses component with an address", () => {
 it("renders the addresses component without an address", () => {
   const props = {
     addresses: {},
-    addressColors: {}
+    addressColors: {},
+    handleChangeAddress: () => {},
+    handleHoverAddress: () => {}
   };
   const { getByText } = render(<AddressesComponent {...props} />);
   expect(getByText("There is no traffic")).toBeInTheDocument();
diff --git a/console/react/src/common/amqp/connection.js b/console/react/src/common/amqp/connection.js
index bd8e3c0..773810e 100644
--- a/console/react/src/common/amqp/connection.js
+++ b/console/react/src/common/amqp/connection.js
@@ -179,6 +179,11 @@ class ConnectionManager {
     }
   };
 
+  on_reconnected = () => {
+    const self = this;
+    setTimeout(self.on_connection_open, 100);
+  };
+
   createSenderReceiver = options => {
     return new Promise((resolve, reject) => {
       var timeout = options.timeout || 10000;
@@ -199,7 +204,7 @@ class ConnectionManager {
         // in case this connection dies
         this.rhea.on("disconnected", this.on_disconnected);
         // in case this connection dies and is then reconnected automatically
-        this.rhea.on("connection_open", this.on_connection_open);
+        this.rhea.on("connection_open", this.on_reconnected);
         // receive messages here
         this.connection.on("message", this.on_message);
         resolve(context);
@@ -222,10 +227,12 @@ class ConnectionManager {
   };
 
   connect = options => {
+    this.options = options;
     return new Promise((resolve, reject) => {
       var finishConnecting = () => {
         this.createSenderReceiver(options).then(
           results => {
+            this.on_connection_open();
             resolve(results);
           },
           error => {
@@ -295,7 +302,6 @@ class ConnectionManager {
         clearTimeout(timer);
         // prevent future disconnects from calling reject
         this.rhea.removeListener("disconnected", timedOut);
-        this.on_connection_open();
         resolve({ context: context });
       };
       // register an event handler for when the connection opens
diff --git a/console/react/src/common/amqp/correlator.js b/console/react/src/common/amqp/correlator.js
index c9f3e99..1148336 100644
--- a/console/react/src/common/amqp/correlator.js
+++ b/console/react/src/common/amqp/correlator.js
@@ -25,9 +25,8 @@ class Correlator {
     this._correlationID = 0;
     this.maxCorrelatorDepth = 10;
   }
-  corr() {
-    return ++this._correlationID + "";
-  }
+  corr = () => `${++this._correlationID}`;
+
   // Associate this correlation id with the promise's resolve and reject methods
   register(id, resolve, reject) {
     this._objects[id] = { resolver: resolve, rejector: reject };
@@ -37,10 +36,16 @@ class Correlator {
   resolve(context) {
     var correlationID = context.message.correlation_id;
     // call the promise's resolve function with a copy of the rhea response (so we don't keep any references to internal rhea data)
-    this._objects[correlationID].resolver({
-      response: utils.copy(context.message.body),
-      context: context
-    });
+    if (this._objects[correlationID]) {
+      this._objects[correlationID].resolver({
+        response: utils.copy(context.message.body),
+        context: context
+      });
+    } else {
+      console.log(
+        `recieved message without a corresponding correlationID ${correlationID}`
+      );
+    }
     delete this._objects[correlationID];
   }
   reject(id, error) {
diff --git a/console/react/src/common/connectionClose.js b/console/react/src/common/connectionClose.js
index dae1253..aee5200 100644
--- a/console/react/src/common/connectionClose.js
+++ b/console/react/src/common/connectionClose.js
@@ -19,8 +19,16 @@ under the License.
 
 import React from "react";
 import { Button, Modal } from "@patternfly/react-core";
+import PropTypes from "prop-types";
 
 class ConnectionClose extends React.Component {
+  static propTypes = {
+    extraInfo: PropTypes.object.isRequired,
+    service: PropTypes.object.isRequired,
+    handleAddNotification: PropTypes.func.isRequired,
+    asButton: PropTypes.bool,
+    notifyClick: PropTypes.func
+  };
   constructor(props) {
     super(props);
     this.state = {
@@ -47,8 +55,7 @@ class ConnectionClose extends React.Component {
           { adminStatus: "deleted" }
         )
         .then(results => {
-          let statusCode =
-            results.context.message.application_properties.statusCode;
+          let statusCode = results.context.message.application_properties.statusCode;
           if (statusCode < 200 || statusCode >= 300) {
             console.log(
               `error ${record.name} ${results.context.message.application_properties.statusDescription}`
diff --git a/console/react/src/common/contextMenu.test.js b/console/react/src/common/contextMenuComponent.test.js
similarity index 100%
rename from console/react/src/common/contextMenu.test.js
rename to console/react/src/common/contextMenuComponent.test.js
diff --git a/console/react/src/common/dropdownPanel.js b/console/react/src/common/dropdownPanel.js
index c34ca1c..b93f12a 100644
--- a/console/react/src/common/dropdownPanel.js
+++ b/console/react/src/common/dropdownPanel.js
@@ -24,8 +24,13 @@ import {
   AccordionContent,
   AccordionToggle
 } from "@patternfly/react-core";
+import PropTypes from "prop-types";
 
 class DropdownPanel extends React.Component {
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    panel: PropTypes.object.isRequired
+  };
   constructor(props) {
     super(props);
     this.state = {
diff --git a/console/react/src/common/qdrService.js b/console/react/src/common/qdrService.js
index d1387c7..c869a9f 100644
--- a/console/react/src/common/qdrService.js
+++ b/console/react/src/common/qdrService.js
@@ -25,7 +25,7 @@ const DEFAULT_INTERVAL = 5000;
 export class QDRService {
   constructor(hooks) {
     this.utilities = utils;
-    this.hooks = hooks;
+    this.hooks = hooks || function() {};
     this.schema = null;
     this.initManagement();
   }
diff --git a/console/react/src/common/tableToolbar.js b/console/react/src/common/tableToolbar.js
index 5d04d21..609b874 100644
--- a/console/react/src/common/tableToolbar.js
+++ b/console/react/src/common/tableToolbar.js
@@ -20,7 +20,6 @@ under the License.
 import React from "react";
 import {
   Dropdown,
-  DropdownPosition,
   DropdownToggle,
   DropdownItem,
   Pagination,
@@ -36,9 +35,7 @@ class TableToolbar extends React.Component {
     this.state = {
       isDropDownOpen: false,
       searchValue:
-        this.props.filterBy && this.props.filterBy.value
-          ? this.props.filterBy.value
-          : "",
+        this.props.filterBy && this.props.filterBy.value ? this.props.filterBy.value : "",
       filterField: this.props.fields[0].title
     };
 
@@ -86,10 +83,11 @@ class TableToolbar extends React.Component {
 
     this.buildDropdown = () => {
       const { isDropDownOpen } = this.state;
+      //           position={DropdownPosition.right}
+
       return (
         <Dropdown
           onSelect={this.onDropDownSelect}
-          position={DropdownPosition.right}
           toggle={
             <DropdownToggle onToggle={this.onDropDownToggle}>
               {this.state.filterField}
@@ -97,9 +95,7 @@ class TableToolbar extends React.Component {
           }
           isOpen={isDropDownOpen}
           dropdownItems={this.props.fields.map(f => {
-            return (
-              <DropdownItem key={`item-${f.title}`}>{f.title}</DropdownItem>
-            );
+            return <DropdownItem key={`item-${f.title}`}>{f.title}</DropdownItem>;
           })}
         />
       );
@@ -126,16 +122,10 @@ class TableToolbar extends React.Component {
     return (
       <Toolbar className="pf-l-toolbar pf-u-mx-xl pf-u-my-md table-toolbar">
         <ToolbarGroup>
-          <ToolbarItem className="pf-u-mr-md">
-            {this.buildDropdown()}
-          </ToolbarItem>
-          <ToolbarItem className="pf-u-mr-xl">
-            {this.buildSearchBox()}
-          </ToolbarItem>
+          <ToolbarItem className="pf-u-mr-md">{this.buildDropdown()}</ToolbarItem>
+          <ToolbarItem className="pf-u-mr-xl">{this.buildSearchBox()}</ToolbarItem>
         </ToolbarGroup>
-        {this.props.actionButtons && (
-          <ToolbarGroup>{actionsButtons}</ToolbarGroup>
-        )}
+        {this.props.actionButtons && <ToolbarGroup>{actionsButtons}</ToolbarGroup>}
         {!this.props.hidePagination && (
           <ToolbarGroup className="toolbar-pagination">
             <ToolbarItem>
@@ -145,9 +135,7 @@ class TableToolbar extends React.Component {
                 page={this.props.page}
                 perPage={this.props.perPage}
                 onSetPage={(_evt, value) => this.props.onSetPage(value)}
-                onPerPageSelect={(_evt, value) =>
-                  this.props.onPerPageSelect(value)
-                }
+                onPerPageSelect={(_evt, value) => this.props.onPerPageSelect(value)}
                 variant={"top"}
               />
             </ToolbarItem>
diff --git a/console/react/src/common/updated.js b/console/react/src/common/updated.js
index 1813edd..9780f34 100644
--- a/console/react/src/common/updated.js
+++ b/console/react/src/common/updated.js
@@ -27,9 +27,9 @@ class Updated extends Component {
 
   render() {
     return (
-      <pre aria-label="last-updated" data-pf-content="true" className="overview-loading">
+      <span aria-label="last-updated" data-pf-content="true" className="overview-loading">
         {`Updated ${this.props.service.utilities.strDate(this.props.lastUpdated)}`}
-      </pre>
+      </span>
     );
   }
 }
diff --git a/console/react/src/details/createTablePage.js b/console/react/src/details/createTablePage.js
index cd14c5a..616fad9 100644
--- a/console/react/src/details/createTablePage.js
+++ b/console/react/src/details/createTablePage.js
@@ -20,6 +20,7 @@ under the License.
 import React from "react";
 import { PageSection, PageSectionVariants } from "@patternfly/react-core";
 import {
+  Alert,
   Button,
   Stack,
   StackItem,
@@ -62,7 +63,8 @@ class CreateTablePage extends React.Component {
       redirectState: { page: 1 },
       redirectPath: "/dashboard",
       lastUpdated: new Date(),
-      record: {}
+      record: {},
+      errorText: null
     };
 
     // if we get to this page and we don't have a props.location.state.entity
@@ -76,21 +78,16 @@ class CreateTablePage extends React.Component {
         this.props.location.state.entity);
 
     if (!this.entity) {
-      this.state.redirect = true;
+      this.props.history.push("/dashboard");
     } else {
       this.dataSource = !detailsDataMap[this.entity]
         ? new defaultData(this.props.service, this.props.schema)
-        : new detailsDataMap[this.entity](
-          this.props.service,
-          this.props.schema
-        );
+        : new detailsDataMap[this.entity](this.props.service, this.props.schema);
       this.locationState = this.props.locationState;
 
       const attributes = this.dataSource.schemaAttributes(this.entity);
       for (let attributeKey in attributes) {
-        this.state.record[attributeKey] = this.getDefault(
-          attributes[attributeKey]
-        );
+        this.state.record[attributeKey] = this.getDefault(attributes[attributeKey]);
       }
     }
   }
@@ -119,6 +116,9 @@ class CreateTablePage extends React.Component {
   schemaToForm = () => {
     const attributes = this.dataSource.schemaAttributes(this.entity);
     const formGroups = [];
+    if (this.state.errorText) {
+      formGroups.push(<Alert variant="danger" isInline title={this.state.errorText} />);
+    }
     for (let attributeKey in attributes) {
       if (attributeKey !== "identity") {
         const attribute = attributes[attributeKey];
@@ -166,9 +166,7 @@ class CreateTablePage extends React.Component {
                     aria-describedby="entiy-form-field"
                     name={attributeKey}
                     isDisabled={readOnly}
-                    onChange={value =>
-                      this.handleTextInputChange(value, attributeKey)
-                    }
+                    onChange={value => this.handleTextInputChange(value, attributeKey)}
                   />
                 </FormGroup>
               );
@@ -177,9 +175,7 @@ class CreateTablePage extends React.Component {
                 <FormGroup {...formGroupProps} key={attributeKey}>
                   <FormSelect
                     value={this.state.record[attributeKey]}
-                    onChange={value =>
-                      this.handleTextInputChange(value, attributeKey)
-                    }
+                    onChange={value => this.handleTextInputChange(value, attributeKey)}
                     id={id}
                     name={attributeKey}
                   >
@@ -202,9 +198,7 @@ class CreateTablePage extends React.Component {
                     label={attributeKey}
                     id={id}
                     name={attributeKey}
-                    onChange={value =>
-                      this.handleTextInputChange(value, attributeKey)
-                    }
+                    onChange={value => this.handleTextInputChange(value, attributeKey)}
                   />
                 </FormGroup>
               );
@@ -244,31 +238,25 @@ class CreateTablePage extends React.Component {
         attributes[attr] = record[attr];
     }
 
-    // call update
+    // call create
     this.props.service.management.connection
       .sendMethod(this.props.routerId, this.entity, attributes, "CREATE")
       .then(results => {
-        let statusCode =
-          results.context.message.application_properties.statusCode;
+        let statusCode = results.context.message.application_properties.statusCode;
         if (statusCode < 200 || statusCode >= 300) {
-          let message =
-            results.context.message.application_properties.statusDescription;
-          const msg = `Create failed with message: ${message}`;
+          let message = results.context.message.application_properties.statusDescription;
+          //const msg = `Create failed with message: ${message}`;
           console.log(
             `error Create failed ${results.context.message.application_properties.statusDescription}`
           );
-          this.props.handleAddNotification("action", msg, new Date(), "danger");
+          //this.props.handleAddNotification("action", msg, new Date(), "danger");
+          this.setState({ errorText: message });
         } else {
           const msg = `Created ${this.props.entity} ${record.name}`;
           console.log(`success ${msg}`);
-          this.props.handleAddNotification(
-            "action",
-            msg,
-            new Date(),
-            "success"
-          );
+          this.props.handleAddNotification("action", msg, new Date(), "success");
+          this.handleCancel();
         }
-        this.handleCancel();
       });
   };
 
@@ -286,17 +274,11 @@ class CreateTablePage extends React.Component {
 
     return (
       <React.Fragment>
-        <PageSection
-          variant={PageSectionVariants.light}
-          className="overview-table-page"
-        >
+        <PageSection variant={PageSectionVariants.light} className="overview-table-page">
           <Stack>
             <StackItem className="overview-header details">
               <Breadcrumb>
-                <BreadcrumbItem
-                  className="link-button"
-                  onClick={this.breadcrumbSelected}
-                >
+                <BreadcrumbItem className="link-button" onClick={this.breadcrumbSelected}>
                   {this.icap(this.entity)}
                 </BreadcrumbItem>
               </Breadcrumb>
@@ -319,7 +301,9 @@ class CreateTablePage extends React.Component {
             <StackItem id="update-form">
               <Card>
                 <CardBody>
-                  <Form isHorizontal aria-label="create-entity-form">{this.schemaToForm()}</Form>
+                  <Form isHorizontal aria-label="create-entity-form">
+                    {this.schemaToForm()}
+                  </Form>
                 </CardBody>
               </Card>
             </StackItem>
diff --git a/console/react/src/details/createTablePage.test.js b/console/react/src/details/createTablePage.test.js
index 09c51ea..28c4a07 100644
--- a/console/react/src/details/createTablePage.test.js
+++ b/console/react/src/details/createTablePage.test.js
@@ -39,9 +39,7 @@ it("renders a CreateTablePage", async () => {
     handleAddNotification: () => {},
     handleActionCancel: () => {}
   };
-  const { getByLabelText, getByText, getByTestId } = render(
-    <CreateTablePage {...props} />
-  );
+  const { getByLabelText, getByText } = render(<CreateTablePage {...props} />);
 
   // the create form should be present
   const createForm = getByLabelText("create-entity-form");
diff --git a/console/react/src/details/dataSources/defaultData.js b/console/react/src/details/dataSources/defaultData.js
index d448d1d..5711768 100644
--- a/console/react/src/details/dataSources/defaultData.js
+++ b/console/react/src/details/dataSources/defaultData.js
@@ -127,6 +127,10 @@ class DefaultData {
       });
     });
   };
+
+  validate = record => {
+    return { validated: true };
+  };
 }
 
 export default DefaultData;
diff --git a/console/react/src/details/dataSources/logsData.js b/console/react/src/details/dataSources/logsData.js
index daa2553..2490928 100644
--- a/console/react/src/details/dataSources/logsData.js
+++ b/console/react/src/details/dataSources/logsData.js
@@ -26,6 +26,26 @@ class LogsData extends DefaultData {
       module: { readOnly: true }
     };
   }
+
+  validate = record => {
+    let validated = true;
+    let errorText = "";
+    const enables = ["trace", "debug", "info", "notice", "warning", "error", "critical"];
+
+    const enableParts = record.enable.split(",");
+    if (enableParts.length === 1 && enableParts[0] === "") {
+    } else {
+      enableParts.forEach(part => {
+        part = part.trim();
+        if (part.endsWith("+")) part = part.slice(0, -1);
+        if (!enables.includes(part)) {
+          errorText = `enable must be one of ${enables.join(", ")}`;
+          validated = false;
+        }
+      });
+    }
+    return { validated, errorText };
+  };
 }
 
 export default LogsData;
diff --git a/console/react/src/details/deleteEntity.test.js b/console/react/src/details/deleteEntity.test.js
index ac30005..48356c1 100644
--- a/console/react/src/details/deleteEntity.test.js
+++ b/console/react/src/details/deleteEntity.test.js
@@ -19,19 +19,37 @@ under the License.
 
 import React from "react";
 import { render, fireEvent } from "@testing-library/react";
+import { service, login } from "../serviceTest";
 import DeleteEntity from "./deleteEntity";
 
-it("renders DeleteEntity", () => {
+it("renders DeleteEntity", async () => {
   const entity = "listener";
+  const routerName = "A";
+  const recordName = "testListener";
+
+  await login();
+  expect(service.management.connection.is_connected()).toBe(true);
+
   const props = {
     entity,
-    record: { name: "testListener" }
+    service,
+    record: {
+      name: recordName,
+      routerId: service.utilities.idFromName(routerName, "_topo"),
+      identity: `${entity}/:amqp:${recordName}`
+    },
+    handleAddNotification: () => {}
   };
 
   const { getByLabelText } = render(<DeleteEntity {...props} />);
   const button = getByLabelText("delete-entity-button");
   expect(button).toBeInTheDocument();
 
+  // so the delete confirmation popup
   fireEvent.click(button);
-  expect(getByLabelText("confirm-delete")).toBeInTheDocument();
+  const confirmButton = getByLabelText("confirm-delete");
+  expect(confirmButton).toBeInTheDocument();
+
+  // try to delete
+  fireEvent.click(confirmButton);
 });
diff --git a/console/react/src/details/detailsTablePage.js b/console/react/src/details/detailsTablePage.js
index b216f18..8d1fbc9 100644
--- a/console/react/src/details/detailsTablePage.js
+++ b/console/react/src/details/detailsTablePage.js
@@ -144,7 +144,11 @@ class DetailTablesPage extends React.Component {
 
   icap = s => s.charAt(0).toUpperCase() + s.slice(1);
 
-  parentItem = () => this.locationState().currentRecord.name;
+  parentItem = () => {
+    if (this.locationState().currentRecord.name)
+      return this.locationState().currentRecord.name;
+    return `${this.entity}/${this.locationState().currentRecord.identity}`;
+  };
 
   breadcrumbSelected = () => {
     if (this.props.details) {
diff --git a/console/react/src/common/updated.js b/console/react/src/details/emptyTablePage.js
similarity index 55%
copy from console/react/src/common/updated.js
copy to console/react/src/details/emptyTablePage.js
index 1813edd..8a4b80b 100644
--- a/console/react/src/common/updated.js
+++ b/console/react/src/details/emptyTablePage.js
@@ -17,21 +17,37 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import React, { Component } from "react";
+import React from "react";
+import {
+  EmptyState,
+  EmptyStateVariant,
+  EmptyStateBody,
+  EmptyStateIcon,
+  Bullseye,
+  Title
+} from "@patternfly/react-core";
 
-class Updated extends Component {
+import { SearchIcon } from "@patternfly/react-icons";
+import { safePlural } from "../common/qdrGlobals";
+
+class EmptyTable extends React.Component {
   constructor(props) {
     super(props);
     this.state = {};
   }
-
   render() {
     return (
-      <pre aria-label="last-updated" data-pf-content="true" className="overview-loading">
-        {`Updated ${this.props.service.utilities.strDate(this.props.lastUpdated)}`}
-      </pre>
+      <Bullseye id="emptyResults">
+        <EmptyState variant={EmptyStateVariant.small}>
+          <EmptyStateIcon icon={SearchIcon} />
+          <Title headingLevel="h2" size="lg">
+            No results found
+          </Title>
+          <EmptyStateBody>There are no {safePlural(2, this.props.entity)}</EmptyStateBody>
+        </EmptyState>
+      </Bullseye>
     );
   }
 }
 
-export default Updated;
+export default EmptyTable;
diff --git a/console/react/src/details/dataSources/logsData.js b/console/react/src/details/emptyTablePage.test.js
similarity index 75%
copy from console/react/src/details/dataSources/logsData.js
copy to console/react/src/details/emptyTablePage.test.js
index daa2553..878d699 100644
--- a/console/react/src/details/dataSources/logsData.js
+++ b/console/react/src/details/emptyTablePage.test.js
@@ -17,15 +17,14 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import DefaultData from "./defaultData";
+import React from "react";
+import { render } from "@testing-library/react";
+import EmptyTable from "./emptyTablePage";
 
-class LogsData extends DefaultData {
-  constructor(service, schema) {
-    super(service, schema);
-    this.updateMetaData = {
-      module: { readOnly: true }
-    };
-  }
-}
+it("renders an EmptyTable", async () => {
+  const props = {
+    entity: "test"
+  };
 
-export default LogsData;
+  render(<EmptyTable {...props} />);
+});
diff --git a/console/react/src/details/entityListTable.js b/console/react/src/details/entityListTable.js
index 403dc63..9a6b50d 100644
--- a/console/react/src/details/entityListTable.js
+++ b/console/react/src/details/entityListTable.js
@@ -30,6 +30,7 @@ import { Button, Pagination } from "@patternfly/react-core";
 import { Redirect } from "react-router-dom";
 import TableToolbar from "../common/tableToolbar";
 import { dataMap, defaultData } from "./entityData";
+import EmptyTable from "./emptyTablePage";
 
 // If the breadcrumb on the detailsTablePage was used to return to this page,
 // we will have saved state info in props.location.state
@@ -444,11 +445,17 @@ class EntityListTable extends React.Component {
           hidePagination={true}
           actionButtons={actionButtons()}
         />
-        <Table {...tableProps}>
-          <TableHeader />
-          <TableBody />
-        </Table>
-        {this.renderPagination("bottom")}
+        {this.state.rows.length > 0 ? (
+          <React.Fragment>
+            <Table {...tableProps}>
+              <TableHeader />
+              <TableBody />
+            </Table>
+            {this.renderPagination("bottom")}
+          </React.Fragment>
+        ) : (
+          <EmptyTable entity={this.props.entity} />
+        )}
         {this.state.action && this.doAction()}
       </React.Fragment>
     );
diff --git a/console/react/src/details/routerSelect.js b/console/react/src/details/routerSelect.js
index f6af128..7658fff 100644
--- a/console/react/src/details/routerSelect.js
+++ b/console/react/src/details/routerSelect.js
@@ -18,11 +18,7 @@ under the License.
 */
 
 import React from "react";
-import {
-  OptionsMenu,
-  OptionsMenuItem,
-  OptionsMenuToggleWithText
-} from "@patternfly/react-core";
+import { OptionsMenu, OptionsMenuItem, OptionsMenuToggle } from "@patternfly/react-core";
 import { utils } from "../common/amqp/utilities";
 
 class RouterSelect extends React.Component {
@@ -33,6 +29,19 @@ class RouterSelect extends React.Component {
       selectedOption: "",
       routers: []
     };
+
+    this.onToggle = () => {
+      this.setState({
+        isOpen: !this.state.isOpen
+      });
+    };
+
+    this.onSelect = event => {
+      const routerName = event.target.textContent;
+      this.setState({ selectedOption: routerName, isOpen: false }, () => {
+        this.props.handleRouterSelected(this.nameToId[routerName]);
+      });
+    };
   }
 
   componentDidMount = () => {
@@ -49,21 +58,9 @@ class RouterSelect extends React.Component {
     });
   };
 
-  onToggle = () => {
-    this.setState({
-      isOpen: !this.state.isOpen
-    });
-  };
-
-  onSelect = event => {
-    const routerName = event.target.textContent;
-    this.setState({ selectedOption: routerName, isOpen: false }, () => {
-      this.props.handleRouterSelected(this.nameToId[routerName]);
-    });
-  };
-
   render() {
     const { routers, selectedOption, isOpen } = this.state;
+
     const menuItems = routers.map(r => (
       <OptionsMenuItem
         onSelect={this.onSelect}
@@ -76,12 +73,12 @@ class RouterSelect extends React.Component {
     ));
 
     const toggle = (
-      <OptionsMenuToggleWithText toggleText={selectedOption} onToggle={this.onToggle} />
+      <OptionsMenuToggle onToggle={this.onToggle} toggleTemplate={selectedOption} />
     );
 
     return (
       <OptionsMenu
-        id="routerSelect"
+        id="options-menu-single-option-example"
         menuItems={menuItems}
         isOpen={isOpen}
         toggle={toggle}
diff --git a/console/react/src/details/updateTablePage.js b/console/react/src/details/updateTablePage.js
index c893698..7b61b15 100644
--- a/console/react/src/details/updateTablePage.js
+++ b/console/react/src/details/updateTablePage.js
@@ -20,6 +20,7 @@ under the License.
 import React from "react";
 import { PageSection, PageSectionVariants } from "@patternfly/react-core";
 import {
+  Alert,
   Button,
   Stack,
   StackItem,
@@ -82,7 +83,8 @@ class UpdateTablePage extends React.Component {
       redirectPath: "/dashboard",
       lastUpdated: new Date(),
       changes: false,
-      record: this.fixNull(this.props.locationState.currentRecord)
+      record: this.fixNull(this.props.locationState.currentRecord),
+      errorText: null
     };
     this.originalRecord = utils.copy(this.state.record);
   }
@@ -116,6 +118,9 @@ class UpdateTablePage extends React.Component {
     const record = this.state.record;
     const attributes = this.dataSource.schemaAttributes(this.entity);
     const formGroups = [];
+    if (this.state.errorText) {
+      formGroups.push(<Alert variant="danger" isInline title={this.state.errorText} />);
+    }
     for (let attributeKey in attributes) {
       const attribute = attributes[attributeKey];
       let type = attribute.type;
@@ -234,6 +239,11 @@ class UpdateTablePage extends React.Component {
         attributes["outputFile"] = record.outputFile === "" ? null : record.outputFile;
       }
     }
+    const { validated, errorText } = this.dataSource.validate(record);
+    if (!validated) {
+      this.setState({ errorText });
+      return;
+    }
     // call update
     this.props.service.management.connection
       .sendMethod(record.routerId || record.nodeId, this.entity, attributes, "UPDATE")
@@ -242,15 +252,18 @@ class UpdateTablePage extends React.Component {
         if (statusCode < 200 || statusCode >= 300) {
           const msg = `Updated ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`;
           console.log(`error ${msg}`);
-          this.props.handleAddNotification("action", msg, new Date(), "danger");
+          this.setState({
+            errorText: results.context.message.application_properties.statusDescription
+          });
+          //this.props.handleAddNotification("action", msg, new Date(), "danger");
         } else {
           const msg = `Updated ${this.props.entity} ${record.name}`;
           console.log(`success ${msg}`);
           this.props.handleAddNotification("action", msg, new Date(), "success");
+          const props = this.props;
+          props.locationState.currentRecord = record;
+          this.props.handleActionCancel(props);
         }
-        const props = this.props;
-        props.locationState.currentRecord = record;
-        this.props.handleActionCancel(props);
       });
   };
 
diff --git a/console/react/src/overview/dashboard/alertList.js b/console/react/src/overview/dashboard/alertList.js
index f6c617f..8bb37b7 100644
--- a/console/react/src/overview/dashboard/alertList.js
+++ b/console/react/src/overview/dashboard/alertList.js
@@ -30,19 +30,36 @@ class AlertList extends React.Component {
   }
 
   hideAlert = alert => {
+    alert.hiding = true;
+    alert.adding = false;
+    this.setState({ alerts: this.state.alerts });
+    const self = this;
+    alert.timer = setTimeout(() => self.alertRemoved(alert), 1000);
+  };
+
+  addAlert = (type, message) => {
+    const { alerts } = this.state;
+    const alert = { key: this.nextIndex++, type, message, adding: true };
+    const self = this;
+    alert.timer = setTimeout(() => self.hideAlert(alert), 4000);
+    alerts.unshift(alert);
+    this.setState({ alerts });
+  };
+
+  alertRemoved = alert => {
     const { alerts } = this.state;
     const index = alerts.findIndex(a => a.key === alert.key);
     if (index >= 0) alerts.splice(index, 1);
     this.setState({ alerts });
   };
 
-  addAlert = (severity, message) => {
-    const { alerts } = this.state;
-    const alert = { key: this.nextIndex++, type: severity, message };
+  handleMouseOver = alert => {
+    clearTimeout(alert.timer);
+  };
+
+  handleMouseOut = alert => {
     const self = this;
-    setTimeout(() => self.hideAlert(alert), 5000);
-    alerts.unshift(alert);
-    this.setState({ alerts });
+    alert.timer = setTimeout(() => self.hideAlert(alert), 2000);
   };
 
   render() {
@@ -51,11 +68,17 @@ class AlertList extends React.Component {
         {this.state.alerts.map((alert, i) => (
           <Alert
             key={`alert-${i}`}
+            className={alert.adding ? "alert-in" : alert.hiding ? "alert-out" : ""}
+            onMouseOver={() => this.handleMouseOver(alert)}
+            onMouseOut={() => this.handleMouseOut(alert)}
             variant={alert.type}
             title={alert.type}
             isInline
             action={
-              <AlertActionCloseButton aria-label="alert-close-button" onClose={() => this.hideAlert(alert)} />
+              <AlertActionCloseButton
+                aria-label="alert-close-button"
+                onClose={() => this.hideAlert(alert)}
+              />
             }
           >
             {alert.message.length > 40
diff --git a/console/react/src/overview/dashboard/alertList.test.js b/console/react/src/overview/dashboard/alertList.test.js
index 55614a7..2ee6958 100644
--- a/console/react/src/overview/dashboard/alertList.test.js
+++ b/console/react/src/overview/dashboard/alertList.test.js
@@ -21,7 +21,7 @@ import React from "react";
 import { render } from "@testing-library/react";
 import AlertList from "./alertList";
 
-it("renders the AlertList component", () => {
+it("renders the AlertList component", async () => {
   let ref = null;
   const props = {};
   const { getByLabelText, queryByLabelText } = render(
@@ -43,5 +43,6 @@ it("renders the AlertList component", () => {
   // hide the alert
   ref.hideAlert(alert);
   // the alert close button should now be gone
-  expect(queryByLabelText("alert-close-button")).toBeNull();
+  // TODO: The alert fades out over 5 seconds. Find a way to test that.
+  //expect(queryByLabelText("alert-close-button")).toBeNull();
 });
diff --git a/console/react/src/overview/dashboard/chartData.js b/console/react/src/overview/dashboard/chartData.js
index 508efda..cf4b53f 100644
--- a/console/react/src/overview/dashboard/chartData.js
+++ b/console/react/src/overview/dashboard/chartData.js
@@ -20,13 +20,7 @@ under the License.
 class ChartData {
   constructor(service) {
     this.service = service;
-    this.rates = [];
-    this.rawData = [];
-    this.rateStorage = {};
-    this.initialized = false;
-    for (let i = 0; i < 60 * 60; i++) {
-      this.rates.push(0);
-    }
+    this.reset();
     this.isRate = false;
   }
 
@@ -37,6 +31,16 @@ class ChartData {
     this.initialized = true;
   };
 
+  reset = () => {
+    this.rates = [];
+    this.rawData = [];
+    this.rateStorage = {};
+    this.initialized = false;
+    for (let i = 0; i < 60 * 60; i++) {
+      this.rates.push(0);
+    }
+  };
+
   addData = datum => {
     if (!this.initialized) {
       this.init(datum);
@@ -54,6 +58,7 @@ class ChartData {
       );
       datum = Math.round(avg.val);
     }
+    if (datum < 0) datum = 0;
     this.rates.push(datum);
     this.rates.splice(0, 1);
   };
diff --git a/console/react/src/overview/dashboard/dashboardPage.js b/console/react/src/overview/dashboard/dashboardPage.js
index 23d2221..a0b471e 100644
--- a/console/react/src/overview/dashboard/dashboardPage.js
+++ b/console/react/src/overview/dashboard/dashboardPage.js
@@ -63,7 +63,7 @@ class DashboardPage extends React.Component {
                             this.state.timePeriod === 60 ? "selected" : ""
                           }`}
                         >
-                          Min
+                          Minute
                         </li>
                         <li
                           onClick={() => this.setTimePeriod(60 * 60)}
diff --git a/console/react/src/overview/dashboard/delayedDeliveriesCard.js b/console/react/src/overview/dashboard/delayedDeliveriesCard.js
index 990d029..33d627f 100644
--- a/console/react/src/overview/dashboard/delayedDeliveriesCard.js
+++ b/console/react/src/overview/dashboard/delayedDeliveriesCard.js
@@ -170,7 +170,7 @@ class DelayedDeliveriesCard extends React.Component {
 
     const caption = (
       <React.Fragment>
-        <span className="caption">Links with delayed deliveries</span>
+        <span className="caption">Connections with delayed deliveries</span>
         <div className="updated">
           Updated at {this.lastUpdateString()} | Next {this.nextUpdateString()}
         </div>
diff --git a/console/react/src/overview/dashboard/inflightChart.js b/console/react/src/overview/dashboard/inflightChart.js
index 9d489e1..b79d4cb 100644
--- a/console/react/src/overview/dashboard/inflightChart.js
+++ b/console/react/src/overview/dashboard/inflightChart.js
@@ -24,7 +24,7 @@ import * as d3 from "d3";
 class InflightChart extends ChartBase {
   constructor(props) {
     super(props);
-    this.title = "Deliveries in flight";
+    this.title = "Messages in flight";
     this.color = d3.rgb(ChartThemeColor.green);
     this.setStyle(this.color, 0.3);
     this.isRate = false;
@@ -48,8 +48,7 @@ class InflightChart extends ChartBase {
             );
             inflight +=
               result.linkType === "endpoint" && result.linkDir === "out"
-                ? parseInt(result.unsettledCount) +
-                parseInt(result.undeliveredCount)
+                ? parseInt(result.unsettledCount) + parseInt(result.undeliveredCount)
                 : 0;
           }
         }
diff --git a/console/react/src/overview/dashboard/layout.js b/console/react/src/overview/dashboard/layout.js
index fc6e6b6..e823899 100644
--- a/console/react/src/overview/dashboard/layout.js
+++ b/console/react/src/overview/dashboard/layout.js
@@ -57,7 +57,7 @@ import { utils } from "../../common/amqp/utilities";
 import throughputData from "./throughputData";
 import inflightData from "./inflightData";
 
-class PageLayout extends React.Component {
+class PageLayout extends React.PureComponent {
   constructor(props) {
     super(props);
     this.state = {
@@ -142,6 +142,7 @@ class PageLayout extends React.Component {
       this.lastLocation = this.props.location.pathname;
       this.setState({ connected: false });
     } else if (whatHappened === "reconnect") {
+      this.throughputChartData.reset();
       this.handleAddNotification(
         "event",
         "Connection to router resumed",
diff --git a/console/react/src/overview/dashboard/notificationDrawer.js b/console/react/src/overview/dashboard/notificationDrawer.js
index ccd6cf5..81c8540 100644
--- a/console/react/src/overview/dashboard/notificationDrawer.js
+++ b/console/react/src/overview/dashboard/notificationDrawer.js
@@ -49,7 +49,7 @@ class NotificationDrawer extends React.Component {
           isOpen: false,
           events: []
         },
-        event: { title: "Events", isOpen: false, events: [] }
+        event: { title: "Notifications", isOpen: false, events: [] }
       }
     };
     this.severityToIcon = {
diff --git a/console/react/src/overview/dashboard/throughputChart.js b/console/react/src/overview/dashboard/throughputChart.js
index 6600df4..9df2398 100644
--- a/console/react/src/overview/dashboard/throughputChart.js
+++ b/console/react/src/overview/dashboard/throughputChart.js
@@ -22,7 +22,7 @@ import ChartBase from "./chartBase";
 class ThroughputChart extends ChartBase {
   constructor(props) {
     super(props);
-    this.title = "Deliveries per sec";
+    this.title = "Messages delivered";
     this.color = "#99C2EB"; //ChartThemeColor.blue;
     this.setStyle(this.color);
     this.ariaLabel = "throughput-chart";
diff --git a/console/react/src/overview/dataSources/routerData.js b/console/react/src/overview/dataSources/routerData.js
index 67c015e..f88aecd 100644
--- a/console/react/src/overview/dataSources/routerData.js
+++ b/console/react/src/overview/dataSources/routerData.js
@@ -22,7 +22,6 @@ class RouterData {
     this.service = service;
     this.fields = [
       { title: "Router", field: "name" },
-      { title: "Area", field: "area" },
       { title: "Mode", field: "mode" },
       {
         title: "Addresses",
diff --git a/console/react/src/overview/overviewTable.js b/console/react/src/overview/overviewTable.js
index 2832876..e14b010 100644
--- a/console/react/src/overview/overviewTable.js
+++ b/console/react/src/overview/overviewTable.js
@@ -171,6 +171,7 @@ class OverviewTable extends React.Component {
         extraInfo={extraInfo}
         service={this.props.service}
         detailClick={this.detailClick}
+        handleAddNotification={this.props.handleAddNotification}
       />
     );
   };
diff --git a/console/react/src/topology/legend.js b/console/react/src/topology/legend.js
index 76fbf1b..3c7a38f 100644
--- a/console/react/src/topology/legend.js
+++ b/console/react/src/topology/legend.js
@@ -126,18 +126,18 @@ export class Legend {
       let node = lookFor.find(lf => lf.cmp(n));
       if (node) {
         if (!legendNodes.nodes.some(ln => ln.key === node.title)) {
-          let newNode = legendNodes.addUsing(
-            node.title,
-            node.text,
-            node.role,
-            undefined,
-            0,
-            0,
-            i,
-            0,
-            false,
-            node.props ? node.props : {}
-          );
+          let newNode = legendNodes.addUsing({
+            id: node.title,
+            name: node.text,
+            nodeType: node.role,
+            nodeIndex: undefined,
+            x: 0,
+            y: 0,
+            connectionContainer: i,
+            resultIndex: 0,
+            fixed: 0,
+            properties: node.props ? node.props : {}
+          });
           if (node.cdir) {
             newNode.cdir = node.cdir;
           }
diff --git a/console/react/src/topology/links.js b/console/react/src/topology/links.js
index b05663c..1dabbe8 100644
--- a/console/react/src/topology/links.js
+++ b/console/react/src/topology/links.js
@@ -28,6 +28,7 @@ class Link {
     this.cls = cls;
     this.uid = uid;
   }
+
   markerId(end) {
     let selhigh = this.highlighted ? "highlighted" : this.selected ? "selected" : "";
     if (selhigh === "" && !this.left && !this.right) selhigh = "unknown";
@@ -40,15 +41,18 @@ export class Links {
     this.links = [];
     this.logger = logger;
   }
+
   reset() {
     this.links.length = 0;
   }
+
   getLinkSource(nodesIndex) {
     for (let i = 0; i < this.links.length; ++i) {
       if (this.links[i].target === nodesIndex) return i;
     }
     return -1;
   }
+
   getLink(_source, _target, dir, cls, uid) {
     for (let i = 0; i < this.links.length; i++) {
       let s = this.links[i].source,
@@ -74,6 +78,7 @@ export class Links {
       uid = uid + "." + this.links.length;
     return this.links.push(new Link(_source, _target, dir, cls, uid)) - 1;
   }
+
   linkFor(source, target) {
     for (let i = 0; i < this.links.length; ++i) {
       if (this.links[i].source === source && this.links[i].target === target)
@@ -151,7 +156,10 @@ export class Links {
         if (!connectionsPerContainer[connection.container])
           connectionsPerContainer[connection.container] = [];
         let linksDir = getLinkDir(connection, onode);
-        if (linksDir === "unknown") unknowns.push(nodeIds[source]);
+        if (linksDir === "unknown") {
+          continue;
+          //unknowns.push(nodeIds[source]);
+        }
         connectionsPerContainer[connection.container].push({
           source: source,
           linksDir: linksDir,
@@ -189,18 +197,18 @@ export class Links {
           height,
           localStorage
         );
-        let node = nodes.getOrCreateNode(
-          nodeIds[container.source],
+        let node = nodes.getOrCreateNode({
+          id: nodeIds[container.source],
           name,
-          container.connection.role,
-          nodes.getLength(),
-          position.x,
-          position.y,
-          container.connection.container,
-          container.resultsIndex,
-          position.fixed,
-          container.connection.properties
-        );
+          nodeType: container.connection.role,
+          nodeIndex: nodes.getLength(),
+          x: position.x,
+          y: position.y,
+          connectionContainer: container.connection.container,
+          resultIndex: container.resultsIndex,
+          fixed: position.fixed,
+          properties: container.connection.properties
+        });
         node.host = container.connection.host;
         node.cdir = container.linksDir;
         node.user = container.connection.user;
@@ -283,6 +291,7 @@ var getLinkDir = function(connection, onode) {
   if (outCount > 0) return "out";
   return "unknown";
 };
+
 var getKey = function(containers) {
   let parts = {};
   let connection = containers[0].connection;
diff --git a/console/react/src/topology/nodes.js b/console/react/src/topology/nodes.js
index d25b93f..29c6c6b 100644
--- a/console/react/src/topology/nodes.js
+++ b/console/react/src/topology/nodes.js
@@ -379,20 +379,21 @@ export class Nodes {
     }
     return undefined;
   }
-  getOrCreateNode(
-    id,
-    name,
-    nodeType,
-    nodeIndex,
-    x,
-    y,
-    connectionContainer,
-    resultIndex,
-    fixed,
-    properties
-  ) {
-    properties = properties || {};
-    let gotNode = this.find(connectionContainer, properties, name);
+  getOrCreateNode = nodeObj => {
+    const {
+      id,
+      name,
+      nodeType,
+      nodeIndex,
+      x,
+      y,
+      connectionContainer,
+      resultIndex,
+      fixed,
+      properties
+    } = nodeObj;
+    const props = properties || {};
+    let gotNode = this.find(connectionContainer, props, name);
     if (gotNode) {
       return gotNode;
     }
@@ -401,7 +402,7 @@ export class Nodes {
       id,
       name,
       nodeType,
-      properties,
+      props,
       routerId,
       x,
       y,
@@ -410,37 +411,17 @@ export class Nodes {
       fixed,
       connectionContainer
     );
-  }
+  };
   add(obj) {
     this.nodes.push(obj);
     return obj;
   }
-  addUsing(
-    id,
-    name,
-    nodeType,
-    nodeIndex,
-    x,
-    y,
-    connectContainer,
-    resultIndex,
-    fixed,
-    properties
-  ) {
-    let obj = this.getOrCreateNode(
-      id,
-      name,
-      nodeType,
-      nodeIndex,
-      x,
-      y,
-      connectContainer,
-      resultIndex,
-      fixed,
-      properties
-    );
+
+  addUsing = nodeInfo => {
+    let obj = this.getOrCreateNode(nodeInfo);
     return this.add(obj);
-  }
+  };
+
   clearHighlighted() {
     for (let i = 0; i < this.nodes.length; ++i) {
       this.nodes[i].highlighted = false;
@@ -472,18 +453,18 @@ export class Nodes {
       }
       position.fixed = position.fixed ? true : false;
       let parts = id.split("/");
-      this.addUsing(
+      this.addUsing({
         id,
         name,
-        parts[1],
-        this.nodes.length,
-        position.x,
-        position.y,
-        name,
-        undefined,
-        position.fixed,
-        {}
-      );
+        nodeType: parts[1],
+        nodeIndex: this.nodes.length,
+        x: position.x,
+        y: position.y,
+        connectionContainer: name,
+        resultIndex: undefined,
+        fixed: position.fixed,
+        properties: {}
+      });
     }
     return animate;
   }
diff --git a/console/react/src/topology/topoUtils.js b/console/react/src/topology/topoUtils.js
index 58f1a5b..8fc12ac 100644
--- a/console/react/src/topology/topoUtils.js
+++ b/console/react/src/topology/topoUtils.js
@@ -19,6 +19,7 @@ under the License.
 
 /* global Set */
 import { utils } from "../common/amqp/utilities.js";
+import * as d3 from "d3";
 
 // highlight the paths between the selected node and the hovered node
 function findNextHopNode(from, d, nodeInfo, selected_node, nodes) {
@@ -250,16 +251,12 @@ export function connectionPopupHTML(d, nodeInfo) {
   return HTML;
 }
 
-export function getSizes(topologyRef) {
+export function getSizes(id) {
   const gap = 5;
-  let topoWidth =
-    topologyRef.offsetWidth > 0 ? topologyRef.offsetWidth : window.innerWidth;
-  let width = topoWidth - gap;
-  let top = topologyRef.offsetTop;
-  let height = window.innerHeight - top - gap;
-  if (width < 10 || height < 10) {
-    console.log(`page width and height are abnormal w: ${width} h: ${height}`);
-    return [0, 0];
+  const sel = d3.select(`#${id}`);
+  if (!sel.empty()) {
+    const brect = sel.node().getBoundingClientRect();
+    return { width: brect.width - gap, height: brect.height - gap };
   }
-  return { width, height };
+  return { width: window.innerWidth - 200, height: window.innerHeight - 100 };
 }
diff --git a/console/react/src/topology/topologyPage.js b/console/react/src/topology/topologyPage.js
index 56f2df9..b907b82 100644
--- a/console/react/src/topology/topologyPage.js
+++ b/console/react/src/topology/topologyPage.js
@@ -24,9 +24,7 @@ import TopologyViewer from "./topologyViewer";
 class TopologyPage extends Component {
   constructor(props) {
     super(props);
-    this.state = {
-      lastUpdated: new Date()
-    };
+    this.state = {};
   }
 
   render() {
diff --git a/console/react/src/topology/topologyViewer.js b/console/react/src/topology/topologyViewer.js
index 610efb0..d9423fb 100644
--- a/console/react/src/topology/topologyViewer.js
+++ b/console/react/src/topology/topologyViewer.js
@@ -47,9 +47,9 @@ import {
   updateState
 } from "./svgUtils.js";
 import { QDRLogger } from "../common/qdrGlobals";
-const TOPOOPTIONSKEY = "topologyLegendOptions";
+const TOPOOPTIONSKEY = "topologyLegendOptionsKey";
 
-class TopologyPage extends Component {
+class TopologyViewer extends Component {
   constructor(props) {
     super(props);
     // restore the state of the legend sections
@@ -61,8 +61,8 @@ class TopologyPage extends Component {
             open: false,
             dots: false,
             congestion: false,
-            addresses: [],
-            addressColors: []
+            addresses: {},
+            addressColors: {}
           },
           legend: {
             open: true
@@ -130,16 +130,31 @@ class TopologyPage extends Component {
   componentDidMount = () => {
     window.addEventListener("resize", this.resize);
     // we only need to update connections during steady-state
-    this.props.service.management.topology.setUpdateEntities(["connection"]);
+    this.props.service.management.topology.setUpdateEntities([
+      "connection",
+      "router.link"
+    ]);
     // poll the routers for their latest entities (set to connection above)
     this.props.service.management.topology.startUpdating();
-
-    // create the svg
-    this.init();
+    this.props.service.management.topology.ensureAllEntities(
+      [
+        {
+          entity: "router.link",
+          attrs: ["linkType", "connectionId", "linkDir", "owningAddr"],
+          force: true
+        }
+      ],
+      () => {
+        // create the svg
+        setTimeout(this.init, 1);
+      }
+    );
 
     // get notified when a router is added/dropped and when
     // the number of connections for a router changes
-    this.props.service.management.topology.addChangedAction("topology", this.init);
+    this.props.service.management.topology.addChangedAction("topology", () => {
+      return this.init;
+    });
   };
 
   componentWillUnmount = () => {
@@ -147,6 +162,12 @@ class TopologyPage extends Component {
     this.props.service.management.topology.stopUpdating();
     this.props.service.management.topology.delChangedAction("topology");
     this.props.service.management.topology.delUpdatedAction("connectionPopupHTML");
+
+    d3.select("#SVG_ID .links").remove();
+    d3.select("#SVG_ID .nodes").remove();
+    d3.select("#SVG_ID circle.flow").remove();
+    d3.select("#SVG_ID").remove();
+
     this.traffic.remove();
     this.forceData.nodes.savePositions();
     window.removeEventListener("resize", this.resize);
@@ -155,14 +176,14 @@ class TopologyPage extends Component {
 
   resize = () => {
     if (!this.svg) return;
-    const { width, height } = getSizes(this.topologyRef);
+    const { width, height } = getSizes("topology");
     this.width = width;
     this.height = height;
     if (this.width > 0) {
       // set attrs and 'resume' force
       this.svg.attr("width", this.width);
       this.svg.attr("height", this.height);
-      this.backgroundMap.setWidthHeight(width, height);
+      if (this.backgroundMap) this.backgroundMap.setWidthHeight(width, height);
       this.force.size([width, height]).resume();
     }
   };
@@ -185,7 +206,7 @@ class TopologyPage extends Component {
 
   // initialize the nodes and links array from the QDRService.topology._nodeInfo object
   init = () => {
-    const { width, height } = getSizes(this.topologyRef);
+    const { width, height } = getSizes("topology");
     this.width = width;
     this.height = height;
     if (this.width < 768) {
@@ -203,7 +224,9 @@ class TopologyPage extends Component {
     d3.select("#SVG_ID .links").remove();
     d3.select("#SVG_ID .nodes").remove();
     d3.select("#SVG_ID circle.flow").remove();
-    if (d3.select("#SVG_ID").empty()) {
+    d3.select("#SVG_ID").remove();
+    this.svg = null;
+    if (!this.svg) {
       this.svg = d3
         .select("#topology")
         .append("svg")
@@ -213,22 +236,25 @@ class TopologyPage extends Component {
         .attr("aria-label", "topology-svg")
         .on("click", this.clearPopups);
       // read the map data from the data file and build the map layer
-      this.backgroundMap.init(this, this.svg, this.width, this.height).then(() => {
-        this.forceData.nodes.saveLonLat(this.backgroundMap);
-        this.backgroundMap.setMapOpacity(this.state.legendOptions.map.show);
-      });
+      if (this.backgroundMap) {
+        this.backgroundMap.init(this, this.svg, this.width, this.height).then(() => {
+          this.forceData.nodes.saveLonLat(this.backgroundMap);
+          this.backgroundMap.setMapOpacity(this.state.legendOptions.map.show);
+        });
+      }
       addDefs(this.svg);
       addGradient(this.svg);
+
+      // handles to link and node element groups
+      this.path = this.svg
+        .append("svg:g")
+        .attr("class", "links")
+        .selectAll("g");
+      this.circle = this.svg
+        .append("svg:g")
+        .attr("class", "nodes")
+        .selectAll("g");
     }
-    // handles to link and node element groups
-    this.path = this.svg
-      .append("svg:g")
-      .attr("class", "links")
-      .selectAll("g");
-    this.circle = this.svg
-      .append("svg:g")
-      .attr("class", "nodes")
-      .selectAll("g");
 
     this.traffic.remove();
     if (this.state.legendOptions.traffic.dots)
@@ -279,17 +305,14 @@ class TopologyPage extends Component {
       .on("tick", this.tick)
       .on("end", () => {
         this.forceData.nodes.savePositions();
-        this.forceData.nodes.saveLonLat(this.backgroundMap);
+        if (this.backgroundMap) this.forceData.nodes.saveLonLat(this.backgroundMap);
       })
       .start();
-    for (let i = 0; i < this.forceData.nodes.nodes.length; i++) {
-      this.forceData.nodes.nodes[i].sx = this.forceData.nodes.nodes[i].x;
-      this.forceData.nodes.nodes[i].sy = this.forceData.nodes.nodes[i].y;
-    }
+    this.circle.call(this.force.drag);
 
     // app starts here
 
-    if (unknowns.length === 0) this.restart();
+    this.restart();
     // the legend
     this.legend = new Legend(this.forceData.nodes, this.QDRLog);
     this.updateLegend();
@@ -323,7 +346,7 @@ class TopologyPage extends Component {
       });
     }
     // if any clients don't yet have link directions, get the links for those nodes and restart the graph
-    if (unknowns.length > 0) setTimeout(this.resolveUnknowns, 10, nodeInfo, unknowns);
+    //if (unknowns.length > 0) setTimeout(this.resolveUnknowns, 10, nodeInfo, unknowns);
 
     var continueForce = function(extra) {
       if (extra > 0) {
@@ -372,7 +395,7 @@ class TopologyPage extends Component {
           .nodes(this.forceData.nodes.nodes)
           .links(this.forceData.links.links)
           .start();
-        this.forceData.nodes.saveLonLat(this.backgroundMap);
+        if (this.backgroundMap) this.forceData.nodes.saveLonLat(this.backgroundMap);
         this.restart();
         this.updateLegend();
       }
@@ -570,7 +593,7 @@ class TopologyPage extends Component {
       })
       .on("mousedown", d => {
         // mouse down for circle
-        this.backgroundMap.cancelZoom();
+        if (this.backgroundMap) this.backgroundMap.cancelZoom();
         this.current_node = d;
         if (d3.event && d3.event.button !== 0) {
           // ignore all but left button
@@ -582,7 +605,7 @@ class TopologyPage extends Component {
       })
       .on("mouseup", function(d) {
         // mouse up for circle
-        self.backgroundMap.restartZoom();
+        if (self.backgroundMap) self.backgroundMap.restartZoom();
         if (!self.mousedown_node) return;
 
         // unenlarge target node
@@ -598,7 +621,7 @@ class TopologyPage extends Component {
           cur_mouse[1] !== self.initial_mouse_down_position[1]
         ) {
           self.forceData.nodes.setFixed(d, true);
-          self.forceData.nodes.saveLonLat(self.backgroundMap);
+          if (self.backgroundMap) self.forceData.nodes.saveLonLat(self.backgroundMap);
           self.forceData.nodes.savePositions();
           self.restart();
           self.resetMouseVars();
@@ -687,10 +710,6 @@ class TopologyPage extends Component {
   tick = () => {
     // move the circles
     this.circle.attr("transform", d => {
-      if (isNaN(d.x) || isNaN(d.px)) {
-        d.x = d.px = d.sx;
-        d.y = d.py = d.sy;
-      }
       // don't let the edges of the circle go beyond the edges of the svg
       let r = Nodes.radius(d.nodeType);
       d.x = Math.max(Math.min(d.x, this.width - r), r);
@@ -699,7 +718,7 @@ class TopologyPage extends Component {
     });
 
     // draw lines from node centers
-    this.path.selectAll("path").attr("d", function(d) {
+    this.path.selectAll("path").attr("d", (d, i) => {
       return `M${d.source.x},${d.source.y}L${d.target.x},${d.target.y}`;
     });
   };
@@ -898,8 +917,10 @@ class TopologyPage extends Component {
     }
   };
   handleUpdateMapColor = (which, color) => {
-    let mapOptions = this.backgroundMap.updateMapColor(which, color);
-    this.setState({ mapOptions });
+    if (this.backgroundMap) {
+      let mapOptions = this.backgroundMap.updateMapColor(which, color);
+      this.setState({ mapOptions });
+    }
   };
 
   // the mouse was hovered over one of the addresses in the legend
@@ -915,16 +936,18 @@ class TopologyPage extends Component {
   handleUpdateMapShown = checked => {
     const { legendOptions } = this.state;
     legendOptions.map.show = checked;
-    this.setState({ legendOptions }, () => {
-      this.backgroundMap.setMapOpacity(checked);
-      this.backgroundMap.setBackgroundColor();
-      if (checked) {
-        this.backgroundMap.restartZoom();
-      } else {
-        this.backgroundMap.cancelZoom();
-      }
-      this.saveLegendOptions(legendOptions);
-    });
+    if (this.backgroundMap) {
+      this.setState({ legendOptions }, () => {
+        this.backgroundMap.setMapOpacity(checked);
+        this.backgroundMap.setBackgroundColor();
+        if (checked) {
+          this.backgroundMap.restartZoom();
+        } else {
+          this.backgroundMap.cancelZoom();
+        }
+        this.saveLegendOptions(legendOptions);
+      });
+    }
   };
 
   handleContextHide = () => {
@@ -981,6 +1004,7 @@ class TopologyPage extends Component {
             handleChangeTrafficFlowAddress={this.handleChangeTrafficFlowAddress}
             handleUpdateMapColor={this.handleUpdateMapColor}
             handleUpdateMapShown={this.handleUpdateMapShown}
+            handleHoverAddress={this.handleHoverAddress}
           />
         }
         controlBar={<TopologyControlBar controlButtons={controlButtons} />}
@@ -988,13 +1012,7 @@ class TopologyPage extends Component {
         sideBarOpen={false}
         className="qdrTopology"
       >
-        <div className="diagram">
-          <div
-            aria-label="topology-diagram"
-            ref={el => (this.topologyRef = el)}
-            id="topology"
-          ></div>
-        </div>
+        <div className="diagram" aria-label="topology-diagram" id="topology"></div>
         {this.state.showContextMenu && (
           <ContextMenu
             contextEventPosition={this.contextEventPosition}
@@ -1031,4 +1049,4 @@ class TopologyPage extends Component {
   }
 }
 
-export default TopologyPage;
+export default TopologyViewer;
diff --git a/console/react/src/topology/topologyViewer.test.js b/console/react/src/topology/topologyViewer.test.js
index a2a093e..94884c7 100644
--- a/console/react/src/topology/topologyViewer.test.js
+++ b/console/react/src/topology/topologyViewer.test.js
@@ -45,7 +45,7 @@ it("renders the TopologyViewer component", async () => {
   expect(getByLabelText("topology-diagram")).toBeInTheDocument();
 
   // make sure it created the svg
-  expect(getByLabelText("topology-svg")).toBeInTheDocument();
+  await waitForElement(() => getByLabelText("topology-svg"));
 
   // the svg should have a router circle
   await waitForElement(() => getByTestId("router-0"));
diff --git a/console/react/src/topology/traffic.js b/console/react/src/topology/traffic.js
index b15b9fa..4b4caa8 100644
--- a/console/react/src/topology/traffic.js
+++ b/console/react/src/topology/traffic.js
@@ -43,7 +43,25 @@ export class Traffic {
         this.addAnimationType(t, converter, radius);
       }.bind(this)
     );
+    // called by angular when mouse enters one of the address legends
+    this.$scope.enterLegend = address => {
+      // fade all flows that aren't for this address
+      this.fadeOtherAddresses(address);
+    };
+    // called when the mouse leaves one of the address legends
+    this.$scope.leaveLegend = () => {
+      this.unFadeAll();
+    };
   }
+  fadeOtherAddresses = address => {
+    d3.selectAll("circle.flow").classed("fade", function(d) {
+      return d.address !== address;
+    });
+  };
+  unFadeAll = () => {
+    d3.selectAll("circle.flow").classed("fade", false);
+  };
+
   // stop updating the traffic data
   stop() {
     if (this.interval) {
diff --git a/console/react/src/topology/trafficComponent.js b/console/react/src/topology/trafficComponent.js
index ab377fb..0f0bf3d 100644
--- a/console/react/src/topology/trafficComponent.js
+++ b/console/react/src/topology/trafficComponent.js
@@ -20,8 +20,18 @@ under the License.
 import React, { Component } from "react";
 import { Checkbox } from "@patternfly/react-core";
 import AddressesComponent from "../common/addressesComponent";
+import PropTypes from "prop-types";
 
 class TrafficComponent extends Component {
+  static propTypes = {
+    dots: PropTypes.bool.isRequired,
+    congestion: PropTypes.bool.isRequired,
+    addresses: PropTypes.object.isRequired,
+    addressColors: PropTypes.object.isRequired,
+    handleChangeTrafficAnimation: PropTypes.func.isRequired,
+    handleChangeTrafficFlowAddress: PropTypes.func.isRequired,
+    handleHoverAddress: PropTypes.func.isRequired
+  };
   constructor(props) {
     super(props);
     this.state = {};


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