You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2026/01/14 16:00:25 UTC

(superset) branch master updated: feat(dashboard): chart customizations modal and plugins (#36062)

This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 35ac4c74fd feat(dashboard): chart customizations modal and plugins (#36062)
35ac4c74fd is described below

commit 35ac4c74fdd7d911684d44e8253b986ec065a492
Author: Damian Pendrak <dp...@gmail.com>
AuthorDate: Wed Jan 14 17:00:18 2026 +0100

    feat(dashboard): chart customizations modal and plugins (#36062)
---
 .../e2e/dashboard/horizontalFilterBar.test.ts      |    1 +
 .../cypress/e2e/dashboard/nativeFilters.test.ts    |    3 +
 .../cypress-base/cypress/e2e/dashboard/utils.ts    |   10 +-
 .../superset-ui-core/src/chart/types/Base.ts       |    1 +
 .../superset-ui-core/src/query/constants.ts        |    1 +
 .../packages/superset-ui-core/src/query/index.ts   |    1 +
 .../superset-ui-core/src/query/types/Dashboard.ts  |   81 +-
 .../superset-ui-core/src/query/types/Query.ts      |    2 +
 .../src/query/types/QueryFormData.ts               |    5 +-
 .../src/DeckGLContainer.tsx                        |    6 +-
 .../legacy-preset-chart-deckgl/src/Multi/Multi.tsx |  127 +-
 .../src/Multi/controlPanel.ts                      |   29 +-
 ...ckglLayerVisibilityCustomizationPlugin.test.tsx |  484 +++++++
 .../DeckglLayerVisibilityCustomizationPlugin.tsx   |  204 +++
 .../DeckglLayerVisibility/buildQuery.test.ts}      |   40 +-
 .../DeckglLayerVisibility/buildQuery.ts}           |   33 +-
 .../DeckglLayerVisibility/controlPanel.ts}         |   28 +-
 .../components/DeckglLayerVisibility/index.ts}     |   40 +-
 .../DeckglLayerVisibility/transformProps.test.ts   |  100 ++
 .../DeckglLayerVisibility/transformProps.ts}       |   33 +-
 .../components/DeckglLayerVisibility}/types.ts     |   39 +-
 .../useDeckLayerMetadata.test.ts                   |  195 +++
 .../DeckglLayerVisibility/useDeckLayerMetadata.ts  |   87 ++
 .../DynamicGroupBy/DynamicGroupByPlugin.tsx        |  140 ++
 .../components/DynamicGroupBy/buildQuery.ts}       |   33 +-
 .../components/DynamicGroupBy/controlPanel.ts      |   80 ++
 .../components/DynamicGroupBy/images/thumbnail.png |  Bin 0 -> 5658 bytes
 .../components/DynamicGroupBy/index.ts}            |   41 +-
 .../components/DynamicGroupBy/transformProps.ts}   |   50 +-
 .../components/DynamicGroupBy}/types.ts            |   74 +-
 .../TimeColumn/TimeColumnFilterPlugin.tsx          |  127 ++
 .../components/TimeColumn/buildQuery.ts            |   44 +
 .../components/TimeColumn/controlPanel.ts}         |   41 +-
 .../components/TimeColumn/images/thumbnail.png     |  Bin 0 -> 5658 bytes
 .../components/TimeColumn/index.ts}                |   40 +-
 .../components/TimeColumn/transformProps.ts        |   60 +
 .../components/TimeColumn/types.ts                 |   48 +
 .../components/TimeGrain/TimeGrainFilterPlugin.tsx |  159 +++
 .../components/TimeGrain/buildQuery.ts             |   44 +
 .../components/TimeGrain/controlPanel.ts}          |   41 +-
 .../components/TimeGrain/images/thumbnail.png      |  Bin 0 -> 5658 bytes
 .../components/TimeGrain/index.ts}                 |   40 +-
 .../components/TimeGrain/transformProps.ts}        |   50 +-
 .../components/TimeGrain/types.ts                  |   42 +
 .../src/chartCustomizations/components/common.ts   |   57 +
 .../components/index.ts}                           |   28 +-
 .../components}/types.ts                           |   34 +-
 superset-frontend/src/constants.ts                 |    7 +
 .../dashboard/actions/chartCustomizationActions.ts |  378 +++--
 superset-frontend/src/dashboard/actions/hydrate.js |    9 +-
 .../src/dashboard/actions/nativeFilters.ts         |   23 +-
 .../index.tsx                                      |  206 +--
 .../DashboardBuilder/DashboardContainer.test.tsx   |  211 +--
 .../DashboardBuilder/DashboardContainer.tsx        |  152 +-
 .../dashboard/components/DashboardBuilder/state.ts |   27 +-
 .../src/dashboard/components/SliceHeader/index.tsx |    4 +-
 .../components/SyncDashboardState/index.tsx        |    5 +-
 .../components/gridComponents/Chart/Chart.jsx      |    6 +-
 .../ChartCustomization/ChartCustomizationForm.tsx  | 1457 --------------------
 .../ChartCustomization/ChartCustomizationModal.tsx |  700 ----------
 .../ChartCustomizationTitleContainer.tsx           |  184 ---
 .../ChartCustomizationTitlePane.tsx                |  124 --
 .../ChartCustomization/groupBySelectors.ts         |  262 ----
 .../nativeFilters/ChartCustomization/selectors.ts  |   47 -
 .../useChartCustomizationModal.tsx                 |   52 -
 .../nativeFilters/ChartCustomization/utils.ts      |   63 -
 .../nativeFilters/ConfigModal/SharedStyles.tsx     |    1 -
 .../FilterBar/ActionButtons/index.tsx              |   15 +-
 .../CustomizationsOutOfScopeCollapsible/index.tsx  |   67 +
 .../nativeFilters/FilterBar/FilterBar.test.tsx     |   45 +-
 .../FilterBar/FilterBarSettings/index.tsx          |   29 +-
 .../useFilterConfigModal.tsx                       |   33 +-
 .../FilterBar/FilterControls/FilterControl.tsx     |  280 +---
 .../{FilterControl.tsx => FilterControlShared.tsx} |  244 +---
 .../FilterControls/FilterControls.test.tsx         |  186 +++
 .../FilterBar/FilterControls/FilterControls.tsx    |  206 ++-
 .../FilterBar/FilterControls/FilterValue.tsx       |   72 +-
 .../FilterControls}/GroupByFilterCard.tsx          |  403 +++---
 .../FilterBar/FilterControls/types.ts              |   19 +-
 .../nativeFilters/FilterBar/Horizontal.tsx         |   15 +-
 .../FilterBar/HorizontalFilterBar.test.tsx         |    2 +
 .../nativeFilters/FilterBar/Vertical.tsx           |   23 +-
 .../components/nativeFilters/FilterBar/index.tsx   |  226 ++-
 .../components/nativeFilters/FilterBar/state.ts    |   67 +-
 .../components/nativeFilters/FilterBar/types.ts    |    7 +
 .../FilterBar/useFilterControlFactory.tsx          |    5 +-
 .../components/nativeFilters/FilterBar/utils.ts    |   10 +-
 .../nativeFilters/FilterCard/FilterCard.test.tsx   |    8 +-
 .../nativeFilters/FilterCard/FilterCardContent.tsx |    4 +-
 .../nativeFilters/FilterCard/NameRow.tsx           |   25 +-
 .../nativeFilters/FilterCard/TypeRow.tsx           |    8 +-
 .../components/nativeFilters/FilterCard/types.ts   |    5 +-
 .../FilterCard/useFilterDependencies.ts            |    5 +-
 .../nativeFilters/FilterCard/useFilterScope.ts     |    4 +-
 .../ConfigModalContent/ConfigModalContent.tsx      |  164 +++
 .../CustomizationContentRenderer.tsx               |  133 ++
 .../ConfigModalContent/FilterContentRenderer.tsx   |  136 ++
 .../ConfigModalContent/index.ts}                   |   31 +-
 .../ConfigModalSidebar/ConfigModalSidebar.tsx      |  192 +++
 .../ConfigModalSidebar/ItemSection.tsx             |   77 ++
 .../ConfigModalSidebar/index.ts}                   |   29 +-
 .../FiltersConfigModal/DividerConfigForm.tsx       |   64 +-
 .../FiltersConfigModal/DraggableFilter.test.tsx    |  230 +++
 .../FiltersConfigModal/DraggableFilter.tsx         |   74 +-
 .../FiltersConfigForm/DatasetSelect.tsx            |    4 +-
 .../FiltersConfigForm/DefaultValue.tsx             |   20 +-
 .../FiltersConfigForm/FiltersConfigForm.tsx        |  371 +++--
 .../FiltersConfigForm/constants.ts                 |   11 +
 .../FiltersConfigForm/getControlItemsMap.tsx       |   14 +-
 .../FiltersConfigModal/FiltersConfigForm/state.ts  |   16 +-
 .../FiltersConfigModal/FiltersConfigModal.test.tsx |  149 +-
 .../FiltersConfigModal/FiltersConfigModal.tsx      |  956 ++++++-------
 .../FiltersConfigModal/ItemTitleContainer.tsx      |  194 +++
 .../FiltersConfigModal/ItemTitlePane.tsx           |   93 ++
 .../FiltersConfigModal/NativeFiltersModal.test.tsx |    9 +-
 .../FiltersConfigModal/NewItemDropdown.tsx         |   93 ++
 .../types.ts => FiltersConfigModal/hooks/index.ts} |   39 +-
 .../hooks/useCustomizationOperations.ts            |  127 ++
 .../hooks/useFilterOperations.ts                   |  248 ++++
 .../hooks/useItemStateManager.ts                   |  154 +++
 .../FiltersConfigModal/hooks/useModalSaveLogic.ts  |  430 ++++++
 .../nativeFilters/FiltersConfigModal/state.ts      |   16 +-
 .../transformers/customizationTransformer.ts       |  163 +++
 .../transformers/filterTransformer.ts              |  172 +++
 .../transformers/index.ts}                         |   27 +-
 .../nativeFilters/FiltersConfigModal/types.ts      |   80 +-
 .../nativeFilters/FiltersConfigModal/utils.test.ts |  139 ++
 .../nativeFilters/FiltersConfigModal/utils.ts      |  160 ++-
 .../dashboard/components/nativeFilters/state.ts    |  191 ++-
 .../dashboard/components/nativeFilters/utils.ts    |   31 +-
 .../src/dashboard/reducers/dashboardInfo.js        |  101 +-
 .../dashboard/reducers/groupByCustomizations.ts    |  198 ---
 .../src/dashboard/reducers/nativeFilters.test.ts   |   77 ++
 .../src/dashboard/reducers/nativeFilters.ts        |   51 +-
 superset-frontend/src/dashboard/types.ts           |   20 +-
 .../dashboard/util/activeAllDashboardFilters.ts    |    5 +-
 .../src/dashboard/util/calculateScopes.ts          |   75 +
 .../util/charts/getFormDataWithExtraFilters.ts     |  119 +-
 .../util/getFormDataWithExtraFilters.test.ts       |  114 ++
 .../src/dashboard/util/getRelatedCharts.ts         |   13 +-
 .../util/useFilterFocusHighlightStyles.test.tsx    |    4 +
 .../util/useFilterFocusHighlightStyles.ts          |   10 +-
 superset-frontend/src/dataMask/actions.ts          |    3 +
 superset-frontend/src/dataMask/reducer.ts          |  160 ++-
 .../controls/SelectAsyncControl/index.tsx          |   18 +-
 superset-frontend/src/views/store.ts               |    2 -
 .../src/visualizations/presets/MainPreset.js       |   20 +-
 superset/commands/dashboard/exceptions.py          |    4 +
 superset/commands/dashboard/update.py              |   18 +
 superset/daos/dashboard.py                         |   64 +
 superset/dashboards/api.py                         |   91 ++
 superset/dashboards/schemas.py                     |    6 +
 tests/integration_tests/dashboards/api_tests.py    |  325 +++++
 .../dashboards/test_chart_customizations_dao.py    |  339 +++++
 154 files changed, 9604 insertions(+), 6024 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
