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/10/26 17:26:02 UTC

[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Using pf4 topology wrapper component

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 885a457  Using pf4 topology wrapper component
885a457 is described below

commit 885a457005ebe55225938b6c5324641b8352c082
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Sat Oct 26 13:25:42 2019 -0400

    Using pf4 topology wrapper component
---
 console/react/src/App.css                          | 118 +++++++++-
 console/react/src/App.js                           |   2 +-
 console/react/src/amqp/topology.js                 |   4 -
 console/react/src/connect-form.js                  |  75 +++---
 console/react/src/connectPage.js                   |   6 +-
 ...xtMenuComponent.jsx => contextMenuComponent.js} |   2 +
 console/react/src/layout.js                        | 205 +++++++++-------
 console/react/src/overview/dataSources/logsData.js |  72 +++++-
 console/react/src/overview/logDetails.js           | 100 ++++++--
 console/react/src/overview/overviewTable.js        |  11 +-
 console/react/src/qdrGlobals.js                    |   1 -
 console/react/src/qdrService.js                    |   3 +-
 console/react/src/topology/contextMenu.js          |  80 +++++++
 console/react/src/topology/legend.js               |   3 +-
 console/react/src/topology/legendComponent.js      | 134 +++--------
 console/react/src/topology/map.js                  |  55 ++++-
 .../{mapLegendComponent.jsx => mapComponent.js}    |   0
 console/react/src/topology/nodes.js                |   5 +-
 console/react/src/topology/svgUtils.js             | 111 ++++-----
 console/react/src/topology/topoUtils.js            |  10 +-
 console/react/src/topology/topologyPage.js         |  41 ++++
 console/react/src/topology/topologyToolbar.js      | 129 ++++++++++
 .../topology/{qdrTopology.js => topologyViewer.js} | 259 +++++++++------------
 console/react/src/topology/traffic.js              |   2 +
 24 files changed, 940 insertions(+), 488 deletions(-)

diff --git a/console/react/src/App.css b/console/react/src/App.css
index eb0bfb2..9c45680 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -130,6 +130,13 @@ g.selected line {
   stroke: #fff;
 }
 
