chore(Dashboard): Simplify scoping logic for cross/native filters (#30719)

This commit is contained in:
Geido 2024-10-30 14:19:26 +02:00 committed by GitHub
parent 73768f6313
commit d5a98e0189
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 89 additions and 304 deletions

View File

@ -378,7 +378,7 @@ export function cancelNativeFilterSettings() {
.should('be.visible')
.should('have.text', 'There are unsaved changes.');
cy.get(nativeFilters.modal.footer)
.find(nativeFilters.modal.yesCancelButton)
.find(nativeFilters.modal.confirmCancelButton)
.contains('cancel')
.click({ force: true });
cy.get(nativeFilters.modal.container).should('not.exist');

View File

@ -322,7 +322,9 @@ export const nativeFilters = {
footer: '.ant-modal-footer',
saveButton: dataTestLocator('native-filter-modal-save-button'),
cancelButton: dataTestLocator('native-filter-modal-cancel-button'),
yesCancelButton: '[type="button"]',
confirmCancelButton: dataTestLocator(
'native-filter-modal-confirm-cancel-button',
),
alertXUnsavedFilters: '.ant-alert-message',
tabsList: {
filterItemsContainer: dataTestLocator('filter-title-container'),

View File

@ -212,7 +212,7 @@ class Dashboard extends PureComponent {
applyFilters() {
const { appliedFilters } = this;
const { activeFilters, ownDataCharts, datasources, slices } = this.props;
const { activeFilters, ownDataCharts, slices } = this.props;
// refresh charts if a filter was removed, added, or changed
@ -231,22 +231,12 @@ class Dashboard extends PureComponent {
) {
// filterKey is removed?
affectedChartIds.push(
...getRelatedCharts(
appliedFilters,
activeFilters,
slices,
datasources,
)[filterKey],
...getRelatedCharts(appliedFilters, activeFilters, slices)[filterKey],
);
} else if (!appliedFilterKeys.includes(filterKey)) {
// filterKey is newly added?
affectedChartIds.push(
...getRelatedCharts(
activeFilters,
appliedFilters,
slices,
datasources,
)[filterKey],
...getRelatedCharts(activeFilters, appliedFilters, slices)[filterKey],
);
} else {
// if filterKey changes value,
@ -261,12 +251,9 @@ class Dashboard extends PureComponent {
)
) {
affectedChartIds.push(
...getRelatedCharts(
activeFilters,
appliedFilters,
slices,
datasources,
)[filterKey],
...getRelatedCharts(activeFilters, appliedFilters, slices)[
filterKey
],
);
}

View File

@ -17,13 +17,8 @@
* under the License.
*/
import { FC, useCallback, useRef, useState } from 'react';
import {
NativeFilterScope,
styled,
t,
useComponentDidUpdate,
} from '@superset-ui/core';
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { NativeFilterScope, styled, t } from '@superset-ui/core';
import { Radio } from 'src/components/Radio';
import { AntdForm, Typography } from 'src/components';
import { ScopingType } from './types';
@ -32,7 +27,7 @@ import { getDefaultScopeValue, isScopingAll } from './utils';
type FilterScopeProps = {
pathToFormValue?: string[];
updateFormValues: (values: any) => void;
updateFormValues: (values: any, triggerFormChange?: boolean) => void;
formFilterScope?: NativeFilterScope;
forceUpdate: Function;
filterScope?: NativeFilterScope;
@ -64,17 +59,19 @@ const FilterScope: FC<FilterScopeProps> = ({
chartId,
initiallyExcludedCharts,
}) => {
const [initialFilterScope] = useState(
filterScope || getDefaultScopeValue(chartId, initiallyExcludedCharts),
const initialFilterScope = useMemo(
() => filterScope || getDefaultScopeValue(chartId, initiallyExcludedCharts),
[chartId, filterScope, initiallyExcludedCharts],
);
const lastSpecificScope = useRef(initialFilterScope);
const [initialScopingType] = useState(
const initialScopingType = useMemo(
() =>
isScopingAll(initialFilterScope, chartId)
? ScopingType.All
: ScopingType.Specific,
[chartId, initialFilterScope],
);
const [hasScopeBeenModified, setHasScopeBeenModified] =
useState(!!filterScope);
const [hasScopeBeenModified, setHasScopeBeenModified] = useState(false);
const onUpdateFormValues = useCallback(
(formValues: any) => {
@ -87,26 +84,24 @@ const FilterScope: FC<FilterScopeProps> = ({
[formScopingType, updateFormValues],
);
const updateScopes = useCallback(() => {
if (filterScope || hasScopeBeenModified) {
const updateScopes = useCallback(
updatedFormValues => {
if (hasScopeBeenModified) {
return;
}
const newScope = getDefaultScopeValue(chartId, initiallyExcludedCharts);
updateFormValues({
scope: newScope,
scoping: isScopingAll(newScope, chartId)
? ScopingType.All
: ScopingType.Specific,
});
}, [
chartId,
filterScope,
hasScopeBeenModified,
initiallyExcludedCharts,
updateFormValues,
]);
useComponentDidUpdate(updateScopes);
updateFormValues(updatedFormValues, false);
},
[hasScopeBeenModified, updateFormValues],
);
useEffect(() => {
const updatedFormValues = {
scope: initialFilterScope,
scoping: initialScopingType,
};
updateScopes(updatedFormValues);
}, [initialFilterScope, initialScopingType, updateScopes]);
return (
<Wrapper>

View File

@ -583,9 +583,9 @@ const FiltersConfigForm = (
!datasetId || datasetDetails || formFilter?.dataset?.label;
const updateFormValues = useCallback(
(values: any) => {
(values: any, triggerFormChange = true) => {
setNativeFilterFieldValues(form, filterId, values);
formChanged();
if (triggerFormChange) formChanged();
},
[filterId, form, formChanged],
);

View File

@ -297,7 +297,6 @@ function FiltersConfigModal({
setRemovedFilters,
setOrderedFilters,
setSaveAlertVisible,
filterChanges,
filterId => {
setFilterChanges(prev => ({
...prev,
@ -510,7 +509,8 @@ function FiltersConfigModal({
unsavedFiltersIds.length > 0 ||
form.isFieldsTouched() ||
changed ||
didChangeOrder
didChangeOrder ||
Object.values(removedFilters).some(f => f?.isPending)
) {
setSaveAlertVisible(true);
} else {

View File

@ -61,6 +61,7 @@ export function CancelConfirmationAlert({
buttonSize="small"
buttonStyle="primary"
onClick={onConfirm}
data-test="native-filter-modal-confirm-cancel-button"
>
{t('Yes, cancel')}
</Button>

View File

@ -170,7 +170,6 @@ export const createHandleRemoveItem =
val: string[] | ((prevState: string[]) => string[]),
) => void,
setSaveAlertVisible: Function,
filterChanges: FilterChangesType,
removeFilter: (filterId: string) => void,
) =>
(filterId: string) => {

View File

@ -19,11 +19,9 @@
import {
AppliedCrossFilterType,
DatasourceType,
Filter,
NativeFilterType,
} from '@superset-ui/core';
import { DatasourcesState } from '../types';
import { getRelatedCharts } from './getRelatedCharts';
const slices = {
@ -32,48 +30,11 @@ const slices = {
'3': { datasource: 'ds1', slice_id: 3 },
} as any;
const datasources: DatasourcesState = {
ds1: {
uid: 'ds1',
datasource_name: 'ds1',
table_name: 'table1',
description: '',
id: 100,
columns: [{ column_name: 'column1' }, { column_name: 'column2' }],
column_names: ['column1', 'column2'],
column_types: [],
type: DatasourceType.Table,
metrics: [],
column_formats: {},
currency_formats: {},
verbose_map: {},
main_dttm_col: '',
filter_select_enabled: true,
},
ds2: {
uid: 'ds2',
datasource_name: 'ds2',
table_name: 'table2',
description: '',
id: 200,
columns: [{ column_name: 'column3' }, { column_name: 'column4' }],
column_names: ['column3', 'column4'],
column_types: [],
type: DatasourceType.Table,
metrics: [],
column_formats: {},
currency_formats: {},
verbose_map: {},
main_dttm_col: '',
filter_select_enabled: true,
},
};
test('Return chart ids matching the dataset id with native filter', () => {
test('Return all chart ids in global scope with native filters', () => {
const filters = {
filterKey1: {
filterType: 'filter_select',
chartsInScope: [1, 2],
chartsInScope: [1, 2, 3],
scope: {
excluded: [],
rootPath: [],
@ -88,54 +49,54 @@ test('Return chart ids matching the dataset id with native filter', () => {
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices, datasources);
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
filterKey1: [1],
filterKey1: [1, 2, 3],
});
});
test('Return chart ids matching the dataset id with cross filter', () => {
const filters = {
'3': {
filterType: undefined,
scope: [1, 2],
targets: [],
values: null,
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
'3': [1],
});
});
test('Return chart ids matching the column name with native filter', () => {
test('Return only chart ids in specific scope with native filters', () => {
const filters = {
filterKey1: {
filterType: 'filter_select',
chartsInScope: [1, 2],
chartsInScope: [1, 3],
scope: {
excluded: [],
rootPath: [],
},
targets: [
{
column: { name: 'column3' },
datasetId: 999,
column: { name: 'column1' },
datasetId: 100,
},
],
type: NativeFilterType.NativeFilter,
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices, datasources);
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
filterKey1: [2],
filterKey1: [1, 3],
});
});
test('Return chart ids matching the column name with cross filter', () => {
test('Return all chart ids with cross filter in global scope', () => {
const filters = {
'3': {
filterType: undefined,
scope: [1, 2, 3],
targets: [],
values: null,
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
'3': [1, 2],
});
});
test('Return only chart ids in specific scope with cross filter', () => {
const filters = {
'1': {
filterType: undefined,
@ -147,108 +108,8 @@ test('Return chart ids matching the column name with cross filter', () => {
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices, datasources);
const result = getRelatedCharts(filters, null, slices);
expect(result).toEqual({
'1': [2],
});
});
test('Return chart ids when column display name matches with native filter', () => {
const filters = {
filterKey1: {
filterType: 'filter_select',
chartsInScope: [1, 2],
scope: {
excluded: [],
rootPath: [],
},
targets: [
{
column: { name: 'column4', displayName: 'column4' },
datasetId: 999,
},
],
type: NativeFilterType.NativeFilter,
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
filterKey1: [2],
});
});
test('Return chart ids when column display name matches with cross filter', () => {
const filters = {
'1': {
filterType: undefined,
scope: [1, 2],
targets: [],
values: {
filters: [{ col: 'column4' }],
},
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
'1': [2],
});
});
test('Return scope when filterType is not filter_select', () => {
const filters = {
filterKey1: {
filterType: 'filter_time',
chartsInScope: [3, 4],
} as Filter,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
filterKey1: [3, 4],
});
});
test('Return an empty array if no matching charts found with native filter', () => {
const filters = {
filterKey1: {
filterType: 'filter_select',
chartsInScope: [1, 2],
scope: {
excluded: [],
rootPath: [],
},
targets: [
{
column: { name: 'nonexistent_column' },
datasetId: 300,
},
],
type: NativeFilterType.NativeFilter,
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
filterKey1: [],
});
});
test('Return an empty array if no matching charts found with cross filter', () => {
const filters = {
'1': {
filterType: undefined,
scope: [1, 2],
targets: [],
values: {
filters: [{ col: 'nonexistent_column' }],
},
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
'1': [],
});
});

View File

@ -20,56 +20,31 @@
import {
AppliedCrossFilterType,
AppliedNativeFilterType,
ensureIsArray,
Filter,
isAppliedCrossFilterType,
isAppliedNativeFilterType,
isNativeFilter,
} from '@superset-ui/core';
import { Slice } from 'src/types/Chart';
import { DatasourcesState } from '../types';
function checkForExpression(formData?: Record<string, any>) {
const groupby = ensureIsArray(formData?.groupby) ?? [];
const allColumns = ensureIsArray(formData?.all_columns) ?? [];
const checkColumns = groupby.concat(allColumns);
return checkColumns.some(
(col: string | Record<string, any>) =>
typeof col !== 'string' && col.expressionType !== undefined,
);
function isGlobalScope(scope: number[], slices: Record<string, Slice>) {
return scope.length === Object.keys(slices).length;
}
function getRelatedChartsForSelectFilter(
nativeFilter: AppliedNativeFilterType | Filter,
slices: Record<string, Slice>,
chartsInScope: number[],
datasources: DatasourcesState,
) {
return Object.values(slices)
.filter(slice => {
const { datasource, slice_id } = slice;
// not in scope, ignore
if (!chartsInScope.includes(slice_id)) return false;
const { slice_id } = slice;
// all have been selected, always apply
if (isGlobalScope(chartsInScope, slices)) return true;
// hand-picked in scope, always apply
if (chartsInScope.includes(slice_id)) return true;
const chartDatasource = datasource
? datasources[datasource]
: Object.values(datasources).find(ds => ds.id === slice.datasource_id);
const { column, datasetId } = nativeFilter.targets?.[0] ?? {};
const datasourceColumnNames = chartDatasource?.column_names ?? [];
// same datasource, always apply
if (chartDatasource?.id === datasetId) return true;
// let backend deal with adhoc columns and jinja
const hasSqlExpression = checkForExpression(slice.form_data);
if (hasSqlExpression) {
return true;
}
return datasourceColumnNames.some(
col => col === column?.name || col === column?.displayName,
);
return false;
})
.map(slice => slice.slice_id);
}
@ -78,52 +53,23 @@ function getRelatedChartsForCrossFilter(
crossFilter: AppliedCrossFilterType,
slices: Record<string, Slice>,
scope: number[],
datasources: DatasourcesState,
): number[] {
const sourceSlice = slices[filterKey];
const filters = crossFilter?.values?.filters ?? [];
if (!sourceSlice) return [];
const sourceDatasource = sourceSlice.datasource
? datasources[sourceSlice.datasource]
: Object.values(datasources).find(
ds => ds.id === sourceSlice.datasource_id,
);
return Object.values(slices)
.filter(slice => {
// cross-filter emitter
if (slice.slice_id === Number(filterKey)) return false;
// not in scope, ignore
if (!scope.includes(slice.slice_id)) return false;
const targetDatasource = slice.datasource
? datasources[slice.datasource]
: Object.values(datasources).find(ds => ds.id === slice.datasource_id);
// same datasource, always apply
if (targetDatasource === sourceDatasource) return true;
const targetColumnNames = targetDatasource?.column_names ?? [];
// let backend deal with adhoc columns and jinja
const hasSqlExpression = checkForExpression(slice.form_data);
if (hasSqlExpression) {
return true;
}
for (const filter of filters) {
// let backend deal with adhoc columns
if (
typeof filter.col !== 'string' &&
filter.col.expressionType !== undefined
) {
return true;
}
// shared column names, different datasources, apply filter
if (targetColumnNames.includes(filter.col)) return true;
}
// hand-picked in the scope, always apply
const fullScope = [
...scope.filter(s => String(s) !== filterKey),
Number(filterKey),
];
if (isGlobalScope(fullScope, slices)) return true;
// hand-picked in the scope, always apply
if (scope.includes(slice.slice_id)) return true;
return false;
})
@ -140,7 +86,6 @@ export function getRelatedCharts(
AppliedNativeFilterType | AppliedCrossFilterType | Filter
> | null,
slices: Record<string, Slice>,
datasources: DatasourcesState,
) {
const related = Object.entries(filters).reduce((acc, [filterKey, filter]) => {
const isCrossFilter =
@ -168,7 +113,6 @@ export function getRelatedCharts(
actualCrossFilter,
slices,
chartsInScope,
datasources,
),
};
}
@ -186,7 +130,6 @@ export function getRelatedCharts(
nativeFilter,
slices,
chartsInScope,
datasources,
),
};
}

View File

@ -51,8 +51,6 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
dashboardFilters,
);
const datasources =
useSelector((state: RootState) => state.datasources) || {};
const slices =
useSelector((state: RootState) => state.sliceEntities.slices) || {};
@ -60,7 +58,6 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
nativeFilters.filters as Record<string, Filter>,
null,
slices,
datasources,
);
const highlightedFilterId =