Skip to content

Commit

Permalink
feat(native-filters): add temporal support to select filter (apache#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
villebro authored and Allan Caetano de Oliveira committed May 21, 2021
1 parent f52ba02 commit bb5a945
Show file tree
Hide file tree
Showing 21 changed files with 391 additions and 60 deletions.
92 changes: 91 additions & 1 deletion superset-frontend/spec/javascripts/filters/utils_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,18 @@
*/

import {
GenericDataType,
getNumberFormatter,
getTimeFormatter,
NumberFormats,
TimeFormats,
} from '@superset-ui/core';
import {
getDataRecordFormatter,
getRangeExtraFormData,
getSelectExtraFormData,
} from '../../../src/filters/utils';
} from 'src/filters/utils';
import { FALSE_STRING, NULL_STRING, TRUE_STRING } from 'src/utils/common';

describe('Filter utils', () => {
describe('getRangeExtraFormData', () => {
Expand Down Expand Up @@ -157,4 +166,85 @@ describe('Filter utils', () => {
});
});
});

describe('getDataRecordFormatter', () => {
it('default formatter returns expected values', () => {
const formatter = getDataRecordFormatter();
expect(formatter(null, GenericDataType.STRING)).toEqual(NULL_STRING);
expect(formatter(null, GenericDataType.NUMERIC)).toEqual(NULL_STRING);
expect(formatter(null, GenericDataType.TEMPORAL)).toEqual(NULL_STRING);
expect(formatter(null, GenericDataType.BOOLEAN)).toEqual(NULL_STRING);
expect(formatter('foo', GenericDataType.STRING)).toEqual('foo');
expect(formatter('foo', GenericDataType.NUMERIC)).toEqual('foo');
expect(formatter('foo', GenericDataType.TEMPORAL)).toEqual('foo');
expect(formatter('foo', GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter(true, GenericDataType.BOOLEAN)).toEqual(TRUE_STRING);
expect(formatter(false, GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter('true', GenericDataType.BOOLEAN)).toEqual(TRUE_STRING);
expect(formatter('false', GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter('TRUE', GenericDataType.BOOLEAN)).toEqual(TRUE_STRING);
expect(formatter('FALSE', GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter(0, GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter(1, GenericDataType.BOOLEAN)).toEqual(TRUE_STRING);
expect(formatter(2, GenericDataType.BOOLEAN)).toEqual(TRUE_STRING);
expect(formatter(0, GenericDataType.STRING)).toEqual('0');
expect(formatter(0, GenericDataType.NUMERIC)).toEqual('0');
expect(formatter(0, GenericDataType.TEMPORAL)).toEqual('0');
expect(formatter(1234567.89, GenericDataType.STRING)).toEqual(
'1234567.89',
);
expect(formatter(1234567.89, GenericDataType.NUMERIC)).toEqual(
'1234567.89',
);
expect(formatter(1234567.89, GenericDataType.TEMPORAL)).toEqual(
'1234567.89',
);
expect(formatter(1234567.89, GenericDataType.BOOLEAN)).toEqual(
TRUE_STRING,
);
});

it('formatter with defined formatters returns expected values', () => {
const formatter = getDataRecordFormatter({
timeFormatter: getTimeFormatter(TimeFormats.DATABASE_DATETIME),
numberFormatter: getNumberFormatter(NumberFormats.SMART_NUMBER),
});
expect(formatter(null, GenericDataType.STRING)).toEqual(NULL_STRING);
expect(formatter(null, GenericDataType.NUMERIC)).toEqual(NULL_STRING);
expect(formatter(null, GenericDataType.TEMPORAL)).toEqual(NULL_STRING);
expect(formatter(null, GenericDataType.BOOLEAN)).toEqual(NULL_STRING);
expect(formatter('foo', GenericDataType.STRING)).toEqual('foo');
expect(formatter('foo', GenericDataType.NUMERIC)).toEqual('foo');
expect(formatter('foo', GenericDataType.TEMPORAL)).toEqual('foo');
expect(formatter('foo', GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter(0, GenericDataType.STRING)).toEqual('0');
expect(formatter(0, GenericDataType.NUMERIC)).toEqual('0');
expect(formatter(0, GenericDataType.TEMPORAL)).toEqual(
'1970-01-01 00:00:00',
);
expect(formatter(0, GenericDataType.BOOLEAN)).toEqual(FALSE_STRING);
expect(formatter(1234567.89, GenericDataType.STRING)).toEqual(
'1234567.89',
);
expect(formatter(1234567.89, GenericDataType.NUMERIC)).toEqual('1.23M');
expect(formatter(1234567.89, GenericDataType.TEMPORAL)).toEqual(
'1970-01-01 00:20:34',
);
expect(formatter(1234567.89, GenericDataType.BOOLEAN)).toEqual(
TRUE_STRING,
);
expect(formatter('1970-01-01 00:00:00', GenericDataType.STRING)).toEqual(
'1970-01-01 00:00:00',
);
expect(formatter('1970-01-01 00:00:00', GenericDataType.NUMERIC)).toEqual(
'1970-01-01 00:00:00',
);
expect(formatter('1970-01-01 00:00:00', GenericDataType.BOOLEAN)).toEqual(
FALSE_STRING,
);
expect(
formatter('1970-01-01 00:00:00', GenericDataType.TEMPORAL),
).toEqual('1970-01-01 00:00:00');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,48 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Behavior, DataMask, t, tn, ensureIsArray } from '@superset-ui/core';
import {
createMultiFormatter,
Behavior,
DataMask,
ensureIsArray,
GenericDataType,
t,
tn,
} from '@superset-ui/core';
import React, { useEffect, useState } from 'react';
import { Select } from 'src/common/components';
import { PluginFilterSelectProps } from './types';
import { Styles, StyledSelect } from '../common';
import { getSelectExtraFormData } from '../../utils';
import { StyledSelect, Styles } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';

const { Option } = Select;

const timeFormatter = createMultiFormatter({
id: 'smart_date_verbose',
label: 'Adaptive temporal formatter',
formats: {
millisecond: '%Y-%m-%d %H:%M:%S.%L',
second: '%Y-%m-%d %H:%M:%S',
minute: '%Y-%m-%d %H:%M',
hour: '%Y-%m-%d %H:%M:%M',
day: '%Y-%m-%d',
week: '%Y-%m-%d',
month: '%Y-%m-%d',
year: '%Y-%m-%d',
},
});

export default function PluginFilterSelect(props: PluginFilterSelectProps) {
const { data, formData, height, width, behaviors, setDataMask } = props;
const {
coltypeMap,
data,
formData,
height,
width,
behaviors,
setDataMask,
} = props;
const {
defaultValue,
enableEmptyFilter,
Expand All @@ -37,10 +68,16 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
inputRef,
} = formData;

const [values, setValues] = useState<(string | number)[]>(defaultValue ?? []);
const [values, setValues] = useState<(string | number | boolean)[]>(
defaultValue ?? [],
);
const groupby = ensureIsArray<string>(formData.groupby);

let { groupby = [] } = formData;
groupby = Array.isArray(groupby) ? groupby : [groupby];
const [col] = groupby;
const datatype: GenericDataType = coltypeMap[col];
const labelFormatter = getDataRecordFormatter({
timeFormatter,
});

const handleChange = (
value?: (number | string)[] | number | string | null,
Expand All @@ -50,7 +87,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
);
setValues(resultValue);

const [col] = groupby;
const emptyFilter =
enableEmptyFilter && !inverseSelection && resultValue?.length === 0;

Expand Down Expand Up @@ -104,6 +140,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
<Styles height={height} width={width}>
<StyledSelect
allowClear
// @ts-ignore
value={values}
showSearch={showSearch}
mode={multiSelect ? 'multiple' : undefined}
Expand All @@ -113,10 +150,11 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
ref={inputRef}
>
{(data || []).map(row => {
const option = `${groupby.map(col => row[col])[0]}`;
const [value] = groupby.map(col => row[col]);
return (
<Option key={option} value={option}>
{option}
// @ts-ignore
<Option key={`${value}`} value={value}>
{labelFormatter(value, datatype)}
</Option>
);
})}
Expand Down
24 changes: 14 additions & 10 deletions superset-frontend/src/filters/components/Select/buildQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ import { DEFAULT_FORM_DATA, PluginFilterSelectQueryFormData } from './types';

export default function buildQuery(formData: PluginFilterSelectQueryFormData) {
const { sortAscending } = { ...DEFAULT_FORM_DATA, ...formData };
return buildQueryContext(formData, baseQueryObject => [
{
...baseQueryObject,
apply_fetch_values_predicate: true,
groupby: baseQueryObject.columns,
orderby: sortAscending
? baseQueryObject.columns.map(column => [column, true])
: [],
},
]);
return buildQueryContext(formData, baseQueryObject => {
const { columns, filters = [] } = baseQueryObject;
return [
{
...baseQueryObject,
apply_fetch_values_predicate: true,
groupby: columns,
filters: filters.concat(
columns.map(column => ({ col: column, op: 'IS NOT NULL' })),
),
orderby: sortAscending ? columns.map(column => [column, true]) : [],
},
];
});
}
17 changes: 12 additions & 5 deletions superset-frontend/src/filters/components/Select/transformProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps } from '@superset-ui/core';
import { DEFAULT_FORM_DATA } from './types';
import { GenericDataType } from '@superset-ui/core';
import { DEFAULT_FORM_DATA, PluginFilterSelectChartProps } from './types';

export default function transformProps(chartProps: ChartProps) {
export default function transformProps(
chartProps: PluginFilterSelectChartProps,
) {
const { formData, height, hooks, queriesData, width, behaviors } = chartProps;
const newFormData = { ...DEFAULT_FORM_DATA, ...formData };
const { setDataMask = () => {} } = hooks;

const { data } = queriesData[0];
const [queryData] = queriesData;
const { colnames = [], coltypes = [], data } = queryData || [];
const coltypeMap: Record<string, GenericDataType> = colnames.reduce(
(accumulator, item, index) => ({ ...accumulator, [item]: coltypes[index] }),
{},
);

return {
coltypeMap,
width,
behaviors,
height,
Expand Down
8 changes: 8 additions & 0 deletions superset-frontend/src/filters/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
* under the License.
*/
import {
ChartProps,
Behavior,
DataRecord,
GenericDataType,
QueryFormData,
SetDataMaskHook,
ChartDataResponseResult,
} from '@superset-ui/core';
import { RefObject } from 'react';
import { PluginFilterStylesProps } from '../types';
Expand All @@ -39,7 +42,12 @@ export type PluginFilterSelectQueryFormData = QueryFormData &
PluginFilterStylesProps &
PluginFilterSelectCustomizeProps;

export interface PluginFilterSelectChartProps extends ChartProps {
queriesData: ChartDataResponseResult[];
}

export type PluginFilterSelectProps = PluginFilterStylesProps & {
coltypeMap: Record<string, GenericDataType>;
data: DataRecord[];
setDataMask: SetDataMaskHook;
behaviors: Behavior[];
Expand Down
53 changes: 52 additions & 1 deletion superset-frontend/src/filters/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryObjectFilterClause } from '@superset-ui/core';
import {
DataRecordValue,
GenericDataType,
NumberFormatter,
QueryObjectFilterClause,
TimeFormatter,
} from '@superset-ui/core';
import { FALSE_STRING, NULL_STRING, TRUE_STRING } from 'src/utils/common';

export const getSelectExtraFormData = (
col: string,
Expand Down Expand Up @@ -67,3 +74,47 @@ export const getRangeExtraFormData = (
},
};
};

export interface DataRecordValueFormatter {
(value: DataRecordValue, dtype: GenericDataType): string;
}

export function getDataRecordFormatter({
timeFormatter,
numberFormatter,
}: {
timeFormatter?: TimeFormatter;
numberFormatter?: NumberFormatter;
} = {}): DataRecordValueFormatter {
return (value, dtype) => {
if (value === null || value === undefined) {
return NULL_STRING;
}
if (typeof value === 'boolean') {
return value ? TRUE_STRING : FALSE_STRING;
}
if (dtype === GenericDataType.BOOLEAN) {
try {
return JSON.parse(String(value).toLowerCase())
? TRUE_STRING
: FALSE_STRING;
} catch {
return FALSE_STRING;
}
}
if (typeof value === 'string') {
return value;
}
if (timeFormatter && dtype === GenericDataType.TEMPORAL) {
return timeFormatter(value);
}
if (
numberFormatter &&
typeof value === 'number' &&
dtype === GenericDataType.NUMERIC
) {
return numberFormatter(value);
}
return String(value);
};
}
2 changes: 2 additions & 0 deletions superset-frontend/src/utils/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
// ATTENTION: If you change any constants, make sure to also change constants.py

export const NULL_STRING = '<NULL>';
export const TRUE_STRING = 'TRUE';
export const FALSE_STRING = 'FALSE';

// moment time format strings
export const SHORT_DATE = 'MMM D, YYYY';
Expand Down
Loading

0 comments on commit bb5a945

Please sign in to comment.