mirror of https://github.com/apache/superset.git
[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:
parent
4530047c76
commit
51c0470f0b
|
@ -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());
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 });
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: [],
|
|
||||||
};
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ const defaultProps = {
|
||||||
datasource_id: 1,
|
datasource_id: 1,
|
||||||
datasource_type: 'type',
|
datasource_type: 'type',
|
||||||
actions: {
|
actions: {
|
||||||
setFormOpts: () => {
|
fetchFieldOptions: () => {
|
||||||
// noop
|
// noop
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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>")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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__':
|
||||||
|
|
Loading…
Reference in New Issue