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