diff --git a/superset/assets/javascripts/explore/actions/exploreActions.js b/superset/assets/javascripts/explore/actions/exploreActions.js
index 32fa3c5b474..f539aa11e64 100644
--- a/superset/assets/javascripts/explore/actions/exploreActions.js
+++ b/superset/assets/javascripts/explore/actions/exploreActions.js
@@ -14,6 +14,11 @@ export function setDatasource(datasource) {
return { type: SET_DATASOURCE, datasource };
}
+export const SET_DATASOURCES = 'SET_DATASOURCES';
+export function setDatasources(datasources) {
+ return { type: SET_DATASOURCES, datasources };
+}
+
export const FETCH_DATASOURCE_STARTED = 'FETCH_DATASOURCE_STARTED';
export function fetchDatasourceStarted() {
return { type: FETCH_DATASOURCE_STARTED };
@@ -29,6 +34,21 @@ export function fetchDatasourceFailed(error) {
return { type: FETCH_DATASOURCE_FAILED, error };
}
+export const FETCH_DATASOURCES_STARTED = 'FETCH_DATASOURCES_STARTED';
+export function fetchDatasourcesStarted() {
+ return { type: FETCH_DATASOURCES_STARTED };
+}
+
+export const FETCH_DATASOURCES_SUCCEEDED = 'FETCH_DATASOURCES_SUCCEEDED';
+export function fetchDatasourcesSucceeded() {
+ return { type: FETCH_DATASOURCES_SUCCEEDED };
+}
+
+export const FETCH_DATASOURCES_FAILED = 'FETCH_DATASOURCES_FAILED';
+export function fetchDatasourcesFailed(error) {
+ return { type: FETCH_DATASOURCES_FAILED, error };
+}
+
export const RESET_FIELDS = 'RESET_FIELDS';
export function resetControls() {
return { type: RESET_FIELDS };
@@ -61,6 +81,24 @@ export function fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery = false)
};
}
+export function fetchDatasources() {
+ return function (dispatch) {
+ dispatch(fetchDatasourcesStarted());
+ const url = '/superset/datasources/';
+ $.ajax({
+ type: 'GET',
+ url,
+ success: (data) => {
+ dispatch(setDatasources(data));
+ dispatch(fetchDatasourcesSucceeded());
+ },
+ error(error) {
+ dispatch(fetchDatasourcesFailed(error.responseJSON.error));
+ },
+ });
+ };
+}
+
export const TOGGLE_FAVE_STAR = 'TOGGLE_FAVE_STAR';
export function toggleFaveStar(isStarred) {
return { type: TOGGLE_FAVE_STAR, isStarred };
diff --git a/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx b/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx
index 5512b96722a..0b0839778a2 100644
--- a/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/CheckboxControl_spec.jsx
@@ -4,24 +4,30 @@ import { Checkbox } from 'react-bootstrap';
import sinon from 'sinon';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
-import { mount } from 'enzyme';
+import { shallow } from 'enzyme';
import CheckboxControl from '../../../../javascripts/explore/components/controls/CheckboxControl';
+import ControlHeader from '../../../../javascripts/explore/components/ControlHeader';
const defaultProps = {
name: 'show_legend',
onChange: sinon.spy(),
value: false,
+ label: 'checkbox label',
};
describe('CheckboxControl', () => {
let wrapper;
beforeEach(() => {
- wrapper = mount();
+ wrapper = shallow();
});
it('renders a Checkbox', () => {
- expect(wrapper.find(Checkbox)).to.have.lengthOf(1);
+ const controlHeader = wrapper.find(ControlHeader);
+ expect(controlHeader).to.have.lengthOf(1);
+
+ const headerWrapper = controlHeader.shallow();
+ expect(headerWrapper.find(Checkbox)).to.have.length(1);
});
});
diff --git a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
index e0a1a845130..e548d21a60a 100644
--- a/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/SaveModal_spec.jsx
@@ -1,33 +1,57 @@
import React from 'react';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
-import { shallow } from 'enzyme';
+import { shallow, mount } from 'enzyme';
import { Modal, Button, Radio } from 'react-bootstrap';
import sinon from 'sinon';
-import { defaultFormData } from '../../../../javascripts/explore/stores/store';
-import { SaveModal } from '../../../../javascripts/explore/components/SaveModal';
+import * as exploreUtils from '../../../../javascripts/explore/exploreUtils';
+import * as saveModalActions from '../../../../javascripts/explore/actions/saveModalActions';
+import SaveModal from '../../../../javascripts/explore/components/SaveModal';
-const defaultProps = {
- can_edit: true,
- onHide: () => ({}),
- actions: {
- saveSlice: sinon.spy(),
- },
- form_data: defaultFormData,
- user_id: '1',
- dashboards: [],
- slice: {},
-};
+const $ = window.$ = require('jquery');
describe('SaveModal', () => {
- let wrapper;
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+ const initialState = {
+ chart: {},
+ saveModal: {
+ dashboards: [],
+ },
+ explore: {
+ can_overwrite: true,
+ user_id: '1',
+ datasource: {},
+ slice: {
+ slice_id: 1,
+ slice_name: 'title',
+ },
+ alert: null,
+ },
+ };
+ const store = mockStore(initialState);
- beforeEach(() => {
- wrapper = shallow();
- });
+ const defaultProps = {
+ onHide: () => ({}),
+ actions: saveModalActions,
+ form_data: {},
+ };
+ const mockEvent = {
+ target: {
+ value: 'mock event target',
+ },
+ value: 'mock value',
+ };
+ const getWrapper = () => (shallow(, {
+ context: { store },
+ }).dive());
it('renders a Modal with 7 inputs and 2 buttons', () => {
+ const wrapper = getWrapper();
expect(wrapper.find(Modal)).to.have.lengthOf(1);
expect(wrapper.find('input')).to.have.lengthOf(2);
expect(wrapper.find(Button)).to.have.lengthOf(2);
@@ -35,42 +59,167 @@ describe('SaveModal', () => {
});
it('does not show overwrite option for new slice', () => {
- defaultProps.slice = null;
- const wrapperNewSlice = shallow();
+ const wrapperNewSlice = getWrapper();
+ wrapperNewSlice.setProps({ slice: null });
expect(wrapperNewSlice.find('#overwrite-radio')).to.have.lengthOf(0);
expect(wrapperNewSlice.find('#saveas-radio')).to.have.lengthOf(1);
});
it('disable overwrite option for non-owner', () => {
- defaultProps.slice = {};
- defaultProps.can_overwrite = false;
- const wrapperForNonOwner = shallow();
+ const wrapperForNonOwner = getWrapper();
+ wrapperForNonOwner.setProps({ can_overwrite: false });
const overwriteRadio = wrapperForNonOwner.find('#overwrite-radio');
expect(overwriteRadio).to.have.lengthOf(1);
expect(overwriteRadio.prop('disabled')).to.equal(true);
});
it('saves a new slice', () => {
- defaultProps.slice = {
- slice_id: 1,
- slice_name: 'title',
- };
- defaultProps.can_overwrite = false;
- const wrapperForNewSlice = shallow();
+ const wrapperForNewSlice = getWrapper();
+ wrapperForNewSlice.setProps({ can_overwrite: false });
+ wrapperForNewSlice.instance().changeAction('saveas');
const saveasRadio = wrapperForNewSlice.find('#saveas-radio');
saveasRadio.simulate('click');
expect(wrapperForNewSlice.state().action).to.equal('saveas');
});
it('overwrite a slice', () => {
- defaultProps.slice = {
- slice_id: 1,
- slice_name: 'title',
- };
- defaultProps.can_overwrite = true;
- const wrapperForOverwrite = shallow();
+ const wrapperForOverwrite = getWrapper();
const overwriteRadio = wrapperForOverwrite.find('#overwrite-radio');
overwriteRadio.simulate('click');
expect(wrapperForOverwrite.state().action).to.equal('overwrite');
});
+
+ it('componentDidMount', () => {
+ sinon.spy(SaveModal.prototype, 'componentDidMount');
+ sinon.spy(saveModalActions, 'fetchDashboards');
+ mount(, {
+ context: { store },
+ });
+ expect(SaveModal.prototype.componentDidMount.calledOnce).to.equal(true);
+ expect(saveModalActions.fetchDashboards.calledOnce).to.equal(true);
+
+ SaveModal.prototype.componentDidMount.restore();
+ saveModalActions.fetchDashboards.restore();
+ });
+
+ it('onChange', () => {
+ const wrapper = getWrapper();
+
+ wrapper.instance().onChange('newSliceName', mockEvent);
+ expect(wrapper.state().newSliceName).to.equal(mockEvent.target.value);
+
+ wrapper.instance().onChange('saveToDashboardId', mockEvent);
+ expect(wrapper.state().saveToDashboardId).to.equal(mockEvent.value);
+
+ wrapper.instance().onChange('newDashboardName', mockEvent);
+ expect(wrapper.state().newDashboardName).to.equal(mockEvent.target.value);
+ });
+
+ describe('saveOrOverwrite', () => {
+ beforeEach(() => {
+ sinon.stub(exploreUtils, 'getExploreUrl').callsFake(() => ('mockURL'));
+ sinon.stub(saveModalActions, 'saveSlice').callsFake(() => {
+ const d = $.Deferred();
+ d.resolve('done');
+ return d.promise();
+ });
+ });
+ afterEach(() => {
+ exploreUtils.getExploreUrl.restore();
+ saveModalActions.saveSlice.restore();
+ });
+
+ it('should save slice', () => {
+ const wrapper = getWrapper();
+ wrapper.instance().saveOrOverwrite(true);
+ expect(saveModalActions.saveSlice.getCall(0).args[0]).to.equal('mockURL');
+ });
+ it('existing dashboard', () => {
+ const wrapper = getWrapper();
+ const saveToDashboardId = 100;
+
+ wrapper.setState({ addToDash: 'existing' });
+ wrapper.instance().saveOrOverwrite(true);
+ expect(wrapper.state().alert).to.equal('Please select a dashboard');
+
+ wrapper.setState({ saveToDashboardId });
+ wrapper.instance().saveOrOverwrite(true);
+ const args = exploreUtils.getExploreUrl.getCall(0).args;
+ expect(args[4].save_to_dashboard_id).to.equal(saveToDashboardId);
+ });
+ it('new dashboard', () => {
+ const wrapper = getWrapper();
+ const newDashboardName = 'new dashboard name';
+
+ wrapper.setState({ addToDash: 'new' });
+ wrapper.instance().saveOrOverwrite(true);
+ expect(wrapper.state().alert).to.equal('Please enter a dashboard name');
+
+ wrapper.setState({ newDashboardName });
+ wrapper.instance().saveOrOverwrite(true);
+ const args = exploreUtils.getExploreUrl.getCall(0).args;
+ expect(args[4].new_dashboard_name).to.equal(newDashboardName);
+ });
+ });
+
+ describe('should fetchDashboards', () => {
+ let dispatch;
+ let request;
+ let ajaxStub;
+ const userID = 1;
+ beforeEach(() => {
+ dispatch = sinon.spy();
+ ajaxStub = sinon.stub($, 'ajax');
+ });
+ afterEach(() => {
+ ajaxStub.restore();
+ });
+ const mockDashboardData = {
+ pks: ['value'],
+ result: [
+ { dashboard_title: 'dashboard title' },
+ ],
+ };
+ const makeRequest = () => {
+ request = saveModalActions.fetchDashboards(userID);
+ request(dispatch);
+ };
+
+ it('makes the ajax request', () => {
+ makeRequest();
+ expect(ajaxStub.callCount).to.equal(1);
+ });
+
+ it('calls correct url', () => {
+ const url = '/dashboardmodelviewasync/api/read?_flt_0_owners=' + userID;
+ makeRequest();
+ expect(ajaxStub.getCall(0).args[0].url).to.be.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(saveModalActions.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(saveModalActions.FETCH_DASHBOARDS_SUCCEEDED);
+ });
+ });
+
+ it('removeAlert', () => {
+ sinon.spy(saveModalActions, 'removeSaveModalAlert');
+ const wrapper = getWrapper();
+ wrapper.setProps({ alert: 'old alert' });
+
+ wrapper.instance().removeAlert();
+ expect(saveModalActions.removeSaveModalAlert.callCount).to.equal(1);
+ expect(wrapper.state().alert).to.be.a('null');
+ saveModalActions.removeSaveModalAlert.restore();
+ });
});
diff --git a/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx b/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx
index f2729db1163..74d0d041a87 100644
--- a/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx
+++ b/superset/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx
@@ -1,7 +1,9 @@
import React from 'react';
import { expect } from 'chai';
import { describe, it } from 'mocha';
+import { shallow } from 'enzyme';
+import { OverlayTrigger } from 'react-bootstrap';
import URLShortLinkButton from '../../../../javascripts/explore/components/URLShortLinkButton';
describe('URLShortLinkButton', () => {
@@ -14,4 +16,8 @@ describe('URLShortLinkButton', () => {
it('renders', () => {
expect(React.isValidElement()).to.equal(true);
});
+ it('renders OverlayTrigger', () => {
+ const wrapper = shallow();
+ expect(wrapper.find(OverlayTrigger)).have.length(1);
+ });
});