[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:
parent
9029701f24
commit
316fdcb4d0
|
|
@ -1,4 +1,5 @@
|
|||
import { WORLD_HEALTH_DASHBOARD, CHECK_DASHBOARD_FAVORITE_ENDPOINT } from './dashboard.helper';
|
||||
import readResponseBlob from '../../utils/readResponseBlob';
|
||||
|
||||
export default () => describe('top-level controls', () => {
|
||||
let sliceIds = [];
|
||||
|
|
@ -61,8 +62,9 @@ export default () => describe('top-level controls', () => {
|
|||
|
||||
cy.wait(forceRefreshRequests).then((xhrs) => {
|
||||
// is_cached in response should be false
|
||||
xhrs.forEach((xhr) => {
|
||||
expect(xhr.response.body.is_cached).to.equal(false);
|
||||
xhrs.forEach(async (xhr) => {
|
||||
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 });
|
||||
|
||||
cy.wait(aliases).then((requests) => {
|
||||
requests.forEach((request) => {
|
||||
const requestBody = request.request.body.substring('form_data='.length);
|
||||
const requestParams = JSON.parse(decodeURIComponent(requestBody));
|
||||
requests.forEach((xhr) => {
|
||||
const requestFormData = xhr.request.body;
|
||||
const requestParams = JSON.parse(requestFormData.get('form_data'));
|
||||
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';
|
||||
|
||||
export default () => describe('load', () => {
|
||||
|
|
@ -24,9 +25,10 @@ export default () => describe('load', () => {
|
|||
it('should load dashboard', () => {
|
||||
// wait and verify one-by-one
|
||||
cy.wait(aliases).then((requests) => {
|
||||
requests.forEach((xhr) => {
|
||||
requests.forEach(async (xhr) => {
|
||||
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}`);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
|
||||
import readResponseBlob from '../../../utils/readResponseBlob';
|
||||
|
||||
// Big Number Total
|
||||
|
||||
|
|
@ -42,10 +43,12 @@ export default () => describe('Big Number Total', () => {
|
|||
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC, groupby: ['state'] };
|
||||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
cy.wait(['@getJson']).then((data) => {
|
||||
cy.verifyResponseCodes(data);
|
||||
cy.wait(['@getJson']).then(async (xhr) => {
|
||||
cy.verifyResponseCodes(xhr);
|
||||
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 readResponseBlob from '../../../utils/readResponseBlob';
|
||||
|
||||
// Table
|
||||
|
||||
|
|
@ -59,10 +60,11 @@ export default () => describe('Table chart', () => {
|
|||
|
||||
cy.visitChartByParams(JSON.stringify(formData));
|
||||
|
||||
cy.wait('@getJson').then((data) => {
|
||||
cy.verifyResponseCodes(data);
|
||||
cy.wait('@getJson').then(async (xhr) => {
|
||||
cy.verifyResponseCodes(xhr);
|
||||
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.wait('@getJson').then((data) => {
|
||||
cy.verifyResponseCodes(data);
|
||||
cy.wait('@getJson').then(async (xhr) => {
|
||||
cy.verifyResponseCodes(xhr);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@
|
|||
// -- This is will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
import readResponseBlob from '../utils/readResponseBlob';
|
||||
|
||||
const BASE_EXPLORE_URL = '/superset/explore/?form_data=';
|
||||
|
||||
Cypress.Commands.add('login', () => {
|
||||
|
|
@ -50,11 +52,14 @@ Cypress.Commands.add('visitChartByParams', (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
|
||||
expect(data.status).to.eq(200);
|
||||
if (data.response.body.error) {
|
||||
expect(data.response.body.error).to.eq(null);
|
||||
expect(xhr.status).to.eq(200);
|
||||
|
||||
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 }) => {
|
||||
cy.wait(waitAlias).then((data) => {
|
||||
cy.verifyResponseCodes(data);
|
||||
cy.wait(waitAlias).then(async (xhr) => {
|
||||
cy.verifyResponseCodes(xhr);
|
||||
|
||||
const responseBody = await readResponseBlob(xhr.response.body);
|
||||
if (querySubstring) {
|
||||
expect(data.response.body.query).contains(querySubstring);
|
||||
expect(responseBody.query).contains(querySubstring);
|
||||
}
|
||||
|
||||
cy.verifySliceContainer(chartSelector);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@
|
|||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
// The following is a workaround for Cypress not supporting fetch.
|
||||
// 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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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/theme": "^0.0.62",
|
||||
"@data-ui/xy-chart": "^0.0.61",
|
||||
"@superset-ui/core": "^0.0.5",
|
||||
"@vx/legend": "^0.0.170",
|
||||
"@vx/responsive": "0.0.172",
|
||||
"@vx/scale": "^0.0.165",
|
||||
"abortcontroller-polyfill": "^1.1.9",
|
||||
"babel-register": "^6.24.1",
|
||||
"bootstrap": "^3.3.6",
|
||||
"bootstrap-slider": "^10.0.0",
|
||||
|
|
@ -158,6 +160,7 @@
|
|||
"eslint-plugin-prettier": "^2.6.0",
|
||||
"eslint-plugin-react": "^7.0.1",
|
||||
"exports-loader": "^0.7.0",
|
||||
"fetch-mock": "^7.0.0-alpha.6",
|
||||
"file-loader": "^1.1.11",
|
||||
"gl": "^4.0.4",
|
||||
"ignore-styles": "^5.0.1",
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
import 'babel-polyfill';
|
||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||
import jsdom from 'jsdom';
|
||||
import { configure } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
|
|
|||
|
|
@ -1,37 +1,123 @@
|
|||
import fetchMock from 'fetch-mock';
|
||||
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 actions from '../../../src/chart/chartAction';
|
||||
|
||||
describe('chart actions', () => {
|
||||
const MOCK_URL = '/mockURL';
|
||||
let dispatch;
|
||||
let urlStub;
|
||||
let ajaxStub;
|
||||
let request;
|
||||
let loggerStub;
|
||||
|
||||
const setupDefaultFetchMock = () => {
|
||||
fetchMock.post(MOCK_URL, { json: {} }, { overwriteRoutes: true });
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
setupSupersetClient();
|
||||
setupDefaultFetchMock();
|
||||
});
|
||||
|
||||
afterAll(fetchMock.restore);
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = sinon.spy();
|
||||
urlStub = sinon.stub(exploreUtils, 'getExploreUrlAndPayload')
|
||||
.callsFake(() => ({ url: 'mockURL', payload: {} }));
|
||||
ajaxStub = sinon.stub($, 'ajax');
|
||||
urlStub = sinon
|
||||
.stub(exploreUtils, 'getExploreUrlAndPayload')
|
||||
.callsFake(() => ({ url: MOCK_URL, payload: {} }));
|
||||
loggerStub = sinon.stub(Logger, 'append');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
urlStub.restore();
|
||||
ajaxStub.restore();
|
||||
loggerStub.restore();
|
||||
fetchMock.resetHistory();
|
||||
});
|
||||
|
||||
it('should handle query timeout', () => {
|
||||
ajaxStub.rejects({ statusText: 'timeout' });
|
||||
request = actions.runQuery({});
|
||||
const promise = request(dispatch, sinon.stub().returns({
|
||||
explore: {
|
||||
controls: [],
|
||||
},
|
||||
}));
|
||||
promise.then(() => {
|
||||
expect(dispatch.callCount).toBe(3);
|
||||
expect(dispatch.args[0][0].type).toBe(actions.CHART_UPDATE_TIMEOUT);
|
||||
it('should dispatch CHART_UPDATE_STARTED action before 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[0][0].type).toBe(actions.CHART_UPDATE_STARTED);
|
||||
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
chartUpdateStartTime: PropTypes.number,
|
||||
latestQueryFormData: PropTypes.object,
|
||||
queryRequest: PropTypes.object,
|
||||
queryResponse: PropTypes.object,
|
||||
lastRendered: PropTypes.number,
|
||||
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 { requiresQuery, ANNOTATION_SOURCE_TYPES } from '../modules/AnnotationTypes';
|
||||
import { addDangerToast } from '../messageToasts/actions';
|
||||
import { Logger, LOG_ACTIONS_LOAD_CHART } from '../logger';
|
||||
import { COMMON_ERR_MESSAGES } from '../utils/common';
|
||||
import { t } from '../locales';
|
||||
|
||||
const $ = (window.$ = require('jquery'));
|
||||
|
||||
export const CHART_UPDATE_STARTED = 'CHART_UPDATE_STARTED';
|
||||
export function chartUpdateStarted(queryRequest, latestQueryFormData, key) {
|
||||
return { type: CHART_UPDATE_STARTED, queryRequest, latestQueryFormData, key };
|
||||
export function chartUpdateStarted(queryController, latestQueryFormData, key) {
|
||||
return { type: CHART_UPDATE_STARTED, queryController, latestQueryFormData, key };
|
||||
}
|
||||
|
||||
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 function annotationQueryStarted(annotation, queryRequest, key) {
|
||||
return { type: ANNOTATION_QUERY_STARTED, annotation, queryRequest, key };
|
||||
export function annotationQueryStarted(annotation, queryController, key) {
|
||||
return { type: ANNOTATION_QUERY_STARTED, annotation, queryController, key };
|
||||
}
|
||||
|
||||
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 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,
|
||||
dataType: 'json',
|
||||
signal,
|
||||
timeout: timeout * 1000,
|
||||
});
|
||||
dispatch(annotationQueryStarted(annotation, queryRequest, sliceKey));
|
||||
return queryRequest
|
||||
.then(queryResponse => dispatch(annotationQuerySuccess(annotation, queryResponse, sliceKey)))
|
||||
})
|
||||
.then(({ json }) => dispatch(annotationQuerySuccess(annotation, json, sliceKey)))
|
||||
.catch((err) => {
|
||||
if (err.statusText === 'timeout') {
|
||||
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));
|
||||
} else if (err.statusText !== 'abort') {
|
||||
dispatch(annotationQueryFailed(annotation, err.responseJSON, sliceKey));
|
||||
|
|
@ -135,30 +138,30 @@ export function runQuery(formData, force = false, timeout = 60, key) {
|
|||
force,
|
||||
});
|
||||
const logStart = Logger.getTimestamp();
|
||||
const queryRequest = $.ajax({
|
||||
type: 'POST',
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
dispatch(chartUpdateStarted(controller, payload, key));
|
||||
|
||||
const queryPromise = SupersetClient.post({
|
||||
url,
|
||||
dataType: 'json',
|
||||
data: {
|
||||
form_data: JSON.stringify(payload),
|
||||
},
|
||||
postPayload: { form_data: payload },
|
||||
signal,
|
||||
timeout: timeout * 1000,
|
||||
});
|
||||
const queryPromise = Promise.resolve(dispatch(chartUpdateStarted(queryRequest, payload, key)))
|
||||
.then(() => queryRequest)
|
||||
.then((queryResponse) => {
|
||||
})
|
||||
.then(({ json }) => {
|
||||
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
||||
slice_id: key,
|
||||
is_cached: queryResponse.is_cached,
|
||||
is_cached: json.is_cached,
|
||||
force_refresh: force,
|
||||
row_count: queryResponse.rowcount,
|
||||
row_count: json.rowcount,
|
||||
datasource: formData.datasource,
|
||||
start_offset: logStart,
|
||||
duration: Logger.getTimestamp() - logStart,
|
||||
has_extra_filters: formData.extra_filters && formData.extra_filters.length > 0,
|
||||
viz_type: formData.viz_type,
|
||||
});
|
||||
return dispatch(chartUpdateSucceeded(queryResponse, key));
|
||||
return dispatch(chartUpdateSucceeded(json, key));
|
||||
})
|
||||
.catch((err) => {
|
||||
Logger.append(LOG_ACTIONS_LOAD_CHART, {
|
||||
|
|
@ -170,30 +173,30 @@ export function runQuery(formData, force = false, timeout = 60, key) {
|
|||
});
|
||||
if (err.statusText === 'timeout') {
|
||||
dispatch(chartUpdateTimeout(err.statusText, timeout, key));
|
||||
} else if (err.statusText === 'abort') {
|
||||
} else if (err.statusText === 'AbortError') {
|
||||
dispatch(chartUpdateStopped(key));
|
||||
} else {
|
||||
let errObject;
|
||||
let errObject = err;
|
||||
if (err.responseJSON) {
|
||||
errObject = err.responseJSON;
|
||||
} else if (err.stack) {
|
||||
errObject = {
|
||||
error: t('Unexpected error: ') + err.description,
|
||||
error:
|
||||
t('Unexpected error: ') +
|
||||
(err.description || t('(no description, click to see stack trace)')),
|
||||
stacktrace: err.stack,
|
||||
};
|
||||
} else if (err.responseText && err.responseText.indexOf('CSRF') >= 0) {
|
||||
errObject = {
|
||||
error: COMMON_ERR_MESSAGES.SESSION_TIMED_OUT,
|
||||
};
|
||||
} else {
|
||||
errObject = {
|
||||
error: t('Unexpected error.'),
|
||||
};
|
||||
}
|
||||
dispatch(chartUpdateFailed(errObject, key));
|
||||
}
|
||||
});
|
||||
|
||||
const annotationLayers = formData.annotation_layers || [];
|
||||
|
||||
return Promise.all([
|
||||
queryPromise,
|
||||
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) {
|
||||
return function (dispatch) {
|
||||
const { url, payload } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url,
|
||||
data: {
|
||||
form_data: JSON.stringify(payload),
|
||||
},
|
||||
success: (response) => {
|
||||
const redirectUrl = new URI(window.location);
|
||||
redirectUrl
|
||||
.pathname('/superset/sqllab')
|
||||
.search({ datasourceKey: formData.datasource, sql: response.query });
|
||||
window.open(redirectUrl.href(), '_blank');
|
||||
},
|
||||
error: (xhr, status, error) => dispatch(sqllabRedirectFailed(error, formData.slice_id)),
|
||||
});
|
||||
return (dispatch) => {
|
||||
const { url } = getExploreUrlAndPayload({ formData, endpointType: 'query' });
|
||||
return SupersetClient.get({ url })
|
||||
.then(({ json }) => {
|
||||
const redirectUrl = new URL(window.location);
|
||||
redirectUrl.pathname = '/superset/sqllab';
|
||||
for (const key of redirectUrl.searchParams.keys()) {
|
||||
redirectUrl.searchParams.delete(key);
|
||||
}
|
||||
redirectUrl.searchParams.set('datasourceKey', formData.datasource);
|
||||
redirectUrl.searchParams.set('sql', json.query);
|
||||
window.open(redirectUrl.href, '_blank');
|
||||
})
|
||||
.catch(() => dispatch(addDangerToast(t('An error occurred while loading the SQL'))));
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
/* eslint-disable global-require */
|
||||
/* eslint global-require: 0, no-console: 0 */
|
||||
import $ from 'jquery';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||
|
||||
import airbnb from './modules/colorSchemes/airbnb';
|
||||
import categoricalSchemes from './modules/colorSchemes/categorical';
|
||||
import lyft from './modules/colorSchemes/lyft';
|
||||
import { getInstance } from './modules/ColorSchemeManager';
|
||||
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 () {
|
||||
$(':checkbox[data-checkbox-api-prefix]').change(function () {
|
||||
const $this = $(this);
|
||||
|
|
@ -22,10 +22,9 @@ $(document).ready(function () {
|
|||
ev.preventDefault();
|
||||
|
||||
const targetUrl = ev.currentTarget.href;
|
||||
$.ajax(targetUrl)
|
||||
.then(() => {
|
||||
location.reload();
|
||||
});
|
||||
$.ajax(targetUrl).then(() => {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -37,9 +36,18 @@ getInstance()
|
|||
.setDefaultSchemeName('bnbColors');
|
||||
|
||||
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
|
||||
window.$ = $;
|
||||
window.jQuery = $;
|
||||
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}
|
||||
lastRendered={chart.lastRendered}
|
||||
queryResponse={chart.queryResponse}
|
||||
queryRequest={chart.queryRequest}
|
||||
queryController={chart.queryController}
|
||||
triggerQuery={chart.triggerQuery}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ class ExploreViewContainer extends React.Component {
|
|||
this.addHistory = this.addHistory.bind(this);
|
||||
this.handleResize = this.handleResize.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() {
|
||||
|
|
@ -124,7 +127,9 @@ class ExploreViewContainer extends React.Component {
|
|||
}
|
||||
|
||||
onStop() {
|
||||
return this.props.chart.queryRequest.abort();
|
||||
if (this.props.chart && this.props.chart.queryController) {
|
||||
this.props.chart.queryController.abort();
|
||||
}
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
|
|
@ -262,7 +267,7 @@ class ExploreViewContainer extends React.Component {
|
|||
>
|
||||
{this.state.showModal && (
|
||||
<SaveModal
|
||||
onHide={this.toggleModal.bind(this)}
|
||||
onHide={this.toggleModal}
|
||||
actions={this.props.actions}
|
||||
form_data={this.props.form_data}
|
||||
/>
|
||||
|
|
@ -271,9 +276,9 @@ class ExploreViewContainer extends React.Component {
|
|||
<div className="col-sm-4">
|
||||
<QueryAndSaveBtns
|
||||
canAdd="True"
|
||||
onQuery={this.onQuery.bind(this)}
|
||||
onSave={this.toggleModal.bind(this)}
|
||||
onStop={this.onStop.bind(this)}
|
||||
onQuery={this.onQuery}
|
||||
onSave={this.toggleModal}
|
||||
onStop={this.onStop}
|
||||
loading={this.props.chart.chartStatus === 'loading'}
|
||||
chartIsStale={this.state.chartIsStale}
|
||||
errorMessage={this.renderErrorMessage()}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { CompactPicker } from 'react-color';
|
||||
import { Button } from 'react-bootstrap';
|
||||
|
||||
import $ from 'jquery';
|
||||
import mathjs from 'mathjs';
|
||||
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import SelectControl from './SelectControl';
|
||||
import TextControl from './TextControl';
|
||||
import CheckboxControl from './CheckboxControl';
|
||||
|
|
@ -83,10 +82,24 @@ const defaultProps = {
|
|||
export default class AnnotationLayer extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { name, annotationType, sourceType,
|
||||
color, opacity, style, width, showMarkers, hideLine, value,
|
||||
overrides, show, titleColumn, descriptionColumns,
|
||||
timeColumn, intervalEndColumn } = props;
|
||||
const {
|
||||
name,
|
||||
annotationType,
|
||||
sourceType,
|
||||
color,
|
||||
opacity,
|
||||
style,
|
||||
width,
|
||||
showMarkers,
|
||||
hideLine,
|
||||
value,
|
||||
overrides,
|
||||
show,
|
||||
titleColumn,
|
||||
descriptionColumns,
|
||||
timeColumn,
|
||||
intervalEndColumn,
|
||||
} = props;
|
||||
this.state = {
|
||||
// base
|
||||
name,
|
||||
|
|
@ -119,8 +132,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
this.applyAnnotation = this.applyAnnotation.bind(this);
|
||||
this.fetchOptions = this.fetchOptions.bind(this);
|
||||
this.handleAnnotationType = this.handleAnnotationType.bind(this);
|
||||
this.handleAnnotationSourceType =
|
||||
this.handleAnnotationSourceType.bind(this);
|
||||
this.handleAnnotationSourceType = this.handleAnnotationSourceType.bind(this);
|
||||
this.handleValue = this.handleValue.bind(this);
|
||||
this.isValidForm = this.isValidForm.bind(this);
|
||||
}
|
||||
|
|
@ -139,7 +151,10 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
isValidFormula(value, annotationType) {
|
||||
if (annotationType === AnnotationTypes.FORMULA) {
|
||||
try {
|
||||
mathjs.parse(value).compile().eval({ x: 0 });
|
||||
mathjs
|
||||
.parse(value)
|
||||
.compile()
|
||||
.eval({ x: 0 });
|
||||
} catch (err) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -148,10 +163,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
}
|
||||
|
||||
isValidForm() {
|
||||
const {
|
||||
name, annotationType, sourceType,
|
||||
value, timeColumn, intervalEndColumn,
|
||||
} = this.state;
|
||||
const { name, annotationType, sourceType, value, timeColumn, intervalEndColumn } = this.state;
|
||||
const errors = [nonEmpty(name), nonEmpty(annotationType), nonEmpty(value)];
|
||||
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
if (annotationType === AnnotationTypes.EVENT) {
|
||||
|
|
@ -166,7 +178,6 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
return !errors.filter(x => x).length;
|
||||
}
|
||||
|
||||
|
||||
handleAnnotationType(annotationType) {
|
||||
this.setState({
|
||||
annotationType,
|
||||
|
|
@ -199,31 +210,25 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
fetchOptions(annotationType, sourceType, isLoadingOptions) {
|
||||
if (isLoadingOptions === true) {
|
||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/annotationlayermodelview/api/read?',
|
||||
}).then((data) => {
|
||||
const layers = data ? data.result.map(layer => ({
|
||||
value: layer.id,
|
||||
label: layer.name,
|
||||
})) : [];
|
||||
SupersetClient.get({ endpoint: '/annotationlayermodelview/api/read?' }).then(({ json }) => {
|
||||
const layers = json
|
||||
? json.result.map(layer => ({
|
||||
value: layer.id,
|
||||
label: layer.name,
|
||||
}))
|
||||
: [];
|
||||
this.setState({
|
||||
isLoadingOptions: false,
|
||||
valueOptions: layers,
|
||||
});
|
||||
});
|
||||
} else if (requiresQuery(sourceType)) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: '/superset/user_slices',
|
||||
}).then(data =>
|
||||
SupersetClient.get({ endpoint: '/superset/user_slices' }).then(({ json }) =>
|
||||
this.setState({
|
||||
isLoadingOptions: false,
|
||||
valueOptions: data.filter(
|
||||
x => getSupportedSourceTypes(annotationType)
|
||||
.find(v => v === x.viz_type))
|
||||
.map(x => ({ value: x.id, label: x.title, slice: x }),
|
||||
),
|
||||
valueOptions: json
|
||||
.filter(x => getSupportedSourceTypes(annotationType).find(v => v === x.viz_type))
|
||||
.map(x => ({ value: x.id, label: x.title, slice: x })),
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
|
@ -266,26 +271,26 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderValueConfiguration() {
|
||||
const { annotationType, sourceType, value,
|
||||
valueOptions, isLoadingOptions } = this.state;
|
||||
const { annotationType, sourceType, value, valueOptions, isLoadingOptions } = this.state;
|
||||
let label = '';
|
||||
let description = '';
|
||||
if (requiresQuery(sourceType)) {
|
||||
if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
|
||||
label = t('Annotation Layer');
|
||||
description = t('Select the Annotation Layer you would like to use.');
|
||||
label = 'Annotation Layer';
|
||||
description = 'Select the Annotation Layer you would like to use.';
|
||||
} else {
|
||||
label = t('Chart');
|
||||
label = label = t('Chart');
|
||||
description = `Use a pre defined Superset Chart as a source for annotations and overlays.
|
||||
'your chart must be one of these visualization types:
|
||||
'[${getSupportedSourceTypes(annotationType)
|
||||
.map(x => ((x in vizTypes && 'label' in vizTypes[x]) ? vizTypes[x].label : '')).join(', ')}]'`;
|
||||
your chart must be one of these visualization types:
|
||||
[${getSupportedSourceTypes(annotationType)
|
||||
.map(x => (x in vizTypes && 'label' in vizTypes[x] ? vizTypes[x].label : ''))
|
||||
.join(', ')}]`;
|
||||
}
|
||||
} else if (annotationType === AnnotationTypes.FORMULA) {
|
||||
label = t('Formula');
|
||||
description = t(`Expects a formula with depending time parameter 'x'
|
||||
label = 'Formula';
|
||||
description = `Expects a formula with depending time parameter 'x'
|
||||
in milliseconds since epoch. mathjs is used to evaluate the formulas.
|
||||
Example: '2x+5'`);
|
||||
Example: '2x+5'`;
|
||||
}
|
||||
if (requiresQuery(sourceType)) {
|
||||
return (
|
||||
|
|
@ -300,10 +305,11 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
isLoading={isLoadingOptions}
|
||||
value={value}
|
||||
onChange={this.handleValue}
|
||||
validationErrors={!value ? [t('Mandatory')] : []}
|
||||
validationErrors={!value ? ['Mandatory'] : []}
|
||||
/>
|
||||
);
|
||||
} if (annotationType === AnnotationTypes.FORMULA) {
|
||||
}
|
||||
if (annotationType === AnnotationTypes.FORMULA) {
|
||||
return (
|
||||
<TextControl
|
||||
name="annotation-layer-value"
|
||||
|
|
@ -314,7 +320,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
placeholder=""
|
||||
value={value}
|
||||
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() {
|
||||
const { annotationType, sourceType, value, valueOptions, overrides, titleColumn,
|
||||
timeColumn, intervalEndColumn, descriptionColumns } = this.state;
|
||||
const {
|
||||
annotationType,
|
||||
sourceType,
|
||||
value,
|
||||
valueOptions,
|
||||
overrides,
|
||||
titleColumn,
|
||||
timeColumn,
|
||||
intervalEndColumn,
|
||||
descriptionColumns,
|
||||
} = this.state;
|
||||
const slice = (valueOptions.find(x => x.value === value) || {}).slice;
|
||||
if (sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE && slice) {
|
||||
const columns = (slice.data.groupby || []).concat(
|
||||
(slice.data.all_columns || [])).map(x => ({ value: x, label: x }));
|
||||
const timeColumnOptions = slice.data.include_time ?
|
||||
[{ value: '__timestamp', label: '__timestamp' }].concat(columns) : columns;
|
||||
const columns = (slice.data.groupby || [])
|
||||
.concat(slice.data.all_columns || [])
|
||||
.map(x => ({ value: x, label: x }));
|
||||
const timeColumnOptions = slice.data.include_time
|
||||
? [{ value: '__timestamp', label: '__timestamp' }].concat(columns)
|
||||
: columns;
|
||||
return (
|
||||
<div style={{ marginRight: '2rem' }}>
|
||||
<PopoverSection
|
||||
isSelected
|
||||
onSelect={() => {
|
||||
}}
|
||||
onSelect={() => {}}
|
||||
title="Annotation Slice Configuration"
|
||||
info={
|
||||
`This section allows you to configure how to use the slice
|
||||
to generate annotations.`
|
||||
}
|
||||
info={`This section allows you to configure how to use the slice
|
||||
to generate annotations.`}
|
||||
>
|
||||
{
|
||||
(
|
||||
annotationType === AnnotationTypes.EVENT ||
|
||||
annotationType === AnnotationTypes.INTERVAL
|
||||
) &&
|
||||
{(annotationType === AnnotationTypes.EVENT ||
|
||||
annotationType === AnnotationTypes.INTERVAL) && (
|
||||
<SelectControl
|
||||
hovered
|
||||
name="annotation-layer-time-column"
|
||||
label={
|
||||
annotationType === AnnotationTypes.INTERVAL ?
|
||||
'Interval Start column' : 'Event Time Column'
|
||||
annotationType === AnnotationTypes.INTERVAL
|
||||
? 'Interval Start column'
|
||||
: 'Event Time Column'
|
||||
}
|
||||
description={'This column must contain date/time information.'}
|
||||
validationErrors={!timeColumn ? ['Mandatory'] : []}
|
||||
|
|
@ -361,9 +373,8 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
value={timeColumn}
|
||||
onChange={v => this.setState({ timeColumn: v })}
|
||||
/>
|
||||
}
|
||||
{
|
||||
annotationType === AnnotationTypes.INTERVAL &&
|
||||
)}
|
||||
{annotationType === AnnotationTypes.INTERVAL && (
|
||||
<SelectControl
|
||||
hovered
|
||||
name="annotation-layer-intervalEnd"
|
||||
|
|
@ -374,20 +385,17 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
value={intervalEndColumn}
|
||||
onChange={v => this.setState({ intervalEndColumn: v })}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<SelectControl
|
||||
hovered
|
||||
name="annotation-layer-title"
|
||||
label="Title Column"
|
||||
description={'Pick a title for you annotation.'}
|
||||
options={
|
||||
[{ value: '', label: 'None' }].concat(columns)
|
||||
}
|
||||
options={[{ value: '', label: 'None' }].concat(columns)}
|
||||
value={titleColumn}
|
||||
onChange={v => this.setState({ titleColumn: v })}
|
||||
/>
|
||||
{
|
||||
annotationType !== AnnotationTypes.TIME_SERIES &&
|
||||
{annotationType !== AnnotationTypes.TIME_SERIES && (
|
||||
<SelectControl
|
||||
hovered
|
||||
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
|
||||
annotation. If you don't select a column all of them will be shown.`}
|
||||
multi
|
||||
options={
|
||||
columns
|
||||
}
|
||||
options={columns}
|
||||
value={descriptionColumns}
|
||||
onChange={v => this.setState({ descriptionColumns: v })}
|
||||
/>
|
||||
}
|
||||
)}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<CheckboxControl
|
||||
hovered
|
||||
|
|
@ -473,14 +479,17 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
return ('');
|
||||
return '';
|
||||
}
|
||||
|
||||
renderDisplayConfiguration() {
|
||||
const { color, opacity, style, width, showMarkers, hideLine, annotationType } = this.state;
|
||||
const colorScheme = [...getScheme(this.props.colorScheme)];
|
||||
if (color && color !== AUTOMATIC_COLOR &&
|
||||
!colorScheme.find(x => x.toLowerCase() === color.toLowerCase())) {
|
||||
if (
|
||||
color &&
|
||||
color !== AUTOMATIC_COLOR &&
|
||||
!colorScheme.find(x => x.toLowerCase() === color.toLowerCase())
|
||||
) {
|
||||
colorScheme.push(color);
|
||||
}
|
||||
return (
|
||||
|
|
@ -493,12 +502,12 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
<SelectControl
|
||||
name="annotation-layer-stroke"
|
||||
label={t('Style')}
|
||||
// see '../../../visualizations/nvd3_vis.css'
|
||||
// see '../../../visualizations/nvd3_vis.css'
|
||||
options={[
|
||||
{ value: 'solid', label: 'Solid' },
|
||||
{ value: 'dashed', label: 'Dashed' },
|
||||
{ value: 'longDashed', label: 'Long Dashed' },
|
||||
{ value: 'dotted', label: 'Dotted' },
|
||||
{ value: 'solid', label: 'Solid' },
|
||||
{ value: 'dashed', label: 'Dashed' },
|
||||
{ value: 'longDashed', label: 'Long Dashed' },
|
||||
{ value: 'dotted', label: 'Dotted' },
|
||||
]}
|
||||
value={style}
|
||||
onChange={v => this.setState({ style: v })}
|
||||
|
|
@ -506,12 +515,12 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
<SelectControl
|
||||
name="annotation-layer-opacity"
|
||||
label={t('Opacity')}
|
||||
// see '../../../visualizations/nvd3_vis.css'
|
||||
// see '../../../visualizations/nvd3_vis.css'
|
||||
options={[
|
||||
{ value: '', label: 'Solid' },
|
||||
{ value: 'opacityLow', label: '0.2' },
|
||||
{ value: 'opacityMedium', label: '0.5' },
|
||||
{ value: 'opacityHigh', label: '0.8' },
|
||||
{ value: '', label: 'Solid' },
|
||||
{ value: 'opacityLow', label: '0.2' },
|
||||
{ value: 'opacityMedium', label: '0.5' },
|
||||
{ value: 'opacityHigh', label: '0.8' },
|
||||
]}
|
||||
value={opacity}
|
||||
onChange={v => this.setState({ opacity: v })}
|
||||
|
|
@ -530,7 +539,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
bsSize="xsmall"
|
||||
onClick={() => this.setState({ color: AUTOMATIC_COLOR })}
|
||||
>
|
||||
{t('Automatic Color')}
|
||||
Automatic Color
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -541,42 +550,36 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
value={width}
|
||||
onChange={v => this.setState({ width: v })}
|
||||
/>
|
||||
{annotationType === AnnotationTypes.TIME_SERIES &&
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-layer-show-markers"
|
||||
label={t('Show Markers')}
|
||||
description={t('Shows or hides markers for the time series')}
|
||||
value={showMarkers}
|
||||
onChange={v => this.setState({ showMarkers: v })}
|
||||
/>
|
||||
}
|
||||
{annotationType === AnnotationTypes.TIME_SERIES &&
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-layer-hide-line"
|
||||
label={t('Hide Line')}
|
||||
description={t('Hides the Line for the time series')}
|
||||
value={hideLine}
|
||||
onChange={v => this.setState({ hideLine: v })}
|
||||
/>
|
||||
}
|
||||
{annotationType === AnnotationTypes.TIME_SERIES && (
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-layer-show-markers"
|
||||
label="Show Markers"
|
||||
description={'Shows or hides markers for the time series'}
|
||||
value={showMarkers}
|
||||
onChange={v => this.setState({ showMarkers: v })}
|
||||
/>
|
||||
)}
|
||||
{annotationType === AnnotationTypes.TIME_SERIES && (
|
||||
<CheckboxControl
|
||||
hovered
|
||||
name="annotation-layer-hide-line"
|
||||
label="Hide Line"
|
||||
description={'Hides the Line for the time series'}
|
||||
value={hideLine}
|
||||
onChange={v => this.setState({ hideLine: v })}
|
||||
/>
|
||||
)}
|
||||
</PopoverSection>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isNew, name, annotationType,
|
||||
sourceType, show } = this.state;
|
||||
const { isNew, name, annotationType, sourceType, show } = this.state;
|
||||
const isValid = this.isValidForm();
|
||||
return (
|
||||
<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={{ marginRight: '2rem' }}>
|
||||
<PopoverSection
|
||||
|
|
@ -604,50 +607,43 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
description={t('Choose the Annotation Layer Type')}
|
||||
label={t('Annotation Layer Type')}
|
||||
name="annotation-layer-type"
|
||||
options={getSupportedAnnotationTypes(this.props.vizType).map(
|
||||
x => ({ value: x, label: getAnnotationTypeLabel(x) }))}
|
||||
options={getSupportedAnnotationTypes(this.props.vizType).map(x => ({
|
||||
value: x,
|
||||
label: getAnnotationTypeLabel(x),
|
||||
}))}
|
||||
value={annotationType}
|
||||
onChange={this.handleAnnotationType}
|
||||
/>
|
||||
{!!getSupportedSourceTypes(annotationType).length &&
|
||||
{!!getSupportedSourceTypes(annotationType).length && (
|
||||
<SelectControl
|
||||
hovered
|
||||
description={t('Choose the source of your annotations')}
|
||||
label={t('Annotation Source')}
|
||||
description="Choose the source of your annotations"
|
||||
label="Annotation Source"
|
||||
name="annotation-source-type"
|
||||
options={getSupportedSourceTypes(annotationType).map(
|
||||
x => ({ value: x, label: getAnnotationSourceTypeLabels(x) }))}
|
||||
options={getSupportedSourceTypes(annotationType).map(x => ({
|
||||
value: x,
|
||||
label: getAnnotationSourceTypeLabels(x),
|
||||
}))}
|
||||
value={sourceType}
|
||||
onChange={this.handleAnnotationSourceType}
|
||||
/>
|
||||
}
|
||||
{ this.renderValueConfiguration() }
|
||||
)}
|
||||
{this.renderValueConfiguration()}
|
||||
</PopoverSection>
|
||||
</div>
|
||||
{ this.renderSliceConfiguration() }
|
||||
{ this.renderDisplayConfiguration() }
|
||||
{this.renderSliceConfiguration()}
|
||||
{this.renderDisplayConfiguration()}
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
bsSize="sm"
|
||||
onClick={this.deleteAnnotation}
|
||||
>
|
||||
{ !isNew ? t('Remove') : t('Cancel') }
|
||||
<Button bsSize="sm" onClick={this.deleteAnnotation}>
|
||||
{!isNew ? t('Remove') : t('Cancel')}
|
||||
</Button>
|
||||
<div>
|
||||
<Button
|
||||
bsSize="sm"
|
||||
disabled={!isValid}
|
||||
onClick={this.applyAnnotation}
|
||||
>
|
||||
<Button bsSize="sm" disabled={!isValid} onClick={this.applyAnnotation}>
|
||||
{t('Apply')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
bsSize="sm"
|
||||
disabled={!isValid}
|
||||
onClick={this.submitAnnotation}
|
||||
>
|
||||
<Button bsSize="sm" disabled={!isValid} onClick={this.submitAnnotation}>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -656,5 +652,6 @@ export default class AnnotationLayer extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
AnnotationLayer.propTypes = propTypes;
|
||||
AnnotationLayer.defaultProps = defaultProps;
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import { now } from '../../modules/dates';
|
|||
import { getChartKey } from '../exploreUtils';
|
||||
import { getControlsState, getFormDataFromControls } from '../store';
|
||||
|
||||
export default function (bootstrapData) {
|
||||
export default function getInitialState(bootstrapData) {
|
||||
const controls = getControlsState(bootstrapData, bootstrapData.form_data);
|
||||
const rawFormData = { ...bootstrapData.form_data };
|
||||
|
||||
const bootstrappedState = {
|
||||
...bootstrapData,
|
||||
common: {
|
||||
|
|
@ -20,11 +21,15 @@ export default function (bootstrapData) {
|
|||
isDatasourceMetaLoading: false,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
const slice = bootstrappedState.slice;
|
||||
|
||||
const sliceFormData = slice
|
||||
? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data))
|
||||
: null;
|
||||
|
||||
const chartKey = getChartKey(bootstrappedState);
|
||||
|
||||
return {
|
||||
featureFlags: bootstrapData.common.feature_flags,
|
||||
charts: {
|
||||
|
|
@ -36,7 +41,7 @@ export default function (bootstrapData) {
|
|||
chartUpdateStartTime: now(),
|
||||
latestQueryFormData: getFormDataFromControls(controls),
|
||||
sliceFormData,
|
||||
queryRequest: null,
|
||||
queryController: null,
|
||||
queryResponse: null,
|
||||
triggerQuery: true,
|
||||
lastRendered: 0,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue