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

[explore] improve metric(s) and groupby(s) controls #2921

Merged
merged 4 commits into from
Jun 9, 2017
Merged
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
34 changes: 34 additions & 0 deletions superset/assets/javascripts/components/ColumnOption.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';

import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';

const propTypes = {
column: PropTypes.object.isRequired,
};

export default function ColumnOption({ column }) {
return (
<span>
<span className="m-r-5 option-label">
{column.verbose_name || column.column_name}
</span>
{column.description &&
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="info"
tooltip={column.description}
label={`descr-${column.column_name}`}
/>
}
{column.expression && column.expression !== column.column_name &&
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="question-circle-o"
tooltip={column.expression}
label={`expr-${column.column_name}`}
/>
}
</span>);
}
ColumnOption.propTypes = propTypes;
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ import { slugify } from '../modules/utils';
const propTypes = {
label: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
icon: PropTypes.string,
className: PropTypes.string,
};
const defaultProps = {
icon: 'question-circle-o',
};

export default function InfoTooltipWithTrigger({ label, tooltip }) {
export default function InfoTooltipWithTrigger({ label, tooltip, icon, className }) {
return (
<OverlayTrigger
placement="right"
overlay={<Tooltip id={`${slugify(label)}-tooltip`}>{tooltip}</Tooltip>}
>
<i className="fa fa-question-circle-o" />
<i className={`fa fa-${icon} ${className}`} />
</OverlayTrigger>
);
}

InfoTooltipWithTrigger.propTypes = propTypes;
InfoTooltipWithTrigger.defaultProps = defaultProps;
32 changes: 32 additions & 0 deletions superset/assets/javascripts/components/MetricOption.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';

import InfoTooltipWithTrigger from './InfoTooltipWithTrigger';

const propTypes = {
metric: PropTypes.object.isRequired,
};

export default function MetricOption({ metric }) {
return (
<div>
<span className="m-r-5 option-label">
{metric.verbose_name || metric.metric_name}
</span>
{metric.description &&
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="info"
tooltip={metric.description}
label={`descr-${metric.metric_name}`}
/>
}
<InfoTooltipWithTrigger
className="m-r-5 text-muted"
icon="question-circle-o"
tooltip={metric.expression}
label={`expr-${metric.metric_name}`}
/>
</div>);
}
MetricOption.propTypes = propTypes;
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,15 @@ class ChartContainer extends React.PureComponent {
getMockedSliceObject() {
const props = this.props;
const getHeight = () => {
const headerHeight = this.props.standalone ? 0 : 100;
const headerHeight = props.standalone ? 0 : 100;
return parseInt(props.height, 10) - headerHeight;
};
return {
viewSqlQuery: this.props.queryResponse.query,
viewSqlQuery: props.queryResponse.query,
containerId: props.containerId,
datasource: props.datasource,
selector: this.state.selector,
formData: this.props.formData,
formData: props.formData,
container: {
html: (data) => {
// this should be a callback to clear the contents of the slice container
Expand Down Expand Up @@ -128,10 +129,9 @@ class ChartContainer extends React.PureComponent {
},

data: {
csv_endpoint: getExploreUrl(this.props.formData, 'csv'),
json_endpoint: getExploreUrl(this.props.formData, 'json'),
standalone_endpoint: getExploreUrl(
this.props.formData, 'standalone'),
csv_endpoint: getExploreUrl(props.formData, 'csv'),
json_endpoint: getExploreUrl(props.formData, 'json'),
standalone_endpoint: getExploreUrl(props.formData, 'standalone'),
},

};
Expand Down Expand Up @@ -308,6 +308,7 @@ function mapStateToProps(state) {
chartStatus: state.chartStatus,
chartUpdateEndTime: state.chartUpdateEndTime,
chartUpdateStartTime: state.chartUpdateStartTime,
datasource: state.datasource,
column_formats: state.datasource ? state.datasource.column_formats : null,
containerId: state.slice ? `slice-container-${state.slice.slice_id}` : 'slice-container',
formData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const propTypes = {
onChange: PropTypes.func,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]),
showHeader: PropTypes.bool,
optionRenderer: PropTypes.func,
valueRenderer: PropTypes.func,
valueKey: PropTypes.string,
options: PropTypes.array,
};

const defaultProps = {
Expand All @@ -27,6 +31,9 @@ const defaultProps = {
multi: false,
onChange: () => {},
showHeader: true,
optionRenderer: opt => opt.label,
valueRenderer: opt => opt.label,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be valueRenderer: opt => opt.value?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit confusing but essentially the optionRenderer applies to what's in the dropdown and the valueRenderer applies to the selected item(s). It's nice to have this level of control, for instance we could show the expression behind the metric only on the selected metric, not in the list if we wanted to.

So they both should show the label.

valueKey: 'value',
};

export default class SelectControl extends React.PureComponent {
Expand All @@ -42,14 +49,17 @@ export default class SelectControl extends React.PureComponent {
}
}
onChange(opt) {
let optionValue = opt ? opt.value : null;
let optionValue = opt ? opt[this.props.valueKey] : null;
// if multi, return options values as an array
if (this.props.multi) {
optionValue = opt ? opt.map(o => o.value) : null;
optionValue = opt ? opt.map(o => o[this.props.valueKey]) : null;
}
this.props.onChange(optionValue);
}
getOptions(props) {
if (props.options) {
return props.options;
}
// Accepts different formats of input
const options = props.choices.map((c) => {
let option;
Expand Down Expand Up @@ -94,11 +104,13 @@ export default class SelectControl extends React.PureComponent {
placeholder: `Select (${this.state.options.length})`,
options: this.state.options,
value: this.props.value,
valueKey: this.props.valueKey,
autosize: false,
clearable: this.props.clearable,
isLoading: this.props.isLoading,
onChange: this.onChange,
optionRenderer: opt => opt.label,
optionRenderer: this.props.optionRenderer,
valueRenderer: this.props.valueRenderer,
};
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
const selectWrap = this.props.freeForm ?
Expand Down
49 changes: 39 additions & 10 deletions superset/assets/javascripts/explore/stores/controls.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { formatSelectOptionsForRange, formatSelectOptions } from '../../modules/utils';
import * as v from '../validators';
import MetricOption from '../../components/MetricOption';
import ColumnOption from '../../components/ColumnOption';

const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format';

Expand All @@ -18,6 +20,7 @@ const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];

const SERIES_LIMITS = [0, 5, 10, 25, 50, 100, 500];


export const TIME_STAMP_OPTIONS = [
['smart_date', 'Adaptative formating'],
['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'],
Expand Down Expand Up @@ -58,10 +61,13 @@ export const controls = {
multi: true,
label: 'Metrics',
validators: [v.nonEmpty],
valueKey: 'metric_name',
optionRenderer: m => <MetricOption metric={m} />,
valueRenderer: m => <MetricOption metric={m} />,
default: control =>
control.choices && control.choices.length > 0 ? [control.choices[0][0]] : null,
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
options: (state.datasource) ? state.datasource.metrics : [],
}),
description: 'One or many metrics to display',
},
Expand Down Expand Up @@ -92,21 +98,29 @@ export const controls = {
label: 'Metric',
clearable: false,
description: 'Choose the metric',
validators: [v.nonEmpty],
optionRenderer: m => <MetricOption metric={m} />,
valueRenderer: m => <MetricOption metric={m} />,
valueKey: 'metric_name',
default: control =>
control.choices && control.choices.length > 0 ? control.choices[0][0] : null,
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.metrics_combo : null,
options: (state.datasource) ? state.datasource.metrics : [],
}),
},

metric_2: {
type: 'SelectControl',
label: 'Right Axis Metric',
choices: [],
default: [],
default: null,
validators: [v.nonEmpty],
clearable: true,
description: 'Choose a metric for right axis',
valueKey: 'metric_name',
optionRenderer: m => <MetricOption metric={m} />,
valueRenderer: m => <MetricOption metric={m} />,
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
options: (state.datasource) ? state.datasource.metrics : [],
}),
},

Expand Down Expand Up @@ -311,8 +325,11 @@ export const controls = {
label: 'Group by',
default: [],
description: 'One or many controls to group by',
optionRenderer: c => <ColumnOption column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.gb_cols : [],
options: (state.datasource) ? state.datasource.columns : [],
}),
},

