Skip to content

Commit

Permalink
[explore] improve metric(s) and groupby(s) controls (#2921)
Browse files Browse the repository at this point in the history
* [explore] improve metric(s) and groupby(s) controls

- surface verbose_name, description & expression in controls
- [table viz] surface verbose name in table header

* Fixing tests

* Addressing comments

* Fixing tests (once more)
  • Loading branch information
mistercrunch authored Jun 9, 2017
1 parent 34f381b commit 16141ec
Show file tree
Hide file tree
Showing 14 changed files with 275 additions and 29 deletions.
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,
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

0 comments on commit 16141ec

Please sign in to comment.