Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

“Add Widget” Dialog steps #3443

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/app/assets/less/ant.less
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
@import '~antd/lib/tag/style/index';
@import '~antd/lib/grid/style/index';
@import '~antd/lib/switch/style/index';
@import '~antd/lib/steps/style/index';
@import '~antd/lib/layout/style/index';
@import 'inc/ant-variables';

// Remove bold in labels for Ant checkboxes and radio buttons
Expand Down
190 changes: 135 additions & 55 deletions client/app/components/dashboards/AddWidgetDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { debounce, each, values, map, includes, first, identity } from 'lodash';
import { debounce, each, map, includes, identity } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'antd/lib/select';
import Modal from 'antd/lib/modal';
import Steps from 'antd/lib/steps';
import Button from 'antd/lib/button';
import Radio from 'antd/lib/radio';
import Layout from 'antd/lib/layout';
import Icon from 'antd/lib/icon';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { BigMessage } from '@/components/BigMessage';
import highlight from '@/lib/highlight';
Expand All @@ -18,16 +22,17 @@ import { toastr } from '@/services/ng';
import { Widget } from '@/services/widget';
import { Query } from '@/services/query';

const { Option, OptGroup } = Select;

class AddWidgetDialog extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
dialog: DialogPropType.isRequired,
};

steps = []

