[explore v2] populate dynamic select field options (#1543)

* pass field options in viz json

* move field options to fetch_datasource_metadata

* on control panels container mount, fetch datasource meta data and set dynamic field choices

* render options for select fields

* use component class rather than sic

* fix linting

* fix whitespace

* delete unused var

* only render fields once datasource meta has returned

* fix typo

* add datasources and fix column formatting

* fix tests

* never used function

* fix tests

* add test for fetch_datasource_metadata

* remove unneeded props
This commit is contained in:
Alanna Scott 2016-11-08 15:55:49 -08:00 committed by GitHub
parent 4530047c76
commit 51c0470f0b
11 changed files with 204 additions and 207 deletions

View File

@ -1,11 +1,6 @@
const $ = window.$ = require('jquery'); const $ = window.$ = require('jquery');
export const SET_DATASOURCE = 'SET_DATASOURCE'; export const SET_DATASOURCE = 'SET_DATASOURCE';
export const SET_TIME_COLUMN_OPTS = 'SET_TIME_COLUMN_OPTS'; export const SET_FIELD_OPTIONS = 'SET_FIELD_OPTIONS';
export const SET_TIME_GRAIN_OPTS = 'SET_TIME_GRAIN_OPTS';
export const SET_GROUPBY_COLUMN_OPTS = 'SET_GROUPBY_COLUMN_OPTS';
export const SET_METRICS_OPTS = 'SET_METRICS_OPTS';
export const SET_COLUMN_OPTS = 'SET_COLUMN_OPTS';
export const SET_ORDERING_OPTS = 'SET_ORDERING_OPTS';
export const TOGGLE_SEARCHBOX = 'TOGGLE_SEARCHBOX'; export const TOGGLE_SEARCHBOX = 'TOGGLE_SEARCHBOX';
export const SET_FILTER_COLUMN_OPTS = 'SET_FILTER_COLUMN_OPTS'; export const SET_FILTER_COLUMN_OPTS = 'SET_FILTER_COLUMN_OPTS';
export const ADD_FILTER = 'ADD_FILTER'; export const ADD_FILTER = 'ADD_FILTER';
@ -19,47 +14,36 @@ export const CLEAR_ALL_OPTS = 'CLEAR_ALL_OPTS';
export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE'; export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE';
export const SET_FIELD_VALUE = 'SET_FIELD_VALUE'; export const SET_FIELD_VALUE = 'SET_FIELD_VALUE';
export function setTimeColumnOpts(timeColumnOpts) { export function setFieldOptions(options) {
return { type: SET_TIME_COLUMN_OPTS, timeColumnOpts }; return { type: SET_FIELD_OPTIONS, options };
}
export function setTimeGrainOpts(timeGrainOpts) {
return { type: SET_TIME_GRAIN_OPTS, timeGrainOpts };
}
export function setGroupByColumnOpts(groupByColumnOpts) {
return { type: SET_GROUPBY_COLUMN_OPTS, groupByColumnOpts };
}
export function setMetricsOpts(metricsOpts) {
return { type: SET_METRICS_OPTS, metricsOpts };
}
export function setColumnOpts(columnOpts) {
return { type: SET_COLUMN_OPTS, columnOpts };
}
export function setOrderingOpts(orderingOpts) {
return { type: SET_ORDERING_OPTS, orderingOpts };
}
export function setFilterColumnOpts(filterColumnOpts) {
return { type: SET_FILTER_COLUMN_OPTS, filterColumnOpts };
} }
export function clearAllOpts() { export function clearAllOpts() {
return { type: CLEAR_ALL_OPTS }; return { type: CLEAR_ALL_OPTS };
} }
export function setFormOpts(datasourceId, datasourceType) { export function setDatasourceType(datasourceType) {
return { type: SET_DATASOURCE_TYPE, datasourceType };
}
export const FETCH_STARTED = 'FETCH_STARTED';
export function fetchStarted() {
return { type: FETCH_STARTED };
}
export const FETCH_SUCCEEDED = 'FETCH_SUCCEEDED';
export function fetchSucceeded() {
return { type: FETCH_SUCCEEDED };
}
export const FETCH_FAILED = 'FETCH_FAILED';
export function fetchFailed() {
return { type: FETCH_FAILED };
}
export function fetchFieldOptions(datasourceId, datasourceType) {
return function (dispatch) { return function (dispatch) {
const timeColumnOpts = []; dispatch(fetchStarted());
const groupByColumnOpts = [];
const metricsOpts = [];
const filterColumnOpts = [];
const timeGrainOpts = [];
const columnOpts = [];
const orderingOpts = [];
if (datasourceId) { if (datasourceId) {
const params = [`datasource_id=${datasourceId}`, `datasource_type=${datasourceType}`]; const params = [`datasource_id=${datasourceId}`, `datasource_type=${datasourceType}`];
@ -67,41 +51,15 @@ export function setFormOpts(datasourceId, datasourceType) {
$.get(url, (data, status) => { $.get(url, (data, status) => {
if (status === 'success') { if (status === 'success') {
data.time_columns.forEach((d) => { // populate options for select type fields
if (d) timeColumnOpts.push({ value: d, label: d }); dispatch(setFieldOptions(data.field_options));
}); dispatch(fetchSucceeded());
data.groupby_cols.forEach((d) => { } else if (status === 'error') {
if (d) groupByColumnOpts.push({ value: d, label: d }); dispatch(fetchFailed());
});
data.metrics.forEach((d) => {
if (d) metricsOpts.push({ value: d[1], label: d[0] });
});
data.filter_cols.forEach((d) => {
if (d) filterColumnOpts.push({ value: d, label: d });
});
data.time_grains.forEach((d) => {
if (d) timeGrainOpts.push({ value: d, label: d });
});
data.columns.forEach((d) => {
if (d) columnOpts.push({ value: d, label: d });
});
data.ordering_cols.forEach((d) => {
if (d) orderingOpts.push({ value: d, label: d });
});
// Repopulate options for controls
dispatch(setTimeColumnOpts(timeColumnOpts));
dispatch(setTimeGrainOpts(timeGrainOpts));
dispatch(setGroupByColumnOpts(groupByColumnOpts));
dispatch(setMetricsOpts(metricsOpts));
dispatch(setFilterColumnOpts(filterColumnOpts));
dispatch(setColumnOpts(columnOpts));
dispatch(setOrderingOpts(orderingOpts));
} }
}); });
} else { } else {
// Clear all Select options // in what case don't we have a datasource id?
dispatch(clearAllOpts());
} }
}; };
} }

View File

@ -13,6 +13,8 @@ const propTypes = {
datasource_id: PropTypes.number.isRequired, datasource_id: PropTypes.number.isRequired,
datasource_type: PropTypes.string.isRequired, datasource_type: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired, actions: PropTypes.object.isRequired,
fields: PropTypes.object.isRequired,
isDatasourceMetaLoading: PropTypes.bool.isRequired,
}; };
const defaultProps = { const defaultProps = {
@ -23,7 +25,7 @@ class ControlPanelsContainer extends React.Component {
componentWillMount() { componentWillMount() {
const { datasource_id, datasource_type } = this.props; const { datasource_id, datasource_type } = this.props;
if (datasource_id) { if (datasource_id) {
this.props.actions.setFormOpts(datasource_id, datasource_type); this.props.actions.fetchFieldOptions(datasource_id, datasource_type);
} }
} }
@ -47,27 +49,30 @@ class ControlPanelsContainer extends React.Component {
render() { render() {
return ( return (
<Panel> <Panel>
<div className="scrollbar-container"> {!this.props.isDatasourceMetaLoading &&
<div className="scrollbar-content"> <div className="scrollbar-container">
{this.sectionsToRender().map((section) => ( <div className="scrollbar-content">
<ControlPanelSection {this.sectionsToRender().map((section) => (
key={section.label} <ControlPanelSection
label={section.label} key={section.label}
tooltip={section.description} label={section.label}
> tooltip={section.description}
{section.fieldSetRows.map((fieldSets, i) => ( >
<FieldSetRow {section.fieldSetRows.map((fieldSets, i) => (
key={`${section.label}-fieldSetRow-${i}`} <FieldSetRow
fieldSets={fieldSets} key={`${section.label}-fieldSetRow-${i}`}
fieldOverrides={this.fieldOverrides()} fieldSets={fieldSets}
onChange={this.onChange.bind(this)} fieldOverrides={this.fieldOverrides()}
/> onChange={this.onChange.bind(this)}
))} fields={this.props.fields}
</ControlPanelSection> />
))} ))}
{/* TODO: add filters section */} </ControlPanelSection>
))}
{/* TODO: add filters section */}
</div>
</div> </div>
</div> }
</Panel> </Panel>
); );
} }
@ -78,6 +83,8 @@ ControlPanelsContainer.defaultProps = defaultProps;
function mapStateToProps(state) { function mapStateToProps(state) {
return { return {
isDatasourceMetaLoading: state.isDatasourceMetaLoading,
fields: state.fields,
datasource_id: state.datasource_id, datasource_id: state.datasource_id,
datasource_type: state.datasource_type, datasource_type: state.datasource_type,
viz_type: state.viz.form_data.viz_type, viz_type: state.viz.form_data.viz_type,

View File

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import FieldSet from './FieldSet'; import FieldSet from './FieldSet';
import { fields } from '../stores/store';
const NUM_COLUMNS = 12; const NUM_COLUMNS = 12;
const propTypes = { const propTypes = {
fields: PropTypes.object.isRequired,
fieldSets: PropTypes.array.isRequired, fieldSets: PropTypes.array.isRequired,
fieldOverrides: PropTypes.object, fieldOverrides: PropTypes.object,
onChange: PropTypes.func, onChange: PropTypes.func,
@ -15,29 +15,33 @@ const defaultProps = {
onChange: () => {}, onChange: () => {},
}; };
function getFieldData(fs, fieldOverrides) { export default class FieldSetRow extends React.Component {
let fieldData = fields[fs]; getFieldData(fs) {
if (fieldOverrides.hasOwnProperty(fs)) { const { fields, fieldOverrides } = this.props;
const overrideData = fieldOverrides[fs]; let fieldData = fields[fs];
fieldData = Object.assign({}, fieldData, overrideData); if (fieldOverrides.hasOwnProperty(fs)) {
const overrideData = fieldOverrides[fs];
fieldData = Object.assign({}, fieldData, overrideData);
}
return fieldData;
} }
return fieldData;
}
export default function FieldSetRow({ fieldSets, fieldOverrides, onChange }) { render() {
const colSize = NUM_COLUMNS / fieldSets.length; const colSize = NUM_COLUMNS / this.props.fieldSets.length;
return ( const { onChange } = this.props;
<div className="row"> return (
{fieldSets.map((fs) => { <div className="row">
const fieldData = getFieldData(fs, fieldOverrides); {this.props.fieldSets.map((fs) => {
return ( const fieldData = this.getFieldData(fs);
<div className={`col-lg-${colSize} col-xs-12`} key={fs}> return (
<FieldSet name={fs} onChange={onChange} {...fieldData} /> <div className={`col-lg-${colSize} col-xs-12`} key={fs}>
</div> <FieldSet name={fs} onChange={onChange} {...fieldData} />
); </div>
})} );
</div> })}
); </div>
);
}
} }
FieldSetRow.propTypes = propTypes; FieldSetRow.propTypes = propTypes;

View File

@ -5,6 +5,7 @@ import { slugify } from '../../modules/utils';
const propTypes = { const propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
choices: PropTypes.array,
label: PropTypes.string, label: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
@ -20,6 +21,7 @@ export default class SelectField extends React.Component {
onChange(opt) { onChange(opt) {
this.props.onChange(this.props.name, opt.target.value); this.props.onChange(this.props.name, opt.target.value);
} }
render() { render() {
return ( return (
<FormGroup controlId={`formControlsSelect-${slugify(this.props.label)}`}> <FormGroup controlId={`formControlsSelect-${slugify(this.props.label)}`}>
@ -32,8 +34,7 @@ export default class SelectField extends React.Component {
placeholder="select" placeholder="select"
onChange={this.onChange.bind(this)} onChange={this.onChange.bind(this)}
> >
<option value="select">select</option> {this.props.choices.map((c) => <option key={c[0]} value={c[0]}>{c[1]}</option>)}
<option value="other">...</option>
</FormControl> </FormControl>
</FormGroup> </FormGroup>
); );

View File

@ -4,24 +4,39 @@ import { addToArr, removeFromArr, alterInArr } from '../../../utils/reducerUtils
export const exploreReducer = function (state, action) { export const exploreReducer = function (state, action) {
const actionHandlers = { const actionHandlers = {
[actions.SET_TIME_COLUMN_OPTS]() { [actions.SET_DATASOURCE]() {
return Object.assign({}, state, { timeColumnOpts: action.timeColumnOpts }); return Object.assign({}, state, { datasourceId: action.datasourceId });
}, },
[actions.SET_TIME_GRAIN_OPTS]() {
return Object.assign({}, state, { timeGrainOpts: action.timeGrainOpts }); [actions.FETCH_STARTED]() {
return Object.assign({}, state, { isDatasourceMetaLoading: true });
}, },
[actions.SET_GROUPBY_COLUMN_OPTS]() {
return Object.assign({}, state, { groupByColumnOpts: action.groupByColumnOpts }); [actions.FETCH_SUCCEEDED]() {
return Object.assign({}, state, { isDatasourceMetaLoading: false });
}, },
[actions.SET_METRICS_OPTS]() {
return Object.assign({}, state, { metricsOpts: action.metricsOpts }); [actions.FETCH_FAILED]() {
// todo(alanna) handle failure/error state
return Object.assign({}, state, { isDatasourceMetaLoading: false });
}, },
[actions.SET_COLUMN_OPTS]() {
return Object.assign({}, state, { columnOpts: action.columnOpts }); [actions.SET_FIELD_OPTIONS]() {
const newState = Object.assign({}, state);
const optionsByFieldName = action.options;
const fieldNames = Object.keys(optionsByFieldName);
fieldNames.forEach((fieldName) => {
newState.fields[fieldName].choices = optionsByFieldName[fieldName];
});
return Object.assign({}, state, newState);
}, },
[actions.SET_ORDERING_OPTS]() {
return Object.assign({}, state, { orderingOpts: action.orderingOpts }); [actions.TOGGLE_SEARCHBOX]() {
return Object.assign({}, state, { searchBox: action.searchBox });
}, },
[actions.SET_FILTER_COLUMN_OPTS]() { [actions.SET_FILTER_COLUMN_OPTS]() {
return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts }); return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts });
}, },

View File

@ -10,7 +10,6 @@ export const fieldTypes = [
'TextAreaFeild', 'TextAreaFeild',
'TextField', 'TextField',
]; ];
const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format'; const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format';
// input choices & options // input choices & options
@ -708,13 +707,12 @@ export const visTypes = {
}, },
}; };
// todo: complete the choices and default keys from forms.py
export const fields = { export const fields = {
datasource: { datasource: {
type: 'SelectField', type: 'SelectField',
label: 'Datasource', label: 'Datasource',
default: '', default: null,
choices: [['datasource', 'datasource']], choices: [],
description: '', description: '',
}, },
@ -729,23 +727,23 @@ export const fields = {
metrics: { metrics: {
type: 'SelectMultipleSortableField', type: 'SelectMultipleSortableField',
label: 'Metrics', label: 'Metrics',
choices: [[1, 1]], choices: [],
default: ['todo'], default: null,
description: 'One or many metrics to display', description: 'One or many metrics to display',
}, },
order_by_cols: { order_by_cols: {
type: 'SelectMultipleSortableField', type: 'SelectMultipleSortableField',
label: 'Ordering', label: 'Ordering',
choices: 'todo: order_by_choices', choices: [],
description: 'One or many metrics to display', description: 'One or many metrics to display',
}, },
metric: { metric: {
type: 'SelectField', type: 'SelectField',
label: 'Metric', label: 'Metric',
choices: 'todo: ', choices: [],
default: 'todo: ', default: null,
description: 'Choose the metric', description: 'Choose the metric',
}, },
@ -825,7 +823,7 @@ export const fields = {
type: 'SelectField', type: 'SelectField',
label: 'YScale Interval', label: 'YScale Interval',
choices: formatSelectOptionsForRange(1, 50), choices: formatSelectOptionsForRange(1, 50),
default: '1', default: null,
description: 'Number of steps to take between ticks when ' + description: 'Number of steps to take between ticks when ' +
'displaying the Y scale', 'displaying the Y scale',
}, },
@ -834,7 +832,7 @@ export const fields = {
type: 'CheckboxField', type: 'CheckboxField',
label: 'Stacked Bars', label: 'Stacked Bars',
default: false, default: false,
description: '', description: null,
}, },
show_markers: { show_markers: {
@ -882,7 +880,7 @@ export const fields = {
type: 'SelectField', type: 'SelectField',
label: 'Color Metric', label: 'Color Metric',
choices: [], choices: [],
default: '', default: null,
description: 'A metric to use for color', description: 'A metric to use for color',
}, },
@ -910,30 +908,30 @@ export const fields = {
columns: { columns: {
type: 'SelectMultipleSortableField', type: 'SelectMultipleSortableField',
label: 'Columns', label: 'Columns',
choices: [[1, 1]], choices: [],
description: 'One or many fields to pivot as columns', description: 'One or many fields to pivot as columns',
}, },
all_columns: { all_columns: {
type: 'SelectMultipleSortableField', type: 'SelectMultipleSortableField',
label: 'Columns', label: 'Columns',
choices: [['all_columns', 'all_columns']], choices: [],
description: 'Columns to display', description: 'Columns to display',
}, },
all_columns_x: { all_columns_x: {
type: 'SelectField', type: 'SelectField',
label: 'X', label: 'X',
choices: [['all_columns_x', 'all_columns_x']], choices: [],
default: '', default: null,
description: 'Columns to display', description: 'Columns to display',
}, },
all_columns_y: { all_columns_y: {
type: 'SelectField', type: 'SelectField',
label: 'Y', label: 'Y',
choices: [['all_columns_y', 'all_columns_y']], choices: [],
default: '', default: null,
description: 'Columns to display', description: 'Columns to display',
}, },
@ -944,7 +942,7 @@ export const fields = {
['', 'default'], ['', 'default'],
['now', 'now'], ['now', 'now'],
], ],
default: '', default: null,
description: 'Defines the origin where time buckets start, ' + description: 'Defines the origin where time buckets start, ' +
'accepts natural dates as in `now`, `sunday` or `1970-01-01`', 'accepts natural dates as in `now`, `sunday` or `1970-01-01`',
}, },
@ -971,6 +969,10 @@ export const fields = {
'6 hour', '6 hour',
'1 day', '1 day',
'7 days', '7 days',
'week',
'week_starting_sunday',
'week_ending_saturday',
'month',
]), ]),
description: 'The time granularity for the visualization. Note that you ' + description: 'The time granularity for the visualization. Note that you ' +
'can type and use simple natural language as in `10 seconds`, ' + 'can type and use simple natural language as in `10 seconds`, ' +
@ -1024,8 +1026,8 @@ export const fields = {
granularity_sqla: { granularity_sqla: {
type: 'SelectField', type: 'SelectField',
label: 'Time Column', label: 'Time Column',
default: 'granularity_sqla', default: null,
choices: [['granularity_sqla', 'granularity_sqla']], choices: [],
description: 'The time column for the visualization. Note that you ' + description: 'The time column for the visualization. Note that you ' +
'can define arbitrary expression that return a DATETIME ' + 'can define arbitrary expression that return a DATETIME ' +
'column in the table or. Also note that the ' + 'column in the table or. Also note that the ' +
@ -1035,7 +1037,7 @@ export const fields = {
time_grain: { time_grain: {
label: 'Time Grain', label: 'Time Grain',
choices: ['grains-choices'], choices: [],
default: 'Time Column', default: 'Time Column',
description: 'The time granularity for the visualization. This ' + description: 'The time granularity for the visualization. This ' +
'applies a date transformation to alter ' + 'applies a date transformation to alter ' +
@ -1047,7 +1049,7 @@ export const fields = {
resample_rule: { resample_rule: {
type: 'FreeFormSelectField', type: 'FreeFormSelectField',
label: 'Resample Rule', label: 'Resample Rule',
default: '', default: null,
choices: formatSelectOptions(['', '1T', '1H', '1D', '7D', '1M', '1AS']), choices: formatSelectOptions(['', '1T', '1H', '1D', '7D', '1M', '1AS']),
description: 'Pandas resample rule', description: 'Pandas resample rule',
}, },
@ -1055,7 +1057,7 @@ export const fields = {
resample_how: { resample_how: {
type: 'SelectField', type: 'SelectField',
label: 'Resample How', label: 'Resample How',
default: '', default: null,
choices: formatSelectOptions(['', 'mean', 'sum', 'median']), choices: formatSelectOptions(['', 'mean', 'sum', 'median']),
description: 'Pandas resample how', description: 'Pandas resample how',
}, },
@ -1063,7 +1065,7 @@ export const fields = {
resample_fillmethod: { resample_fillmethod: {
type: 'SelectField', type: 'SelectField',
label: 'Resample Fill Method', label: 'Resample Fill Method',
default: '', default: null,
choices: formatSelectOptions(['', 'ffill', 'bfill']), choices: formatSelectOptions(['', 'ffill', 'bfill']),
description: 'Pandas resample fill method', description: 'Pandas resample fill method',
}, },
@ -1129,7 +1131,7 @@ export const fields = {
number_format: { number_format: {
type: 'SelectField', type: 'SelectField',
label: 'Number format', label: 'Number format',
default: '.3s', default: D3_TIME_FORMAT_OPTIONS[0],
choices: D3_TIME_FORMAT_OPTIONS, choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS, description: D3_FORMAT_DOCS,
}, },
@ -1137,7 +1139,7 @@ export const fields = {
row_limit: { row_limit: {
type: 'SelectField', type: 'SelectField',
label: 'Row limit', label: 'Row limit',
default: '', default: null,
choices: formatSelectOptions(ROW_LIMIT_OPTIONS), choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
}, },
@ -1152,8 +1154,8 @@ export const fields = {
timeseries_limit_metric: { timeseries_limit_metric: {
type: 'SelectField', type: 'SelectField',
label: 'Sort By', label: 'Sort By',
choices: [['', ''], ['timeseries_limit_metric', 'timeseries_limit_metric']], choices: [],
default: '', default: null,
description: 'Metric used to define the top series', description: 'Metric used to define the top series',
}, },
@ -1169,7 +1171,7 @@ export const fields = {
rolling_periods: { rolling_periods: {
type: 'IntegerField', type: 'IntegerField',
label: 'Periods', label: 'Periods',
validators: ['todo: [validators.optional()]'], validators: [],
description: 'Defines the size of the rolling window function, ' + description: 'Defines the size of the rolling window function, ' +
'relative to the time granularity selected', 'relative to the time granularity selected',
}, },
@ -1177,8 +1179,8 @@ export const fields = {
series: { series: {
type: 'SelectField', type: 'SelectField',
label: 'Series', label: 'Series',
choices: formatSelectOptions(['', 'series']), choices: [],
default: '', default: null,
description: 'Defines the grouping of entities. ' + description: 'Defines the grouping of entities. ' +
'Each series is shown as a specific color on the chart and ' + 'Each series is shown as a specific color on the chart and ' +
'has a legend toggle', 'has a legend toggle',
@ -1187,32 +1189,32 @@ export const fields = {
entity: { entity: {
type: 'SelectField', type: 'SelectField',
label: 'Entity', label: 'Entity',
choices: formatSelectOptions(['', 'entity']), choices: [],
default: '', default: null,
description: 'This define the element to be plotted on the chart', description: 'This define the element to be plotted on the chart',
}, },
x: { x: {
type: 'SelectField', type: 'SelectField',
label: 'X Axis', label: 'X Axis',
choices: formatSelectOptions(['', 'metrics assigned to x']), choices: [],
default: '', default: null,
description: 'Metric assigned to the [X] axis', description: 'Metric assigned to the [X] axis',
}, },
y: { y: {
type: 'SelectField', type: 'SelectField',
label: 'Y Axis', label: 'Y Axis',
choices: formatSelectOptions(['', 'metrics assigned to x']), choices: [],
default: '', default: null,
description: 'Metric assigned to the [Y] axis', description: 'Metric assigned to the [Y] axis',
}, },
size: { size: {
type: 'SelectField', type: 'SelectField',
label: 'Bubble Size', label: 'Bubble Size',
default: '', default: null,
choices: formatSelectOptions(['', 'bubble-size']), choices: [],
}, },
url: { url: {
@ -1271,7 +1273,7 @@ export const fields = {
type: 'SelectField', type: 'SelectField',
label: 'Table Timestamp Format', label: 'Table Timestamp Format',
default: 'smart_date', default: 'smart_date',
choices: formatSelectOptions(TIME_STAMP_OPTIONS), choices: TIME_STAMP_OPTIONS,
description: 'Timestamp Format', description: 'Timestamp Format',
}, },
@ -1287,7 +1289,7 @@ export const fields = {
type: 'SelectCustomMultiField', type: 'SelectCustomMultiField',
label: 'X axis format', label: 'X axis format',
default: 'smart_date', default: 'smart_date',
choices: formatSelectOptions(TIME_STAMP_OPTIONS), choices: TIME_STAMP_OPTIONS,
description: D3_FORMAT_DOCS, description: D3_FORMAT_DOCS,
}, },
@ -1477,7 +1479,7 @@ export const fields = {
type: 'IntegerField', type: 'IntegerField',
label: 'Period Ratio', label: 'Period Ratio',
default: '', default: '',
validators: 'todo: [validators.optional()]', validators: [],
description: '[integer] Number of period to compare against, ' + description: '[integer] Number of period to compare against, ' +
'this is relative to the granularity selected', 'this is relative to the granularity selected',
}, },
@ -1494,7 +1496,7 @@ export const fields = {
time_compare: { time_compare: {
type: 'TextField', type: 'TextField',
label: 'Time Shift', label: 'Time Shift',
default: '', default: null,
description: 'Overlay a timeseries from a ' + description: 'Overlay a timeseries from a ' +
'relative time period. Expects relative time delta ' + 'relative time period. Expects relative time delta ' +
'in natural language (example: 24 hours, 7 days, ' + 'in natural language (example: 24 hours, 7 days, ' +
@ -1510,7 +1512,7 @@ export const fields = {
mapbox_label: { mapbox_label: {
type: 'SelectMultipleSortableField', type: 'SelectMultipleSortableField',
label: 'label', label: 'label',
choices: "todo: formatSelectOptions(['count'] + datasource.column_names)", choices: [],
description: '`count` is COUNT(*) if a group by is used. ' + description: '`count` is COUNT(*) if a group by is used. ' +
'Numerical columns will be aggregated with the aggregator. ' + 'Numerical columns will be aggregated with the aggregator. ' +
'Non-numerical columns will be used to label points. ' + 'Non-numerical columns will be used to label points. ' +
@ -1555,8 +1557,8 @@ export const fields = {
point_radius: { point_radius: {
type: 'SelectField', type: 'SelectField',
label: 'Point Radius', label: 'Point Radius',
default: 'Auto', default: null,
choices: "todo: formatSelectOptions(['Auto'] + datasource.column_names)", choices: [],
description: 'The radius of individual points (ones that are not in a cluster). ' + description: 'The radius of individual points (ones that are not in a cluster). ' +
'Either a numerical column or `Auto`, which scales the point based ' + 'Either a numerical column or `Auto`, which scales the point based ' +
'on the largest cluster', 'on the largest cluster',
@ -1582,7 +1584,7 @@ export const fields = {
type: 'IntegerField', type: 'IntegerField',
label: 'Zoom', label: 'Zoom',
default: 11, default: 11,
validators: 'todo: [validators.optional()]', validators: [],
description: 'Zoom level of the map', description: 'Zoom level of the map',
places: 8, places: 8,
}, },
@ -1646,25 +1648,10 @@ export const defaultViz = {
}; };
export const initialState = { export const initialState = {
isDatasourceMetaLoading: false,
datasources: null, datasources: null,
datasource_id: null, datasource_id: null,
datasource_type: null, datasource_type: null,
timeColumnOpts: [], fields,
timeGrainOpts: [],
groupByColumnOpts: [],
metricsOpts: [],
columnOpts: [],
orderingOpts: [],
filterColumnOpts: [],
viz: defaultViz, viz: defaultViz,
}; };
export const defaultOpts = {
timeColumnOpts: [],
timeGrainOpts: [],
groupByColumnOpts: [],
metricsOpts: [],
filterColumnOpts: [],
columnOpts: [],
orderingOpts: [],
};

View File

@ -13,7 +13,7 @@ const defaultProps = {
datasource_id: 1, datasource_id: 1,
datasource_type: 'type', datasource_type: 'type',
actions: { actions: {
setFormOpts: () => { fetchFieldOptions: () => {
// noop // noop
}, },
}, },

View File

@ -8,6 +8,7 @@ import { shallow } from 'enzyme';
import SelectField from '../../../../javascripts/explorev2/components/SelectField'; import SelectField from '../../../../javascripts/explorev2/components/SelectField';
const defaultProps = { const defaultProps = {
choices: [[10, 10], [20, 20]],
name: 'row_limit', name: 'row_limit',
label: 'Row Limit', label: 'Row Limit',
onChange: sinon.spy(), onChange: sinon.spy(),

View File

@ -2106,6 +2106,9 @@ class Caravel(BaseCaravelView):
.first() .first()
) )
datasources = db.session.query(datasource_class).all()
datasources = sorted(datasources, key=lambda ds: ds.full_name)
# Check if datasource exists # Check if datasource exists
if not datasource: if not datasource:
return json_error_response(DATASOURCE_MISSING_ERR) return json_error_response(DATASOURCE_MISSING_ERR)
@ -2113,23 +2116,38 @@ class Caravel(BaseCaravelView):
if not self.datasource_access(datasource): if not self.datasource_access(datasource):
return json_error_response(DATASOURCE_ACCESS_ERR) return json_error_response(DATASOURCE_ACCESS_ERR)
gb_cols = [(col, col) for col in datasource.groupby_column_names]
order_by_choices = [] order_by_choices = []
for s in sorted(datasource.num_cols): for s in sorted(datasource.num_cols):
order_by_choices.append(s + ' [asc]') order_by_choices.append((json.dumps([s, True]), s + ' [asc]'))
order_by_choices.append(s + ' [desc]') order_by_choices.append((json.dumps([s, False]), s + ' [desc]'))
column_opts = {
"groupby_cols": datasource.groupby_column_names, field_options = {
"metrics": datasource.metrics_combo, 'datasource': [(d.id, d.full_name) for d in datasources],
"filter_cols": datasource.filterable_column_names, 'metrics': datasource.metrics_combo,
"columns": datasource.column_names, 'order_by_cols': order_by_choices,
"ordering_cols": order_by_choices 'metric': datasource.metrics_combo,
'secondary_metric': datasource.metrics_combo,
'groupby': gb_cols,
'columns': gb_cols,
'all_columns': datasource.column_names,
'all_columns_x': datasource.column_names,
'all_columns_y': datasource.column_names,
'granularity_sqla': datasource.dttm_cols,
'timeseries_limit_metric': [('', '')] + datasource.metrics_combo,
'series': gb_cols,
'entity': gb_cols,
'x': datasource.metrics_combo,
'y': datasource.metrics_combo,
'size': datasource.metrics_combo,
'mapbox_label': datasource.column_names,
'point_radius': ["Auto"] + datasource.column_names,
} }
form_data = dict(
column_opts.items() + datasource.time_column_grains.items()
)
return Response( return Response(
json.dumps(form_data), mimetype="application/json") json.dumps({'field_options': field_options}),
mimetype="application/json"
)
@has_access @has_access
@expose("/queries/<last_updated_ms>") @expose("/queries/<last_updated_ms>")

View File

@ -317,6 +317,7 @@ class BaseViz(object):
if not payload: if not payload:
is_cached = False is_cached = False
cache_timeout = self.cache_timeout cache_timeout = self.cache_timeout
payload = { payload = {
'cache_timeout': cache_timeout, 'cache_timeout': cache_timeout,
'cache_key': cache_key, 'cache_key': cache_key,

View File

@ -387,6 +387,11 @@ class CoreTests(CaravelTestCase):
elif backend == 'postgresql': elif backend == 'postgresql':
self.assertEqual(len(data.get('indexes')), 5) self.assertEqual(len(data.get('indexes')), 5)
def test_fetch_datasource_metadata(self):
self.login(username='admin')
url = '/caravel/fetch_datasource_metadata?datasource_type=table&datasource_id=1';
resp = json.loads(self.get_resp(url))
self.assertEqual(len(resp['field_options']), 19)
if __name__ == '__main__': if __name__ == '__main__':