diff --git a/superset/assets/javascripts/explorev2/components/FieldSet.jsx b/superset/assets/javascripts/explorev2/components/FieldSet.jsx
index b2a11823aa2c215d36ac13c6b8fa11a79c411e29..e2483a46ecd6839de883eb9a05a07b2c9f83374d 100644
--- a/superset/assets/javascripts/explorev2/components/FieldSet.jsx
+++ b/superset/assets/javascripts/explorev2/components/FieldSet.jsx
@@ -3,14 +3,18 @@ import TextField from './TextField';
import CheckboxField from './CheckboxField';
import TextAreaField from './TextAreaField';
import SelectField from './SelectField';
+import MetricList from './MetricList';
+import MetricField from './MetricField';
import ControlLabelWithTooltip from './ControlLabelWithTooltip';
const fieldMap = {
- TextField,
CheckboxField,
- TextAreaField,
+ MetricField,
+ MetricList,
SelectField,
+ TextAreaField,
+ TextField,
};
const fieldTypes = Object.keys(fieldMap);
diff --git a/superset/assets/javascripts/explorev2/components/Filters.jsx b/superset/assets/javascripts/explorev2/components/Filters.jsx
index be0cd11289e61e25fed541589fc1c852bd707d05..e752df51f6a76c1c4329c359b6c31dea2d44a620 100644
--- a/superset/assets/javascripts/explorev2/components/Filters.jsx
+++ b/superset/assets/javascripts/explorev2/components/Filters.jsx
@@ -1,5 +1,4 @@
import React from 'react';
-// import { Tab, Row, Col, Nav, NavItem } from 'react-bootstrap';
import Filter from './Filter';
import { Button } from 'react-bootstrap';
import { connect } from 'react-redux';
diff --git a/superset/assets/javascripts/explorev2/components/MetricField.jsx b/superset/assets/javascripts/explorev2/components/MetricField.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e21df6b47688e1e0528c259e43427c6cdbf1fe3e
--- /dev/null
+++ b/superset/assets/javascripts/explorev2/components/MetricField.jsx
@@ -0,0 +1,303 @@
+import React, { PropTypes } from 'react';
+import Select from 'react-select';
+import {
+ Col,
+ FormControl,
+ FormGroup,
+ InputGroup,
+ Label,
+ OverlayTrigger,
+ Popover,
+ Radio,
+ Row,
+} from 'react-bootstrap';
+
+const numericAggFunctions = {
+ SUM: 'SUM({})',
+ MIN: 'MIN({})',
+ MAX: 'MAX({})',
+ AVG: 'AVG({})',
+ COUNT_DISTINCT: 'COUNT(DISTINCT {})',
+ COUNT: 'COUNT({})',
+};
+const NUMERIC_TYPES = ['INT', 'INTEGER', 'BIGINT', 'DOUBLE', 'FLOAT', 'NUMERIC'];
+const nonNumericAggFunctions = {
+ 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 MetricField extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ aggregate: null,
+ label: props.initialLabel,
+ metricType: props.initialMetricType,
+ metricName: null,
+ sql: props.initialSql,
+ };
+ this.getColumnOptions = this.getColumnOptions.bind(this);
+ }
+ onChange() {
+ console.log(this.state);
+ this.props.onChange(this.state);
+ }
+ onDelete() {
+ this.props.onDelete();
+ }
+ getColumnOptions() {
+ return this.props.datasource.columns.map(col => ({
+ value: col.column_name,
+ label: col.column_name,
+ column: col,
+ }));
+ }
+ setMetricType(v) {
+ this.setState({ metricType: v });
+ }
+ getMetricOptions() {
+ return this.props.datasource.metrics.map(metric => ({
+ value: metric.metric_name,
+ label: metric.metric_name,
+ metric,
+ type: 'metric',
+ }));
+ }
+ 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(opt) {
+ let metricName;
+ let label;
+ if (opt) {
+ metricName = opt.metric.metric_name;
+ label = metricName;
+ }
+ this.setState({ label, metricName }, this.onChange);
+ }
+ changeColumn(opt) {
+ let column;
+ let aggregate = this.state.aggregate;
+ if (opt) {
+ column = opt.column;
+ if (!aggregate) {
+ if (NUMERIC_TYPES.includes(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 (NUMERIC_TYPES.includes(column.type)) {
+ aggregateOptions = Object.keys(numericAggFunctions);
+ } else {
+ aggregateOptions = Object.keys(nonNumericAggFunctions);
+ }
+ }
+ const metricType = this.state.metricType;
+ return (
+
+
+
+ Label
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SQL
+
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ if (!this.props.datasource) {
+ return null;
+ }
+ let deleteButton;
+ if (this.props.onDelete) {
+ deleteButton = ;
+ }
+ const trigger = (
+
+
+
+ );
+ return (
+
+ );
+ }
+}
+
+MetricField.propTypes = propTypes;
+MetricField.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explorev2/components/MetricList.jsx b/superset/assets/javascripts/explorev2/components/MetricList.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..2aec25b78d963b9427da622bea5af45f449f00b4
--- /dev/null
+++ b/superset/assets/javascripts/explorev2/components/MetricList.jsx
@@ -0,0 +1,84 @@
+import React, { PropTypes } from 'react';
+import MetricField from './MetricField';
+import ControlLabelWithTooltip from './ControlLabelWithTooltip';
+
+const propTypes = {
+ initialMetrics: PropTypes.array.isRequired,
+ datasource: PropTypes.object,
+ onChange: PropTypes.func,
+};
+
+const defaultProps = {
+ initialMetrics: [],
+ onChange: () => {},
+};
+
+export default class MetricList extends React.Component {
+ constructor(props) {
+ super(props);
+ const metrics = this.props.initialMetrics.slice();
+ if (props.initialMetrics.length === 0) {
+ metrics.push({
+ metricType: 'free',
+ metricLabel: 'row_count',
+ sql: 'COUNT(*)',
+ });
+ }
+ this.state = {
+ metrics,
+ };
+ }
+ changeMetric(i, metric) {
+ const metrics = this.state.metrics.slice();
+ metrics[i] = metric;
+ this.setState({ metrics }, this.onChange);
+ }
+ addMetric() {
+ const metrics = this.state.metrics.slice();
+ const name = 'unlabeled metric ' + this.state.metrics.length;
+ metrics.push({
+ initialMetricType: 'free',
+ initialLabel: name,
+ initialSql: '',
+ });
+ this.setState({ metrics }, this.onChange);
+ }
+ onChange() {
+ console.log(this.state.metrics);
+ this.props.onChange(this.state.metrics);
+ }
+ deleteMetric(metric) {
+ this.setState({ metrics: this.state.metrics.filter(m => m !== metric) });
+ }
+
+ render() {
+ if (!this.props.datasource) {
+ return null;
+ }
+ const metrics = this.state.metrics || [];
+ return (
+
+
+
+ {metrics.map((metric, i) => (
+
+ ))}
+
+
+
+
+
+ );
+ }
+}
+
+MetricList.propTypes = propTypes;
+MetricList.defaultProps = defaultProps;
diff --git a/superset/assets/javascripts/explorev2/components/TextField.jsx b/superset/assets/javascripts/explorev2/components/TextField.jsx
index 3d6137611372636179ba54d997a0a3cbf9d80cde..dc631b329711fe15368cb4fd388e3a9d24d5d885 100644
--- a/superset/assets/javascripts/explorev2/components/TextField.jsx
+++ b/superset/assets/javascripts/explorev2/components/TextField.jsx
@@ -21,13 +21,14 @@ export default class TextField extends React.Component {
this.props.onChange(this.props.name, event.target.value);
}
render() {
+ const value = this.props.value || '';
return (
);
diff --git a/superset/assets/javascripts/explorev2/main.css b/superset/assets/javascripts/explorev2/main.css
index 145b70711c4e8156f9b4bf9a95918bc0ef8fb247..cf447087b6d042458680af4529297b0eb7c90888 100644
--- a/superset/assets/javascripts/explorev2/main.css
+++ b/superset/assets/javascripts/explorev2/main.css
@@ -25,3 +25,9 @@
margin-top: 0px;
margin-right: 3px;
}
+#popover-positioned-right {
+ width: 400px;
+}
+.popover {
+ max-width: 100%;
+}
diff --git a/superset/assets/javascripts/explorev2/stores/fields.js b/superset/assets/javascripts/explorev2/stores/fields.js
index 0866f090d625b24b31d278b2360865baf0db2c90..e2a64c1b0c39cbde39dcdf72eb9a9118bc4c0e8b 100644
--- a/superset/assets/javascripts/explorev2/stores/fields.js
+++ b/superset/assets/javascripts/explorev2/stores/fields.js
@@ -46,17 +46,6 @@ export const fields = {
description: 'The type of visualization to display',
},
- metrics: {
- type: 'SelectField',
- multi: true,
- label: 'Metrics',
- mapStateToProps: (state) => ({
- choices: (state.datasource) ? state.datasource.metrics_combo : [],
- }),
- default: [],
- description: 'One or many metrics to display',
- },
-
order_by_cols: {
type: 'SelectField',
multi: true,
@@ -69,15 +58,26 @@ export const fields = {
},
metric: {
- type: 'SelectField',
+ type: 'MetricField',
label: 'Metric',
default: null,
description: 'Choose the metric',
mapStateToProps: (state) => ({
- choices: (state.datasource) ? state.datasource.metrics_combo : [],
+ datasource: state.datasource,
}),
},
+ metrics: {
+ type: 'MetricList',
+ multi: true,
+ label: 'Metrics',
+ mapStateToProps: (state) => ({
+ datasource: state.datasource,
+ }),
+ default: [],
+ description: 'One or many metrics to display',
+ },
+
metric_2: {
type: 'SelectField',
label: 'Right Axis Metric',
diff --git a/superset/assets/javascripts/explorev2/stores/visTypes.js b/superset/assets/javascripts/explorev2/stores/visTypes.js
index 876293c5dd1c6d8bbbb97d378ce6a509ddd5455d..ce93b368d7c1da7b38bd3f4d7db7ef82c5a5ddd9 100644
--- a/superset/assets/javascripts/explorev2/stores/visTypes.js
+++ b/superset/assets/javascripts/explorev2/stores/visTypes.js
@@ -307,7 +307,9 @@ const visTypes = {
{
label: null,
fieldSetRows: [
- ['series', 'metric', 'limit'],
+ ['series'],
+ ['metric'],
+ ['limit'],
['size_from', 'size_to'],
['rotation'],
],
diff --git a/superset/assets/stylesheets/superset.css b/superset/assets/stylesheets/superset.css
index 37863fce1a8046cad5a09987134fc10786ae7e20..bc1442a8deffb6c1b8b8f74b27da59e41243c13c 100644
--- a/superset/assets/stylesheets/superset.css
+++ b/superset/assets/stylesheets/superset.css
@@ -193,3 +193,6 @@ div.widget .slice_container {
.table-condensed {
font-size: 12px;
}
+.dimmed {
+ opacity: 0.5;
+}
diff --git a/superset/models.py b/superset/models.py
index cef46a4c57a8b77fb2dd8b738bc04113a1a5b3ba..5569baa90d39742d8d58ff0f41de95710c91e69f 100644
--- a/superset/models.py
+++ b/superset/models.py
@@ -684,6 +684,8 @@ class Queryable(object):
d = {
'id': self.id,
+ 'columns': [col.data for col in self.columns],
+ 'metrics': [m.data for m in self.metrics],
'type': self.type,
'name': self.name,
'metrics_combo': self.metrics_combo,
@@ -1426,24 +1428,70 @@ class SqlaTable(Model, Queryable, AuditMixinNullable, ImportMixin):
sqla.event.listen(SqlaTable, 'after_insert', set_perm)
sqla.event.listen(SqlaTable, 'after_update', set_perm)
+class MetricAbstract(object):
+ metric_name = Column(String(512))
+ verbose_name = Column(String(1024))
+ metric_type = Column(String(32))
+ expression = Column(Text)
+ description = Column(Text)
+ is_restricted = Column(Boolean, default=False, nullable=True)
-class SqlMetric(Model, AuditMixinNullable, ImportMixin):
+ @property
+ def data(self):
+ attrs = (
+ 'metric_name', 'metric_type', 'description',
+ )
+ d = {s: getattr(self, s) for s in attrs}
+ return d
+
+
+class DruidMetric(Model, AuditMixinNullable, MetricAbstract):
+
+ """ORM object referencing Druid metrics for a datasource"""
+
+ __tablename__ = 'metrics'
+ id = Column(Integer, primary_key=True)
+ datasource_name = Column(
+ String(255),
+ ForeignKey('datasources.datasource_name'))
+ # Setting enable_typechecks=False disables polymorphic inheritance.
+ datasource = relationship(
+ 'DruidDatasource',
+ backref=backref('metrics', cascade='all, delete-orphan'),
+ enable_typechecks=False)
+ json = Column(Text)
+ d3format = Column(String(128))
+
+ @property
+ def json_obj(self):
+ try:
+ obj = json.loads(self.json)
+ except Exception:
+ obj = {}
+ return obj
+
+ @property
+ def perm(self):
+ return (
+ "{parent_name}.[{obj.metric_name}](id:{obj.id})"
+ ).format(obj=self,
+ parent_name=self.datasource.full_name
+ ) if self.datasource else None
+
+
+
+
+class SqlMetric(Model, AuditMixinNullable, ImportMixin, MetricAbstract):
"""ORM object for metrics, each table can have multiple metrics"""
__tablename__ = 'sql_metrics'
id = Column(Integer, primary_key=True)
- metric_name = Column(String(512))
- verbose_name = Column(String(1024))
- metric_type = Column(String(32))
table_id = Column(Integer, ForeignKey('tables.id'))
table = relationship(
'SqlaTable',
backref=backref('metrics', cascade='all, delete-orphan'),
foreign_keys=[table_id])
- expression = Column(Text)
- description = Column(Text)
- is_restricted = Column(Boolean, default=False, nullable=True)
d3format = Column(String(128))
export_fields = (
@@ -1483,7 +1531,18 @@ class SqlMetric(Model, AuditMixinNullable, ImportMixin):
return metric_to_import
-class TableColumn(Model, AuditMixinNullable, ImportMixin):
+class ColumnAbstract(object):
+ """Incomplete placeholder for things common to Column fields"""
+ @property
+ def data(self):
+ attrs = (
+ 'column_name', 'verbose_name', 'is_dttm', 'type',
+ )
+ d = {s: getattr(self, s) for s in attrs}
+ return d
+
+
+class TableColumn(Model, AuditMixinNullable, ImportMixin, ColumnAbstract):
"""ORM object for table columns, each table can have multiple columns"""
@@ -2415,46 +2474,7 @@ class Log(Model):
return wrapper
-class DruidMetric(Model, AuditMixinNullable):
-
- """ORM object referencing Druid metrics for a datasource"""
-
- __tablename__ = 'metrics'
- id = Column(Integer, primary_key=True)
- metric_name = Column(String(512))
- verbose_name = Column(String(1024))
- metric_type = Column(String(32))
- datasource_name = Column(
- String(255),
- ForeignKey('datasources.datasource_name'))
- # Setting enable_typechecks=False disables polymorphic inheritance.
- datasource = relationship(
- 'DruidDatasource',
- backref=backref('metrics', cascade='all, delete-orphan'),
- enable_typechecks=False)
- json = Column(Text)
- description = Column(Text)
- is_restricted = Column(Boolean, default=False, nullable=True)
- d3format = Column(String(128))
-
- @property
- def json_obj(self):
- try:
- obj = json.loads(self.json)
- except Exception:
- obj = {}
- return obj
-
- @property
- def perm(self):
- return (
- "{parent_name}.[{obj.metric_name}](id:{obj.id})"
- ).format(obj=self,
- parent_name=self.datasource.full_name
- ) if self.datasource else None
-
-
-class DruidColumn(Model, AuditMixinNullable):
+class DruidColumn(Model, AuditMixinNullable, ColumnAbstract):
"""ORM model for storing Druid datasource column metadata"""