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/03 18:51:47 UTC
[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Removed unused
files. Added notification drawer
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 8c5e99d Removed unused files. Added notification drawer
8c5e99d is described below
commit 8c5e99dc44abec23a24007810e139861b5d48921
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Sun Nov 3 13:51:28 2019 -0500
Removed unused files. Added notification drawer
---
console/react/src/App.css | 121 +++++++
console/react/src/App.js | 1 +
console/react/src/REST.js | 169 ----------
console/react/src/connectionClose.js | 120 +++++--
.../connectionData.js} | 30 +-
console/react/src/details/enitiesPage.js | 4 +-
console/react/src/details/entityData.js | 4 +-
console/react/src/details/entityListTable.js | 9 +-
console/react/src/edge-table-pagination.js | 24 --
console/react/src/edge-table-toolbar.js | 133 --------
console/react/src/edge-table.js | 289 -----------------
console/react/src/empty-edge-class-table.js | 43 ---
console/react/src/empty-selection.js | 33 --
console/react/src/field-details.js | 225 -------------
console/react/src/graph.js | 358 ---------------------
console/react/src/layout.js | 38 ++-
console/react/src/network-name.js | 67 ----
console/react/src/nodes.js | 260 ---------------
console/react/src/nodeslinks.js | 144 ---------
console/react/src/notificationDrawer.js | 293 +++++++++++++++++
console/react/src/qdrGlobals.js | 11 +
console/react/src/show-d3-svg.js | 236 --------------
console/react/src/topology-context.js | 139 --------
23 files changed, 577 insertions(+), 2174 deletions(-)
diff --git a/console/react/src/App.css b/console/react/src/App.css
index 758ca39..b1eff31 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -1176,3 +1176,124 @@ span.entity-type i.link-type-router-control:before {
.details-header {
border-bottom: 1px solid #eaeaea;
}
+
+#NotificationDrawer {
+ position: absolute;
+ top: 4.8em;
+ color: black;
+ right: 0em;
+}
+
+.drawer-pf-title {
+ display: flex;
+ border: 1px solid lightgray;
+ background-color: #f3f3f3;
+ height: 2em;
+ justify-content: space-between;
+ position: unset;
+}
+
+#NotificationDrawer.compact {
+ transition: width 0.25s ease;
+ width: 20em;
+}
+
+#NotificationDrawer.expanded {
+ transition: width 0.25s ease;
+ width: 50em;
+}
+
+.drawer-pf-title h3 {
+ margin: 0.5em 0;
+}
+
+.drawer-pf-title svg {
+ fill: #333333;
+}
+
+.notification-button dl.pf-c-accordion {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+.notification-button dl.pf-c-accordion * {
+ border-left-color: transparent;
+}
+
+.notification-button dl.pf-c-accordion dt {
+ background-image: linear-gradient(to bottom, #fafafa 0, #ededed 100%);
+ background-repeat: repeat-x;
+}
+
+.notification-button dl.pf-c-accordion h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.notification-button button.pf-c-accordion__toggle span {
+ font-weight: normal;
+ display: block;
+ font-size: 14px;
+ line-height: normal;
+}
+
+.notification-button button.pf-c-accordion__toggle {
+ display: flex;
+}
+
+.notification-button .pf-c-accordion__toggle svg {
+ position: absolute;
+ left: 0.5em;
+}
+
+.notification-button button.pf-c-accordion__toggle {
+ text-align: center;
+}
+
+.notification-button .pf-c-accordion__toggle-text {
+ margin: auto;
+}
+
+.drawer-pf-notification {
+ display: flex;
+ justify-content: space-between;
+}
+
+.drawer-pf-notification-info,
+.drawer-pf-notification-message {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.drawer-pf-notification-content {
+ display: flex;
+ flex-direction: column;
+ margin: auto;
+}
+
+#NotificationDrawer .pf-c-accordion__expanded-content-body {
+ padding: 0;
+}
+
+#NotificationDrawer .drawer-pf-action-link {
+ border-left: 1px solid #d1d1d1;
+}
+
+#NotificationDrawer button.pf-m-expanded,
+#NotificationDrawer dd.pf-m-expanded {
+ border-left-color: blue;
+}
+
+#NotificationDrawer .panel-body {
+ padding-left: 0;
+}
+
+#NotificationDrawer .drawer-pf-notification {
+ padding-left: 15px;
+ font-size: 16px;
+}
+
+#NotificationDrawer .drawer-pf-notification.unread {
+ font-weight: bold;
+ color: black;
+}
diff --git a/console/react/src/App.js b/console/react/src/App.js
index cb56d14..6f66bb7 100644
--- a/console/react/src/App.js
+++ b/console/react/src/App.js
@@ -3,6 +3,7 @@ import "@patternfly/patternfly/patternfly.css";
import "@patternfly/patternfly/patternfly-addons.css";
import "patternfly/dist/css/patternfly.css";
+import "patternfly/dist/css/patternfly-additions.css";
import "@patternfly/patternfly/components/Nav/nav.css";
import "./App.css";
import PageLayout from "./layout";
diff --git a/console/react/src/REST.js b/console/react/src/REST.js
deleted file mode 100644
index 427ac22..0000000
--- a/console/react/src/REST.js
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2019 Red Hat Inc. A division of IBM
- *
- * Licensed 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.
- */
-
-/* The web front-end will use this class to talk to the backend (/public/app.js)
- */
-class REST {
- constructor() {
- this.url = `${window.location.protocol}//${window.location.host}`;
- }
-
- getRouterState = router => {
- const body = {
- what: "getState",
- router: router
- };
- return this.doPost(body);
- };
-
- saveNetwork = networkInfo => {
- const body = {
- what: "saveNetwork",
- network: networkInfo
- };
- return this.doPost(body);
- };
-
- doPost = body =>
- new Promise((resolve, reject) => {
- fetch(`${this.url}/api`, {
- headers: {
- Accept: "application/json",
- "Content-Type": "application/json"
- },
- method: "POST",
- body: JSON.stringify(body)
- })
- .then(response => {
- if (response.status < 200 || response.status > 299) {
- reject(response.statusText);
- return {};
- }
- return response.json();
- })
- .then(myJson => {
- resolve(myJson);
- })
- // network error?
- .catch(error => reject(error));
- });
-
- // example of how to periodically do a GET request
- examplePoll = () =>
- new Promise((resolve, reject) => {
- // strategy defines how we want to handle various return codes
- // 200 means OK, in this example we want to resolve
- // 404 means NOT_FOUND, in this example we want to resolve so caller knows it wasn't found
- // 500 means there was a communication error, in this example we want to reject
- const strategy = { "200": "resolve", "404": "resolve", "500": "reject" };
- // call GET until the return code is what you want or the timeout is reached
- poll(`${this.url}/api`, strategy).then(
- res => {
- resolve(res);
- },
- e => {
- reject(e);
- }
- );
- });
-
- exampleDelete = name =>
- new Promise((resolve, reject) => {
- // console.log(` *** deleting ${name} ***`);
- fetch(`${this.url}/api/${name}`, {
- method: "DELETE"
- }).then(() => {
- // status 200 means the thing we want to delete is still there, so keep waiting
- // status 404 means the thing we want to delete is now gone, so resolve
- // status 500 is an error
- const strategy = { "200": "wait", "404": "resolve", "500": "reject" };
- // call GET for name until it returns that the name is gone or times out
- poll(`${this.url}/api/${name}`, strategy).then(
- res => {
- resolve(res);
- },
- e => {
- reject(e);
- }
- );
- });
- });
-
- exampleBatch(names) {
- return new Promise((resolve, reject) => {
- Promise.all(names.map(name => this.exampleDelete(name))).then(
- () => {
- resolve();
- },
- firstError => {
- reject(firstError);
- }
- );
- });
- }
-}
-
-// poll for a condition
-const poll = (url, strategy, timeout, interval) => {
- const endTime = Number(new Date()) + (timeout || 10000);
- interval = interval || 1000;
- const s200 = strategy["200"];
- const s404 = strategy["404"];
- const s500 = strategy["500"];
- let lastStatus = 0;
-
- const checkCondition = (resolve, reject) => {
- // If the condition is met, we're done!
- fetch(url)
- .then(res => {
- lastStatus = res.status;
- const ret = {};
- // decide whether to resolve, reject, or wait
- if (res.status >= 200 && res.status <= 299) {
- ret[s200] = res.json();
- return ret;
- } else if (res.status === 404) {
- ret[s404] = [];
- return ret;
- }
- ret[s500] = res.status;
- return ret;
- })
- .then(json => {
- if (json.resolve) {
- resolve(json.resolve);
- } else if (json.reject) {
- reject(json.reject);
- }
- // If the condition isn't met but the timeout hasn't elapsed, go again
- else if (Number(new Date()) < endTime) {
- setTimeout(checkCondition, interval, resolve, reject);
- }
- // Didn't match and too much time, reject!
- else {
- const msg = { message: "timeout", status: lastStatus };
- reject(new Error(JSON.stringify(msg)));
- }
- })
- .catch(e => {
- console.log(`poll caught error ${e}`);
- reject(e);
- });
- };
- return new Promise(checkCondition);
-};
-
-export default REST;
diff --git a/console/react/src/connectionClose.js b/console/react/src/connectionClose.js
index 7b64f1f..9da3d13 100644
--- a/console/react/src/connectionClose.js
+++ b/console/react/src/connectionClose.js
@@ -18,40 +18,106 @@ under the License.
*/
import React from "react";
-import { Button } from "@patternfly/react-core";
+import { Button, Modal } from "@patternfly/react-core";
class ConnectionClose extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isModalOpen: false,
+ closing: false
+ };
+ }
+
+ handleModalToggle = () => {
+ this.setState(({ isModalOpen }) => ({
+ isModalOpen: !isModalOpen
+ }));
+ };
+
closeConnection = () => {
- const record = this.props.extraInfo.rowData.data;
- this.props.service.management.connection
- .sendMethod(
- record.nodeId || record.routerId,
- "connection",
- { adminStatus: "deleted", identity: record.identity },
- "UPDATE",
- { adminStatus: "deleted" }
- )
- .then(results => {
- let statusCode =
- results.context.message.application_properties.statusCode;
- if (statusCode < 200 || statusCode >= 300) {
- console.log(
- `error ${record.name} ${results.context.message.application_properties.statusDescription}`
- );
- } else {
- console.log(
- `success ${record.name} ${results.context.message.application_properties.statusDescription}`
- );
- }
- });
+ this.setState({ closing: true }, () => {
+ const record = this.props.extraInfo.rowData.data;
+ this.props.service.management.connection
+ .sendMethod(
+ record.nodeId || record.routerId,
+ "connection",
+ { adminStatus: "deleted", identity: record.identity },
+ "UPDATE",
+ { adminStatus: "deleted" }
+ )
+ .then(results => {
+ let statusCode =
+ results.context.message.application_properties.statusCode;
+ if (statusCode < 200 || statusCode >= 300) {
+ console.log(
+ `error ${record.name} ${results.context.message.application_properties.statusDescription}`
+ );
+ this.props.handleAddNotification(
+ "action",
+ `Close connection ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`,
+ new Date(),
+ "error"
+ );
+ } else {
+ this.props.handleAddNotification(
+ "action",
+ `Closed connection ${record.name}`,
+ new Date(),
+ "success"
+ );
+ console.log(
+ `success ${record.name} ${results.context.message.application_properties.statusDescription}`
+ );
+ }
+ this.setState({ isModalOpen: false, closing: false }, () => {
+ if (this.props.notifyClick) {
+ this.props.notifyClick();
+ }
+ });
+ });
+ });
};
render() {
- if (this.props.extraInfo.rowData.data.role === "normal") {
+ const { isModalOpen, closing } = this.state;
+ const record = this.props.extraInfo.rowData.data;
+ if (record.role === "normal") {
return (
- <Button className="link-button" onClick={this.closeConnection}>
- Close
- </Button>
+ <React.Fragment>
+ <Button className="link-button" onClick={this.handleModalToggle}>
+ Close
+ </Button>
+ <Modal
+ isSmall
+ title="Close conection"
+ isOpen={isModalOpen}
+ onClose={this.handleModalToggle}
+ actions={[
+ <Button
+ key="confirm"
+ variant="primary"
+ onClick={this.closeConnection}
+ isDisabled={closing}
+ >
+ Confirm
+ </Button>,
+ <Button
+ key="cancel"
+ variant="link"
+ onClick={this.handleModalToggle}
+ isDisabled={closing}
+ >
+ Cancel
+ </Button>
+ ]}
+ isFooterLeftAligned
+ >
+ {closing
+ ? `Closing connection ${record.name}`
+ : `Close connection ${record.name}?`}
+ </Modal>
+ </React.Fragment>
);
} else {
return <React.Fragment />;
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/dataSources/connectionData.js
similarity index 62%
copy from console/react/src/details/entityData.js
copy to console/react/src/details/dataSources/connectionData.js
index 8ab6fc2..568ac4c 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/dataSources/connectionData.js
@@ -17,17 +17,23 @@ specific language governing permissions and limitations
under the License.
*/
-import AddressData from "./dataSources/addressData";
-import LinkData from "./dataSources/linkData";
-import ListenerData from "./dataSources/listenerData";
+import DefaultData from "./defaultData";
+import ConnectionClose from "../../connectionClose";
-import DefaultData from "./dataSources/defaultData";
+class ConnectionData extends DefaultData {
+ constructor(service, schema) {
+ super(service, schema);
+ this.extraFields = [
+ {
+ title: "",
+ field: "connection",
+ noSort: true,
+ formatter: ConnectionClose
+ }
+ ];
+ this.detailEntity = "router.link";
+ this.detailName = "Link";
+ }
+}
-const dataMap = {
- "router.address": AddressData,
- "router.link": LinkData,
- listener: ListenerData
-};
-
-const defaultData = DefaultData;
-export { dataMap, defaultData };
+export default ConnectionData;
diff --git a/console/react/src/details/enitiesPage.js b/console/react/src/details/enitiesPage.js
index 6392968..51529b9 100644
--- a/console/react/src/details/enitiesPage.js
+++ b/console/react/src/details/enitiesPage.js
@@ -82,7 +82,7 @@ class EntitiesPage extends React.Component {
return (
<EntityListTable
ref={el => (this.listTableRef = el)}
- service={this.props.service}
+ {...this.props}
entity={this.state.entity}
schema={this.schema}
routerId={this.state.routerId}
@@ -97,7 +97,7 @@ class EntitiesPage extends React.Component {
details={true}
locationState={this.state.detailsState}
entity={this.state.entity}
- service={this.props.service}
+ {...this.props}
lastUpdated={this.lastUpdated}
schema={this.schema}
handleSelectEntity={this.handleSelectEntity}
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/entityData.js
index 8ab6fc2..351072d 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/entityData.js
@@ -20,13 +20,15 @@ under the License.
import AddressData from "./dataSources/addressData";
import LinkData from "./dataSources/linkData";
import ListenerData from "./dataSources/listenerData";
+import ConnectionData from "./dataSources/connectionData";
import DefaultData from "./dataSources/defaultData";
const dataMap = {
"router.address": AddressData,
"router.link": LinkData,
- listener: ListenerData
+ listener: ListenerData,
+ connection: ConnectionData
};
const defaultData = DefaultData;
diff --git a/console/react/src/details/entityListTable.js b/console/react/src/details/entityListTable.js
index 287d872..1aa3703 100644
--- a/console/react/src/details/entityListTable.js
+++ b/console/react/src/details/entityListTable.js
@@ -31,7 +31,7 @@ import { Redirect } from "react-router-dom";
import TableToolbar from "../tableToolbar";
import { dataMap, defaultData } from "./entityData";
-// If the breadcrumb on the details page was used to return to this page,
+// If the breadcrumb on the detailsTablePage was used to return to this page,
// we will have saved state info in props.location.state
const propFromLocation = (props, which, defaultValue) => {
return props &&
@@ -203,12 +203,17 @@ class EntityListTable extends React.Component {
<Component
value={value}
extraInfo={extraInfo}
- service={this.props.service}
detailClick={this.detailClick}
+ notifyClick={this.notifyClick}
+ {...this.props}
/>
);
};
+ notifyClick = () => {
+ this.update();
+ };
+
onSort = (_event, index, direction) => {
this.setState({ sortBy: { index, direction } }, () => {
const { allRows, page, perPage } = this.state;
diff --git a/console/react/src/edge-table-pagination.js b/console/react/src/edge-table-pagination.js
deleted file mode 100644
index 9ddac74..0000000
--- a/console/react/src/edge-table-pagination.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from "react";
-import { Pagination } from "@patternfly/react-core";
-
-class EdgeTablePagination extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
-
- render() {
- return (
- <Pagination
- itemCount={this.props.rows}
- perPage={this.props.perPage}
- page={this.props.page}
- onSetPage={this.props.onSetPage}
- widgetId="pagination-options-menu-top"
- onPerPageSelect={this.props.onPerPageSelect}
- />
- );
- }
-}
-
-export default EdgeTablePagination;
diff --git a/console/react/src/edge-table-toolbar.js b/console/react/src/edge-table-toolbar.js
deleted file mode 100644
index 2afec75..0000000
--- a/console/react/src/edge-table-toolbar.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import React from "react";
-import {
- Button,
- ButtonVariant,
- InputGroup,
- TextInput,
- Toolbar,
- ToolbarGroup,
- ToolbarItem
-} from "@patternfly/react-core";
-import {
- SearchIcon,
- SortAlphaDownIcon,
- SortAlphaUpIcon
-} from "@patternfly/react-icons";
-import Confirm from "./confirm";
-
-class EdgeTableToolbar extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- isDropDownOpen: false,
- isKebabOpen: false,
- searchValue: ""
- };
- this.handleTextInputChange = searchValue => {
- this.setState({ searchValue });
- };
-
- this.onDropDownToggle = isOpen => {
- this.setState({
- isDropDownOpen: isOpen
- });
- };
-
- this.onDropDownSelect = event => {
- this.setState({
- isDropDownOpen: !this.state.isDropDownOpen
- });
- };
-
- this.onKebabToggle = isOpen => {
- this.setState({
- isKebabOpen: isOpen
- });
- };
-
- this.onKebabSelect = event => {
- this.setState({
- isKebabOpen: !this.state.isKebabOpen
- });
- };
-
- this.buildSearchBox = () => {
- return (
- <InputGroup>
- <TextInput
- value={this.props.filterText}
- type="search"
- onChange={this.props.handleChangeFilter}
- aria-label="search text input"
- placeholder="search for edge namespaces"
- />
- <Button
- variant={ButtonVariant.tertiary}
- aria-label="search button for search input"
- >
- <SearchIcon />
- </Button>
- </InputGroup>
- );
- };
- }
-
- isDeleteDisabled = () => this.props.rows.every(r => !r.selected);
- deleteText = () => {
- return (
- <React.Fragment>
- <h2>Are you sure you want to delete:</h2>
- <ul>
- {this.props.rows
- .filter(r => r.selected)
- .map((r, i) => {
- return <li key={`key-${i}`}>{r.name}</li>;
- })}
- </ul>
- </React.Fragment>
- );
- };
- render() {
- return (
- <Toolbar className="pf-l-toolbar pf-u-justify-content-space-between pf-u-mx-xl pf-u-my-md">
- <ToolbarGroup>
- <ToolbarItem className="pf-u-mr-md">
- {this.buildSearchBox()}
- </ToolbarItem>
- <ToolbarItem>
- <Button
- variant="plain"
- onClick={this.props.toggleAlphaSort}
- aria-label="Sort A-Z"
- >
- {this.props.sortDown ? (
- <SortAlphaDownIcon />
- ) : (
- <SortAlphaUpIcon />
- )}
- </Button>
- </ToolbarItem>
- </ToolbarGroup>
- <ToolbarGroup className="edge-table-actions">
- <ToolbarItem className="pf-u-mx-sm">
- <Confirm
- handleConfirm={this.props.handleDeleteEdge}
- buttonText="Delete"
- title="Confirm delete"
- isDeleteDisabled={this.isDeleteDisabled()}
- >
- {this.deleteText()}
- </Confirm>
- </ToolbarItem>
- <ToolbarItem className="pf-u-mx-sm">
- <Button aria-label="Add" onClick={this.props.handleAddEdge}>
- Add
- </Button>
- </ToolbarItem>
- </ToolbarGroup>
- </Toolbar>
- );
- }
-}
-
-export default EdgeTableToolbar;
diff --git a/console/react/src/edge-table.js b/console/react/src/edge-table.js
deleted file mode 100644
index ef9d7ed..0000000
--- a/console/react/src/edge-table.js
+++ /dev/null
@@ -1,289 +0,0 @@
-import React from "react";
-import { Button, ClipboardCopy, TextInput } from "@patternfly/react-core";
-import {
- cellWidth,
- Table,
- TableHeader,
- TableBody,
- TableVariant
-} from "@patternfly/react-table";
-import EdgeTableToolbar from "./edge-table-toolbar";
-import EdgeTablePagination from "./edge-table-pagination";
-import EmptyEdgeClassTable from "./empty-edge-class-table";
-import Graph from "./graph";
-import { RouterStates } from "./nodes";
-
-const YAML = 0;
-const STATE_ICON = 1;
-const STATE_TEXT = 2;
-const NAME = 3;
-
-class EdgeTable extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- columns: [
- {
- title: "yaml",
- cellFormatters: [this.formatYaml],
- transforms: [cellWidth(5)]
- },
- {
- title: "State",
- cellFormatters: [this.formatState],
- transforms: [cellWidth(5)]
- },
- { title: "", cellFormatters: [this.formatStateDescription] },
- { title: "Name", cellFormatters: [this.formatName] }
- ],
- filterText: "",
- sortDown: true,
- editingEdgeRow: -1,
- page: 1,
- perPage: 5
- };
-
- this.rows = [];
- }
-
- onSelect = (event, isSelected, rowId) => {
- // the internal rows array may be different from the props.rows array
- const realRowIndex =
- rowId >= 0
- ? this.props.rows.findIndex(r => r.key === this.rows[rowId].key)
- : rowId;
- this.props.handleSelectEdgeRow(realRowIndex, isSelected);
- };
-
- handleEdgeNameBlur = () => {
- this.onSelect("", false, -1);
- this.setState({ editingEdgeRow: -1 });
- };
-
- handleEdgeNameClick = rowIndex => {
- this.onSelect("", true, rowIndex);
- this.setState({ editingEdgeRow: rowIndex });
- };
-
- handleEdgeKeyPress = event => {
- if (event.key === "Enter") {
- this.handleEdgeNameBlur();
- }
- };
-
- formatYaml = (value, _xtraInfo) => {
- const cells = _xtraInfo.rowData.cells;
- let yaml = <div className="state-placeholder"></div>;
- if (cells[0] && cells[1] === 1) {
- yaml = (
- <ClipboardCopy
- className="state-copy"
- onClick={(event, text) => {
- const clipboard = event.currentTarget.parentElement;
- const el = document.createElement("input");
- el.value = JSON.stringify(cells[0]);
- clipboard.appendChild(el);
- el.select();
- document.execCommand("copy");
- clipboard.removeChild(el);
- }}
- />
- );
- }
- return yaml;
- };
-
- formatStateDescription = (value, _xtraInfo) => {
- return <div className="state-text">{RouterStates[value]}</div>;
- };
-
- formatState = (value, _xtraInfo) => {
- return (
- <Graph
- id={`State-${_xtraInfo.rowIndex}`}
- thumbNail={true}
- legend={true}
- dimensions={{ width: 30, height: 30 }}
- nodes={[
- {
- key: `edge-key-${_xtraInfo.rowIndex}-${value}`,
- r: 10,
- type: "interior",
- state: value,
- x: 0,
- y: 0
- }
- ]}
- links={[]}
- notifyCurrentRouter={() => {}}
- />
- );
- };
- formatName = (value, _xtraInfo) => {
- const realRowIndex = this.props.rows.findIndex(
- r => r.key === _xtraInfo.rowData.key
- );
- if (this.state.editingEdgeRow === _xtraInfo.rowIndex) {
- // the internal rows array may be different from the props.rows array
- return (
- <TextInput
- value={this.props.rows[realRowIndex].name}
- type="text"
- autoFocus
- onChange={val => this.props.handleEdgeNameChange(val, realRowIndex)}
- onBlur={this.handleEdgeNameBlur}
- onKeyPress={this.handleEdgeKeyPress}
- aria-label="text input example"
- />
- );
- }
- return (
- <Button
- variant="link"
- isInline
- onClick={() => this.handleEdgeNameClick(_xtraInfo.rowIndex)}
- >
- {this.rows[_xtraInfo.rowIndex].cells[NAME]}
- </Button>
- );
- };
-
- onSelect = (event, isSelected, rowId) => {
- // the internal rows array may be different from the props.rows array
- const realRowIndex =
- rowId >= 0
- ? this.props.rows.findIndex(r => r.key === this.rows[rowId].key)
- : rowId;
- this.props.handleSelectEdgeRow(realRowIndex, isSelected);
- };
-
- toggleAlphaSort = () => {
- this.setState({ sortDown: !this.state.sortDown });
- };
-
- genTable = () => {
- const { columns, filterText } = this.state;
- if (this.props.rows.length > 0) {
- if (this.state.editingEdgeRow === -1 || this.rows.length === 0) {
- this.rows = this.props.rows.map(r => ({
- cells: [r.yaml, r.state, r.state, r.name],
- selected: r.selected,
- key: r.key
- }));
- // sort the rows
- this.rows = this.rows.sort((a, b) =>
- a.cells[NAME] < b.cells[NAME]
- ? -1
- : a.cells[NAME] > b.cells[NAME]
- ? 1
- : 0
- );
- if (!this.state.sortDown) {
- this.rows = this.rows.reverse();
- }
- // filter the rows
- if (filterText !== "") {
- this.rows = this.rows.filter(
- r => r.cells[NAME].indexOf(filterText) >= 0
- );
- }
- // only show rows on current page
- const start = (this.state.page - 1) * this.state.perPage;
- const end = Math.min(this.rows.length, start + this.state.perPage);
- this.rows = this.rows.slice(start, end);
- } else {
- // pickup any changed info
- this.rows.forEach(r => {
- const rrow = this.props.rows.find(rr => rr.key === r.key);
- if (rrow) {
- r.selected = rrow.selected;
- r.cells[YAML] = rrow.yaml;
- r.cells[STATE_ICON] = rrow.state;
- r.cells[STATE_TEXT] = rrow.state;
- r.cells[NAME] = rrow.name;
- }
- });
- }
-
- return (
- <React.Fragment>
- <Table
- className="edge-table"
- variant={TableVariant.compact}
- onSelect={this.onSelect}
- cells={columns}
- rows={this.rows}
- >
- <TableHeader />
- <TableBody />
- </Table>
- </React.Fragment>
- );
- }
- return <EmptyEdgeClassTable handleAddEdge={this.props.handleAddEdge} />;
- };
-
- handleChangeFilter = filterText => {
- let { page } = this.state;
- if (filterText !== "") {
- page = 1;
- }
- this.setState({ filterText, page });
- };
-
- genToolbar = () => {
- if (this.props.rows.length > 0) {
- return (
- <React.Fragment>
- <label>Edge namespaces</label>
- <EdgeTableToolbar
- handleAddEdge={this.props.handleAddEdge}
- handleDeleteEdge={this.props.handleDeleteEdge}
- handleChangeFilter={this.handleChangeFilter}
- toggleAlphaSort={this.toggleAlphaSort}
- filterText={this.state.filterText}
- sortDown={this.state.sortDown}
- rows={this.props.rows}
- />
- </React.Fragment>
- );
- }
- };
-
- onSetPage = (_event, pageNumber) => {
- this.setState({
- page: pageNumber
- });
- };
-
- onPerPageSelect = (_event, perPage) => {
- this.setState({
- perPage
- });
- };
-
- genPagination = () => {
- if (this.props.rows.length > 0) {
- return (
- <EdgeTablePagination
- rows={this.props.rows.length}
- perPage={this.state.perPage}
- page={this.state.page}
- onPerPageSelect={this.onPerPageSelect}
- onSetPage={this.onSetPage}
- />
- );
- }
- };
- render() {
- return (
- <React.Fragment>
- {this.genToolbar()}
- {this.genTable()}
- {this.genPagination()}
- </React.Fragment>
- );
- }
-}
-
-export default EdgeTable;
diff --git a/console/react/src/empty-edge-class-table.js b/console/react/src/empty-edge-class-table.js
deleted file mode 100644
index a4defc1..0000000
--- a/console/react/src/empty-edge-class-table.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from "react";
-import {
- Title,
- Button,
- EmptyState,
- EmptyStateVariant,
- EmptyStateIcon,
- EmptyStateBody,
- EmptyStateSecondaryActions
-} from "@patternfly/react-core";
-import { CubesIcon } from "@patternfly/react-icons";
-
-class EmptyEdgeClassTable extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
-
- render() {
- return (
- <EmptyState variant={EmptyStateVariant.small}>
- <EmptyStateIcon icon={CubesIcon} />
- <Title headingLevel="h5" size="lg">
- No edge namespaces
- </Title>
- <EmptyStateBody>
- This edge class does not contain any edge namespaces yet.
- </EmptyStateBody>
- <EmptyStateSecondaryActions>
- <Button
- variant="primary"
- aria-label="Add"
- onClick={this.props.handleAddEdge}
- >
- Add an edge namespace
- </Button>
- </EmptyStateSecondaryActions>
- </EmptyState>
- );
- }
-}
-
-export default EmptyEdgeClassTable;
diff --git a/console/react/src/empty-selection.js b/console/react/src/empty-selection.js
deleted file mode 100644
index 73a6eef..0000000
--- a/console/react/src/empty-selection.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import React from "react";
-import {
- Title,
- EmptyState,
- EmptyStateVariant,
- EmptyStateIcon,
- EmptyStateBody
-} from "@patternfly/react-core";
-import { CubesIcon } from "@patternfly/react-icons";
-
-class EmptySelection extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
-
- render() {
- return (
- <EmptyState variant={EmptyStateVariant.small} className="empty-selection">
- <EmptyStateIcon icon={CubesIcon} />
- <Title headingLevel="h5" size="lg">
- Nothing selected
- </Title>
- <EmptyStateBody>
- Select a cluster, edge class, or line between them to view/edit their
- information.
- </EmptyStateBody>
- </EmptyState>
- );
- }
-}
-
-export default EmptySelection;
diff --git a/console/react/src/field-details.js b/console/react/src/field-details.js
deleted file mode 100644
index f2318eb..0000000
--- a/console/react/src/field-details.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import React from "react";
-import {
- ActionGroup,
- Button,
- ClipboardCopy,
- Form,
- FormGroup,
- TextInput,
- Radio
-} from "@patternfly/react-core";
-import EdgeTable from "./edge-table";
-import Graph from "./graph";
-import Confirm from "./confirm";
-
-class FieldDetails extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
- }
-
- currentNode = () => {
- let current = this.props.networkInfo.nodes.find(
- n => n.key === this.props.selectedKey
- );
- return current;
- };
-
- formElement = (field, currentNode) => {
- if (field.type === "text") {
- let isRequired = field.isRequired;
- if (typeof field.isRequired === "function")
- isRequired = field.isRequired(
- field.title,
- this.props.networkInfo,
- this.props.selectedKey
- );
- return (
- <TextInput
- isRequired={isRequired}
- type="text"
- id={field.title}
- name={field.title}
- aria-describedby="simple-form-name-helper"
- value={currentNode[field.title]}
- onChange={newVal =>
- this.props.handleEditField(newVal, field.title, currentNode.key)
- }
- />
- );
- }
- if (field.type === "radio") {
- return field.options.map((o, i) => {
- return (
- <Radio
- key={`key-radio-${o}-${i}`}
- id={o}
- value={o}
- name={field.title}
- label={o}
- aria-label={o}
- onChange={() => this.props.handleRadioChange(o, field.title)}
- isChecked={currentNode[field.title] === o}
- />
- );
- });
- } else if (field.type === "states") {
- return field.options.map((o, i) => {
- let yaml = <div className="state-placeholder"></div>;
- if (currentNode.yaml && i === 1) {
- yaml = (
- <ClipboardCopy
- className="state-copy"
- onClick={(event, text) => {
- const clipboard = event.currentTarget.parentElement;
- const el = document.createElement("input");
- el.value = JSON.stringify(currentNode.yaml);
- clipboard.appendChild(el);
- el.select();
- document.execCommand("copy");
- clipboard.removeChild(el);
- }}
- />
- );
- }
- return (
- <div
- className="state-container"
- key={`key-checkbox-${o}-${i}`}
- id={`${field.title}-${i}`}
- >
- {yaml}
- <Graph
- id={`State-${i}`}
- thumbNail={true}
- legend={true}
- dimensions={{ width: 30, height: 30 }}
- nodes={[
- {
- key: `legend-key-${i}`,
- r: 10,
- type: "interior",
- state: i,
- x: 0,
- y: 0
- }
- ]}
- links={[]}
- notifyCurrentRouter={() => {}}
- />
- <div className="state-text">{o}</div>
- </div>
- );
- });
- } else if (field.type === "label") {
- const currentLink = this.props.networkInfo.links.find(
- n => n.key === this.props.selectedKey
- );
- return (
- <span className="link-label">
- {typeof currentLink[field.title] === "function"
- ? currentLink[field.title]()
- : currentLink[field.title]}
- </span>
- );
- }
- };
-
- extra = currentNode => {
- if (this.props.details.extra) {
- return (
- <EdgeTable
- rows={currentNode.rows}
- networkInfo={this.props.networkInfo}
- handleAddEdge={this.props.handleAddEdge}
- handleDeleteEdge={this.props.handleDeleteEdge}
- handleEdgeNameChange={this.props.handleEdgeNameChange}
- handleSelectEdgeRow={this.props.handleSelectEdgeRow}
- />
- );
- }
- };
-
- render() {
- const currentNode = this.currentNode();
- return (
- <Form
- onSubmit={e => {
- e.preventDefault();
- return false;
- }}
- >
- <h1>{this.props.details.title}</h1>
- <ActionGroup>
- {this.props.details.actions.map(action => {
- if (action.confirm) {
- return (
- <Confirm
- key={action.title}
- handleConfirm={action.onClick}
- variant="secondary"
- isDeleteDisabled={
- action.isDisabled
- ? action.isDisabled(
- action.title,
- this.props.networkInfo,
- this.props.selectedKey
- )
- : false
- }
- buttonText={action.title}
- title={`Confirm ${action.title}`}
- >
- <h2>Are you sure?</h2>
- </Confirm>
- );
- } else
- return (
- <Button
- key={action.title}
- variant="secondary"
- onClick={action.onClick}
- isDisabled={
- action.isDisabled
- ? action.isDisabled(
- action.title,
- this.props.networkInfo,
- this.props.selectedKey
- )
- : false
- }
- >
- {action.title}
- </Button>
- );
- })}
- </ActionGroup>
- {this.props.details.fields.map(field => {
- return (
- <FormGroup
- key={field.title}
- label={field.title}
- isRequired={
- typeof field.isRequired === "function"
- ? field.isRequired(
- field.title,
- this.props.networkInfo,
- this.props.selectedKey
- )
- : field.isRequired
- }
- fieldId={field.title}
- helperText={field.help}
- isInline={field.type === "radio"}
- >
- {this.formElement(field, currentNode)}
- </FormGroup>
- );
- })}
- <FormGroup fieldId="extra">{this.extra(currentNode)}</FormGroup>
- </Form>
- );
- }
-}
-
-export default FieldDetails;
diff --git a/console/react/src/graph.js b/console/react/src/graph.js
deleted file mode 100644
index d629d1d..0000000
--- a/console/react/src/graph.js
+++ /dev/null
@@ -1,358 +0,0 @@
-import React from "react";
-
-import * as d3 from "d3";
-import { addDefs } from "./nodes";
-
-class Graph extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {};
- this.force = d3.layout
- .force()
- .size([this.props.dimensions.width, this.props.dimensions.height])
- .linkDistance(l => {
- if (this.props.thumbNail) return 40;
- else if (l.type === "router") return 150;
- else if (l.type === "edge") return 20;
- return 50;
- })
- .charge(-800)
- .friction(0.1)
- .gravity(0.001);
-
- this.mouse_down_position = null;
- }
-
- // called only once when the component is initialized
- componentDidMount() {
- const svg = d3.select(this.svg);
- if (!this.props.thumbNail && !this.props.legend) {
- addDefs(svg);
- }
-
- this.force.on("tick", () => {
- // after force calculation starts, call updateGraph
- // which uses d3 to manipulate the attributes,
- // and React doesn't have to go through lifecycle on each tick
- d3.select(this.svgg).call(this.updateGraph);
- });
- // call this manually to create svg circles and lines
- this.shouldComponentUpdate(this.props);
- }
-
- // called each time one of the properties changes
- shouldComponentUpdate(nextProps) {
- this.d3Graph = d3.select(this.svgg);
- this.appendNodes(this.d3Graph, nextProps, nextProps.nodes);
- this.appendLinks(this.d3Graph, nextProps.links, "connector", ".node");
- this.d3Graph.call(this.updateGraph);
-
- // warning: d3's force function modifies the nodes and links arrays
- this.force.nodes(nextProps.nodes).links(nextProps.links);
- this.force.start();
- if (nextProps) this.refresh(nextProps);
- return false;
- }
-
- appendNodes = (selection, nextProps, nodes) => {
- const subNodes = selection.selectAll(".node").data(nodes, node => node.key);
- subNodes
- .enter()
- .append("g")
- .attr("class", "node")
- .attr("id", n => n.key)
- .call(selection => this.enterNode(selection, nextProps));
- subNodes.exit().remove();
- subNodes.call(this.updateNode);
- subNodes.call(this.force.drag);
- };
-
- appendLinks = (selection, clinks, linkClass, before) => {
- const subLinks = selection
- .selectAll(`.${linkClass}`)
- .data(clinks, link => link.key);
- subLinks
- .enter()
- .insert("g", before)
- .attr("class", linkClass)
- .call(this.enterLink);
- subLinks.exit().remove();
- subLinks.call(this.updateLink);
- };
-
- // new node/nodes are present
- // append all the stuff and set the attributes that don't change
- enterNode = (selection, props) => {
- const graph = this;
- const routers = selection.filter(d => d.type === "interior");
- const edges = selection.filter(d => d.type === "edgeClass");
-
- selection.append("circle").attr("r", d => d.r);
-
- routers.append("path").attr("d", d =>
- d3.svg
- .arc()
- .innerRadius(0)
- .outerRadius(d.r)({
- startAngle: 0,
- endAngle: (d.state * 2.0 * Math.PI) / 3.0
- })
- );
-
- selection
- .classed("edgeClass", d => d.type === "edgeClass")
- .classed("interior", d => d.type === "interior");
-
- if (!props.thumbNail || props.legend) {
- selection.classed("network", true);
- }
- if (!props.thumbNail) {
- selection
- .append("text")
- .attr("x", d => d.r + 5)
- .attr("dy", ".35em")
- .text(d => d.Name);
- edges
- .append("text")
- .classed("edge-count", true)
- .attr("x", -24)
- .attr("dy", "0.35em")
- .text(""); // updated in refresh
- }
- /* // this creates an octagon
- const sqr2o2 = Math.sqrt(2.0) / 2.0;
- const points = `1 0 ${sqr2o2} ${sqr2o2} 0 1 -${sqr2o2} ${sqr2o2} -1 0 -${sqr2o2} -${sqr2o2} 0 -1 ${sqr2o2} -${sqr2o2}`;
- selection
- .filter(d => d.type === "edgeClass")
- .append("polygon")
- .attr("points", points)
- .attr("transform", `scale(60) rotate(22.5)`);
-*/
- selection
- .on("mouseover", function(n) {
- if (graph.props.thumbNail) return;
- n.over = true;
- graph.updateNode(d3.select(this));
- })
- .on("mouseout", function(n) {
- if (graph.props.thumbNail) return;
- n.over = false;
- graph.updateNode(d3.select(this));
- })
- .on("click", function(n) {
- if (graph.props.thumbNail) return;
- // if there was a selected node and it was not the one we just clicked on:
- // create a link between the selected node and the clicked on node
- if (graph.props.selectedKey && graph.props.selectedKey !== n.key) {
- graph.props.notifyCreateLink(n.key, graph.props.selectedKey);
- }
-
- // see if the node was dragged (same === false)
- const same = graph.samePos(
- d3.mouse(this.parentNode),
- graph.mouse_down_position,
- "node"
- );
- if (same) {
- if (graph.props.selectedKey === n.key) {
- graph.props.notifyCurrentRouter(null);
- } else {
- graph.props.notifyCurrentRouter(n.key);
- }
- } else {
- graph.props.notifyCurrentRouter(n.key);
- }
- graph.refresh(graph.props);
- })
- .on("mousedown", function(n) {
- graph.mouse_down_position = d3.mouse(this.parentNode);
- graph.draggingNode = true;
- })
- .on("mouseup", n => {
- if (graph.props.thumbNail) return;
- if (n.type !== "edge") n.fixed = true;
- });
- };
-
- samePos = (pos1, pos2, where) => {
- if (pos1 && pos2) {
- if (pos1[0] === pos2[0] && pos1[1] === pos2[1]) return true;
- }
- return false;
- };
-
- arcTween = (oldData, newData, arc) => {
- const copy = { ...oldData };
- return function() {
- const interpolateStartAngle = d3.interpolate(
- oldData.startAngle,
- newData.startAngle
- ),
- interpolateEndAngle = d3.interpolate(
- oldData.endAngle,
- newData.endAngle
- );
-
- return function(t) {
- copy.startAngle = interpolateStartAngle(t);
- copy.endAngle = interpolateEndAngle(t);
- return arc(copy);
- };
- };
- };
- // called each time a property changes
- // update the classes/text based on the new properties
- refresh = props => {
- if (props.thumbNail) return;
- d3.selectAll("g.node.network").classed(
- "selected",
- d => d.key === props.selectedKey
- );
-
- // update the interior node state
- d3.selectAll("g.node.network.interior path").attr("d", d =>
- d3.svg
- .arc()
- .innerRadius(0)
- .outerRadius(d.r)({
- startAngle: 0,
- endAngle: (d.state * 2.0 * Math.PI) / 3.0
- })
- );
-
- d3.selectAll("svg text").each(function(d) {
- d3.select(this).text(d.Name);
- });
- d3.selectAll("g.connector").classed(
- "selected",
- d => d.key === props.selectedKey
- );
- d3.selectAll("text.edge-count").text(d => {
- return d.rows.length > 0 ? `Edges: ${d.rows.length}` : "";
- });
- };
-
- // update the node's positions
- updateNode = selection => {
- selection.attr("transform", d => {
- let container = {
- width: this.props.dimensions.width,
- height: this.props.dimensions.height
- };
- let r = 15;
- d.x = Math.max(Math.min(d.x, container.width - r), r);
- d.y = Math.max(Math.min(d.y, container.height - r), r);
- return `translate(${d.x || 0},${d.y || 0}) ${d.over ? "scale(1.1)" : ""}`;
- });
- };
-
- markerId = (link, end) => {
- return `--${end === "end" ? link.size : link.size}`;
- };
-
- // called with a selection that represents all the new links between nodes
- // here we add the lines and set their attributes
- enterLink = selection => {
- const graph = this;
-
- // add a visible line with an arrow
- selection
- .append("path")
- .classed("link", true)
- .attr("stroke-width", d => d.size)
- .attr("marker-end", d => {
- return d.right ? `url(#end--20)` : null;
- })
- .attr("marker-start", d => {
- if (d.type === "edge") return null;
- if (this.props.thumbNail) return null;
- return d.left || (!d.left && !d.right) ? `url(#start--20)` : null;
- });
-
- if (!this.props.thumbNail && !this.props.legend) {
- // add an invisible wide path to make it easier to mouseover
- selection
- .append("path")
- .classed("hittarget", true)
- .on("click", function(d) {
- d3.select(this.parentNode).classed("selected", true);
- graph.notifyCurrentConnector(d);
- graph.refresh(graph.props);
- })
- .on("mouseover", function(n) {
- d3.select(this.parentNode).classed("over", true);
- })
- .on("mouseout", function(n) {
- d3.select(this.parentNode).classed("over", false);
- });
- }
- this.refresh(this.props);
- };
-
- notifyCurrentConnector = d => {
- if (this.props.notifyCurrentConnector) this.props.notifyCurrentConnector(d);
- };
-
- // update the links' positions
- updateLink = selection => {
- const stxy = d => {
- let sx = d.source.x || this.props.nodes[d.source].x;
- let tx = d.target.x || this.props.nodes[d.target].x;
- let sy = d.source.y || this.props.nodes[d.source].y;
- let ty = d.target.y || this.props.nodes[d.target].y;
-
- if (d.source.parentKey !== d.target.parentKey) {
- const snode = this.props.nodes.find(n => n.key === d.source.parentKey);
- const tnode = this.props.nodes.find(n => n.key === d.target.parentKey);
- if (snode && tnode) {
- sx += snode.kx;
- tx += tnode.kx;
- sy += snode.ky;
- ty += tnode.ky;
- }
- }
- const deltaX = tx - sx;
- const deltaY = ty - sy;
- const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
- const normX = deltaX / dist;
- const normY = deltaY / dist;
- const sourcePadding = d.source.r || this.props.nodes[d.source].r;
- const targetPadding = d.target.r || this.props.nodes[d.target].r;
- const sourceX = sx + sourcePadding * normX;
- const sourceY = sy + sourcePadding * normY;
- const targetX = tx - targetPadding * normX;
- const targetY = ty - targetPadding * normY;
- return { x1: sourceX, y1: sourceY, x2: targetX, y2: targetY };
- };
- selection.attr("d", d => {
- const endp = stxy(d);
- return `M${endp.x1},${endp.y1}L${endp.x2},${endp.y2}`;
- });
- };
-
- // called each animation tick to update the positions
- updateGraph = selection => {
- selection.selectAll(".node").call(this.updateNode);
- selection.selectAll(".link").call(this.updateLink);
- selection.selectAll(".hittarget").call(this.updateLink);
- };
-
- render() {
- const { width, height } = this.props.dimensions;
- return (
- <React.Fragment>
- <svg
- width={width}
- height={height}
- ref={el => (this.svg = el)}
- xmlns="http://www.w3.org/2000/svg"
- >
- <g ref={el => (this.svgg = el)} />
- </svg>
- </React.Fragment>
- );
- }
-}
-
-export default Graph;
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index 82c9534..242d1d8 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -46,7 +46,7 @@ import {
import accessibleStyles from "@patternfly/patternfly/utilities/Accessibility/accessibility.css";
import { css } from "@patternfly/react-styles";
-import { BellIcon, PowerOffIcon } from "@patternfly/react-icons";
+import { PowerOffIcon } from "@patternfly/react-icons";
import DropdownMenu from "./DropdownMenu";
import ConnectPage from "./connectPage";
import DashboardPage from "./overview/dashboard/dashboardPage";
@@ -58,6 +58,7 @@ import MessageFlowPage from "./chord/chordPage";
import LogDetails from "./overview/logDetails";
import { QDRService } from "./qdrService";
import ConnectForm from "./connect-form";
+import NotificationDrawer from "./notificationDrawer";
import { utils } from "./amqp/utilities";
const avatarImg = require("./assets/img_avatar.svg");
@@ -129,6 +130,12 @@ class PageLayout extends React.Component {
this.setState({ connectPath: "", connected: false }, () => {
this.handleConnectCancel();
this.service.disconnect();
+ this.handleAddNotification(
+ "event",
+ "Manually disconnected",
+ new Date(),
+ "info"
+ );
});
} else {
const connectOptions = JSON.parse(JSON.stringify(connectInfo));
@@ -150,6 +157,7 @@ class PageLayout extends React.Component {
}
}
this.handleConnectCancel();
+ this.handleAddNotification("event", "Connected", new Date(), "info");
this.setState({
activeItem,
@@ -211,6 +219,17 @@ class PageLayout extends React.Component {
return this.state.connected;
};
+ handleAddNotification = (section, message, timestamp, severity) => {
+ if (this.notificationRef) {
+ this.notificationRef.addNotification({
+ section,
+ message,
+ timestamp,
+ severity
+ });
+ }
+ };
+
render() {
const { activeItem, activeGroup } = this.state;
const { isNavOpenDesktop, isNavOpenMobile, isMobileView } = this.state;
@@ -267,14 +286,8 @@ class PageLayout extends React.Component {
<PowerOffIcon />
</Button>
</ToolbarItem>
- <ToolbarItem>
- <Button
- id="default-example-uid-01"
- aria-label="Notifications actions"
- variant={ButtonVariant.plain}
- >
- <BellIcon />
- </Button>
+ <ToolbarItem className="notification-button">
+ <NotificationDrawer ref={el => (this.notificationRef = el)} />
</ToolbarItem>
</ToolbarGroup>
<ToolbarGroup>
@@ -340,7 +353,12 @@ class PageLayout extends React.Component {
{...(more.exact ? "exact" : "")}
render={props =>
this.state.connected ? (
- <Component service={this.service} {...props} {...more} />
+ <Component
+ service={this.service}
+ handleAddNotification={this.handleAddNotification}
+ {...props}
+ {...more}
+ />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
diff --git a/console/react/src/network-name.js b/console/react/src/network-name.js
deleted file mode 100644
index c1e817d..0000000
--- a/console/react/src/network-name.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import React from "react";
-import {
- FormGroup,
- Text,
- TextContent,
- TextInput,
- Split,
- SplitItem
-} from "@patternfly/react-core";
-
-class NetworkName extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- editing: true,
- editingName: this.props.networkInfo.name
- };
- }
-
- handleSaveClick = () => {
- let editing = !this.state.editing;
- this.setState({ editing });
- if (!editing) this.props.handleNetworkNameChange(this.state.editingName);
- };
-
- handleCancelClick = () => {
- let { editing, editingName } = this.state;
- editing = false;
- editingName = this.props.networkInfo.name;
- this.setState({ editing, editingName });
- };
-
- handleLocalEditName = editingName => {
- this.setState({ editingName });
- };
-
- render() {
- return (
- <React.Fragment>
- <Split gutter="md" className="network-name">
- <SplitItem>
- <TextContent className="enter-prompt">
- <Text component="h3">Enter a network name to get started</Text>
- </TextContent>
- </SplitItem>
- <SplitItem isFilled>
- <FormGroup label="" isRequired fieldId="simple-form-name">
- <TextInput
- className={this.state.editing ? "editing" : "not-editing"}
- isRequired
- isDisabled={!this.state.editing}
- type="text"
- id="network-name"
- name="network-name"
- aria-describedby="network-name-helper"
- value={this.state.editingName}
- onChange={this.handleLocalEditName}
- />
- </FormGroup>
- </SplitItem>
- </Split>
- </React.Fragment>
- );
- }
-}
-
-export default NetworkName;
diff --git a/console/react/src/nodes.js b/console/react/src/nodes.js
deleted file mode 100644
index 9127d6a..0000000
--- a/console/react/src/nodes.js
+++ /dev/null
@@ -1,260 +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 * as d3 from "d3";
-
-export const RouterStates = [
- "NEW",
- "READY TO DEPLOY",
- "CLUSTER HEARD FROM",
- "IN NETWORK"
-];
-
-const nodeProperties = {
- // router types
- "inter-router": {
- radius: 28,
- refX: {
- end: 32,
- start: -19
- },
- linkDistance: [150, 70],
- charge: [-1800, -900]
- },
- edge: {
- radius: 20,
- refX: {
- end: 24,
- start: -14
- },
- linkDistance: [110, 55],
- charge: [-1350, -900]
- },
- // generated nodes from connections. key is from connection.role
- normal: {
- radius: 15,
- refX: {
- end: 20,
- start: -7
- },
- linkDistance: [75, 40],
- charge: [-900, -900]
- }
-};
-// aliases
-nodeProperties._topo = nodeProperties["inter-router"];
-nodeProperties._edge = nodeProperties["edge"];
-nodeProperties["on-demand"] = nodeProperties["normal"];
-nodeProperties["route-container"] = nodeProperties["normal"];
-
-export class Nodes {
- constructor() {
- this.nodes = [];
- }
- static radius(type) {
- if (nodeProperties[type].radius) return nodeProperties[type].radius;
- return 15;
- }
- static maxRadius() {
- let max = 0;
- for (let key in nodeProperties) {
- max = Math.max(max, nodeProperties[key].radius);
- }
- return max;
- }
- static refX(end, r) {
- for (let key in nodeProperties) {
- if (nodeProperties[key].radius === parseInt(r)) {
- return nodeProperties[key].refX[end];
- }
- }
- return 0;
- }
- // return all possible values of node radii
- static discrete() {
- let values = {};
- for (let key in nodeProperties) {
- values[nodeProperties[key].radius] = true;
- }
- return Object.keys(values);
- }
- // vary the following force graph attributes based on nodeCount
- static forceScale(nodeCount, minmax) {
- let count = Math.max(Math.min(nodeCount, 80), 6);
- let x = d3.scale
- .linear()
- .domain([6, 80])
- .range(minmax);
- return x(count);
- }
- linkDistance(d, nodeCount) {
- let range = nodeProperties[d.target.nodeType].linkDistance;
- return Nodes.forceScale(nodeCount, range);
- }
- charge(d, nodeCount) {
- let charge = nodeProperties[d.nodeType].charge;
- return Nodes.forceScale(nodeCount, charge);
- }
- gravity(d, nodeCount) {
- return Nodes.forceScale(nodeCount, [0.0001, 0.1]);
- }
- setFixed(d, fixed) {
- let n = this.find(d.container, d.properties, d.name);
- if (n) {
- n.fixed = fixed;
- }
- d.setFixed(fixed);
- }
- getLength() {
- return this.nodes.length;
- }
- get(index) {
- if (index < this.getLength()) {
- return this.nodes[index];
- }
- return undefined;
- }
- nodeFor(name) {
- for (let i = 0; i < this.nodes.length; ++i) {
- if (this.nodes[i].name === name) return this.nodes[i];
- }
- return null;
- }
- nodeExists(connectionContainer) {
- return this.nodes.findIndex(function(node) {
- return node.container === connectionContainer;
- });
- }
-
- find(connectionContainer, properties, name) {
- properties = properties || {};
- for (let i = 0; i < this.nodes.length; ++i) {
- if (
- this.nodes[i].name === name ||
- this.nodes[i].container === connectionContainer
- ) {
- if (properties.product) this.nodes[i].properties = properties;
- return this.nodes[i];
- }
- }
- return undefined;
- }
- clearHighlighted() {
- for (let i = 0; i < this.nodes.length; ++i) {
- this.nodes[i].highlighted = false;
- }
- }
-}
-
-// Generate a marker for each combination of:
-// start|end, ''|selected highlighted, and each possible node radius
-export function addDefs(svg) {
- let sten = ["start", "end"];
- let states = [""];
- let radii = Nodes.discrete();
- let defs = [];
- for (let isten = 0; isten < sten.length; isten++) {
- for (let istate = 0; istate < states.length; istate++) {
- for (let iradii = 0; iradii < radii.length; iradii++) {
- defs.push({
- sten: sten[isten],
- state: states[istate],
- r: radii[iradii]
- });
- }
- }
- }
-
- svg
- .insert("svg:defs", "svg g")
- .attr("class", "marker-defs")
- .selectAll("marker")
- .data(defs)
- .enter()
- .append("svg:marker")
- .attr("id", function(d) {
- return [d.sten, d.state, d.r].join("-");
- })
- .attr("viewBox", "0 -5 10 10")
- .attr("refX", function(d) {
- return -1;
- //return Nodes.refX(d.sten, d.r);
- })
- .attr("markerWidth", 14)
- .attr("markerHeight", 14)
- .attr("markerUnits", "userSpaceOnUse")
- .attr("orient", "auto")
- .append("svg:path")
- .attr("d", function(d) {
- return d.sten === "end"
- ? "M 0 -5 L 10 0 L 0 5 z"
- : "M 10 -5 L 0 0 L 10 5 z";
- })
- .attr("fill", "#000000");
-
- addStyles(
- sten,
- {
- selected: "#33F",
- highlighted: "#6F6",
- unknown: "#888"
- },
- radii
- );
-}
-export function addGradient(svg) {
- // gradient for sender/receiver client
- let grad = svg
- .append("svg:defs")
- .append("linearGradient")
- .attr("id", "half-circle")
- .attr("x1", "0%")
- .attr("x2", "0%")
- .attr("y1", "100%")
- .attr("y2", "0%");
- grad
- .append("stop")
- .attr("offset", "50%")
- .style("stop-color", "#C0F0C0");
- grad
- .append("stop")
- .attr("offset", "50%")
- .style("stop-color", "#F0F000");
-}
-
-function addStyles(stend, stateColor, radii) {
- // the <style>
- let element = document.querySelector("style");
- // Reference to the stylesheet
- let sheet = element.sheet;
-
- let states = Object.keys(stateColor);
- // create styles for each combo of 'stend-state-radii'
- for (let istend = 0; istend < stend.length; istend++) {
- for (let istate = 0; istate < states.length; istate++) {
- let selectors = [];
- for (let iradii = 0; iradii < radii.length; iradii++) {
- selectors.push(`#${stend[istend]}-${states[istate]}-${radii[iradii]}`);
- }
- let color = stateColor[states[istate]];
- let sels = `${selectors.join(",")} {fill: ${color}; stroke: ${color};}`;
- sheet.insertRule(sels, 0);
- }
- }
-}
diff --git a/console/react/src/nodeslinks.js b/console/react/src/nodeslinks.js
deleted file mode 100644
index 0249ecb..0000000
--- a/console/react/src/nodeslinks.js
+++ /dev/null
@@ -1,144 +0,0 @@
-class NodesLinks {
- static nextNodeIndex = 1;
- static nextLinkIndex = 1;
- static nextEdgeIndex = 1;
- static nextEdgeClassIndex = 1;
- link = (s, t, i) => ({
- source: s,
- target: t,
- key: `link-${i}`,
- size: 2,
- type: "connector",
- left: true,
- right: false
- });
- setLinks = (start, count) => {
- let links = [];
- for (let i = start; i < start + count - 1; i++) {
- links.push(this.link(i, i + 1, NodesLinks.nextLinkIndex++));
- }
- return links;
- };
-
- node = (type, i, x, y) => ({
- key: `key_${type}_${i}`,
- val: i,
- r: 20,
- x: x,
- y: y,
- type: type
- });
-
- getXY = (start, count, midX, midY, rotate, displace) => {
- const ang = (start * 2.0 * Math.PI) / count + rotate;
- const x = midX + Math.cos(ang) * displace;
- const y = midY + Math.sin(ang) * displace;
- return { x, y };
- };
-
- addNodesInCircle = (nodes, start, count, midX, midY, displace, rotate) => {
- rotate = rotate || 0;
- for (let i = start; i < start + count; i++) {
- const { x, y } = this.getXY(
- i - start,
- count,
- midX,
- midY,
- rotate,
- displace
- );
- nodes.push(this.node("R", i, x, y));
- }
- };
-
- addNode = (type, networkInfo, dimensions) => {
- let i;
- if (type === "edgeClass") {
- i = NodesLinks.nextEdgeClassIndex++;
- } else {
- i = NodesLinks.nextNodeIndex++;
- }
- const x = dimensions.width / 2;
- const y = dimensions.height / 2;
- const newNode = this.node(type, i, x, y);
- newNode.type = type;
- if (type === "interior") {
- newNode.Name = `hub-${i}`;
- newNode["Route-suffix"] = "";
- newNode.Namespace = "";
- newNode.state = 0;
- } else if (type === "edgeClass") {
- newNode.Name = `EC-${i}`;
- newNode.rows = [];
- newNode.r = 60;
- }
- networkInfo.nodes.push(newNode);
- return newNode;
- };
-
- addLink = (toIndex, fromIndex, links, nodes) => {
- if (!this.linkBetween(links, toIndex, fromIndex)) {
- const link = this.link(fromIndex, toIndex, NodesLinks.nextLinkIndex++);
- if (
- nodes[toIndex].type === "interior" &&
- nodes[fromIndex].type === "interior"
- ) {
- link["connector type"] = "inter-router";
- } else {
- link["connector type"] = "edge";
- }
- link.connector = () => nodes[toIndex].Name;
- link.listener = () => nodes[fromIndex].Name;
- links.push(link);
- return link;
- }
- };
-
- getEdgeName = () => {
- return `edge-${NodesLinks.nextEdgeIndex++}`;
- };
- getEdgeKey = () => {
- return NodesLinks.nextEdgeIndex;
- };
-
- // return true if there are any links between toIndex and fromIndex
- linkBetween = (links, toIndex, fromIndex) => {
- return links.some(
- l =>
- (l.source.index === toIndex && l.target.index === fromIndex) ||
- (l.source.index === fromIndex && l.target.index === toIndex)
- );
- };
- linkIndex = (links, nodeIndex) => {
- return links.findIndex(
- l => l.source.index === nodeIndex || l.target.index === nodeIndex
- );
- };
-
- setNodesLinks = (networkInfo, dimensions) => {
- const nodes = [];
- let links = [];
- const midX = dimensions.width / 2;
- const midY = dimensions.height / 2;
- const displace = dimensions.height / 2;
- // create the routers
- // set their starting positions in a circle
- if (networkInfo.routers.length === 1) {
- nodes.push(this.node("R", 0, midX, midY));
- } else {
- this.addNodesInCircle(
- nodes,
- 0,
- networkInfo.routers.length,
- midX,
- midY,
- displace
- );
- }
- if (networkInfo.routers.length > 1)
- links = this.setLinks(0, networkInfo.routers.length);
- return { nodes, links };
- };
-}
-
-export default NodesLinks;
diff --git a/console/react/src/notificationDrawer.js b/console/react/src/notificationDrawer.js
new file mode 100644
index 0000000..0ade746
--- /dev/null
+++ b/console/react/src/notificationDrawer.js
@@ -0,0 +1,293 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { NotificationBadge } from "@patternfly/react-core";
+import { Button } from "@patternfly/react-core";
+import {
+ Accordion,
+ AccordionItem,
+ AccordionContent,
+ AccordionToggle
+} from "@patternfly/react-core";
+
+import {
+ AngleDoubleLeftIcon,
+ AngleDoubleRightIcon,
+ BellIcon,
+ TimesIcon
+} from "@patternfly/react-icons";
+
+import { safePlural } from "./qdrGlobals";
+
+class NotificationDrawer extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isShown: false, // is the drawer shown
+ expanded: false, // is the drawer wide
+ isAnyUnread: false,
+ accordionSections: {
+ action: {
+ title: "Management Actions",
+ isOpen: false,
+ events: []
+ },
+ event: { title: "Events", isOpen: false, events: [] }
+ }
+ };
+ this.severityToIcon = {
+ info: { icon: "pficon-info", color: "#313131" },
+ error: { icon: "pficon-error-circle-o", color: "red" },
+ warning: { icon: "pficon-warning-triangle-o", color: "yellow" },
+ success: { icon: "pficon-ok", color: "green" }
+ };
+ }
+
+ componentDidMount() {
+ document.addEventListener("mousedown", this.handleClickOutside);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("mousedown", this.handleClickOutside);
+ }
+
+ handleClickOutside = event => {
+ if (this.notificationRef && !this.notificationRef.contains(event.target)) {
+ // don't close if the click was on the bell icon since
+ // that icon's event handler will toggle the drawer
+ if (this.buttonRef && this.buttonRef.contains(event.target)) return;
+ this.close();
+ }
+ };
+
+ addNotification = ({ section, message, timestamp, severity }) => {
+ const { accordionSections } = this.state;
+ const event = { message, timestamp, severity };
+ event.date = timestamp.toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit"
+ });
+ event.time = timestamp.toLocaleTimeString(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ event.isRead = false;
+ accordionSections[section].events.unshift(event);
+ this.setState({ accordionSections, isAnyUnread: true });
+ };
+
+ close = () => {
+ this.setState({ isShown: false });
+ };
+
+ toggleDrawer = sectionKey => {
+ const { accordionSections } = this.state;
+ accordionSections[sectionKey].isOpen = !accordionSections[sectionKey]
+ .isOpen;
+ this.setState(accordionSections);
+ };
+
+ toggleExpand = () => {
+ this.setState({ expanded: !this.state.expanded });
+ };
+
+ toggle = () => {
+ this.setState({ isShown: !this.state.isShown });
+ };
+
+ setAnyUnread = accordionSections => {
+ let isAnyUnread = false;
+ for (let sectionKey in accordionSections) {
+ isAnyUnread =
+ isAnyUnread ||
+ accordionSections[sectionKey].events.some(e => !e.isRead);
+ }
+ this.setState({ accordionSections, isAnyUnread });
+ };
+
+ markAsRead = event => {
+ event.isRead = true;
+ const { accordionSections } = this.state;
+ this.setAnyUnread(accordionSections);
+ };
+
+ clearAll = sectionKey => {
+ const { accordionSections } = this.state;
+ accordionSections[sectionKey].events = [];
+ this.setAnyUnread(accordionSections);
+ };
+
+ markAllRead = sectionKey => {
+ const { accordionSections } = this.state;
+ accordionSections[sectionKey].events.forEach(e => (e.isRead = true));
+ this.setAnyUnread(accordionSections);
+ };
+
+ hasUnread = section => {
+ return !section.events.some(e => e.isRead);
+ };
+
+ render() {
+ const DrawerTitle = (
+ <div className="drawer-pf-title">
+ <Button variant="plain" aria-label="expand" onClick={this.toggleExpand}>
+ {this.state.expanded ? (
+ <AngleDoubleRightIcon />
+ ) : (
+ <AngleDoubleLeftIcon />
+ )}
+ </Button>
+ <h3 className="text-center">Notifications Drawer</h3>
+ <Button variant="plain" aria-label="close" onClick={this.close}>
+ <TimesIcon />
+ </Button>
+ </div>
+ );
+
+ const severityIcon = event => {
+ return (
+ <i
+ className={`pf pficon ${this.severityToIcon[event.severity].icon}`}
+ style={{ color: this.severityToIcon[event.severity].color }}
+ ></i>
+ );
+ };
+
+ return (
+ <>
+ <div ref={el => (this.buttonRef = el)}>
+ <NotificationBadge
+ isRead={!this.state.isAnyUnread}
+ onClick={this.toggle}
+ aria-label="Notifications"
+ >
+ <BellIcon />
+ </NotificationBadge>
+ </div>
+ {this.state.isShown && (
+ <div
+ ref={el => (this.notificationRef = el)}
+ id="NotificationDrawer"
+ className={`drawer-pf drawer-pf-notifications-non-clickable
+ ${this.state.expanded ? "expanded" : "compact"}`}
+ >
+ {DrawerTitle}
+ <Accordion>
+ {Object.keys(this.state.accordionSections).map(sectionKey => {
+ const section = this.state.accordionSections[sectionKey];
+ return (
+ <AccordionItem key={`${sectionKey}-item`}>
+ <AccordionToggle
+ onClick={() => this.toggleDrawer(sectionKey)}
+ isExpanded={section.isOpen}
+ key={sectionKey}
+ id={sectionKey}
+ >
+ {section.title}
+ <span className="panel-counter">{`${
+ section.events.filter(e => !e.isRead).length
+ } new ${safePlural(
+ section.events.filter(e => !e.isRead).length,
+ "event"
+ )}`}</span>
+ </AccordionToggle>
+ <AccordionContent
+ key={`${sectionKey}-content`}
+ isHidden={!section.isOpen}
+ isFixed
+ >
+ <div
+ className={`panel-body ${
+ section.events.length === 0 ? "hidden" : ""
+ }`}
+ >
+ {section.events.map((event, i) => {
+ return (
+ <div
+ key={`${sectionKey}-event-${i}`}
+ className={`drawer-pf-notification ${
+ event.isRead ? "" : "unread"
+ }`}
+ onClick={() => this.markAsRead(event)}
+ >
+ {severityIcon(event)}
+ <div className="drawer-pf-notification-content">
+ <span className="drawer-pf-notification-message">
+ {event.message}
+ </span>
+ <div className="drawer-pf-notification-info">
+ <span className="date">{event.date}</span>
+ <span className="time">{event.time}</span>
+ </div>
+ </div>
+ </div>
+ );
+ })}
+ </div>
+ <div
+ className={`blank-slate-pf ${
+ section.events.length === 0 ? "" : "hidden"
+ }`}
+ >
+ <div className="blank-slate-pf-icon">
+ <span className="pficon pficon-info"></span>
+ </div>
+ <h1>There are no notifications to display.</h1>
+ </div>
+ <div
+ className={`drawer-pf-action ${
+ section.events.length > 0 ? "" : "hidden"
+ }`}
+ >
+ <div className="drawer-pf-action-link">
+ <button
+ className={`btn btn-link ${
+ this.hasUnread(section) ? "" : "disabled"
+ }`}
+ onClick={() => this.markAllRead(sectionKey)}
+ >
+ Mark All Read
+ </button>
+ </div>
+ <div className="drawer-pf-action-link">
+ <button
+ className="btn btn-link"
+ onClick={() => this.clearAll(sectionKey)}
+ >
+ <span className="pficon pficon-close"></span>
+ Clear All
+ </button>
+ </div>
+ </div>
+ </AccordionContent>
+ </AccordionItem>
+ );
+ })}
+ </Accordion>
+ </div>
+ )}
+ </>
+ );
+ }
+}
+
+export default NotificationDrawer;
diff --git a/console/react/src/qdrGlobals.js b/console/react/src/qdrGlobals.js
index a121bfb..8dc2b47 100644
--- a/console/react/src/qdrGlobals.js
+++ b/console/react/src/qdrGlobals.js
@@ -63,3 +63,14 @@ export var getConfigVars = () =>
document.title = s.QDR_CONSOLE_TITLE;
resolve(s);
});
+
+export const safePlural = (count, str) => {
+ if (count === 1) return str;
+ var es = ["x", "ch", "ss", "sh"];
+ for (var i = 0; i < es.length; ++i) {
+ if (str.endsWith(es[i])) return str + "es";
+ }
+ if (str.endsWith("y")) return str.substr(0, str.length - 2) + "ies";
+ if (str.endsWith("s")) return str;
+ return str + "s";
+};
diff --git a/console/react/src/show-d3-svg.js b/console/react/src/show-d3-svg.js
deleted file mode 100644
index 4144b04..0000000
--- a/console/react/src/show-d3-svg.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import React from "react";
-import Graph from "./graph";
-
-class ShowD3SVG extends React.Component {
- constructor(props) {
- super(props);
- this.state = this.setNodesLinks();
- }
-
- // if the number of routers has changed
- // recreate the nodes and links arrays
- componentDidUpdate(prevProps) {
- if (this.props.routers !== prevProps.routers) {
- this.setState(this.addClients(this.setNodesLinks()));
- }
- }
-
- addClients = state => {
- const { nodes, links } = state;
- const midX = this.props.dimensions.width / 2;
- const midY = this.props.dimensions.height / 2;
- for (const r in this.props.routerInfo) {
- if (this.props.routerInfo.hasOwnProperty(r)) {
- const info = this.props.routerInfo[r];
- const parent = nodes.find(n => n.name === r);
- if (parent) {
- // addNodesInCircle = (nodes, start, count, midX, midY, displace, rotate) => {
- info.forEach((inf, i) => {
- const node = this.node(nodes.length, midX, midY);
- node.parent = parent.val;
- node.key = `${node.key}.C${i}`;
- node.name = `C${i}`;
- node.type = inf.client;
- const { x, y } = this.getXY(
- i,
- info.length,
- parent.x,
- parent.y,
- 0,
- 40
- );
- node.x = x;
- node.y = y;
- nodes.push(node);
- const l = this.link(parent.val, node.val);
- l.type = "client";
- links.push(l);
- });
- }
- }
- }
- return state;
- };
-
- link = (s, t) => ({
- source: s,
- target: t,
- key: `${s}:${t}`,
- size: 2,
- type: "router"
- });
- setLinks = (topology, start, count) => {
- let links = [];
- if (topology === "linear") {
- for (let i = start; i < start + count - 1; i++) {
- links.push(this.link(i, i + 1));
- }
- } else if (topology === "mesh") {
- for (let i = start; i < start + count - 1; i++) {
- for (let j = i + 1; j < start + count; j++) {
- links.push(this.link(i, j));
- }
- }
- } else if (topology === "star") {
- for (let i = start; i < start + count; i++) {
- links.push(this.link(start, i));
- }
- } else if (topology === "ring") {
- for (let i = start; i < start + count - 1; i++) {
- links.push(this.link(i, i + 1));
- }
- if (start + count > 2) links.push(this.link(start + count - 1, start));
- } else if (topology === "ted") {
- if (count < 3) {
- links = this.setLinks("linear", 0, count);
- } else {
- links = this.setLinks("mesh", 2, count - 2);
- links.push(this.link(0, 2));
- links.push(this.link(0, count - 1));
- links.push(this.link(1, Math.floor((count - 2) / 2) + 1));
- links.push(this.link(1, Math.floor((count - 2) / 2) + 2));
- }
- } else if (topology === "bar_bell") {
- links.push(this.link(0, Math.ceil(count / 2)));
- links = links.concat(this.setLinks("ring", 0, Math.ceil(count / 2)));
- links = links.concat(
- this.setLinks("ring", Math.ceil(count / 2), Math.floor(count / 2))
- );
- } else if (topology === "random") {
- // random int from min to max inclusive
- let randomIntFromInterval = (min, max) =>
- Math.floor(Math.random() * (max - min + 1) + min);
- // are two nodes already connected
- let isConnected = (s, t) => {
- if (s === t) return true;
- return links.some(l => {
- return (
- (l.source === s && l.target === t) ||
- (l.target === s && l.source === t)
- );
- });
- };
-
- // connect all nodes
- for (let i = 1; i < this.props.routers; i++) {
- let source = randomIntFromInterval(0, i - 1);
- links.push(this.link(source, i));
- }
- // randomly add n-1 connections
- for (let i = 0; i < this.props.routers - 1; i++) {
- let source = randomIntFromInterval(0, this.props.routers - 1);
- let target = randomIntFromInterval(0, this.props.routers - 1);
- if (!isConnected(source, target)) {
- links.push(this.link(source, target));
- }
- }
- }
- return links;
- };
-
- node = (i, x, y) => ({
- key: `key_${i}`,
- name: `R${i}`,
- val: i,
- size: this.props.radius ? this.props.radius : 15,
- x: x,
- y: y,
- parentKey: "cluster",
- r: 8
- });
-
- getXY = (start, count, midX, midY, rotate, displace) => {
- const ang = (start * 2.0 * Math.PI) / count + rotate;
- const x = midX + Math.cos(ang) * displace;
- const y = midY + Math.sin(ang) * displace;
- return { x, y };
- };
-
- addNodesInCircle = (nodes, start, count, midX, midY, displace, rotate) => {
- rotate = rotate || 0;
- for (let i = start; i < start + count; i++) {
- const { x, y } = this.getXY(
- i - start,
- count,
- midX,
- midY,
- rotate,
- displace
- );
- nodes.push(this.node(i, x, y));
- }
- };
- setNodesLinks = () => {
- const nodes = [];
- let links = [];
- const midX = this.props.dimensions.width / 2;
- const midY = this.props.dimensions.height / 2;
- const displace = this.props.dimensions.height / 2;
-
- // create the routers
- // set their starting positions in a circle
- if (this.props.topology === "ted" && this.props.routers > 1) {
- nodes.push(this.node(0, this.props.dimensions.width, midY));
- nodes.push(this.node(1, 0, midY));
- this.addNodesInCircle(
- nodes,
- 2,
- this.props.routers - 2,
- midX,
- midY,
- displace,
- Math.PI / (this.props.routers - 2)
- );
- } else if (this.props.topology === "bar_bell" && this.props.routers > 1) {
- this.addNodesInCircle(
- nodes,
- 0, // start
- Math.ceil(this.props.routers / 2), // count
- this.props.dimensions.width / 4, // midX
- midY, // midY
- displace / 3 // displace
- );
- this.addNodesInCircle(
- nodes,
- Math.ceil(this.props.routers / 2),
- Math.floor(this.props.routers / 2),
- this.props.dimensions.width * 0.75,
- midY,
- displace / 3,
- Math.PI
- );
- } else if (this.props.center && this.props.routers > 1) {
- nodes.push(this.node(0, midX, midY));
- this.addNodesInCircle(
- nodes,
- 1,
- this.props.routers - 1,
- midX,
- midY,
- displace
- );
- } else if (this.props.routers === 1) {
- nodes.push(this.node(0, midX, midY));
- } else {
- this.addNodesInCircle(nodes, 0, this.props.routers, midX, midY, displace);
- }
- if (this.props.routers > 1)
- links = this.setLinks(this.props.topology, 0, this.props.routers);
- return { nodes: nodes, links: links };
- };
-
- render() {
- const { nodes, links } = this.state;
- return (
- <Graph
- nodes={nodes}
- links={links}
- dimensions={this.props.dimensions}
- thumbNail={this.props.thumbNail}
- notifyCurrentRouter={this.props.notifyCurrentRouter}
- />
- );
- }
-}
-
-export default ShowD3SVG;
diff --git a/console/react/src/topology-context.js b/console/react/src/topology-context.js
deleted file mode 100644
index 85d7951..0000000
--- a/console/react/src/topology-context.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import React from "react";
-import FieldDetails from "./field-details";
-import { RouterStates } from "./nodes";
-import EmptySelection from "./empty-selection";
-
-class TopologyContext extends React.Component {
- constructor(props) {
- super(props);
- this.state = {};
-
- this.contexts = {
- interior: {
- title: "Namespace",
- fields: [
- { title: "Name", type: "text", isRequired: true },
- {
- title: "State",
- type: "states",
- options: RouterStates
- },
- {
- title: "Type",
- type: "radio",
- options: ["Kube", "okd", "OC 3.11", "OC 4.1", "unknown"]
- },
- { title: "Route-suffix", type: "text" },
- { title: "Namespace", type: "text" }
- ],
- actions: [
- {
- title: "Delete",
- onClick: this.props.handleDeleteRouter,
- confirm: true
- }
- ]
- },
- edgeClass: {
- title: "Edge class",
- fields: [{ title: "Name", type: "text", isRequired: true }],
- actions: [
- {
- title: "Delete",
- onClick: this.props.handleDeleteRouter,
- confirm: true
- }
- ],
- extra: { title: "Edge namespaces", type: "edgeTable" }
- },
- edge: {
- title: "Edge namespace",
- fields: [{ title: "Name", type: "text", isRequired: true }],
- actions: [
- {
- title: "Delete",
- onClick: this.props.handleDeleteRouter,
- confirm: true
- }
- ]
- },
- connector: {
- title: "Connection",
- fields: [
- { title: "connector type", type: "label" },
- { title: "connector", type: "label" },
- { title: "listener", type: "label" }
- ],
- actions: [
- {
- title: "Delete",
- onClick: this.props.handleDeleteConnection,
- confirm: true
- },
- {
- title: "Reverse",
- onClick: this.props.handleReverseConnection,
- isDisabled: this.isActionDisabled
- }
- ]
- }
- };
- }
-
- isActionDisabled = (title, networkInfo, selectedKey) => {
- if (title === "Reverse") {
- const currentLink = networkInfo.links.find(l => l.key === selectedKey);
- if (currentLink) {
- if (currentLink["connector type"] === "edge") return true;
- }
- }
- return false;
- };
-
- isRequired = (title, networkInfo, selectedKey) => {
- // if there are any links going to this node, suffix and namespace are required
- if (title === "Route-suffix" || title === "Namespace")
- return networkInfo.links.some(l => l.source.key === selectedKey);
- return false;
- };
-
- render() {
- let currentContext = null;
- const currentNode = this.props.networkInfo.nodes.find(
- n => n.key === this.props.selectedKey
- );
- if (currentNode) {
- currentContext = this.contexts[currentNode.type];
- } else {
- const currentLink = this.props.networkInfo.links.find(
- l => l.key === this.props.selectedKey
- );
- if (currentLink) {
- currentContext = this.contexts[currentLink.type];
- }
- }
-
- if (!currentContext) {
- return (
- <div>
- <EmptySelection />
- </div>
- );
- }
- return (
- <FieldDetails
- details={currentContext}
- networkInfo={this.props.networkInfo}
- selectedKey={this.props.selectedKey}
- handleEditField={this.props.handleEditField}
- handleAddEdge={this.props.handleAddEdge}
- handleDeleteEdge={this.props.handleDeleteEdge}
- handleEdgeNameChange={this.props.handleEdgeNameChange}
- handleSelectEdgeRow={this.props.handleSelectEdgeRow}
- handleRadioChange={this.props.handleRadioChange}
- />
- );
- }
-}
-
-export default TopologyContext;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org