mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
feat(native-filters): add optional time col to time range (#15117)
This commit is contained in:
parent
9ba2983f42
commit
9c3c3fa125
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
|
@ -44,6 +44,7 @@ export interface NativeFiltersFormItem {
|
||||
isInstant: boolean;
|
||||
adhoc_filters?: AdhocFilter[];
|
||||
time_range?: string;
|
||||
granularity_sqla?: string;
|
||||
}
|
||||
|
||||
export interface NativeFiltersForm {
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -55,6 +55,7 @@ export interface Filter {
|
||||
};
|
||||
sortMetric?: string | null;
|
||||
adhoc_filters?: AdhocFilter[];
|
||||
granularity_sqla?: string;
|
||||
time_range?: string;
|
||||
requiredFirst?: boolean;
|
||||
tabsInScope?: string[];
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user