chore: Improves the native filters UI/UX - iteration 5 (#14882)

This commit is contained in:
Michael S. Molina 2021-05-28 04:29:16 -03:00 committed by GitHub
parent fce8ac27f0
commit 8519a09086
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 444 additions and 379 deletions

View File

@ -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: '*';
} }
} }

View File

@ -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)');

View File

@ -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

View File

@ -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>
</>
); );
}; };

View File

@ -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}

View File

@ -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',
); );
} }
}; };