refactor: Migrate saveModalActions to TypeScript (#28046)
This commit is contained in:
parent
b160fd40c8
commit
920f2f437e
|
|
@ -1,259 +0,0 @@
|
||||||
/**
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one
|
|
||||||
* or more contributor license agreements. See the NOTICE file
|
|
||||||
* distributed with this work for additional information
|
|
||||||
* regarding copyright ownership. The ASF licenses this file
|
|
||||||
* to you under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance
|
|
||||||
* with the License. You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing,
|
|
||||||
* software distributed under the License is distributed on an
|
|
||||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
||||||
* KIND, either express or implied. See the License for the
|
|
||||||
* specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*/
|
|
||||||
import rison from 'rison';
|
|
||||||
import { SupersetClient, t } from '@superset-ui/core';
|
|
||||||
import { addSuccessToast } from 'src/components/MessageToasts/actions';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { buildV1ChartDataPayload } from '../exploreUtils';
|
|
||||||
import { Operators } from '../constants';
|
|
||||||
|
|
||||||
const ADHOC_FILTER_REGEX = /^adhoc_filters/;
|
|
||||||
|
|
||||||
export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED';
|
|
||||||
export function fetchDashboardsSucceeded(choices) {
|
|
||||||
return { type: FETCH_DASHBOARDS_SUCCEEDED, choices };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED';
|
|
||||||
export function fetchDashboardsFailed(userId) {
|
|
||||||
return { type: FETCH_DASHBOARDS_FAILED, userId };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SET_SAVE_CHART_MODAL_VISIBILITY =
|
|
||||||
'SET_SAVE_CHART_MODAL_VISIBILITY';
|
|
||||||
export function setSaveChartModalVisibility(isVisible) {
|
|
||||||
return { type: SET_SAVE_CHART_MODAL_VISIBILITY, isVisible };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED';
|
|
||||||
export function saveSliceFailed() {
|
|
||||||
return { type: SAVE_SLICE_FAILED };
|
|
||||||
}
|
|
||||||
export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS';
|
|
||||||
export function saveSliceSuccess(data) {
|
|
||||||
return { type: SAVE_SLICE_SUCCESS, data };
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractAdhocFiltersFromFormData = formDataToHandle =>
|
|
||||||
Object.entries(formDataToHandle).reduce(
|
|
||||||
(acc, [key, value]) =>
|
|
||||||
ADHOC_FILTER_REGEX.test(key)
|
|
||||||
? { ...acc, [key]: value?.filter(f => !f.isExtra) }
|
|
||||||
: acc,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasTemporalRangeFilter = formData =>
|
|
||||||
(formData?.adhoc_filters || []).some(
|
|
||||||
filter => filter.operator === Operators.TemporalRange,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const getSlicePayload = (
|
|
||||||
sliceName,
|
|
||||||
formDataWithNativeFilters,
|
|
||||||
dashboards,
|
|
||||||
owners,
|
|
||||||
formDataFromSlice = {},
|
|
||||||
) => {
|
|
||||||
const adhocFilters = extractAdhocFiltersFromFormData(
|
|
||||||
formDataWithNativeFilters,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Retain adhoc_filters from the slice if no adhoc_filters are present
|
|
||||||
// after overwriting a chart. This ensures the dashboard can continue
|
|
||||||
// to filter the chart. Before, any time range filter applied in the dashboard
|
|
||||||
// would end up as an extra filter and when overwriting the chart the original
|
|
||||||
// time range adhoc_filter was lost
|
|
||||||
if (!isEmpty(formDataFromSlice)) {
|
|
||||||
Object.keys(adhocFilters || {}).forEach(adhocFilterKey => {
|
|
||||||
if (isEmpty(adhocFilters[adhocFilterKey])) {
|
|
||||||
formDataFromSlice?.[adhocFilterKey]?.forEach(filter => {
|
|
||||||
if (filter.operator === Operators.TemporalRange && !filter.isExtra) {
|
|
||||||
adhocFilters[adhocFilterKey].push({
|
|
||||||
...filter,
|
|
||||||
comparator: 'No filter',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// This loop iterates through the adhoc_filters array in formDataWithNativeFilters.
|
|
||||||
// If a filter is of type TEMPORAL_RANGE and isExtra, it sets its comparator to
|
|
||||||
// 'No filter' and adds the modified filter to the adhocFilters array. This ensures that all
|
|
||||||
// TEMPORAL_RANGE filters are converted to 'No filter' when saving a chart.
|
|
||||||
if (!hasTemporalRangeFilter(adhocFilters)) {
|
|
||||||
formDataWithNativeFilters?.adhoc_filters?.forEach(filter => {
|
|
||||||
if (filter.operator === Operators.TemporalRange && filter.isExtra) {
|
|
||||||
adhocFilters.adhoc_filters.push({ ...filter, comparator: 'No filter' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = {
|
|
||||||
...formDataWithNativeFilters,
|
|
||||||
...adhocFilters,
|
|
||||||
dashboards,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [datasourceId, datasourceType] = formData.datasource.split('__');
|
|
||||||
const payload = {
|
|
||||||
params: JSON.stringify(formData),
|
|
||||||
slice_name: sliceName,
|
|
||||||
viz_type: formData.viz_type,
|
|
||||||
datasource_id: parseInt(datasourceId, 10),
|
|
||||||
datasource_type: datasourceType,
|
|
||||||
dashboards,
|
|
||||||
owners,
|
|
||||||
query_context: JSON.stringify(
|
|
||||||
buildV1ChartDataPayload({
|
|
||||||
formData,
|
|
||||||
force: false,
|
|
||||||
resultFormat: 'json',
|
|
||||||
resultType: 'full',
|
|
||||||
setDataMask: null,
|
|
||||||
ownState: null,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return payload;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addToasts = (isNewSlice, sliceName, addedToDashboard) => {
|
|
||||||
const toasts = [];
|
|
||||||
if (isNewSlice) {
|
|
||||||
toasts.push(addSuccessToast(t('Chart [%s] has been saved', sliceName)));
|
|
||||||
} else {
|
|
||||||
toasts.push(
|
|
||||||
addSuccessToast(t('Chart [%s] has been overwritten', sliceName)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addedToDashboard) {
|
|
||||||
if (addedToDashboard.new) {
|
|
||||||
toasts.push(
|
|
||||||
addSuccessToast(
|
|
||||||
t(
|
|
||||||
'Dashboard [%s] just got created and chart [%s] was added to it',
|
|
||||||
addedToDashboard.title,
|
|
||||||
sliceName,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
toasts.push(
|
|
||||||
addSuccessToast(
|
|
||||||
t(
|
|
||||||
'Chart [%s] was added to dashboard [%s]',
|
|
||||||
sliceName,
|
|
||||||
addedToDashboard.title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return toasts;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update existing slice
|
|
||||||
export const updateSlice =
|
|
||||||
(slice, sliceName, dashboards, addedToDashboard) =>
|
|
||||||
async (dispatch, getState) => {
|
|
||||||
const { slice_id: sliceId, owners, form_data: formDataFromSlice } = slice;
|
|
||||||
const {
|
|
||||||
explore: {
|
|
||||||
form_data: { url_params: _, ...formData },
|
|
||||||
},
|
|
||||||
} = getState();
|
|
||||||
try {
|
|
||||||
const response = await SupersetClient.put({
|
|
||||||
endpoint: `/api/v1/chart/${sliceId}`,
|
|
||||||
jsonPayload: getSlicePayload(
|
|
||||||
sliceName,
|
|
||||||
formData,
|
|
||||||
dashboards,
|
|
||||||
owners,
|
|
||||||
formDataFromSlice,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(saveSliceSuccess());
|
|
||||||
addToasts(false, sliceName, addedToDashboard).map(dispatch);
|
|
||||||
return response.json;
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(saveSliceFailed());
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new slice
|
|
||||||
export const createSlice =
|
|
||||||
(sliceName, dashboards, addedToDashboard) => async (dispatch, getState) => {
|
|
||||||
const {
|
|
||||||
explore: {
|
|
||||||
form_data: { url_params: _, ...formData },
|
|
||||||
},
|
|
||||||
} = getState();
|
|
||||||
try {
|
|
||||||
const response = await SupersetClient.post({
|
|
||||||
endpoint: `/api/v1/chart/`,
|
|
||||||
jsonPayload: getSlicePayload(sliceName, formData, dashboards),
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(saveSliceSuccess());
|
|
||||||
addToasts(true, sliceName, addedToDashboard).map(dispatch);
|
|
||||||
return response.json;
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(saveSliceFailed());
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new dashboard
|
|
||||||
export const createDashboard = dashboardName => async dispatch => {
|
|
||||||
try {
|
|
||||||
const response = await SupersetClient.post({
|
|
||||||
endpoint: `/api/v1/dashboard/`,
|
|
||||||
jsonPayload: { dashboard_title: dashboardName },
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.json;
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(saveSliceFailed());
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get dashboards the slice is added to
|
|
||||||
export const getSliceDashboards = slice => async dispatch => {
|
|
||||||
try {
|
|
||||||
const response = await SupersetClient.get({
|
|
||||||
endpoint: `/api/v1/chart/${slice.slice_id}?q=${rison.encode({
|
|
||||||
columns: ['dashboards.id'],
|
|
||||||
})}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.json.result.dashboards.map(({ id }) => id);
|
|
||||||
} catch (error) {
|
|
||||||
dispatch(saveSliceFailed());
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -16,10 +16,11 @@
|
||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import fetchMock from 'fetch-mock';
|
import fetchMock from 'fetch-mock';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
import { ADD_TOAST } from 'src/components/MessageToasts/actions';
|
import { ADD_TOAST } from 'src/components/MessageToasts/actions';
|
||||||
|
import { DatasourceType } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
createDashboard,
|
createDashboard,
|
||||||
createSlice,
|
createSlice,
|
||||||
|
|
@ -28,24 +29,62 @@ import {
|
||||||
SAVE_SLICE_SUCCESS,
|
SAVE_SLICE_SUCCESS,
|
||||||
updateSlice,
|
updateSlice,
|
||||||
getSlicePayload,
|
getSlicePayload,
|
||||||
|
PayloadSlice,
|
||||||
|
QueryFormData,
|
||||||
} from './saveModalActions';
|
} from './saveModalActions';
|
||||||
|
|
||||||
|
// Define test constants and mock data using imported types
|
||||||
const sliceId = 10;
|
const sliceId = 10;
|
||||||
const sliceName = 'New chart';
|
const sliceName = 'New chart';
|
||||||
const vizType = 'sample_viz_type';
|
const vizType = 'sample_viz_type';
|
||||||
const datasourceId = 11;
|
const datasourceId = 22;
|
||||||
const datasourceType = 'sample_datasource_type';
|
const datasourceType = DatasourceType.Table;
|
||||||
const dashboards = [12, 13];
|
const dashboards = [12, 13];
|
||||||
const queryContext = { sampleKey: 'sampleValue' };
|
const queryContext = { sampleKey: 'sampleValue' };
|
||||||
const formData = {
|
const owners = [0];
|
||||||
|
|
||||||
|
const formData: Partial<QueryFormData> = {
|
||||||
viz_type: vizType,
|
viz_type: vizType,
|
||||||
datasource: `${datasourceId}__${datasourceType}`,
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
dashboards,
|
dashboards,
|
||||||
};
|
};
|
||||||
const mockExploreState = { explore: { form_data: formData } };
|
|
||||||
|
|
||||||
const sliceResponsePayload = {
|
const mockExploreState: Partial<QueryFormData> = {
|
||||||
id: 10,
|
explore: {
|
||||||
|
can_add: false,
|
||||||
|
can_download: false,
|
||||||
|
can_overwrite: false,
|
||||||
|
isDatasourceMetaLoading: false,
|
||||||
|
isStarred: false,
|
||||||
|
triggerRender: false,
|
||||||
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
|
verbose_map: { '': '' },
|
||||||
|
main_dttm_col: '',
|
||||||
|
datasource_name: null,
|
||||||
|
description: null,
|
||||||
|
},
|
||||||
|
controls: {},
|
||||||
|
form_data: {
|
||||||
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
|
viz_type: '',
|
||||||
|
},
|
||||||
|
slice: {
|
||||||
|
slice_id: 0,
|
||||||
|
slice_name: '',
|
||||||
|
description: null,
|
||||||
|
cache_timeout: null,
|
||||||
|
is_managed_externally: false,
|
||||||
|
},
|
||||||
|
controlsTransferred: [],
|
||||||
|
standalone: false,
|
||||||
|
force: false,
|
||||||
|
common: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const sliceResponsePayload: Partial<PayloadSlice> = {
|
||||||
|
slice_id: sliceId,
|
||||||
|
owners: [],
|
||||||
|
form_data: formData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sampleError = new Error('sampleError');
|
const sampleError = new Error('sampleError');
|
||||||
|
|
@ -57,66 +96,124 @@ jest.mock('../exploreUtils', () => ({
|
||||||
/**
|
/**
|
||||||
* Tests updateSlice action
|
* Tests updateSlice action
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const updateSliceEndpoint = `glob:*/api/v1/chart/${sliceId}`;
|
const updateSliceEndpoint = `glob:*/api/v1/chart/${sliceId}`;
|
||||||
test('updateSlice handles success', async () => {
|
test('updateSlice handles success', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
|
||||||
const dispatch = sinon.spy();
|
const dispatchSpy = sinon.spy();
|
||||||
const getState = sinon.spy(() => mockExploreState);
|
const dispatch = (action: any) => {
|
||||||
|
dispatchSpy(action);
|
||||||
|
};
|
||||||
|
const getState = () => mockExploreState;
|
||||||
|
|
||||||
const slice = await updateSlice(
|
const slice = await updateSlice(
|
||||||
{ slice_id: sliceId },
|
{
|
||||||
|
slice_id: sliceId,
|
||||||
|
owners: owners as [],
|
||||||
|
form_data: formData,
|
||||||
|
slice_name: '',
|
||||||
|
description: '',
|
||||||
|
description_markdown: '',
|
||||||
|
slice_url: '',
|
||||||
|
viz_type: '',
|
||||||
|
thumbnail_url: '',
|
||||||
|
changed_on: 0,
|
||||||
|
changed_on_humanized: '',
|
||||||
|
modified: '',
|
||||||
|
datasource_id: 0,
|
||||||
|
datasource_type: datasourceType,
|
||||||
|
datasource_url: '',
|
||||||
|
datasource_name: '',
|
||||||
|
created_by: {
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
sliceName,
|
sliceName,
|
||||||
[],
|
[],
|
||||||
)(dispatch, getState);
|
)(dispatch as Dispatch<any>, getState);
|
||||||
|
|
||||||
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(2);
|
expect(dispatchSpy.callCount).toBe(2);
|
||||||
expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
||||||
expect(dispatch.getCall(1).args[0].type).toBe(ADD_TOAST);
|
expect(dispatchSpy.getCall(1).args[0].type).toBe('ADD_TOAST');
|
||||||
expect(dispatch.getCall(1).args[0].payload.toastType).toBe('SUCCESS_TOAST');
|
expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe(
|
||||||
expect(dispatch.getCall(1).args[0].payload.text).toBe(
|
'SUCCESS_TOAST',
|
||||||
|
);
|
||||||
|
expect(dispatchSpy.getCall(1).args[0].payload.text).toBe(
|
||||||
'Chart [New chart] has been overwritten',
|
'Chart [New chart] has been overwritten',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(slice).toEqual(sliceResponsePayload);
|
expect(slice).toEqual(sliceResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updateSlice handles failure', async () => {
|
test('updateSlice handles failure', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.put(updateSliceEndpoint, { throws: sampleError });
|
fetchMock.put(updateSliceEndpoint, { throws: sampleError });
|
||||||
const dispatch = sinon.spy();
|
|
||||||
const getState = sinon.spy(() => mockExploreState);
|
const dispatchSpy = sinon.spy();
|
||||||
|
const dispatch = (action: any) => {
|
||||||
|
dispatchSpy(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getState = () => mockExploreState;
|
||||||
|
|
||||||
let caughtError;
|
let caughtError;
|
||||||
try {
|
try {
|
||||||
await updateSlice({ slice_id: sliceId }, sliceName, [])(dispatch, getState);
|
await updateSlice(
|
||||||
|
{
|
||||||
|
slice_id: sliceId,
|
||||||
|
owners: [],
|
||||||
|
form_data: formData,
|
||||||
|
slice_name: '',
|
||||||
|
description: '',
|
||||||
|
description_markdown: '',
|
||||||
|
slice_url: '',
|
||||||
|
viz_type: '',
|
||||||
|
thumbnail_url: '',
|
||||||
|
changed_on: 0,
|
||||||
|
changed_on_humanized: '',
|
||||||
|
modified: '',
|
||||||
|
datasource_id: 0,
|
||||||
|
datasource_type: datasourceType,
|
||||||
|
datasource_url: '',
|
||||||
|
datasource_name: '',
|
||||||
|
created_by: {
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sliceName,
|
||||||
|
[],
|
||||||
|
)(dispatch as Dispatch<any>, getState);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
caughtError = error;
|
caughtError = error;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(caughtError).toEqual(sampleError);
|
expect(caughtError).toEqual(sampleError);
|
||||||
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(4);
|
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(4);
|
||||||
expect(dispatch.callCount).toBe(1);
|
expect(dispatchSpy.callCount).toBe(1);
|
||||||
expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
|
expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests createSlice action
|
* Tests createSlice action
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const createSliceEndpoint = `glob:*/api/v1/chart/`;
|
const createSliceEndpoint = `glob:*/api/v1/chart/`;
|
||||||
test('createSlice handles success', async () => {
|
test('createSlice handles success', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.post(createSliceEndpoint, sliceResponsePayload);
|
fetchMock.post(createSliceEndpoint, sliceResponsePayload);
|
||||||
const dispatch = sinon.spy();
|
const dispatchSpy = sinon.spy();
|
||||||
const getState = sinon.spy(() => mockExploreState);
|
const dispatch = (action: any) => dispatchSpy(action);
|
||||||
const slice = await createSlice(sliceName, [])(dispatch, getState);
|
const getState = () => mockExploreState;
|
||||||
|
const slice: Partial<PayloadSlice> = await createSlice(sliceName, [])(
|
||||||
|
dispatch as Dispatch,
|
||||||
|
getState,
|
||||||
|
);
|
||||||
expect(fetchMock.calls(createSliceEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(createSliceEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(2);
|
expect(dispatchSpy.callCount).toBe(2);
|
||||||
expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
||||||
expect(dispatch.getCall(1).args[0].type).toBe(ADD_TOAST);
|
expect(dispatchSpy.getCall(1).args[0].type).toBe(ADD_TOAST);
|
||||||
expect(dispatch.getCall(1).args[0].payload.toastType).toBe('SUCCESS_TOAST');
|
expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe(
|
||||||
expect(dispatch.getCall(1).args[0].payload.text).toBe(
|
'SUCCESS_TOAST',
|
||||||
|
);
|
||||||
|
expect(dispatchSpy.getCall(1).args[0].payload.text).toBe(
|
||||||
'Chart [New chart] has been saved',
|
'Chart [New chart] has been saved',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -126,19 +223,22 @@ test('createSlice handles success', async () => {
|
||||||
test('createSlice handles failure', async () => {
|
test('createSlice handles failure', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.post(createSliceEndpoint, { throws: sampleError });
|
fetchMock.post(createSliceEndpoint, { throws: sampleError });
|
||||||
const dispatch = sinon.spy();
|
|
||||||
const getState = sinon.spy(() => mockExploreState);
|
const dispatchSpy = sinon.spy();
|
||||||
let caughtError;
|
const dispatch = (action: any) => dispatchSpy(action);
|
||||||
|
const getState = () => mockExploreState;
|
||||||
|
|
||||||
|
let caughtError: Error | undefined;
|
||||||
try {
|
try {
|
||||||
await createSlice(sliceName, [])(dispatch, getState);
|
await createSlice(sliceName, [])(dispatch as Dispatch, getState);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
caughtError = error;
|
caughtError = error;
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(caughtError).toEqual(sampleError);
|
expect(caughtError).toEqual(sampleError);
|
||||||
expect(fetchMock.calls(createSliceEndpoint)).toHaveLength(4);
|
expect(fetchMock.calls(createSliceEndpoint)).toHaveLength(4);
|
||||||
expect(dispatch.callCount).toBe(1);
|
expect(dispatchSpy.callCount).toBe(1);
|
||||||
expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
|
expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_FAILED);
|
||||||
});
|
});
|
||||||
|
|
||||||
const dashboardName = 'New dashboard';
|
const dashboardName = 'New dashboard';
|
||||||
|
|
@ -155,7 +255,9 @@ test('createDashboard handles success', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.post(createDashboardEndpoint, dashboardResponsePayload);
|
fetchMock.post(createDashboardEndpoint, dashboardResponsePayload);
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
const dashboard = await createDashboard(dashboardName)(dispatch);
|
const dashboard = await createDashboard(dashboardName)(
|
||||||
|
dispatch as Dispatch<any>,
|
||||||
|
);
|
||||||
expect(fetchMock.calls(createDashboardEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(createDashboardEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(0);
|
expect(dispatch.callCount).toBe(0);
|
||||||
expect(dashboard).toEqual(dashboardResponsePayload);
|
expect(dashboard).toEqual(dashboardResponsePayload);
|
||||||
|
|
@ -167,7 +269,7 @@ test('createDashboard handles failure', async () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
let caughtError;
|
let caughtError;
|
||||||
try {
|
try {
|
||||||
await createDashboard(dashboardName)(dispatch);
|
await createDashboard(dashboardName)(dispatch as Dispatch<any>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
caughtError = error;
|
caughtError = error;
|
||||||
}
|
}
|
||||||
|
|
@ -181,24 +283,60 @@ test('createDashboard handles failure', async () => {
|
||||||
test('updateSlice with add to new dashboard handles success', async () => {
|
test('updateSlice with add to new dashboard handles success', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
|
||||||
const dispatch = sinon.spy();
|
const dispatchSpy = sinon.spy();
|
||||||
const getState = sinon.spy(() => mockExploreState);
|
const dispatch = (action: any) => dispatchSpy(action);
|
||||||
const slice = await updateSlice({ slice_id: sliceId }, sliceName, [], {
|
const getState = () => mockExploreState;
|
||||||
new: true,
|
|
||||||
title: dashboardName,
|
const slice = await updateSlice(
|
||||||
})(dispatch, getState);
|
{
|
||||||
|
slice_id: sliceId,
|
||||||
|
owners: [],
|
||||||
|
form_data: {
|
||||||
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
|
viz_type: '',
|
||||||
|
adhoc_filters: [],
|
||||||
|
dashboards: [],
|
||||||
|
},
|
||||||
|
slice_name: '',
|
||||||
|
description: '',
|
||||||
|
description_markdown: '',
|
||||||
|
slice_url: '',
|
||||||
|
viz_type: '',
|
||||||
|
thumbnail_url: '',
|
||||||
|
changed_on: 0,
|
||||||
|
changed_on_humanized: '',
|
||||||
|
modified: '',
|
||||||
|
datasource_id: 0,
|
||||||
|
datasource_type: datasourceType,
|
||||||
|
datasource_url: '',
|
||||||
|
datasource_name: '',
|
||||||
|
created_by: {
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sliceName,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
title: dashboardName,
|
||||||
|
},
|
||||||
|
)(dispatch as Dispatch<any>, getState);
|
||||||
|
|
||||||
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(3);
|
expect(dispatchSpy.callCount).toBe(3);
|
||||||
expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
||||||
expect(dispatch.getCall(1).args[0].type).toBe(ADD_TOAST);
|
expect(dispatchSpy.getCall(1).args[0].type).toBe(ADD_TOAST);
|
||||||
expect(dispatch.getCall(1).args[0].payload.toastType).toBe('SUCCESS_TOAST');
|
expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe(
|
||||||
expect(dispatch.getCall(1).args[0].payload.text).toBe(
|
'SUCCESS_TOAST',
|
||||||
|
);
|
||||||
|
expect(dispatchSpy.getCall(1).args[0].payload.text).toBe(
|
||||||
'Chart [New chart] has been overwritten',
|
'Chart [New chart] has been overwritten',
|
||||||
);
|
);
|
||||||
expect(dispatch.getCall(2).args[0].type).toBe(ADD_TOAST);
|
expect(dispatchSpy.getCall(2).args[0].type).toBe(ADD_TOAST);
|
||||||
expect(dispatch.getCall(2).args[0].payload.toastType).toBe('SUCCESS_TOAST');
|
expect(dispatchSpy.getCall(2).args[0].payload.toastType).toBe(
|
||||||
expect(dispatch.getCall(2).args[0].payload.text).toBe(
|
'SUCCESS_TOAST',
|
||||||
|
);
|
||||||
|
expect(dispatchSpy.getCall(2).args[0].payload.text).toBe(
|
||||||
'Dashboard [New dashboard] just got created and chart [New chart] was added to it',
|
'Dashboard [New dashboard] just got created and chart [New chart] was added to it',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -208,39 +346,71 @@ test('updateSlice with add to new dashboard handles success', async () => {
|
||||||
test('updateSlice with add to existing dashboard handles success', async () => {
|
test('updateSlice with add to existing dashboard handles success', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload);
|
||||||
const dispatch = sinon.spy();
|
const dispatchSpy = sinon.spy();
|
||||||
const getState = sinon.spy(() => mockExploreState);
|
const dispatch = (action: any) => dispatchSpy(action);
|
||||||
const slice = await updateSlice({ slice_id: sliceId }, sliceName, [], {
|
const getState = () => mockExploreState;
|
||||||
new: false,
|
const slice = await updateSlice(
|
||||||
title: dashboardName,
|
{
|
||||||
})(dispatch, getState);
|
slice_id: sliceId,
|
||||||
|
owners: [],
|
||||||
|
form_data: {
|
||||||
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
|
viz_type: '',
|
||||||
|
adhoc_filters: [],
|
||||||
|
dashboards: [],
|
||||||
|
},
|
||||||
|
slice_name: '',
|
||||||
|
description: '',
|
||||||
|
description_markdown: '',
|
||||||
|
slice_url: '',
|
||||||
|
viz_type: '',
|
||||||
|
thumbnail_url: '',
|
||||||
|
changed_on: 0,
|
||||||
|
changed_on_humanized: '',
|
||||||
|
modified: '',
|
||||||
|
datasource_id: 0,
|
||||||
|
datasource_type: datasourceType,
|
||||||
|
datasource_url: '',
|
||||||
|
datasource_name: '',
|
||||||
|
created_by: {
|
||||||
|
id: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sliceName,
|
||||||
|
[],
|
||||||
|
{
|
||||||
|
new: false,
|
||||||
|
title: dashboardName,
|
||||||
|
},
|
||||||
|
)(dispatch as Dispatch<any>, getState);
|
||||||
|
|
||||||
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(updateSliceEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(3);
|
expect(dispatchSpy.callCount).toBe(3);
|
||||||
expect(dispatch.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
expect(dispatchSpy.getCall(0).args[0].type).toBe(SAVE_SLICE_SUCCESS);
|
||||||
expect(dispatch.getCall(1).args[0].type).toBe(ADD_TOAST);
|
expect(dispatchSpy.getCall(1).args[0].type).toBe(ADD_TOAST);
|
||||||
expect(dispatch.getCall(1).args[0].payload.toastType).toBe('SUCCESS_TOAST');
|
expect(dispatchSpy.getCall(1).args[0].payload.toastType).toBe(
|
||||||
expect(dispatch.getCall(1).args[0].payload.text).toBe(
|
'SUCCESS_TOAST',
|
||||||
|
);
|
||||||
|
expect(dispatchSpy.getCall(1).args[0].payload.text).toBe(
|
||||||
'Chart [New chart] has been overwritten',
|
'Chart [New chart] has been overwritten',
|
||||||
);
|
);
|
||||||
expect(dispatch.getCall(2).args[0].type).toBe(ADD_TOAST);
|
expect(dispatchSpy.getCall(2).args[0].type).toBe(ADD_TOAST);
|
||||||
expect(dispatch.getCall(2).args[0].payload.toastType).toBe('SUCCESS_TOAST');
|
expect(dispatchSpy.getCall(2).args[0].payload.toastType).toBe(
|
||||||
expect(dispatch.getCall(2).args[0].payload.text).toBe(
|
'SUCCESS_TOAST',
|
||||||
|
);
|
||||||
|
expect(dispatchSpy.getCall(2).args[0].payload.text).toBe(
|
||||||
'Chart [New chart] was added to dashboard [New dashboard]',
|
'Chart [New chart] was added to dashboard [New dashboard]',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(slice).toEqual(sliceResponsePayload);
|
expect(slice).toEqual(sliceResponsePayload);
|
||||||
});
|
});
|
||||||
|
|
||||||
const slice = { slice_id: 10 };
|
|
||||||
const dashboardSlicesResponsePayload = {
|
const dashboardSlicesResponsePayload = {
|
||||||
result: {
|
result: {
|
||||||
dashboards: [{ id: 21 }, { id: 22 }, { id: 23 }],
|
dashboards: [{ id: 21 }, { id: 22 }, { id: 23 }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDashboardSlicesReturnValue = [21, 22, 23];
|
const getDashboardSlicesReturnValue = [21, 22, 23];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests getSliceDashboards action
|
* Tests getSliceDashboards action
|
||||||
*/
|
*/
|
||||||
|
|
@ -249,10 +419,20 @@ const getSliceDashboardsEndpoint = `glob:*/api/v1/chart/${sliceId}?q=(columns:!(
|
||||||
test('getSliceDashboards with slice handles success', async () => {
|
test('getSliceDashboards with slice handles success', async () => {
|
||||||
fetchMock.reset();
|
fetchMock.reset();
|
||||||
fetchMock.get(getSliceDashboardsEndpoint, dashboardSlicesResponsePayload);
|
fetchMock.get(getSliceDashboardsEndpoint, dashboardSlicesResponsePayload);
|
||||||
const dispatch = sinon.spy();
|
const dispatchSpy = sinon.spy();
|
||||||
const sliceDashboards = await getSliceDashboards(slice)(dispatch);
|
const dispatch = (action: any) => dispatchSpy(action);
|
||||||
|
const sliceDashboards = await getSliceDashboards({
|
||||||
|
slice_id: 10,
|
||||||
|
owners: [],
|
||||||
|
form_data: {
|
||||||
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
|
viz_type: '',
|
||||||
|
adhoc_filters: [],
|
||||||
|
dashboards: [],
|
||||||
|
},
|
||||||
|
})(dispatch as Dispatch<any>);
|
||||||
expect(fetchMock.calls(getSliceDashboardsEndpoint)).toHaveLength(1);
|
expect(fetchMock.calls(getSliceDashboardsEndpoint)).toHaveLength(1);
|
||||||
expect(dispatch.callCount).toBe(0);
|
expect(dispatchSpy.callCount).toBe(0);
|
||||||
expect(sliceDashboards).toEqual(getDashboardSlicesReturnValue);
|
expect(sliceDashboards).toEqual(getDashboardSlicesReturnValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -262,7 +442,16 @@ test('getSliceDashboards with slice handles failure', async () => {
|
||||||
const dispatch = sinon.spy();
|
const dispatch = sinon.spy();
|
||||||
let caughtError;
|
let caughtError;
|
||||||
try {
|
try {
|
||||||
await getSliceDashboards(slice)(dispatch);
|
await getSliceDashboards({
|
||||||
|
slice_id: sliceId,
|
||||||
|
owners: [],
|
||||||
|
form_data: {
|
||||||
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
|
viz_type: '',
|
||||||
|
adhoc_filters: [],
|
||||||
|
dashboards: [],
|
||||||
|
},
|
||||||
|
})(dispatch as Dispatch<any>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
caughtError = error;
|
caughtError = error;
|
||||||
}
|
}
|
||||||
|
|
@ -276,14 +465,14 @@ test('getSliceDashboards with slice handles failure', async () => {
|
||||||
describe('getSlicePayload', () => {
|
describe('getSlicePayload', () => {
|
||||||
const sliceName = 'Test Slice';
|
const sliceName = 'Test Slice';
|
||||||
const formDataWithNativeFilters = {
|
const formDataWithNativeFilters = {
|
||||||
datasource: '22__table',
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
viz_type: 'pie',
|
viz_type: 'pie',
|
||||||
adhoc_filters: [],
|
adhoc_filters: [],
|
||||||
};
|
};
|
||||||
const dashboards = [5];
|
const dashboards = [5];
|
||||||
const owners = [1];
|
const owners = [0];
|
||||||
const formDataFromSlice = {
|
const formDataFromSlice: QueryFormData = {
|
||||||
datasource: '22__table',
|
datasource: `${datasourceId}__${datasourceType}`,
|
||||||
viz_type: 'pie',
|
viz_type: 'pie',
|
||||||
adhoc_filters: [
|
adhoc_filters: [
|
||||||
{
|
{
|
||||||
|
|
@ -294,6 +483,7 @@ describe('getSlicePayload', () => {
|
||||||
expressionType: 'SIMPLE',
|
expressionType: 'SIMPLE',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
dashboards: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should return the correct payload when no adhoc_filters are present in formDataWithNativeFilters', () => {
|
test('should return the correct payload when no adhoc_filters are present in formDataWithNativeFilters', () => {
|
||||||
|
|
@ -301,7 +491,7 @@ describe('getSlicePayload', () => {
|
||||||
sliceName,
|
sliceName,
|
||||||
formDataWithNativeFilters,
|
formDataWithNativeFilters,
|
||||||
dashboards,
|
dashboards,
|
||||||
owners,
|
owners as [],
|
||||||
formDataFromSlice,
|
formDataFromSlice,
|
||||||
);
|
);
|
||||||
expect(result).toHaveProperty('params');
|
expect(result).toHaveProperty('params');
|
||||||
|
|
@ -315,13 +505,13 @@ describe('getSlicePayload', () => {
|
||||||
expect(result).toHaveProperty('dashboards', dashboards);
|
expect(result).toHaveProperty('dashboards', dashboards);
|
||||||
expect(result).toHaveProperty('owners', owners);
|
expect(result).toHaveProperty('owners', owners);
|
||||||
expect(result).toHaveProperty('query_context');
|
expect(result).toHaveProperty('query_context');
|
||||||
expect(JSON.parse(result.params).adhoc_filters).toEqual(
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
||||||
formDataFromSlice.adhoc_filters,
|
formDataWithNativeFilters.adhoc_filters,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return the correct payload when adhoc_filters are present in formDataWithNativeFilters', () => {
|
test('should return the correct payload when adhoc_filters are present in formDataWithNativeFilters', () => {
|
||||||
const formDataWithAdhocFilters = {
|
const formDataWithAdhocFilters: QueryFormData = {
|
||||||
...formDataWithNativeFilters,
|
...formDataWithNativeFilters,
|
||||||
adhoc_filters: [
|
adhoc_filters: [
|
||||||
{
|
{
|
||||||
|
|
@ -337,7 +527,7 @@ describe('getSlicePayload', () => {
|
||||||
sliceName,
|
sliceName,
|
||||||
formDataWithAdhocFilters,
|
formDataWithAdhocFilters,
|
||||||
dashboards,
|
dashboards,
|
||||||
owners,
|
owners as [],
|
||||||
formDataFromSlice,
|
formDataFromSlice,
|
||||||
);
|
);
|
||||||
expect(result).toHaveProperty('params');
|
expect(result).toHaveProperty('params');
|
||||||
|
|
@ -351,13 +541,13 @@ describe('getSlicePayload', () => {
|
||||||
expect(result).toHaveProperty('dashboards', dashboards);
|
expect(result).toHaveProperty('dashboards', dashboards);
|
||||||
expect(result).toHaveProperty('owners', owners);
|
expect(result).toHaveProperty('owners', owners);
|
||||||
expect(result).toHaveProperty('query_context');
|
expect(result).toHaveProperty('query_context');
|
||||||
expect(JSON.parse(result.params).adhoc_filters).toEqual(
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
||||||
formDataWithAdhocFilters.adhoc_filters,
|
formDataWithAdhocFilters.adhoc_filters,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return the correct payload when formDataWithNativeFilters has a filter with isExtra set to true', () => {
|
test('should return the correct payload when formDataWithNativeFilters has a filter with isExtra set to true', () => {
|
||||||
const formDataWithAdhocFiltersWithExtra = {
|
const formDataWithAdhocFiltersWithExtra: QueryFormData = {
|
||||||
...formDataWithNativeFilters,
|
...formDataWithNativeFilters,
|
||||||
adhoc_filters: [
|
adhoc_filters: [
|
||||||
{
|
{
|
||||||
|
|
@ -373,7 +563,7 @@ describe('getSlicePayload', () => {
|
||||||
sliceName,
|
sliceName,
|
||||||
formDataWithAdhocFiltersWithExtra,
|
formDataWithAdhocFiltersWithExtra,
|
||||||
dashboards,
|
dashboards,
|
||||||
owners,
|
owners as [],
|
||||||
formDataFromSlice,
|
formDataFromSlice,
|
||||||
);
|
);
|
||||||
expect(result).toHaveProperty('params');
|
expect(result).toHaveProperty('params');
|
||||||
|
|
@ -387,13 +577,13 @@ describe('getSlicePayload', () => {
|
||||||
expect(result).toHaveProperty('dashboards', dashboards);
|
expect(result).toHaveProperty('dashboards', dashboards);
|
||||||
expect(result).toHaveProperty('owners', owners);
|
expect(result).toHaveProperty('owners', owners);
|
||||||
expect(result).toHaveProperty('query_context');
|
expect(result).toHaveProperty('query_context');
|
||||||
expect(JSON.parse(result.params).adhoc_filters).toEqual(
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
||||||
formDataFromSlice.adhoc_filters,
|
formDataFromSlice.adhoc_filters,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return the correct payload when formDataWithNativeFilters has a filter with isExtra set to true in mixed chart', () => {
|
test('should return the correct payload when formDataWithNativeFilters has a filter with isExtra set to true in mixed chart', () => {
|
||||||
const formDataFromSliceWithAdhocFilterB = {
|
const formDataFromSliceWithAdhocFilterB: QueryFormData = {
|
||||||
...formDataFromSlice,
|
...formDataFromSlice,
|
||||||
adhoc_filters_b: [
|
adhoc_filters_b: [
|
||||||
{
|
{
|
||||||
|
|
@ -405,7 +595,7 @@ describe('getSlicePayload', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const formDataWithAdhocFiltersWithExtra = {
|
const formDataWithAdhocFiltersWithExtra: QueryFormData = {
|
||||||
...formDataWithNativeFilters,
|
...formDataWithNativeFilters,
|
||||||
viz_type: 'mixed_timeseries',
|
viz_type: 'mixed_timeseries',
|
||||||
adhoc_filters: [
|
adhoc_filters: [
|
||||||
|
|
@ -433,14 +623,13 @@ describe('getSlicePayload', () => {
|
||||||
sliceName,
|
sliceName,
|
||||||
formDataWithAdhocFiltersWithExtra,
|
formDataWithAdhocFiltersWithExtra,
|
||||||
dashboards,
|
dashboards,
|
||||||
owners,
|
owners as [],
|
||||||
formDataFromSliceWithAdhocFilterB,
|
formDataFromSliceWithAdhocFilterB,
|
||||||
);
|
);
|
||||||
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
||||||
expect(JSON.parse(result.params).adhoc_filters).toEqual(
|
|
||||||
formDataFromSliceWithAdhocFilterB.adhoc_filters,
|
formDataFromSliceWithAdhocFilterB.adhoc_filters,
|
||||||
);
|
);
|
||||||
expect(JSON.parse(result.params).adhoc_filters_b).toEqual(
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
||||||
formDataFromSliceWithAdhocFilterB.adhoc_filters_b,
|
formDataFromSliceWithAdhocFilterB.adhoc_filters_b,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import rison from 'rison';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import {
|
||||||
|
DatasourceType,
|
||||||
|
QueryFormData,
|
||||||
|
SimpleAdhocFilter,
|
||||||
|
SupersetClient,
|
||||||
|
t,
|
||||||
|
} from '@superset-ui/core';
|
||||||
|
import { addSuccessToast } from 'src/components/MessageToasts/actions';
|
||||||
|
import { isEmpty } from 'lodash';
|
||||||
|
import { Slice } from 'src/dashboard/types';
|
||||||
|
import { Operators } from '../constants';
|
||||||
|
import { buildV1ChartDataPayload } from '../exploreUtils';
|
||||||
|
|
||||||
|
export interface PayloadSlice extends Slice {
|
||||||
|
params: string;
|
||||||
|
dashboards: number[];
|
||||||
|
query_context: string;
|
||||||
|
}
|
||||||
|
const ADHOC_FILTER_REGEX = /^adhoc_filters/;
|
||||||
|
|
||||||
|
export const FETCH_DASHBOARDS_SUCCEEDED = 'FETCH_DASHBOARDS_SUCCEEDED';
|
||||||
|
export function fetchDashboardsSucceeded(choices: string[]) {
|
||||||
|
return { type: FETCH_DASHBOARDS_SUCCEEDED, choices };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FETCH_DASHBOARDS_FAILED = 'FETCH_DASHBOARDS_FAILED';
|
||||||
|
export function fetchDashboardsFailed(userId: string) {
|
||||||
|
return { type: FETCH_DASHBOARDS_FAILED, userId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SET_SAVE_CHART_MODAL_VISIBILITY =
|
||||||
|
'SET_SAVE_CHART_MODAL_VISIBILITY';
|
||||||
|
export function setSaveChartModalVisibility(isVisible: boolean) {
|
||||||
|
return { type: SET_SAVE_CHART_MODAL_VISIBILITY, isVisible };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED';
|
||||||
|
export function saveSliceFailed() {
|
||||||
|
return { type: SAVE_SLICE_FAILED };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS';
|
||||||
|
export function saveSliceSuccess(data: Partial<QueryFormData>) {
|
||||||
|
return { type: SAVE_SLICE_SUCCESS, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAdhocFiltersFromFormData(
|
||||||
|
formDataToHandle: QueryFormData,
|
||||||
|
): Partial<QueryFormData> {
|
||||||
|
const result: Partial<QueryFormData> = {};
|
||||||
|
Object.entries(formDataToHandle).forEach(([key, value]) => {
|
||||||
|
if (ADHOC_FILTER_REGEX.test(key) && Array.isArray(value)) {
|
||||||
|
result[key] = (value as SimpleAdhocFilter[]).filter(
|
||||||
|
(f: SimpleAdhocFilter) => !f.isExtra,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTemporalRangeFilter = (formData: Partial<QueryFormData>): boolean =>
|
||||||
|
(formData?.adhoc_filters || []).some(
|
||||||
|
(filter: SimpleAdhocFilter) => filter.operator === Operators.TemporalRange,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getSlicePayload = (
|
||||||
|
sliceName: string,
|
||||||
|
formDataWithNativeFilters: QueryFormData = {} as QueryFormData,
|
||||||
|
dashboards: number[],
|
||||||
|
owners: [],
|
||||||
|
formDataFromSlice: QueryFormData = {} as QueryFormData,
|
||||||
|
): Partial<PayloadSlice> => {
|
||||||
|
const adhocFilters: Partial<QueryFormData> = extractAdhocFiltersFromFormData(
|
||||||
|
formDataWithNativeFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isEmpty(formDataFromSlice) &&
|
||||||
|
formDataWithNativeFilters.adhoc_filters &&
|
||||||
|
formDataWithNativeFilters.adhoc_filters.length > 0
|
||||||
|
) {
|
||||||
|
Object.keys(adhocFilters).forEach(adhocFilterKey => {
|
||||||
|
if (isEmpty(adhocFilters[adhocFilterKey])) {
|
||||||
|
const sourceFilters = formDataFromSlice[adhocFilterKey];
|
||||||
|
if (Array.isArray(sourceFilters)) {
|
||||||
|
const targetArray = adhocFilters[adhocFilterKey] || [];
|
||||||
|
sourceFilters.forEach(filter => {
|
||||||
|
if (filter.operator === Operators.TemporalRange) {
|
||||||
|
targetArray.push({
|
||||||
|
...filter,
|
||||||
|
comparator: filter.comparator || 'No filter',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
adhocFilters[adhocFilterKey] = targetArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTemporalRangeFilter(adhocFilters)) {
|
||||||
|
formDataWithNativeFilters.adhoc_filters?.forEach(
|
||||||
|
(filter: SimpleAdhocFilter) => {
|
||||||
|
if (filter.operator === Operators.TemporalRange && filter.isExtra) {
|
||||||
|
if (!adhocFilters.adhoc_filters) {
|
||||||
|
adhocFilters.adhoc_filters = [];
|
||||||
|
}
|
||||||
|
adhocFilters.adhoc_filters.push({
|
||||||
|
...filter,
|
||||||
|
comparator: 'No filter',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const formData = {
|
||||||
|
...formDataWithNativeFilters,
|
||||||
|
...adhocFilters,
|
||||||
|
dashboards,
|
||||||
|
};
|
||||||
|
let datasourceId = 0;
|
||||||
|
let datasourceType: DatasourceType = DatasourceType.Table;
|
||||||
|
|
||||||
|
if (formData.datasource) {
|
||||||
|
const [id, typeString] = formData.datasource.split('__');
|
||||||
|
datasourceId = parseInt(id, 10);
|
||||||
|
|
||||||
|
const formattedTypeString =
|
||||||
|
typeString.charAt(0).toUpperCase() + typeString.slice(1);
|
||||||
|
if (formattedTypeString in DatasourceType) {
|
||||||
|
datasourceType =
|
||||||
|
DatasourceType[formattedTypeString as keyof typeof DatasourceType];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: Partial<PayloadSlice> = {
|
||||||
|
params: JSON.stringify(formData),
|
||||||
|
slice_name: sliceName,
|
||||||
|
viz_type: formData.viz_type,
|
||||||
|
datasource_id: datasourceId,
|
||||||
|
datasource_type: datasourceType,
|
||||||
|
dashboards,
|
||||||
|
owners,
|
||||||
|
query_context: JSON.stringify(
|
||||||
|
buildV1ChartDataPayload({
|
||||||
|
formData,
|
||||||
|
force: false,
|
||||||
|
resultFormat: 'json',
|
||||||
|
resultType: 'full',
|
||||||
|
setDataMask: null,
|
||||||
|
ownState: null,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToasts = (
|
||||||
|
isNewSlice: boolean,
|
||||||
|
sliceName: string,
|
||||||
|
addedToDashboard?: {
|
||||||
|
title: string;
|
||||||
|
new?: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const toasts = [];
|
||||||
|
if (isNewSlice) {
|
||||||
|
toasts.push(addSuccessToast(t('Chart [%s] has been saved', sliceName)));
|
||||||
|
} else {
|
||||||
|
toasts.push(
|
||||||
|
addSuccessToast(t('Chart [%s] has been overwritten', sliceName)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedToDashboard) {
|
||||||
|
if (addedToDashboard.new) {
|
||||||
|
toasts.push(
|
||||||
|
addSuccessToast(
|
||||||
|
t(
|
||||||
|
'Dashboard [%s] just got created and chart [%s] was added to it',
|
||||||
|
addedToDashboard.title,
|
||||||
|
sliceName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toasts.push(
|
||||||
|
addSuccessToast(
|
||||||
|
t(
|
||||||
|
'Chart [%s] was added to dashboard [%s]',
|
||||||
|
sliceName,
|
||||||
|
addedToDashboard.title,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return toasts;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateSlice =
|
||||||
|
(
|
||||||
|
slice: Slice,
|
||||||
|
sliceName: string,
|
||||||
|
dashboards: number[],
|
||||||
|
addedToDashboard?: {
|
||||||
|
title: string;
|
||||||
|
new?: boolean;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
async (dispatch: Dispatch, getState: () => Partial<QueryFormData>) => {
|
||||||
|
const { slice_id: sliceId, owners, form_data: formDataFromSlice } = slice;
|
||||||
|
const formData = getState().explore?.form_data;
|
||||||
|
try {
|
||||||
|
const response = await SupersetClient.put({
|
||||||
|
endpoint: `/api/v1/chart/${sliceId}`,
|
||||||
|
jsonPayload: getSlicePayload(
|
||||||
|
sliceName,
|
||||||
|
formData,
|
||||||
|
dashboards,
|
||||||
|
owners as [],
|
||||||
|
formDataFromSlice,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(saveSliceSuccess(response.json));
|
||||||
|
addToasts(false, sliceName, addedToDashboard).map(dispatch);
|
||||||
|
return response.json;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(saveSliceFailed());
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createSlice =
|
||||||
|
(
|
||||||
|
sliceName: string,
|
||||||
|
dashboards: number[],
|
||||||
|
addedToDashboard?: {
|
||||||
|
title: string;
|
||||||
|
new?: boolean;
|
||||||
|
},
|
||||||
|
) =>
|
||||||
|
async (dispatch: Dispatch, getState: () => Partial<QueryFormData>) => {
|
||||||
|
const formData = getState().explore?.form_data;
|
||||||
|
try {
|
||||||
|
const response = await SupersetClient.post({
|
||||||
|
endpoint: `/api/v1/chart/`,
|
||||||
|
jsonPayload: getSlicePayload(
|
||||||
|
sliceName,
|
||||||
|
formData,
|
||||||
|
dashboards,
|
||||||
|
[],
|
||||||
|
{} as QueryFormData,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(saveSliceSuccess(response.json));
|
||||||
|
addToasts(true, sliceName, addedToDashboard).map(dispatch);
|
||||||
|
return response.json;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(saveSliceFailed());
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDashboard =
|
||||||
|
(dashboardName: string) => async (dispatch: Dispatch) => {
|
||||||
|
try {
|
||||||
|
const response = await SupersetClient.post({
|
||||||
|
endpoint: `/api/v1/dashboard/`,
|
||||||
|
jsonPayload: { dashboard_title: dashboardName },
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json;
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(saveSliceFailed());
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSliceDashboards =
|
||||||
|
(slice: Partial<Slice>) => async (dispatch: Dispatch) => {
|
||||||
|
try {
|
||||||
|
const response = await SupersetClient.get({
|
||||||
|
endpoint: `/api/v1/chart/${slice.slice_id}?q=${rison.encode({
|
||||||
|
columns: ['dashboards.id'],
|
||||||
|
})}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json.result.dashboards.map(
|
||||||
|
({ id }: { id: number }) => id,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(saveSliceFailed());
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export { QueryFormData };
|
||||||
Loading…
Reference in New Issue