mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
[SIP-4] replace chart ajax calls with SupersetClient (#5875)
* [deps] add @superset-ui/core * [superset-client] initialize SupersetClient in app setup * [superset-client] add abortcontroller-polyfill * [superset-client] replace all chart ajax calls with SupersetClient * [tests] add fetch-mock dep and helpers/setupSupersetClient.js * [superset client][charts][tests] fix and improve chartActions_spec * [deps] @superset-ui/core@^0.0.4 * [common] add better SupersetClient initialization error * [cypress] add readResponseBlob helper, fix broken fetch-based tests * [cypress] fix tests from rebase * [deps] @superset-ui/core@^0.0.5 * [cypress][fetch] fix controls test for fetch * [cypress][dashboard][fetch] fix filter test for fetch * [superset-client] configure protocol on init * yarn.lock * undo Chart.jsx revert * yarn again * [superset-client] fix chartAction unit tests
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { WORLD_HEALTH_DASHBOARD, CHECK_DASHBOARD_FAVORITE_ENDPOINT } from './dashboard.helper';
|
import { WORLD_HEALTH_DASHBOARD, CHECK_DASHBOARD_FAVORITE_ENDPOINT } from './dashboard.helper';
|
||||||
|
import readResponseBlob from '../../utils/readResponseBlob';
|
||||||
|
|
||||||
export default () => describe('top-level controls', () => {
|
export default () => describe('top-level controls', () => {
|
||||||
let sliceIds = [];
|
let sliceIds = [];
|
||||||
@@ -61,8 +62,9 @@ export default () => describe('top-level controls', () => {
|
|||||||
|
|
||||||
cy.wait(forceRefreshRequests).then((xhrs) => {
|
cy.wait(forceRefreshRequests).then((xhrs) => {
|
||||||
// is_cached in response should be false
|
// is_cached in response should be false
|
||||||
xhrs.forEach((xhr) => {
|
xhrs.forEach(async (xhr) => {
|
||||||
expect(xhr.response.body.is_cached).to.equal(false);
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
|
expect(responseBody.is_cached).to.equal(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ export default () => describe('dashboard filter', () => {
|
|||||||
.type('South Asia{enter}', { force: true });
|
.type('South Asia{enter}', { force: true });
|
||||||
|
|
||||||
cy.wait(aliases).then((requests) => {
|
cy.wait(aliases).then((requests) => {
|
||||||
requests.forEach((request) => {
|
requests.forEach((xhr) => {
|
||||||
const requestBody = request.request.body.substring('form_data='.length);
|
const requestFormData = xhr.request.body;
|
||||||
const requestParams = JSON.parse(decodeURIComponent(requestBody));
|
const requestParams = JSON.parse(requestFormData.get('form_data'));
|
||||||
expect(requestParams.extra_filters[0])
|
expect(requestParams.extra_filters[0])
|
||||||
.deep.eq({ col: 'region', op: 'in', val: ['South+Asia'] });
|
.deep.eq({ col: 'region', op: 'in', val: ['South Asia'] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import readResponseBlob from '../../utils/readResponseBlob';
|
||||||
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper';
|
||||||
|
|
||||||
export default () => describe('load', () => {
|
export default () => describe('load', () => {
|
||||||
@@ -24,9 +25,10 @@ export default () => describe('load', () => {
|
|||||||
it('should load dashboard', () => {
|
it('should load dashboard', () => {
|
||||||
// wait and verify one-by-one
|
// wait and verify one-by-one
|
||||||
cy.wait(aliases).then((requests) => {
|
cy.wait(aliases).then((requests) => {
|
||||||
requests.forEach((xhr) => {
|
requests.forEach(async (xhr) => {
|
||||||
expect(xhr.status).to.eq(200);
|
expect(xhr.status).to.eq(200);
|
||||||
expect(xhr.response.body).to.have.property('error', null);
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
|
expect(responseBody).to.have.property('error', null);
|
||||||
cy.get(`#slice-container-${xhr.response.body.form_data.slice_id}`);
|
cy.get(`#slice-container-${xhr.response.body.form_data.slice_id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
|
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
|
||||||
|
import readResponseBlob from '../../../utils/readResponseBlob';
|
||||||
|
|
||||||
// Big Number Total
|
// Big Number Total
|
||||||
|
|
||||||
@@ -42,10 +43,12 @@ export default () => describe('Big Number Total', () => {
|
|||||||
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC, groupby: ['state'] };
|
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC, groupby: ['state'] };
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
cy.wait(['@getJson']).then((data) => {
|
cy.wait(['@getJson']).then(async (xhr) => {
|
||||||
cy.verifyResponseCodes(data);
|
cy.verifyResponseCodes(xhr);
|
||||||
cy.verifySliceContainer();
|
cy.verifySliceContainer();
|
||||||
expect(data.response.body.query).not.contains(formData.groupby[0]);
|
|
||||||
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
|
expect(responseBody.query).not.contains(formData.groupby[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FORM_DATA_DEFAULTS, NUM_METRIC, SIMPLE_FILTER } from './shared.helper';
|
import { FORM_DATA_DEFAULTS, NUM_METRIC, SIMPLE_FILTER } from './shared.helper';
|
||||||
|
import readResponseBlob from '../../../utils/readResponseBlob';
|
||||||
|
|
||||||
// Table
|
// Table
|
||||||
|
|
||||||
@@ -59,10 +60,11 @@ export default () => describe('Table chart', () => {
|
|||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
|
|
||||||
cy.wait('@getJson').then((data) => {
|
cy.wait('@getJson').then(async (xhr) => {
|
||||||
cy.verifyResponseCodes(data);
|
cy.verifyResponseCodes(xhr);
|
||||||
cy.verifySliceContainer('table');
|
cy.verifySliceContainer('table');
|
||||||
expect(data.response.body.data.records.length).to.eq(limit);
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
|
expect(responseBody.data.records.length).to.eq(limit);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,10 +87,11 @@ export default () => describe('Table chart', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
cy.visitChartByParams(JSON.stringify(formData));
|
cy.visitChartByParams(JSON.stringify(formData));
|
||||||
cy.wait('@getJson').then((data) => {
|
cy.wait('@getJson').then(async (xhr) => {
|
||||||
cy.verifyResponseCodes(data);
|
cy.verifyResponseCodes(xhr);
|
||||||
cy.verifySliceContainer('table');
|
cy.verifySliceContainer('table');
|
||||||
const records = data.response.body.data.records;
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
|
const { records } = responseBody.data;
|
||||||
expect(records[0].num).greaterThan(records[records.length - 1].num);
|
expect(records[0].num).greaterThan(records[records.length - 1].num);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
// -- This is will overwrite an existing command --
|
// -- This is will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
import readResponseBlob from '../utils/readResponseBlob';
|
||||||
|
|
||||||
const BASE_EXPLORE_URL = '/superset/explore/?form_data=';
|
const BASE_EXPLORE_URL = '/superset/explore/?form_data=';
|
||||||
|
|
||||||
Cypress.Commands.add('login', () => {
|
Cypress.Commands.add('login', () => {
|
||||||
@@ -50,11 +52,14 @@ Cypress.Commands.add('visitChartByParams', (params) => {
|
|||||||
cy.visit(`${BASE_EXPLORE_URL}${params}`);
|
cy.visit(`${BASE_EXPLORE_URL}${params}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('verifyResponseCodes', (data) => {
|
Cypress.Commands.add('verifyResponseCodes', async (xhr) => {
|
||||||
// After a wait response check for valid response
|
// After a wait response check for valid response
|
||||||
expect(data.status).to.eq(200);
|
expect(xhr.status).to.eq(200);
|
||||||
if (data.response.body.error) {
|
|
||||||
expect(data.response.body.error).to.eq(null);
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
|
|
||||||
|
if (responseBody.error) {
|
||||||
|
expect(responseBody.error).to.eq(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,11 +77,12 @@ Cypress.Commands.add('verifySliceContainer', (chartSelector) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('verifySliceSuccess', ({ waitAlias, querySubstring, chartSelector }) => {
|
Cypress.Commands.add('verifySliceSuccess', ({ waitAlias, querySubstring, chartSelector }) => {
|
||||||
cy.wait(waitAlias).then((data) => {
|
cy.wait(waitAlias).then(async (xhr) => {
|
||||||
cy.verifyResponseCodes(data);
|
cy.verifyResponseCodes(xhr);
|
||||||
|
|
||||||
|
const responseBody = await readResponseBlob(xhr.response.body);
|
||||||
if (querySubstring) {
|
if (querySubstring) {
|
||||||
expect(data.response.body.query).contains(querySubstring);
|
expect(responseBody.query).contains(querySubstring);
|
||||||
}
|
}
|
||||||
|
|
||||||
cy.verifySliceContainer(chartSelector);
|
cy.verifySliceContainer(chartSelector);
|
||||||
|
|||||||
@@ -13,8 +13,11 @@
|
|||||||
// https://on.cypress.io/configuration
|
// https://on.cypress.io/configuration
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import './commands';
|
import './commands';
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// The following is a workaround for Cypress not supporting fetch.
|
||||||
// require('./commands')
|
// By setting window.fetch = null, we force the fetch polyfill to fall back
|
||||||
|
// to xhr as described here https://github.com/cypress-io/cypress/issues/95
|
||||||
|
Cypress.on('window:before:load', (win) => {
|
||||||
|
win.fetch = null; // eslint-disable-line no-param-reassign
|
||||||
|
});
|
||||||
|
|||||||
11
superset/assets/cypress/utils/readResponseBlob.js
Normal file
11
superset/assets/cypress/utils/readResponseBlob.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// This function returns a promise that resolves to the value
|
||||||
|
// of the passed response blob. It assumes the blob should be read as text,
|
||||||
|
// and that the response can be parsed as JSON. This is needed to read
|
||||||
|
// the value of any fetch-based response.
|
||||||
|
export default function readResponseBlob(blob) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(JSON.parse(reader.result));
|
||||||
|
reader.readAsText(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -51,9 +51,11 @@
|
|||||||
"@data-ui/sparkline": "^0.0.54",
|
"@data-ui/sparkline": "^0.0.54",
|
||||||
"@data-ui/theme": "^0.0.62",
|
"@data-ui/theme": "^0.0.62",
|
||||||
"@data-ui/xy-chart": "^0.0.61",
|
"@data-ui/xy-chart": "^0.0.61",
|
||||||
|
"@superset-ui/core": "^0.0.5",
|
||||||
"@vx/legend": "^0.0.170",
|
"@vx/legend": "^0.0.170",
|
||||||
"@vx/responsive": "0.0.172",
|
"@vx/responsive": "0.0.172",
|
||||||
"@vx/scale": "^0.0.165",
|
"@vx/scale": "^0.0.165",
|
||||||
|
"abortcontroller-polyfill": "^1.1.9",
|
||||||
"babel-register": "^6.24.1",
|
"babel-register": "^6.24.1",
|
||||||
"bootstrap": "^3.3.6",
|
"bootstrap": "^3.3.6",
|
||||||
"bootstrap-slider": "^10.0.0",
|
"bootstrap-slider": "^10.0.0",
|
||||||
@@ -158,6 +160,7 @@
|
|||||||
"eslint-plugin-prettier": "^2.6.0",
|
"eslint-plugin-prettier": "^2.6.0",
|
||||||
"eslint-plugin-react": "^7.0.1",
|
"eslint-plugin-react": "^7.0.1",
|
||||||
"exports-loader": "^0.7.0",
|
"exports-loader": "^0.7.0",
|
||||||
|
"fetch-mock": "^7.0.0-alpha.6",
|
||||||
"file-loader": "^1.1.11",
|
"file-loader": "^1.1.11",
|
||||||
"gl": "^4.0.4",
|
"gl": "^4.0.4",
|
||||||
"ignore-styles": "^5.0.1",
|
"ignore-styles": "^5.0.1",
|
||||||
|
|||||||
10
superset/assets/spec/helpers/setupSupersetClient.js
Normal file
10
superset/assets/spec/helpers/setupSupersetClient.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
|
|
||||||
|
export default function setupSupersetClient() {
|
||||||
|
// The following is needed to mock out SupersetClient requests
|
||||||
|
// including CSRF authentication and initialization
|
||||||
|
global.FormData = window.FormData; // used by SupersetClient
|
||||||
|
fetchMock.get('glob:*superset/csrf_token/*', { csrf_token: '1234' });
|
||||||
|
SupersetClient.configure({ protocol: 'http', host: 'localhost' }).init();
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint no-native-reassign: 0 */
|
/* eslint no-native-reassign: 0 */
|
||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
|
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
import { configure } from 'enzyme';
|
import { configure } from 'enzyme';
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
import Adapter from 'enzyme-adapter-react-16';
|
||||||
|
|||||||
@@ -1,37 +1,123 @@
|
|||||||
|
import fetchMock from 'fetch-mock';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import $ from 'jquery';
|
|
||||||
|
import { Logger } from '../../../src/logger';
|
||||||
|
import setupSupersetClient from '../../helpers/setupSupersetClient';
|
||||||
import * as exploreUtils from '../../../src/explore/exploreUtils';
|
import * as exploreUtils from '../../../src/explore/exploreUtils';
|
||||||
import * as actions from '../../../src/chart/chartAction';
|
import * as actions from '../../../src/chart/chartAction';
|
||||||
|
|
||||||
describe('chart actions', () => {
|
describe('chart actions', () => {
|
||||||
|
const MOCK_URL = '/mockURL';
|
||||||
let dispatch;
|
let dispatch;
|
||||||
let urlStub;
|
let urlStub;
|
||||||
let ajaxStub;
|
let loggerStub;
|
||||||
let request;
|
|
||||||
|
const setupDefaultFetchMock = () => {
|
||||||
|
fetchMock.post(MOCK_URL, { json: {} }, { overwriteRoutes: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
setupSupersetClient();
|
||||||
|
setupDefaultFetchMock();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(fetchMock.restore);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dispatch = sinon.spy();
|
dispatch = sinon.spy();
|
||||||
urlStub = sinon.stub(exploreUtils, 'getExploreUrlAndPayload')
|
urlStub = sinon
|
||||||
.callsFake(() => ({ url: 'mockURL', payload: {} }));
|
.stub(exploreUtils, 'getExploreUrlAndPayload')
|
||||||
ajaxStub = sinon.stub($, 'ajax');
|
.callsFake(() => ({ url: MOCK_URL, payload: {} }));
|
||||||
|
loggerStub = sinon.stub(Logger, 'append');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
urlStub.restore();
|
urlStub.restore();
|
||||||
ajaxStub.restore();
|
loggerStub.restore();
|
||||||
|
fetchMock.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle query timeout', () => {
|
it('should dispatch CHART_UPDATE_STARTED action before the query', () => {
|
||||||
ajaxStub.rejects({ statusText: 'timeout' });
|
const actionThunk = actions.runQuery({});
|
||||||
request = actions.runQuery({});
|
|
||||||
const promise = request(dispatch, sinon.stub().returns({
|
return actionThunk(dispatch).then(() => {
|
||||||
explore: {
|
// chart update, trigger query, update form data, success
|
||||||
controls: [],
|
expect(dispatch.callCount).toBe(4);
|
||||||
},
|
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||||
}));
|
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
|
||||||
promise.then(() => {
|
|
||||||
expect(dispatch.callCount).toBe(3);
|
return Promise.resolve();
|
||||||
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch TRIGGER_QUERY action with the query', () => {
|
||||||
|
const actionThunk = actions.runQuery({});
|
||||||
|
return actionThunk(dispatch).then(() => {
|
||||||
|
// chart update, trigger query, update form data, success
|
||||||
|
expect(dispatch.callCount).toBe(4);
|
||||||
|
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||||
|
expect(dispatch.args[1][0].type).toBe(actions.TRIGGER_QUERY);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => {
|
||||||
|
const actionThunk = actions.runQuery({});
|
||||||
|
return actionThunk(dispatch).then(() => {
|
||||||
|
// chart update, trigger query, update form data, success
|
||||||
|
expect(dispatch.callCount).toBe(4);
|
||||||
|
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||||
|
expect(dispatch.args[2][0].type).toBe(actions.UPDATE_QUERY_FORM_DATA);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => {
|
||||||
|
const actionThunk = actions.runQuery({});
|
||||||
|
return actionThunk(dispatch).then(() => {
|
||||||
|
// chart update, trigger query, update form data, success
|
||||||
|
expect(dispatch.callCount).toBe(4);
|
||||||
|
expect(fetchMock.calls(MOCK_URL)).toHaveLength(1);
|
||||||
|
expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_SUCCEEDED);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should CHART_UPDATE_TIMEOUT action upon query timeout', () => {
|
||||||
|
const unresolvingPromise = new Promise(() => {});
|
||||||
|
fetchMock.post(MOCK_URL, () => unresolvingPromise, { overwriteRoutes: true });
|
||||||
|
|
||||||
|
const timeoutInSec = 1 / 1000;
|
||||||
|
const actionThunk = actions.runQuery({}, false, timeoutInSec);
|
||||||
|
|
||||||
|
return actionThunk(dispatch).then(() => {
|
||||||
|
// chart update, trigger query, update form data, fail
|
||||||
|
expect(dispatch.callCount).toBe(4);
|
||||||
|
expect(dispatch.args[3][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
|
||||||
|
setupDefaultFetchMock();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => {
|
||||||
|
fetchMock.post(MOCK_URL, { throws: { error: 'misc error' } }, { overwriteRoutes: true });
|
||||||
|
|
||||||
|
const timeoutInSec = 1 / 1000;
|
||||||
|
const actionThunk = actions.runQuery({}, false, timeoutInSec);
|
||||||
|
|
||||||
|
return actionThunk(dispatch).then(() => {
|
||||||
|
// chart update, trigger query, update form data, fail
|
||||||
|
expect(dispatch.callCount).toBe(4);
|
||||||
|
const updateFailedAction = dispatch.args[3][0];
|
||||||
|
expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED);
|
||||||
|
expect(updateFailedAction.queryResponse.error).toBe('misc error');
|
||||||
|
setupDefaultFetchMock();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ const propTypes = {
|
|||||||
chartUpdateEndTime: PropTypes.number,
|
chartUpdateEndTime: PropTypes.number,
|
||||||
chartUpdateStartTime: PropTypes.number,
|
chartUpdateStartTime: PropTypes.number,
|
||||||
latestQueryFormData: PropTypes.object,
|
latestQueryFormData: PropTypes.object,
|
||||||
queryRequest: PropTypes.object,
|
|
||||||
queryResponse: PropTypes.object,
|
queryResponse: PropTypes.object,
|
||||||
lastRendered: PropTypes.number,
|
lastRendered: PropTypes.number,
|
||||||
triggerQuery: PropTypes.bool,
|
triggerQuery: PropTypes.bool,
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import URI from 'urijs';
|
/* global window, AbortController */
|
||||||
|
/* eslint no-undef: 'error' */
|
||||||
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../explore/exploreUtils';
|
import { getExploreUrlAndPayload, getAnnotationJsonUrl } from '../explore/exploreUtils';
|
||||||
import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
|
import { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
|
||||||
|
import { addDangerToast } from '../messageToasts/actions';
|
||||||
import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger';
|
import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger';
|
||||||
import { COMMON_ERR_MESSAGES } from '../utils/common';
|
import { COMMON_ERR_MESSAGES } from '../utils/common';
|
||||||
import { t } from '../locales';
|
import { t } from '../locales';
|
||||||
|
|
||||||
const $ = (window.$ = require('jquery'));
|
|
||||||
|
|
||||||
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
|
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
|
||||||
export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
|
export function chartUpdateStarted(queryController, latestQueryFormData, key) {
|
||||||
return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key };
|
return { type: CHART_UPDATE_STARTED, queryController, latestQueryFormData, key };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
|
export const CHART_UPDATE_SUCCEEDED = 'CHART_UPDATE_SUCCEEDED';
|
||||||
@@ -54,8 +54,8 @@ export function annotationQuerySuccess(annotation, queryResponse, key) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
|
export const ANNOTATION_QUERY_STARTED = 'ANNOTATION_QUERY_STARTED';
|
||||||
export function annotationQueryStarted(annotation, queryRequest, key) {
|
export function annotationQueryStarted(annotation, queryController, key) {
|
||||||
return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
|
return { type: ANNOTATION_QUERY_STARTED, annotation, queryController, key };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
|
export const ANNOTATION_QUERY_FAILED = 'ANNOTATION_QUERY_FAILED';
|
||||||
@@ -85,18 +85,21 @@ export function runAnnotationQuery(annotation, timeout = 60, formData = null, ke
|
|||||||
);
|
);
|
||||||
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
|
const isNative = annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE;
|
||||||
const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
|
const url = getAnnotationJsonUrl(annotation.value, sliceFormData, isNative);
|
||||||
const queryRequest = $.ajax({
|
const controller = new AbortController();
|
||||||
|
const { signal } = controller;
|
||||||
|
|
||||||
|
dispatch(annotationQueryStarted(annotation, controller, sliceKey));
|
||||||
|
|
||||||
|
return SupersetClient.get({
|
||||||
url,
|
url,
|
||||||
dataType: 'json',
|
signal,
|
||||||
timeout: timeout * 1000,
|
timeout: timeout * 1000,
|
||||||
});
|
})
|
||||||
dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
|
.then(({ json }) => dispatch(annotationQuerySuccess(annotation, json, sliceKey)))
|
||||||
return queryRequest
|
|
||||||
.then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.statusText === 'timeout') {
|
if (err.statusText === 'timeout') {
|
||||||
dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
|
dispatch(annotationQueryFailed(annotation, { error: 'Query Timeout' }, sliceKey));
|
||||||
} else if ((err.responseJSON.error || '').toLowerCase().startsWith('no data')) {
|
} else if ((err.responseJSON.error || '').toLowerCase().includes('no data')) {
|
||||||
dispatch(annotationQuerySuccess(annotation, err, sliceKey));
|
dispatch(annotationQuerySuccess(annotation, err, sliceKey));
|
||||||
} else if (err.statusText !== 'abort') {
|
} else if (err.statusText !== 'abort') {
|
||||||
dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
|
dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
|
||||||
@@ -135,30 +138,30 @@ export function runQuery(formData, force = false, timeout = 60, key) {
|
|||||||
force,
|
force,
|
||||||
});
|
});
|
||||||
const logStart = Logger.getTimestamp();
|
const logStart = Logger.getTimestamp();
|
||||||
const queryRequest = $.ajax({
|
const controller = new AbortController();
|
||||||
type: 'POST',
|
const { signal } = controller;
|
||||||
|
|
||||||
|
dispatch(chartUpdateStarted(controller, payload, key));
|
||||||
|
|
||||||
|
const queryPromise = SupersetClient.post({
|
||||||
url,
|
url,
|
||||||
dataType: 'json',
|
postPayload: { form_data: payload },
|
||||||
data: {
|
signal,
|
||||||
form_data: JSON.stringify(payload),
|
|
||||||
},
|
|
||||||
timeout: timeout * 1000,
|
timeout: timeout * 1000,
|
||||||
});
|
})
|
||||||
const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
|
.then(({ json }) => {
|
||||||
.then(() => queryRequest)
|
|
||||||
.then((queryResponse) => {
|
|
||||||
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
||||||
slice_id: key,
|
slice_id: key,
|
||||||
is_cached: queryResponse.is_cached,
|
is_cached: json.is_cached,
|
||||||
force_refresh: force,
|
force_refresh: force,
|
||||||
row_count: queryResponse.rowcount,
|
row_count: json.rowcount,
|
||||||
datasource: formData.datasource,
|
datasource: formData.datasource,
|
||||||
start_offset: logStart,
|
start_offset: logStart,
|
||||||
duration: Logger.getTimestamp() - logStart,
|
duration: Logger.getTimestamp() - logStart,
|
||||||
has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
|
has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
|
||||||
viz_type: formData.viz_type,
|
viz_type: formData.viz_type,
|
||||||
});
|
});
|
||||||
return dispatch(chartUpdateSucceeded(queryResponse, key));
|
return dispatch(chartUpdateSucceeded(json, key));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
||||||
@@ -170,30 +173,30 @@ export function runQuery(formData, force = false, timeout = 60, key) {
|
|||||||
});
|
});
|
||||||
if (err.statusText === 'timeout') {
|
if (err.statusText === 'timeout') {
|
||||||
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
|
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
|
||||||
} else if (err.statusText === 'abort') {
|
} else if (err.statusText === 'AbortError') {
|
||||||
dispatch(chartUpdateStopped(key));
|
dispatch(chartUpdateStopped(key));
|
||||||
} else {
|
} else {
|
||||||
let errObject;
|
let errObject = err;
|
||||||
if (err.responseJSON) {
|
if (err.responseJSON) {
|
||||||
errObject = err.responseJSON;
|
errObject = err.responseJSON;
|
||||||
} else if (err.stack) {
|
} else if (err.stack) {
|
||||||
errObject = {
|
errObject = {
|
||||||
error: t('Unexpected error: ') + err.description,
|
error:
|
||||||
|
t('Unexpected error: ') +
|
||||||
|
(err.description || t('(no description, click to see stack trace)')),
|
||||||
stacktrace: err.stack,
|
stacktrace: err.stack,
|
||||||
};
|
};
|
||||||
} else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
|
} else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
|
||||||
errObject = {
|
errObject = {
|
||||||
error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
|
error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
errObject = {
|
|
||||||
error: t('Unexpected error.'),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
dispatch(chartUpdateFailed(errObject, key));
|
dispatch(chartUpdateFailed(errObject, key));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const annotationLayers = formData.annotation_layers || [];
|
const annotationLayers = formData.annotation_layers || [];
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
queryPromise,
|
queryPromise,
|
||||||
dispatch(triggerQuery(false, key)),
|
dispatch(triggerQuery(false, key)),
|
||||||
@@ -203,29 +206,21 @@ export function runQuery(formData, force = false, timeout = 60, key) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SQLLAB_REDIRECT_FAILED = 'SQLLAB_REDIRECT_FAILED';
|
|
||||||
export function sqllabRedirectFailed(error, key) {
|
|
||||||
return { type: SQLLAB_REDIRECT_FAILED, error, key };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redirectSQLLab(formData) {
|
export function redirectSQLLab(formData) {
|
||||||
return function (dispatch) {
|
return (dispatch) => {
|
||||||
const { url, payload } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
|
const { url } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
|
||||||
$.ajax({
|
return SupersetClient.get({ url })
|
||||||
type: 'POST',
|
.then(({ json }) => {
|
||||||
url,
|
const redirectUrl = new URL(window.location);
|
||||||
data: {
|
redirectUrl.pathname = '/superset/sqllab';
|
||||||
form_data: JSON.stringify(payload),
|
for (const key of redirectUrl.searchParams.keys()) {
|
||||||
},
|
redirectUrl.searchParams.delete(key);
|
||||||
success: (response) => {
|
}
|
||||||
const redirectUrl = new URI(window.location);
|
redirectUrl.searchParams.set('datasourceKey', formData.datasource);
|
||||||
redirectUrl
|
redirectUrl.searchParams.set('sql', json.query);
|
||||||
.pathname('/superset/sqllab')
|
window.open(redirectUrl.href, '_blank');
|
||||||
.search({ datasourceKey: formData.datasource, sql: response.query });
|
})
|
||||||
window.open(redirectUrl.href(), '_blank');
|
.catch(() => dispatch(addDangerToast(t('An error occurred while loading the SQL'))));
|
||||||
},
|
|
||||||
error: (xhr, status, error) => dispatch(sqllabRedirectFailed(error, formData.slice_id)),
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable global-require */
|
/* eslint global-require: 0, no-console: 0 */
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
|
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||||
|
|
||||||
import airbnb from './modules/colorSchemes/airbnb';
|
import airbnb from './modules/colorSchemes/airbnb';
|
||||||
import categoricalSchemes from './modules/colorSchemes/categorical';
|
import categoricalSchemes from './modules/colorSchemes/categorical';
|
||||||
import lyft from './modules/colorSchemes/lyft';
|
import lyft from './modules/colorSchemes/lyft';
|
||||||
import { getInstance } from './modules/ColorSchemeManager';
|
import { getInstance } from './modules/ColorSchemeManager';
|
||||||
import { toggleCheckbox } from './modules/utils';
|
import { toggleCheckbox } from './modules/utils';
|
||||||
|
|
||||||
// Everything imported in this file ends up in the common entry file
|
|
||||||
// be mindful of double-imports
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$(':checkbox[data-checkbox-api-prefix]').change(function () {
|
$(':checkbox[data-checkbox-api-prefix]').change(function () {
|
||||||
const $this = $(this);
|
const $this = $(this);
|
||||||
@@ -22,10 +22,9 @@ $(document).ready(function () {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const targetUrl = ev.currentTarget.href;
|
const targetUrl = ev.currentTarget.href;
|
||||||
$.ajax(targetUrl)
|
$.ajax(targetUrl).then(() => {
|
||||||
.then(() => {
|
location.reload();
|
||||||
location.reload();
|
});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -37,9 +36,18 @@ getInstance()
|
|||||||
.setDefaultSchemeName('bnbColors');
|
.setDefaultSchemeName('bnbColors');
|
||||||
|
|
||||||
export function appSetup() {
|
export function appSetup() {
|
||||||
// A set of hacks to allow apps to run within a FAB template
|
// A set of hacks to allow apps to run within a FAB template
|
||||||
// this allows for the server side generated menus to function
|
// this allows for the server side generated menus to function
|
||||||
window.$ = $;
|
window.$ = $;
|
||||||
window.jQuery = $;
|
window.jQuery = $;
|
||||||
require('bootstrap');
|
require('bootstrap');
|
||||||
|
|
||||||
|
SupersetClient.configure({
|
||||||
|
protocol: (window.location && window.location.protocol) || '',
|
||||||
|
host: (window.location && window.location.host) || '',
|
||||||
|
})
|
||||||
|
.init()
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('Error initializing SupersetClient', error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class ExploreChartPanel extends React.PureComponent {
|
|||||||
latestQueryFormData={chart.latestQueryFormData}
|
latestQueryFormData={chart.latestQueryFormData}
|
||||||
lastRendered={chart.lastRendered}
|
lastRendered={chart.lastRendered}
|
||||||
queryResponse={chart.queryResponse}
|
queryResponse={chart.queryResponse}
|
||||||
queryRequest={chart.queryRequest}
|
queryController={chart.queryController}
|
||||||
triggerQuery={chart.triggerQuery}
|
triggerQuery={chart.triggerQuery}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ class ExploreViewContainer extends React.Component {
|
|||||||
this.addHistory = this.addHistory.bind(this);
|
this.addHistory = this.addHistory.bind(this);
|
||||||
this.handleResize = this.handleResize.bind(this);
|
this.handleResize = this.handleResize.bind(this);
|
||||||
this.handlePopstate = this.handlePopstate.bind(this);
|
this.handlePopstate = this.handlePopstate.bind(this);
|
||||||
|
this.onStop = this.onStop.bind(this);
|
||||||
|
this.onQuery = this.onQuery.bind(this);
|
||||||
|
this.toggleModal = this.toggleModal.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -124,7 +127,9 @@ class ExploreViewContainer extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onStop() {
|
onStop() {
|
||||||
return this.props.chart.queryRequest.abort();
|
if (this.props.chart && this.props.chart.queryController) {
|
||||||
|
this.props.chart.queryController.abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getWidth() {
|
getWidth() {
|
||||||
@@ -262,7 +267,7 @@ class ExploreViewContainer extends React.Component {
|
|||||||
>
|
>
|
||||||
{this.state.showModal && (
|
{this.state.showModal && (
|
||||||
<SaveModal
|
<SaveModal
|
||||||
onHide={this.toggleModal.bind(this)}
|
onHide={this.toggleModal}
|
||||||
actions={this.props.actions}
|
actions={this.props.actions}
|
||||||
form_data={this.props.form_data}
|
form_data={this.props.form_data}
|
||||||
/>
|
/>
|
||||||
@@ -271,9 +276,9 @@ class ExploreViewContainer extends React.Component {
|
|||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<QueryAndSaveBtns
|
<QueryAndSaveBtns
|
||||||
canAdd="True"
|
canAdd="True"
|
||||||
onQuery={this.onQuery.bind(this)}
|
onQuery={this.onQuery}
|
||||||
onSave={this.toggleModal.bind(this)}
|
onSave={this.toggleModal}
|
||||||
onStop={this.onStop.bind(this)}
|
onStop={this.onStop}
|
||||||
loading={this.props.chart.chartStatus === 'loading'}
|
loading={this.props.chart.chartStatus === 'loading'}
|
||||||
chartIsStale={this.state.chartIsStale}
|
chartIsStale={this.state.chartIsStale}
|
||||||
errorMessage={this.renderErrorMessage()}
|
errorMessage={this.renderErrorMessage()}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { CompactPicker } from 'react-color';
|
import { CompactPicker } from 'react-color';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
import $ from 'jquery';
|
|
||||||
import mathjs from 'mathjs';
|
import mathjs from 'mathjs';
|
||||||
|
|
||||||
|
import { SupersetClient } from '@superset-ui/core';
|
||||||
import SelectControl from './SelectControl';
|
import SelectControl from './SelectControl';
|
||||||
import TextControl from './TextControl';
|
import TextControl from './TextControl';
|
||||||
import CheckboxControl from './CheckboxControl';
|
import CheckboxControl from './CheckboxControl';
|
||||||
@@ -83,10 +82,24 @@ const defaultProps = {
|
|||||||
export default class AnnotationLayer extends React.PureComponent {
|
export default class AnnotationLayer extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
const { name, annotationType, sourceType,
|
const {
|
||||||
color, opacity, style, width, showMarkers, hideLine, value,
|
name,
|
||||||
overrides, show, titleColumn, descriptionColumns,
|
annotationType,
|
||||||
timeColumn, intervalEndColumn } = props;
|
sourceType,
|
||||||
|
color,
|
||||||
|
opacity,
|
||||||
|
style,
|
||||||
|
width,
|
||||||
|
showMarkers,
|
||||||
|
hideLine,
|
||||||
|
value,
|
||||||
|
overrides,
|
||||||
|
show,
|
||||||
|
titleColumn,
|
||||||
|
descriptionColumns,
|
||||||
|
timeColumn,
|
||||||
|
intervalEndColumn,
|
||||||
|
} = props;
|
||||||
this.state = {
|
this.state = {
|
||||||
// base
|
// base
|
||||||
name,
|
name,
|
||||||
@@ -119,8 +132,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
this.applyAnnotation = this.applyAnnotation.bind(this);
|
this.applyAnnotation = this.applyAnnotation.bind(this);
|
||||||
this.fetchOptions = this.fetchOptions.bind(this);
|
this.fetchOptions = this.fetchOptions.bind(this);
|
||||||
this.handleAnnotationType = this.handleAnnotationType.bind(this);
|
this.handleAnnotationType = this.handleAnnotationType.bind(this);
|
||||||
this.handleAnnotationSourceType =
|
this.handleAnnotationSourceType = this.handleAnnotationSourceType.bind(this);
|
||||||
this.handleAnnotationSourceType.bind(this);
|
|
||||||
this.handleValue = this.handleValue.bind(this);
|
this.handleValue = this.handleValue.bind(this);
|
||||||
this.isValidForm = this.isValidForm.bind(this);
|
this.isValidForm = this.isValidForm.bind(this);
|
||||||
}
|
}
|
||||||
@@ -139,7 +151,10 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
isValidFormula(value, annotationType) {
|
isValidFormula(value, annotationType) {
|
||||||
if (annotationType === AnnotationTypes.FORMULA) {
|
if (annotationType === AnnotationTypes.FORMULA) {
|
||||||
try {
|
try {
|
||||||
mathjs.parse(value).compile().eval({ x: 0 });
|
mathjs
|
||||||
|
.parse(value)
|
||||||
|
.compile()
|
||||||
|
.eval({ x: 0 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -148,10 +163,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isValidForm() {
|
isValidForm() {
|
||||||
const {
|
const { name, annotationType, sourceType, value, timeColumn, intervalEndColumn } = this.state;
|
||||||
name, annotationType, sourceType,
|
|
||||||
value, timeColumn, intervalEndColumn,
|
|
||||||
} = this.state;
|
|
||||||
const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)];
|
const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)];
|
||||||
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) {
|
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||||
if (annotationType === AnnotationTypes.EVENT) {
|
if (annotationType === AnnotationTypes.EVENT) {
|
||||||
@@ -166,7 +178,6 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
return !errors.filter(x => x).length;
|
return !errors.filter(x => x).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleAnnotationType(annotationType) {
|
handleAnnotationType(annotationType) {
|
||||||
this.setState({
|
this.setState({
|
||||||
annotationType,
|
annotationType,
|
||||||
@@ -199,31 +210,25 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
fetchOptions(annotationType, sourceType, isLoadingOptions) {
|
fetchOptions(annotationType, sourceType, isLoadingOptions) {
|
||||||
if (isLoadingOptions === true) {
|
if (isLoadingOptions === true) {
|
||||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||||
$.ajax({
|
SupersetClient.get({ endpoint: '/annotationlayermodelview/api/read?' }).then(({ json }) => {
|
||||||
type: 'GET',
|
const layers = json
|
||||||
url: '/annotationlayermodelview/api/read?',
|
? json.result.map(layer => ({
|
||||||
}).then((data) => {
|
value: layer.id,
|
||||||
const layers = data ? data.result.map(layer => ({
|
label: layer.name,
|
||||||
value: layer.id,
|
}))
|
||||||
label: layer.name,
|
: [];
|
||||||
})) : [];
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoadingOptions: false,
|
isLoadingOptions: false,
|
||||||
valueOptions: layers,
|
valueOptions: layers,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (requiresQuery(sourceType)) {
|
} else if (requiresQuery(sourceType)) {
|
||||||
$.ajax({
|
SupersetClient.get({ endpoint: '/superset/user_slices' }).then(({ json }) =>
|
||||||
type: 'GET',
|
|
||||||
url: '/superset/user_slices',
|
|
||||||
}).then(data =>
|
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoadingOptions: false,
|
isLoadingOptions: false,
|
||||||
valueOptions: data.filter(
|
valueOptions: json
|
||||||
x => getSupportedSourceTypes(annotationType)
|
.filter(x => getSupportedSourceTypes(annotationType).find(v => v === x.viz_type))
|
||||||
.find(v => v === x.viz_type))
|
.map(x => ({ value: x.id, label: x.title, slice: x })),
|
||||||
.map(x => ({ value: x.id, label: x.title, slice: x }),
|
|
||||||
),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -266,26 +271,26 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderValueConfiguration() {
|
renderValueConfiguration() {
|
||||||
const { annotationType, sourceType, value,
|
const { annotationType, sourceType, value, valueOptions, isLoadingOptions } = this.state;
|
||||||
valueOptions, isLoadingOptions } = this.state;
|
|
||||||
let label = '';
|
let label = '';
|
||||||
let description = '';
|
let description = '';
|
||||||
if (requiresQuery(sourceType)) {
|
if (requiresQuery(sourceType)) {
|
||||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||||
label = t('Annotation Layer');
|
label = 'Annotation Layer';
|
||||||
description = t('Select the Annotation Layer you would like to use.');
|
description = 'Select the Annotation Layer you would like to use.';
|
||||||
} else {
|
} else {
|
||||||
label = t('Chart');
|
label = label = t('Chart');
|
||||||
description = `Use a pre defined Superset Chart as a source for annotations and overlays.
|
description = `Use a pre defined Superset Chart as a source for annotations and overlays.
|
||||||
'your chart must be one of these visualization types:
|
your chart must be one of these visualization types:
|
||||||
'[${getSupportedSourceTypes(annotationType)
|
[${getSupportedSourceTypes(annotationType)
|
||||||
.map(x => ((x in vizTypes && 'label' in vizTypes[x]) ? vizTypes[x].label : '')).join(', ')}]'`;
|
.map(x => (x in vizTypes && 'label' in vizTypes[x] ? vizTypes[x].label : ''))
|
||||||
|
.join(', ')}]`;
|
||||||
}
|
}
|
||||||
} else if (annotationType === AnnotationTypes.FORMULA) {
|
} else if (annotationType === AnnotationTypes.FORMULA) {
|
||||||
label = t('Formula');
|
label = 'Formula';
|
||||||
description = t(`Expects a formula with depending time parameter 'x'
|
description = `Expects a formula with depending time parameter 'x'
|
||||||
in milliseconds since epoch. mathjs is used to evaluate the formulas.
|
in milliseconds since epoch. mathjs is used to evaluate the formulas.
|
||||||
Example: '2x+5'`);
|
Example: '2x+5'`;
|
||||||
}
|
}
|
||||||
if (requiresQuery(sourceType)) {
|
if (requiresQuery(sourceType)) {
|
||||||
return (
|
return (
|
||||||
@@ -300,10 +305,11 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
isLoading={isLoadingOptions}
|
isLoading={isLoadingOptions}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.handleValue}
|
onChange={this.handleValue}
|
||||||
validationErrors={!value ? [t('Mandatory')] : []}
|
validationErrors={!value ? ['Mandatory'] : []}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} if (annotationType === AnnotationTypes.FORMULA) {
|
}
|
||||||
|
if (annotationType === AnnotationTypes.FORMULA) {
|
||||||
return (
|
return (
|
||||||
<TextControl
|
<TextControl
|
||||||
name="annotation-layer-value"
|
name="annotation-layer-value"
|
||||||
@@ -314,7 +320,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
placeholder=""
|
placeholder=""
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.handleValue}
|
onChange={this.handleValue}
|
||||||
validationErrors={this.isValidFormula(value, annotationType) ? [t('Bad formula.')] : []}
|
validationErrors={this.isValidFormula(value, annotationType) ? ['Bad formula.'] : []}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -322,37 +328,43 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSliceConfiguration() {
|
renderSliceConfiguration() {
|
||||||
const { annotationType, sourceType, value, valueOptions, overrides, titleColumn,
|
const {
|
||||||
timeColumn, intervalEndColumn, descriptionColumns } = this.state;
|
annotationType,
|
||||||
|
sourceType,
|
||||||
|
value,
|
||||||
|
valueOptions,
|
||||||
|
overrides,
|
||||||
|
titleColumn,
|
||||||
|
timeColumn,
|
||||||
|
intervalEndColumn,
|
||||||
|
descriptionColumns,
|
||||||
|
} = this.state;
|
||||||
const slice = (valueOptions.find(x => x.value === value) || {}).slice;
|
const slice = (valueOptions.find(x => x.value === value) || {}).slice;
|
||||||
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) {
|
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) {
|
||||||
const columns = (slice.data.groupby || []).concat(
|
const columns = (slice.data.groupby || [])
|
||||||
(slice.data.all_columns || [])).map(x => ({ value: x, label: x }));
|
.concat(slice.data.all_columns || [])
|
||||||
const timeColumnOptions = slice.data.include_time ?
|
.map(x => ({ value: x, label: x }));
|
||||||
[{ value: '__timestamp', label: '__timestamp' }].concat(columns) : columns;
|
const timeColumnOptions = slice.data.include_time
|
||||||
|
? [{ value: '__timestamp', label: '__timestamp' }].concat(columns)
|
||||||
|
: columns;
|
||||||
return (
|
return (
|
||||||
<div style={{ marginRight: '2rem' }}>
|
<div style={{ marginRight: '2rem' }}>
|
||||||
<PopoverSection
|
<PopoverSection
|
||||||
isSelected
|
isSelected
|
||||||
onSelect={() => {
|
onSelect={() => {}}
|
||||||
}}
|
|
||||||
title="Annotation Slice Configuration"
|
title="Annotation Slice Configuration"
|
||||||
info={
|
info={`This section allows you to configure how to use the slice
|
||||||
`This section allows you to configure how to use the slice
|
to generate annotations.`}
|
||||||
to generate annotations.`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{
|
{(annotationType === AnnotationTypes.EVENT ||
|
||||||
(
|
annotationType === AnnotationTypes.INTERVAL) && (
|
||||||
annotationType === AnnotationTypes.EVENT ||
|
|
||||||
annotationType === AnnotationTypes.INTERVAL
|
|
||||||
) &&
|
|
||||||
<SelectControl
|
<SelectControl
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-time-column"
|
name="annotation-layer-time-column"
|
||||||
label={
|
label={
|
||||||
annotationType === AnnotationTypes.INTERVAL ?
|
annotationType === AnnotationTypes.INTERVAL
|
||||||
'Interval Start column' : 'Event Time Column'
|
? 'Interval Start column'
|
||||||
|
: 'Event Time Column'
|
||||||
}
|
}
|
||||||
description={'This column must contain date/time information.'}
|
description={'This column must contain date/time information.'}
|
||||||
validationErrors={!timeColumn ? ['Mandatory'] : []}
|
validationErrors={!timeColumn ? ['Mandatory'] : []}
|
||||||
@@ -361,9 +373,8 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
value={timeColumn}
|
value={timeColumn}
|
||||||
onChange={v => this.setState({ timeColumn: v })}
|
onChange={v => this.setState({ timeColumn: v })}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{
|
{annotationType === AnnotationTypes.INTERVAL && (
|
||||||
annotationType === AnnotationTypes.INTERVAL &&
|
|
||||||
<SelectControl
|
<SelectControl
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-intervalEnd"
|
name="annotation-layer-intervalEnd"
|
||||||
@@ -374,20 +385,17 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
value={intervalEndColumn}
|
value={intervalEndColumn}
|
||||||
onChange={v => this.setState({ intervalEndColumn: v })}
|
onChange={v => this.setState({ intervalEndColumn: v })}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<SelectControl
|
<SelectControl
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-title"
|
name="annotation-layer-title"
|
||||||
label="Title Column"
|
label="Title Column"
|
||||||
description={'Pick a title for you annotation.'}
|
description={'Pick a title for you annotation.'}
|
||||||
options={
|
options={[{ value: '', label: 'None' }].concat(columns)}
|
||||||
[{ value: '', label: 'None' }].concat(columns)
|
|
||||||
}
|
|
||||||
value={titleColumn}
|
value={titleColumn}
|
||||||
onChange={v => this.setState({ titleColumn: v })}
|
onChange={v => this.setState({ titleColumn: v })}
|
||||||
/>
|
/>
|
||||||
{
|
{annotationType !== AnnotationTypes.TIME_SERIES && (
|
||||||
annotationType !== AnnotationTypes.TIME_SERIES &&
|
|
||||||
<SelectControl
|
<SelectControl
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-title"
|
name="annotation-layer-title"
|
||||||
@@ -395,13 +403,11 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
description={`Pick one or more columns that should be shown in the
|
description={`Pick one or more columns that should be shown in the
|
||||||
annotation. If you don't select a column all of them will be shown.`}
|
annotation. If you don't select a column all of them will be shown.`}
|
||||||
multi
|
multi
|
||||||
options={
|
options={columns}
|
||||||
columns
|
|
||||||
}
|
|
||||||
value={descriptionColumns}
|
value={descriptionColumns}
|
||||||
onChange={v => this.setState({ descriptionColumns: v })}
|
onChange={v => this.setState({ descriptionColumns: v })}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
<div style={{ marginTop: '1rem' }}>
|
<div style={{ marginTop: '1rem' }}>
|
||||||
<CheckboxControl
|
<CheckboxControl
|
||||||
hovered
|
hovered
|
||||||
@@ -473,14 +479,17 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ('');
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDisplayConfiguration() {
|
renderDisplayConfiguration() {
|
||||||
const { color, opacity, style, width, showMarkers, hideLine, annotationType } = this.state;
|
const { color, opacity, style, width, showMarkers, hideLine, annotationType } = this.state;
|
||||||
const colorScheme = [...getScheme(this.props.colorScheme)];
|
const colorScheme = [...getScheme(this.props.colorScheme)];
|
||||||
if (color && color !== AUTOMATIC_COLOR &&
|
if (
|
||||||
!colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
|
color &&
|
||||||
|
color !== AUTOMATIC_COLOR &&
|
||||||
|
!colorScheme.find(x => x.toLowerCase() === color.toLowerCase())
|
||||||
|
) {
|
||||||
colorScheme.push(color);
|
colorScheme.push(color);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -493,12 +502,12 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
<SelectControl
|
<SelectControl
|
||||||
name="annotation-layer-stroke"
|
name="annotation-layer-stroke"
|
||||||
label={t('Style')}
|
label={t('Style')}
|
||||||
// see '../../../visualizations/nvd3_vis.css'
|
// see '../../../visualizations/nvd3_vis.css'
|
||||||
options={[
|
options={[
|
||||||
{ value: 'solid', label: 'Solid' },
|
{ value: 'solid', label: 'Solid' },
|
||||||
{ value: 'dashed', label: 'Dashed' },
|
{ value: 'dashed', label: 'Dashed' },
|
||||||
{ value: 'longDashed', label: 'Long Dashed' },
|
{ value: 'longDashed', label: 'Long Dashed' },
|
||||||
{ value: 'dotted', label: 'Dotted' },
|
{ value: 'dotted', label: 'Dotted' },
|
||||||
]}
|
]}
|
||||||
value={style}
|
value={style}
|
||||||
onChange={v => this.setState({ style: v })}
|
onChange={v => this.setState({ style: v })}
|
||||||
@@ -506,12 +515,12 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
<SelectControl
|
<SelectControl
|
||||||
name="annotation-layer-opacity"
|
name="annotation-layer-opacity"
|
||||||
label={t('Opacity')}
|
label={t('Opacity')}
|
||||||
// see '../../../visualizations/nvd3_vis.css'
|
// see '../../../visualizations/nvd3_vis.css'
|
||||||
options={[
|
options={[
|
||||||
{ value: '', label: 'Solid' },
|
{ value: '', label: 'Solid' },
|
||||||
{ value: 'opacityLow', label: '0.2' },
|
{ value: 'opacityLow', label: '0.2' },
|
||||||
{ value: 'opacityMedium', label: '0.5' },
|
{ value: 'opacityMedium', label: '0.5' },
|
||||||
{ value: 'opacityHigh', label: '0.8' },
|
{ value: 'opacityHigh', label: '0.8' },
|
||||||
]}
|
]}
|
||||||
value={opacity}
|
value={opacity}
|
||||||
onChange={v => this.setState({ opacity: v })}
|
onChange={v => this.setState({ opacity: v })}
|
||||||
@@ -530,7 +539,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
bsSize="xsmall"
|
bsSize="xsmall"
|
||||||
onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
|
onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
|
||||||
>
|
>
|
||||||
{t('Automatic Color')}
|
Automatic Color
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -541,42 +550,36 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
value={width}
|
value={width}
|
||||||
onChange={v => this.setState({ width: v })}
|
onChange={v => this.setState({ width: v })}
|
||||||
/>
|
/>
|
||||||
{annotationType === AnnotationTypes.TIME_SERIES &&
|
{annotationType === AnnotationTypes.TIME_SERIES && (
|
||||||
<CheckboxControl
|
<CheckboxControl
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-show-markers"
|
name="annotation-layer-show-markers"
|
||||||
label={t('Show Markers')}
|
label="Show Markers"
|
||||||
description={t('Shows or hides markers for the time series')}
|
description={'Shows or hides markers for the time series'}
|
||||||
value={showMarkers}
|
value={showMarkers}
|
||||||
onChange={v => this.setState({ showMarkers: v })}
|
onChange={v => this.setState({ showMarkers: v })}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{annotationType === AnnotationTypes.TIME_SERIES &&
|
{annotationType === AnnotationTypes.TIME_SERIES && (
|
||||||
<CheckboxControl
|
<CheckboxControl
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-hide-line"
|
name="annotation-layer-hide-line"
|
||||||
label={t('Hide Line')}
|
label="Hide Line"
|
||||||
description={t('Hides the Line for the time series')}
|
description={'Hides the Line for the time series'}
|
||||||
value={hideLine}
|
value={hideLine}
|
||||||
onChange={v => this.setState({ hideLine: v })}
|
onChange={v => this.setState({ hideLine: v })}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
</PopoverSection>
|
</PopoverSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isNew, name, annotationType,
|
const { isNew, name, annotationType, sourceType, show } = this.state;
|
||||||
sourceType, show } = this.state;
|
|
||||||
const isValid = this.isValidForm();
|
const isValid = this.isValidForm();
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{this.props.error && <span style={{ color: 'red' }}>ERROR: {this.props.error}</span>}
|
||||||
this.props.error &&
|
|
||||||
<span style={{ color: 'red' }}>
|
|
||||||
ERROR: {this.props.error}
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||||
<div style={{ marginRight: '2rem' }}>
|
<div style={{ marginRight: '2rem' }}>
|
||||||
<PopoverSection
|
<PopoverSection
|
||||||
@@ -604,50 +607,43 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
description={t('Choose the Annotation Layer Type')}
|
description={t('Choose the Annotation Layer Type')}
|
||||||
label={t('Annotation Layer Type')}
|
label={t('Annotation Layer Type')}
|
||||||
name="annotation-layer-type"
|
name="annotation-layer-type"
|
||||||
options={getSupportedAnnotationTypes(this.props.vizType).map(
|
options={getSupportedAnnotationTypes(this.props.vizType).map(x => ({
|
||||||
x => ({ value: x, label: getAnnotationTypeLabel(x) }))}
|
value: x,
|
||||||
|
label: getAnnotationTypeLabel(x),
|
||||||
|
}))}
|
||||||
value={annotationType}
|
value={annotationType}
|
||||||
onChange={this.handleAnnotationType}
|
onChange={this.handleAnnotationType}
|
||||||
/>
|
/>
|
||||||
{!!getSupportedSourceTypes(annotationType).length &&
|
{!!getSupportedSourceTypes(annotationType).length && (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
hovered
|
hovered
|
||||||
description={t('Choose the source of your annotations')}
|
description="Choose the source of your annotations"
|
||||||
label={t('Annotation Source')}
|
label="Annotation Source"
|
||||||
name="annotation-source-type"
|
name="annotation-source-type"
|
||||||
options={getSupportedSourceTypes(annotationType).map(
|
options={getSupportedSourceTypes(annotationType).map(x => ({
|
||||||
x => ({ value: x, label: getAnnotationSourceTypeLabels(x) }))}
|
value: x,
|
||||||
|
label: getAnnotationSourceTypeLabels(x),
|
||||||
|
}))}
|
||||||
value={sourceType}
|
value={sourceType}
|
||||||
onChange={this.handleAnnotationSourceType}
|
onChange={this.handleAnnotationSourceType}
|
||||||
/>
|
/>
|
||||||
}
|
)}
|
||||||
{ this.renderValueConfiguration() }
|
{this.renderValueConfiguration()}
|
||||||
</PopoverSection>
|
</PopoverSection>
|
||||||
</div>
|
</div>
|
||||||
{ this.renderSliceConfiguration() }
|
{this.renderSliceConfiguration()}
|
||||||
{ this.renderDisplayConfiguration() }
|
{this.renderDisplayConfiguration()}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Button
|
<Button bsSize="sm" onClick={this.deleteAnnotation}>
|
||||||
bsSize="sm"
|
{!isNew ? t('Remove') : t('Cancel')}
|
||||||
onClick={this.deleteAnnotation}
|
|
||||||
>
|
|
||||||
{ !isNew ? t('Remove') : t('Cancel') }
|
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button bsSize="sm" disabled={!isValid} onClick={this.applyAnnotation}>
|
||||||
bsSize="sm"
|
|
||||||
disabled={!isValid}
|
|
||||||
onClick={this.applyAnnotation}
|
|
||||||
>
|
|
||||||
{t('Apply')}
|
{t('Apply')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button bsSize="sm" disabled={!isValid} onClick={this.submitAnnotation}>
|
||||||
bsSize="sm"
|
|
||||||
disabled={!isValid}
|
|
||||||
onClick={this.submitAnnotation}
|
|
||||||
>
|
|
||||||
{t('OK')}
|
{t('OK')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -656,5 +652,6 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnnotationLayer.propTypes = propTypes;
|
AnnotationLayer.propTypes = propTypes;
|
||||||
AnnotationLayer.defaultProps = defaultProps;
|
AnnotationLayer.defaultProps = defaultProps;
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { now } from '../../modules/dates';
|
|||||||
import { getChartKey } from '../exploreUtils';
|
import { getChartKey } from '../exploreUtils';
|
||||||
import { getControlsState, getFormDataFromControls } from '../store';
|
import { getControlsState, getFormDataFromControls } from '../store';
|
||||||
|
|
||||||
export default function (bootstrapData) {
|
export default function getInitialState(bootstrapData) {
|
||||||
const controls = getControlsState(bootstrapData, bootstrapData.form_data);
|
const controls = getControlsState(bootstrapData, bootstrapData.form_data);
|
||||||
const rawFormData = { ...bootstrapData.form_data };
|
const rawFormData = { ...bootstrapData.form_data };
|
||||||
|
|
||||||
const bootstrappedState = {
|
const bootstrappedState = {
|
||||||
...bootstrapData,
|
...bootstrapData,
|
||||||
common: {
|
common: {
|
||||||
@@ -20,11 +21,15 @@ export default function (bootstrapData) {
|
|||||||
isDatasourceMetaLoading: false,
|
isDatasourceMetaLoading: false,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const slice = bootstrappedState.slice;
|
const slice = bootstrappedState.slice;
|
||||||
|
|
||||||
const sliceFormData = slice
|
const sliceFormData = slice
|
||||||
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const chartKey = getChartKey(bootstrappedState);
|
const chartKey = getChartKey(bootstrappedState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
featureFlags: bootstrapData.common.feature_flags,
|
featureFlags: bootstrapData.common.feature_flags,
|
||||||
charts: {
|
charts: {
|
||||||
@@ -36,7 +41,7 @@ export default function (bootstrapData) {
|
|||||||
chartUpdateStartTime: now(),
|
chartUpdateStartTime: now(),
|
||||||
latestQueryFormData: getFormDataFromControls(controls),
|
latestQueryFormData: getFormDataFromControls(controls),
|
||||||
sliceFormData,
|
sliceFormData,
|
||||||
queryRequest: null,
|
queryController: null,
|
||||||
queryResponse: null,
|
queryResponse: null,
|
||||||
triggerQuery: true,
|
triggerQuery: true,
|
||||||
lastRendered: 0,
|
lastRendered: 0,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user