Skip to content

Commit 2578b35

Browse files
payosegeidogithub-actions[bot]actions-usermsyavuz
authored andcommitted
feat(filter): Add Slider Range Inputs Option for Numerical Range Filters (apache#33170)
Co-authored-by: Geido <60598000+geido@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Mehmet Salih Yavuz <salih.yavuz@proton.me>
1 parent cee5c25 commit 2578b35

File tree

6 files changed

+127
-22
lines changed

6 files changed

+127
-22
lines changed

superset-frontend/cypress-base/cypress/e2e/dashboard/utils.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,9 +366,26 @@ export function saveNativeFilterSettings(charts: ChartSpec[]) {
366366
cy.get(nativeFilters.modal.footer)
367367
.contains('Save')
368368
.should('be.visible')
369-
.click();
369+
.click({ force: true });
370+
371+
// Wait for modal to either close or remain open
372+
cy.get('body').should($body => {
373+
const modalExists = $body.find(nativeFilters.modal.container).length > 0;
374+
if (modalExists) {
375+
cy.get(nativeFilters.modal.footer)
376+
.contains('Save')
377+
.should('be.visible')
378+
.click({ force: true });
379+
}
380+
});
381+
382+
// Ensure modal is closed
370383
cy.get(nativeFilters.modal.container).should('not.exist');
371-
charts.forEach(waitForChartLoad);
384+
385+
// Wait for all charts to load
386+
charts.forEach(chart => {
387+
waitForChartLoad(chart);
388+
});
372389
}
373390

374391
/** ************************************************************************

superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterControl.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ const HorizontalFormItem = styled(StyledFormItem)<{
147147
}
148148
149149
.ant-form-item-label {
150+
display: flex;
151+
align-items: center;
150152
overflow: visible;
151153
padding-bottom: 0;
152154
margin-right: ${({ theme }) => theme.gridUnit * 2}px;
@@ -162,7 +164,7 @@ const HorizontalFormItem = styled(StyledFormItem)<{
162164
}
163165
164166
.ant-form-item-control {
165-
width: ${({ inverseSelection }) => (inverseSelection ? 252 : 164)}px;
167+
min-width: ${({ inverseSelection }) => (inverseSelection ? 252 : 164)}px;
166168
}
167169
168170
.select-container {

superset-frontend/src/dashboard/components/nativeFilters/selectors.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ const cachedNativeFilterDataForChart: Record<
342342
rejectedColumns: Set<string>;
343343
}
344344
> = {};
345+
345346
export const selectNativeIndicatorsForChart = (
346347
nativeFilters: Filters,
347348
dataMask: DataMaskStateWithId,
@@ -355,17 +356,16 @@ export const selectNativeIndicatorsForChart = (
355356

356357
const cachedFilterData = cachedNativeFilterDataForChart[chartId];
357358
if (
358-
cachedNativeIndicatorsForChart[chartId] &&
359-
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
360-
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
361-
cachedFilterData?.nativeFilters === nativeFilters &&
362-
cachedFilterData?.chartLayoutItems === chartLayoutItems &&
363-
cachedFilterData?.chartConfiguration === chartConfiguration &&
364-
cachedFilterData?.dataMask === dataMask
359+
cachedNativeIndicatorsForChart[chartId]?.length &&
360+
areObjectsEqual(cachedFilterData.appliedColumns, appliedColumns) &&
361+
areObjectsEqual(cachedFilterData.rejectedColumns, rejectedColumns) &&
362+
areObjectsEqual(cachedFilterData.nativeFilters, nativeFilters) &&
363+
areObjectsEqual(cachedFilterData.chartLayoutItems, chartLayoutItems) &&
364+
areObjectsEqual(cachedFilterData.chartConfiguration, chartConfiguration) &&
365+
areObjectsEqual(cachedFilterData.dataMask, dataMask)
365366
) {
366367
return cachedNativeIndicatorsForChart[chartId];
367368
}
368-
369369
const nativeFilterIndicators =
370370
nativeFilters &&
371371
Object.values(nativeFilters)

superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx

Lines changed: 87 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
* under the License.
1818
*/
1919
import { AppSection, GenericDataType } from '@superset-ui/core';
20-
import { fireEvent, render, screen } from 'spec/helpers/testing-library';
20+
import userEvent from '@testing-library/user-event';
21+
import { render, screen } from 'spec/helpers/testing-library';
2122
import RangeFilterPlugin from './RangeFilterPlugin';
23+
import { RangeDisplayMode } from './types';
2224
import { SingleValueType } from './SingleValueType';
2325
import transformProps from './transformProps';
2426

@@ -101,34 +103,40 @@ describe('RangeFilterPlugin', () => {
101103
jest.clearAllMocks();
102104
});
103105

104-
it('should render two numerical inputs', () => {
106+
it('should render two numerical inputs and a slider by default', () => {
105107
getWrapper();
106108

107109
const inputs = screen.getAllByRole('spinbutton');
108110
expect(inputs).toHaveLength(2);
109111

110112
expect(inputs[0]).toHaveValue('10');
111113
expect(inputs[1]).toHaveValue('70');
114+
115+
// For a range slider, there are two slider handles
116+
const sliders = screen.getAllByRole('slider');
117+
expect(sliders.length).toBeGreaterThan(0);
112118
});
113119

114-
it('should set the data mask to error when the range is incorrect', () => {
120+
it('should set the data mask to error when the range is incorrect', async () => {
115121
getWrapper({ filterState: { value: [null, null] } });
116122

117123
const inputs = screen.getAllByRole('spinbutton');
118124
const fromInput = inputs[0];
119125
const toInput = inputs[1];
120126

121-
fireEvent.change(fromInput, { target: { value: 20 } });
127+
userEvent.clear(fromInput);
128+
userEvent.type(fromInput, '20');
122129

123-
fireEvent.change(toInput, { target: { value: 10 } });
130+
userEvent.clear(toInput);
131+
userEvent.type(toInput, '10');
124132

125-
fireEvent.blur(toInput);
133+
userEvent.tab();
126134

127135
expect(setDataMask).toHaveBeenCalledWith({
128136
extraFormData: {},
129137
filterState: {
130138
label: '',
131-
validateMessage: 'Please provide a valid range',
139+
validateMessage: 'Numbers must be within 10 and 100',
132140
validateStatus: 'error',
133141
value: null,
134142
},
@@ -186,8 +194,10 @@ describe('RangeFilterPlugin', () => {
186194
value: [20, null],
187195
},
188196
});
189-
const input = screen.getByRole('spinbutton');
190-
expect(input).toHaveValue('20');
197+
198+
const inputs = screen.getAllByRole('spinbutton');
199+
expect(inputs).toHaveLength(1);
200+
expect(inputs[0]).toHaveValue('20');
191201
});
192202

193203
it('should call setDataMask with correct less than filter', () => {
@@ -214,8 +224,10 @@ describe('RangeFilterPlugin', () => {
214224
validateStatus: undefined,
215225
},
216226
});
217-
const input = screen.getByRole('spinbutton');
218-
expect(input).toHaveValue('60');
227+
228+
const inputs = screen.getAllByRole('spinbutton');
229+
expect(inputs).toHaveLength(1);
230+
expect(inputs[0]).toHaveValue('60');
219231
});
220232

221233
it('should call setDataMask with correct exact filter', () => {
@@ -242,5 +254,69 @@ describe('RangeFilterPlugin', () => {
242254
validateMessage: '',
243255
},
244256
});
257+
258+
const inputs = screen.getAllByRole('spinbutton');
259+
expect(inputs).toHaveLength(1);
260+
expect(inputs[0]).toHaveValue('10');
261+
});
262+
263+
describe('Range Display Modes', () => {
264+
it('should render only the slider in slider mode', () => {
265+
getWrapper({
266+
formData: {
267+
rangeDisplayMode: RangeDisplayMode.Slider,
268+
},
269+
});
270+
271+
const sliders = screen.getAllByRole('slider');
272+
expect(sliders.length).toBeGreaterThan(0);
273+
274+
expect(screen.queryAllByRole('spinbutton')).toHaveLength(0);
275+
});
276+
277+
it('should render only inputs in input mode', () => {
278+
getWrapper({
279+
formData: {
280+
rangeDisplayMode: RangeDisplayMode.Input,
281+
},
282+
});
283+
284+
const inputs = screen.getAllByRole('spinbutton');
285+
expect(inputs).toHaveLength(2);
286+
287+
expect(screen.queryAllByRole('slider')).toHaveLength(0);
288+
});
289+
290+
it('should render both slider and inputs in slider-and-input mode', () => {
291+
getWrapper({
292+
formData: {
293+
rangeDisplayMode: RangeDisplayMode.SliderAndInput,
294+
},
295+
});
296+
297+
// Should show inputs
298+
const inputs = screen.getAllByRole('spinbutton');
299+
expect(inputs).toHaveLength(2);
300+
301+
// Should show slider
302+
const sliders = screen.getAllByRole('slider');
303+
expect(sliders.length).toBeGreaterThan(0);
304+
});
305+
306+
it('should default to slider-and-input mode when not specified', () => {
307+
getWrapper({
308+
formData: {
309+
// No rangeDisplayMode specified
310+
},
311+
});
312+
313+
// Should show inputs
314+
const inputs = screen.getAllByRole('spinbutton');
315+
expect(inputs).toHaveLength(2);
316+
317+
// Should show slider
318+
const sliders = screen.getAllByRole('slider');
319+
expect(sliders.length).toBeGreaterThan(0);
320+
});
245321
});
246322
});

superset-frontend/src/filters/components/Range/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ import { RefObject } from 'react';
2626
import { PluginFilterHooks, PluginFilterStylesProps } from '../types';
2727
import { FilterBarOrientation } from '../../../dashboard/types';
2828

29+
export enum RangeDisplayMode {
30+
Slider = 'slider',
31+
Input = 'input',
32+
SliderAndInput = 'slider-and-input',
33+
}
34+
2935
interface PluginFilterSelectCustomizeProps {
3036
max?: number;
3137
min?: number;
38+
rangeDisplayMode?: RangeDisplayMode;
3239
}
3340

3441
export type PluginFilterRangeQueryFormData = QueryFormData &

superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ const ControlContainer = styled.div<{
4444
${({ validateStatus, theme }) =>
4545
validateStatus && `border-color: ${theme.colors[validateStatus]?.base}`}
4646
}
47+
& > div {
48+
width: 100%;
49+
}
4750
`;
4851

4952
export default function TimeFilterPlugin(props: PluginFilterTimeProps) {

0 commit comments

Comments
 (0)