mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
chore: Improves the native filters UI/UX - iteration 5 (#14882)
This commit is contained in:
parent
fce8ac27f0
commit
8519a09086
@ -35,7 +35,7 @@ const StyledItem = styled(Form.Item)`
|
|||||||
&::after {
|
&::after {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: ${theme.colors.error.base};
|
color: ${theme.colors.error.base};
|
||||||
font-size: ${theme.typography.sizes.m}px;
|
font-size: ${theme.typography.sizes.s}px;
|
||||||
content: '*';
|
content: '*';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,8 @@ const addFilterFlow = async () => {
|
|||||||
userEvent.click(screen.getByTestId(getTestId('collapsable')));
|
userEvent.click(screen.getByTestId(getTestId('collapsable')));
|
||||||
userEvent.click(screen.getByTestId(getTestId('create-filter')));
|
userEvent.click(screen.getByTestId(getTestId('create-filter')));
|
||||||
// select filter
|
// select filter
|
||||||
userEvent.click(screen.getByText('Select filter'));
|
userEvent.click(screen.getByText('Value'));
|
||||||
userEvent.click(screen.getByText('Time filter'));
|
userEvent.click(screen.getByText('Time range'));
|
||||||
userEvent.type(screen.getByTestId(getModalTestId('name-input')), FILTER_NAME);
|
userEvent.type(screen.getByTestId(getModalTestId('name-input')), FILTER_NAME);
|
||||||
userEvent.click(screen.getByText('Save'));
|
userEvent.click(screen.getByText('Save'));
|
||||||
await screen.findByText('All Filters (1)');
|
await screen.findByText('All Filters (1)');
|
||||||
|
@ -42,11 +42,9 @@ export const StyledFilterTitle = styled.span`
|
|||||||
|
|
||||||
export const StyledAddFilterBox = styled.div`
|
export const StyledAddFilterBox = styled.div`
|
||||||
color: ${({ theme }) => theme.colors.primary.dark1};
|
color: ${({ theme }) => theme.colors.primary.dark1};
|
||||||
text-align: left;
|
padding: ${({ theme }) => theme.gridUnit * 2}px;
|
||||||
padding: ${({ theme }) => theme.gridUnit * 2}px 0;
|
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
margin: ${({ theme }) => theme.gridUnit * 3}px 0 0
|
cursor: pointer;
|
||||||
${({ theme }) => -theme.gridUnit * 2}px;
|
|
||||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light1};
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${({ theme }) => theme.colors.primary.base};
|
color: ${({ theme }) => theme.colors.primary.base};
|
||||||
@ -89,12 +87,19 @@ const FilterTabsContainer = styled(LineEditableTabs)`
|
|||||||
|
|
||||||
& > .ant-tabs-content-holder {
|
& > .ant-tabs-content-holder {
|
||||||
border-left: 1px solid ${theme.colors.grayscale.light2};
|
border-left: 1px solid ${theme.colors.grayscale.light2};
|
||||||
margin-right: ${theme.gridUnit * 4}px;
|
padding-right: ${theme.gridUnit * 4}px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .ant-tabs-content-holder ~ .ant-tabs-content-holder {
|
& > .ant-tabs-content-holder ~ .ant-tabs-content-holder {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.ant-tabs-card > .ant-tabs-nav .ant-tabs-ink-bar {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
&.ant-tabs-left
|
&.ant-tabs-left
|
||||||
> .ant-tabs-content-holder
|
> .ant-tabs-content-holder
|
||||||
> .ant-tabs-content
|
> .ant-tabs-content
|
||||||
@ -104,9 +109,11 @@ const FilterTabsContainer = styled(LineEditableTabs)`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-tabs-nav-list {
|
.ant-tabs-nav-list {
|
||||||
padding-top: ${theme.gridUnit * 4}px;
|
overflow-x: hidden;
|
||||||
padding-right: ${theme.gridUnit * 2}px;
|
overflow-y: auto;
|
||||||
padding-bottom: ${theme.gridUnit * 4}px;
|
padding-top: ${theme.gridUnit * 2}px;
|
||||||
|
padding-right: ${theme.gridUnit}px;
|
||||||
|
padding-bottom: ${theme.gridUnit * 3}px;
|
||||||
padding-left: ${theme.gridUnit * 3}px;
|
padding-left: ${theme.gridUnit * 3}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,6 +142,24 @@ const FilterTabsContainer = styled(LineEditableTabs)`
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
text-transform: unset;
|
text-transform: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav-more {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-extra-content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledHeader = styled.div`
|
||||||
|
${({ theme }) => `
|
||||||
|
color: ${theme.colors.grayscale.dark1};
|
||||||
|
font-size: ${theme.typography.sizes.l}px;
|
||||||
|
padding-top: ${theme.gridUnit * 4}px;
|
||||||
|
padding-right: ${theme.gridUnit * 4}px;
|
||||||
|
padding-left: ${theme.gridUnit * 4}px;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -164,14 +189,18 @@ const FilterTabs: FC<FilterTabsProps> = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
activeKey={currentFilterId}
|
activeKey={currentFilterId}
|
||||||
onEdit={onEdit}
|
onEdit={onEdit}
|
||||||
addIcon={
|
hideAdd
|
||||||
<StyledAddFilterBox>
|
tabBarExtraContent={{
|
||||||
<PlusOutlined />{' '}
|
left: <StyledHeader>{t('Filters')}</StyledHeader>,
|
||||||
<span data-test="add-filter-button" aria-label="Add filter">
|
right: (
|
||||||
{t('Add filter')}
|
<StyledAddFilterBox onClick={() => onEdit('', 'add')}>
|
||||||
</span>
|
<PlusOutlined />{' '}
|
||||||
</StyledAddFilterBox>
|
<span data-test="add-filter-button" aria-label="Add filter">
|
||||||
}
|
{t('Add filter')}
|
||||||
|
</span>
|
||||||
|
</StyledAddFilterBox>
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{filterIds.map(id => (
|
{filterIds.map(id => (
|
||||||
<LineEditableTabs.TabPane
|
<LineEditableTabs.TabPane
|
||||||
|
@ -41,7 +41,8 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Checkbox, Form, Input } from 'src/common/components';
|
import { FormItem } from 'src/components/Form';
|
||||||
|
import { Checkbox, Input } from 'src/common/components';
|
||||||
import { Select } from 'src/components/Select';
|
import { Select } from 'src/components/Select';
|
||||||
import SupersetResourceSelect, {
|
import SupersetResourceSelect, {
|
||||||
cachedSupersetGet,
|
cachedSupersetGet,
|
||||||
@ -91,22 +92,32 @@ const StyledRowContainer = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledFormItem = styled(Form.Item)`
|
export const StyledFormItem = styled(FormItem)`
|
||||||
width: 49%;
|
width: 49%;
|
||||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||||
|
|
||||||
|
& .ant-form-item-label {
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
& .ant-form-item-control-input {
|
& .ant-form-item-control-input {
|
||||||
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledRowFormItem = styled(Form.Item)`
|
export const StyledRowFormItem = styled(FormItem)`
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
|
padding-bottom: 0px;
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
|
|
||||||
|
& .ant-form-item-label {
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-form-item-control-input-content > div > div {
|
.ant-form-item-control-input-content > div > div {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .ant-form-item-control-input {
|
& .ant-form-item-control-input {
|
||||||
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
min-height: ${({ theme }) => theme.gridUnit * 10}px;
|
||||||
}
|
}
|
||||||
@ -118,7 +129,7 @@ export const StyledLabel = styled.span`
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CleanFormItem = styled(Form.Item)`
|
const CleanFormItem = styled(FormItem)`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -151,6 +162,13 @@ const StyledCollapse = styled(Collapse)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledTabs = styled(Tabs)`
|
const StyledTabs = styled(Tabs)`
|
||||||
|
.ant-tabs-nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0px;
|
||||||
|
background: white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-tabs-nav-list {
|
.ant-tabs-nav-list {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@ -164,6 +182,9 @@ const StyledAsterisk = styled.span`
|
|||||||
color: ${({ theme }) => theme.colors.error.base};
|
color: ${({ theme }) => theme.colors.error.base};
|
||||||
font-family: SimSun, sans-serif;
|
font-family: SimSun, sans-serif;
|
||||||
margin-right: ${({ theme }) => theme.gridUnit - 1}px;
|
margin-right: ${({ theme }) => theme.gridUnit - 1}px;
|
||||||
|
&:before {
|
||||||
|
content: '*';
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FilterTabs = {
|
const FilterTabs = {
|
||||||
@ -208,6 +229,15 @@ const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
|
|||||||
|
|
||||||
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
|
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
|
||||||
|
|
||||||
|
const FILTER_TYPE_NAME_MAPPING = {
|
||||||
|
[t('Select filter')]: t('Value'),
|
||||||
|
[t('Range filter')]: t('Numerical range'),
|
||||||
|
[t('Time filter')]: t('Time range'),
|
||||||
|
[t('Time column')]: t('Time column'),
|
||||||
|
[t('Time grain')]: t('Time grain'),
|
||||||
|
[t('Group By')]: t('Group by'),
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@ -418,396 +448,402 @@ const FiltersConfigForm = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<StyledTabs
|
||||||
<StyledTabs
|
defaultActiveKey={FilterTabs.configuration.key}
|
||||||
defaultActiveKey={FilterTabs.configuration.key}
|
activeKey={activeTabKey}
|
||||||
activeKey={activeTabKey}
|
onChange={activeKey => setActiveTabKey(activeKey)}
|
||||||
onChange={activeKey => setActiveTabKey(activeKey)}
|
centered
|
||||||
centered
|
>
|
||||||
|
<TabPane
|
||||||
|
tab={FilterTabs.configuration.name}
|
||||||
|
key={FilterTabs.configuration.key}
|
||||||
|
forceRender
|
||||||
>
|
>
|
||||||
<TabPane
|
<StyledContainer>
|
||||||
tab={FilterTabs.configuration.name}
|
<StyledFormItem
|
||||||
key={FilterTabs.configuration.key}
|
name={['filters', filterId, 'name']}
|
||||||
forceRender
|
label={<StyledLabel>{t('Filter name')}</StyledLabel>}
|
||||||
>
|
initialValue={filterToEdit?.name}
|
||||||
<StyledContainer>
|
rules={[{ required: !removed, message: t('Name is required') }]}
|
||||||
<StyledFormItem
|
>
|
||||||
name={['filters', filterId, 'name']}
|
<Input {...getFiltersConfigModalTestId('name-input')} />
|
||||||
label={<StyledLabel>{t('Filter name')}</StyledLabel>}
|
</StyledFormItem>
|
||||||
initialValue={filterToEdit?.name}
|
<StyledFormItem
|
||||||
rules={[{ required: !removed, message: t('Name is required') }]}
|
name={['filters', filterId, 'filterType']}
|
||||||
>
|
rules={[{ required: !removed, message: t('Name is required') }]}
|
||||||
<Input {...getFiltersConfigModalTestId('name-input')} />
|
initialValue={filterToEdit?.filterType || 'filter_select'}
|
||||||
</StyledFormItem>
|
label={<StyledLabel>{t('Filter Type')}</StyledLabel>}
|
||||||
<StyledFormItem
|
{...getFiltersConfigModalTestId('filter-type')}
|
||||||
name={['filters', filterId, 'filterType']}
|
>
|
||||||
rules={[{ required: !removed, message: t('Name is required') }]}
|
<Select
|
||||||
initialValue={filterToEdit?.filterType || 'filter_select'}
|
options={nativeFilterVizTypes.map(filterType => {
|
||||||
label={<StyledLabel>{t('Filter Type')}</StyledLabel>}
|
// @ts-ignore
|
||||||
{...getFiltersConfigModalTestId('filter-type')}
|
const name = nativeFilterItems[filterType]?.value.name;
|
||||||
>
|
const mappedName = name
|
||||||
<Select
|
? FILTER_TYPE_NAME_MAPPING[name]
|
||||||
options={nativeFilterVizTypes.map(filterType => ({
|
: undefined;
|
||||||
|
return {
|
||||||
value: filterType,
|
value: filterType,
|
||||||
// @ts-ignore
|
label: mappedName || name,
|
||||||
label: nativeFilterItems[filterType]?.value.name,
|
};
|
||||||
}))}
|
})}
|
||||||
onChange={({ value }: { value: string }) => {
|
onChange={({ value }: { value: string }) => {
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
filterType: value,
|
filterType: value,
|
||||||
defaultDataMask: null,
|
defaultDataMask: null,
|
||||||
});
|
});
|
||||||
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledFormItem>
|
||||||
|
</StyledContainer>
|
||||||
|
{hasDataset && (
|
||||||
|
<StyledRowContainer>
|
||||||
|
<StyledFormItem
|
||||||
|
name={['filters', filterId, 'dataset']}
|
||||||
|
initialValue={{ value: initialDatasetId }}
|
||||||
|
label={<StyledLabel>{t('Dataset')}</StyledLabel>}
|
||||||
|
rules={[
|
||||||
|
{ required: !removed, message: t('Dataset is required') },
|
||||||
|
]}
|
||||||
|
{...getFiltersConfigModalTestId('datasource-input')}
|
||||||
|
>
|
||||||
|
<SupersetResourceSelect
|
||||||
|
initialId={initialDatasetId}
|
||||||
|
resource="dataset"
|
||||||
|
searchColumn="table_name"
|
||||||
|
transformItem={datasetToSelectOption}
|
||||||
|
isMulti={false}
|
||||||
|
onError={onDatasetSelectError}
|
||||||
|
defaultOptions={Object.values(loadedDatasets).map(
|
||||||
|
datasetToSelectOption,
|
||||||
|
)}
|
||||||
|
onChange={e => {
|
||||||
|
// We need reset column when dataset changed
|
||||||
|
if (datasetId && e?.value !== datasetId) {
|
||||||
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
|
defaultDataMask: null,
|
||||||
|
column: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
</StyledFormItem>
|
||||||
</StyledContainer>
|
{hasColumn && (
|
||||||
{hasDataset && (
|
|
||||||
<StyledRowContainer>
|
|
||||||
<StyledFormItem
|
<StyledFormItem
|
||||||
name={['filters', filterId, 'dataset']}
|
// don't show the column select unless we have a dataset
|
||||||
initialValue={{ value: initialDatasetId }}
|
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||||
label={<StyledLabel>{t('Dataset')}</StyledLabel>}
|
name={['filters', filterId, 'column']}
|
||||||
|
initialValue={initColumn}
|
||||||
|
label={<StyledLabel>{t('Column')}</StyledLabel>}
|
||||||
rules={[
|
rules={[
|
||||||
{ required: !removed, message: t('Dataset is required') },
|
{ required: !removed, message: t('Field is required') },
|
||||||
]}
|
]}
|
||||||
{...getFiltersConfigModalTestId('datasource-input')}
|
data-test="field-input"
|
||||||
>
|
>
|
||||||
<SupersetResourceSelect
|
<ColumnSelect
|
||||||
initialId={initialDatasetId}
|
form={form}
|
||||||
resource="dataset"
|
filterId={filterId}
|
||||||
searchColumn="table_name"
|
datasetId={datasetId}
|
||||||
transformItem={datasetToSelectOption}
|
onChange={() => {
|
||||||
isMulti={false}
|
// We need reset default value when when column changed
|
||||||
onError={onDatasetSelectError}
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
defaultOptions={Object.values(loadedDatasets).map(
|
defaultDataMask: null,
|
||||||
datasetToSelectOption,
|
});
|
||||||
)}
|
|
||||||
onChange={e => {
|
|
||||||
// We need reset column when dataset changed
|
|
||||||
if (datasetId && e?.value !== datasetId) {
|
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
|
||||||
defaultDataMask: null,
|
|
||||||
column: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
</StyledFormItem>
|
||||||
{hasColumn && (
|
)}
|
||||||
<StyledFormItem
|
</StyledRowContainer>
|
||||||
// don't show the column select unless we have a dataset
|
)}
|
||||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
<StyledCollapse
|
||||||
name={['filters', filterId, 'column']}
|
defaultActiveKey={FilterPanels.basic.key}
|
||||||
initialValue={initColumn}
|
expandIconPosition="right"
|
||||||
label={<StyledLabel>{t('Column')}</StyledLabel>}
|
>
|
||||||
rules={[
|
<Collapse.Panel
|
||||||
{ required: !removed, message: t('Field is required') },
|
header={FilterPanels.basic.name}
|
||||||
]}
|
key={FilterPanels.basic.key}
|
||||||
data-test="field-input"
|
>
|
||||||
>
|
{hasFilledDataset && (
|
||||||
<ColumnSelect
|
<CleanFormItem
|
||||||
form={form}
|
name={['filters', filterId, 'defaultValueFormData']}
|
||||||
filterId={filterId}
|
hidden
|
||||||
datasetId={datasetId}
|
initialValue={newFormData}
|
||||||
onChange={() => {
|
/>
|
||||||
// We need reset default value when when column changed
|
)}
|
||||||
|
<CleanFormItem
|
||||||
|
name={['filters', filterId, 'defaultValueQueriesData']}
|
||||||
|
hidden
|
||||||
|
initialValue={null}
|
||||||
|
/>
|
||||||
|
<CollapsibleControl
|
||||||
|
title={t('Filter has default value')}
|
||||||
|
checked={hasDefaultValue}
|
||||||
|
onChange={value => setHasDefaultValue(value)}
|
||||||
|
>
|
||||||
|
<StyledRowFormItem
|
||||||
|
name={['filters', filterId, 'defaultDataMask']}
|
||||||
|
initialValue={filterToEdit?.defaultDataMask}
|
||||||
|
data-test="default-input"
|
||||||
|
label={<StyledLabel>{t('Default Value')}</StyledLabel>}
|
||||||
|
required
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (rule, value) => {
|
||||||
|
const hasValue = !!value.filterState?.value;
|
||||||
|
if (hasValue) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(t('Default value is required')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{showDefaultValue ? (
|
||||||
|
<DefaultValue
|
||||||
|
setDataMask={dataMask => {
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
defaultDataMask: null,
|
defaultDataMask: dataMask,
|
||||||
});
|
});
|
||||||
|
form.validateFields([
|
||||||
|
['filters', filterId, 'defaultDataMask'],
|
||||||
|
]);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}}
|
}}
|
||||||
|
filterId={filterId}
|
||||||
|
hasDataset={hasDataset}
|
||||||
|
form={form}
|
||||||
|
formData={newFormData}
|
||||||
|
enableNoResults={enableNoResults}
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
) : (
|
||||||
)}
|
t('Fill all required fields to enable "Default Value"')
|
||||||
</StyledRowContainer>
|
)}
|
||||||
)}
|
</StyledRowFormItem>
|
||||||
<StyledCollapse defaultActiveKey={FilterPanels.basic.key}>
|
</CollapsibleControl>
|
||||||
<Collapse.Panel
|
{Object.keys(controlItems)
|
||||||
header={FilterPanels.basic.name}
|
.filter(key => BASIC_CONTROL_ITEMS.includes(key))
|
||||||
key={FilterPanels.basic.key}
|
.map(key => controlItems[key])}
|
||||||
|
<StyledRowFormItem
|
||||||
|
name={['filters', filterId, 'isInstant']}
|
||||||
|
initialValue={filterToEdit?.isInstant || false}
|
||||||
|
valuePropName="checked"
|
||||||
|
colon={false}
|
||||||
>
|
>
|
||||||
{hasFilledDataset && (
|
<Checkbox data-test="apply-changes-instantly-checkbox">
|
||||||
<CleanFormItem
|
{t('Apply changes instantly')}
|
||||||
name={['filters', filterId, 'defaultValueFormData']}
|
</Checkbox>
|
||||||
hidden
|
</StyledRowFormItem>
|
||||||
initialValue={newFormData}
|
</Collapse.Panel>
|
||||||
/>
|
{((hasDataset && hasAdditionalFilters) || hasMetrics) && (
|
||||||
)}
|
<Collapse.Panel
|
||||||
<CleanFormItem
|
header={FilterPanels.advanced.name}
|
||||||
name={['filters', filterId, 'defaultValueQueriesData']}
|
key={FilterPanels.advanced.key}
|
||||||
hidden
|
>
|
||||||
initialValue={null}
|
{isCascadingFilter && (
|
||||||
/>
|
<CollapsibleControl
|
||||||
<CollapsibleControl
|
title={t('Filter is hierarchical')}
|
||||||
title={t('Filter has default value')}
|
checked={!!parentFilter}
|
||||||
checked={hasDefaultValue}
|
onChange={checked => {
|
||||||
onChange={value => setHasDefaultValue(value)}
|
if (checked) {
|
||||||
>
|
// execute after render
|
||||||
<StyledRowFormItem
|
setTimeout(
|
||||||
name={['filters', filterId, 'defaultDataMask']}
|
() =>
|
||||||
initialValue={filterToEdit?.defaultDataMask}
|
form.validateFields([
|
||||||
data-test="default-input"
|
['filters', filterId, 'parentFilter'],
|
||||||
label={<StyledLabel>{t('Default Value')}</StyledLabel>}
|
]),
|
||||||
required
|
0,
|
||||||
rules={[
|
);
|
||||||
{
|
}
|
||||||
required: true,
|
}}
|
||||||
},
|
|
||||||
{
|
|
||||||
validator: (rule, value) => {
|
|
||||||
const hasValue = !!value.filterState?.value;
|
|
||||||
if (hasValue) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(t('Default value is required')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
{showDefaultValue ? (
|
<StyledRowFormItem
|
||||||
<DefaultValue
|
name={['filters', filterId, 'parentFilter']}
|
||||||
setDataMask={dataMask => {
|
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
||||||
|
initialValue={parentFilter}
|
||||||
|
data-test="parent-filter-input"
|
||||||
|
required
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('Parent filter is required'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder={t('None')}
|
||||||
|
options={parentFilterOptions}
|
||||||
|
isClearable
|
||||||
|
/>
|
||||||
|
</StyledRowFormItem>
|
||||||
|
</CollapsibleControl>
|
||||||
|
)}
|
||||||
|
{Object.keys(controlItems)
|
||||||
|
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
|
||||||
|
.map(key => controlItems[key])}
|
||||||
|
{hasDataset && hasAdditionalFilters && (
|
||||||
|
<CollapsibleControl
|
||||||
|
title={t('Pre-filter available values')}
|
||||||
|
checked={
|
||||||
|
!!filterToEdit?.adhoc_filters || !!filterToEdit?.time_range
|
||||||
|
}
|
||||||
|
onChange={checked => {
|
||||||
|
if (checked) {
|
||||||
|
// execute after render
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
form.validateFields([
|
||||||
|
['filters', filterId, 'adhoc_filters'],
|
||||||
|
]),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledRowFormItem
|
||||||
|
name={['filters', filterId, 'adhoc_filters']}
|
||||||
|
initialValue={filterToEdit?.adhoc_filters}
|
||||||
|
required
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('Pre-filter is required'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AdhocFilterControl
|
||||||
|
columns={
|
||||||
|
datasetDetails?.columns?.filter(
|
||||||
|
(c: ColumnMeta) => c.filterable,
|
||||||
|
) || []
|
||||||
|
}
|
||||||
|
savedMetrics={datasetDetails?.metrics || []}
|
||||||
|
datasource={datasetDetails}
|
||||||
|
onChange={(filters: AdhocFilter[]) => {
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
defaultDataMask: dataMask,
|
adhoc_filters: filters,
|
||||||
});
|
});
|
||||||
form.validateFields([
|
|
||||||
['filters', filterId, 'defaultDataMask'],
|
|
||||||
]);
|
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}}
|
}}
|
||||||
filterId={filterId}
|
label={
|
||||||
hasDataset={hasDataset}
|
<span>
|
||||||
form={form}
|
<StyledAsterisk />
|
||||||
formData={newFormData}
|
<StyledLabel>{t('Pre-filter')}</StyledLabel>
|
||||||
enableNoResults={enableNoResults}
|
</span>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
</StyledRowFormItem>
|
||||||
t('Fill all required fields to enable "Default Value"')
|
<StyledRowFormItem
|
||||||
)}
|
name={['filters', filterId, 'time_range']}
|
||||||
</StyledRowFormItem>
|
label={<StyledLabel>{t('Time range')}</StyledLabel>}
|
||||||
</CollapsibleControl>
|
initialValue={filterToEdit?.time_range || 'No filter'}
|
||||||
{Object.keys(controlItems)
|
|
||||||
.filter(key => BASIC_CONTROL_ITEMS.includes(key))
|
|
||||||
.map(key => controlItems[key])}
|
|
||||||
<StyledRowFormItem
|
|
||||||
name={['filters', filterId, 'isInstant']}
|
|
||||||
initialValue={filterToEdit?.isInstant || false}
|
|
||||||
valuePropName="checked"
|
|
||||||
colon={false}
|
|
||||||
>
|
|
||||||
<Checkbox data-test="apply-changes-instantly-checkbox">
|
|
||||||
{t('Apply changes instantly')}
|
|
||||||
</Checkbox>
|
|
||||||
</StyledRowFormItem>
|
|
||||||
</Collapse.Panel>
|
|
||||||
{((hasDataset && hasAdditionalFilters) || hasMetrics) && (
|
|
||||||
<Collapse.Panel
|
|
||||||
header={FilterPanels.advanced.name}
|
|
||||||
key={FilterPanels.advanced.key}
|
|
||||||
>
|
|
||||||
{isCascadingFilter && (
|
|
||||||
<CollapsibleControl
|
|
||||||
title={t('Filter is hierarchical')}
|
|
||||||
checked={!!parentFilter}
|
|
||||||
onChange={checked => {
|
|
||||||
if (checked) {
|
|
||||||
// execute after render
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
form.validateFields([
|
|
||||||
['filters', filterId, 'parentFilter'],
|
|
||||||
]),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<StyledRowFormItem
|
<DateFilterControl
|
||||||
name={['filters', filterId, 'parentFilter']}
|
name="time_range"
|
||||||
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
onChange={timeRange => {
|
||||||
initialValue={parentFilter}
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
data-test="parent-filter-input"
|
time_range: timeRange,
|
||||||
required
|
});
|
||||||
rules={[
|
forceUpdate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StyledRowFormItem>
|
||||||
|
</CollapsibleControl>
|
||||||
|
)}
|
||||||
|
<CollapsibleControl
|
||||||
|
title={t('Sort filter values')}
|
||||||
|
onChange={checked => onSortChanged(checked || undefined)}
|
||||||
|
checked={
|
||||||
|
typeof filterToEdit?.controlValues?.sortAscending ===
|
||||||
|
'boolean'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StyledRowContainer>
|
||||||
|
<StyledFormItem
|
||||||
|
name={[
|
||||||
|
'filters',
|
||||||
|
filterId,
|
||||||
|
'controlValues',
|
||||||
|
'sortAscending',
|
||||||
|
]}
|
||||||
|
initialValue={filterToEdit?.controlValues?.sortAscending}
|
||||||
|
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
form={form}
|
||||||
|
filterId={filterId}
|
||||||
|
name="sortAscending"
|
||||||
|
options={[
|
||||||
{
|
{
|
||||||
required: true,
|
value: true,
|
||||||
message: t('Parent filter is required'),
|
label: t('Sort ascending'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: false,
|
||||||
|
label: t('Sort descending'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
onChange={({ value }: { value: boolean }) =>
|
||||||
<Select
|
onSortChanged(value)
|
||||||
placeholder={t('None')}
|
|
||||||
options={parentFilterOptions}
|
|
||||||
isClearable
|
|
||||||
/>
|
|
||||||
</StyledRowFormItem>
|
|
||||||
</CollapsibleControl>
|
|
||||||
)}
|
|
||||||
{Object.keys(controlItems)
|
|
||||||
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
|
|
||||||
.map(key => controlItems[key])}
|
|
||||||
{hasDataset && hasAdditionalFilters && (
|
|
||||||
<CollapsibleControl
|
|
||||||
title={t('Pre-filter available values')}
|
|
||||||
checked={
|
|
||||||
!!filterToEdit?.adhoc_filters ||
|
|
||||||
!!filterToEdit?.time_range
|
|
||||||
}
|
|
||||||
onChange={checked => {
|
|
||||||
if (checked) {
|
|
||||||
// execute after render
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
form.validateFields([
|
|
||||||
['filters', filterId, 'adhoc_filters'],
|
|
||||||
]),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}}
|
/>
|
||||||
>
|
</StyledFormItem>
|
||||||
<StyledRowFormItem
|
{hasMetrics && (
|
||||||
name={['filters', filterId, 'adhoc_filters']}
|
|
||||||
initialValue={filterToEdit?.adhoc_filters}
|
|
||||||
required
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: t('Adhoc filters is required'),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<AdhocFilterControl
|
|
||||||
columns={
|
|
||||||
datasetDetails?.columns?.filter(
|
|
||||||
(c: ColumnMeta) => c.filterable,
|
|
||||||
) || []
|
|
||||||
}
|
|
||||||
savedMetrics={datasetDetails?.metrics || []}
|
|
||||||
datasource={datasetDetails}
|
|
||||||
onChange={(filters: AdhocFilter[]) => {
|
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
|
||||||
adhoc_filters: filters,
|
|
||||||
});
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
<StyledAsterisk>*</StyledAsterisk>
|
|
||||||
<StyledLabel>{t('Adhoc filters')}</StyledLabel>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</StyledRowFormItem>
|
|
||||||
<StyledRowFormItem
|
|
||||||
name={['filters', filterId, 'time_range']}
|
|
||||||
label={<StyledLabel>{t('Time range')}</StyledLabel>}
|
|
||||||
initialValue={filterToEdit?.time_range || 'No filter'}
|
|
||||||
>
|
|
||||||
<DateFilterControl
|
|
||||||
name="time_range"
|
|
||||||
onChange={timeRange => {
|
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
|
||||||
time_range: timeRange,
|
|
||||||
});
|
|
||||||
forceUpdate();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</StyledRowFormItem>
|
|
||||||
</CollapsibleControl>
|
|
||||||
)}
|
|
||||||
<CollapsibleControl
|
|
||||||
title={t('Sort filter values')}
|
|
||||||
onChange={checked => onSortChanged(checked || undefined)}
|
|
||||||
checked={
|
|
||||||
typeof filterToEdit?.controlValues?.sortAscending ===
|
|
||||||
'boolean'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StyledRowContainer>
|
|
||||||
<StyledFormItem
|
<StyledFormItem
|
||||||
name={[
|
name={['filters', filterId, 'sortMetric']}
|
||||||
'filters',
|
initialValue={filterToEdit?.sortMetric}
|
||||||
filterId,
|
label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
|
||||||
'controlValues',
|
data-test="field-input"
|
||||||
'sortAscending',
|
|
||||||
]}
|
|
||||||
initialValue={filterToEdit?.controlValues?.sortAscending}
|
|
||||||
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
|
|
||||||
>
|
>
|
||||||
<Select
|
<SelectControl
|
||||||
form={form}
|
form={form}
|
||||||
filterId={filterId}
|
filterId={filterId}
|
||||||
name="sortAscending"
|
name="sortMetric"
|
||||||
options={[
|
options={metrics.map((metric: Metric) => ({
|
||||||
{
|
value: metric.metric_name,
|
||||||
value: true,
|
label: metric.verbose_name ?? metric.metric_name,
|
||||||
label: t('Sort ascending'),
|
}))}
|
||||||
},
|
onChange={(value: string | null): void => {
|
||||||
{
|
if (value !== undefined) {
|
||||||
value: false,
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
label: t('Sort descending'),
|
sortMetric: value,
|
||||||
},
|
});
|
||||||
]}
|
forceUpdate();
|
||||||
onChange={({ value }: { value: boolean }) =>
|
}
|
||||||
onSortChanged(value)
|
}}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</StyledFormItem>
|
</StyledFormItem>
|
||||||
{hasMetrics && (
|
)}
|
||||||
<StyledFormItem
|
</StyledRowContainer>
|
||||||
name={['filters', filterId, 'sortMetric']}
|
</CollapsibleControl>
|
||||||
initialValue={filterToEdit?.sortMetric}
|
</Collapse.Panel>
|
||||||
label={<StyledLabel>{t('Sort Metric')}</StyledLabel>}
|
)}
|
||||||
data-test="field-input"
|
</StyledCollapse>
|
||||||
>
|
</TabPane>
|
||||||
<SelectControl
|
<TabPane
|
||||||
form={form}
|
tab={FilterTabs.scoping.name}
|
||||||
filterId={filterId}
|
key={FilterTabs.scoping.key}
|
||||||
name="sortMetric"
|
forceRender
|
||||||
options={metrics.map((metric: Metric) => ({
|
>
|
||||||
value: metric.metric_name,
|
<FilterScope
|
||||||
label: metric.verbose_name ?? metric.metric_name,
|
updateFormValues={(values: any) =>
|
||||||
}))}
|
setNativeFilterFieldValues(form, filterId, values)
|
||||||
onChange={(value: string | null): void => {
|
}
|
||||||
if (value !== undefined) {
|
pathToFormValue={['filters', filterId]}
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
forceUpdate={forceUpdate}
|
||||||
sortMetric: value,
|
scope={filterToEdit?.scope}
|
||||||
});
|
formScope={formFilter?.scope}
|
||||||
forceUpdate();
|
formScoping={formFilter?.scoping}
|
||||||
}
|
/>
|
||||||
}}
|
</TabPane>
|
||||||
/>
|
</StyledTabs>
|
||||||
</StyledFormItem>
|
|
||||||
)}
|
|
||||||
</StyledRowContainer>
|
|
||||||
</CollapsibleControl>
|
|
||||||
</Collapse.Panel>
|
|
||||||
)}
|
|
||||||
</StyledCollapse>
|
|
||||||
</TabPane>
|
|
||||||
<TabPane
|
|
||||||
tab={FilterTabs.scoping.name}
|
|
||||||
key={FilterTabs.scoping.key}
|
|
||||||
forceRender
|
|
||||||
>
|
|
||||||
<FilterScope
|
|
||||||
updateFormValues={(values: any) =>
|
|
||||||
setNativeFilterFieldValues(form, filterId, values)
|
|
||||||
}
|
|
||||||
pathToFormValue={['filters', filterId]}
|
|
||||||
forceUpdate={forceUpdate}
|
|
||||||
scope={filterToEdit?.scope}
|
|
||||||
formScope={formFilter?.scope}
|
|
||||||
formScoping={formFilter?.scoping}
|
|
||||||
/>
|
|
||||||
</TabPane>
|
|
||||||
</StyledTabs>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -231,7 +231,7 @@ export function FiltersConfigModal({
|
|||||||
<StyledModalWrapper
|
<StyledModalWrapper
|
||||||
visible={isOpen}
|
visible={isOpen}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
title={t('Filters configuration and scoping')}
|
title={t('Filters configuration')}
|
||||||
width="50%"
|
width="50%"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
|
@ -64,7 +64,7 @@ export const validateForm = async (
|
|||||||
addValidationError(
|
addValidationError(
|
||||||
filterId,
|
filterId,
|
||||||
'isInstant',
|
'isInstant',
|
||||||
'For parent filters changes must be applied instantly',
|
'For hierarchical filters changes must be applied instantly',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user