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