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

[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Progress on details pages

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 679c5f4  Progress on details pages
679c5f4 is described below

commit 679c5f418809e029ac99760f5b2093a36d615d0a
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Sat Nov 2 10:36:31 2019 -0400

    Progress on details pages
---
 console/react/src/App.css                          |  25 ++-
 .../react/src/details/dataSources/addressData.js   |   1 +
 .../src/details/dataSources/connectionData.js      | 130 --------------
 .../react/src/details/dataSources/defaultData.js   |  32 +++-
 console/react/src/details/dataSources/linkData.js  | 196 ++++-----------------
 .../{addressData.js => listenerData.js}            |  37 ++--
 console/react/src/details/enitiesPage.js           |  79 +++++++--
 console/react/src/details/entityData.js            |  10 +-
 console/react/src/details/entityListTable.js       |  92 ++++++----
 .../react/src/{overview => }/detailsTablePage.js   |  98 ++++++-----
 console/react/src/layout.js                        |  10 +-
 console/react/src/overview/overviewTable.js        |   3 +-
 console/react/src/qdrService.js                    |   2 +
 console/react/src/tableToolbar.jsx                 |  51 ++++--
 14 files changed, 343 insertions(+), 423 deletions(-)

diff --git a/console/react/src/App.css b/console/react/src/App.css
index 386930b..758ca39 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -2,9 +2,12 @@
   height: 100vh;
 }
 
