mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
feat(native-filters): Hide time filters if loaded datasets don't have temporal columns (#15225)
* feat(native-filters): Hide time filters if loaded datasets don't have temporal columns * Remove "px" suffixes to fix warnings * Disable an option instead of hiding filter types * Fix tests * Add 2 more tests
This commit is contained in:
parent
ea8507b4e2
commit
c7c63751e9
@ -164,6 +164,7 @@ export default {
|
|||||||
column_name: 'num_girls',
|
column_name: 'num_girls',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
column_types: [0, 1, 2],
|
||||||
id,
|
id,
|
||||||
granularity_sqla: [['ds', 'ds']],
|
granularity_sqla: [['ds', 'ds']],
|
||||||
name: 'birth_names',
|
name: 'birth_names',
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
SupersetApiError,
|
SupersetApiError,
|
||||||
t,
|
t,
|
||||||
|
GenericDataType,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ColumnMeta,
|
ColumnMeta,
|
||||||
@ -102,7 +103,7 @@ export const StyledFormItem = styled(FormItem)`
|
|||||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
|
||||||
& .ant-form-item-label {
|
& .ant-form-item-label {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ant-form-item-control-input {
|
& .ant-form-item-control-input {
|
||||||
@ -111,12 +112,12 @@ export const StyledFormItem = styled(FormItem)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledRowFormItem = styled(FormItem)`
|
export const StyledRowFormItem = styled(FormItem)`
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0;
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0;
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
|
|
||||||
& .ant-form-item-label {
|
& .ant-form-item-label {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-form-item-control-input-content > div > div {
|
.ant-form-item-control-input-content > div > div {
|
||||||
@ -154,17 +155,17 @@ const StyledCollapse = styled(Collapse)`
|
|||||||
margin-right: ${({ theme }) => theme.gridUnit * -4}px;
|
margin-right: ${({ theme }) => theme.gridUnit * -4}px;
|
||||||
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
border-radius: 0px;
|
border-radius: 0;
|
||||||
|
|
||||||
.ant-collapse-header {
|
.ant-collapse-header {
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
margin-top: -1px;
|
margin-top: -1px;
|
||||||
border-radius: 0px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse-content {
|
.ant-collapse-content {
|
||||||
border: 0px;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-collapse-content-box {
|
.ant-collapse-content-box {
|
||||||
@ -172,8 +173,8 @@ const StyledCollapse = styled(Collapse)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.ant-collapse > .ant-collapse-item {
|
&.ant-collapse > .ant-collapse-item {
|
||||||
border: 0px;
|
border: 0;
|
||||||
border-radius: 0px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -182,17 +183,17 @@ const StyledTabs = styled(Tabs)`
|
|||||||
position: sticky;
|
position: sticky;
|
||||||
margin-left: ${({ theme }) => theme.gridUnit * -4}px;
|
margin-left: ${({ theme }) => theme.gridUnit * -4}px;
|
||||||
margin-right: ${({ theme }) => theme.gridUnit * -4}px;
|
margin-right: ${({ theme }) => theme.gridUnit * -4}px;
|
||||||
top: 0px;
|
top: 0;
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs-nav-list {
|
.ant-tabs-nav-list {
|
||||||
padding: 0px;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-form-item-label {
|
.ant-form-item-label {
|
||||||
padding-bottom: 0px;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -245,6 +246,8 @@ const FILTERS_WITHOUT_COLUMN = [
|
|||||||
|
|
||||||
const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
|
const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
|
||||||
|
|
||||||
|
const TIME_FILTERS = ['filter_time', 'filter_timegrain', 'filter_timecolumn'];
|
||||||
|
|
||||||
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
|
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
|
||||||
|
|
||||||
// TODO: Rename the filter plugins and remove this mapping
|
// TODO: Rename the filter plugins and remove this mapping
|
||||||
@ -257,6 +260,11 @@ const FILTER_TYPE_NAME_MAPPING = {
|
|||||||
[t('Group By')]: t('Group by'),
|
[t('Group By')]: t('Group by'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: add column_types field to DatasourceMeta
|
||||||
|
const hasTemporalColumns = (
|
||||||
|
dataset: DatasourceMeta & { column_types: GenericDataType[] },
|
||||||
|
) => dataset?.column_types?.includes(GenericDataType.TEMPORAL);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The configuration form for a specific filter.
|
* The configuration form for a specific filter.
|
||||||
* Assigns field values to `filters[filterId]` in the form.
|
* Assigns field values to `filters[filterId]` in the form.
|
||||||
@ -283,7 +291,7 @@ const FiltersConfigForm = (
|
|||||||
|
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>();
|
const [datasetDetails, setDatasetDetails] = useState<Record<string, any>>();
|
||||||
const defaultFormFilter = useMemo(() => {}, []);
|
const defaultFormFilter = useMemo(() => ({}), []);
|
||||||
const formFilter =
|
const formFilter =
|
||||||
form.getFieldValue('filters')?.[filterId] || defaultFormFilter;
|
form.getFieldValue('filters')?.[filterId] || defaultFormFilter;
|
||||||
|
|
||||||
@ -299,6 +307,22 @@ const FiltersConfigForm = (
|
|||||||
({ datasources }) => datasources,
|
({ datasources }) => datasources,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const doLoadedDatasetsHaveTemporalColumns = useMemo(
|
||||||
|
() =>
|
||||||
|
Object.values(loadedDatasets).some(dataset =>
|
||||||
|
hasTemporalColumns(dataset),
|
||||||
|
),
|
||||||
|
[loadedDatasets],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showTimeRangePicker = useMemo(() => {
|
||||||
|
const currentDataset = Object.values(loadedDatasets).find(
|
||||||
|
dataset => dataset.id === formFilter.dataset?.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentDataset ? hasTemporalColumns(currentDataset) : true;
|
||||||
|
}, [formFilter.dataset?.value, loadedDatasets]);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value
|
const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value
|
||||||
?.datasourceCount;
|
?.datasourceCount;
|
||||||
@ -576,9 +600,21 @@ const FiltersConfigForm = (
|
|||||||
const mappedName = name
|
const mappedName = name
|
||||||
? FILTER_TYPE_NAME_MAPPING[name]
|
? FILTER_TYPE_NAME_MAPPING[name]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const isDisabled =
|
||||||
|
TIME_FILTERS.includes(filterType) &&
|
||||||
|
!doLoadedDatasetsHaveTemporalColumns;
|
||||||
return {
|
return {
|
||||||
value: filterType,
|
value: filterType,
|
||||||
label: mappedName || name,
|
label: isDisabled ? (
|
||||||
|
<Tooltip
|
||||||
|
title={t('Datasets do not contain a temporal column')}
|
||||||
|
>
|
||||||
|
{mappedName || name}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
mappedName || name
|
||||||
|
),
|
||||||
|
isDisabled,
|
||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
onChange={({ value }: { value: string }) => {
|
onChange={({ value }: { value: string }) => {
|
||||||
@ -845,28 +881,30 @@ const FiltersConfigForm = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledRowFormItem>
|
</StyledRowFormItem>
|
||||||
<StyledRowFormItem
|
{showTimeRangePicker && (
|
||||||
name={['filters', filterId, 'time_range']}
|
<StyledRowFormItem
|
||||||
label={<StyledLabel>{t('Time range')}</StyledLabel>}
|
name={['filters', filterId, 'time_range']}
|
||||||
initialValue={filterToEdit?.time_range || 'No filter'}
|
label={<StyledLabel>{t('Time range')}</StyledLabel>}
|
||||||
required={!hasAdhoc}
|
initialValue={filterToEdit?.time_range || 'No filter'}
|
||||||
rules={[
|
required={!hasAdhoc}
|
||||||
{
|
rules={[
|
||||||
validator: preFilterValidator,
|
{
|
||||||
},
|
validator: preFilterValidator,
|
||||||
]}
|
},
|
||||||
>
|
]}
|
||||||
<DateFilterControl
|
>
|
||||||
name="time_range"
|
<DateFilterControl
|
||||||
onChange={timeRange => {
|
name="time_range"
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
onChange={timeRange => {
|
||||||
time_range: timeRange,
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
});
|
time_range: timeRange,
|
||||||
forceUpdate();
|
});
|
||||||
validatePreFilter();
|
forceUpdate();
|
||||||
}}
|
validatePreFilter();
|
||||||
/>
|
}}
|
||||||
</StyledRowFormItem>
|
/>
|
||||||
|
</StyledRowFormItem>
|
||||||
|
)}
|
||||||
{hasTimeRange && (
|
{hasTimeRange && (
|
||||||
<StyledRowFormItem
|
<StyledRowFormItem
|
||||||
name={['filters', filterId, 'granularity_sqla']}
|
name={['filters', filterId, 'granularity_sqla']}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
import { flatMapDeep } from 'lodash';
|
import { flatMapDeep } from 'lodash';
|
||||||
import { FormInstance } from 'antd/lib/form';
|
import { FormInstance } from 'antd/lib/form';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CustomControlItem } from '@superset-ui/chart-controls';
|
import { CustomControlItem, DatasourceMeta } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
const FILTERS_FIELD_NAME = 'filters';
|
const FILTERS_FIELD_NAME = 'filters';
|
||||||
|
|
||||||
@ -64,7 +64,9 @@ type DatasetSelectValue = {
|
|||||||
label: string;
|
label: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const datasetToSelectOption = (item: any): DatasetSelectValue => ({
|
export const datasetToSelectOption = (
|
||||||
|
item: DatasourceMeta & { table_name: string },
|
||||||
|
): DatasetSelectValue => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
label: item.table_name,
|
label: item.table_name,
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import React from 'react';
|
||||||
import { Preset } from '@superset-ui/core';
|
import { Preset } from '@superset-ui/core';
|
||||||
|
import fetchMock from 'fetch-mock';
|
||||||
|
import userEvent, { specialChars } from '@testing-library/user-event';
|
||||||
import {
|
import {
|
||||||
SelectFilterPlugin,
|
SelectFilterPlugin,
|
||||||
RangeFilterPlugin,
|
RangeFilterPlugin,
|
||||||
@ -25,9 +28,7 @@ import {
|
|||||||
TimeGrainFilterPlugin,
|
TimeGrainFilterPlugin,
|
||||||
} from 'src/filters/components';
|
} from 'src/filters/components';
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import fetchMock from 'fetch-mock';
|
import mockDatasource, { datasourceId } from 'spec/fixtures/mockDatasource';
|
||||||
import React from 'react';
|
|
||||||
import userEvent, { specialChars } from '@testing-library/user-event';
|
|
||||||
import {
|
import {
|
||||||
FiltersConfigModal,
|
FiltersConfigModal,
|
||||||
FiltersConfigModalProps,
|
FiltersConfigModalProps,
|
||||||
@ -49,7 +50,18 @@ class MainPreset extends Preset {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialStoreState = {
|
const initialStoreState = {
|
||||||
datasources: [{ id: 1, table_name: 'Datasource 1' }],
|
datasources: mockDatasource,
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeWithDatasourceWithoutTemporalColumns = {
|
||||||
|
...initialStoreState,
|
||||||
|
datasources: {
|
||||||
|
...initialStoreState.datasources,
|
||||||
|
[datasourceId]: {
|
||||||
|
...initialStoreState.datasources[datasourceId],
|
||||||
|
column_types: [0, 1],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.get('glob:*/api/v1/dataset/1', {
|
fetchMock.get('glob:*/api/v1/dataset/1', {
|
||||||
@ -114,6 +126,7 @@ const DEFAULT_VALUE_REQUIRED_REGEX = /^default value is required$/i;
|
|||||||
const PARENT_REQUIRED_REGEX = /^parent filter is required$/i;
|
const PARENT_REQUIRED_REGEX = /^parent filter is required$/i;
|
||||||
const PRE_FILTER_REQUIRED_REGEX = /^pre-filter is required$/i;
|
const PRE_FILTER_REQUIRED_REGEX = /^pre-filter is required$/i;
|
||||||
const FILL_REQUIRED_FIELDS_REGEX = /fill all required fields to enable/;
|
const FILL_REQUIRED_FIELDS_REGEX = /fill all required fields to enable/;
|
||||||
|
const TIME_RANGE_PREFILTER_REGEX = /^time range$/i;
|
||||||
|
|
||||||
const props: FiltersConfigModalProps = {
|
const props: FiltersConfigModalProps = {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -128,7 +141,7 @@ beforeAll(() => {
|
|||||||
|
|
||||||
function defaultRender(
|
function defaultRender(
|
||||||
overrides?: Partial<FiltersConfigModalProps>,
|
overrides?: Partial<FiltersConfigModalProps>,
|
||||||
initialState?: {},
|
initialState = initialStoreState,
|
||||||
) {
|
) {
|
||||||
return render(<FiltersConfigModal {...props} {...overrides} />, {
|
return render(<FiltersConfigModal {...props} {...overrides} />, {
|
||||||
useRedux: true,
|
useRedux: true,
|
||||||
@ -243,6 +256,20 @@ test('renders a time grain filter type', () => {
|
|||||||
expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument();
|
expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('render time filter types as disabled if there are no temporal columns in the dataset', () => {
|
||||||
|
defaultRender(undefined, storeWithDatasourceWithoutTemporalColumns);
|
||||||
|
userEvent.click(screen.getByText(VALUE_REGEX));
|
||||||
|
expect(screen.getByText(TIME_RANGE_REGEX).closest('div')).toHaveClass(
|
||||||
|
'Select__option--is-disabled',
|
||||||
|
);
|
||||||
|
expect(screen.getByText(TIME_GRAIN_REGEX).closest('div')).toHaveClass(
|
||||||
|
'Select__option--is-disabled',
|
||||||
|
);
|
||||||
|
expect(screen.getByText(TIME_COLUMN_REGEX).closest('div')).toHaveClass(
|
||||||
|
'Select__option--is-disabled',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('validates the name', async () => {
|
test('validates the name', async () => {
|
||||||
defaultRender();
|
defaultRender();
|
||||||
userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
userEvent.click(screen.getByRole('button', { name: SAVE_REGEX }));
|
||||||
@ -287,6 +314,14 @@ test('validates the pre-filter value', async () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("doesn't render time range pre-filter if there are no temporal columns in datasource", async () => {
|
||||||
|
defaultRender(undefined, storeWithDatasourceWithoutTemporalColumns);
|
||||||
|
userEvent.click(screen.getByText(ADVANCED_REGEX));
|
||||||
|
userEvent.click(getCheckbox(PRE_FILTER_REGEX));
|
||||||
|
expect(
|
||||||
|
screen.queryByText(TIME_RANGE_PREFILTER_REGEX),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
/*
|
/*
|
||||||
TODO
|
TODO
|
||||||
adds a new value filter type with all fields filled
|
adds a new value filter type with all fields filled
|
||||||
|
Loading…
Reference in New Issue
Block a user