From 717bc2f40b48448b97948c8372d19d3c328fe71b Mon Sep 17 00:00:00 2001 From: Peter Teixeira Date: Mon, 13 Feb 2017 12:13:49 -0500 Subject: [PATCH 1/5] Finish UI portion of ServiceDetail page Finish off (most of) the visual portion of the ServiceDetail page. I still need to attach the interactions, including the modals, and I would like to tool around some more with the request history panel. The existing pane is filterable and can be paged through, and I would like to at least get filterable done. Unfortunately, that is probably going to involve making my own component rather than reusing the `UITable` component. --- .../components/serviceDetail/DetailHeader.jsx | 58 ++++++++++ .../serviceDetail/LoadBalancersPanel.jsx | 33 ++++++ .../components/serviceDetail/OwnersPanel.jsx | 32 ++++++ .../serviceDetail/RecentRequestsPanel.jsx | 102 ++++++++++++++++++ .../serviceDetail/ServiceDetail.jsx | 67 ++++++++---- .../serviceDetail/UpstreamsPanel.jsx | 42 ++++++++ .../app/components/serviceDetail/util.jsx | 44 ++++++++ .../app/components/services/Services.jsx | 2 +- BaragonUI/app/router.jsx | 4 +- 9 files changed, 362 insertions(+), 22 deletions(-) create mode 100644 BaragonUI/app/components/serviceDetail/DetailHeader.jsx create mode 100644 BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx create mode 100644 BaragonUI/app/components/serviceDetail/OwnersPanel.jsx create mode 100644 BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx create mode 100644 BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx create mode 100644 BaragonUI/app/components/serviceDetail/util.jsx diff --git a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx new file mode 100644 index 000000000..8d06e1165 --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx @@ -0,0 +1,58 @@ +import React, { PropTypes } from 'react'; + +import JSONButton from '../common/JSONButton'; + +const showJSONButton = (object) => { + return ( + + JSON + + ); +}; + +const ButtonContainer = ({editable, object}) => { + if (!editable) { + return ( +
+ {showJSONButton(object)} +
+ ); + } + + return ( +
+ {showJSONButton(object)} + Reload Configs + Remove Upstream + Delete +
+ ); +}; + +ButtonContainer.propTypes = { + editable: PropTypes.bool, + object: PropTypes.object, +}; + +const DetailHeader = ({id, basePath, editable, object}) => { + return ( +
+
+

{ id }

+
+
+

{ basePath }

+
+ +
+ ); +}; + +DetailHeader.propTypes = { + id: PropTypes.string, + basePath: PropTypes.string, + editable: PropTypes.bool, + object: PropTypes.object, +}; + +export default DetailHeader; diff --git a/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx b/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx new file mode 100644 index 000000000..f507a637d --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx @@ -0,0 +1,33 @@ +import React, { PropTypes } from 'react'; +import { Link } from 'react-router'; + +import { asGroups } from './util'; + +const renderLoadBalancerGroup = (loadBalancerGroup) => { + return ( +
  • + {loadBalancerGroup} +
  • + ); +}; + +const LoadBalancersPanel = ({loadBalancerGroups}) => { + return ( +
    +
    +
    +

    Load Balancer Groups

    +
    +
    + { asGroups(loadBalancerGroups, 2, renderLoadBalancerGroup) } +
    +
    +
    + ); +}; + +LoadBalancersPanel.propTypes = { + loadBalancerGroups: PropTypes.array, +}; + +export default LoadBalancersPanel; diff --git a/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx b/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx new file mode 100644 index 000000000..d97da7abf --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx @@ -0,0 +1,32 @@ +import React, { PropTypes } from 'react'; + +import { asGroups } from './util'; + +const renderOwner = (owner, index) => { + return ( +
  • + {owner} +
  • + ); +}; + +const OwnersPanel = ({owners}) => { + return ( +
    +
    +
    +

    Owners

    +
    +
    + { asGroups(owners, 2, renderOwner) } +
    +
    +
    + ); +}; + +OwnersPanel.propTypes = { + owners: PropTypes.array, +}; + +export default OwnersPanel; diff --git a/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx b/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx new file mode 100644 index 000000000..54d0a3c73 --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx @@ -0,0 +1,102 @@ +import React, { PropTypes } from 'react'; +import { Link } from 'react-router'; + +import UITable from '../common/table/UITable'; +import Column from '../common/table/Column'; +import JSONButton from '../common/JSONButton'; + +import { iconByState } from './util'; + +const requestJSONButton = (request) => { + if (request) { + return ( + + {'{ }'} + + ); + } else { + return {'{ }'}; + } +}; + +const iconForRequest = (request) => { + return ; +}; + +const requestLink = ({loadBalancerRequestId}) => { + return ( + + {loadBalancerRequestId} + + ); +}; + +const HistoryTable = ({history}) => { + if (! history) { + return ( +
    +

    No recent requests

    +
    + ); + } + + return ( + request.loadBalancerRequestId} + paginated={false} + > + + + request.action || 'UPDATE'} + /> + request.loadBalancerState} + /> + + + ); +}; + +HistoryTable.propTypes = { + history: PropTypes.arrayOf(PropTypes.object), +}; + +const RecentRequestsPanel = ({requests}) => { + return ( +
    +
    +
    +

    Recent Requests

    +
    +
    + +
    +
    +
    + ); +}; + +RecentRequestsPanel.propTypes = { + requests: PropTypes.arrayOf(PropTypes.object), +}; + +export default RecentRequestsPanel; diff --git a/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx b/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx index 605dcb617..fde283293 100644 --- a/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx +++ b/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx @@ -1,29 +1,58 @@ -import React, { Component, PropTypes } from 'react'; +import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; -import classNames from 'classnames'; -import { Link } from 'react-router'; import rootComponent from '../../rootComponent'; import { refresh } from '../../actions/ui/serviceDetail'; import Utils from '../../utils'; -class ServiceDetail extends Component { - render() { - // TODO - render out info nicely - // - JSON button (or other display?) for request history items - // - reload / delete service / json button (use existing modals) - // - Remove upstream (requires new modal + building request json) - // - submit request modal should either show request json or link to page for it - return

    Service Detail

    - } +import DetailHeader from './DetailHeader'; +import OwnersPanel from './OwnersPanel'; +import LoadBalancersPanel from './LoadBalancersPanel'; +import RecentRequestsPanel from './RecentRequestsPanel'; +import UpstreamsPanel from './UpstreamsPanel'; - static propTypes = { - params: PropTypes.object.isRequired, - service: React.PropTypes.object, - requestHistory: React.PropTypes.array - }; -} +const ServiceDetail = ({service, requestHistory, editable}) => { + const {service: serviceObject, upstreams} = service; + const { + serviceId, + owners, + loadBalancerGroups, + serviceBasePath: basePath + } = serviceObject; + return ( +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + ); +}; + +ServiceDetail.propTypes = { + service: PropTypes.object, + requestHistory: PropTypes.array, + editable: PropTypes.bool, +}; export default connect((state, ownProps) => ({ service: Utils.maybe(state, ['api', 'service', ownProps.params.serviceId, 'data']), - requestHistory: Utils.maybe(state, ['api', 'requestHistory', ownProps.params.serviceId, 'data']) + requestHistory: Utils.maybe(state, ['api', 'requestHistory', ownProps.params.serviceId, 'data']), + editable: true, }))(rootComponent(ServiceDetail, (props) => refresh(props.params.serviceId))); diff --git a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx new file mode 100644 index 000000000..7ae57f1ff --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx @@ -0,0 +1,42 @@ +import React, { PropTypes } from 'react'; +import { Glyphicon } from 'react-bootstrap'; + +import { asGroups } from './util'; + +const renderUpstream = (editable) => ({upstream, rackId, group}) => { + const editButton = ( + + + + ); + + return ( +

    + {editable ? editButton : null} + {upstream} {rackId ? `(${rackId})` : ''} {group ? `(${group})` : ''} +

    + ); +}; + + +const UpstreamsPanel = ({upstreams, editable}) => { + return ( +
    +
    +
    +

    Upstreams

    +
    +
    + { asGroups(upstreams, 2, renderUpstream(editable)) } +
    +
    +
    + ); +}; + +UpstreamsPanel.propTypes = { + upstreams: PropTypes.array, + editable: PropTypes.bool, +}; + +export default UpstreamsPanel; diff --git a/BaragonUI/app/components/serviceDetail/util.jsx b/BaragonUI/app/components/serviceDetail/util.jsx new file mode 100644 index 000000000..431b5b53e --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/util.jsx @@ -0,0 +1,44 @@ +import React from 'react'; + +// [T], PosInt -> [[T]] +// [1, 2, 3, 4, 5, 6, 7], 2 -> [[1, 2], [3, 4], [5, 6], [7]] +const chunk = (arr, size) => { + return _.chain(arr) + .groupBy((elem, index) => Math.floor(index / size)) + .toArray() + .value(); +}; + +// ((T, PosInt) -> DOMElement) -> ([T], PosInt) -> DOMElement +const rowRenderer = (itemRenderer, columns) => (row, index) => { + return ( +
    + +
    + ); +}; + +// [T], ((T, PosInt) -> DOMElement) -> DOMElement +export const asGroups = (arr, columns, itemRenderer) => { + return chunk(arr, arr.length / columns) + .map(rowRenderer(itemRenderer, columns)); +}; + +export const iconByState = (state) => { + switch (state) { + case 'SUCCESS': + return 'glyphicon glyphicon-ok-circle active'; + case 'FAILED': + return 'glyphicon glyphicon-ban-circle inactive'; + case 'WAITING': + return 'glyphicon glyphicon-time'; + case 'CANCELING': + return 'glyphicon glyphicon-remove-circle inactive'; + case 'INVALID_REQUEST_NOOP': + return 'glyphicon glyphicon-exclamation-sign inactive'; + default: + return 'glyphicon glyphicon-question-sign'; + } +}; diff --git a/BaragonUI/app/components/services/Services.jsx b/BaragonUI/app/components/services/Services.jsx index f21f21ce0..70b5c09af 100644 --- a/BaragonUI/app/components/services/Services.jsx +++ b/BaragonUI/app/components/services/Services.jsx @@ -30,7 +30,7 @@ class Services extends Component { id="serviceId" key="serviceId" cellData={ - (rowData) => ({rowData.service.serviceId}) + (rowData) => ({rowData.service.serviceId}) } sortable={true} /> diff --git a/BaragonUI/app/router.jsx b/BaragonUI/app/router.jsx index 0fd187691..56bd3a2c5 100644 --- a/BaragonUI/app/router.jsx +++ b/BaragonUI/app/router.jsx @@ -19,9 +19,9 @@ const routes = ( - `${params.serviceId}`} /> + `${params.serviceId}`} /> - `${params.groupId}`} /> + `${params.groupId}`} /> `${params.requestId}`} /> ); From c3da152b0205fd98bda04ec58a045de236c3da22 Mon Sep 17 00:00:00 2001 From: Peter Teixeira Date: Mon, 13 Feb 2017 15:44:56 -0500 Subject: [PATCH 2/5] Attach modals to service detail page Attach the modals to remove upstreams, reload configs, and delete the modal to the service detail page. Still to be done is paginating and filtering the request history in the 'Recent Requests' panel. --- BaragonUI/app/actions/api/services.es6 | 23 ++++++- .../modalButtons/DeleteServiceModal.jsx | 27 ++++++-- .../modalButtons/RemoveUpstreamButton.jsx | 47 ++++++++++++++ .../modalButtons/RemoveUpstreamModal.jsx | 62 ++++++++++++++++++ .../modalButtons/RemoveUpstreamsButton.jsx | 48 ++++++++++++++ .../modalButtons/RemoveUpstreamsModal.jsx | 63 +++++++++++++++++++ .../components/serviceDetail/DetailHeader.jsx | 61 ++++++++++++++---- .../serviceDetail/ServiceDetail.jsx | 32 ++++++++-- .../serviceDetail/UpstreamsPanel.jsx | 28 ++++++--- BaragonUI/app/styles/stylus/main.styl | 5 ++ 10 files changed, 365 insertions(+), 31 deletions(-) create mode 100644 BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx create mode 100644 BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx create mode 100644 BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx create mode 100644 BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx diff --git a/BaragonUI/app/actions/api/services.es6 b/BaragonUI/app/actions/api/services.es6 index 43f764d41..354c4fbba 100644 --- a/BaragonUI/app/actions/api/services.es6 +++ b/BaragonUI/app/actions/api/services.es6 @@ -1,5 +1,7 @@ import { buildApiAction, buildJsonApiAction } from './base'; +const buildRequestId = (serviceId) => `${serviceId}-${Date.now()}`; + export const FetchBaragonServices = buildApiAction( 'FETCH_BARAGON_SERVICES', {url: '/state'} @@ -17,8 +19,8 @@ export const FetchService = buildApiAction( export const DeleteService = buildJsonApiAction( 'DELETE_SERVICE', 'DELETE', - (serviceId) => ({ - url: `/state/${serviceId}`, + (serviceId, noValidate = false, noReload = false) => ({ + url: `/state/${serviceId}?noValidate=${noValidate}&noReload=${noReload}`, }) ); @@ -29,3 +31,20 @@ export const ReloadService = buildJsonApiAction( url: `/state/${serviceId}/reload`, }) ); + +export const RemoveUpstreams = buildJsonApiAction( + 'REMOVE_UPSTREAMS', + 'POST', + (loadBalancerService, upstreams, noValidate, noReload) => ({ + url: '/request', + body: { + loadBalancerService, + noValidate, + noReload, + loadBalancerRequestId: buildRequestId(loadBalancerService.serviceId), + addUpstreams: [], + removeUpstreams: upstreams, + }, + }), + (serviceId) => serviceId, +); diff --git a/BaragonUI/app/components/common/modalButtons/DeleteServiceModal.jsx b/BaragonUI/app/components/common/modalButtons/DeleteServiceModal.jsx index a93852913..58d69c297 100644 --- a/BaragonUI/app/components/common/modalButtons/DeleteServiceModal.jsx +++ b/BaragonUI/app/components/common/modalButtons/DeleteServiceModal.jsx @@ -23,16 +23,35 @@ class DeleteServiceModal extends Component { action="Delete" onConfirm={this.props.deleteService} buttonStyle="danger" - formElements={[]}> -

    Are you sure you want to reload configs this service?

    + formElements={[ + { + name: 'noValidate', + type: FormModal.INPUT_TYPES.BOOLEAN, + label: 'Validate new configuration after applying changes' + }, + { + name: 'noReload', + type: FormModal.INPUT_TYPES.BOOLEAN, + label: 'Reload configuration after applying changes', + }, + ]}> +

    Are you sure you sure you want to delete this service?

    {this.props.serviceId}
    +

    + Deleting a service will remove the entry from Baragon's state node as + well as clearing the locks on any associated base paths. It will also + remove the configs from the load balancer (they will be backed up for + reference). +

    ); } } const mapDispatchToProps = (dispatch, ownProps) => ({ - deleteService: () => dispatch(DeleteService.trigger(ownProps.serviceId)).then(response => (ownProps.then && ownProps.then(response))) + deleteService: (data) => dispatch(DeleteService + .trigger(ownProps.serviceId, data.noValidate, data.noReload)) + .then(response => (ownProps.then && ownProps.then(response))) }); export default connect( @@ -40,4 +59,4 @@ export default connect( mapDispatchToProps, null, { withRef: true } -)(DeleteServiceModal); \ No newline at end of file +)(DeleteServiceModal); diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx new file mode 100644 index 000000000..b6b09de0c --- /dev/null +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx @@ -0,0 +1,47 @@ +import React, { Component, PropTypes } from 'react'; + +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import ToolTip from 'react-bootstrap/lib/Tooltip'; +import { Glyphicon } from 'react-bootstrap'; + +import { getClickComponent } from '../modal/ModalWrapper'; + +import RemoveUpstreamModal from './RemoveUpstreamModal'; + +const removeTooltip = ( + + Remove this upstream from the service + +); + +export default class RemoveUpstreamButton extends Component { + static propTypes = { + // TODO be more specific + loadBalancerService: PropTypes.object, + upstream: PropTypes.object, + children: PropTypes.node, + afterRemoveUpstream: PropTypes.func, + }; + + static defaultProps = { + children: ( + + + + ) + }; + + render() { + return ( + + {getClickComponent(this)} + + + ); + } +} diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx new file mode 100644 index 000000000..84c57f800 --- /dev/null +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx @@ -0,0 +1,62 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; + +import { RemoveUpstreams } from '../../../actions/api/services'; + +import FormModal from '../modal/FormModal'; + +class RemoveUpstreamModal extends Component { + static propTypes = { + loadBalancerService: PropTypes.object.isRequired, + upstream: PropTypes.object.isRequired, + removeUpstream: PropTypes.func.isRequired, + then: PropTypes.func, + }; + + show() { + this.refs.removeUpstreamModal.show(); + } + + render() { + return ( + +

    Are you sure you want to remove this upstream?

    +
    {this.props.upstream.upstream}
    +

    + This will post a new request to remove this upstream from the nginx + config. It will not alter any other upstreams or options. +

    +
    + ); + } +} + +const mapDispatchToProps = (dispatch, ownProps) => ({ + removeUpstream: (data) => dispatch(RemoveUpstreams + .trigger(ownProps.loadBalancerService, [ownProps.upstream], data.noValidate, data.noReload)) + .then(response => (ownProps.then && ownProps.then(response))) +}); + +export default connect( + null, + mapDispatchToProps, + null, + { withRef: true } +)(RemoveUpstreamModal); diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx new file mode 100644 index 000000000..bddcdb1bf --- /dev/null +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx @@ -0,0 +1,48 @@ +import React, { PropTypes } from 'react'; + +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import ToolTip from 'react-bootstrap/lib/Tooltip'; + +import { getClickComponent } from '../modal/ModalWrapper'; + +import RemoveUpstreamsModal from './RemoveUpstreamsModal'; + +const removeTooltip = ( + + Remove all upstreams from this service + +); + +export default class RemoveUpstreamsButton extends React.Component { + static propTypes = { + // TODO be more specific + loadBalancerService: PropTypes.object, + upstreams: PropTypes.array, + children: PropTypes.node, + afterRemoveUpstreams: PropTypes.func, + }; + + static defaultProps = { + children: ( + + + Remove Upstreams + + + ) + }; + + render() { + return ( + + {getClickComponent(this)} + + + ); + } +} diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx new file mode 100644 index 000000000..350376e82 --- /dev/null +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx @@ -0,0 +1,63 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; + +import { RemoveUpstreams } from '../../../actions/api/services'; + +import FormModal from '../modal/FormModal'; + +class RemoveUpstreamsModal extends Component { + static propTypes = { + loadBalancerService: PropTypes.object.isRequired, + upstreams: PropTypes.array.isRequired, + removeUpstreams: PropTypes.func.isRequired, + then: PropTypes.func, + }; + + show() { + this.refs.removeUpstreamsModal.show(); + } + + render() { + return ( + +

    Are you sure you want to remove all upstreams for this service?

    +
    {this.props.loadBalancerService.serviceId}
    +

    + This will post a new request to remove all the current upstreams + for a service. This will effectively 'undo' the request by creating + empty config files. +

    +
    + ); + } +} + +const mapDispatchToProps = (dispatch, ownProps) => ({ + removeUpstreams: (data) => dispatch(RemoveUpstreams + .trigger(ownProps.loadBalancerService, ownProps.upstreams, data.noValidate, data.noReload)) + .then(response => (ownProps.then && ownProps.then(response))) +}); + +export default connect( + null, + mapDispatchToProps, + null, + { withRef: true } +)(RemoveUpstreamsModal); diff --git a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx index 8d06e1165..7f11cf73d 100644 --- a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx +++ b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx @@ -1,40 +1,64 @@ import React, { PropTypes } from 'react'; import JSONButton from '../common/JSONButton'; +import RemoveUpstreamsButton from '../common/modalButtons/RemoveUpstreamsButton'; +import ReloadServiceButton from '../common/modalButtons/ReloadServiceButton'; +import DeleteServiceButton from '../common/modalButtons/DeleteServiceButton'; -const showJSONButton = (object) => { +const showJSONButton = (serviceJson) => { return ( - + JSON ); }; -const ButtonContainer = ({editable, object}) => { +const ButtonContainer = ({editable, serviceJson, upstreams, + afterRemoveUpstreams, afterReload, afterDelete}) => { if (!editable) { return (
    - {showJSONButton(object)} + {showJSONButton(serviceJson)}
    ); } return (
    - {showJSONButton(object)} - Reload Configs - Remove Upstream - Delete + {showJSONButton(serviceJson)} + + Reload Configs + + + + Delete +
    ); }; ButtonContainer.propTypes = { - editable: PropTypes.bool, - object: PropTypes.object, + editable: PropTypes.bool.isRequired, + serviceJson: PropTypes.object.isRequired, + // TODO be more specific + upstreams: PropTypes.array.isRequired, + afterRemoveUpstreams: PropTypes.func, + afterReload: PropTypes.func, + afterDelete: PropTypes.func, }; -const DetailHeader = ({id, basePath, editable, object}) => { +const DetailHeader = ({id, basePath, editable, serviceJson, upstreams, + afterRemoveUpstreams, afterReload, afterDelete}) => { return (
    @@ -43,7 +67,14 @@ const DetailHeader = ({id, basePath, editable, object}) => {

    { basePath }

    - +
    ); }; @@ -52,7 +83,11 @@ DetailHeader.propTypes = { id: PropTypes.string, basePath: PropTypes.string, editable: PropTypes.bool, - object: PropTypes.object, + serviceJson: PropTypes.object, + upstreams: PropTypes.array, + afterRemoveUpstreams: PropTypes.func, + afterReload: PropTypes.func, + afterDelete: PropTypes.func, }; export default DetailHeader; diff --git a/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx b/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx index fde283293..c1462f01a 100644 --- a/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx +++ b/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react'; import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; import rootComponent from '../../rootComponent'; import { refresh } from '../../actions/ui/serviceDetail'; import Utils from '../../utils'; @@ -10,7 +11,8 @@ import LoadBalancersPanel from './LoadBalancersPanel'; import RecentRequestsPanel from './RecentRequestsPanel'; import UpstreamsPanel from './UpstreamsPanel'; -const ServiceDetail = ({service, requestHistory, editable}) => { +const ServiceDetail = ({service, requestHistory, editable, + afterRemoveUpstreams, afterRemoveUpstream, afterReload, afterDelete}) => { const {service: serviceObject, upstreams} = service; const { serviceId, @@ -23,9 +25,14 @@ const ServiceDetail = ({service, requestHistory, editable}) => {
    @@ -37,7 +44,9 @@ const ServiceDetail = ({service, requestHistory, editable}) => {
    @@ -49,10 +58,23 @@ ServiceDetail.propTypes = { service: PropTypes.object, requestHistory: PropTypes.array, editable: PropTypes.bool, + afterRemoveUpstreams: PropTypes.func.isRequired, + afterRemoveUpstream: PropTypes.func.isRequired, + afterReload: PropTypes.func.isRequired, + afterDelete: PropTypes.func.isRequired, }; -export default connect((state, ownProps) => ({ +const mapStateToProps = (state, ownProps) => ({ service: Utils.maybe(state, ['api', 'service', ownProps.params.serviceId, 'data']), requestHistory: Utils.maybe(state, ['api', 'requestHistory', ownProps.params.serviceId, 'data']), - editable: true, -}))(rootComponent(ServiceDetail, (props) => refresh(props.params.serviceId))); + editable: window.config.allowEdit, +}); + +const mapDispatchToProps = (dispatch, ownProps) => ({ + afterRemoveUpstreams: () => refresh(ownProps.params.serviceId)(dispatch), + afterRemoveUpstream: () => refresh(ownProps.params.serviceId)(dispatch), + afterReload: () => refresh(ownProps.params.serviceId)(dispatch), + afterDelete: () => ownProps.router.push('/services'), +}); + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(rootComponent(ServiceDetail, (props) => refresh(props.params.serviceId)))); diff --git a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx index 7ae57f1ff..1b76ca436 100644 --- a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx @@ -1,25 +1,31 @@ import React, { PropTypes } from 'react'; -import { Glyphicon } from 'react-bootstrap'; import { asGroups } from './util'; -const renderUpstream = (editable) => ({upstream, rackId, group}) => { +import RemoveUpstreamButton from '../common/modalButtons/RemoveUpstreamButton'; + +const renderUpstream = (editable, loadBalancerService, afterRemoveUpstream) => (upstream) => { + const {upstream: upstreamName, rackId, group} = upstream; const editButton = ( - + ); return ( -

    +

    {editable ? editButton : null} - {upstream} {rackId ? `(${rackId})` : ''} {group ? `(${group})` : ''} + {upstreamName} {rackId ? `(${rackId})` : ''} {group ? `(${group})` : ''}

    ); }; -const UpstreamsPanel = ({upstreams, editable}) => { +const UpstreamsPanel = ({upstreams, loadBalancerService, afterRemoveUpstream, editable}) => { return (
    @@ -27,7 +33,13 @@ const UpstreamsPanel = ({upstreams, editable}) => {

    Upstreams

    - { asGroups(upstreams, 2, renderUpstream(editable)) } + { + asGroups( + upstreams, + 2, + renderUpstream(editable, loadBalancerService, afterRemoveUpstream) + ) + }
    @@ -36,6 +48,8 @@ const UpstreamsPanel = ({upstreams, editable}) => { UpstreamsPanel.propTypes = { upstreams: PropTypes.array, + loadBalancerService: PropTypes.object, + afterRemoveUpstream: PropTypes.func, editable: PropTypes.bool, }; diff --git a/BaragonUI/app/styles/stylus/main.styl b/BaragonUI/app/styles/stylus/main.styl index 6e1383173..9fb45f44e 100644 --- a/BaragonUI/app/styles/stylus/main.styl +++ b/BaragonUI/app/styles/stylus/main.styl @@ -112,3 +112,8 @@ code .help color #999 font-size 0.9em + +.active + color $green +.inactive + color $orange From 2acfe4c8bba232c0131442c2d74d74080f672503 Mon Sep 17 00:00:00 2001 From: Peter Teixeira Date: Tue, 14 Feb 2017 10:43:35 -0500 Subject: [PATCH 3/5] Attach pagination/search to requests panel Limit the number of results displayed at a time to 5, and add buttons so that the user can page through the results. Also add a quick search box to the table. Neither the paging not the search are nearly as nice as on the existing system; in particular, the table doesn't have the "Showing {} from {} (filtered from {})" text on the original, and the search uses `fuzzy`'s default implementation, which means that the results can seem a little bit off kilter. --- .../modalButtons/RemoveUpstreamButton.jsx | 8 +- .../modalButtons/RemoveUpstreamsButton.jsx | 8 +- .../components/serviceDetail/DetailHeader.jsx | 8 +- .../serviceDetail/LoadBalancersPanel.jsx | 2 +- .../components/serviceDetail/OwnersPanel.jsx | 2 +- .../serviceDetail/RecentRequestsPanel.jsx | 78 +++++++++++++------ .../serviceDetail/ServiceDetail.jsx | 8 +- .../serviceDetail/UpstreamsPanel.jsx | 7 +- .../app/components/serviceDetail/util.jsx | 9 +++ 9 files changed, 97 insertions(+), 33 deletions(-) diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx index b6b09de0c..75b8c467b 100644 --- a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamButton.jsx @@ -16,9 +16,13 @@ const removeTooltip = ( export default class RemoveUpstreamButton extends Component { static propTypes = { - // TODO be more specific loadBalancerService: PropTypes.object, - upstream: PropTypes.object, + upstream: PropTypes.shape({ + group: PropTypes.string, + rackId: PropTypes.string, + requestId: PropTypes.string.isRequired, + upstream: PropTypes.string.isRequired, + }), children: PropTypes.node, afterRemoveUpstream: PropTypes.func, }; diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx index bddcdb1bf..e878baed4 100644 --- a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsButton.jsx @@ -15,9 +15,13 @@ const removeTooltip = ( export default class RemoveUpstreamsButton extends React.Component { static propTypes = { - // TODO be more specific loadBalancerService: PropTypes.object, - upstreams: PropTypes.array, + upstreams: PropTypes.arrayOf(PropTypes.shape({ + group: PropTypes.string, + rackId: PropTypes.string, + requestId: PropTypes.string.isRequired, + upstream: PropTypes.string.isRequired, + })), children: PropTypes.node, afterRemoveUpstreams: PropTypes.func, }; diff --git a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx index 7f11cf73d..8a94ddfc4 100644 --- a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx +++ b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx @@ -50,8 +50,12 @@ const ButtonContainer = ({editable, serviceJson, upstreams, ButtonContainer.propTypes = { editable: PropTypes.bool.isRequired, serviceJson: PropTypes.object.isRequired, - // TODO be more specific - upstreams: PropTypes.array.isRequired, + upstreams: PropTypes.arrayOf(PropTypes.shape({ + group: PropTypes.string, + rackId: PropTypes.string, + requestId: PropTypes.string.isRequired, + upstream: PropTypes.string.isRequired, + })).isRequired, afterRemoveUpstreams: PropTypes.func, afterReload: PropTypes.func, afterDelete: PropTypes.func, diff --git a/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx b/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx index f507a637d..d44326e65 100644 --- a/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx @@ -27,7 +27,7 @@ const LoadBalancersPanel = ({loadBalancerGroups}) => { }; LoadBalancersPanel.propTypes = { - loadBalancerGroups: PropTypes.array, + loadBalancerGroups: PropTypes.arrayOf(PropTypes.string), }; export default LoadBalancersPanel; diff --git a/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx b/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx index d97da7abf..d0e138d4b 100644 --- a/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx @@ -26,7 +26,7 @@ const OwnersPanel = ({owners}) => { }; OwnersPanel.propTypes = { - owners: PropTypes.array, + owners: PropTypes.arrayOf(PropTypes.string), }; export default OwnersPanel; diff --git a/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx b/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx index 54d0a3c73..1966f1993 100644 --- a/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx @@ -1,11 +1,11 @@ -import React, { PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import { Link } from 'react-router'; import UITable from '../common/table/UITable'; import Column from '../common/table/Column'; import JSONButton from '../common/JSONButton'; -import { iconByState } from './util'; +import { iconByState, matches } from './util'; const requestJSONButton = (request) => { if (request) { @@ -31,7 +31,7 @@ const requestLink = ({loadBalancerRequestId}) => { ); }; -const HistoryTable = ({history}) => { +const HistoryTable = ({history, filter}) => { if (! history) { return (
    @@ -40,11 +40,14 @@ const HistoryTable = ({history}) => { ); } + const contents = matches(filter, history); + return ( request.loadBalancerRequestId} - paginated={false} + paginated={true} + rowChunkSize={5} > { HistoryTable.propTypes = { history: PropTypes.arrayOf(PropTypes.object), + filter: PropTypes.string, }; -const RecentRequestsPanel = ({requests}) => { - return ( -
    -
    -
    -

    Recent Requests

    -
    -
    - -
    -
    -
    - ); -}; -RecentRequestsPanel.propTypes = { - requests: PropTypes.arrayOf(PropTypes.object), -}; +export default class RecentRequestsPanel extends Component { + static propTypes = { + requests: PropTypes.arrayOf(PropTypes.object) + } -export default RecentRequestsPanel; + state = { + filter: '' + } + + handleSearch = (evt) => { + this.setState({filter: evt.target.value}); + } + + render() { + return ( +
    +
    +
    +

    Recent Requests

    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + ); + } +} diff --git a/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx b/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx index c1462f01a..252cc3354 100644 --- a/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx +++ b/BaragonUI/app/components/serviceDetail/ServiceDetail.jsx @@ -56,7 +56,13 @@ const ServiceDetail = ({service, requestHistory, editable, ServiceDetail.propTypes = { service: PropTypes.object, - requestHistory: PropTypes.array, + requestHistory: PropTypes.arrayOf(PropTypes.shape({ + agentResponses: PropTypes.object, + loadBalancerRequestId: PropTypes.string, + loadBalancerState: PropTypes.string, + message: PropTypes.string, + request: PropTypes.object, + })), editable: PropTypes.bool, afterRemoveUpstreams: PropTypes.func.isRequired, afterRemoveUpstream: PropTypes.func.isRequired, diff --git a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx index 1b76ca436..d09ad2509 100644 --- a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx @@ -47,7 +47,12 @@ const UpstreamsPanel = ({upstreams, loadBalancerService, afterRemoveUpstream, ed }; UpstreamsPanel.propTypes = { - upstreams: PropTypes.array, + upstreams: PropTypes.arrayOf(PropTypes.shape({ + group: PropTypes.string, + rackId: PropTypes.string, + requestId: PropTypes.string.isRequired, + upstream: PropTypes.string.isRequired, + })), loadBalancerService: PropTypes.object, afterRemoveUpstream: PropTypes.func, editable: PropTypes.bool, diff --git a/BaragonUI/app/components/serviceDetail/util.jsx b/BaragonUI/app/components/serviceDetail/util.jsx index 431b5b53e..fcdabf670 100644 --- a/BaragonUI/app/components/serviceDetail/util.jsx +++ b/BaragonUI/app/components/serviceDetail/util.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import fuzzy from 'fuzzy'; // [T], PosInt -> [[T]] // [1, 2, 3, 4, 5, 6, 7], 2 -> [[1, 2], [3, 4], [5, 6], [7]] @@ -42,3 +43,11 @@ export const iconByState = (state) => { return 'glyphicon glyphicon-question-sign'; } }; + + +export const matches = (filter, elements) => { + return fuzzy.filter(filter, elements, { + extract: (element) => element.loadBalancerRequestId, + returnMatchInfo: true + }).map((match) => match.original); +}; From 2d2625431e90acf783aa9f46285c6fe786b745a3 Mon Sep 17 00:00:00 2001 From: Peter Teixeira Date: Wed, 15 Feb 2017 09:02:25 -0500 Subject: [PATCH 4/5] Fix issues that were raised in code review Involved two major changes: migrating the single-page utils code into the package utils code, and changing how the request submission was handled. In particular, w/r/t the latter, it had been the case that there as an action creator in the `api/services.es6` file that was responsible for sending remove upstreams requests. That has now been removed, in favor of delegating to the more general action creator in `api/requests.es6` that can handle any kind of request, and just creating the request body at the point where it is triggered. --- BaragonUI/app/actions/api/requests.es6 | 8 +-- BaragonUI/app/actions/api/services.es6 | 19 +----- .../modalButtons/RemoveUpstreamModal.jsx | 16 ++++- .../modalButtons/RemoveUpstreamsModal.jsx | 16 ++++- .../serviceDetail/ButtonContainer.jsx | 64 +++++++++++++++++++ .../components/serviceDetail/DetailHeader.jsx | 61 +----------------- .../serviceDetail/LoadBalancersPanel.jsx | 4 +- .../components/serviceDetail/OwnersPanel.jsx | 5 +- .../serviceDetail/RecentRequestsPanel.jsx | 21 ++++-- .../serviceDetail/UpstreamsPanel.jsx | 5 +- .../app/components/serviceDetail/util.jsx | 53 --------------- BaragonUI/app/reducers/api/index.es6 | 7 +- BaragonUI/app/{utils.es6 => utils.jsx} | 56 +++++++++++++++- 13 files changed, 175 insertions(+), 160 deletions(-) create mode 100644 BaragonUI/app/components/serviceDetail/ButtonContainer.jsx delete mode 100644 BaragonUI/app/components/serviceDetail/util.jsx rename BaragonUI/app/{utils.es6 => utils.jsx} (68%) diff --git a/BaragonUI/app/actions/api/requests.es6 b/BaragonUI/app/actions/api/requests.es6 index 72bf0b528..c8fea83ae 100644 --- a/BaragonUI/app/actions/api/requests.es6 +++ b/BaragonUI/app/actions/api/requests.es6 @@ -17,9 +17,9 @@ export const FetchRequestHistory = buildApiAction( export const SubmitRequest = buildJsonApiAction( 'SUBMIT_REQUEST', 'POST', - (request) => ({ - url: `/request`, - request + (body) => ({ + url: '/request', + body }), (request) => request.loadBalancerRequestId ); @@ -31,4 +31,4 @@ export const FetchRequestResponse = buildApiAction( renderNotFoundIf404 }), (requestId) => requestId -); \ No newline at end of file +); diff --git a/BaragonUI/app/actions/api/services.es6 b/BaragonUI/app/actions/api/services.es6 index 354c4fbba..226c0e480 100644 --- a/BaragonUI/app/actions/api/services.es6 +++ b/BaragonUI/app/actions/api/services.es6 @@ -1,6 +1,6 @@ import { buildApiAction, buildJsonApiAction } from './base'; -const buildRequestId = (serviceId) => `${serviceId}-${Date.now()}`; + export const FetchBaragonServices = buildApiAction( 'FETCH_BARAGON_SERVICES', @@ -31,20 +31,3 @@ export const ReloadService = buildJsonApiAction( url: `/state/${serviceId}/reload`, }) ); - -export const RemoveUpstreams = buildJsonApiAction( - 'REMOVE_UPSTREAMS', - 'POST', - (loadBalancerService, upstreams, noValidate, noReload) => ({ - url: '/request', - body: { - loadBalancerService, - noValidate, - noReload, - loadBalancerRequestId: buildRequestId(loadBalancerService.serviceId), - addUpstreams: [], - removeUpstreams: upstreams, - }, - }), - (serviceId) => serviceId, -); diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx index 84c57f800..540063c3a 100644 --- a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamModal.jsx @@ -1,7 +1,8 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { RemoveUpstreams } from '../../../actions/api/services'; +import { SubmitRequest } from '../../../actions/api/requests'; +import Utils from '../../../utils'; import FormModal from '../modal/FormModal'; @@ -48,9 +49,18 @@ class RemoveUpstreamModal extends Component { } } +const buildRequest = (ownProps, data) => ({ + loadBalancerService: ownProps.loadBalancerService, + noValidate: data.noValidate, + noReload: data.noReload, + loadBalancerRequestId: Utils.buildRequestId(ownProps.loadBalancerService.serviceId), + addUpstreams: [], + removeUpstreams: [ownProps.upstream] +}); + const mapDispatchToProps = (dispatch, ownProps) => ({ - removeUpstream: (data) => dispatch(RemoveUpstreams - .trigger(ownProps.loadBalancerService, [ownProps.upstream], data.noValidate, data.noReload)) + removeUpstream: (data) => dispatch(SubmitRequest + .trigger(buildRequest(ownProps, data))) .then(response => (ownProps.then && ownProps.then(response))) }); diff --git a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx index 350376e82..a512f0481 100644 --- a/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx +++ b/BaragonUI/app/components/common/modalButtons/RemoveUpstreamsModal.jsx @@ -1,7 +1,8 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; -import { RemoveUpstreams } from '../../../actions/api/services'; +import { SubmitRequest } from '../../../actions/api/requests'; +import Utils from '../../../utils'; import FormModal from '../modal/FormModal'; @@ -49,9 +50,18 @@ class RemoveUpstreamsModal extends Component { } } +const buildRequest = (ownProps, data) => ({ + loadBalancerService: ownProps.loadBalancerService, + noValidate: data.noValidate, + noReload: data.noReload, + loadBalancerRequestId: Utils.buildRequestId(ownProps.loadBalancerService.serviceId), + addUpstreams: [], + removeUpstreams: ownProps.upstreams, +}); + const mapDispatchToProps = (dispatch, ownProps) => ({ - removeUpstreams: (data) => dispatch(RemoveUpstreams - .trigger(ownProps.loadBalancerService, ownProps.upstreams, data.noValidate, data.noReload)) + removeUpstreams: (data) => dispatch(SubmitRequest + .trigger(buildRequest(ownProps, data))) .then(response => (ownProps.then && ownProps.then(response))) }); diff --git a/BaragonUI/app/components/serviceDetail/ButtonContainer.jsx b/BaragonUI/app/components/serviceDetail/ButtonContainer.jsx new file mode 100644 index 000000000..ef605f35a --- /dev/null +++ b/BaragonUI/app/components/serviceDetail/ButtonContainer.jsx @@ -0,0 +1,64 @@ +import React, { PropTypes } from 'react'; + +import JSONButton from '../common/JSONButton'; +import ReloadServiceButton from '../common/modalButtons/ReloadServiceButton'; +import RemoveUpstreamsButton from '../common/modalButtons/RemoveUpstreamsButton'; +import DeleteServiceButton from '../common/modalButtons/DeleteServiceButton'; + +const showJSONButton = (serviceJson) => { + return ( + + JSON + + ); +}; + +const ButtonContainer = ({editable, serviceJson, upstreams, + afterRemoveUpstreams, afterReload, afterDelete}) => { + if (!editable) { + return ( +
    + {showJSONButton(serviceJson)} +
    + ); + } + + return ( +
    + {showJSONButton(serviceJson)} + + Reload Configs + + + + Delete + +
    + ); +}; + +ButtonContainer.propTypes = { + editable: PropTypes.bool.isRequired, + serviceJson: PropTypes.object.isRequired, + upstreams: PropTypes.arrayOf(PropTypes.shape({ + group: PropTypes.string, + rackId: PropTypes.string, + requestId: PropTypes.string.isRequired, + upstream: PropTypes.string.isRequired, + })).isRequired, + afterRemoveUpstreams: PropTypes.func, + afterReload: PropTypes.func, + afterDelete: PropTypes.func, +}; + +export default ButtonContainer; diff --git a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx index 8a94ddfc4..f51af7ed9 100644 --- a/BaragonUI/app/components/serviceDetail/DetailHeader.jsx +++ b/BaragonUI/app/components/serviceDetail/DetailHeader.jsx @@ -1,65 +1,6 @@ import React, { PropTypes } from 'react'; -import JSONButton from '../common/JSONButton'; -import RemoveUpstreamsButton from '../common/modalButtons/RemoveUpstreamsButton'; -import ReloadServiceButton from '../common/modalButtons/ReloadServiceButton'; -import DeleteServiceButton from '../common/modalButtons/DeleteServiceButton'; - -const showJSONButton = (serviceJson) => { - return ( - - JSON - - ); -}; - -const ButtonContainer = ({editable, serviceJson, upstreams, - afterRemoveUpstreams, afterReload, afterDelete}) => { - if (!editable) { - return ( -
    - {showJSONButton(serviceJson)} -
    - ); - } - - return ( -
    - {showJSONButton(serviceJson)} - - Reload Configs - - - - Delete - -
    - ); -}; - -ButtonContainer.propTypes = { - editable: PropTypes.bool.isRequired, - serviceJson: PropTypes.object.isRequired, - upstreams: PropTypes.arrayOf(PropTypes.shape({ - group: PropTypes.string, - rackId: PropTypes.string, - requestId: PropTypes.string.isRequired, - upstream: PropTypes.string.isRequired, - })).isRequired, - afterRemoveUpstreams: PropTypes.func, - afterReload: PropTypes.func, - afterDelete: PropTypes.func, -}; +import ButtonContainer from './ButtonContainer'; const DetailHeader = ({id, basePath, editable, serviceJson, upstreams, afterRemoveUpstreams, afterReload, afterDelete}) => { diff --git a/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx b/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx index d44326e65..663d57229 100644 --- a/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/LoadBalancersPanel.jsx @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; import { Link } from 'react-router'; -import { asGroups } from './util'; +import Utils from '../../utils'; const renderLoadBalancerGroup = (loadBalancerGroup) => { return ( @@ -19,7 +19,7 @@ const LoadBalancersPanel = ({loadBalancerGroups}) => {

    Load Balancer Groups

    - { asGroups(loadBalancerGroups, 2, renderLoadBalancerGroup) } + { Utils.asGroups(loadBalancerGroups, 2, renderLoadBalancerGroup) }
    diff --git a/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx b/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx index d0e138d4b..28c6de94d 100644 --- a/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/OwnersPanel.jsx @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; -import { asGroups } from './util'; +import Utils from '../../utils'; + const renderOwner = (owner, index) => { return ( @@ -18,7 +19,7 @@ const OwnersPanel = ({owners}) => {

    Owners

    - { asGroups(owners, 2, renderOwner) } + { Utils.asGroups(owners, 2, renderOwner) }
    diff --git a/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx b/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx index 1966f1993..f6ebf377d 100644 --- a/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/RecentRequestsPanel.jsx @@ -1,11 +1,11 @@ import React, { Component, PropTypes } from 'react'; import { Link } from 'react-router'; +import fuzzy from 'fuzzy'; import UITable from '../common/table/UITable'; import Column from '../common/table/Column'; import JSONButton from '../common/JSONButton'; - -import { iconByState, matches } from './util'; +import Utils from '../../utils'; const requestJSONButton = (request) => { if (request) { @@ -20,7 +20,7 @@ const requestJSONButton = (request) => { }; const iconForRequest = (request) => { - return ; + return ; }; const requestLink = ({loadBalancerRequestId}) => { @@ -40,11 +40,22 @@ const HistoryTable = ({history, filter}) => { ); } - const contents = matches(filter, history); + let tableContent; + if (filter.trim().length === 0) { + tableContent = history; + } else { + const fuzzyObjects = fuzzy.filter(filter, history, { + extract: (request) => request.loadBalancerRequestId, + returnMatchInfo: true + }); + tableContent = Utils.fuzzyFilter(filter, fuzzyObjects, (request) => { + return request.loadBalancerRequestId; + }); + } return ( request.loadBalancerRequestId} paginated={true} rowChunkSize={5} diff --git a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx index d09ad2509..ff12d396f 100644 --- a/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx +++ b/BaragonUI/app/components/serviceDetail/UpstreamsPanel.jsx @@ -1,8 +1,7 @@ import React, { PropTypes } from 'react'; -import { asGroups } from './util'; - import RemoveUpstreamButton from '../common/modalButtons/RemoveUpstreamButton'; +import Utils from '../../utils'; const renderUpstream = (editable, loadBalancerService, afterRemoveUpstream) => (upstream) => { const {upstream: upstreamName, rackId, group} = upstream; @@ -34,7 +33,7 @@ const UpstreamsPanel = ({upstreams, loadBalancerService, afterRemoveUpstream, ed
    { - asGroups( + Utils.asGroups( upstreams, 2, renderUpstream(editable, loadBalancerService, afterRemoveUpstream) diff --git a/BaragonUI/app/components/serviceDetail/util.jsx b/BaragonUI/app/components/serviceDetail/util.jsx deleted file mode 100644 index fcdabf670..000000000 --- a/BaragonUI/app/components/serviceDetail/util.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import fuzzy from 'fuzzy'; - -// [T], PosInt -> [[T]] -// [1, 2, 3, 4, 5, 6, 7], 2 -> [[1, 2], [3, 4], [5, 6], [7]] -const chunk = (arr, size) => { - return _.chain(arr) - .groupBy((elem, index) => Math.floor(index / size)) - .toArray() - .value(); -}; - -// ((T, PosInt) -> DOMElement) -> ([T], PosInt) -> DOMElement -const rowRenderer = (itemRenderer, columns) => (row, index) => { - return ( -
    -
      - { row.map(itemRenderer) } -
    -
    - ); -}; - -// [T], ((T, PosInt) -> DOMElement) -> DOMElement -export const asGroups = (arr, columns, itemRenderer) => { - return chunk(arr, arr.length / columns) - .map(rowRenderer(itemRenderer, columns)); -}; - -export const iconByState = (state) => { - switch (state) { - case 'SUCCESS': - return 'glyphicon glyphicon-ok-circle active'; - case 'FAILED': - return 'glyphicon glyphicon-ban-circle inactive'; - case 'WAITING': - return 'glyphicon glyphicon-time'; - case 'CANCELING': - return 'glyphicon glyphicon-remove-circle inactive'; - case 'INVALID_REQUEST_NOOP': - return 'glyphicon glyphicon-exclamation-sign inactive'; - default: - return 'glyphicon glyphicon-question-sign'; - } -}; - - -export const matches = (filter, elements) => { - return fuzzy.filter(filter, elements, { - extract: (element) => element.loadBalancerRequestId, - returnMatchInfo: true - }).map((match) => match.original); -}; diff --git a/BaragonUI/app/reducers/api/index.es6 b/BaragonUI/app/reducers/api/index.es6 index 4c1ea7637..33cbd141d 100644 --- a/BaragonUI/app/reducers/api/index.es6 +++ b/BaragonUI/app/reducers/api/index.es6 @@ -1,4 +1,3 @@ -import _ from 'underscore'; import { combineReducers } from 'redux'; import buildApiActionReducer from './base'; import buildKeyedApiActionReducer from './keyed'; @@ -25,17 +24,16 @@ import { FetchGroupTargetCount, FetchGroupAgents, FetchGroupKnownAgents -} from '../../actions/api/groups' +} from '../../actions/api/groups'; import { FetchBaragonServices, FetchService, DeleteService, - ReloadService + ReloadService, } from '../../actions/api/services'; - const status = buildApiActionReducer(FetchBaragonStatus); const workers = buildApiActionReducer(FetchBaragonServiceWorkers, []); const queuedRequests = buildApiActionReducer(FetchQueuedRequests, []); @@ -58,6 +56,7 @@ export default combineReducers({ workers, queuedRequests, groups, + group, basePaths, targetCount, agents, diff --git a/BaragonUI/app/utils.es6 b/BaragonUI/app/utils.jsx similarity index 68% rename from BaragonUI/app/utils.es6 rename to BaragonUI/app/utils.jsx index 3886d7260..42bb9ef65 100644 --- a/BaragonUI/app/utils.es6 +++ b/BaragonUI/app/utils.jsx @@ -1,5 +1,26 @@ +import React from 'react'; import moment from 'moment'; +// [T], PosInt -> [[T]] +// [1, 2, 3, 4, 5, 6, 7], 2 -> [[1, 2], [3, 4], [5, 6], [7]] +const chunk = (arr, size) => { + return _.chain(arr) + .groupBy((elem, index) => Math.floor(index / size)) + .toArray() + .value(); +}; + +// ((T, PosInt) -> DOMElement) -> ([T], PosInt) -> DOMElement +const rowRenderer = (itemRenderer, columns) => (row, index) => { + return ( +
    +
      + { row.map(itemRenderer) } +
    +
    + ); +}; + const Utils = { isIn(needle, haystack) { return !_.isEmpty(haystack) && haystack.indexOf(needle) >= 0; @@ -67,12 +88,12 @@ const Utils = { return false; }, - fuzzyFilter(filter, fuzzyObjects) { + fuzzyFilter(filter, fuzzyObjects, primaryField = (it) => it.id) { const maxScore = _.max(fuzzyObjects, (fuzzyObject) => fuzzyObject.score).score; _.chain(fuzzyObjects).map((fuzzyObject) => { - if (fuzzyObject.original.id.toLowerCase().startsWith(filter.toLowerCase())) { + if (primaryField(fuzzyObject.original).toLowerCase().startsWith(filter.toLowerCase())) { fuzzyObject.score = fuzzyObject.score * 10; - } else if (fuzzyObject.original.id.toLowerCase().indexOf(filter.toLowerCase()) > -1) { + } else if (primaryField(fuzzyObject.original).toLowerCase().indexOf(filter.toLowerCase()) > -1) { fuzzyObject.score = fuzzyObject.score * 5; } return fuzzyObject; @@ -144,6 +165,35 @@ const Utils = { } } return array.join('&'); + }, + + buildRequestId(serviceId) { + return `${serviceId}-${Date.now()}`; + }, + + // Render the elements in an array as `columns` number of columns + // of list-groups, using `itemRenderer` to turn each item into a + // list-group-item. + asGroups(arr, columns, itemRenderer) { + return chunk(arr, arr.length / columns) + .map(rowRenderer(itemRenderer, columns)); + }, + + iconByState(state) { + switch (state) { + case 'SUCCESS': + return 'glyphicon glyphicon-ok-circle active'; + case 'FAILED': + return 'glyphicon glyphicon-ban-circle inactive'; + case 'WAITING': + return 'glyphicon glyphicon-time'; + case 'CANCELING': + return 'glyphicon glyphicon-remove-circle inactive'; + case 'INVALID_REQUEST_NOOP': + return 'glyphicon glyphicon-exclamation-sign inactive'; + default: + return 'glyphicon glyphicon-question-sign'; + } } }; From dd99f39e28203bc9d20246a7862ac4eeb5b8fb79 Mon Sep 17 00:00:00 2001 From: Peter Teixeira Date: Fri, 17 Feb 2017 09:37:54 -0500 Subject: [PATCH 5/5] Remove extra newline --- BaragonUI/app/actions/api/services.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/BaragonUI/app/actions/api/services.es6 b/BaragonUI/app/actions/api/services.es6 index 226c0e480..88d31877b 100644 --- a/BaragonUI/app/actions/api/services.es6 +++ b/BaragonUI/app/actions/api/services.es6 @@ -1,7 +1,5 @@ import { buildApiAction, buildJsonApiAction } from './base'; - - export const FetchBaragonServices = buildApiAction( 'FETCH_BARAGON_SERVICES', {url: '/state'}