[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:
Chris Williams
2018-10-15 16:52:19 -07:00
committed by GitHub
parent 9029701f24
commit 316fdcb4d0
20 changed files with 927 additions and 696 deletions

View File

@@ -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);
}); });
}); });
}); });

View File

@@ -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'] });
}); });
}); });
}); });

View File

@@ -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}`);
}); });
}); });

View File

@@ -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]);
}); });
}); });
}); });

View File

@@ -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);
}); });
}); });

View File

@@ -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);

View File

@@ -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
});

View 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);
});
}

View File

@@ -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",

View 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();
}

View File

@@ -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';

View File

@@ -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();
}); });
}); });
}); });

View File

@@ -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,

View File

@@ -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)),
});
}; };
} }

View File

@@ -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);
});
} }

View File

@@ -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}
/> />
); );

View File

@@ -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()}

View File

@@ -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;

View File

@@ -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