feat(datasets): Allow swap dataset after deletion (#30364)

This commit is contained in:
Antonio Rivero 2024-09-25 18:12:45 +02:00 committed by GitHub
parent 39f1b714a5
commit 18c2376b50
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 214 additions and 16 deletions

View File

@ -286,3 +286,175 @@ test('SQL ad-hoc filter values', () => {
sqlExpression: 'select * from sample_column_1;', sqlExpression: 'select * from sample_column_1;',
}); });
}); });
test('no controlState value but valid column in datasource', () => {
const controlState = {
...sharedControls.columns,
options: [], // no options in the control state
};
expect(
getValues({
...controlState,
value: 'sample_column_1', // column only available in datasource
}),
).toEqual('sample_column_1');
expect(
getValues({
...controlState,
value: 'non_existing_column',
}),
).toEqual(controlState.default);
});
test('no controlState value but valid saved metric in datasource', () => {
const controlState = {
...sharedControls.metrics,
savedMetrics: [], // no saved metrics in the control state
};
expect(
getValues({
...controlState,
value: 'saved_metric_2', // metric only available in datasource
}),
).toEqual('saved_metric_2');
expect(
getValues({
...controlState,
value: 'non_existing_metric',
}),
).toEqual(controlState.default);
});
test('no controlState value but valid adhoc metric in datasource', () => {
const controlState = {
...sharedControls.metrics,
columns: [], // no columns in control state
};
expect(
getValues({
...controlState,
value: {
expressionType: 'SIMPLE',
column: { column_name: 'sample_column_1' }, // only in datasource
},
}),
).toEqual({
expressionType: 'SIMPLE',
column: { column_name: 'sample_column_1' },
});
expect(
getValues({
...controlState,
value: {
expressionType: 'SIMPLE',
column: { column_name: 'non_existing_column' },
},
}),
).toEqual(controlState.default);
});
test('no controlState value but valid adhoc filter in datasource', () => {
const controlState = {
...sharedControls.adhoc_filters,
columns: [], // no columns in control state
};
expect(
getValues({
...controlState,
value: {
expressionType: 'SIMPLE',
subject: 'sample_column_1', // column available in datasource
},
}),
).toEqual({
expressionType: 'SIMPLE',
subject: 'sample_column_1',
});
expect(
getValues({
...controlState,
value: {
expressionType: 'SIMPLE',
subject: 'non_existing_column',
},
}),
).toEqual(controlState.default);
});
test('SQL ad-hoc metric values without controlState columns', () => {
const controlState = {
...sharedControls.metrics,
columns: [], // No columns in controlState
};
expect(
getValues({
...controlState,
value: {
expressionType: 'SQL',
sqlExpression: 'SELECT COUNT(*) FROM sample_table;',
},
}),
).toEqual({
datasourceWarning: true,
expressionType: 'SQL',
sqlExpression: 'SELECT COUNT(*) FROM sample_table;',
});
expect(
getValues({
...controlState,
value: {
expressionType: 'SQL',
sqlExpression: 'SELECT column FROM non_existing_table;',
},
}),
).toEqual({
datasourceWarning: true,
expressionType: 'SQL',
sqlExpression: 'SELECT column FROM non_existing_table;',
});
});
test('SQL ad-hoc filter values without controlState columns', () => {
const controlState = {
...sharedControls.adhoc_filters,
columns: [], // No columns in controlState
};
expect(
getValues({
...controlState,
value: {
expressionType: 'SQL',
sqlExpression: 'SELECT * FROM sample_table WHERE column = 1;',
},
}),
).toEqual({
datasourceWarning: true,
expressionType: 'SQL',
sqlExpression: 'SELECT * FROM sample_table WHERE column = 1;',
});
expect(
getValues({
...controlState,
value: {
expressionType: 'SQL',
sqlExpression: 'SELECT * FROM non_existing_table;',
},
}),
).toEqual({
datasourceWarning: true,
expressionType: 'SQL',
sqlExpression: 'SELECT * FROM non_existing_table;',
});
});

View File

