diff --git a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx index 3582229dd80..3efe2bba29d 100644 --- a/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx +++ b/superset/assets/javascripts/explore/components/EmbedCodeButton.jsx @@ -35,7 +35,8 @@ export default class EmbedCodeButton extends React.Component { ` width="${this.state.width}"\n` + ` height="${this.state.height}"\n` + ' seamless\n' + - ' frameBorder="0" scrolling="no"\n' + + ' frameBorder="0"\n' + + ' scrolling="no"\n' + ` src="${srcLink}"\n` + '>\n' + '' diff --git a/superset/assets/javascripts/explorev2/actions/exploreActions.js b/superset/assets/javascripts/explorev2/actions/exploreActions.js index b5e89a7840b..5373ef5d3b8 100644 --- a/superset/assets/javascripts/explorev2/actions/exploreActions.js +++ b/superset/assets/javascripts/explorev2/actions/exploreActions.js @@ -2,16 +2,16 @@ const $ = window.$ = require('jquery'); const FAVESTAR_BASE_URL = '/superset/favstar/slice'; -export const SET_FIELD_OPTIONS = 'SET_FIELD_OPTIONS'; -export function setFieldOptions(options) { - return { type: SET_FIELD_OPTIONS, options }; -} - export const SET_DATASOURCE_TYPE = 'SET_DATASOURCE_TYPE'; export function setDatasourceType(datasourceType) { return { type: SET_DATASOURCE_TYPE, datasourceType }; } +export const SET_DATASOURCE = 'SET_DATASOURCE'; +export function setDatasource(datasource) { + return { type: SET_DATASOURCE, datasource }; +} + export const FETCH_STARTED = 'FETCH_STARTED'; export function fetchStarted() { return { type: FETCH_STARTED }; @@ -27,7 +27,7 @@ export function fetchFailed(error) { return { type: FETCH_FAILED, error }; } -export function fetchFieldOptions(datasourceId, datasourceType) { +export function fetchDatasourceMetadata(datasourceId, datasourceType) { return function (dispatch) { dispatch(fetchStarted()); @@ -38,7 +38,7 @@ export function fetchFieldOptions(datasourceId, datasourceType) { type: 'GET', url, success: (data) => { - dispatch(setFieldOptions(data.field_options)); + dispatch(setDatasource(data)); dispatch(fetchSucceeded()); }, error(error) { diff --git a/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx b/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx index 02adb78a3c3..c4f11ad435e 100644 --- a/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx +++ b/superset/assets/javascripts/explorev2/components/ControlPanelsContainer.jsx @@ -7,6 +7,7 @@ import { Panel, Alert } from 'react-bootstrap'; import visTypes, { sectionsToRender, commonControlPanelSections } from '../stores/visTypes'; import ControlPanelSection from './ControlPanelSection'; import FieldSetRow from './FieldSetRow'; +import FieldSet from './FieldSet'; import Filters from './Filters'; const propTypes = { @@ -17,48 +18,65 @@ const propTypes = { form_data: PropTypes.object.isRequired, y_axis_zero: PropTypes.any, alert: PropTypes.string, + exploreState: PropTypes.object.isRequired, }; class ControlPanelsContainer extends React.Component { + constructor(props) { + super(props); + this.fieldOverrides = this.fieldOverrides.bind(this); + this.getFieldData = this.getFieldData.bind(this); + this.removeAlert = this.removeAlert.bind(this); + this.onChange = this.onChange.bind(this); + } componentWillMount() { const datasource_id = this.props.form_data.datasource; const datasource_type = this.props.datasource_type; if (datasource_id) { - this.props.actions.fetchFieldOptions(datasource_id, datasource_type); + this.props.actions.fetchDatasourceMetadata(datasource_id, datasource_type); } } - componentWillReceiveProps(nextProps) { if (nextProps.form_data.datasource !== this.props.form_data.datasource) { if (nextProps.form_data.datasource) { - this.props.actions.fetchFieldOptions( + this.props.actions.fetchDatasourceMetadata( nextProps.form_data.datasource, nextProps.datasource_type); } } } - - onChange(name, value, label) { - this.props.actions.setFieldValue(this.props.datasource_type, name, value, label); + onChange(name, value) { + this.props.actions.setFieldValue(this.props.datasource_type, name, value); + } + getFieldData(fs) { + const fieldOverrides = this.fieldOverrides(); + if (!this.props.fields) { + return null; + } + let fieldData = this.props.fields[fs] || {}; + if (fieldOverrides.hasOwnProperty(fs)) { + const overrideData = fieldOverrides[fs]; + fieldData = Object.assign({}, fieldData, overrideData); + } + if (fieldData.mapStateToProps) { + Object.assign(fieldData, fieldData.mapStateToProps(this.props.exploreState)); + } + return fieldData; } - sectionsToRender() { return sectionsToRender(this.props.form_data.viz_type, this.props.datasource_type); } - filterSectionsToRender() { const filterSections = this.props.datasource_type === 'table' ? [commonControlPanelSections.filters[0]] : commonControlPanelSections.filters; return filterSections; } - fieldOverrides() { const viz = visTypes[this.props.form_data.viz_type]; - return viz.fieldOverrides; + return viz.fieldOverrides || {}; } removeAlert() { this.props.actions.removeControlPanelAlert(); } - render() { return (
@@ -68,7 +86,7 @@ class ControlPanelsContainer extends React.Component { {this.props.alert} @@ -81,12 +99,16 @@ class ControlPanelsContainer extends React.Component { > {section.fieldSetRows.map((fieldSets, i) => ( ( +
+ ))} /> ))} @@ -119,6 +141,7 @@ function mapStateToProps(state) { alert: state.controlPanelAlert, isDatasourceMetaLoading: state.isDatasourceMetaLoading, fields: state.fields, + exploreState: state, }; } diff --git a/superset/assets/javascripts/explorev2/components/FieldSetRow.jsx b/superset/assets/javascripts/explorev2/components/FieldSetRow.jsx index dbdaca72039..625c8761e3f 100644 --- a/superset/assets/javascripts/explorev2/components/FieldSetRow.jsx +++ b/superset/assets/javascripts/explorev2/components/FieldSetRow.jsx @@ -1,52 +1,23 @@ import React, { PropTypes } from 'react'; -import FieldSet from './FieldSet'; const NUM_COLUMNS = 12; const propTypes = { - fields: PropTypes.object.isRequired, - fieldSets: PropTypes.array.isRequired, - fieldOverrides: PropTypes.object, - onChange: PropTypes.func, - form_data: PropTypes.object.isRequired, + fields: PropTypes.arrayOf(PropTypes.object).isRequired, }; -const defaultProps = { - fieldOverrides: {}, - onChange: () => {}, -}; - -export default class FieldSetRow extends React.Component { - getFieldData(fs) { - const { fields, fieldOverrides } = this.props; - let fieldData = fields[fs]; - if (fieldOverrides.hasOwnProperty(fs)) { - const overrideData = fieldOverrides[fs]; - fieldData = Object.assign({}, fieldData, overrideData); - } - return fieldData; - } - render() { - const colSize = NUM_COLUMNS / this.props.fieldSets.length; - return ( -
- {this.props.fieldSets.map((fs) => { - const fieldData = this.getFieldData(fs); - return ( -
-
-
- ); - })} -
- ); - } +function FieldSetRow(props) { + const colSize = NUM_COLUMNS / props.fields.length; + return ( +
+ {props.fields.map((field, i) => ( +
+ {field} +
+ ))} +
+ ); } FieldSetRow.propTypes = propTypes; -FieldSetRow.defaultProps = defaultProps; +export default FieldSetRow; diff --git a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js index 09a057e4a0a..460544ab354 100644 --- a/superset/assets/javascripts/explorev2/reducers/exploreReducer.js +++ b/superset/assets/javascripts/explorev2/reducers/exploreReducer.js @@ -39,25 +39,9 @@ export const exploreReducer = function (state, action) { return Object.assign({}, state, { saveModalAlert: `fetching dashboards failed for ${action.userId}` }); }, - - [actions.SET_FIELD_OPTIONS]() { - const newState = Object.assign({}, state); - const optionsByFieldName = action.options; - const fieldNames = Object.keys(optionsByFieldName); - - fieldNames.forEach((fieldName) => { - if (fieldName === 'filterable_cols') { - newState.filterColumnOpts = optionsByFieldName[fieldName]; - } else { - newState.fields[fieldName].choices = optionsByFieldName[fieldName]; - if (fieldName === 'metric' && state.viz.form_data.viz_type === 'dual_line') { - newState.fields.metric_2.choices = optionsByFieldName[fieldName]; - } - } - }); - return Object.assign({}, state, newState); + [actions.SET_DATASOURCE]() { + return Object.assign({}, state, { datasource: action.datasource }); }, - [actions.SET_FILTER_COLUMN_OPTS]() { return Object.assign({}, state, { filterColumnOpts: action.filterColumnOpts }); }, diff --git a/superset/assets/javascripts/explorev2/stores/fields.js b/superset/assets/javascripts/explorev2/stores/fields.js index cbe195482c1..0866f090d62 100644 --- a/superset/assets/javascripts/explorev2/stores/fields.js +++ b/superset/assets/javascripts/explorev2/stores/fields.js @@ -31,13 +31,15 @@ export const fields = { label: 'Datasource', clearable: false, default: null, - choices: [], + mapStateToProps: (state) => ({ + choices: state.datasources || [], + }), description: '', }, viz_type: { type: 'SelectField', - label: 'Viz', + label: 'Visualization Type', clearable: false, default: 'table', choices: formatSelectOptions(Object.keys(visTypes)), @@ -47,9 +49,10 @@ export const fields = { metrics: { type: 'SelectField', multi: true, - freeForm: false, label: 'Metrics', - choices: [], + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.metrics_combo : [], + }), default: [], description: 'One or many metrics to display', }, @@ -57,19 +60,22 @@ export const fields = { order_by_cols: { type: 'SelectField', multi: true, - freeForm: false, label: 'Ordering', - choices: [], default: [], description: 'One or many metrics to display', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.order_by_choices : [], + }), }, metric: { type: 'SelectField', label: 'Metric', - choices: [], default: null, description: 'Choose the metric', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.metrics_combo : [], + }), }, metric_2: { @@ -219,9 +225,11 @@ export const fields = { secondary_metric: { type: 'SelectField', label: 'Color Metric', - choices: [], default: null, description: 'A metric to use for color', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.metrics_combo : [], + }), }, country_fieldtype: { @@ -241,17 +249,17 @@ export const fields = { groupby: { type: 'SelectField', multi: true, - freeForm: false, label: 'Group by', - choices: [], default: [], description: 'One or many fields to group by', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.gb_cols : [], + }), }, columns: { type: 'SelectField', multi: true, - freeForm: false, label: 'Columns', choices: [], default: [], @@ -261,32 +269,36 @@ export const fields = { all_columns: { type: 'SelectField', multi: true, - freeForm: false, label: 'Columns', - choices: [], default: [], description: 'Columns to display', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), }, all_columns_x: { type: 'SelectField', label: 'X', - choices: [], default: null, description: 'Columns to display', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), }, all_columns_y: { type: 'SelectField', label: 'Y', - choices: [], default: null, description: 'Columns to display', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), }, druid_time_origin: { type: 'SelectField', - multi: false, freeForm: true, label: 'Origin', choices: [ @@ -300,7 +312,6 @@ export const fields = { bottom_margin: { type: 'SelectField', - multi: false, freeForm: true, label: 'Bottom Margin', choices: formatSelectOptions(['auto', 50, 75, 100, 125, 150, 200]), @@ -310,7 +321,6 @@ export const fields = { granularity: { type: 'SelectField', - multi: false, freeForm: true, label: 'Time Granularity', default: 'one day', @@ -353,7 +363,6 @@ export const fields = { link_length: { type: 'SelectField', - multi: false, freeForm: true, label: 'Link Length', default: '200', @@ -363,7 +372,6 @@ export const fields = { charge: { type: 'SelectField', - multi: false, freeForm: true, label: 'Charge', default: '-500', @@ -386,29 +394,32 @@ export const fields = { type: 'SelectField', label: 'Time Column', default: null, - choices: [], description: 'The time column for the visualization. Note that you ' + 'can define arbitrary expression that return a DATETIME ' + 'column in the table or. Also note that the ' + 'filter below is applied against this column or ' + 'expression', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), }, time_grain_sqla: { type: 'SelectField', label: 'Time Grain', - choices: [], default: 'Time Column', description: 'The time granularity for the visualization. This ' + 'applies a date transformation to alter ' + 'your time column and defines a new time granularity. ' + 'The options here are defined on a per database ' + 'engine basis in the Superset source code.', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.time_grain_sqla : [], + }), }, resample_rule: { type: 'SelectField', - multi: false, freeForm: true, label: 'Resample Rule', default: null, @@ -418,7 +429,6 @@ export const fields = { resample_how: { type: 'SelectField', - multi: false, freeForm: true, label: 'Resample How', default: null, @@ -428,7 +438,6 @@ export const fields = { resample_fillmethod: { type: 'SelectField', - multi: false, freeForm: true, label: 'Resample Fill Method', default: null, @@ -438,7 +447,6 @@ export const fields = { since: { type: 'SelectField', - multi: false, freeForm: true, label: 'Since', default: '7 days ago', @@ -458,7 +466,6 @@ export const fields = { until: { type: 'SelectField', - multi: false, freeForm: true, label: 'Until', default: 'now', @@ -474,7 +481,6 @@ export const fields = { max_bubble_size: { type: 'SelectField', - multi: false, freeForm: true, label: 'Max Bubble Size', default: '25', @@ -483,7 +489,6 @@ export const fields = { whisker_options: { type: 'SelectField', - multi: false, freeForm: true, label: 'Whisker/outlier options', default: 'Tukey', @@ -497,7 +502,7 @@ export const fields = { }, treemap_ratio: { - type: 'IntegerField', + type: 'TextField', label: 'Ratio', default: 0.5 * (1 + Math.sqrt(5)), // d3 default, golden ratio description: 'Target aspect ratio for treemap tiles.', @@ -505,7 +510,6 @@ export const fields = { number_format: { type: 'SelectField', - multi: false, freeForm: true, label: 'Number format', default: D3_TIME_FORMAT_OPTIONS[0], @@ -515,7 +519,6 @@ export const fields = { row_limit: { type: 'SelectField', - multi: false, freeForm: true, label: 'Row limit', default: null, @@ -524,7 +527,6 @@ export const fields = { limit: { type: 'SelectField', - multi: false, freeForm: true, label: 'Series limit', choices: formatSelectOptions(SERIES_LIMITS), @@ -535,9 +537,11 @@ export const fields = { timeseries_limit_metric: { type: 'SelectField', label: 'Sort By', - choices: [], default: null, description: 'Metric used to define the top series', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.metrics_combo : [], + }), }, rolling_type: { @@ -550,7 +554,7 @@ export const fields = { }, rolling_periods: { - type: 'IntegerField', + type: 'TextField', label: 'Periods', validators: [], description: 'Defines the size of the rolling window function, ' + @@ -560,42 +564,52 @@ export const fields = { series: { type: 'SelectField', label: 'Series', - choices: [], default: null, description: 'Defines the grouping of entities. ' + 'Each series is shown as a specific color on the chart and ' + 'has a legend toggle', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.gb_cols : [], + }), }, entity: { type: 'SelectField', label: 'Entity', - choices: [], default: null, description: 'This define the element to be plotted on the chart', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.gb_cols : [], + }), }, x: { type: 'SelectField', label: 'X Axis', - choices: [], default: null, description: 'Metric assigned to the [X] axis', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.gb_cols : [], + }), }, y: { type: 'SelectField', label: 'Y Axis', - choices: [], default: null, description: 'Metric assigned to the [Y] axis', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.metrics_combo : [], + }), }, size: { type: 'SelectField', label: 'Bubble Size', default: null, - choices: [], + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.metrics_combo : [], + }), }, url: { @@ -652,7 +666,6 @@ export const fields = { table_timestamp_format: { type: 'SelectField', - multi: false, freeForm: true, label: 'Table Timestamp Format', default: 'smart_date', @@ -662,7 +675,6 @@ export const fields = { series_height: { type: 'SelectField', - multi: false, freeForm: true, label: 'Series Height', default: '25', @@ -672,7 +684,6 @@ export const fields = { page_length: { type: 'SelectField', - multi: false, freeForm: true, label: 'Page Length', default: 0, @@ -682,7 +693,6 @@ export const fields = { x_axis_format: { type: 'SelectField', - multi: false, freeForm: true, label: 'X axis format', default: 'smart_date', @@ -692,7 +702,6 @@ export const fields = { y_axis_format: { type: 'SelectField', - multi: false, freeForm: true, label: 'Y axis format', default: '.3s', @@ -890,7 +899,7 @@ export const fields = { }, num_period_compare: { - type: 'IntegerField', + type: 'TextField', label: 'Period Ratio', default: '', validators: [], @@ -926,14 +935,15 @@ export const fields = { mapbox_label: { type: 'SelectField', multi: true, - freeForm: false, label: 'label', - choices: [], default: [], description: '`count` is COUNT(*) if a group by is used. ' + 'Numerical columns will be aggregated with the aggregator. ' + 'Non-numerical columns will be used to label points. ' + 'Leave empty to get a count of points in each cluster.', + mapStateToProps: (state) => ({ + choices: (state.datasource) ? state.datasource.all_cols : [], + }), }, mapbox_style: { @@ -953,7 +963,6 @@ export const fields = { clustering_radius: { type: 'SelectField', - multi: false, freeForm: true, label: 'Clustering Radius', default: '60', @@ -977,10 +986,12 @@ export const fields = { type: 'SelectField', label: 'Point Radius', default: null, - choices: [], description: 'The radius of individual points (ones that are not in a cluster). ' + 'Either a numerical column or `Auto`, which scales the point based ' + 'on the largest cluster', + mapStateToProps: (state) => ({ + choices: state.fields.point_radius.choices, + }), }, point_radius_unit: { @@ -992,7 +1003,7 @@ export const fields = { }, global_opacity: { - type: 'IntegerField', + type: 'TextField', label: 'Opacity', default: 1, description: 'Opacity of all clusters, points, and labels. ' + @@ -1000,7 +1011,7 @@ export const fields = { }, viewport_zoom: { - type: 'IntegerField', + type: 'TextField', label: 'Zoom', default: 11, validators: [], @@ -1009,7 +1020,7 @@ export const fields = { }, viewport_latitude: { - type: 'IntegerField', + type: 'TextField', label: 'Default latitude', default: 37.772123, description: 'Latitude of default viewport', @@ -1017,7 +1028,7 @@ export const fields = { }, viewport_longitude: { - type: 'IntegerField', + type: 'TextField', label: 'Default longitude', default: -122.405293, description: 'Longitude of default viewport', @@ -1033,7 +1044,6 @@ export const fields = { mapbox_color: { type: 'SelectField', - multi: false, freeForm: true, label: 'RGB Color', default: 'rgb(0, 122, 135)', diff --git a/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx b/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx index a171a9a1415..837269b9353 100644 --- a/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx @@ -31,13 +31,17 @@ describe('EmbedCodeButton', () => { width: '2000', srcLink: 'http://localhost/endpoint_url', }); - const embedHTML = ` - `; + const embedHTML = ( + '\n' + + '' + ); expect(wrapper.instance().generateEmbedHTML()).to.equal(embedHTML); }); }); diff --git a/superset/assets/spec/javascripts/explorev2/components/FieldSetRow_spec.js b/superset/assets/spec/javascripts/explorev2/components/FieldSetRow_spec.js index 4dcf91b366f..6db65c722cb 100644 --- a/superset/assets/spec/javascripts/explorev2/components/FieldSetRow_spec.js +++ b/superset/assets/spec/javascripts/explorev2/components/FieldSetRow_spec.js @@ -1,31 +1,18 @@ import React from 'react'; import { expect } from 'chai'; -import { describe, it, beforeEach } from 'mocha'; +import { describe, it } from 'mocha'; import { shallow } from 'enzyme'; -import { fields } from '../../../../javascripts/explorev2/stores/fields'; -import { defaultFormData } from '../../../../javascripts/explorev2/stores/store'; import FieldSetRow from '../../../../javascripts/explorev2/components/FieldSetRow'; -import FieldSet from '../../../../javascripts/explorev2/components/FieldSet'; - -const defaultProps = { - fields, - fieldSets: ['columns', 'metrics'], - form_data: defaultFormData(), -}; describe('FieldSetRow', () => { - let wrapper; - - beforeEach(() => { - wrapper = shallow(); - }); - - it('renders a single row element', () => { + it('renders a single row with one element', () => { + const wrapper = shallow(]} />); expect(wrapper.find('.row')).to.have.lengthOf(1); + expect(wrapper.find('.row').find('a')).to.have.lengthOf(1); }); - - it('renders a FieldSet for each item in fieldSets array', () => { - const length = defaultProps.fieldSets.length; - expect(wrapper.find(FieldSet)).to.have.lengthOf(length); + it('renders a single row with two elements', () => { + const wrapper = shallow(, ]} />); + expect(wrapper.find('.row')).to.have.lengthOf(1); + expect(wrapper.find('.row').find('a')).to.have.lengthOf(2); }); }); diff --git a/superset/models.py b/superset/models.py index f14cf6d5162..84701a17db2 100644 --- a/superset/models.py +++ b/superset/models.py @@ -671,6 +671,34 @@ class Queryable(object): else: return "/superset/explore/{obj.type}/{obj.id}/".format(obj=self) + @property + def data(self): + """data representation of the datasource sent to the frontend""" + gb_cols = [(col, col) for col in self.groupby_column_names] + all_cols = [(c, c) for c in self.column_names] + order_by_choices = [] + for s in sorted(self.column_names): + order_by_choices.append((json.dumps([s, True]), s + ' [asc]')) + order_by_choices.append((json.dumps([s, False]), s + ' [desc]')) + + d = { + 'id': self.id, + 'type': self.type, + 'name': self.name, + 'metrics_combo': self.metrics_combo, + 'order_by_choices': order_by_choices, + 'gb_cols': gb_cols, + 'all_cols': all_cols, + 'filterable_cols': self.filterable_column_names, + } + if (self.type == 'table'): + grains = self.database.grains() or [] + if grains: + grains = [(g.name, g.name) for g in grains] + d['granularity_sqla'] = [(c, c) for c in self.dttm_cols] + d['time_grain_sqla'] = grains + return d + class Database(Model, AuditMixinNullable): diff --git a/superset/views.py b/superset/views.py index 87bcad686c2..fb8d6faa628 100755 --- a/superset/views.py +++ b/superset/views.py @@ -2546,65 +2546,24 @@ class Superset(BaseSupersetView): @expose("/fetch_datasource_metadata") @log_this def fetch_datasource_metadata(self): - session = db.session datasource_type = request.args.get('datasource_type') datasource_class = SourceRegistry.sources[datasource_type] datasource = ( - session.query(datasource_class) + db.session.query(datasource_class) .filter_by(id=request.args.get('datasource_id')) .first() ) - datasources = db.session.query(datasource_class).all() - datasources = sorted(datasources, key=lambda ds: ds.full_name) - # Check if datasource exists if not datasource: return json_error_response(DATASOURCE_MISSING_ERR) + # Check permission for datasource if not self.datasource_access(datasource): return json_error_response(DATASOURCE_ACCESS_ERR) - gb_cols = [(col, col) for col in datasource.groupby_column_names] - all_cols = [(c, c) for c in datasource.column_names] - order_by_choices = [] - for s in sorted(datasource.column_names): - order_by_choices.append((json.dumps([s, True]), s + ' [asc]')) - order_by_choices.append((json.dumps([s, False]), s + ' [desc]')) - - field_options = { - 'datasource': [(d.id, d.full_name) for d in datasources], - 'metrics': datasource.metrics_combo, - 'order_by_cols': order_by_choices, - 'metric': datasource.metrics_combo, - 'secondary_metric': datasource.metrics_combo, - 'groupby': gb_cols, - 'columns': gb_cols, - 'all_columns': all_cols, - 'all_columns_x': all_cols, - 'all_columns_y': all_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': all_cols, - 'point_radius': [(c, c) for c in (["Auto"] + datasource.column_names)], - 'filterable_cols': datasource.filterable_column_names, - } - - if (datasource_type == 'table'): - grains = datasource.database.grains() - grain_choices = [] - if grains: - grain_choices = [(grain.name, grain.name) for grain in grains] - field_options['granularity_sqla'] = \ - [(c, c) for c in datasource.dttm_cols] - field_options['time_grain_sqla'] = grain_choices - return Response( - json.dumps({'field_options': field_options}), + json.dumps(datasource.data), mimetype="application/json" ) diff --git a/tests/core_tests.py b/tests/core_tests.py index 9c580c15ca4..44d97b88d90 100644 --- a/tests/core_tests.py +++ b/tests/core_tests.py @@ -486,10 +486,19 @@ class CoreTests(SupersetTestCase): def test_fetch_datasource_metadata(self): self.login(username='admin') - url = '/superset/fetch_datasource_metadata?datasource_type=table&' \ - 'datasource_id=1' - resp = json.loads(self.get_resp(url)) - self.assertEqual(len(resp['field_options']), 21) + url = ( + '/superset/fetch_datasource_metadata?' + 'datasource_type=table&' + 'datasource_id=1' + ) + resp = self.get_json_resp(url) + keys = [ + 'name', 'filterable_cols', 'gb_cols', 'type', 'all_cols', + 'order_by_choices', 'metrics_combo', 'granularity_sqla', + 'time_grain_sqla', 'id', + ] + for k in keys: + self.assertIn(k, resp.keys()) def test_fetch_all_tables(self): self.login(username='admin')