Skip to content

feat(filter): Add Slider Range Inputs Option for Numerical Range Filters #33170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
82d54ff
Re-implement Slider in Numerical Range filter
payose Apr 17, 2025
f2fceea
fixed tooltip icon
payose Apr 17, 2025
26c1584
User can create a numerical range filter with different display modes
payose Apr 22, 2025
0e6d614
User can create a numerical range filter with different display modes
payose Apr 23, 2025
87551ba
Update e2e.ts
payose Apr 23, 2025
1cef9c4
korbit ai review changes
payose Apr 23, 2025
4c52c73
fix for when single value is enabled
payose Apr 28, 2025
6e7a4d1
fixes for styled divs
payose Apr 30, 2025
abad65d
single value test update
payose Apr 30, 2025
26bcf6b
Fix range filter validation logic for required values
payose May 2, 2025
08ac2be
fix(Native Filters): Keep default filter values when configuring crea…
geido Apr 22, 2025
4f305d9
chore(🦾): bump python pandas subpackage(s) (#33263)
github-actions[bot] Apr 29, 2025
4b384b1
chore(🦾): bump python deprecation subpackage(s) (#33260)
github-actions[bot] Apr 29, 2025
6312194
chore(🦾): bump python packaging 24.2 -> 25.0 (#33259)
github-actions[bot] Apr 29, 2025
92899fa
removed un-needed comment
payose May 2, 2025
094c9f2
use min/max as base value for increment/decrememt
payose May 5, 2025
af9756b
CI fix
payose May 6, 2025
53d32d0
review fixes
payose May 8, 2025
1050419
fix for typed input defaulting to min/max value
payose May 10, 2025
e825961
fix: stack rangefilters in the horizontal overflow
payose May 27, 2025
0e2c1f7
fix(RangeFilter): prevent overflow
msyavuz Jun 4, 2025
a2c3977
fix slider track in configform
payose Jun 6, 2025
b38c3c6
add tooltip to more-filters dropdown
payose Jun 18, 2025
52869cb
conflicts resolved
payose Jun 28, 2025
972b57c
Range filter display modes update
payose Jun 30, 2025
dedded8
range display mode update
payose Jun 30, 2025
136c639
ci fix
payose Jun 30, 2025
a696c5f
ci fix - display mode :: input mode & input and slider mode
payose Jun 30, 2025
75eaabd
ci fix - display mode :: input mode & input and slider mode
payose Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -183,46 +183,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');
});
});

Expand Down
21 changes: 19 additions & 2 deletions superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}

/** ************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,96 @@
* under the License.
*/
import {
CSSProperties,
cloneElement,
forwardRef,
ReactElement,
RefObject,
useEffect,
useImperativeHandle,
useLayoutEffect,
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;

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -206,6 +290,7 @@ export const DropdownContainer = forwardRef(
overflowedItems.length,
previousWidth,
width,
popoverVisible,
]);

useEffect(() => {
Expand Down Expand Up @@ -376,5 +461,3 @@ export const DropdownContainer = forwardRef(
);
},
);

export type { DropdownItem, DropdownRef };
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
Loading
Loading