From d0e91966086963f964a44dd3d77e308b849db0d7 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Thu, 3 Nov 2016 18:05:35 -0700 Subject: [PATCH 1/4] Adding Metric & Metrics field --- .../javascripts/components/ColumnOption.jsx | 6 +- .../explore/components/controls/Metric.jsx | 280 ++++++++++++++++++ .../components/controls/MetricControl.jsx | 28 ++ .../components/controls/MetricListControl.jsx | 77 +++++ superset/assets/javascripts/explore/main.css | 6 + .../javascripts/explore/stores/controls.jsx | 43 ++- superset/connectors/base/models.py | 2 +- superset/connectors/sqla/models.py | 11 +- 8 files changed, 420 insertions(+), 33 deletions(-) create mode 100644 superset/assets/javascripts/explore/components/controls/Metric.jsx create mode 100644 superset/assets/javascripts/explore/components/controls/MetricControl.jsx create mode 100644 superset/assets/javascripts/explore/components/controls/MetricListControl.jsx diff --git a/superset/assets/javascripts/components/ColumnOption.jsx b/superset/assets/javascripts/components/ColumnOption.jsx index c150937a0b2d1..b00421dde1c91 100644 --- a/superset/assets/javascripts/components/ColumnOption.jsx +++ b/superset/assets/javascripts/components/ColumnOption.jsx @@ -5,11 +5,15 @@ import InfoTooltipWithTrigger from './InfoTooltipWithTrigger'; const propTypes = { column: PropTypes.object.isRequired, + prefix: PropTypes.string, }; -export default function ColumnOption({ column }) { +export default function ColumnOption({ column, prefix }) { return ( + {prefix && + {prefix} + } {column.verbose_name || column.column_name} diff --git a/superset/assets/javascripts/explore/components/controls/Metric.jsx b/superset/assets/javascripts/explore/components/controls/Metric.jsx new file mode 100644 index 0000000000000..e0c17b103df97 --- /dev/null +++ b/superset/assets/javascripts/explore/components/controls/Metric.jsx @@ -0,0 +1,280 @@ +import React, { PropTypes } from 'react'; +import Select from 'react-select'; +import { + Col, + FormControl, + FormGroup, + InputGroup, + Label, + OverlayTrigger, + Popover, + Radio, + Row, +} from 'react-bootstrap'; + +import MetricOption from '../../../components/MetricOption'; +import ColumnOption from '../../../components/ColumnOption'; + +const NUMERIC_TYPES = ['INT', 'INTEGER', 'BIGINT', 'DOUBLE', 'FLOAT', 'NUMERIC']; +function isNum(type) { + return NUMERIC_TYPES.some(s => type.startsWith(s)); +} +const nonNumericAggFunctions = { + COUNT_DISTINCT: 'COUNT(DISTINCT {})', + COUNT: 'COUNT({})', +}; +const numericAggFunctions = { + SUM: 'SUM({})', + AVG: 'AVG({})', + MIN: 'MIN({})', + MAX: 'MAX({})', + COUNT_DISTINCT: 'COUNT(DISTINCT {})', + COUNT: 'COUNT({})', +}; + +const propTypes = { + datasource: PropTypes.object, + column: PropTypes.string, + metricType: PropTypes.string, + onChange: PropTypes.func, + initialMetricType: PropTypes.string, + initialLabel: PropTypes.string, + initialSql: PropTypes.string, + onDelete: PropTypes.func, +}; + +const defaultProps = { + initialMetricType: 'free', + initialLabel: 'row_count', + initialSql: 'COUNT(*)', +}; + +export default class Metric extends React.Component { + constructor(props) { + super(props); + this.state = { + aggregate: null, + label: props.initialLabel, + metricType: props.initialMetricType, + metricName: null, + sql: props.initialSql, + }; + } + onChange() { + this.props.onChange(this.state); + } + onDelete() { + this.props.onDelete(); + } + setMetricType(v) { + this.setState({ metricType: v }); + } + changeLabel(e) { + const label = e.target.value; + this.setState({ label }, this.onChange); + } + changeExpression(e) { + const sql = e.target.value; + this.setState({ sql, columnName: null, aggregate: null }, this.onChange); + } + optionify(arr) { + return arr.map(s => ({ value: s, label: s })); + } + changeColumnSection() { + let label; + if (this.state.aggregate && this.state.column) { + label = this.state.aggregate + '__' + this.state.column.column_name; + } else { + label = ''; + } + this.setState({ label }, this.onChange); + } + changeAggregate(opt) { + const aggregate = opt ? opt.value : null; + this.setState({ aggregate }, this.changeColumnSection); + } + changeRadio(e) { + this.setState({ metricType: e.target.value }); + } + changeMetric(metric) { + let label; + if (metric) { + label = metric.metric_name; + } + this.setState({ label, metric }, this.onChange); + } + changeColumn(column) { + let aggregate = this.state.aggregate; + if (column) { + if (!aggregate) { + if (isNum(column.type)) { + aggregate = 'SUM'; + } else { + aggregate = 'COUNT_DISTINCT'; + } + } + } else { + aggregate = null; + } + this.setState({ column, aggregate }, this.changeColumnSection); + } + renderOverlay() { + let aggregateOptions = []; + const column = this.state.column; + if (column) { + if (isNum(column.type)) { + aggregateOptions = Object.keys(numericAggFunctions); + } else { + aggregateOptions = Object.keys(nonNumericAggFunctions); + } + } + const metricType = this.state.metricType; + return ( + + + + Label + + + +
+
+ + + + + +
+ ( +
+ Aggregate: {o.label} +
+ )} + /> +
+ +
+
+
+
+ + + + + + } + valueRenderer={c => } + />); + } + renderMetricSelect() { + return ( + ( +
+ agg: {o.label} +
+ )} + />); + } + renderOverlay() { const metricType = this.state.metricType; return ( @@ -143,47 +203,23 @@ export default class Metric extends React.Component {
-
+
- ( -
- Aggregate: {o.label} -
- )} - /> + {this.renderAggSelect()}
@@ -201,41 +237,31 @@ export default class Metric extends React.Component { /> -