提交 8f5977f4 编写于 作者: M Maxime Beauchemin

Metric field

上级 14ed10bd
......@@ -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);
......
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';
......
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 (
<Popover id="popover-positioned-right" title="Metric Definition">
<FormGroup bsSize="sm" controlId="label">
<InputGroup bsSize="sm">
<InputGroup.Addon>Label</InputGroup.Addon>
<FormControl
type="text"
value={this.state.label}
placeholder="Label"
onChange={this.changeLabel.bind(this)}
/>
</InputGroup>
</FormGroup>
<hr />
<div className={metricType !== 'column' ? 'dimmed' : '' }>
<Row>
<Col md={1}>
<Radio
name="metricType"
inline
value="column"
onChange={this.changeRadio.bind(this)}
checked={this.state.metricType === 'column'}
/>
</Col>
<Col md={11}>
<div className="m-b-5">
<Select
name="select-schema"
placeholder="Column"
onFocus={this.setMetricType.bind(this, 'column')}
options={this.getColumnOptions()}
onChange={this.changeColumn.bind(this)}
value={this.state.column ? this.state.column.column_name : null}
autosize={false}
valueRenderer={(o) => (
<div>
<span className="text-muted">Column:</span> {o.label}
</div>
)}
/>
</div>
<div>
<Select
name="select-schema"
placeholder="Aggregate function"
onFocus={this.setMetricType.bind(this, 'column')}
disabled={aggregateOptions.length === 0}
options={this.optionify(aggregateOptions)}
value={this.state.aggregate}
autosize={false}
onChange={this.changeAggregate.bind(this)}
valueRenderer={(o) => (
<div>
<span className="text-muted">Aggregate:</span> {o.label}
</div>
)}
/>
</div>
</Col>
</Row>
</div>
<hr />
<div className={metricType !== 'metric' ? 'dimmed' : '' }>
<Row>
<Col md={1}>
<Radio
inline
name="metricType"
value="metric"
onChange={this.changeRadio.bind(this)}
checked={this.state.metricType === 'metric'}
/>
</Col>
<Col md={11}>
<Select
name="select-schema"
placeholder="Predefined metric"
options={this.getMetricOptions()}
onFocus={this.setMetricType.bind(this, 'metric')}
value={this.state.metricName}
autosize={false}
onChange={this.changeMetric.bind(this)}
valueRenderer={(o) => (
<div>
<span className="text-muted">Metric:</span> {o.label}
</div>
)}
/>
</Col>
</Row>
</div>
<hr />
<div className={metricType !== 'free' ? 'dimmed' : '' }>
<Row>
<Col md={1}>
<Radio
inline
name="metricType"
value="free"
onChange={this.changeRadio.bind(this)}
checked={this.state.metricType === 'free'}
/>
</Col>
<Col md={11}>
<FormGroup bsSize="sm" controlId="sql">
<InputGroup bsSize="sm">
<InputGroup.Addon>SQL</InputGroup.Addon>
<FormControl
type="text"
value={this.state.sql}
onFocus={this.setMetricType.bind(this, 'free')}
placeholder="Free form SQL"
onChange={this.changeExpression.bind(this)}
/>
</InputGroup>
</FormGroup>
</Col>
</Row>
</div>
</Popover>
);
}
render() {
if (!this.props.datasource) {
return null;
}
let deleteButton;
if (this.props.onDelete) {
deleteButton = <i className="fa fa-times" onClick={this.onDelete.bind(this)} role="button" />;
}
const trigger = (
<OverlayTrigger
trigger="click"
placement="right"
rootClose
overlay={this.renderOverlay()}
>
<i className="fa fa-edit" role="button" />
</OverlayTrigger>
);
return (
<Label
className="MetricField lead"
style={{ fontSize: '14px', display: 'inline-block', margin: '0 3 3 0', padding: '5px' }}
>
<span>
{this.state.label} {trigger} {deleteButton}
</span>
</Label>
);
}
}
MetricField.propTypes = propTypes;
MetricField.defaultProps = defaultProps;
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 (
<div className="MetricList">
<ControlLabelWithTooltip
label={this.props.label}
description={this.props.description}
/>
<div className="MetricList">
{metrics.map((metric, i) => (
<MetricField
key={i}
datasource={this.props.datasource}
onChange={this.changeMetric.bind(this, i)}
onDelete={this.deleteMetric.bind(this, metric)}
/>
))}
<a onClick={this.addMetric.bind(this)} role="button">
<i className="fa fa-plus-circle" />
</a>
</div>
</div>
);
}
}
MetricList.propTypes = propTypes;
MetricList.defaultProps = defaultProps;
......@@ -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 (
<FormGroup controlId="formInlineName" bsSize="small">
<FormControl
type="text"
placeholder=""
onChange={this.onChange.bind(this)}
value={this.props.value}
value={value}
/>
</FormGroup>
);
......
......@@ -25,3 +25,9 @@
margin-top: 0px;
margin-right: 3px;
}
#popover-positioned-right {
width: 400px;
}
.popover {
max-width: 100%;
}
......@@ -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',
......
......@@ -307,7 +307,9 @@ const visTypes = {
{
label: null,
fieldSetRows: [
['series', 'metric', 'limit'],
['series'],
['metric'],
['limit'],
['size_from', 'size_to'],
['rotation'],
],
......
......@@ -193,3 +193,6 @@ div.widget .slice_container {
.table-condensed {
font-size: 12px;
}
.dimmed {
opacity: 0.5;
}
......@@ -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"""
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册