mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -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
|
||||
* 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 (
|
||||
<Styles height={height} width={width}>
|
||||
{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}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user