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/12 17:42:45 UTC
[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Added schema,
entity operations, notifications
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 632aacf Added schema, entity operations, notifications
632aacf is described below
commit 632aacf045c8e4d9595ca8c7302aa01b43a4218d
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Tue Nov 12 12:42:25 2019 -0500
Added schema, entity operations, notifications
---
console/react/public/config.json | 3 +
console/react/src/App.css | 139 ++++++++-
console/react/src/App.js | 2 +-
console/react/src/alertList.js | 71 +++++
console/react/src/config.json | 3 -
console/react/src/connect-form.js | 45 ++-
console/react/src/connectPage.js | 3 +-
console/react/src/connecting.js | 30 ++
console/react/src/connectionClose.js | 7 +-
console/react/src/contextMenuComponent.js | 53 +---
.../connectionData.js => createEntity.js} | 31 +-
console/react/src/details/createTablePage.js | 336 ++++++++++++++++++++
.../dataSources/{connectionData.js => autoLink.js} | 19 +-
.../src/details/dataSources/connectionData.js | 11 +
.../react/src/details/dataSources/defaultData.js | 82 ++++-
console/react/src/details/dataSources/linkData.js | 8 +-
console/react/src/details/dataSources/logsData.js | 161 +---------
.../react/src/details/dataSources/routerData.js | 3 -
console/react/src/details/deleteEntity.js | 143 +++++++++
console/react/src/details/enitiesPage.js | 83 ++++-
console/react/src/details/entityData.js | 6 +-
console/react/src/details/entityListTable.js | 83 ++++-
console/react/src/details/schema/schemaPage.js | 226 ++++++++++++++
.../connectionData.js => updateEntity.js} | 33 +-
console/react/src/details/updateTablePage.js | 337 +++++++++++++++++++++
console/react/src/detailsTablePage.js | 34 ++-
console/react/src/index.js | 14 +-
console/react/src/layout.js | 91 +++---
console/react/src/notificationDrawer.js | 18 +-
.../react/src/overview/dashboard/dashboardPage.js | 5 +-
.../overview/dashboard/delayedDeliveriesCard.js | 6 +-
console/react/src/pleaseWait.js | 51 ++++
console/react/src/qdrGlobals.js | 10 -
console/react/src/tableToolbar.jsx | 34 +--
console/react/yarn.lock | 36 +--
python/qpid_dispatch_internal/dispatch.py | 1 +
36 files changed, 1793 insertions(+), 425 deletions(-)
diff --git a/console/react/public/config.json b/console/react/public/config.json
new file mode 100644
index 0000000..6b19668
--- /dev/null
+++ b/console/react/public/config.json
@@ -0,0 +1,3 @@
+{
+ "title": "Apache Qpid Dispach Console"
+}
diff --git a/console/react/src/App.css b/console/react/src/App.css
index b1eff31..4e4240b 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -1001,7 +1001,7 @@ div.qdrChord .legend-text {
background-color: transparent;
color: blue;
font-weight: bold;
- white-space: normal;
+ white-space: nowrap;
padding-left: 0;
}
@@ -1140,19 +1140,20 @@ div.details-table ul.entities-list {
span.entity-type i {
padding-right: 1em;
font-style: normal;
+ font-family: FontAwesome;
}
span.entity-type i.address-local:before {
- font-family: FontAwesome;
content: "\f0ac";
}
span.entity-type i.address-mobile:before {
- font-family: FontAwesome;
content: "\f109";
}
span.entity-type i.address-router:before {
- font-family: FontAwesome;
content: "\f047";
}
+span.entity-type i.address-topo:before {
+ content: "\f126";
+}
span.entity-type i.link-type-endpoint:before {
content: "\f109";
@@ -1297,3 +1298,133 @@ span.entity-type i.link-type-router-control:before {
font-weight: bold;
color: black;
}
+
+.details-table-header {
+ display: flex;
+ justify-content: space-between;
+}
+
+.detail-action-button {
+ margin-right: 1em;
+}
+
+#update-form .pf-c-form__helper-text {
+ text-align: left;
+}
+
+#alert-list-container {
+ position: absolute;
+ right: 6em;
+ top: 3em;
+}
+
+/* login form */
+.spinning-clockwise {
+ -webkit-animation: spinc 4s linear infinite;
+ -moz-animation: spinc 4s linear infinite;
+ animation: spinc 4s linear infinite;
+}
+@-moz-keyframes spinc {
+ 100% {
+ -moz-transform: rotate(360deg);
+ }
+}
+@-webkit-keyframes spinc {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+@keyframes spinc {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+.spinning-cclockwise {
+ -webkit-animation: spincc 4s linear infinite;
+ -moz-animation: spincc 4s linear infinite;
+ animation: spincc 4s linear infinite;
+}
+@-moz-keyframes spincc {
+ 100% {
+ -moz-transform: rotate(-360deg);
+ }
+}
+@-webkit-keyframes spincc {
+ 100% {
+ -webkit-transform: rotate(-360deg);
+ }
+}
+@keyframes spincc {
+ 100% {
+ -webkit-transform: rotate(-360deg);
+ transform: rotate(-360deg);
+ }
+}
+
+#topicCogWrapper {
+ position: relative;
+ margin: auto;
+ width: 5.5em;
+ height: 5em;
+}
+#topicCogMain {
+ width: 4em;
+ height: 4em;
+ position: absolute;
+ top: 0.5em;
+ left: 0;
+}
+
+#topicCogUpper {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 2em;
+ height: 2em;
+}
+#topicCogLower {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 2em;
+ height: 2em;
+}
+
+.topic-creating-wrapper {
+ text-align: center;
+ position: absolute;
+ top: 10em;
+ right: 10em;
+}
+
+.topic-creating-message {
+ margin-top: 2em;
+}
+
+div.connecting {
+ opacity: 0.2;
+}
+
+/* schema page */
+
+.list-group-item-heading {
+ text-align: left;
+}
+
+.list-group-item-text {
+ text-align: left;
+}
+
+.list-view-pf-description {
+ display: block;
+}
+
+.list-group-item-fqt {
+ font-weight: bold;
+ font-size: 14px;
+}
+
+#schema-page .pficon.list-view-pf-icon-sm {
+ border: 0;
+}
diff --git a/console/react/src/App.js b/console/react/src/App.js
index 6f66bb7..ff8b629 100644
--- a/console/react/src/App.js
+++ b/console/react/src/App.js
@@ -13,7 +13,7 @@ class App extends Component {
render() {
return (
<div className="App pf-m-redhat-font">
- <PageLayout />
+ <PageLayout config={this.props.config} />
</div>
);
}
diff --git a/console/react/src/alertList.js b/console/react/src/alertList.js
new file mode 100644
index 0000000..36c8ecf
--- /dev/null
+++ b/console/react/src/alertList.js
@@ -0,0 +1,71 @@
+/*
+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 { Alert, AlertActionCloseButton } from "@patternfly/react-core";
+
+class AlertList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ alerts: []
+ };
+ this.nextIndex = 0;
+ }
+
+ hideAlert = alert => {
+ const { alerts } = this.state;
+ const index = alerts.findIndex(a => a.key === alert.key);
+ if (index >= 0) alerts.splice(index, 1);
+ this.setState({ alerts });
+ };
+
+ addAlert = (severity, message) => {
+ const { alerts } = this.state;
+ const alert = { key: this.nextIndex++, type: severity, message };
+ const self = this;
+ setTimeout(() => self.hideAlert(alert), 5000);
+ alerts.unshift(alert);
+ this.setState({ alerts });
+ };
+
+ render() {
+ return (
+ <div id="alert-list-container">
+ {this.state.alerts.map((alert, i) => (
+ <Alert
+ key={`alert-${i}`}
+ variant={alert.type}
+ title={alert.type}
+ isInline
+ action={
+ <AlertActionCloseButton onClose={() => this.hideAlert(alert)} />
+ }
+ >
+ {alert.message.length > 40
+ ? `${alert.message.substr(0, 40)}...`
+ : alert.message}
+ </Alert>
+ ))}
+ </div>
+ );
+ }
+}
+
+export default AlertList;
diff --git a/console/react/src/config.json b/console/react/src/config.json
deleted file mode 100644
index 6f7d7a7..0000000
--- a/console/react/src/config.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "title": "The Apache Qpid Dispach Console"
-}
diff --git a/console/react/src/connect-form.js b/console/react/src/connect-form.js
index a7a9871..37f4f2e 100644
--- a/console/react/src/connect-form.js
+++ b/console/react/src/connect-form.js
@@ -24,7 +24,7 @@ import {
Text,
TextVariants
} from "@patternfly/react-core";
-
+import PleaseWait from "./pleaseWait";
const CONNECT_KEY = "QDRSettings";
class ConnectForm extends React.Component {
@@ -36,7 +36,9 @@ class ConnectForm extends React.Component {
port: "",
username: "",
password: "",
- isShown: this.props.isConnectFormOpen
+ isShown: this.props.isConnectFormOpen,
+ connecting: false,
+ connectError: null
};
}
@@ -70,7 +72,28 @@ class ConnectForm extends React.Component {
};
handleConnect = () => {
- this.props.handleConnect(this.props.fromPath, this.state);
+ if (this.props.isConnected) {
+ // handle disconnects from the main page
+ this.props.handleConnect(this.props.fromPath);
+ } else {
+ const connectOptions = JSON.parse(JSON.stringify(this.state));
+ if (connectOptions.username === "") connectOptions.username = undefined;
+ if (connectOptions.password === "") connectOptions.password = undefined;
+ connectOptions.reconnect = true;
+
+ this.setState({ connecting: true }, () => {
+ this.props.service.connect(connectOptions).then(
+ r => {
+ this.setState({ connecting: false });
+ this.props.handleConnect(this.props.fromPath, r);
+ },
+ e => {
+ console.log(e);
+ this.setState({ connecting: false, connectError: e.msg });
+ }
+ );
+ });
+ }
};
toggleDrawerHide = () => {
@@ -78,12 +101,19 @@ class ConnectForm extends React.Component {
};
render() {
- const { isShown, address, port, username, password } = this.state;
+ const {
+ isShown,
+ address,
+ port,
+ username,
+ password,
+ connecting
+ } = this.state;
return isShown ? (
<div>
<div className="connect-modal">
- <div className="">
+ <div className={connecting ? "connecting" : ""}>
<Form isHorizontal>
<TextContent className="connect-title">
<Text component={TextVariants.h1}>Connect</Text>
@@ -161,6 +191,11 @@ class ConnectForm extends React.Component {
</ActionGroup>
</Form>
</div>
+ <PleaseWait
+ isOpen={connecting}
+ title="Connecting"
+ message="Connecting to the router, please wait..."
+ />
</div>
</div>
) : null;
diff --git a/console/react/src/connectPage.js b/console/react/src/connectPage.js
index 5975c4c..32688f6 100644
--- a/console/react/src/connectPage.js
+++ b/console/react/src/connectPage.js
@@ -47,6 +47,7 @@ class ConnectPage extends React.Component {
{showForm ? (
<ConnectForm
prefix="form"
+ service={this.props.service}
handleConnect={this.props.handleConnect}
handleConnectCancel={this.handleConnectCancel}
fromPath={from.pathname}
@@ -58,7 +59,7 @@ class ConnectPage extends React.Component {
<div className="left-content">
<TextContent>
<Text component="h1" className="console-banner">
- Apache Qpid Dispatch Console
+ {this.props.config.title}
</Text>
</TextContent>
<TextContent>
diff --git a/console/react/src/connecting.js b/console/react/src/connecting.js
new file mode 100644
index 0000000..0d24c3a
--- /dev/null
+++ b/console/react/src/connecting.js
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Red Hat Inc.
+ *
+ * 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.
+ */
+import React from "react";
+
+class Connecting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {};
+ }
+
+ render() {
+ return <div id="connecting">Connecting</div>;
+ }
+}
+
+export default Connecting;
diff --git a/console/react/src/connectionClose.js b/console/react/src/connectionClose.js
index 9da3d13..b27ba05 100644
--- a/console/react/src/connectionClose.js
+++ b/console/react/src/connectionClose.js
@@ -57,7 +57,7 @@ class ConnectionClose extends React.Component {
"action",
`Close connection ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`,
new Date(),
- "error"
+ "danger"
);
} else {
this.props.handleAddNotification(
@@ -85,7 +85,10 @@ class ConnectionClose extends React.Component {
if (record.role === "normal") {
return (
<React.Fragment>
- <Button className="link-button" onClick={this.handleModalToggle}>
+ <Button
+ className={`${this.props.asButton ? "" : "link-button"}`}
+ onClick={this.handleModalToggle}
+ >
Close
</Button>
<Modal
diff --git a/console/react/src/contextMenuComponent.js b/console/react/src/contextMenuComponent.js
index a5912a7..ffa4fba 100644
--- a/console/react/src/contextMenuComponent.js
+++ b/console/react/src/contextMenuComponent.js
@@ -6,55 +6,25 @@ class ContextMenuComponent extends React.Component {
this.state = {};
}
- componentDidMount = () => {
- this.registerHandlers();
- };
-
- componentWillUnmount = () => {
- this.unregisterHandlers();
- };
-
- registerHandlers = () => {
- document.addEventListener("mousedown", this.handleOutsideClick);
- document.addEventListener("touchstart", this.handleOutsideClick);
- document.addEventListener("scroll", this.handleHide);
- document.addEventListener("contextmenu", this.handleContextMenuEvent);
- window.addEventListener("resize", this.handleHide);
- };
-
- unregisterHandlers = () => {
- document.removeEventListener("mousedown", this.handleOutsideClick);
- document.removeEventListener("touchstart", this.handleOutsideClick);
- document.removeEventListener("scroll", this.handleHide);
- document.removeEventListener("contextmenu", this.handleContextMenuEvent);
- window.removeEventListener("resize", this.handleHide);
- };
-
- handleHide = e => {
- this.unregisterHandlers();
- this.props.handleContextHide();
- };
+ componentDidMount() {
+ document.addEventListener("mousedown", this.handleClickOutside);
+ }
- handleContextMenuEvent = e => {
- // if the event happened to an svg circle, don't hide the context menu
- if (!e.target || e.target.nodeName !== "circle") {
- this.handleHide(e);
- }
- };
+ componentWillUnmount() {
+ document.removeEventListener("mousedown", this.handleClickOutside);
+ }
- handleOutsideClick = e => {
- if (
- e.target.nodeName !== "LI" &&
- !e.target.className.includes(this.props.parentClass)
- ) {
- this.handleHide(e);
+ handleClickOutside = event => {
+ if (this.listRef && this.listRef.contains(event.target)) {
+ return;
}
+ this.props.handleContextHide();
};
proxyClick = (item, e) => {
if (item.action && item.enabled(this.props.contextEventData)) {
item.action(item, this.props.contextEventData, e);
- this.handleHide(e);
+ this.props.handleContextHide();
}
};
@@ -90,6 +60,7 @@ class ContextMenuComponent extends React.Component {
<ul
className={`context-menu ${this.props.className || ""}`}
style={style}
+ ref={el => (this.listRef = el)}
>
{menuItems}
</ul>
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/createEntity.js
similarity index 63%
copy from console/react/src/details/dataSources/connectionData.js
copy to console/react/src/details/createEntity.js
index 568ac4c..3f5dfe2 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/createEntity.js
@@ -17,23 +17,22 @@ specific language governing permissions and limitations
under the License.
*/
-import DefaultData from "./defaultData";
-import ConnectionClose from "../../connectionClose";
+import React from "react";
+import { Button } from "@patternfly/react-core";
-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";
+class CreateEntity extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ handleClick = () => {
+ this.props.handleEntityAction("create");
+ };
+
+ render() {
+ return <Button onClick={this.handleClick}>Create</Button>;
}
}
-export default ConnectionData;
+export default CreateEntity;
diff --git a/console/react/src/details/createTablePage.js b/console/react/src/details/createTablePage.js
new file mode 100644
index 0000000..7a4ceb5
--- /dev/null
+++ b/console/react/src/details/createTablePage.js
@@ -0,0 +1,336 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+ Button,
+ Stack,
+ StackItem,
+ TextContent,
+ Text,
+ TextVariants,
+ Breadcrumb,
+ BreadcrumbItem
+} from "@patternfly/react-core";
+
+import {
+ Form,
+ FormGroup,
+ TextInput,
+ FormSelectOption,
+ FormSelect,
+ Checkbox,
+ ActionGroup
+} from "@patternfly/react-core";
+
+import { cellWidth } from "@patternfly/react-table";
+import { Card, CardBody } from "@patternfly/react-core";
+import { Redirect } from "react-router-dom";
+import { dataMap as detailsDataMap, defaultData } from "./entityData";
+
+class CreateTablePage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ columns: [
+ { title: "Attribute", transforms: [cellWidth(20)] },
+ {
+ title: "Value",
+ transforms: [cellWidth("max")],
+ props: { className: "pf-u-text-align-left" }
+ }
+ ],
+ rows: [],
+ redirect: false,
+ redirectState: { page: 1 },
+ redirectPath: "/dashboard",
+ lastUpdated: new Date(),
+ record: {}
+ };
+
+ // if we get to this page and we don't have a props.location.state.entity
+ // then redirect back to the dashboard.
+ // this can happen if we get here from a bookmark or browser refresh
+ this.entity =
+ this.props.entity ||
+ (this.props &&
+ this.props.location &&
+ this.props.location.state &&
+ this.props.location.state.entity);
+
+ if (!this.entity) {
+ this.state.redirect = true;
+ } else {
+ this.dataSource = !detailsDataMap[this.entity]
+ ? new defaultData(this.props.service, this.props.schema)
+ : new detailsDataMap[this.entity](
+ this.props.service,
+ this.props.schema
+ );
+ this.locationState = this.props.locationState;
+
+ const attributes = this.dataSource.schemaAttributes(this.entity);
+ for (let attributeKey in attributes) {
+ this.state.record[attributeKey] = this.getDefault(
+ attributes[attributeKey]
+ );
+ }
+ }
+ }
+
+ handleTextInputChange = (value, key) => {
+ console.log(`handleTextInputChange was passed ${value} ${key}`);
+ const { record } = this.state;
+ record[key] = value;
+ this.setState({ record });
+ };
+
+ getDefault = attribute => {
+ let defaultVal = "";
+ if (attribute.default) defaultVal = attribute.default;
+ if (typeof attribute.default === "undefined") {
+ if (attribute.type === "boolean") {
+ defaultVal = false;
+ } else if (Array.isArray(attribute.type)) {
+ defaultVal = attribute.type[0];
+ } else if (attribute.type === "integer") {
+ defaultVal = 0;
+ }
+ }
+ return defaultVal;
+ };
+
+ schemaToForm = () => {
+ const attributes = this.dataSource.schemaAttributes(this.entity);
+ const formGroups = [];
+ for (let attributeKey in attributes) {
+ if (attributeKey !== "identity") {
+ const attribute = attributes[attributeKey];
+ if (!attribute.graph) {
+ let type = attribute.type;
+ let options = [];
+ let readOnly = attributeKey === "identity";
+ if (type === "list") readOnly = true;
+ if (Array.isArray(attribute.type)) {
+ type = "select";
+ options = attribute.type;
+ }
+ let required = attribute.required;
+ if (
+ this.dataSource.updateMetaData &&
+ this.dataSource.updateMetaData[attributeKey]
+ ) {
+ const override = this.dataSource.updateMetaData[attributeKey];
+ if (override.readOnly) {
+ type = "string";
+ readOnly = true;
+ }
+ if (override.type === "select") {
+ type = "select";
+ options = override.options;
+ }
+ }
+ if (!readOnly) {
+ // no need to display readonly fields on create form
+ const id = `form-${attributeKey}`;
+ const formGroupProps = {
+ label: attributeKey,
+ isRequired: required,
+ fieldId: id,
+ helperText: attribute.description
+ };
+ if (type === "string" || type === "integer") {
+ formGroups.push(
+ <FormGroup {...formGroupProps} key={attributeKey}>
+ <TextInput
+ value={this.state.record[attributeKey]}
+ isRequired={required}
+ type={type === "string" ? "text" : "number"}
+ id={id}
+ aria-describedby="entiy-form-field"
+ name={attributeKey}
+ isDisabled={readOnly}
+ onChange={value =>
+ this.handleTextInputChange(value, attributeKey)
+ }
+ />
+ </FormGroup>
+ );
+ } else if (type === "select") {
+ formGroups.push(
+ <FormGroup {...formGroupProps} key={attributeKey}>
+ <FormSelect
+ value={this.state.record[attributeKey]}
+ onChange={value =>
+ this.handleTextInputChange(value, attributeKey)
+ }
+ id={id}
+ name={attributeKey}
+ >
+ {options.map((option, index) => (
+ <FormSelectOption
+ isDisabled={false}
+ key={`${attributeKey}-${index}`}
+ value={option}
+ label={option}
+ />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ );
+ } else if (type === "boolean") {
+ formGroups.push(
+ <FormGroup {...formGroupProps} key={attributeKey}>
+ <Checkbox
+ isChecked={this.state.record[attributeKey]}
+ label={attributeKey}
+ id={id}
+ name={attributeKey}
+ onChange={value =>
+ this.handleTextInputChange(value, attributeKey)
+ }
+ />
+ </FormGroup>
+ );
+ }
+ }
+ }
+ }
+ }
+ return formGroups;
+ };
+
+ toString = val => {
+ return val === null ? "" : String(val);
+ };
+
+ icap = s => s.charAt(0).toUpperCase() + s.slice(1);
+
+ parentItem = () => this.state.record.name;
+
+ breadcrumbSelected = () => {
+ this.props.handleSelectEntity(this.entity);
+ };
+
+ handleCancel = () => {
+ this.props.handleActionCancel(this.props);
+ };
+
+ handleCreate = () => {
+ const { record } = this.state;
+ const attributes = {};
+ const schemaAttributes = this.dataSource.schemaAttributes(this.entity);
+ for (let attr in record) {
+ if (
+ this.getDefault(schemaAttributes[attr]) !== record[attr] ||
+ schemaAttributes[attr].required
+ )
+ attributes[attr] = record[attr];
+ }
+ console.log(`creating ${this.entity}`);
+ console.log(attributes);
+
+ // call update
+ this.props.service.management.connection
+ .sendMethod(this.props.routerId, this.entity, attributes, "CREATE")
+ .then(results => {
+ let statusCode =
+ results.context.message.application_properties.statusCode;
+ if (statusCode < 200 || statusCode >= 300) {
+ let message =
+ results.context.message.application_properties.statusDescription;
+ const msg = `Create failed with message: ${message}`;
+ console.log(
+ `error Create failed ${results.context.message.application_properties.statusDescription}`
+ );
+ this.props.handleAddNotification("action", msg, new Date(), "danger");
+ } else {
+ const msg = `Created ${this.props.entity} ${record.name}`;
+ console.log(`success ${msg}`);
+ this.props.handleAddNotification(
+ "action",
+ msg,
+ new Date(),
+ "success"
+ );
+ }
+ this.handleCancel();
+ });
+ };
+
+ render() {
+ if (this.state.redirect) {
+ return (
+ <Redirect
+ to={{
+ pathname: this.state.redirectPath,
+ state: this.state.redirectState
+ }}
+ />
+ );
+ }
+
+ return (
+ <React.Fragment>
+ <PageSection
+ variant={PageSectionVariants.light}
+ className="overview-table-page"
+ >
+ <Stack>
+ <StackItem className="overview-header details">
+ <Breadcrumb>
+ <BreadcrumbItem
+ className="link-button"
+ onClick={this.breadcrumbSelected}
+ >
+ {this.icap(this.entity)}
+ </BreadcrumbItem>
+ </Breadcrumb>
+
+ <TextContent className="details-table-header">
+ <Text className="overview-title" component={TextVariants.h1}>
+ {this.parentItem()}
+ </Text>
+ <ActionGroup>
+ <Button
+ className="detail-action-button link-button"
+ onClick={this.handleCancel}
+ >
+ Cancel
+ </Button>
+ <Button onClick={this.handleCreate}>Create</Button>
+ </ActionGroup>
+ </TextContent>
+ </StackItem>
+ <StackItem id="update-form">
+ <Card>
+ <CardBody>
+ <Form isHorizontal>{this.schemaToForm()}</Form>
+ </CardBody>
+ </Card>
+ </StackItem>
+ </Stack>
+ </PageSection>
+ </React.Fragment>
+ );
+ }
+}
+
+export default CreateTablePage;
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/dataSources/autoLink.js
similarity index 71%
copy from console/react/src/details/dataSources/connectionData.js
copy to console/react/src/details/dataSources/autoLink.js
index 568ac4c..d5dc74c 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/dataSources/autoLink.js
@@ -18,22 +18,15 @@ under the License.
*/
import DefaultData from "./defaultData";
-import ConnectionClose from "../../connectionClose";
-class ConnectionData extends DefaultData {
+class AutoLinkData extends DefaultData {
constructor(service, schema) {
super(service, schema);
- this.extraFields = [
- {
- title: "",
- field: "connection",
- noSort: true,
- formatter: ConnectionClose
- }
- ];
- this.detailEntity = "router.link";
- this.detailName = "Link";
+
+ this.updateMetaData = {
+ operStatus: { readOnly: true }
+ };
}
}
-export default ConnectionData;
+export default AutoLinkData;
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/dataSources/connectionData.js
index 568ac4c..2271434 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/dataSources/connectionData.js
@@ -17,6 +17,7 @@ specific language governing permissions and limitations
under the License.
*/
+import React from "react";
import DefaultData from "./defaultData";
import ConnectionClose from "../../connectionClose";
@@ -34,6 +35,16 @@ class ConnectionData extends DefaultData {
this.detailEntity = "router.link";
this.detailName = "Link";
}
+
+ detailActions = (entity, props, record) => {
+ return (
+ <ConnectionClose
+ asButton={true}
+ extraInfo={{ rowData: { data: record } }}
+ {...props}
+ />
+ );
+ };
}
export default ConnectionData;
diff --git a/console/react/src/details/dataSources/defaultData.js b/console/react/src/details/dataSources/defaultData.js
index 0ed5235..6e4b401 100644
--- a/console/react/src/details/dataSources/defaultData.js
+++ b/console/react/src/details/dataSources/defaultData.js
@@ -17,23 +17,61 @@ specific language governing permissions and limitations
under the License.
*/
+import React from "react";
import { utils } from "../../amqp/utilities";
+import DeleteEntity from "../deleteEntity";
+import UpdateEntity from "../updateEntity";
+import CreateEntity from "../createEntity";
class DefaultData {
constructor(service, schema) {
this.service = service;
this.schema = schema;
+ this.actionMap = {
+ DELETE: DeleteEntity,
+ UPDATE: UpdateEntity,
+ CREATE: CreateEntity
+ };
}
- hasType = () => {
- return false;
- };
+ schemaAttributes = entity => this.schema.entityTypes[entity].attributes;
+ schemaOperations = entity => this.schema.entityTypes[entity].operations;
- actions = entity => {
- return this.schema.entityTypes[entity].operations.filter(
- action => action !== "READ" && action !== "UPDATE"
- );
- };
+ // emit a single button/component
+ entityAction = ({
+ component: Component,
+ props,
+ record,
+ click,
+ i,
+ asButton
+ }) => (
+ <Component
+ key={`action-${i}`}
+ record={record}
+ notifyClick={click}
+ {...props}
+ asButton={asButton}
+ />
+ );
+
+ // action buttons for the detailsTablePage for a single record
+ detailActions = (entity, props, record, click) => (
+ <>
+ {this.actions(entity)
+ .filter(action => action !== "CREATE")
+ .map((action, i) =>
+ this.entityAction({
+ component: this.actionMap[action],
+ props,
+ record,
+ click,
+ i,
+ asButton: true
+ })
+ )}
+ </>
+ );
// called by detailsTablePage to display a single record
fetchRecord = (currentRecord, schema, entity) => {
@@ -61,6 +99,34 @@ class DefaultData {
});
};
+ // return a list of operations allowed for this entity
+ actions = entity =>
+ this.schema.entityTypes[entity].operations.filter(
+ action => action !== "READ"
+ );
+
+ // action button for the entityListTable
+ actionButton = ({ action, props, click, record, i, asButton }) =>
+ this.entityAction({
+ component: this.actionMap[action],
+ props,
+ click,
+ record,
+ i,
+ asButton
+ });
+
+ // actions menu on entityListTable for each record
+ actionMenuItems = (entity, click) => {
+ const actions = this.actions(entity).filter(action => action !== "CREATE");
+ return actions.map(action => ({
+ title: action,
+ onClick: (event, rowId, rowData, extra) => {
+ click({ action, entity, event, rowId, rowData, extra });
+ }
+ }));
+ };
+
// called by entityListTable to get the list of records
doFetch = (page, perPage, routerId, entity) => {
return new Promise(resolve => {
diff --git a/console/react/src/details/dataSources/linkData.js b/console/react/src/details/dataSources/linkData.js
index 47bee54..69636b3 100644
--- a/console/react/src/details/dataSources/linkData.js
+++ b/console/react/src/details/dataSources/linkData.js
@@ -47,7 +47,13 @@ const LinkType = ({ value, extraInfo }) => {
class LinkData extends DefaultData {
constructor(service, schema) {
super(service, schema);
- this.service = service;
+
+ this.updateMetaData = {
+ peer: { readOnly: true },
+ linkName: { readOnly: true },
+ owningAddr: { readOnly: true }
+ };
+
this.extraFields = [{ title: "Dir", field: "linkDir", formatter: LinkDir }];
this.detailEntity = "router.link";
this.detailName = "Link";
diff --git a/console/react/src/details/dataSources/logsData.js b/console/react/src/details/dataSources/logsData.js
index 4d2128c..daa2553 100644
--- a/console/react/src/details/dataSources/logsData.js
+++ b/console/react/src/details/dataSources/logsData.js
@@ -17,160 +17,15 @@ specific language governing permissions and limitations
under the License.
*/
-import React from "react";
-import { Button } from "@patternfly/react-core";
-class LogRecords extends React.Component {
- detailClick = () => {
- this.props.detailClick(this.props.value, this.props.extraInfo);
- };
- render() {
- if (
- this.props.extraInfo.rowData.enable.title !== "" &&
- this.props.value !== "0"
- ) {
- return (
- <Button className="link-button" onClick={this.detailClick}>
- {this.props.value}
- </Button>
- );
- } else {
- return this.props.value;
- }
+import DefaultData from "./defaultData";
+
+class LogsData extends DefaultData {
+ constructor(service, schema) {
+ super(service, schema);
+ this.updateMetaData = {
+ module: { readOnly: true }
+ };
}
}
-class LogsData {
- constructor(service) {
- this.service = service;
- this.fields = [
- { title: "Router", field: "node" },
- { title: "Enable", field: "enable" },
- { title: "Module", field: "name" },
- {
- title: "Info",
- field: "infoCount",
- numeric: true,
- formatter: LogRecords
- },
- {
- title: "Trace",
- field: "traceCount",
- numeric: true,
- formatter: LogRecords
- },
- {
- title: "Debug",
- field: "debugCount",
- numeric: true,
- formatter: LogRecords
- },
- {
- title: "Notice",
- field: "noticeCount",
- numeric: true,
- formatter: LogRecords
- },
- {
- title: "Warning",
- field: "warningCount",
- numeric: true,
- formatter: LogRecords
- },
- {
- title: "Error",
- field: "errorCount",
- numeric: true,
- formatter: LogRecords
- },
- {
- title: "Critical",
- field: "criticalCount",
- numeric: true,
- formatter: LogRecords
- }
- ];
- this.detailEntity = "log";
- this.detailName = "Log";
- this.detailPath = "/logs";
- this.detailFormatter = true;
- }
- hasType = () => {
- return true;
- };
-
- fetchRecord = (currentRecord, schema) => {
- return new Promise(resolve => {
- this.service.management.topology.fetchEntities(
- currentRecord.nodeId,
- { entity: "logStats" },
- data => {
- const record = data[currentRecord.nodeId]["logStats"];
- const identityIndex = record.attributeNames.indexOf("name");
- const result = record.results.find(
- r => r[identityIndex] === currentRecord.name
- );
- let obj = this.service.utilities.flatten(
- record.attributeNames,
- result
- );
- obj = this.service.utilities.formatAttributes(
- obj,
- schema.entityTypes["logStats"]
- );
- resolve(obj);
- }
- );
- });
- };
-
- doFetch = (page, perPage) => {
- return new Promise(resolve => {
- // an array of logStat records that have router name and log.enable added
- let logModules = [];
- const insertEnable = (record, logData) => {
- // find the logData result for this record
- const moduleIndex = logData.attributeNames.indexOf("module");
- const enableIndex = logData.attributeNames.indexOf("enable");
- const logRec = logData.results.find(
- r => r[moduleIndex] === record.name
- );
- if (logRec) {
- record.enable =
- logRec[enableIndex] === null ? "" : String(logRec[enableIndex]);
- } else {
- record.enable = "";
- }
- };
- this.service.management.topology.fetchAllEntities(
- [{ entity: "log" }, { entity: "logStats" }],
- nodes => {
- // each router is a node in nodes
- for (let node in nodes) {
- const nodeName = this.service.utilities.nameFromId(node);
- let response = nodes[node]["logStats"];
- // response is an array of records for this node/router
- response.results.forEach(result => {
- // result is a single log record for this router
- let logStat = this.service.utilities.flatten(
- response.attributeNames,
- result
- );
-
- logStat.node = nodeName;
- logStat.nodeId = node;
- insertEnable(logStat, nodes[node]["log"]);
- logModules.push(logStat);
- });
- }
- resolve({
- data: logModules,
- page,
- perPage
- });
- }
- );
- });
- };
-}
-
export default LogsData;
diff --git a/console/react/src/details/dataSources/routerData.js b/console/react/src/details/dataSources/routerData.js
index eb4c39e..67c015e 100644
--- a/console/react/src/details/dataSources/routerData.js
+++ b/console/react/src/details/dataSources/routerData.js
@@ -43,9 +43,6 @@ class RouterData {
this.detailEntity = "router";
this.detailName = "Router";
}
- hasType = () => {
- return true;
- };
fetchRecord = (currentRecord, schema) => {
return new Promise(resolve => {
diff --git a/console/react/src/details/deleteEntity.js b/console/react/src/details/deleteEntity.js
new file mode 100644
index 0000000..01e1912
--- /dev/null
+++ b/console/react/src/details/deleteEntity.js
@@ -0,0 +1,143 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { Button, Modal } from "@patternfly/react-core";
+
+class DeleteEntity extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isModalOpen: false,
+ closing: false,
+ closed: false
+ };
+ }
+
+ handleModalShow = () => {
+ console.log("handleModalShow DELETE");
+ this.setState({ isModalOpen: true, closed: false });
+ };
+
+ handleModalHide = () => {
+ this.setState({ isModalOpen: false, closed: true }, () => {
+ if (this.props.cancelledAction) {
+ this.props.cancelledAction("DELETE");
+ }
+ });
+ };
+
+ getName = record => {
+ return record.name !== null
+ ? record.name
+ : `${this.props.entity}/${record.identity}`;
+ };
+
+ delete = () => {
+ this.setState({ closing: true }, () => {
+ const record = this.props.record;
+ const name = this.getName(record);
+ this.props.service.management.connection
+ .sendMethod(
+ record.nodeId || record.routerId,
+ this.props.entity,
+ { identity: record.identity },
+ "DELETE"
+ )
+ .then(results => {
+ let statusCode =
+ results.context.message.application_properties.statusCode;
+ if (statusCode < 200 || statusCode >= 300) {
+ const msg = `Deleted ${name} failed with message: ${results.context.message.application_properties.statusDescription}`;
+ console.log(`error ${msg}`);
+ this.props.handleAddNotification(
+ "action",
+ msg,
+ new Date(),
+ "danger"
+ );
+ } else {
+ const msg = `Deleted ${this.props.entity} ${name}`;
+ console.log(`success ${msg}`);
+ this.props.handleAddNotification(
+ "action",
+ msg,
+ new Date(),
+ "success"
+ );
+ }
+ this.setState(
+ { isModalOpen: false, closing: false, closed: true },
+ () => {
+ if (this.props.notifyClick) {
+ this.props.notifyClick("Done");
+ }
+ }
+ );
+ });
+ });
+ };
+
+ render() {
+ const { isModalOpen, closing } = this.state;
+ const record = this.props.record;
+ const name = this.getName(record);
+ return (
+ <React.Fragment>
+ {!this.props.showNow && (
+ <Button
+ className={`${this.props.asButton ? "" : "link-button"}`}
+ onClick={this.handleModalShow}
+ >
+ Delete
+ </Button>
+ )}
+ <Modal
+ isSmall
+ title={`Delete this ${this.props.entity}?`}
+ isOpen={isModalOpen || (this.props.showNow && !this.state.closed)}
+ onClose={this.handleModalHide}
+ actions={[
+ <Button
+ key="confirm"
+ variant="primary"
+ onClick={this.delete}
+ isDisabled={closing}
+ >
+ Delete
+ </Button>,
+ <Button
+ key="cancel"
+ variant="link"
+ onClick={this.handleModalHide}
+ isDisabled={closing}
+ >
+ Cancel
+ </Button>
+ ]}
+ isFooterLeftAligned
+ >
+ {closing ? `Deleting ${name}` : `${name}`}
+ </Modal>
+ </React.Fragment>
+ );
+ }
+}
+
+export default DeleteEntity;
diff --git a/console/react/src/details/enitiesPage.js b/console/react/src/details/enitiesPage.js
index 51529b9..f80b02f 100644
--- a/console/react/src/details/enitiesPage.js
+++ b/console/react/src/details/enitiesPage.js
@@ -23,6 +23,8 @@ import { Stack, StackItem } from "@patternfly/react-core";
import { Split, SplitItem } from "@patternfly/react-core";
import DetailsTablePage from "../detailsTablePage";
+import UpdateTablePage from "./updateTablePage";
+import CreateTablePage from "./createTablePage";
import EntityListTable from "./entityListTable";
import EntityList from "./entityList";
import RouterSelect from "./routerSelect";
@@ -35,7 +37,8 @@ class EntitiesPage extends React.Component {
loading: false,
lastUpdated: new Date(),
entity: null,
- routerId: null
+ routerId: null,
+ showTable: "entities"
};
this.schema = this.props.service.management.schema();
}
@@ -47,22 +50,57 @@ class EntitiesPage extends React.Component {
// called from entityList to change entity summary
handleSwitchEntity = entity => {
if (this.listTableRef) this.listTableRef.reset();
- this.setState({ entity, showDetails: false, detailsState: {} });
+ this.setState({ entity, showTable: "entities", detailsState: {} });
};
- // called from breadcrumb on entityListTable to return to current entity summary
+ // called from breadcrumb on detailsTablePage to return to current entity summary
handleSelectEntity = entity => {
- this.setState({ entity, showDetails: false });
+ this.setState({ entity, showTable: "entities" });
+ };
+
+ handleEntityAction = (action, record) => {
+ if (action === "Done") action = "entities";
+ this.setState({
+ actionState: {
+ currentRecord: record,
+ entity: this.props.entity
+ },
+ showTable: action
+ });
+ };
+
+ handleActionCancel = props => {
+ const { detailsState } = this.state;
+ const { page, sortBy, filterBy, perPage } = detailsState;
+ const extraInfo = { rowData: { data: props.locationState.currentRecord } };
+ if (!props.locationState.currentRecord) {
+ this.handleSwitchEntity(this.state.entity);
+ } else {
+ this.handleDetailClick(
+ props.locationState.currentRecord.name,
+ extraInfo,
+ {
+ page,
+ sortBy,
+ filterBy,
+ perPage
+ }
+ );
+ }
};
handleRouterSelected = routerId => {
- this.setState({ routerId, showDetails: false });
+ this.setState({ routerId, showTable: "entities" });
};
+ // clicked on 1st column in the entityTable
+ // show the details page for this record
handleDetailClick = (value, extraInfo, stateInfo) => {
+ // pass along the current state of the entity table
+ // so we can restore it if the breadcrumb on the details page is clicked
this.setState({
detailsState: {
- value: extraInfo.rowData.cells[extraInfo.columnIndex],
+ value: value,
currentRecord: extraInfo.rowData.data,
entity: this.props.entity,
page: stateInfo.page,
@@ -71,14 +109,15 @@ class EntitiesPage extends React.Component {
perPage: stateInfo.perPage,
property: extraInfo.property
},
- showDetails: true
+ showTable: "details"
});
};
render() {
+ const TABLE = this.state.showTable.toUpperCase();
const entityTable = () => {
if (this.state.entity) {
- if (!this.state.showDetails) {
+ if (TABLE === "ENTITIES") {
return (
<EntityListTable
ref={el => (this.listTableRef = el)}
@@ -89,9 +128,10 @@ class EntitiesPage extends React.Component {
lastUpdated={this.lastUpdated}
handleDetailClick={this.handleDetailClick}
detailsState={this.state.detailsState}
+ handleEntityAction={this.handleEntityAction}
/>
);
- } else {
+ } else if (TABLE === "DETAILS") {
return (
<DetailsTablePage
details={true}
@@ -101,6 +141,31 @@ class EntitiesPage extends React.Component {
lastUpdated={this.lastUpdated}
schema={this.schema}
handleSelectEntity={this.handleSelectEntity}
+ handleEntityAction={this.handleEntityAction}
+ />
+ );
+ } else if (TABLE === "UPDATE") {
+ return (
+ <UpdateTablePage
+ entity={this.state.entity}
+ {...this.props}
+ schema={this.schema}
+ locationState={this.state.actionState}
+ handleSelectEntity={this.handleSelectEntity}
+ handleActionCancel={this.handleActionCancel}
+ handleEntityAction={this.handleEntityAction}
+ />
+ );
+ } else if (TABLE === "CREATE") {
+ return (
+ <CreateTablePage
+ entity={this.state.entity}
+ routerId={this.state.routerId}
+ {...this.props}
+ schema={this.schema}
+ locationState={this.state.actionState}
+ handleSelectEntity={this.handleSelectEntity}
+ handleActionCancel={this.handleActionCancel}
/>
);
}
diff --git a/console/react/src/details/entityData.js b/console/react/src/details/entityData.js
index 351072d..53e6366 100644
--- a/console/react/src/details/entityData.js
+++ b/console/react/src/details/entityData.js
@@ -19,8 +19,10 @@ under the License.
import AddressData from "./dataSources/addressData";
import LinkData from "./dataSources/linkData";
+import AutoLinkData from "./dataSources/autoLink";
import ListenerData from "./dataSources/listenerData";
import ConnectionData from "./dataSources/connectionData";
+import LogsData from "./dataSources/logsData";
import DefaultData from "./dataSources/defaultData";
@@ -28,7 +30,9 @@ const dataMap = {
"router.address": AddressData,
"router.link": LinkData,
listener: ListenerData,
- connection: ConnectionData
+ connection: ConnectionData,
+ log: LogsData,
+ autoLink: AutoLinkData
};
const defaultData = DefaultData;
diff --git a/console/react/src/details/entityListTable.js b/console/react/src/details/entityListTable.js
index 1aa3703..2ff0924 100644
--- a/console/react/src/details/entityListTable.js
+++ b/console/react/src/details/entityListTable.js
@@ -57,7 +57,8 @@ class EntityListTable extends React.Component {
rows: [],
redirect: false,
redirectState: {},
- hasChecked: false
+ action: null,
+ data: null
};
this.initDataSource();
this.columns = [];
@@ -97,6 +98,9 @@ class EntityListTable extends React.Component {
if (this.dataSource.extraFields) {
this.dataSource.fields.push(...this.dataSource.extraFields);
}
+ if (this.dataSource.actionColumn) {
+ this.dataSource.fields.push(this.dataSource.actionColumn);
+ }
};
setupFields = () => {
@@ -164,6 +168,9 @@ class EntityListTable extends React.Component {
};
detailLink = (value, extraInfo) => {
+ if (value === null) {
+ value = `${this.props.entity}/${extraInfo.rowData.data.identity}`;
+ }
return (
<Button
className="link-button"
@@ -327,10 +334,8 @@ class EntityListTable extends React.Component {
rows = [...this.state.rows];
rows[rowId].selected = isSelected;
}
- const hasChecked = this.state.rows.some(row => row.selected);
this.setState({
- rows,
- hasChecked
+ rows
});
};
@@ -351,21 +356,57 @@ class EntityListTable extends React.Component {
);
};
- handleAction = action => {};
+ // an action was clicked on a row's kebab menu
+ handleAction = ({ action, rowData }) => {
+ console.log(`handleActions ${action}`);
+ console.log(rowData);
+
+ if (action === "UPDATE") {
+ this.props.handleEntityAction(action, rowData.data);
+ } else {
+ this.setState({ action, data: rowData.data });
+ }
+ };
+
+ cancelledAction = () => {
+ this.setState({ action: null });
+ };
+
+ // show the confirmation modal for an action
+ doAction = () => {
+ const props = {
+ showNow: true,
+ cancelledAction: this.cancelledAction,
+ ...this.props
+ };
+ return this.dataSource.actionButton({
+ action: this.state.action,
+ props: props,
+ click: this.didAction,
+ record: this.state.data,
+ i: 0,
+ asButton: false
+ });
+ };
+
+ // called by action modal after action is performed or cancelled
+ didAction = () => {
+ this.setState({ action: null, data: null }, this.update);
+ };
render() {
const tableProps = {
cells: this.columns,
rows: this.state.rows,
+ actions: this.dataSource.actionMenuItems(
+ this.props.entity,
+ this.handleAction
+ ),
"aria-label": this.props.entity,
sortBy: this.state.sortBy,
onSort: this.onSort,
variant: TableVariant.compact
};
- if (this.dataSource.actions(this.props.entity).includes("DELETE")) {
- tableProps.onSelect = this.onSelect;
- tableProps.canSelectAll = true;
- }
if (this.state.redirect) {
return (
@@ -378,6 +419,25 @@ class EntityListTable extends React.Component {
);
}
+ // map of actions to buttons for the table toolbar
+ const actionButtons = () => {
+ // don't show UPDATE or DELETE for the entire list of records
+ const actions = this.dataSource
+ .actions(this.props.entity)
+ .filter(action => action !== "UPDATE" && action !== "DELETE");
+ const buttons = {};
+ actions.forEach((action, i) => {
+ buttons[action] = this.dataSource.actionButton({
+ action,
+ props: this.props,
+ click: this.handleAction,
+ i,
+ asButton: true
+ });
+ });
+ return buttons;
+ };
+
return (
<React.Fragment>
<TableToolbar
@@ -391,15 +451,14 @@ class EntityListTable extends React.Component {
filterBy={this.state.filterBy}
handleChangeFilterValue={this.handleChangeFilterValue}
hidePagination={true}
- actions={this.dataSource.actions(this.props.entity)}
- hasChecked={this.state.hasChecked}
- handleAction={this.handleAction}
+ actionButtons={actionButtons()}
/>
<Table {...tableProps}>
<TableHeader />
<TableBody />
</Table>
{this.renderPagination("bottom")}
+ {this.state.action && this.doAction()}
</React.Fragment>
);
}
diff --git a/console/react/src/details/schema/schemaPage.js b/console/react/src/details/schema/schemaPage.js
new file mode 100644
index 0000000..8cd5e6c
--- /dev/null
+++ b/console/react/src/details/schema/schemaPage.js
@@ -0,0 +1,226 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+ Stack,
+ StackItem,
+ TextContent,
+ Text,
+ TextVariants
+} from "@patternfly/react-core";
+import { Card, CardBody } from "@patternfly/react-core";
+
+class SchemaPage extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ root: {
+ key: "entities",
+ title: "Schema entities",
+ description:
+ "List of management entities. Click on an entity to view its attributes.",
+ hidden: false
+ }
+ };
+ this.initRoot(this.state.root, this.props.schema.entityTypes);
+ }
+
+ initRoot = (root, schema) => {
+ root.attributes = [
+ {
+ key: Object.keys(schema).length,
+ value: "Entities"
+ }
+ ];
+ root.children = [];
+ const entities = Object.keys(schema).sort();
+ for (let i = 0; i < entities.length; i++) {
+ const entity = entities[i];
+ const child = { title: entity, key: entity };
+ this.initChild(child, schema[entity]);
+ root.children.push(child);
+ }
+ };
+
+ initChild = (child, obj) => {
+ child.hidden = true;
+ child.description = obj.description;
+ child.attributes = [];
+ if (obj.attributes) {
+ child.hidden = false;
+ child.attributes.push({
+ key: Object.keys(obj.attributes).length,
+ value: "Attributes"
+ });
+ child.children = [];
+ for (const attr in obj.attributes) {
+ const sub = { title: attr, key: `${child.key}-${attr}` };
+ this.initChild(sub, obj.attributes[attr]);
+ child.children.push(sub);
+ }
+ }
+ if (obj.operations) {
+ child.attributes.push({
+ key: "Operations",
+ value: `[${obj.operations.join(", ")}]`
+ });
+ }
+ if (obj.fullyQualifiedType) {
+ child.fqt = obj.fullyQualifiedType;
+ }
+ if (obj.type) {
+ child.attributes.push({
+ key: "type",
+ value:
+ obj.type.constructor === Array ? `[${obj.type.join(", ")}]` : obj.type
+ });
+ }
+ if (obj.default) {
+ child.attributes.push({ key: "default", value: obj.default });
+ }
+ if (obj.required) {
+ child.attributes.push({ key: "required", value: "" });
+ }
+ if (obj.unique) {
+ child.attributes.push({ key: "unique", value: "" });
+ }
+ if (obj.graph) {
+ child.attributes.push({ key: "statistic", value: "" });
+ }
+ };
+
+ toggleChildren = (event, parent) => {
+ event.stopPropagation();
+ if (parent.children && parent.children.length > 0) {
+ parent.children.forEach(child => {
+ child.hidden = !child.hidden;
+ });
+ this.setState({ root: this.state.root });
+ }
+ };
+
+ folderIconClass = item => {
+ if (item.children) {
+ return item.children.some(child => !child.hidden)
+ ? "pficon-folder-open"
+ : "pficon-folder-close";
+ }
+ return "pficon-catalog";
+ };
+
+ attrIconClass = attr => {
+ const attrMap = {
+ type: "pficon-builder-image",
+ Operations: "pficon-maintenance",
+ default: "pficon-info",
+ unique: "pficon-locked",
+ statistic: "fa fa-icon fa-tachometer"
+ };
+ if (attrMap[attr.key]) return `pficon ${attrMap[attr.key]}`;
+ return "pficon pficon-repository";
+ };
+
+ render() {
+ const TreeItem = itemInfo => {
+ return (
+ !itemInfo.hidden && (
+ <div
+ key={itemInfo.key}
+ className={`list-group-item-container container-fluid`}
+ onClick={event => this.toggleChildren(event, itemInfo)}
+ >
+ <div className="list-group-item">
+ <div className="list-group-item-header">
+ <div className="list-view-pf-main-info">
+ <div className="list-view-pf-left">
+ <span
+ className={`pficon ${this.folderIconClass(
+ itemInfo
+ )} list-view-pf-icon-sm`}
+ ></span>
+ </div>
+ <div className="list-view-pf-body">
+ <div className="list-view-pf-description">
+ <div className="list-group-item-heading">
+ {itemInfo.title}
+ </div>
+ <div className="list-group-item-text">
+ {itemInfo.description}
+ {itemInfo.fqt && (
+ <div className="list-group-item-fqt">
+ {itemInfo.fqt}
+ </div>
+ )}
+ </div>
+ </div>
+ <div className="list-view-pf-additional-info">
+ {itemInfo.attributes &&
+ itemInfo.attributes.map((attr, i) => (
+ <div
+ className="list-view-pf-additional-info-item"
+ key={`${itemInfo.key}-${i}`}
+ >
+ <span className={this.attrIconClass(attr)}></span>
+ <strong>{attr.key}</strong>
+ {attr.value}
+ </div>
+ ))}
+ </div>
+ </div>
+ </div>
+ </div>
+ {itemInfo.children &&
+ itemInfo.children.map(childInfo => TreeItem(childInfo))}
+ </div>
+ </div>
+ )
+ );
+ };
+
+ return (
+ <PageSection variant={PageSectionVariants.light} id="schema-page">
+ <Stack>
+ <StackItem>
+ <TextContent>
+ <Text className="overview-title" component={TextVariants.h1}>
+ Schema
+ </Text>
+ </TextContent>
+ </StackItem>
+ <StackItem>
+ <Card>
+ <CardBody>
+ <div className="container-fluid">
+ <div className="list-group tree-list-view-pf">
+ {TreeItem(this.state.root)}
+ </div>
+ </div>
+ </CardBody>
+ </Card>
+ </StackItem>
+ </Stack>
+ </PageSection>
+ );
+ }
+}
+
+export default SchemaPage;
diff --git a/console/react/src/details/dataSources/connectionData.js b/console/react/src/details/updateEntity.js
similarity index 62%
copy from console/react/src/details/dataSources/connectionData.js
copy to console/react/src/details/updateEntity.js
index 568ac4c..01c4abb 100644
--- a/console/react/src/details/dataSources/connectionData.js
+++ b/console/react/src/details/updateEntity.js
@@ -17,23 +17,24 @@ specific language governing permissions and limitations
under the License.
*/
-import DefaultData from "./defaultData";
-import ConnectionClose from "../../connectionClose";
+import React from "react";
+import { Button } from "@patternfly/react-core";
-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";
+class UpdateEntity extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {};
+ }
+
+ handleClick = () => {
+ this.props.handleEntityAction("update", this.props.record);
+ };
+
+ render() {
+ console.log("rendering update button");
+ console.log(this.props);
+ return <Button onClick={this.handleClick}>Update</Button>;
}
}
-export default ConnectionData;
+export default UpdateEntity;
diff --git a/console/react/src/details/updateTablePage.js b/console/react/src/details/updateTablePage.js
new file mode 100644
index 0000000..d738fef
--- /dev/null
+++ b/console/react/src/details/updateTablePage.js
@@ -0,0 +1,337 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+import React from "react";
+import { PageSection, PageSectionVariants } from "@patternfly/react-core";
+import {
+ Button,
+ Stack,
+ StackItem,
+ TextContent,
+ Text,
+ TextVariants,
+ Breadcrumb,
+ BreadcrumbItem
+} from "@patternfly/react-core";
+
+import {
+ Form,
+ FormGroup,
+ TextInput,
+ FormSelectOption,
+ FormSelect,
+ Checkbox,
+ ActionGroup
+} from "@patternfly/react-core";
+
+import { cellWidth } from "@patternfly/react-table";
+import { Card, CardBody } from "@patternfly/react-core";
+import { Redirect } from "react-router-dom";
+import { dataMap as detailsDataMap, defaultData } from "./entityData";
+import { utils } from "../amqp/utilities";
+
+class UpdateTablePage extends React.Component {
+ constructor(props) {
+ super(props);
+ // if we get to this page and we don't have a props.location.state.entity
+ // then redirect back to the dashboard.
+ // this can happen if we get here from a bookmark or browser refresh
+ this.entity =
+ this.props.entity ||
+ (this.props &&
+ this.props.location &&
+ this.props.location.state &&
+ this.props.location.state.entity);
+
+ if (!this.entity) {
+ this.state.redirect = true;
+ } else {
+ this.dataSource = !detailsDataMap[this.entity]
+ ? new defaultData(this.props.service, this.props.schema)
+ : new detailsDataMap[this.entity](
+ this.props.service,
+ this.props.schema
+ );
+ }
+
+ this.state = {
+ columns: [
+ { title: "Attribute", transforms: [cellWidth(20)] },
+ {
+ title: "Value",
+ transforms: [cellWidth("max")],
+ props: { className: "pf-u-text-align-left" }
+ }
+ ],
+ rows: [],
+ redirect: false,
+ redirectState: { page: 1 },
+ redirectPath: "/dashboard",
+ lastUpdated: new Date(),
+ changes: false,
+ record: this.fixNull(this.props.locationState.currentRecord)
+ };
+ this.originalRecord = utils.copy(this.state.record);
+ }
+
+ fixNull = rec => {
+ const record = utils.copy(rec);
+ const attributes = this.dataSource.schemaAttributes(this.entity);
+ for (const attr in record) {
+ if (record[attr] === null) {
+ if (attributes[attr].type === "string") {
+ record[attr] = "";
+ } else if (attributes[attr].type === "integer") {
+ record[attr] = 0;
+ }
+ }
+ }
+ return record;
+ };
+
+ handleTextInputChange = (value, key) => {
+ const { record } = this.state;
+ record[key] = value;
+ let changes = false;
+ for (let attr in record) {
+ changes = changes || record[attr] !== this.originalRecord[attr];
+ }
+ this.setState({ record, changes });
+ };
+
+ schemaToForm = () => {
+ const record = this.state.record;
+ const attributes = this.dataSource.schemaAttributes(this.entity);
+ const formGroups = [];
+ for (let attributeKey in attributes) {
+ const attribute = attributes[attributeKey];
+ let type = attribute.type;
+ let options = [];
+ let readOnly = attributeKey === "identity";
+ if (type === "list") readOnly = true;
+ if (type === "integer" && attribute.graph) readOnly = true;
+ let required = attribute.required;
+ const value = record[attributeKey];
+ if (
+ this.dataSource.updateMetaData &&
+ this.dataSource.updateMetaData[attributeKey]
+ ) {
+ const override = this.dataSource.updateMetaData[attributeKey];
+ if (override.readOnly) {
+ type = "string";
+ readOnly = true;
+ }
+ if (override.type === "select") {
+ type = "select";
+ options = override.options;
+ }
+ }
+ const id = `form-${attributeKey}`;
+ const formGroupProps = {
+ label: attributeKey,
+ isRequired: required,
+ fieldId: id,
+ helperText: attribute.description
+ };
+ if (!readOnly) {
+ if (type === "string" || type === "integer") {
+ formGroups.push(
+ <FormGroup {...formGroupProps} key={attributeKey}>
+ <TextInput
+ value={record[attributeKey]}
+ isRequired={required}
+ type={type === "string" ? "text" : "number"}
+ id={id}
+ aria-describedby="entiy-form-field"
+ name={attributeKey}
+ isDisabled={readOnly}
+ onChange={value =>
+ this.handleTextInputChange(value, attributeKey)
+ }
+ />
+ </FormGroup>
+ );
+ } else if (type === "select") {
+ formGroups.push(
+ <FormGroup {...formGroupProps} key={attributeKey}>
+ <FormSelect
+ value={value}
+ onChange={value =>
+ this.handleTextInputChange(value, attributeKey)
+ }
+ id={id}
+ name={attributeKey}
+ >
+ {options.map((option, index) => (
+ <FormSelectOption
+ isDisabled={false}
+ key={`${attributeKey}-${index}`}
+ value={option}
+ label={option}
+ />
+ ))}
+ </FormSelect>
+ </FormGroup>
+ );
+ } else if (type === "boolean") {
+ formGroups.push(
+ <FormGroup {...formGroupProps} key={attributeKey}>
+ <Checkbox
+ isChecked={
+ record[attributeKey] === null ? false : record[attributeKey]
+ }
+ onChange={value =>
+ this.handleTextInputChange(value, attributeKey)
+ }
+ label={attributeKey}
+ id={id}
+ name={attributeKey}
+ />
+ </FormGroup>
+ );
+ }
+ }
+ }
+ return formGroups;
+ };
+
+ toString = val => {
+ return val === null ? "" : String(val);
+ };
+
+ icap = s => s.charAt(0).toUpperCase() + s.slice(1);
+
+ parentItem = () => this.state.record.name;
+
+ breadcrumbSelected = () => {
+ this.props.handleSelectEntity(this.entity);
+ };
+
+ handleCancel = () => {
+ this.props.handleActionCancel(this.props);
+ };
+
+ handleUpdate = () => {
+ const record = this.state.record;
+ const attributes = {};
+ // identity is needed to update the record
+ attributes["identity"] = record.identity;
+ // pass any other attributes that have changed
+ for (const attr in record) {
+ if (record[attr] !== this.originalRecord[attr]) {
+ attributes[attr] = record[attr];
+ } else if (attr === "outputFile")
+ attributes["outputFile"] =
+ record.outputFile === "" ? null : record.outputFile;
+ }
+ // call update
+ this.props.service.management.connection
+ .sendMethod(
+ record.routerId || record.nodeId,
+ this.entity,
+ attributes,
+ "UPDATE"
+ )
+ .then(results => {
+ let statusCode =
+ results.context.message.application_properties.statusCode;
+ if (statusCode < 200 || statusCode >= 300) {
+ const msg = `Updated ${record.name} failed with message: ${results.context.message.application_properties.statusDescription}`;
+ console.log(`error ${msg}`);
+ this.props.handleAddNotification("action", msg, new Date(), "danger");
+ } else {
+ const msg = `Updated ${this.props.entity} ${record.name}`;
+ console.log(`success ${msg}`);
+ this.props.handleAddNotification(
+ "action",
+ msg,
+ new Date(),
+ "success"
+ );
+ }
+ const props = this.props;
+ props.locationState.currentRecord = record;
+ this.props.handleActionCancel(props);
+ });
+ };
+
+ render() {
+ if (this.state.redirect) {
+ return (
+ <Redirect
+ to={{
+ pathname: this.state.redirectPath,
+ state: this.state.redirectState
+ }}
+ />
+ );
+ }
+
+ return (
+ <React.Fragment>
+ <PageSection
+ variant={PageSectionVariants.light}
+ className="overview-table-page"
+ >
+ <Stack>
+ <StackItem className="overview-header details">
+ <Breadcrumb>
+ <BreadcrumbItem
+ className="link-button"
+ onClick={this.breadcrumbSelected}
+ >
+ {this.icap(this.entity)}
+ </BreadcrumbItem>
+ </Breadcrumb>
+
+ <TextContent className="details-table-header">
+ <Text className="overview-title" component={TextVariants.h1}>
+ {this.parentItem()}
+ </Text>
+ <ActionGroup>
+ <Button
+ className="detail-action-button link-button"
+ onClick={this.handleCancel}
+ >
+ Cancel
+ </Button>
+ <Button
+ onClick={this.handleUpdate}
+ isDisabled={!this.state.changes}
+ >
+ Update
+ </Button>
+ </ActionGroup>
+ </TextContent>
+ </StackItem>
+ <StackItem id="update-form">
+ <Card>
+ <CardBody>
+ <Form isHorizontal>{this.schemaToForm()}</Form>
+ </CardBody>
+ </Card>
+ </StackItem>
+ </Stack>
+ </PageSection>
+ </React.Fragment>
+ );
+ }
+}
+
+export default UpdateTablePage;
diff --git a/console/react/src/detailsTablePage.js b/console/react/src/detailsTablePage.js
index c253ca6..44d726d 100644
--- a/console/react/src/detailsTablePage.js
+++ b/console/react/src/detailsTablePage.js
@@ -80,13 +80,11 @@ class DetailTablesPage extends React.Component {
this.props.service,
this.props.schema
);
- this.locationState = this.props.locationState;
} else {
this.dataSource = new dataMap[this.entity](
this.props.service,
this.props.schema
);
- this.locationState = this.props.location.state;
}
}
}
@@ -102,6 +100,12 @@ class DetailTablesPage extends React.Component {
}
};
+ locationState = () => {
+ return this.props.details
+ ? this.props.locationState
+ : this.props.location.state;
+ };
+
update = () => {
this.mapRows().then(
rows => {
@@ -130,7 +134,7 @@ class DetailTablesPage extends React.Component {
}
this.dataSource
.fetchRecord(
- this.locationState.currentRecord,
+ this.locationState().currentRecord,
this.props.schema,
this.entity
)
@@ -152,8 +156,7 @@ class DetailTablesPage extends React.Component {
icap = s => s.charAt(0).toUpperCase() + s.slice(1);
- parentItem = () =>
- this.locationState.currentRecord[this.locationState.property];
+ parentItem = () => this.locationState().currentRecord.name;
breadcrumbSelected = () => {
if (this.props.details) {
@@ -162,11 +165,15 @@ class DetailTablesPage extends React.Component {
this.setState({
redirect: true,
redirectPath: `/overview/${this.entity}`,
- redirectState: this.locationState
+ redirectState: this.locationState()
});
}
};
+ handleActionClicked = (action, record) => {
+ this.props.handleEntityAction(action, record);
+ };
+
render() {
if (this.state.redirect) {
return (
@@ -179,6 +186,18 @@ class DetailTablesPage extends React.Component {
);
}
+ const actionsButtons = () => {
+ console.log("generating actionButtons for detailsTablePage");
+ console.log(this.locationState().currentRecord);
+ return this.dataSource.detailActions(
+ this.entity,
+ this.props,
+ this.locationState().currentRecord,
+ event =>
+ this.handleActionClicked(event, this.locationState().currentRecord)
+ );
+ };
+
return (
<React.Fragment>
<PageSection
@@ -196,7 +215,7 @@ class DetailTablesPage extends React.Component {
</BreadcrumbItem>
</Breadcrumb>
- <TextContent>
+ <TextContent className="details-table-header">
<Text className="overview-title" component={TextVariants.h1}>
{this.parentItem()}
</Text>
@@ -206,6 +225,7 @@ class DetailTablesPage extends React.Component {
lastUpdated={this.state.lastUpdated}
/>
)}
+ {this.props.details && actionsButtons()}
</TextContent>
</StackItem>
<StackItem className="overview-table">
diff --git a/console/react/src/index.js b/console/react/src/index.js
index c1684e8..7af6900 100644
--- a/console/react/src/index.js
+++ b/console/react/src/index.js
@@ -3,7 +3,19 @@ import ReactDOM from "react-dom";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
-ReactDOM.render(<App />, document.getElementById("root"));
+let config = { title: "Apache Qpid Dispatch Console" };
+fetch("/config.json")
+ .then(res => res.json())
+ .then(cfg => {
+ config = cfg;
+ console.log("successfully loaded console title from /config.json");
+ })
+ .catch(error => {
+ console.log("/config.json not found. Using default console title");
+ })
+ .finally(() =>
+ ReactDOM.render(<App config={config} />, document.getElementById("root"))
+ );
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index 242d1d8..ebc6f43 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -55,6 +55,7 @@ import DetailsTablePage from "./detailsTablePage";
import EntitiesPage from "./details/enitiesPage";
import TopologyPage from "./topology/topologyPage";
import MessageFlowPage from "./chord/chordPage";
+import SchemaPage from "./details/schema/schemaPage";
import LogDetails from "./overview/logDetails";
import { QDRService } from "./qdrService";
import ConnectForm from "./connect-form";
@@ -125,7 +126,7 @@ class PageLayout extends React.Component {
this.isDropdownOpen = false;
};
- handleConnect = (connectPath, connectInfo) => {
+ handleConnect = (connectPath, result) => {
if (this.state.connected) {
this.setState({ connectPath: "", connected: false }, () => {
this.handleConnectCancel();
@@ -138,38 +139,31 @@ class PageLayout extends React.Component {
);
});
} else {
- const connectOptions = JSON.parse(JSON.stringify(connectInfo));
- if (connectOptions.username === "") connectOptions.username = undefined;
- if (connectOptions.password === "") connectOptions.password = undefined;
- connectOptions.reconnect = true;
-
- this.service.connect(connectOptions).then(
- r => {
- this.schema = this.service.schema;
- if (connectPath === "/") connectPath = "/dashboard";
- const activeItem = connectPath.split("/").pop();
- // find the active group for this item
- let activeGroup = "overview";
- for (const group in this.nav) {
- if (this.nav[group].some(item => item.name === activeItem)) {
- activeGroup = group;
- break;
- }
- }
- this.handleConnectCancel();
- this.handleAddNotification("event", "Connected", new Date(), "info");
-
- this.setState({
- activeItem,
- activeGroup,
- connected: true,
- connectPath
- });
- },
- e => {
- console.log(e);
+ this.schema = this.service.schema;
+ if (connectPath === "/") connectPath = "/dashboard";
+ const activeItem = connectPath.split("/").pop();
+ // find the active group for this item
+ let activeGroup = "overview";
+ for (const group in this.nav) {
+ if (this.nav[group].some(item => item.name === activeItem)) {
+ activeGroup = group;
+ break;
}
+ }
+ this.handleConnectCancel();
+ this.handleAddNotification(
+ "event",
+ `Console connected to router`,
+ new Date(),
+ "success"
);
+
+ this.setState({
+ activeItem,
+ activeGroup,
+ connected: true,
+ connectPath
+ });
}
};
@@ -318,7 +312,7 @@ class PageLayout extends React.Component {
const Header = (
<PageHeader
className="topology-header"
- logo={<span className="logo-text">Apache Qpid Dispatch Console</span>}
+ logo={<span className="logo-text">{this.props.config.title}</span>}
toolbar={PageToolbar}
avatar={<Avatar src={avatarImg} alt="Avatar image" />}
showNavToggle
@@ -384,6 +378,7 @@ class PageLayout extends React.Component {
return (
<ConnectForm
ref={el => (this.connectFormRef = el)}
+ service={this.service}
isConnectFormOpen={this.isConnectFormOpen}
fromPath={"/"}
handleConnect={this.handleConnect}
@@ -420,10 +415,20 @@ class PageLayout extends React.Component {
<PrivateRoute path="/flow" component={MessageFlowPage} />
<PrivateRoute path="/logs" component={LogDetails} />
<PrivateRoute path="/entities" component={EntitiesPage} />
+ <PrivateRoute
+ path="/schema"
+ schema={this.schema}
+ component={SchemaPage}
+ />
<Route
path="/login"
render={props => (
- <ConnectPage {...props} handleConnect={this.handleConnect} />
+ <ConnectPage
+ {...props}
+ service={this.service}
+ config={this.props.config}
+ handleConnect={this.handleConnect}
+ />
)}
/>
</Switch>
@@ -434,23 +439,3 @@ class PageLayout extends React.Component {
}
export default PageLayout;
-
-/* <ToolbarItem>
- <ConnectForm prefix="toolbar" handleConnect={this.handleConnect} />
- </ToolbarItem>
-
-
-
- <Dropdown
- isPlain
- position="right"
- onSelect={this.onDropdownSelect}
- isOpen={isDropdownOpen}
- toggle={
- <DropdownToggle onToggle={this.onDropdownToggle}>
- anonymous
- </DropdownToggle>
- }
- dropdownItems={userDropdownItems}
- />
- */
diff --git a/console/react/src/notificationDrawer.js b/console/react/src/notificationDrawer.js
index 0ade746..583d678 100644
--- a/console/react/src/notificationDrawer.js
+++ b/console/react/src/notificationDrawer.js
@@ -18,13 +18,13 @@ under the License.
*/
import React from "react";
-import { NotificationBadge } from "@patternfly/react-core";
-import { Button } from "@patternfly/react-core";
import {
Accordion,
AccordionItem,
AccordionContent,
- AccordionToggle
+ AccordionToggle,
+ Button,
+ NotificationBadge
} from "@patternfly/react-core";
import {
@@ -33,7 +33,7 @@ import {
BellIcon,
TimesIcon
} from "@patternfly/react-icons";
-
+import AlertList from "./alertList";
import { safePlural } from "./qdrGlobals";
class NotificationDrawer extends React.Component {
@@ -55,6 +55,7 @@ class NotificationDrawer extends React.Component {
this.severityToIcon = {
info: { icon: "pficon-info", color: "#313131" },
error: { icon: "pficon-error-circle-o", color: "red" },
+ danger: { icon: "pficon-error-circle-o", color: "red" },
warning: { icon: "pficon-warning-triangle-o", color: "yellow" },
success: { icon: "pficon-ok", color: "green" }
};
@@ -92,7 +93,13 @@ class NotificationDrawer extends React.Component {
});
event.isRead = false;
accordionSections[section].events.unshift(event);
- this.setState({ accordionSections, isAnyUnread: true });
+ if (this.alertListRef) {
+ this.alertListRef.addAlert(severity, message);
+ }
+ this.setState({
+ accordionSections,
+ isAnyUnread: true
+ });
};
close = () => {
@@ -183,6 +190,7 @@ class NotificationDrawer extends React.Component {
<BellIcon />
</NotificationBadge>
</div>
+ {<AlertList ref={el => (this.alertListRef = el)} />}
{this.state.isShown && (
<div
ref={el => (this.notificationRef = el)}
diff --git a/console/react/src/overview/dashboard/dashboardPage.js b/console/react/src/overview/dashboard/dashboardPage.js
index 9feefcd..a7fca39 100644
--- a/console/react/src/overview/dashboard/dashboardPage.js
+++ b/console/react/src/overview/dashboard/dashboardPage.js
@@ -98,7 +98,10 @@ class DashboardPage extends React.Component {
<ActiveAddressesCard service={this.props.service} />
</SplitItem>
<SplitItem className="fill-card">
- <DelayedDeliveriesCard service={this.props.service} />
+ <DelayedDeliveriesCard
+ {...this.props}
+ service={this.props.service}
+ />
</SplitItem>
</Split>
</StackItem>
diff --git a/console/react/src/overview/dashboard/delayedDeliveriesCard.js b/console/react/src/overview/dashboard/delayedDeliveriesCard.js
index 72220b4..e6aaf4d 100644
--- a/console/react/src/overview/dashboard/delayedDeliveriesCard.js
+++ b/console/react/src/overview/dashboard/delayedDeliveriesCard.js
@@ -26,7 +26,11 @@ class DelayedDeliveriesCard extends React.Component {
closeButton = (value, extraInfo) => {
return (
- <ConnectionClose extraInfo={extraInfo} service={this.props.service} />
+ <ConnectionClose
+ extraInfo={extraInfo}
+ {...this.props}
+ service={this.props.service}
+ />
);
};
diff --git a/console/react/src/pleaseWait.js b/console/react/src/pleaseWait.js
new file mode 100644
index 0000000..228538b
--- /dev/null
+++ b/console/react/src/pleaseWait.js
@@ -0,0 +1,51 @@
+import React from "react";
+import { TextContent, Text, TextVariants } from "@patternfly/react-core";
+import PropTypes from "prop-types";
+
+import { CogIcon } from "@patternfly/react-icons";
+
+class PleaseWait extends React.Component {
+ static propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ title: PropTypes.string.isRequired,
+ message: PropTypes.string.isRequired
+ };
+
+ state = {};
+
+ render() {
+ return (
+ this.props.isOpen && (
+ <div className="topic-creating-wrapper">
+ <div id="topicCogWrapper">
+ <CogIcon
+ id="topicCogMain"
+ className="spinning-clockwise"
+ color="#AAAAAA"
+ />
+ <CogIcon
+ id="topicCogUpper"
+ className="spinning-cclockwise"
+ color="#AAAAAA"
+ />
+ <CogIcon
+ id="topicCogLower"
+ className="spinning-cclockwise"
+ color="#AAAAAA"
+ />
+ </div>
+ <TextContent>
+ <Text component={TextVariants.h3}>{this.props.title}</Text>
+ </TextContent>
+ <TextContent>
+ <Text className="topic-creating-message" component={TextVariants.p}>
+ {this.props.message}
+ </Text>
+ </TextContent>
+ </div>
+ )
+ );
+ }
+}
+
+export default PleaseWait;
diff --git a/console/react/src/qdrGlobals.js b/console/react/src/qdrGlobals.js
index 8dc2b47..67076ad 100644
--- a/console/react/src/qdrGlobals.js
+++ b/console/react/src/qdrGlobals.js
@@ -17,8 +17,6 @@ specific language governing permissions and limitations
under the License.
*/
-import config from "./config.json";
-/* globals Promise */
export var QDRFolder = (function() {
function Folder(title) {
this.title = title;
@@ -56,14 +54,6 @@ export const QDRTemplatePath = "html/";
export const QDR_LAST_LOCATION = "QDRLastLocation";
export const QDR_INTERVAL = "QDRInterval";
-export var getConfigVars = () =>
- new Promise(resolve => {
- const s = {};
- s.QDR_CONSOLE_TITLE = config.title;
- document.title = s.QDR_CONSOLE_TITLE;
- resolve(s);
- });
-
export const safePlural = (count, str) => {
if (count === 1) return str;
var es = ["x", "ch", "ss", "sh"];
diff --git a/console/react/src/tableToolbar.jsx b/console/react/src/tableToolbar.jsx
index 69824d5..b8748bb 100644
--- a/console/react/src/tableToolbar.jsx
+++ b/console/react/src/tableToolbar.jsx
@@ -19,7 +19,6 @@ under the License.
import React from "react";
import {
- Button,
Dropdown,
DropdownPosition,
DropdownToggle,
@@ -116,28 +115,13 @@ class TableToolbar extends React.Component {
};
render() {
- const actions =
- this.props.actions &&
- this.props.actions.map(action => {
- let variant = "primary";
- let isDisabled = false;
- if (action === "DELETE" && !this.props.hasChecked) {
- variant = "tertiary";
- isDisabled = true;
- }
- return (
- <ToolbarItem className="pf-u-mx-md" key={action}>
- <Button
- aria-label={action}
- onClick={() => this.props.handleAction(action)}
- variant={variant}
- isDisabled={isDisabled}
- >
- {action}
- </Button>
- </ToolbarItem>
- );
- });
+ const actionsButtons =
+ this.props.actionButtons &&
+ Object.keys(this.props.actionButtons).map(action => (
+ <ToolbarItem className="pf-u-mx-md" key={`toolbar-item-${action}`}>
+ {this.props.actionButtons[action]}
+ </ToolbarItem>
+ ));
return (
<Toolbar className="pf-l-toolbar pf-u-mx-xl pf-u-my-md table-toolbar">
@@ -149,7 +133,9 @@ class TableToolbar extends React.Component {
{this.buildSearchBox()}
</ToolbarItem>
</ToolbarGroup>
- {this.props.actions && <ToolbarGroup>{actions}</ToolbarGroup>}
+ {this.props.actionButtons && (
+ <ToolbarGroup>{actionsButtons}</ToolbarGroup>
+ )}
{!this.props.hidePagination && (
<ToolbarGroup className="toolbar-pagination">
<ToolbarItem>
diff --git a/console/react/yarn.lock b/console/react/yarn.lock
index 78edc2e..59b7b90 100644
--- a/console/react/yarn.lock
+++ b/console/react/yarn.lock
@@ -4830,7 +4830,7 @@ debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
-debuglog@*, debuglog@^1.0.1:
+debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -7108,7 +7108,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -8677,11 +8677,6 @@ lodash-es@^4.17.11:
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
-lodash._baseindexof@*:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
- integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
-
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -8690,33 +8685,11 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
-lodash._bindcallback@*:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
- integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
-
-lodash._cacheindexof@*:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
- integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
-
-lodash._createcache@*:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
- integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
- dependencies:
- lodash._getnative "^3.0.0"
-
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
-lodash._getnative@*, lodash._getnative@^3.0.0:
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
- integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
-
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
@@ -8777,11 +8750,6 @@ lodash.memoize@^4.1.2:
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
-lodash.restparam@*:
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
- integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
-
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
diff --git a/python/qpid_dispatch_internal/dispatch.py b/python/qpid_dispatch_internal/dispatch.py
index 8b86dae..b6e5d5f 100644
--- a/python/qpid_dispatch_internal/dispatch.py
+++ b/python/qpid_dispatch_internal/dispatch.py
@@ -71,6 +71,7 @@ class QdDll(ctypes.PyDLL):
self._prototype(self.qd_connection_manager_delete_listener, None, [self.qd_dispatch_p, ctypes.c_void_p])
self._prototype(self.qd_connection_manager_delete_connector, None, [self.qd_dispatch_p, ctypes.c_void_p])
self._prototype(self.qd_connection_manager_delete_ssl_profile, ctypes.c_bool, [self.qd_dispatch_p, ctypes.c_void_p])
+ self._prototype(self.qd_connection_manager_delete_sasl_plugin, ctypes.c_bool, [self.qd_dispatch_p, ctypes.c_void_p])
self._prototype(self.qd_dispatch_configure_address, None, [self.qd_dispatch_p, py_object])
self._prototype(self.qd_dispatch_configure_link_route, None, [self.qd_dispatch_p, py_object])
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org