+#topologyPage {
+  padding: 0;
+}
+
+#topologyPage .pf-l-stack__item {
+  border-bottom: 1px solid #aaa;
+}
 path.hittarget {
   stroke-width: 15px;
   stroke: transparent;
@@ -370,9 +377,14 @@ div.state-container button.pf-c-clipboard-copy__group-copy {
 .overview-charts-page,
 .overview-table {
   background-color: #eaeaea !important;
-  padding: 3em;
+  padding: 1.5em;
+}
+.overview-table {
+  height: 100%;
+}
+.overview-table .pf-c-card__body {
+  padding-left: 1em;
 }
-
 .overview-table-page {
   padding-left: 0 !important;
   padding-right: 0 !important;
@@ -392,11 +404,18 @@ div.state-container button.pf-c-clipboard-copy__group-copy {
   width: 100%;
 }
 
+.qdrTopology {
+  width: 100%;
+  height: 100%;
+  background-color: white;
+}
 .qdrTopology dl.pf-c-accordion,
 .qdrChord dl.pf-c-accordion {
+  /*
   position: absolute;
   width: 20em;
   right: 1em;
+  */
   padding-top: 0;
   padding-bottom: 0;
   margin-top: 1em;
@@ -683,6 +702,14 @@ path.empty {
   border-color: black;
 }
 
+.topology-toolbar-item .pf-c-popover__content > button,
+.topology-toolbar-item .pf-c-popover__content .pf-c-title.pf-m-xl {
+  display: none;
+}
+
+.topology-toolbar-item .pf-c-popover__content {
+  padding: 2em 2em 0.5em;
+}
 .qdrPopup {
   position: absolute;
   z-index: 200;
@@ -696,7 +723,7 @@ path.empty {
 }
 
 #popover-div {
-  position: absolute;
+  position: fixed;
   z-index: 200;
   border-radius: 4px;
   border: 1px solid gray;
@@ -797,9 +824,14 @@ div.qdrChord .legend-text {
   font-weight: bold;
 }
 
+.colored-dot span.colored-dot-dot > span {
+  padding-top: 5px;
+}
+
 .colored-dot span.colored-dot-text {
   margin-left: 0.5em;
   color: black;
+  font-size: 16px;
 }
 
 .qdrTopology .addresses .legend-line,
@@ -807,12 +839,31 @@ div.qdrChord .legend-text {
   margin-left: 1.5em;
 }
 
-.qdrTopology .pf-c-options-menu__toggle {
-  display: none;
+@media (min-width: 768px) {
+  div.qdrTopology .pf-topology-side-bar.shown {
+    width: calc(100% - 330px);
+    max-width: 330px;
+  }
+  div.qdrTopology .pf-topology-container__with-sidebar .pf-topology-content {
+    /*
+      width: 180px;
+    min-width: calc(100% - 330px);
+    */
+    width: 100%;
+    height: 100%;
+    background-color: white;
+  }
 }
 
-.context-menu {
+.diagram {
+  width: 100%;
   position: absolute;
+  top: 0;
+  height: 100%;
+}
+
+.context-menu {
+  position: fixed;
   background-color: white;
   border-top: 1px solid #888;
   border-left: 1px solid #888;
@@ -931,6 +982,7 @@ div.qdrChord .legend-text {
   font-size: 14px;
   padding: 0;
   background-color: white;
+  overflow: hidden;
 }
 
 .pf-c-button.pf-m-primary.link-button,
@@ -940,6 +992,7 @@ div.qdrChord .legend-text {
   color: blue;
   font-weight: bold;
   white-space: normal;
+  padding-left: 0;
 }
 
 .pf-c-button.pf-m-primary.link-button:hover,
@@ -967,3 +1020,56 @@ div.qdrChord .legend-text {
 .overview-table tr {
   border-bottom-color: #eaeaea !important;
 }
+
+.log-record {
+  display: flex;
+  flex-wrap: wrap;
+  flex-direction: column;
+  align-items: flex-start;
+}
+
+.log-message {
+  background-color: #eaeaea;
+  width: 100%;
+}
+
+div#topologyLegend {
+  position: absolute;
+  bottom: 2em;
+  right: 1em;
+  padding: 0;
+  width: 14em;
+}
+
+div#topologyLegend header {
+  padding: 0.5em 1em;
+  background-color: #efefef;
+  display: flex;
+  justify-content: space-between;
+}
+
+div#topologyLegend .pf-c-modal-box__body {
+  padding: 1em;
+}
+
+div#topologyLegend .pf-c-title {
+  font-size: unset;
+  padding-top: 0.25em;
+}
+
+div.qdrTopology .topology-toolbar-button {
+  position: relative;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  color: black;
+
+  background-color: rgba(0, 0, 0, 0);
+  border: 1px solid #eaeaea;
+  border-bottom: 1px solid #212121;
+}
+
+div.qdrTopology .topology-toolbar-button i {
+  margin-left: 16px;
+  margin-right: 8px;
+}
diff --git a/console/react/src/App.js b/console/react/src/App.js
index 91f09e7..f896d4c 100644
--- a/console/react/src/App.js
+++ b/console/react/src/App.js
@@ -12,7 +12,7 @@ class App extends Component {
 
   render() {
     return (
-      <div className="App">
+      <div className="App pf-m-redhat-font">
         <PageLayout />
       </div>
     );
diff --git a/console/react/src/amqp/topology.js b/console/react/src/amqp/topology.js
index 8afa75a..98b03da 100644
--- a/console/react/src/amqp/topology.js
+++ b/console/react/src/amqp/topology.js
@@ -141,10 +141,6 @@ class Topology {
   }
 
   get() {
-    if (typeof this.getCounter === "undefined") {
-      this.getCounter = 0;
-    }
-    console.log(`topology: get - ${this.getCounter++}`);
     return new Promise(
       function(resolve, reject) {
         this.connection.sendMgmtQuery("GET-MGMT-NODES").then(
diff --git a/console/react/src/connect-form.js b/console/react/src/connect-form.js
index 7f0073a..678be78 100644
--- a/console/react/src/connect-form.js
+++ b/console/react/src/connect-form.js
@@ -25,34 +25,47 @@ import {
   TextVariants
 } from "@patternfly/react-core";
 
+const CONNECT_KEY = "QDRSettings";
+
 class ConnectForm extends React.Component {
   constructor(props) {
     super(props);
 
     this.state = {
-      value: "please choose",
-      value1: "",
-      value2: "",
-      value3: "",
-      value4: ""
-    };
-    this.handleTextInputChange1 = value1 => {
-      this.setState({ value1 });
-    };
-    this.handleTextInputChange2 = value2 => {
-      this.setState({ value2 });
-    };
-    this.handleTextInputChange3 = value3 => {
-      this.setState({ value3 });
-    };
-    this.handleTextInputChange4 = value4 => {
-      this.setState({ value4 });
+      address: "",
+      port: "",
+      username: "",
+      password: ""
     };
   }
 
+  componentDidMount = () => {
+    let savedValues = localStorage.getItem(CONNECT_KEY);
+    if (!savedValues) {
+      savedValues = {
+        address: "localhost",
+        port: "5673",
+        username: "",
+        password: ""
+      };
+    } else {
+      savedValues = JSON.parse(savedValues);
+    }
+    this.setState(savedValues);
+  };
+  handleTextInputChange = (field, value) => {
+    const formValues = Object.assign(this.state);
+    formValues[field] = value;
+    this.setState(formValues, () => {
+      const state2Save = JSON.parse(JSON.stringify(formValues));
+      // don't save the password
+      state2Save.password = "";
+      localStorage.setItem(CONNECT_KEY, JSON.stringify(state2Save));
+    });
+  };
+
   handleConnect = () => {
-    this.toggleDrawerHide();
-    this.props.handleConnect(this.props.fromPath);
+    this.props.handleConnect(this.props.fromPath, this.state);
   };
 
   toggleDrawerHide = () => {
@@ -60,7 +73,7 @@ class ConnectForm extends React.Component {
   };
 
   render() {
-    const { value1, value2, value3, value4 } = this.state;
+    const { address, port, username, password } = this.state;
 
     return (
       <div>
@@ -80,13 +93,15 @@ class ConnectForm extends React.Component {
                 fieldId={`form-address-${this.props.prefix}`}
               >
                 <TextInput
-                  value={value1}
+                  value={address}
                   isRequired
                   type="text"
                   id={`form-address-${this.props.prefix}`}
                   aria-describedby="horizontal-form-address-helper"
                   name="form-address"
-                  onChange={this.handleTextInputChange1}
+                  onChange={value =>
+                    this.handleTextInputChange("address", value)
+                  }
                 />
               </FormGroup>
               <FormGroup
@@ -95,8 +110,8 @@ class ConnectForm extends React.Component {
                 fieldId={`form-port-${this.props.prefix}`}
               >
                 <TextInput
-                  value={value2}
-                  onChange={this.handleTextInputChange2}
+                  value={port}
+                  onChange={value => this.handleTextInputChange("port", value)}
                   isRequired
                   type="number"
                   id={`form-port-${this.props.prefix}`}
@@ -108,8 +123,10 @@ class ConnectForm extends React.Component {
                 fieldId={`form-user-${this.props.prefix}`}
               >
                 <TextInput
-                  value={value3}
-                  onChange={this.handleTextInputChange3}
+                  value={username}
+                  onChange={value =>
+                    this.handleTextInputChange("username", value)
+                  }
                   isRequired
                   id={`form-user-${this.props.prefix}`}
                   name="form-user"
@@ -120,8 +137,10 @@ class ConnectForm extends React.Component {
                 fieldId={`form-password-${this.props.prefix}`}
               >
                 <TextInput
-                  value={value4}
-                  onChange={this.handleTextInputChange4}
+                  value={password}
+                  onChange={value =>
+                    this.handleTextInputChange("password", value)
+                  }
                   type="password"
                   id={`form-password-${this.props.prefix}`}
                   name="form-password"
diff --git a/console/react/src/connectPage.js b/console/react/src/connectPage.js
index cd537d1..6d070b7 100644
--- a/console/react/src/connectPage.js
+++ b/console/react/src/connectPage.js
@@ -39,10 +39,8 @@ class ConnectPage extends React.Component {
           </TextContent>
           <TextContent>
             <Text component="p">
-              The console provides limited information about the clients that
-              are attached to the router network and is therefore more
-              appropriate for administrators needing to know the layout and
-              health of the router network.
+              This console provides information about routers and their
+              connected clients.
             </Text>
           </TextContent>
         </div>
diff --git a/console/react/src/contextMenuComponent.jsx b/console/react/src/contextMenuComponent.js
similarity index 95%
rename from console/react/src/contextMenuComponent.jsx
rename to console/react/src/contextMenuComponent.js
index 18dd46c..5ae59a3 100644
--- a/console/react/src/contextMenuComponent.jsx
+++ b/console/react/src/contextMenuComponent.js
@@ -8,6 +8,8 @@ class ContextMenuComponent extends React.Component {
 
   componentDidMount = () => {
     this.registerHandlers();
+    console.log(`contextMenuComponent mounted with`);
+    console.log(this.props.contextEventPosition);
   };
 
   componentWillUnmount = () => {
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index 13d198f..cf59f4f 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -54,7 +54,7 @@ import ConnectPage from "./connectPage";
 import DashboardPage from "./overview/dashboard/dashboardPage";
 import OverviewTablePage from "./overview/overviewTablePage";
 import DetailsTablePage from "./overview/detailsTablePage";
-import TopologyPage from "./topology/qdrTopology";
+import TopologyPage from "./topology/topologyPage";
 import MessageFlowPage from "./chord/qdrChord";
 import LogDetails from "./overview/logDetails";
 import { QDRService } from "./qdrService";
@@ -70,11 +70,28 @@ class PageLayout extends React.Component {
       isDropdownOpen: false,
       activeGroup: "overview",
       activeItem: "dashboard",
-      isConnectFormOpen: false
+      isConnectFormOpen: false,
+      isNavOpenDesktop: true,
+      isNavOpenMobile: false,
+      isMobileView: false
     };
-    this.tables = ["routers", "addresses", "links", "connections", "logs"];
     this.hooks = { setLocation: this.setLocation };
     this.service = new QDRService(this.hooks);
+    this.nav = {
+      overview: [
+        { name: "dashboard" },
+        { name: "routers", pre: true },
+        { name: "addresses", pre: true },
+        { name: "links", pre: true },
+        { name: "connections", pre: true },
+        { name: "logs", pre: true }
+      ],
+      visualizations: [
+        { name: "topology" },
+        { name: "flow", title: "Message flow" }
+      ],
+      details: [{ name: "entities" }, { name: "schema" }]
+    };
   }
 
   setLocation = where => {
@@ -93,24 +110,45 @@ class PageLayout extends React.Component {
     });
   };
 
-  handleConnect = connectPath => {
+  handleConnect = (connectPath, connectInfo) => {
     if (this.state.connected) {
-      this.service.disconnect();
-      this.setState({ connected: false });
+      this.setState(
+        { connectPath: "", connected: false, isConnectFormOpen: false },
+        () => {
+          this.service.disconnect();
+        }
+      );
     } else {
-      this.service
-        .connect({ address: "localhost", port: 5673, reconnect: true })
-        .then(
-          r => {
-            this.setState({
-              connected: true,
-              connectPath
-            });
-          },
-          e => {
-            console.log(e);
+      const connectOptions = JSON.parse(JSON.stringify(connectInfo));
+      if (connectOptions.username === "") connectOptions.username = undefined;
+      if (connectOptions.password === "") connectOptions.password = undefined;
+      connectOptions.reconnect = true;
+
+      this.service.connect(connectOptions).then(
+        r => {
+          if (connectPath === "/") connectPath = "/dashboard";
+          const activeItem = connectPath.split("/").pop();
+          // find the active group for this item
+          let activeGroup = "overview";
+          for (const group in this.nav) {
+            if (this.nav[group].some(item => item.name === activeItem)) {
+              activeGroup = group;
+              break;
+            }
           }
-        );
+
+          this.setState({
+            isConnectFormOpen: false,
+            activeItem,
+            activeGroup,
+            connected: true,
+            connectPath
+          });
+        },
+        e => {
+          console.log(e);
+        }
+      );
     }
   };
 
@@ -131,78 +169,59 @@ class PageLayout extends React.Component {
     this.setState({ isConnectFormOpen: false });
   };
 
+  onNavToggleDesktop = () => {
+    this.setState({
+      isNavOpenDesktop: !this.state.isNavOpenDesktop
+    });
+  };
+
+  onNavToggleMobile = () => {
+    this.setState({
+      isNavOpenMobile: !this.state.isNavOpenMobile
+    });
+  };
+
+  onPageResize = ({ mobileView, windowSize }) => {
+    this.setState({
+      isMobileView: mobileView
+    });
+  };
+
   render() {
     const { isDropdownOpen, activeItem, activeGroup } = this.state;
+    const { isNavOpenDesktop, isNavOpenMobile, isMobileView } = this.state;
 
     const PageNav = (
       <Nav onSelect={this.onNavSelect} aria-label="Nav" className="pf-m-dark">
         <NavList>
-          <NavExpandable
-            title="Overview"
-            groupId="overview"
-            isActive={activeGroup === "overview"}
-            isExpanded
-          >
-            <NavItem
-              groupId="overview"
-              itemId="dashboard"
-              isActive={activeItem === "dashboard"}
-            >
-              <Link to="/dashboard">Dashboard</Link>
-            </NavItem>
-            {this.tables.map(t => {
-              return (
-                <NavItem
-                  groupId="overview"
-                  itemId={t}
-                  isActive={activeItem === { t }}
-                  key={t}
-                >
-                  <Link to={`/overview/${t}`}>{this.icap(t)}</Link>
-                </NavItem>
-              );
-            })}
-          </NavExpandable>
-          <NavExpandable
-            title="Visualizations"
-            groupId="visualizations"
-            isActive={activeGroup === "visualizations"}
-          >
-            <NavItem
-              groupId="visualizations"
-              itemId="topology"
-              isActive={activeItem === "topology"}
-            >
-              <Link to="/topology">Topology</Link>
-            </NavItem>
-            <NavItem
-              groupId="visualizations"
-              itemId="flow"
-              isActive={activeItem === "flow"}
-            >
-              <Link to="/flow">Message flow</Link>
-            </NavItem>
-          </NavExpandable>
-          <NavExpandable
-            title="Details"
-            groupId="detailsGroup"
-            isActive={activeGroup === "detailsGroup"}
-          >
-            <NavItem
-              groupId="detailsGroup"
-              itemId="entities"
-              isActive={activeItem === "entities"}
-            >
-              <Link to="/entities">Entities</Link>
-            </NavItem>
-            <NavItem
-              groupId="detailsGroup"
-              itemId="schema"
-              isActive={activeItem === "schema"}
-            >
-              <Link to="/schema">Schema</Link>
-            </NavItem>
-          </NavExpandable>
+          {Object.keys(this.nav).map(section => {
+            const Section = this.icap(section);
+            return (
+              <NavExpandable
+                title={Section}
+                groupId={section}
+                isActive={activeGroup === section}
+                isExpanded
+                key={section}
+              >
+                {this.nav[section].map(item => {
+                  const key = item.name;
+                  return (
+                    <NavItem
+                      groupId={section}
+                      itemId={key}
+                      isActive={activeItem === key}
+                      key={key}
+                    >
+                      <Link to={`/${item.pre ? section + "/" : ""}${key}`}>
+                        {item.title ? item.title : this.icap(key)}
+                      </Link>
+                    </NavItem>
+                  );
+                })}
+              </NavExpandable>
+            );
+          })}
         </NavList>
       </Nav>
     );
@@ -270,16 +289,26 @@ class PageLayout extends React.Component {
         toolbar={PageToolbar}
         avatar={<Avatar src={avatarImg} alt="Avatar image" />}
         showNavToggle
+        onNavToggle={
+          isMobileView ? this.onNavToggleMobile : this.onNavToggleDesktop
+        }
+        isNavOpen={isMobileView ? isNavOpenMobile : isNavOpenDesktop}
       />
     );
-    const pageId = "main-content-page-layout-expandable-nav";
+    const pageId = "main-content-page-layout-manual-nav";
     const PageSkipToContent = (
       <SkipToContent href={`#${pageId}`}>Skip to Content</SkipToContent>
     );
 
     const sidebar = PageNav => {
       if (this.state.connected) {
-        return <PageSidebar nav={PageNav} className="pf-m-dark" />;
+        return (
+          <PageSidebar
+            nav={PageNav}
+            isNavOpen={isMobileView ? isNavOpenMobile : isNavOpenDesktop}
+            theme="dark"
+          />
+        );
       }
       return <React.Fragment />;
     };
@@ -316,6 +345,7 @@ class PageLayout extends React.Component {
       if (this.state.isConnectFormOpen) {
         return (
           <ConnectForm
+            fromPath={"/"}
             handleConnect={this.handleConnect}
             handleConnectCancel={this.handleConnectCancel}
             isConnected={this.state.connected}
@@ -331,8 +361,9 @@ class PageLayout extends React.Component {
         <Page
           header={Header}
           sidebar={sidebar(PageNav)}
-          isManagedSidebar
+          onPageResize={this.onPageResize}
           skipToContent={PageSkipToContent}
+          mainContainerId={pageId}
         >
           {connectForm()}
           <Switch>
diff --git a/console/react/src/overview/dataSources/logsData.js b/console/react/src/overview/dataSources/logsData.js
index d54769b..045ecd9 100644
--- a/console/react/src/overview/dataSources/logsData.js
+++ b/console/react/src/overview/dataSources/logsData.js
@@ -17,6 +17,28 @@ specific language governing permissions and limitations
 under the License.
 */
 
+import React from "react";
+import { Button } from "@patternfly/react-core";
+class LogRecords extends React.Component {
+  detailClick = () => {
+    this.props.detailClick(this.props.value, this.props.extraInfo);
+  };
+  render() {
+    if (
+      this.props.extraInfo.rowData.enable.title !== "" &&
+      this.props.value !== "0"
+    ) {
+      return (
+        <Button className="link-button" onClick={this.detailClick}>
+          {this.props.value}
+        </Button>
+      );
+    } else {
+      return this.props.value;
+    }
+  }
+}
+
 class LogsData {
   constructor(service) {
     this.service = service;
@@ -24,17 +46,53 @@ class LogsData {
       { title: "Router", field: "node" },
       { title: "Enable", field: "enable" },
       { title: "Module", field: "name" },
-      { title: "Trace", field: "traceCount", numeric: true },
-      { title: "Info", field: "infoCount", numeric: true },
-      { title: "Debug", field: "debugCount", numeric: true },
-      { title: "Notice", field: "noticeCount", numeric: true },
-      { title: "Warning", field: "warningCount", numeric: true },
-      { title: "Error", field: "errorCount", numeric: true },
-      { title: "Critical", field: "criticalCount", numeric: true }
+      {
+        title: "Info",
+        field: "infoCount",
+        numeric: true,
+        formatter: LogRecords
+      },
+      {
+        title: "Trace",
+        field: "traceCount",
+        numeric: true,
+        formatter: LogRecords
+      },
+      {
+        title: "Debug",
+        field: "debugCount",
+        numeric: true,
+        formatter: LogRecords
+      },
+      {
+        title: "Notice",
+        field: "noticeCount",
+        numeric: true,
+        formatter: LogRecords
+      },
+      {
+        title: "Warning",
+        field: "warningCount",
+        numeric: true,
+        formatter: LogRecords
+      },
+      {
+        title: "Error",
+        field: "errorCount",
+        numeric: true,
+        formatter: LogRecords
+      },
+      {
+        title: "Critical",
+        field: "criticalCount",
+        numeric: true,
+        formatter: LogRecords
+      }
     ];
     this.detailEntity = "log";
     this.detailName = "Log";
     this.detailPath = "/logs";
+    this.detailFormatter = true;
   }
 
   fetchRecord = (currentRecord, schema) => {
diff --git a/console/react/src/overview/logDetails.js b/console/react/src/overview/logDetails.js
index 22dff64..617c74f 100644
--- a/console/react/src/overview/logDetails.js
+++ b/console/react/src/overview/logDetails.js
@@ -25,8 +25,7 @@ import {
   DataListItem,
   DataListItemRow,
   DataListItemCells,
-  DataListCell,
-  DataListContent
+  DataListCell
 } from "@patternfly/react-core";
 import {
   Stack,
@@ -39,7 +38,6 @@ import {
 } from "@patternfly/react-core";
 
 import { Redirect } from "react-router-dom";
-import { dataMap } from "./entityData";
 
 class LogDetails extends React.Component {
   constructor(props) {
@@ -47,7 +45,8 @@ class LogDetails extends React.Component {
     this.state = {
       redirect: false,
       redirectPath: "/dashboard",
-      lastUpdated: new Date()
+      lastUpdated: new Date(),
+      logRecords: []
     };
     // if we get to this page and we don't have a props.location.state.entity
     // then redirect back to the dashboard.
@@ -59,6 +58,8 @@ class LogDetails extends React.Component {
       this.props.location.state.entity;
     if (!this.entity) {
       this.state.redirect = true;
+    } else {
+      this.info = props.location.state;
     }
   }
 
@@ -73,13 +74,57 @@ class LogDetails extends React.Component {
     }
   };
 
-  update = () => {};
+  update = () => {
+    if (!this.info) return;
+    const nodeId = this.info.currentRecord.nodeId;
+    var gotLogInfo = (nodeId, response, context) => {
+      let statusCode = context.message.application_properties.statusCode;
+      if (statusCode < 200 || statusCode >= 300) {
+        console.log("Error " + context.message.statusDescription);
+      } else {
+        let levelLogs = response.filter(result => {
+          if (result[1] == null) result[1] = "error";
+          return (
+            result[1].toUpperCase() === this.info.property.toUpperCase() &&
+            result[0] === this.info.currentRecord.name
+          );
+        });
+        levelLogs.length = Math.min(levelLogs.length, 100);
+        let logRecords = levelLogs.map((result, i) => {
+          return [
+            <DataListCell key={`log-message-${i}`}>
+              <div className="log-record">
+                <div className="log-date">{Date(result[5]).toString()}</div>
+                <div className="log-source">{result[3]}</div>
+                <div className="log-message">{result[2]}</div>
+              </div>
+            </DataListCell>
+          ];
+        });
+        this.setState({ logRecords, lastUpdated: new Date() });
+      }
+    };
+    this.props.service.management.connection
+      .sendMethod(nodeId, undefined, {}, "GET-LOG", {
+        module: this.info.currentRecord.name
+      })
+      .then(response => {
+        gotLogInfo(nodeId, response.response, response.context);
+      });
+  };
   icap = s => s.charAt(0).toUpperCase() + s.slice(1);
 
   parentItem = () => {
-    // otherwise return the 1st field
-    return this.props.location.state.value;
+    return `${this.info.currentRecord.node} ${this.info.currentRecord.name} ${this.info.property}`;
   };
+  breadcrumbSelected = () => {
+    this.setState({
+      redirect: true,
+      redirectPath: `/overview/${this.entity}`,
+      redirectState: this.props.location.state
+    });
+  };
+
   render() {
     if (this.state.redirect) {
       return (
@@ -104,9 +149,7 @@ class LogDetails extends React.Component {
               <Breadcrumb>
                 <BreadcrumbItem
                   className="link-button"
-                  onClick={() =>
-                    this.breadcrumbSelected(`/overview/${this.entity}`)
-                  }
+                  onClick={this.breadcrumbSelected}
                 >
                   {this.icap(this.entity)}
                 </BreadcrumbItem>
@@ -115,7 +158,7 @@ class LogDetails extends React.Component {
 
               <TextContent>
                 <Text className="overview-title" component={TextVariants.h1}>
-                  {`Logs ${this.parentItem()} attributes`}
+                  {`${this.parentItem()} log records`}
                 </Text>
                 <Text className="overview-loading" component={TextVariants.pre}>
                   {`Updated ${this.props.service.utilities.strDate(
@@ -127,6 +170,30 @@ class LogDetails extends React.Component {
 
             <StackItem className="overview-table">
               <DataList aria-label="Simple data list example">
+                {this.state.logRecords.map((rec, i) => {
+                  return (
+                    <DataListItem
+                      key={`log-item-${i}`}
+                      aria-labelledby={`simple-item1-${i}`}
+                    >
+                      <DataListItemRow>
+                        <DataListItemCells dataListCells={rec} />
+                      </DataListItemRow>
+                    </DataListItem>
+                  );
+                })}
+              </DataList>
+            </StackItem>
+          </Stack>
+        </PageSection>
+      </React.Fragment>
+    );
+  }
+}
+
+export default LogDetails;
+
+/*
                 <DataListItem aria-labelledby="simple-item1">
                   <DataListItemRow>
                     <DataListItemCells
@@ -164,13 +231,4 @@ class LogDetails extends React.Component {
                     />
                   </DataListItemRow>
                 </DataListItem>
-              </DataList>
-            </StackItem>
-          </Stack>
-        </PageSection>
-      </React.Fragment>
-    );
-  }
-}
-
-export default LogDetails;
+*/
diff --git a/console/react/src/overview/overviewTable.js b/console/react/src/overview/overviewTable.js
index 6555199..e08f1f6 100644
--- a/console/react/src/overview/overviewTable.js
+++ b/console/react/src/overview/overviewTable.js
@@ -87,7 +87,10 @@ class OverviewTable extends React.Component {
         );
       }
     });
-    this.dataSource.fields[0].cellFormatters.push(this.detailLink);
+    // if the dataSource did not provide its own cell formatter for details
+    if (!this.dataSource.detailFormatter) {
+      this.dataSource.fields[0].cellFormatters.push(this.detailLink);
+    }
 
     this.setState({ columns: this.dataSource.fields }, () => {
       this.update();
@@ -139,13 +142,14 @@ class OverviewTable extends React.Component {
     this.setState({
       redirect: true,
       redirectState: {
-        value: extraInfo.rowData.cells[0],
+        value: extraInfo.rowData.cells[extraInfo.columnIndex],
         currentRecord: extraInfo.rowData.data,
         entity: this.entity,
         page: this.state.page,
         sortBy: this.state.sortBy,
         filterBy: this.state.filterBy,
-        perPage: this.state.perPage
+        perPage: this.state.perPage,
+        property: extraInfo.property
       }
     });
   };
@@ -169,6 +173,7 @@ class OverviewTable extends React.Component {
         value={value}
         extraInfo={extraInfo}
         service={this.props.service}
+        detailClick={this.detailClick}
       />
     );
   };
diff --git a/console/react/src/qdrGlobals.js b/console/react/src/qdrGlobals.js
index b43aabc..a121bfb 100644
--- a/console/react/src/qdrGlobals.js
+++ b/console/react/src/qdrGlobals.js
@@ -53,7 +53,6 @@ export class QDRLogger {
 }
 
 export const QDRTemplatePath = "html/";
-export const QDR_SETTINGS_KEY = "QDRSettings";
 export const QDR_LAST_LOCATION = "QDRLastLocation";
 export const QDR_INTERVAL = "QDRInterval";
 
diff --git a/console/react/src/qdrService.js b/console/react/src/qdrService.js
index 6ca41c7..9fcac79 100644
--- a/console/react/src/qdrService.js
+++ b/console/react/src/qdrService.js
@@ -73,7 +73,8 @@ export class QDRService {
   disconnect() {
     this.management.connection.disconnect();
     delete this.management;
-    this.management = new dm(this.$location.protocol(), DEFAULT_INTERVAL);
+    const url = utils.getUrlParts(window.location);
+    this.management = new dm(url.protocol, DEFAULT_INTERVAL);
   }
 }
 
diff --git a/console/react/src/topology/contextMenu.js b/console/react/src/topology/contextMenu.js
new file mode 100644
index 0000000..f68751d
--- /dev/null
+++ b/console/react/src/topology/contextMenu.js
@@ -0,0 +1,80 @@
+/*
+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 ContextMenuComponent from "../contextMenuComponent";
+
+class ContextMenu extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+    this.contextMenuItems = [
+      {
+        title: "Freeze in place",
+        action: this.setFixed,
+        enabled: data => !this.isFixed(data)
+      },
+      {
+        title: "Unfreeze",
+        action: this.setFixed,
+        enabled: this.isFixed,
+        endGroup: true
+      },
+      {
+        title: "Unselect",
+        action: this.setSelected,
+        enabled: this.isSelected
+      },
+      {
+        title: "Select",
+        action: this.setSelected,
+        enabled: data => !this.isSelected(data)
+      }
+    ];
+  }
+
+  setFixed = (item, data) => {
+    data.setFixed(item.title !== "Unfreeze");
+  };
+
+  isFixed = data => {
+    return data.isFixed();
+  };
+
+  setSelected = (item, data) => {
+    this.props.setSelected(item, data);
+  };
+
+  isSelected = data => {
+    return data.selected ? true : false;
+  };
+
+  render() {
+    return (
+      <ContextMenuComponent
+        contextEventPosition={this.props.contextEventPosition}
+        contextEventData={this.props.contextEventData}
+        handleContextHide={this.props.handleContextHide}
+        menuItems={this.contextMenuItems}
+      />
+    );
+  }
+}
+
+export default ContextMenu;
diff --git a/console/react/src/topology/legend.js b/console/react/src/topology/legend.js
index cf96682..82a49d7 100644
--- a/console/react/src/topology/legend.js
+++ b/console/react/src/topology/legend.js
@@ -89,7 +89,6 @@ const lookFor = [
 export class Legend {
   constructor(nodes, QDRLog) {
     this.nodes = nodes;
-    this.log = QDRLog;
   }
 
   // create a new legend container svg
@@ -124,7 +123,7 @@ export class Legend {
       lsvg = d3.select("#topo_svg_legend svg g").selectAll("g");
     }
     // add a node to legendNodes for each node type that is currently in the svg
-    let legendNodes = new Nodes(this.log);
+    let legendNodes = new Nodes();
     this.nodes.nodes.forEach((n, i) => {
       let node = lookFor.find(lf => lf.cmp(n));
       if (node) {
diff --git a/console/react/src/topology/legendComponent.js b/console/react/src/topology/legendComponent.js
index 5432fa9..7ee02ae 100644
--- a/console/react/src/topology/legendComponent.js
+++ b/console/react/src/topology/legendComponent.js
@@ -18,120 +18,46 @@ under the License.
 */
 
 import React, { Component } from "react";
-import {
-  Accordion,
-  AccordionItem,
-  AccordionContent,
-  AccordionToggle
-} from "@patternfly/react-core";
-
-import ArrowsComponent from "./arrowsComponent";
-import TrafficComponent from "./trafficComponent";
-import MapLegendComponent from "./mapLegendComponent";
+import { Legend } from "./legend.js";
 
 class LegendComponent extends Component {
   constructor(props) {
     super(props);
     this.state = {};
+    this.legend = new Legend(this.props.nodes);
   }
 
-  render() {
-    const toggle = id => {
-      const idOpen = this.props[`${id}Open`];
-      this.props.handleOpenChange(id, !idOpen);
-    };
+  componentDidMount = () => {
+    this.legend.update();
+  };
 
+  render() {
     return (
-      <Accordion>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("traffic")}
-            isExpanded={this.props.trafficOpen}
-            id="traffic"
-          >
-            Traffic
-          </AccordionToggle>
-          <AccordionContent
-            id="traffic-expand"
-            isHidden={!this.props.trafficOpen}
-            isFixed
-          >
-            <TrafficComponent
-              addresses={this.props.addresses}
-              addressColors={this.props.addressColors}
-              open={this.props.trafficOpen}
-              handleChangeTrafficAnimation={
-                this.props.handleChangeTrafficAnimation
-              }
-              handleChangeTrafficFlowAddress={
-                this.props.handleChangeTrafficFlowAddress
-              }
-              handleHoverAddress={this.props.handleHoverAddress}
-              dots={this.props.dots}
-              congestion={this.props.congestion}
-            />
-          </AccordionContent>
-        </AccordionItem>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("legend")}
-            isExpanded={this.props.legendOpen}
-            id="legend"
-          >
-            Legend
-          </AccordionToggle>
-          <AccordionContent
-            id="legend-expand"
-            isHidden={!this.props.legendOpen}
-            isFixed
-          >
-            <div id="topo_svg_legend"></div>
-          </AccordionContent>
-        </AccordionItem>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("map")}
-            isExpanded={this.props.mapOpen}
-            id="map"
-          >
-            Background map
-          </AccordionToggle>
-          <AccordionContent
-            id="map-expand"
-            isHidden={!this.props.mapOpen}
-            isFixed
-          >
-            <MapLegendComponent
-              open={this.props.mapOpen}
-              mapShown={this.props.mapShown}
-              areaColor={this.props.areaColor}
-              oceanColor={this.props.oceanColor}
-              handleUpdateMapColor={this.props.handleUpdateMapColor}
-              handleUpdateMapShown={this.props.handleUpdateMapShown}
-            />
-          </AccordionContent>
-        </AccordionItem>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("arrows")}
-            isExpanded={this.props.arrowsOpen}
-            id="arrows"
-          >
-            Arrows
-          </AccordionToggle>
-          <AccordionContent
-            id="arrows-expand"
-            isHidden={!this.props.arrowsOpen}
-            isFixed
+      <div
+        id="topologyLegend"
+        className="pf-c-modal-box pf-u-box-shadow-md pf-m-sm"
+        role="dialog"
+        aria-modal="true"
+        aria-labelledby="modal-lg-title"
+        aria-describedby="modal-lg-description"
+      >
+        <header>
+          <h1 className="pf-c-title pf-m-xl" id="modal-lg-title">
+            Topology Legend
+          </h1>
+          <button
+            className="pf-c-button pf-m-plain"
+            type="button"
+            aria-label="Close"
+            onClick={this.props.handleCloseLegend}
           >
-            <ArrowsComponent
-              handleChangeArrows={this.props.handleChangeArrows}
-              routerArrows={this.props.routerArrows}
-              clientArrows={this.props.clientArrows}
-            />
-          </AccordionContent>
-        </AccordionItem>
-      </Accordion>
+            <i className="fas fa-times" aria-hidden="true"></i>
+          </button>
+        </header>
+        <div className="pf-c-modal-box__body" id="modal-lg-description">
+          <div id="topo_svg_legend"></div>{" "}
+        </div>
+      </div>
     );
   }
 }
diff --git a/console/react/src/topology/map.js b/console/react/src/topology/map.js
index 2ac3578..592b8a3 100644
--- a/console/react/src/topology/map.js
+++ b/console/react/src/topology/map.js
@@ -21,7 +21,7 @@ import * as d3 from "d3";
 import * as topojson from "topojson-client";
 
 const maxnorth = 84;
-const maxsouth = 60;
+const maxsouth = 74;
 const MAPOPTIONSKEY = "QDRMapOptions";
 const MAPPOSITIONKEY = "QDRMapPosition";
 const defaultLandColor = "#A3D3E0";
@@ -75,7 +75,56 @@ export class BackgroundMap {
     d3.select(".pf-c-page__main").style("background-color", color);
   }
 
-  init($scope, svg, width, height) {
+  setWidthHeight(width, height) {
+    if (!this.initialized) return;
+    this.width = width;
+    this.height = height;
+    // track last translation and scale event we processed
+    this.rotate = 20;
+    this.scaleExtent = [1, 10];
+
+    // setup the projection with some defaults
+    this.projection = d3.geo
+      .mercator()
+      .rotate([this.rotate, 0])
+      .scale(1)
+      .translate([width / 2, height / 2]);
+
+    // this path will hold the land coordinates once they are loaded
+    this.geoPath = d3.geo.path().projection(this.projection);
+
+    // set up the scale extent and initial scale for the projection
+    var b = getMapBounds(this.projection, Math.max(maxnorth, maxsouth)),
+      s = width / (b[1][0] - b[0][0]);
+    this.scaleExtent = [s, 15 * s];
+
+    this.projection.scale(this.scaleExtent[0]);
+
+    this.zoom = d3.behavior
+      .zoom()
+      .scaleExtent(this.scaleExtent)
+      .scale(this.projection.scale())
+      .translate([0, 0]) // not linked directly to projection
+      .on("zoom", this.zoomed);
+
+    this.geo
+      .select("rect.ocean")
+      .attr("width", width)
+      .attr("height", height)
+      .attr("fill", "#FFF");
+
+    if (this.options.show) {
+      this.svg.call(this.zoom).on("dblclick.zoom", null);
+    }
+
+    // restore map rotate, scale, translate
+    this.restoreState();
+
+    // draw with current positions
+    this.geo.selectAll(".land").attr("d", this.geoPath);
+  }
+
+  init(_unused, svg, width, height) {
     return new Promise((resolve, reject) => {
       if (this.initialized) {
         resolve();
@@ -109,7 +158,7 @@ export class BackgroundMap {
       this.lastProjection = savedOptions
         ? JSON.parse(savedOptions)
         : {
-            rotate: 20,
+            rotate: -10.884378033730373,
             scale: this.scaleExtent[0],
             translate: [width / 2, height / 2]
           };
diff --git a/console/react/src/topology/mapLegendComponent.jsx b/console/react/src/topology/mapComponent.js
similarity index 100%
rename from console/react/src/topology/mapLegendComponent.jsx
rename to console/react/src/topology/mapComponent.js
diff --git a/console/react/src/topology/nodes.js b/console/react/src/topology/nodes.js
index 8b1c3c7..ceea875 100644
--- a/console/react/src/topology/nodes.js
+++ b/console/react/src/topology/nodes.js
@@ -224,7 +224,6 @@ nodeProperties["route-container"] = nodeProperties["normal"];
 export class Nodes {
   constructor(logger) {
     this.nodes = [];
-    this.logger = logger;
   }
   static radius(type) {
     if (nodeProperties[type].radius) return nodeProperties[type].radius;
@@ -232,7 +231,7 @@ export class Nodes {
   }
   static textOffset(type) {
     let r = Nodes.radius(type);
-    let ret = type === "inter-router" || type === "_topo" ? r + 30 : 0;
+    let ret = type === "inter-router" || type === "_topo" ? r + 10 : 0;
     return ret;
   }
   static maxRadius() {
@@ -291,7 +290,7 @@ export class Nodes {
     if (index < this.getLength()) {
       return this.nodes[index];
     }
-    this.logger.error(
+    console.log(
       `Attempted to get node[${index}] but there were only ${this.getLength()} nodes`
     );
     return undefined;
diff --git a/console/react/src/topology/svgUtils.js b/console/react/src/topology/svgUtils.js
index a053603..f0dae10 100644
--- a/console/react/src/topology/svgUtils.js
+++ b/console/react/src/topology/svgUtils.js
@@ -17,6 +17,7 @@
 import { Nodes } from "./nodes.js";
 import { utils } from "../amqp/utilities.js";
 
+// update the node's classes based on the node's data
 export function updateState(circle) {
   circle
     .selectAll("circle")
@@ -36,61 +37,61 @@ export function updateState(circle) {
 
 export function appendCircle(g) {
   // add new circles and set their attr/class/behavior
-  return g
-    .append("svg:circle")
-    .attr("class", "node")
-    .attr("r", function(d) {
-      return Nodes.radius(d.nodeType);
-    })
-    .attr("fill", function(d) {
-      if (d.cdir === "both" && !utils.isConsole(d)) {
-        return "url(#half-circle)";
-      }
-      return null;
-    })
-    .classed("fixed", function(d) {
-      return d.fixed ? d.fixed & 1 : false;
-    })
-    .classed("normal", function(d) {
-      return d.nodeType === "normal" || utils.isConsole(d);
-    })
-    .classed("in", function(d) {
-      return d.cdir === "in";
-    })
-    .classed("out", function(d) {
-      return d.cdir === "out";
-    })
-    .classed("inout", function(d) {
-      return d.cdir === "both";
-    })
-    .classed("inter-router", function(d) {
-      return d.nodeType === "inter-router" || d.nodeType === "_topo";
-    })
-    .classed("on-demand", function(d) {
-      return d.nodeType === "on-demand";
-    })
-    .classed("edge", function(d) {
-      return d.nodeType === "edge" || d.nodeType === "_edge";
-    })
-    .classed("console", function(d) {
-      return utils.isConsole(d);
-    })
-    .classed("artemis", function(d) {
-      return utils.isArtemis(d);
-    })
-    .classed("qpid-cpp", function(d) {
-      return utils.isQpid(d);
-    })
-    .classed("route-container", function(d) {
-      return (
-        !utils.isArtemis(d) &&
-        !utils.isQpid(d) &&
-        d.nodeType === "route-container"
-      );
-    })
-    .classed("client", function(d) {
-      return d.nodeType === "normal" && !d.properties.console_identifier;
-    });
+  return (
+    g
+      .append("svg:circle")
+      .attr("class", "node")
+      // the following attrs and classes won't change after the node is created
+      .attr("r", function(d) {
+        return Nodes.radius(d.nodeType);
+      })
+      .attr("fill", function(d) {
+        if (d.cdir === "both" && !utils.isConsole(d)) {
+          return "url(#half-circle)";
+        }
+        return null;
+      })
+      .classed("normal", function(d) {
+        return d.nodeType === "normal" || utils.isConsole(d);
+      })
+      .classed("in", function(d) {
+        return d.cdir === "in";
+      })
+      .classed("out", function(d) {
+        return d.cdir === "out";
+      })
+      .classed("inout", function(d) {
+        return d.cdir === "both";
+      })
+      .classed("inter-router", function(d) {
+        return d.nodeType === "inter-router" || d.nodeType === "_topo";
+      })
+      .classed("on-demand", function(d) {
+        return d.nodeType === "on-demand";
+      })
+      .classed("edge", function(d) {
+        return d.nodeType === "edge" || d.nodeType === "_edge";
+      })
+      .classed("console", function(d) {
+        return utils.isConsole(d);
+      })
+      .classed("artemis", function(d) {
+        return utils.isArtemis(d);
+      })
+      .classed("qpid-cpp", function(d) {
+        return utils.isQpid(d);
+      })
+      .classed("route-container", function(d) {
+        return (
+          !utils.isArtemis(d) &&
+          !utils.isQpid(d) &&
+          d.nodeType === "route-container"
+        );
+      })
+      .classed("client", function(d) {
+        return d.nodeType === "normal" && !d.properties.console_identifier;
+      })
+  );
 }
 
 export function appendContent(g) {
diff --git a/console/react/src/topology/topoUtils.js b/console/react/src/topology/topoUtils.js
index 00eb196..0511d7b 100644
--- a/console/react/src/topology/topoUtils.js
+++ b/console/react/src/topology/topoUtils.js
@@ -268,17 +268,15 @@ export function connectionPopupHTML(d, nodeInfo) {
   return HTML;
 }
 
-export function getSizes(topologyRef, log) {
+export function getSizes(topologyRef) {
   const gap = 5;
-  let legendWidth = 194;
   let topoWidth = topologyRef.offsetWidth;
-  if (topoWidth < 768) legendWidth = 0;
-  let width = topoWidth - gap - legendWidth;
+  let width = topoWidth - gap;
   let top = topologyRef.offsetTop;
   let height = window.innerHeight - top - gap;
   if (width < 10 || height < 10) {
-    log.log(`page width and height are abnormal w: ${width} h: ${height}`);
+    console.log(`page width and height are abnormal w: ${width} h: ${height}`);
     return [0, 0];
   }
-  return [width, height];
+  return { width, height };
 }
diff --git a/console/react/src/topology/topologyPage.js b/console/react/src/topology/topologyPage.js
new file mode 100644
index 0000000..f2af09e
--- /dev/null
+++ b/console/react/src/topology/topologyPage.js
@@ -0,0 +1,41 @@
+/*
+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 { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import TopologyViewer from "./topologyViewer";
+
+class TopologyPage extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      lastUpdated: new Date()
+    };
+  }
+
+  render() {
+    return (
+      <PageSection variant={PageSectionVariants.light} id="topologyPage">
+        <TopologyViewer service={this.props.service} />
+      </PageSection>
+    );
+  }
+}
+
+export default TopologyPage;
diff --git a/console/react/src/topology/topologyToolbar.js b/console/react/src/topology/topologyToolbar.js
new file mode 100644
index 0000000..218159b
--- /dev/null
+++ b/console/react/src/topology/topologyToolbar.js
@@ -0,0 +1,129 @@
+import React from "react";
+import { Toolbar, ToolbarGroup, ToolbarItem } from "@patternfly/react-core";
+import { Popover, PopoverPosition, Button } from "@patternfly/react-core";
+import TrafficComponent from "./trafficComponent";
+import MapComponent from "./mapComponent";
+import ArrowsComponent from "./arrowsComponent";
+
+class TopologyToolbar extends React.Component {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isOpen: { traffic: false, arrows: false, map: false }
+    };
+  }
+  handleShowPopover = event => {
+    console.log("handleShowPopover called");
+    const { isOpen } = this.state;
+    const value = event.target.value;
+    isOpen[value] = !isOpen[value];
+    this.setState({ isOpen });
+  };
+
+  shouldClose = tip => {
+    console.log("should close called");
+    const { isOpen } = this.state;
+    const value = tip.reference.value;
+    isOpen[value] = false;
+    this.setState({ isOpen });
+  };
+
+  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">
+            <Popover
+              className="topology-toolbar-item"
+              position={PopoverPosition.bottom}
+              enableFlip={false}
+              appendTo={() => document.getElementById("root")}
+              aria-label="Popover traffic"
+              closeBtnAriaLabel="Close Popover traffic"
+              bodyContent={
+                <TrafficComponent
+                  addresses={this.props.legendOptions.traffic.addresses}
+                  addressColors={this.props.legendOptions.traffic.addressColors}
+                  handleChangeTrafficAnimation={
+                    this.props.handleChangeTrafficAnimation
+                  }
+                  handleChangeTrafficFlowAddress={
+                    this.props.handleChangeTrafficFlowAddress
+                  }
+                  handleHoverAddress={this.props.handleHoverAddress}
+                  dots={this.props.legendOptions.traffic.dots}
+                  congestion={this.props.legendOptions.traffic.congestion}
+                />
+              }
+            >
+              <Button className="topology-toolbar-button" value="traffic">
+                <span className="pf-c-dropdown__toggle-text">Traffic</span>
+                <i
+                  className="fas fa-caret-down pf-c-dropdown__toggle-icon"
+                  aria-hidden="true"
+                ></i>
+              </Button>
+            </Popover>
+          </ToolbarItem>
+          <ToolbarItem className="pf-u-mr-md">
+            <Popover
+              className="topology-toolbar-item"
+              position={PopoverPosition.bottom}
+              enableFlip={false}
+              appendTo={() => document.getElementById("root")}
+              aria-label="Popover map"
+              closeBtnAriaLabel="Close Popover map"
+              bodyContent={
+                <MapComponent
+                  mapShown={this.props.legendOptions.map.show}
+                  areaColor={this.props.mapOptions.areaColor}
+                  oceanColor={this.props.mapOptions.oceanColor}
+                  handleUpdateMapColor={this.props.handleUpdateMapColor}
+                  handleUpdateMapShown={this.props.handleUpdateMapShown}
+                />
+              }
+            >
+              <Button className="topology-toolbar-button" value="map">
+                <span className="pf-c-dropdown__toggle-text">
+                  Background map
+                </span>
+                <i
+                  className="fas fa-caret-down pf-c-dropdown__toggle-icon"
+                  aria-hidden="true"
+                ></i>
+              </Button>
+            </Popover>
+          </ToolbarItem>
+          <ToolbarItem className="pf-u-mr-md">
+            <Popover
+              className="topology-toolbar-item"
+              position={PopoverPosition.bottom}
+              enableFlip={false}
+              appendTo={() => document.getElementById("root")}
+              aria-label="Popover arrows"
+              closeBtnAriaLabel="Close Popover arrows"
+              bodyContent={
+                <ArrowsComponent
+                  routerArrows={this.props.legendOptions.arrows.routerArrows}
+                  clientArrows={this.props.legendOptions.arrows.clientArrows}
+                  handleChangeArrows={this.props.handleChangeArrows}
+                />
+              }
+            >
+              <Button className="topology-toolbar-button" value="arrows">
+                <span className="pf-c-dropdown__toggle-text">Arrows</span>
+                <i
+                  className="fas fa-caret-down pf-c-dropdown__toggle-icon"
+                  aria-hidden="true"
+                ></i>
+              </Button>
+            </Popover>
+          </ToolbarItem>
+        </ToolbarGroup>
+      </Toolbar>
+    );
+  }
+}
+
+export default TopologyToolbar;
diff --git a/console/react/src/topology/qdrTopology.js b/console/react/src/topology/topologyViewer.js
similarity index 86%
rename from console/react/src/topology/qdrTopology.js
rename to console/react/src/topology/topologyViewer.js
index a73ec5a..a720dcf 100644
--- a/console/react/src/topology/qdrTopology.js
+++ b/console/react/src/topology/topologyViewer.js
@@ -19,6 +19,13 @@ under the License.
 
 import React, { Component } from "react";
 import * as d3 from "d3";
+import {
+  TopologyView,
+  TopologyControlBar,
+  createTopologyControlButtons,
+  TopologySideBar
+} from "@patternfly/react-topology";
+
 import QDRPopup from "../qdrPopup";
 import { Traffic } from "./traffic.js";
 import { separateAddresses } from "../chord/filters.js";
@@ -28,10 +35,11 @@ import { nextHop, connectionPopupHTML, getSizes } from "./topoUtils.js";
 import { BackgroundMap } from "./map.js";
 import { utils } from "../amqp/utilities.js";
 import { Legend } from "./legend.js";
-import LegendComponent from "./legendComponent";
 import RouterInfoComponent from "./routerInfoComponent";
 import ClientInfoComponent from "./clientInfoComponent";
-import ContextMenuComponent from "../contextMenuComponent";
+import ContextMenu from "./contextMenu";
+import TopologyToolbar from "./topologyToolbar";
+import LegendComponent from "./legendComponent";
 import {
   appendCircle,
   appendContent,
@@ -80,7 +88,8 @@ class TopologyPage extends Component {
       legendOptions: savedOptions,
       showRouterInfo: false,
       showClientInfo: false,
-      showContextMenu: false
+      showContextMenu: false,
+      showLegend: false
     };
     this.QDRLog = new QDRLogger(console, "Topology");
     this.popupCancelled = true;
@@ -117,30 +126,6 @@ class TopologyPage extends Component {
       }
     );
     this.state.mapOptions = this.backgroundMap.mapOptions;
-
-    this.contextMenuItems = [
-      {
-        title: "Freeze in place",
-        action: this.setFixed,
-        enabled: data => !this.isFixed(data)
-      },
-      {
-        title: "Unfreeze",
-        action: this.setFixed,
-        enabled: this.isFixed,
-        endGroup: true
-      },
-      {
-        title: "Unselect",
-        action: this.setSelected,
-        enabled: this.isSelected
-      },
-      {
-        title: "Select",
-        action: this.setSelected,
-        enabled: data => !this.isSelected(data)
-      }
-    ];
   }
 
   // called only once when the component is initialized
@@ -156,9 +141,10 @@ class TopologyPage extends Component {
 
     // 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",
+      this.init
+    );
   };
 
   componentWillUnmount = () => {
@@ -173,24 +159,16 @@ class TopologyPage extends Component {
 
   resize = () => {
     if (!this.svg) return;
-    let sizes = getSizes(this.topologyRef, this.QDRLog);
-    this.width = sizes[0];
-    this.height = sizes[1];
+    const { width, height } = getSizes(this.topologyRef);
+    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.force.size(sizes).resume();
+      this.backgroundMap.setWidthHeight(width, height);
+      this.force.size([width, height]).resume();
     }
-    this.updateLegend();
-  };
-
-  setFixed = (item, data) => {
-    data.setFixed(item.title !== "Unfreeze");
-  };
-
-  isFixed = data => {
-    return data.isFixed();
   };
 
   setSelected = (item, data) => {
@@ -203,9 +181,7 @@ class TopologyPage extends Component {
     this.selected_node = data.selected ? data : null;
     this.restart();
   };
-  isSelected = data => {
-    return data.selected ? true : false;
-  };
+
   updateLegend = () => {
     this.legend.update();
   };
@@ -213,9 +189,9 @@ class TopologyPage extends Component {
 
   // initialize the nodes and links array from the QDRService.topology._nodeInfo object
   init = () => {
-    let sizes = getSizes(this.topologyRef, this.QDRLog);
-    this.width = sizes[0];
-    this.height = sizes[1];
+    const { width, height } = getSizes(this.topologyRef);
+    this.width = width;
+    this.height = height;
     if (this.width < 768) {
       const legendOptions = this.state.legendOptions;
       legendOptions.map.open = false;
@@ -455,41 +431,10 @@ class TopologyPage extends Component {
     // path is a selection of all g elements under the g.links svg:group
     // here we associate the links.links array with the {g.links g} selection
     // based on the link.uid
-    this.path = this.path.data(this.forceData.links.links, function(d) {
+    this.path = this.path.data(this.forceData.links.links, d => {
       return d.uid;
     });
 
-    // update each existing {g.links g.link} element
-    this.path
-      .select(".link")
-      .classed("selected", function(d) {
-        return d.selected;
-      })
-      .classed("highlighted", function(d) {
-        return d.highlighted;
-      })
-      .classed("unknown", function(d) {
-        return !d.right && !d.left;
-      });
-
-    // reset the markers based on current highlighted/selected
-    if (
-      !this.state.legendOptions.traffic.open ||
-      !this.state.legendOptions.traffic.congestion
-    ) {
-      this.path
-        .select(".link")
-        .attr("marker-end", d => {
-          if (!this.showMarker(d)) return null;
-          return d.right ? `url(#end${d.markerId("end")})` : null;
-        })
-        .attr("marker-start", d => {
-          if (!this.showMarker(d)) return null;
-          return d.left || (!d.left && !d.right)
-            ? `url(#start${d.markerId("start")})`
-            : null;
-        });
-    }
     // add new links. if a link with a new uid is found in the data, add a new path
     let enterpath = this.path
       .enter()
@@ -548,23 +493,10 @@ class TopologyPage extends Component {
     enterpath
       .append("path")
       .attr("class", "link")
-      .attr("marker-end", d => {
-        if (!this.showMarker(d)) return null;
-        return d.right ? `url(#end${d.markerId("end")})` : null;
-      })
-      .attr("marker-start", d => {
-        if (!this.showMarker(d)) return null;
-        return d.left || (!d.left && !d.right)
-          ? `url(#start${d.markerId("start")})`
-          : null;
-      })
       .attr("id", function(d) {
         const si = d.source.uid();
         const ti = d.target.uid();
         return ["path", si, ti].join("-");
-      })
-      .classed("unknown", function(d) {
-        return !d.right && !d.left;
       });
 
     enterpath
@@ -575,6 +507,24 @@ class TopologyPage extends Component {
     // remove old links
     this.path.exit().remove();
 
+    // update each {g.links g.link} element
+    this.path
+      .select(".link")
+      .classed("selected", d => d.selected)
+      .classed("highlighted", d => d.highlighted)
+      .classed("unknown", d => !d.right && !d.left)
+      // reset the markers based on current highlighted/selected
+      .attr("marker-end", d => {
+        if (!this.showMarker(d)) return null;
+        return d.right ? `url(#end${d.markerId("end")})` : null;
+      })
+      .attr("marker-start", d => {
+        if (!this.showMarker(d)) return null;
+        return d.left || (!d.left && !d.right)
+          ? `url(#start${d.markerId("start")})`
+          : null;
+      });
+
     // circle (node) group
     this.circle = d3
       .select("g.nodes")
@@ -583,16 +533,10 @@ class TopologyPage extends Component {
         return d.uid();
       });
 
-    // update existing nodes visual states
-    updateState(this.circle);
-
     // add new circle nodes
     let enterCircle = this.circle
       .enter()
       .append("g")
-      .classed("multiple", function(d) {
-        return d.normals && d.normals.length > 1;
-      })
       .attr("id", function(d) {
         return (d.nodeType !== "normal" ? "router" : "client") + "-" + d.index;
       });
@@ -656,9 +600,7 @@ class TopologyPage extends Component {
         }
         this.mousedown_node = d;
         // mouse position relative to svg
-        this.initial_mouse_down_position = d3
-          .mouse(this.topologyRef.parentNode.parentNode.parentNode)
-          .slice();
+        this.initial_mouse_down_position = d3.mouse(this.svg.node());
       })
       .on("mouseup", function(d) {
         // mouse up for circle
@@ -678,10 +620,10 @@ 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);
           self.forceData.nodes.savePositions();
-          self.forceData.nodes.saveLonLat(this.backgroundMap);
-          self.resetMouseVars();
           self.restart();
+          self.resetMouseVars();
           return;
         }
 
@@ -737,6 +679,9 @@ class TopologyPage extends Component {
     // remove old nodes
     this.circle.exit().remove();
 
+    // update all nodes visual states
+    updateState(this.circle);
+
     // add text to client circles if there are any that represent multiple clients
     this.svg.selectAll(".subtext").remove();
     let multiples = this.svg.selectAll(".multiple");
@@ -866,19 +811,15 @@ class TopologyPage extends Component {
       this.setState({ showPopup: false });
       return;
     }
-    let width = this.topologyRef.offsetWidth;
-    let top = this.topologyRef.offsetTop - 5;
-
     // position the popup
     d3.select("#popover-div")
       .style("left", event.pageX + 5 + "px")
-      .style("top", event.pageY - top + "px");
+      .style("top", `${event.pageY}px`);
     // show popup
-    let pwidth = this.popupRef.offsetWidth;
     this.setState({ showPopup: true }, () =>
       d3
         .select("#popover-div")
-        .style("left", Math.min(width - pwidth, event.pageX + 5) + "px")
+        //.style("left", Math.min(width - pwidth, event.pageX + 5) + "px")
         .on("mouseout", () => {
           this.setState({ showPopup: false });
         })
@@ -1015,51 +956,65 @@ class TopologyPage extends Component {
     this.setState({ showContextMenu: false });
   };
 
+  // clicked on the Legend button in the control bar
+  handleLegendClick = id => {
+    this.setState({ showLegend: !this.state.showLegend });
+  };
+
+  // clicked on the x button on the legend
+  handleCloseLegend = () => {
+    this.setState({ showLegend: false });
+  };
+
   render() {
+    const controlButtons = createTopologyControlButtons({
+      legendCallback: this.handleLegendClick
+    });
     return (
-      <div className="qdrTopology">
-        <LegendComponent
-          addresses={this.state.legendOptions.traffic.addresses}
-          addressColors={this.state.legendOptions.traffic.addressColors}
-          trafficOpen={this.state.legendOptions.traffic.open}
-          legendOpen={this.state.legendOptions.legend.open}
-          mapOpen={this.state.legendOptions.map.open}
-          mapShown={this.state.legendOptions.map.show}
-          arrowsOpen={this.state.legendOptions.arrows.open}
-          dots={this.state.legendOptions.traffic.dots}
-          congestion={this.state.legendOptions.traffic.congestion}
-          routerArrows={this.state.legendOptions.arrows.routerArrows}
-          clientArrows={this.state.legendOptions.arrows.clientArrows}
-          areaColor={this.state.mapOptions.areaColor}
-          oceanColor={this.state.mapOptions.oceanColor}
-          handleOpenChange={this.handleOpenChange}
-          handleChangeArrows={this.handleChangeArrows}
-          handleChangeTrafficAnimation={this.handleChangeTrafficAnimation}
-          handleChangeTrafficFlowAddress={this.handleChangeTrafficFlowAddress}
-          handleUpdateMapColor={this.handleUpdateMapColor}
-          handleUpdateMapShown={this.handleUpdateMapShown}
-        />
+      <TopologyView
+        viewToolbar={
+          <TopologyToolbar
+            legendOptions={this.state.legendOptions}
+            mapOptions={this.state.mapOptions}
+            handleOpenChange={this.handleOpenChange}
+            handleChangeArrows={this.handleChangeArrows}
+            handleChangeTrafficAnimation={this.handleChangeTrafficAnimation}
+            handleChangeTrafficFlowAddress={this.handleChangeTrafficFlowAddress}
+            handleUpdateMapColor={this.handleUpdateMapColor}
+            handleUpdateMapShown={this.handleUpdateMapShown}
+          />
+        }
+        controlBar={<TopologyControlBar controlButtons={controlButtons} />}
+        sideBar={<TopologySideBar show={false}></TopologySideBar>}
+        sideBarOpen={false}
+        className="qdrTopology"
+      >
         <div className="diagram">
-          {this.state.showContextMenu ? (
-            <ContextMenuComponent
-              ref={el => (this.contextRef = el)}
-              contextEventPosition={this.contextEventPosition}
-              contextEventData={this.contextEventData}
-              handleContextHide={this.handleContextHide}
-              menuItems={this.contextMenuItems}
-            />
-          ) : (
-            <React.Fragment />
-          )}
           <div ref={el => (this.topologyRef = el)} id="topology"></div>
-          <div
-            id="popover-div"
-            className={this.state.showPopup ? "qdrPopup" : "qdrPopup hidden"}
-            ref={el => (this.popupRef = el)}
-          >
-            <QDRPopup content={this.state.popupContent}></QDRPopup>
-          </div>
         </div>
+        {this.state.showContextMenu && (
+          <ContextMenu
+            contextEventPosition={this.contextEventPosition}
+            contextEventData={this.contextEventData}
+            handleContextHide={this.handleContextHide}
+            setSelected={this.setSelected}
+          />
+        )}
+        {this.state.showLegend && (
+          <LegendComponent
+            nodes={this.forceData.nodes}
+            handleCloseLegend={this.handleCloseLegend}
+          />
+        )}
+
+        <div
+          id="popover-div"
+          className={this.state.showPopup ? "qdrPopup" : "qdrPopup hidden"}
+          ref={el => (this.popupRef = el)}
+        >
+          <QDRPopup content={this.state.popupContent}></QDRPopup>
+        </div>
+
         {this.state.showRouterInfo ? (
           <RouterInfoComponent
             d={this.d}
@@ -1078,7 +1033,7 @@ class TopologyPage extends Component {
         ) : (
           <div />
         )}
-      </div>
+      </TopologyView>
     );
   }
 }
diff --git a/console/react/src/topology/traffic.js b/console/react/src/topology/traffic.js
index 4cc51b0..fc1b278 100644
--- a/console/react/src/topology/traffic.js
+++ b/console/react/src/topology/traffic.js
@@ -220,6 +220,7 @@ class Congestion extends TrafficAnimation {
               color: congestion,
               small: small
             };
+            /*
             path
               .classed("traffic", true)
               .attr("marker-start", function() {
@@ -228,6 +229,7 @@ class Congestion extends TrafficAnimation {
               .attr("marker-end", function() {
                 return null;
               });
+              */
             path
               .transition()
               .duration(1000)


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