+/*
 #main-content-page-layout-manual-nav {
   overflow-y: hidden;
 }
+*/
+
 .App {
   text-align: center;
   height: 100%;
@@ -1107,6 +1110,7 @@ div.details-table ul.entities-list {
 .entities-list li {
   padding: 0.25em 2em;
   border-left: 2px solid white;
+  margin-top: 8px;
 }
 .entities-list li:hover {
   background-color: #eaeaea;
@@ -1117,10 +1121,12 @@ div.details-table ul.entities-list {
   background-color: #eaeaea;
 }
 
+/*
 #entityList {
   max-height: calc(100vh - 140px);
   padding-top: 0.5em;
 }
+  */
 
 .split-left {
   text-align: left;
@@ -1135,7 +1141,7 @@ span.entity-type i {
   padding-right: 1em;
   font-style: normal;
 }
-span.entity-type i:before {
+span.entity-type i.address-local:before {
   font-family: FontAwesome;
   content: "\f0ac";
 }
@@ -1148,8 +1154,25 @@ span.entity-type i.address-router:before {
   content: "\f047";
 }
 
+span.entity-type i.link-type-endpoint:before {
+  content: "\f109";
+}
+span.entity-type i.link-type-inter-router:before {
+  content: "\f07e";
+}
+span.entity-type i.link-type-router-control:before {
+  content: "\f013";
+}
+
 .details-table .pf-l-toolbar {
   margin: 0 !important;
   padding: 1em 1em;
   border-bottom: 1px solid #eaeaea;
 }
+
+.details-table table {
+  margin-left: 0.25em;
+}
+.details-header {
+  border-bottom: 1px solid #eaeaea;
+}
diff --git a/console/react/src/details/dataSources/addressData.js b/console/react/src/details/dataSources/addressData.js
index 528c286..13c7e97 100644
--- a/console/react/src/details/dataSources/addressData.js
+++ b/console/react/src/details/dataSources/addressData.js
@@ -38,6 +38,7 @@ class AddressData extends DefaultData {
   constructor(service, schema) {
     super(service, schema);
     this.typeFormatter = AddressType;
+    this.detailName = "router.address";
   }
 }
 
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/dataSources/connectionData.js
deleted file mode 100644
index b126e31..0000000
--- a/console/react/src/details/dataSources/connectionData.js
+++ /dev/null
@@ -1,130 +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.
-*/
-
-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
-      }
-    ];
-    this.detailEntity = "connection";
-    this.detailName = "Connection";
-  }
-
-  hasType = () => {
-    return true;
-  };
-
-  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/details/dataSources/defaultData.js b/console/react/src/details/dataSources/defaultData.js
index aa7ed8e..0ed5235 100644
--- a/console/react/src/details/dataSources/defaultData.js
+++ b/console/react/src/details/dataSources/defaultData.js
@@ -31,23 +31,47 @@ class DefaultData {
 
   actions = entity => {
     return this.schema.entityTypes[entity].operations.filter(
-      action => action !== "READ"
+      action => action !== "READ" && action !== "UPDATE"
     );
   };
 
-  fetchRecord = (currentRecord, schema) => {
+  // called by detailsTablePage to display a single record
+  fetchRecord = (currentRecord, schema, entity) => {
     return new Promise(resolve => {
-      resolve(null);
+      this.service.management.topology.fetchEntities(
+        currentRecord.routerId,
+        [{ entity: entity }],
+        data => {
+          const record = data[currentRecord.routerId][entity];
+          const identityIndex = record.attributeNames.indexOf("identity");
+          const result = record.results.find(
+            r => r[identityIndex] === currentRecord.identity
+          );
+          let object = this.service.utilities.flatten(
+            record.attributeNames,
+            result
+          );
+          object = this.service.utilities.formatAttributes(
+            object,
+            schema.entityTypes[entity]
+          );
+          resolve(object);
+        }
+      );
     });
   };
 
+  // called by entityListTable to get the list of records
   doFetch = (page, perPage, routerId, entity) => {
     return new Promise(resolve => {
       this.service.management.topology.fetchEntities(
         routerId,
         { entity },
         results => {
-          const data = utils.flattenAll(results[routerId][entity]);
+          const data = utils.flattenAll(results[routerId][entity], f => {
+            f.routerId = routerId;
+            return f;
+          });
           resolve({ data, page, perPage });
         }
       );
diff --git a/console/react/src/details/dataSources/linkData.js b/console/react/src/details/dataSources/linkData.js
index 89ee4bc..47bee54 100644
--- a/console/react/src/details/dataSources/linkData.js
+++ b/console/react/src/details/dataSources/linkData.js
@@ -18,175 +18,41 @@ under the License.
 */
 
 import React from "react";
-
-const LinkDir = ({ value }) => (
-  <span>
-    <i
-      className={`link-dir-${value} fa fa-arrow-circle-${
-        value === "in" ? "right" : "left"
-      }`}
-    ></i>
-    {value}
-  </span>
-);
-
-class LinkData {
-  constructor(service) {
+import DefaultData from "./defaultData";
+
+const LinkDir = ({ value, extraInfo }) => {
+  const dir = extraInfo.rowData.data.linkDir;
+  return (
+    <span className="entity-type">
+      <i
+        className={`link-dir-${dir} fa fa-arrow-circle-${
+          dir === "in" ? "right" : "left"
+        }`}
+      ></i>
+      {dir}
+    </span>
+  );
+};
+
+const LinkType = ({ value, extraInfo }) => {
+  const type = extraInfo.rowData.data.linkType;
+  return (
+    <span className="entity-type">
+      <i className={`link-type-${type} fa`}></i>
+      {type}
+    </span>
+  );
+};
+
+class LinkData extends DefaultData {
+  constructor(service, schema) {
+    super(service, schema);
     this.service = service;
-    this.fields = [
-      { 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",
-        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.extraFields = [{ title: "Dir", field: "linkDir", formatter: LinkDir }];
     this.detailEntity = "router.link";
     this.detailName = "Link";
+    this.typeFormatter = LinkType;
   }
-  hasType = () => {
-    return true;
-  };
-
-  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.service.management.topology.fetchAllEntities(
-        { entity: "router.link" },
-        nodes => {
-          // we have all the data now in the nodes object
-          let linkFields = [];
-          var getLinkName = (node, link) => {
-            let namestr = this.service.utilities.nameFromId(node);
-            return `${namestr}:${link.identity}`;
-          };
-          var fixAddress = link => {
-            let addresses = [];
-            let owningAddr = link.owningAddr || "";
-            let rawAddress = owningAddr;
-            /*
-               - "L*"  =>  "* (local)"
-               - "M0*" =>  "* (direct)"
-               - "M1*" =>  "* (dequeue)"
-               - "MX*" =>  "* (phase X)"
-          */
-            let address = undefined;
-            let starts = { L: "(local)", M0: "(direct)", M1: "(dequeue)" };
-            for (let start in starts) {
-              if (owningAddr.startsWith(start)) {
-                let ends = owningAddr.substr(start.length);
-                address = ends + " " + starts[start];
-                rawAddress = ends;
-                break;
-              }
-            }
-            if (!address) {
-              // check for MX*
-              if (owningAddr.length > 3) {
-                if (owningAddr[0] === "M") {
-                  let phase = parseInt(owningAddr.substr(1));
-                  if (phase && !isNaN(phase)) {
-                    let phaseStr = phase + "";
-                    let star = owningAddr.substr(2 + phaseStr.length);
-                    address = `${star} (phase ${phaseStr})`;
-                  }
-                }
-              }
-            }
-            addresses[0] = address || owningAddr;
-            addresses[1] = rawAddress;
-            return addresses;
-          };
-          for (let node in nodes) {
-            const response = nodes[node]["router.link"];
-            for (let i = 0; i < response.results.length; i++) {
-              const result = response.results[i];
-              const link = this.service.utilities.flatten(
-                response.attributeNames,
-                result
-              );
-              let linkName = getLinkName(node, link);
-              let addresses = fixAddress(link);
-
-              linkFields.push({
-                link: linkName,
-                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],
-
-                capacity: link.capacity,
-                undeliveredCount: link.undeliveredCount,
-                unsettledCount: link.unsettledCount,
-
-                rawAddress: addresses[1],
-                name: link.name,
-                connectionId: link.connectionId,
-                peer: link.peer,
-                type: link.type,
-
-                nodeId: node,
-                identity: link.identity
-              });
-            }
-          }
-          resolve({ data: linkFields, page, perPage });
-        }
-      );
-    });
-  };
 }
 
 export default LinkData;
diff --git a/console/react/src/details/dataSources/addressData.js b/console/react/src/details/dataSources/listenerData.js
similarity index 57%
copy from console/react/src/details/dataSources/addressData.js
copy to console/react/src/details/dataSources/listenerData.js
index 528c286..f23b099 100644
--- a/console/react/src/details/dataSources/addressData.js
+++ b/console/react/src/details/dataSources/listenerData.js
@@ -19,26 +19,33 @@ under the License.
 
 import React from "react";
 import DefaultData from "./defaultData";
-import { utils } from "../../amqp/utilities";
 
-const AddressType = ({ value, extraInfo }) => {
-  const data = extraInfo.rowData.data;
-  const identity = utils.identity_clean(data.identity);
-  const cls = utils.addr_class(identity);
-
-  return (
-    <span className="entity-type">
-      <i className={`address-${cls}`}></i>
-      {cls}
-    </span>
-  );
+const HostPort = ({ value, extraInfo }) => {
+  const host = extraInfo.rowData.data.host;
+  const port = extraInfo.rowData.data.port;
+  return <span className="entity-type">{`${host}:${port}`}</span>;
 };
 
-class AddressData extends DefaultData {
+class ListenerData extends DefaultData {
   constructor(service, schema) {
     super(service, schema);
-    this.typeFormatter = AddressType;
+    this.service = service;
+    this.extraFields = [
+      { title: "Role", field: "role" },
+      {
+        title: "Host:Port",
+        field: "host",
+        formatter: HostPort,
+        filter: this.hostPortFilter
+      }
+    ];
+    this.detailEntity = "listener";
   }
+
+  hostPortFilter = (data, filterValue) => {
+    const hostport = `${data.host}${data.port}`;
+    return hostport.includes(filterValue);
+  };
 }
 
-export default AddressData;
+export default ListenerData;
diff --git a/console/react/src/details/enitiesPage.js b/console/react/src/details/enitiesPage.js
index 038ea6d..6392968 100644
--- a/console/react/src/details/enitiesPage.js
+++ b/console/react/src/details/enitiesPage.js
@@ -22,6 +22,7 @@ import { PageSection, PageSectionVariants } from "@patternfly/react-core";
 import { Stack, StackItem } from "@patternfly/react-core";
 import { Split, SplitItem } from "@patternfly/react-core";
 
+import DetailsTablePage from "../detailsTablePage";
 import EntityListTable from "./entityListTable";
 import EntityList from "./entityList";
 import RouterSelect from "./routerSelect";
@@ -43,22 +44,78 @@ class EntitiesPage extends React.Component {
     this.setState({ lastUpdated });
   };
 
+  // called from entityList to change entity summary
+  handleSwitchEntity = entity => {
+    if (this.listTableRef) this.listTableRef.reset();
+    this.setState({ entity, showDetails: false, detailsState: {} });
+  };
+
+  // called from breadcrumb on entityListTable to return to current entity summary
   handleSelectEntity = entity => {
-    this.setState({ entity });
+    this.setState({ entity, showDetails: false });
   };
 
   handleRouterSelected = routerId => {
-    this.setState({ routerId });
+    this.setState({ routerId, showDetails: false });
+  };
+
+  handleDetailClick = (value, extraInfo, stateInfo) => {
+    this.setState({
+      detailsState: {
+        value: extraInfo.rowData.cells[extraInfo.columnIndex],
+        currentRecord: extraInfo.rowData.data,
+        entity: this.props.entity,
+        page: stateInfo.page,
+        sortBy: stateInfo.sortBy,
+        filterBy: stateInfo.filterBy,
+        perPage: stateInfo.perPage,
+        property: extraInfo.property
+      },
+      showDetails: true
+    });
   };
 
   render() {
+    const entityTable = () => {
+      if (this.state.entity) {
+        if (!this.state.showDetails) {
+          return (
+            <EntityListTable
+              ref={el => (this.listTableRef = el)}
+              service={this.props.service}
+              entity={this.state.entity}
+              schema={this.schema}
+              routerId={this.state.routerId}
+              lastUpdated={this.lastUpdated}
+              handleDetailClick={this.handleDetailClick}
+              detailsState={this.state.detailsState}
+            />
+          );
+        } else {
+          return (
+            <DetailsTablePage
+              details={true}
+              locationState={this.state.detailsState}
+              entity={this.state.entity}
+              service={this.props.service}
+              lastUpdated={this.lastUpdated}
+              schema={this.schema}
+              handleSelectEntity={this.handleSelectEntity}
+            />
+          );
+        }
+      } else {
+        return null;
+      }
+    };
+
     return (
       <PageSection
         variant={PageSectionVariants.light}
         className="details-table-page"
       >
         <Stack>
-          <StackItem className="details-header pf-u-box-shadow-sm-bottom">
+          <StackItem className="details-header">
             <Split>
               <SplitItem isFilled className="split-left">
                 <span className="prompt">Router</span>{" "}
@@ -80,22 +137,10 @@ class EntitiesPage extends React.Component {
               <SplitItem id="entityList">
                 <EntityList
                   schema={this.schema}
-                  handleSelectEntity={this.handleSelectEntity}
+                  handleSelectEntity={this.handleSwitchEntity}
                 />
               </SplitItem>
-              <SplitItem isFilled>
-                {this.state.entity ? (
-                  <EntityListTable
-                    {...this.props}
-                    entity={this.state.entity}
-                    schema={this.schema}
-                    routerId={this.state.routerId}
-                    lastUpdated={this.lastUpdated}
-                  />
-                ) : (
-                  <React.Fragment />
-                )}
-              </SplitItem>
+              <SplitItem isFilled>{entityTable()}</SplitItem>
             </Split>
           </StackItem>
         </Stack>
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/entityData.js
index 35f99c2..8ab6fc2 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/entityData.js
@@ -17,16 +17,16 @@ 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";
-//import LogsData from "./dataSources/logsData";
+import LinkData from "./dataSources/linkData";
+import ListenerData from "./dataSources/listenerData";
 
 import DefaultData from "./dataSources/defaultData";
 
 const dataMap = {
-  "router.address": AddressData
+  "router.address": AddressData,
+  "router.link": LinkData,
+  listener: ListenerData
 };
 
 const defaultData = DefaultData;
diff --git a/console/react/src/details/entityListTable.js b/console/react/src/details/entityListTable.js
index f079c2d..287d872 100644
--- a/console/react/src/details/entityListTable.js
+++ b/console/react/src/details/entityListTable.js
@@ -26,20 +26,20 @@ import {
   TableBody,
   TableVariant
 } from "@patternfly/react-table";
-import { Button, Checkbox, Pagination } from "@patternfly/react-core";
+import { Button, Pagination } from "@patternfly/react-core";
 import { Redirect } from "react-router-dom";
 import TableToolbar from "../tableToolbar";
 import { dataMap, defaultData } from "./entityData";
 
 // If the breadcrumb on the details page was used to return to this page,
 // 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]
+const propFromLocation = (props, which, defaultValue) => {
+  return props &&
+    props.detailsState &&
+    typeof props.detailsState[which] !== "undefined"
+    ? props.detailsState[which]
     : defaultValue;
+};
 
 class EntityListTable extends React.Component {
   constructor(props) {
@@ -56,7 +56,8 @@ class EntityListTable extends React.Component {
       allRows: [],
       rows: [],
       redirect: false,
-      redirectState: {}
+      redirectState: {},
+      hasChecked: false
     };
     this.initDataSource();
     this.columns = [];
@@ -93,6 +94,9 @@ class EntityListTable extends React.Component {
         formatter: this.dataSource.typeFormatter
       });
     }
+    if (this.dataSource.extraFields) {
+      this.dataSource.fields.push(...this.dataSource.extraFields);
+    }
   };
 
   setupFields = () => {
@@ -125,24 +129,29 @@ class EntityListTable extends React.Component {
 
   update = () => {
     if (this.props.entity && this.props.routerId) {
-      this.fetch(
-        this.state.page,
-        this.state.perPage,
-        this.props.routerId,
-        this.props.entity
-      );
+      this.fetch(this.state.page, this.state.perPage);
     }
   };
 
-  fetch = (page, perPage, routerId, entity) => {
+  fetch = (page, perPage) => {
     // get the data. Note: The current page number might change if
     // the number of rows is less than before
+    const routerId = this.props.routerId;
+    const entity = this.props.entity;
     this.dataSource.doFetch(page, perPage, routerId, entity).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;
+      allRows.forEach(row => {
+        const prevRow = this.state.allRows.find(
+          r => r.data.name === row.data.name
+        );
+        if (prevRow && prevRow.selected) {
+          row.selected = true;
+        }
+      });
       this.setState({
         rows,
         page,
@@ -166,19 +175,14 @@ class EntityListTable extends React.Component {
   };
 
   detailClick = (value, extraInfo) => {
-    this.setState({
-      redirect: true,
-      redirectState: {
-        value: extraInfo.rowData.cells[extraInfo.columnIndex],
-        currentRecord: extraInfo.rowData.data,
-        entity: this.props.entity,
-        page: this.state.page,
-        sortBy: this.state.sortBy,
-        filterBy: this.state.filterBy,
-        perPage: this.state.perPage,
-        property: extraInfo.property
-      }
-    });
+    const stateInfo = {
+      page: this.state.page,
+      perPage: this.state.perPage,
+      routerId: this.props.routerId,
+      entity: this.props.entity,
+      filterBy: this.state.filterBy
+    };
+    this.props.handleDetailClick(value, extraInfo, stateInfo);
   };
 
   // cell formatter
@@ -260,6 +264,9 @@ class EntityListTable extends React.Component {
     ) {
       const cellIndex = this.cellIndex(filterField);
       rows = rows.filter(r => {
+        if (this.dataSource.fields[cellIndex].filter) {
+          return this.dataSource.fields[cellIndex].filter(r.data, filterValue);
+        }
         return r.cells[cellIndex].includes(filterValue);
       });
     }
@@ -315,11 +322,32 @@ class EntityListTable extends React.Component {
       rows = [...this.state.rows];
       rows[rowId].selected = isSelected;
     }
+    const hasChecked = this.state.rows.some(row => row.selected);
     this.setState({
-      rows
+      rows,
+      hasChecked
     });
   };
 
+  // called from entitiesPage when a new entity is selected from the list.
+  // we need to reset the page, sortBy, and filterBy for the new entity
+  reset = () => {
+    this.setState(
+      {
+        page: 1,
+        sortBy: { index: 0, direction: SortByDirection.asc },
+        filterBy: {}
+      },
+      () => {
+        if (this.toolbarRef) {
+          this.toolbarRef.reset();
+        }
+      }
+    );
+  };
+
+  handleAction = action => {};
+
   render() {
     const tableProps = {
       cells: this.columns,
@@ -329,7 +357,7 @@ class EntityListTable extends React.Component {
       onSort: this.onSort,
       variant: TableVariant.compact
     };
-    if (this.dataSource.actions(this.props.entity).includes("DELETE") > 0) {
+    if (this.dataSource.actions(this.props.entity).includes("DELETE")) {
       tableProps.onSelect = this.onSelect;
       tableProps.canSelectAll = true;
     }
@@ -348,15 +376,19 @@ class EntityListTable extends React.Component {
     return (
       <React.Fragment>
         <TableToolbar
+          ref={el => (this.toolbarRef = el)}
           total={this.state.total}
           page={this.state.page}
           perPage={this.state.perPage}
           onSetPage={this.onSetPage}
           onPerPageSelect={this.onPerPageSelect}
           fields={this.dataSource.fields}
+          filterBy={this.state.filterBy}
           handleChangeFilterValue={this.handleChangeFilterValue}
           hidePagination={true}
           actions={this.dataSource.actions(this.props.entity)}
+          hasChecked={this.state.hasChecked}
+          handleAction={this.handleAction}
         />
         <Table {...tableProps}>
           <TableHeader />
diff --git a/console/react/src/overview/detailsTablePage.js b/console/react/src/detailsTablePage.js
similarity index 70%
rename from console/react/src/overview/detailsTablePage.js
rename to console/react/src/detailsTablePage.js
index f296427..c253ca6 100644
--- a/console/react/src/overview/detailsTablePage.js
+++ b/console/react/src/detailsTablePage.js
@@ -38,8 +38,9 @@ import {
 } from "@patternfly/react-table";
 import { Card, CardBody } from "@patternfly/react-core";
 import { Redirect } from "react-router-dom";
-import { dataMap } from "./entityData";
-import Updated from "../updated";
+import { dataMap } from "./overview/entityData";
+import { dataMap as detailsDataMap, defaultData } from "./details/entityData";
+import Updated from "./updated";
 
 class DetailTablesPage extends React.Component {
   constructor(props) {
@@ -63,23 +64,36 @@ class DetailTablesPage extends React.Component {
     // 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.props.entity ||
+      (this.props &&
+        this.props.location &&
+        this.props.location.state &&
+        this.props.location.state.entity);
+
+    if (!this.entity) {
       this.state.redirect = true;
     } else {
-      this.dataSource = new dataMap[this.entity](this.props.service);
+      if (this.props.details) {
+        this.dataSource = !detailsDataMap[this.entity]
+          ? new defaultData(this.props.service, this.props.schema)
+          : new detailsDataMap[this.entity](
+              this.props.service,
+              this.props.schema
+            );
+        this.locationState = this.props.locationState;
+      } else {
+        this.dataSource = new dataMap[this.entity](
+          this.props.service,
+          this.props.schema
+        );
+        this.locationState = this.props.location.state;
+      }
     }
   }
 
   componentDidMount = () => {
-    this.props.service.management.getSchema().then(schema => {
-      this.schema = schema;
-      this.timer = setInterval(this.update, 5000);
-      this.update();
-    });
+    this.timer = setInterval(this.update, 5000);
+    this.update();
   };
 
   componentWillUnmount = () => {
@@ -91,7 +105,11 @@ class DetailTablesPage extends React.Component {
   update = () => {
     this.mapRows().then(
       rows => {
-        this.setState({ rows, lastUpdated: new Date() });
+        this.setState({ rows, lastUpdated: new Date() }, () => {
+          if (this.props.details) {
+            this.props.lastUpdated(this.state.lastUpdated);
+          }
+        });
       },
       error => {
         console.log(`detailsTablePage: ${error}`);
@@ -111,7 +129,11 @@ class DetailTablesPage extends React.Component {
         reject("no data source");
       }
       this.dataSource
-        .fetchRecord(this.props.location.state.currentRecord, this.schema)
+        .fetchRecord(
+          this.locationState.currentRecord,
+          this.props.schema,
+          this.entity
+        )
         .then(data => {
           for (const attribute in data) {
             if (
@@ -130,24 +152,19 @@ class DetailTablesPage extends React.Component {
 
   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;
-  };
+  parentItem = () =>
+    this.locationState.currentRecord[this.locationState.property];
 
   breadcrumbSelected = () => {
-    this.setState({
-      redirect: true,
-      redirectPath: `/overview/${this.entity}`,
-      redirectState: this.props.location.state
-    });
+    if (this.props.details) {
+      this.props.handleSelectEntity(this.entity);
+    } else {
+      this.setState({
+        redirect: true,
+        redirectPath: `/overview/${this.entity}`,
+        redirectState: this.locationState
+      });
+    }
   };
 
   render() {
@@ -173,25 +190,22 @@ class DetailTablesPage extends React.Component {
               <Breadcrumb>
                 <BreadcrumbItem
                   className="link-button"
-                  onClick={() =>
-                    this.breadcrumbSelected(`/overview/${this.entity}`)
-                  }
+                  onClick={this.breadcrumbSelected}
                 >
                   {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`}
+                  {this.parentItem()}
                 </Text>
-                <Updated
-                  service={this.props.service}
-                  lastUpdated={this.state.lastUpdated}
-                />
+                {!this.props.details && (
+                  <Updated
+                    service={this.props.service}
+                    lastUpdated={this.state.lastUpdated}
+                  />
+                )}
               </TextContent>
             </StackItem>
             <StackItem className="overview-table">
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index c56278b..82c9534 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -51,7 +51,7 @@ import DropdownMenu from "./DropdownMenu";
 import ConnectPage from "./connectPage";
 import DashboardPage from "./overview/dashboard/dashboardPage";
 import OverviewTablePage from "./overview/overviewTablePage";
-import DetailsTablePage from "./overview/detailsTablePage";
+import DetailsTablePage from "./detailsTablePage";
 import EntitiesPage from "./details/enitiesPage";
 import TopologyPage from "./topology/topologyPage";
 import MessageFlowPage from "./chord/chordPage";
@@ -95,6 +95,7 @@ class PageLayout extends React.Component {
     };
   }
 
+  // the connection to the routers was lost
   setLocation = whatHappened => {
     if (whatHappened === "disconnect") {
       this.setState({ connected: false });
@@ -137,6 +138,7 @@ class PageLayout extends React.Component {
 
       this.service.connect(connectOptions).then(
         r => {
+          this.schema = this.service.schema;
           if (connectPath === "/") connectPath = "/dashboard";
           const activeItem = connectPath.split("/").pop();
           // find the active group for this item
@@ -391,7 +393,11 @@ class PageLayout extends React.Component {
               path="/overview/:entity"
               component={OverviewTablePage}
             />
-            <PrivateRoute path="/details" component={DetailsTablePage} />
+            <PrivateRoute
+              path="/details"
+              schema={this.schema}
+              component={DetailsTablePage}
+            />
             <PrivateRoute path="/topology" component={TopologyPage} />
             <PrivateRoute path="/flow" component={MessageFlowPage} />
             <PrivateRoute path="/logs" component={LogDetails} />
diff --git a/console/react/src/overview/overviewTable.js b/console/react/src/overview/overviewTable.js
index 7a42778..98f1a8a 100644
--- a/console/react/src/overview/overviewTable.js
+++ b/console/react/src/overview/overviewTable.js
@@ -282,7 +282,8 @@ class OverviewTable extends React.Component {
       return (
         <Redirect
           to={{
-            pathname: this.dataSource.detailPath || "/details",
+            pathname:
+              (this.dataSource && this.dataSource.detailPath) || "/details",
             state: this.state.redirectState
           }}
         />
diff --git a/console/react/src/qdrService.js b/console/react/src/qdrService.js
index 1771a6a..4f8c1b3 100644
--- a/console/react/src/qdrService.js
+++ b/console/react/src/qdrService.js
@@ -28,6 +28,7 @@ export class QDRService {
     this.management = new dm(url.protocol, DEFAULT_INTERVAL);
     this.utilities = utils;
     this.hooks = hooks;
+    this.schema = null;
   }
 
   onReconnect() {
@@ -50,6 +51,7 @@ export class QDRService {
           );
 
           self.management.getSchema().then(schema => {
+            self.schema = schema;
             //console.log("got schema after connection");
             //console.log(schema);
             self.management.topology.setUpdateEntities([]);
diff --git a/console/react/src/tableToolbar.jsx b/console/react/src/tableToolbar.jsx
index 5d76eef..69824d5 100644
--- a/console/react/src/tableToolbar.jsx
+++ b/console/react/src/tableToolbar.jsx
@@ -36,13 +36,17 @@ class TableToolbar extends React.Component {
     super(props);
     this.state = {
       isDropDownOpen: false,
-      searchValue: "",
-      filterBy: this.props.fields[0].title
+      searchValue:
+        this.props.filterBy && this.props.filterBy.value
+          ? this.props.filterBy.value
+          : "",
+      filterField: this.props.fields[0].title
     };
+
     this.handleTextInputChange = value => {
       this.setState({ searchValue: value }, () => {
         this.props.handleChangeFilterValue(
-          this.state.filterBy,
+          this.state.filterField,
           this.state.searchValue
         );
       });
@@ -58,12 +62,12 @@ class TableToolbar extends React.Component {
       this.setState(
         {
           isDropDownOpen: !this.state.isDropDownOpen,
-          filterBy: event.target.text,
+          filterField: event.target.text,
           searchValue: ""
         },
         () =>
           this.props.handleChangeFilterValue(
-            this.state.filterBy,
+            this.state.filterField,
             this.state.searchValue
           )
       );
@@ -89,7 +93,7 @@ class TableToolbar extends React.Component {
           position={DropdownPosition.right}
           toggle={
             <DropdownToggle onToggle={this.onDropDownToggle}>
-              {this.state.filterBy}
+              {this.state.filterField}
             </DropdownToggle>
           }
           isOpen={isDropDownOpen}
@@ -103,12 +107,37 @@ class TableToolbar extends React.Component {
     };
   }
 
+  reset = () => {
+    this.setState({
+      isDropDownOpen: false,
+      searchValue: "",
+      filterField: this.props.fields[0].title
+    });
+  };
+
   render() {
-    const actions = this.props.actions.map(action => (
-      <ToolbarItem className="pf-u-mx-md">
-        <Button aria-label={action}>{action}</Button>
-      </ToolbarItem>
-    ));
+    const actions =
+      this.props.actions &&
+      this.props.actions.map(action => {
+        let variant = "primary";
+        let isDisabled = false;
+        if (action === "DELETE" && !this.props.hasChecked) {
+          variant = "tertiary";
+          isDisabled = true;
+        }
+        return (
+          <ToolbarItem className="pf-u-mx-md" key={action}>
+            <Button
+              aria-label={action}
+              onClick={() => this.props.handleAction(action)}
+              variant={variant}
+              isDisabled={isDisabled}
+            >
+              {action}
+            </Button>
+          </ToolbarItem>
+        );
+      });
 
     return (
       <Toolbar className="pf-l-toolbar pf-u-mx-xl pf-u-my-md table-toolbar">


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