diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index a09fb00b556bc..1f346a13d405b 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -97,7 +97,6 @@ These features flags currently default to True and **will be removed in a future [//]: # "PLEASE KEEP THE LIST SORTED ALPHABETICALLY" - AVOID_COLORS_COLLISION -- DASHBOARD_CROSS_FILTERS - DRILL_TO_DETAIL - ENABLE_JAVASCRIPT_CONTROLS - KV_STORE diff --git a/UPDATING.md b/UPDATING.md index b680f2701c395..64ad91df752fd 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -23,6 +23,7 @@ This file documents any backwards-incompatible changes in Superset and assists people when migrating to a new version. ## Next +- [31794](/~https://github.com/apache/superset/pull/31794) Removed the previously deprecated `DASHBOARD_CROSS_FILTERS` feature flag. - [31774](/~https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling. - [31582](/~https://github.com/apache/superset/pull/31582) Removed the legacy Area, Bar, Event Flow, Heatmap, Histogram, Line, Sankey, and Sankey Loop charts. They were all automatically migrated to their ECharts counterparts with the exception of the Event Flow and Sankey Loop charts which were removed as they were not actively maintained and not widely used. If you were using the Event Flow or Sankey Loop charts, you will need to find an alternative solution. - [31198](/~https://github.com/apache/superset/pull/31198) Disallows by default the use of the following ClickHouse functions: "version", "currentDatabase", "hostName". diff --git a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts index 3bf3c0c6e394a..75d8d540ccded 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -30,8 +30,6 @@ export enum FeatureFlag { AvoidColorsCollision = 'AVOID_COLORS_COLLISION', ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL', ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF', - /** @deprecated */ - DashboardCrossFilters = 'DASHBOARD_CROSS_FILTERS', DashboardVirtualization = 'DASHBOARD_VIRTUALIZATION', DashboardRbac = 'DASHBOARD_RBAC', DatapanelClosedByDefault = 'DATAPANEL_CLOSED_BY_DEFAULT', diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx index b3b3afbb81e52..53aefa382a267 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx @@ -119,9 +119,7 @@ const ChartContextMenu = ( canDrillBy && isDisplayed(ContextMenuItem.DrillBy); - const showCrossFilters = - isFeatureEnabled(FeatureFlag.DashboardCrossFilters) && - isDisplayed(ContextMenuItem.CrossFilter); + const showCrossFilters = isDisplayed(ContextMenuItem.CrossFilter); const isCrossFilteringSupportedByChart = getChartMetadataRegistry() .get(formData.viz_type) diff --git a/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx b/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx index c06857d556dea..e1db166269b5c 100644 --- a/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx +++ b/superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx @@ -29,7 +29,6 @@ const CONTEXT_MENU_TEST_ID = 'chart-context-menu'; // @ts-ignore global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, [FeatureFlag.DrillToDetail]: true, [FeatureFlag.DrillBy]: true, }; diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index d93e30a107b13..94405486209a6 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -24,8 +24,6 @@ import { logging, Behavior, t, - isFeatureEnabled, - FeatureFlag, getChartMetadataRegistry, VizType, } from '@superset-ui/core'; @@ -90,10 +88,7 @@ class ChartRenderer extends Component { )?.suppressContextMenu; this.state = { showContextMenu: - props.source === ChartSource.Dashboard && - !suppressContextMenu && - (isFeatureEnabled(FeatureFlag.DrillToDetail) || - isFeatureEnabled(FeatureFlag.DashboardCrossFilters)), + props.source === ChartSource.Dashboard && !suppressContextMenu, inContextMenu: false, legendState: undefined, }; diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 4ade6f33278fb..a4b0c9a748952 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -353,16 +353,14 @@ export function saveDashboardRequest(data, id, saveType) { if (lastModifiedTime) { dispatch(saveDashboardRequestSuccess(lastModifiedTime)); } - if (isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - const { chartConfiguration, globalChartConfiguration } = - handleChartConfiguration(); - dispatch( - saveChartConfiguration({ - chartConfiguration, - globalChartConfiguration, - }), - ); - } + const { chartConfiguration, globalChartConfiguration } = + handleChartConfiguration(); + dispatch( + saveChartConfiguration({ + chartConfiguration, + globalChartConfiguration, + }), + ); dispatch(saveDashboardFinished()); dispatch(addSuccessToast(t('This dashboard was saved successfully.'))); return response; @@ -437,10 +435,8 @@ export function saveDashboardRequest(data, id, saveType) { ) { let chartConfiguration = {}; let globalChartConfiguration = {}; - if (isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - ({ chartConfiguration, globalChartConfiguration } = - handleChartConfiguration()); - } + ({ chartConfiguration, globalChartConfiguration } = + handleChartConfiguration()); const updatedDashboard = saveType === SAVE_TYPE_OVERWRITE_CONFIRMED ? data diff --git a/superset-frontend/src/dashboard/actions/dashboardState.test.js b/superset-frontend/src/dashboard/actions/dashboardState.test.js index 4e30c38fb1d4f..0bb4fbffdaf54 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.test.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.test.js @@ -57,6 +57,7 @@ describe('dashboardState actions', () => { present: mockDashboardData.positions, future: {}, }, + charts: {}, }; const newDashboardData = mockDashboardData; @@ -153,7 +154,12 @@ describe('dashboardState actions', () => { it('dispatches SET_OVERRIDE_CONFIRM when an inspect value has diff', async () => { const id = 192; - const { getState, dispatch } = setup(); + const { getState, dispatch } = setup({ + charts: { + 123: { id: 123 }, + 456: { id: 456 }, + }, + }); const thunk = saveDashboardRequest( newDashboardData, id, @@ -172,7 +178,12 @@ describe('dashboardState actions', () => { it('should post dashboard data with after confirm the overwrite values', async () => { const id = 192; - const { getState, dispatch } = setup(); + const { getState, dispatch } = setup({ + charts: { + 123: { id: 123 }, + 456: { id: 456 }, + }, + }); const confirmedDashboardData = { ...newDashboardData, css: updatedCss, diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 8dd36a8063231..55754b1c271ea 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -230,16 +230,14 @@ export const hydrateDashboard = filterConfig: metadata?.native_filter_configuration || [], }); - if (isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - const { chartConfiguration, globalChartConfiguration } = - getCrossFiltersConfiguration( - dashboardLayout.present, - metadata, - chartQueries, - ); - metadata.chart_configuration = chartConfiguration; - metadata.global_chart_configuration = globalChartConfiguration; - } + const { chartConfiguration, globalChartConfiguration } = + getCrossFiltersConfiguration( + dashboardLayout.present, + metadata, + chartQueries, + ); + metadata.chart_configuration = chartConfiguration; + metadata.global_chart_configuration = globalChartConfiguration; const { roles } = user; const canEdit = canUserEditDashboard(dashboard, user); diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx index 48e70c8fd468a..762e120390858 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.jsx +++ b/superset-frontend/src/dashboard/components/Dashboard.jsx @@ -18,7 +18,7 @@ */ import { PureComponent } from 'react'; import PropTypes from 'prop-types'; -import { isFeatureEnabled, t, FeatureFlag } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { PluginContext } from 'src/components/DynamicPlugins'; import Loading from 'src/components/Loading'; @@ -163,10 +163,7 @@ class Dashboard extends PureComponent { editMode, } = this.props; const { appliedFilters, appliedOwnDataCharts } = this; - if ( - isFeatureEnabled(FeatureFlag.DashboardCrossFilters) && - !chartConfiguration - ) { + if (!chartConfiguration) { // For a first loading we need to wait for cross filters charts data loaded to get all active filters // for correct comparing of filters to avoid unnecessary requests return; diff --git a/superset-frontend/src/dashboard/components/Dashboard.test.jsx b/superset-frontend/src/dashboard/components/Dashboard.test.jsx index e3421ee04057d..3d841661b1c7e 100644 --- a/superset-frontend/src/dashboard/components/Dashboard.test.jsx +++ b/superset-frontend/src/dashboard/components/Dashboard.test.jsx @@ -48,6 +48,7 @@ describe('Dashboard', () => { removeSliceFromDashboard() {}, triggerQuery() {}, logEvent() {}, + clearDataMaskState() {}, }, dashboardState, dashboardInfo, @@ -61,6 +62,7 @@ describe('Dashboard', () => { userId: dashboardInfo.userId, impressionId: 'id', loadStats: {}, + dashboardId: 123, }; const ChildrenComponent = () =>
Test
; @@ -125,9 +127,30 @@ describe('Dashboard', () => { let refreshSpy; beforeEach(() => { - wrapper = setup({ activeFilters: OVERRIDE_FILTERS }); - wrapper.instance().appliedFilters = OVERRIDE_FILTERS; - prevProps = wrapper.instance().props; + jest.clearAllMocks(); + getRelatedCharts.mockReset(); + + // Create a deep copy of OVERRIDE_FILTERS for prevProps + const initialFilters = JSON.parse(JSON.stringify(OVERRIDE_FILTERS)); + + // Setup with required props + wrapper = setup({ + activeFilters: initialFilters, + editMode: false, // Ensure not in edit mode + chartConfiguration: {}, // Ensure chartConfiguration exists + ownDataCharts: {}, + }); + wrapper.instance().appliedFilters = initialFilters; + + // Set up prevProps with the initial filters + prevProps = { + ...wrapper.instance().props, + activeFilters: initialFilters, + editMode: false, + chartConfiguration: {}, + ownDataCharts: {}, + }; + refreshSpy = sinon.spy(wrapper.instance(), 'refreshCharts'); }); @@ -148,27 +171,36 @@ describe('Dashboard', () => { }); it('should not call refresh when there is no change', () => { + const sameFilters = JSON.parse(JSON.stringify(OVERRIDE_FILTERS)); wrapper.setProps({ - activeFilters: OVERRIDE_FILTERS, + activeFilters: sameFilters, }); wrapper.instance().componentDidUpdate(prevProps); expect(refreshSpy.callCount).toBe(0); - expect(wrapper.instance().appliedFilters).toBe(OVERRIDE_FILTERS); + expect(wrapper.instance().appliedFilters).toEqual(sameFilters); }); it('should call refresh when native filters changed', () => { getRelatedCharts.mockReturnValue([230]); + + const newFilters = { + ...OVERRIDE_FILTERS, + ...getAllActiveFilters({ + dataMask: dataMaskWith1Filter, + nativeFilters: singleNativeFiltersState.filters, + allSliceIds: [227, 229, 230], + }), + }; + wrapper.setProps({ - activeFilters: { - ...OVERRIDE_FILTERS, - ...getAllActiveFilters({ - dataMask: dataMaskWith1Filter, - nativeFilters: singleNativeFiltersState.filters, - allSliceIds: [227, 229, 230], - }), - }, + ...wrapper.instance().props, + activeFilters: newFilters, + editMode: false, + chartConfiguration: {}, }); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(wrapper.instance().appliedFilters).toEqual({ ...OVERRIDE_FILTERS, @@ -190,34 +222,51 @@ describe('Dashboard', () => { it('should call refresh if a filter is added', () => { getRelatedCharts.mockReturnValue([1]); + const newFilter = { gender: { values: ['boy', 'girl'], scope: [1] }, }; - wrapper.setProps({ + + const newProps = { + ...wrapper.instance().props, activeFilters: newFilter, - }); + }; + wrapper.setProps(newProps); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(wrapper.instance().appliedFilters).toEqual(newFilter); }); it('should call refresh if a filter is removed', () => { getRelatedCharts.mockReturnValue([]); - wrapper.setProps({ + + const newProps = { + ...wrapper.instance().props, activeFilters: {}, - }); + }; + wrapper.setProps(newProps); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(wrapper.instance().appliedFilters).toEqual({}); }); it('should call refresh if a filter is changed', () => { getRelatedCharts.mockReturnValue([1]); + const newFilters = { ...OVERRIDE_FILTERS, '1_region': { values: ['Canada'], scope: [1] }, }; - wrapper.setProps({ + + const newProps = { + ...wrapper.instance().props, activeFilters: newFilters, - }); + }; + wrapper.setProps(newProps); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(wrapper.instance().appliedFilters).toEqual(newFilters); expect(refreshSpy.getCall(0).args[0]).toEqual([1]); @@ -225,41 +274,58 @@ describe('Dashboard', () => { it('should call refresh with multiple chart ids', () => { getRelatedCharts.mockReturnValue([1, 2]); + const newFilters = { ...OVERRIDE_FILTERS, '2_country_name': { values: ['New Country'], scope: [1, 2] }, }; - wrapper.setProps({ + + const newProps = { + ...wrapper.instance().props, activeFilters: newFilters, - }); + }; + wrapper.setProps(newProps); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(wrapper.instance().appliedFilters).toEqual(newFilters); expect(refreshSpy.getCall(0).args[0]).toEqual([1, 2]); }); it('should call refresh if a filter scope is changed', () => { + getRelatedCharts.mockReturnValue([2]); + const newFilters = { ...OVERRIDE_FILTERS, '3_country_name': { values: ['USA'], scope: [2] }, }; - wrapper.setProps({ + const newProps = { + ...wrapper.instance().props, activeFilters: newFilters, - }); + }; + wrapper.setProps(newProps); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(refreshSpy.getCall(0).args[0]).toEqual([2]); }); it('should call refresh with empty [] if a filter is changed but scope is not applicable', () => { getRelatedCharts.mockReturnValue([]); + const newFilters = { ...OVERRIDE_FILTERS, '3_country_name': { values: ['CHINA'], scope: [] }, }; - wrapper.setProps({ + const newProps = { + ...wrapper.instance().props, activeFilters: newFilters, - }); + }; + wrapper.setProps(newProps); + wrapper.instance().componentDidUpdate(prevProps); + expect(refreshSpy.callCount).toBe(1); expect(refreshSpy.getCall(0).args[0]).toEqual([]); }); diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index e5caed6968642..7b890eb8a9b57 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -395,9 +395,6 @@ const DashboardBuilder = () => { const fullSizeChartId = useSelector( state => state.dashboardState.fullSizeChartId, ); - const crossFiltersEnabled = isFeatureEnabled( - FeatureFlag.DashboardCrossFilters, - ); const filterBarOrientation = useSelector( ({ dashboardInfo }) => isFeatureEnabled(FeatureFlag.HorizontalFilterBar) @@ -475,8 +472,7 @@ const DashboardBuilder = () => { ELEMENT_ON_SCREEN_OPTIONS, ); - const showFilterBar = - (crossFiltersEnabled || nativeFiltersEnabled) && !editMode; + const showFilterBar = !editMode; const offset = FILTER_BAR_HEADER_HEIGHT + diff --git a/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx b/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx index 5d93fd6900b04..a28f85f779872 100644 --- a/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx +++ b/superset-frontend/src/dashboard/components/OverwriteConfirm/OverwriteConfirmModal.test.tsx @@ -20,6 +20,7 @@ import thunk from 'redux-thunk'; import configureStore from 'redux-mock-store'; import fetchMock from 'fetch-mock'; import { mockAllIsIntersecting } from 'react-intersection-observer/test-utils'; +import { act } from 'react-test-renderer'; import { fireEvent, render, waitFor } from 'spec/helpers/testing-library'; import { overwriteConfirmMetadata } from 'spec/fixtures/mockDashboardState'; @@ -61,10 +62,24 @@ test('requests update dashboard api when save button is clicked', async () => { last_modified_time: +new Date(), result: overwriteConfirmMetadata.data, }); + + // Initialize store with required state const store = mockStore({ - dashboardLayout: {}, + dashboardLayout: { + present: {}, + }, dashboardFilters: {}, + dashboardInfo: { + metadata: {}, + crossFiltersEnabled: false, + }, + charts: {}, + dataMask: {}, + nativeFilters: { + filters: {}, + }, }); + const { findByTestId } = render( { store, }, ); + const saveButton = await findByTestId('overwrite-confirm-save-button'); expect(fetchMock.calls(updateDashboardEndpoint)).toHaveLength(0); - fireEvent.click(saveButton); - expect(fetchMock.calls(updateDashboardEndpoint)).toHaveLength(0); - mockAllIsIntersecting(true); - fireEvent.click(saveButton); - await waitFor(() => - expect(fetchMock.calls(updateDashboardEndpoint)?.[0]?.[1]?.body).toEqual( - JSON.stringify(overwriteConfirmMetadata.data), - ), - ); + + await act(async () => { + mockAllIsIntersecting(true); + fireEvent.click(saveButton); + }); + + // Wait for the fetch mock to be called and verify the request body + await waitFor(() => { + const calls = fetchMock.calls(updateDashboardEndpoint); + expect(calls).toHaveLength(1); + expect(JSON.parse(calls[0][1].body)).toEqual(overwriteConfirmMetadata.data); + }); + + // Verify the action was dispatched await waitFor(() => expect(store.getActions()).toContainEqual({ type: 'SET_OVERRIDE_CONFIRM', diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index af8cbd739b778..0364a3735ff80 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -130,7 +130,6 @@ const SliceHeaderControls = (props: SliceHeaderControlsProps) => { useSelector( ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ) && - isFeatureEnabled(FeatureFlag.DashboardCrossFilters) && getChartMetadataRegistry() .get(props.slice.viz_type) ?.behaviors?.includes(Behavior.InteractiveChart); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx index e3a80d21e9f30..c52a0f0ef9ff2 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/FilterBarSettings.test.tsx @@ -20,12 +20,18 @@ import fetchMock from 'fetch-mock'; import { waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { render, screen, within } from 'spec/helpers/testing-library'; +import { render, screen } from 'spec/helpers/testing-library'; import { DashboardInfo, FilterBarOrientation } from 'src/dashboard/types'; import * as mockedMessageActions from 'src/components/MessageToasts/actions'; import { FeatureFlag } from '@superset-ui/core'; import FilterBarSettings from '.'; +// Mock the number formatter registration to avoid duplicate warnings +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + getNumberFormatter: jest.fn(), +})); + const initialState: { dashboardInfo: DashboardInfo } = { dashboardInfo: { id: 1, @@ -104,33 +110,7 @@ test('Dropdown trigger does not render without dashboard edit permissions', asyn expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument(); }); -test('Dropdown trigger renders with FF DASHBOARD_CROSS_FILTERS on', async () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, - }; - await setup(); - - expect(screen.getByRole('img', { name: 'gear' })).toBeInTheDocument(); -}); - -test('Dropdown trigger does not render with FF DASHBOARD_CROSS_FILTERS off', async () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: false, - }; - await setup({ - dash_edit_perm: false, - }); - - expect(screen.queryByRole('img', { name: 'gear' })).not.toBeInTheDocument(); -}); - test('Popover shows cross-filtering option on by default', async () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, - }; await setup(); userEvent.click(screen.getByLabelText('gear')); expect(screen.getByText('Enable cross-filtering')).toBeInTheDocument(); @@ -138,10 +118,6 @@ test('Popover shows cross-filtering option on by default', async () => { }); test('Can enable/disable cross-filtering', async () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, - }; fetchMock.reset(); fetchMock.put('glob:*/api/v1/dashboard/1', { result: {}, @@ -165,10 +141,14 @@ test('Popover opens with "Vertical" selected', async () => { await setup(); userEvent.click(screen.getByLabelText('gear')); userEvent.hover(screen.getByText('Orientation of filter bar')); + expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument(); + + // Look for the check icon in a different way + const menuItems = screen.getAllByRole('menuitem'); expect( - within(screen.getAllByRole('menuitem')[2]).getByLabelText('check'), + menuItems[2].querySelector('[aria-label="check"]'), ).toBeInTheDocument(); }); @@ -180,10 +160,13 @@ test('Popover opens with "Horizontal" selected', async () => { await setup({ filterBarOrientation: FilterBarOrientation.Horizontal }); userEvent.click(screen.getByLabelText('gear')); userEvent.hover(screen.getByText('Orientation of filter bar')); + expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument(); + + const menuItems = screen.getAllByRole('menuitem'); expect( - within(screen.getAllByRole('menuitem')[3]).getByLabelText('check'), + menuItems[3].querySelector('[aria-label="check"]'), ).toBeInTheDocument(); }); @@ -206,13 +189,12 @@ test('On selection change, send request and update checked value', async () => { userEvent.click(screen.getByLabelText('gear')); userEvent.hover(screen.getByText('Orientation of filter bar')); - expect(await screen.findByText('Vertical (Left)')).toBeInTheDocument(); - expect(screen.getByText('Horizontal (Top)')).toBeInTheDocument(); + const menuItems = screen.getAllByRole('menuitem'); expect( - within(screen.getAllByRole('menuitem')[2]).getByLabelText('check'), + menuItems[2].querySelector('[aria-label="check"]'), ).toBeInTheDocument(); expect( - within(screen.getAllByRole('menuitem')[3]).queryByLabelText('check'), + menuItems[3].querySelector('[aria-label="check"]'), ).not.toBeInTheDocument(); userEvent.click(screen.getByText('Horizontal (Top)')); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx index 9afc05b40c55f..404d73799959d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx @@ -83,13 +83,9 @@ const FilterBarSettings = () => { ); const [selectedFilterBarOrientation, setSelectedFilterBarOrientation] = useState(filterBarOrientation); - const isCrossFiltersFeatureEnabled = isFeatureEnabled( - FeatureFlag.DashboardCrossFilters, - ); - const shouldEnableCrossFilters = - isCrossFiltersEnabled && isCrossFiltersFeatureEnabled; + const [crossFiltersEnabled, setCrossFiltersEnabled] = useState( - shouldEnableCrossFilters, + isCrossFiltersEnabled, ); const canEdit = useSelector( ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, @@ -188,7 +184,7 @@ const FilterBarSettings = () => { divider: canSetHorizontalFilterBar, }); } - if (isCrossFiltersFeatureEnabled && canEdit) { + if (canEdit) { items.push({ key: CROSS_FILTERS_MENU_KEY, label: crossFiltersMenuItem, @@ -222,7 +218,6 @@ const FilterBarSettings = () => { crossFiltersMenuItem, dashboardId, filterValues, - isCrossFiltersFeatureEnabled, ]); if (!menuItems.length) { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx index d959026792a65..18126b2f99838 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx @@ -62,15 +62,12 @@ import crossFiltersSelector from '../CrossFilters/selectors'; import CrossFilter from '../CrossFilters/CrossFilter'; import { useFilterOutlined } from '../useFilterOutlined'; import { useChartsVerboseMaps } from '../utils'; -import { CrossFilterIndicator } from '../../selectors'; type FilterControlsProps = { dataMaskSelected: DataMaskStateWithId; onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void; }; -const EMPTY_ARRAY: CrossFilterIndicator[] = []; - const FilterControls: FC = ({ dataMaskSelected, onFilterSelectionChange, @@ -94,20 +91,15 @@ const FilterControls: FC = ({ const chartLayoutItems = useChartLayoutItems(); const verboseMaps = useChartsVerboseMaps(); - const isCrossFiltersEnabled = isFeatureEnabled( - FeatureFlag.DashboardCrossFilters, - ); const selectedCrossFilters = useMemo( () => - isCrossFiltersEnabled - ? crossFiltersSelector({ - dataMask, - chartIds, - chartLayoutItems, - verboseMaps, - }) - : EMPTY_ARRAY, - [chartIds, chartLayoutItems, dataMask, isCrossFiltersEnabled, verboseMaps], + crossFiltersSelector({ + dataMask, + chartIds, + chartLayoutItems, + verboseMaps, + }), + [chartIds, chartLayoutItems, dataMask, verboseMaps], ); const { filterControlFactory, filtersWithValues } = useFilterControlFactory( dataMaskSelected, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx index 0e5b82aed2651..13761afc4a930 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Horizontal.tsx @@ -18,13 +18,7 @@ */ import { FC, memo, useMemo } from 'react'; -import { - DataMaskStateWithId, - FeatureFlag, - isFeatureEnabled, - styled, - t, -} from '@superset-ui/core'; +import { DataMaskStateWithId, styled, t } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { RootState } from 'src/dashboard/types'; import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems'; @@ -35,7 +29,6 @@ import { useChartsVerboseMaps, getFilterBarTestId } from './utils'; import { HorizontalBarProps } from './types'; import FilterBarSettings from './FilterBarSettings'; import crossFiltersSelector from './CrossFilters/selectors'; -import { CrossFilterIndicator } from '../selectors'; const HorizontalBar = styled.div` ${({ theme }) => ` @@ -72,7 +65,6 @@ const FilterBarEmptyStateContainer = styled.div` `} `; -const EMPTY_ARRAY: CrossFilterIndicator[] = []; const HorizontalFilterBar: FC = ({ actions, dataMaskSelected, @@ -85,22 +77,17 @@ const HorizontalFilterBar: FC = ({ ); const chartIds = useChartIds(); const chartLayoutItems = useChartLayoutItems(); - const isCrossFiltersEnabled = isFeatureEnabled( - FeatureFlag.DashboardCrossFilters, - ); const verboseMaps = useChartsVerboseMaps(); const selectedCrossFilters = useMemo( () => - isCrossFiltersEnabled - ? crossFiltersSelector({ - dataMask, - chartIds, - chartLayoutItems, - verboseMaps, - }) - : EMPTY_ARRAY, - [chartIds, chartLayoutItems, dataMask, isCrossFiltersEnabled, verboseMaps], + crossFiltersSelector({ + dataMask, + chartIds, + chartLayoutItems, + verboseMaps, + }), + [chartIds, chartLayoutItems, dataMask, verboseMaps], ); const hasFilters = filterValues.length > 0 || selectedCrossFilters.length > 0; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx index b76b65c13395c..dccf747973540 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/Vertical.tsx @@ -30,7 +30,7 @@ import { FC, } from 'react'; import cx from 'classnames'; -import { FeatureFlag, isFeatureEnabled, styled, t } from '@superset-ui/core'; +import { styled, t } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import Loading from 'src/components/Loading'; import { EmptyStateSmall } from 'src/components/EmptyState'; @@ -190,14 +190,6 @@ const VerticalFilterBar: FC = ({ [canEdit, dataMaskSelected, filterValues.length, onSelectionChange], ); - const crossFilters = useMemo( - () => - isFeatureEnabled(FeatureFlag.DashboardCrossFilters) ? ( - - ) : null, - [], - ); - return ( = ({ ) : (
<> - {crossFilters} + {filterControls}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts index 232ff57de2ffe..0539a4317a77e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts @@ -21,11 +21,9 @@ import { DataMaskStateWithId, DataMaskType, ensureIsArray, - FeatureFlag, Filters, FilterState, getColumnLabel, - isFeatureEnabled, NativeFilterType, NO_TIME_RANGE, QueryFormColumn, @@ -289,39 +287,37 @@ export const selectChartCrossFilters = ( filterEmitter = false, ): Indicator[] | CrossFilterIndicator[] => { let crossFilterIndicators: any = []; - if (isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - crossFilterIndicators = Object.values(chartConfiguration) - .filter(chartConfig => { - const inScope = - chartConfig.crossFilters?.chartsInScope?.includes(chartId); - if (!filterEmitter && inScope) { - return true; - } - if (filterEmitter && !inScope) { - return true; - } - return false; - }) - .map(chartConfig => { - const filterIndicator = getCrossFilterIndicator( - Number(chartConfig.id), - dataMask[chartConfig.id], - chartLayoutItems, - ); - const filterStatus = getStatus({ - label: filterIndicator.value, - column: filterIndicator.column - ? getColumnLabel(filterIndicator.column) - : undefined, - type: DataMaskType.CrossFilters, - appliedColumns, - rejectedColumns, - }); + crossFilterIndicators = Object.values(chartConfiguration) + .filter(chartConfig => { + const inScope = + chartConfig.crossFilters?.chartsInScope?.includes(chartId); + if (!filterEmitter && inScope) { + return true; + } + if (filterEmitter && !inScope) { + return true; + } + return false; + }) + .map(chartConfig => { + const filterIndicator = getCrossFilterIndicator( + Number(chartConfig.id), + dataMask[chartConfig.id], + chartLayoutItems, + ); + const filterStatus = getStatus({ + label: filterIndicator.value, + column: filterIndicator.column + ? getColumnLabel(filterIndicator.column) + : undefined, + type: DataMaskType.CrossFilters, + appliedColumns, + rejectedColumns, + }); - return { ...filterIndicator, status: filterStatus }; - }) - .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied); - } + return { ...filterIndicator, status: filterStatus }; + }) + .filter(filter => filter.status === IndicatorStatus.CrossFilterApplied); return crossFilterIndicators; }; @@ -379,16 +375,14 @@ export const selectNativeIndicatorsForChart = ( }); let crossFilterIndicators: any = []; - if (isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - crossFilterIndicators = selectChartCrossFilters( - dataMask, - chartId, - chartLayoutItems, - chartConfiguration, - appliedColumns, - rejectedColumns, - ); - } + crossFilterIndicators = selectChartCrossFilters( + dataMask, + chartId, + chartLayoutItems, + chartConfiguration, + appliedColumns, + rejectedColumns, + ); const indicators = crossFilterIndicators.concat(nativeFilterIndicators); cachedNativeIndicatorsForChart[chartId] = indicators; cachedNativeFilterDataForChart[chartId] = { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts index db21db547d597..fa27873732814 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.test.ts @@ -45,6 +45,12 @@ describe('nativeFilterGate', () => { }); it('should return false for native filter chart with cross filter support', () => { + isFeatureEnabledMock.mockImplementation(feature => { + if (feature === FeatureFlag.DASHBOARD_CROSS_FILTERS) return false; + if (feature === FeatureFlag.DASHBOARD_NATIVE_FILTERS) return false; + return false; + }); + expect( nativeFilterGate([Behavior.NativeFilter, Behavior.InteractiveChart]), ).toEqual(false); @@ -59,9 +65,11 @@ describe('nativeFilterGate', () => { beforeAll(() => { isFeatureEnabledMock = jest .spyOn(uiCore, 'isFeatureEnabled') - .mockImplementation((featureFlag: FeatureFlag) => - [FeatureFlag.DashboardCrossFilters].includes(featureFlag), - ); + .mockImplementation(feature => { + if (feature === FeatureFlag.DASHBOARD_CROSS_FILTERS) return true; + if (feature === FeatureFlag.DASHBOARD_NATIVE_FILTERS) return true; + return false; + }); }); afterAll(() => { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts index 734e0ce91fe9d..868803254c88c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/utils.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/utils.ts @@ -23,12 +23,12 @@ import { EXTRA_FORM_DATA_APPEND_KEYS, EXTRA_FORM_DATA_OVERRIDE_KEYS, ExtraFormData, - isFeatureEnabled, - FeatureFlag, Filter, getChartMetadataRegistry, QueryFormData, t, + isFeatureEnabled, + FeatureFlag, } from '@superset-ui/core'; import { LayoutItem } from 'src/dashboard/types'; import extractUrlParams from 'src/dashboard/util/extractUrlParams'; @@ -148,11 +148,22 @@ export function getExtraFormData( } export function nativeFilterGate(behaviors: Behavior[]): boolean { - return ( - !behaviors.includes(Behavior.NativeFilter) || - (isFeatureEnabled(FeatureFlag.DashboardCrossFilters) && - behaviors.includes(Behavior.InteractiveChart)) - ); + const hasNativeFilter = behaviors.includes(Behavior.NativeFilter); + + // If it's not a native filter, always allow + if (!hasNativeFilter) { + return true; + } + + // Native filters are not allowed when feature flags are disabled + if (!isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS)) { + return false; + } + + // When feature flag is enabled: + // - Pure native filters are not allowed + // - Native filters with interactive chart behavior are allowed + return behaviors.includes(Behavior.InteractiveChart); } export const findTabsWithChartsInScope = ( diff --git a/superset-frontend/src/dashboard/util/crossFilters.test.ts b/superset-frontend/src/dashboard/util/crossFilters.test.ts index 07e8064e7d00e..06a3aa2069f99 100644 --- a/superset-frontend/src/dashboard/util/crossFilters.test.ts +++ b/superset-frontend/src/dashboard/util/crossFilters.test.ts @@ -17,7 +17,7 @@ * under the License. */ import sinon, { SinonStub } from 'sinon'; -import { Behavior, FeatureFlag } from '@superset-ui/core'; +import { Behavior } from '@superset-ui/core'; import * as core from '@superset-ui/core'; import { getCrossFiltersConfiguration } from './crossFilters'; import { DEFAULT_CROSS_FILTER_SCOPING } from '../constants'; @@ -146,11 +146,6 @@ afterEach(() => { }); test('Generate correct cross filters configuration without initial configuration', () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, - }; - // @ts-ignore expect(getCrossFiltersConfiguration(DASHBOARD_LAYOUT, {}, CHARTS)).toEqual({ chartConfiguration: { @@ -180,11 +175,6 @@ test('Generate correct cross filters configuration without initial configuration }); test('Generate correct cross filters configuration with initial configuration', () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, - }; - expect( getCrossFiltersConfiguration( DASHBOARD_LAYOUT, @@ -221,25 +211,7 @@ test('Generate correct cross filters configuration with initial configuration', }); }); -test('Return undefined if DASHBOARD_CROSS_FILTERS feature flag is disabled', () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: false, - }; - expect( - getCrossFiltersConfiguration( - DASHBOARD_LAYOUT, - CHART_CONFIG_METADATA, - CHARTS, - ), - ).toEqual(undefined); -}); - test('Recalculate charts in global filter scope when charts change', () => { - // @ts-ignore - global.featureFlags = { - [FeatureFlag.DashboardCrossFilters]: true, - }; expect( getCrossFiltersConfiguration( { diff --git a/superset-frontend/src/dashboard/util/crossFilters.ts b/superset-frontend/src/dashboard/util/crossFilters.ts index 123435a430a1f..04d3e368fc0c9 100644 --- a/superset-frontend/src/dashboard/util/crossFilters.ts +++ b/superset-frontend/src/dashboard/util/crossFilters.ts @@ -19,10 +19,8 @@ import { cloneDeep } from 'lodash'; import { Behavior, - FeatureFlag, getChartMetadataRegistry, isDefined, - isFeatureEnabled, } from '@superset-ui/core'; import { getChartIdsInFilterScope } from './getChartIdsInFilterScope'; import { @@ -38,8 +36,7 @@ import { CHART_TYPE } from './componentTypes'; export const isCrossFiltersEnabled = ( metadataCrossFiltersEnabled: boolean | undefined, ): boolean => - isFeatureEnabled(FeatureFlag.DashboardCrossFilters) && - (metadataCrossFiltersEnabled === undefined || metadataCrossFiltersEnabled); + metadataCrossFiltersEnabled === undefined || metadataCrossFiltersEnabled; export const getCrossFiltersConfiguration = ( dashboardLayout: DashboardLayout, @@ -49,10 +46,6 @@ export const getCrossFiltersConfiguration = ( >, charts: ChartsState, ) => { - if (!isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - return undefined; - } - const chartLayoutItems = Object.values(dashboardLayout).filter( item => item?.type === CHART_TYPE, ); diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts index b79f17fb1dbec..6d240b498953e 100644 --- a/superset-frontend/src/dataMask/reducer.ts +++ b/superset-frontend/src/dataMask/reducer.ts @@ -24,8 +24,6 @@ import { DataMask, DataMaskStateWithId, DataMaskWithId, - isFeatureEnabled, - FeatureFlag, Filter, FilterConfiguration, Filters, @@ -148,16 +146,14 @@ const dataMaskReducer = produce( // TODO: update hydrate to .ts // @ts-ignore case HYDRATE_DASHBOARD: - if (isFeatureEnabled(FeatureFlag.DashboardCrossFilters)) { - Object.keys( - // @ts-ignore - action.data.dashboardInfo?.metadata?.chart_configuration, - ).forEach(id => { - cleanState[id] = { - ...getInitialDataMask(id), // take initial data - }; - }); - } + Object.keys( + // @ts-ignore + action.data.dashboardInfo?.metadata?.chart_configuration, + ).forEach(id => { + cleanState[id] = { + ...getInitialDataMask(id), // take initial data + }; + }); fillNativeFilters( // @ts-ignore action.data.dashboardInfo?.metadata?.native_filter_configuration ?? diff --git a/superset/config.py b/superset/config.py index 7ab30185bceb4..3c2b0fd3bfa48 100644 --- a/superset/config.py +++ b/superset/config.py @@ -491,7 +491,6 @@ class D3TimeFormat(TypedDict, total=False): "LISTVIEWS_DEFAULT_CARD_VIEW": False, # When True, this escapes HTML (rather than rendering it) in Markdown components "ESCAPE_MARKDOWN_HTML": False, - "DASHBOARD_CROSS_FILTERS": True, # deprecated "DASHBOARD_VIRTUALIZATION": True, # This feature flag is stil in beta and is not recommended for production use. "GLOBAL_ASYNC_QUERIES": False,