mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
feat(native-filters): add markers and number formatter + simple tests (#14981)
This commit is contained in:
parent
8c01c13d90
commit
355223d3fe
@ -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 }) => (
|
||||||
|
<SuperChart
|
||||||
|
chartType="filter_range"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
queriesData={[{ data: [{ min: 10, max: 100 }] }]}
|
||||||
|
filterState={{ value: [10, 70] }}
|
||||||
|
formData={{
|
||||||
|
groupby: ['SP_POP_TOTL'],
|
||||||
|
adhoc_filters: [],
|
||||||
|
extra_filters: [],
|
||||||
|
viz_type: 'filter_range',
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
hooks={{
|
||||||
|
setDataMask: action('setDataMask'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
@ -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
|
||||||
|
<RangeFilterPlugin
|
||||||
|
// @ts-ignore
|
||||||
|
{...transformProps({
|
||||||
|
...rangeProps,
|
||||||
|
formData: { ...rangeProps.formData, ...props },
|
||||||
|
})}
|
||||||
|
setDataMask={setDataMask}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { t } from '@superset-ui/core';
|
import { getNumberFormatter, NumberFormats, t } from '@superset-ui/core';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Slider } from 'src/common/components';
|
import { Slider } from 'src/common/components';
|
||||||
import { PluginFilterRangeProps } from './types';
|
import { PluginFilterRangeProps } from './types';
|
||||||
@ -35,6 +35,8 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
|||||||
inputRef,
|
inputRef,
|
||||||
filterState,
|
filterState,
|
||||||
} = props;
|
} = props;
|
||||||
|
const numberFormatter = getNumberFormatter(NumberFormats.SMART_NUMBER);
|
||||||
|
|
||||||
const [row] = data;
|
const [row] = data;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { min, max }: { min: number; max: number } = row;
|
const { min, max }: { min: number; max: number } = row;
|
||||||
@ -43,15 +45,55 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
|||||||
const [value, setValue] = useState<[number, number]>(
|
const [value, setValue] = useState<[number, number]>(
|
||||||
defaultValue ?? [min, max],
|
defaultValue ?? [min, max],
|
||||||
);
|
);
|
||||||
|
const [marks, setMarks] = useState<{ [key: number]: string }>({});
|
||||||
|
|
||||||
const handleAfterChange = (value: [number, number]) => {
|
const getBounds = (
|
||||||
const [lower, upper] = value;
|
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);
|
setValue(value);
|
||||||
|
const { lower, upper } = getBounds(value);
|
||||||
|
setMarks(getMarks(lower, upper));
|
||||||
|
|
||||||
setDataMask({
|
setDataMask({
|
||||||
extraFormData: getRangeExtraFormData(col, lower, upper),
|
extraFormData: getRangeExtraFormData(col, lower, upper),
|
||||||
filterState: {
|
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]);
|
handleAfterChange(filterState.value ?? [min, max]);
|
||||||
}, [JSON.stringify(filterState.value)]);
|
}, [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 (
|
return (
|
||||||
<Styles height={height} width={width}>
|
<Styles height={height} width={width}>
|
||||||
{Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
|
{Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
|
||||||
@ -80,10 +116,12 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
|||||||
range
|
range
|
||||||
min={min}
|
min={min}
|
||||||
max={max}
|
max={max}
|
||||||
value={value}
|
value={value ?? [min, max]}
|
||||||
onAfterChange={handleAfterChange}
|
onAfterChange={handleAfterChange}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
tipFormatter={value => numberFormatter(value)}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
marks={marks}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
Loading…
Reference in New Issue
Block a user