Expand Down Expand Up @@ -650,10 +667,14 @@ export const controls = {
x: {
type: 'SelectControl',
label: 'X Axis',
default: null,
description: 'Metric assigned to the [X] axis',
default: null,
validators: [v.nonEmpty],
optionRenderer: m => <MetricOption metric={m} />,
valueRenderer: m => <MetricOption metric={m} />,
valueKey: 'metric_name',
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
options: (state.datasource) ? state.datasource.metrics : [],
}),
},

Expand All @@ -662,17 +683,25 @@ export const controls = {
label: 'Y Axis',
default: null,
description: 'Metric assigned to the [Y] axis',
validators: [v.nonEmpty],
optionRenderer: m => <MetricOption metric={m} />,
valueRenderer: m => <MetricOption metric={m} />,
valueKey: 'metric_name',
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
options: (state.datasource) ? state.datasource.metrics : [],
}),
},

size: {
type: 'SelectControl',
label: 'Bubble Size',
default: null,
validators: [v.nonEmpty],
optionRenderer: m => <MetricOption metric={m} />,
valueRenderer: m => <MetricOption metric={m} />,
valueKey: 'metric_name',
mapStateToProps: state => ({
choices: (state.datasource) ? state.datasource.metrics_combo : [],
options: (state.datasource) ? state.datasource.metrics : [],
}),
},

Expand Down
47 changes: 47 additions & 0 deletions superset/assets/spec/javascripts/components/ColumnOption_spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { shallow } from 'enzyme';

import ColumnOption from '../../../javascripts/components/ColumnOption';
import InfoTooltipWithTrigger from '../../../javascripts/components/InfoTooltipWithTrigger';

describe('ColumnOption', () => {
const defaultProps = {
column: {
column_name: 'foo',
verbose_name: 'Foo',
expression: 'SUM(foo)',
description: 'Foo is the greatest column of all',
},
};

let wrapper;
let props;
const factory = o => <ColumnOption {...o} />;
beforeEach(() => {
wrapper = shallow(factory(defaultProps));
props = Object.assign({}, defaultProps);
});
it('is a valid element', () => {
expect(React.isValidElement(<ColumnOption {...defaultProps} />)).to.equal(true);
});
it('shows a label with verbose_name', () => {
const lbl = wrapper.find('.option-label');
expect(lbl).to.have.length(1);
expect(lbl.first().text()).to.equal('Foo');
});
it('shows 2 InfoTooltipWithTrigger', () => {
expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(2);
});
it('shows only 1 InfoTooltipWithTrigger when no descr', () => {
props.column.description = null;
wrapper = shallow(factory(props));
expect(wrapper.find(InfoTooltipWithTrigger)).to.have.length(1);
});
it('shows a label with column_name when no verbose_name', () => {
props.column.verbose_name = null;
wrapper = shallow(factory(props));
expect(wrapper.find('.option-label').first().text()).to.equal('foo');
});
});
Loading