diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.stories.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.stories.tsx new file mode 100644 index 0000000000..fb442e86bc --- /dev/null +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.stories.tsx @@ -0,0 +1,78 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { + SuperChart, + getChartTransformPropsRegistry, + GenericDataType, +} from '@superset-ui/core'; +import RangeFilterPlugin from './index'; +import transformProps from './transformProps'; + +new RangeFilterPlugin().configure({ key: 'filter_range' }).register(); + +getChartTransformPropsRegistry().registerValue('filter_range', transformProps); + +export default { + title: 'Filter Plugins', +}; + +export const range = ({ width, height }: { width: number; height: number }) => ( + +); diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx new file mode 100644 index 0000000000..cf9420ecff --- /dev/null +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx @@ -0,0 +1,121 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { AppSection, GenericDataType } from '@superset-ui/core'; +import React from 'react'; +import { render } from 'spec/helpers/testing-library'; +import RangeFilterPlugin from './RangeFilterPlugin'; +import transformProps from './transformProps'; + +const rangeProps = { + formData: { + datasource: '3__table', + groupby: ['SP_POP_TOTL'], + adhocFilters: [], + extraFilters: [], + extraFormData: {}, + granularitySqla: 'ds', + metrics: [ + { + aggregate: 'MIN', + column: { + column_name: 'SP_POP_TOTL', + id: 1, + type_generic: GenericDataType.NUMERIC, + }, + expressionType: 'SIMPLE', + hasCustomLabel: true, + label: 'min', + }, + { + aggregate: 'MAX', + column: { + column_name: 'SP_POP_TOTL', + id: 2, + type_generic: GenericDataType.NUMERIC, + }, + expressionType: 'SIMPLE', + hasCustomLabel: true, + label: 'max', + }, + ], + rowLimit: 1000, + showSearch: true, + defaultValue: [10, 70], + timeRangeEndpoints: ['inclusive', 'exclusive'], + urlParams: {}, + vizType: 'filter_range', + inputRef: { current: null }, + }, + height: 20, + hooks: {}, + filterState: { value: [10, 70] }, + queriesData: [ + { + rowcount: 1, + colnames: ['min', 'max'], + coltypes: [GenericDataType.NUMERIC, GenericDataType.NUMERIC], + data: [{ min: 10, max: 100 }], + applied_filters: [], + rejected_filters: [], + }, + ], + width: 220, + behaviors: ['NATIVE_FILTER'], + isRefreshing: false, + appSection: AppSection.DASHBOARD, +}; + +describe('RangeFilterPlugin', () => { + const setDataMask = jest.fn(); + const getWrapper = (props = {}) => + render( + // @ts-ignore + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call setDataMask with correct filter', () => { + getWrapper(); + expect(setDataMask).toHaveBeenCalledWith({ + extraFormData: { + filters: [ + { + col: 'SP_POP_TOTL', + op: '<=', + val: 70, + }, + ], + }, + filterState: { + label: 'x ≤ 70', + value: [10, 70], + }, + }); + }); +}); diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx index 083c41e67e..8d88e516bc 100644 --- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { t } from '@superset-ui/core'; +import { getNumberFormatter, NumberFormats, t } from '@superset-ui/core'; import React, { useEffect, useState } from 'react'; import { Slider } from 'src/common/components'; import { PluginFilterRangeProps } from './types'; @@ -35,6 +35,8 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { inputRef, filterState, } = props; + const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER); + const [row] = data; // @ts-ignore const { min, max }: { min: number; max: number } = row; @@ -43,15 +45,55 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { const [value, setValue] = useState<[number, number]>( defaultValue ?? [min, max], ); + const [marks, setMarks] = useState<{ [key: number]: string }>({}); - const handleAfterChange = (value: [number, number]) => { - const [lower, upper] = value; + const getBounds = ( + value: [number, number], + ): { lower: number | null; upper: number | null } => { + const [lowerRaw, upperRaw] = value; + return { + lower: lowerRaw > min ? lowerRaw : null, + upper: upperRaw < max ? upperRaw : null, + }; + }; + + const getLabel = (lower: number | null, upper: number | null): string => { + if (lower !== null && upper !== null) { + return `${numberFormatter(lower)} ≤ x ≤ ${numberFormatter(upper)}`; + } + if (lower !== null) { + return `x ≥ ${numberFormatter(lower)}`; + } + if (upper !== null) { + return `x ≤ ${numberFormatter(upper)}`; + } + return ''; + }; + + const getMarks = ( + lower: number | null, + upper: number | null, + ): { [key: number]: string } => { + const newMarks: { [key: number]: string } = {}; + if (lower !== null) { + newMarks[lower] = numberFormatter(lower); + } + if (upper !== null) { + newMarks[upper] = numberFormatter(upper); + } + return newMarks; + }; + + const handleAfterChange = (value: [number, number]): void => { setValue(value); + const { lower, upper } = getBounds(value); + setMarks(getMarks(lower, upper)); setDataMask({ extraFormData: getRangeExtraFormData(col, lower, upper), filterState: { - value, + value: lower !== null || upper !== null ? value : null, + label: getLabel(lower, upper), }, }); }; @@ -64,12 +106,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { handleAfterChange(filterState.value ?? [min, max]); }, [JSON.stringify(filterState.value)]); - useEffect(() => { - handleAfterChange(defaultValue ?? [min, max]); - // I think after Config Modal update some filter it re-creates default value for all other filters - // so we can process it like this `JSON.stringify` or start to use `Immer` - }, [JSON.stringify(defaultValue)]); - return ( {Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? ( @@ -80,10 +116,12 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { range min={min} max={max} - value={value} + value={value ?? [min, max]} onAfterChange={handleAfterChange} onChange={handleChange} + tipFormatter={value => numberFormatter(value)} ref={inputRef} + marks={marks} /> )}