You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2019/11/27 13:36:10 UTC
[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Changes
requested from reviews
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 <ea...@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-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org