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 2025/07/04 10:31:30 UTC

(superset) branch master updated: feat(filter): Add Slider Range Inputs Option for Numerical Range Filters (#33170)

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 ef14a5fbb4 feat(filter): Add Slider Range Inputs Option for Numerical Range Filters (#33170)
ef14a5fbb4 is described below

commit ef14a5fbb47a540b0c91f40fa397fa9c6d18fa2d
Author: Pius Iniobong <in...@gmail.com>
AuthorDate: Fri Jul 4 11:31:23 2025 +0100

    feat(filter): Add Slider Range Inputs Option for Numerical Range Filters (#33170)
    
    Co-authored-by: Geido <60...@users.noreply.github.com>
    Co-authored-by: github-actions[bot] <41...@users.noreply.github.com>
    Co-authored-by: GitHub Action <ac...@github.com>
    Co-authored-by: Mehmet Salih Yavuz <sa...@proton.me>
---
 .../dashboard/nativeFilters.noInitState.test.ts    | 151 ++++--
 .../cypress-base/cypress/e2e/dashboard/utils.ts    |  21 +-
 .../src/components/DropdownContainer/index.tsx     | 133 +++++-
 .../FilterBar/FilterControls/FilterControl.tsx     |   4 +-
 .../FiltersConfigForm/FiltersConfigForm.tsx        | 186 ++++++--
 .../components/nativeFilters/selectors.ts          |  16 +-
 .../components/Range/RangeFilterPlugin.test.tsx    |  98 +++-
 .../filters/components/Range/RangeFilterPlugin.tsx | 517 ++++++++++++++++-----
 .../src/filters/components/Range/types.ts          |   7 +
 .../filters/components/Time/TimeFilterPlugin.tsx   |   3 +
 superset-frontend/src/filters/components/common.ts |  17 +-
 11 files changed, 921 insertions(+), 232 deletions(-)

diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
index 60900ad934..6ca48c68c6 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/nativeFilters.noInitState.test.ts
@@ -174,46 +174,139 @@ describe('Native filters', () => {
       validateFilterContentOnDashboard(testItems.topTenChart.filterColumnYear);
     });
 
-    it('User can create a numerical range filter', () => {
-      visitDashboard();
-      enterNativeFilterEditModal(false);
-      fillNativeFilterForm(
-        testItems.filterType.numerical,
-        testItems.filterNumericalColumn,
-        testItems.datasetForNativeFilter,
-        testItems.filterNumericalColumn,
-      );
-      saveNativeFilterSettings([]);
+    describe.only('Numerical Range Filter - Display Modes', () => {
+      beforeEach(() => {
+        visitDashboard();
+      });
 
-      // Assertions
-      cy.get('[data-test="range-filter-from-input"]')
-        .should('be.visible')
-        .click();
+      const expandFilterConfiguration = () => {
+        cy.get('.ant-collapse-header')
+          .contains('Filter Configuration')
+          .should('be.visible')
+          .then($header => {
+            cy.wrap($header)
+              .closest('.ant-collapse-item')
+              .invoke('hasClass', 'ant-collapse-item-active')
+              .then(isExpanded => {
+                if (!isExpanded) cy.wrap($header).click();
+              });
+          });
 
-      cy.get('[data-test="range-filter-from-input"]').type('{selectall}40');
+        cy.get('.ant-collapse-content-box').should('be.visible');
+      };
 
-      cy.get('[data-test="range-filter-to-input"]')
-        .should('be.visible')
-        .click();
+      const selectRangeTypeOption = (label: string) => {
+        cy.contains('Range Type')
+          .should('be.visible')
+          .closest('.ant-form-item')
+          .within(() => {
+            cy.get('.ant-select-selector').click();
+          });
 
-      cy.get('[data-test="range-filter-to-input"]').type('{selectall}50');
-      cy.get(nativeFilters.applyFilter).click({
-        force: true,
-      });
+        cy.get('.ant-select-dropdown:visible')
+          .contains('.ant-select-item-option', label)
+          .click();
+      };
 
-      // Assert that the URL contains 'native_filters'
-      cy.url().then(u => {
-        const ur = new URL(u);
-        expect(ur.search).to.include('native_filters');
+      const applyAndAssertInputs = (from: string, to: string) => {
+        // Set 'from' input
+        cy.get('[data-test="range-filter-from-input"]').clear();
+        cy.get('[data-test="range-filter-from-input"]').type(from);
+        cy.get('[data-test="range-filter-from-input"]').blur();
 
+        // Set 'to' input
+        cy.get('[data-test="range-filter-to-input"]').clear();
+        cy.get('[data-test="range-filter-to-input"]').type(to);
+        cy.get('[data-test="range-filter-to-input"]').blur();
+
+        // Assert values without chaining after .invoke()
         cy.get('[data-test="range-filter-from-input"]')
           .invoke('val')
-          .should('equal', '40');
+          .then(val => {
+            expect(val).to.equal(from);
+          });
 
-        // Assert that the "To" input has the correct value
         cy.get('[data-test="range-filter-to-input"]')
           .invoke('val')
-          .should('equal', '50');
+          .then(val => {
+            expect(val).to.equal(to);
+          });
+      };
+
+      it('User can create a numerical range filter with "Range Inputs" display mode', () => {
+        enterNativeFilterEditModal(false);
+
+        fillNativeFilterForm(
+          testItems.filterType.numerical,
+          testItems.filterNumericalColumn,
+          testItems.datasetForNativeFilter,
+          testItems.filterNumericalColumn,
+        );
+
+        expandFilterConfiguration();
+        selectRangeTypeOption('Range Inputs');
+
+        saveNativeFilterSettings([]);
+        cy.wait(500); // allow filter to mount
+
+        applyAndAssertInputs('40', '70');
+      });
+
+      it('User can change the display mode to "Slider"', () => {
+        enterNativeFilterEditModal(false);
+
+        fillNativeFilterForm(
+          testItems.filterType.numerical,
+          testItems.filterNumericalColumn,
+          testItems.datasetForNativeFilter,
+          testItems.filterNumericalColumn,
+        );
+
+        expandFilterConfiguration();
+
+        cy.contains('Range Type')
+          .should('be.visible')
+          .closest('.ant-form-item')
+          .within(() => {
+            cy.get('.ant-select-selector').click({ force: true });
+          });
+
+        cy.get('.ant-select-dropdown:visible .ant-select-item-option')
+          .contains(/^Slider$/)
+          .click({ force: true });
+
+        cy.get('.ant-select-selector').should('contain.text', 'Slider');
+
+        saveNativeFilterSettings([]);
+
+        cy.get('.ant-slider', { timeout: 10000 }).should('be.visible');
+
+        cy.get('[data-test="range-filter-from-input"]', {
+          timeout: 5000,
+        }).should('not.exist');
+        cy.get('[data-test="range-filter-to-input"]', { timeout: 5000 }).should(
+          'not.exist',
+        );
+      });
+
+      it('User can change the display mode to "Slider and range input"', () => {
+        enterNativeFilterEditModal(false);
+
+        // Re-create filter
+        fillNativeFilterForm(
+          testItems.filterType.numerical,
+          testItems.filterNumericalColumn,
+          testItems.datasetForNativeFilter,
+          testItems.filterNumericalColumn,
+        );
+
+        expandFilterConfiguration();
+        selectRangeTypeOption('Slider and range input');
+
+        saveNativeFilterSettings([]);
+        cy.wait(500);
+
+        applyAndAssertInputs('40', '70');
       });
     });
 
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
index 83326f965a..8ed39621ba 100644
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
+++ b/superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
@@ -363,9 +363,26 @@ export function saveNativeFilterSettings(charts: ChartSpec[]) {
   cy.get(nativeFilters.modal.footer)
     .contains('Save')
     .should('be.visible')
-    .click();
+    .click({ force: true });
+
+  // Wait for modal to either close or remain open
+  cy.get('body').should($body => {
+    const modalExists = $body.find(nativeFilters.modal.container).length > 0;
+    if (modalExists) {
+      cy.get(nativeFilters.modal.footer)
+        .contains('Save')
+        .should('be.visible')
+        .click({ force: true });
+    }
+  });
+
+  // Ensure modal is closed
   cy.get(nativeFilters.modal.container).should('not.exist');
-  charts.forEach(waitForChartLoad);
+
+  // Wait for all charts to load
+  charts.forEach(chart => {
+    waitForChartLoad(chart);
+  });
 }
 
 /** ************************************************************************
diff --git a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/index.tsx
index b9835e3e13..7fab3e498c 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/index.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/index.tsx
@@ -17,8 +17,10 @@
  * under the License.
  */
 import {
+  CSSProperties,
   cloneElement,
   forwardRef,
+  ReactElement,
   RefObject,
   useEffect,
   useImperativeHandle,
@@ -26,17 +28,85 @@ import {
   useMemo,
   useState,
   useRef,
+  ReactNode,
+  useCallback,
 } from 'react';
 
 import { Global } from '@emotion/react';
 import { css, t, useTheme, usePrevious } from '@superset-ui/core';
 import { useResizeDetector } from 'react-resize-detector';
 import { Badge, Icons, Button, Tooltip, Popover } from '..';
-import type {
-  DropdownContainerProps,
-  DropdownItem,
-  DropdownRef,
-} from './types';
+/**
+ * Container item.
+ */
+export interface DropdownItem {
+  /**
+   * String that uniquely identifies the item.
+   */
+  id: string;
+  /**
+   * The element to be rendered.
+   */
+  element: ReactElement;
+}
+
+/**
+ * Horizontal container that displays overflowed items in a dropdown.
+ * It shows an indicator of how many items are currently overflowing.
+ */
+export interface DropdownContainerProps {
+  /**
+   * Array of items. The id property is used to uniquely identify
+   * the elements when rendering or dealing with event handlers.
+   */
+  items: DropdownItem[];
+  /**
+   * Event handler called every time an element moves between
+   * main container and dropdown.
+   */
+  onOverflowingStateChange?: (overflowingState: {
+    notOverflowed: string[];
+    overflowed: string[];
+  }) => void;
+  /**
+   * Option to customize the content of the dropdown.
+   */
+  dropdownContent?: (overflowedItems: DropdownItem[]) => ReactElement;
+  /**
+   * Dropdown ref.
+   */
+  dropdownRef?: RefObject<HTMLDivElement>;
+  /**
+   * Dropdown additional style properties.
+   */
+  dropdownStyle?: CSSProperties;
+  /**
+   * Displayed count in the dropdown trigger.
+   */
+  dropdownTriggerCount?: number;
+  /**
+   * Icon of the dropdown trigger.
+   */
+  dropdownTriggerIcon?: ReactElement;
+  /**
+   * Text of the dropdown trigger.
+   */
+  dropdownTriggerText?: string;
+  /**
+   * Text of the dropdown trigger tooltip
+   */
+  dropdownTriggerTooltip?: ReactNode | null;
+  /**
+   * Main container additional style properties.
+   */
+  style?: CSSProperties;
+  /**
+   * Force render popover content before it's first opened
+   */
+  forceRender?: boolean;
+}
+
+export type DropdownRef = HTMLDivElement & { open: () => void };
 
 const MAX_HEIGHT = 500;
 
@@ -73,6 +143,37 @@ export const DropdownContainer = forwardRef(
 
     const [showOverflow, setShowOverflow] = useState(false);
 
+    // callback to update item widths so that the useLayoutEffect runs whenever
+    // width of any of the child changes
+    const recalculateItemWidths = useCallback(() => {
+      const mainItemsContainerNode = current?.children.item(0);
+      if (mainItemsContainerNode) {
+        const visibleChildrenElements = Array.from(
+          mainItemsContainerNode.children,
+        );
+        setItemsWidth(prevGlobalWidths => {
+          if (prevGlobalWidths.length !== items.length) {
+            return prevGlobalWidths;
+          }
+
+          const newGlobalWidths = [...prevGlobalWidths];
+          let changed = false;
+          visibleChildrenElements.forEach((child, indexInVisible) => {
+            const originalItemIndex = indexInVisible;
+            if (originalItemIndex < newGlobalWidths.length) {
+              const newWidth = child.getBoundingClientRect().width;
+              if (newGlobalWidths[originalItemIndex] !== newWidth) {
+                newGlobalWidths[originalItemIndex] = newWidth;
+                changed = true;
+              }
+            }
+          });
+
+          return changed ? newGlobalWidths : prevGlobalWidths;
+        });
+      }
+    }, [current?.children, items.length]);
+
     const reduceItems = (items: DropdownItem[]): [DropdownItem[], string[]] =>
       items.reduce(
         ([items, ids], item) => {
@@ -122,24 +223,7 @@ export const DropdownContainer = forwardRef(
         childrenArray.map(child => resizeObserver.unobserve(child));
         resizeObserver.disconnect();
       };
-    }, [items.length]);
-
-    // callback to update item widths so that the useLayoutEffect runs whenever
-    // width of any of the child changes
-    const recalculateItemWidths = () => {
-      const container = current?.children.item(0);
-      if (container) {
-        const { children } = container;
-        const childrenArray = Array.from(children);
-
-        const currentWidths = childrenArray.map(
-          child => child.getBoundingClientRect().width,
-        );
-
-        // Update state with new widths
-        setItemsWidth(currentWidths);
-      }
-    };
+    }, [items.length, current, recalculateItemWidths]);
 
     useLayoutEffect(() => {
       if (popoverVisible) {
@@ -206,6 +290,7 @@ export const DropdownContainer = forwardRef(
       overflowedItems.length,
       previousWidth,
       width,
+      popoverVisible,
     ]);
 
     useEffect(() => {
@@ -376,5 +461,3 @@ export const DropdownContainer = forwardRef(
     );
   },
 );
-
-export type { DropdownItem, DropdownRef };
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 97b4d31889..99a94c88d2 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx
@@ -162,6 +162,8 @@ const HorizontalFormItem = styled(StyledFormItem)<{
   }
 
   .ant-form-item-label {
+    display: flex;
+    align-items: center;
     overflow: visible;
     padding-bottom: 0;
     margin-right: ${({ theme }) => theme.sizeUnit * 2}px;
@@ -177,7 +179,7 @@ const HorizontalFormItem = styled(StyledFormItem)<{
   }
 
   .ant-form-item-control {
-    width: ${({ inverseSelection }) => (inverseSelection ? 252 : 164)}px;
+    min-width: ${({ inverseSelection }) => (inverseSelection ? 252 : 164)}px;
   }
 
   .select-container {
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 20e22c4b83..8224f6aced 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -82,6 +82,7 @@ import DateFilterControl from 'src/explore/components/controls/DateFilterControl
 import AdhocFilterControl from 'src/explore/components/controls/FilterControl/AdhocFilterControl';
 import { waitForAsyncData } from 'src/middleware/asyncEvent';
 import { SingleValueType } from 'src/filters/components/Range/SingleValueType';
+import { RangeDisplayMode } from 'src/filters/components/Range/types';
 import {
   getFormData,
   mergeExtraFormData,
@@ -736,6 +737,12 @@ const FiltersConfigForm = (
       />
     </StyledRowFormItem>
   );
+  const isValidFilterValue = (value: any, isRangeFilter: boolean) => {
+    if (isRangeFilter) {
+      return Array.isArray(value) && (value[0] !== null || value[1] !== null);
+    }
+    return !!value;
+  };
   return (
     <Tabs
       activeKey={activeTabKey}
@@ -1168,20 +1175,69 @@ const FiltersConfigForm = (
                                     </CollapsibleControl>
                                   </FormItem>
                                 ) : (
-                                  <FormItem
-                                    name={['filters', filterId, 'rangeFilter']}
-                                  >
-                                    <CollapsibleControl
-                                      initialValue={hasEnableSingleValue}
-                                      title={t('Single Value')}
-                                      onChange={checked => {
-                                        onEnableSingleValueChanged(
-                                          checked
-                                            ? SingleValueType.Exact
-                                            : undefined,
-                                        );
-                                        formChanged();
-                                      }}
+                                  <>
+                                    <FormItem
+                                      name={[
+                                        'filters',
+                                        filterId,
+                                        'rangeFilter',
+                                      ]}
+                                    >
+                                      <CollapsibleControl
+                                        initialValue={hasEnableSingleValue}
+                                        title={t('Single Value')}
+                                        onChange={checked => {
+                                          onEnableSingleValueChanged(
+                                            checked
+                                              ? SingleValueType.Exact
+                                              : undefined,
+                                          );
+                                          formChanged();
+                                        }}
+                                      >
+                                        <StyledRowFormItem
+                                          expanded={expanded}
+                                          name={[
+                                            'filters',
+                                            filterId,
+                                            'controlValues',
+                                            'enableSingleValue',
+                                          ]}
+                                          initialValue={enableSingleValue}
+                                          label={
+                                            <StyledLabel>
+                                              {t('Single value type')}
+                                            </StyledLabel>
+                                          }
+                                        >
+                                          <Radio.GroupWrapper
+                                            onChange={value => {
+                                              onEnableSingleValueChanged(
+                                                value.target.value,
+                                              );
+                                              formChanged();
+                                            }}
+                                            options={[
+                                              {
+                                                label: t('Minimum'),
+                                                value: SingleValueType.Minimum,
+                                              },
+                                              {
+                                                label: t('Exact'),
+                                                value: SingleValueType.Exact,
+                                              },
+                                              {
+                                                label: t('Maximum'),
+                                                value: SingleValueType.Maximum,
+                                              },
+                                            ]}
+                                          />
+                                        </StyledRowFormItem>
+                                      </CollapsibleControl>
+                                    </FormItem>
+
+                                    <FormItem
+                                      name={['filters', filterId, 'rangeType']}
                                     >
                                       <StyledRowFormItem
                                         expanded={expanded}
@@ -1189,40 +1245,66 @@ const FiltersConfigForm = (
                                           'filters',
                                           filterId,
                                           'controlValues',
-                                          'enableSingleValue',
+                                          'rangeDisplayMode',
                                         ]}
-                                        initialValue={enableSingleValue}
+                                        initialValue={
+                                          formFilter?.controlValues
+                                            ?.rangeDisplayMode ||
+                                          filterToEdit?.controlValues
+                                            ?.rangeDisplayMode ||
+                                          RangeDisplayMode.SliderAndInput
+                                        }
                                         label={
                                           <StyledLabel>
-                                            {t('Single value type')}
+                                            {t('Range Type')}
                                           </StyledLabel>
                                         }
                                       >
-                                        <Radio.GroupWrapper
-                                          onChange={value => {
-                                            onEnableSingleValueChanged(
-                                              value.target.value,
-                                            );
-                                            formChanged();
-                                          }}
+                                        <Select
+                                          ariaLabel={t('Range Type')}
                                           options={[
                                             {
-                                              label: t('Minimum'),
-                                              value: SingleValueType.Minimum,
+                                              value: RangeDisplayMode.Slider,
+                                              label: t('Slider'),
                                             },
                                             {
-                                              label: t('Exact'),
-                                              value: SingleValueType.Exact,
+                                              value: RangeDisplayMode.Input,
+                                              label: t('Range Inputs'),
                                             },
                                             {
-                                              label: t('Maximum'),
-                                              value: SingleValueType.Maximum,
+                                              value:
+                                                RangeDisplayMode.SliderAndInput,
+                                              label: t(
+                                                'Slider and range input',
+                                              ),
                                             },
                                           ]}
+                                          onChange={value => {
+                                            const previous =
+                                              form.getFieldValue('filters')?.[
+                                                filterId
+                                              ].controlValues || {};
+                                            const rangeDisplayMode =
+                                              value ||
+                                              RangeDisplayMode.SliderAndInput;
+                                            setNativeFilterFieldValues(
+                                              form,
+                                              filterId,
+                                              {
+                                                controlValues: {
+                                                  ...previous,
+                                                  rangeDisplayMode,
+                                                },
+                                              },
+                                            );
+
+                                            forceUpdate();
+                                            formChanged();
+                                          }}
                                         />
                                       </StyledRowFormItem>
-                                    </CollapsibleControl>
-                                  </FormItem>
+                                    </FormItem>
+                                  </>
                                 )}
                               </>
                             ),
@@ -1269,6 +1351,25 @@ const FiltersConfigForm = (
                                   setNativeFilterFieldValues(form, filterId, {
                                     defaultDataMask: null,
                                   });
+                                } else {
+                                  // When the checkbox is checked, explicitly set an empty default data mask
+                                  // for range filters to trigger validation
+                                  if (
+                                    formFilter?.filterType === 'filter_range'
+                                  ) {
+                                    setNativeFilterFieldValues(form, filterId, {
+                                      defaultDataMask: {
+                                        extraFormData: {},
+                                        filterState: {
+                                          value: [null, null],
+                                        },
+                                      },
+                                    });
+                                  }
+                                  // Validate immediately when the checkbox is checked
+                                  form.validateFields([
+                                    ['filters', filterId, 'defaultDataMask'],
+                                  ]);
                                 }
                                 formChanged();
                               }}
@@ -1292,12 +1393,22 @@ const FiltersConfigForm = (
                                   rules={[
                                     {
                                       validator: () => {
-                                        if (
+                                        // For range filters, check if at least one of the values in the array is non-null
+                                        const value =
                                           formFilter?.defaultDataMask
-                                            ?.filterState?.value
-                                        ) {
-                                          // requires managing the error as the DefaultValue
-                                          // component does not use an Antdesign compatible input
+                                            ?.filterState?.value;
+                                        const isRangeFilter =
+                                          formFilter?.filterType ===
+                                          'filter_range';
+
+                                        // Check if value exists and is valid
+                                        const hasValidValue =
+                                          isValidFilterValue(
+                                            value,
+                                            isRangeFilter,
+                                          );
+
+                                        if (hasValidValue) {
                                           const formValidationFields =
                                             form.getFieldsError();
                                           setErroredFilters(
@@ -1315,6 +1426,7 @@ const FiltersConfigForm = (
                                           );
                                           return Promise.resolve();
                                         }
+
                                         setErroredFilters(
                                           prevErroredFilters => {
                                             if (
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts
index 40b362e86c..066737d7a1 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/selectors.ts
@@ -342,6 +342,7 @@ const cachedNativeFilterDataForChart: Record<
     rejectedColumns: Set<string>;
   }
 > = {};
+
 export const selectNativeIndicatorsForChart = (
   nativeFilters: Filters,
   dataMask: DataMaskStateWithId,
@@ -355,17 +356,16 @@ export const selectNativeIndicatorsForChart = (
 
   const cachedFilterData = cachedNativeFilterDataForChart[chartId];
   if (
-    cachedNativeIndicatorsForChart[chartId] &&
-    areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
-    areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
-    cachedFilterData?.nativeFilters === nativeFilters &&
-    cachedFilterData?.chartLayoutItems === chartLayoutItems &&
-    cachedFilterData?.chartConfiguration === chartConfiguration &&
-    cachedFilterData?.dataMask === dataMask
+    cachedNativeIndicatorsForChart[chartId]?.length &&
+    areObjectsEqual(cachedFilterData.appliedColumns, appliedColumns) &&
+    areObjectsEqual(cachedFilterData.rejectedColumns, rejectedColumns) &&
+    areObjectsEqual(cachedFilterData.nativeFilters, nativeFilters) &&
+    areObjectsEqual(cachedFilterData.chartLayoutItems, chartLayoutItems) &&
+    areObjectsEqual(cachedFilterData.chartConfiguration, chartConfiguration) &&
+    areObjectsEqual(cachedFilterData.dataMask, dataMask)
   ) {
     return cachedNativeIndicatorsForChart[chartId];
   }
-
   const nativeFilterIndicators =
     nativeFilters &&
     Object.values(nativeFilters)
diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
index 855482027e..6cd53f52c4 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
@@ -17,8 +17,10 @@
  * under the License.
  */
 import { AppSection, GenericDataType } from '@superset-ui/core';
-import { fireEvent, render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from 'spec/helpers/testing-library';
 import RangeFilterPlugin from './RangeFilterPlugin';
+import { RangeDisplayMode } from './types';
 import { SingleValueType } from './SingleValueType';
 import transformProps from './transformProps';
 
@@ -101,7 +103,7 @@ describe('RangeFilterPlugin', () => {
     jest.clearAllMocks();
   });
 
-  it('should render two numerical inputs', () => {
+  it('should render two numerical inputs and a slider by default', () => {
     getWrapper();
 
     const inputs = screen.getAllByRole('spinbutton');
@@ -109,26 +111,32 @@ describe('RangeFilterPlugin', () => {
 
     expect(inputs[0]).toHaveValue('10');
     expect(inputs[1]).toHaveValue('70');
+
+    // For a range slider, there are two slider handles
+    const sliders = screen.getAllByRole('slider');
+    expect(sliders.length).toBeGreaterThan(0);
   });
 
-  it('should set the data mask to error when the range is incorrect', () => {
+  it('should set the data mask to error when the range is incorrect', async () => {
     getWrapper({ filterState: { value: [null, null] } });
 
     const inputs = screen.getAllByRole('spinbutton');
     const fromInput = inputs[0];
     const toInput = inputs[1];
 
-    fireEvent.change(fromInput, { target: { value: 20 } });
+    userEvent.clear(fromInput);
+    userEvent.type(fromInput, '20');
 
-    fireEvent.change(toInput, { target: { value: 10 } });
+    userEvent.clear(toInput);
+    userEvent.type(toInput, '10');
 
-    fireEvent.blur(toInput);
+    userEvent.tab();
 
     expect(setDataMask).toHaveBeenCalledWith({
       extraFormData: {},
       filterState: {
         label: '',
-        validateMessage: 'Please provide a valid range',
+        validateMessage: 'Numbers must be within 10 and 100',
         validateStatus: 'error',
         value: null,
       },
@@ -186,8 +194,10 @@ describe('RangeFilterPlugin', () => {
         value: [20, null],
       },
     });
-    const input = screen.getByRole('spinbutton');
-    expect(input).toHaveValue('20');
+
+    const inputs = screen.getAllByRole('spinbutton');
+    expect(inputs).toHaveLength(1);
+    expect(inputs[0]).toHaveValue('20');
   });
 
   it('should call setDataMask with correct less than filter', () => {
@@ -214,8 +224,10 @@ describe('RangeFilterPlugin', () => {
         validateStatus: undefined,
       },
     });
-    const input = screen.getByRole('spinbutton');
-    expect(input).toHaveValue('60');
+
+    const inputs = screen.getAllByRole('spinbutton');
+    expect(inputs).toHaveLength(1);
+    expect(inputs[0]).toHaveValue('60');
   });
 
   it('should call setDataMask with correct exact filter', () => {
@@ -242,5 +254,69 @@ describe('RangeFilterPlugin', () => {
         validateMessage: '',
       },
     });
+
+    const inputs = screen.getAllByRole('spinbutton');
+    expect(inputs).toHaveLength(1);
+    expect(inputs[0]).toHaveValue('10');
+  });
+
+  describe('Range Display Modes', () => {
+    it('should render only the slider in slider mode', () => {
+      getWrapper({
+        formData: {
+          rangeDisplayMode: RangeDisplayMode.Slider,
+        },
+      });
+
+      const sliders = screen.getAllByRole('slider');
+      expect(sliders.length).toBeGreaterThan(0);
+
+      expect(screen.queryAllByRole('spinbutton')).toHaveLength(0);
+    });
+
+    it('should render only inputs in input mode', () => {
+      getWrapper({
+        formData: {
+          rangeDisplayMode: RangeDisplayMode.Input,
+        },
+      });
+
+      const inputs = screen.getAllByRole('spinbutton');
+      expect(inputs).toHaveLength(2);
+
+      expect(screen.queryAllByRole('slider')).toHaveLength(0);
+    });
+
+    it('should render both slider and inputs in slider-and-input mode', () => {
+      getWrapper({
+        formData: {
+          rangeDisplayMode: RangeDisplayMode.SliderAndInput,
+        },
+      });
+
+      // Should show inputs
+      const inputs = screen.getAllByRole('spinbutton');
+      expect(inputs).toHaveLength(2);
+
+      // Should show slider
+      const sliders = screen.getAllByRole('slider');
+      expect(sliders.length).toBeGreaterThan(0);
+    });
+
+    it('should default to slider-and-input mode when not specified', () => {
+      getWrapper({
+        formData: {
+          // No rangeDisplayMode specified
+        },
+      });
+
+      // Should show inputs
+      const inputs = screen.getAllByRole('spinbutton');
+      expect(inputs).toHaveLength(2);
+
+      // Should show slider
+      const sliders = screen.getAllByRole('slider');
+      expect(sliders.length).toBeGreaterThan(0);
+    });
   });
 });
diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
index 0bd6e74d5a..8e6504d954 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
@@ -23,14 +23,17 @@ import {
   isEqualArray,
   NumberFormats,
   styled,
+  useTheme,
   t,
 } from '@superset-ui/core';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
 import { FilterBarOrientation } from 'src/dashboard/types';
-import Metadata from '@superset-ui/core/components/Metadata';
+// import Metadata from '@superset-ui/core/components/Metadata';
 import { isNumber } from 'lodash';
-import { FormItem, InputNumber } from '@superset-ui/core/components';
-import { PluginFilterRangeProps } from './types';
+import { InputNumber } from '@superset-ui/core/components/Input';
+import Slider from '@superset-ui/core/components/Slider';
+import { FormItem, Tooltip, Icons } from '@superset-ui/core/components';
+import { PluginFilterRangeProps, RangeDisplayMode } from './types';
 import { StatusMessage, FilterPluginStyle } from '../common';
 import { getRangeExtraFormData } from '../../utils';
 import { SingleValueType } from './SingleValueType';
@@ -49,13 +52,75 @@ const StyledDivider = styled.span`
 const Wrapper = styled.div`
   display: flex;
   justify-content: space-between;
+  width: 100%;
 
-  .ant-input-number {
-    width: 100%;
+  .antd5-input-number {
+    min-width: 80px;
     position: relative;
   }
 `;
 
+const SliderWrapper = styled.div`
+  ${({ theme }) => `
+    margin: ${theme.sizeUnit * 4}px 0;
+    padding: 0 ${theme.sizeUnit}px;
+  `}
+`;
+
+const TooltipContainer = styled.div`
+  ${({ theme }) => `
+    position: absolute;
+    top: -${theme.sizeUnit * 10}px;
+    right: 0px;
+    z-index: 100;
+    display: flex;
+    align-items: center;
+
+    .tooltip-icon {
+      margin-left: ${theme.sizeUnit * 2}px;
+    }
+  `}
+`;
+
+const HorizontalLayout = styled.div`
+  ${({ theme }) => `
+    display: flex;
+    flex-direction: column;
+    gap: ${theme.sizeUnit * 4}px;
+    width: 100%;
+
+    .controls-container {
+      display: flex;
+      align-items: center;
+      gap: ${theme.sizeUnit * 4}px;
+      width: 100%;
+
+      .slider-wrapper {
+        display: flex;
+        align-items: center;
+        flex: 2;
+      }
+
+      .slider-container {
+        flex: 1;
+        min-width: 180px;
+      }
+
+      .inputs-container {
+        min-width: 160px;
+        max-width: 200px;
+      }
+
+    }
+
+    .message-container {
+      width: 100%;
+      text-align: center;
+      padding-top: ${theme.sizeUnit * 2}px;
+    }
+  `}
+`;
+
 const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER);
 
 const getLabel = (
@@ -89,15 +154,21 @@ const validateRange = (
   enableSingleValue?: SingleValueType,
 ): { isValid: boolean; errorMessage: string | null } => {
   const [inputMin, inputMax] = values;
-  const requiredError = t('Filter value is required');
-  const rangeError = t('Please provide a value within range');
+  const requiredError = t('Please provide a valid min or max value');
+  const minMaxError = t('Min value cannot be greater than max value');
+  const rangeError = t('Numbers must be within %(min)s and %(max)s', {
+    min,
+    max,
+  });
+
+  // Single value validation
   if (enableSingleValue !== undefined) {
     const isSingleMin =
       enableSingleValue === SingleValueType.Minimum ||
       enableSingleValue === SingleValueType.Exact;
     const value = isSingleMin ? inputMin : inputMax;
 
-    if (!value && enableEmptyFilter) {
+    if (!isNumber(value)) {
       return { isValid: false, errorMessage: requiredError };
     }
 
@@ -109,33 +180,43 @@ const validateRange = (
   }
 
   // Range validation
-  if (enableEmptyFilter && (inputMin === null || inputMax === null)) {
-    return { isValid: false, errorMessage: t('Please provide a valid range') };
+  // Allow empty ranges if enableEmptyFilter is false
+  if (!enableEmptyFilter && inputMin === null && inputMax === null) {
+    return { isValid: true, errorMessage: null };
   }
 
-  if (!enableEmptyFilter && (inputMin !== null) !== (inputMax !== null)) {
-    return { isValid: false, errorMessage: t('Please provide a valid range') };
+  // If enableEmptyFilter is true, at least one value is required
+  if (enableEmptyFilter && inputMin === null && inputMax === null) {
+    return {
+      isValid: false,
+      errorMessage: requiredError,
+    };
   }
 
-  if (inputMin !== null && inputMax !== null) {
-    if (inputMin > inputMax) {
-      return {
-        isValid: false,
-        errorMessage: t('Minimum value cannot be higher than maximum value'),
-      };
-    }
-    if (inputMin < min || inputMax > max) {
-      return {
-        isValid: false,
-        errorMessage: t('Your range is not within the dataset range'),
-      };
-    }
+  // Check relationship between min and max when both are provided
+  if (inputMin !== null && inputMax !== null && inputMin > inputMax) {
+    return {
+      isValid: false,
+      errorMessage: minMaxError,
+    };
+  }
+
+  //   Check individual value bounds if provided
+  if (
+    (inputMin !== null && inputMin < min) ||
+    (inputMax !== null && inputMax > max)
+  ) {
+    return {
+      isValid: false,
+      errorMessage: rangeError,
+    };
   }
 
   return { isValid: true, errorMessage: null };
 };
 
 export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
+  const theme = useTheme();
   const {
     data,
     formData,
@@ -150,16 +231,23 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
     filterState,
     inputRef,
     filterBarOrientation = FilterBarOrientation.Vertical,
+    isOverflowingFilterBar,
   } = props;
+
   const [row] = data;
   // @ts-ignore
   const { min, max }: { min: number; max: number } = row;
   const { groupby, enableSingleValue, enableEmptyFilter, defaultValue } =
     formData;
+
+  // Get the display mode from formData
+  const rangeDisplayMode =
+    formData?.rangeDisplayMode || RangeDisplayMode.SliderAndInput;
+
   const minIndex = 0;
   const maxIndex = 1;
+
   const enableSingleExactValue = enableSingleValue === SingleValueType.Exact;
-  const rangeInput = enableSingleValue === undefined;
 
   const [col = ''] = ensureIsArray(groupby).map(getColumnLabel);
 
@@ -168,6 +256,30 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
   );
   const [error, setError] = useState<string | null>(null);
 
+  // Prepare slider value from input value, converting nulls to min/max
+  const sliderValue = useMemo(() => {
+    const [inputMin, inputMax] = inputValue;
+
+    // For single value filters
+    if (
+      enableSingleValue === SingleValueType.Minimum ||
+      enableSingleValue === SingleValueType.Exact
+    ) {
+      return inputMin !== null ? inputMin : min;
+    }
+    if (enableSingleValue === SingleValueType.Maximum) {
+      return inputMax !== null ? inputMax : max;
+    }
+
+    // For range filters - use placeholders when values are null
+    // If only min is provided, set max to the dataset max
+    // If only max is provided, set min to the dataset min
+    const sliderMin = inputMin !== null ? inputMin : min;
+    const sliderMax = inputMax !== null ? inputMax : max;
+
+    return [sliderMin, sliderMax];
+  }, [inputValue, min, max, enableSingleValue]);
+
   const updateDataMaskError = useCallback(
     (errorMessage: string | null) => {
       setDataMask({
@@ -206,39 +318,27 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
       return;
     }
 
-    if (
-      filterState.validateStatus === 'error' &&
-      error !== filterState.validateMessage
-    ) {
+    if (filterState.validateStatus === 'error') {
       setError(filterState.validateMessage);
 
-      const [inputMin, inputMax] = inputValue;
-
-      const { isValid, errorMessage } = validateRange(
-        inputValue,
-        min,
-        max,
-        enableEmptyFilter,
-        enableSingleValue,
-      );
-
-      const isDefaultError =
-        inputMin === null &&
-        inputMax === null &&
-        filterState.validateStatus === 'error';
-
-      if (!isValid || isDefaultError) {
-        setError(errorMessage);
-        updateDataMaskError(errorMessage);
-        return;
+      // Only re-validate if message changed to prevents redundant validation
+      if (error !== filterState.validateMessage) {
+        const { isValid, errorMessage } = validateRange(
+          inputValue,
+          min,
+          max,
+          enableEmptyFilter,
+          enableSingleValue,
+        );
+
+        if (!isValid) {
+          setError(errorMessage);
+          updateDataMaskError(errorMessage);
+        } else {
+          setError(null);
+          updateDataMaskValue(inputValue);
+        }
       }
-
-      setError(null);
-      updateDataMaskValue(inputValue);
-      return;
-    }
-    if (filterState.validateStatus === 'error') {
-      setError(filterState.validateMessage);
       return;
     }
 
@@ -261,6 +361,7 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
     }
   }, [JSON.stringify(filterState.value)]);
 
+  // Get just the filter behavior text without the range information (which is shown in the tooltip)
   const metadataText = useMemo(() => {
     switch (enableSingleValue) {
       case SingleValueType.Minimum:
@@ -270,19 +371,44 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
       case SingleValueType.Exact:
         return t('Filters for values equal to this exact value.');
       default:
-        return '';
+        return null;
     }
   }, [enableSingleValue]);
 
+  const keyPressed = useRef(false);
+
+  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
+    keyPressed.current = !!/^[0-9]$/.test(event.key);
+  };
+
   const handleChange = useCallback(
     (newValue: number | null, index: 0 | 1) => {
       if (row?.min === undefined && row?.max === undefined) {
         return;
       }
+
+      // When using increment/decrement buttons on an empty input,
+      // use the dataset min/max as the base value
+      let adjustedValue = newValue;
+      if (newValue !== null && inputValue[index] === null) {
+        if (keyPressed.current) {
+          adjustedValue = newValue;
+          keyPressed.current = false;
+        } else if (index === minIndex && newValue === 1) {
+          adjustedValue = min + 1;
+        } else if (index === minIndex && newValue === -1) {
+          adjustedValue = min - 1;
+        } else if (index === maxIndex && newValue === 1) {
+          adjustedValue = max + 1;
+        } else if (index === maxIndex && newValue === -1) {
+          adjustedValue = max - 1;
+        }
+      }
+
       const newInputValue: [number | null, number | null] =
         index === minIndex
-          ? [newValue, inputValue[maxIndex]]
-          : [inputValue[minIndex], newValue];
+          ? [adjustedValue, inputValue[maxIndex]]
+          : [inputValue[minIndex], adjustedValue];
 
       setInputValue(newInputValue);
 
@@ -303,29 +429,116 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
       setError(null);
       updateDataMaskValue(newInputValue);
     },
-    [col, min, max, enableEmptyFilter, enableSingleValue, setDataMask],
+    [
+      min,
+      max,
+      enableEmptyFilter,
+      enableSingleValue,
+      updateDataMaskError,
+      updateDataMaskValue,
+      inputValue,
+    ],
+  );
+
+  // Handler for slider change
+  const handleSliderChange = useCallback(
+    (value: number | number[]) => {
+      let newInputValue: RangeValue;
+
+      if (enableSingleValue !== undefined) {
+        const singleValue =
+          typeof value === 'number'
+            ? value
+            : Array.isArray(value) && value.length > 0
+              ? value[0]
+              : (min + max) / 2;
+
+        if (enableSingleValue === SingleValueType.Minimum) {
+          newInputValue = [singleValue, null];
+        } else if (enableSingleValue === SingleValueType.Maximum) {
+          newInputValue = [null, singleValue];
+        } else {
+          newInputValue = [singleValue, singleValue];
+        }
+      } else {
+        const arrayValue = Array.isArray(value) ? value : [min, max];
+        const [sliderMin, sliderMax] =
+          arrayValue.length >= 2 ? [arrayValue[0], arrayValue[1]] : [min, max];
+        newInputValue = [sliderMin, sliderMax];
+      }
+
+      setInputValue(newInputValue);
+      setError(null);
+      updateDataMaskValue(newInputValue);
+    },
+    [min, max, enableSingleValue, updateDataMaskValue],
   );
 
-  const formItemExtra = useMemo(() => {
+  const getMessageAndStatus = useCallback(() => {
+    const defaultMessage = t('Choose numbers between %(min)s and %(max)s', {
+      min,
+      max,
+    });
+
     if (error) {
-      return <StatusMessage status="error">{error}</StatusMessage>;
+      return { message: error, status: 'error' as const };
+    }
+
+    if (enableSingleValue !== undefined && metadataText) {
+      return { message: metadataText, status: 'help' as const };
     }
-    return undefined;
-  }, [error]);
+
+    return { message: defaultMessage, status: 'help' as const };
+  }, [error, min, max, enableSingleValue, metadataText]);
+
+  const MessageDisplay = useCallback(() => {
+    const { message, status } = getMessageAndStatus();
+
+    if (filterBarOrientation === FilterBarOrientation.Vertical) {
+      return <StatusMessage status={status}>{message}</StatusMessage>;
+    }
+
+    return null;
+  }, [getMessageAndStatus, filterBarOrientation]);
+
+  const InfoTooltip = useCallback(() => {
+    const { message, status } = getMessageAndStatus();
+
+    return (
+      <Tooltip title={message} placement="top">
+        <Icons.InfoCircleOutlined
+          iconSize="m"
+          iconColor={
+            status === 'error'
+              ? theme.colors.error.base
+              : theme.colors.grayscale.base
+          }
+          className="tooltip-icon"
+        />
+      </Tooltip>
+    );
+  }, [getMessageAndStatus]);
 
   useEffect(() => {
-    switch (enableSingleValue) {
-      case SingleValueType.Minimum:
-      case SingleValueType.Exact:
-        handleChange(null, maxIndex);
-        break;
-      case SingleValueType.Maximum:
-        handleChange(null, minIndex);
-        break;
-      default:
-        break;
+    if (enableSingleValue !== undefined) {
+      switch (enableSingleValue) {
+        case SingleValueType.Minimum:
+        case SingleValueType.Exact:
+          if (inputValue[maxIndex] !== null) {
+            handleChange(null, maxIndex);
+          }
+          break;
+        case SingleValueType.Maximum:
+          if (inputValue[minIndex] !== null) {
+            handleChange(null, minIndex);
+          }
+          break;
+        default:
+          break;
+      }
     }
 
+    // Reset data mask when single value mode changes
     setDataMask({
       extraFormData: {},
       filterState: {
@@ -335,53 +548,123 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
     });
   }, [enableSingleValue]);
 
+  const renderSlider = () => {
+    if (enableSingleValue !== undefined) {
+      return (
+        <SliderWrapper>
+          <Slider
+            min={min}
+            max={max}
+            value={Array.isArray(sliderValue) ? sliderValue[0] : sliderValue}
+            onChange={handleSliderChange}
+            tooltip={{
+              formatter: val => (val !== null ? numberFormatter(val) : ''),
+            }}
+          />
+        </SliderWrapper>
+      );
+    }
+    return (
+      <SliderWrapper data-test="range-filter-slider">
+        <Slider
+          min={min}
+          max={max}
+          range
+          value={Array.isArray(sliderValue) ? sliderValue : [min, sliderValue]}
+          onChange={handleSliderChange}
+          tooltip={{
+            formatter: val => (val !== null ? numberFormatter(val) : ''),
+          }}
+        />
+      </SliderWrapper>
+    );
+  };
+
+  const renderInputs = () => (
+    <Wrapper
+      tabIndex={-1}
+      ref={inputRef}
+      onFocus={setFocusedFilter}
+      onBlur={unsetFocusedFilter}
+      onMouseEnter={setHoveredFilter}
+      onMouseLeave={unsetHoveredFilter}
+      onMouseDown={() => setFilterActive(true)}
+      onMouseUp={() => setFilterActive(false)}
+    >
+      {/* Conditionally render based on enableSingleValue */}
+      {(enableSingleValue === undefined ||
+        enableSingleValue === SingleValueType.Minimum ||
+        enableSingleValue === SingleValueType.Exact) && (
+        <InputNumber
+          value={inputValue[minIndex]}
+          onChange={val => handleChange(val, minIndex)}
+          onKeyDown={handleKeyDown}
+          placeholder={`${min}`}
+          style={{ width: '100%' }}
+          status={filterState.validateStatus}
+          data-test="range-filter-from-input"
+        />
+      )}
+
+      {enableSingleValue === undefined && <StyledDivider>-</StyledDivider>}
+
+      {(enableSingleValue === undefined ||
+        enableSingleValue === SingleValueType.Maximum) && (
+        <InputNumber
+          value={inputValue[maxIndex]}
+          onChange={val => handleChange(val, maxIndex)}
+          onKeyDown={handleKeyDown}
+          placeholder={`${max}`}
+          style={{ width: '100%' }}
+          data-test="range-filter-to-input"
+          status={filterState.validateStatus}
+        />
+      )}
+    </Wrapper>
+  );
+
   return (
     <FilterPluginStyle height={height} width={width}>
       {Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
         <h4>{t('Chosen non-numeric column')}</h4>
       ) : (
-        <FormItem
-          aria-labelledby={`filter-name-${formData.nativeFilterId}`}
-          extra={formItemExtra}
-        >
-          <Wrapper
-            tabIndex={-1}
-            ref={inputRef}
-            onFocus={setFocusedFilter}
-            onBlur={unsetFocusedFilter}
-            onMouseEnter={setHoveredFilter}
-            onMouseLeave={unsetHoveredFilter}
-            onMouseDown={() => setFilterActive(true)}
-            onMouseUp={() => setFilterActive(false)}
-          >
-            {(enableSingleValue === SingleValueType.Minimum ||
-              enableSingleValue === SingleValueType.Exact ||
-              enableSingleValue === undefined) && (
-              <InputNumber
-                value={inputValue[minIndex]}
-                onChange={val => handleChange(val, minIndex)}
-                placeholder={`${min}`}
-                status={filterState.validateStatus}
-                data-test="range-filter-from-input"
-              />
-            )}
-            {enableSingleValue === undefined && (
-              <StyledDivider>-</StyledDivider>
-            )}
-            {(enableSingleValue === SingleValueType.Maximum ||
-              enableSingleValue === undefined) && (
-              <InputNumber
-                value={inputValue[maxIndex]}
-                onChange={val => handleChange(val, maxIndex)}
-                placeholder={`${max}`}
-                data-test="range-filter-to-input"
-                status={filterState.validateStatus}
-              />
-            )}
-          </Wrapper>
-          {(rangeInput ||
-            filterBarOrientation === FilterBarOrientation.Vertical) &&
-            !filterState.validateStatus && <Metadata value={metadataText} />}
+        <FormItem aria-labelledby={`filter-name-${formData.nativeFilterId}`}>
+          {filterBarOrientation === FilterBarOrientation.Horizontal &&
+          !isOverflowingFilterBar ? (
+            <HorizontalLayout>
+              <div className="controls-container">
+                <InfoTooltip />
+                {(rangeDisplayMode === RangeDisplayMode.Slider ||
+                  rangeDisplayMode === RangeDisplayMode.SliderAndInput) && (
+                  <div className="slider-wrapper">
+                    <div className="slider-container">{renderSlider()}</div>
+                  </div>
+                )}
+                {(rangeDisplayMode === RangeDisplayMode.Input ||
+                  rangeDisplayMode === RangeDisplayMode.SliderAndInput) && (
+                  <div className="inputs-container">{renderInputs()}</div>
+                )}
+              </div>
+            </HorizontalLayout>
+          ) : (
+            <>
+              <div style={{ position: 'relative' }}>
+                {isOverflowingFilterBar && (
+                  <TooltipContainer>
+                    <InfoTooltip />
+                  </TooltipContainer>
+                )}
+                {(rangeDisplayMode === RangeDisplayMode.Slider ||
+                  rangeDisplayMode === RangeDisplayMode.SliderAndInput) &&
+                  renderSlider()}
+                {(rangeDisplayMode === RangeDisplayMode.Input ||
+                  rangeDisplayMode === RangeDisplayMode.SliderAndInput) &&
+                  renderInputs()}
+
+                <MessageDisplay />
+              </div>
+            </>
+          )}
         </FormItem>
       )}
     </FilterPluginStyle>
diff --git a/superset-frontend/src/filters/components/Range/types.ts b/superset-frontend/src/filters/components/Range/types.ts
index 25a18bb3e8..d020ebf96d 100644
--- a/superset-frontend/src/filters/components/Range/types.ts
+++ b/superset-frontend/src/filters/components/Range/types.ts
@@ -26,9 +26,16 @@ import { RefObject } from 'react';
 import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
 import { FilterBarOrientation } from '../../../dashboard/types';
 
+export enum RangeDisplayMode {
+  Slider = 'slider',
+  Input = 'input',
+  SliderAndInput = 'slider-and-input',
+}
+
 interface PluginFilterSelectCustomizeProps {
   max?: number;
   min?: number;
+  rangeDisplayMode?: RangeDisplayMode;
 }
 
 export type PluginFilterRangeQueryFormData = QueryFormData &
diff --git a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx
index 91a62e4c98..15d2c8b446 100644
--- a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx
@@ -48,6 +48,9 @@ const ControlContainer = styled.div<{
     ${({ validateStatus, theme }) =>
       validateStatus && `border-color: ${theme.colors[validateStatus]?.base}`}
   }
+  & > div {
+    width: 100%;
+  }
 `;
 
 export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
diff --git a/superset-frontend/src/filters/components/common.ts b/superset-frontend/src/filters/components/common.ts
index 2074309526..6ba9eabb14 100644
--- a/superset-frontend/src/filters/components/common.ts
+++ b/superset-frontend/src/filters/components/common.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { styled } from '@superset-ui/core';
+import { FormItem } from '@superset-ui/core/components';
 import { PluginFilterStylesProps } from './types';
 
 export const RESPONSIVE_WIDTH = 0;
@@ -26,8 +27,20 @@ export const FilterPluginStyle = styled.div<PluginFilterStylesProps>`
   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';
+  status?: 'error' | 'warning' | 'info' | 'help';
+  centerText?: boolean;
 }>`
-  color: ${({ theme, status = 'error' }) => theme.colors[status]?.base};
+  color: ${({ theme, status = 'error' }) =>
+    status === 'help'
+      ? theme.colors.grayscale.light1
+      : theme.colors[status]?.base};
+  text-align: ${({ centerText }) => (centerText ? 'center' : 'left')};
+  width: 100%;
 `;