mirror of
https://github.com/apache/superset.git
synced 2026-09-01 21:11:28 +00:00
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
981 lines
28 KiB
TypeScript
981 lines
28 KiB
TypeScript
/**
|
|
* 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 fetchMock from 'fetch-mock';
|
|
import { Dispatch } from 'redux';
|
|
import { ADD_TOAST } from 'src/components/MessageToasts/actions';
|
|
import {
|
|
DatasourceType,
|
|
isFeatureEnabled,
|
|
QueryFormData,
|
|
SimpleAdhocFilter,
|
|
VizType,
|
|
} from '@superset-ui/core';
|
|
import {
|
|
createDashboard,
|
|
createSlice,
|
|
getSliceDashboards,
|
|
SAVE_SLICE_FAILED,
|
|
SAVE_SLICE_SUCCESS,
|
|
updateSlice,
|
|
getSlicePayload,
|
|
PayloadSlice,
|
|
} from './saveModalActions';
|
|
import { Operators } from '../constants';
|
|
|
|
jest.mock('@superset-ui/core', () => ({
|
|
...jest.requireActual('@superset-ui/core'),
|
|
isFeatureEnabled: jest.fn(),
|
|
}));
|
|
|
|
const mockedIsFeatureEnabled = isFeatureEnabled as jest.Mock;
|
|
|
|
// Define test constants and mock data using imported types
|
|
const sliceId = 10;
|
|
const sliceName = 'New chart';
|
|
const vizType = 'sample_viz_type';
|
|
const datasourceId = 22;
|
|
const datasourceType = DatasourceType.Table;
|
|
const dashboards = [12, 13];
|
|
const queryContext = { sampleKey: 'sampleValue' };
|
|
const editors = [0];
|
|
|
|
const formData: Partial<QueryFormData> = {
|
|
viz_type: vizType,
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
dashboards,
|
|
};
|
|
|
|
const mockExploreState: Partial<QueryFormData> = {
|
|
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,
|
|
editors: [],
|
|
form_data: formData,
|
|
};
|
|
|
|
const sampleError = new Error('sampleError');
|
|
const updateSliceEndpoint = `glob:*/api/v1/chart/${sliceId}`;
|
|
|
|
jest.mock('../exploreUtils', () => ({
|
|
buildV1ChartDataPayload: jest.fn(() => queryContext),
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
fetchMock.clearHistory().removeRoutes();
|
|
mockedIsFeatureEnabled.mockReturnValue(false);
|
|
});
|
|
|
|
test('existing-chart overwrite sends only still-matching normalization metadata', async () => {
|
|
mockedIsFeatureEnabled.mockReturnValue(true);
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const dispatch = jest.fn();
|
|
const getState = () => ({
|
|
explore: {
|
|
form_data: {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: vizType,
|
|
row_limit: 10000,
|
|
show_legend: true,
|
|
object_control: { a: 1, b: 2 },
|
|
},
|
|
},
|
|
versionHistory: {
|
|
chartNormalization: {
|
|
chartId: sliceId,
|
|
hydrationSessionId: 'hydration-a',
|
|
saveAttemptId: null,
|
|
invalidatedControls: { show_legend: true as const },
|
|
transitions: {
|
|
row_limit: {
|
|
control: 'row_limit',
|
|
from_present: true as const,
|
|
from_value: null,
|
|
to_present: true as const,
|
|
to_value: 10000,
|
|
},
|
|
show_legend: {
|
|
control: 'show_legend',
|
|
from_present: false as const,
|
|
to_present: true as const,
|
|
to_value: true,
|
|
},
|
|
object_control: {
|
|
control: 'object_control',
|
|
from_present: false as const,
|
|
to_present: true as const,
|
|
to_value: { b: 2, a: 1 },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await updateSlice(
|
|
{ ...sliceResponsePayload, slice_id: sliceId } as never,
|
|
sliceName,
|
|
[],
|
|
)(dispatch, getState);
|
|
|
|
const request = fetchMock.callHistory.lastCall(updateSliceEndpoint);
|
|
const body = JSON.parse(request?.options.body as string);
|
|
expect(body.normalization_changes).toEqual([
|
|
{
|
|
control: 'row_limit',
|
|
from_present: true,
|
|
from_value: null,
|
|
to_present: true,
|
|
to_value: 10000,
|
|
},
|
|
{
|
|
control: 'object_control',
|
|
from_present: false,
|
|
to_present: true,
|
|
to_value: { b: 2, a: 1 },
|
|
},
|
|
]);
|
|
expect(dispatch).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: 'BEGIN_CHART_NORMALIZATION_SAVE' }),
|
|
);
|
|
expect(dispatch).toHaveBeenCalledWith(
|
|
expect.objectContaining({ type: 'COMPLETE_CHART_NORMALIZATION_SAVE' }),
|
|
);
|
|
});
|
|
|
|
test('matches normalization metadata against finalized payload filters', async () => {
|
|
mockedIsFeatureEnabled.mockReturnValue(true);
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const extraTemporalFilter = {
|
|
expressionType: 'SIMPLE',
|
|
clause: 'WHERE',
|
|
subject: 'ds',
|
|
operator: Operators.TemporalRange,
|
|
comparator: '',
|
|
isExtra: true,
|
|
} as SimpleAdhocFilter;
|
|
const savedTemporalFilter = {
|
|
...extraTemporalFilter,
|
|
comparator: 'No filter',
|
|
isExtra: false,
|
|
};
|
|
const dispatch = jest.fn();
|
|
const getState = () => ({
|
|
explore: {
|
|
form_data: {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: vizType,
|
|
adhoc_filters: [extraTemporalFilter],
|
|
},
|
|
},
|
|
versionHistory: {
|
|
chartNormalization: {
|
|
chartId: sliceId,
|
|
hydrationSessionId: 'hydration-a',
|
|
saveAttemptId: null,
|
|
invalidatedControls: {},
|
|
transitions: {
|
|
adhoc_filters: {
|
|
control: 'adhoc_filters',
|
|
from_present: false as const,
|
|
to_present: true as const,
|
|
to_value: [savedTemporalFilter],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await updateSlice(
|
|
{ ...sliceResponsePayload, slice_id: sliceId } as never,
|
|
sliceName,
|
|
[],
|
|
)(dispatch, getState);
|
|
|
|
const request = fetchMock.callHistory.lastCall(updateSliceEndpoint);
|
|
const body = JSON.parse(request?.options.body as string);
|
|
expect(body.normalization_changes).toEqual([
|
|
expect.objectContaining({
|
|
control: 'adhoc_filters',
|
|
to_value: [savedTemporalFilter],
|
|
}),
|
|
]);
|
|
});
|
|
|
|
/**
|
|
* Tests updateSlice action
|
|
*/
|
|
test('updateSlice handles success', async () => {
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => {
|
|
dispatchSpy(action);
|
|
};
|
|
const getState = () => mockExploreState;
|
|
|
|
const slice = await updateSlice(
|
|
{
|
|
slice_id: sliceId,
|
|
editors: editors 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,
|
|
[],
|
|
)(dispatch as Dispatch<any>, getState);
|
|
expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(1);
|
|
expect(dispatchSpy.mock.calls.length).toBe(2);
|
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS);
|
|
expect(dispatchSpy.mock.calls[1][0].type).toBe('ADD_TOAST');
|
|
expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST');
|
|
expect(dispatchSpy.mock.calls[1][0].payload.text).toBe(
|
|
'Chart [New chart] has been overwritten',
|
|
);
|
|
expect(slice).toEqual(sliceResponsePayload);
|
|
});
|
|
|
|
test('updateSlice handles failure', async () => {
|
|
fetchMock.put(
|
|
updateSliceEndpoint,
|
|
{ throws: sampleError },
|
|
{ name: updateSliceEndpoint },
|
|
);
|
|
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => {
|
|
dispatchSpy(action);
|
|
};
|
|
|
|
const getState = () => mockExploreState;
|
|
|
|
let caughtError;
|
|
try {
|
|
await updateSlice(
|
|
{
|
|
slice_id: sliceId,
|
|
editors: [],
|
|
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) {
|
|
caughtError = error;
|
|
}
|
|
|
|
expect(caughtError).toEqual(sampleError);
|
|
expect(fetchMock.callHistory.calls(updateSliceEndpoint)).toHaveLength(4);
|
|
expect(dispatchSpy.mock.calls.length).toBe(1);
|
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED);
|
|
});
|
|
|
|
/**
|
|
* Tests createSlice action
|
|
*/
|
|
const createSliceEndpoint = `glob:*/api/v1/chart/`;
|
|
test('createSlice handles success', async () => {
|
|
fetchMock.post(createSliceEndpoint, sliceResponsePayload, {
|
|
name: createSliceEndpoint,
|
|
});
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => dispatchSpy(action);
|
|
const getState = () => mockExploreState;
|
|
const slice: Partial<PayloadSlice> = await createSlice(sliceName, [])(
|
|
dispatch as Dispatch,
|
|
getState,
|
|
);
|
|
expect(fetchMock.callHistory.calls(createSliceEndpoint)).toHaveLength(1);
|
|
expect(dispatchSpy.mock.calls.length).toBe(2);
|
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS);
|
|
expect(dispatchSpy.mock.calls[1][0].type).toBe(ADD_TOAST);
|
|
expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST');
|
|
expect(dispatchSpy.mock.calls[1][0].payload.text).toBe(
|
|
'Chart [New chart] has been saved',
|
|
);
|
|
|
|
expect(slice).toEqual(sliceResponsePayload);
|
|
});
|
|
|
|
test('createSlice handles failure', async () => {
|
|
fetchMock.post(createSliceEndpoint, { throws: sampleError });
|
|
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => dispatchSpy(action);
|
|
const getState = () => mockExploreState;
|
|
|
|
let caughtError: Error | undefined;
|
|
try {
|
|
await createSlice(sliceName, [])(dispatch as Dispatch, getState);
|
|
} catch (error) {
|
|
caughtError = error;
|
|
}
|
|
|
|
expect(caughtError).toEqual(sampleError);
|
|
expect(fetchMock.callHistory.calls(createSliceEndpoint)).toHaveLength(4);
|
|
expect(dispatchSpy.mock.calls.length).toBe(1);
|
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED);
|
|
});
|
|
|
|
const dashboardName = 'New dashboard';
|
|
const dashboardResponsePayload = {
|
|
id: 14,
|
|
};
|
|
|
|
/**
|
|
* Tests createDashboard action
|
|
*/
|
|
|
|
const createDashboardEndpoint = `glob:*/api/v1/dashboard/`;
|
|
test('createDashboard handles success', async () => {
|
|
fetchMock.post(createDashboardEndpoint, dashboardResponsePayload, {
|
|
name: createDashboardEndpoint,
|
|
});
|
|
const dispatch = jest.fn();
|
|
const dashboard = await createDashboard(dashboardName)(
|
|
dispatch as Dispatch<any>,
|
|
);
|
|
expect(fetchMock.callHistory.calls(createDashboardEndpoint)).toHaveLength(1);
|
|
expect(dispatch.mock.calls.length).toBe(0);
|
|
expect(dashboard).toEqual(dashboardResponsePayload);
|
|
});
|
|
|
|
test('createDashboard handles failure', async () => {
|
|
fetchMock.post(
|
|
createDashboardEndpoint,
|
|
{ throws: sampleError },
|
|
{ name: createDashboardEndpoint },
|
|
);
|
|
const dispatch = jest.fn();
|
|
let caughtError;
|
|
try {
|
|
await createDashboard(dashboardName)(dispatch as Dispatch<any>);
|
|
} catch (error) {
|
|
caughtError = error;
|
|
}
|
|
|
|
expect(caughtError).toEqual(sampleError);
|
|
expect(fetchMock.callHistory.calls(createDashboardEndpoint)).toHaveLength(4);
|
|
expect(dispatch.mock.calls.length).toBe(1);
|
|
expect(dispatch.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED);
|
|
});
|
|
|
|
test('updateSlice with add to new dashboard handles success', async () => {
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => dispatchSpy(action);
|
|
const getState = () => mockExploreState;
|
|
|
|
const slice = await updateSlice(
|
|
{
|
|
slice_id: sliceId,
|
|
editors: [],
|
|
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.callHistory.calls(updateSliceEndpoint)).toHaveLength(1);
|
|
expect(dispatchSpy.mock.calls.length).toBe(3);
|
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS);
|
|
expect(dispatchSpy.mock.calls[1][0].type).toBe(ADD_TOAST);
|
|
expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST');
|
|
expect(dispatchSpy.mock.calls[1][0].payload.text).toBe(
|
|
'Chart [New chart] has been overwritten',
|
|
);
|
|
expect(dispatchSpy.mock.calls[2][0].type).toBe(ADD_TOAST);
|
|
expect(dispatchSpy.mock.calls[2][0].payload.toastType).toBe('SUCCESS_TOAST');
|
|
expect(dispatchSpy.mock.calls[2][0].payload.text).toBe(
|
|
'Dashboard [New dashboard] just got created and chart [New chart] was added to it',
|
|
);
|
|
|
|
expect(slice).toEqual(sliceResponsePayload);
|
|
});
|
|
|
|
test('updateSlice with add to existing dashboard handles success', async () => {
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => dispatchSpy(action);
|
|
const getState = () => mockExploreState;
|
|
const slice = await updateSlice(
|
|
{
|
|
slice_id: sliceId,
|
|
editors: [],
|
|
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.callHistory.calls(updateSliceEndpoint)).toHaveLength(1);
|
|
expect(dispatchSpy.mock.calls.length).toBe(3);
|
|
expect(dispatchSpy.mock.calls[0][0].type).toBe(SAVE_SLICE_SUCCESS);
|
|
expect(dispatchSpy.mock.calls[1][0].type).toBe(ADD_TOAST);
|
|
expect(dispatchSpy.mock.calls[1][0].payload.toastType).toBe('SUCCESS_TOAST');
|
|
expect(dispatchSpy.mock.calls[1][0].payload.text).toBe(
|
|
'Chart [New chart] has been overwritten',
|
|
);
|
|
expect(dispatchSpy.mock.calls[2][0].type).toBe(ADD_TOAST);
|
|
expect(dispatchSpy.mock.calls[2][0].payload.toastType).toBe('SUCCESS_TOAST');
|
|
expect(dispatchSpy.mock.calls[2][0].payload.text).toBe(
|
|
'Chart [New chart] was added to dashboard [New dashboard]',
|
|
);
|
|
|
|
expect(slice).toEqual(sliceResponsePayload);
|
|
});
|
|
|
|
const dashboardSlicesResponsePayload = {
|
|
result: {
|
|
dashboards: [{ id: 21 }, { id: 22 }, { id: 23 }],
|
|
},
|
|
};
|
|
const getDashboardSlicesReturnValue = [21, 22, 23];
|
|
/**
|
|
* Tests getSliceDashboards action
|
|
*/
|
|
|
|
const getSliceDashboardsEndpoint = `glob:*/api/v1/chart/${sliceId}?q=(select_columns:!(dashboards.id))`;
|
|
test('getSliceDashboards with slice handles success', async () => {
|
|
fetchMock.get(getSliceDashboardsEndpoint, dashboardSlicesResponsePayload, {
|
|
name: getSliceDashboardsEndpoint,
|
|
});
|
|
const dispatchSpy = jest.fn();
|
|
const dispatch = (action: any) => dispatchSpy(action);
|
|
const sliceDashboards = await getSliceDashboards({
|
|
slice_id: 10,
|
|
editors: [],
|
|
form_data: {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: '',
|
|
adhoc_filters: [],
|
|
dashboards: [],
|
|
},
|
|
})(dispatch as Dispatch<any>);
|
|
expect(fetchMock.callHistory.calls(getSliceDashboardsEndpoint)).toHaveLength(
|
|
1,
|
|
);
|
|
expect(dispatchSpy.mock.calls.length).toBe(0);
|
|
expect(sliceDashboards).toEqual(getDashboardSlicesReturnValue);
|
|
});
|
|
|
|
test('getSliceDashboards with slice handles failure', async () => {
|
|
fetchMock.get(
|
|
getSliceDashboardsEndpoint,
|
|
{ throws: sampleError },
|
|
{ name: getSliceDashboardsEndpoint },
|
|
);
|
|
const dispatch = jest.fn();
|
|
let caughtError;
|
|
try {
|
|
await getSliceDashboards({
|
|
slice_id: sliceId,
|
|
editors: [],
|
|
form_data: {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: '',
|
|
adhoc_filters: [],
|
|
dashboards: [],
|
|
},
|
|
})(dispatch as Dispatch<any>);
|
|
} catch (error) {
|
|
caughtError = error;
|
|
}
|
|
|
|
expect(caughtError).toEqual(sampleError);
|
|
expect(fetchMock.callHistory.calls(getSliceDashboardsEndpoint)).toHaveLength(
|
|
4,
|
|
);
|
|
expect(dispatch.mock.calls.length).toBe(1);
|
|
expect(dispatch.mock.calls[0][0].type).toBe(SAVE_SLICE_FAILED);
|
|
});
|
|
|
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
|
describe('getSlicePayload', () => {
|
|
const sliceName = 'Test Slice';
|
|
const formDataWithNativeFilters = {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: VizType.Pie,
|
|
adhoc_filters: [],
|
|
};
|
|
const dashboards = [5];
|
|
const editors = [0];
|
|
const formDataFromSlice: QueryFormData = {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: VizType.Pie,
|
|
adhoc_filters: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
},
|
|
],
|
|
dashboards: [],
|
|
};
|
|
|
|
test('should return the correct payload when no adhoc_filters are present in formDataWithNativeFilters', async () => {
|
|
const result = await getSlicePayload(
|
|
sliceName,
|
|
formDataWithNativeFilters,
|
|
dashboards,
|
|
editors as [],
|
|
formDataFromSlice,
|
|
);
|
|
expect(result).toHaveProperty('params');
|
|
expect(result).toHaveProperty('slice_name', sliceName);
|
|
expect(result).toHaveProperty(
|
|
'viz_type',
|
|
formDataWithNativeFilters.viz_type,
|
|
);
|
|
expect(result).toHaveProperty('datasource_id', 22);
|
|
expect(result).toHaveProperty('datasource_type', 'table');
|
|
expect(result).toHaveProperty('dashboards', dashboards);
|
|
expect(result).toHaveProperty('editors', editors);
|
|
expect(result).toHaveProperty('query_context');
|
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
|
formDataWithNativeFilters.adhoc_filters,
|
|
);
|
|
});
|
|
|
|
test('should return the correct payload when adhoc_filters are present in formDataWithNativeFilters', async () => {
|
|
const formDataWithAdhocFilters: QueryFormData = {
|
|
...formDataWithNativeFilters,
|
|
adhoc_filters: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
},
|
|
],
|
|
};
|
|
const result = await getSlicePayload(
|
|
sliceName,
|
|
formDataWithAdhocFilters,
|
|
dashboards,
|
|
editors as [],
|
|
formDataFromSlice,
|
|
);
|
|
expect(result).toHaveProperty('params');
|
|
expect(result).toHaveProperty('slice_name', sliceName);
|
|
expect(result).toHaveProperty(
|
|
'viz_type',
|
|
formDataWithAdhocFilters.viz_type,
|
|
);
|
|
expect(result).toHaveProperty('datasource_id', 22);
|
|
expect(result).toHaveProperty('datasource_type', 'table');
|
|
expect(result).toHaveProperty('dashboards', dashboards);
|
|
expect(result).toHaveProperty('editors', editors);
|
|
expect(result).toHaveProperty('query_context');
|
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
|
formDataWithAdhocFilters.adhoc_filters,
|
|
);
|
|
});
|
|
|
|
test('should return the correct payload when formDataWithNativeFilters has a filter with isExtra set to true', async () => {
|
|
const formDataWithAdhocFiltersWithExtra: QueryFormData = {
|
|
...formDataWithNativeFilters,
|
|
adhoc_filters: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
},
|
|
],
|
|
};
|
|
const result = await getSlicePayload(
|
|
sliceName,
|
|
formDataWithAdhocFiltersWithExtra,
|
|
dashboards,
|
|
editors as [],
|
|
formDataFromSlice,
|
|
);
|
|
expect(result).toHaveProperty('params');
|
|
expect(result).toHaveProperty('slice_name', sliceName);
|
|
expect(result).toHaveProperty(
|
|
'viz_type',
|
|
formDataWithAdhocFiltersWithExtra.viz_type,
|
|
);
|
|
expect(result).toHaveProperty('datasource_id', 22);
|
|
expect(result).toHaveProperty('datasource_type', 'table');
|
|
expect(result).toHaveProperty('dashboards', dashboards);
|
|
expect(result).toHaveProperty('editors', editors);
|
|
expect(result).toHaveProperty('query_context');
|
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
|
formDataFromSlice.adhoc_filters,
|
|
);
|
|
});
|
|
|
|
test('should return the correct payload when formDataWithNativeFilters has a filter with isExtra set to true in mixed chart', async () => {
|
|
const formDataFromSliceWithAdhocFilterB: QueryFormData = {
|
|
...formDataFromSlice,
|
|
adhoc_filters_b: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
},
|
|
],
|
|
};
|
|
|
|
const formDataWithAdhocFiltersWithExtra: QueryFormData = {
|
|
...formDataWithNativeFilters,
|
|
viz_type: VizType.MixedTimeseries,
|
|
adhoc_filters: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
isExtra: true,
|
|
},
|
|
],
|
|
adhoc_filters_b: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
isExtra: true,
|
|
},
|
|
],
|
|
};
|
|
const result = await getSlicePayload(
|
|
sliceName,
|
|
formDataWithAdhocFiltersWithExtra,
|
|
dashboards,
|
|
editors as [],
|
|
formDataFromSliceWithAdhocFilterB,
|
|
);
|
|
|
|
expect(JSON.parse(result.params as string).adhoc_filters).toEqual(
|
|
formDataFromSliceWithAdhocFilterB.adhoc_filters,
|
|
);
|
|
expect(JSON.parse(result.params as string).adhoc_filters_b).toEqual(
|
|
formDataFromSliceWithAdhocFilterB.adhoc_filters_b,
|
|
);
|
|
});
|
|
|
|
test('should return the correct payload when formDataFromSliceWithAdhocFilter has no time range filters in mixed chart', async () => {
|
|
const formDataFromSliceWithAdhocFilterB: QueryFormData = {
|
|
...formDataFromSlice,
|
|
adhoc_filters: [],
|
|
adhoc_filters_b: [],
|
|
};
|
|
|
|
const formDataWithAdhocFiltersWithExtra: QueryFormData = {
|
|
...formDataWithNativeFilters,
|
|
viz_type: VizType.MixedTimeseries,
|
|
adhoc_filters: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
isExtra: true,
|
|
},
|
|
],
|
|
adhoc_filters_b: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
isExtra: true,
|
|
},
|
|
],
|
|
};
|
|
const result = await getSlicePayload(
|
|
sliceName,
|
|
formDataWithAdhocFiltersWithExtra,
|
|
dashboards,
|
|
editors as [],
|
|
formDataFromSliceWithAdhocFilterB,
|
|
);
|
|
|
|
const hasTemporalRange = (
|
|
JSON.parse(result.params as string).adhoc_filters_b || []
|
|
).some(
|
|
(filter: SimpleAdhocFilter) =>
|
|
filter.operator === Operators.TemporalRange,
|
|
);
|
|
|
|
expect(hasTemporalRange).toBe(true);
|
|
});
|
|
|
|
test('should reset isExtra flag to false for temporal filter when saving as a new chart', async () => {
|
|
const formDataWithTemporalFilterWithExtra: QueryFormData = {
|
|
...formDataWithNativeFilters,
|
|
adhoc_filters: [
|
|
{
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: '2004 : ',
|
|
expressionType: 'SIMPLE',
|
|
isExtra: true,
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await getSlicePayload(
|
|
sliceName,
|
|
formDataWithTemporalFilterWithExtra,
|
|
dashboards,
|
|
editors as [],
|
|
{} as QueryFormData,
|
|
);
|
|
|
|
const savedFilters = JSON.parse(result.params as string).adhoc_filters;
|
|
|
|
expect(savedFilters).toHaveLength(1);
|
|
expect(savedFilters[0]).toMatchObject({
|
|
clause: 'WHERE',
|
|
subject: 'year',
|
|
operator: 'TEMPORAL_RANGE',
|
|
comparator: 'No filter',
|
|
expressionType: 'SIMPLE',
|
|
isExtra: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
test('existing-chart overwrite covers stash-removed keys as drop transitions', async () => {
|
|
mockedIsFeatureEnabled.mockReturnValue(true);
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const dispatch = jest.fn();
|
|
const getState = () => ({
|
|
explore: {
|
|
// The stash removed order_desc from active form data...
|
|
form_data: {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: vizType,
|
|
row_limit: 10000,
|
|
},
|
|
// ...and holds it with the value it had when hidden.
|
|
hiddenFormData: { order_desc: true },
|
|
},
|
|
versionHistory: {
|
|
chartNormalization: {
|
|
chartId: sliceId,
|
|
hydrationSessionId: 'hydration-drop',
|
|
saveAttemptId: null,
|
|
invalidatedControls: {},
|
|
transitions: {},
|
|
},
|
|
},
|
|
});
|
|
|
|
await updateSlice(
|
|
{
|
|
...sliceResponsePayload,
|
|
slice_id: sliceId,
|
|
// Persisted params carry the key the stash removed, same value.
|
|
form_data: { ...formData, order_desc: true },
|
|
} as never,
|
|
sliceName,
|
|
[],
|
|
)(dispatch, getState);
|
|
|
|
const request = fetchMock.callHistory.lastCall(updateSliceEndpoint);
|
|
const body = JSON.parse(request?.options.body as string);
|
|
expect(body.normalization_changes).toEqual([
|
|
{
|
|
control: 'order_desc',
|
|
from_present: true,
|
|
from_value: true,
|
|
to_present: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('a stashed value the user changed before hiding is not covered', async () => {
|
|
mockedIsFeatureEnabled.mockReturnValue(true);
|
|
fetchMock.put(updateSliceEndpoint, sliceResponsePayload, {
|
|
name: updateSliceEndpoint,
|
|
});
|
|
const dispatch = jest.fn();
|
|
const getState = () => ({
|
|
explore: {
|
|
form_data: {
|
|
datasource: `${datasourceId}__${datasourceType}`,
|
|
viz_type: vizType,
|
|
row_limit: 10000,
|
|
},
|
|
// Stash holds a USER-edited value; persisted differs, so the removal
|
|
// stays recorded.
|
|
hiddenFormData: { order_desc: false },
|
|
},
|
|
versionHistory: {
|
|
chartNormalization: {
|
|
chartId: sliceId,
|
|
hydrationSessionId: 'hydration-drop-2',
|
|
saveAttemptId: null,
|
|
invalidatedControls: {},
|
|
transitions: {},
|
|
},
|
|
},
|
|
});
|
|
|
|
await updateSlice(
|
|
{
|
|
...sliceResponsePayload,
|
|
slice_id: sliceId,
|
|
form_data: { ...formData, order_desc: true },
|
|
} as never,
|
|
sliceName,
|
|
[],
|
|
)(dispatch, getState);
|
|
|
|
const request = fetchMock.callHistory.lastCall(updateSliceEndpoint);
|
|
const body = JSON.parse(request?.options.body as string);
|
|
expect(body.normalization_changes).toBeUndefined();
|
|
});
|