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 <eal...@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-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org