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 2024/10/15 13:48:12 UTC

(superset) branch geido/refactor/native-filters-form-perf created (now 2e44285172)

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

diegopucci pushed a change to branch geido/refactor/native-filters-form-perf
in repository https://gitbox.apache.org/repos/asf/superset.git


      at 2e44285172 refactor(Dashboard): Native filters form update endpoint

This branch includes the following new commits:

     new 2e44285172 refactor(Dashboard): Native filters form update endpoint

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(superset) 01/01: refactor(Dashboard): Native filters form update endpoint

Posted by di...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch geido/refactor/native-filters-form-perf
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 2e44285172580f50813646cfef6c08b05fb42458
Author: Diego Pucci <di...@gmail.com>
AuthorDate: Tue Oct 15 16:47:53 2024 +0300

    refactor(Dashboard): Native filters form update endpoint
---
 .../src/dashboard/actions/dashboardInfo.ts         |   6 +
 .../src/dashboard/actions/dashboardState.js        |   4 +-
 .../src/dashboard/actions/nativeFilters.ts         | 108 +++++-------
 .../FilterBar/FilterConfigurationLink/index.tsx    |   8 +-
 .../FilterBar/FilterControls/FilterValue.tsx       |   1 -
 .../FilterScope/FilterScope.test.tsx               |   1 +
 .../FiltersConfigForm/FiltersConfigForm.tsx        |  61 +++++--
 .../FiltersConfigForm/getControlItemsMap.test.tsx  |   1 +
 .../FiltersConfigForm/getControlItemsMap.tsx       |   4 +
 .../FiltersConfigModal/FiltersConfigModal.test.tsx | 196 ++++++++++++++++++---
 .../FiltersConfigModal/FiltersConfigModal.tsx      | 147 ++++++++++++----
 .../nativeFilters/FiltersConfigModal/types.ts      |  11 ++
 .../nativeFilters/FiltersConfigModal/utils.ts      | 115 ++++++------
 .../src/dashboard/reducers/dashboardInfo.js        |  37 ++++
 .../src/dashboard/reducers/nativeFilters.ts        |  45 ++++-
 superset-frontend/src/dataMask/actions.ts          |  35 ++--
 superset-frontend/src/dataMask/reducer.ts          |  41 ++++-
 superset/commands/dashboard/exceptions.py          |   4 +
 superset/commands/dashboard/update.py              |  17 +-
 superset/constants.py                              |   1 +
 superset/daos/dashboard.py                         |  64 +++++++
 superset/dashboards/api.py                         |  97 +++++++++-
 superset/dashboards/schemas.py                     |   6 +
 tests/integration_tests/dashboards/api_tests.py    | 175 ++++++++++++++++++
 24 files changed, 960 insertions(+), 225 deletions(-)