constructor(props) {
super(props);

this.state = {
saveInProgress: false,
selectedQuery: null,
Expand All @@ -38,6 +43,7 @@ class AddWidgetDialog extends React.Component {
selectedVis: null,
parameterMappings: [],
isLoaded: false,
currStepIdx: 0,
};

const searchQueries = debounce(this.searchQueries.bind(this), 200);
Expand All @@ -46,6 +52,19 @@ class AddWidgetDialog extends React.Component {
this.setState({ searchTerm });
searchQueries(searchTerm);
};

this.steps = [{
id: 'query',
title: 'Select Query',
allowNext: () => this.state.selectedQuery,
}, {
id: 'params',
title: 'Review Parameters',
active: () => this.state.parameterMappings.length > 0,
}, {
id: 'visualization',
title: 'Select Visualization',
}];
}

componentDidMount() {
Expand Down Expand Up @@ -76,9 +95,10 @@ class AddWidgetDialog extends React.Component {
this.props.dashboard.getParametersDefs(),
param => param.name,
);
const params = query.getParametersDefs();
this.setState({
selectedQuery: query,
parameterMappings: map(query.getParametersDefs(), param => ({
parameterMappings: map(params, param => ({
name: param.name,
type: includes(existingParamNames, param.name)
? MappingType.DashboardMapToExisting : MappingType.DashboardAddNew,
Expand Down Expand Up @@ -256,70 +276,130 @@ class AddWidgetDialog extends React.Component {
}

renderVisualizationInput() {
let visualizationGroups = {};
if (this.state.selectedQuery) {
each(this.state.selectedQuery.visualizations, (vis) => {
visualizationGroups[vis.type] = visualizationGroups[vis.type] || [];
visualizationGroups[vis.type].push(vis);
});
}
visualizationGroups = values(visualizationGroups);
const radioStyle = {
display: 'block',
height: '30px',
lineHeight: '30px',
};
const { selectedQuery } = this.state;
return (
<div>
<div className="form-group">
<label htmlFor="choose-visualization">Choose Visualization</label>
<Select
id="choose-visualization"
className="w-100"
defaultValue={first(this.state.selectedQuery.visualizations).id}
onChange={visualizationId => this.selectVisualization(this.state.selectedQuery, visualizationId)}
dropdownClassName="ant-dropdown-in-bootstrap-modal"
<Layout style={{ position: 'relative' }}>
<Button type="dashed" size="small" style={{ position: 'absolute', bottom: 0, left: 0, zIndex: 1 }}>
<Icon type="plus" /> New Visualization
</Button>
<Layout.Sider width={200} theme="light">
<Radio.Group
onChange={e => this.selectVisualization(selectedQuery, e.target.value)}
defaultValue={this.state.selectedVis.id}
>
{visualizationGroups.map(visualizations => (
<OptGroup label={visualizations[0].type} key={visualizations[0].type}>
{visualizations.map(visualization => (
<Option value={visualization.id} key={visualization.id}>{visualization.name}</Option>
))}
</OptGroup>
{selectedQuery.visualizations.map(({ id, name }) => (
<Radio style={radioStyle} value={id} key={id}>{name}</Radio>
))}
</Select>
</div>
</div>
</Radio.Group>
</Layout.Sider>
<Layout.Content style={{ display: 'flex', height: 331, alignItems: 'center', justifyContent: 'center' }}>
- Visualization {this.state.selectedVis.name} placeholder -
</Layout.Content>
</Layout>
);
}

renderStepContent(currStep) {
switch (currStep.id) {
case 'query':
return (
<React.Fragment>
{this.renderQueryInput()}
{this.renderSearchQueryResults()}
</React.Fragment>
);
case 'visualization':
return this.renderVisualizationInput();
case 'params':
return (
<ParameterMappingListInput
key="parameters-list"
id="parameter-mappings"
mappings={this.state.parameterMappings}
existingParams={this.props.dashboard.getParametersDefs()}
onChange={mappings => this.updateParamMappings(mappings)}
/>
);
// no default
}
}

renderPrevButton() {
const { currStepIdx } = this.state;
if (currStepIdx === 0) {
return null;
}

return (
<Button
key="prev"
onClick={() => this.setState({ currStepIdx: currStepIdx - 1 })}
>
Previous
</Button>
);
}

renderNextOrDoneButton(steps) {
const { currStepIdx } = this.state;

// next
if (currStepIdx < steps.length - 1) {
const { allowNext } = steps[currStepIdx];
return (
<Button
key="next"
type="primary"
onClick={() => this.setState({ currStepIdx: currStepIdx + 1 })}
disabled={allowNext ? !allowNext() : false}
>
Next
</Button>
);
}

// done
return (
<Button
key="done"
type="primary"
onClick={() => this.saveWidget()}
loading={this.state.saveInProgress}
>
Add to Dashboard
</Button>
);
}

render() {
const existingParams = this.props.dashboard.getParametersDefs();
const { dialog } = this.props;
const { currStepIdx } = this.state;

// filter by active()
const steps = this.steps.filter(step => (step.active ? step.active() : true));
const currStep = steps[currStepIdx];

return (
<Modal
{...dialog.props}
title="Add Widget"
onOk={() => this.saveWidget()}
okButtonProps={{
loading: this.state.saveInProgress,
disabled: !this.state.selectedQuery,
}}
okText="Add to Dashboard"
width={700}
footer={[this.renderPrevButton(), this.renderNextOrDoneButton(steps)]}
width={800}
>
{this.renderQueryInput()}
{!this.state.selectedQuery && this.renderSearchQueryResults()}
{this.state.selectedQuery && this.renderVisualizationInput()}

{
(this.state.parameterMappings.length > 0) && [
<label key="parameters-title" htmlFor="parameter-mappings">Parameters</label>,
<ParameterMappingListInput
key="parameters-list"
id="parameter-mappings"
mappings={this.state.parameterMappings}
existingParams={existingParams}
onChange={mappings => this.updateParamMappings(mappings)}
/>,
]
}
<Steps current={currStepIdx + (this.state.saveInProgress ? 1 : 0)} progressDot>
{steps.map(({ title }) => (
<Steps.Step key={title} title={title} />
))}
<Steps.Step key="done" title="Done" />
</Steps>
<div className="step-content" style={{ minHeight: 331, marginTop: 35, overlfowY: 'scroll' }}>
{this.renderStepContent(currStep)}
</div>
</Modal>
);
}
Expand Down