fix(Filters): Apply native & cross filters on common columns (#30438)

This commit is contained in:
Geido 2024-10-16 02:13:05 +03:00 committed by GitHub
parent 4cac7feb67
commit 362948324c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 656 additions and 38 deletions

View File

@ -3053,6 +3053,12 @@
},
"type": "array"
},
"column_names": {
"items": {
"type": "string"
},
"type": "array"
},
"currency_formats": {
"type": "object"
},

View File

@ -83,6 +83,7 @@ export interface Dataset {
owners?: Owner[];
filter_select?: boolean;
filter_select_enabled?: boolean;
column_names?: string[];
}
export interface ControlPanelState {

View File

@ -80,6 +80,24 @@ export type Filter = {
description: string;
};
export type AppliedFilter = {
values: {
filters: Record<string, any>[];
} | null;
};
export type AppliedCrossFilterType = {
filterType: undefined;
targets: number[];
scope: number[];
} & AppliedFilter;
export type AppliedNativeFilterType = {
filterType: 'filter_select';
scope: number[];
targets: Partial<NativeFilterTarget>[];
} & AppliedFilter;
export type FilterWithDataMask = Filter & { dataMask: DataMaskWithId };
export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
@ -89,6 +107,24 @@ export type Divider = Partial<Omit<Filter, 'id' | 'type'>> & {
type: typeof NativeFilterType.Divider;
};
export function isAppliedCrossFilterType(
filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
): filterElement is AppliedCrossFilterType {
return (
filterElement.filterType === undefined &&
filterElement.hasOwnProperty('values')
);
}
export function isAppliedNativeFilterType(
filterElement: AppliedCrossFilterType | AppliedNativeFilterType | Filter,
): filterElement is AppliedNativeFilterType {
return (
filterElement.filterType === 'filter_select' &&
filterElement.hasOwnProperty('values')
);
}
export function isNativeFilter(
filterElement: Filter | Divider,
): filterElement is Filter {

View File

@ -24,6 +24,10 @@ import {
FilterWithDataMask,
Divider,
isNativeFilterWithDataMask,
isAppliedCrossFilterType,
isAppliedNativeFilterType,
AppliedCrossFilterType,
AppliedNativeFilterType,
} from '@superset-ui/core';
const filter: Filter = {
@ -51,6 +55,20 @@ const filterDivider: Divider = {
description: 'Divider description.',
};
const appliedCrossFilter: AppliedCrossFilterType = {
filterType: undefined,
targets: [1, 2],
scope: [1, 2],
values: null,
};
const appliedNativeFilter: AppliedNativeFilterType = {
filterType: 'filter_select',
scope: [1, 2],
targets: [{}],
values: null,
};
test('filter type guard', () => {
expect(isNativeFilter(filter)).toBeTruthy();
expect(isNativeFilter(filterWithDataMask)).toBeTruthy();
@ -68,3 +86,13 @@ test('filter divider type guard', () => {
expect(isFilterDivider(filterWithDataMask)).toBeFalsy();
expect(isFilterDivider(filterDivider)).toBeTruthy();
});
test('applied cross filter type guard', () => {
expect(isAppliedCrossFilterType(appliedCrossFilter)).toBeTruthy();
expect(isAppliedCrossFilterType(appliedNativeFilter)).toBeFalsy();
});
test('applied native filter type guard', () => {
expect(isAppliedNativeFilterType(appliedNativeFilter)).toBeTruthy();
expect(isAppliedNativeFilterType(appliedCrossFilter)).toBeFalsy();
});

View File

@ -26,6 +26,7 @@ export default {
{
id: 'DefaultsID',
filterType: 'filter_select',
chartsInScope: [],
targets: [{}],
cascadeParentIds: [],
},

View File

@ -133,6 +133,7 @@ export const singleNativeFiltersState = {
id: [NATIVE_FILTER_ID],
name: 'eth',
type: 'text',
filterType: 'filter_select',
targets: [{ datasetId: 13, column: { name: 'ethnic_minority' } }],
defaultDataMask: {
filterState: {

View File

@ -61,7 +61,7 @@ import {
dashboardInfoChanged,
SAVE_CHART_CONFIG_COMPLETE,
} from './dashboardInfo';
import { fetchDatasourceMetadata } from './datasources';
import { fetchDatasourceMetadata, setDatasources } from './datasources';
import { updateDirectPathToFilter } from './dashboardFilters';
import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters';
import getOverwriteItems from '../util/getOverwriteItems';
@ -341,6 +341,17 @@ export function saveDashboardRequest(data, id, saveType) {
filterConfig: metadata.native_filter_configuration,
});
}
// fetch datasets to make sure they are up to date
SupersetClient.get({
endpoint: `/api/v1/dashboard/${id}/datasets`,
headers: { 'Content-Type': 'application/json' },
}).then(({ json }) => {
const datasources = json?.result ?? [];
if (datasources.length) {
dispatch(setDatasources(datasources));
}
});
}
if (lastModifiedTime) {
dispatch(saveDashboardRequestSuccess(lastModifiedTime));

View File

@ -41,6 +41,7 @@ import { areObjectsEqual } from '../../reduxUtils';
import getLocationHash from '../util/getLocationHash';
import isDashboardEmpty from '../util/isDashboardEmpty';
import { getAffectedOwnDataCharts } from '../util/charts/getOwnDataCharts';
import { getRelatedCharts } from '../util/getRelatedCharts';
const propTypes = {
actions: PropTypes.shape({
@ -211,9 +212,10 @@ class Dashboard extends PureComponent {
applyFilters() {
const { appliedFilters } = this;
const { activeFilters, ownDataCharts } = this.props;
const { activeFilters, ownDataCharts, datasources, slices } = this.props;
// refresh charts if a filter was removed, added, or changed
const currFilterKeys = Object.keys(activeFilters);
const appliedFilterKeys = Object.keys(appliedFilters);
@ -228,10 +230,24 @@ class Dashboard extends PureComponent {
appliedFilterKeys.includes(filterKey)
) {
// filterKey is removed?
affectedChartIds.push(...appliedFilters[filterKey].scope);
affectedChartIds.push(
...getRelatedCharts(
appliedFilters,
activeFilters,
slices,
datasources,
)[filterKey],
);
} else if (!appliedFilterKeys.includes(filterKey)) {
// filterKey is newly added?
affectedChartIds.push(...activeFilters[filterKey].scope);
affectedChartIds.push(
...getRelatedCharts(
activeFilters,
appliedFilters,
slices,
datasources,
)[filterKey],
);
} else {
// if filterKey changes value,
// update charts in its scope
@ -244,7 +260,14 @@ class Dashboard extends PureComponent {
},
)
) {
affectedChartIds.push(...activeFilters[filterKey].scope);
affectedChartIds.push(
...getRelatedCharts(
activeFilters,
appliedFilters,
slices,
datasources,
)[filterKey],
);
}
// if filterKey changes scope,

View File

@ -37,6 +37,9 @@ import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
import dashboardState from 'spec/fixtures/mockDashboardState';
import { sliceEntitiesForChart as sliceEntities } from 'spec/fixtures/mockSliceEntities';
import { getAllActiveFilters } from 'src/dashboard/util/activeAllDashboardFilters';
import { getRelatedCharts } from 'src/dashboard/util/getRelatedCharts';
jest.mock('src/dashboard/util/getRelatedCharts');
describe('Dashboard', () => {
const props = {
@ -130,6 +133,7 @@ describe('Dashboard', () => {
afterEach(() => {
refreshSpy.restore();
jest.clearAllMocks();
});
it('should not call refresh when is editMode', () => {
@ -153,6 +157,9 @@ describe('Dashboard', () => {
});
it('should call refresh when native filters changed', () => {
getRelatedCharts.mockReturnValue({
[NATIVE_FILTER_ID]: [230],
});
wrapper.setProps({
activeFilters: {
...OVERRIDE_FILTERS,
@ -170,11 +177,27 @@ describe('Dashboard', () => {
[NATIVE_FILTER_ID]: {
scope: [230],
values: extraFormData,
filterType: 'filter_select',
targets: [
{
datasetId: 13,
column: {
name: 'ethnic_minority',
},
},
],
},
});
});
it('should call refresh if a filter is added', () => {
getRelatedCharts.mockReturnValue({
'1_region': [1],
'2_country_name': [1, 2],
'3_region': [1],
'3_country_name': [],
gender: [1],
});
const newFilter = {
gender: { values: ['boy', 'girl'], scope: [1] },
};
@ -186,6 +209,12 @@ describe('Dashboard', () => {
});
it('should call refresh if a filter is removed', () => {
getRelatedCharts.mockReturnValue({
'1_region': [1],
'2_country_name': [1, 2],
'3_region': [1],
'3_country_name': [],
});
wrapper.setProps({
activeFilters: {},
});

View File

@ -51,6 +51,11 @@ test('renders diff viewer when it contains overwriteConfirmMetadata', async () =
test('requests update dashboard api when save button is clicked', async () => {
const updateDashboardEndpoint = `glob:*/api/v1/dashboard/${overwriteConfirmMetadata.dashboardId}`;
const fetchDatasetsEndpoint = `glob:*/api/v1/dashboard/${overwriteConfirmMetadata.dashboardId}/datasets`;
// mock fetch datasets
fetchMock.get(fetchDatasetsEndpoint, []);
fetchMock.put(updateDashboardEndpoint, {
id: overwriteConfirmMetadata.dashboardId,
last_modified_time: +new Date(),

View File

@ -39,6 +39,7 @@ export const nativeFiltersInfo: NativeFiltersState = {
id: 'DefaultsID',
name: 'test',
filterType: 'filter_select',
chartsInScope: [],
targets: [
{
datasetId: 0,

View File

@ -54,9 +54,13 @@ export const getAllActiveFilters = ({
chartConfiguration?.[filterId]?.crossFilters?.chartsInScope ??
allSliceIds ??
[];
const filterType = nativeFilters?.[filterId]?.filterType;
const targets = nativeFilters?.[filterId]?.targets ?? scope;
// Iterate over all roots to find all affected charts
activeFilters[filterId] = {
scope,
filterType,
targets,
values: extraFormData,
};
});

View File

@ -294,14 +294,14 @@ test('Recalculate charts in global filter scope when charts change', () => {
id: 2,
crossFilters: {
scope: 'global',
chartsInScope: [1],
chartsInScope: [1, 3],
},
},
'3': {
id: 3,
crossFilters: {
scope: 'global',
chartsInScope: [],
chartsInScope: [1, 2],
},
},
},

View File

@ -52,20 +52,6 @@ export const getCrossFiltersConfiguration = (
return undefined;
}
const chartsByDataSource: Record<string, Set<number>> = Object.values(
charts,
).reduce((acc: Record<string, Set<number>>, chart) => {
if (!chart.form_data) {
return acc;
}
const { datasource } = chart.form_data;
if (!acc[datasource]) {
acc[datasource] = new Set();
}
acc[datasource].add(chart.id);
return acc;
}, {});
const globalChartConfiguration = metadata.global_chart_configuration?.scope
? {
scope: metadata.global_chart_configuration.scope,
@ -111,13 +97,10 @@ export const getCrossFiltersConfiguration = (
},
};
}
const chartDataSource = charts[chartId].form_data.datasource;
chartConfiguration[chartId].crossFilters.chartsInScope =
isCrossFilterScopeGlobal(chartConfiguration[chartId].crossFilters.scope)
? globalChartConfiguration.chartsInScope.filter(
id =>
id !== Number(chartId) &&
chartsByDataSource[chartDataSource]?.has(id),
id => id !== Number(chartId),
)
: getChartIdsInFilterScope(
chartConfiguration[chartId].crossFilters.scope,

View File

@ -0,0 +1,254 @@
/**
* 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 {
AppliedCrossFilterType,
DatasourceType,
Filter,
NativeFilterType,
} from '@superset-ui/core';
import { DatasourcesState } from '../types';
import { getRelatedCharts } from './getRelatedCharts';
const slices = {
'1': { datasource: 'ds1', slice_id: 1 },
'2': { datasource: 'ds2', slice_id: 2 },
'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', () => {
const filters = {
filterKey1: {
filterType: 'filter_select',
chartsInScope: [1, 2],
scope: {
excluded: [],
rootPath: [],
},
targets: [
{
column: { name: 'column1' },
datasetId: 100,
},
],
type: NativeFilterType.NativeFilter,
} as unknown as Filter,
};
const result = getRelatedCharts(filters, null, slices, datasources);
expect(result).toEqual({
filterKey1: [1],
});
});
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', () => {
const filters = {
filterKey1: {
filterType: 'filter_select',
chartsInScope: [1, 2],
scope: {
excluded: [],
rootPath: [],
},
targets: [
{
column: { name: 'column3' },
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 matching the column name with cross filter', () => {
const filters = {
'1': {
filterType: undefined,
scope: [1, 2],
targets: [],
values: {
filters: [{ col: 'column3' }],
},
} as AppliedCrossFilterType,
};
const result = getRelatedCharts(filters, null, slices, datasources);
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

@ -0,0 +1,200 @@
/**
* 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 {
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 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 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,
);
})
.map(slice => slice.slice_id);
}
function getRelatedChartsForCrossFilter(
filterKey: string,
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;
}
return false;
})
.map(slice => slice.slice_id);
}
export function getRelatedCharts(
filters: Record<
string,
AppliedNativeFilterType | AppliedCrossFilterType | Filter
>,
checkFilters: Record<
string,
AppliedNativeFilterType | AppliedCrossFilterType | Filter
> | null,
slices: Record<string, Slice>,
datasources: DatasourcesState,
) {
const related = Object.entries(filters).reduce((acc, [filterKey, filter]) => {
const isCrossFilter =
Object.keys(slices).includes(filterKey) &&
isAppliedCrossFilterType(filter);
const chartsInScope = Array.isArray(filter.scope)
? filter.scope
: ((filter as Filter).chartsInScope ?? []);
if (isCrossFilter) {
const checkFilter = checkFilters?.[filterKey] as AppliedCrossFilterType;
const crossFilter = filter as AppliedCrossFilterType;
const wasRemoved = !!(
((filter.values && filter.values.filters === undefined) ||
filter.values?.filters?.length === 0) &&
checkFilter?.values?.filters?.length
);
const actualCrossFilter = wasRemoved ? checkFilter : crossFilter;
return {
...acc,
[filterKey]: getRelatedChartsForCrossFilter(
filterKey,
actualCrossFilter,
slices,
chartsInScope,
datasources,
),
};
}
const nativeFilter = filter as AppliedNativeFilterType | Filter;
// on highlight, a standard native filter is passed
// on apply, an applied native filter is passed
if (
isAppliedNativeFilterType(nativeFilter) ||
isNativeFilter(nativeFilter)
) {
return {
...acc,
[filterKey]: getRelatedChartsForSelectFilter(
nativeFilter,
slices,
chartsInScope,
datasources,
),
};
}
return {
...acc,
[filterKey]: chartsInScope,
};
}, {});
return related;
}

View File

@ -24,6 +24,9 @@ import reducerIndex from 'spec/helpers/reducerIndex';
import { screen, render } from 'spec/helpers/testing-library';
import { initialState } from 'src/SqlLab/fixtures';
import useFilterFocusHighlightStyles from './useFilterFocusHighlightStyles';
import { getRelatedCharts } from './getRelatedCharts';
jest.mock('./getRelatedCharts');
const TestComponent = ({ chartId }: { chartId: number }) => {
const styles = useFilterFocusHighlightStyles(chartId);
@ -38,6 +41,7 @@ describe('useFilterFocusHighlightStyles', () => {
{ ...mockState, ...(initialState as any), ...customState },
compose(applyMiddleware(thunk)),
);
const mockGetRelatedCharts = getRelatedCharts as jest.Mock;
const renderWrapper = (chartId: number, store = createMockStore()) =>
render(<TestComponent chartId={chartId} />, {
@ -57,6 +61,9 @@ describe('useFilterFocusHighlightStyles', () => {
});
it('should return unfocused styles if chart is not in scope of focused native filter', async () => {
mockGetRelatedCharts.mockReturnValue({
'test-filter': [],
});
const store = createMockStore({
nativeFilters: {
focusedFilterId: 'test-filter',
@ -76,6 +83,9 @@ describe('useFilterFocusHighlightStyles', () => {
});
it('should return unfocused styles if chart is not in scope of hovered native filter', async () => {
mockGetRelatedCharts.mockReturnValue({
'test-filter': [],
});
const store = createMockStore({
nativeFilters: {
hoveredFilterId: 'test-filter',
@ -96,6 +106,9 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return focused styles if chart is in scope of focused native filter', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [chartId],
});
const store = createMockStore({
nativeFilters: {
focusedFilterId: 'testFilter',
@ -116,6 +129,9 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return focused styles if chart is in scope of hovered native filter', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [chartId],
});
const store = createMockStore({
nativeFilters: {
hoveredFilterId: 'testFilter',
@ -136,6 +152,9 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return unfocused styles if focusedFilterField is targeting a different chart', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [],
});
const store = createMockStore({
dashboardState: {
focusedFilterField: {
@ -159,6 +178,9 @@ describe('useFilterFocusHighlightStyles', () => {
it('should return focused styles if focusedFilterField chart equals our own', async () => {
const chartId = 18;
mockGetRelatedCharts.mockReturnValue({
testFilter: [chartId],
});
const store = createMockStore({
dashboardState: {
focusedFilterField: {

View File

@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useTheme } from '@superset-ui/core';
import { Filter, useTheme } from '@superset-ui/core';
import { useSelector } from 'react-redux';
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
import { DashboardState, RootState } from 'src/dashboard/types';
import { getRelatedCharts } from './getRelatedCharts';
const selectFocusedFilterScope = (
dashboardState: DashboardState,
@ -41,6 +42,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
const dashboardState = useSelector(
(state: RootState) => state.dashboardState,
);
const dashboardFilters = useSelector(
(state: RootState) => state.dashboardFilters,
);
@ -49,6 +51,18 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
dashboardFilters,
);
const datasources =
useSelector((state: RootState) => state.datasources) || {};
const slices =
useSelector((state: RootState) => state.sliceEntities.slices) || {};
const relatedCharts = getRelatedCharts(
nativeFilters.filters as Record<string, Filter>,
null,
slices,
datasources,
);
const highlightedFilterId =
nativeFilters?.focusedFilterId || nativeFilters?.hoveredFilterId;
if (!(focusedFilterScope || highlightedFilterId)) {
@ -69,11 +83,7 @@ const useFilterFocusHighlightStyles = (chartId: number) => {
};
if (highlightedFilterId) {
if (
nativeFilters.filters[highlightedFilterId]?.chartsInScope?.includes(
chartId,
)
) {
if (relatedCharts[highlightedFilterId]?.includes(chartId)) {
return focusedChartStyles;
}
} else if (

View File

@ -74,6 +74,8 @@ export type Slice = {
query_context?: object;
is_managed_externally: boolean;
owners?: number[];
datasource?: string;
datasource_id?: number;
};
export default Chart;

View File

@ -489,6 +489,11 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable=
del data["description"]
data.update({"metrics": filtered_metrics})
data.update({"columns": filtered_columns})
all_columns = {
column_["column_name"]: column_["verbose_name"] or column_["column_name"]
for column_ in filtered_columns
}
verbose_map = {"__timestamp": "Time"}
verbose_map.update(
{
@ -496,14 +501,9 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable=
for metric in filtered_metrics
}
)
verbose_map.update(
{
column_["column_name"]: column_["verbose_name"]
or column_["column_name"]
for column_ in filtered_columns
}
)
verbose_map.update(all_columns)
data["verbose_map"] = verbose_map
data["column_names"] = set(all_columns.values()) | set(self.column_names)
return data

View File

@ -262,6 +262,7 @@ class DashboardDatasetSchema(Schema):
owners = fields.List(fields.Dict())
columns = fields.List(fields.Dict())
column_types = fields.List(fields.Int())
column_names = fields.List(fields.Str())
metrics = fields.List(fields.Dict())
order_by_choices = fields.List(fields.List(fields.Str()))
verbose_map = fields.Dict(fields.Str(), fields.Str())