@ -27,6 +27,7 @@ import {
JsonValue, JsonValue,
SimpleAdhocFilter, SimpleAdhocFilter,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { isEmpty } from 'lodash';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
const isControlValueCompatibleWithDatasource = ( const isControlValueCompatibleWithDatasource = (
@ -34,24 +35,32 @@ const isControlValueCompatibleWithDatasource = (
controlState: ControlState, controlState: ControlState,
value: any, value: any,
) => { ) => {
// A datasource might have been deleted, in which case we can't validate
// only using the control state since it might have been hydrated with
// the wrong options or columns (empty arrays).
if (controlState.options && typeof value === 'string') { if (controlState.options && typeof value === 'string') {
if ( if (
controlState.options.some( (!isEmpty(controlState.options) &&
(option: [string | number, string] | { column_name: string }) => controlState.options.some(
Array.isArray(option) (option: [string | number, string] | { column_name: string }) =>
? option[0] === value Array.isArray(option)
: option.column_name === value, ? option[0] === value
) : option.column_name === value,
)) ||
!isEmpty(datasource?.columns)
) { ) {
return datasource.columns.some(column => column.column_name === value); return datasource.columns.some(
(column: Column) => column.column_name === value,
);
} }
} }
if ( if (
controlState.savedMetrics && controlState.savedMetrics &&
isSavedMetric(value) && isSavedMetric(value) &&
controlState.savedMetrics.some( (controlState.savedMetrics.some(
(savedMetric: Metric) => savedMetric.metric_name === value, (savedMetric: Metric) => savedMetric.metric_name === value,
) ) ||
!isEmpty(datasource?.metrics))
) { ) {
return datasource.metrics.some( return datasource.metrics.some(
(metric: Metric) => metric.metric_name === value, (metric: Metric) => metric.metric_name === value,
@ -60,11 +69,13 @@ const isControlValueCompatibleWithDatasource = (
if ( if (
controlState.columns && controlState.columns &&
(isAdhocMetricSimple(value) || isSimpleAdhocFilter(value)) && (isAdhocMetricSimple(value) || isSimpleAdhocFilter(value)) &&
controlState.columns.some( ((!isEmpty(controlState.columns) &&
(column: Column) => controlState.columns.some(
column.column_name === (value as AdhocMetric).column?.column_name || (column: Column) =>
column.column_name === (value as SimpleAdhocFilter).subject, column.column_name === (value as AdhocMetric).column?.column_name ||
) column.column_name === (value as SimpleAdhocFilter).subject,
)) ||
!isEmpty(datasource?.columns))
) { ) {
return datasource.columns.some( return datasource.columns.some(
(column: Column) => (column: Column) =>

View File

@ -43,7 +43,10 @@ import { getItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { getFormDataWithDashboardContext } from 'src/explore/controlUtils/getFormDataWithDashboardContext'; import { getFormDataWithDashboardContext } from 'src/explore/controlUtils/getFormDataWithDashboardContext';
const isValidResult = (rv: JsonObject): boolean => const isValidResult = (rv: JsonObject): boolean =>
rv?.result?.form_data && isDefined(rv?.result?.dataset?.id); rv?.result?.form_data && rv?.result?.dataset;
const hasDatasetId = (rv: JsonObject): boolean =>
isDefined(rv?.result?.dataset?.id);
const fetchExploreData = async (exploreUrlParams: URLSearchParams) => { const fetchExploreData = async (exploreUrlParams: URLSearchParams) => {
try { try {
@ -52,7 +55,19 @@ const fetchExploreData = async (exploreUrlParams: URLSearchParams) => {
endpoint: 'api/v1/explore/', endpoint: 'api/v1/explore/',
})(exploreUrlParams); })(exploreUrlParams);
if (isValidResult(rv)) { if (isValidResult(rv)) {
return rv; if (hasDatasetId(rv)) {
return rv;
}
// Since there's no dataset id but the API responded with a valid payload,
// we assume the dataset was deleted, so we preserve some values from previous
// state so if the user decide to swap the datasource, the chart config remains
fallbackExploreInitialData.form_data = {
...rv.result.form_data,
...fallbackExploreInitialData.form_data,
};
if (rv.result?.slice) {
fallbackExploreInitialData.slice = rv.result.slice;
}
} }
let message = t('Failed to load chart data'); let message = t('Failed to load chart data');
const responseError = rv?.result?.message; const responseError = rv?.result?.message;