diff --git a/superset-frontend/src/dashboard/actions/dashboardInfo.ts b/superset-frontend/src/dashboard/actions/dashboardInfo.ts
index 14659b9fc3..ea25e8c715 100644
--- a/superset-frontend/src/dashboard/actions/dashboardInfo.ts
+++ b/superset-frontend/src/dashboard/actions/dashboardInfo.ts
@@ -29,11 +29,17 @@ import {
 import { onSave } from './dashboardState';
 
 export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED';
+export const DASHBOARD_INFO_FILTERS_CHANGED = 'DASHBOARD_INFO_FILTERS_CHANGED';
 
 // updates partially changed dashboard info
 export function dashboardInfoChanged(newInfo: { metadata: any }) {
   return { type: DASHBOARD_INFO_UPDATED, newInfo };
 }
+
+export function nativeFiltersConfigChanged(newInfo: Record<string, any>) {
+  return { type: DASHBOARD_INFO_FILTERS_CHANGED, newInfo };
+}
+
 export const SAVE_CHART_CONFIG_BEGIN = 'SAVE_CHART_CONFIG_BEGIN';
 export const SAVE_CHART_CONFIG_COMPLETE = 'SAVE_CHART_CONFIG_COMPLETE';
 export const SAVE_CHART_CONFIG_FAIL = 'SAVE_CHART_CONFIG_FAIL';
diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js
index 9b0c181821..443db787e0 100644
--- a/superset-frontend/src/dashboard/actions/dashboardState.js
+++ b/superset-frontend/src/dashboard/actions/dashboardState.js
@@ -63,7 +63,7 @@ import {
 } from './dashboardInfo';
 import { fetchDatasourceMetadata } from './datasources';
 import { updateDirectPathToFilter } from './dashboardFilters';
-import { SET_FILTER_CONFIG_COMPLETE } from './nativeFilters';
+import { SET_IN_SCOPE_STATUS_OF_FILTERS } from './nativeFilters';
 import getOverwriteItems from '../util/getOverwriteItems';
 import {
   applyColors,
@@ -337,7 +337,7 @@ export function saveDashboardRequest(data, id, saveType) {
         }
         if (metadata.native_filter_configuration) {
           dispatch({
-            type: SET_FILTER_CONFIG_COMPLETE,
+            type: SET_IN_SCOPE_STATUS_OF_FILTERS,
             filterConfig: metadata.native_filter_configuration,
           });
         }
diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts
index 968805dbb7..f594080b70 100644
--- a/superset-frontend/src/dashboard/actions/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts
@@ -19,28 +19,31 @@
 import { FilterConfiguration, Filters, makeApi } from '@superset-ui/core';
 import { Dispatch } from 'redux';
 import { cloneDeep } from 'lodash';
-import {
-  SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
-  setDataMaskForFilterConfigComplete,
-} from 'src/dataMask/actions';
+import { setDataMaskForFilterChangesComplete } from 'src/dataMask/actions';
 import { HYDRATE_DASHBOARD } from './hydrate';
-import { dashboardInfoChanged } from './dashboardInfo';
-import { DashboardInfo } from '../types';
+import {
+  dashboardInfoChanged,
+  nativeFiltersConfigChanged,
+} from './dashboardInfo';
+import { SaveFilterChangesType } from '../components/nativeFilters/FiltersConfigModal/types';
 
-export const SET_FILTER_CONFIG_BEGIN = 'SET_FILTER_CONFIG_BEGIN';
-export interface SetFilterConfigBegin {
-  type: typeof SET_FILTER_CONFIG_BEGIN;
+export const SET_NATIVE_FILTERS_CONFIG_BEGIN =
+  'SET_NATIVE_FILTERS_CONFIG_BEGIN';
+export interface SetNativeFiltersConfigBegin {
+  type: typeof SET_NATIVE_FILTERS_CONFIG_BEGIN;
   filterConfig: FilterConfiguration;
 }
 
-export const SET_FILTER_CONFIG_COMPLETE = 'SET_FILTER_CONFIG_COMPLETE';
-export interface SetFilterConfigComplete {
-  type: typeof SET_FILTER_CONFIG_COMPLETE;
-  filterConfig: FilterConfiguration;
+export const SET_NATIVE_FILTERS_CONFIG_COMPLETE =
+  'SET_NATIVE_FILTERS_CONFIG_COMPLETE';
+export interface SetNativeFiltersConfigComplete {
+  type: typeof SET_NATIVE_FILTERS_CONFIG_COMPLETE;
+  filterChanges: SaveFilterChangesType;
 }
-export const SET_FILTER_CONFIG_FAIL = 'SET_FILTER_CONFIG_FAIL';
-export interface SetFilterConfigFail {
-  type: typeof SET_FILTER_CONFIG_FAIL;
+
+export const SET_NATIVE_FILTERS_CONFIG_FAIL = 'SET_NATIVE_FILTERS_CONFIG_FAIL';
+export interface SetNativeFiltersConfigFail {
+  type: typeof SET_NATIVE_FILTERS_CONFIG_FAIL;
   filterConfig: FilterConfiguration;
 }
 export const SET_IN_SCOPE_STATUS_OF_FILTERS = 'SET_IN_SCOPE_STATUS_OF_FILTERS';
@@ -49,60 +52,45 @@ export interface SetInScopeStatusOfFilters {
   filterConfig: FilterConfiguration;
 }
 
+const isFilterChangesEmpty = (filterChanges: SaveFilterChangesType) =>
+  Object.values(filterChanges).every(
+    array => Array.isArray(array) && !array.length,
+  );
+
 export const setFilterConfiguration =
-  (filterConfig: FilterConfiguration) =>
+  (filterChanges: SaveFilterChangesType) =>
   async (dispatch: Dispatch, getState: () => any) => {
+    if (isFilterChangesEmpty(filterChanges)) {
+      return;
+    }
+
+    const { id } = getState().dashboardInfo;
+    const oldFilters = getState().nativeFilters?.filters;
+
     dispatch({
-      type: SET_FILTER_CONFIG_BEGIN,
-      filterConfig,
+      type: SET_NATIVE_FILTERS_CONFIG_BEGIN,
+      filterChanges,
     });
-    const { id, metadata } = getState().dashboardInfo;
-    const oldFilters = getState().nativeFilters?.filters;
 
-    // TODO extract this out when makeApi supports url parameters
-    const updateDashboard = makeApi<
-      Partial<DashboardInfo>,
-      { result: DashboardInfo }
+    const updateFilters = makeApi<
+      SaveFilterChangesType,
+      { result: SaveFilterChangesType }
     >({
       method: 'PUT',
-      endpoint: `/api/v1/dashboard/${id}`,
+      endpoint: `/api/v1/dashboard/${id}/filters`,
     });
-
-    const mergedFilterConfig = filterConfig.map(filter => {
-      const oldFilter = oldFilters[filter.id];
-      if (!oldFilter) {
-        return filter;
-      }
-      return { ...oldFilter, ...filter };
-    });
-
     try {
-      const response = await updateDashboard({
-        json_metadata: JSON.stringify({
-          ...metadata,
-          native_filter_configuration: mergedFilterConfig,
-        }),
-      });
-      dispatch(
-        dashboardInfoChanged({
-          metadata: JSON.parse(response.result.json_metadata),
-        }),
-      );
+      const response = await updateFilters(filterChanges);
+      dispatch(nativeFiltersConfigChanged(response.result));
       dispatch({
-        type: SET_FILTER_CONFIG_COMPLETE,
-        filterConfig: mergedFilterConfig,
+        type: SET_NATIVE_FILTERS_CONFIG_COMPLETE,
+        filterChanges,
       });
-      dispatch(
-        setDataMaskForFilterConfigComplete(mergedFilterConfig, oldFilters),
-      );
+      dispatch(setDataMaskForFilterChangesComplete(filterChanges, oldFilters));
     } catch (err) {
       dispatch({
-        type: SET_FILTER_CONFIG_FAIL,
-        filterConfig: mergedFilterConfig,
-      });
-      dispatch({
-        type: SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL,
-        filterConfig: mergedFilterConfig,
+        type: SET_NATIVE_FILTERS_CONFIG_FAIL,
+        filterConfig: filterChanges,
       });
     }
   };
@@ -221,9 +209,9 @@ export function updateCascadeParentIds(
 }
 
 export type AnyFilterAction =
-  | SetFilterConfigBegin
-  | SetFilterConfigComplete
-  | SetFilterConfigFail
+  | SetNativeFiltersConfigBegin
+  | SetNativeFiltersConfigComplete
+  | SetNativeFiltersConfigFail
   | SetInScopeStatusOfFilters
   | SetBootstrapData
   | SetFocusedNativeFilter
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx
index d7e8564eb5..7be1e7814c 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterConfigurationLink/index.tsx
@@ -21,9 +21,10 @@ import { ReactNode, FC, useCallback, useState, memo } from 'react';
 import { useDispatch } from 'react-redux';
 import { setFilterConfiguration } from 'src/dashboard/actions/nativeFilters';
 import Button from 'src/components/Button';
-import { FilterConfiguration, styled } from '@superset-ui/core';
+import { styled } from '@superset-ui/core';
 import FiltersConfigModal from 'src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal';
 import { getFilterBarTestId } from '../utils';
+import { SaveFilterChangesType } from '../../FiltersConfigModal/types';
 
 export interface FCBProps {
   createNewOnOpen?: boolean;
@@ -46,14 +47,13 @@ export const FilterConfigurationLink: FC<FCBProps> = ({
 }) => {
   const dispatch = useDispatch();
   const [isOpen, setOpen] = useState(false);
-
   const close = useCallback(() => {
     setOpen(false);
   }, [setOpen]);
 
   const submit = useCallback(
-    async (filterConfig: FilterConfiguration) => {
-      dispatch(await setFilterConfiguration(filterConfig));
+    async (filterChanges: SaveFilterChangesType) => {
+      dispatch(await setFilterConfiguration(filterChanges));
       close();
     },
     [dispatch, close],
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 2d85f413c3..45ccd4dd41 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -182,7 +182,6 @@ const FilterValue: FC<FilterControlProps> = ({
           if (isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) {
             // deal with getChartDataRequest transforming the response data
             const result = 'result' in json ? json.result[0] : json;
-
             if (response.status === 200) {
               setState([result]);
               handleFilterLoadFinish();
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx
index 93f3dd6ec1..aece899a16 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx
@@ -47,6 +47,7 @@ describe('FilterScope', () => {
     activeFilterPanelKeys: `DefaultFilterId-${FilterPanels.configuration.key}`,
     isActive: true,
     validateDependencies: jest.fn(),
+    onModifyFilter: jest.fn(),
   };
 
   const MockModal = ({ scope }: { scope?: object }) => {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 77a423df76..b7c66412fa 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -39,8 +39,9 @@ import {
   t,
   ClientErrorObject,
   getClientErrorObject,
+  SLOW_DEBOUNCE,
 } from '@superset-ui/core';
-import { isEqual } from 'lodash';
+import { debounce, isEqual } from 'lodash';
 import {
   forwardRef,
   useCallback,
@@ -306,6 +307,7 @@ export interface FiltersConfigFormProps {
   filterToEdit?: Filter;
   removedFilters: Record<string, FilterRemoval>;
   restoreFilter: (filterId: string) => void;
+  onModifyFilter: (filterId: string) => void;
   form: FormInstance<NativeFiltersForm>;
   getAvailableFilters: (
     filterId: string,
@@ -346,6 +348,7 @@ const FiltersConfigForm = (
     restoreFilter,
     handleActiveFilterPanelChange,
     setErroredFilters,
+    onModifyFilter,
     validateDependencies,
     getDependencySuggestion,
     isActive,
@@ -372,6 +375,12 @@ const FiltersConfigForm = (
   const formValues = filters?.[filterId];
   const formFilter = formValues || undoFormValues || defaultFormFilter;
 
+  const handleModifyFilter = useCallback(() => {
+    if (onModifyFilter) {
+      onModifyFilter(filterId);
+    }
+  }, [onModifyFilter, filterId]);
+
   const dependencies: string[] =
     formFilter?.dependencies || filterToEdit?.cascadeParentIds || [];
 
@@ -412,12 +421,28 @@ const FiltersConfigForm = (
     filterToEdit?.targets[0]?.datasetId ??
     mostUsedDataset(loadedDatasets, charts);
 
+  const formChanged = useCallback(() => {
+    form.setFields([
+      {
+        name: 'changed',
+        value: true,
+      },
+    ]);
+    handleModifyFilter();
+  }, [form, handleModifyFilter]);
+
+  const debouncedFormChanged = useCallback(
+    debounce(formChanged, SLOW_DEBOUNCE),
+    [],
+  );
+
   const { controlItems = {}, mainControlItems = {} } = formFilter
     ? getControlItemsMap({
         expanded,
         datasetId,
         disabled: false,
         forceUpdate,
+        formChanged,
         form,
         filterId,
         filterType: formFilter?.filterType,
@@ -488,7 +513,6 @@ const FiltersConfigForm = (
         groupby: formFilter?.column,
         ...formFilter,
       });
-
       formData.extra_form_data = dependenciesDefaultValues;
 
       setNativeFilterFieldValuesWrapper({
@@ -549,6 +573,7 @@ const FiltersConfigForm = (
     groupby: hasColumn ? formFilter?.column : undefined,
     ...formFilter,
   });
+
   newFormData.extra_form_data = dependenciesDefaultValues;
 
   const [hasDefaultValue, isRequired, defaultValueTooltip, setHasDefaultValue] =
@@ -557,15 +582,6 @@ const FiltersConfigForm = (
   const showDataset =
     !datasetId || datasetDetails || formFilter?.dataset?.label;
 
-  const formChanged = useCallback(() => {
-    form.setFields([
-      {
-        name: 'changed',
-        value: true,
-      },
-    ]);
-  }, [form]);
-
   const updateFormValues = useCallback(
     (values: any) => {
       setNativeFilterFieldValues(form, filterId, values);
@@ -794,6 +810,7 @@ const FiltersConfigForm = (
             granularity_sqla: column,
           });
           forceUpdate();
+          formChanged();
         }}
       />
     </StyledRowFormItem>
@@ -817,7 +834,7 @@ const FiltersConfigForm = (
             hidden
             initialValue={NativeFilterType.NativeFilter}
           >
-            <Input />
+            <Input onChange={formChanged} />
           </StyledFormItem>
           <StyledFormItem
             expanded={expanded}
@@ -826,7 +843,10 @@ const FiltersConfigForm = (
             initialValue={filterToEdit?.name}
             rules={[{ required: !isRemoved, message: t('Name is required') }]}
           >
-            <Input {...getFiltersConfigModalTestId('name-input')} />
+            <Input
+              {...getFiltersConfigModalTestId('name-input')}
+              onChange={debouncedFormChanged}
+            />
           </StyledFormItem>
           <StyledFormItem
             expanded={expanded}
@@ -870,6 +890,7 @@ const FiltersConfigForm = (
                   column: null,
                 });
                 forceUpdate();
+                formChanged();
               }}
             />
           </StyledFormItem>
@@ -920,6 +941,7 @@ const FiltersConfigForm = (
                       });
                     }
                     forceUpdate();
+                    formChanged();
                   }}
                 />
               </StyledFormItem>
@@ -1018,6 +1040,7 @@ const FiltersConfigForm = (
                             adhoc_filters: filters,
                           });
                           forceUpdate();
+                          formChanged();
                           validatePreFilter();
                         }}
                         label={
@@ -1050,6 +1073,7 @@ const FiltersConfigForm = (
                               time_range: timeRange,
                             });
                             forceUpdate();
+                            formChanged();
                             validatePreFilter();
                           }}
                         />
@@ -1085,6 +1109,7 @@ const FiltersConfigForm = (
                       <Radio.Group
                         onChange={value => {
                           onSortChanged(value.target.value);
+                          formChanged();
                         }}
                       >
                         <Radio value>{t('Sort ascending')}</Radio>
@@ -1124,6 +1149,7 @@ const FiltersConfigForm = (
                               });
                               forceUpdate();
                             }
+                            formChanged();
                           }}
                         />
                       </StyledRowSubFormItem>
@@ -1156,9 +1182,10 @@ const FiltersConfigForm = (
                       }
                     >
                       <Radio.Group
-                        onChange={value =>
-                          onEnableSingleValueChanged(value.target.value)
-                        }
+                        onChange={value => {
+                          onEnableSingleValueChanged(value.target.value);
+                          formChanged();
+                        }}
                       >
                         <Radio value={SingleValueType.Minimum}>
                           {t('Minimum')}
@@ -1187,7 +1214,7 @@ const FiltersConfigForm = (
               initialValue={filterToEdit?.description}
               label={<StyledLabel>{t('Description')}</StyledLabel>}
             >
-              <TextArea />
+              <TextArea onChange={debouncedFormChanged} />
             </StyledFormItem>
             <CleanFormItem
               name={['filters', filterId, 'defaultValueQueriesData']}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
index 93d31990ad..9d2fc12e0c 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.test.tsx
@@ -72,6 +72,7 @@ const createProps: () => ControlItemsProps = () => ({
   filterId: 'filterId',
   filterToEdit: filterMock,
   filterType: 'filterType',
+  formChanged: jest.fn(),
 });
 
 const createControlItems = () => [
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
index 62fd4917ec..ed92809988 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -48,6 +48,7 @@ export interface ControlItemsProps {
   datasetId: number;
   disabled: boolean;
   forceUpdate: Function;
+  formChanged: Function;
   form: FormInstance<NativeFiltersForm>;
   filterId: string;
   filterType: string;
@@ -65,6 +66,7 @@ export default function getControlItemsMap({
   datasetId,
   disabled,
   forceUpdate,
+  formChanged,
   form,
   filterId,
   filterType,
@@ -137,6 +139,7 @@ export default function getControlItemsMap({
                   defaultDataMask: null,
                 });
                 forceUpdate();
+                formChanged();
               }}
             />
           </StyledFormItem>
@@ -200,6 +203,7 @@ export default function getControlItemsMap({
                       defaultDataMask: null,
                     });
                   }
+                  formChanged();
                   forceUpdate();
                 }}
               >
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
index a56e969f1b..8e61ee736a 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx
@@ -23,7 +23,12 @@ import chartQueries from 'spec/fixtures/mockChartQueries';
 import { dashboardLayout } from 'spec/fixtures/mockDashboardLayout';
 import mockDatasource, { datasourceId, id } from 'spec/fixtures/mockDatasource';
 import { buildNativeFilter } from 'spec/fixtures/mockNativeFilters';
-import { render, screen, waitFor } from 'spec/helpers/testing-library';
+import {
+  fireEvent,
+  render,
+  screen,
+  waitFor,
+} from 'spec/helpers/testing-library';
 import {
   RangeFilterPlugin,
   SelectFilterPlugin,
@@ -147,6 +152,10 @@ beforeAll(() => {
   new MainPreset().register();
 });
 
+afterEach(() => {
+  jest.restoreAllMocks();
+});
+
 function defaultRender(initialState: any = defaultState(), modalProps = props) {
   return render(<FiltersConfigModal {...modalProps} />, {
     initialState,
@@ -373,29 +382,24 @@ test('deletes a filter', async () => {
     dashboardLayout,
   };
   const onSave = jest.fn();
+
   defaultRender(state, {
     ...props,
     createNewOnOpen: false,
     onSave,
   });
   const removeButtons = screen.getAllByRole('img', { name: 'trash' });
-  // remove NATIVE_FILTER-3 which isn't a dependency of any other filter
   userEvent.click(removeButtons[2]);
+
   userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+
   await waitFor(() =>
     expect(onSave).toHaveBeenCalledWith(
-      expect.arrayContaining([
-        expect.objectContaining({
-          type: 'NATIVE_FILTER',
-          id: 'NATIVE_FILTER-1',
-          cascadeParentIds: ['NATIVE_FILTER-2'],
-        }),
-        expect.objectContaining({
-          type: 'NATIVE_FILTER',
-          id: 'NATIVE_FILTER-2',
-          cascadeParentIds: [],
-        }),
-      ]),
+      expect.objectContaining({
+        deleted: expect.arrayContaining(['NATIVE_FILTER-3']),
+        modified: expect.arrayContaining([]),
+        reordered: expect.arrayContaining([]),
+      }),
     ),
   );
 });
@@ -420,23 +424,161 @@ test('deletes a filter including dependencies', async () => {
     onSave,
   });
   const removeButtons = screen.getAllByRole('img', { name: 'trash' });
-  // remove NATIVE_FILTER-2 which is a dependency of NATIVE_FILTER-1
   userEvent.click(removeButtons[1]);
   userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
   await waitFor(() =>
     expect(onSave).toHaveBeenCalledWith(
-      expect.arrayContaining([
-        expect.objectContaining({
-          type: 'NATIVE_FILTER',
-          id: 'NATIVE_FILTER-1',
-          cascadeParentIds: [],
-        }),
-        expect.objectContaining({
-          type: 'NATIVE_FILTER',
-          id: 'NATIVE_FILTER-3',
-          cascadeParentIds: [],
-        }),
-      ]),
+      expect.objectContaining({
+        deleted: ['NATIVE_FILTER-2'],
+        modified: expect.arrayContaining([
+          expect.objectContaining({
+            id: 'NATIVE_FILTER-1',
+          }),
+        ]),
+        reordered: [],
+      }),
+    ),
+  );
+});
+
+test('switches the order between two filters', async () => {
+  const nativeFilterState = [
+    buildNativeFilter('NATIVE_FILTER-1', 'state', []),
+    buildNativeFilter('NATIVE_FILTER-2', 'country', []),
+  ];
+
+  const state = {
+    ...defaultState(),
+    dashboardInfo: {
+      metadata: { native_filter_configuration: nativeFilterState },
+    },
+    dashboardLayout,
+  };
+
+  const onSave = jest.fn();
+
+  defaultRender(state, {
+    ...props,
+    createNewOnOpen: false,
+    onSave,
+  });
+
+  const draggableFilters = screen.getAllByRole('tab');
+
+  fireEvent.dragStart(draggableFilters[0]);
+
+  fireEvent.dragOver(draggableFilters[1]);
+
+  fireEvent.drop(draggableFilters[1]);
+
+  fireEvent.dragEnd(draggableFilters[0]);
+
+  userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+
+  await waitFor(() =>
+    expect(onSave).toHaveBeenCalledWith(
+      expect.objectContaining({
+        deleted: [],
+        modified: [],
+        reordered: expect.arrayContaining([
+          'NATIVE_FILTER-2',
+          'NATIVE_FILTER-1',
+        ]),
+      }),
+    ),
+  );
+});
+
+test('rearranges three filters and deletes one of them', async () => {
+  const nativeFilterState = [
+    buildNativeFilter('NATIVE_FILTER-1', 'state', []),
+    buildNativeFilter('NATIVE_FILTER-2', 'country', []),
+    buildNativeFilter('NATIVE_FILTER-3', 'product', []),
+  ];
+
+  const state = {
+    ...defaultState(),
+    dashboardInfo: {
+      metadata: { native_filter_configuration: nativeFilterState },
+    },
+    dashboardLayout,
+  };
+
+  const onSave = jest.fn();
+
+  defaultRender(state, {
+    ...props,
+    createNewOnOpen: false,
+    onSave,
+  });
+
+  const draggableFilters = screen.getAllByRole('tab');
+  const deleteButtons = screen.getAllByRole('img', { name: 'trash' });
+  userEvent.click(deleteButtons[1]);
+
+  fireEvent.dragStart(draggableFilters[0]);
+  fireEvent.dragOver(draggableFilters[2]);
+  fireEvent.drop(draggableFilters[2]);
+  fireEvent.dragEnd(draggableFilters[0]);
+
+  userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+
+  await waitFor(() =>
+    expect(onSave).toHaveBeenCalledWith(
+      expect.objectContaining({
+        modified: [],
+        deleted: ['NATIVE_FILTER-2'],
+        reordered: expect.arrayContaining([
+          'NATIVE_FILTER-2',
+          'NATIVE_FILTER-3',
+          'NATIVE_FILTER-1',
+        ]),
+      }),
+    ),
+  );
+});
+
+test('modifies the name of a filter', async () => {
+  jest.useFakeTimers();
+  const nativeFilterState = [
+    buildNativeFilter('NATIVE_FILTER-1', 'state', []),
+    buildNativeFilter('NATIVE_FILTER-2', 'country', []),
+  ];
+
+  const state = {
+    ...defaultState(),
+    dashboardInfo: {
+      metadata: { native_filter_configuration: nativeFilterState },
+    },
+    dashboardLayout,
+  };
+
+  const onSave = jest.fn();
+
+  defaultRender(state, {
+    ...props,
+    createNewOnOpen: false,
+    onSave,
+  });
+
+  const filterNameInput = screen.getByRole('textbox', {
+    name: FILTER_NAME_REGEX,
+  });
+
+  userEvent.clear(filterNameInput);
+  userEvent.type(filterNameInput, 'New Filter Name');
+
+  jest.runAllTimers();
+
+  userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
+
+  await waitFor(() =>
+    expect(onSave).toHaveBeenCalledWith(
+      expect.objectContaining({
+        modified: expect.arrayContaining([
+          expect.objectContaining({ name: 'New Filter Name' }),
+        ]),
+      }),
     ),
   );
 });
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
index 7ad2838d26..9432075689 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
@@ -20,7 +20,6 @@ import { memo, useEffect, useCallback, useMemo, useState, useRef } from 'react';
 import { uniq, isEqual, sortBy, debounce, isEmpty } from 'lodash';
 import {
   Filter,
-  FilterConfiguration,
   NativeFilterType,
   Divider,
   styled,
@@ -44,7 +43,12 @@ import FiltersConfigForm, {
 } from './FiltersConfigForm/FiltersConfigForm';
 import Footer from './Footer/Footer';
 import { useOpenModal, useRemoveCurrentFilter } from './state';
-import { FilterRemoval, NativeFiltersForm } from './types';
+import {
+  FilterChangesType,
+  FilterRemoval,
+  NativeFiltersForm,
+  SaveFilterChangesType,
+} from './types';
 import {
   createHandleSave,
   createHandleRemoveItem,
@@ -114,7 +118,7 @@ export interface FiltersConfigModalProps {
   isOpen: boolean;
   initialFilterId?: string;
   createNewOnOpen?: boolean;
-  onSave: (filterConfig: FilterConfiguration) => Promise<void>;
+  onSave: (filterChanges: SaveFilterChangesType) => Promise<void>;
   onCancel: () => void;
 }
 export const ALLOW_DEPENDENCIES = [
@@ -128,6 +132,11 @@ const DEFAULT_REMOVED_FILTERS: Record<string, FilterRemoval> = {};
 const DEFAULT_FORM_VALUES: NativeFiltersForm = {
   filters: {},
 };
+const DEFAULT_FILTER_CHANGES: FilterChangesType = {
+  modified: [],
+  deleted: [],
+  reordered: [],
+};
 
 /**
  * This is the modal to configure all the dashboard-native filters.
@@ -155,6 +164,27 @@ function FiltersConfigModal({
   const filterConfig = useFilterConfiguration();
   const filterConfigMap = useFilterConfigMap();
 
+  // this state contains the changes that we'll be sent through the PATCH endpoint
+  const [filterChanges, setFilterChanges] = useState<FilterChangesType>(
+    DEFAULT_FILTER_CHANGES,
+  );
+
+  const resetFilterChanges = () => {
+    setFilterChanges(DEFAULT_FILTER_CHANGES);
+  };
+
+  const handleModifyFilter = useCallback(
+    (filterId: string) => {
+      if (!filterChanges.modified.includes(filterId)) {
+        setFilterChanges(prev => ({
+          ...prev,
+          modified: [...prev.modified, filterId],
+        }));
+      }
+    },
+    [filterChanges.modified],
+  );
+
   // new filter ids belong to filters have been added during
   // this configuration session, and only exist in the form state until we submit.
   const [newFilterIds, setNewFilterIds] = useState<string[]>(
@@ -200,11 +230,17 @@ function FiltersConfigModal({
   const restoreFilter = useCallback(
     (id: string) => {
       const removal = removedFilters[id];
-      // gotta clear the removal timeout to prevent the filter from getting deleted
+      // Clear the removal timeout if the filter is pending deletion
       if (removal?.isPending) clearTimeout(removal.timerId);
+
       setRemovedFilters(current => ({ ...current, [id]: null }));
+
+      setFilterChanges(prev => ({
+        ...prev,
+        deleted: prev.deleted.filter(deletedId => deletedId !== id),
+      }));
     },
-    [removedFilters],
+    [removedFilters, setRemovedFilters],
   );
   const initialFilterOrder = useMemo(
     () => Object.keys(filterConfigMap),
@@ -239,18 +275,13 @@ function FiltersConfigModal({
     (type: NativeFilterType) => {
       const newFilterId = generateFilterId(type);
       setNewFilterIds([...newFilterIds, newFilterId]);
+      handleModifyFilter(newFilterId);
       setCurrentFilterId(newFilterId);
       setSaveAlertVisible(false);
       setOrderedFilters([...orderedFilters, newFilterId]);
       setActiveFilterPanelKey(getActiveFilterPanelKey(newFilterId));
     },
-    [
-      newFilterIds,
-      orderedFilters,
-      setCurrentFilterId,
-      setOrderedFilters,
-      setNewFilterIds,
-    ],
+    [newFilterIds, handleModifyFilter, orderedFilters],
   );
 
   useOpenModal(isOpen, addFilter, createNewOnOpen);
@@ -266,6 +297,13 @@ function FiltersConfigModal({
     setRemovedFilters,
     setOrderedFilters,
     setSaveAlertVisible,
+    filterChanges,
+    filterId => {
+      setFilterChanges(prev => ({
+        ...prev,
+        deleted: [...prev.deleted, filterId],
+      }));
+    },
   );
 
   // After this, it should be as if the modal was just opened fresh.
@@ -276,6 +314,7 @@ function FiltersConfigModal({
     setRemovedFilters(DEFAULT_REMOVED_FILTERS);
     setSaveAlertVisible(false);
     setFormValues(DEFAULT_FORM_VALUES);
+    resetFilterChanges();
     setErroredFilters(DEFAULT_EMPTY_FILTERS);
     if (filterIds.length > 0) {
       setActiveFilterPanelKey(getActiveFilterPanelKey(filterIds[0]));
@@ -329,19 +368,32 @@ function FiltersConfigModal({
           value: id,
           type: filterConfigMap[id]?.filterType,
         })),
-    [canBeUsedAsDependency, filterIds, getFilterTitle],
+    [canBeUsedAsDependency, filterConfigMap, filterIds, getFilterTitle],
   );
 
+  /**
+   * Manages dependencies of filters associated with a deleted filter.
+   *
+   * @param values the native filters form
+   * @returns the updated filterConfigMap
+   */
   const cleanDeletedParents = (values: NativeFiltersForm | null) => {
+    const modifiedParentFilters = new Set<string>();
     const updatedFilterConfigMap = Object.keys(filterConfigMap).reduce(
       (acc, key) => {
         const filter = filterConfigMap[key];
         const cascadeParentIds = filter.cascadeParentIds?.filter(id =>
           canBeUsedAsDependency(id),
         );
-        if (cascadeParentIds) {
+
+        if (
+          cascadeParentIds &&
+          !isEqual(cascadeParentIds, filter.cascadeParentIds)
+        ) {
           dispatch(updateCascadeParentIds(key, cascadeParentIds));
+          modifiedParentFilters.add(key);
         }
+
         return {
           ...acc,
           [key]: {
@@ -357,18 +409,24 @@ function FiltersConfigModal({
     if (filters) {
       Object.keys(filters).forEach(key => {
         const filter = filters[key];
+
         if (!('dependencies' in filter)) {
           return;
         }
-        const { dependencies } = filter;
-        if (dependencies) {
-          filter.dependencies = dependencies.filter(id =>
-            canBeUsedAsDependency(id),
-          );
+
+        const originalDependencies = filter.dependencies || [];
+        const cleanedDependencies = originalDependencies.filter(id =>
+          canBeUsedAsDependency(id),
+        );
+
+        if (!isEqual(cleanedDependencies, originalDependencies)) {
+          filter.dependencies = cleanedDependencies;
+          modifiedParentFilters.add(key);
         }
       });
     }
-    return updatedFilterConfigMap;
+
+    return [updatedFilterConfigMap, modifiedParentFilters];
   };
 
   const handleErroredFilters = useCallback(() => {
@@ -407,18 +465,35 @@ function FiltersConfigModal({
     handleErroredFilters();
 
     if (values) {
-      const updatedFilterConfigMap = cleanDeletedParents(values);
-      createHandleSave(
-        updatedFilterConfigMap,
-        orderedFilters,
-        removedFilters,
-        onSave,
-        values,
-      )();
+      const [updatedFilterConfigMap, modifiedParentFilters] =
+        cleanDeletedParents(values);
+
+      const allModified = [
+        ...new Set([
+          ...(modifiedParentFilters as Set<string>),
+          ...filterChanges.modified,
+        ]),
+      ];
+
+      const actualChanges = {
+        ...filterChanges,
+        modified:
+          allModified.length && filterChanges.deleted.length
+            ? allModified.filter(id => !filterChanges.deleted.includes(id))
+            : allModified,
+        reordered:
+          filterChanges.reordered.length &&
+          !isEqual(filterChanges.reordered, initialFilterOrder)
+            ? filterChanges.reordered
+            : [],
+      };
+
+      createHandleSave(onSave, actualChanges, values, updatedFilterConfigMap)();
       resetForm(true);
     } else {
       configFormRef.current?.changeTab?.('configuration');
     }
+    resetFilterChanges();
   };
 
   const handleConfirmCancel = () => {
@@ -447,6 +522,10 @@ function FiltersConfigModal({
     const removed = newOrderedFilter.splice(dragIndex, 1)[0];
     newOrderedFilter.splice(targetIndex, 0, removed);
     setOrderedFilters(newOrderedFilter);
+    setFilterChanges(prev => ({
+      ...prev,
+      reordered: newOrderedFilter,
+    }));
   };
 
   const buildDependencyMap = useCallback(() => {
@@ -596,28 +675,34 @@ function FiltersConfigModal({
                 setErroredFilters={setErroredFilters}
                 validateDependencies={validateDependencies}
                 getDependencySuggestion={getDependencySuggestion}
+                onModifyFilter={handleModifyFilter}
               />
             )}
           </div>
         );
       }),
     [
-      renderedFilters,
       orderedFilters,
+      renderedFilters,
       currentFilterId,
       filterConfigMap,
+      expanded,
       form,
       removedFilters,
       restoreFilter,
       getAvailableFilters,
       activeFilterPanelKey,
+      handleActiveFilterPanelChange,
       validateDependencies,
       getDependencySuggestion,
-      handleActiveFilterPanelChange,
-      expanded,
+      handleModifyFilter,
     ],
   );
 
+  useEffect(() => {
+    resetFilterChanges();
+  }, []);
+
   return (
     <StyledModalWrapper
       visible={isOpen}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
index 6f40ea6542..41d53791f6 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/types.ts
@@ -21,6 +21,7 @@ import {
   DataMask,
   NativeFilterType,
   NativeFilterScope,
+  Filter,
 } from '@superset-ui/core';
 
 export interface NativeFiltersFormItem {
@@ -60,6 +61,16 @@ export interface NativeFiltersForm {
   changed?: boolean;
 }
 
+export type FilterChangesType = {
+  modified: string[];
+  deleted: string[];
+  reordered: string[];
+};
+
+export type SaveFilterChangesType = {
+  modified: Filter[];
+} & Omit<FilterChangesType, 'modified'>;
+
 export type FilterRemoval =
   | null
   | {
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
index ffe53378ab..f0d09d9aa3 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/utils.ts
@@ -20,15 +20,15 @@ import { FormInstance } from 'src/components';
 import { nanoid } from 'nanoid';
 import { getInitialDataMask } from 'src/dataMask/reducer';
 import {
-  Filter,
   FilterConfiguration,
   NativeFilterType,
-  Divider,
   NativeFilterTarget,
   logging,
+  Filter,
+  Divider,
 } from '@superset-ui/core';
 import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
-import { FilterRemoval, NativeFiltersForm } from './types';
+import { FilterChangesType, FilterRemoval, NativeFiltersForm } from './types';
 
 export const REMOVAL_DELAY_SECS = 5;
 
@@ -92,62 +92,69 @@ export const validateForm = async (
 
 export const createHandleSave =
   (
-    filterConfigMap: Record<string, Filter | Divider>,
-    filterIds: string[],
-    removedFilters: Record<string, FilterRemoval>,
     saveForm: Function,
+    filterChanges: FilterChangesType,
     values: NativeFiltersForm,
+    filterConfigMap: Record<string, Filter | Divider>,
   ) =>
   async () => {
-    const newFilterConfig: FilterConfiguration = filterIds
-      .filter(id => !removedFilters[id])
-      .map(id => {
-        // create a filter config object from the form inputs
-        const formInputs = values.filters?.[id];
-        // if user didn't open a filter, return the original config
-        if (!formInputs) return filterConfigMap[id];
-        if (formInputs.type === NativeFilterType.Divider) {
-          return {
-            id,
-            type: NativeFilterType.Divider,
-            scope: {
-              rootPath: [DASHBOARD_ROOT_ID],
-              excluded: [],
-            },
-            title: formInputs.title,
-            description: formInputs.description,
-          };
-        }
-        const target: Partial<NativeFilterTarget> = {};
-        if (formInputs.dataset) {
-          target.datasetId = formInputs.dataset.value;
-        }
-        if (formInputs.dataset && formInputs.column) {
-          target.column = { name: formInputs.column };
-        }
+    const transformFilter = (id: string) => {
+      const formInputs = values.filters?.[id] || filterConfigMap[id];
+      if (!formInputs) {
+        return undefined;
+      }
+      if (formInputs.type === NativeFilterType.Divider) {
         return {
           id,
-          adhoc_filters: formInputs.adhoc_filters,
-          time_range: formInputs.time_range,
-          controlValues: formInputs.controlValues ?? {},
-          granularity_sqla: formInputs.granularity_sqla,
-          requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
-            rf => rf,
-          ),
-          name: formInputs.name,
-          filterType: formInputs.filterType,
-          // for now there will only ever be one target
-          targets: [target],
-          defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
-          cascadeParentIds: formInputs.dependencies || [],
-          scope: formInputs.scope,
-          sortMetric: formInputs.sortMetric,
-          type: formInputs.type,
-          description: (formInputs.description || '').trim(),
+          type: NativeFilterType.Divider,
+          scope: {
+            rootPath: [DASHBOARD_ROOT_ID],
+            excluded: [],
+          },
+          title: formInputs.title,
+          description: formInputs.description,
         };
-      });
+      }
 
-    await saveForm(newFilterConfig);
+      const target: Partial<NativeFilterTarget> = {};
+      if (formInputs.dataset) {
+        target.datasetId = formInputs.dataset.value;
+      }
+      if (formInputs.dataset && formInputs.column) {
+        target.column = { name: formInputs.column };
+      }
+
+      return {
+        id,
+        adhoc_filters: formInputs.adhoc_filters,
+        time_range: formInputs.time_range,
+        controlValues: formInputs.controlValues ?? {},
+        granularity_sqla: formInputs.granularity_sqla,
+        requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
+          rf => rf,
+        ),
+        name: formInputs.name,
+        filterType: formInputs.filterType,
+        targets: [target],
+        defaultDataMask: formInputs.defaultDataMask ?? getInitialDataMask(),
+        cascadeParentIds: formInputs.dependencies || [],
+        scope: formInputs.scope,
+        sortMetric: formInputs.sortMetric,
+        type: formInputs.type,
+        description: (formInputs.description || '').trim(),
+      };
+      return undefined;
+    };
+
+    const transformedModified = filterChanges.modified
+      .map(transformFilter)
+      .filter(Boolean);
+
+    const newFilterChanges = {
+      ...filterChanges,
+      modified: transformedModified,
+    };
+    await saveForm(newFilterChanges);
   };
 
 export const createHandleRemoveItem =
@@ -163,6 +170,8 @@ export const createHandleRemoveItem =
       val: string[] | ((prevState: string[]) => string[]),
     ) => void,
     setSaveAlertVisible: Function,
+    filterChanges: FilterChangesType,
+    removeFilter: (filterId: string) => void,
   ) =>
   (filterId: string) => {
     const completeFilterRemoval = (filterId: string) => {
@@ -187,12 +196,14 @@ export const createHandleRemoveItem =
       ...removedFilters,
       [filterId]: { isPending: true, timerId },
     }));
+    removeFilter(filterId);
+    // eslint-disable-next-line no-param-reassign
     setSaveAlertVisible(false);
   };
 
 export const NATIVE_FILTER_PREFIX = 'NATIVE_FILTER-';
 export const NATIVE_FILTER_DIVIDER_PREFIX = 'NATIVE_FILTER_DIVIDER-';
-export const generateFilterId = (type: NativeFilterType) => {
+export const generateFilterId = (type: NativeFilterType): string => {
   const prefix =
     type === NativeFilterType.NativeFilter
       ? NATIVE_FILTER_PREFIX
diff --git a/superset-frontend/src/dashboard/reducers/dashboardInfo.js b/superset-frontend/src/dashboard/reducers/dashboardInfo.js
index 3503c441b6..29f4dd2d3b 100644
--- a/superset-frontend/src/dashboard/reducers/dashboardInfo.js
+++ b/superset-frontend/src/dashboard/reducers/dashboardInfo.js
@@ -21,6 +21,7 @@ import {
   DASHBOARD_INFO_UPDATED,
   SET_FILTER_BAR_ORIENTATION,
   SET_CROSS_FILTERS_ENABLED,
+  DASHBOARD_INFO_FILTERS_CHANGED,
 } from '../actions/dashboardInfo';
 import { HYDRATE_DASHBOARD } from '../actions/hydrate';
 
@@ -33,6 +34,42 @@ export default function dashboardStateReducer(state = {}, action) {
         // server-side compare last_modified_time in second level
         last_modified_time: Math.round(new Date().getTime() / 1000),
       };
+    case DASHBOARD_INFO_FILTERS_CHANGED: {
+      const { modified = [], deleted = [], reordered = [] } = action.newInfo;
+      let updatedFilters = state.metadata.native_filter_configuration || [];
+      if (deleted.length > 0) {
+        updatedFilters = updatedFilters.filter(
+          filter => !deleted.includes(filter.id),
+        );
+      }
+      if (modified.length > 0) {
+        modified.forEach(modifiedFilter => {
+          const existingFilterIndex = updatedFilters.findIndex(
+            filter => filter.id === modifiedFilter.id,
+          );
+          if (existingFilterIndex > -1) {
+            updatedFilters[existingFilterIndex] = modifiedFilter;
+          } else {
+            updatedFilters = [...updatedFilters, modifiedFilter];
+          }
+        });
+      }
+      if (reordered.length > 0) {
+        updatedFilters = reordered
+          .map(reorderedId =>
+            updatedFilters.find(filter => filter.id === reorderedId),
+          )
+          .filter(Boolean);
+      }
+      return {
+        ...state,
+        metadata: {
+          ...state.metadata,
+          native_filter_configuration: updatedFilters,
+        },
+        last_modified_time: Math.round(new Date().getTime() / 1000),
+      };
+    }
     case HYDRATE_DASHBOARD:
       return {
         ...state,
diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
index c68ff3c19b..8ca3f0eea6 100644
--- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts
+++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts
@@ -18,7 +18,7 @@
  */
 import {
   AnyFilterAction,
-  SET_FILTER_CONFIG_COMPLETE,
+  SET_NATIVE_FILTERS_CONFIG_COMPLETE,
   SET_IN_SCOPE_STATUS_OF_FILTERS,
   SET_FOCUSED_NATIVE_FILTER,
   UNSET_FOCUSED_NATIVE_FILTER,
@@ -28,6 +28,7 @@ import {
 } from 'src/dashboard/actions/nativeFilters';
 import { FilterConfiguration, NativeFiltersState } from '@superset-ui/core';
 import { HYDRATE_DASHBOARD } from '../actions/hydrate';
+import { SaveFilterChangesType } from '../components/nativeFilters/FiltersConfigModal/types';
 
 export function getInitialState({
   filterConfig,
@@ -37,7 +38,6 @@ export function getInitialState({
   state?: NativeFiltersState;
 }): NativeFiltersState {
   const state: Partial<NativeFiltersState> = {};
-
   const filters = {};
   if (filterConfig) {
     filterConfig.forEach(filter => {
@@ -52,6 +52,43 @@ export function getInitialState({
   return state as NativeFiltersState;
 }
 
+function handleFilterChangesComplete(
+  state: NativeFiltersState,
+  changes: SaveFilterChangesType,
+) {
+  const { modified = [], deleted = [], reordered = [] } = changes;
+
+  let updatedFilters = { ...state.filters };
+
+  if (deleted.length > 0) {
+    deleted.forEach(id => {
+      if (updatedFilters[id]) {
+        delete updatedFilters[id];
+      }
+    });
+  }
+
+  if (modified.length > 0) {
+    const modifiedFilters = Object.fromEntries(
+      modified.map(filter => [filter.id, filter]),
+    );
+    updatedFilters = { ...updatedFilters, ...modifiedFilters };
+  }
+
+  if (reordered.length > 0) {
+    updatedFilters = Object.fromEntries(
+      reordered
+        .map(id => [id, updatedFilters[id]])
+        .filter(([, filter]) => filter),
+    );
+  }
+
+  return {
+    ...state,
+    filters: updatedFilters,
+  } as NativeFiltersState;
+}
+
 export default function nativeFilterReducer(
   state: NativeFiltersState = {
     filters: {},
@@ -64,10 +101,12 @@ export default function nativeFilterReducer(
         filters: action.data.nativeFilters.filters,
       };
 
-    case SET_FILTER_CONFIG_COMPLETE:
     case SET_IN_SCOPE_STATUS_OF_FILTERS:
       return getInitialState({ filterConfig: action.filterConfig, state });
 
+    case SET_NATIVE_FILTERS_CONFIG_COMPLETE:
+      return handleFilterChangesComplete(state, action.filterChanges);
+
     case SET_FOCUSED_NATIVE_FILTER:
       return {
         ...state,
diff --git a/superset-frontend/src/dataMask/actions.ts b/superset-frontend/src/dataMask/actions.ts
index cb7ed393d7..e8c7d6c4f3 100644
--- a/superset-frontend/src/dataMask/actions.ts
+++ b/superset-frontend/src/dataMask/actions.ts
@@ -16,7 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { DataMask, FilterConfiguration, Filters } from '@superset-ui/core';
+import { DataMask, Filters } from '@superset-ui/core';
+import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
 import { getInitialDataMask } from './reducer';
 
 export const CLEAR_DATA_MASK_STATE = 'CLEAR_DATA_MASK_STATE';
@@ -37,32 +38,25 @@ export interface INITDATAMASK {
   dataMask: DataMask;
 }
 
-export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE =
-  'SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE';
-
-export interface SetDataMaskForFilterConfigComplete {
-  type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE;
-  filterConfig: FilterConfiguration;
+export const SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE =
+  'SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE';
+export interface SetDataMaskForFilterChangesComplete {
+  type: typeof SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE;
+  filterChanges: SaveFilterChangesType;
   filters?: Filters;
 }
 
-export const SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL =
-  'SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL';
-
-export interface SetDataMaskForFilterConfigFail {
-  type: typeof SET_DATA_MASK_FOR_FILTER_CONFIG_FAIL;
-  filterConfig: FilterConfiguration;
-}
-export function setDataMaskForFilterConfigComplete(
-  filterConfig: FilterConfiguration,
+export function setDataMaskForFilterChangesComplete(
+  filterChanges: SaveFilterChangesType,
   filters?: Filters,
-): SetDataMaskForFilterConfigComplete {
+): SetDataMaskForFilterChangesComplete {
   return {
-    type: SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
-    filterConfig,
+    type: SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE,
+    filterChanges,
     filters,
   };
 }
+
 export function updateDataMask(
   filterId: string | number,
   dataMask: DataMask,
@@ -87,5 +81,4 @@ export function clearDataMaskState(): ClearDataMaskState {
 export type AnyDataMaskAction =
   | ClearDataMaskState
   | UpdateDataMask
-  | SetDataMaskForFilterConfigFail
-  | SetDataMaskForFilterConfigComplete;
+  | SetDataMaskForFilterChangesComplete;
diff --git a/superset-frontend/src/dataMask/reducer.ts b/superset-frontend/src/dataMask/reducer.ts
index 28158630ad..b79f17fb1d 100644
--- a/superset-frontend/src/dataMask/reducer.ts
+++ b/superset-frontend/src/dataMask/reducer.ts
@@ -32,10 +32,11 @@ import {
 } from '@superset-ui/core';
 import { NATIVE_FILTER_PREFIX } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/utils';
 import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate';
+import { SaveFilterChangesType } from 'src/dashboard/components/nativeFilters/FiltersConfigModal/types';
 import {
   AnyDataMaskAction,
   CLEAR_DATA_MASK_STATE,
-  SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
+  SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE,
   UPDATE_DATA_MASK,
 } from './actions';
 import { areObjectsEqual } from '../reduxUtils';
@@ -100,6 +101,37 @@ function fillNativeFilters(
   });
 }
 
+function updateDataMaskForFilterChanges(
+  filterChanges: SaveFilterChangesType,
+  mergedDataMask: DataMaskStateWithId,
+  draftDataMask: DataMaskStateWithId,
+  initialDataMask?: Filters,
+) {
+  const dataMask = initialDataMask || {};
+
+  Object.entries(dataMask).forEach(([key, value]) => {
+    mergedDataMask[key] = { ...value, ...value.defaultDataMask };
+  });
+
+  filterChanges.deleted.forEach((filterId: string) => {
+    delete mergedDataMask[filterId];
+  });
+
+  filterChanges.modified.forEach((filter: Filter) => {
+    mergedDataMask[filter.id] = {
+      ...getInitialDataMask(filter.id),
+      ...filter.defaultDataMask,
+      ...filter,
+    };
+  });
+
+  Object.values(draftDataMask).forEach(filter => {
+    if (!String(filter?.id).startsWith(NATIVE_FILTER_PREFIX)) {
+      mergedDataMask[filter?.id] = filter;
+    }
+  });
+}
+
 const dataMaskReducer = produce(
   (draft: DataMaskStateWithId, action: AnyDataMaskAction) => {
     const cleanState = {};
@@ -136,15 +168,14 @@ const dataMaskReducer = produce(
           action.data.dataMask,
         );
         return cleanState;
-      case SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE:
-        fillNativeFilters(
-          action.filterConfig ?? [],
+      case SET_DATA_MASK_FOR_FILTER_CHANGES_COMPLETE:
+        updateDataMaskForFilterChanges(
+          action.filterChanges,
           cleanState,
           draft,
           action.filters,
         );
         return cleanState;
-
       default:
         return draft;
     }
diff --git a/superset/commands/dashboard/exceptions.py b/superset/commands/dashboard/exceptions.py
index 9281119b32..a4b7be1261 100644
--- a/superset/commands/dashboard/exceptions.py
+++ b/superset/commands/dashboard/exceptions.py
@@ -58,6 +58,10 @@ class DashboardUpdateFailedError(UpdateFailedError):
     message = _("Dashboard could not be updated.")
 
 
+class DashboardNativeFiltersUpdateFailedError(UpdateFailedError):
+    message = _("Dashboard native filters could not be patched.")
+
+
 class DashboardDeleteFailedError(DeleteFailedError):
     message = _("Dashboard could not be deleted.")
 
diff --git a/superset/commands/dashboard/update.py b/superset/commands/dashboard/update.py
index 031db1af31..a87ac899d1 100644
--- a/superset/commands/dashboard/update.py
+++ b/superset/commands/dashboard/update.py
@@ -27,6 +27,7 @@ from superset.commands.base import BaseCommand, UpdateMixin
 from superset.commands.dashboard.exceptions import (
     DashboardForbiddenError,
     DashboardInvalidError,
+    DashboardNativeFiltersUpdateFailedError,
     DashboardNotFoundError,
     DashboardSlugExistsValidationError,
     DashboardUpdateFailedError,
@@ -67,7 +68,6 @@ class UpdateDashboardCommand(UpdateMixin, BaseCommand):
                 dashboard,
                 data=json.loads(self._properties.get("json_metadata", "{}")),
             )
-
         return dashboard
 
     def validate(self) -> None:
@@ -187,3 +187,18 @@ class UpdateDashboardCommand(UpdateMixin, BaseCommand):
         deleted_tabs = find_deleted_tabs()
         reports = find_reports_containing_tabs(deleted_tabs)
         deactivate_reports(reports)
+
+
+class UpdateDashboardNativeFiltersCommand(UpdateDashboardCommand):
+    @transaction(
+        on_error=partial(on_error, reraise=DashboardNativeFiltersUpdateFailedError)
+    )
+    def run(self) -> Model:
+        super().validate()
+        assert self._model
+
+        dashboard = DashboardDAO.update_native_filters_config(
+            self._model, self._properties
+        )
+
+        return dashboard
diff --git a/superset/constants.py b/superset/constants.py
index d233f271c6..f6cf2a1157 100644
--- a/superset/constants.py
+++ b/superset/constants.py
@@ -173,6 +173,7 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
     "columnar_metadata": "columnar_upload",
     "csv_metadata": "csv_upload",
     "slack_channels": "write",
+    "put_filters": "write",
 }
 
 EXTRA_FORM_DATA_APPEND_KEYS = {
diff --git a/superset/daos/dashboard.py b/superset/daos/dashboard.py
index 8196c197b2..1f1f0fc015 100644
--- a/superset/daos/dashboard.py
+++ b/superset/daos/dashboard.py
@@ -28,6 +28,7 @@ from superset.commands.dashboard.exceptions import (
     DashboardAccessDeniedError,
     DashboardForbiddenError,
     DashboardNotFoundError,
+    DashboardUpdateFailedError,
 )
 from superset.daos.base import BaseDAO
 from superset.dashboards.filters import DashboardAccessFilter, is_uuid
@@ -318,6 +319,69 @@ class DashboardDAO(BaseDAO[Dashboard]):
         db.session.add(dash)
         return dash
 
+    @classmethod
+    def update_native_filters_config(
+        cls,
+        dashboard: Dashboard | None = None,
+        attributes: dict[str, Any] | None = None,
+    ) -> Dashboard:
+        if not dashboard:
+            raise DashboardUpdateFailedError("Dashboard not found")
+
+        if attributes:
+            metadata = json.loads(dashboard.json_metadata or "{}")
+            native_filter_configuration = metadata.get(
+                "native_filter_configuration", []
+            )
+            updated_configuration = []
+
+            # Modify / Delete existing filters
+            for conf in native_filter_configuration:
+                deleted_filter = next(
+                    (f for f in attributes.get("deleted", []) if f == conf.get("id")),
+                    None,
+                )
+                if deleted_filter:
+                    continue
+
+                modified_filter = next(
+                    (
+                        f
+                        for f in attributes.get("modified", [])
+                        if f.get("id") == conf.get("id")
+                    ),
+                    None,
+                )
+                if modified_filter:
+                    updated_configuration.append(modified_filter)
+                else:
+                    updated_configuration.append(conf)
+
+            # Append new filters
+            for new_filter in attributes.get("modified", []):
+                if new_filter.get("id") not in [
+                    f.get("id") for f in updated_configuration
+                ]:
+                    updated_configuration.append(new_filter)
+
+            # Reorder filters
+            reordered_filter_ids = attributes.get("reordered", [])
+            if reordered_filter_ids:
+                filter_map = {
+                    filter_config["id"]: filter_config
+                    for filter_config in updated_configuration
+                }
+                updated_configuration = [
+                    filter_map[filter_id]
+                    for filter_id in reordered_filter_ids
+                    if filter_id in filter_map
+                ]
+
+            metadata["native_filter_configuration"] = updated_configuration
+            dashboard.json_metadata = json.dumps(metadata)
+
+        return dashboard
+
     @staticmethod
     def add_favorite(dashboard: Dashboard) -> None:
         ids = DashboardDAO.favorited_ids([dashboard])
diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py
index cb7e30ef02..2f510f0380 100644
--- a/superset/dashboards/api.py
+++ b/superset/dashboards/api.py
@@ -47,6 +47,7 @@ from superset.commands.dashboard.exceptions import (
     DashboardDeleteFailedError,
     DashboardForbiddenError,
     DashboardInvalidError,
+    DashboardNativeFiltersUpdateFailedError,
     DashboardNotFoundError,
     DashboardUpdateFailedError,
 )
@@ -55,7 +56,10 @@ from superset.commands.dashboard.fave import AddFavoriteDashboardCommand
 from superset.commands.dashboard.importers.dispatcher import ImportDashboardsCommand
 from superset.commands.dashboard.permalink.create import CreateDashboardPermalinkCommand
 from superset.commands.dashboard.unfave import DelFavoriteDashboardCommand
-from superset.commands.dashboard.update import UpdateDashboardCommand
+from superset.commands.dashboard.update import (
+    UpdateDashboardCommand,
+    UpdateDashboardNativeFiltersCommand,
+)
 from superset.commands.database.exceptions import DatasetValidationError
 from superset.commands.exceptions import TagForbiddenError
 from superset.commands.importers.exceptions import NoValidFilesFoundError
@@ -80,6 +84,7 @@ from superset.dashboards.schemas import (
     DashboardCopySchema,
     DashboardDatasetSchema,
     DashboardGetResponseSchema,
+    DashboardNativeFiltersConfigUpdateSchema,
     DashboardPostSchema,
     DashboardPutSchema,
     EmbeddedDashboardConfigSchema,
@@ -175,6 +180,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
         "copy_dash",
         "cache_dashboard_screenshot",
         "screenshot",
+        "put_filters",
     }
     resource_name = "dashboard"
     allow_browser_login = True
@@ -262,6 +268,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
 
     add_model_schema = DashboardPostSchema()
     edit_model_schema = DashboardPutSchema()
+    update_filters_model_schema = DashboardNativeFiltersConfigUpdateSchema()
     chart_entity_response_schema = ChartEntityResponseSchema()
     dashboard_get_response_schema = DashboardGetResponseSchema()
     dashboard_dataset_schema = DashboardDatasetSchema()
@@ -675,6 +682,94 @@ class DashboardRestApi(BaseSupersetModelRestApi):
             response = self.response_422(message=str(ex))
         return response
 
+    @expose("/<pk>/filters", methods=("PUT",))
+    @protect()
+    @safe
+    @statsd_metrics
+    @event_logger.log_this_with_context(
+        action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.put",
+        log_to_statsd=False,
+    )
+    @requires_json
+    def put_filters(self, pk: int) -> Response:
+        """
+        Modify native filters configuration for a dashboard.
+        ---
+        put:
+          summary: Update native filters configuration for a dashboard.
+          parameters:
+          - in: path
+            schema:
+              type: integer
+            name: pk
+          requestBody:
+            description: Native filters configuration
+            required: true
+            content:
+              application/json:
+                schema:
+                  $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
+          responses:
+            200:
+              description: Dashboard native filters updated
+              content:
+                application/json:
+                  schema:
+                    type: object
+                    properties:
+                      id:
+                        type: number
+                      result:
+                        $ref: '#/components/schemas/{{self.__class__.__name__}}.put'
+                      last_modified_time:
+                        type: number
+            400:
+              $ref: '#/components/responses/400'
+            401:
+              $ref: '#/components/responses/401'
+            403:
+              $ref: '#/components/responses/403'
+            404:
+              $ref: '#/components/responses/404'
+            422:
+              $ref: '#/components/responses/422'
+            500:
+              $ref: '#/components/responses/500'
+        """
+        try:
+            item = self.update_filters_model_schema.load(request.json, partial=True)
+        except ValidationError as error:
+            return self.response_400(message=error.messages)
+
+        try:
+            changed_model = UpdateDashboardNativeFiltersCommand(pk, item).run()
+            last_modified_time = changed_model.changed_on.replace(
+                microsecond=0
+            ).timestamp()
+            response = self.response(
+                200,
+                id=changed_model.id,
+                result=item,
+                last_modified_time=last_modified_time,
+            )
+        except DashboardNotFoundError:
+            response = self.response_404()
+        except DashboardForbiddenError:
+            response = self.response_403()
+        except TagForbiddenError as ex:
+            response = self.response(403, message=str(ex))
+        except DashboardInvalidError as ex:
+            return self.response_422(message=ex.normalized_messages())
+        except DashboardNativeFiltersUpdateFailedError as ex:
+            logger.error(
+                "Error changing native filters for dashboard %s: %s",
+                self.__class__.__name__,
+                str(ex),
+                exc_info=True,
+            )
+            response = self.response_422(message=str(ex))
+        return response
+
     @expose("/<pk>", methods=("DELETE",))
     @protect()
     @safe
diff --git a/superset/dashboards/schemas.py b/superset/dashboards/schemas.py
index b0a47aba41..8f6c56738c 100644
--- a/superset/dashboards/schemas.py
+++ b/superset/dashboards/schemas.py
@@ -398,6 +398,12 @@ class DashboardPutSchema(BaseDashboardSchema):
     )
 
 
+class DashboardNativeFiltersConfigUpdateSchema(BaseDashboardSchema):
+    deleted = fields.List(fields.String(), allow_none=False)
+    modified = fields.List(fields.Raw(), allow_none=False)
+    reordered = fields.List(fields.String(), allow_none=False)
+
+
 class DashboardScreenshotPostSchema(Schema):
     dataMask = fields.Dict(
         keys=fields.Str(),
diff --git a/tests/integration_tests/dashboards/api_tests.py b/tests/integration_tests/dashboards/api_tests.py
index f88bdccc0e..b6d7dc2cad 100644
--- a/tests/integration_tests/dashboards/api_tests.py
+++ b/tests/integration_tests/dashboards/api_tests.py
@@ -85,6 +85,15 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
         "published": False,
     }
 
+    dashboard_put_filters_data = {
+        "modified": [
+            {"id": "native_filter_1", "name": "Filter 1"},
+            {"id": "native_filter_2", "name": "Filter 2"},
+        ],
+        "deleted": [],
+        "reordered": [],
+    }
+
     @pytest.fixture()
     def create_dashboards(self):
         with self.create_app().app_context():
@@ -1719,6 +1728,172 @@ class TestDashboardApi(ApiOwnersTestCaseMixin, InsertChartMixin, SupersetTestCas
         db.session.delete(model)
         db.session.commit()
 
+    def test_add_dashboard_filters(self):
+        """
+        Dashboard API: Test that a filter was added
+        """
+        admin = self.get_user("admin")
+        admin_role = self.get_role("Admin")
+        dashboard_id = self.insert_dashboard(
+            "title1", "slug1", [admin.id], roles=[admin_role.id]
+        ).id
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/dashboard/{dashboard_id}/filters"
+        rv = self.put_assert_metric(uri, self.dashboard_put_filters_data, "put_filters")
+        self.assertEqual(rv.status_code, 200)
+        model = db.session.query(Dashboard).get(dashboard_id)
+        json_metadata = model.json_metadata
+        native_filter_config = json.loads(json_metadata)["native_filter_configuration"]
+
+        self.assertEqual(native_filter_config[0]["name"], "Filter 1")
+        db.session.delete(model)
+        db.session.commit()
+
+    def test_modify_dashboard_filters_values(self):
+        """
+        Dashboard API: Test that a filter was added
+        """
+        admin = self.get_user("admin")
+        admin_role = self.get_role("Admin")
+        json_metadata = {
+            "native_filter_configuration": [
+                {
+                    "id": "native_filter_1",
+                    "name": "Filter X",
+                    "filterType": "filter_select",
+                    "cascadeParentIds": [],
+                }
+            ]
+        }
+        dashboard_id = self.insert_dashboard(
+            "title1",
+            "slug1",
+            [admin.id],
+            roles=[admin_role.id],
+            json_metadata=json.dumps(json_metadata),
+        ).id
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/dashboard/{dashboard_id}/filters"
+        rv = self.put_assert_metric(uri, self.dashboard_put_filters_data, "put_filters")
+
+        self.assertEqual(rv.status_code, 200)
+        model = db.session.query(Dashboard).get(dashboard_id)
+        json_metadata = model.json_metadata
+        native_filter_config = json.loads(json_metadata)["native_filter_configuration"]
+
+        self.assertEqual(native_filter_config[0]["name"], "Filter 1")
+        db.session.delete(model)
+        db.session.commit()
+
+    def test_modfify_dashboard_filters_order(self):
+        """
+        Dashboard API: Test filters reordered
+        """
+        admin = self.get_user("admin")
+        admin_role = self.get_role("Admin")
+        json_metadata = {
+            "native_filter_configuration": [
+                {
+                    "id": "native_filter_1",
+                    "name": "Filter 1",
+                    "filterType": "filter_select",
+                    "cascadeParentIds": [],
+                },
+                {
+                    "id": "native_filter_2",
+                    "name": "Filter 2",
+                    "filterType": "filter_select",
+                    "cascadeParentIds": [],
+                },
+            ]
+        }
+        dashboard_id = self.insert_dashboard(
+            "title1",
+            "slug1",
+            [admin.id],
+            roles=[admin_role.id],
+            json_metadata=json.dumps(json_metadata),
+        ).id
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/dashboard/{dashboard_id}/filters"
+        put_data = {
+            **self.dashboard_put_filters_data,
+            "reordered": ["native_filter_2", "native_filter_1"],
+        }
+        rv = self.put_assert_metric(uri, put_data, "put_filters")
+        self.assertEqual(rv.status_code, 200)
+        model = db.session.query(Dashboard).get(dashboard_id)
+        json_metadata = model.json_metadata
+        native_filter_config = json.loads(json_metadata)["native_filter_configuration"]
+
+        self.assertEqual(native_filter_config[0]["name"], "Filter 2")
+        db.session.delete(model)
+        db.session.commit()
+
+    def test_dashboard_filters_deleted(self):
+        """
+        Dashboard API: Test filters deleted
+        """
+        admin = self.get_user("admin")
+        admin_role = self.get_role("Admin")
+        json_metadata = {
+            "native_filter_configuration": [
+                {
+                    "id": "native_filter_1",
+                    "name": "Filter 1",
+                    "filterType": "filter_select",
+                    "cascadeParentIds": [],
+                },
+                {
+                    "id": "native_filter_2",
+                    "name": "Filter 2",
+                    "filterType": "filter_select",
+                    "cascadeParentIds": [],
+                },
+            ]
+        }
+        dashboard_id = self.insert_dashboard(
+            "title1",
+            "slug1",
+            [admin.id],
+            roles=[admin_role.id],
+            json_metadata=json.dumps(json_metadata),
+        ).id
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/dashboard/{dashboard_id}/filters"
+        put_data = {
+            **self.dashboard_put_filters_data,
+            "deleted": ["native_filter_1"],
+        }
+        rv = self.put_assert_metric(uri, put_data, "put_filters")
+        self.assertEqual(rv.status_code, 200)
+        model = db.session.query(Dashboard).get(dashboard_id)
+        json_metadata = model.json_metadata
+        native_filter_config = json.loads(json_metadata)["native_filter_configuration"]
+
+        self.assertEqual(native_filter_config[0]["name"], "Filter 2")
+        db.session.delete(model)
+        db.session.commit()
+
+    def test_modify_dashboard_filters_invalid_data(self):
+        """
+        Dashboard API: Test modify filters with invalid data
+        """
+        admin = self.get_user("admin")
+        admin_role = self.get_role("Admin")
+        dashboard_id = self.insert_dashboard(
+            "title1", "slug1", [admin.id], roles=[admin_role.id]
+        ).id
+        self.login(ADMIN_USERNAME)
+        uri = f"api/v1/dashboard/{dashboard_id}/filters"
+        put_data = {"invalid_key": "invalid_value"}
+        rv = self.put_assert_metric(uri, put_data, "put_filters")
+        self.assertEqual(rv.status_code, 400)
+
+        model = db.session.query(Dashboard).get(dashboard_id)
+        db.session.delete(model)
+        db.session.commit()
+
     def test_dashboard_get_list_no_username(self):
         """
         Dashboard API: Tests that no username is returned