feat(native-filters): add optional time col to time range (#15117)

This commit is contained in:
Ville Brofeldt 2021-06-15 13:25:28 +03:00 committed by GitHub
parent 9ba2983f42
commit 9c3c3fa125
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 152 additions and 64 deletions

View File

@ -17,18 +17,20 @@
* under the License.
*/
import React from 'react';
import { render, waitFor } from 'spec/helpers/testing-library';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import fetchMock from 'fetch-mock';
import * as utils from 'src/utils/getClientErrorObject';
import { Column, JsonObject } from '@superset-ui/core';
import userEvent from '@testing-library/user-event';
import { ColumnSelect } from './ColumnSelect';
fetchMock.get('http://localhost/api/v1/dataset/123', {
body: {
result: {
columns: [
{ column_name: 'column_name_01' },
{ column_name: 'column_name_02' },
{ column_name: 'column_name_03' },
{ column_name: 'column_name_01', is_dttm: true },
{ column_name: 'column_name_02', is_dttm: false },
{ column_name: 'column_name_03', is_dttm: false },
],
},
},
@ -37,9 +39,9 @@ fetchMock.get('http://localhost/api/v1/dataset/456', {
body: {
result: {
columns: [
{ column_name: 'column_name_04' },
{ column_name: 'column_name_05' },
{ column_name: 'column_name_06' },
{ column_name: 'column_name_04', is_dttm: false },
{ column_name: 'column_name_05', is_dttm: false },
{ column_name: 'column_name_06', is_dttm: false },
],
},
},
@ -47,24 +49,35 @@ fetchMock.get('http://localhost/api/v1/dataset/456', {
fetchMock.get('http://localhost/api/v1/dataset/789', { status: 404 });
const createProps = () => ({
const createProps = (extraProps: JsonObject = {}) => ({
filterId: 'filterId',
form: { getFieldValue: jest.fn(), setFields: jest.fn() },
datasetId: 123,
value: 'column_name_01',
onChange: jest.fn(),
...extraProps,
});
afterAll(() => {
fetchMock.restore();
});
test('Should render', () => {
test('Should render', async () => {
const props = createProps();
const { container } = render(<ColumnSelect {...(props as any)} />, {
useRedux: true,
});
expect(container.children).toHaveLength(1);
userEvent.type(screen.getByRole('combobox'), 'column_name');
await waitFor(() => {
expect(screen.getByTitle('column_name_01')).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByTitle('column_name_02')).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByTitle('column_name_03')).toBeInTheDocument();
});
});
test('Should call "setFields" when "datasetId" changes', () => {
@ -94,3 +107,23 @@ test('Should call "getClientErrorObject" when api returns an error', async () =>
expect(spy).toBeCalled();
});
});
test('Should filter results', async () => {
const props = createProps({
filterValues: (column: Column) => column.is_dttm,
});
const { container } = render(<ColumnSelect {...(props as any)} />, {
useRedux: true,
});
expect(container.children).toHaveLength(1);
userEvent.type(screen.getByRole('combobox'), 'column_name');
await waitFor(() => {
expect(screen.getByTitle('column_name_01')).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.queryByTitle('column_name_02')).not.toBeInTheDocument();
});
await waitFor(() => {
expect(screen.queryByTitle('column_name_03')).not.toBeInTheDocument();
});
});

View File

@ -18,7 +18,7 @@
*/
import React, { useCallback, useState } from 'react';
import { FormInstance } from 'antd/lib/form';
import { SupersetClient, t } from '@superset-ui/core';
import { Column, SupersetClient, t } from '@superset-ui/core';
import { useChangeEffect } from 'src/common/hooks/useChangeEffect';
import { Select } from 'src/common/components';
import { useToasts } from 'src/messageToasts/enhancers/withToasts';
@ -27,7 +27,10 @@ import { cacheWrapper } from 'src/utils/cacheWrapper';
import { NativeFiltersForm } from '../types';
interface ColumnSelectProps {
allowClear?: boolean;
filterValues?: (column: Column) => boolean;
form: FormInstance<NativeFiltersForm>;
formField?: string;
filterId: string;
datasetId?: number;
value?: string;
@ -45,7 +48,10 @@ const cachedSupersetGet = cacheWrapper(
/** Special purpose AsyncSelect that selects a column from a dataset */
// eslint-disable-next-line import/prefer-default-export
export function ColumnSelect({
allowClear = false,
filterValues = () => true,
form,
formField = 'column',
filterId,
datasetId,
value,
@ -55,7 +61,7 @@ export function ColumnSelect({
const { addDangerToast } = useToasts();
const resetColumnField = useCallback(() => {
form.setFields([
{ name: ['filters', filterId, 'column'], touched: false, value: null },
{ name: ['filters', filterId, formField], touched: false, value: null },
]);
}, [form, filterId]);
@ -69,6 +75,7 @@ export function ColumnSelect({
}).then(
({ json: { result } }) => {
const columns = result.columns
.filter(filterValues)
.map((col: any) => col.column_name)
.sort((a: string, b: string) => a.localeCompare(b));
if (!columns.includes(value)) {
@ -97,6 +104,7 @@ export function ColumnSelect({
options={options}
placeholder={t('Select a column')}
showSearch
allowClear={allowClear}
/>
);
}

View File

@ -20,6 +20,7 @@ import {
AdhocFilter,
Behavior,
ChartDataResponseResult,
Column,
getChartMetadataRegistry,
JsonResponse,
styled,
@ -29,6 +30,7 @@ import {
import {
ColumnMeta,
DatasourceMeta,
InfoTooltipWithTrigger,
Metric,
} from '@superset-ui/chart-controls';
import { FormInstance } from 'antd/lib/form';
@ -838,6 +840,39 @@ const FiltersConfigForm = (
}}
/>
</StyledRowFormItem>
{hasTimeRange && (
<StyledRowFormItem
name={['filters', filterId, 'granularity_sqla']}
label={
<>
<StyledLabel>{t('Time column')}</StyledLabel>&nbsp;
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
'Optional time column if time range should apply to another column than the default time column',
)}
/>
</>
}
initialValue={filterToEdit?.granularity_sqla}
>
<ColumnSelect
allowClear
form={form}
formField="granularity_sqla"
filterId={filterId}
filterValues={(column: Column) => !!column.is_dttm}
datasetId={datasetId}
onChange={column => {
// We need reset default value when when column changed
setNativeFilterFieldValues(form, filterId, {
granularity_sqla: column,
});
forceUpdate();
}}
/>
</StyledRowFormItem>
)}
</CollapsibleControl>
)}
{formFilter?.filterType !== 'filter_range' && (
@ -846,63 +881,71 @@ const FiltersConfigForm = (
onChange={checked => onSortChanged(checked || undefined)}
checked={hasSorting}
>
<StyledRowContainer>
<StyledFormItem
name={[
'filters',
filterId,
'controlValues',
'sortAscending',
<StyledFormItem
name={[
'filters',
filterId,
'controlValues',
'sortAscending',
]}
initialValue={filterToEdit?.controlValues?.sortAscending}
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
>
<Select
form={form}
filterId={filterId}
name="sortAscending"
options={[
{
value: true,
label: t('Sort ascending'),
},
{
value: false,
label: t('Sort descending'),
},
]}
initialValue={filterToEdit?.controlValues?.sortAscending}
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
onChange={({ value }: { value: boolean }) =>
onSortChanged(value)
}
/>
</StyledFormItem>
{hasMetrics && (
<StyledRowFormItem
name={['filters', filterId, 'sortMetric']}
initialValue={filterToEdit?.sortMetric}
label={
<>
<StyledLabel>{t('Sort Metric')}</StyledLabel>&nbsp;
<InfoTooltipWithTrigger
placement="top"
tooltip={t(
'If a metric is specified, sorting will be done based on the metric value',
)}
/>
</>
}
data-test="field-input"
>
<Select
<SelectControl
form={form}
filterId={filterId}
name="sortAscending"
options={[
{
value: true,
label: t('Sort ascending'),
},
{
value: false,
label: t('Sort descending'),
},
]}
onChange={({ value }: { value: boolean }) =>
onSortChanged(value)
}
name="sortMetric"
options={metrics.map((metric: Metric) => ({
value: metric.metric_name,
label: metric.verbose_name ?? metric.metric_name,
}))}
onChange={(value: string | null): void => {
if (value !== undefined) {
setNativeFilterFieldValues(form, filterId, {
sortMetric: value,
});
forceUpdate();
}
}}
/>
</StyledFormItem>
{hasMetrics && (
<StyledFormItem
name={['filters', filterId, 'sortMetric']}
initialValue={filterToEdit?.sortMetric}
label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
data-test="field-input"
>
<SelectControl
form={form}
filterId={filterId}
name="sortMetric"
options={metrics.map((metric: Metric) => ({
value: metric.metric_name,
label: metric.verbose_name ?? metric.metric_name,
}))}
onChange={(value: string | null): void => {
if (value !== undefined) {
setNativeFilterFieldValues(form, filterId, {
sortMetric: value,
});
forceUpdate();
}
}}
/>
</StyledFormItem>
)}
</StyledRowContainer>
</StyledRowFormItem>
)}
</CollapsibleControl>
)}
</Collapse.Panel>

View File

@ -44,6 +44,7 @@ export interface NativeFiltersFormItem {
isInstant: boolean;
adhoc_filters?: AdhocFilter[];
time_range?: string;
granularity_sqla?: string;
}
export interface NativeFiltersForm {

View File

@ -140,6 +140,7 @@ export const createHandleSave = (
adhoc_filters: formInputs.adhoc_filters,
time_range: formInputs.time_range,
controlValues: formInputs.controlValues ?? {},
granularity_sqla: formInputs.granularity_sqla,
requiredFirst: Object.values(formInputs.requiredFirst ?? {}).find(
rf => rf,
),

View File

@ -55,6 +55,7 @@ export interface Filter {
};
sortMetric?: string | null;
adhoc_filters?: AdhocFilter[];
granularity_sqla?: string;
time_range?: string;
requiredFirst?: boolean;
tabsInScope?: string[];

View File

@ -46,6 +46,7 @@ export const getFormData = ({
sortMetric,
adhoc_filters,
time_range,
granularity_sqla,
}: Partial<Filter> & {
datasetId?: number;
inputRef?: RefObject<HTMLInputElement>;
@ -74,7 +75,7 @@ export const getFormData = ({
adhoc_filters: adhoc_filters ?? [],
extra_filters: [],
extra_form_data: cascadingFilters,
granularity_sqla: 'ds',
granularity_sqla,
metrics: ['count'],
row_limit: 1000,
showSearch: true,