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/07 15:48:11 UTC

[qpid-dispatch] 03/03: Overview detail pages mostly working

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

commit 4d273db81efc3906d3a1fc4dd3aee2e09492b68c
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Mon Oct 7 11:47:52 2019 -0400

    Overview detail pages mostly working
---
 console/react/src/App.css                          |  61 +++-
 console/react/src/App.js                           |   1 +
 console/react/src/amqp/management.js               |   5 +
 console/react/src/amqp/utilities.js                |  32 ++-
 console/react/src/connect-form.js                  |  26 +-
 console/react/src/connectPage.js                   |  55 ++--
 console/react/src/layout.js                        | 253 ++++++++--------
 console/react/src/overview/addressesTable.js       | 161 -----------
 console/react/src/overview/connectionsTable.js     |  95 ------
 .../react/src/overview/dashboard/dashboardPage.js  |  48 ++--
 .../react/src/overview/dataSources/addressData.js  | 191 +++++++++++++
 .../src/overview/dataSources/connectionData.js     | 169 +++++++++++
 .../{linksTable.js => dataSources/linkData.js}     | 131 +++++----
 .../{routersTable.js => dataSources/routerData.js} |  68 +++--
 console/react/src/overview/detailsTablePage.js     | 218 ++++++++++++++
 console/react/src/overview/entityData.js           |  32 +++
 console/react/src/overview/overviewTable.js        | 318 +++++++++++++++++++++
 console/react/src/overview/overviewTableBase.js    | 213 --------------
 console/react/src/overview/overviewTablePage.js    | 102 ++++---
 console/react/src/overview/tableToolbar.jsx        |  19 ++
 console/react/src/qdrService.js                    |   5 +-
 21 files changed, 1409 insertions(+), 794 deletions(-)

diff --git a/console/react/src/App.css b/console/react/src/App.css
index 1f7a1f0..5601c16 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -378,10 +378,16 @@ div.state-container button.pf-c-clipboard-copy__group-copy {
   padding-right: 0 !important;
 }
 .overview-header {
-  padding: 0.5em;
-  font-size: 5em;
+  padding: 2em;
   text-align: left;
 }
+.overview-header.details {
+  padding: 1em 2em;
+}
+.overview-header.details .pf-c-breadcrumb__item {
+  font-size: 1.125em;
+  margin-bottom: 1em;
+}
 .fill-card {
   width: 100%;
 }
@@ -834,13 +840,16 @@ div.qdrChord .legend-text {
 }
 
 .overview-charts-page .pf-c-card__header.pf-c-title {
-  text-align: left;
   font-size: 22px;
 }
 
