Add filters for graphs table
Custom graphs table extended with filters by graph type and level. Implements blueprint ui-custom-graph Change-Id: Id7684711bc97c7c759953e4d1368642765919d0f
This commit is contained in:
parent
fd61dfdcfa
commit
84a8aad1f2
|
@ -90,4 +90,3 @@ export const DEPLOYMENT_GRAPH_LEVELS = [
|
|||
'plugin',
|
||||
'cluster'
|
||||
];
|
||||
|
||||
|
|
|
@ -5491,6 +5491,21 @@ input[type=range] {
|
|||
}
|
||||
}
|
||||
}
|
||||
.alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.deployment-graphs-toolbar {
|
||||
.buttons {
|
||||
margin-bottom: 15px;
|
||||
.btn-filters {
|
||||
padding: 7px 10px 8px;
|
||||
}
|
||||
}
|
||||
.filters well, .active-sorters-filters {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upload-graph-form {
|
||||
|
|
|
@ -20,6 +20,7 @@ import ReactTestUtils from 'react-addons-test-utils';
|
|||
import {MultiSelectControl} from 'views/controls';
|
||||
|
||||
var renderControl;
|
||||
const OPTIONS_NUMBER = 10;
|
||||
|
||||
suite('Multiselect Control', () => {
|
||||
setup(() => {
|
||||
|
@ -28,7 +29,7 @@ suite('Multiselect Control', () => {
|
|||
{...props}
|
||||
name='multiselect'
|
||||
label='Label For Multiselect'
|
||||
options={_.times(5, (n) => ({name: 'option' + n, title: 'option' + n}))}
|
||||
options={_.times(OPTIONS_NUMBER, (n) => ({name: 'option' + n, title: 'option' + n}))}
|
||||
onChange={sinon.spy()}
|
||||
toggle={sinon.spy()}
|
||||
/>
|
||||
|
@ -83,7 +84,8 @@ suite('Multiselect Control', () => {
|
|||
control.refs.all.refs.input.checked = true;
|
||||
ReactTestUtils.Simulate.change(control.refs.all.refs.input);
|
||||
assert.deepEqual(
|
||||
control.props.onChange.args[2][0], _.times(5, (n) => 'option' + n), 'all values are chosen'
|
||||
control.props.onChange.args[2][0],
|
||||
_.times(OPTIONS_NUMBER, (n) => 'option' + n), 'all values are chosen'
|
||||
);
|
||||
control.refs.all.refs.input.checked = false;
|
||||
ReactTestUtils.Simulate.change(control.refs.all.refs.input);
|
||||
|
@ -96,7 +98,7 @@ suite('Multiselect Control', () => {
|
|||
var control = renderControl({isOpen: true, addOptionsFilter: true});
|
||||
assert.equal(
|
||||
ReactTestUtils.scryRenderedDOMComponentsWithClass(control, 'checkbox-group').length,
|
||||
6,
|
||||
OPTIONS_NUMBER + 1, // additional checkbox is options filter
|
||||
'All options presented by default'
|
||||
);
|
||||
var optionsFilter = control.refs.optionsFilter.refs.input;
|
||||
|
|
|
@ -345,7 +345,12 @@
|
|||
"graph_type": "Type \"__graphType__\"",
|
||||
"download_graph": "Download",
|
||||
"delete_graph": "Delete",
|
||||
"upload_graph": "Upload New Workflow"
|
||||
"upload_graph": "Upload New Workflow",
|
||||
"filter_by": "Filter By",
|
||||
"filter_tooltip": "Filter Workflows",
|
||||
"filter_by_graph_type": "Type",
|
||||
"filter_by_graph_level": "Level",
|
||||
"no_graphs_matched_filters": "No workflows matched applied filters."
|
||||
},
|
||||
"deployment_history": {
|
||||
"timeline_mode_tooltip": "Timeline View",
|
||||
|
|
|
@ -16,13 +16,16 @@
|
|||
import _ from 'underscore';
|
||||
import i18n from 'i18n';
|
||||
import React from 'react';
|
||||
import {DEPLOYMENT_GRAPH_LEVELS} from 'consts';
|
||||
import utils from 'utils';
|
||||
import {Tooltip, MultiSelectControl} from 'views/controls';
|
||||
import models from 'models';
|
||||
import {backboneMixin} from 'component_mixins';
|
||||
import {UploadGraphDialog, DeleteGraphDialog} from 'views/dialogs';
|
||||
|
||||
var WorkflowsTab;
|
||||
var ns = 'cluster_page.workflows_tab.';
|
||||
|
||||
WorkflowsTab = React.createClass({
|
||||
var WorkflowsTab = React.createClass({
|
||||
mixins: [
|
||||
backboneMixin({
|
||||
modelOrCollection: (props) => props.cluster.get('deploymentGraphs'),
|
||||
|
@ -48,6 +51,68 @@ WorkflowsTab = React.createClass({
|
|||
.then(() => ({plugins}));
|
||||
}
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
filters: [
|
||||
{
|
||||
name: 'graph_type',
|
||||
label: i18n(ns + 'filter_by_graph_type'),
|
||||
values: [],
|
||||
options: () => _.uniq(this.props.cluster.get('deploymentGraphs').invokeMap('getType')),
|
||||
addOptionsFilter: true
|
||||
}, {
|
||||
name: 'graph_level',
|
||||
label: i18n(ns + 'filter_by_graph_level'),
|
||||
values: [],
|
||||
options: () => DEPLOYMENT_GRAPH_LEVELS
|
||||
}
|
||||
],
|
||||
areFiltersVisible: false,
|
||||
openFilter: null
|
||||
};
|
||||
},
|
||||
toggleFilters() {
|
||||
this.setState({
|
||||
areFiltersVisible: !this.state.areFiltersVisible,
|
||||
openFilter: null
|
||||
});
|
||||
},
|
||||
toggleFilter(name, visible) {
|
||||
var {openFilter} = this.state;
|
||||
var isFilterOpen = openFilter === name;
|
||||
visible = _.isBoolean(visible) ? visible : !isFilterOpen;
|
||||
this.setState({
|
||||
openFilter: visible ? name : isFilterOpen ? null : openFilter
|
||||
});
|
||||
},
|
||||
changeFilter(name, values) {
|
||||
var {filters} = this.state;
|
||||
_.find(filters, {name}).values = values;
|
||||
this.setState({filters});
|
||||
},
|
||||
resetFilters() {
|
||||
var {filters} = this.state;
|
||||
_.each(filters, (filter) => {
|
||||
filter.values = [];
|
||||
});
|
||||
this.setState({filters});
|
||||
},
|
||||
normalizeAppliedFilters() {
|
||||
var deploymentGraphs = this.props.cluster.get('deploymentGraphs');
|
||||
var filterValueChecks = {
|
||||
graph_type: (type) => deploymentGraphs.some((graph) => graph.getType() === type)
|
||||
};
|
||||
_.each(this.state.filters, ({name, values}) => {
|
||||
if (values.length && filterValueChecks[name]) {
|
||||
var normalizedValues = _.filter(values, filterValueChecks[name]);
|
||||
if (!_.isEqual(values, normalizedValues)) this.changeFilter(name, normalizedValues);
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteGraph(graph) {
|
||||
DeleteGraphDialog.show({graph})
|
||||
.then(this.normalizeAppliedFilters);
|
||||
},
|
||||
downloadMergedGraph() {},
|
||||
downloadSingleGraph() {},
|
||||
uploadGraph() {
|
||||
|
@ -56,90 +121,173 @@ WorkflowsTab = React.createClass({
|
|||
.then(() => cluster.get('deploymentGraphs').fetch());
|
||||
},
|
||||
render() {
|
||||
var {areFiltersVisible, openFilter, filters} = this.state;
|
||||
var {cluster, plugins} = this.props;
|
||||
var ns = 'cluster_page.workflows_tab.';
|
||||
var graphTypes = _.uniq(cluster.get('deploymentGraphs').invokeMap('getType'));
|
||||
|
||||
var areFiltersApplied = _.some(filters, ({values}) => values.length);
|
||||
var chosenGraphLevels = _.find(filters, {name: 'graph_level'}).values;
|
||||
var chosenGraphTypes = _.find(filters, {name: 'graph_type'}).values;
|
||||
var graphs = cluster.get('deploymentGraphs').filter(
|
||||
(graph) => (!chosenGraphTypes.length || _.includes(chosenGraphTypes, graph.getType())) &&
|
||||
(!chosenGraphLevels.length || _.includes(chosenGraphLevels, graph.getLevel()))
|
||||
);
|
||||
var graphTypes = _.uniq(_.invokeMap(graphs, 'getType'));
|
||||
|
||||
return (
|
||||
<div className='row deployment-graphs'>
|
||||
<div className='title col-xs-6'>
|
||||
{i18n(ns + 'title')}
|
||||
</div>
|
||||
<div className='title col-xs-6'>
|
||||
<button
|
||||
className='btn btn-success btn-upload-graph pull-right'
|
||||
onClick={this.uploadGraph}
|
||||
>
|
||||
<i className='glyphicon glyphicon-plus-white' />
|
||||
{i18n(ns + 'upload_graph')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='wrapper col-xs-12'>
|
||||
<table className='table table-hover workflows-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{i18n(ns + 'graph_name_header')}</th>
|
||||
<th>{i18n(ns + 'graph_level_header')}</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.map(graphTypes, (graphType) => {
|
||||
var graphs = cluster.get('deploymentGraphs').filter(
|
||||
(graph) => graph.getType() === graphType
|
||||
);
|
||||
return [
|
||||
<tr key='subheader' className='subheader'>
|
||||
<td colSpan='3'>
|
||||
{i18n(ns + 'graph_type', {graphType})}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className='btn btn-link btn-download-merged-graph'
|
||||
onClick={() => this.downloadMergedGraph(graphType)}
|
||||
>
|
||||
{i18n(ns + 'download_graph')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
].concat(
|
||||
_.map(graphs, (graph) => {
|
||||
var level = graph.getLevel();
|
||||
return <tr key={graph.id}>
|
||||
<td>{graph.get('name') || '-'}</td>
|
||||
<td className='level'>
|
||||
{level}
|
||||
|
||||
{level === 'plugin' &&
|
||||
<span>
|
||||
({plugins.get(graph.get('relations')[0].model_id).get('title')})
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{level === 'cluster' &&
|
||||
<button
|
||||
className='btn btn-link btn-remove-graph'
|
||||
onClick={() => DeleteGraphDialog.show({graph})}
|
||||
>
|
||||
{i18n(ns + 'delete_graph')}
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div className='deployment-graphs'>
|
||||
<div className='row'>
|
||||
<div className='title col-xs-12'>
|
||||
{i18n(ns + 'title')}
|
||||
</div>
|
||||
<div className='wrapper col-xs-12'>
|
||||
<div className='deployment-graphs-toolbar'>
|
||||
<div className='buttons'>
|
||||
<Tooltip wrap text={i18n(ns + 'filter_tooltip')}>
|
||||
<button
|
||||
onClick={this.toggleFilters}
|
||||
className={utils.classNames({
|
||||
'btn btn-default pull-left btn-filters': true,
|
||||
active: areFiltersVisible
|
||||
})}
|
||||
>
|
||||
<i className='glyphicon glyphicon-filter' />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<div className='btn-group pull-right' data-toggle='buttons'>
|
||||
<button
|
||||
className='btn btn-success btn-upload-graph'
|
||||
onClick={this.uploadGraph}
|
||||
>
|
||||
<i className='glyphicon glyphicon-plus-white' />
|
||||
{i18n(ns + 'upload_graph')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{areFiltersVisible && (
|
||||
<div className='filters'>
|
||||
<div className='well clearfix'>
|
||||
<div className='well-heading'>
|
||||
<i className='glyphicon glyphicon-filter' /> {i18n(ns + 'filter_by')}
|
||||
{areFiltersApplied &&
|
||||
<button
|
||||
className='btn btn-link btn-download-graph'
|
||||
onClick={() => this.downloadSingleGraph(graph)}
|
||||
className='btn btn-link pull-right btn-reset-filters'
|
||||
onClick={this.resetFilters}
|
||||
>
|
||||
{i18n(ns + 'download_graph')}
|
||||
<i className='glyphicon discard-changes-icon' />
|
||||
{i18n('common.reset_button')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>;
|
||||
})
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
</div>
|
||||
{_.map(filters,
|
||||
(filter) => <MultiSelectControl
|
||||
{...filter}
|
||||
key={filter.name}
|
||||
className={utils.classNames('filter-control', ['filter-by-' + filter.name])}
|
||||
onChange={_.partial(this.changeFilter, filter.name)}
|
||||
isOpen={openFilter === filter.name}
|
||||
toggle={_.partial(this.toggleFilter, filter.name)}
|
||||
options={
|
||||
_.map(filter.options(), (value) => ({name: value, title: value}))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!areFiltersVisible && areFiltersApplied &&
|
||||
<div className='active-sorters-filters'>
|
||||
<div className='active-filters row' onClick={this.toggleFilters}>
|
||||
<strong className='col-xs-1'>{i18n(ns + 'filter_by')}</strong>
|
||||
<div className='col-xs-11'>
|
||||
{_.map(filters, ({name, label, values}) => {
|
||||
if (!values.length) return null;
|
||||
return <div key={name}>
|
||||
<strong>{label + ':'}</strong> <span>{values.join(', ')}</span>
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
className='btn btn-link btn-reset-filters'
|
||||
onClick={this.resetFilters}
|
||||
>
|
||||
<i className='glyphicon discard-changes-icon' />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-xs-12'>
|
||||
{graphs.length ?
|
||||
<table className='table table-hover workflows-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{i18n(ns + 'graph_name_header')}</th>
|
||||
<th>{i18n(ns + 'graph_level_header')}</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.map(graphTypes,
|
||||
(graphType) => [
|
||||
<tr key='subheader' className='subheader'>
|
||||
<td colSpan='3'>
|
||||
{i18n(ns + 'graph_type', {graphType})}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className='btn btn-link btn-download-merged-graph'
|
||||
onClick={() => this.downloadMergedGraph(graphType)}
|
||||
>
|
||||
{i18n(ns + 'download_graph')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
].concat(
|
||||
_.map(graphs, (graph) => {
|
||||
if (graph.getType() !== graphType) return null;
|
||||
|
||||
var level = graph.getLevel();
|
||||
return <tr key={graph.id}>
|
||||
<td>{graph.get('name') || '-'}</td>
|
||||
<td className='level'>
|
||||
{level}
|
||||
|
||||
{level === 'plugin' &&
|
||||
<span>
|
||||
({plugins.get(graph.get('relations')[0].model_id).get('title')})
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{level === 'cluster' &&
|
||||
<button
|
||||
className='btn btn-link btn-remove-graph'
|
||||
onClick={() => this.deleteGraph(graph)}
|
||||
>
|
||||
{i18n(ns + 'delete_graph')}
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
className='btn btn-link btn-download-graph'
|
||||
onClick={() => this.downloadSingleGraph(graph)}
|
||||
>
|
||||
{i18n(ns + 'download_graph')}
|
||||
</button>
|
||||
</td>
|
||||
</tr>;
|
||||
})
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
:
|
||||
<div className='alert alert-warning'>{i18n(ns + 'no_graphs_matched_filters')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -492,7 +492,8 @@ export var MultiSelectControl = React.createClass({
|
|||
return {
|
||||
values: [],
|
||||
isOpen: false,
|
||||
addOptionsFilter: false
|
||||
addOptionsFilter: false,
|
||||
optionsNumberToShowFilter: 10
|
||||
};
|
||||
},
|
||||
onChange(name, checked, isLabel = false) {
|
||||
|
@ -537,7 +538,8 @@ export var MultiSelectControl = React.createClass({
|
|||
},
|
||||
render() {
|
||||
var {
|
||||
values, dynamicValues, isOpen, className, toggle, extraContent, addOptionsFilter
|
||||
values, dynamicValues, isOpen, className, toggle, extraContent,
|
||||
addOptionsFilter, optionsNumberToShowFilter
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.options.length) return null;
|
||||
|
@ -582,7 +584,7 @@ export var MultiSelectControl = React.createClass({
|
|||
</button>
|
||||
{isOpen &&
|
||||
<Popover toggle={toggle}>
|
||||
{addOptionsFilter &&
|
||||
{addOptionsFilter && this.props.options.length >= optionsNumberToShowFilter &&
|
||||
<Input
|
||||
type='text'
|
||||
ref='optionsFilter'
|
||||
|
|
Loading…
Reference in New Issue