diff --git a/superset/assets/javascripts/explore/actions/chartActions.js b/superset/assets/javascripts/explore/actions/chartActions.js new file mode 100644 index 0000000000000..6c9291dc41ece --- /dev/null +++ b/superset/assets/javascripts/explore/actions/chartActions.js @@ -0,0 +1,70 @@ +import { getExploreUrl } from '../exploreUtils'; +import { getFormDataFromControls } from '../stores/store'; +import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; +import { triggerQuery } from './exploreActions'; + +const $ = window.$ = require('jquery'); + +export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED'; +export function chartUpdateStarted(queryRequest, latestQueryFormData) { + return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData }; +} + +export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED'; +export function chartUpdateSucceeded(queryResponse) { + return { type: CHART_UPDATE_SUCCEEDED, queryResponse }; +} + +export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED'; +export function chartUpdateStopped(queryRequest) { + if (queryRequest) { + queryRequest.abort(); + } + return { type: CHART_UPDATE_STOPPED }; +} + +export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT'; +export function chartUpdateTimeout(statusText) { + return { type: CHART_UPDATE_TIMEOUT, statusText }; +} + +export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED'; +export function chartUpdateFailed(queryResponse) { + return { type: CHART_UPDATE_FAILED, queryResponse }; +} + +export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS'; +export function updateChartStatus(status) { + return { type: UPDATE_CHART_STATUS, status }; +} + +export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED'; +export function chartRenderingFailed(error) { + return { type: CHART_RENDERING_FAILED, error }; +} + +export const RUN_QUERY = 'RUN_QUERY'; +export function runQuery(formData, force = false) { + return function (dispatch, getState) { + const { explore } = getState(); + const lastQueryFormData = getFormDataFromControls(explore.controls); + const url = getExploreUrl(formData, 'json', force); + const queryRequest = $.ajax({ + url, + dataType: 'json', + success(queryResponse) { + dispatch(chartUpdateSucceeded(queryResponse)); + }, + error(err) { + if (err.statusText === 'timeout') { + dispatch(chartUpdateTimeout(err.statusText)); + } else if (err.statusText !== 'abort') { + dispatch(chartUpdateFailed(err.responseJSON)); + } + }, + timeout: QUERY_TIMEOUT_THRESHOLD, + }); + dispatch(chartUpdateStarted(queryRequest, lastQueryFormData)); + dispatch(triggerQuery(false)); + }; +} diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js index d45acd5834b9e..32fa3c5b4745f 100644 --- a/superset/assets/javascripts/explore/actions/exploreActions.js +++ b/superset/assets/javascripts/explore/actions/exploreActions.js @@ -1,6 +1,4 @@ /* eslint camelcase: 0 */ -import { getExploreUrl } from '../exploreUtils'; -import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; const $ = window.$ = require('jquery'); @@ -37,8 +35,8 @@ export function resetControls() { } export const TRIGGER_QUERY = 'TRIGGER_QUERY'; -export function triggerQuery() { - return { type: TRIGGER_QUERY }; +export function triggerQuery(value = true) { + return { type: TRIGGER_QUERY, value }; } export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false) { @@ -95,39 +93,6 @@ export function setControlValue(controlName, value, validationErrors) { return { type: SET_FIELD_VALUE, controlName, value, validationErrors }; } -export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED'; -export function chartUpdateStarted(queryRequest) { - return { type: CHART_UPDATE_STARTED, queryRequest }; -} - -export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED'; -export function chartUpdateSucceeded(queryResponse) { - return { type: CHART_UPDATE_SUCCEEDED, queryResponse }; -} - -export const CHART_UPDATE_STOPPED = 'CHART_UPDATE_STOPPED'; -export function chartUpdateStopped(queryRequest) { - if (queryRequest) { - queryRequest.abort(); - } - return { type: CHART_UPDATE_STOPPED }; -} - -export const CHART_UPDATE_TIMEOUT = 'CHART_UPDATE_TIMEOUT'; -export function chartUpdateTimeout(statusText) { - return { type: CHART_UPDATE_TIMEOUT, statusText }; -} - -export const CHART_UPDATE_FAILED = 'CHART_UPDATE_FAILED'; -export function chartUpdateFailed(queryResponse) { - return { type: CHART_UPDATE_FAILED, queryResponse }; -} - -export const CHART_RENDERING_FAILED = 'CHART_RENDERING_FAILED'; -export function chartRenderingFailed(error) { - return { type: CHART_RENDERING_FAILED, error }; -} - export const UPDATE_EXPLORE_ENDPOINTS = 'UPDATE_EXPLORE_ENDPOINTS'; export function updateExploreEndpoints(jsonUrl, csvUrl, standaloneUrl) { return { type: UPDATE_EXPLORE_ENDPOINTS, jsonUrl, csvUrl, standaloneUrl }; @@ -143,95 +108,11 @@ export function removeChartAlert() { return { type: REMOVE_CHART_ALERT }; } -export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED'; -export function fetchDashboardsSucceeded(choices) { - return { type: FETCH_DASHBOARDS_SUCCEEDED, choices }; -} - -export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED'; -export function fetchDashboardsFailed(userId) { - return { type: FETCH_DASHBOARDS_FAILED, userId }; -} - -export function fetchDashboards(userId) { - return function (dispatch) { - const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userId; - $.ajax({ - type: 'GET', - url, - success: (data) => { - const choices = []; - for (let i = 0; i < data.pks.length; i++) { - choices.push({ value: data.pks[i], label: data.result[i].dashboard_title }); - } - dispatch(fetchDashboardsSucceeded(choices)); - }, - error: () => { - dispatch(fetchDashboardsFailed(userId)); - }, - }); - }; -} - -export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED'; -export function saveSliceFailed() { - return { type: SAVE_SLICE_FAILED }; -} -export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS'; -export function saveSliceSuccess(data) { - return { type: SAVE_SLICE_SUCCESS, data }; -} - -export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT'; -export function removeSaveModalAlert() { - return { type: REMOVE_SAVE_MODAL_ALERT }; -} - -export function saveSlice(url) { - return function (dispatch) { - return $.get(url, (data, status) => { - if (status === 'success') { - dispatch(saveSliceSuccess(data)); - } else { - dispatch(saveSliceFailed()); - } - }); - }; -} - export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE'; export function updateChartTitle(slice_name) { return { type: UPDATE_CHART_TITLE, slice_name }; } -export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS'; -export function updateChartStatus(status) { - return { type: UPDATE_CHART_STATUS, status }; -} - -export const RUN_QUERY = 'RUN_QUERY'; -export function runQuery(formData, force = false) { - return function (dispatch) { - const url = getExploreUrl(formData, 'json', force); - const queryRequest = $.ajax({ - url, - dataType: 'json', - success(queryResponse) { - dispatch(chartUpdateSucceeded(queryResponse)); - }, - error(err) { - if (err.statusText === 'timeout') { - dispatch(chartUpdateTimeout(err.statusText)); - } else if (err.statusText !== 'abort') { - dispatch(chartUpdateFailed(err.responseJSON)); - } - }, - timeout: QUERY_TIMEOUT_THRESHOLD, - }); - dispatch(chartUpdateStarted(queryRequest)); - }; -} - export const RENDER_TRIGGERED = 'RENDER_TRIGGERED'; export function renderTriggered() { return { type: RENDER_TRIGGERED }; diff --git a/superset/assets/javascripts/explore/actions/saveModalActions.js b/superset/assets/javascripts/explore/actions/saveModalActions.js new file mode 100644 index 0000000000000..b1111287f288c --- /dev/null +++ b/superset/assets/javascripts/explore/actions/saveModalActions.js @@ -0,0 +1,57 @@ +const $ = window.$ = require('jquery'); + +export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED'; +export function fetchDashboardsSucceeded(choices) { + return { type: FETCH_DASHBOARDS_SUCCEEDED, choices }; +} + +export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED'; +export function fetchDashboardsFailed(userId) { + return { type: FETCH_DASHBOARDS_FAILED, userId }; +} + +export function fetchDashboards(userId) { + return function (dispatch) { + const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userId; + return $.ajax({ + type: 'GET', + url, + success: (data) => { + const choices = []; + for (let i = 0; i < data.pks.length; i++) { + choices.push({ value: data.pks[i], label: data.result[i].dashboard_title }); + } + dispatch(fetchDashboardsSucceeded(choices)); + }, + error: () => { + dispatch(fetchDashboardsFailed(userId)); + }, + }); + }; +} + +export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED'; +export function saveSliceFailed() { + return { type: SAVE_SLICE_FAILED }; +} +export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS'; +export function saveSliceSuccess(data) { + return { type: SAVE_SLICE_SUCCESS, data }; +} + +export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT'; +export function removeSaveModalAlert() { + return { type: REMOVE_SAVE_MODAL_ALERT }; +} + +export function saveSlice(url) { + return function (dispatch) { + return $.get(url, (data, status) => { + if (status === 'success') { + dispatch(saveSliceSuccess(data)); + } else { + dispatch(saveSliceFailed()); + } + }); + }; +} diff --git a/superset/assets/javascripts/explore/components/ChartContainer.jsx b/superset/assets/javascripts/explore/components/ChartContainer.jsx index ab2be2fcdf24c..3e93b9e6a4be7 100644 --- a/superset/assets/javascripts/explore/components/ChartContainer.jsx +++ b/superset/assets/javascripts/explore/components/ChartContainer.jsx @@ -322,29 +322,29 @@ class ChartContainer extends React.PureComponent { ChartContainer.propTypes = propTypes; -function mapStateToProps(state) { - const formData = getFormDataFromControls(state.controls); +function mapStateToProps({ explore, chart }) { + const formData = getFormDataFromControls(explore.controls); return { - alert: state.chartAlert, - can_overwrite: state.can_overwrite, - can_download: state.can_download, - chartStatus: state.chartStatus, - chartUpdateEndTime: state.chartUpdateEndTime, - chartUpdateStartTime: state.chartUpdateStartTime, - datasource: state.datasource, - column_formats: state.datasource ? state.datasource.column_formats : null, - containerId: state.slice ? `slice-container-${state.slice.slice_id}` : 'slice-container', + alert: explore.chartAlert, + can_overwrite: explore.can_overwrite, + can_download: explore.can_download, + datasource: explore.datasource, + column_formats: explore.datasource ? explore.datasource.column_formats : null, + containerId: explore.slice ? `slice-container-${explore.slice.slice_id}` : 'slice-container', formData, - latestQueryFormData: state.latestQueryFormData, - isStarred: state.isStarred, - queryResponse: state.queryResponse, - slice: state.slice, - standalone: state.standalone, + isStarred: explore.isStarred, + slice: explore.slice, + standalone: explore.standalone, table_name: formData.datasource_name, viz_type: formData.viz_type, - triggerRender: state.triggerRender, - datasourceType: state.datasource.type, - datasourceId: state.datasource_id, + triggerRender: explore.triggerRender, + datasourceType: explore.datasource.type, + datasourceId: explore.datasource_id, + chartStatus: chart.chartStatus, + chartUpdateEndTime: chart.chartUpdateEndTime, + chartUpdateStartTime: chart.chartUpdateStartTime, + latestQueryFormData: chart.latestQueryFormData, + queryResponse: chart.queryResponse, }; } diff --git a/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx b/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx index e3b29855a8075..8a8c2d8802b80 100644 --- a/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx +++ b/superset/assets/javascripts/explore/components/ControlPanelsContainer.jsx @@ -96,12 +96,12 @@ class ControlPanelsContainer extends React.Component { ControlPanelsContainer.propTypes = propTypes; -function mapStateToProps(state) { +function mapStateToProps({ explore }) { return { - alert: state.controlPanelAlert, - isDatasourceMetaLoading: state.isDatasourceMetaLoading, - controls: state.controls, - exploreState: state, + alert: explore.controlPanelAlert, + isDatasourceMetaLoading: explore.isDatasourceMetaLoading, + controls: explore.controls, + exploreState: explore, }; } diff --git a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx index ae94f6a02efcc..bb96dbc3a3270 100644 --- a/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx +++ b/superset/assets/javascripts/explore/components/ExploreViewContainer.jsx @@ -8,12 +8,15 @@ import ControlPanelsContainer from './ControlPanelsContainer'; import SaveModal from './SaveModal'; import QueryAndSaveBtns from './QueryAndSaveBtns'; import { getExploreUrl } from '../exploreUtils'; -import * as actions from '../actions/exploreActions'; import { getFormDataFromControls } from '../stores/store'; +import * as exploreActions from '../actions/exploreActions'; +import * as saveModalActions from '../actions/saveModalActions'; +import * as chartActions from '../actions/chartActions'; const propTypes = { actions: PropTypes.object.isRequired, datasource_type: PropTypes.string.isRequired, + isDatasourceMetaLoading: PropTypes.bool.isRequired, chartStatus: PropTypes.string, controls: PropTypes.object.isRequired, forcedHeight: PropTypes.string, @@ -85,7 +88,6 @@ class ExploreViewContainer extends React.Component { return `${window.innerHeight - navHeight}px`; } - triggerQueryIfNeeded() { if (this.props.triggerQuery && !this.hasErrors()) { this.props.actions.runQuery(this.props.form_data); @@ -172,7 +174,9 @@ class ExploreViewContainer extends React.Component {
@@ -186,21 +190,23 @@ class ExploreViewContainer extends React.Component { ExploreViewContainer.propTypes = propTypes; -function mapStateToProps(state) { - const form_data = getFormDataFromControls(state.controls); +function mapStateToProps({ explore, chart }) { + const form_data = getFormDataFromControls(explore.controls); return { - chartStatus: state.chartStatus, - datasource_type: state.datasource.type, - controls: state.controls, + isDatasourceMetaLoading: explore.isDatasourceMetaLoading, + datasource_type: explore.datasource.type, + controls: explore.controls, form_data, - standalone: state.standalone, - triggerQuery: state.triggerQuery, - forcedHeight: state.forced_height, - queryRequest: state.queryRequest, + standalone: explore.standalone, + triggerQuery: explore.triggerQuery, + forcedHeight: explore.forced_height, + queryRequest: chart.queryRequest, + chartStatus: chart.chartStatus, }; } function mapDispatchToProps(dispatch) { + const actions = Object.assign({}, exploreActions, saveModalActions, chartActions); return { actions: bindActionCreators(actions, dispatch), }; diff --git a/superset/assets/javascripts/explore/components/SaveModal.jsx b/superset/assets/javascripts/explore/components/SaveModal.jsx index 4dbff36acfea6..beb07b27ea4d2 100644 --- a/superset/assets/javascripts/explore/components/SaveModal.jsx +++ b/superset/assets/javascripts/explore/components/SaveModal.jsx @@ -1,10 +1,11 @@ /* eslint camelcase: 0 */ import React from 'react'; import PropTypes from 'prop-types'; -import $ from 'jquery'; +import { connect } from 'react-redux'; + import { Modal, Alert, Button, Radio } from 'react-bootstrap'; import Select from 'react-select'; -import { connect } from 'react-redux'; +import { getExploreUrl } from '../exploreUtils'; const propTypes = { can_overwrite: PropTypes.bool, @@ -102,12 +103,7 @@ class SaveModal extends React.Component { } sliceParams.goto_dash = gotodash; - const baseUrl = `/superset/explore/${this.props.datasource.type}/${this.props.datasource.id}/`; - sliceParams.datasource_name = this.props.datasource.name; - - const saveUrl = `${baseUrl}?form_data=` + - `${encodeURIComponent(JSON.stringify(this.props.form_data))}` + - `&${$.param(sliceParams, true)}`; + const saveUrl = getExploreUrl(this.props.form_data, 'base', false, null, sliceParams); this.props.actions.saveSlice(saveUrl) .then((data) => { // Go to new slice url or dashboard url @@ -234,14 +230,14 @@ class SaveModal extends React.Component { SaveModal.propTypes = propTypes; -function mapStateToProps(state) { +function mapStateToProps({ explore, saveModal }) { return { - datasource: state.datasource, - slice: state.slice, - can_overwrite: state.can_overwrite, - user_id: state.user_id, - dashboards: state.dashboards, - alert: state.saveModalAlert, + datasource: explore.datasource, + slice: explore.slice, + can_overwrite: explore.can_overwrite, + user_id: explore.user_id, + dashboards: saveModal.dashboards, + alert: explore.saveModalAlert, }; } diff --git a/superset/assets/javascripts/explore/index.jsx b/superset/assets/javascripts/explore/index.jsx index 0fe4fcaba9524..c6585fd12c127 100644 --- a/superset/assets/javascripts/explore/index.jsx +++ b/superset/assets/javascripts/explore/index.jsx @@ -11,7 +11,8 @@ import AlertsWrapper from '../components/AlertsWrapper'; import { getControlsState, getFormDataFromControls } from './stores/store'; import { initJQueryAjax } from '../modules/utils'; import ExploreViewContainer from './components/ExploreViewContainer'; -import { exploreReducer } from './reducers/exploreReducer'; +import rootReducer from './reducers/index'; + import { appSetup } from '../common'; import './main.css'; import '../../stylesheets/reactable-pagination.css'; @@ -28,23 +29,30 @@ delete bootstrapData.form_data; // Initial state const bootstrappedState = Object.assign( bootstrapData, { - chartStatus: null, - chartUpdateEndTime: null, - chartUpdateStartTime: now(), - dashboards: [], controls, - latestQueryFormData: getFormDataFromControls(controls), filterColumnOpts: [], isDatasourceMetaLoading: false, isStarred: false, - queryResponse: null, triggerQuery: true, triggerRender: false, alert: null, }, ); -const store = createStore(exploreReducer, bootstrappedState, +const initState = { + chart: { + chartStatus: null, + chartUpdateEndTime: null, + chartUpdateStartTime: now(), + latestQueryFormData: getFormDataFromControls(controls), + queryResponse: null, + }, + saveModal: { + dashboards: [], + }, + explore: bootstrappedState, +}; +const store = createStore(rootReducer, initState, compose(applyMiddleware(thunk), initEnhancer(false)), ); diff --git a/superset/assets/javascripts/explore/reducers/chartReducer.js b/superset/assets/javascripts/explore/reducers/chartReducer.js new file mode 100644 index 0000000000000..c41771b44c313 --- /dev/null +++ b/superset/assets/javascripts/explore/reducers/chartReducer.js @@ -0,0 +1,72 @@ +/* eslint camelcase: 0 */ +import { now } from '../../modules/dates'; +import * as actions from '../actions/chartActions'; +import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; + +export default function chartReducer(state = {}, action) { + const actionHandlers = { + [actions.CHART_UPDATE_SUCCEEDED]() { + return Object.assign( + {}, + state, + { + chartStatus: 'success', + queryResponse: action.queryResponse, + }, + ); + }, + [actions.CHART_UPDATE_STARTED]() { + return Object.assign({}, state, + { + chartStatus: 'loading', + chartUpdateEndTime: null, + chartUpdateStartTime: now(), + queryRequest: action.queryRequest, + latestQueryFormData: action.latestQueryFormData, + }); + }, + [actions.CHART_UPDATE_STOPPED]() { + return Object.assign({}, state, + { + chartStatus: 'stopped', + chartAlert: 'Updating chart was stopped', + }); + }, + [actions.CHART_RENDERING_FAILED]() { + return Object.assign({}, state, { + chartStatus: 'failed', + chartAlert: 'An error occurred while rendering the visualization: ' + action.error, + }); + }, + [actions.CHART_UPDATE_TIMEOUT]() { + return Object.assign({}, state, { + chartStatus: 'failed', + chartAlert: 'Query timeout - visualization query are set to timeout at ' + + `${QUERY_TIMEOUT_THRESHOLD / 1000} seconds. ` + + 'Perhaps your data has grown, your database is under unusual load, ' + + 'or you are simply querying a data source that is to large to be processed within the timeout range. ' + + 'If that is the case, we recommend that you summarize your data further.', + }); + }, + [actions.CHART_UPDATE_FAILED]() { + return Object.assign({}, state, { + chartStatus: 'failed', + chartAlert: action.queryResponse ? action.queryResponse.error : 'Network error.', + chartUpdateEndTime: now(), + queryResponse: action.queryResponse, + }); + }, + [actions.UPDATE_CHART_STATUS]() { + const newState = Object.assign({}, state, { chartStatus: action.status }); + if (action.status === 'success' || action.status === 'failed') { + newState.chartUpdateEndTime = now(); + } + return newState; + }, + }; + + if (action.type in actionHandlers) { + return actionHandlers[action.type](); + } + return state; +} diff --git a/superset/assets/javascripts/explore/reducers/exploreReducer.js b/superset/assets/javascripts/explore/reducers/exploreReducer.js index 96e36e3765754..bc1072f49163f 100644 --- a/superset/assets/javascripts/explore/reducers/exploreReducer.js +++ b/superset/assets/javascripts/explore/reducers/exploreReducer.js @@ -1,23 +1,18 @@ /* eslint camelcase: 0 */ import { getControlsState, getFormDataFromControls } from '../stores/store'; import * as actions from '../actions/exploreActions'; -import { now } from '../../modules/dates'; -import { QUERY_TIMEOUT_THRESHOLD } from '../../constants'; -export const exploreReducer = function (state, action) { +export default function exploreReducer(state = {}, action) { const actionHandlers = { [actions.TOGGLE_FAVE_STAR]() { return Object.assign({}, state, { isStarred: action.isStarred }); }, - [actions.FETCH_DATASOURCE_STARTED]() { return Object.assign({}, state, { isDatasourceMetaLoading: true }); }, - [actions.FETCH_DATASOURCE_SUCCEEDED]() { return Object.assign({}, state, { isDatasourceMetaLoading: false }); }, - [actions.FETCH_DATASOURCE_FAILED]() { // todo(alanna) handle failure/error state return Object.assign({}, state, @@ -29,16 +24,25 @@ export const exploreReducer = function (state, action) { [actions.SET_DATASOURCE]() { return Object.assign({}, state, { datasource: action.datasource }); }, - [actions.REMOVE_CONTROL_PANEL_ALERT]() { - return Object.assign({}, state, { controlPanelAlert: null }); + [actions.FETCH_DATASOURCES_STARTED]() { + return Object.assign({}, state, { isDatasourcesLoading: true }); }, - [actions.FETCH_DASHBOARDS_SUCCEEDED]() { - return Object.assign({}, state, { dashboards: action.choices }); + [actions.FETCH_DATASOURCES_SUCCEEDED]() { + return Object.assign({}, state, { isDatasourcesLoading: false }); }, - - [actions.FETCH_DASHBOARDS_FAILED]() { + [actions.FETCH_DATASOURCES_FAILED]() { + // todo(alanna) handle failure/error state return Object.assign({}, state, - { saveModalAlert: `fetching dashboards failed for ${action.userId}` }); + { + isDatasourcesLoading: false, + controlPanelAlert: action.error, + }); + }, + [actions.SET_DATASOURCES]() { + return Object.assign({}, state, { datasources: action.datasources }); + }, + [actions.REMOVE_CONTROL_PANEL_ALERT]() { + return Object.assign({}, state, { controlPanelAlert: null }); }, [actions.SET_FIELD_VALUE]() { const controls = Object.assign({}, state.controls); @@ -52,70 +56,11 @@ export const exploreReducer = function (state, action) { } return Object.assign({}, state, changes); }, - [actions.CHART_UPDATE_SUCCEEDED]() { - return Object.assign( - {}, - state, - { - chartStatus: 'success', - queryResponse: action.queryResponse, - }, - ); - }, - [actions.CHART_UPDATE_STARTED]() { - return Object.assign({}, state, - { - chartStatus: 'loading', - chartUpdateEndTime: null, - chartUpdateStartTime: now(), - triggerQuery: false, - queryRequest: action.queryRequest, - latestQueryFormData: getFormDataFromControls(state.controls), - }); - }, - [actions.CHART_UPDATE_STOPPED]() { - return Object.assign({}, state, - { - chartStatus: 'stopped', - chartAlert: 'Updating chart was stopped', - }); - }, - [actions.CHART_RENDERING_FAILED]() { - return Object.assign({}, state, { - chartStatus: 'failed', - chartAlert: 'An error occurred while rendering the visualization: ' + action.error, - }); - }, [actions.TRIGGER_QUERY]() { return Object.assign({}, state, { - triggerQuery: true, + triggerQuery: action.value, }); }, - [actions.CHART_UPDATE_TIMEOUT]() { - return Object.assign({}, state, { - chartStatus: 'failed', - chartAlert: 'Query timeout - visualization query are set to timeout at ' + - `${QUERY_TIMEOUT_THRESHOLD / 1000} seconds. ` + - 'Perhaps your data has grown, your database is under unusual load, ' + - 'or you are simply querying a data source that is to large to be processed within the timeout range. ' + - 'If that is the case, we recommend that you summarize your data further.', - }); - }, - [actions.CHART_UPDATE_FAILED]() { - return Object.assign({}, state, { - chartStatus: 'failed', - chartAlert: action.queryResponse ? action.queryResponse.error : 'Network error.', - chartUpdateEndTime: now(), - queryResponse: action.queryResponse, - }); - }, - [actions.UPDATE_CHART_STATUS]() { - const newState = Object.assign({}, state, { chartStatus: action.status }); - if (action.status === 'success' || action.status === 'failed') { - newState.chartUpdateEndTime = now(); - } - return newState; - }, [actions.UPDATE_CHART_TITLE]() { const updatedSlice = Object.assign({}, state.slice, { slice_name: action.slice_name }); return Object.assign({}, state, { slice: updatedSlice }); @@ -126,15 +71,6 @@ export const exploreReducer = function (state, action) { } return state; }, - [actions.SAVE_SLICE_FAILED]() { - return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' }); - }, - [actions.SAVE_SLICE_SUCCESS](data) { - return Object.assign({}, state, { data }); - }, - [actions.REMOVE_SAVE_MODAL_ALERT]() { - return Object.assign({}, state, { saveModalAlert: null }); - }, [actions.RESET_FIELDS]() { const controls = getControlsState(state, getFormDataFromControls(state.controls)); return Object.assign({}, state, { controls }); @@ -147,4 +83,4 @@ export const exploreReducer = function (state, action) { return actionHandlers[action.type](); } return state; -}; +} diff --git a/superset/assets/javascripts/explore/reducers/index.js b/superset/assets/javascripts/explore/reducers/index.js new file mode 100644 index 0000000000000..0d5acb04c7e88 --- /dev/null +++ b/superset/assets/javascripts/explore/reducers/index.js @@ -0,0 +1,11 @@ +import { combineReducers } from 'redux'; + +import chart from './chartReducer'; +import saveModal from './saveModalReducer'; +import explore from './exploreReducer'; + +export default combineReducers({ + chart, + saveModal, + explore, +}); diff --git a/superset/assets/javascripts/explore/reducers/saveModalReducer.js b/superset/assets/javascripts/explore/reducers/saveModalReducer.js new file mode 100644 index 0000000000000..912d5315f3049 --- /dev/null +++ b/superset/assets/javascripts/explore/reducers/saveModalReducer.js @@ -0,0 +1,28 @@ +/* eslint camelcase: 0 */ +import * as actions from '../actions/saveModalActions'; + +export default function saveModalReducer(state = {}, action) { + const actionHandlers = { + [actions.FETCH_DASHBOARDS_SUCCEEDED]() { + return Object.assign({}, state, { dashboards: action.choices }); + }, + [actions.FETCH_DASHBOARDS_FAILED]() { + return Object.assign({}, state, + { saveModalAlert: `fetching dashboards failed for ${action.userId}` }); + }, + [actions.SAVE_SLICE_FAILED]() { + return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' }); + }, + [actions.SAVE_SLICE_SUCCESS](data) { + return Object.assign({}, state, { data }); + }, + [actions.REMOVE_SAVE_MODAL_ALERT]() { + return Object.assign({}, state, { saveModalAlert: null }); + }, + }; + + if (action.type in actionHandlers) { + return actionHandlers[action.type](); + } + return state; +} diff --git a/superset/assets/spec/javascripts/explore/chartActions_spec.js b/superset/assets/spec/javascripts/explore/chartActions_spec.js new file mode 100644 index 0000000000000..b2e069ab971a8 --- /dev/null +++ b/superset/assets/spec/javascripts/explore/chartActions_spec.js @@ -0,0 +1,36 @@ +import { it, describe } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import $ from 'jquery'; +import * as exploreUtils from '../../../javascripts/explore/exploreUtils'; +import * as actions from '../../../javascripts/explore/actions/chartActions'; + +describe('chart actions', () => { + let dispatch; + let urlStub; + let ajaxStub; + let request; + + beforeEach(() => { + dispatch = sinon.spy(); + urlStub = sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL')); + ajaxStub = sinon.stub($, 'ajax'); + }); + + afterEach(() => { + urlStub.restore(); + ajaxStub.restore(); + }); + + it('should handle query timeout', () => { + ajaxStub.yieldsTo('error', { statusText: 'timeout' }); + request = actions.runQuery({}); + request(dispatch, sinon.stub().returns({ + explore: { + controls: [], + }, + })); + expect(dispatch.callCount).to.equal(3); + expect(dispatch.args[0][0].type).to.equal(actions.CHART_UPDATE_TIMEOUT); + }); +}); diff --git a/superset/assets/spec/javascripts/explore/exploreActions_spec.js b/superset/assets/spec/javascripts/explore/exploreActions_spec.js index 9fa02e4b12484..5d2926de2ef12 100644 --- a/superset/assets/spec/javascripts/explore/exploreActions_spec.js +++ b/superset/assets/spec/javascripts/explore/exploreActions_spec.js @@ -4,9 +4,8 @@ import { expect } from 'chai'; import sinon from 'sinon'; import $ from 'jquery'; import * as actions from '../../../javascripts/explore/actions/exploreActions'; -import * as exploreUtils from '../../../javascripts/explore/exploreUtils'; import { defaultState } from '../../../javascripts/explore/stores/store'; -import { exploreReducer } from '../../../javascripts/explore/reducers/exploreReducer'; +import exploreReducer from '../../../javascripts/explore/reducers/exploreReducer'; describe('reducers', () => { it('sets correct control value given a key and value', () => { @@ -81,69 +80,4 @@ describe('fetching actions', () => { expect(dispatch.getCall(4).args[0].type).to.equal(actions.TRIGGER_QUERY); }); }); - - describe('fetchDashboards', () => { - const userID = 1; - const mockDashboardData = { - pks: ['value'], - result: [ - { dashboard_title: 'dashboard title' }, - ], - }; - const makeRequest = () => { - request = actions.fetchDashboards(userID); - request(dispatch); - }; - - it('makes the ajax request', () => { - makeRequest(); - expect(ajaxStub.calledOnce).to.be.true; - }); - - it('calls correct url', () => { - const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userID; - makeRequest(); - expect(ajaxStub.getCall(0).args[0].url).to.equal(url); - }); - - it('calls correct actions on error', () => { - ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } }); - makeRequest(); - expect(dispatch.callCount).to.equal(1); - expect(dispatch.getCall(0).args[0].type).to.equal(actions.FETCH_DASHBOARDS_FAILED); - }); - - it('calls correct actions on success', () => { - ajaxStub.yieldsTo('success', mockDashboardData); - makeRequest(); - expect(dispatch.callCount).to.equal(1); - expect(dispatch.getCall(0).args[0].type).to.equal(actions.FETCH_DASHBOARDS_SUCCEEDED); - }); - }); -}); - -describe('runQuery', () => { - let dispatch; - let urlStub; - let ajaxStub; - let request; - - beforeEach(() => { - dispatch = sinon.spy(); - urlStub = sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL')); - ajaxStub = sinon.stub($, 'ajax'); - }); - - afterEach(() => { - urlStub.restore(); - ajaxStub.restore(); - }); - - it('should handle query timeout', () => { - ajaxStub.yieldsTo('error', { statusText: 'timeout' }); - request = actions.runQuery({}); - request(dispatch); - expect(dispatch.callCount).to.equal(2); - expect(dispatch.args[0][0].type).to.equal(actions.CHART_UPDATE_TIMEOUT); - }); });