+.dashboard-header {
+  display: flex;
+}
 .overview-charts-page div.time-period {
   font-size: 18px;
   color: #888;
+  text-align: left;
 }
 
 .chart-container {
@@ -871,8 +880,7 @@ div.qdrChord .legend-text {
 }
 
 .duration-tabs {
-  position: absolute;
-  right: 1.5em;
+  margin-left: auto;
 }
 
 .duration-tabs li {
@@ -911,3 +919,46 @@ div.qdrChord .legend-text {
 .table-toolbar .pf-l-toolbar__item {
   margin-right: 0 !important;
 }
+
+.overview-header .pf-c-content {
+  display: flex;
+}
+
+.overview-loading {
+  margin-left: auto;
+  color: #999;
+  border: 0;
+  font-size: 14px;
+  padding: 0;
+  background-color: white;
+}
+
+.pf-c-button.pf-m-primary.link-button,
+.overview-header.details .pf-c-breadcrumb__item.link-button {
+  border: 0;
+  background-color: transparent;
+  color: blue;
+  font-weight: bold;
+  white-space: normal;
+}
+
+.pf-c-button.pf-m-primary.link-button:hover,
+.overview-header.details .pf-c-breadcrumb__item.link-button:hover {
+  text-decoration: underline;
+}
+
+.noWrap {
+  white-space: nowrap;
+}
+
+.link-dir-in,
+.link-dir-out {
+  padding-right: 0.5em;
+}
+.link-dir-in {
+  color: red;
+}
+
+.link-dir-out {
+  color: blue;
+}
diff --git a/console/react/src/App.js b/console/react/src/App.js
index d686a5d..91f09e7 100644
--- a/console/react/src/App.js
+++ b/console/react/src/App.js
@@ -1,5 +1,6 @@
 import React, { Component } from "react";
 import "@patternfly/patternfly/patternfly.css";
+import "@patternfly/patternfly/patternfly-addons.css";
 
 import "patternfly/dist/css/patternfly.css";
 import "@patternfly/patternfly/components/Nav/nav.css";
diff --git a/console/react/src/amqp/management.js b/console/react/src/amqp/management.js
index 0bdb748..19ca359 100644
--- a/console/react/src/amqp/management.js
+++ b/console/react/src/amqp/management.js
@@ -27,6 +27,11 @@ export class Management {
   getSchema(callback) {
     var self = this;
     return new Promise(function(resolve, reject) {
+      if (self.connection.schema) {
+        if (callback) callback(self.connection.schema);
+        resolve(self.connection.schema);
+        return;
+      }
       self.connection.sendMgmtQuery("GET-SCHEMA").then(
         function(responseAndContext) {
           var response = responseAndContext.response;
diff --git a/console/react/src/amqp/utilities.js b/console/react/src/amqp/utilities.js
index a42247e..0814b34 100644
--- a/console/react/src/amqp/utilities.js
+++ b/console/react/src/amqp/utilities.js
@@ -19,7 +19,7 @@ var ddd = typeof window === "undefined" ? require("d3") : d3;
 
 var utils = {
   isAConsole: function(properties, connectionId, nodeType, key) {
-    return this.isConsole({
+    return utils.isConsole({
       properties: properties,
       connectionId: connectionId,
       nodeType: nodeType,
@@ -73,7 +73,7 @@ var utils = {
       };
     let results = [];
     for (let i = 0; i < entity.results.length; i++) {
-      let f = filter(this.flatten(entity.attributeNames, entity.results[i]));
+      let f = filter(utils.flatten(entity.attributeNames, entity.results[i]));
       if (f) results.push(f);
     }
     return results;
@@ -235,6 +235,34 @@ var utils = {
     const url = document.createElement("a");
     url.setAttribute("href", fullUrl);
     return url;
+  },
+  Icap: s => s[0].toUpperCase() + s.slice(1, s.length - 1),
+
+  // get last token in string that looks like "/fooo/baaar/baaaz"
+  entityFromProps: props => {
+    if (props && props.location && props.location.pathname) {
+      return props.location.pathname.split("/").slice(-1)[0];
+    }
+    return "";
+  },
+
+  formatAttributes: (record, entityType) => {
+    for (const attrib in record) {
+      const schemaAttrib = entityType.attributes[attrib];
+      if (schemaAttrib) {
+        if (schemaAttrib.type === "integer") {
+          record[attrib] = utils.pretty(record[attrib]);
+        } else if (record[attrib] === null) {
+          record[attrib] = "";
+        } else if (schemaAttrib.type === "map") {
+          record[attrib] = JSON.stringify(record[attrib], null, 2);
+        } else {
+          record[attrib] = String(record[attrib]);
+        }
+      }
+    }
+    return record;
   }
 };
+
 export { utils };
diff --git a/console/react/src/connect-form.js b/console/react/src/connect-form.js
index 4735b4c..6ef9a74 100644
--- a/console/react/src/connect-form.js
+++ b/console/react/src/connect-form.js
@@ -20,14 +20,11 @@ import {
   TextInput,
   ActionGroup,
   Button,
-  ButtonVariant,
   TextContent,
   Text,
   TextVariants
 } from "@patternfly/react-core";
 
-import { PowerOffIcon } from "@patternfly/react-icons";
-
 class ConnectForm extends React.Component {
   constructor(props) {
     super(props);
@@ -37,9 +34,7 @@ class ConnectForm extends React.Component {
       value1: "",
       value2: "",
       value3: "",
-      value4: "",
-      formVisible: !this.props.buttonHidden,
-      buttonVisible: this.props.buttonHidden ? false : true
+      value4: ""
     };
     this.handleTextInputChange1 = value1 => {
       this.setState({ value1 });
@@ -57,11 +52,11 @@ class ConnectForm extends React.Component {
 
   handleConnect = () => {
     this.toggleDrawerHide();
-    this.props.handleConnect();
+    this.props.handleConnect(this.props.fromPath);
   };
 
   toggleDrawerHide = () => {
-    this.setState({ formVisible: !this.state.formVisible });
+    this.props.handleConnectCancel();
   };
 
   render() {
@@ -69,20 +64,7 @@ class ConnectForm extends React.Component {
 
     return (
       <div>
-        <Button
-          id="notificationButton"
-          onClick={this.toggleDrawerHide}
-          aria-label="Notifications actions"
-          variant={ButtonVariant.plain}
-          className={this.state.buttonVisible ? "" : "hidden"}
-        >
-          <PowerOffIcon />
-        </Button>
-        <div
-          className={
-            this.state.formVisible ? "connect-modal" : "connect-modal hidden"
-          }
-        >
+        <div className="connect-modal">
           <div className="">
             <Form isHorizontal>
               <TextContent className="connect-title">
diff --git a/console/react/src/connectPage.js b/console/react/src/connectPage.js
index 50b0e9b..cd537d1 100644
--- a/console/react/src/connectPage.js
+++ b/console/react/src/connectPage.js
@@ -10,40 +10,43 @@ import ConnectForm from "./connect-form";
 class ConnectPage extends React.Component {
   constructor(props) {
     super(props);
-    this.state = {};
+    this.state = { showForm: true };
   }
 
+  handleConnectCancel = () => {
+    this.setState({ showForm: false });
+  };
   render() {
+    const { showForm } = this.state;
+    const { from } = this.props.location.state || { from: { pathname: "/" } };
     return (
-      <React.Fragment>
-        <PageSection
-          variant={PageSectionVariants.light}
-          className="connect-page"
-        >
-          <div className="left-content">
-            <TextContent>
-              <Text component="h1" className="console-banner">
-                Apache Qpid Dispatch Console
-              </Text>
-            </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.
-              </Text>
-            </TextContent>
-          </div>
-        </PageSection>
-        <PageSection>
+      <PageSection variant={PageSectionVariants.light} className="connect-page">
+        {showForm ? (
           <ConnectForm
             prefix="form"
             handleConnect={this.props.handleConnect}
-            buttonHidden={true}
+            handleConnectCancel={this.handleConnectCancel}
+            fromPath={from.pathname}
           />
-        </PageSection>
-      </React.Fragment>
+        ) : (
+          <React.Fragment />
+        )}
+        <div className="left-content">
+          <TextContent>
+            <Text component="h1" className="console-banner">
+              Apache Qpid Dispatch Console
+            </Text>
+          </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.
+            </Text>
+          </TextContent>
+        </div>
+      </PageSection>
     );
   }
 }
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index afae49e..e81ee11 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -26,7 +26,6 @@ import {
   DropdownToggle,
   DropdownItem,
   DropdownSeparator,
-  KebabToggle,
   Page,
   PageHeader,
   SkipToContent,
@@ -40,14 +39,23 @@ import {
   PageSidebar
 } from "@patternfly/react-core";
 
+import {
+  BrowserRouter as Router,
+  Switch,
+  Route,
+  Link,
+  Redirect
+} from "react-router-dom";
+
 import accessibleStyles from "@patternfly/patternfly/utilities/Accessibility/accessibility.css";
 import spacingStyles from "@patternfly/patternfly/utilities/Spacing/spacing.css";
 import { css } from "@patternfly/react-styles";
-import { BellIcon, CogIcon } from "@patternfly/react-icons";
-import ConnectForm from "./connect-form";
+import { BellIcon, CogIcon, PowerOffIcon } from "@patternfly/react-icons";
+//import ConnectForm from "./connect-form";
 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 MessageFlowPage from "./chord/qdrChord";
 import { QDRService } from "./qdrService";
@@ -58,23 +66,16 @@ class PageLayout extends React.Component {
     super(props);
     this.state = {
       connected: false,
+      connectPath: "",
       isDropdownOpen: false,
-      isKebabDropdownOpen: false,
       activeGroup: "overview",
-      activeItem: "dashboard"
+      activeItem: "dashboard",
+      detailInfo: null,
+      detailMeta: null
     };
     this.tables = ["routers", "addresses", "links", "connections", "logs"];
 
     /*
-      connections: [
-        { title: "name", displayName: "host" },
-        { title: "container" },
-        { title: "role" },
-        { title: "dir" },
-        { title: "security" },
-        { title: "authentication" },
-        { title: "close" }
-      ],
       logs: [
         { title: "Module" },
         { title: "Notice" },
@@ -91,7 +92,7 @@ class PageLayout extends React.Component {
   }
 
   setLocation = where => {
-    //console.log(`setLocation to ${where}`);
+    //this.setState({ connectPath: where })
   };
 
   onDropdownToggle = isDropdownOpen => {
@@ -106,49 +107,55 @@ class PageLayout extends React.Component {
     });
   };
 
-  onKebabDropdownToggle = isKebabDropdownOpen => {
-    this.setState({
-      isKebabDropdownOpen
-    });
-  };
-
-  onKebabDropdownSelect = event => {
-    this.setState({
-      isKebabDropdownOpen: !this.state.isKebabDropdownOpen
-    });
-  };
-
-  handleConnect = event => {
+  handleConnect = connectPath => {
     this.service
       .connect({ address: "localhost", port: 5673, reconnect: true })
       .then(
         r => {
-          //console.log(r);
+          this.setState({
+            connected: true,
+            connectPath
+          });
         },
         e => {
           console.log(e);
         }
       );
-    this.setState({
-      connected: true
-    });
   };
 
+  handleConnectCancel = () => {};
   onNavSelect = result => {
     this.setState({
       activeItem: result.itemId,
-      activeGroup: result.groupId
+      activeGroup: result.groupId,
+      connectPath: ""
     });
   };
   icap = s => s.charAt(0).toUpperCase() + s.slice(1);
 
-  render() {
-    const {
-      isDropdownOpen,
-      isKebabDropdownOpen,
+  showDetailTable = (_value, detailInfo, activeItem, detailMeta) => {
+    this.setState({
+      activeGroup: "detailsTable",
       activeItem,
-      activeGroup
-    } = this.state;
+      detailInfo,
+      detailMeta,
+      connectPath: "/details"
+    });
+  };
+
+  BreadcrumbSelected = connectPath => {
+    this.setState({
+      connectPath
+    });
+  };
+
+  toggleConnectForm = event => {
+    console.log("taggleConnectForm called with event.target");
+    console.log(event.target);
+  };
+
+  render() {
+    const { isDropdownOpen, activeItem, activeGroup } = this.state;
 
     const PageNav = (
       <Nav onSelect={this.onNavSelect} aria-label="Nav" className="pf-m-dark">
@@ -164,7 +171,7 @@ class PageLayout extends React.Component {
               itemId="dashboard"
               isActive={activeItem === "dashboard"}
             >
-              Dashboard
+              <Link to="/dashboard">Dashboard</Link>
             </NavItem>
             {this.tables.map(t => {
               return (
@@ -174,7 +181,7 @@ class PageLayout extends React.Component {
                   isActive={activeItem === { t }}
                   key={t}
                 >
-                  {this.icap(t)}
+                  <Link to={`/overview/${t}`}>{this.icap(t)}</Link>
                 </NavItem>
               );
             })}
@@ -189,62 +196,42 @@ class PageLayout extends React.Component {
               itemId="topology"
               isActive={activeItem === "topology"}
             >
-              Topology
+              <Link to="/topology">Topology</Link>
             </NavItem>
             <NavItem
               groupId="visualizations"
               itemId="flow"
               isActive={activeItem === "flow"}
             >
-              Message flow
+              <Link to="/flow">Message flow</Link>
             </NavItem>
           </NavExpandable>
           <NavExpandable
             title="Details"
-            groupId="grp-3"
-            isActive={activeGroup === "grp-3"}
+            groupId="detailsGroup"
+            isActive={activeGroup === "detailsGroup"}
           >
             <NavItem
-              groupId="grp-3"
-              itemId="grp-3_itm-1"
-              isActive={activeItem === "grp-3_itm-1"}
+              groupId="detailsGroup"
+              itemId="entities"
+              isActive={activeItem === "entities"}
             >
-              Entities
+              <Link to="/entities">Entities</Link>
             </NavItem>
             <NavItem
-              groupId="grp-3"
-              itemId="grp-3_itm-2"
-              isActive={activeItem === "grp-3_itm-2"}
+              groupId="detailsGroup"
+              itemId="schema"
+              isActive={activeItem === "schema"}
             >
-              Schema
+              <Link to="/schema">Schema</Link>
             </NavItem>
           </NavExpandable>
         </NavList>
       </Nav>
     );
-    const kebabDropdownItems = [
-      <DropdownItem key="notif">
-        <BellIcon /> Notifications
-      </DropdownItem>,
-      <DropdownItem key="sett">
-        <CogIcon /> Settings
-      </DropdownItem>
-    ];
     const userDropdownItems = [
-      <DropdownItem key="link">Link</DropdownItem>,
       <DropdownItem component="button" key="action">
-        Action
-      </DropdownItem>,
-      <DropdownItem isDisabled key="dis">
-        Disabled Link
-      </DropdownItem>,
-      <DropdownItem isDisabled component="button" key="button">
-        Disabled Action
-      </DropdownItem>,
-      <DropdownSeparator key="sep0" />,
-      <DropdownItem key="sep">Separated Link</DropdownItem>,
-      <DropdownItem component="button" key="sep1">
-        Separated Action
+        Logout
       </DropdownItem>
     ];
     const PageToolbar = (
@@ -256,41 +243,27 @@ class PageLayout extends React.Component {
           )}
         >
           <ToolbarItem>
-            <ConnectForm prefix="toolbar" handleConnect={this.handleConnect} />
-          </ToolbarItem>
-          <ToolbarItem>
             <Button
-              id="default-example-uid-01"
-              aria-label="Notifications actions"
+              id="connectButton"
+              onClick={this.toggleConnectForm}
+              aria-label="Toggle Connect Form"
               variant={ButtonVariant.plain}
             >
-              <BellIcon />
+              <PowerOffIcon />
             </Button>
           </ToolbarItem>
           <ToolbarItem>
             <Button
-              id="default-example-uid-02"
-              aria-label="Settings actions"
+              id="default-example-uid-01"
+              aria-label="Notifications actions"
               variant={ButtonVariant.plain}
             >
-              <CogIcon />
+              <BellIcon />
             </Button>
           </ToolbarItem>
         </ToolbarGroup>
         <ToolbarGroup>
           <ToolbarItem
-            className={css(accessibleStyles.hiddenOnLg, spacingStyles.mr_0)}
-          >
-            <Dropdown
-              isPlain
-              position="right"
-              onSelect={this.onKebabDropdownSelect}
-              toggle={<KebabToggle onToggle={this.onKebabDropdownToggle} />}
-              isOpen={isKebabDropdownOpen}
-              dropdownItems={kebabDropdownItems}
-            />
-          </ToolbarItem>
-          <ToolbarItem
             className={css(
               accessibleStyles.screenReader,
               accessibleStyles.visibleOnMd
@@ -322,54 +295,82 @@ class PageLayout extends React.Component {
         showNavToggle
       />
     );
-    const Sidebar = <PageSidebar nav={PageNav} className="pf-m-dark" />;
     const pageId = "main-content-page-layout-expandable-nav";
     const PageSkipToContent = (
       <SkipToContent href={`#${pageId}`}>Skip to Content</SkipToContent>
     );
-    const activeItemToPage = () => {
-      if (this.state.activeGroup === "overview") {
-        if (this.state.activeItem === "dashboard") {
-          return <DashboardPage service={this.service} />;
-        }
-        return (
-          <OverviewTablePage
-            entity={this.state.activeItem}
-            service={this.service}
-          />
-        );
-      } else if (this.state.activeGroup === "visualizations") {
-        if (this.state.activeItem === "topology") {
-          return <TopologyPage service={this.service} />;
-        } else {
-          return <MessageFlowPage service={this.service} />;
-        }
+
+    const sidebar = PageNav => {
+      if (this.state.connected) {
+        return <PageSidebar nav={PageNav} className="pf-m-dark" />;
       }
-      //console.log("using overview charts page");
-      return <DashboardPage service={this.service} />;
+      return <React.Fragment />;
     };
 
-    if (!this.state.connected) {
-      return (
-        <Page header={Header} skipToContent={PageSkipToContent}>
-          <ConnectPage handleConnect={this.handleConnect} />
-        </Page>
-      );
-    }
+    // don't allow access to this component unless we are logged in
+    const PrivateRoute = ({ component: Component, path: rpath, ...more }) => (
+      <Route
+        path={rpath}
+        {...(more.exact ? "exact" : "")}
+        render={props =>
+          this.state.connected ? (
+            <Component service={this.service} {...props} {...more} />
+          ) : (
+            <Redirect
+              to={{ pathname: "/login", state: { from: props.location } }}
+            />
+          )
+        }
+      />
+    );
+
+    // When we need to display a different component(page),
+    // we render a <Redirect> object
+    const redirectAfterConnect = () => {
+      let { connectPath } = this.state;
+      if (connectPath !== "") {
+        if (connectPath === "/login") connectPath = "/";
+        return <Redirect to={connectPath} />;
+      }
+      return <React.Fragment />;
+    };
 
     return (
-      <React.Fragment>
+      <Router>
+        {redirectAfterConnect()}
         <Page
           header={Header}
-          sidebar={Sidebar}
+          sidebar={sidebar(PageNav)}
           isManagedSidebar
           skipToContent={PageSkipToContent}
         >
-          {activeItemToPage()}
+          <Switch>
+            <PrivateRoute path="/" exact component={DashboardPage} />
+            <PrivateRoute path="/dashboard" exact component={DashboardPage} />
+            <PrivateRoute
+              path="/overview/:entity"
+              component={OverviewTablePage}
+            />
+            <PrivateRoute path="/details" component={DetailsTablePage} />
+            <PrivateRoute path="/topology" component={TopologyPage} />
+            <PrivateRoute path="/flow" component={MessageFlowPage} />
+            <Route
+              path="/login"
+              render={props => (
+                <ConnectPage {...props} handleConnect={this.handleConnect} />
+              )}
+            />
+          </Switch>
         </Page>
-      </React.Fragment>
+      </Router>
     );
   }
 }
 
 export default PageLayout;
+
+/*          <ToolbarItem>
+            <ConnectForm prefix="toolbar" handleConnect={this.handleConnect} />
+          </ToolbarItem>
+
+          */
diff --git a/console/react/src/overview/addressesTable.js b/console/react/src/overview/addressesTable.js
deleted file mode 100644
index b69345c..0000000
--- a/console/react/src/overview/addressesTable.js
+++ /dev/null
@@ -1,161 +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 { sortable } from "@patternfly/react-table";
-
-import OverviewTableBase from "./overviewTableBase";
-
-class AddressesTable extends OverviewTableBase {
-  constructor(props) {
-    super(props);
-    this.fields = [
-      { title: "Address", field: "address", transforms: [sortable] },
-      { title: "Class", field: "class" },
-      { title: "Phase", field: "phase" },
-      { title: "In-proc", field: "inproc" },
-      { title: "Local", field: "local" },
-      { title: "Remote", field: "remote" },
-      { title: "In", field: "in" },
-      { title: "Out", field: "out" }
-    ];
-  }
-
-  doFetch = (page, perPage) => {
-    return new Promise(resolve => {
-      var addr_phase = addr => {
-        if (!addr) return "-";
-        if (addr[0] === "M") return addr[1];
-        return "";
-      };
-      var prettyVal = val => {
-        return this.props.service.utilities.pretty(val || "-");
-      };
-      let addressFields = [];
-      let addressObjs = {};
-      // send the requests for all connection and router info for all routers
-      this.props.service.management.topology.fetchAllEntities(
-        { entity: "router.address" },
-        nodes => {
-          for (let node in nodes) {
-            let response = nodes[node]["router.address"];
-            response.results.forEach(result => {
-              let address = this.props.service.utilities.flatten(
-                response.attributeNames,
-                result
-              );
-
-              var addNull = (oldVal, newVal) => {
-                if (oldVal != null && newVal != null) return oldVal + newVal;
-                if (oldVal != null) return oldVal;
-                return newVal;
-              };
-
-              let uid = address.identity;
-              let identity = this.props.service.utilities.identity_clean(uid);
-
-              if (
-                !addressObjs[
-                  this.props.service.utilities.addr_text(identity) +
-                    this.props.service.utilities.addr_class(identity)
-                ]
-              )
-                addressObjs[
-                  this.props.service.utilities.addr_text(identity) +
-                    this.props.service.utilities.addr_class(identity)
-                ] = {
-                  address: this.props.service.utilities.addr_text(identity),
-                  class: this.props.service.utilities.addr_class(identity),
-                  phase: addr_phase(identity),
-                  inproc: address.inProcess,
-                  local: address.subscriberCount,
-                  remote: address.remoteCount,
-                  in: address.deliveriesIngress,
-                  out: address.deliveriesEgress,
-                  thru: address.deliveriesTransit,
-                  toproc: address.deliveriesToContainer,
-                  fromproc: address.deliveriesFromContainer,
-                  uid: uid
-                };
-              else {
-                let sumObj =
-                  addressObjs[
-                    this.props.service.utilities.addr_text(identity) +
-                      this.props.service.utilities.addr_class(identity)
-                  ];
-                sumObj.inproc = addNull(sumObj.inproc, address.inProcess);
-                sumObj.local = addNull(sumObj.local, address.subscriberCount);
-                sumObj.remote = addNull(sumObj.remote, address.remoteCount);
-                sumObj["in"] = addNull(sumObj["in"], address.deliveriesIngress);
-                sumObj.out = addNull(sumObj.out, address.deliveriesEgress);
-                sumObj.thru = addNull(sumObj.thru, address.deliveriesTransit);
-                sumObj.toproc = addNull(
-                  sumObj.toproc,
-                  address.deliveriesToContainer
-                );
-                sumObj.fromproc = addNull(
-                  sumObj.fromproc,
-                  address.deliveriesFromContainer
-                );
-              }
-            });
-          }
-          for (let obj in addressObjs) {
-            addressObjs[obj].inproc = prettyVal(addressObjs[obj].inproc);
-            addressObjs[obj].local = prettyVal(addressObjs[obj].local);
-            addressObjs[obj].remote = prettyVal(addressObjs[obj].remote);
-            addressObjs[obj]["in"] = prettyVal(addressObjs[obj]["in"]);
-            addressObjs[obj].out = prettyVal(addressObjs[obj].out);
-            addressObjs[obj].thru = prettyVal(addressObjs[obj].thru);
-            addressObjs[obj].toproc = prettyVal(addressObjs[obj].toproc);
-            addressObjs[obj].fromproc = prettyVal(addressObjs[obj].fromproc);
-            addressFields.push(addressObjs[obj]);
-          }
-          if (addressFields.length === 0) return;
-          // update the grid's data
-          addressFields.sort((a, b) => {
-            return a.address + a["class"] < b.address + b["class"]
-              ? -1
-              : a.address + a["class"] > b.address + b["class"]
-              ? 1
-              : 0;
-          });
-          addressFields[0].title = addressFields[0].address;
-          for (let i = 1; i < addressFields.length; ++i) {
-            // if this address is the same as the previous address, add a class to the display titles
-            if (addressFields[i].address === addressFields[i - 1].address) {
-              addressFields[i - 1].title =
-                addressFields[i - 1].address +
-                " (" +
-                addressFields[i - 1]["class"] +
-                ")";
-              addressFields[i].title =
-                addressFields[i].address +
-                " (" +
-                addressFields[i]["class"] +
-                ")";
-            } else addressFields[i].title = addressFields[i].address;
-          }
-          resolve(this.slice(addressFields, page, perPage));
-        }
-      );
-    });
-  };
-}
-
-export default AddressesTable;
diff --git a/console/react/src/overview/connectionsTable.js b/console/react/src/overview/connectionsTable.js
deleted file mode 100644
index 1ee140f..0000000
--- a/console/react/src/overview/connectionsTable.js
+++ /dev/null
@@ -1,95 +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 { sortable } from "@patternfly/react-table";
-import OverviewTableBase from "./overviewTableBase";
-
-class LinksTable extends OverviewTableBase {
-  constructor(props) {
-    super(props);
-    this.fields = [
-      { title: "Host", field: "name", transforms: [sortable] },
-      { title: "Container", field: "container", transforms: [sortable] },
-      { title: "Role", field: "role", transforms: [sortable] },
-      { title: "Dir", field: "dir", transforms: [sortable] },
-      { title: "Security", field: "security", transforms: [sortable] },
-      {
-        title: "Authentication",
-        field: "authentication",
-        transforms: [sortable]
-      },
-      { title: "Close", field: "close" }
-    ];
-  }
-  doFetch = (page, perPage) => {
-    return new Promise(resolve => {
-      this.props.service.management.topology.fetchAllEntities(
-        { entity: "connection" },
-        nodes => {
-          // we have all the data now in the nodes object
-          let connectionFields = [];
-          for (let node in nodes) {
-            const response = nodes[node]["connection"];
-            for (let i = 0; i < response.results.length; i++) {
-              const result = response.results[i];
-              const connection = this.props.service.utilities.flatten(
-                response.attributeNames,
-                result
-              );
-              let auth = "no_auth";
-              let sasl = connection.sasl;
-              if (connection.isAuthenticated) {
-                auth = sasl;
-                if (sasl === "ANONYMOUS") auth = "anonymous-user";
-                else {
-                  if (sasl === "GSSAPI") sasl = "Kerberos";
-                  if (sasl === "EXTERNAL") sasl = "x.509";
-                  auth = connection.user + "(" + connection.sslCipher + ")";
-                }
-              }
-
-              let sec = "no-security";
-              if (connection.isEncrypted) {
-                if (sasl === "GSSAPI") sec = "Kerberos";
-                else
-                  sec = connection.sslProto + "(" + connection.sslCipher + ")";
-              }
-
-              let host = connection.host;
-              let connField = {
-                host: host,
-                security: sec,
-                authentication: auth,
-                routerId: node,
-                uid: host + connection.container + connection.identity
-              };
-              response.attributeNames.forEach(function(attribute, i) {
-                connField[attribute] = result[i];
-              });
-              connectionFields.push(connField);
-            }
-          }
-          resolve(this.slice(connectionFields, page, perPage));
-        }
-      );
-    });
-  };
-}
-
-export default LinksTable;
diff --git a/console/react/src/overview/dashboard/dashboardPage.js b/console/react/src/overview/dashboard/dashboardPage.js
index e3b853c..9feefcd 100644
--- a/console/react/src/overview/dashboard/dashboardPage.js
+++ b/console/react/src/overview/dashboard/dashboardPage.js
@@ -51,32 +51,34 @@ class DashboardPage extends React.Component {
           <StackItem>
             <Card>
               <CardHeader>
-                <div>Router network statistics</div>
+                <div className="dashboard-header">
+                  <div>Router network statistics</div>
+                  <div className="duration-tabs">
+                    <nav className="pf-c-nav" aria-label="Local">
+                      <ul className="pf-c-nav__tertiary-list">
+                        <li
+                          onClick={() => this.setTimePeriod(60)}
+                          className={`pf-c-nav__item ${
+                            this.state.timePeriod === 60 ? "selected" : ""
+                          }`}
+                        >
+                          Min
+                        </li>
+                        <li
+                          onClick={() => this.setTimePeriod(60 * 60)}
+                          className={`pf-c-nav__item ${
+                            this.state.timePeriod === 60 ? "" : "selected"
+                          }`}
+                        >
+                          Hour
+                        </li>
+                      </ul>
+                    </nav>
+                  </div>
+                </div>
                 <div className="time-period">
                   For the past {this.timePeriodString()}
                 </div>
-                <div className="duration-tabs">
-                  <nav className="pf-c-nav" aria-label="Local">
-                    <ul className="pf-c-nav__tertiary-list">
-                      <li
-                        onClick={() => this.setTimePeriod(60)}
-                        className={`pf-c-nav__item ${
-                          this.state.timePeriod === 60 ? "selected" : ""
-                        }`}
-                      >
-                        Min
-                      </li>
-                      <li
-                        onClick={() => this.setTimePeriod(60 * 60)}
-                        className={`pf-c-nav__item ${
-                          this.state.timePeriod === 60 ? "" : "selected"
-                        }`}
-                      >
-                        Hour
-                      </li>
-                    </ul>
-                  </nav>
-                </div>
               </CardHeader>
               <CardBody>
                 <ThroughputChart
diff --git a/console/react/src/overview/dataSources/addressData.js b/console/react/src/overview/dataSources/addressData.js
new file mode 100644
index 0000000..ee581f2
--- /dev/null
+++ b/console/react/src/overview/dataSources/addressData.js
@@ -0,0 +1,191 @@
+/*
+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.
+*/
+
+class AddressData {
+  constructor(service) {
+    this.service = service;
+    this.fields = [
+      { title: "Address", field: "address" },
+      { title: "Class", field: "class" },
+      { title: "Phase", field: "phase" },
+      { title: "In-proc", field: "inproc", numeric: true },
+      { title: "Local", field: "local", numeric: true },
+      { title: "Remote", field: "remote", numeric: true },
+      { title: "In", field: "in", numeric: true },
+      { title: "Out", field: "out", numeric: true }
+    ];
+    this.detailName = "Address";
+    this.detailField = "title";
+    this.hideFields = ["title"];
+  }
+
+  fetchRecord = (currentRecord, schema) => {
+    return new Promise(resolve => {
+      this.service.management.topology.fetchEntities(
+        currentRecord.nodeId,
+        [{ entity: "router.address" }],
+        data => {
+          const record = data[currentRecord.nodeId]["router.address"];
+          const identityIndex = record.attributeNames.indexOf("identity");
+          const result = record.results.find(
+            r => r[identityIndex] === currentRecord.uid
+          );
+          let address = this.service.utilities.flatten(
+            record.attributeNames,
+            result
+          );
+          address = this.service.utilities.formatAttributes(
+            address,
+            schema.entityTypes["router.address"]
+          );
+          resolve(address);
+        }
+      );
+    });
+  };
+
+  doFetch = (page, perPage) => {
+    return new Promise(resolve => {
+      var addr_phase = addr => {
+        if (!addr) return "-";
+        if (addr[0] === "M") return addr[1];
+        return "";
+      };
+      let addressFields = [];
+      let addressObjs = {};
+      var addNull = (oldVal, newVal) => {
+        if (oldVal != null && newVal != null) return oldVal + newVal;
+        if (oldVal != null) return oldVal;
+        return newVal;
+      };
+      // send the requests to get the router.address records for every router
+      this.service.management.topology.fetchAllEntities(
+        { entity: "router.address" },
+        nodes => {
+          // each router is a node in nodes
+          for (let node in nodes) {
+            let response = nodes[node]["router.address"];
+            // response is an array of router.address records for this node/router
+            response.results.forEach(result => {
+              // result is a single address record for this node/router
+              let address = this.service.utilities.flatten(
+                response.attributeNames,
+                result
+              );
+              // address is now an object with attribute names as keys and their values
+              let uid = address.identity;
+              let identity = this.service.utilities.identity_clean(uid);
+
+              // if this is the 1st time we've seen this address:class
+              if (
+                !addressObjs[
+                  this.service.utilities.addr_text(identity) +
+                    this.service.utilities.addr_class(identity)
+                ]
+              ) {
+                // create a new addressObjs record
+                addressObjs[
+                  this.service.utilities.addr_text(identity) +
+                    this.service.utilities.addr_class(identity)
+                ] = {
+                  address: this.service.utilities.addr_text(identity),
+                  class: this.service.utilities.addr_class(identity),
+                  phase: addr_phase(identity),
+                  inproc: address.inProcess,
+                  local: address.subscriberCount,
+                  remote: address.remoteCount,
+                  in: address.deliveriesIngress,
+                  out: address.deliveriesEgress,
+                  thru: address.deliveriesTransit,
+                  toproc: address.deliveriesToContainer,
+                  fromproc: address.deliveriesFromContainer,
+                  identity: identity,
+                  nodeId: node,
+                  uid: uid
+                };
+              } else {
+                // we've seen this address:class before. add the values
+                // into the addressObjs
+                let sumObj =
+                  addressObjs[
+                    this.service.utilities.addr_text(identity) +
+                      this.service.utilities.addr_class(identity)
+                  ];
+                sumObj.inproc = addNull(sumObj.inproc, address.inProcess);
+                sumObj.local = addNull(sumObj.local, address.subscriberCount);
+                sumObj.remote = addNull(sumObj.remote, address.remoteCount);
+                sumObj["in"] = addNull(sumObj["in"], address.deliveriesIngress);
+                sumObj.out = addNull(sumObj.out, address.deliveriesEgress);
+                sumObj.thru = addNull(sumObj.thru, address.deliveriesTransit);
+                sumObj.toproc = addNull(
+                  sumObj.toproc,
+                  address.deliveriesToContainer
+                );
+                sumObj.fromproc = addNull(
+                  sumObj.fromproc,
+                  address.deliveriesFromContainer
+                );
+              }
+            });
+          }
+          // At this point we have created and summed all the address records.
+          for (let obj in addressObjs) {
+            addressFields.push(addressObjs[obj]);
+          }
+
+          // Two records that have the same address
+          // are differenciated by adding a class to both records' title.
+          // To do this we need to sort the array by address:class
+          addressFields.sort((a, b) => {
+            return a.address + a["class"] < b.address + b["class"]
+              ? -1
+              : a.address + a["class"] > b.address + b["class"]
+              ? 1
+              : 0;
+          });
+
+          // Loop through the sorted array to find records with the same address.
+          // Construct a title field that has "address (class)" for records with
+          // duplicate addresses, and just the address for unique records
+          if (addressFields.length) {
+            addressFields[0].title = addressFields[0].address;
+            for (let i = 1; i < addressFields.length; ++i) {
+              // if this address is the same as the previous address, add a class to the display titles
+              if (addressFields[i].address === addressFields[i - 1].address) {
+                addressFields[i - 1].title =
+                  addressFields[i - 1].address +
+                  " (" +
+                  addressFields[i - 1]["class"] +
+                  ")";
+                addressFields[i].title =
+                  addressFields[i].address +
+                  " (" +
+                  addressFields[i]["class"] +
+                  ")";
+              } else addressFields[i].title = addressFields[i].address;
+            }
+          }
+          resolve({ data: addressFields, page, perPage });
+        }
+      );
+    });
+  };
+}
+
+export default AddressData;
diff --git a/console/react/src/overview/dataSources/connectionData.js b/console/react/src/overview/dataSources/connectionData.js
new file mode 100644
index 0000000..4e69543
--- /dev/null
+++ b/console/react/src/overview/dataSources/connectionData.js
@@ -0,0 +1,169 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { Button } from "@patternfly/react-core";
+
+class ConnectionClose extends React.Component {
+  closeConnection = () => {
+    const record = this.props.extraInfo.rowData.data;
+    this.props.service.management.connection
+      .sendMethod(
+        record.nodeId,
+        "connection",
+        { adminStatus: "deleted", identity: record.identity },
+        "UPDATE",
+        { adminStatus: "deleted" }
+      )
+      .then(results => {
+        let statusCode =
+          results.context.message.application_properties.statusCode;
+        if (statusCode < 200 || statusCode >= 300) {
+          console.log(
+            `error ${record.name} ${results.context.message.application_properties.statusDescription}`
+          );
+        } else {
+          console.log(
+            `success ${record.name} ${results.context.message.application_properties.statusDescription}`
+          );
+        }
+      });
+  };
+
+  render() {
+    if (this.props.extraInfo.rowData.data.role === "normal") {
+      return (
+        <Button className="link-button" onClick={this.closeConnection}>
+          Close
+        </Button>
+      );
+    } else {
+      return <React.Fragment />;
+    }
+  }
+}
+
+class ConnectionData {
+  constructor(service) {
+    this.service = service;
+    this.fields = [
+      {
+        title: "Host",
+        field: "name"
+      },
+      { title: "Container", field: "container" },
+      { title: "Role", field: "role" },
+      { title: "Dir", field: "dir" },
+      { title: "Security", field: "security" },
+      {
+        title: "Authentication",
+        field: "authentication"
+      },
+      {
+        title: "",
+        noSort: true,
+        formatter: ConnectionClose
+      }
+    ];
+    this.detailEntity = "connection";
+    this.detailName = "Connection";
+  }
+
+  fetchRecord = (currentRecord, schema) => {
+    return new Promise(resolve => {
+      this.service.management.topology.fetchEntities(
+        currentRecord.nodeId,
+        [{ entity: "connection" }],
+        data => {
+          const record = data[currentRecord.nodeId]["connection"];
+          const identityIndex = record.attributeNames.indexOf("identity");
+          const result = record.results.find(
+            r => r[identityIndex] === currentRecord.identity
+          );
+          let connection = this.service.utilities.flatten(
+            record.attributeNames,
+            result
+          );
+          connection = this.service.utilities.formatAttributes(
+            connection,
+            schema.entityTypes["connection"]
+          );
+          resolve(connection);
+        }
+      );
+    });
+  };
+
+  doFetch = (page, perPage) => {
+    return new Promise(resolve => {
+      this.service.management.topology.fetchAllEntities(
+        { entity: "connection" },
+        nodes => {
+          // we have all the data now in the nodes object
+          let connectionFields = [];
+          for (let node in nodes) {
+            const response = nodes[node]["connection"];
+            for (let i = 0; i < response.results.length; i++) {
+              const result = response.results[i];
+              const connection = this.service.utilities.flatten(
+                response.attributeNames,
+                result
+              );
+              let auth = "no_auth";
+              let sasl = connection.sasl;
+              if (connection.isAuthenticated) {
+                auth = sasl;
+                if (sasl === "ANONYMOUS") auth = "anonymous-user";
+                else {
+                  if (sasl === "GSSAPI") sasl = "Kerberos";
+                  if (sasl === "EXTERNAL") sasl = "x.509";
+                  auth = connection.user + "(" + connection.sslCipher + ")";
+                }
+              }
+
+              let sec = "no-security";
+              if (connection.isEncrypted) {
+                if (sasl === "GSSAPI") sec = "Kerberos";
+                else
+                  sec = connection.sslProto + "(" + connection.sslCipher + ")";
+              }
+
+              let host = connection.host;
+              let connField = {
+                host: host,
+                security: sec,
+                authentication: auth,
+                nodeId: node,
+                uid: host + connection.container + connection.identity,
+                identity: connection.identity
+              };
+              response.attributeNames.forEach(function(attribute, i) {
+                connField[attribute] = result[i];
+              });
+              connectionFields.push(connField);
+            }
+          }
+          resolve({ data: connectionFields, page, perPage });
+        }
+      );
+    });
+  };
+}
+
+export default ConnectionData;
diff --git a/console/react/src/overview/linksTable.js b/console/react/src/overview/dataSources/linkData.js
similarity index 61%
rename from console/react/src/overview/linksTable.js
rename to console/react/src/overview/dataSources/linkData.js
index d528bc3..5b2bf50 100644
--- a/console/react/src/overview/linksTable.js
+++ b/console/react/src/overview/dataSources/linkData.js
@@ -17,46 +17,90 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import { sortable } from "@patternfly/react-table";
-import OverviewTableBase from "./overviewTableBase";
+import React from "react";
 
-class LinksTable extends OverviewTableBase {
-  constructor(props) {
-    super(props);
+const LinkDir = ({ value }) => (
+  <span>
+    <i
+      className={`link-dir-${value} fa fa-arrow-circle-${
+        value === "in" ? "right" : "left"
+      }`}
+    ></i>
+    {value}
+  </span>
+);
+
+class LinkData {
+  constructor(service) {
+    this.service = service;
     this.fields = [
-      { title: "Link", field: "name" },
-      { title: "Type", field: "linkType", transforms: [sortable] },
-      { title: "Dir", field: "linkDir" },
+      { title: "Link", field: "link", noWrap: true },
+      { title: "Type", field: "linkType", noWrap: true },
+      { title: "Dir", field: "linkDir", formatter: LinkDir },
       { title: "Admin status", field: "adminStatus" },
       { title: "Oper status", field: "operStatus" },
-      { title: "Deliveries", field: "deliveryCount" },
-      { title: "Rate", field: "rate" },
-      { title: "Delayed 1 sec", field: "delayed1Sec" },
-      { title: "Delayed 10 secs", field: "delayed10Sec" },
-      { title: "Outstanding", field: "outstanding" },
-      { title: "Address", field: "owningAddr", transforms: [sortable] }
+      {
+        title: "Deliveries",
+        field: "deliveryCount",
+        noWrap: true,
+        numeric: true
+      },
+      { title: "Rate", field: "rate", numeric: true },
+      {
+        title: "Delayed 1 sec",
+        field: "delayed1Sec",
+        numeric: true
+      },
+      {
+        title: "Delayed 10 secs",
+        field: "delayed10Sec",
+        numeric: true
+      },
+      {
+        title: "Outstanding",
+        field: "outstanding",
+        numeric: true
+      },
+      { title: "Address", field: "owningAddr" }
     ];
+    this.detailEntity = "router.link";
+    this.detailName = "Link";
   }
+
+  fetchRecord = (currentRecord, schema) => {
+    return new Promise(resolve => {
+      this.service.management.topology.fetchEntities(
+        currentRecord.nodeId,
+        [{ entity: "router.link" }],
+        data => {
+          const record = data[currentRecord.nodeId]["router.link"];
+          const identityIndex = record.attributeNames.indexOf("identity");
+          const result = record.results.find(
+            r => r[identityIndex] === currentRecord.identity
+          );
+          let link = this.service.utilities.flatten(
+            record.attributeNames,
+            result
+          );
+          link = this.service.utilities.formatAttributes(
+            link,
+            schema.entityTypes["router.link"]
+          );
+          resolve(link);
+        }
+      );
+    });
+  };
+
   doFetch = (page, perPage) => {
     return new Promise(resolve => {
-      this.props.service.management.topology.fetchAllEntities(
+      this.service.management.topology.fetchAllEntities(
         { entity: "router.link" },
         nodes => {
           // we have all the data now in the nodes object
           let linkFields = [];
-          const now = new Date();
-          var prettyVal = value => {
-            return typeof value === "undefined"
-              ? "-"
-              : this.props.service.utilities.pretty(value);
-          };
-          var uncounts = link => {
-            return this.props.service.utilities.pretty(
-              link.undeliveredCount + link.unsettledCount
-            );
-          };
           var getLinkName = (node, link) => {
-            let namestr = this.props.service.utilities.nameFromId(node);
+            let namestr = this.service.utilities.nameFromId(node);
             return `${namestr}:${link.identity}`;
           };
           var fixAddress = link => {
@@ -100,7 +144,7 @@ class LinksTable extends OverviewTableBase {
             const response = nodes[node]["router.link"];
             for (let i = 0; i < response.results.length; i++) {
               const result = response.results[i];
-              const link = this.props.service.utilities.flatten(
+              const link = this.service.utilities.flatten(
                 response.attributeNames,
                 result
               );
@@ -109,48 +153,37 @@ class LinksTable extends OverviewTableBase {
 
               linkFields.push({
                 link: linkName,
-                title: linkName,
-                outstanding: uncounts(link),
-                operStatus: link.operStatus,
+                linkType: link.linkType,
+                linkDir: link.linkDir,
                 adminStatus: link.adminStatus,
+                operStatus: link.operStatus,
+                deliveryCount: link.deliveryCount,
+                rate: link.settleRate,
+                delayed1Sec: link.deliveriesDelayed1Sec,
+                delayed10Sec: link.deliveriesDelayed10Sec,
+                outstanding: link.undeliveredCount + link.unsettledCount,
                 owningAddr: addresses[0],
 
-                acceptedCount: prettyVal(link.acceptedCount),
-                modifiedCount: prettyVal(link.modifiedCount),
-                presettledCount: prettyVal(link.presettledCount),
-                rejectedCount: prettyVal(link.rejectedCount),
-                releasedCount: prettyVal(link.releasedCount),
-                deliveryCount: prettyVal(link.deliveryCount),
-
-                rate: prettyVal(link.settleRate),
-                deliveriesDelayed10Sec: prettyVal(link.deliveriesDelayed10Sec),
-                deliveriesDelayed1Sec: prettyVal(link.deliveriesDelayed1Sec),
                 capacity: link.capacity,
                 undeliveredCount: link.undeliveredCount,
                 unsettledCount: link.unsettledCount,
 
                 rawAddress: addresses[1],
-                rawDeliveryCount: link.deliveryCount,
                 name: link.name,
-                linkName: link.linkName,
                 connectionId: link.connectionId,
-                linkDir: link.linkDir,
-                linkType: link.linkType,
                 peer: link.peer,
                 type: link.type,
 
-                uid: linkName,
-                timestamp: now,
                 nodeId: node,
                 identity: link.identity
               });
             }
           }
-          resolve(this.slice(linkFields, page, perPage));
+          resolve({ data: linkFields, page, perPage });
         }
       );
     });
   };
 }
 
-export default LinksTable;
+export default LinkData;
diff --git a/console/react/src/overview/routersTable.js b/console/react/src/overview/dataSources/routerData.js
similarity index 54%
rename from console/react/src/overview/routersTable.js
rename to console/react/src/overview/dataSources/routerData.js
index b846c31..67c015e 100644
--- a/console/react/src/overview/routersTable.js
+++ b/console/react/src/overview/dataSources/routerData.js
@@ -17,24 +17,57 @@ specific language governing permissions and limitations
 under the License.
 */
 
-import { sortable } from "@patternfly/react-table";
-import OverviewTableBase from "./overviewTableBase";
-
-class RoutersTable extends OverviewTableBase {
-  constructor(props) {
-    super(props);
+class RouterData {
+  constructor(service) {
+    this.service = service;
     this.fields = [
-      { title: "Router", field: "name", transforms: [sortable] },
+      { title: "Router", field: "name" },
       { title: "Area", field: "area" },
       { title: "Mode", field: "mode" },
-      { title: "Addresses", field: "addrCount" },
-      { title: "Links", field: "linkCount" },
-      { title: "External connections", field: "connections" }
+      {
+        title: "Addresses",
+        field: "addrCount",
+        numeric: true
+      },
+      {
+        title: "Links",
+        field: "linkCount",
+        numeric: true
+      },
+      {
+        title: "External connections",
+        field: "connections",
+        numeric: true
+      }
     ];
+    this.detailEntity = "router";
+    this.detailName = "Router";
   }
+
+  fetchRecord = (currentRecord, schema) => {
+    return new Promise(resolve => {
+      this.service.management.topology.fetchEntities(
+        currentRecord.nodeId,
+        [{ entity: "router" }],
+        results => {
+          const record = results[currentRecord.nodeId].router;
+          let router = this.service.utilities.flatten(
+            record.attributeNames,
+            record.results[0]
+          );
+          router = this.service.utilities.formatAttributes(
+            router,
+            schema.entityTypes.router
+          );
+          resolve(router);
+        }
+      );
+    });
+  };
+
   doFetch = (page, perPage) => {
     return new Promise(resolve => {
-      this.props.service.management.topology.fetchAllEntities(
+      this.service.management.topology.fetchAllEntities(
         [{ entity: "connection", attrs: ["role"] }, { entity: "router" }],
         nodes => {
           // we have all the data now in the nodes object
@@ -42,26 +75,27 @@ class RoutersTable extends OverviewTableBase {
           for (let node in nodes) {
             let connections = 0;
             for (let i = 0; i < nodes[node]["connection"].results.length; ++i) {
-              // we only requested "role" so it will be at [0]
+              // we only requested "role" so it will be at results[0]
               if (nodes[node]["connection"].results[i][0] !== "inter-router")
                 ++connections;
             }
             let routerRow = {
-              connections: connections,
+              connections,
               nodeId: node,
-              id: this.props.service.utilities.nameFromId(node)
+              id: this.service.utilities.nameFromId(node)
             };
             nodes[node]["router"].attributeNames.forEach((routerAttr, i) => {
-              if (routerAttr !== "routerId" && routerAttr !== "id")
+              if (routerAttr !== "id") {
                 routerRow[routerAttr] = nodes[node]["router"].results[0][i];
+              }
             });
             allRouterFields.push(routerRow);
           }
-          resolve(this.slice(allRouterFields, page, perPage));
+          resolve({ data: allRouterFields, page, perPage });
         }
       );
     });
   };
 }
 
-export default RoutersTable;
+export default RouterData;
diff --git a/console/react/src/overview/detailsTablePage.js b/console/react/src/overview/detailsTablePage.js
new file mode 100644
index 0000000..d945455
--- /dev/null
+++ b/console/react/src/overview/detailsTablePage.js
@@ -0,0 +1,218 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+  Stack,
+  StackItem,
+  TextContent,
+  Text,
+  TextVariants,
+  Breadcrumb,
+  BreadcrumbItem
+} from "@patternfly/react-core";
+
+import {
+  cellWidth,
+  Table,
+  TableHeader,
+  TableBody,
+  TableVariant
+} from "@patternfly/react-table";
+import { Card, CardBody } from "@patternfly/react-core";
+import { Redirect } from "react-router-dom";
+import { dataMap } from "./entityData";
+
+class DetailTablesPage extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      columns: [
+        { title: "Attribute", transforms: [cellWidth(20)] },
+        {
+          title: "Value",
+          transforms: [cellWidth("max")],
+          props: { className: "pf-u-text-align-left" }
+        }
+      ],
+      rows: [],
+      redirect: false,
+      redirectState: { page: 1 },
+      redirectPath: "/dashboard",
+      lastUpdated: new Date()
+    };
+    // if we get to this page and we don't have a props.location.state.entity
+    // then redirect back to the dashboard.
+    // this can happen if we get here from a bookmark or browser refresh
+    this.entity =
+      this.props &&
+      this.props.location &&
+      this.props.location.state &&
+      this.props.location.state.entity;
+    if (!dataMap[this.entity]) {
+      this.state.redirect = true;
+    } else {
+      this.dataSource = new dataMap[this.entity](this.props.service);
+    }
+  }
+
+  componentDidMount = () => {
+    this.props.service.management.getSchema().then(schema => {
+      this.schema = schema;
+      this.timer = setInterval(this.update, 5000);
+      this.update();
+    });
+  };
+
+  componentWillUnmount = () => {
+    if (this.timer) {
+      clearInterval(this.timer);
+    }
+  };
+
+  update = () => {
+    this.mapRows().then(
+      rows => {
+        this.setState({ rows, lastUpdated: new Date() });
+      },
+      error => {
+        console.log(`detailsTablePage: ${error}`);
+      }
+    );
+  };
+
+  toString = val => {
+    return val === null ? "" : String(val);
+  };
+
+  mapRows = () => {
+    return new Promise((resolve, reject) => {
+      const rows = [];
+      if (!this.dataSource) {
+        reject("no data source");
+      }
+      this.dataSource
+        .fetchRecord(this.props.location.state.currentRecord, this.schema)
+        .then(data => {
+          for (const attribute in data) {
+            if (
+              !this.dataSource.hideFields ||
+              this.dataSource.hideFields.indexOf(attribute) === -1
+            ) {
+              rows.push({
+                cells: [attribute, this.toString(data[attribute])]
+              });
+            }
+          }
+          resolve(rows);
+        });
+    });
+  };
+
+  icap = s => s.charAt(0).toUpperCase() + s.slice(1);
+
+  parentItem = () => {
+    // if we have a specific field that should be used
+    // as the record's title, return it
+    if (this.dataSource.detailField) {
+      return this.props.location.state.currentRecord[
+        this.dataSource.detailField
+      ];
+    }
+    // otherwise return the 1st field
+    return this.props.location.state.value;
+  };
+
+  breadcrumbSelected = () => {
+    this.setState({
+      redirect: true,
+      redirectPath: `/overview/${this.entity}`,
+      redirectState: this.props.location.state
+    });
+  };
+
+  render() {
+    if (this.state.redirect) {
+      return (
+        <Redirect
+          to={{
+            pathname: this.state.redirectPath,
+            state: this.state.redirectState
+          }}
+        />
+      );
+    }
+
+    return (
+      <React.Fragment>
+        <PageSection
+          variant={PageSectionVariants.light}
+          className="overview-table-page"
+        >
+          <Stack>
+            <StackItem className="overview-header details">
+              <Breadcrumb>
+                <BreadcrumbItem
+                  className="link-button"
+                  onClick={() =>
+                    this.breadcrumbSelected(`/overview/${this.entity}`)
+                  }
+                >
+                  {this.icap(this.entity)}
+                </BreadcrumbItem>
+                <BreadcrumbItem isActive>{this.parentItem()}</BreadcrumbItem>
+              </Breadcrumb>
+
+              <TextContent>
+                <Text className="overview-title" component={TextVariants.h1}>
+                  {`${
+                    this.dataSource.detailName
+                  } ${this.parentItem()} attributes`}
+                </Text>
+                <Text className="overview-loading" component={TextVariants.pre}>
+                  {`Updated ${this.props.service.utilities.strDate(
+                    this.state.lastUpdated
+                  )}`}
+                </Text>
+              </TextContent>
+            </StackItem>
+            <StackItem className="overview-table">
+              <Card>
+                <CardBody>
+                  <Table
+                    cells={this.state.columns}
+                    rows={this.state.rows}
+                    variant={TableVariant.compact}
+                    aria-label={this.entity}
+                  >
+                    <TableHeader />
+                    <TableBody />
+                  </Table>
+                </CardBody>
+              </Card>
+            </StackItem>
+          </Stack>
+        </PageSection>
+      </React.Fragment>
+    );
+  }
+}
+
+export default DetailTablesPage;
diff --git a/console/react/src/overview/entityData.js b/console/react/src/overview/entityData.js
new file mode 100644
index 0000000..f625328
--- /dev/null
+++ b/console/react/src/overview/entityData.js
@@ -0,0 +1,32 @@
+/*
+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 RouterData from "./dataSources/routerData";
+import AddressData from "./dataSources/addressData";
+import LinkData from "./dataSources/linkData";
+import ConnectionData from "./dataSources/connectionData";
+
+const dataMap = {
+  routers: RouterData,
+  connections: ConnectionData,
+  links: LinkData,
+  addresses: AddressData
+};
+
+export { RouterData, AddressData, LinkData, ConnectionData, dataMap };
diff --git a/console/react/src/overview/overviewTable.js b/console/react/src/overview/overviewTable.js
new file mode 100644
index 0000000..2d77f20
--- /dev/null
+++ b/console/react/src/overview/overviewTable.js
@@ -0,0 +1,318 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import {
+  sortable,
+  SortByDirection,
+  Table,
+  TableHeader,
+  TableBody,
+  TableVariant
+} from "@patternfly/react-table";
+import { Button, Pagination } from "@patternfly/react-core";
+import { Redirect } from "react-router-dom";
+import TableToolbar from "./tableToolbar";
+import { dataMap } from "./entityData";
+
+// If the breadcrumb on the details page was used to return to this page,
+// we will have saved state info in props.location.state
+const propFromLocation = (props, which, defaultValue) =>
+  props &&
+  props.location &&
+  props.location.state &&
+  typeof props.location.state[which] !== "undefined"
+    ? props.location.state[which]
+    : defaultValue;
+
+class OverviewTable extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      sortBy: propFromLocation(props, "sortBy", {
+        index: 0,
+        direction: SortByDirection.asc
+      }),
+      filterBy: propFromLocation(props, "filterBy", {}),
+      perPage: propFromLocation(props, "perPage", 10),
+      total: 1,
+      page: propFromLocation(props, "page", 1),
+      columns: [],
+      allRows: [],
+      rows: [],
+      redirect: false,
+      redirectState: {}
+    };
+    this.entity = this.props.service.utilities.entityFromProps(props);
+    if (!dataMap[this.entity]) {
+      this.state.redirect = true;
+    } else {
+      this.dataSource = new dataMap[this.entity](this.props.service);
+    }
+  }
+
+  componentDidMount = () => {
+    this.mounted = true;
+    if (!this.dataSource) return;
+    // initialize the columns and get the data
+    this.dataSource.fields.forEach(f => {
+      if (!f.noSort) f.transforms = [sortable];
+      f.cellFormatters = [];
+      if (f.numeric) {
+        f.cellFormatters.push(this.prettier);
+      }
+      if (f.noWrap) {
+        f.cellFormatters.push(this.noWrap);
+      }
+      if (f.formatter) {
+        f.cellFormatters.push((value, extraInfo) =>
+          this.formatter(f.formatter, value, extraInfo)
+        );
+      }
+    });
+    if (!this.dataSource.fields[0].cellFormatters)
+      this.dataSource.fields[0].cellFormatters = [];
+    this.dataSource.fields[0].cellFormatters.push(this.detailLink);
+
+    this.setState({ columns: this.dataSource.fields }, () => {
+      this.update();
+      this.timer = setInterval(this.update, 5000);
+    });
+  };
+
+  componentWillUnmount = () => {
+    this.mounted = false;
+    clearInterval(this.timer);
+  };
+
+  update = () => {
+    this.fetch(this.state.page, this.state.perPage);
+  };
+
+  fetch = (page, perPage) => {
+    // get the data. Note: The current page number might change if
+    // the number of rows is less than before
+    this.dataSource.doFetch(page, perPage).then(results => {
+      const sliced = this.slice(results.data, results.page, results.perPage);
+      // if fetch was called and the component was unmounted before
+      // the results arrived, don't call setState
+      if (!this.mounted) return;
+      const { rows, page, total, allRows } = sliced;
+      this.setState({
+        rows,
+        page,
+        perPage,
+        total,
+        allRows
+      });
+      this.props.lastUpdated(new Date());
+    });
+  };
+
+  detailLink = (value, extraInfo) => {
+    return (
+      <Button
+        className="link-button"
+        onClick={() => this.detailClick(value, extraInfo)}
+      >
+        {value}
+      </Button>
+    );
+  };
+
+  noWrap = (value, extraInfo) => {
+    return <span className="noWrap">{value}</span>;
+  };
+
+  prettier = (value, extraInfo) => {
+    return typeof value === "undefined"
+      ? "-"
+      : this.props.service.utilities.pretty(value);
+  };
+
+  formatter = (Component, value, extraInfo) => {
+    return (
+      <Component
+        value={value}
+        extraInfo={extraInfo}
+        service={this.props.service}
+      />
+    );
+  };
+
+  detailClick = (value, extraInfo) => {
+    this.setState({
+      redirect: true,
+      redirectState: {
+        value: extraInfo.rowData.cells[0],
+        currentRecord: extraInfo.rowData.data,
+        entity: this.entity,
+        page: this.state.page,
+        sortBy: this.state.sortBy,
+        filterBy: this.state.filterBy,
+        perPage: this.state.perPage
+      }
+    });
+  };
+
+  onSort = (_event, index, direction) => {
+    this.setState({ sortBy: { index, direction } }, () => {
+      const { allRows, page, perPage } = this.state;
+      let rows = this.filter(allRows);
+      rows = this.sort(rows);
+      rows = this.page(rows, rows.length, page, perPage);
+      this.setState({ rows });
+    });
+  };
+
+  renderPagination(variant = "top") {
+    const { page, perPage, total } = this.state;
+    return (
+      <Pagination
+        itemCount={total}
+        page={page}
+        perPage={perPage}
+        onSetPage={(_evt, value) => this.onSetPage(value)}
+        onPerPageSelect={(_evt, value) => this.onPerPageSelect(value)}
+        variant={variant}
+      />
+    );
+  }
+
+  onSetPage = value => {
+    this.fetch(value, this.state.perPage);
+  };
+  onPerPageSelect = value => {
+    this.fetch(1, value);
+  };
+  handleChangeFilterValue = (field, value) => {
+    this.setState({ filterBy: { field, value } }, this.update);
+  };
+
+  field2Row = field => ({
+    cells: this.dataSource.fields.map(f => field[f.field]),
+    data: field
+  });
+
+  cellIndex = field => {
+    return this.dataSource.fields.findIndex(f => {
+      return f.title === field;
+    });
+  };
+
+  filter = rows => {
+    const filterField = this.state.filterBy.field;
+    const filterValue = this.state.filterBy.value;
+    if (
+      typeof filterField !== "undefined" &&
+      typeof filterValue !== "undefined" &&
+      filterValue !== ""
+    ) {
+      const cellIndex = this.cellIndex(filterField);
+      rows = rows.filter(r => {
+        return r.cells[cellIndex].includes(filterValue);
+      });
+    }
+    return rows;
+  };
+
+  page = (rows, total, page, perPage) => {
+    const newPages = Math.ceil(total / perPage);
+    page = Math.min(page, newPages);
+    const start = perPage * (page - 1);
+    const end = Math.min(start + perPage, rows.length);
+    return rows.slice(start, end);
+  };
+
+  slice = (fields, page, perPage) => {
+    let allRows = fields.map(f => this.field2Row(f));
+    let rows = this.filter(allRows);
+    const total = rows.length;
+    rows = this.sort(rows);
+    rows = this.page(rows, total, page, perPage);
+    return { rows, page, total, allRows };
+  };
+
+  sort = rows => {
+    const { index, direction } = this.state.sortBy;
+    if (typeof index === "undefined" || typeof direction === "undefined") {
+      return rows;
+    }
+
+    if (this.dataSource.fields[index].numeric) {
+      rows.sort((a, b) => {
+        if (direction === SortByDirection.desc)
+          return a > b ? -1 : a < b ? 1 : 0;
+        return a < b ? -1 : a > b ? 1 : 0;
+      });
+    } else {
+      rows.sort((a, b) => {
+        return a.cells[index] < b.cells[index]
+          ? -1
+          : a.cells[index] > b.cells[index]
+          ? 1
+          : 0;
+      });
+      if (direction === SortByDirection.desc) {
+        rows = rows.reverse();
+      }
+    }
+    return rows;
+  };
+
+  render() {
+    if (this.state.redirect) {
+      return (
+        <Redirect
+          to={{
+            pathname: "/details",
+            state: this.state.redirectState
+          }}
+        />
+      );
+    }
+    return (
+      <React.Fragment>
+        <TableToolbar
+          total={this.state.total}
+          page={this.state.page}
+          perPage={this.state.perPage}
+          onSetPage={this.onSetPage}
+          onPerPageSelect={this.onPerPageSelect}
+          fields={this.dataSource.fields}
+          handleChangeFilterValue={this.handleChangeFilterValue}
+        />
+        <Table
+          cells={this.state.columns}
+          rows={this.state.rows}
+          aria-label={this.entity}
+          sortBy={this.state.sortBy}
+          onSort={this.onSort}
+          variant={TableVariant.compact}
+        >
+          <TableHeader />
+          <TableBody />
+        </Table>
+        {this.renderPagination("bottom")}
+      </React.Fragment>
+    );
+  }
+}
+
+export default OverviewTable;
diff --git a/console/react/src/overview/overviewTableBase.js b/console/react/src/overview/overviewTableBase.js
deleted file mode 100644
index 54625ce..0000000
--- a/console/react/src/overview/overviewTableBase.js
+++ /dev/null
@@ -1,213 +0,0 @@
-import React from "react";
-import {
-  SortByDirection,
-  Table,
-  TableHeader,
-  TableBody
-} from "@patternfly/react-table";
-import { Pagination, Title } from "@patternfly/react-core";
-
-import TableToolbar from "./tableToolbar";
-
-class OverviewTableBase extends React.Component {
-  constructor(props) {
-    super(props);
-    this.state = {
-      sortBy: {},
-      filterBy: {},
-      perPage: 10,
-      total: 1,
-      page: 1,
-      loading: true,
-      columns: [],
-      allRows: [],
-      rows: [
-        {
-          cells: ["QDR.A", "0", "interior", "1", "2", "3"]
-        },
-        {
-          cells: [
-            {
-              title: <div>QDR.B</div>,
-              props: { title: "hover title", colSpan: 3 }
-            },
-            "2",
-            "3",
-            "4"
-          ]
-        },
-        {
-          cells: [
-            "QDR.C",
-            "0",
-            "interior",
-            "3",
-            {
-              title: "four",
-              props: { textCenter: false }
-            },
-            "5"
-          ]
-        }
-      ]
-    };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    console.log("overviewTable componentDidMount");
-    // initialize the columns and get the data
-    this.setState({ columns: this.fields }, () => {
-      this.update();
-      this.timer = setInterval(this.upate, 5000);
-    });
-  }
-
-  componentWillUnmount = () => {
-    this.mounted = false;
-    clearInterval(this.timer);
-  };
-
-  update = () => {
-    this.fetch(this.state.page, this.state.perPage);
-  };
-
-  fetch = (page, perPage) => {
-    this.setState({ loading: true });
-    // doFetch is defined in the derived class
-    this.doFetch(page, perPage).then(sliced => {
-      // if fetch was called and the component was unmounted before
-      // the results arrived, don't call setState
-      if (!this.mounted) return;
-      const { rows, page, total, allRows } = sliced;
-      this.setState({
-        rows,
-        loading: false,
-        page,
-        perPage,
-        total,
-        allRows
-      });
-    });
-  };
-
-  onSort = (_event, index, direction) => {
-    const rows = this.sort(this.state.allRows, index, direction);
-    this.setState({ rows, page: 1, sortBy: { index, direction } });
-  };
-
-  renderPagination(variant = "top") {
-    const { page, perPage, total } = this.state;
-    return (
-      <Pagination
-        itemCount={total}
-        page={page}
-        perPage={perPage}
-        onSetPage={(_evt, value) => this.onSetPage(value)}
-        onPerPageSelect={(_evt, value) => this.onPerPageSelect(value)}
-        variant={variant}
-      />
-    );
-  }
-
-  onSetPage = value => {
-    this.fetch(value, this.state.perPage);
-  };
-  onPerPageSelect = value => {
-    this.fetch(1, value);
-  };
-  handleChangeFilterValue = (field, value) => {
-    this.setState({ filterBy: { field, value } }, this.update);
-    console.log(`handleChangeFilterValue(${field}, ${value})`);
-  };
-
-  field2Row = field => ({
-    cells: this.fields.map(f => field[f.field])
-  });
-
-  cellIndex = field => {
-    return this.fields.findIndex(f => {
-      return f.title === field;
-    });
-  };
-  slice = (fields, page, perPage) => {
-    const filterField = this.state.filterBy.field;
-    const filterValue = this.state.filterBy.value;
-    let rows = fields.map(f => this.field2Row(f));
-    if (
-      typeof filterField !== "undefined" &&
-      typeof filterValue !== "undefined" &&
-      filterValue !== ""
-    ) {
-      const cellIndex = this.cellIndex(filterField);
-      rows = rows.filter(r => {
-        return r.cells[cellIndex].includes(filterValue);
-      });
-    }
-    rows = this.sort(rows);
-    const total = rows.length;
-    const newPages = Math.ceil(total / perPage);
-    page = Math.min(page, newPages);
-    const start = perPage * (page - 1);
-    const end = Math.min(start + perPage, rows.length);
-    const slicedRows = rows.slice(start, end);
-    return { rows: slicedRows, page, total, allRows: rows };
-  };
-
-  sort = rows => {
-    if (
-      typeof this.state.index === "undefined" ||
-      typeof this.state.direction === "undefined"
-    ) {
-      return rows;
-    }
-    rows.sort((a, b) =>
-      a.cells[this.sate.index] < b.cells[this.sate.index]
-        ? -1
-        : a.cells[this.sate.index] > b.cells[this.sate.index]
-        ? 1
-        : 0
-    );
-    if (this.sate.direction === SortByDirection.desc) {
-      rows = rows.reverse();
-    }
-    return rows;
-  };
-
-  render() {
-    const { loading } = this.state;
-    return (
-      <React.Fragment>
-        <TableToolbar
-          total={this.state.total}
-          page={this.state.page}
-          perPage={this.state.perPage}
-          onSetPage={this.onSetPage}
-          onPerPageSelect={this.onPerPageSelect}
-          fields={this.fields}
-          handleChangeFilterValue={this.handleChangeFilterValue}
-        />
-        {!loading && (
-          <Table
-            cells={this.state.columns}
-            rows={this.state.rows}
-            aria-label={this.props.entity}
-            sortBy={this.state.sortBy}
-            onSort={this.onSort}
-          >
-            <TableHeader />
-            <TableBody />
-          </Table>
-        )}
-        {this.renderPagination("bottom")}
-        {loading && (
-          <center>
-            <Title size="3xl">Please wait while loading data</Title>
-          </center>
-        )}
-      </React.Fragment>
-    );
-  }
-}
-
-export default OverviewTableBase;
diff --git a/console/react/src/overview/overviewTablePage.js b/console/react/src/overview/overviewTablePage.js
index 92adfd2..8e19a65 100644
--- a/console/react/src/overview/overviewTablePage.js
+++ b/console/react/src/overview/overviewTablePage.js
@@ -1,3 +1,22 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
 import React from "react";
 import { PageSection, PageSectionVariants } from "@patternfly/react-core";
 import {
@@ -9,68 +28,45 @@ import {
 } from "@patternfly/react-core";
 import { Card, CardBody } from "@patternfly/react-core";
 
-import RoutersTable from "./routersTable";
-import AddressesTable from "./addressesTable";
-import LinksTable from "./linksTable";
-import ConnectionsTable from "./connectionsTable";
+import OverviewTable from "./overviewTable";
 
 class OverviewTablePage extends React.Component {
   constructor(props) {
     super(props);
-    this.state = {};
+    this.state = { loading: false, lastUpdated: new Date() };
   }
 
-  whichTable = () => {
-    if (this.props.entity === "routers") {
-      return (
-        <RoutersTable entity={this.props.entity} service={this.props.service} />
-      );
-    }
-    if (this.props.entity === "addresses") {
-      return (
-        <AddressesTable
-          entity={this.props.entity}
-          service={this.props.service}
-        />
-      );
-    }
-    if (this.props.entity === "links") {
-      return (
-        <LinksTable entity={this.props.entity} service={this.props.service} />
-      );
-    }
-    if (this.props.entity === "connections") {
-      return (
-        <ConnectionsTable
-          entity={this.props.entity}
-          service={this.props.service}
-        />
-      );
-    }
+  lastUpdated = lastUpdated => {
+    this.setState({ lastUpdated });
   };
   render() {
     return (
-      <React.Fragment>
-        <PageSection
-          variant={PageSectionVariants.light}
-          className="overview-table-page"
-        >
-          <Stack>
-            <StackItem className="overview-header">
-              <TextContent>
-                <Text className="overview-title" component={TextVariants.h1}>
-                  {this.props.entity}
-                </Text>
-              </TextContent>
-            </StackItem>
-            <StackItem className="overview-table">
-              <Card>
-                <CardBody>{this.whichTable()}</CardBody>
-              </Card>
-            </StackItem>
-          </Stack>
-        </PageSection>
-      </React.Fragment>
+      <PageSection
+        variant={PageSectionVariants.light}
+        className="overview-table-page"
+      >
+        <Stack>
+          <StackItem className="overview-header">
+            <TextContent>
+              <Text className="overview-title" component={TextVariants.h1}>
+                {this.props.service.utilities.entityFromProps(this.props)}
+              </Text>
+              <Text className="overview-loading" component={TextVariants.pre}>
+                {`Updated ${this.props.service.utilities.strDate(
+                  this.state.lastUpdated
+                )}`}
+              </Text>
+            </TextContent>
+          </StackItem>
+          <StackItem className="overview-table">
+            <Card>
+              <CardBody>
+                <OverviewTable {...this.props} lastUpdated={this.lastUpdated} />
+              </CardBody>
+            </Card>
+          </StackItem>
+        </Stack>
+      </PageSection>
     );
   }
 }
diff --git a/console/react/src/overview/tableToolbar.jsx b/console/react/src/overview/tableToolbar.jsx
index 79154d0..e0fe006 100644
--- a/console/react/src/overview/tableToolbar.jsx
+++ b/console/react/src/overview/tableToolbar.jsx
@@ -1,3 +1,22 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
 import React from "react";
 import {
   Dropdown,
diff --git a/console/react/src/qdrService.js b/console/react/src/qdrService.js
index 55a6cc9..1d3fbd9 100644
--- a/console/react/src/qdrService.js
+++ b/console/react/src/qdrService.js
@@ -52,8 +52,9 @@ export class QDRService {
             self.onDisconnect.bind(self)
           );
 
-          self.management.getSchema().then(() => {
-            //console.log("got schema after connection");
+          self.management.getSchema().then(schema => {
+            console.log("got schema after connection");
+            console.log(schema);
             self.management.topology.setUpdateEntities([]);
             //console.log("requesting a topology");
             self.management.topology


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