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