index 17fbf587b7..f5d71d753b 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/horizontalFilterBar.test.ts
@@ -96,6 +96,7 @@ describe('Horizontal FilterBar', () => {
     cy.get(nativeFilters.filtersPanel.filterGear).click({
       force: true,
     });
+    cy.get('.ant-dropdown-menu').should('be.visible');
     cy.getBySel('filter-bar__create-filter').should('exist');
     cy.getBySel('filterbar-action-buttons').should('exist');
   });
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.test.ts
index d70a370139..c2e8eaf12d 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.test.ts
@@ -378,6 +378,7 @@ describe('Native filters', () => {
       cy.get(nativeFilters.filtersPanel.filterGear).click({
         force: true,
       });
+      cy.get('.ant-dropdown-menu').should('be.visible');
       cy.get(nativeFilters.filterFromDashboardView.createFilterButton).should(
         'be.visible',
       );
@@ -405,6 +406,8 @@ describe('Native filters', () => {
     it('Verify setting options and tooltips for value filter', () => {
       enterNativeFilterEditModal(false);
       cy.contains('Filter value is required').scrollIntoView();
+      cy.get('body').trigger('mousemove', { clientX: 0, clientY: 0 });
+      cy.wait(300);
 
       cy.contains('Filter value is required').should('be.visible').click({
         force: true,
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
index 8ed39621ba..0822523cff 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
@@ -237,6 +237,7 @@ export function enterNativeFilterEditModal(waitForDataset = true) {
   cy.get(nativeFilters.filtersPanel.filterGear).click({
     force: true,
   });
+  cy.get('.ant-dropdown-menu').should('be.visible');
   cy.get(nativeFilters.filterFromDashboardView.createFilterButton).click({
     force: true,
   });
@@ -252,7 +253,9 @@ export function enterNativeFilterEditModal(waitForDataset = true) {
  * @summary helper for adding new filter
  ************************************************************************* */
 export function clickOnAddFilterInModal() {
-  return cy.get(nativeFilters.modal.addNewFilterButton).click({ force: true });
+  cy.get('[data-test="new-item-dropdown-button"]').trigger('mouseover');
+  cy.get('.ant-dropdown-menu').should('be.visible');
+  cy.contains('.ant-dropdown-menu-item', 'Add filter').click();
 }
 
 /** ************************************************************************
@@ -459,10 +462,13 @@ export function checkNativeFilterTooltip(index: number, value: string) {
   cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
     .eq(index)
     .trigger('mouseover');
-  cy.contains(`${value}`);
+  cy.contains(`${value}`).should('be.visible');
+  cy.wait(100);
   cy.get(nativeFilters.filterConfigurationSections.infoTooltip)
     .eq(index)
     .trigger('mouseout');
+  cy.get('body').trigger('mousemove', { clientX: 0, clientY: 0 });
+  cy.wait(500);
 }
 
 /** ************************************************************************
diff --git a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
index f438593dca..15459d1114 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/types/Base.ts
@@ -25,6 +25,7 @@ export type HandlerFunction = (...args: unknown[]) => void;
 export enum Behavior {
   InteractiveChart = 'INTERACTIVE_CHART',
   NativeFilter = 'NATIVE_FILTER',
+  ChartCustomization = 'CHART_CUSTOMIZATION',
 
   /**
    * Include `DRILL_TO_DETAIL` behavior if plugin handles `contextmenu` event
diff --git a/superset-frontend/packages/superset-ui-core/src/query/constants.ts b/superset-frontend/packages/superset-ui-core/src/query/constants.ts
index 0c2df20d3a..3d91620a1b 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/constants.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/constants.ts
@@ -49,6 +49,7 @@ export const EXTRA_FORM_DATA_OVERRIDE_REGULAR_MAPPINGS: Record<
   time_grain: 'time_grain',
   time_range: 'time_range',
   time_compare: 'time_compare',
+  visible_deckgl_layers: 'visible_deckgl_layers',
 };
 
 export const EXTRA_FORM_DATA_OVERRIDE_REGULAR_KEYS = Object.keys(
diff --git a/superset-frontend/packages/superset-ui-core/src/query/index.ts b/superset-frontend/packages/superset-ui-core/src/query/index.ts
index 434fe95798..69ecc0cb92 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/index.ts
@@ -39,6 +39,7 @@ export * from './types/Column';
 export * from './types/Datasource';
 export * from './types/Metric';
 export * from './types/Query';
+export * from './types/Dashboard';
 
 export * from './api/v1/types';
 export { default as makeApi } from './api/v1/makeApi';
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
index ac4b19cae5..a2eb7979c4 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts
@@ -19,6 +19,11 @@
 
 import { AdhocFilter, DataMask } from '@superset-ui/core';
 
+export interface ColumnOption {
+  label: string;
+  value: string;
+}
+
 export interface NativeFilterColumn {
   name: string;
   displayName?: string;
@@ -44,6 +49,11 @@ export enum NativeFilterType {
   Divider = 'DIVIDER',
 }
 
+export enum ChartCustomizationType {
+  ChartCustomization = 'CHART_CUSTOMIZATION',
+  Divider = 'CHART_CUSTOMIZATION_DIVIDER',
+}
+
 export enum DataMaskType {
   NativeFilters = 'nativeFilters',
   CrossFilters = 'crossFilters',
@@ -61,9 +71,7 @@ export type Filter = {
   name: string;
   scope: NativeFilterScope;
   filterType: string;
-  // for now there will only ever be one target
-  // when multiple targets are supported, change this to Target[]
-  targets: [Partial<NativeFilterTarget>];
+  targets: Partial<NativeFilterTarget>[];
   controlValues: {
     [key: string]: any;
   };
@@ -80,6 +88,35 @@ export type Filter = {
   description: string;
 };
 
+export type ChartCustomization = {
+  id: string;
+  type: typeof ChartCustomizationType.ChartCustomization;
+  name: string;
+  filterType: string;
+  targets: Partial<NativeFilterTarget>[];
+  scope: NativeFilterScope;
+  chartsInScope?: number[];
+  tabsInScope?: string[];
+  cascadeParentIds?: string[];
+  defaultDataMask: DataMask;
+  controlValues: {
+    sortAscending?: boolean;
+    sortMetric?: string;
+    [key: string]: any;
+  };
+  description?: string;
+  removed?: boolean;
+};
+
+export type ChartCustomizationDivider = Partial<
+  Omit<ChartCustomization, 'id' | 'type'>
+> & {
+  id: string;
+  title: string;
+  description: string;
+  type: typeof ChartCustomizationType.Divider;
+};
+
 export type AppliedFilter = {
   values: {
     filters: Record<string, any>[];
@@ -146,10 +183,30 @@ export function isFilterDivider(
   return filterElement.type === NativeFilterType.Divider;
 }
 
+export function isChartCustomization(
+  filterElement:
+    | Filter
+    | Divider
+    | ChartCustomization
+    | ChartCustomizationDivider,
+): filterElement is ChartCustomization {
+  return filterElement.type === ChartCustomizationType.ChartCustomization;
+}
+
+export function isChartCustomizationDivider(
+  filterElement: ChartCustomization | ChartCustomizationDivider,
+): filterElement is ChartCustomizationDivider {
+  return filterElement.type === ChartCustomizationType.Divider;
+}
+
 export type FilterConfiguration = Array<Filter | Divider>;
 
 export type Filters = {
-  [filterId: string]: Filter | Divider;
+  [filterId: string]:
+    | Filter
+    | Divider
+    | ChartCustomization
+    | ChartCustomizationDivider;
 };
 
 export type PartialFilters = {
@@ -162,6 +219,22 @@ export type NativeFiltersState = {
   hoveredFilterId?: string;
 };
 
+export type ChartCustomizationConfiguration = Array<
+  ChartCustomization | ChartCustomizationDivider
+>;
+
+export type ChartCustomizations = {
+  [chartCustomizationId: string]:
+    | ChartCustomization
+    | ChartCustomizationDivider;
+};
+
+export type PartialChartCustomizations = {
+  [chartCustomizationId: string]: Partial<
+    ChartCustomizations[keyof ChartCustomizations]
+  >;
+};
+
 export type DashboardComponentMetadata = {
   nativeFilters: NativeFiltersState;
   dataMask: DataMaskStateWithId;
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
index 9a8033a9c6..fcbfd3ed7a 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts
@@ -153,6 +153,8 @@ export interface QueryObject
   series_columns?: QueryFormColumn[];
   series_limit?: number;
   series_limit_metric?: Maybe<QueryFormMetric>;
+
+  visible_deckgl_layers?: number[];
 }
 
 export interface QueryContext {
diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/QueryFormData.ts b/superset-frontend/packages/superset-ui-core/src/query/types/QueryFormData.ts
index 3409c61fcc..a3de0fb3b9 100644
--- a/superset-frontend/packages/superset-ui-core/src/query/types/QueryFormData.ts
+++ b/superset-frontend/packages/superset-ui-core/src/query/types/QueryFormData.ts
@@ -131,7 +131,10 @@ export type ExtraFormDataOverrideRegular = Partial<
 > &
   Partial<Pick<SqlaFormData, 'granularity'>> &
   Partial<Pick<BaseFormData, 'time_range'>> &
-  Partial<Pick<QueryObject, 'time_column' | 'time_grain' | 'time_compare'>>;
+  Partial<Pick<QueryObject, 'time_column' | 'time_grain' | 'time_compare'>> & {
+    /** deck.gl layer visibility filter - controls which layers are visible in deck.gl multi-layer charts */
+    visible_deckgl_layers?: number[];
+  };
 
 /** These parameters override those already present in the form data/query object */
 export type ExtraFormDataOverride = ExtraFormDataOverrideRegular &
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
index 7a929d506f..b22de70878 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/DeckGLContainer.tsx
@@ -38,6 +38,7 @@ import DeckGL from '@deck.gl/react';
 import type { Layer } from '@deck.gl/core';
 import { JsonObject, JsonValue, usePrevious } from '@superset-ui/core';
 import { styled } from '@apache-superset/core/ui';
+import { Device } from '@luma.gl/core';
 import Tooltip, { TooltipProps } from './components/Tooltip';
 import 'mapbox-gl/dist/mapbox-gl.css';
 import { Viewport } from './utils/fitViewport';
@@ -168,7 +169,10 @@ export const DeckGLContainer = memo(
             layers={layers()}
             viewState={viewState}
             onViewStateChange={onViewStateChange}
-            onAfterRender={(context: any) => {
+            onAfterRender={(context: {
+              device: Device;
+              gl: WebGL2RenderingContext;
+            }) => {
               glContextRef.current = context.gl;
             }}
           >
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
index afdfa07dbc..c6d2b48e32 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx
@@ -19,14 +19,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { memo, useCallback, useEffect, useRef, useState } from 'react';
+import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useSelector } from 'react-redux';
 import { isEqual } from 'lodash';
+import { createSelector } from '@reduxjs/toolkit';
 import {
   AdhocFilter,
   ContextMenuFilters,
   DataMask,
   Datasource,
   ensureIsArray,
+  ExtraFormData,
   FilterState,
   HandlerFunction,
   isDefined,
@@ -37,6 +40,7 @@ import {
   SupersetClient,
   usePrevious,
 } from '@superset-ui/core';
+import { styled } from '@apache-superset/core/ui';
 import { Layer } from '@deck.gl/core';
 
 import {
@@ -59,6 +63,13 @@ import { getPoints as getPointsHex } from '../layers/Hex/Hex';
 import { getPoints as getPointsGeojson } from '../layers/Geojson/Geojson';
 import { getPoints as getPointsScreengrid } from '../layers/Screengrid/Screengrid';
 
+type DataMaskState = Record<
+  string,
+  DataMask & {
+    extraFormData?: ExtraFormData & { visible_deckgl_layers?: number[] };
+  }
+>;
+
 export type DeckMultiProps = {
   formData: QueryFormData;
   payload: JsonObject;
@@ -79,9 +90,29 @@ export type DeckMultiProps = {
   emitCrossFilters?: boolean;
 };
 
+const MultiWrapper = styled.div<{ height: number; width: number }>`
+  position: relative;
+  height: ${({ height }) => height}px;
+  width: ${({ width }) => width}px;
+`;
+
+const selectDataMask = createSelector(
+  (state: { dataMask?: DataMaskState }) => state.dataMask,
+  dataMask => dataMask || {},
+);
+
 const DeckMulti = (props: DeckMultiProps) => {
   const containerRef = useRef<DeckGLContainerHandle>();
 
+  const dataMask = useSelector(selectDataMask);
+
+  const layerVisibilityFilter = Object.values(dataMask).find(
+    mask => mask?.extraFormData?.visible_deckgl_layers !== undefined,
+  );
+
+  const visibleDeckLayersFromRedux =
+    layerVisibilityFilter?.extraFormData?.visible_deckgl_layers;
+
   const getAdjustedViewport = useCallback(() => {
     let viewport = { ...props.viewport };
     const points = [
@@ -114,6 +145,7 @@ const DeckMulti = (props: DeckMultiProps) => {
   const [subSlicesLayers, setSubSlicesLayers] = useState<Record<number, Layer>>(
     {},
   );
+  const [layerOrder, setLayerOrder] = useState<number[]>([]);
 
   const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
     const { current } = containerRef;
@@ -263,9 +295,8 @@ const DeckMulti = (props: DeckMultiProps) => {
             }));
           })
           .catch(error => {
-            console.error(
-              `Error loading layer for slice ${subsliceCopy.slice_id}:`,
-              error,
+            throw new Error(
+              `Error loading layer for slice ${subsliceCopy.slice_id}: ${error}`,
             );
           });
       }
@@ -277,44 +308,98 @@ const DeckMulti = (props: DeckMultiProps) => {
     (
       formData: QueryFormData,
       payload: JsonObject,
-      viewport?: Viewport,
+      visibleLayers?: number[],
     ): void => {
       setViewport(getAdjustedViewport());
       setSubSlicesLayers({});
 
+      let visibleDeckLayers = visibleLayers;
+
+      if (!visibleDeckLayers) {
+        visibleDeckLayers = (
+          formData.extra_form_data as ExtraFormData & {
+            visible_deckgl_layers?: number[];
+          }
+        )?.visible_deckgl_layers;
+      }
+
+      const deckSlicesOrder = formData.deck_slices || [];
+
       payload.data.slices.forEach(
         (subslice: { slice_id: number } & JsonObject, payloadIndex: number) => {
+          if (visibleDeckLayers && Array.isArray(visibleDeckLayers)) {
+            if (!visibleDeckLayers.includes(subslice.slice_id)) {
+              return;
+            }
+          }
+
           loadSingleLayer(subslice, formData, payloadIndex);
         },
       );
+
+      const orderedSliceIds = deckSlicesOrder.filter((sliceId: number) => {
+        const subslice = payload.data.slices.find(
+          (s: { slice_id: number }) => s.slice_id === sliceId,
+        );
+        if (!subslice) return false;
+        if (visibleDeckLayers && Array.isArray(visibleDeckLayers)) {
+          return visibleDeckLayers.includes(sliceId);
+        }
+        return true;
+      });
+
+      setLayerOrder(orderedSliceIds);
     },
     [getAdjustedViewport, loadSingleLayer],
   );
 
   const prevDeckSlices = usePrevious(props.formData.deck_slices);
+  const prevVisibleLayersRedux = usePrevious(visibleDeckLayersFromRedux);
+
   useEffect(() => {
     const { formData, payload } = props;
-    const hasChanges = !isEqual(prevDeckSlices, formData.deck_slices);
-    if (hasChanges) {
-      loadLayers(formData, payload);
+
+    const deckSlicesChanged = !isEqual(prevDeckSlices, formData.deck_slices);
+    const visibilityFilterChanged = !isEqual(
+      prevVisibleLayersRedux,
+      visibleDeckLayersFromRedux,
+    );
+
+    if (deckSlicesChanged || visibilityFilterChanged) {
+      loadLayers(formData, payload, undefined);
     }
-  }, [loadLayers, prevDeckSlices, props]);
+  }, [
+    loadLayers,
+    prevDeckSlices,
+    prevVisibleLayersRedux,
+    visibleDeckLayersFromRedux,
+    props,
+  ]);
 
   const { payload, formData, setControlValue, height, width } = props;
-  const layers = Object.values(subSlicesLayers);
+
+  const layers = useMemo(
+    () =>
+      layerOrder
+        .map(sliceId => subSlicesLayers[sliceId])
+        .filter(layer => layer !== undefined),
+    [layerOrder, subSlicesLayers],
+  );
 
   return (
-    <DeckGLContainerStyledWrapper
-      ref={containerRef}
-      mapboxApiAccessToken={payload.data.mapboxApiKey}
-      viewport={viewport}
-      layers={layers}
-      mapStyle={formData.mapbox_style}
-      setControlValue={setControlValue}
-      onViewportChange={setViewport}
-      height={height}
-      width={width}
-    />
+    <MultiWrapper height={height} width={width}>
+      <DeckGLContainerStyledWrapper
+        ref={containerRef}
+        mapboxApiAccessToken={payload.data.mapboxApiKey}
+        viewport={viewport}
+        layers={layers}
+        mapStyle={formData.mapbox_style}
+        setControlValue={setControlValue}
+        onViewportChange={setViewport}
+        height={height}
+        width={width}
+      />
+    </MultiWrapper>
   );
 };
 
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts
index eaf990e07a..1ad60394d7 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/controlPanel.ts
@@ -34,26 +34,37 @@ export default {
             config: {
               type: 'SelectAsyncControl',
               multi: true,
-              label: t('deck.gl charts'),
+              label: t('deck.gl layers (charts)'),
               validators: [validateNonEmpty],
               default: [],
               description: t(
-                'Pick a set of deck.gl charts to layer on top of one another',
+                'Select layers in the order you want them stacked. First selected appears at the bottom.Layers let you combine multiple visualizations on one map. Each layer is a saved deck.gl chart (like scatter plots, polygons, or arcs) that displays different data or insights. Stack them to reveal patterns and relationships across your data.',
               ),
               dataEndpoint:
                 'api/v1/chart/?q=(filters:!((col:viz_type,opr:sw,value:deck)))',
               placeholder: t('Select charts'),
               onAsyncErrorMessage: t('Error while fetching charts'),
-              mutator: (data: {
-                result?: { id: number; slice_name: string }[];
-              }) => {
+              mutator: (
+                data: {
+                  result?: { id: number; slice_name: string }[];
+                },
+                value: number[] | undefined,
+              ) => {
                 if (!data?.result) {
                   return [];
                 }
-                return data.result.map(o => ({
-                  value: o.id,
-                  label: o.slice_name,
-                }));
+                const selectedIds = Array.isArray(value) ? value : [];
+
+                return data.result.map(o => {
+                  const selectedIndex = selectedIds.indexOf(o.id);
+                  const indexLabel =
+                    selectedIndex !== -1 ? ` [${selectedIndex + 1}]` : '';
+
+                  return {
+                    value: o.id,
+                    label: `${o.slice_name}${indexLabel}`,
+                  };
+                });
               },
             },
           },
diff --git a/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/DeckglLayerVisibilityCustomizationPlugin.test.tsx b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/DeckglLayerVisibilityCustomizationPlugin.test.tsx
new file mode 100644
index 0000000000..f3ef1d3389
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/DeckglLayerVisibilityCustomizationPlugin.test.tsx
@@ -0,0 +1,484 @@
+/**
+ * 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { SupersetClient } from '@superset-ui/core';
+import DeckglLayerVisibilityCustomizationPlugin from './DeckglLayerVisibilityCustomizationPlugin';
+import { PluginDeckglLayerVisibilityProps } from './types';
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  SupersetClient: {
+    get: jest.fn(),
+  },
+}));
+
+const mockSupersetClientGet = SupersetClient.get as jest.Mock;
+
+const defaultProps: PluginDeckglLayerVisibilityProps = {
+  formData: {
+    viz_type: 'deckgl_layer_visibility',
+    defaultToAllLayersVisible: true,
+    datasource: '1__table',
+  },
+  height: 400,
+  width: 600,
+  filterState: {},
+  setDataMask: jest.fn(),
+};
+
+const mockCharts = {
+  chart1: {
+    form_data: {
+      viz_type: 'deck_multi',
+      deck_slices: [1, 2, 3],
+    },
+  },
+  chart2: {
+    form_data: {
+      viz_type: 'deck_multi',
+      deck_slices: [4, 5],
+    },
+  },
+  chart3: {
+    form_data: {
+      viz_type: 'line',
+    },
+  },
+};
+
+const mockApiResponse = {
+  json: {
+    result: [
+      { id: 1, slice_name: 'Scatter Layer', viz_type: 'deck_scatter' },
+      { id: 2, slice_name: 'Arc Layer', viz_type: 'deck_arc' },
+      { id: 3, slice_name: 'Path Layer', viz_type: 'deck_path' },
+      { id: 4, slice_name: 'Hex Layer', viz_type: 'deck_hex' },
+      { id: 5, slice_name: 'Grid Layer', viz_type: 'deck_grid' },
+    ],
+  },
+};
+
+test('displays loading state initially', () => {
+  mockSupersetClientGet.mockImplementation(() => new Promise(() => {}));
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: mockCharts },
+    },
+  });
+
+  expect(screen.getByText('Loading deck.gl layers...')).toBeInTheDocument();
+});
+
+test('displays message when no deck.gl multi layer charts are found', async () => {
+  mockSupersetClientGet.mockResolvedValue({ json: { result: [] } });
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: {
+        slices: {
+          chart1: {
+            form_data: {
+              viz_type: 'line',
+            },
+          },
+        },
+      },
+    },
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(
+        'No deck.gl multi layer charts found in this dashboard.',
+      ),
+    ).toBeInTheDocument();
+  });
+});
+
+test('renders layer selection control with layers from API', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: mockCharts },
+    },
+  });
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  expect(screen.getByRole('combobox')).toBeInTheDocument();
+});
+
+test('collects unique layer IDs from multiple deck_multi charts', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: mockCharts },
+    },
+  });
+
+  await waitFor(() => {
+    expect(mockSupersetClientGet).toHaveBeenCalled();
+  });
+
+  const callArgs = mockSupersetClientGet.mock.calls[0][0];
+  expect(callArgs.endpoint).toContain('/api/v1/chart/?q=');
+});
+
+test('handles layer selection and calls setDataMask', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+  const setDataMaskMock = jest.fn();
+
+  render(
+    <DeckglLayerVisibilityCustomizationPlugin
+      {...defaultProps}
+      setDataMask={setDataMaskMock}
+    />,
+    {
+      useRedux: true,
+      initialState: {
+        sliceEntities: { slices: mockCharts },
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  const select = screen.getByRole('combobox');
+  await userEvent.click(select);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText('Scatter Layer (deck_scatter)'),
+    ).toBeInTheDocument();
+  });
+
+  await userEvent.click(screen.getByText('Scatter Layer (deck_scatter)'));
+
+  await waitFor(() => {
+    expect(setDataMaskMock).toHaveBeenCalledWith({
+      filterState: {
+        value: [1],
+      },
+      extraFormData: {
+        visible_deckgl_layers: [2, 3, 4, 5],
+      },
+    });
+  });
+});
+
+test('initializes with filterState value when provided', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+
+  render(
+    <DeckglLayerVisibilityCustomizationPlugin
+      {...defaultProps}
+      filterState={{ value: [1, 2] }}
+    />,
+    {
+      useRedux: true,
+      initialState: {
+        sliceEntities: { slices: mockCharts },
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  const select = screen.getByRole('combobox');
+  await userEvent.click(select);
+
+  await waitFor(() => {
+    const selectedItems = screen.getAllByRole('option', { selected: true });
+    expect(selectedItems).toHaveLength(2);
+  });
+});
+
+test('initializes all layers visible when defaultToAllLayersVisible is true and no prior state', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+  const setDataMaskMock = jest.fn();
+
+  render(
+    <DeckglLayerVisibilityCustomizationPlugin
+      {...defaultProps}
+      formData={{
+        viz_type: 'deckgl_layer_visibility',
+        defaultToAllLayersVisible: true,
+        datasource: '1__table',
+      }}
+      setDataMask={setDataMaskMock}
+    />,
+    {
+      useRedux: true,
+      initialState: {
+        sliceEntities: { slices: mockCharts },
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(setDataMaskMock).toHaveBeenCalledWith({
+      filterState: {
+        value: [],
+      },
+      extraFormData: {
+        visible_deckgl_layers: [1, 2, 3, 4, 5],
+      },
+    });
+  });
+});
+
+test('does not auto-initialize when defaultToAllLayersVisible is false', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+  const setDataMaskMock = jest.fn();
+
+  render(
+    <DeckglLayerVisibilityCustomizationPlugin
+      {...defaultProps}
+      formData={{
+        viz_type: 'deckgl_layer_visibility',
+        defaultToAllLayersVisible: false,
+        datasource: '1__table',
+      }}
+      setDataMask={setDataMaskMock}
+    />,
+    {
+      useRedux: true,
+      initialState: {
+        sliceEntities: { slices: mockCharts },
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  expect(setDataMaskMock).not.toHaveBeenCalled();
+});
+
+test('handles multiple layer selection', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+  const setDataMaskMock = jest.fn();
+
+  render(
+    <DeckglLayerVisibilityCustomizationPlugin
+      {...defaultProps}
+      setDataMask={setDataMaskMock}
+    />,
+    {
+      useRedux: true,
+      initialState: {
+        sliceEntities: { slices: mockCharts },
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  const select = screen.getByRole('combobox');
+  await userEvent.click(select);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText('Scatter Layer (deck_scatter)'),
+    ).toBeInTheDocument();
+  });
+
+  await userEvent.click(screen.getByText('Scatter Layer (deck_scatter)'));
+  await userEvent.click(screen.getByText('Arc Layer (deck_arc)'));
+
+  await waitFor(() => {
+    expect(setDataMaskMock).toHaveBeenLastCalledWith({
+      filterState: {
+        value: expect.arrayContaining([1, 2]),
+      },
+      extraFormData: {
+        visible_deckgl_layers: expect.arrayContaining([3, 4, 5]),
+      },
+    });
+  });
+});
+
+test('displays tooltip info icon', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: mockCharts },
+    },
+  });
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  const tooltipIcon = screen.getByRole('img', { name: /info-circle/i });
+  expect(tooltipIcon).toBeInTheDocument();
+
+  await userEvent.hover(tooltipIcon);
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(
+        'Choose layers to hide from all deck.gl Multiple Layer charts in this dashboard.',
+      ),
+    ).toBeInTheDocument();
+  });
+});
+
+test('handles charts with undefined deck_slices', async () => {
+  mockSupersetClientGet.mockResolvedValue({ json: { result: [] } });
+
+  const chartsWithUndefined = {
+    chart1: {
+      form_data: {
+        viz_type: 'deck_multi',
+      },
+    },
+  };
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: chartsWithUndefined },
+    },
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(
+        'No deck.gl multi layer charts found in this dashboard.',
+      ),
+    ).toBeInTheDocument();
+  });
+});
+
+test('handles charts with non-array deck_slices', async () => {
+  mockSupersetClientGet.mockResolvedValue({ json: { result: [] } });
+
+  const chartsWithInvalidSlices = {
+    chart1: {
+      form_data: {
+        viz_type: 'deck_multi',
+        deck_slices: 'invalid',
+      },
+    },
+  };
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: chartsWithInvalidSlices },
+    },
+  });
+
+  await waitFor(() => {
+    expect(
+      screen.getByText(
+        'No deck.gl multi layer charts found in this dashboard.',
+      ),
+    ).toBeInTheDocument();
+  });
+});
+
+test('deduplicates layer IDs from multiple charts', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+
+  const chartsWithDuplicates = {
+    chart1: {
+      form_data: {
+        viz_type: 'deck_multi',
+        deck_slices: [1, 2, 3],
+      },
+    },
+    chart2: {
+      form_data: {
+        viz_type: 'deck_multi',
+        deck_slices: [2, 3, 4],
+      },
+    },
+  };
+
+  render(<DeckglLayerVisibilityCustomizationPlugin {...defaultProps} />, {
+    useRedux: true,
+    initialState: {
+      sliceEntities: { slices: chartsWithDuplicates },
+    },
+  });
+
+  await waitFor(() => {
+    expect(mockSupersetClientGet).toHaveBeenCalled();
+  });
+
+  const callArgs = mockSupersetClientGet.mock.calls[0][0];
+  expect(callArgs.endpoint).toContain('/api/v1/chart/?q=');
+});
+
+test('respects existing visible_deckgl_layers from Redux state', async () => {
+  mockSupersetClientGet.mockResolvedValue(mockApiResponse);
+  const setDataMaskMock = jest.fn();
+
+  render(
+    <DeckglLayerVisibilityCustomizationPlugin
+      {...defaultProps}
+      formData={{
+        viz_type: 'deckgl_layer_visibility',
+        defaultToAllLayersVisible: true,
+        datasource: '1__table',
+      }}
+      setDataMask={setDataMaskMock}
+    />,
+    {
+      useRedux: true,
+      initialState: {
+        sliceEntities: { slices: mockCharts },
+        dataMask: {
+          filter1: {
+            extraFormData: {
+              visible_deckgl_layers: [1, 2],
+            },
+          },
+        },
+      },
+    },
+  );
+
+  await waitFor(() => {
+    expect(screen.getByText('Exclude layers (deck.gl)')).toBeInTheDocument();
+  });
+
+  expect(setDataMaskMock).not.toHaveBeenCalled();
+});
diff --git a/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/DeckglLayerVisibilityCustomizationPlugin.tsx b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/DeckglLayerVisibilityCustomizationPlugin.tsx
new file mode 100644
index 0000000000..dca9832dd6
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/DeckglLayerVisibilityCustomizationPlugin.tsx
@@ -0,0 +1,204 @@
+/**
+ * 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 { useEffect, useState, useMemo, useRef, useCallback } from 'react';
+import { t } from '@apache-superset/core';
+import { DataMask, ExtraFormData } from '@superset-ui/core';
+import { useTheme } from '@apache-superset/core/ui';
+import {
+  Select,
+  FormItem,
+  Tooltip,
+  Icons,
+  Flex,
+} from '@superset-ui/core/components';
+import { useSelector } from 'react-redux';
+import { createSelector } from '@reduxjs/toolkit';
+import { PluginDeckglLayerVisibilityProps } from './types';
+import { useDeckLayerMetadata } from './useDeckLayerMetadata';
+import { FilterPluginStyle } from '../common';
+import { Slice } from 'src/dashboard/types';
+
+type SliceEntitiesState = {
+  sliceEntities?: {
+    slices: Record<number, Slice>;
+  };
+};
+
+type DataMaskState = Record<
+  string,
+  DataMask & {
+    extraFormData?: ExtraFormData & { visible_deckgl_layers?: number[] };
+  }
+>;
+
+const EMPTY_OBJECT = {};
+
+const selectAllLayerIds = createSelector(
+  [
+    (state: SliceEntitiesState) =>
+      state.sliceEntities?.slices || (EMPTY_OBJECT as Record<number, Slice>),
+  ],
+  slices => {
+    const ids: number[] = [];
+    Object.values(slices).forEach(slice => {
+      if (slice.form_data?.viz_type === 'deck_multi') {
+        const deckSlices = slice.form_data.deck_slices as number[] | undefined;
+        if (deckSlices && Array.isArray(deckSlices)) {
+          ids.push(...deckSlices);
+        }
+      }
+    });
+    return [...new Set(ids)];
+  },
+);
+
+export default function DeckglLayerVisibilityCustomizationPlugin(
+  props: PluginDeckglLayerVisibilityProps,
+) {
+  const { formData, filterState, setDataMask, width, height } = props;
+  const theme = useTheme();
+  const [hiddenLayers, setHiddenLayers] = useState<number[]>(
+    filterState?.value || [],
+  );
+  const hasInitialized = useRef(false);
+
+  const allLayerIds = useSelector(selectAllLayerIds);
+  const dataMask = useSelector(
+    (state: { dataMask?: DataMaskState }) =>
+      state.dataMask || (EMPTY_OBJECT as DataMaskState),
+  );
+
+  const visibleDeckLayersFromRedux = useMemo(() => {
+    const layerVisibilityFilter = Object.values(dataMask).find(
+      mask => mask?.extraFormData?.visible_deckgl_layers !== undefined,
+    );
+    return layerVisibilityFilter?.extraFormData?.visible_deckgl_layers;
+  }, [dataMask]);
+
+  const { layers: apiLayers, isLoading: isLoadingMetadata } =
+    useDeckLayerMetadata(allLayerIds);
+
+  const allLayerIdsFromApi = useMemo(
+    () => apiLayers.map(layer => layer.sliceId),
+    [apiLayers],
+  );
+
+  useEffect(() => {
+    if (
+      !hasInitialized.current &&
+      formData.defaultToAllLayersVisible &&
+      apiLayers.length > 0 &&
+      !filterState?.value &&
+      visibleDeckLayersFromRedux === undefined
+    ) {
+      hasInitialized.current = true;
+      setHiddenLayers([]);
+
+      setDataMask({
+        filterState: {
+          value: [],
+        },
+        extraFormData: {
+          visible_deckgl_layers: allLayerIdsFromApi,
+        } as ExtraFormData,
+      });
+    }
+  }, [
+    formData.defaultToAllLayersVisible,
+    apiLayers.length,
+    filterState?.value,
+    visibleDeckLayersFromRedux,
+    allLayerIdsFromApi,
+    setDataMask,
+  ]);
+
+  const handleLayerChange = useCallback(
+    (selectedHiddenLayers: number[]) => {
+      setHiddenLayers(selectedHiddenLayers);
+
+      const visibleLayers = allLayerIdsFromApi.filter(
+        id => !selectedHiddenLayers.includes(id),
+      );
+
+      setDataMask({
+        filterState: {
+          value: selectedHiddenLayers,
+        },
+        extraFormData: {
+          visible_deckgl_layers: visibleLayers,
+        } as ExtraFormData,
+      });
+    },
+    [allLayerIdsFromApi, setDataMask],
+  );
+
+  const selectOptions = useMemo(
+    () =>
+      apiLayers.map(layer => ({
+        label: `${layer.name} (${layer.type})`,
+        value: layer.sliceId,
+      })),
+    [apiLayers],
+  );
+
+  if (isLoadingMetadata && apiLayers.length === 0) {
+    return (
+      <FilterPluginStyle height={height} width={width}>
+        <div>{t('Loading deck.gl layers...')}</div>
+      </FilterPluginStyle>
+    );
+  }
+
+  return (
+    <FilterPluginStyle height={height} width={width}>
+      {apiLayers.length === 0 ? (
+        <div>{t('No deck.gl multi layer charts found in this dashboard.')}</div>
+      ) : (
+        <FormItem
+          label={
+            <Flex gap={theme.sizeUnit}>
+              <span>{t('Exclude layers (deck.gl)')}</span>
+              <Tooltip
+                title={t(
+                  'Choose layers to hide from all deck.gl Multiple Layer charts in this dashboard.',
+                )}
+              >
+                <span className="tooltip-icon">
+                  <Icons.InfoCircleOutlined
+                    iconSize="m"
+                    iconColor={theme.colorIcon}
+                  />
+                </span>
+              </Tooltip>
+            </Flex>
+          }
+        >
+          <Select
+            mode="multiple"
+            value={hiddenLayers}
+            onChange={handleLayerChange}
+            options={selectOptions}
+            placeholder={t('Select layers to hide')}
+            allowClear
+          />
+        </FormItem>
+      )}
+    </FilterPluginStyle>
+  );
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/constants.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/buildQuery.test.ts
similarity index 50%
copy from superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/constants.ts
copy to superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/buildQuery.test.ts
index 783f7131fc..2ed2e63a98 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/constants.ts
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/buildQuery.test.ts
@@ -16,23 +16,31 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { GenericDataType } from '@apache-superset/core/api/core';
+import buildQuery from './buildQuery';
+import { DeckglLayerVisibilityFormData } from './types';
 
-export const INPUT_HEIGHT = 32;
+test('returns empty query context', () => {
+  const formData: DeckglLayerVisibilityFormData = {
+    viz_type: 'deckgl_layer_visibility',
+    defaultToAllLayersVisible: true,
+    datasource: '1__table',
+  };
 
-export const INPUT_WIDTH = 270;
+  const result = buildQuery(formData);
 
-export const TIME_FILTER_INPUT_WIDTH = 350;
+  expect(result).toHaveProperty('queries');
+  expect(result.queries).toEqual([]);
+});
 
-export const FILTER_SUPPORTED_TYPES = {
-  filter_time: [GenericDataType.Temporal],
-  filter_timegrain: [GenericDataType.Temporal],
-  filter_timecolumn: [GenericDataType.Temporal],
-  filter_select: [
-    GenericDataType.Boolean,
-    GenericDataType.String,
-    GenericDataType.Numeric,
-    GenericDataType.Temporal,
-  ],
-  filter_range: [GenericDataType.Numeric],
-};
+test('returns empty query context with defaultToAllLayersVisible false', () => {
+  const formData: DeckglLayerVisibilityFormData = {
+    viz_type: 'deckgl_layer_visibility',
+    defaultToAllLayersVisible: false,
+    datasource: '1__table',
+  };
+
+  const result = buildQuery(formData);
+
+  expect(result).toHaveProperty('queries');
+  expect(result.queries).toEqual([]);
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/buildQuery.ts
similarity index 61%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/buildQuery.ts
index 10af257b26..8073f99a60 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/buildQuery.ts
@@ -16,28 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { buildQueryContext, BuildQuery } from '@superset-ui/core';
+import { DeckglLayerVisibilityFormData } from './types';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
-
-export interface FilterCardRowProps {
-  filter: Filter;
-}
+/**
+ * Layer visibility filter doesn't need to query data from the backend.
+ * It operates on chart metadata available in the dashboard state.
+ * Returns an empty query context.
+ */
+const buildQuery: BuildQuery<DeckglLayerVisibilityFormData> = formData =>
+  buildQueryContext(formData, () => []);
 
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
-}
+export default buildQuery;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/controlPanel.ts
similarity index 61%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/controlPanel.ts
index 10af257b26..5b7bc3dc3b 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/controlPanel.ts
@@ -16,28 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { ControlPanelConfig } from '@superset-ui/chart-controls';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
+const config: ControlPanelConfig = {
+  controlPanelSections: [],
+};
 
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
-
-export interface FilterCardRowProps {
-  filter: Filter;
-}
-
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
-}
+export default config;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/index.ts
similarity index 50%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
copy to superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/index.ts
index ec0dc1353c..4e9630e08c 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/index.ts
@@ -16,21 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo } from 'react';
 import { t } from '@apache-superset/core';
-import { getChartMetadataRegistry } from '@superset-ui/core';
-import { Row, RowLabel, RowValue } from './Styles';
-import { FilterCardRowProps } from './types';
+import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
 
-export const TypeRow = ({ filter }: FilterCardRowProps) => {
-  const metadata = useMemo(
-    () => getChartMetadataRegistry().get(filter.filterType),
-    [filter.filterType],
-  );
-  return (
-    <Row>
-      <RowLabel>{t('Filter type')}</RowLabel>
-      <RowValue>{metadata?.name}</RowValue>
-    </Row>
-  );
-};
+export default class DeckglLayerVisibilityCustomizationPlugin extends ChartPlugin {
+  constructor() {
+    const metadata = new ChartMetadata({
+      name: t('Deck.gl Layer Visibility'),
+      description: t('Chart customization to control deck.gl layer visibility'),
+      behaviors: [Behavior.InteractiveChart, Behavior.ChartCustomization],
+      tags: [t('Deckgl'), t('Experimental')],
+      thumbnail: '',
+      enableNoResults: false,
+    });
+
+    super({
+      buildQuery,
+      controlPanel,
+      loadChart: () => import('./DeckglLayerVisibilityCustomizationPlugin'),
+      metadata,
+      transformProps,
+    });
+  }
+}
diff --git a/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/transformProps.test.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/transformProps.test.ts
new file mode 100644
index 0000000000..65fb401bd6
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/transformProps.test.ts
@@ -0,0 +1,100 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import transformProps from './transformProps';
+import { DeckglLayerVisibilityFormData } from './types';
+
+test('transforms props correctly with all required fields', () => {
+  const setDataMaskMock = jest.fn();
+  const formData: DeckglLayerVisibilityFormData = {
+    viz_type: 'deckgl_layer_visibility',
+    defaultToAllLayersVisible: true,
+    datasource: '1__table',
+  };
+
+  const chartProps = {
+    formData,
+    height: 400,
+    width: 600,
+    filterState: { value: [1, 2] },
+    hooks: { setDataMask: setDataMaskMock },
+    ownState: { availableLayers: [] },
+  } as unknown as ChartProps;
+
+  const result = transformProps(chartProps);
+
+  expect(result).toEqual({
+    formData,
+    height: 400,
+    width: 600,
+    filterState: { value: [1, 2] },
+    setDataMask: setDataMaskMock,
+    ownState: { availableLayers: [] },
+  });
+});
+
+test('transforms props with empty filter state', () => {
+  const setDataMaskMock = jest.fn();
+  const formData: DeckglLayerVisibilityFormData = {
+    viz_type: 'deckgl_layer_visibility',
+    defaultToAllLayersVisible: false,
+    datasource: '1__table',
+  };
+
+  const chartProps = {
+    formData,
+    height: 300,
+    width: 500,
+    filterState: {},
+    hooks: { setDataMask: setDataMaskMock },
+    ownState: undefined,
+  } as unknown as ChartProps;
+
+  const result = transformProps(chartProps);
+
+  expect(result).toEqual({
+    formData,
+    height: 300,
+    width: 500,
+    filterState: {},
+    setDataMask: setDataMaskMock,
+    ownState: undefined,
+  });
+});
+
+test('preserves setDataMask function reference', () => {
+  const setDataMaskMock = jest.fn();
+  const formData: DeckglLayerVisibilityFormData = {
+    viz_type: 'deckgl_layer_visibility',
+    defaultToAllLayersVisible: true,
+    datasource: '1__table',
+  };
+
+  const chartProps = {
+    formData,
+    height: 200,
+    width: 400,
+    filterState: {},
+    hooks: { setDataMask: setDataMaskMock },
+  } as unknown as ChartProps;
+
+  const result = transformProps(chartProps);
+
+  expect(result.setDataMask).toBe(setDataMaskMock);
+});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/transformProps.ts
similarity index 61%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/transformProps.ts
index 10af257b26..f8dcf7a081 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/transformProps.ts
@@ -16,28 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { ChartProps } from '@superset-ui/core';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
-
-export interface FilterCardRowProps {
-  filter: Filter;
-}
+export default function transformProps(chartProps: ChartProps) {
+  const { formData, height, width, filterState, hooks, ownState } = chartProps;
 
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
+  return {
+    formData,
+    height,
+    width,
+    filterState,
+    setDataMask: hooks.setDataMask,
+    ownState,
+  };
 }
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/types.ts
similarity index 56%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/types.ts
index 10af257b26..35d7ba8648 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/types.ts
@@ -16,28 +16,31 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { QueryFormData, DataMask } from '@superset-ui/core';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
+export interface DeckglLayerVisibilityFormData extends QueryFormData {
+  defaultToAllLayersVisible?: boolean;
 }
 
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
+export const DEFAULT_FORM_DATA: Partial<DeckglLayerVisibilityFormData> = {
+  defaultToAllLayersVisible: true,
+};
 
-export interface FilterCardRowProps {
-  filter: Filter;
+export interface LayerInfo {
+  sliceId: number;
+  name: string;
+  type: string;
 }
 
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
+export interface PluginDeckglLayerVisibilityProps {
+  formData: DeckglLayerVisibilityFormData;
+  height: number;
+  width: number;
+  filterState: {
+    value?: number[];
+  };
+  setDataMask: (dataMask: DataMask) => void;
+  ownState?: {
+    availableLayers?: LayerInfo[];
+  };
 }
diff --git a/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.test.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.test.ts
new file mode 100644
index 0000000000..4b19a795f5
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.test.ts
@@ -0,0 +1,195 @@
+/**
+ * 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 { renderHook } from '@testing-library/react-hooks';
+import { SupersetClient } from '@superset-ui/core';
+import { useDeckLayerMetadata } from './useDeckLayerMetadata';
+
+jest.mock('@superset-ui/core', () => ({
+  ...jest.requireActual('@superset-ui/core'),
+  SupersetClient: {
+    get: jest.fn(),
+  },
+}));
+
+const mockSupersetClientGet = SupersetClient.get as jest.Mock;
+
+beforeEach(() => {
+  jest.clearAllMocks();
+});
+
+test('returns empty layers when sliceIds is empty', () => {
+  const { result } = renderHook(() => useDeckLayerMetadata([]));
+
+  expect(result.current.layers).toEqual([]);
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.error).toBe(null);
+});
+
+test('fetches layer metadata successfully', async () => {
+  const mockResponse = {
+    json: {
+      result: [
+        { id: 1, slice_name: 'Layer 1', viz_type: 'deck_scatter' },
+        { id: 2, slice_name: 'Layer 2', viz_type: 'deck_arc' },
+      ],
+    },
+  };
+  mockSupersetClientGet.mockResolvedValue(mockResponse);
+
+  const { result, waitForNextUpdate } = renderHook(() =>
+    useDeckLayerMetadata([1, 2]),
+  );
+
+  expect(result.current.isLoading).toBe(true);
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.layers).toEqual([
+    { sliceId: 1, name: 'Layer 1', type: 'deck_scatter' },
+    { sliceId: 2, name: 'Layer 2', type: 'deck_arc' },
+  ]);
+  expect(result.current.error).toBe(null);
+  expect(mockSupersetClientGet).toHaveBeenCalledWith({
+    endpoint: expect.stringContaining('/api/v1/chart/?q='),
+  });
+});
+
+test('handles API error and returns fallback layers', async () => {
+  const errorMessage = 'Network error';
+  mockSupersetClientGet.mockRejectedValue(new Error(errorMessage));
+
+  const { result, waitForNextUpdate } = renderHook(() =>
+    useDeckLayerMetadata([1, 2, 3]),
+  );
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.error).toBe(errorMessage);
+  expect(result.current.layers).toEqual([
+    { sliceId: 1, name: 'Layer 1', type: 'unknown' },
+    { sliceId: 2, name: 'Layer 2', type: 'unknown' },
+    { sliceId: 3, name: 'Layer 3', type: 'unknown' },
+  ]);
+});
+
+test('handles non-Error object rejection', async () => {
+  mockSupersetClientGet.mockRejectedValue('String error');
+
+  const { result, waitForNextUpdate } = renderHook(() =>
+    useDeckLayerMetadata([1]),
+  );
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.error).toBe('Unknown error');
+  expect(result.current.layers).toEqual([
+    { sliceId: 1, name: 'Layer 1', type: 'unknown' },
+  ]);
+});
+
+test('refetches when sliceIds change', async () => {
+  const mockResponse1 = {
+    json: {
+      result: [{ id: 1, slice_name: 'Layer 1', viz_type: 'deck_scatter' }],
+    },
+  };
+  const mockResponse2 = {
+    json: {
+      result: [
+        { id: 2, slice_name: 'Layer 2', viz_type: 'deck_arc' },
+        { id: 3, slice_name: 'Layer 3', viz_type: 'deck_geojson' },
+      ],
+    },
+  };
+
+  mockSupersetClientGet
+    .mockResolvedValueOnce(mockResponse1)
+    .mockResolvedValueOnce(mockResponse2);
+
+  const { result, rerender, waitForNextUpdate } = renderHook(
+    ({ ids }) => useDeckLayerMetadata(ids),
+    {
+      initialProps: { ids: [1] },
+    },
+  );
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.layers).toHaveLength(1);
+  expect(result.current.layers[0].sliceId).toBe(1);
+
+  rerender({ ids: [2, 3] });
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.layers).toHaveLength(2);
+  expect(result.current.layers[0].sliceId).toBe(2);
+  expect(result.current.layers[1].sliceId).toBe(3);
+  expect(mockSupersetClientGet).toHaveBeenCalledTimes(2);
+});
+
+test('handles empty result from API', async () => {
+  const mockResponse = {
+    json: {
+      result: [],
+    },
+  };
+  mockSupersetClientGet.mockResolvedValue(mockResponse);
+
+  const { result, waitForNextUpdate } = renderHook(() =>
+    useDeckLayerMetadata([1, 2]),
+  );
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+  expect(result.current.layers).toEqual([]);
+  expect(result.current.error).toBe(null);
+});
+
+test('does not refetch when sliceIds array has same values', async () => {
+  const mockResponse = {
+    json: {
+      result: [{ id: 1, slice_name: 'Layer 1', viz_type: 'deck_scatter' }],
+    },
+  };
+  mockSupersetClientGet.mockResolvedValue(mockResponse);
+
+  const { result, rerender, waitForNextUpdate } = renderHook(
+    ({ ids }) => useDeckLayerMetadata(ids),
+    {
+      initialProps: { ids: [1] },
+    },
+  );
+
+  await waitForNextUpdate();
+
+  expect(result.current.isLoading).toBe(false);
+
+  const callCount = mockSupersetClientGet.mock.calls.length;
+
+  rerender({ ids: [1] });
+
+  expect(mockSupersetClientGet).toHaveBeenCalledTimes(callCount);
+});
diff --git a/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.ts b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.ts
new file mode 100644
index 0000000000..dfa7c65632
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DeckglLayerVisibility/useDeckLayerMetadata.ts
@@ -0,0 +1,87 @@
+/**
+ * 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 { useEffect, useState } from 'react';
+import { SupersetClient } from '@superset-ui/core';
+import rison from 'rison';
+import { LayerInfo } from './types';
+
+interface SliceResponse {
+  id: number;
+  slice_name: string;
+  viz_type: string;
+}
+
+export const useDeckLayerMetadata = (
+  sliceIds: number[],
+): {
+  layers: LayerInfo[];
+  isLoading: boolean;
+  error: string | null;
+} => {
+  const [layers, setLayers] = useState<LayerInfo[]>([]);
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    if (!sliceIds || sliceIds.length === 0) {
+      setLayers([]);
+      return;
+    }
+
+    const fetchMetadata = async () => {
+      setIsLoading(true);
+      setError(null);
+
+      try {
+        const queryParams = rison.encode({
+          columns: ['id', 'slice_name', 'viz_type'],
+          filters: [{ col: 'id', opr: 'in', value: sliceIds }],
+        });
+        const endpoint = `/api/v1/chart/?q=${queryParams}`;
+
+        const response = await SupersetClient.get({ endpoint });
+
+        const slices: SliceResponse[] = response.json.result || [];
+
+        const layerInfos: LayerInfo[] = slices.map(slice => ({
+          sliceId: slice.id,
+          name: slice.slice_name,
+          type: slice.viz_type,
+        }));
+
+        setLayers(layerInfos);
+      } catch (err) {
+        setError(err instanceof Error ? err.message : 'Unknown error');
+
+        const fallbackLayers = sliceIds.map(id => ({
+          sliceId: id,
+          name: `Layer ${id}`,
+          type: 'unknown',
+        }));
+        setLayers(fallbackLayers);
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    fetchMetadata();
+  }, [sliceIds.join(',')]);
+
+  return { layers, isLoading, error };
+};
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
new file mode 100644
index 0000000000..8a8c045e0d
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/DynamicGroupByPlugin.tsx
@@ -0,0 +1,140 @@
+/**
+ * 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 { t, tn } from '@apache-superset/core';
+import { ensureIsArray, ExtraFormData } from '@superset-ui/core';
+import { useEffect, useState, useMemo } from 'react';
+import {
+  FormItem,
+  type FormItemProps,
+  Select,
+  type SelectValue,
+} from '@superset-ui/core/components';
+import { FilterPluginStyle, StatusMessage } from '../common';
+import { PluginFilterGroupByProps, ColumnOption, ColumnData } from './types';
+
+const EMPTY_OBJECT = {};
+
+export default function PluginFilterDynamicGroupBy(
+  props: PluginFilterGroupByProps,
+) {
+  const {
+    data,
+    formData,
+    height,
+    width,
+    setDataMask,
+    setHoveredFilter,
+    unsetHoveredFilter,
+    setFocusedFilter,
+    unsetFocusedFilter,
+    setFilterActive,
+    filterState,
+    inputRef,
+  } = props;
+  const { defaultValue } = formData;
+
+  const [value, setValue] = useState<string[]>(
+    ensureIsArray<string>(defaultValue ?? []),
+  );
+
+  const handleChange = (values: SelectValue) => {
+    const resultValue: string[] = ensureIsArray<string>(
+      values as string | string[] | null | undefined,
+    );
+
+    const extraFormData: ExtraFormData = {
+      custom_form_data: {
+        groupby: resultValue,
+      },
+    };
+
+    setValue(resultValue);
+    setDataMask({
+      extraFormData,
+      filterState: {
+        label: resultValue.join(', '),
+        value: resultValue.length ? resultValue : null,
+      },
+    });
+  };
+
+  useEffect(() => {
+    handleChange(defaultValue ?? []);
+  }, [JSON.stringify(defaultValue)]);
+
+  useEffect(() => {
+    handleChange(filterState.value ?? []);
+  }, [JSON.stringify(filterState.value)]);
+
+  const placeholderText =
+    (data || []).length === 0
+      ? t('No data')
+      : tn('%s option', '%s options', data.length, data.length);
+
+  const formItemData: FormItemProps = useMemo(() => {
+    if (filterState.validateMessage) {
+      return {
+        extra: (
+          <StatusMessage status={filterState.validateStatus}>
+            {filterState.validateMessage}
+          </StatusMessage>
+        ),
+      };
+    }
+    return EMPTY_OBJECT as FormItemProps;
+  }, [filterState.validateMessage, filterState.validateStatus]);
+
+  const options = useMemo(
+    () =>
+      (data || []).map((row: ColumnOption | ColumnData) => {
+        const columnName = 'column_name' in row ? row.column_name : row.value;
+        const label =
+          ('verbose_name' in row && row.verbose_name) ||
+          ('label' in row && row.label) ||
+          columnName;
+        return {
+          label,
+          value: columnName,
+        };
+      }),
+    [data],
+  );
+
+  return (
+    <FilterPluginStyle height={height} width={width}>
+      <FormItem validateStatus={filterState.validateStatus} {...formItemData}>
+        <div onMouseEnter={setHoveredFilter} onMouseLeave={unsetHoveredFilter}>
+          <Select
+            name={formData.nativeFilterId}
+            allowClear
+            mode="multiple"
+            value={value}
+            placeholder={placeholderText}
+            onChange={handleChange}
+            onBlur={unsetFocusedFilter}
+            onFocus={setFocusedFilter}
+            ref={inputRef}
+            options={options}
+            onOpenChange={setFilterActive}
+          />
+        </div>
+      </FormItem>
+    </FilterPluginStyle>
+  );
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/buildQuery.ts
similarity index 61%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/DynamicGroupBy/buildQuery.ts
index 10af257b26..6c9eeff43d 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/buildQuery.ts
@@ -16,28 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { buildQueryContext, QueryFormData } from '@superset-ui/core';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
-
-export interface FilterCardRowProps {
-  filter: Filter;
-}
-
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
+export default function buildQuery(formData: QueryFormData) {
+  return buildQueryContext(formData, () => [
+    {
+      result_type: 'columns',
+      columns: [],
+      metrics: [],
+      orderby: [],
+    },
+  ]);
 }
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/controlPanel.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/controlPanel.ts
new file mode 100644
index 0000000000..a09523b6cf
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/controlPanel.ts
@@ -0,0 +1,80 @@
+/**
+ * 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 { ControlPanelConfig } from '@superset-ui/chart-controls';
+import { t } from '@apache-superset/core';
+
+const config: ControlPanelConfig = {
+  controlPanelSections: [
+    {
+      label: t('Query'),
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'groupby',
+            config: {
+              type: 'SelectControl',
+              label: t('Column'),
+              description: t('Column to group by'),
+              default: null,
+              clearable: true,
+              required: true,
+            },
+          },
+        ],
+      ],
+    },
+    {
+      label: t('UI Configuration'),
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'canSelectMultiple',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Can select multiple values'),
+              default: true,
+              renderTrigger: true,
+              resetConfig: true,
+              affectsDataMask: true,
+              description: t('Allow users to select multiple values'),
+            },
+          },
+        ],
+        [
+          {
+            name: 'enableEmptyFilter',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Chart customization value is required'),
+              default: false,
+              renderTrigger: true,
+              description: t(
+                'User must select a value before applying the chart customization',
+              ),
+            },
+          },
+        ],
+      ],
+    },
+  ],
+};
+
+export default config;
diff --git a/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/images/thumbnail.png b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/images/thumbnail.png
new file mode 100644
index 0000000000..7afef30bd4
Binary files /dev/null and b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/images/thumbnail.png differ
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/index.ts
similarity index 51%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
copy to superset-frontend/src/chartCustomizations/components/DynamicGroupBy/index.ts
index ec0dc1353c..4b81919fca 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/index.ts
@@ -16,21 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo } from 'react';
 import { t } from '@apache-superset/core';
-import { getChartMetadataRegistry } from '@superset-ui/core';
-import { Row, RowLabel, RowValue } from './Styles';
-import { FilterCardRowProps } from './types';
+import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
+import thumbnail from './images/thumbnail.png';
 
-export const TypeRow = ({ filter }: FilterCardRowProps) => {
-  const metadata = useMemo(
-    () => getChartMetadataRegistry().get(filter.filterType),
-    [filter.filterType],
-  );
-  return (
-    <Row>
-      <RowLabel>{t('Filter type')}</RowLabel>
-      <RowValue>{metadata?.name}</RowValue>
-    </Row>
-  );
-};
+export default class ChartCustomizationDynamicGroupByPlugin extends ChartPlugin {
+  constructor() {
+    const metadata = new ChartMetadata({
+      name: t('Dynamic group by'),
+      description: t('Dynamically select grouping columns from a dataset'),
+      behaviors: [Behavior.ChartCustomization],
+      tags: [t('Grouping'), t('Dynamic')],
+      thumbnail,
+      datasourceCount: 1,
+    });
+
+    super({
+      buildQuery,
+      controlPanel,
+      loadChart: () => import('./DynamicGroupByPlugin'),
+      metadata,
+      transformProps,
+    });
+  }
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/transformProps.ts
similarity index 50%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/DynamicGroupBy/transformProps.ts
index 10af257b26..b86d04c0c4 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/transformProps.ts
@@ -16,28 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { ChartProps } from '@superset-ui/core';
+import { noOp } from 'src/utils/common';
+import { DEFAULT_FORM_DATA } from './types';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
+export default function transformProps(chartProps: ChartProps) {
+  const { formData, height, hooks, queriesData, width, filterState, inputRef } =
+    chartProps;
+  const {
+    setDataMask = noOp,
+    setHoveredFilter = noOp,
+    unsetHoveredFilter = noOp,
+    setFocusedFilter = noOp,
+    unsetFocusedFilter = noOp,
+    setFilterActive = noOp,
+  } = hooks;
 
-export interface FilterCardRowProps {
-  filter: Filter;
-}
+  const { data } = queriesData[0];
 
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
+  return {
+    filterState,
+    width,
+    height,
+    data,
+    formData: { ...DEFAULT_FORM_DATA, ...formData },
+    setDataMask,
+    setHoveredFilter,
+    unsetHoveredFilter,
+    setFocusedFilter,
+    unsetFocusedFilter,
+    setFilterActive,
+    inputRef,
+  };
 }
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/types.ts b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
similarity index 51%
rename from superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/types.ts
rename to superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
index ab2d536b03..46374249da 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/DynamicGroupBy/types.ts
@@ -16,9 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DataMask } from '@superset-ui/core';
+import { FilterState, QueryFormData } from '@superset-ui/core';
+import { RefObject } from 'react';
+import type { RefSelectProps } from '@superset-ui/core/components';
+import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
 
-interface DatasetReference {
+export interface DatasetReference {
   value: string | number;
   label?: string;
   table_name?: string;
@@ -30,61 +33,50 @@ export interface ColumnOption {
   value: string;
 }
 
-export interface GroupByCustomization {
-  name: string;
-  dataset: string | number | DatasetReference | null;
+interface PluginFilterGroupByCustomizeProps {
+  dataset?: string | number | DatasetReference | null;
   datasetInfo?: {
     label: string;
     value: number;
     table_name: string;
   };
-  column: string | string[] | null;
+  column?: string | string[] | null;
   description?: string;
   sortFilter?: boolean;
   sortAscending?: boolean;
   sortMetric?: string;
   hasDefaultValue?: boolean;
-  defaultValue?: string;
+  defaultValue?: string | string[] | null;
   isRequired?: boolean;
   selectFirst?: boolean;
-  defaultDataMask?: DataMask;
-  defaultValueQueriesData?: ColumnOption[] | null;
-  aggregation?: string;
   canSelectMultiple?: boolean;
-  controlValues?: {
-    enableEmptyFilter?: boolean;
-  };
+  aggregation?: string;
+  enableEmptyFilter?: boolean;
+  inputRef?: RefObject<HTMLInputElement>;
 }
 
-export interface FilterOption {
-  label: string;
-  value: string;
-}
+export type PluginFilterGroupByQueryFormData = QueryFormData &
+  PluginFilterStylesProps &
+  PluginFilterGroupByCustomizeProps;
 
-export interface ChartCustomizationItem {
-  id: string;
-  title?: string;
-  removed?: boolean;
-  dataset?: string | null;
-  description?: string;
-  removeTimerId?: number;
-  chartId?: number;
-  settings?: {
-    sortFilter: boolean;
-    hasDefaultValue: boolean;
-    isRequired: boolean;
-    selectFirstByDefault: boolean;
-  };
-  customization: GroupByCustomization;
+export interface ColumnData {
+  column_name: string;
+  verbose_name?: string | null;
+  dtype?: number;
 }
 
-export interface ChartCustomizationChangesType {
-  modified: string[];
-  deleted: string[];
-  reordered: string[];
-}
+export type PluginFilterGroupByProps = PluginFilterStylesProps & {
+  data: (ColumnOption | ColumnData)[];
+  filterState: FilterState;
+  formData: PluginFilterGroupByQueryFormData;
+  inputRef: RefObject<RefSelectProps>;
+} & PluginFilterHooks;
 
-export interface ChartCustomizationRemoval {
-  isPending: boolean;
-  timerId: number;
-}
+export const DEFAULT_FORM_DATA: PluginFilterGroupByCustomizeProps = {
+  dataset: null,
+  column: null,
+  sortFilter: false,
+  sortAscending: true,
+  canSelectMultiple: true,
+  defaultValue: null,
+};
diff --git a/superset-frontend/src/chartCustomizations/components/TimeColumn/TimeColumnFilterPlugin.tsx b/superset-frontend/src/chartCustomizations/components/TimeColumn/TimeColumnFilterPlugin.tsx
new file mode 100644
index 0000000000..6faf49f117
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeColumn/TimeColumnFilterPlugin.tsx
@@ -0,0 +1,127 @@
+/**
+ * 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 { t, tn } from '@apache-superset/core';
+import { ensureIsArray, ExtraFormData } from '@superset-ui/core';
+import { GenericDataType } from '@apache-superset/core/api/core';
+import { useEffect, useState } from 'react';
+import {
+  FormItem,
+  type FormItemProps,
+  Select,
+} from '@superset-ui/core/components';
+import { FilterPluginStyle, StatusMessage } from '../common';
+import { PluginFilterTimeColumnProps } from './types';
+
+export default function PluginFilterTimeColumn(
+  props: PluginFilterTimeColumnProps,
+) {
+  const {
+    data,
+    formData,
+    height,
+    width,
+    setDataMask,
+    setHoveredFilter,
+    unsetHoveredFilter,
+    setFocusedFilter,
+    unsetFocusedFilter,
+    setFilterActive,
+    filterState,
+    inputRef,
+  } = props;
+  const { defaultValue } = formData;
+
+  const [value, setValue] = useState<string[]>(defaultValue ?? []);
+
+  const handleChange = (value?: string[] | string | null) => {
+    const resultValue: string[] = ensureIsArray<string>(value);
+    setValue(resultValue);
+    const extraFormData: ExtraFormData = {};
+    if (resultValue.length) {
+      extraFormData.granularity_sqla = resultValue[0];
+    }
+
+    setDataMask({
+      extraFormData,
+      filterState: {
+        value: resultValue.length ? resultValue : null,
+      },
+    });
+  };
+
+  useEffect(() => {
+    handleChange(defaultValue ?? null);
+    // I think after Config Modal update some filter it re-creates default value for all other filters
+    // so we can process it like this `JSON.stringify` or start to use `Immer`
+  }, [JSON.stringify(defaultValue)]);
+
+  useEffect(() => {
+    handleChange(filterState.value ?? null);
+  }, [JSON.stringify(filterState.value)]);
+
+  const timeColumns = (data || []).filter(
+    row => row.dtype === GenericDataType.Temporal,
+  );
+
+  const placeholderText =
+    timeColumns.length === 0
+      ? t('No time columns')
+      : tn('%s option', '%s options', timeColumns.length, timeColumns.length);
+
+  const formItemData: FormItemProps = {};
+  if (filterState.validateMessage) {
+    formItemData.extra = (
+      <StatusMessage status={filterState.validateStatus}>
+        {filterState.validateMessage}
+      </StatusMessage>
+    );
+  }
+
+  const options = timeColumns.map(
+    (row: { column_name: string; verbose_name: string | null }) => {
+      const { column_name: columnName, verbose_name: verboseName } = row;
+      return {
+        label: verboseName ?? columnName,
+        value: columnName,
+      };
+    },
+  );
+
+  return (
+    <FilterPluginStyle height={height} width={width}>
+      <FormItem validateStatus={filterState.validateStatus} {...formItemData}>
+        <Select
+          name={formData.nativeFilterId}
+          allowClear
+          value={value}
+          placeholder={placeholderText}
+          // @ts-ignore
+          onChange={handleChange}
+          onBlur={unsetFocusedFilter}
+          onFocus={setFocusedFilter}
+          onMouseEnter={setHoveredFilter}
+          onMouseLeave={unsetHoveredFilter}
+          ref={inputRef}
+          options={options}
+          onOpenChange={setFilterActive}
+        />
+      </FormItem>
+    </FilterPluginStyle>
+  );
+}
diff --git a/superset-frontend/src/chartCustomizations/components/TimeColumn/buildQuery.ts b/superset-frontend/src/chartCustomizations/components/TimeColumn/buildQuery.ts
new file mode 100644
index 0000000000..c271a69795
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeColumn/buildQuery.ts
@@ -0,0 +1,44 @@
+/**
+ * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core';
+
+/**
+ * The buildQuery function is used to create an instance of QueryContext that's
+ * sent to the chart data endpoint. In addition to containing information of which
+ * datasource to use, it specifies the type (e.g. full payload, samples, query) and
+ * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
+ * the datasource as opposed to using a cached copy of the data, if available.
+ *
+ * More importantly though, QueryContext contains a property `queries`, which is an array of
+ * QueryObjects specifying individual data requests to be made. A QueryObject specifies which
+ * columns, metrics and filters, among others, to use during the query. Usually it will be enough
+ * to specify just one query based on the baseQueryObject, but for some more advanced use cases
+ * it is possible to define post processing operations in the QueryObject, or multiple queries
+ * if a viz needs multiple different result sets.
+ */
+export default function buildQuery(formData: QueryFormData) {
+  return buildQueryContext(formData, () => [
+    {
+      result_type: 'columns',
+      columns: [],
+      metrics: [],
+      orderby: [],
+    },
+  ]);
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/chartCustomizations/components/TimeColumn/controlPanel.ts
similarity index 56%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
copy to superset-frontend/src/chartCustomizations/components/TimeColumn/controlPanel.ts
index ec0dc1353c..cef113e386 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
+++ b/superset-frontend/src/chartCustomizations/components/TimeColumn/controlPanel.ts
@@ -16,21 +16,32 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo } from 'react';
+import { ControlPanelConfig } from '@superset-ui/chart-controls';
 import { t } from '@apache-superset/core';
-import { getChartMetadataRegistry } from '@superset-ui/core';
-import { Row, RowLabel, RowValue } from './Styles';
-import { FilterCardRowProps } from './types';
 
-export const TypeRow = ({ filter }: FilterCardRowProps) => {
-  const metadata = useMemo(
-    () => getChartMetadataRegistry().get(filter.filterType),
-    [filter.filterType],
-  );
-  return (
-    <Row>
-      <RowLabel>{t('Filter type')}</RowLabel>
-      <RowValue>{metadata?.name}</RowValue>
-    </Row>
-  );
+const config: ControlPanelConfig = {
+  controlPanelSections: [
+    {
+      label: t('UI Configuration'),
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'enableEmptyFilter',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Filter value is required'),
+              default: false,
+              renderTrigger: true,
+              description: t(
+                'User must select a value before applying the filter',
+              ),
+            },
+          },
+        ],
+      ],
+    },
+  ],
 };
+
+export default config;
diff --git a/superset-frontend/src/chartCustomizations/components/TimeColumn/images/thumbnail.png b/superset-frontend/src/chartCustomizations/components/TimeColumn/images/thumbnail.png
new file mode 100644
index 0000000000..7afef30bd4
Binary files /dev/null and b/superset-frontend/src/chartCustomizations/components/TimeColumn/images/thumbnail.png differ
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/chartCustomizations/components/TimeColumn/index.ts
similarity index 52%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
copy to superset-frontend/src/chartCustomizations/components/TimeColumn/index.ts
index ec0dc1353c..6971be03d2 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
+++ b/superset-frontend/src/chartCustomizations/components/TimeColumn/index.ts
@@ -16,21 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo } from 'react';
 import { t } from '@apache-superset/core';
-import { getChartMetadataRegistry } from '@superset-ui/core';
-import { Row, RowLabel, RowValue } from './Styles';
-import { FilterCardRowProps } from './types';
+import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
+import thumbnail from './images/thumbnail.png';
 
-export const TypeRow = ({ filter }: FilterCardRowProps) => {
-  const metadata = useMemo(
-    () => getChartMetadataRegistry().get(filter.filterType),
-    [filter.filterType],
-  );
-  return (
-    <Row>
-      <RowLabel>{t('Filter type')}</RowLabel>
-      <RowValue>{metadata?.name}</RowValue>
-    </Row>
-  );
-};
+export default class ChartCustomizationTimeColumnPlugin extends ChartPlugin {
+  constructor() {
+    const metadata = new ChartMetadata({
+      name: t('Time column'),
+      description: t('Time column chart customization plugin'),
+      behaviors: [Behavior.InteractiveChart, Behavior.ChartCustomization],
+      tags: [t('Experimental')],
+      thumbnail,
+    });
+
+    super({
+      buildQuery,
+      controlPanel,
+      loadChart: () => import('./TimeColumnFilterPlugin'),
+      metadata,
+      transformProps,
+    });
+  }
+}
diff --git a/superset-frontend/src/chartCustomizations/components/TimeColumn/transformProps.ts b/superset-frontend/src/chartCustomizations/components/TimeColumn/transformProps.ts
new file mode 100644
index 0000000000..1f50260273
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeColumn/transformProps.ts
@@ -0,0 +1,60 @@
+/**
+ * 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 { ChartProps } from '@superset-ui/core';
+import { noOp } from 'src/utils/common';
+import { DEFAULT_FORM_DATA } from './types';
+
+export default function transformProps(chartProps: ChartProps) {
+  const {
+    behaviors,
+    formData,
+    height,
+    hooks,
+    queriesData,
+    width,
+    filterState,
+    inputRef,
+  } = chartProps;
+  const {
+    setDataMask = noOp,
+    setHoveredFilter = noOp,
+    unsetHoveredFilter = noOp,
+    setFocusedFilter = noOp,
+    unsetFocusedFilter = noOp,
+    setFilterActive = noOp,
+  } = hooks;
+
+  const { data } = queriesData[0];
+
+  return {
+    filterState,
+    behaviors,
+    width,
+    height,
+    data,
+    formData: { ...DEFAULT_FORM_DATA, ...formData },
+    setDataMask,
+    setHoveredFilter,
+    unsetHoveredFilter,
+    setFocusedFilter,
+    unsetFocusedFilter,
+    setFilterActive,
+    inputRef,
+  };
+}
diff --git a/superset-frontend/src/chartCustomizations/components/TimeColumn/types.ts b/superset-frontend/src/chartCustomizations/components/TimeColumn/types.ts
new file mode 100644
index 0000000000..9767c53f12
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeColumn/types.ts
@@ -0,0 +1,48 @@
+/**
+ * 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 {
+  Behavior,
+  DataRecord,
+  FilterState,
+  QueryFormData,
+} from '@superset-ui/core';
+import { RefObject } from 'react';
+import type { RefSelectProps } from '@superset-ui/core/components';
+import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
+
+interface PluginFilterTimeColumnCustomizeProps {
+  defaultValue?: string[] | null;
+  inputRef?: RefObject<HTMLInputElement>;
+}
+
+export type PluginFilterTimeColumnQueryFormData = QueryFormData &
+  PluginFilterStylesProps &
+  PluginFilterTimeColumnCustomizeProps;
+
+export type PluginFilterTimeColumnProps = PluginFilterStylesProps & {
+  behaviors: Behavior[];
+  data: DataRecord[];
+  filterState: FilterState;
+  formData: PluginFilterTimeColumnQueryFormData;
+  inputRef: RefObject<RefSelectProps>;
+} & PluginFilterHooks;
+
+export const DEFAULT_FORM_DATA: PluginFilterTimeColumnCustomizeProps = {
+  defaultValue: null,
+};
diff --git a/superset-frontend/src/chartCustomizations/components/TimeGrain/TimeGrainFilterPlugin.tsx b/superset-frontend/src/chartCustomizations/components/TimeGrain/TimeGrainFilterPlugin.tsx
new file mode 100644
index 0000000000..4fdd0a33aa
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeGrain/TimeGrainFilterPlugin.tsx
@@ -0,0 +1,159 @@
+/**
+ * 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 { t, tn } from '@apache-superset/core';
+import {
+  ensureIsArray,
+  ExtraFormData,
+  TimeGranularity,
+} from '@superset-ui/core';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+  FormItem,
+  type FormItemProps,
+  LabeledValue,
+  Select,
+  type SelectValue,
+} from '@superset-ui/core/components';
+import { propertyComparator } from '@superset-ui/core/components/Select/utils';
+import { FilterPluginStyle, StatusMessage } from '../common';
+import { PluginFilterTimeGrainProps } from './types';
+
+export default function PluginFilterTimegrain(
+  props: PluginFilterTimeGrainProps,
+) {
+  const {
+    data,
+    formData,
+    height,
+    width,
+    setDataMask,
+    setHoveredFilter,
+    unsetHoveredFilter,
+    setFocusedFilter,
+    unsetFocusedFilter,
+    setFilterActive,
+    filterState,
+    inputRef,
+  } = props;
+  const { defaultValue } = formData;
+
+  const [value, setValue] = useState<string[]>(defaultValue ?? []);
+  const durationMap = useMemo(
+    () =>
+      data.reduce(
+        (agg, { duration, name }: { duration: string; name: string }) => ({
+          ...agg,
+          [duration]: name,
+        }),
+        {} as { [key in string]: string },
+      ),
+    [JSON.stringify(data)],
+  );
+
+  const handleChange = (values: SelectValue) => {
+    const resultValue: string[] = ensureIsArray<string>(
+      values as string | string[] | null | undefined,
+    );
+    const [timeGrain] = resultValue;
+    const label = timeGrain ? durationMap[timeGrain] : undefined;
+
+    const extraFormData: ExtraFormData = {};
+    if (timeGrain) {
+      extraFormData.time_grain_sqla = timeGrain as TimeGranularity;
+    }
+    setValue(resultValue);
+    setDataMask({
+      extraFormData,
+      filterState: {
+        label,
+        value: resultValue.length ? resultValue : null,
+      },
+    });
+  };
+
+  useEffect(() => {
+    handleChange(defaultValue ?? []);
+    // Config Modal updates regenerate default values for filters
+    // Using JSON.stringify for deep comparison until Immer is adopted
+  }, [JSON.stringify(defaultValue)]);
+
+  useEffect(() => {
+    handleChange(filterState.value ?? []);
+  }, [JSON.stringify(filterState.value)]);
+
+  const placeholderText =
+    (data || []).length === 0
+      ? t('No data')
+      : tn('%s option', '%s options', data.length, data.length);
+
+  const formItemData: FormItemProps = {};
+  if (filterState.validateMessage) {
+    formItemData.extra = (
+      <StatusMessage status={filterState.validateStatus}>
+        {filterState.validateMessage}
+      </StatusMessage>
+    );
+  }
+
+  const options = (data || []).map(
+    (row: { name: string; duration: string }) => {
+      const { name, duration } = row;
+      return {
+        label: name,
+        value: duration,
+      };
+    },
+  );
+
+  const sortComparator = useCallback(
+    (a: LabeledValue, b: LabeledValue) => {
+      if (formData.sortAscending === undefined) {
+        return 0;
+      }
+      const labelComparator = propertyComparator('label');
+      if (formData.sortAscending) {
+        return labelComparator(a, b);
+      }
+      return labelComparator(b, a);
+    },
+    [formData.sortAscending],
+  );
+
+  return (
+    <FilterPluginStyle height={height} width={width}>
+      <FormItem validateStatus={filterState.validateStatus} {...formItemData}>
+        <div onMouseEnter={setHoveredFilter} onMouseLeave={unsetHoveredFilter}>
+          <Select
+            name={formData.nativeFilterId}
+            allowClear
+            value={value}
+            placeholder={placeholderText}
+            onChange={handleChange}
+            onBlur={unsetFocusedFilter}
+            onFocus={setFocusedFilter}
+            ref={inputRef}
+            options={options}
+            onOpenChange={setFilterActive}
+            sortComparator={sortComparator}
+          />
+        </div>
+      </FormItem>
+    </FilterPluginStyle>
+  );
+}
diff --git a/superset-frontend/src/chartCustomizations/components/TimeGrain/buildQuery.ts b/superset-frontend/src/chartCustomizations/components/TimeGrain/buildQuery.ts
new file mode 100644
index 0000000000..5a20aef2a8
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeGrain/buildQuery.ts
@@ -0,0 +1,44 @@
+/**
+ * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core';
+
+/**
+ * The buildQuery function is used to create an instance of QueryContext that's
+ * sent to the chart data endpoint. In addition to containing information of which
+ * datasource to use, it specifies the type (e.g. full payload, samples, query) and
+ * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
+ * the datasource as opposed to using a cached copy of the data, if available.
+ *
+ * More importantly though, QueryContext contains a property `queries`, which is an array of
+ * QueryObjects specifying individual data requests to be made. A QueryObject specifies which
+ * columns, metrics and filters, among others, to use during the query. Usually it will be enough
+ * to specify just one query based on the baseQueryObject, but for some more advanced use cases
+ * it is possible to define post processing operations in the QueryObject, or multiple queries
+ * if a viz needs multiple different result sets.
+ */
+export default function buildQuery(formData: QueryFormData) {
+  return buildQueryContext(formData, () => [
+    {
+      result_type: 'timegrains',
+      columns: [],
+      metrics: [],
+      orderby: [],
+    },
+  ]);
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/chartCustomizations/components/TimeGrain/controlPanel.ts
similarity index 56%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
copy to superset-frontend/src/chartCustomizations/components/TimeGrain/controlPanel.ts
index ec0dc1353c..49049f70ba 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
+++ b/superset-frontend/src/chartCustomizations/components/TimeGrain/controlPanel.ts
@@ -16,21 +16,32 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo } from 'react';
+import { ControlPanelConfig } from '@superset-ui/chart-controls';
 import { t } from '@apache-superset/core';
-import { getChartMetadataRegistry } from '@superset-ui/core';
-import { Row, RowLabel, RowValue } from './Styles';
-import { FilterCardRowProps } from './types';
 
-export const TypeRow = ({ filter }: FilterCardRowProps) => {
-  const metadata = useMemo(
-    () => getChartMetadataRegistry().get(filter.filterType),
-    [filter.filterType],
-  );
-  return (
-    <Row>
-      <RowLabel>{t('Filter type')}</RowLabel>
-      <RowValue>{metadata?.name}</RowValue>
-    </Row>
-  );
+const config: ControlPanelConfig = {
+  controlPanelSections: [
+    {
+      label: t('UI Configuration'),
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'enableEmptyFilter',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Customization value is required'),
+              default: false,
+              renderTrigger: true,
+              description: t(
+                'User must select a value before applying the customization',
+              ),
+            },
+          },
+        ],
+      ],
+    },
+  ],
 };
+
+export default config;
diff --git a/superset-frontend/src/chartCustomizations/components/TimeGrain/images/thumbnail.png b/superset-frontend/src/chartCustomizations/components/TimeGrain/images/thumbnail.png
new file mode 100644
index 0000000000..7afef30bd4
Binary files /dev/null and b/superset-frontend/src/chartCustomizations/components/TimeGrain/images/thumbnail.png differ
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx b/superset-frontend/src/chartCustomizations/components/TimeGrain/index.ts
similarity index 52%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
copy to superset-frontend/src/chartCustomizations/components/TimeGrain/index.ts
index ec0dc1353c..8c2c95632c 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/TypeRow.tsx
+++ b/superset-frontend/src/chartCustomizations/components/TimeGrain/index.ts
@@ -16,21 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useMemo } from 'react';
 import { t } from '@apache-superset/core';
-import { getChartMetadataRegistry } from '@superset-ui/core';
-import { Row, RowLabel, RowValue } from './Styles';
-import { FilterCardRowProps } from './types';
+import { Behavior, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
+import thumbnail from './images/thumbnail.png';
 
-export const TypeRow = ({ filter }: FilterCardRowProps) => {
-  const metadata = useMemo(
-    () => getChartMetadataRegistry().get(filter.filterType),
-    [filter.filterType],
-  );
-  return (
-    <Row>
-      <RowLabel>{t('Filter type')}</RowLabel>
-      <RowValue>{metadata?.name}</RowValue>
-    </Row>
-  );
-};
+export default class ChartCustomizationTimeGrainPlugin extends ChartPlugin {
+  constructor() {
+    const metadata = new ChartMetadata({
+      name: t('Time grain'),
+      description: t('Time grain chart customization plugin'),
+      behaviors: [Behavior.InteractiveChart, Behavior.ChartCustomization],
+      tags: [t('Experimental')],
+      thumbnail,
+    });
+
+    super({
+      buildQuery,
+      controlPanel,
+      loadChart: () => import('./TimeGrainFilterPlugin'),
+      metadata,
+      transformProps,
+    });
+  }
+}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/TimeGrain/transformProps.ts
similarity index 50%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/TimeGrain/transformProps.ts
index 10af257b26..b86d04c0c4 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/TimeGrain/transformProps.ts
@@ -16,28 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
+import { ChartProps } from '@superset-ui/core';
+import { noOp } from 'src/utils/common';
+import { DEFAULT_FORM_DATA } from './types';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
+export default function transformProps(chartProps: ChartProps) {
+  const { formData, height, hooks, queriesData, width, filterState, inputRef } =
+    chartProps;
+  const {
+    setDataMask = noOp,
+    setHoveredFilter = noOp,
+    unsetHoveredFilter = noOp,
+    setFocusedFilter = noOp,
+    unsetFocusedFilter = noOp,
+    setFilterActive = noOp,
+  } = hooks;
 
-export interface FilterCardRowProps {
-  filter: Filter;
-}
+  const { data } = queriesData[0];
 
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
+  return {
+    filterState,
+    width,
+    height,
+    data,
+    formData: { ...DEFAULT_FORM_DATA, ...formData },
+    setDataMask,
+    setHoveredFilter,
+    unsetHoveredFilter,
+    setFocusedFilter,
+    unsetFocusedFilter,
+    setFilterActive,
+    inputRef,
+  };
 }
diff --git a/superset-frontend/src/chartCustomizations/components/TimeGrain/types.ts b/superset-frontend/src/chartCustomizations/components/TimeGrain/types.ts
new file mode 100644
index 0000000000..611566f0d1
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/TimeGrain/types.ts
@@ -0,0 +1,42 @@
+/**
+ * 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 { FilterState, QueryFormData, DataRecord } from '@superset-ui/core';
+import { RefObject } from 'react';
+import type { RefSelectProps } from '@superset-ui/core/components';
+import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
+
+interface PluginFilterTimeGrainCustomizeProps {
+  defaultValue?: string[] | null;
+  inputRef?: RefObject<HTMLInputElement>;
+}
+
+export type PluginFilterTimeGrainQueryFormData = QueryFormData &
+  PluginFilterStylesProps &
+  PluginFilterTimeGrainCustomizeProps;
+
+export type PluginFilterTimeGrainProps = PluginFilterStylesProps & {
+  data: DataRecord[];
+  filterState: FilterState;
+  formData: PluginFilterTimeGrainQueryFormData;
+  inputRef: RefObject<RefSelectProps>;
+} & PluginFilterHooks;
+
+export const DEFAULT_FORM_DATA: PluginFilterTimeGrainCustomizeProps = {
+  defaultValue: null,
+};
diff --git a/superset-frontend/src/chartCustomizations/components/common.ts b/superset-frontend/src/chartCustomizations/components/common.ts
new file mode 100644
index 0000000000..38d98a5ae0
--- /dev/null
+++ b/superset-frontend/src/chartCustomizations/components/common.ts
@@ -0,0 +1,57 @@
+/**
+ * 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 { styled } from '@apache-superset/core/ui';
+import { FormItem } from '@superset-ui/core/components';
+import { PluginFilterStylesProps } from './types';
+
+export const RESPONSIVE_WIDTH = 0;
+
+export const FilterPluginStyle = styled.div<PluginFilterStylesProps>`
+  min-height: ${({ height }) => height}px;
+  width: ${({ width }) => (width === RESPONSIVE_WIDTH ? '100%' : `${width}px`)};
+`;
+
+export const StyledFormItem = styled(FormItem)`
+  &.ant-row.ant-form-item {
+    margin: 0;
+  }
+`;
+
+export const StatusMessage = styled.div<{
+  status?: 'error' | 'warning' | 'info' | 'help';
+  centerText?: boolean;
+}>`
+  color: ${({ theme, status = 'error' }) => {
+    if (status === 'help') {
+      return theme.colorTextSecondary;
+    }
+    switch (status) {
+      case 'error':
+        return theme.colorError;
+      case 'warning':
+        return theme.colorWarning;
+      case 'info':
+        return theme.colorInfo;
+      default:
+        return theme.colorError;
+    }
+  }};
+  text-align: ${({ centerText }) => (centerText ? 'center' : 'left')};
+  width: 100%;
+`;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/index.ts
similarity index 61%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/index.ts
index 10af257b26..be5ee7956f 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/index.ts
@@ -16,28 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
-
-export interface FilterCardRowProps {
-  filter: Filter;
-}
-
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
-}
+export { default as ChartCustomizationTimeColumnPlugin } from './TimeColumn';
+export { default as ChartCustomizationTimeGrainPlugin } from './TimeGrain';
+export { default as ChartCustomizationDynamicGroupBy } from './DynamicGroupBy';
+export { default as DeckglLayerVisibilityCustomizationPlugin } from './DeckglLayerVisibility';
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts b/superset-frontend/src/chartCustomizations/components/types.ts
similarity index 61%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
copy to superset-frontend/src/chartCustomizations/components/types.ts
index 10af257b26..2d9d5c9632 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterCard/types.ts
+++ b/superset-frontend/src/chartCustomizations/components/types.ts
@@ -16,28 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { ReactNode } from 'react';
-import { Filter } from '@superset-ui/core';
 
-export enum FilterCardPlacement {
-  Right = 'right',
-  Bottom = 'bottom',
-  Left = 'left',
-}
-
-export interface FilterCardProps {
-  children: ReactNode;
-  filter: Filter;
-  getPopupContainer?: (node: HTMLElement) => HTMLElement;
-  isVisible?: boolean;
-  placement: FilterCardPlacement;
-}
+import { SetDataMaskHook } from '@superset-ui/core';
+import { FilterBarOrientation } from 'src/dashboard/types';
 
-export interface FilterCardRowProps {
-  filter: Filter;
+export interface PluginFilterStylesProps {
+  height: number;
+  width: number;
+  orientation?: FilterBarOrientation;
+  overflow?: boolean;
 }
 
-export interface DependencyValueProps {
-  dependency: Filter;
-  hasSeparator?: boolean;
+export interface PluginFilterHooks {
+  setDataMask: SetDataMaskHook;
+  setFocusedFilter: () => void;
+  unsetFocusedFilter: () => void;
+  setHoveredFilter: () => void;
+  unsetHoveredFilter: () => void;
+  setFilterActive: (isActive: boolean) => void;
 }
diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts
index fddd72288a..efd4a516e4 100644
--- a/superset-frontend/src/constants.ts
+++ b/superset-frontend/src/constants.ts
@@ -190,6 +190,13 @@ export enum FilterPlugins {
   TimeGrain = 'filter_timegrain',
 }
 
+export enum ChartCustomizationPlugins {
+  DynamicGroupBy = 'chart_customization_dynamic_groupby',
+  TimeGrain = 'chart_customization_timegrain',
+  TimeColumn = 'chart_customization_timecolumn',
+  DeckglLayerVisibility = 'chart_customization_deckgl_layer_visibility',
+}
+
 export enum Actions {
   CREATE = 'create',
   UPDATE = 'update',
diff --git a/superset-frontend/src/dashboard/actions/chartCustomizationActions.ts b/superset-frontend/src/dashboard/actions/chartCustomizationActions.ts
index b81fe81825..672cd10420 100644
--- a/superset-frontend/src/dashboard/actions/chartCustomizationActions.ts
+++ b/superset-frontend/src/dashboard/actions/chartCustomizationActions.ts
@@ -19,77 +19,58 @@
 import { AnyAction } from 'redux';
 import { ThunkAction, ThunkDispatch } from 'redux-thunk';
 import { t } from '@apache-superset/core';
-import { makeApi, getClientErrorObject, DataMask } from '@superset-ui/core';
+import {
+  makeApi,
+  getClientErrorObject,
+  ChartCustomization,
+  ChartCustomizationDivider,
+  ColumnOption,
+  Filter,
+  Filters,
+} from '@superset-ui/core';
 import { addDangerToast } from 'src/components/MessageToasts/actions';
 import { DashboardInfo, RootState } from 'src/dashboard/types';
 import {
-  ChartCustomizationItem,
-  FilterOption,
-  ColumnOption,
-} from 'src/dashboard/components/nativeFilters/ChartCustomization/types';
-import { triggerQuery } from 'src/components/Chart/chartAction';
-import { removeDataMask, updateDataMask } from 'src/dataMask/actions';
-import { onSave } from './dashboardState';
+  removeDataMask,
+  setDataMaskForFilterChangesComplete,
+} from 'src/dataMask/actions';
+import { dashboardInfoChanged } from './dashboardInfo';
+import {
+  SET_NATIVE_FILTERS_CONFIG_COMPLETE,
+  SET_IN_SCOPE_STATUS_OF_FILTERS,
+} from './nativeFilters';
+import { SaveFilterChangesType } from '../components/nativeFilters/FiltersConfigModal/types';
 
-const createUpdateDashboardApi = (id: number) =>
+const createUpdateChartCustomizationsApi = (id: number) =>
   makeApi<
-    Partial<DashboardInfo>,
-    { result: Partial<DashboardInfo>; last_modified_time: number }
+    {
+      modified: (
+        | (ChartCustomization & { cascadeParentIds: string[] })
+        | ChartCustomizationDivider
+      )[];
+      deleted: string[];
+      reordered?: string[];
+    },
+    { result: (ChartCustomization | ChartCustomizationDivider)[] }
   >({
     method: 'PUT',
-    endpoint: `/api/v1/dashboard/${id}`,
+    endpoint: `/api/v1/dashboard/${id}/chart_customizations`,
   });
 
-export interface ChartCustomizationSavePayload {
-  id: string;
-  title?: string;
-  description?: string;
-  removed?: boolean;
-  chartId?: number;
-  customization: {
-    name: string;
-    dataset:
-      | string
-      | number
-      | {
-          value: string | number;
-          label?: string;
-          table_name?: string;
-          schema?: string;
-        }
-      | null;
-    datasetInfo?: {
-      label: string;
-      value: number;
-      table_name: string;
-    };
-    column: string | string[] | null;
-    description?: string;
-    sortFilter?: boolean;
-    sortAscending?: boolean;
-    sortMetric?: string;
-    hasDefaultValue?: boolean;
-    defaultValue?: string;
-    isRequired?: boolean;
-    selectFirst?: boolean;
-    defaultDataMask?: DataMask;
-    defaultValueQueriesData?: ColumnOption[] | null;
-    aggregation?: string;
-    canSelectMultiple?: boolean;
-  };
-}
-
 export const SAVE_CHART_CUSTOMIZATION_COMPLETE =
   'SAVE_CHART_CUSTOMIZATION_COMPLETE';
 
 export function setChartCustomization(
-  chartCustomization: ChartCustomizationItem[],
+  chartCustomization: ChartCustomization[],
 ) {
   return { type: SAVE_CHART_CUSTOMIZATION_COMPLETE, chartCustomization };
 }
 
 export function saveChartCustomization(
-  chartCustomizationItems: ChartCustomizationSavePayload[],
+  modifiedCustomizations: (ChartCustomization | ChartCustomizationDivider)[],
+  deletedIds: string[],
+  reorderedIds: string[] = [],
+  resetDataMask: boolean = false,
 ): ThunkAction<
   Promise<{ result: Partial<DashboardInfo>; last_modified_time: number }>,
   RootState,
@@ -100,119 +81,97 @@ export function saveChartCustomization(
     dispatch: ThunkDispatch<RootState, null, AnyAction>,
     getState: () => RootState,
   ) {
-    const { id, metadata, json_metadata } = getState().dashboardInfo;
-
-    const currentState = getState();
-    const currentChartCustomizationItems =
-      currentState.dashboardInfo.metadata?.chart_customization_config || [];
-
-    const existingItemsMap = new Map(
-      currentChartCustomizationItems.map(item => [item.id, item]),
-    );
-
-    const updatedItemsMap = new Map(existingItemsMap);
-
-    chartCustomizationItems.forEach(newItem => {
-      if (newItem.removed) {
-        updatedItemsMap.delete(newItem.id);
-      } else {
-        const chartCustomizationItem: ChartCustomizationItem = {
-          id: newItem.id,
-          title: newItem.title,
-          removed: newItem.removed,
-          chartId: newItem.chartId,
-          customization: newItem.customization,
-        };
-        updatedItemsMap.set(newItem.id, chartCustomizationItem);
+    const { id, metadata } = getState().dashboardInfo;
+
+    const modifiedItems = modifiedCustomizations.map(item => {
+      if ('cascadeParentIds' in item) {
+        return {
+          ...item,
+          cascadeParentIds: item.cascadeParentIds || [],
+        } as ChartCustomization & { cascadeParentIds: string[] };
       }
+      return item as ChartCustomizationDivider;
     });
 
-    const simpleItems = Array.from(updatedItemsMap.values());
-
-    dispatch(setChartCustomization(simpleItems));
-
-    const removedItems = currentChartCustomizationItems.filter(
-      existingItem => !updatedItemsMap.has(existingItem.id),
-    );
-
-    removedItems.forEach(removedItem => {
-      const customizationFilterId = `chart_customization_${removedItem.id}`;
-      dispatch(removeDataMask(customizationFilterId));
+    deletedIds.forEach((customizationId: string) => {
+      dispatch(removeDataMask(customizationId));
     });
 
-    simpleItems.forEach(item => {
-      const customizationFilterId = `chart_customization_${item.id}`;
-
-      if (item.customization?.column) {
-        const existingDataMask = getState().dataMask[customizationFilterId];
-
-        const existingFilterState = existingDataMask?.filterState;
-
-        dispatch(removeDataMask(customizationFilterId));
-
-        const dataMask = {
-          extraFormData: {},
-          filterState: {
-            value:
-              existingFilterState?.value ||
-              item.customization?.defaultDataMask?.filterState?.value ||
-              [],
-          },
-          ownState: {
-            column: item.customization.column,
-          },
-        };
-
-        dispatch(updateDataMask(customizationFilterId, dataMask));
-      } else {
-        dispatch(removeDataMask(customizationFilterId));
-      }
-    });
-
-    const updateDashboard = createUpdateDashboardApi(id);
+    const updateChartCustomizations = createUpdateChartCustomizationsApi(id);
 
     try {
-      let parsedMetadata: any = {};
-      try {
-        parsedMetadata = json_metadata ? JSON.parse(json_metadata) : metadata;
-      } catch (e) {
-        console.error('Error parsing json_metadata:', e);
-        parsedMetadata = metadata || {};
-      }
+      const response = await updateChartCustomizations({
+        modified: modifiedItems,
+        deleted: deletedIds,
+        reordered: reorderedIds,
+      });
 
-      const updatedMetadata = {
-        ...parsedMetadata,
-        native_filter_configuration: (
-          parsedMetadata.native_filter_configuration || []
-        ).filter(
-          (item: any) =>
-            !(
-              item.type === 'CHART_CUSTOMIZATION' &&
-              item.id === 'chart_customization_groupby'
-            ),
-        ),
-        chart_customization_config: simpleItems,
-      };
+      const currentMetadata = getState().dashboardInfo.metadata;
+      const currentConfig = currentMetadata?.chart_customization_config || [];
+
+      const mergedResult = response.result.map(
+        (item: ChartCustomization | ChartCustomizationDivider) => {
+          const existing = currentConfig.find(
+            (c: ChartCustomization | ChartCustomizationDivider) =>
+              c.id === item.id,
+          );
+          if (!existing) {
+            return item;
+          }
+          return {
+            ...item,
+            chartsInScope:
+              (item as ChartCustomization).chartsInScope ??
+              (existing as ChartCustomization).chartsInScope,
+            tabsInScope:
+              (item as ChartCustomization).tabsInScope ??
+              (existing as ChartCustomization).tabsInScope,
+            scope:
+              (item as ChartCustomization).scope ??
+              (existing as ChartCustomization).scope,
+          };
+        },
+      );
 
-      const response = await updateDashboard({
-        json_metadata: JSON.stringify(updatedMetadata),
+      dispatch({
+        type: SET_NATIVE_FILTERS_CONFIG_COMPLETE,
+        filterChanges: mergedResult,
       });
 
-      const lastModifiedTime = response.last_modified_time;
+      dispatch(
+        dashboardInfoChanged({
+          metadata: {
+            ...currentMetadata,
+            chart_customization_config: mergedResult,
+          },
+        }),
+      );
 
-      if (lastModifiedTime) {
-        dispatch(onSave(lastModifiedTime));
-      }
+      if (resetDataMask) {
+        const oldConfig = metadata?.chart_customization_config || [];
+        const oldCustomizationsById = oldConfig.reduce<
+          Record<string, ChartCustomization | ChartCustomizationDivider>
+        >((acc, customization) => {
+          acc[customization.id] = customization;
+          return acc;
+        }, {});
+
+        const customizationFilterChanges: SaveFilterChangesType = {
+          modified: modifiedCustomizations as unknown as Filter[],
+          deleted: deletedIds,
+          reordered: reorderedIds,
+        };
 
-      const { dashboardState } = getState();
-      const chartIds = dashboardState.sliceIds || [];
-      if (chartIds.length > 0) {
-        chartIds.forEach(chartId => {
-          dispatch(triggerQuery(true, chartId));
-        });
+        dispatch(
+          setDataMaskForFilterChangesComplete(
+            customizationFilterChanges,
+            oldCustomizationsById as unknown as Filters,
+            true,
+          ),
+        );
       }
 
-      return response;
+      return { result: {}, last_modified_time: Date.now() };
     } catch (errorObject) {
       const { error } = await getClientErrorObject(errorObject);
       dispatch(
@@ -223,45 +182,6 @@ export function saveChartCustomization(
   };
 }
 
-export const INITIALIZE_CHART_CUSTOMIZATION = 'INITIALIZE_CHART_CUSTOMIZATION';
-export interface InitializeChartCustomization {
-  type: typeof INITIALIZE_CHART_CUSTOMIZATION;
-  chartCustomizationItems: ChartCustomizationItem[];
-}
-
-export function initializeChartCustomization(
-  chartCustomizationItems: ChartCustomizationItem[],
-): ThunkAction<void, RootState, null, AnyAction> {
-  return (dispatch: ThunkDispatch<RootState, null, AnyAction>) => {
-    dispatch({
-      type: INITIALIZE_CHART_CUSTOMIZATION,
-      chartCustomizationItems,
-    });
-
-    chartCustomizationItems.forEach(item => {
-      const customizationFilterId = `chart_customization_${item.id}`;
-
-      if (item.customization?.column) {
-        dispatch(removeDataMask(customizationFilterId));
-
-        const dataMask = {
-          extraFormData: {},
-          filterState: {
-            value:
-              item.customization?.defaultDataMask?.filterState?.value || [],
-          },
-          ownState: {
-            column: item.customization.column,
-          },
-        };
-        dispatch(updateDataMask(customizationFilterId, dataMask));
-      } else {
-        dispatch(removeDataMask(customizationFilterId));
-      }
-    });
-  };
-}
-
 export const SET_CHART_CUSTOMIZATION_DATA_LOADING =
   'SET_CHART_CUSTOMIZATION_DATA_LOADING';
 export interface SetChartCustomizationDataLoading {
@@ -285,12 +205,12 @@ export const SET_CHART_CUSTOMIZATION_DATA = 'SET_CHART_CUSTOMIZATION_DATA';
 export interface SetChartCustomizationData {
   type: typeof SET_CHART_CUSTOMIZATION_DATA;
   itemId: string;
-  data: FilterOption[];
+  data: ColumnOption[];
 }
 
 export function setChartCustomizationData(
   itemId: string,
-  data: FilterOption[],
+  data: ColumnOption[],
 ): SetChartCustomizationData {
   return {
     type: SET_CHART_CUSTOMIZATION_DATA,
@@ -317,7 +237,7 @@ export function loadChartCustomizationData(
       return;
     }
 
-    dispatch(setChartCustomizationDataLoading(itemId, false));
+    dispatch(setChartCustomizationDataLoading(itemId, true));
   };
 }
 
@@ -325,11 +245,11 @@ export const SET_PENDING_CHART_CUSTOMIZATION =
   'SET_PENDING_CHART_CUSTOMIZATION';
 export interface SetPendingChartCustomization {
   type: typeof SET_PENDING_CHART_CUSTOMIZATION;
-  pendingCustomization: ChartCustomizationSavePayload;
+  pendingCustomization: ChartCustomization;
 }
 
 export function setPendingChartCustomization(
-  pendingCustomization: ChartCustomizationSavePayload,
+  pendingCustomization: ChartCustomization,
 ): SetPendingChartCustomization {
   return {
     type: SET_PENDING_CHART_CUSTOMIZATION,
@@ -380,9 +300,75 @@ export function clearAllChartCustomizationsFromMetadata() {
   return clearAllChartCustomizations();
 }
 
+export function setInScopeStatusOfCustomizations(
+  customizationScopes: {
+    customizationId: string;
+    chartsInScope: number[];
+    tabsInScope: string[];
+  }[],
+): ThunkAction<void, RootState, null, AnyAction> {
+  return (
+    dispatch: ThunkDispatch<RootState, null, AnyAction>,
+    getState: () => RootState,
+  ) => {
+    const { filters } = getState().nativeFilters;
+
+    const scopeConfig = customizationScopes
+      .map(({ customizationId, chartsInScope, tabsInScope }) => {
+        const existing = filters[customizationId];
+        if (!existing) return null;
+        return {
+          ...existing,
+          chartsInScope,
+          tabsInScope,
+        };
+      })
+      .filter(Boolean);
+
+    if (scopeConfig.length > 0) {
+      dispatch({
+        type: SET_IN_SCOPE_STATUS_OF_FILTERS,
+        filterConfig: scopeConfig,
+      });
+    }
+
+    const { metadata } = getState().dashboardInfo;
+    const customizationConfig = metadata?.chart_customization_config || [];
+
+    const scopeMap = new Map(
+      customizationScopes.map(
+        ({ customizationId, chartsInScope, tabsInScope }) => [
+          customizationId,
+          { chartsInScope, tabsInScope },
+        ],
+      ),
+    );
+
+    const updatedConfig = customizationConfig.map(customization => {
+      const scope = scopeMap.get(customization.id);
+      if (!scope) {
+        return customization;
+      }
+      return {
+        ...customization,
+        chartsInScope: scope.chartsInScope,
+        tabsInScope: scope.tabsInScope,
+      };
+    });
+
+    dispatch(
+      dashboardInfoChanged({
+        metadata: {
+          ...metadata,
+          chart_customization_config: updatedConfig,
+        },
+      }),
+    );
+  };
+}
+
 export type AnyChartCustomizationAction =
   | ReturnType<typeof setChartCustomization>
-  | InitializeChartCustomization
   | SetChartCustomizationDataLoading
   | SetChartCustomizationData
   | SetPendingChartCustomization
diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js
index b6f11988e1..0c545e2240 100644
--- a/superset-frontend/src/dashboard/actions/hydrate.js
+++ b/superset-frontend/src/dashboard/actions/hydrate.js
@@ -225,8 +225,12 @@ export const hydrateDashboard =
       directPathToChild.push(directLinkComponentId);
     }
 
+    const chartCustomizations = metadata?.chart_customization_config || [];
+    const filters = metadata?.native_filter_configuration || [];
+    const combinedFilters = [...filters, ...chartCustomizations];
+
     const nativeFilters = getInitialNativeFilterState({
-      filterConfig: metadata?.native_filter_configuration || [],
+      filterConfig: combinedFilters,
     });
 
     const { chartConfiguration, globalChartConfiguration } =
@@ -244,8 +248,6 @@ export const hydrateDashboard =
       metadata.cross_filters_enabled,
     );
 
-    const chartCustomizationItems = metadata?.chart_customization_config || [];
-
     return dispatch({
       type: HYDRATE_DASHBOARD,
       data: {
@@ -311,7 +313,6 @@ export const hydrateDashboard =
           datasetsStatus:
             dashboardState?.datasetsStatus || ResourceStatus.Loading,
           chartStates: chartStates || dashboardState?.chartStates || {},
-          chartCustomizationItems,
         },
         dashboardLayout,
       },
diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts
index f9d93aa226..4a628af3f4 100644
--- a/superset-frontend/src/dashboard/actions/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts
@@ -23,6 +23,7 @@ import {
   makeApi,
 } from '@superset-ui/core';
 import { Dispatch } from 'redux';
+import { RootState } from 'src/dashboard/types';
 import { cloneDeep } from 'lodash';
 import { setDataMaskForFilterChangesComplete } from 'src/dataMask/actions';
 import { HYDRATE_DASHBOARD } from './hydrate';
@@ -64,7 +65,7 @@ const isFilterChangesEmpty = (filterChanges: SaveFilterChangesType) =>
 
 export const setFilterConfiguration =
   (filterChanges: SaveFilterChangesType) =>
-  async (dispatch: Dispatch, getState: () => any) => {
+  async (dispatch: Dispatch, getState: () => RootState) => {
     if (isFilterChangesEmpty(filterChanges)) {
       return;
     }
@@ -77,20 +78,17 @@ export const setFilterConfiguration =
       filterChanges,
     });
 
-    const updateFilters = makeApi<
-      SaveFilterChangesType,
-      { result: SaveFilterChangesType }
-    >({
+    const updateFilters = makeApi<SaveFilterChangesType, { result: Filter[] }>({
       method: 'PUT',
       endpoint: `/api/v1/dashboard/${id}/filters`,
     });
     try {
       const response = await updateFilters(filterChanges);
-      dispatch(nativeFiltersConfigChanged(response.result));
       dispatch({
         type: SET_NATIVE_FILTERS_CONFIG_COMPLETE,
         filterChanges: response.result,
       });
+      dispatch(nativeFiltersConfigChanged(response.result));
       dispatch(setDataMaskForFilterChangesComplete(filterChanges, oldFilters));
     } catch (err) {
       dispatch({
@@ -108,7 +106,7 @@ export const setInScopeStatusOfFilters =
       tabsInScope: string[];
     }[],
   ) =>
-  async (dispatch: Dispatch, getState: () => any) => {
+  async (dispatch: Dispatch, getState: () => RootState) => {
     const filters = getState().nativeFilters?.filters;
     const filtersWithScopes = filterScopes.map(scope => ({
       ...filters[scope.filterId],
@@ -119,10 +117,9 @@ export const setInScopeStatusOfFilters =
       type: SET_IN_SCOPE_STATUS_OF_FILTERS,
       filterConfig: filtersWithScopes,
     });
-    // need to update native_filter_configuration in the dashboard metadata
     const metadata = cloneDeep(getState().dashboardInfo.metadata);
-    const filterConfig: FilterConfiguration =
-      metadata.native_filter_configuration || [];
+    const filterConfig =
+      (metadata.native_filter_configuration as FilterConfiguration) || [];
     const mergedFilterConfig = filterConfig.map(filter => {
       const filterWithScope = filtersWithScopes.find(
         scope => scope.id === filter.id,
@@ -130,7 +127,11 @@ export const setInScopeStatusOfFilters =
       if (!filterWithScope) {
         return filter;
       }
-      return { ...filterWithScope, ...filter };
+      return {
+        ...filter,
+        chartsInScope: filterWithScope.chartsInScope,
+        tabsInScope: filterWithScope.tabsInScope,
+      };
     });
     metadata.native_filter_configuration = mergedFilterConfig;
     dispatch(
diff --git a/superset-frontend/src/dashboard/components/GroupByBadge/index.tsx b/superset-frontend/src/dashboard/components/CustomizationsBadge/index.tsx
similarity index 55%
rename from superset-frontend/src/dashboard/components/GroupByBadge/index.tsx
rename to superset-frontend/src/dashboard/components/CustomizationsBadge/index.tsx
index ad44c17d64..6f7fc988b5 100644
--- a/superset-frontend/src/dashboard/components/GroupByBadge/index.tsx
+++ b/superset-frontend/src/dashboard/components/CustomizationsBadge/index.tsx
@@ -20,13 +20,27 @@ import { memo, useMemo, useState, useRef } from 'react';
 import { useSelector } from 'react-redux';
 import { createSelector } from '@reduxjs/toolkit';
 import { t } from '@apache-superset/core';
+import { ChartCustomization, DataMaskStateWithId } from '@superset-ui/core';
 import { styled, useTheme } from '@apache-superset/core/ui';
 import { Icons, Badge, Tooltip, Tag } from '@superset-ui/core/components';
 import { getFilterValueForDisplay } from '../nativeFilters/utils';
-import { ChartCustomizationItem } from '../nativeFilters/ChartCustomization/types';
+import { extractLabel } from '../nativeFilters/selectors';
+import { useChartCustomizationFromRedux } from '../nativeFilters/state';
 import { RootState } from '../../types';
 import { isChartWithoutGroupBy } from '../../util/charts/chartTypeLimitations';
 
+const getCustomizationDataset = (
+  item: ChartCustomization | any,
+): string | number | null => {
+  if (item.targets?.[0]?.datasetId !== undefined) {
+    return item.targets[0].datasetId;
+  }
+  if (item.customization?.dataset !== undefined) {
+    return item.customization.dataset;
+  }
+  return null;
+};
+
 const makeSelectChartDataset = (chartId: number) =>
   createSelector(
     (state: RootState) => state.charts[chartId]?.latestQueryFormData,
@@ -47,7 +61,7 @@ const makeSelectChartFormData = (chartId: number) =>
     latestQueryFormData => latestQueryFormData,
   );
 
-export interface GroupByBadgeProps {
+export interface CustomizationsBadgeProps {
   chartId: number;
 }
 
@@ -150,17 +164,15 @@ const GroupByValue = styled.span`
   overflow: auto;
 `;
 
-export const GroupByBadge = ({ chartId }: GroupByBadgeProps) => {
+export const CustomizationsBadge = ({ chartId }: CustomizationsBadgeProps) => {
   const [tooltipVisible, setTooltipVisible] = useState(false);
   const triggerRef = useRef<HTMLDivElement>(null);
   const theme = useTheme();
 
-  const chartCustomizationItems = useSelector<
-    RootState,
-    ChartCustomizationItem[]
-  >(
-    ({ dashboardInfo }) =>
-      dashboardInfo.metadata?.chart_customization_config || [],
+  const chartCustomizationItems = useChartCustomizationFromRedux();
+
+  const dataMask = useSelector<RootState, DataMaskStateWithId>(
+    state => state.dataMask,
   );
 
   // Use memoized selectors for chart data
@@ -177,28 +189,37 @@ export const GroupByBadge = ({ chartId }: GroupByBadgeProps) => {
   const chartFormData = useSelector(selectChartFormData);
   const chartType = chartFormData?.viz_type;
 
-  const applicableGroupBys = useMemo(() => {
+  const applicableCustomizations = useMemo(() => {
     if (!chartDataset) {
       return [];
     }
 
     return chartCustomizationItems.filter(item => {
-      if (item.removed) return false;
+      if (item.removed) {
+        return false;
+      }
 
-      const targetDataset = item.customization?.dataset;
-      if (!targetDataset) return false;
+      if (item.chartsInScope && !item.chartsInScope.includes(chartId)) {
+        return false;
+      }
+
+      const targetDataset = getCustomizationDataset(item);
+      if (!targetDataset) {
+        return false;
+      }
 
       const targetDatasetId = String(targetDataset);
       const matchesDataset = chartDataset === targetDatasetId;
 
-      const hasColumn = item.customization?.column;
+      const filterState = dataMask[item.id]?.filterState;
+      const hasValue = extractLabel(filterState) !== null;
 
-      return matchesDataset && hasColumn;
+      return matchesDataset && hasValue;
     });
-  }, [chartCustomizationItems, chartDataset]);
+  }, [chartCustomizationItems, chartDataset, chartId, dataMask]);
 
-  const effectiveGroupBys = useMemo(() => {
-    if (!chartType || applicableGroupBys.length === 0) {
+  const effectiveCustomizations = useMemo(() => {
+    if (!chartType || applicableCustomizations.length === 0) {
       return [];
     }
 
@@ -206,129 +227,46 @@ export const GroupByBadge = ({ chartId }: GroupByBadgeProps) => {
       return [];
     }
 
-    if (!chartFormData) {
-      return applicableGroupBys;
-    }
-
-    const existingColumns = new Set<string>();
-
-    const extractColumnNames = (columns: unknown[]): void => {
-      if (Array.isArray(columns)) {
-        columns.forEach((col: unknown) => {
-          if (typeof col === 'string') {
-            existingColumns.add(col);
-          } else if (col && typeof col === 'object' && 'column_name' in col) {
-            existingColumns.add((col as { column_name: string }).column_name);
-          }
-        });
-      }
-    };
-
-    const existingGroupBy = Array.isArray(chartFormData.groupby)
-      ? chartFormData.groupby
-      : chartFormData.groupby
-        ? [chartFormData.groupby]
-        : [];
-    existingGroupBy.forEach((col: string) => existingColumns.add(col));
-
-    if (chartFormData.x_axis) {
-      existingColumns.add(chartFormData.x_axis);
-    }
-
-    const metrics = chartFormData.metrics || [];
-    metrics.forEach((metric: any) => {
-      if (typeof metric === 'string') {
-        existingColumns.add(metric);
-      } else if (metric && typeof metric === 'object' && 'column' in metric) {
-        const metricColumn = metric.column;
-        if (typeof metricColumn === 'string') {
-          existingColumns.add(metricColumn);
-        } else if (
-          metricColumn &&
-          typeof metricColumn === 'object' &&
-          'column_name' in metricColumn
-        ) {
-          existingColumns.add(metricColumn.column_name);
-        }
-      }
-    });
-
-    if (chartFormData.series) {
-      existingColumns.add(chartFormData.series);
-    }
-    if (chartFormData.entity) {
-      existingColumns.add(chartFormData.entity);
-    }
-    if (chartFormData.source) {
-      existingColumns.add(chartFormData.source);
-    }
-    if (chartFormData.target) {
-      existingColumns.add(chartFormData.target);
-    }
-
-    if (chartType === 'pivot_table_v2') {
-      extractColumnNames(chartFormData.groupbyColumns || []);
-    }
-
-    if (chartType === 'box_plot') {
-      extractColumnNames(chartFormData.columns || []);
-    }
-
-    return applicableGroupBys.filter(item => {
-      if (!item.customization?.column) return false;
-
-      let columnNames: string[] = [];
-      if (typeof item.customization.column === 'string') {
-        columnNames = [item.customization.column];
-      } else if (Array.isArray(item.customization.column)) {
-        columnNames = item.customization.column.filter(
-          col => typeof col === 'string' && col.trim() !== '',
-        );
-      } else if (
-        typeof item.customization.column === 'object' &&
-        item.customization.column !== null
-      ) {
-        const columnObj = item.customization.column as any;
-        const columnName =
-          columnObj.column_name || columnObj.name || String(columnObj);
-        if (columnName && columnName.trim() !== '') {
-          columnNames = [columnName];
-        }
-      }
-
-      return columnNames.length > 0;
+    return applicableCustomizations.filter(customization => {
+      const filterState = dataMask[customization.id]?.filterState;
+      const value = filterState?.value;
+      return value !== null && value !== undefined;
     });
-  }, [applicableGroupBys, chartType, chartFormData]);
+  }, [applicableCustomizations, chartType, dataMask]);
 
-  const groupByCount = effectiveGroupBys.length;
+  const customizationsCount = effectiveCustomizations.length;
 
-  if (groupByCount === 0) {
+  if (customizationsCount === 0) {
     return null;
   }
   const tooltipContent = (
     <TooltipContent>
       <div>
         <SectionName>
-          {t('Chart Customization (%d)', applicableGroupBys.length)}
+          {t('Chart Customization (%d)', effectiveCustomizations.length)}
         </SectionName>
         <GroupByInfo>
-          {effectiveGroupBys.map(groupBy => (
-            <GroupByItem key={groupBy.id}>
-              <div>
-                {groupBy.customization?.name &&
-                groupBy.customization?.column ? (
-                  <>
-                    <GroupByName>{groupBy.customization.name}: </GroupByName>
-                    <GroupByValue>
-                      {getFilterValueForDisplay(groupBy.customization.column)}
-                    </GroupByValue>
-                  </>
-                ) : (
-                  groupBy.customization?.name || t('None')
-                )}
-              </div>
-            </GroupByItem>
-          ))}
+          {effectiveCustomizations.map(customization => {
+            const filterState = dataMask[customization.id]?.filterState;
+            const displayValue = filterState?.label || filterState?.value;
+
+            return (
+              <GroupByItem key={customization.id}>
+                <div>
+                  {customization.name && displayValue ? (
+                    <>
+                      <GroupByName>{customization.name}: </GroupByName>
+                      <GroupByValue>
+                        {getFilterValueForDisplay(displayValue)}
+                      </GroupByValue>
+                    </>
+                  ) : (
+                    customization.name || t('None')
+                  )}
+                </div>
+              </GroupByItem>
+            );
+          })}
         </GroupByInfo>
       </div>
     </TooltipContent>
@@ -353,14 +291,14 @@ export const GroupByBadge = ({ chartId }: GroupByBadgeProps) => {
     >
       <StyledTag
         ref={triggerRef}
-        aria-label={t('Group by settings (%s)', groupByCount)}
+        aria-label={t('Chart customizations (%s)', customizationsCount)}
         role="button"
         tabIndex={0}
       >
         <Icons.GroupOutlined iconSize="m" />
         <StyledBadge
-          data-test="applied-groupby-count"
-          count={groupByCount}
+          data-test="applied-customizations-count"
+          count={customizationsCount}
           showZero={false}
         />
       </StyledTag>
@@ -368,4 +306,4 @@ export const GroupByBadge = ({ chartId }: GroupByBadgeProps) => {
   );
 };
 
-export default memo(GroupByBadge);
+export default memo(CustomizationsBadge);
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.test.tsx
index 126b25d345..886563472c 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.test.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.test.tsx
@@ -43,7 +43,40 @@ jest.mock('src/dashboard/containers/DashboardGrid', () => ({
   default: () => <div data-test="mock-dashboard-grid" />,
 }));
 
-function createTestState(overrides = {}) {
+const defaultTestFilter = {
+  id: 'FILTER-1',
+  name: 'Test Filter',
+  filterType: 'filter_select',
+  targets: [
+    {
+      datasetId: 1,
+      column: { name: 'country' },
+    },
+  ],
+  defaultDataMask: {
+    filterState: { value: null },
+  },
+  cascadeParentIds: [],
+  scope: {
+    rootPath: ['ROOT_ID'],
+    excluded: [],
+  },
+  controlValues: {},
+  type: NativeFilterType.NativeFilter,
+};
+
+function createTestState(overrides: Record<string, unknown> = {}) {
+  const nativeFilterConfig = (
+    overrides.dashboardInfo as
+      | { metadata?: { native_filter_configuration?: unknown[] } }
+      | undefined
+  )?.metadata?.native_filter_configuration ?? [defaultTestFilter];
+
+  const nativeFiltersMap: Record<string, unknown> = {};
+  (nativeFilterConfig as Array<{ id: string }>).forEach(filter => {
+    nativeFiltersMap[filter.id] = filter;
+  });
+
   return {
     ...mockState,
     dashboardState: {
@@ -66,31 +99,16 @@ function createTestState(overrides = {}) {
         },
       },
     },
-    nativeFilters: {
-      filters: {
-        'FILTER-1': {
-          id: 'FILTER-1',
-          name: 'Test Filter',
-          filterType: 'filter_select',
-          targets: [
-            {
-              datasetId: 1,
-              column: { name: 'country' },
-            },
-          ],
-          defaultDataMask: {
-            filterState: { value: null },
-          },
-          cascadeParentIds: [],
-          scope: {
-            rootPath: ['ROOT_ID'],
-            excluded: [],
-          },
-          controlValues: {},
-          type: NativeFilterType.NativeFilter,
-        },
+    dashboardInfo: {
+      ...mockState.dashboardInfo,
+      metadata: {
+        ...mockState.dashboardInfo.metadata,
+        native_filter_configuration: nativeFilterConfig,
       },
     },
+    nativeFilters: {
+      filters: nativeFiltersMap,
+    },
     ...overrides,
   };
 }
@@ -114,13 +132,17 @@ function setupWithStore(overrideState = {}) {
 }
 
 let setInScopeStatusMock: jest.SpyInstance;
+const originalSetInScopeStatus = nativeFiltersActions.setInScopeStatusOfFilters;
 
 beforeEach(() => {
   setInScopeStatusMock = jest.spyOn(
     nativeFiltersActions,
     'setInScopeStatusOfFilters',
   );
-  setInScopeStatusMock.mockReturnValue(jest.fn());
+  setInScopeStatusMock.mockImplementation(args => {
+    const thunk = originalSetInScopeStatus(args);
+    return thunk;
+  });
 });
 
 afterEach(() => {
@@ -144,35 +166,37 @@ test('calculates chartsInScope correctly for filters', async () => {
   );
 });
 
-test('recalculates chartsInScope when filter non-scope properties change', async () => {
+test('preserves chartsInScope when filter non-scope properties change', async () => {
   const { store } = setupWithStore();
 
   await waitFor(() => {
     expect(setInScopeStatusMock).toHaveBeenCalled();
   });
 
-  setInScopeStatusMock.mockClear();
+  const stateBeforeUpdate = store.getState();
+  const filterBeforeUpdate =
+    stateBeforeUpdate.nativeFilters.filters['FILTER-1'];
+
+  expect(filterBeforeUpdate.chartsInScope).toEqual([sliceId]);
 
-  // Bug scenario: Editing non-scope properties (e.g., "Sort filter values")
-  // triggers backend save, but response lacks chartsInScope.
-  // The fix ensures useEffect recalculates chartsInScope anyway.
-  const initialState = store.getState();
   store.dispatch({
     type: 'SET_NATIVE_FILTERS_CONFIG_COMPLETE',
     filterChanges: [
       {
-        ...initialState.nativeFilters.filters['FILTER-1'],
+        ...filterBeforeUpdate,
         controlValues: {
-          ...initialState.nativeFilters.filters['FILTER-1'].controlValues,
+          ...filterBeforeUpdate.controlValues,
           sortAscending: false,
         },
       },
     ],
   });
 
-  await waitFor(() => {
-    expect(setInScopeStatusMock).toHaveBeenCalled();
-  });
+  const stateAfterUpdate = store.getState();
+  const filterAfterUpdate = stateAfterUpdate.nativeFilters.filters['FILTER-1'];
+
+  expect(filterAfterUpdate.chartsInScope).toEqual([sliceId]);
+  expect(filterAfterUpdate.controlValues?.sortAscending).toBe(false);
 });
 
 test('handles multiple filters with different scopes', async () => {
@@ -180,8 +204,28 @@ test('handles multiple filters with different scopes', async () => {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const { CHART_ID: _removed, ...cleanLayout } = baseDashboardLayout;
 
+  const multipleFilters = [
+    {
+      id: 'FILTER-1',
+      name: 'Filter 1',
+      filterType: 'filter_select',
+      targets: [{ datasetId: 1, column: { name: 'country' } }],
+      scope: { rootPath: ['ROOT_ID'], excluded: [] },
+      controlValues: {},
+      type: NativeFilterType.NativeFilter,
+    },
+    {
+      id: 'FILTER-2',
+      name: 'Filter 2',
+      filterType: 'filter_select',
+      targets: [{ datasetId: 1, column: { name: 'region' } }],
+      scope: { rootPath: ['ROOT_ID'], excluded: [19] },
+      controlValues: {},
+      type: NativeFilterType.NativeFilter,
+    },
+  ];
+
   const stateWithMultipleFilters = {
-    ...mockState,
     dashboardState: {
       ...mockState.dashboardState,
       sliceIds: [18, 19],
@@ -204,26 +248,17 @@ test('handles multiple filters with different scopes', async () => {
         },
       },
     },
+    dashboardInfo: {
+      ...mockState.dashboardInfo,
+      metadata: {
+        ...mockState.dashboardInfo.metadata,
+        native_filter_configuration: multipleFilters,
+      },
+    },
     nativeFilters: {
       filters: {
-        'FILTER-1': {
-          id: 'FILTER-1',
-          name: 'Filter 1',
-          filterType: 'filter_select',
-          targets: [{ datasetId: 1, column: { name: 'country' } }],
-          scope: { rootPath: ['ROOT_ID'], excluded: [] },
-          controlValues: {},
-          type: NativeFilterType.NativeFilter,
-        },
-        'FILTER-2': {
-          id: 'FILTER-2',
-          name: 'Filter 2',
-          filterType: 'filter_select',
-          targets: [{ datasetId: 1, column: { name: 'region' } }],
-          scope: { rootPath: ['ROOT_ID'], excluded: [19] },
-          controlValues: {},
-          type: NativeFilterType.NativeFilter,
-        },
+        'FILTER-1': multipleFilters[0],
+        'FILTER-2': multipleFilters[1],
       },
     },
   };
@@ -250,20 +285,24 @@ test('handles multiple filters with different scopes', async () => {
 
 test('handles filters with no charts in scope', async () => {
   const stateWithExcludedFilter = createTestState({
-    nativeFilters: {
-      filters: {
-        'FILTER-1': {
-          id: 'FILTER-1',
-          name: 'Excluded Filter',
-          filterType: 'filter_select',
-          targets: [{ datasetId: 1, column: { name: 'country' } }],
-          scope: {
-            rootPath: ['ROOT_ID'],
-            excluded: [sliceId],
+    dashboardInfo: {
+      ...mockState.dashboardInfo,
+      metadata: {
+        ...mockState.dashboardInfo.metadata,
+        native_filter_configuration: [
+          {
+            id: 'FILTER-1',
+            name: 'Excluded Filter',
+            filterType: 'filter_select',
+            targets: [{ datasetId: 1, column: { name: 'country' } }],
+            scope: {
+              rootPath: ['ROOT_ID'],
+              excluded: [sliceId],
+            },
+            controlValues: {},
+            type: NativeFilterType.NativeFilter,
           },
-          controlValues: {},
-          type: NativeFilterType.NativeFilter,
-        },
+        ],
       },
     },
   });
@@ -286,8 +325,12 @@ test('handles filters with no charts in scope', async () => {
 
 test('does not dispatch when there are no filters', () => {
   const stateWithoutFilters = createTestState({
-    nativeFilters: {
-      filters: {},
+    dashboardInfo: {
+      ...mockState.dashboardInfo,
+      metadata: {
+        ...mockState.dashboardInfo.metadata,
+        native_filter_configuration: [],
+      },
     },
   });
 
@@ -301,8 +344,17 @@ test('calculates tabsInScope for filters with tab-scoped charts', async () => {
   // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const { CHART_ID: _removed, ...cleanLayout } = baseDashboardLayout;
 
+  const tabScopedFilter = {
+    id: 'FILTER-TAB-SCOPED',
+    name: 'Tab Scoped Filter',
+    filterType: 'filter_select',
+    targets: [{ datasetId: 1, column: { name: 'region' } }],
+    scope: { rootPath: ['ROOT_ID'], excluded: [22] },
+    controlValues: {},
+    type: NativeFilterType.NativeFilter,
+  };
+
   const stateWithTabs = {
-    ...mockState,
     dashboardState: {
       ...mockState.dashboardState,
       sliceIds: [20, 21, 22],
@@ -356,17 +408,16 @@ test('calculates tabsInScope for filters with tab-scoped charts', async () => {
         },
       },
     },
+    dashboardInfo: {
+      ...mockState.dashboardInfo,
+      metadata: {
+        ...mockState.dashboardInfo.metadata,
+        native_filter_configuration: [tabScopedFilter],
+      },
+    },
     nativeFilters: {
       filters: {
-        'FILTER-TAB-SCOPED': {
-          id: 'FILTER-TAB-SCOPED',
-          name: 'Tab Scoped Filter',
-          filterType: 'filter_select',
-          targets: [{ datasetId: 1, column: { name: 'region' } }],
-          scope: { rootPath: ['ROOT_ID'], excluded: [22] },
-          controlValues: {},
-          type: NativeFilterType.NativeFilter,
-        },
+        'FILTER-TAB-SCOPED': tabScopedFilter,
       },
     },
   };
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
index 95e4b120b9..d7f0be5274 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardContainer.tsx
@@ -27,16 +27,17 @@ import {
   useRef,
   useState,
 } from 'react';
-import { useDispatch, useSelector } from 'react-redux';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
 import { createSelector } from '@reduxjs/toolkit';
+import { isEqual } from 'lodash';
 import {
-  Filter,
-  Filters,
+  ChartCustomizationConfiguration,
+  ChartCustomizationType,
   LabelsColorMapSource,
+  NativeFilterType,
   getLabelsColorMap,
 } from '@superset-ui/core';
 import { ParentSize } from '@visx/responsive';
-import { pick } from 'lodash';
 import Tabs from '@superset-ui/core/components/Tabs';
 import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
 import {
@@ -49,9 +50,9 @@ import {
   DASHBOARD_GRID_ID,
   DASHBOARD_ROOT_DEPTH,
 } from 'src/dashboard/util/constants';
-import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
 import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
 import { setInScopeStatusOfFilters } from 'src/dashboard/actions/nativeFilters';
+import { setInScopeStatusOfCustomizations } from 'src/dashboard/actions/chartCustomizationActions';
 import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
 import {
   applyDashboardLabelsColorOnLoad,
@@ -60,16 +61,30 @@ import {
   ensureSyncedSharedLabelsColors,
   ensureSyncedLabelsColorMap,
 } from 'src/dashboard/actions/dashboardState';
-import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
 import { getColorNamespace, resetColors } from 'src/utils/colorScheme';
+import { calculateScopes } from 'src/dashboard/util/calculateScopes';
+import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
 import { NATIVE_FILTER_DIVIDER_PREFIX } from '../nativeFilters/FiltersConfigModal/utils';
-import { findTabsWithChartsInScope } from '../nativeFilters/utils';
+import { selectFilterConfiguration } from '../nativeFilters/state';
 import { getRootLevelTabsComponent } from './utils';
 
 type DashboardContainerProps = {
   topLevelTabs?: LayoutItem;
 };
 
+interface ScopeData {
+  chartsInScope: number[];
+  tabsInScope: string[];
+}
+
+interface FilterScopeData extends ScopeData {
+  filterId: string;
+}
+
+interface CustomizationScopeData extends ScopeData {
+  customizationId: string;
+}
+
 export const renderedChartIdsSelector: (state: RootState) => number[] =
   createSelector([(state: RootState) => state.charts], charts =>
     Object.values(charts)
@@ -80,32 +95,14 @@ export const renderedChartIdsSelector: (state: RootState) => number[] =
 const useRenderedChartIds = () => {
   const renderedChartIds = useSelector<RootState, number[]>(
     renderedChartIdsSelector,
+    shallowEqual,
   );
-  return useMemo(() => renderedChartIds, [JSON.stringify(renderedChartIds)]);
-};
-
-const useNativeFilterScopes = () => {
-  const nativeFilters = useSelector<RootState, Filters>(
-    state => state.nativeFilters?.filters,
-  );
-  return useMemo(
-    () =>
-      nativeFilters
-        ? Object.values(nativeFilters).map((filter: Filter) =>
-            pick(filter, ['id', 'scope', 'type']),
-          )
-        : [],
-    [nativeFilters],
-  );
+  return renderedChartIds;
 };
 
 const TOP_OF_PAGE_RANGE = 220;
 
 const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
-  const nativeFilterScopes = useNativeFilterScopes();
-  const nativeFilters = useSelector<RootState, Filters>(
-    state => state.nativeFilters?.filters,
-  );
   const dispatch = useDispatch();
 
   const dashboardLayout = useSelector<RootState, DashboardLayout>(
@@ -114,6 +111,14 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
   const dashboardInfo = useSelector<RootState, DashboardInfo>(
     state => state.dashboardInfo,
   );
+  const filterItems = useSelector(selectFilterConfiguration);
+  const chartCustomizations = useSelector<
+    RootState,
+    ChartCustomizationConfiguration
+  >(
+    state => state.dashboardInfo?.metadata?.chart_customization_config || [],
+    shallowEqual,
+  );
   const directPathToChild = useSelector<RootState, string[]>(
     state => state.dashboardState.directPathToChild,
   );
@@ -125,6 +130,8 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
     useState(false);
   const prevRenderedChartIds = useRef<number[]>([]);
   const prevTabIndexRef = useRef<number>();
+  const prevFilterScopesRef = useRef<FilterScopeData[]>([]);
+  const prevCustomizationScopesRef = useRef<CustomizationScopeData[]>([]);
   const tabIndex = useMemo(() => {
     const nextTabIndex = findTabIndexByComponentId({
       currentComponent: getRootLevelTabsComponent(dashboardLayout),
@@ -150,58 +157,57 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
     prevRenderedChartIds.current = [];
   }, [dashboardInfo?.metadata?.color_namespace, dispatch]);
 
+  const chartLayoutItems = useMemo(
+    () =>
+      Object.values(dashboardLayout).filter(item => item?.type === CHART_TYPE),
+    [dashboardLayout],
+  );
+
   useEffect(() => {
-    if (nativeFilterScopes.length === 0) {
+    if (filterItems.length === 0) {
       return;
     }
-    const scopes = nativeFilterScopes.map(filterScope => {
-      if (
-        filterScope.id.startsWith(NATIVE_FILTER_DIVIDER_PREFIX) ||
-        filterScope.id.startsWith('chart_customization_')
-      ) {
-        return {
-          filterId: filterScope.id,
-          tabsInScope: [],
-          chartsInScope: [],
-        };
-      }
-
-      const chartLayoutItems = Object.values(dashboardLayout).filter(
-        item => item?.type === CHART_TYPE,
-      );
 
-      if (!filterScope.scope || !Array.isArray(filterScope.scope.excluded)) {
-        return {
-          filterId: filterScope.id,
-          tabsInScope: [],
-          chartsInScope: [],
-        };
-      }
+    const scopes = calculateScopes(
+      filterItems,
+      chartIds,
+      chartLayoutItems,
+      item =>
+        item.id.startsWith(NATIVE_FILTER_DIVIDER_PREFIX) ||
+        item.type === NativeFilterType.Divider,
+    ).map(scope => ({
+      filterId: scope.id,
+      chartsInScope: scope.chartsInScope,
+      tabsInScope: scope.tabsInScope,
+    }));
+
+    if (!isEqual(scopes, prevFilterScopesRef.current)) {
+      prevFilterScopesRef.current = scopes;
+      dispatch(setInScopeStatusOfFilters(scopes));
+    }
+  }, [chartIds, filterItems, chartLayoutItems, dispatch]);
 
-      const chartsInScope: number[] = getChartIdsInFilterScope(
-        filterScope.scope,
-        chartIds,
-        chartLayoutItems,
-      );
+  useEffect(() => {
+    if (chartCustomizations.length === 0) {
+      return;
+    }
 
-      const tabsInScope = findTabsWithChartsInScope(
-        chartLayoutItems,
-        chartsInScope,
-      );
-      return {
-        filterId: filterScope.id,
-        tabsInScope: Array.from(tabsInScope),
-        chartsInScope,
-      };
-    });
-    dispatch(setInScopeStatusOfFilters(scopes));
-  }, [
-    chartIds,
-    JSON.stringify(nativeFilterScopes),
-    dashboardLayout,
-    dispatch,
-    JSON.stringify(nativeFilters),
-  ]);
+    const scopes = calculateScopes(
+      chartCustomizations,
+      chartIds,
+      chartLayoutItems,
+      item => item.type === ChartCustomizationType.Divider,
+    ).map(scope => ({
+      customizationId: scope.id,
+      chartsInScope: scope.chartsInScope,
+      tabsInScope: scope.tabsInScope,
+    }));
+
+    if (!isEqual(scopes, prevCustomizationScopesRef.current)) {
+      prevCustomizationScopesRef.current = scopes;
+      dispatch(setInScopeStatusOfCustomizations(scopes));
+    }
+  }, [chartIds, chartCustomizations, chartLayoutItems, dispatch]);
 
   const childIds: string[] = useMemo(
     () => (topLevelTabs ? topLevelTabs.children : [DASHBOARD_GRID_ID]),
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
index c7b7a56fdf..a6de9e390c 100644
--- a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
+++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts
@@ -26,6 +26,7 @@ import {
   useFilters,
   useNativeFiltersDataMask,
 } from '../nativeFilters/FilterBar/state';
+import { useChartCustomizationFromRedux } from '../nativeFilters/state';
 import { toggleNativeFiltersBar } from '../../actions/dashboardState';
 
 export const useNativeFilters = () => {
@@ -46,12 +47,22 @@ export const useNativeFilters = () => {
   const filters = useFilters();
   const filterValues = useMemo(() => Object.values(filters), [filters]);
   const expandFilters = getUrlParam(URL_PARAMS.expandFilters);
+  const chartCustomizations = useChartCustomizationFromRedux();
 
   const nativeFiltersEnabled =
-    showNativeFilters && (canEdit || (!canEdit && filterValues.length !== 0));
+    showNativeFilters &&
+    (canEdit ||
+      (!canEdit &&
+        (filterValues.length !== 0 || chartCustomizations.length !== 0)));
 
   const requiredFirstFilter = useMemo(
-    () => filterValues.filter(filter => filter.requiredFirst),
+    () =>
+      filterValues.filter(
+        filter =>
+          'requiredFirst' in filter &&
+          filter.requiredFirst === true &&
+          filter.filterType !== 'filter_time',
+      ),
     [filterValues],
   );
   const dataMask = useNativeFiltersDataMask();
@@ -82,13 +93,21 @@ export const useNativeFilters = () => {
       (isFeatureEnabled(FeatureFlag.FilterBarClosedByDefault) &&
         expandFilters === null) ||
       expandFilters === false ||
-      (filterValues.length === 0 && nativeFiltersEnabled)
+      (filterValues.length === 0 &&
+        chartCustomizations.length === 0 &&
+        nativeFiltersEnabled)
     ) {
       dispatch(toggleNativeFiltersBar(false));
     } else {
       dispatch(toggleNativeFiltersBar(true));
     }
-  }, [dispatch, filterValues.length, expandFilters, nativeFiltersEnabled]);
+  }, [
+    dispatch,
+    filterValues.length,
+    chartCustomizations.length,
+    expandFilters,
+    nativeFiltersEnabled,
+  ]);
 
   useEffect(() => {
     if (showDashboard) {
diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
index 4b98c0f140..4e76ec6fe6 100644
--- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
+++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx
@@ -34,7 +34,7 @@ import { useSelector } from 'react-redux';
 import SliceHeaderControls from 'src/dashboard/components/SliceHeaderControls';
 import { SliceHeaderControlsProps } from 'src/dashboard/components/SliceHeaderControls/types';
 import FiltersBadge from 'src/dashboard/components/FiltersBadge';
-import GroupByBadge from 'src/dashboard/components/GroupByBadge';
+import CustomizationsBadge from 'src/dashboard/components/CustomizationsBadge';
 import { RootState } from 'src/dashboard/types';
 import { getSliceHeaderTooltip } from 'src/dashboard/util/getSliceHeaderTooltip';
 import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
@@ -297,7 +297,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
                 </Tooltip>
               )}
               {!uiConfig.hideChartControls && (
-                <GroupByBadge chartId={slice.slice_id} />
+                <CustomizationsBadge chartId={slice.slice_id} />
               )}
 
               {!uiConfig.hideChartControls && (
diff --git a/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx b/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx
index 51655e057c..bed9965d2c 100644
--- a/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx
+++ b/superset-frontend/src/dashboard/components/SyncDashboardState/index.tsx
@@ -72,7 +72,10 @@ const selectDashboardContextForExplore = createSelector(
     const nativeFilters = Object.keys(filters).reduce<
       Record<string, Pick<Filter | Divider, 'chartsInScope'>>
     >((acc, key) => {
-      acc[key] = pick(filters[key], ['chartsInScope']);
+      const filter = filters[key];
+      if ('chartsInScope' in filter) {
+        acc[key] = pick(filter, ['chartsInScope']);
+      }
       return acc;
     }, {});
 
diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
index ebd07fbf17..9a429f5d22 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/Chart/Chart.jsx
@@ -70,6 +70,7 @@ import {
   getAppliedFilterValues,
 } from '../../../util/activeDashboardFilters';
 import getFormDataWithExtraFilters from '../../../util/charts/getFormDataWithExtraFilters';
+import { useChartCustomizationFromRedux } from '../../nativeFilters/state';
 import { PLACEHOLDER_DATASOURCE } from '../../../constants';
 
 const propTypes = {
@@ -360,10 +361,7 @@ const Chart = props => {
   const chartConfiguration = useSelector(
     state => state.dashboardInfo.metadata?.chart_configuration,
   );
-  const chartCustomizationItems = useSelector(
-    state =>
-      state.dashboardInfo.metadata?.chart_customization_config || EMPTY_ARRAY,
-  );
+  const chartCustomizationItems = useChartCustomizationFromRedux();
   const colorScheme = useSelector(state => state.dashboardState.colorScheme);
   const colorNamespace = useSelector(
     state => state.dashboardState.colorNamespace,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationForm.tsx
deleted file mode 100644
index 277ad9e6c7..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationForm.tsx
+++ /dev/null
@@ -1,1457 +0,0 @@
-/**
- * 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 {
-  FC,
-  useEffect,
-  useMemo,
-  useState,
-  useRef,
-  useCallback,
-  ReactNode,
-} from 'react';
-import { useSelector } from 'react-redux';
-import { t } from '@apache-superset/core';
-import { styled, css, useTheme } from '@apache-superset/core/ui';
-import { debounce } from 'lodash';
-import { DatasourcesState, ChartsState, RootState } from 'src/dashboard/types';
-import {
-  Constants,
-  FormItem,
-  Input,
-  Select,
-  Collapse,
-  InfoTooltip,
-  Loading,
-  Radio,
-  type SelectValue,
-  FormInstance,
-  Checkbox,
-  CheckboxChangeEvent,
-} from '@superset-ui/core/components';
-import { DatasetSelectLabel } from 'src/features/datasets/DatasetSelectLabel';
-import { CollapsibleControl } from '../FiltersConfigModal/FiltersConfigForm/CollapsibleControl';
-import DatasetSelect from '../FiltersConfigModal/FiltersConfigForm/DatasetSelect';
-import { mostUsedDataset } from '../FiltersConfigModal/FiltersConfigForm/utils';
-import { ChartCustomizationItem } from './types';
-import { selectChartCustomizationItems } from './selectors';
-
-const { TextArea } = Input;
-
-interface Metric {
-  metric_name: string;
-  verbose_name?: string;
-}
-
-interface DatasetDetails {
-  id: number;
-  table_name: string;
-  schema?: string;
-  database?: { database_name: string };
-}
-
-interface ApiError {
-  message?: string;
-  error?: string;
-}
-
-interface DatasetColumn {
-  column_name?: string;
-  name?: string;
-  verbose_name?: string;
-  filterable?: boolean;
-}
-
-interface DatasetData {
-  id: number;
-  table_name: string;
-  schema?: string;
-  database?: { database_name: string };
-  metrics?: Metric[];
-  columns?: DatasetColumn[];
-}
-
-interface CachedDataset {
-  data: DatasetData;
-  timestamp: number;
-}
-
-interface ColumnOption {
-  label: string;
-  value: string;
-}
-
-interface Props {
-  form: FormInstance<Record<string, unknown>>;
-  item: ChartCustomizationItem;
-  onUpdate: (updatedItem: ChartCustomizationItem) => void;
-  removedItems: Record<string, { isPending: boolean; timerId?: number } | null>;
-  allItems?: ChartCustomizationItem[];
-}
-
-const datasetCache = new Map<number, CachedDataset>();
-
-const CACHE_TTL = 5 * 60 * 1000;
-
-function getCachedDataset(datasetId: number): DatasetData | null {
-  const cached = datasetCache.get(datasetId);
-  if (!cached) return null;
-
-  if (Date.now() - cached.timestamp > CACHE_TTL) {
-    datasetCache.delete(datasetId);
-    return null;
-  }
-
-  return cached.data;
-}
-
-function setCachedDataset(datasetId: number, data: DatasetData): void {
-  datasetCache.set(datasetId, {
-    data,
-    timestamp: Date.now(),
-  });
-}
-
-const StyledContainer = styled.div`
-  ${({ theme }) => `
-   display: flex;
-   flex-direction: row;
-   gap: ${theme.sizeUnit * 4}px;
-   padding: ${theme.sizeUnit * 2}px;
- `}
-`;
-
-const FORM_ITEM_WIDTH = 300;
-
-const StyledFormItem = styled(FormItem)`
-  ${({ theme }) => `
-    width: ${FORM_ITEM_WIDTH}px;
-    margin-bottom: ${theme.sizeUnit * 4}px;
-  `}
-`;
-
-const CheckboxLabel = styled.span`
-  ${({ theme }) => `
-    font-size: ${theme.fontSizeSM}px;
-    color: ${theme.colorTextSecondary};
-  `}
-`;
-
-const StyledRadioGroup = styled(Radio.Group)`
-  .ant-radio-wrapper {
-    font-size: ${({ theme }) => theme.fontSizeSM}px;
-  }
-`;
-
-const StyledMarginTop = styled.div`
-  margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
-`;
-
-const ChartCustomizationForm: FC<Props> = ({
-  form,
-  item,
-  onUpdate,
-  removedItems,
-  allItems,
-}) => {
-  const theme = useTheme();
-  const customization = useMemo(
-    () => item.customization || {},
-    [item.customization],
-  );
-
-  const isRemoved = !!removedItems[item.id];
-
-  const loadedDatasets = useSelector<RootState, DatasourcesState>(
-    ({ datasources }) => datasources,
-  );
-  const charts = useSelector<RootState, ChartsState>(({ charts }) => charts);
-  const globalChartCustomizationItems = useSelector(
-    selectChartCustomizationItems,
-  );
-
-  const chartCustomizationItems = allItems || globalChartCustomizationItems;
-
-  const [metrics, setMetrics] = useState<Metric[]>([]);
-  const [isDefaultValueLoading, setIsDefaultValueLoading] = useState(false);
-  const [error, setError] = useState<ApiError | null>(null);
-  const [datasetDetails, setDatasetDetails] = useState<DatasetDetails | null>(
-    null,
-  );
-  const [hasDefaultValue, setHasDefaultValue] = useState(
-    customization.hasDefaultValue ?? false,
-  );
-  const [isRequired, setIsRequired] = useState(
-    customization.isRequired ?? false,
-  );
-  const [selectFirst, setSelectFirst] = useState(
-    customization.selectFirst ?? false,
-  );
-
-  const [canSelectMultiple, setCanSelectMultiple] = useState(
-    customization.canSelectMultiple ?? true,
-  );
-
-  const fetchedRef = useRef({
-    dataset: null,
-    column: null,
-    hasDefaultValue: false,
-    defaultValueDataFetched: false,
-  });
-
-  const getDatasetId = useCallback(
-    (
-      dataset:
-        | string
-        | number
-        | { value: string | number }
-        | { id: string | number }
-        | null,
-    ): number | null => {
-      if (!dataset) return null;
-
-      if (typeof dataset === 'number') return dataset;
-      if (typeof dataset === 'string') {
-        const id = Number(dataset);
-        return Number.isNaN(id) ? null : id;
-      }
-      if (
-        typeof dataset === 'object' &&
-        dataset !== null &&
-        'value' in dataset
-      ) {
-        const id = Number(dataset.value);
-        return Number.isNaN(id) ? null : id;
-      }
-      if (typeof dataset === 'object' && dataset !== null && 'id' in dataset) {
-        const id = Number(dataset.id);
-        return Number.isNaN(id) ? null : id;
-      }
-
-      return null;
-    },
-    [],
-  );
-
-  const getFormValues = useCallback(
-    () => form.getFieldValue('filters')?.[item.id] || {},
-    [form, item.id],
-  );
-
-  const excludeDatasetIds = useMemo(() => {
-    const usedIds: number[] = [];
-
-    chartCustomizationItems.forEach(customItem => {
-      if (customItem.id === item.id || customItem.removed) {
-        return;
-      }
-
-      const { dataset } = customItem.customization;
-      const datasetId = getDatasetId(dataset);
-      if (datasetId !== null) {
-        usedIds.push(datasetId);
-      }
-    });
-
-    return usedIds;
-  }, [chartCustomizationItems, item.id, getDatasetId]);
-
-  const datasetValue = useMemo(() => {
-    const datasetId = getDatasetId(customization.dataset);
-
-    if (!datasetId) {
-      return null;
-    }
-
-    const loadedDataset = Object.values(loadedDatasets).find(
-      dataset => dataset.id === Number(datasetId),
-    );
-
-    if (loadedDataset) {
-      return {
-        value: datasetId,
-        label: DatasetSelectLabel({
-          id: Number(datasetId),
-          table_name: loadedDataset.table_name || '',
-          schema: loadedDataset.schema || '',
-          database: {
-            database_name:
-              (loadedDataset.database?.database_name as string) ||
-              (loadedDataset.database?.name as string) ||
-              '',
-          },
-        }),
-        table_name: loadedDataset.table_name,
-        schema: loadedDataset.schema,
-      };
-    }
-
-    if (datasetDetails && datasetDetails.id === datasetId) {
-      return {
-        value: datasetId,
-        label: DatasetSelectLabel({
-          id: Number(datasetId),
-          table_name: datasetDetails.table_name || '',
-          schema: datasetDetails.schema || '',
-          database: {
-            database_name:
-              (datasetDetails.database?.database_name as string) || '',
-          },
-        }),
-        table_name: datasetDetails.table_name,
-        schema: datasetDetails.schema,
-      };
-    }
-
-    if (customization.datasetInfo) {
-      const datasetInfo = customization.datasetInfo as {
-        value: number;
-        label: string;
-        table_name: string;
-        schema?: string;
-      };
-      return {
-        value: datasetId,
-        label: datasetInfo.label,
-        table_name: datasetInfo.table_name,
-        schema: datasetInfo.schema,
-      };
-    }
-
-    return {
-      value: datasetId,
-      label: `Dataset ${datasetId}`,
-    };
-  }, [
-    customization.dataset,
-    customization.datasetInfo,
-    datasetDetails,
-    loadedDatasets,
-    getDatasetId,
-  ]);
-
-  const formChanged = useCallback(() => {
-    form.setFields([{ name: 'changed', value: true }]);
-
-    const formValues = form.getFieldValue('filters')?.[item.id] || {};
-    onUpdate({
-      ...item,
-      customization: {
-        ...customization,
-        ...formValues,
-      },
-    });
-  }, [form, item, customization, onUpdate]);
-
-  const debouncedFormChanged = useMemo(
-    () => debounce(formChanged, Constants.SLOW_DEBOUNCE),
-    [formChanged],
-  );
-
-  const setFormFieldValues = useCallback(
-    (values: object) => {
-      const currentFilters = form.getFieldValue('filters') || {};
-      form.setFieldsValue({
-        filters: {
-          ...currentFilters,
-          [item.id]: {
-            ...currentFilters[item.id],
-            ...values,
-          },
-        },
-      });
-    },
-    [form, item.id],
-  );
-
-  const setChartCustomizationFieldValues = useCallback(
-    (itemId: string, values: Record<string, unknown>) => {
-      const currentFilters = form.getFieldValue('filters') || {};
-      const currentItem = currentFilters[itemId] || {};
-
-      form.setFieldsValue({
-        filters: {
-          ...currentFilters,
-          [itemId]: {
-            ...currentItem,
-            ...values,
-          },
-        },
-      });
-    },
-    [form],
-  );
-
-  const ensureFilterSlot = useCallback(() => {
-    const currentFilters = form.getFieldValue('filters') || {};
-    if (!currentFilters[item.id]) {
-      form.setFieldsValue({
-        filters: {
-          ...currentFilters,
-          [item.id]: {},
-        },
-      });
-    }
-  }, [form, item.id]);
-
-  const fetchDatasetInfo = useCallback(async () => {
-    const formValues = getFormValues();
-    const dataset = formValues.dataset || customization.dataset;
-
-    if (!dataset) {
-      setMetrics([]);
-      return;
-    }
-
-    try {
-      const datasetId = getDatasetId(dataset);
-      if (datasetId === null) return;
-
-      const cachedData = getCachedDataset(datasetId);
-      if (cachedData) {
-        const datasetDetails = {
-          id: cachedData.id,
-          table_name: cachedData.table_name,
-          schema: cachedData.schema,
-          database: cachedData.database,
-        };
-
-        setDatasetDetails(datasetDetails);
-
-        const currentFilters = form.getFieldValue('filters') || {};
-        const currentItemValues = currentFilters[item.id] || {};
-
-        if (
-          currentItemValues.dataset &&
-          typeof currentItemValues.dataset === 'string'
-        ) {
-          const enhancedDataset = {
-            value: Number(currentItemValues.dataset),
-            label: cachedData.table_name,
-            table_name: cachedData.table_name,
-            schema: cachedData.schema,
-          };
-
-          form.setFieldsValue({
-            filters: {
-              ...currentFilters,
-              [item.id]: {
-                ...currentItemValues,
-                dataset: currentItemValues.dataset,
-                datasetInfo: enhancedDataset,
-                ...currentItemValues,
-              },
-            },
-          });
-        }
-
-        if (cachedData.metrics && cachedData.metrics.length > 0) {
-          setMetrics(cachedData.metrics);
-        } else {
-          setMetrics([]);
-        }
-        return;
-      }
-
-      const response = await fetch(`/api/v1/dataset/${datasetId}`);
-      const data = await response.json();
-
-      if (data?.result) {
-        setCachedDataset(datasetId, {
-          ...data.result,
-          metrics: data.result.metrics || [],
-          columns: data.result.columns || [],
-        });
-
-        const datasetDetails = {
-          id: data.result.id,
-          table_name: data.result.table_name,
-          schema: data.result.schema,
-          database: data.result.database,
-        };
-
-        setDatasetDetails(datasetDetails);
-
-        const currentFilters = form.getFieldValue('filters') || {};
-        const currentItemValues = currentFilters[item.id] || {};
-
-        if (
-          currentItemValues.dataset &&
-          typeof currentItemValues.dataset === 'string'
-        ) {
-          const enhancedDataset = {
-            value: Number(currentItemValues.dataset),
-            label: data.result.table_name,
-            table_name: data.result.table_name,
-            schema: data.result.schema,
-          };
-
-          form.setFieldsValue({
-            filters: {
-              ...currentFilters,
-              [item.id]: {
-                ...currentItemValues,
-                dataset: currentItemValues.dataset,
-                datasetInfo: enhancedDataset,
-                ...currentItemValues,
-              },
-            },
-          });
-        }
-
-        if (data.result.metrics && data.result.metrics.length > 0) {
-          setMetrics(data.result.metrics);
-        } else {
-          setMetrics([]);
-        }
-      }
-    } catch (error) {
-      console.error('Error fetching dataset info:', error);
-      setMetrics([]);
-    }
-  }, [form, item.id, customization.dataset, getDatasetId]);
-
-  useEffect(() => {
-    const formValues = form.getFieldValue('filters')?.[item.id] || {};
-    const dataset = formValues.dataset || customization.dataset;
-
-    if (dataset) {
-      const datasetId = getDatasetId(dataset);
-
-      if (datasetId !== null) {
-        fetchDatasetInfo();
-      }
-    }
-  }, [customization.dataset, fetchDatasetInfo, getDatasetId]);
-
-  const fetchDefaultValueData = useCallback(async () => {
-    const formValues = getFormValues();
-    const dataset = formValues.dataset || customization.dataset;
-
-    if (!dataset) {
-      return;
-    }
-
-    setIsDefaultValueLoading(true);
-    try {
-      const datasetId =
-        typeof dataset === 'object' && dataset !== null
-          ? dataset.value
-          : getDatasetId(dataset);
-      if (datasetId === null) {
-        throw new Error('Invalid dataset ID');
-      }
-
-      let data;
-      const cachedData = getCachedDataset(datasetId);
-      if (cachedData) {
-        data = { result: cachedData };
-      } else {
-        const response = await fetch(`/api/v1/dataset/${datasetId}`);
-        data = await response.json();
-        if (data?.result) {
-          setCachedDataset(datasetId, {
-            ...data.result,
-            metrics: data.result.metrics || [],
-            columns: data.result.columns || [],
-          });
-        }
-      }
-
-      if (!data?.result?.columns) {
-        throw new Error('No columns found in dataset');
-      }
-
-      const columns = data.result.columns
-        .filter((col: DatasetColumn) => col.filterable !== false)
-        .map((col: DatasetColumn) => ({
-          label: col.verbose_name || col.column_name || col.name,
-          value: col.column_name || col.name,
-        }));
-
-      ensureFilterSlot();
-      const currentFilters = form.getFieldValue('filters') || {};
-
-      const currentFormValues = getFormValues();
-      const selectFirstEnabled =
-        currentFormValues.selectFirst ?? customization.selectFirst ?? false;
-
-      let autoSelectedColumn = null;
-      if (selectFirstEnabled && columns.length > 0) {
-        autoSelectedColumn = columns[0].value;
-      }
-
-      form.setFieldsValue({
-        filters: {
-          ...currentFilters,
-          [item.id]: {
-            ...currentFilters[item.id],
-            defaultValueQueriesData: columns,
-            filterType: 'filter_select',
-            hasDefaultValue: true,
-            ...(autoSelectedColumn && { column: autoSelectedColumn }),
-            chartConfiguration: {
-              tooltip: {
-                appendToBody: true,
-                confine: true,
-              },
-            },
-          },
-        },
-      });
-
-      onUpdate({
-        ...item,
-        customization: {
-          ...customization,
-          defaultValueQueriesData: columns,
-          hasDefaultValue:
-            formValues.hasDefaultValue ?? customization.hasDefaultValue,
-          ...(autoSelectedColumn && { column: autoSelectedColumn }),
-        },
-      });
-
-      setError(null);
-    } catch (error) {
-      setError(error);
-
-      ensureFilterSlot();
-      const currentFilters = form.getFieldValue('filters') || {};
-
-      form.setFieldsValue({
-        filters: {
-          ...currentFilters,
-          [item.id]: {
-            ...currentFilters[item.id],
-            defaultValueQueriesData: null,
-            hasDefaultValue:
-              currentFilters[item.id]?.hasDefaultValue ??
-              customization.hasDefaultValue ??
-              false,
-          },
-        },
-      });
-    } finally {
-      setIsDefaultValueLoading(false);
-    }
-  }, [customization, ensureFilterSlot, form, item, onUpdate, getDatasetId]);
-
-  useEffect(() => {
-    ensureFilterSlot();
-
-    const defaultDataset = customization.dataset
-      ? String(getDatasetId(customization.dataset) || customization.dataset)
-      : null;
-
-    const initialValues = {
-      filters: {
-        [item.id]: {
-          name: customization.name || '',
-          description: customization.description || '',
-          dataset: defaultDataset,
-          column: customization.column || null,
-          filterType: 'filter_select',
-          sortFilter: customization.sortFilter || false,
-          sortAscending: customization.sortAscending !== false,
-          sortMetric: customization.sortMetric || null,
-          hasDefaultValue: customization.hasDefaultValue || false,
-          isRequired: customization.isRequired || false,
-          selectFirst: customization.selectFirst || false,
-          defaultValue: customization.defaultValue,
-          defaultDataMask: customization.defaultDataMask,
-          defaultValueQueriesData: customization.defaultValueQueriesData,
-        },
-      },
-    };
-
-    form.setFieldsValue(initialValues);
-
-    if (customization.dataset || defaultDataset) {
-      fetchDatasetInfo();
-    }
-
-    if (customization.isRequired) {
-      setTimeout(() => {
-        form
-          .validateFields([['filters', item.id, 'isRequired']])
-          .catch(() => {});
-      }, 0);
-    }
-  }, [
-    item.id,
-    fetchDatasetInfo,
-    customization,
-    form,
-    ensureFilterSlot,
-    loadedDatasets,
-    charts,
-    getDatasetId,
-  ]);
-
-  useEffect(() => {
-    const formValues = form.getFieldValue('filters')?.[item.id] || {};
-    const hasDataset = !!formValues.dataset;
-    const hasColumn = !!formValues.column;
-    const hasDefaultValue = !!formValues.hasDefaultValue;
-    const isRequired = !!formValues.controlValues?.enableEmptyFilter;
-
-    if (hasDataset && fetchedRef.current.dataset !== formValues.dataset) {
-      fetchDatasetInfo();
-    }
-
-    if (isRequired && (!hasDataset || !hasColumn)) {
-      setTimeout(() => {
-        form
-          .validateFields([
-            ['filters', item.id, 'controlValues', 'enableEmptyFilter'],
-          ])
-          .catch(() => {});
-      }, 0);
-    }
-
-    if (
-      hasDataset &&
-      hasColumn &&
-      hasDefaultValue &&
-      (fetchedRef.current.dataset !== formValues.dataset ||
-        fetchedRef.current.column !== formValues.column ||
-        !fetchedRef.current.defaultValueDataFetched)
-    ) {
-      fetchedRef.current = {
-        dataset: formValues.dataset,
-        column: formValues.column,
-        hasDefaultValue,
-        defaultValueDataFetched: true,
-      };
-
-      fetchDefaultValueData();
-    }
-  }, [form, item.id, fetchDatasetInfo, fetchDefaultValueData]);
-
-  useEffect(() => {
-    const formValues = form.getFieldValue('filters')?.[item.id] || {};
-    const selectFirst = formValues.selectFirst ?? customization.selectFirst;
-
-    if (selectFirst) {
-      setHasDefaultValue(false);
-    } else {
-      setHasDefaultValue(
-        formValues.hasDefaultValue ?? customization.hasDefaultValue ?? false,
-      );
-      if (formValues.isRequired !== undefined) {
-        setIsRequired(formValues.isRequired);
-      }
-    }
-
-    setSelectFirst(selectFirst);
-  }, [form, item.id, customization.selectFirst, customization.hasDefaultValue]);
-
-  const isRequiredValidator = useCallback(
-    async (_, enableEmptyFilter) => {
-      if (!enableEmptyFilter) {
-        return Promise.resolve();
-      }
-
-      const current = form.getFieldValue(['filters', item.id]) || {};
-      if (!current.dataset) {
-        return Promise.reject(
-          new Error(
-            t(
-              'Dataset must be selected when "Dynamic group by value is required" is enabled',
-            ),
-          ),
-        );
-      }
-
-      return Promise.resolve();
-    },
-    [form, item.id],
-  );
-
-  const getDefaultValueTooltip = useCallback(() => {
-    if (selectFirst) {
-      return t(
-        'Default value set automatically when "Select first filter value by default" is checked',
-      );
-    }
-    if (isRequired) {
-      return t(
-        'Default value must be set when "Dynamic group by value is required" is checked',
-      );
-    }
-    if (hasDefaultValue) {
-      return t(
-        'Default value must be set when "Dynamic group by has a default value" is checked',
-      );
-    }
-    return t('Set a default value for this filter');
-  }, [selectFirst, isRequired, hasDefaultValue]);
-
-  const hasAllRequiredFields = useCallback(() => {
-    const formValues = form.getFieldValue('filters')?.[item.id] || {};
-    const { name = '', dataset } = formValues;
-    const nameValue = name || customization.name || '';
-
-    const hasExplicitDataset =
-      dataset && typeof dataset === 'string' && dataset.trim() !== '';
-
-    return !!(nameValue.trim() && hasExplicitDataset);
-  }, [form, item.id, customization.name]);
-
-  const shouldShowDefaultValue = useCallback(() => {
-    const allFieldsFilled = hasAllRequiredFields();
-    const isRequiredFromForm = !!form.getFieldValue([
-      'filters',
-      item.id,
-      'controlValues',
-      'enableEmptyFilter',
-    ]);
-
-    if (isRequiredFromForm) {
-      return allFieldsFilled && !isDefaultValueLoading;
-    }
-
-    return hasDefaultValue && allFieldsFilled && !isDefaultValueLoading;
-  }, [
-    hasAllRequiredFields,
-    form,
-    item.id,
-    customization.dataset,
-    hasDefaultValue,
-    isDefaultValueLoading,
-  ]);
-
-  const handleIsRequiredChange = useCallback(
-    ({ target: { checked } }: CheckboxChangeEvent) => {
-      const currentFilters = form.getFieldValue('filters') || {};
-      const currentItem = currentFilters[item.id] || {};
-      const currentControlValues = currentItem.controlValues || {};
-
-      if (checked) {
-        const updatedValues = {
-          controlValues: {
-            ...currentControlValues,
-            enableEmptyFilter: checked,
-          },
-          hasDefaultValue: true,
-        };
-        setChartCustomizationFieldValues(item.id, updatedValues);
-        setHasDefaultValue(true);
-        fetchDefaultValueData();
-      } else {
-        const updatedValues = {
-          controlValues: {
-            ...currentControlValues,
-            enableEmptyFilter: checked,
-          },
-        };
-        setChartCustomizationFieldValues(item.id, updatedValues);
-      }
-
-      formChanged();
-    },
-    [
-      form,
-      item.id,
-      setChartCustomizationFieldValues,
-      formChanged,
-      fetchDefaultValueData,
-    ],
-  );
-
-  return (
-    <div>
-      <StyledContainer>
-        <StyledFormItem
-          name={['filters', item.id, 'name']}
-          label={t('Dynamic group by name')}
-          initialValue={customization.name || ''}
-          rules={[{ required: !isRemoved, message: t('Please enter a name') }]}
-        >
-          <Input
-            placeholder={t('Name your dynamic group by')}
-            onChange={debouncedFormChanged}
-          />
-        </StyledFormItem>
-
-        <StyledFormItem
-          name={['filters', item.id, 'dataset']}
-          label={
-            <>
-              {t('Dataset')}&nbsp;
-              <InfoTooltip
-                tooltip={t('Select the dataset this group by will use')}
-                placement="right"
-              />
-            </>
-          }
-          initialValue={datasetValue}
-          rules={[
-            { required: !isRemoved, message: t('Please select a dataset') },
-          ]}
-        >
-          <DatasetSelect
-            excludeDatasetIds={excludeDatasetIds}
-            value={
-              mostUsedDataset(loadedDatasets, charts)
-                ? {
-                    value: mostUsedDataset(loadedDatasets, charts),
-                    label: `Dataset ${mostUsedDataset(loadedDatasets, charts)}`,
-                  }
-                : undefined
-            }
-            onChange={(dataset: {
-              label: string | ReactNode;
-              value: number;
-            }) => {
-              const datasetId = dataset.value;
-
-              const fetchDatasetAndUpdate = async () => {
-                try {
-                  const cachedData = getCachedDataset(datasetId);
-                  let data;
-
-                  if (cachedData) {
-                    data = { result: cachedData };
-                  } else {
-                    const response = await fetch(
-                      `/api/v1/dataset/${datasetId}`,
-                    );
-                    data = await response.json();
-
-                    if (data?.result) {
-                      setCachedDataset(datasetId, {
-                        ...data.result,
-                        metrics: data.result.metrics || [],
-                        columns: data.result.columns || [],
-                      });
-                    }
-                  }
-
-                  if (data?.result) {
-                    const datasetWithInfo = {
-                      value: datasetId,
-                      label: DatasetSelectLabel({
-                        id: datasetId,
-                        table_name: data.result.table_name || '',
-                        schema: data.result.schema || '',
-                        database: {
-                          database_name:
-                            (data.result.database?.database_name as string) ||
-                            '',
-                        },
-                      }),
-                      table_name: data.result.table_name,
-                      schema: data.result.schema,
-                    };
-
-                    setFormFieldValues({
-                      dataset: datasetWithInfo,
-                      datasetInfo: datasetWithInfo,
-                      column: null,
-                      defaultValueQueriesData: null,
-                      defaultValue: undefined,
-                      defaultDataMask: undefined,
-                    });
-
-                    fetchDatasetInfo();
-                    formChanged();
-                  }
-                } catch (error) {
-                  console.error('Error fetching dataset info:', error);
-
-                  const datasetWithInfo = {
-                    value: datasetId,
-                    label: `Dataset ${datasetId}`,
-                    table_name: `Dataset ${datasetId}`,
-                  };
-
-                  setFormFieldValues({
-                    dataset: datasetWithInfo,
-                    datasetInfo: datasetWithInfo,
-                    column: null,
-                    defaultValueQueriesData: null,
-                    defaultValue: undefined,
-                    defaultDataMask: undefined,
-                  });
-
-                  form.setFields([
-                    {
-                      name: ['filters', item.id, 'dataset'],
-                      value: datasetWithInfo,
-                    },
-                    {
-                      name: ['filters', item.id, 'datasetInfo'],
-                      value: datasetWithInfo,
-                    },
-                  ]);
-
-                  fetchDatasetInfo();
-                  formChanged();
-                }
-              };
-
-              fetchDatasetAndUpdate();
-            }}
-          />
-        </StyledFormItem>
-      </StyledContainer>
-
-      <Collapse
-        defaultActiveKey={['settings']}
-        expandIconPosition="right"
-        modalMode
-      >
-        <Collapse.Panel header={t('Customization settings')} key="settings">
-          <StyledFormItem
-            name={['filters', item.id, 'description']}
-            label={t('Description')}
-            initialValue={customization.description || ''}
-            css={css`
-              width: 100% !important;
-            `}
-          >
-            <TextArea
-              placeholder={t(
-                'Add description that will be displayed when hovering over the label...',
-              )}
-              onChange={debouncedFormChanged}
-              autoSize={{ minRows: 2, maxRows: 3 }}
-              css={css`
-                resize: none;
-              `}
-            />
-          </StyledFormItem>
-
-          <StyledFormItem name={['filters', item.id, 'sortFilter']}>
-            <CollapsibleControl
-              checked={
-                form.getFieldValue('filters')?.[item.id]?.sortFilter ??
-                customization.sortFilter ??
-                false
-              }
-              initialValue={customization.sortFilter ?? false}
-              title={t('Sort filter values')}
-              onChange={checked => {
-                setFormFieldValues({
-                  sortFilter: checked,
-                  ...(checked === false
-                    ? {
-                        sortAscending: undefined,
-                        sortMetric: undefined,
-                      }
-                    : {}),
-                });
-                formChanged();
-              }}
-            >
-              <StyledFormItem
-                name={['filters', item.id, 'sortAscending']}
-                label={<CheckboxLabel>{t('Sort type')}</CheckboxLabel>}
-                initialValue={customization.sortAscending !== false}
-              >
-                <StyledRadioGroup
-                  options={[
-                    { label: t('Sort ascending'), value: true },
-                    { label: t('Sort descending'), value: false },
-                  ]}
-                  onChange={value => {
-                    setFormFieldValues({
-                      sortAscending: value.target.value,
-                    });
-                    formChanged();
-                  }}
-                />
-              </StyledFormItem>
-
-              {customization.sortFilter && metrics.length > 0 && (
-                <StyledFormItem
-                  name={['filters', item.id, 'sortMetric']}
-                  label={
-                    <>
-                      <CheckboxLabel>{t('Sort Metric')}</CheckboxLabel>&nbsp;
-                      <InfoTooltip
-                        placement="top"
-                        tooltip={t(
-                          'If a metric is specified, sorting will be done based on the metric value',
-                        )}
-                      />
-                    </>
-                  }
-                  initialValue={customization.sortMetric}
-                >
-                  <Select
-                    allowClear
-                    ariaLabel={t('Sort metric')}
-                    value={
-                      form.getFieldValue('filters')?.[item.id]?.sortMetric ??
-                      customization.sortMetric
-                    }
-                    options={metrics.map((metric: Metric) => ({
-                      value: metric.metric_name,
-                      label: metric.verbose_name ?? metric.metric_name,
-                    }))}
-                    onChange={(value: SelectValue) => {
-                      const stringValue =
-                        value !== null && value !== undefined
-                          ? String(value)
-                          : undefined;
-
-                      setFormFieldValues({
-                        sortMetric: stringValue,
-                      });
-                      formChanged();
-                    }}
-                  />
-                </StyledFormItem>
-              )}
-            </CollapsibleControl>
-          </StyledFormItem>
-
-          <StyledFormItem
-            name={['filters', item.id, 'defaultValueQueriesData']}
-            hidden
-            initialValue={customization.defaultValueQueriesData || null}
-          >
-            <Input />
-          </StyledFormItem>
-
-          <StyledFormItem name={['filters', item.id, 'hasDefaultValue']}>
-            <CollapsibleControl
-              checked={hasDefaultValue}
-              initialValue={customization.hasDefaultValue ?? false}
-              disabled={
-                !!form.getFieldValue([
-                  'filters',
-                  item.id,
-                  'controlValues',
-                  'enableEmptyFilter',
-                ]) || selectFirst
-              }
-              title={t('Dynamic group by has a default value')}
-              tooltip={getDefaultValueTooltip()}
-              onChange={checked => {
-                setHasDefaultValue(checked);
-                if (checked) {
-                  setSelectFirst(false);
-                }
-                setFormFieldValues({
-                  hasDefaultValue: checked,
-                  ...(checked ? { selectFirst: false } : {}),
-                  ...(checked === false
-                    ? {
-                        defaultDataMask: null,
-                        defaultValue: undefined,
-                        defaultValueQueriesData: null,
-                        column: null,
-                      }
-                    : {}),
-                });
-
-                if (checked) {
-                  fetchDefaultValueData();
-                  setTimeout(() => {
-                    form.validateFields([
-                      ['filters', item.id, 'defaultDataMask'],
-                    ]);
-                  }, 0);
-                } else {
-                  // Clear validation errors when unchecking
-                  form.setFields([
-                    {
-                      name: ['filters', item.id, 'defaultDataMask'],
-                      errors: [],
-                    },
-                  ]);
-                }
-                formChanged();
-              }}
-            >
-              <StyledFormItem
-                name={['filters', item.id, 'defaultDataMask']}
-                initialValue={customization.defaultDataMask || null}
-                label={<CheckboxLabel>{t('Default Value')}</CheckboxLabel>}
-                required={hasDefaultValue}
-                rules={[
-                  {
-                    validator: async () => {
-                      const current =
-                        form.getFieldValue(['filters', item.id]) || {};
-                      if (!current.hasDefaultValue && !current.isRequired) {
-                        return Promise.resolve();
-                      }
-
-                      const allFieldsFilled = hasAllRequiredFields();
-                      if (!allFieldsFilled) {
-                        return Promise.resolve();
-                      }
-
-                      const { column, defaultDataMask } = current;
-                      const hasValidColumn =
-                        column &&
-                        (typeof column === 'string'
-                          ? column.trim() !== ''
-                          : Array.isArray(column)
-                            ? column.length > 0
-                            : false);
-
-                      if (!hasValidColumn) {
-                        return Promise.reject(
-                          new Error(t('Please select a column')),
-                        );
-                      }
-
-                      if (current.isRequired) {
-                        const hasDefaultValue =
-                          defaultDataMask?.filterState?.value &&
-                          (Array.isArray(defaultDataMask.filterState.value)
-                            ? defaultDataMask.filterState.value.length > 0
-                            : defaultDataMask.filterState.value !== null &&
-                              defaultDataMask.filterState.value !== undefined);
-
-                        if (!hasDefaultValue) {
-                          return Promise.reject(
-                            new Error(
-                              t(
-                                'Default value is required when "Dynamic group by value is required" is enabled',
-                              ),
-                            ),
-                          );
-                        }
-                      }
-
-                      return Promise.resolve();
-                    },
-                  },
-                ]}
-              >
-                {(() => {
-                  const showDefaultValue = shouldShowDefaultValue();
-
-                  if (error || showDefaultValue) {
-                    return (
-                      <StyledMarginTop>
-                        {error ? (
-                          <div style={{ color: theme.colorErrorText }}>
-                            {t('Cannot load filter: ')}{' '}
-                            {error.message || error.error || 'Unknown error'}
-                          </div>
-                        ) : (
-                          <div>
-                            <StyledFormItem
-                              name={['filters', item.id, 'column']}
-                              label={t('Select Column')}
-                              initialValue={customization.column || null}
-                            >
-                              <Select
-                                allowClear
-                                placeholder={t('Select column')}
-                                value={customization.column || null}
-                                onChange={(value: string) => {
-                                  if (value) {
-                                    setFormFieldValues({
-                                      column: value,
-                                      defaultDataMask: {
-                                        extraFormData: {},
-                                        filterState: {
-                                          value: [value],
-                                        },
-                                        ownState: {
-                                          column: value,
-                                        },
-                                      },
-                                      chartConfiguration: {
-                                        tooltip: {
-                                          appendToBody: true,
-                                          confine: true,
-                                        },
-                                      },
-                                    });
-
-                                    form.validateFields([
-                                      ['filters', item.id, 'column'],
-                                      ['filters', item.id, 'defaultDataMask'],
-                                    ]);
-                                    formChanged();
-                                  } else {
-                                    setFormFieldValues({
-                                      column: null,
-                                      defaultDataMask: null,
-                                    });
-
-                                    form.validateFields([
-                                      ['filters', item.id, 'column'],
-                                      ['filters', item.id, 'defaultDataMask'],
-                                    ]);
-                                    formChanged();
-                                  }
-                                }}
-                                options={(
-                                  form.getFieldValue('filters')?.[item.id]
-                                    ?.defaultValueQueriesData || []
-                                ).map((option: ColumnOption) => ({
-                                  ...option,
-                                  label: option.label,
-                                }))}
-                                notFoundContent={t(
-                                  'No compatible columns found',
-                                )}
-                              />
-                            </StyledFormItem>
-                          </div>
-                        )}
-                      </StyledMarginTop>
-                    );
-                  }
-
-                  return (
-                    <StyledMarginTop>
-                      {isDefaultValueLoading ? (
-                        <Loading position="inline-centered" />
-                      ) : (
-                        t('Fill all required fields to enable "Default Value"')
-                      )}
-                    </StyledMarginTop>
-                  );
-                })()}
-              </StyledFormItem>
-            </CollapsibleControl>
-          </StyledFormItem>
-
-          <StyledFormItem
-            name={['filters', item.id, 'controlValues', 'enableEmptyFilter']}
-            rules={[
-              {
-                validator: isRequiredValidator,
-              },
-            ]}
-          >
-            <Checkbox
-              checked={
-                !!form.getFieldValue([
-                  'filters',
-                  item.id,
-                  'controlValues',
-                  'enableEmptyFilter',
-                ])
-              }
-              onChange={handleIsRequiredChange}
-            >
-              {t('Dynamic group by value is required')}&nbsp;
-              <InfoTooltip
-                tooltip={t(
-                  'User must select a value before applying the filter',
-                )}
-                placement="right"
-              />
-            </Checkbox>
-          </StyledFormItem>
-
-          <StyledFormItem name={['filters', item.id, 'selectFirst']}>
-            <CollapsibleControl
-              checked={selectFirst}
-              initialValue={customization.selectFirst ?? false}
-              title={t('Select first filter value by default')}
-              tooltip={t(
-                "When using this option, default value can't be set. Using this option may impact the load times for your dashboard.",
-              )}
-              onChange={async checked => {
-                setSelectFirst(checked);
-
-                if (checked) {
-                  setHasDefaultValue(false);
-                  const formValues =
-                    form.getFieldValue('filters')?.[item.id] || {};
-                  const datasetColumns =
-                    formValues.defaultValueQueriesData || [];
-
-                  if (datasetColumns.length > 0) {
-                    const firstColumn = datasetColumns[0];
-                    setFormFieldValues({
-                      selectFirst: checked,
-                      hasDefaultValue: false,
-                      defaultDataMask: null,
-                      defaultValue: undefined,
-                      defaultValueQueriesData: null,
-                      column: firstColumn.value,
-                    });
-                  } else {
-                    await fetchDefaultValueData();
-                    setFormFieldValues({
-                      selectFirst: checked,
-                      hasDefaultValue: false,
-                      defaultDataMask: null,
-                      defaultValue: undefined,
-                      defaultValueQueriesData: null,
-                    });
-                  }
-                } else {
-                  const isRequiredChecked = !!form.getFieldValue([
-                    'filters',
-                    item.id,
-                    'controlValues',
-                    'enableEmptyFilter',
-                  ]);
-
-                  if (isRequiredChecked) {
-                    setHasDefaultValue(true);
-                    setFormFieldValues({
-                      selectFirst: checked,
-                      hasDefaultValue: true,
-                      column: null,
-                    });
-                  } else {
-                    setFormFieldValues({
-                      selectFirst: checked,
-                      column: null,
-                    });
-                  }
-                }
-                formChanged();
-              }}
-            >
-              <div />
-            </CollapsibleControl>
-          </StyledFormItem>
-
-          <StyledFormItem name={['filters', item.id, 'canSelectMultiple']}>
-            <CollapsibleControl
-              checked={canSelectMultiple}
-              initialValue={customization.canSelectMultiple ?? true}
-              title={t('Can select multiple values')}
-              tooltip={t(
-                'Allow users to select multiple values for this filter',
-              )}
-              onChange={checked => {
-                setCanSelectMultiple(checked);
-                setFormFieldValues({
-                  canSelectMultiple: checked,
-                });
-                formChanged();
-              }}
-            >
-              <div />
-            </CollapsibleControl>
-          </StyledFormItem>
-        </Collapse.Panel>
-      </Collapse>
-    </div>
-  );
-};
-
-export default ChartCustomizationForm;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationModal.tsx
deleted file mode 100644
index 415cc023f1..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationModal.tsx
+++ /dev/null
@@ -1,700 +0,0 @@
-/**
- * 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 { useState, useEffect, useMemo, useCallback, memo } from 'react';
-import { t } from '@apache-superset/core';
-import { useTheme } from '@apache-superset/core/ui';
-import { useSelector } from 'react-redux';
-import { isEmpty, isEqual, sortBy, debounce } from 'lodash';
-import { Form } from '@superset-ui/core/components';
-import type {
-  DatasourcesState,
-  ChartsState,
-  RootState,
-} from 'src/dashboard/types';
-import { DatasetSelectLabel } from 'src/features/datasets/DatasetSelectLabel';
-import { mostUsedDataset } from '../FiltersConfigModal/FiltersConfigForm/utils';
-import ChartCustomizationTitlePane from './ChartCustomizationTitlePane';
-import ChartCustomizationForm from './ChartCustomizationForm';
-import { createDefaultChartCustomizationItem } from './utils';
-import { ChartCustomizationItem } from './types';
-import RemovedFilter from '../FiltersConfigModal/FiltersConfigForm/RemovedFilter';
-import { selectChartCustomizationItems } from './selectors';
-import { BaseConfigModal } from '../ConfigModal/BaseConfigModal';
-
-export interface ChartCustomizationModalProps {
-  isOpen: boolean;
-  dashboardId: number;
-  chartId?: number;
-  initialItemId?: string;
-  onCancel: () => void;
-  onSave: (dashboardId: number, items: ChartCustomizationItem[]) => void;
-}
-
-const ChartCustomizationModal = ({
-  isOpen,
-  dashboardId,
-  chartId,
-  initialItemId,
-  onCancel,
-  onSave,
-}: ChartCustomizationModalProps) => {
-  const theme = useTheme();
-  const [expanded, setExpanded] = useState(false);
-  const [form] = Form.useForm();
-  const [items, setItems] = useState<ChartCustomizationItem[]>([]);
-  const [currentId, setCurrentId] = useState<string | null>(null);
-  const [saveAlertVisible, setSaveAlertVisible] = useState(false);
-  const [initialLoadComplete, setInitialLoadComplete] = useState(false);
-  const [removedItems, setRemovedItems] = useState<
-    Record<string, { isPending: boolean; timerId?: number } | null>
-  >({});
-
-  const [itemChanges, setItemChanges] = useState<{
-    modified: string[];
-    deleted: string[];
-    reordered: string[];
-  }>({
-    modified: [],
-    deleted: [],
-    reordered: [],
-  });
-
-  const resetItemChanges = () => {
-    setItemChanges({
-      modified: [],
-      deleted: [],
-      reordered: [],
-    });
-  };
-
-  const [erroredItems, setErroredItems] = useState<string[]>([]);
-
-  const hasUnsavedChanges = useCallback(() => {
-    const changed = form.getFieldValue('changed');
-    const isFieldsTouched = form.isFieldsTouched();
-    const hasNewItems = items.some(item => !item.id.startsWith('existing_'));
-    const hasRemovedItems = items.some(item => item.removed);
-
-    return changed || isFieldsTouched || hasNewItems || hasRemovedItems;
-  }, [form, items]);
-
-  const validateForm = useCallback(async () => {
-    try {
-      const values = await form.validateFields();
-      setErroredItems([]);
-      return values;
-    } catch (error: unknown) {
-      if (error && typeof error === 'object' && 'errorFields' in error) {
-        const { errorFields } = error as {
-          errorFields: Array<{ name: (string | number)[] }>;
-        };
-        const errorItemIds = errorFields
-          .map(field => field.name[1])
-          .filter(
-            (id: string | number) =>
-              id && items.some(item => item.id === String(id)),
-          )
-          .map(String);
-        setErroredItems(errorItemIds);
-      }
-      return null;
-    }
-  }, [form, items]);
-
-  const handleErroredItems = useCallback(() => {
-    const formValidationFields = form.getFieldsError();
-    const erroredItemIds: string[] = [];
-
-    formValidationFields.forEach(field => {
-      const itemId = field.name[1] as string;
-      if (
-        field.errors.length > 0 &&
-        !erroredItemIds.includes(itemId) &&
-        !removedItems[itemId]
-      ) {
-        erroredItemIds.push(itemId);
-      }
-    });
-
-    if (!erroredItemIds.length && erroredItems.length > 0) {
-      setErroredItems([]);
-      return;
-    }
-    if (
-      erroredItemIds.length > 0 &&
-      !isEqual(sortBy(erroredItems), sortBy(erroredItemIds))
-    ) {
-      setErroredItems(erroredItemIds);
-    }
-  }, [form, erroredItems, removedItems]);
-
-  const resetForm = useCallback(
-    (isSaving = false) => {
-      setItems([]);
-      setCurrentId(null);
-      setSaveAlertVisible(false);
-      setInitialLoadComplete(false);
-      setErroredItems([]);
-
-      if (!isSaving) {
-        form.resetFields();
-        form.setFieldsValue({ changed: false });
-      }
-    },
-    [form],
-  );
-
-  const handleSave = useCallback(async () => {
-    const values = await validateForm();
-
-    if (values) {
-      const updatedItems = items.map(item => {
-        const formItemValues = values.filters?.[item.id] || {};
-        const isDeleted = itemChanges.deleted.includes(item.id);
-
-        const rawDataset = formItemValues.dataset;
-
-        const datasetId =
-          typeof rawDataset === 'object' && rawDataset?.value
-            ? rawDataset.value
-            : rawDataset;
-
-        const formDatasetInfo = formItemValues.datasetInfo;
-        const datasetInfo =
-          formDatasetInfo &&
-          typeof formDatasetInfo === 'object' &&
-          formDatasetInfo.table_name
-            ? {
-                value: formDatasetInfo.value,
-                label: formDatasetInfo.label,
-                table_name: formDatasetInfo.table_name,
-                schema: formDatasetInfo.schema,
-              }
-            : item.customization.datasetInfo;
-
-        const updatedItem = {
-          ...item,
-          removed: isDeleted,
-          customization: {
-            ...item.customization,
-            ...formItemValues,
-            dataset: datasetId,
-            datasetInfo,
-          },
-        };
-
-        return updatedItem;
-      });
-
-      onSave(dashboardId, updatedItems);
-      resetForm(true);
-      resetItemChanges();
-      onCancel();
-    } else if (erroredItems.length > 0) {
-      setCurrentId(erroredItems[0]);
-    }
-  }, [
-    validateForm,
-    items,
-    itemChanges,
-    onSave,
-    dashboardId,
-    resetForm,
-    resetItemChanges,
-    onCancel,
-    erroredItems,
-  ]);
-
-  const handleConfirmCancel = useCallback(() => {
-    resetForm();
-    onCancel();
-  }, [resetForm, onCancel]);
-
-  const handleCancel = useCallback(() => {
-    if (hasUnsavedChanges()) {
-      setSaveAlertVisible(true);
-    } else {
-      handleConfirmCancel();
-    }
-  }, [hasUnsavedChanges, handleConfirmCancel]);
-
-  const existingItems = useSelector(selectChartCustomizationItems);
-
-  const loadedDatasets = useSelector<RootState, DatasourcesState>(
-    ({ datasources }) => datasources,
-  );
-  const charts = useSelector<RootState, ChartsState>(({ charts }) => charts);
-
-  const addItem = useCallback(() => {
-    const usedDatasetIds = new Set<number>();
-    items.forEach(existingItem => {
-      if (existingItem.removed) return;
-
-      const { dataset } = existingItem.customization;
-      if (dataset) {
-        let datasetId: number;
-        if (
-          typeof dataset === 'object' &&
-          dataset !== null &&
-          'value' in dataset
-        ) {
-          datasetId = Number((dataset as { value: string | number }).value);
-        } else {
-          datasetId = Number(dataset);
-        }
-
-        if (!Number.isNaN(datasetId)) {
-          usedDatasetIds.add(datasetId);
-        }
-      }
-    });
-    let fallbackDatasetId: number | undefined = mostUsedDataset(
-      loadedDatasets,
-      charts,
-    );
-
-    if (fallbackDatasetId && usedDatasetIds.has(Number(fallbackDatasetId))) {
-      const availableDatasets = Object.values(loadedDatasets).filter(
-        dataset => !usedDatasetIds.has(dataset.id),
-      );
-
-      fallbackDatasetId =
-        availableDatasets.length > 0 ? availableDatasets[0].id : undefined;
-    }
-
-    const item = createDefaultChartCustomizationItem(
-      chartId,
-      fallbackDatasetId,
-    );
-    setItems([...items, item]);
-    setCurrentId(item.id);
-
-    const currentFormValues = form.getFieldsValue();
-    let formattedDataset = null;
-
-    if (fallbackDatasetId) {
-      const datasetInfo = Object.values(loadedDatasets).find(
-        dataset => dataset.id === Number(fallbackDatasetId),
-      );
-
-      formattedDataset = datasetInfo
-        ? {
-            value: fallbackDatasetId,
-            label: DatasetSelectLabel({
-              id: Number(fallbackDatasetId),
-              table_name: datasetInfo.table_name || '',
-              schema: datasetInfo.schema || '',
-              database: {
-                database_name:
-                  (datasetInfo.database?.database_name as string) ||
-                  (datasetInfo.database?.name as string) ||
-                  '',
-              },
-            }),
-            table_name: datasetInfo.table_name,
-            schema: datasetInfo.schema,
-          }
-        : {
-            value: fallbackDatasetId,
-            label: `Dataset ${fallbackDatasetId}`,
-          };
-    }
-
-    form.setFieldsValue({
-      filters: {
-        ...currentFormValues.filters,
-        [item.id]: {
-          name: '',
-          description: '',
-          dataset: formattedDataset,
-          column: null,
-          sortFilter: false,
-          sortAscending: true,
-          sortMetric: null,
-          hasDefaultValue: false,
-          isRequired: false,
-          selectFirst: false,
-        },
-      },
-    });
-  }, [items, chartId, loadedDatasets, charts, form]);
-
-  const restoreItem = useCallback(
-    (id: string) => {
-      const removal = removedItems[id];
-      if (removal?.isPending && removal.timerId) {
-        clearTimeout(removal.timerId);
-      }
-
-      setRemovedItems(current => ({ ...current, [id]: null }));
-
-      setItemChanges(prev => ({
-        ...prev,
-        deleted: prev.deleted.filter(deletedId => deletedId !== id),
-      }));
-    },
-    [removedItems],
-  );
-
-  const handleRemoveItem = useCallback(
-    (id: string) => {
-      const completeItemRemoval = (itemId: string) => {
-        setRemovedItems(removedItems => ({
-          ...removedItems,
-          [itemId]: { isPending: false },
-        }));
-      };
-
-      const timerId = window.setTimeout(() => {
-        completeItemRemoval(id);
-      }, 3000);
-
-      setRemovedItems(removedItems => ({
-        ...removedItems,
-        [id]: { isPending: true, timerId },
-      }));
-
-      setItemChanges(prev => {
-        const newChanges = {
-          ...prev,
-          deleted: [...prev.deleted, id],
-        };
-        return newChanges;
-      });
-
-      setTimeout(() => {
-        handleErroredItems();
-      }, 0);
-
-      if (currentId === id) {
-        const remainingItems = items.filter(item => item.id !== id);
-        if (remainingItems.length > 0) {
-          setCurrentId(remainingItems[0].id);
-        }
-      }
-    },
-    [items, currentId, handleErroredItems],
-  );
-
-  useEffect(() => {
-    const currentItemRemoved = removedItems[currentId || ''];
-    if (currentItemRemoved && !currentItemRemoved.isPending) {
-      const nextItem = items.find(
-        item => !removedItems[item.id] && item.id !== currentId,
-      );
-      setCurrentId(nextItem?.id || null);
-    }
-  }, [currentId, removedItems, items]);
-
-  const handleValuesChange = useMemo(
-    () =>
-      debounce(() => {
-        setSaveAlertVisible(false);
-        handleErroredItems();
-      }, 1000),
-    [handleErroredItems],
-  );
-
-  const handleToggleExpand = useCallback(() => {
-    setExpanded(!expanded);
-  }, [expanded]);
-
-  useEffect(() => {
-    if (isOpen && !initialLoadComplete) {
-      form.resetFields();
-      resetItemChanges();
-
-      if (existingItems && existingItems.length > 0) {
-        setItems(existingItems);
-
-        const initialItem = initialItemId
-          ? existingItems.find(item => item.id === initialItemId)
-          : existingItems[0];
-
-        const selectedItemId = initialItem?.id || existingItems[0].id;
-        setCurrentId(selectedItemId);
-
-        const formFilters: Record<string, any> = {};
-        existingItems.forEach(item => {
-          const datasetId = item.customization.dataset;
-          const savedDatasetInfo = item.customization.datasetInfo as
-            | {
-                value: number;
-                label: string;
-                table_name: string;
-                schema?: string;
-                database?: {
-                  database_name?: string;
-                  name?: string;
-                };
-              }
-            | undefined;
-          const datasetInfo = datasetId
-            ? Object.values(loadedDatasets).find(
-                dataset => dataset.id === Number(datasetId),
-              )
-            : null;
-
-          formFilters[item.id] = {
-            ...item.customization,
-            dataset: datasetId
-              ? typeof datasetId === 'object' && !Array.isArray(datasetId)
-                ? datasetId
-                : savedDatasetInfo && savedDatasetInfo.table_name
-                  ? {
-                      value: datasetId,
-                      label: DatasetSelectLabel({
-                        id: Number(datasetId),
-                        table_name: savedDatasetInfo.table_name || '',
-                        schema: savedDatasetInfo.schema || '',
-                        database: {
-                          database_name:
-                            savedDatasetInfo.database?.database_name ||
-                            savedDatasetInfo.database?.name ||
-                            '',
-                        },
-                      }),
-                      table_name: savedDatasetInfo.table_name,
-                      schema: savedDatasetInfo.schema,
-                    }
-                  : datasetInfo
-                    ? {
-                        value: datasetId,
-                        label: DatasetSelectLabel({
-                          id: Number(datasetId),
-                          table_name: datasetInfo.table_name || '',
-                          schema: datasetInfo.schema || '',
-                          database: {
-                            database_name:
-                              (datasetInfo.database?.database_name as string) ||
-                              (datasetInfo.database?.name as string) ||
-                              '',
-                          },
-                        }),
-                        table_name: datasetInfo.table_name,
-                        schema: datasetInfo.schema,
-                      }
-                    : {
-                        value: datasetId,
-                        label: `Dataset ${datasetId}`,
-                      }
-              : null,
-          };
-        });
-
-        const initialFormValues = {
-          filters: formFilters,
-          changed: false,
-        };
-
-        form.setFieldsValue(initialFormValues);
-      } else {
-        const fallbackDatasetId = mostUsedDataset(loadedDatasets, charts);
-        const newItem = createDefaultChartCustomizationItem(
-          chartId,
-          fallbackDatasetId,
-        );
-        setCurrentId(newItem.id);
-        setItems([newItem]);
-
-        const datasetInfo = fallbackDatasetId
-          ? Object.values(loadedDatasets).find(
-              dataset => dataset.id === Number(fallbackDatasetId),
-            )
-          : null;
-
-        const initialFormValues = {
-          filters: {
-            [newItem.id]: {
-              name: '',
-              description: '',
-              dataset: datasetInfo
-                ? {
-                    value: fallbackDatasetId,
-                    label: DatasetSelectLabel({
-                      id: Number(fallbackDatasetId),
-                      table_name: datasetInfo.table_name || '',
-                      schema: datasetInfo.schema || '',
-                      database: {
-                        database_name:
-                          (datasetInfo.database?.database_name as string) ||
-                          (datasetInfo.database?.name as string) ||
-                          '',
-                      },
-                    }),
-                    table_name: datasetInfo.table_name,
-                    schema: datasetInfo.schema,
-                  }
-                : fallbackDatasetId
-                  ? String(fallbackDatasetId)
-                  : null,
-              column: null,
-              sortFilter: false,
-              sortAscending: true,
-              sortMetric: null,
-              hasDefaultValue: false,
-              isRequired: false,
-              selectFirst: false,
-            },
-          },
-          changed: false,
-        };
-
-        form.setFieldsValue(initialFormValues);
-      }
-      setInitialLoadComplete(true);
-    }
-  }, [
-    isOpen,
-    initialLoadComplete,
-    existingItems,
-    initialItemId,
-    form,
-    chartId,
-    loadedDatasets,
-    charts,
-  ]);
-  useEffect(() => {
-    if (!isOpen) {
-      setInitialLoadComplete(false);
-    }
-  }, [isOpen]);
-
-  useEffect(() => {
-    if (!isEmpty(items)) {
-      setErroredItems(prevErroredItems =>
-        prevErroredItems.filter(
-          f => !items.find(item => item.id === f)?.removed,
-        ),
-      );
-    }
-  }, [items]);
-
-  const leftPane = (
-    <div
-      css={{
-        minWidth: '290px',
-        maxWidth: '290px',
-        height: '100%',
-        display: 'flex',
-        flexDirection: 'column',
-        overflow: 'hidden',
-        borderRight: `1px solid ${theme.colorSplit}`,
-      }}
-    >
-      <ChartCustomizationTitlePane
-        items={items.filter(
-          item => !removedItems[item.id] || removedItems[item.id]?.isPending,
-        )}
-        currentId={currentId}
-        chartId={chartId}
-        setCurrentId={setCurrentId}
-        onChange={setCurrentId}
-        onAdd={addItem}
-        onRemove={handleRemoveItem}
-        restoreItem={restoreItem}
-        removedItems={removedItems}
-        erroredItems={erroredItems}
-      />
-    </div>
-  );
-
-  const rightPane = (
-    <div
-      css={{
-        flex: 1,
-        overflow: 'auto',
-        padding: `${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px 0`,
-      }}
-    >
-      {items
-        .filter(
-          item => !removedItems[item.id] || removedItems[item.id]?.isPending,
-        )
-        .map(item => {
-          const isRemoved = !!removedItems[item.id];
-          return (
-            <div
-              key={item.id}
-              css={{
-                display: item.id === currentId ? 'block' : 'none',
-              }}
-            >
-              {isRemoved ? (
-                <RemovedFilter onClick={() => restoreItem(item.id)} />
-              ) : (
-                <ChartCustomizationForm
-                  form={form}
-                  item={item}
-                  removedItems={removedItems}
-                  allItems={items}
-                  onUpdate={updatedItem => {
-                    setItems(prev =>
-                      prev.map(i =>
-                        i.id === updatedItem.id ? updatedItem : i,
-                      ),
-                    );
-
-                    form.setFieldsValue({
-                      changed: true,
-                    });
-
-                    handleErroredItems();
-                  }}
-                />
-              )}
-            </div>
-          );
-        })}
-    </div>
-  );
-
-  const content = (
-    <div
-      css={{
-        display: 'flex',
-        height: '100%',
-      }}
-    >
-      {leftPane}
-      {rightPane}
-    </div>
-  );
-
-  return (
-    <BaseConfigModal
-      isOpen={isOpen}
-      title={t('Chart customization')}
-      expanded={expanded}
-      onCancel={handleCancel}
-      onSave={handleSave}
-      leftPane={content}
-      rightPane={null}
-      form={form}
-      onValuesChange={handleValuesChange}
-      onToggleExpand={handleToggleExpand}
-      canSave={!erroredItems.length}
-      saveAlertVisible={saveAlertVisible}
-      onDismissSaveAlert={() => setSaveAlertVisible(false)}
-      onConfirmCancel={handleConfirmCancel}
-      testId="chart-customization-modal"
-    />
-  );
-};
-export default memo(ChartCustomizationModal);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationTitleContainer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationTitleContainer.tsx
deleted file mode 100644
index 70cec6e068..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationTitleContainer.tsx
+++ /dev/null
@@ -1,184 +0,0 @@
-/**
- * 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 { FC, forwardRef, MouseEvent } from 'react';
-import { t } from '@apache-superset/core';
-import { styled, css, useTheme } from '@apache-superset/core/ui';
-import { Icons, Flex } from '@superset-ui/core/components';
-import { ChartCustomizationItem } from './types';
-
-interface Props {
-  items: ChartCustomizationItem[];
-  currentId: string | null;
-  onChange: (id: string) => void;
-  onRemove: (id: string) => void;
-  restoreItem: (id: string) => void;
-  removedItems: Record<string, { isPending: boolean; timerId?: number } | null>;
-  erroredItems?: string[];
-}
-
-const FilterTitle = styled.div<{ selected: boolean; errored: boolean }>`
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: ${({ theme }) => theme.sizeUnit * 2}px
-    ${({ theme }) => theme.sizeUnit * 3}px;
-  background-color: ${({ theme, selected }) =>
-    selected ? theme.colorPrimaryBg : theme.colorBgTextHover};
-  border: 1px solid
-    ${({ theme, selected }) => (selected ? theme.colorPrimary : 'transparent')};
-  border-radius: ${({ theme }) => theme.borderRadius}px;
-  cursor: pointer;
-
-  &:hover {
-    background-color: ${({ theme }) => theme.colorBgTextHover};
-  }
-
-  ${({ theme, errored }) =>
-    errored &&
-    `
-    &.errored div, &.errored .warning {
-      color: ${theme.colorErrorText};
-    }
-  `}
-`;
-
-const LabelWrapper = styled.div`
-  display: flex;
-  align-items: center;
-  flex: 1;
-  overflow: hidden;
-`;
-
-const TitleText = styled.div`
-  white-space: nowrap;
-  overflow: hidden;
-  text-overflow: ellipsis;
-`;
-
-const UndoButton = styled.span`
-  margin-left: auto;
-  color: ${({ theme }) => theme.colorPrimary};
-  cursor: pointer;
-  font-size: ${({ theme }) => theme.fontSizeSM}px;
-
-  &:hover {
-    text-decoration: underline;
-  }
-`;
-
-const TrashIcon = styled(Icons.DeleteOutlined)`
-  cursor: pointer;
-  margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
-  color: ${({ theme }) => theme.colorTextSecondary};
-
-  &:hover {
-    color: ${({ theme }) => theme.colorErrorText};
-  }
-`;
-
-const StyledWarning = styled(Icons.ExclamationCircleOutlined)`
-  margin-left: ${({ theme }) => theme.sizeUnit * 2}px;
-  color: ${({ theme }) => theme.colorErrorText};
-`;
-
-const ChartCustomizationTitleContainer: FC<Props> = forwardRef(
-  (
-    {
-      items,
-      currentId,
-      onChange,
-      onRemove,
-      restoreItem,
-      removedItems,
-      erroredItems = [],
-    },
-    ref,
-  ) => {
-    const theme = useTheme();
-    return (
-      <Flex
-        css={css`
-          flex-direction: column;
-          gap: ${theme.sizeUnit * 2}px;
-        `}
-        ref={ref as any}
-      >
-        {items.map(item => {
-          const isRemoved = !!removedItems[item.id];
-          const selected = item.id === currentId;
-          const isErrored = erroredItems.includes(item.id);
-          const displayName =
-            item.customization.name?.trim() || t('[untitled]');
-
-          return (
-            <FilterTitle
-              key={`group-by-title-${item.id}`}
-              role="tab"
-              tabIndex={0}
-              selected={selected}
-              errored={isErrored}
-              onClick={() => onChange(item.id)}
-              onKeyDown={e => {
-                if (e.key === 'Enter' || e.key === ' ') {
-                  e.preventDefault();
-                  onChange(item.id);
-                }
-              }}
-            >
-              <LabelWrapper>
-                <TitleText>
-                  {isRemoved ? t('(Removed)') : displayName}
-                </TitleText>
-                {isRemoved && (
-                  <UndoButton
-                    role="button"
-                    tabIndex={0}
-                    onClick={(e: MouseEvent<HTMLElement>) => {
-                      e.stopPropagation();
-                      restoreItem(item.id);
-                    }}
-                  >
-                    {t('Undo?')}
-                  </UndoButton>
-                )}
-              </LabelWrapper>
-              {!isRemoved && (
-                <>
-                  {isErrored && (
-                    <StyledWarning className="warning" iconSize="s" />
-                  )}
-                  <TrashIcon
-                    iconSize="m"
-                    onClick={(e: MouseEvent<HTMLElement>) => {
-                      e.stopPropagation();
-                      onRemove(item.id);
-                    }}
-                    aria-label={t('Remove group by')}
-                  />
-                </>
-              )}
-            </FilterTitle>
-          );
-        })}
-      </Flex>
-    );
-  },
-);
-
-export default ChartCustomizationTitleContainer;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationTitlePane.tsx b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationTitlePane.tsx
deleted file mode 100644
index d25fc769c4..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/ChartCustomizationTitlePane.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-/**
- * 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 { FC, useRef } from 'react';
-import { t } from '@apache-superset/core';
-import { styled, useTheme } from '@apache-superset/core/ui';
-import { Button, Icons } from '@superset-ui/core/components';
-import ChartCustomizationTitleContainer from './ChartCustomizationTitleContainer';
-import { ChartCustomizationItem } from './types';
-import { createDefaultChartCustomizationItem } from './utils';
-
-interface Props {
-  items: ChartCustomizationItem[];
-  currentId: string | null;
-  chartId?: number;
-  onChange: (id: string) => void;
-  onAdd: (item: ChartCustomizationItem) => void;
-  onRemove: (id: string) => void;
-  restoreItem: (id: string) => void;
-  removedItems: Record<string, { isPending: boolean; timerId?: number } | null>;
-  setCurrentId: (id: string) => void;
-  erroredItems?: string[];
-}
-
-const PaneContainer = styled.div`
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  padding: ${({ theme }) => theme.sizeUnit * 3}px;
-  padding-top: 2px;
-`;
-
-const ChartCustomizationTitlePane: FC<Props> = ({
-  items,
-  currentId,
-  chartId,
-  onChange,
-  onAdd,
-  onRemove,
-  restoreItem,
-  removedItems,
-  setCurrentId,
-  erroredItems = [],
-}) => {
-  const theme = useTheme();
-  const listRef = useRef<HTMLDivElement>(null);
-
-  const handleAdd = () => {
-    const newItem = createDefaultChartCustomizationItem(chartId);
-
-    onAdd(newItem);
-    setCurrentId(newItem.id);
-
-    setTimeout(() => {
-      listRef.current?.scroll({
-        top: listRef.current.scrollHeight,
-        behavior: 'smooth',
-      });
-    }, 50);
-  };
-
-  return (
-    <PaneContainer>
-      <div
-        ref={listRef}
-        css={{
-          flex: 1,
-          overflowY: 'auto',
-        }}
-      >
-        <ChartCustomizationTitleContainer
-          items={items}
-          currentId={currentId}
-          onChange={onChange}
-          onRemove={onRemove}
-          restoreItem={restoreItem}
-          removedItems={removedItems}
-          erroredItems={erroredItems}
-        />
-      </div>
-      <div
-        css={{
-          display: 'flex',
-          justifyContent: 'center',
-          alignItems: 'flex-start',
-          paddingTop: theme.sizeUnit * 3,
-        }}
-      >
-        <Button
-          buttonSize="default"
-          buttonStyle="secondary"
-          aria-label={t('Add dynamic group by')}
-          icon={
-            <Icons.GroupOutlined
-              iconColor={theme.colorPrimaryActive}
-              iconSize="m"
-            />
-          }
-          onClick={handleAdd}
-          data-test="add-groupby-button"
-        >
-          {t('Add dynamic group by')}
-        </Button>
-      </div>
-    </PaneContainer>
-  );
-};
-
-export default ChartCustomizationTitlePane;
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/groupBySelectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/groupBySelectors.ts
deleted file mode 100644
index 7717b3e1de..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/groupBySelectors.ts
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * 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 { createSelector } from '@reduxjs/toolkit';
-import { RootState } from 'src/dashboard/types';
-import { GroupByState } from 'src/dashboard/reducers/groupByCustomizations';
-import { ChartCustomizationItem } from './types';
-
-export const selectGroupByState = (state: RootState): GroupByState =>
-  state.groupByCustomizations?.groupByState || {};
-
-export const selectGroupByValues: (
-  state: RootState,
-  groupById: string,
-) => string[] = createSelector(
-  [selectGroupByState, (state: RootState, groupById: string) => groupById],
-  (groupByState, groupById): string[] =>
-    groupByState[groupById]?.selectedValues || [],
-);
-
-export const selectGroupByLoading: (
-  state: RootState,
-  groupById: string,
-) => boolean = createSelector(
-  [selectGroupByState, (state: RootState, groupById: string) => groupById],
-  (groupByState, groupById): boolean =>
-    groupByState[groupById]?.isLoading || false,
-);
-
-export const selectGroupByOptions: (
-  state: RootState,
-  groupById: string,
-) => Array<{ label: string; value: string }> = createSelector(
-  [selectGroupByState, (state: RootState, groupById: string) => groupById],
-  (groupByState, groupById): Array<{ label: string; value: string }> =>
-    groupByState[groupById]?.availableOptions || [],
-);
-
-export const selectGroupByFormData: (
-  state: RootState,
-  chartId: number,
-) => {
-  groupby?: string[];
-  filters?: Array<{ col: string; op: string; val: string[] }>;
-} = createSelector(
-  [
-    selectGroupByState,
-    (state: RootState) =>
-      state.dashboardInfo.metadata?.chart_customization_config || [],
-    (state: RootState, chartId: number) => chartId,
-  ],
-  (
-    groupByState,
-    chartCustomizationItems: ChartCustomizationItem[],
-    chartId,
-  ) => {
-    const groupByFormData: {
-      groupby?: string[];
-      filters?: Array<{ col: string; op: string; val: string[] }>;
-    } = {};
-
-    const matchingCustomizations = chartCustomizationItems.filter(item => {
-      if (item.removed) return false;
-      return !item.chartId || item.chartId === chartId;
-    });
-
-    const groupByColumns: string[] = [];
-    const allFilters: Array<{ col: string; op: string; val: string[] }> = [];
-
-    matchingCustomizations.forEach(item => {
-      const groupById = `chart_customization_${item.id}`;
-      const groupByValues = groupByState[groupById]?.selectedValues || [];
-
-      if (item.customization?.column) {
-        let columnNames: string[] = [];
-
-        if (typeof item.customization.column === 'string') {
-          columnNames = [item.customization.column];
-        } else if (Array.isArray(item.customization.column)) {
-          columnNames = item.customization.column.filter(
-            col => typeof col === 'string' && col.trim() !== '',
-          );
-        } else if (
-          typeof item.customization.column === 'object' &&
-          item.customization.column !== null
-        ) {
-          const columnObj = item.customization.column as any;
-          const columnName =
-            columnObj.column_name || columnObj.name || String(columnObj);
-          if (columnName && columnName.trim() !== '') {
-            columnNames = [columnName];
-          }
-        } else {
-          return;
-        }
-
-        if (columnNames.length === 0) {
-          return;
-        }
-
-        columnNames.forEach(columnName => {
-          if (!groupByColumns.includes(columnName)) {
-            groupByColumns.push(columnName);
-          }
-        });
-
-        columnNames.forEach(columnName => {
-          if (groupByValues.length > 0) {
-            allFilters.push({
-              col: columnName,
-              op: 'IN',
-              val: groupByValues,
-            });
-          }
-        });
-      }
-    });
-
-    if (groupByColumns.length > 0) {
-      groupByFormData.groupby = groupByColumns;
-    }
-
-    if (allFilters.length > 0) {
-      groupByFormData.filters = allFilters;
-    }
-
-    return groupByFormData;
-  },
-);
-
-export const selectGroupByExtraFormData: (
-  state: RootState,
-  chartId: number,
-  datasetId: string,
-) => {
-  groupby?: string[];
-  order_by_cols?: string[];
-  filters?: Array<{ col: string; op: string; val: string[] }>;
-} = createSelector(
-  [
-    selectGroupByState,
-    (state: RootState) =>
-      state.dashboardInfo.metadata?.chart_customization_config || [],
-    (state: RootState, chartId: number, datasetId: string) => ({
-      chartId,
-      datasetId,
-    }),
-  ],
-  (
-    groupByState,
-    chartCustomizationItems: ChartCustomizationItem[],
-    { chartId, datasetId },
-  ) => {
-    const extraFormData: {
-      groupby?: string[];
-      order_by_cols?: string[];
-      filters?: Array<{ col: string; op: string; val: string[] }>;
-    } = {};
-
-    const matchingCustomizations = chartCustomizationItems.filter(item => {
-      if (item.removed) return false;
-
-      const targetDatasetId = String(item.customization?.dataset || '');
-      const datasetMatches = targetDatasetId === datasetId;
-      const chartMatches = !item.chartId || item.chartId === chartId;
-
-      return datasetMatches && chartMatches;
-    });
-
-    const groupByColumns: string[] = [];
-    const allFilters: Array<{ col: string; op: string; val: string[] }> = [];
-    let orderByConfig: string[] | undefined;
-
-    matchingCustomizations.forEach(item => {
-      const { customization } = item;
-      const groupById = `chart_customization_${item.id}`;
-      const groupByValues = groupByState[groupById]?.selectedValues || [];
-
-      if (customization?.column) {
-        let columnNames: string[] = [];
-
-        if (typeof customization.column === 'string') {
-          columnNames = [customization.column];
-        } else if (Array.isArray(customization.column)) {
-          columnNames = customization.column.filter(
-            col => typeof col === 'string' && col.trim() !== '',
-          );
-        } else if (
-          typeof customization.column === 'object' &&
-          customization.column !== null
-        ) {
-          const columnObj = customization.column as any;
-          const columnName =
-            columnObj.column_name || columnObj.name || String(columnObj);
-          if (columnName && columnName.trim() !== '') {
-            columnNames = [columnName];
-          }
-        } else {
-          return;
-        }
-
-        if (columnNames.length === 0) {
-          return;
-        }
-
-        columnNames.forEach(columnName => {
-          if (!groupByColumns.includes(columnName)) {
-            groupByColumns.push(columnName);
-          }
-        });
-
-        columnNames.forEach(columnName => {
-          if (groupByValues.length > 0) {
-            allFilters.push({
-              col: columnName,
-              op: 'IN',
-              val: groupByValues,
-            });
-          }
-        });
-
-        if (customization.sortFilter && customization.sortMetric) {
-          orderByConfig = [
-            JSON.stringify([
-              customization.sortMetric,
-              !customization.sortAscending,
-            ]),
-          ];
-        }
-      }
-    });
-
-    if (groupByColumns.length > 0) {
-      extraFormData.groupby = groupByColumns;
-    }
-
-    if (allFilters.length > 0) {
-      extraFormData.filters = allFilters;
-    }
-
-    if (orderByConfig) {
-      extraFormData.order_by_cols = orderByConfig;
-    }
-
-    return extraFormData;
-  },
-);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/selectors.ts
deleted file mode 100644
index 547db56cb4..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/selectors.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * 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 { createSelector } from '@reduxjs/toolkit';
-import { RootState } from 'src/dashboard/types';
-import { ChartCustomizationItem } from './types';
-
-const EMPTY_ARRAY: ChartCustomizationItem[] = [];
-
-export const selectChartCustomizationItems = createSelector(
-  (state: RootState) => state.dashboardInfo.metadata,
-  (metadata): ChartCustomizationItem[] => {
-    if (
-      metadata?.chart_customization_config &&
-      metadata.chart_customization_config.length > 0
-    ) {
-      return metadata.chart_customization_config;
-    }
-
-    const legacyCustomization = metadata?.native_filter_configuration?.find(
-      (item: any) =>
-        item.type === 'CHART_CUSTOMIZATION' &&
-        item.id === 'chart_customization_groupby',
-    );
-
-    if (legacyCustomization?.chart_customization) {
-      return legacyCustomization.chart_customization;
-    }
-
-    return EMPTY_ARRAY;
-  },
-);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/useChartCustomizationModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/useChartCustomizationModal.tsx
deleted file mode 100644
index c5298d3b02..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/useChartCustomizationModal.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * 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 { useState, useCallback } from 'react';
-import { useSelector, useDispatch } from 'react-redux';
-import { RootState } from 'src/dashboard/types';
-import { saveChartCustomization } from 'src/dashboard/actions/chartCustomizationActions';
-import { ChartCustomizationItem } from './types';
-
-export const useChartCustomizationModal = (chartId?: number) => {
-  const [isOpen, setIsOpen] = useState(false);
-  const dashboardId = useSelector<RootState, number>(
-    state => state.dashboardInfo.id,
-  );
-
-  const dispatch = useDispatch();
-
-  const openChartCustomizationModal = useCallback(() => setIsOpen(true), []);
-  const closeChartCustomizationModal = useCallback(() => setIsOpen(false), []);
-
-  const handleSave = useCallback(
-    (dashboardId: number, items: ChartCustomizationItem[]) => {
-      dispatch(saveChartCustomization(items));
-      setIsOpen(false);
-    },
-    [dispatch],
-  );
-
-  return {
-    isOpen,
-    dashboardId,
-    chartId,
-    openChartCustomizationModal,
-    closeChartCustomizationModal,
-    handleSave,
-  };
-};
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/utils.ts
deleted file mode 100644
index 1ac7b1eb00..0000000000
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/utils.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/**
- * 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 { t } from '@apache-superset/core';
-import { ChartCustomizationItem, GroupByCustomization } from './types';
-
-export function generateGroupById(): string {
-  return `groupby_${Date.now()}`;
-}
-
-export const getChartCustomizationIds = (items: ChartCustomizationItem[]) =>
-  items.map(item => item.id);
-
-export const createDefaultChartCustomizationItem = (
-  chartId?: number,
-  defaultDatasetId?: number,
-): ChartCustomizationItem => ({
-  id: generateGroupById(),
-  title: t('[untitled]'),
-  dataset: null,
-  description: '',
-  removed: false,
-  chartId,
-  settings: {
-    sortFilter: false,
-    hasDefaultValue: false,
-    isRequired: false,
-    selectFirstByDefault: false,
-  },
-  customization: {
-    name: '',
-    dataset: defaultDatasetId ? String(defaultDatasetId) : null,
-    column: null,
-    sortAscending: true,
-    hasDefaultValue: false,
-    isRequired: false,
-    selectFirst: false,
-  },
-});
-
-export const ensureValidCustomization = (
-  customization: Partial<GroupByCustomization> = {},
-): GroupByCustomization => ({
-  name: customization.name || '',
-  dataset: customization.dataset || null,
-  column: customization.column || null,
-  ...customization,
-});
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ConfigModal/SharedStyles.tsx b/superset-frontend/src/dashboard/components/nativeFilters/ConfigModal/SharedStyles.tsx
index 84a0181fe5..51ee24051a 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/ConfigModal/SharedStyles.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/ConfigModal/SharedStyles.tsx
@@ -41,7 +41,6 @@ export const BaseModalWrapper = styled(StyledModal)<BaseModalWrapperProps>`
 
   .ant-modal-body {
     padding: 0px;
-    overflow: auto;
   }
 
   ${({ expanded }) =>
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx
index 3055b3ec15..24fe7a30a1 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx
@@ -22,13 +22,14 @@ import {
   DataMaskState,
   DataMaskStateWithId,
   isDefined,
+  ChartCustomization,
+  ChartCustomizationDivider,
 } from '@superset-ui/core';
 import { css, SupersetTheme, styled } from '@apache-superset/core/ui';
 import { Button } from '@superset-ui/core/components';
 import { OPEN_FILTER_BAR_WIDTH } from 'src/dashboard/constants';
 import tinycolor from 'tinycolor2';
 import { FilterBarOrientation } from 'src/dashboard/types';
-import { ChartCustomizationItem } from 'src/dashboard/components/nativeFilters/ChartCustomization/types';
 import { getFilterBarTestId } from '../utils';
 
 interface ActionButtonsProps {
@@ -37,7 +38,7 @@ interface ActionButtonsProps {
   onClearAll: () => void;
   dataMaskSelected: DataMaskState;
   dataMaskApplied: DataMaskStateWithId;
-  chartCustomizationItems?: ChartCustomizationItem[];
+  chartCustomizationItems?: (ChartCustomization | ChartCustomizationDivider)[];
   isApplyDisabled: boolean;
   filterBarOrientation?: FilterBarOrientation;
 }
@@ -125,9 +126,13 @@ const ActionButtons = ({
       },
     );
 
-    const hasChartCustomizations = chartCustomizationItems?.some(
-      item => item.customization?.column && !item.removed,
-    );
+    const hasChartCustomizations = chartCustomizationItems?.some(item => {
+      if (item.removed) return false;
+      const mask = dataMaskApplied[item.id] || dataMaskSelected[item.id];
+      const hasValue = isDefined(mask?.filterState?.value);
+      const hasGroupBy = isDefined(mask?.ownState?.column);
+      return hasValue || hasGroupBy;
+    });
 
     return hasSelectedChanges || hasAppliedChanges || hasChartCustomizations;
   }, [dataMaskSelected, dataMaskApplied, chartCustomizationItems]);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CustomizationsOutOfScopeCollapsible/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CustomizationsOutOfScopeCollapsible/index.tsx
new file mode 100644
index 0000000000..b548d6ac41
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CustomizationsOutOfScopeCollapsible/index.tsx
@@ -0,0 +1,67 @@
+/**
+ * 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 { ReactNode } from 'react';
+import { t } from '@apache-superset/core';
+import {
+  ChartCustomization,
+  ChartCustomizationDivider,
+} from '@superset-ui/core';
+import { css, SupersetTheme } from '@apache-superset/core/ui';
+import { Collapse } from '@superset-ui/core/components';
+
+export interface CustomizationsOutOfScopeCollapsibleProps {
+  customizationsOutOfScope: (ChartCustomization | ChartCustomizationDivider)[];
+  renderer: (
+    customization: ChartCustomization | ChartCustomizationDivider,
+    index: number,
+  ) => ReactNode;
+  forceRender?: boolean;
+}
+
+export const CustomizationsOutOfScopeCollapsible = ({
+  customizationsOutOfScope,
+  renderer,
+  forceRender = false,
+}: CustomizationsOutOfScopeCollapsibleProps) => (
+  <Collapse
+    ghost
+    bordered
+    expandIconPosition="end"
+    collapsible={customizationsOutOfScope.length === 0 ? 'disabled' : undefined}
+    items={[
+      {
+        key: 'out-of-scope-customizations',
+        label: (
+          <span
+            css={(theme: SupersetTheme) => css`
+              font-size: ${theme.fontSizeSM}px;
+            `}
+          >
+            {t(
+              'Customizations out of scope (%d)',
+              customizationsOutOfScope.length,
+            )}
+          </span>
+        ),
+        children: customizationsOutOfScope.map(renderer),
+        forceRender,
+      },
+    ]}
+  />
+);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
index 96e2770603..814b9132f0 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
@@ -79,7 +79,7 @@ const addFilterFlow = async () => {
   // open filter config modals
   userEvent.click(screen.getByTestId(getTestId('collapsable')));
   userEvent.click(screen.getByLabelText('setting'));
-  userEvent.click(screen.getByText('Add or edit filters'));
+  userEvent.click(screen.getByText('Filters and customizations'));
   // select filter
   userEvent.click(screen.getByText('Value'));
   userEvent.click(screen.getByText('Time range'));
@@ -103,25 +103,27 @@ describe('FilterBar', () => {
   };
 
   const mockApi = jest.fn(async data => {
-    const json = JSON.parse(data.json_metadata);
-    const filterId = json.native_filter_configuration[0].id;
+    if (!data?.modified?.length) {
+      return {
+        id: 1234,
+        result: [],
+      };
+    }
+    const filterId = data.modified[0].id;
     return {
       id: 1234,
-      result: {
-        json_metadata: `{
-            "label_colors":{"Girls":"#FF69B4","Boys":"#ADD8E6","girl":"#FF69B4","boy":"#ADD8E6"},
-            "native_filter_configuration":[{
-              "id":"${filterId}",
-              "name":"${FILTER_NAME}",
-              "filterType":"filter_time",
-              "targets":[{"datasetId":11,"column":{"name":"color"}}],
-              "defaultDataMask":{"filterState":{"value":null}},
-              "controlValues":{},
-              "cascadeParentIds":[],
-              "scope":{"rootPath":["ROOT_ID"],"excluded":[]}
-            }],
-          }`,
-      },
+      result: [
+        {
+          id: filterId,
+          name: FILTER_NAME,
+          filterType: 'filter_time',
+          targets: [{ datasetId: 11, column: { name: 'color' } }],
+          defaultDataMask: { filterState: { value: null } },
+          controlValues: {},
+          cascadeParentIds: [],
+          scope: { rootPath: ['ROOT_ID'], excluded: [] },
+        },
+      ],
     };
   });
 
@@ -273,6 +275,13 @@ describe('FilterBar', () => {
     };
     const stateWithDivider = {
       ...stateWithoutNativeFilters,
+      dashboardInfo: {
+        ...stateWithoutNativeFilters.dashboardInfo,
+        metadata: {
+          ...stateWithoutNativeFilters.dashboardInfo.metadata,
+          native_filter_configuration: [divider],
+        },
+      },
       nativeFilters: {
         filters: {
           'NATIVE_FILTER_DIVIDER-1': divider,
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 4d7401ab3a..16e3a9fef0 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBarSettings/index.tsx
@@ -33,8 +33,6 @@ import { Space } from '@superset-ui/core/components/Space';
 import { clearDataMaskState } from 'src/dataMask/actions';
 import { useFilters } from 'src/dashboard/components/nativeFilters/FilterBar/state';
 import { useFilterConfigModal } from 'src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal';
-import { useChartCustomizationModal } from '../../ChartCustomization/useChartCustomizationModal';
-import ChartCustomizationModal from '../../ChartCustomization/ChartCustomizationModal';
 import { useCrossFiltersScopingModal } from '../CrossFilters/ScopingModal/useCrossFiltersScopingModal';
 import FilterConfigurationLink from '../FilterConfigurationLink';
 
@@ -82,15 +80,6 @@ const FilterBarSettings = () => {
     ({ dashboardInfo }) => dashboardInfo.id,
   );
 
-  const {
-    isOpen: isChartCustomizationModalOpen,
-    dashboardId: chartCustomizationDashboardId,
-    chartId: chartCustomizationChartId,
-    openChartCustomizationModal,
-    closeChartCustomizationModal,
-    handleSave: handleChartCustomizationSave,
-  } = useChartCustomizationModal();
-
   const [openScopingModal, scopingModal] = useCrossFiltersScopingModal();
 
   const { openFilterConfigModal, FilterConfigModalComponent } =
@@ -148,7 +137,7 @@ const FilterBarSettings = () => {
       } else if (selectedKey === ADD_EDIT_FILTERS_MENU_KEY) {
         openFilterConfigModal();
       } else if (selectedKey === CHART_CUSTOMIZATION_MENU_KEY) {
-        openChartCustomizationModal();
+        openFilterConfigModal();
       }
     },
     [
@@ -156,7 +145,6 @@ const FilterBarSettings = () => {
       toggleCrossFiltering,
       toggleFilterBarOrientation,
       openFilterConfigModal,
-      openChartCustomizationModal,
     ],
   );
 
@@ -183,7 +171,7 @@ const FilterBarSettings = () => {
         key: ADD_EDIT_FILTERS_MENU_KEY,
         label: (
           <FilterConfigurationLink>
-            {t('Add or edit filters')}
+            {t('Filters and customizations')}
           </FilterConfigurationLink>
         ),
       });
@@ -202,10 +190,6 @@ const FilterBarSettings = () => {
       });
       items.push({ type: 'divider' });
     }
-    items.push({
-      key: CHART_CUSTOMIZATION_MENU_KEY,
-      label: t('Chart customization'),
-    });
     if (canEdit) {
       items.push({
         key: 'placement',
@@ -299,15 +283,6 @@ const FilterBarSettings = () => {
       </Dropdown>
       {scopingModal}
       {FilterConfigModalComponent}
-      {isChartCustomizationModalOpen && (
-        <ChartCustomizationModal
-          isOpen={isChartCustomizationModalOpen}
-          dashboardId={chartCustomizationDashboardId}
-          chartId={chartCustomizationChartId}
-          onCancel={closeChartCustomizationModal}
-          onSave={handleChartCustomizationSave}
-        />
-      )}
     </>
   );
 };
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx
index 2a08f7800f..29de53f0af 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/useFilterConfigModal.tsx
@@ -20,7 +20,8 @@
 import { useCallback, useState } from 'react';
 import { useDispatch } from 'react-redux';
 import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
-import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
+import { saveChartCustomization } from 'src/dashboard/actions/chartCustomizationActions';
+import { SaveChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
 import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
 
 interface UseFilterConfigModalProps {
@@ -33,7 +34,7 @@ interface UseFilterConfigModalReturn {
   isFilterConfigModalOpen: boolean;
   openFilterConfigModal: () => void;
   closeFilterConfigModal: () => void;
-  handleFilterSave: (filterChanges: SaveFilterChangesType) => Promise<void>;
+  handleSave: (changes: SaveChangesType) => Promise<void>;
   FilterConfigModalComponent: JSX.Element | null;
 }
 
@@ -53,10 +54,26 @@ export const useFilterConfigModal = ({
     setIsFilterConfigModalOpen(false);
   }, []);
 
-  const handleFilterSave = useCallback(
-    async (filterChanges: SaveFilterChangesType) => {
-      dispatch(await setFilterConfiguration(filterChanges));
-      closeFilterConfigModal();
+  const handleSave = useCallback(
+    async (changes: SaveChangesType) => {
+      try {
+        if (changes.filterChanges) {
+          dispatch(setFilterConfiguration(changes.filterChanges));
+        }
+        if (changes.customizationChanges) {
+          dispatch(
+            saveChartCustomization(
+              changes.customizationChanges.modified,
+              changes.customizationChanges.deleted,
+              changes.customizationChanges.reordered,
+              true,
+            ),
+          );
+        }
+        closeFilterConfigModal();
+      } catch (error) {
+        // Error toast already shown in action, prevent modal close
+      }
     },
     [dispatch, closeFilterConfigModal],
   );
@@ -64,7 +81,7 @@ export const useFilterConfigModal = ({
   const FilterConfigModalComponent = isFilterConfigModalOpen ? (
     <FiltersConfigModal
       isOpen={isFilterConfigModalOpen}
-      onSave={handleFilterSave}
+      onSave={handleSave}
       onCancel={closeFilterConfigModal}
       key={`filters-for-${dashboardId}`}
       createNewOnOpen={createNewOnOpen}
@@ -76,7 +93,7 @@ export const useFilterConfigModal = ({
     isFilterConfigModalOpen,
     openFilterConfigModal,
     closeFilterConfigModal,
-    handleFilterSave,
+    handleSave,
     FilterConfigModalComponent,
   };
 };
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
index 7f6f020e3c..3d41fed342 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
@@ -22,15 +22,8 @@ import {
   InPortal,
   OutPortal,
 } from 'react-reverse-portal';
-import { truncationCSS } from '@superset-ui/core';
-import { styled, SupersetTheme } from '@apache-superset/core/ui';
-import {
-  FormItem as StyledFormItem,
-  Form,
-  Icons,
-  Tooltip,
-} from '@superset-ui/core/components';
 import { FilterBarOrientation } from 'src/dashboard/types';
+import { isChartCustomization } from '@superset-ui/core';
 import { checkIsMissingRequiredValue } from '../utils';
 import FilterValue from './FilterValue';
 import { FilterCard } from '../../FilterCard';
@@ -38,252 +31,13 @@ import { FilterBarScrollContext } from '../Vertical';
 import { FilterControlProps } from './types';
 import { FilterCardPlacement } from '../../FilterCard/types';
 import { useIsFilterInScope } from '../../state';
-
-const FilterStyledIcon = styled.div`
-  position: absolute;
-  right: 0;
-`;
-
-const VerticalFilterControlTitle = styled.h4`
-  font-size: ${({ theme }) => theme.fontSizeSM}px;
-  color: ${({ theme }) => theme.colorText};
-  overflow-wrap: anywhere;
-`;
-
-const HorizontalFilterControlTitle = styled(VerticalFilterControlTitle)`
-  font-weight: ${({ theme }) => theme.fontWeightNormal};
-  color: ${({ theme }) => theme.colorText};
-  margin: 0;
-  ${truncationCSS};
-`;
-
-const HorizontalOverflowFilterControlTitle = styled(
-  HorizontalFilterControlTitle,
-)`
-  max-width: none;
-  margin: ${({ theme }) => `${theme.sizeUnit * 2}px 0 ${theme.sizeUnit}px`};
-`;
-
-const VerticalFilterControlTitleBox = styled.div`
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  justify-content: space-between;
-`;
-
-const HorizontalFilterControlTitleBox = styled(VerticalFilterControlTitleBox)`
-  margin-bottom: unset;
-`;
-
-const HorizontalOverflowFilterControlTitleBox = styled(
-  VerticalFilterControlTitleBox,
-)`
-  width: 100%;
-`;
-
-const AllFilterControlContainer = styled(Form)`
-  // TODO this is a hack related to having form items inside others which is not
-  // normal antd-expected usage
-  .ant-form-item .ant-form-item {
-    margin-bottom: 0 !important;
-  }
-`;
-
-const VerticalFilterControlContainer = styled(AllFilterControlContainer)`
-  width: 100%;
-
-  .ant-form-item {
-    margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px;
-  }
-
-  && .ant-form-item-label > label {
-    text-transform: none;
-    width: 100%;
-    padding-right: ${({ theme }) => theme.sizeUnit * 11}px;
-  }
-  .ant-form-item-tooltip {
-    margin-bottom: ${({ theme }) => theme.sizeUnit}px;
-  }
-`;
-
-const HorizontalFilterControlContainer = styled(AllFilterControlContainer)`
-  && .ant-form-item-label > label {
-    margin-bottom: 0;
-    text-transform: none;
-  }
-  .ant-form-item-tooltip {
-    margin-bottom: ${({ theme }) => theme.sizeUnit}px;
-  }
-`;
-
-const HorizontalOverflowFilterControlContainer = styled(
-  VerticalFilterControlContainer,
-)`
-  && .ant-form-item-label {
-    line-height: 1;
-    & > label {
-      padding-right: unset;
-    }
-  }
-`;
-
-const VerticalFormItem = styled(StyledFormItem)<{
-  inverseSelection: boolean;
-}>`
-  .ant-form-item-label {
-    overflow: visible;
-    label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
-      &::after {
-        display: none;
-      }
-    }
-  }
-
-  .select-container {
-    ${({ inverseSelection }) =>
-      inverseSelection &&
-      `
-      width: 140px;
-    `}
-  }
-
-  .select-bulk-actions {
-    ${({ inverseSelection }) =>
-      inverseSelection &&
-      `
-      flex-direction: column;
-    `}
-  }
-
-  .ant-col {
-    min-height: auto;
-  }
-`;
-
-const HorizontalFormItem = styled(StyledFormItem)<{
-  inverseSelection: boolean;
-}>`
-  && {
-    margin-bottom: 0;
-    align-items: center;
-  }
-
-  && > .ant-row > .ant-form-item-label {
-    display: flex;
-    align-items: center;
-    overflow: visible;
-    padding-bottom: 0;
-    margin-right: ${({ theme }) => theme.sizeUnit * 2}px;
-    label.ant-form-item-required:not(.ant-form-item-required-mark-optional) {
-      &::after {
-        display: none;
-      }
-    }
-
-    & > label::after {
-      display: none;
-    }
-  }
-
-  .ant-form-item-control {
-    min-width: ${({ inverseSelection }) => (inverseSelection ? 252 : 164)}px;
-  }
-
-  .select-container {
-    ${({ inverseSelection }) =>
-      inverseSelection &&
-      `
-      width: 164px;
-    `}
-  }
-
-  .select-bulk-actions {
-    flex-direction: column;
-  }
-`;
-
-const HorizontalOverflowFormItem = VerticalFormItem;
-
-const useFilterControlDisplay = (
-  orientation: FilterBarOrientation,
-  overflow: boolean,
-  inverseSelection: boolean,
-) =>
-  useMemo(() => {
-    if (orientation === FilterBarOrientation.Horizontal) {
-      if (overflow) {
-        return {
-          FilterControlContainer: HorizontalOverflowFilterControlContainer,
-          FormItem: (props: any) => (
-            <HorizontalOverflowFormItem
-              {...props}
-              inverseSelection={inverseSelection}
-            />
-          ),
-          FilterControlTitleBox: HorizontalOverflowFilterControlTitleBox,
-          FilterControlTitle: HorizontalOverflowFilterControlTitle,
-        };
-      }
-      return {
-        FilterControlContainer: HorizontalFilterControlContainer,
-        FormItem: (props: any) => (
-          <HorizontalFormItem {...props} inverseSelection={inverseSelection} />
-        ),
-        FilterControlTitleBox: HorizontalFilterControlTitleBox,
-        FilterControlTitle: HorizontalFilterControlTitle,
-      };
-    }
-    return {
-      FilterControlContainer: VerticalFilterControlContainer,
-      FormItem: (props: any) => (
-        <VerticalFormItem {...props} inverseSelection={inverseSelection} />
-      ),
-      FilterControlTitleBox: VerticalFilterControlTitleBox,
-      FilterControlTitle: VerticalFilterControlTitle,
-    };
-  }, [orientation, overflow, inverseSelection]);
-
-const ToolTipContainer = styled.div`
-  font-size: ${({ theme }) => theme.fontSize}px;
-  display: flex;
-`;
-
-const RequiredFieldIndicator = () => (
-  <span
-    css={(theme: SupersetTheme) => ({
-      color: theme.colorError,
-      fontSize: `${theme.fontSizeSM}px`,
-      paddingLeft: '1px',
-    })}
-  >
-    *
-  </span>
-);
-
-const DescriptionToolTip = ({ description }: { description: string }) => (
-  <ToolTipContainer>
-    <Tooltip
-      title={description}
-      placement="right"
-      overlayInnerStyle={{
-        display: '-webkit-box',
-        WebkitLineClamp: 10,
-        WebkitBoxOrient: 'vertical',
-        overflow: 'hidden',
-        textOverflow: 'ellipsis',
-        whiteSpace: 'normal',
-      }}
-    >
-      <Icons.InfoCircleOutlined
-        className="text-muted"
-        role="button"
-        css={(theme: SupersetTheme) => ({
-          paddingLeft: `${theme.sizeUnit}px`,
-        })}
-      />
-    </Tooltip>
-  </ToolTipContainer>
-);
+import {
+  FilterStyledIcon,
+  RequiredFieldIndicator,
+  DescriptionToolTip,
+  useFilterControlDisplay,
+} from './FilterControlShared';
+import GroupByFilterCard from './GroupByFilterCard';
 
 const FilterControl = ({
   dataMaskSelected,
@@ -355,6 +109,24 @@ const FilterControl = ({
     return FilterCardPlacement.Right;
   }, [orientation, overflow]);
 
+  if (
+    isChartCustomization(filter) &&
+    filter.filterType === 'chart_customization_dynamic_groupby'
+  ) {
+    return (
+      <GroupByFilterCard
+        customizationItem={filter}
+        orientation={
+          orientation === FilterBarOrientation.Horizontal
+            ? 'horizontal'
+            : 'vertical'
+        }
+        dataMaskSelected={dataMaskSelected}
+        onFilterSelectionChange={onFilterSelectionChange}
+      />
+    );
+  }
+
   return (
     <>
       <InPortal node={portalNode}>
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControlShared.tsx
similarity index 54%
copy from superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
copy to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControlShared.tsx
index 7f6f020e3c..6b42fde4d3 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControlShared.tsx
@@ -16,12 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { memo, useContext, useMemo, useState } from 'react';
-import {
-  createHtmlPortalNode,
-  InPortal,
-  OutPortal,
-} from 'react-reverse-portal';
+import { useMemo } from 'react';
 import { truncationCSS } from '@superset-ui/core';
 import { styled, SupersetTheme } from '@apache-superset/core/ui';
 import {
@@ -31,57 +26,52 @@ import {
   Tooltip,
 } from '@superset-ui/core/components';
 import { FilterBarOrientation } from 'src/dashboard/types';
-import { checkIsMissingRequiredValue } from '../utils';
-import FilterValue from './FilterValue';
-import { FilterCard } from '../../FilterCard';
-import { FilterBarScrollContext } from '../Vertical';
-import { FilterControlProps } from './types';
-import { FilterCardPlacement } from '../../FilterCard/types';
-import { useIsFilterInScope } from '../../state';
 
-const FilterStyledIcon = styled.div`
+export const FilterStyledIcon = styled.div`
   position: absolute;
   right: 0;
 `;
 
-const VerticalFilterControlTitle = styled.h4`
+export const VerticalFilterControlTitle = styled.h4`
   font-size: ${({ theme }) => theme.fontSizeSM}px;
   color: ${({ theme }) => theme.colorText};
   overflow-wrap: anywhere;
 `;
 
-const HorizontalFilterControlTitle = styled(VerticalFilterControlTitle)`
+export const HorizontalFilterControlTitle = styled(VerticalFilterControlTitle)`
   font-weight: ${({ theme }) => theme.fontWeightNormal};
   color: ${({ theme }) => theme.colorText};
   margin: 0;
   ${truncationCSS};
 `;
 
-const HorizontalOverflowFilterControlTitle = styled(
+export const HorizontalOverflowFilterControlTitle = styled(
   HorizontalFilterControlTitle,
 )`
   max-width: none;
   margin: ${({ theme }) => `${theme.sizeUnit * 2}px 0 ${theme.sizeUnit}px`};
 `;
 
-const VerticalFilterControlTitleBox = styled.div`
+export const VerticalFilterControlTitleBox = styled.div`
   display: flex;
   flex-direction: row;
   align-items: center;
   justify-content: space-between;
 `;
 
-const HorizontalFilterControlTitleBox = styled(VerticalFilterControlTitleBox)`
+export const HorizontalFilterControlTitleBox = styled(
+  VerticalFilterControlTitleBox,
+)`
   margin-bottom: unset;
 `;
 
-const HorizontalOverflowFilterControlTitleBox = styled(
+export const HorizontalOverflowFilterControlTitleBox = styled(
   VerticalFilterControlTitleBox,
 )`
   width: 100%;
 `;
 
-const AllFilterControlContainer = styled(Form)`
+export const AllFilterControlContainer = styled(Form)`
   // TODO this is a hack related to having form items inside others which is not
   // normal antd-expected usage
   .ant-form-item .ant-form-item {
@@ -89,7 +79,7 @@ const AllFilterControlContainer = styled(Form)`
   }
 `;
 
-const VerticalFilterControlContainer = styled(AllFilterControlContainer)`
+export const VerticalFilterControlContainer = styled(AllFilterControlContainer)`
   width: 100%;
 
   .ant-form-item {
@@ -106,7 +96,9 @@ const VerticalFilterControlContainer = styled(AllFilterControlContainer)`
   }
 `;
 
-const HorizontalFilterControlContainer = styled(AllFilterControlContainer)`
+export const HorizontalFilterControlContainer = styled(
+  AllFilterControlContainer,
+)`
   && .ant-form-item-label > label {
     margin-bottom: 0;
     text-transform: none;
@@ -116,7 +108,7 @@ const HorizontalFilterControlContainer = styled(AllFilterControlContainer)`
   }
 `;
 
-const HorizontalOverflowFilterControlContainer = styled(
+export const HorizontalOverflowFilterControlContainer = styled(
   VerticalFilterControlContainer,
 )`
   && .ant-form-item-label {
@@ -127,8 +119,8 @@ const HorizontalOverflowFilterControlContainer = styled(
   }
 `;
 
-const VerticalFormItem = styled(StyledFormItem)<{
-  inverseSelection: boolean;
+export const VerticalFormItem = styled(StyledFormItem)<{
+  inverseSelection?: boolean;
 }>`
   .ant-form-item-label {
     overflow: visible;
@@ -160,8 +152,8 @@ const VerticalFormItem = styled(StyledFormItem)<{
   }
 `;
 
-const HorizontalFormItem = styled(StyledFormItem)<{
-  inverseSelection: boolean;
+export const HorizontalFormItem = styled(StyledFormItem)<{
+  inverseSelection?: boolean;
 }>`
   && {
     margin-bottom: 0;
@@ -202,53 +194,14 @@ const HorizontalFormItem = styled(StyledFormItem)<{
   }
 `;
 
-const HorizontalOverflowFormItem = VerticalFormItem;
-
-const useFilterControlDisplay = (
-  orientation: FilterBarOrientation,
-  overflow: boolean,
-  inverseSelection: boolean,
-) =>
-  useMemo(() => {
-    if (orientation === FilterBarOrientation.Horizontal) {
-      if (overflow) {
-        return {
-          FilterControlContainer: HorizontalOverflowFilterControlContainer,
-          FormItem: (props: any) => (
-            <HorizontalOverflowFormItem
-              {...props}
-              inverseSelection={inverseSelection}
-            />
-          ),
-          FilterControlTitleBox: HorizontalOverflowFilterControlTitleBox,
-          FilterControlTitle: HorizontalOverflowFilterControlTitle,
-        };
-      }
-      return {
-        FilterControlContainer: HorizontalFilterControlContainer,
-        FormItem: (props: any) => (
-          <HorizontalFormItem {...props} inverseSelection={inverseSelection} />
-        ),
-        FilterControlTitleBox: HorizontalFilterControlTitleBox,
-        FilterControlTitle: HorizontalFilterControlTitle,
-      };
-    }
-    return {
-      FilterControlContainer: VerticalFilterControlContainer,
-      FormItem: (props: any) => (
-        <VerticalFormItem {...props} inverseSelection={inverseSelection} />
-      ),
-      FilterControlTitleBox: VerticalFilterControlTitleBox,
-      FilterControlTitle: VerticalFilterControlTitle,
-    };
-  }, [orientation, overflow, inverseSelection]);
+export const HorizontalOverflowFormItem = VerticalFormItem;
 
 const ToolTipContainer = styled.div`
   font-size: ${({ theme }) => theme.fontSize}px;
   display: flex;
 `;
 
-const RequiredFieldIndicator = () => (
+export const RequiredFieldIndicator = () => (
   <span
     css={(theme: SupersetTheme) => ({
       color: theme.colorError,
@@ -260,7 +213,11 @@ const RequiredFieldIndicator = () => (
   </span>
 );
 
-const DescriptionToolTip = ({ description }: { description: string }) => (
+export const DescriptionToolTip = ({
+  description,
+}: {
+  description: string;
+}) => (
   <ToolTipContainer>
     <Tooltip
       title={description}
@@ -285,120 +242,41 @@ const DescriptionToolTip = ({ description }: { description: string }) => (
   </ToolTipContainer>
 );
 
-const FilterControl = ({
-  dataMaskSelected,
-  filter,
-  icon,
-  onFilterSelectionChange,
-  inView,
-  showOverflow,
-  parentRef,
-  orientation = FilterBarOrientation.Vertical,
-  overflow = false,
-  clearAllTrigger,
-  onClearAllComplete,
-}: FilterControlProps) => {
-  const portalNode = useMemo(() => createHtmlPortalNode(), []);
-  const [isFilterActive, setIsFilterActive] = useState(false);
-
-  const { name = '<undefined>' } = filter;
-
-  const isFilterInScope = useIsFilterInScope();
-  const isMissingRequiredValue =
-    isFilterInScope(filter) &&
-    checkIsMissingRequiredValue(filter, filter.dataMask?.filterState);
-  const validateStatus = isMissingRequiredValue ? 'error' : undefined;
-  const isRequired = !!filter.controlValues?.enableEmptyFilter;
-  const inverseSelection = !!filter.controlValues?.inverseSelection;
-
-  const {
-    FilterControlContainer,
-    FormItem,
-    FilterControlTitleBox,
-    FilterControlTitle,
-  } = useFilterControlDisplay(orientation, overflow, inverseSelection);
-
-  const label = useMemo(
-    () => (
-      <FilterControlTitleBox>
-        <FilterControlTitle
-          id={`filter-name-${filter.id}`}
-          data-test="filter-control-name"
-        >
-          {name}
-        </FilterControlTitle>
-        {isRequired && <RequiredFieldIndicator />}
-        {filter.description?.trim() && (
-          <DescriptionToolTip description={filter.description} />
-        )}
-        <FilterStyledIcon data-test="filter-icon">{icon}</FilterStyledIcon>
-      </FilterControlTitleBox>
-    ),
-    [
-      FilterControlTitleBox,
-      FilterControlTitle,
-      name,
-      isRequired,
-      filter.description,
-      icon,
-    ],
-  );
-
-  const isScrolling = useContext(FilterBarScrollContext);
-  const filterCardPlacement = useMemo(() => {
+export const useFilterControlDisplay = (
+  orientation: FilterBarOrientation,
+  overflow: boolean,
+  inverseSelection?: boolean,
+) =>
+  useMemo(() => {
     if (orientation === FilterBarOrientation.Horizontal) {
       if (overflow) {
-        return FilterCardPlacement.Left;
+        return {
+          FilterControlContainer: HorizontalOverflowFilterControlContainer,
+          FormItem: (props: any) => (
+            <HorizontalOverflowFormItem
+              {...props}
+              inverseSelection={inverseSelection}
+            />
+          ),
+          FilterControlTitleBox: HorizontalOverflowFilterControlTitleBox,
+          FilterControlTitle: HorizontalOverflowFilterControlTitle,
+        };
       }
-      return FilterCardPlacement.Bottom;
+      return {
+        FilterControlContainer: HorizontalFilterControlContainer,
+        FormItem: (props: any) => (
+          <HorizontalFormItem {...props} inverseSelection={inverseSelection} />
+        ),
+        FilterControlTitleBox: HorizontalFilterControlTitleBox,
+        FilterControlTitle: HorizontalFilterControlTitle,
+      };
     }
-    return FilterCardPlacement.Right;
-  }, [orientation, overflow]);
-
-  return (
-    <>
-      <InPortal node={portalNode}>
-        <FilterValue
-          dataMaskSelected={dataMaskSelected}
-          filter={filter}
-          showOverflow={showOverflow}
-          onFilterSelectionChange={onFilterSelectionChange}
-          inView={inView}
-          parentRef={parentRef}
-          setFilterActive={setIsFilterActive}
-          orientation={orientation}
-          overflow={overflow}
-          validateStatus={validateStatus}
-          clearAllTrigger={clearAllTrigger}
-          onClearAllComplete={onClearAllComplete}
-        />
-      </InPortal>
-      <FilterControlContainer
-        layout={
-          orientation === FilterBarOrientation.Horizontal && !overflow
-            ? 'horizontal'
-            : 'vertical'
-        }
-      >
-        <FilterCard
-          filter={filter}
-          isVisible={!isFilterActive && !isScrolling}
-          placement={filterCardPlacement}
-        >
-          <div>
-            <FormItem
-              label={label}
-              htmlFor={filter.id}
-              required={filter?.controlValues?.enableEmptyFilter}
-              validateStatus={validateStatus}
-            >
-              <OutPortal node={portalNode} />
-            </FormItem>
-          </div>
-        </FilterCard>
-      </FilterControlContainer>
-    </>
-  );
-};
-
-export default memo(FilterControl);
+    return {
+      FilterControlContainer: VerticalFilterControlContainer,
+      FormItem: (props: any) => (
+        <VerticalFormItem {...props} inverseSelection={inverseSelection} />
+      ),
+      FilterControlTitleBox: VerticalFilterControlTitleBox,
+      FilterControlTitle: VerticalFilterControlTitle,
+    };
+  }, [orientation, overflow, inverseSelection]);
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.test.tsx
new file mode 100644
index 0000000000..4527347e12
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.test.tsx
@@ -0,0 +1,186 @@
+/**
+ * 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 { render, screen } from 'spec/helpers/testing-library';
+import { stateWithoutNativeFilters } from 'spec/fixtures/mockStore';
+import {
+  ChartCustomizationType,
+  ChartCustomizationDivider,
+} from '@superset-ui/core';
+import { FilterBarOrientation } from 'src/dashboard/types';
+import FilterControls from './FilterControls';
+
+const mockStore = {
+  ...stateWithoutNativeFilters,
+  dashboardInfo: {
+    ...stateWithoutNativeFilters.dashboardInfo,
+    filterBarOrientation: FilterBarOrientation.Vertical,
+  },
+};
+
+jest.mock('react-redux', () => ({
+  ...jest.requireActual('react-redux'),
+  useSelector: jest.fn(selector => selector(mockStore)),
+  useDispatch: () => jest.fn(),
+}));
+
+const defaultProps = {
+  dataMaskSelected: {},
+  onFilterSelectionChange: jest.fn(),
+  onPendingCustomizationDataMaskChange: jest.fn(),
+  chartCustomizationValues: [],
+};
+
+test('renders chart customization divider in vertical mode', () => {
+  const divider: ChartCustomizationDivider = {
+    id: 'CHART_CUSTOMIZATION_DIVIDER-abc123',
+    type: ChartCustomizationType.Divider,
+    title: 'Test Divider',
+    description: 'Test description',
+    removed: false,
+  };
+
+  render(
+    <FilterControls {...defaultProps} chartCustomizationValues={[divider]} />,
+  );
+
+  expect(
+    screen.getByRole('heading', { name: 'Test Divider' }),
+  ).toBeInTheDocument();
+});
+
+test('renders multiple chart customization dividers in vertical mode', () => {
+  const dividers: ChartCustomizationDivider[] = [
+    {
+      id: 'CHART_CUSTOMIZATION_DIVIDER-abc123',
+      type: ChartCustomizationType.Divider,
+      title: 'First Divider',
+      description: 'First description',
+      removed: false,
+    },
+    {
+      id: 'CHART_CUSTOMIZATION_DIVIDER-xyz789',
+      type: ChartCustomizationType.Divider,
+      title: 'Second Divider',
+      description: 'Second description',
+      removed: false,
+    },
+  ];
+
+  render(
+    <FilterControls {...defaultProps} chartCustomizationValues={dividers} />,
+  );
+
+  expect(
+    screen.getByRole('heading', { name: 'First Divider' }),
+  ).toBeInTheDocument();
+  expect(
+    screen.getByRole('heading', { name: 'Second Divider' }),
+  ).toBeInTheDocument();
+});
+
+test('renders chart customization divider in horizontal mode', () => {
+  const horizontalStore = {
+    ...mockStore,
+    dashboardInfo: {
+      ...mockStore.dashboardInfo,
+      filterBarOrientation: FilterBarOrientation.Horizontal,
+    },
+  };
+
+  const useSelector = jest.requireMock('react-redux').useSelector;
+  useSelector.mockImplementation(
+    (selector: (state: typeof horizontalStore) => unknown) =>
+      selector(horizontalStore),
+  );
+
+  const divider: ChartCustomizationDivider = {
+    id: 'CHART_CUSTOMIZATION_DIVIDER-abc123',
+    type: ChartCustomizationType.Divider,
+    title: 'Horizontal Divider',
+    description: 'Horizontal description',
+    removed: false,
+  };
+
+  render(
+    <FilterControls {...defaultProps} chartCustomizationValues={[divider]} />,
+  );
+
+  expect(
+    screen.getByRole('heading', { name: 'Horizontal Divider' }),
+  ).toBeInTheDocument();
+});
+
+test('does not render removed chart customization dividers', () => {
+  const dividers: ChartCustomizationDivider[] = [
+    {
+      id: 'CHART_CUSTOMIZATION_DIVIDER-abc123',
+      type: ChartCustomizationType.Divider,
+      title: 'Visible Divider',
+      description: 'Should be visible',
+      removed: false,
+    },
+    {
+      id: 'CHART_CUSTOMIZATION_DIVIDER-xyz789',
+      type: ChartCustomizationType.Divider,
+      title: 'Hidden Divider',
+      description: 'Should not be visible',
+      removed: true,
+    },
+  ];
+
+  render(
+    <FilterControls {...defaultProps} chartCustomizationValues={dividers} />,
+  );
+
+  expect(
+    screen.getByRole('heading', { name: 'Visible Divider' }),
+  ).toBeInTheDocument();
+  expect(
+    screen.queryByRole('heading', { name: 'Hidden Divider' }),
+  ).not.toBeInTheDocument();
+});
+
+test('renders divider with description icon in vertical mode when description exists', () => {
+  const divider: ChartCustomizationDivider = {
+    id: 'CHART_CUSTOMIZATION_DIVIDER-abc123',
+    type: ChartCustomizationType.Divider,
+    title: 'Divider With Description',
+    description: 'This is a detailed description',
+    removed: false,
+  };
+
+  render(
+    <FilterControls {...defaultProps} chartCustomizationValues={[divider]} />,
+  );
+
+  expect(
+    screen.getByRole('heading', { name: 'Divider With Description' }),
+  ).toBeInTheDocument();
+  expect(screen.queryByTestId('divider-description-icon')).toBeInTheDocument();
+});
+
+test('renders empty state when no chart customizations provided', () => {
+  const { container } = render(
+    <FilterControls {...defaultProps} chartCustomizationValues={[]} />,
+  );
+
+  expect(
+    container.querySelector('.chart-customization-item-wrapper'),
+  ).not.toBeInTheDocument();
+});
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 407ca54096..fd9cf7d6f7 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControls.tsx
@@ -33,6 +33,10 @@ import {
   Filter,
   Divider,
   isNativeFilterWithDataMask,
+  NativeFilterTarget,
+  ChartCustomization,
+  isChartCustomizationDivider,
+  ChartCustomizationDivider,
 } from '@superset-ui/core';
 import { css, SupersetTheme, useTheme, styled } from '@apache-superset/core/ui';
 import {
@@ -40,10 +44,11 @@ import {
   InPortal,
   OutPortal,
 } from 'react-reverse-portal';
-import { useSelector } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
 import {
   useDashboardHasTabs,
   useSelectFiltersInScope,
+  useSelectCustomizationsInScope,
 } from 'src/dashboard/components/nativeFilters/state';
 import { FilterBarOrientation, RootState } from 'src/dashboard/types';
 import {
@@ -54,19 +59,36 @@ import {
 import { Icons } from '@superset-ui/core/components/Icons';
 import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
 import { useChartLayoutItems } from 'src/dashboard/util/useChartLayoutItems';
+import { setPendingChartCustomization } from 'src/dashboard/actions/chartCustomizationActions';
+import { getInitialDataMask } from 'src/dataMask/reducer';
 import { FiltersOutOfScopeCollapsible } from '../FiltersOutOfScopeCollapsible';
+import { CustomizationsOutOfScopeCollapsible } from '../CustomizationsOutOfScopeCollapsible';
 import { useFilterControlFactory } from '../useFilterControlFactory';
 import { FiltersDropdownContent } from '../FiltersDropdownContent';
 import crossFiltersSelector from '../CrossFilters/selectors';
 import CrossFilter from '../CrossFilters/CrossFilter';
 import { useFilterOutlined } from '../useFilterOutlined';
 import { useChartsVerboseMaps } from '../utils';
-import GroupByFilterCard from '../../ChartCustomization/GroupByFilterCard';
-import { selectChartCustomizationItems } from '../../ChartCustomization/selectors';
+import FilterControl from './FilterControl';
+import FilterDivider from './FilterDivider';
+
+function addDataMaskToCustomization(
+  customization: ChartCustomization,
+  dataMaskSelected: DataMaskStateWithId,
+): ChartCustomization & { dataMask: DataMask } {
+  const dataMask =
+    dataMaskSelected[customization.id] ?? getInitialDataMask(customization.id);
+  return { ...customization, dataMask };
+}
 
 type FilterControlsProps = {
   dataMaskSelected: DataMaskStateWithId;
   onFilterSelectionChange: (filter: Filter, dataMask: DataMask) => void;
+  onPendingCustomizationDataMaskChange: (
+    customizationId: string,
+    dataMask: DataMask,
+  ) => void;
+  chartCustomizationValues: (ChartCustomization | ChartCustomizationDivider)[];
   clearAllTriggers?: Record<string, boolean>;
   onClearAllComplete?: (filterId: string) => void;
   hideHeader?: boolean;
@@ -119,11 +141,14 @@ const ChartCustomizationContent = styled.div`
 const FilterControls: FC<FilterControlsProps> = ({
   dataMaskSelected,
   onFilterSelectionChange,
+  onPendingCustomizationDataMaskChange,
+  chartCustomizationValues,
   clearAllTriggers,
   onClearAllComplete,
   hideHeader = false,
 }) => {
   const theme = useTheme();
+  const dispatch = useDispatch();
   const filterBarOrientation = useSelector<RootState, FilterBarOrientation>(
     ({ dashboardInfo }) => dashboardInfo.filterBarOrientation,
   );
@@ -140,8 +165,6 @@ const FilterControls: FC<FilterControlsProps> = ({
   const chartLayoutItems = useChartLayoutItems();
   const verboseMaps = useChartsVerboseMaps();
 
-  const chartCustomizationItems = useSelector(selectChartCustomizationItems);
-
   const selectedCrossFilters = useMemo(
     () =>
       crossFiltersSelector({
@@ -171,6 +194,14 @@ const FilterControls: FC<FilterControlsProps> = ({
   const [filtersInScope, filtersOutOfScope] =
     useSelectFiltersInScope(filtersWithValues);
 
+  const filteredChartCustomizationValues = useMemo(
+    () => chartCustomizationValues.filter(item => !item.removed),
+    [chartCustomizationValues],
+  );
+
+  const [customizationsInScope, customizationsOutOfScope] =
+    useSelectCustomizationsInScope(filteredChartCustomizationValues);
+
   const hasRequiredFirst = useMemo(
     () => filtersWithValues.some(filter => filter.requiredFirst),
     [filtersWithValues],
@@ -178,6 +209,8 @@ const FilterControls: FC<FilterControlsProps> = ({
 
   const dashboardHasTabs = useDashboardHasTabs();
   const showCollapsePanel = dashboardHasTabs && filtersWithValues.length > 0;
+  const showCustomizationCollapsePanel =
+    dashboardHasTabs && filteredChartCustomizationValues.length > 0;
 
   const [sectionsOpen, setSectionsOpen] = useState({
     filters: true,
@@ -191,6 +224,33 @@ const FilterControls: FC<FilterControlsProps> = ({
     }));
   }, []);
 
+  const handleChartCustomizationChange = useCallback(
+    (customizationItem: ChartCustomization, dataMask: DataMask) => {
+      const columnValue = dataMask.ownState?.column;
+      const existingTarget = customizationItem.targets?.[0] || {};
+
+      dispatch(
+        setPendingChartCustomization({
+          ...customizationItem,
+          targets: [
+            {
+              ...existingTarget,
+              ...(columnValue && {
+                column: {
+                  ...existingTarget.column,
+                  name: columnValue,
+                },
+              }),
+            },
+          ] as [Partial<NativeFilterTarget>],
+        }),
+      );
+
+      onPendingCustomizationDataMaskChange(customizationItem.id, dataMask);
+    },
+    [dispatch, onPendingCustomizationDataMaskChange],
+  );
+
   const renderer = useCallback(
     ({ id }: Filter | Divider, index: number | undefined) => {
       const filterIndex = filtersWithValues.findIndex(f => f.id === id);
@@ -207,6 +267,34 @@ const FilterControls: FC<FilterControlsProps> = ({
     [filtersWithValues, portalNodes],
   );
 
+  const customizationRenderer = useCallback(
+    (item: ChartCustomization | ChartCustomizationDivider, index: number) => {
+      if (isChartCustomizationDivider(item)) {
+        return (
+          <FilterDivider
+            key={item.id}
+            title={item.title}
+            description={item.description}
+            orientation={FilterBarOrientation.Vertical}
+          />
+        );
+      }
+      return (
+        <FilterControl
+          key={item.id}
+          filter={addDataMaskToCustomization(item, dataMaskSelected)}
+          dataMaskSelected={dataMaskSelected}
+          onFilterSelectionChange={(_, dataMask) =>
+            handleChartCustomizationChange(item, dataMask)
+          }
+          orientation={FilterBarOrientation.Vertical}
+          overflow={false}
+        />
+      );
+    },
+    [dataMaskSelected, handleChartCustomizationChange],
+  );
+
   const renderVerticalContent = useCallback(
     () => (
       <>
@@ -254,7 +342,7 @@ const FilterControls: FC<FilterControlsProps> = ({
           />
         )}
 
-        {chartCustomizationItems.length > 0 && (
+        {customizationsInScope.length > 0 && (
           <SectionContainer>
             {!hideHeader && (
               <SectionHeader
@@ -289,14 +377,9 @@ const FilterControls: FC<FilterControlsProps> = ({
             {(hideHeader || sectionsOpen.chartCustomization) && (
               <SectionContent>
                 <ChartCustomizationContent>
-                  {chartCustomizationItems
-                    .filter(item => !item.removed)
-                    .map(item => (
-                      <GroupByFilterCard
-                        key={item.id}
-                        customizationItem={item}
-                      />
-                    ))}
+                  {customizationsInScope.map((item, index) =>
+                    customizationRenderer(item, index),
+                  )}
                 </ChartCustomizationContent>
               </SectionContent>
             )}
@@ -305,6 +388,15 @@ const FilterControls: FC<FilterControlsProps> = ({
             )}
           </SectionContainer>
         )}
+
+        {showCustomizationCollapsePanel &&
+          (hideHeader || sectionsOpen.chartCustomization) && (
+            <CustomizationsOutOfScopeCollapsible
+              customizationsOutOfScope={customizationsOutOfScope}
+              renderer={customizationRenderer}
+              forceRender={false}
+            />
+          )}
       </>
     ),
     [
@@ -313,10 +405,16 @@ const FilterControls: FC<FilterControlsProps> = ({
       showCollapsePanel,
       filtersOutOfScope,
       hasRequiredFirst,
-      chartCustomizationItems,
+      customizationsInScope,
+      customizationsOutOfScope,
+      showCustomizationCollapsePanel,
+      customizationRenderer,
       sectionsOpen,
       toggleSection,
+      theme,
       hideHeader,
+      handleChartCustomizationChange,
+      dataMaskSelected,
     ],
   );
 
@@ -382,7 +480,7 @@ const FilterControls: FC<FilterControlsProps> = ({
     const dividerItems = [];
     if (
       (crossFilters.length > 0 || nativeFiltersInScope.length > 0) &&
-      chartCustomizationItems.length > 0
+      chartCustomizationValues.length > 0
     ) {
       dividerItems.push({
         id: 'chart-customization-divider',
@@ -401,9 +499,27 @@ const FilterControls: FC<FilterControlsProps> = ({
       });
     }
 
-    const chartCustomizations = chartCustomizationItems
-      .filter(item => !item.removed)
-      .map(item => ({
+    const chartCustomizations = customizationsInScope.map(item => {
+      if (isChartCustomizationDivider(item)) {
+        return {
+          id: `chart-customization-${item.id}`,
+          element: (
+            <div
+              className="chart-customization-item-wrapper"
+              css={css`
+                flex-shrink: 0;
+              `}
+            >
+              <FilterDivider
+                title={item.title}
+                description={item.description}
+                orientation={FilterBarOrientation.Horizontal}
+              />
+            </div>
+          ),
+        };
+      }
+      return {
         id: `chart-customization-${item.id}`,
         element: (
           <div
@@ -412,13 +528,19 @@ const FilterControls: FC<FilterControlsProps> = ({
               flex-shrink: 0;
             `}
           >
-            <GroupByFilterCard
-              customizationItem={item}
-              orientation="horizontal"
+            <FilterControl
+              filter={addDataMaskToCustomization(item, dataMaskSelected)}
+              dataMaskSelected={dataMaskSelected}
+              onFilterSelectionChange={(_, dataMask) =>
+                handleChartCustomizationChange(item, dataMask)
+              }
+              orientation={FilterBarOrientation.Horizontal}
+              overflow={false}
             />
           </div>
         ),
-      }));
+      };
+    });
 
     return [
       ...chartCustomizations,
@@ -431,8 +553,11 @@ const FilterControls: FC<FilterControlsProps> = ({
     renderer,
     rendererCrossFilter,
     selectedCrossFilters,
-    chartCustomizationItems,
+    customizationsInScope,
     theme,
+    handleChartCustomizationChange,
+    dataMaskSelected,
+    chartCustomizationValues.length,
   ]);
 
   const renderHorizontalContent = useCallback(
@@ -471,17 +596,27 @@ const FilterControls: FC<FilterControlsProps> = ({
           dropdownContent={
             overflowedFiltersInScope.length ||
             overflowedCrossFilters.length ||
-            (filtersOutOfScope.length && showCollapsePanel)
+            (filtersOutOfScope.length && showCollapsePanel) ||
+            (customizationsOutOfScope.length && showCustomizationCollapsePanel)
               ? () => (
-                  <FiltersDropdownContent
-                    overflowedCrossFilters={overflowedCrossFilters}
-                    filtersInScope={overflowedFiltersInScope}
-                    filtersOutOfScope={filtersOutOfScope}
-                    renderer={renderer}
-                    rendererCrossFilter={rendererCrossFilter}
-                    showCollapsePanel={showCollapsePanel}
-                    forceRenderOutOfScope={hasRequiredFirst}
-                  />
+                  <>
+                    <FiltersDropdownContent
+                      overflowedCrossFilters={overflowedCrossFilters}
+                      filtersInScope={overflowedFiltersInScope}
+                      filtersOutOfScope={filtersOutOfScope}
+                      renderer={renderer}
+                      rendererCrossFilter={rendererCrossFilter}
+                      showCollapsePanel={showCollapsePanel}
+                      forceRenderOutOfScope={hasRequiredFirst}
+                    />
+                    {showCustomizationCollapsePanel && (
+                      <CustomizationsOutOfScopeCollapsible
+                        customizationsOutOfScope={customizationsOutOfScope}
+                        renderer={customizationRenderer}
+                        forceRender={false}
+                      />
+                    )}
+                  </>
                 )
               : undefined
           }
@@ -508,6 +643,9 @@ const FilterControls: FC<FilterControlsProps> = ({
       overflowedCrossFilters,
       filtersOutOfScope,
       showCollapsePanel,
+      customizationsOutOfScope,
+      showCustomizationCollapsePanel,
+      customizationRenderer,
       renderer,
       rendererCrossFilter,
       hasRequiredFirst,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index 2c1233d757..238538e7c6 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -39,6 +39,7 @@ import {
   SuperChart,
   ClientErrorObject,
   getClientErrorObject,
+  isChartCustomization,
 } from '@superset-ui/core';
 import { styled } from '@apache-superset/core/ui';
 import { useDispatch, useSelector } from 'react-redux';
@@ -52,6 +53,10 @@ import {
   onFiltersRefreshSuccess,
   setDirectPathToChild,
 } from 'src/dashboard/actions/dashboardState';
+import {
+  setHoveredChartCustomization,
+  unsetHoveredChartCustomization,
+} from 'src/dashboard/actions/nativeFilters';
 import { RESPONSIVE_WIDTH } from 'src/filters/components/common';
 import { dispatchHoverAction, dispatchFocusAction } from './utils';
 import { FilterControlProps } from './types';
@@ -78,7 +83,6 @@ const StyledDiv = styled.div<{
 `;
 
 const queriesDataPlaceholder = [{ data: [{}] }];
-const behaviors = [Behavior.NativeFilter];
 
 const useShouldFilterRefresh = () => {
   const isDashboardRefreshing = useSelector<RootState, boolean>(
@@ -92,7 +96,9 @@ const useShouldFilterRefresh = () => {
   return !isDashboardRefreshing && isFilterRefreshing;
 };
 
-const FilterValue: FC<FilterControlProps> = ({
+export type FilterValueProps = FilterControlProps;
+
+const FilterValue: FC<FilterValueProps> = ({
   dataMaskSelected,
   filter,
   onFilterSelectionChange,
@@ -106,10 +112,21 @@ const FilterValue: FC<FilterControlProps> = ({
   clearAllTrigger,
   onClearAllComplete,
 }) => {
-  const { id, targets, filterType, adhoc_filters, time_range } = filter;
+  const { id, targets, filterType } = filter;
+  const isCustomization = isChartCustomization(filter);
+  const adhocFilters = isCustomization ? undefined : filter.adhoc_filters;
+  const timeRange = isCustomization ? undefined : filter.time_range;
+  const granularitySqla = isCustomization ? undefined : filter.granularity_sqla;
   const metadata = getChartMetadataRegistry().get(filterType);
   const dependencies = useFilterDependencies(id, dataMaskSelected);
   const shouldRefresh = useShouldFilterRefresh();
+
+  const behaviors = useMemo(
+    () => [
+      isCustomization ? Behavior.ChartCustomization : Behavior.NativeFilter,
+    ],
+    [isCustomization],
+  );
   const [state, setState] = useState<ChartDataResponseResult[]>([]);
   const dashboardId = useSelector<RootState, number>(
     state => state.dashboardInfo.id,
@@ -122,12 +139,12 @@ const FilterValue: FC<FilterControlProps> = ({
   const [ownState, setOwnState] = useState<JsonObject>({});
   const [inViewFirstTime, setInViewFirstTime] = useState(inView);
   const inputRef = useRef<HTMLInputElement>(null);
-  const [target] = targets;
+  const [target] = targets || [];
   const {
     datasetId,
     column = {},
-  }: Partial<{ datasetId: number; column: { name?: string } }> = target;
-  const { name: groupby } = column;
+  }: Partial<{ datasetId: number; column: { name?: string } }> = target || {};
+  const groupby = column?.name;
   const hasDataSource = !!datasetId;
   const [isLoading, setIsLoading] = useState<boolean>(hasDataSource);
   const [isRefreshing, setIsRefreshing] = useState(false);
@@ -158,17 +175,18 @@ const FilterValue: FC<FilterControlProps> = ({
       datasetId,
       dependencies,
       groupby,
-      adhoc_filters,
-      time_range,
+      adhoc_filters: adhocFilters,
+      time_range: timeRange,
+      granularity_sqla: granularitySqla,
       dashboardId,
     });
     const filterOwnState = filter.dataMask?.ownState || {};
-    if (filter?.cascadeParentIds?.length) {
+    if ((filter.cascadeParentIds ?? []).length) {
       // Prevent unnecessary backend requests by validating parent filter selections first
 
       let selectedParentFilterValueCounts = 0;
 
-      filter?.cascadeParentIds?.forEach(pId => {
+      (filter.cascadeParentIds ?? []).forEach(pId => {
         const extraFormData = dataMaskSelected?.[pId]?.extraFormData;
         if (extraFormData?.filters?.length) {
           selectedParentFilterValueCounts += extraFormData.filters.length;
@@ -282,27 +300,39 @@ const FilterValue: FC<FilterControlProps> = ({
   );
 
   const setFocusedFilter = useCallback(() => {
-    // don't highlight charts in scope if filter was focused programmatically
+    if (isCustomization) {
+      return;
+    }
     if (outlinedFilterId !== id) {
       dispatchFocusAction(dispatch, id);
     }
-  }, [dispatch, id, outlinedFilterId]);
+  }, [dispatch, id, outlinedFilterId, isCustomization]);
 
   const unsetFocusedFilter = useCallback(() => {
+    if (isCustomization) {
+      return;
+    }
     dispatchFocusAction(dispatch);
     if (outlinedFilterId === id) {
       dispatch(setDirectPathToChild([]));
     }
-  }, [dispatch, id, outlinedFilterId]);
+  }, [dispatch, id, outlinedFilterId, isCustomization]);
 
-  const setHoveredFilter = useCallback(
-    () => dispatchHoverAction(dispatch, id),
-    [dispatch, id],
-  );
-  const unsetHoveredFilter = useCallback(
-    () => dispatchHoverAction(dispatch),
-    [dispatch],
-  );
+  const setHoveredFilter = useCallback(() => {
+    if (isCustomization) {
+      dispatch(setHoveredChartCustomization(id));
+    } else {
+      dispatchHoverAction(dispatch, id);
+    }
+  }, [dispatch, id, isCustomization]);
+
+  const unsetHoveredFilter = useCallback(() => {
+    if (isCustomization) {
+      dispatch(unsetHoveredChartCustomization());
+    } else {
+      dispatchHoverAction(dispatch);
+    }
+  }, [dispatch, isCustomization]);
 
   const hooks = useMemo(
     () => ({
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/GroupByFilterCard.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
similarity index 60%
rename from superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/GroupByFilterCard.tsx
rename to superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
index 37a585b5a4..7145784699 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/ChartCustomization/GroupByFilterCard.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/GroupByFilterCard.tsx
@@ -18,8 +18,17 @@
  */
 import { FC, useCallback, useEffect, useMemo, useState } from 'react';
 import { t } from '@apache-superset/core';
-import { DataMaskStateWithId, useTruncation } from '@superset-ui/core';
-import { styled, css, useTheme } from '@apache-superset/core/ui';
+import {
+  DataMask,
+  DataMaskStateWithId,
+  Filter,
+  useTruncation,
+  ChartCustomization,
+  NativeFilterTarget,
+  Filters,
+  NativeFilterType,
+} from '@superset-ui/core';
+import { styled, css, useTheme, SupersetTheme } from '@apache-superset/core/ui';
 import {
   Typography,
   Select,
@@ -31,18 +40,28 @@ import {
 } from '@superset-ui/core/components';
 import { useDispatch, useSelector } from 'react-redux';
 import { RootState } from 'src/dashboard/types';
-import {
-  setPendingChartCustomization,
-  loadChartCustomizationData,
-} from 'src/dashboard/actions/chartCustomizationActions';
+import { setPendingChartCustomization } from 'src/dashboard/actions/chartCustomizationActions';
 import { TooltipWithTruncation } from 'src/dashboard/components/nativeFilters/FilterCard/TooltipWithTruncation';
-import { dispatchChartCustomizationHoverAction } from '../FilterBar/FilterControls/utils';
-import { mergeExtraFormData } from '../utils';
-import { ChartCustomizationItem } from './types';
+import { addDangerToast } from 'src/components/MessageToasts/actions';
+import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
+import { dispatchChartCustomizationHoverAction } from './utils';
+import { mergeExtraFormData } from '../../utils';
+
+interface ColumnApiResponse {
+  column_name?: string;
+  name?: string;
+  verbose_name?: string;
+  filterable?: boolean;
+}
 
 interface GroupByFilterCardProps {
-  customizationItem: ChartCustomizationItem;
+  customizationItem: ChartCustomization;
   orientation?: 'vertical' | 'horizontal';
+  dataMaskSelected?: DataMaskStateWithId;
+  onFilterSelectionChange?: (
+    filter: Filter | ChartCustomization,
+    dataMask: DataMask,
+  ) => void;
 }
 
 const Row = styled.div`
@@ -88,14 +107,10 @@ const FilterTitle = styled(Typography.Text)`
   margin-bottom: ${({ theme }) => theme.sizeUnit}px;
   display: flex;
   align-items: center;
-  cursor: pointer;
+  cursor: default;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-
-  &:hover {
-    color: ${({ theme }) => theme.colorPrimary};
-  }
 `;
 
 const HorizontalFormItem = styled(FormItem)`
@@ -137,7 +152,6 @@ const HorizontalFormItem = styled(FormItem)`
     width: 100%;
   }
 
-  /* Allow dropdown to expand beyond form item width */
   .ant-select-dropdown {
     min-width: 200px !important;
     max-width: 400px !important;
@@ -152,7 +166,7 @@ const ToolTipContainer = styled.div`
 
 const RequiredFieldIndicator = () => (
   <span
-    css={(theme: any) => ({
+    css={(theme: SupersetTheme) => ({
       color: theme.colorError,
       fontSize: `${theme.fontSizeSM}px`,
       paddingLeft: '1px',
@@ -188,53 +202,32 @@ const DescriptionTooltip = ({ description }: { description: string }) => (
 );
 
 const GroupByFilterCardContent: FC<{
-  customizationItem: ChartCustomizationItem;
+  customizationItem: ChartCustomization;
   hidePopover: () => void;
... 10251 lines suppressed ...