chore: Improves the native filters UI/UX - iteration 2 (#14753)
This commit is contained in:
parent
2b2a8c4bcd
commit
bee6f3ba8a
|
|
@ -90,6 +90,7 @@ const addFilterSetFlow = async () => {
|
|||
// check description
|
||||
expect(screen.getByText('Filters (1)')).toBeInTheDocument();
|
||||
expect(screen.getByText(FILTER_NAME)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getAllByText('Last week').length).toBe(2);
|
||||
|
||||
// apply filters
|
||||
|
|
@ -304,7 +305,7 @@ describe('FilterBar', () => {
|
|||
|
||||
await addFilterFlow();
|
||||
|
||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
|
||||
});
|
||||
|
||||
it('add and apply filter set', async () => {
|
||||
|
|
@ -317,6 +318,8 @@ describe('FilterBar', () => {
|
|||
|
||||
await addFilterFlow();
|
||||
|
||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
||||
|
||||
await addFilterSetFlow();
|
||||
|
||||
// change filter
|
||||
|
|
@ -340,6 +343,7 @@ describe('FilterBar', () => {
|
|||
screen.getByTestId(getTestId('filter-set-wrapper')),
|
||||
).not.toHaveAttribute('data-selected', 'true');
|
||||
userEvent.click(screen.getByTestId(getTestId('filter-set-wrapper')));
|
||||
userEvent.click(screen.getAllByText('Filters (1)')[1]);
|
||||
expect(await screen.findByText('Last week')).toBeInTheDocument();
|
||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
||||
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
|
||||
|
|
@ -355,6 +359,8 @@ describe('FilterBar', () => {
|
|||
|
||||
await addFilterFlow();
|
||||
|
||||
userEvent.click(screen.getByTestId(getTestId('apply-button')));
|
||||
|
||||
await addFilterSetFlow();
|
||||
|
||||
userEvent.click(screen.getByTestId(getTestId('filter-set-menu-button')));
|
||||
|
|
|
|||
|
|
@ -84,31 +84,58 @@ export const FilterTabTitle = styled.span`
|
|||
`;
|
||||
|
||||
const FilterTabsContainer = styled(LineEditableTabs)`
|
||||
// extra selector specificity:
|
||||
&.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab {
|
||||
min-width: ${FILTER_WIDTH}px;
|
||||
margin: 0 ${({ theme }) => theme.gridUnit * 2}px 0 0;
|
||||
padding: ${({ theme }) => theme.gridUnit}px
|
||||
${({ theme }) => theme.gridUnit * 2}px;
|
||||
${({ theme }) => `
|
||||
height: 100%;
|
||||
|
||||
&:hover,
|
||||
&-active {
|
||||
color: ${({ theme }) => theme.colors.grayscale.dark1};
|
||||
border-radius: ${({ theme }) => theme.borderRadius}px;
|
||||
background-color: ${({ theme }) => theme.colors.secondary.light4};
|
||||
& > .ant-tabs-content-holder {
|
||||
border-left: 1px solid ${theme.colors.grayscale.light2};
|
||||
margin-right: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
& > .ant-tabs-content-holder ~ .ant-tabs-content-holder {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove > svg {
|
||||
color: ${({ theme }) => theme.colors.grayscale.base};
|
||||
transition: all 0.3s;
|
||||
&.ant-tabs-left
|
||||
> .ant-tabs-content-holder
|
||||
> .ant-tabs-content
|
||||
> .ant-tabs-tabpane {
|
||||
padding-left: ${theme.gridUnit * 4}px;
|
||||
margin-top: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
|
||||
.ant-tabs-nav-list {
|
||||
padding-top: ${theme.gridUnit * 4}px;
|
||||
padding-right: ${theme.gridUnit * 2}px;
|
||||
padding-bottom: ${theme.gridUnit * 4}px;
|
||||
padding-left: ${theme.gridUnit * 3}px;
|
||||
}
|
||||
|
||||
// extra selector specificity:
|
||||
&.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab {
|
||||
min-width: ${FILTER_WIDTH}px;
|
||||
margin: 0 ${theme.gridUnit * 2}px 0 0;
|
||||
padding: ${theme.gridUnit}px
|
||||
${theme.gridUnit * 2}px;
|
||||
|
||||
&:hover,
|
||||
&-active {
|
||||
color: ${theme.colors.grayscale.dark1};
|
||||
border-radius: ${theme.borderRadius}px;
|
||||
background-color: ${theme.colors.secondary.light4};
|
||||
|
||||
.ant-tabs-tab-remove > svg {
|
||||
color: ${theme.colors.grayscale.base};
|
||||
transition: all 0.3s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
text-align: left;
|
||||
justify-content: space-between;
|
||||
text-transform: unset;
|
||||
}
|
||||
.ant-tabs-tab-btn {
|
||||
text-align: left;
|
||||
justify-content: space-between;
|
||||
text-transform: unset;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
type FilterTabsProps = {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import DateFilterControl from 'src/explore/components/controls/DateFilterControl
|
|||
import { addDangerToast } from 'src/messageToasts/actions';
|
||||
import { ClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||
import Collapse from 'src/components/Collapse';
|
||||
import Button from 'src/components/Button';
|
||||
import { getChartDataRequest } from 'src/chart/chartAction';
|
||||
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
|
||||
|
|
@ -76,6 +77,12 @@ const StyledContainer = styled.div`
|
|||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledDatasetContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const StyledFormItem = styled(Form.Item)`
|
||||
width: 49%;
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
|
|
@ -95,6 +102,36 @@ const CleanFormItem = styled(Form.Item)`
|
|||
margin-bottom: 0;
|
||||
`;
|
||||
|
||||
const StyledCollapse = styled(Collapse)`
|
||||
margin-left: ${({ theme }) => theme.gridUnit * -4 - 1}px;
|
||||
margin-right: ${({ theme }) => theme.gridUnit * -4}px;
|
||||
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-radius: 0px;
|
||||
|
||||
.ant-collapse-header {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||
margin-top: -1px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.ant-collapse-content {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
&.ant-collapse > .ant-collapse-item {
|
||||
border: 0px;
|
||||
border-radius: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTabs = styled(Tabs)`
|
||||
.ant-tabs-nav-list {
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterTabs = {
|
||||
configuration: {
|
||||
key: 'configuration',
|
||||
|
|
@ -106,6 +143,17 @@ const FilterTabs = {
|
|||
},
|
||||
};
|
||||
|
||||
const FilterPanels = {
|
||||
basic: {
|
||||
key: 'basic',
|
||||
name: t('Basic'),
|
||||
},
|
||||
advanced: {
|
||||
key: 'advanced',
|
||||
name: t('Advanced'),
|
||||
},
|
||||
};
|
||||
|
||||
export interface FiltersConfigFormProps {
|
||||
filterId: string;
|
||||
filterToEdit?: Filter;
|
||||
|
|
@ -278,7 +326,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Tabs defaultActiveKey={FilterTabs.configuration.key} centered>
|
||||
<StyledTabs defaultActiveKey={FilterTabs.configuration.key} centered>
|
||||
<TabPane
|
||||
tab={FilterTabs.configuration.name}
|
||||
key={FilterTabs.configuration.key}
|
||||
|
|
@ -317,7 +365,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
{hasDataset && (
|
||||
<>
|
||||
<StyledDatasetContainer>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'dataset']}
|
||||
initialValue={{ value: initialDatasetId }}
|
||||
|
|
@ -375,156 +423,170 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
{hasAdditionalFilters && (
|
||||
<>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'adhoc_filters']}
|
||||
initialValue={filterToEdit?.adhoc_filters}
|
||||
>
|
||||
<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={<StyledLabel>{t('Adhoc filters')}</StyledLabel>}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</StyledDatasetContainer>
|
||||
)}
|
||||
{hasFilledDataset && (
|
||||
<CleanFormItem
|
||||
name={['filters', filterId, 'defaultValueFormData']}
|
||||
hidden
|
||||
initialValue={newFormData}
|
||||
/>
|
||||
)}
|
||||
<CleanFormItem
|
||||
name={['filters', filterId, 'defaultValueQueriesData']}
|
||||
hidden
|
||||
initialValue={null}
|
||||
/>
|
||||
{isCascadingFilter && (
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'parentFilter']}
|
||||
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
||||
initialValue={parentFilterOptions.find(
|
||||
({ value }) => value === filterToEdit?.cascadeParentIds[0],
|
||||
)}
|
||||
data-test="parent-filter-input"
|
||||
<StyledCollapse>
|
||||
<Collapse.Panel
|
||||
header={FilterPanels.basic.name}
|
||||
key={FilterPanels.basic.key}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
isClearable
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
<StyledContainer>
|
||||
<StyledFormItem className="bottom" label={<StyledLabel />}>
|
||||
{hasDataset && hasFilledDataset && (
|
||||
<Button onClick={refreshHandler}>
|
||||
{isDataDirty ? t('Populate') : t('Refresh')}
|
||||
</Button>
|
||||
)}
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'defaultDataMask']}
|
||||
initialValue={filterToEdit?.defaultDataMask}
|
||||
data-test="default-input"
|
||||
label={<StyledLabel>{t('Default Value')}</StyledLabel>}
|
||||
>
|
||||
{showDefaultValue ? (
|
||||
<DefaultValue
|
||||
setDataMask={dataMask => {
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
defaultDataMask: dataMask,
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
filterId={filterId}
|
||||
hasDataset={hasDataset}
|
||||
form={form}
|
||||
formData={newFormData}
|
||||
{hasFilledDataset && (
|
||||
<CleanFormItem
|
||||
name={['filters', filterId, 'defaultValueFormData']}
|
||||
hidden
|
||||
initialValue={newFormData}
|
||||
/>
|
||||
) : hasFilledDataset ? (
|
||||
t('Click "Populate" to get "Default Value" ->')
|
||||
) : (
|
||||
t('Fill all required fields to enable "Default Value"')
|
||||
)}
|
||||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
<StyledCheckboxFormItem
|
||||
name={['filters', filterId, 'isInstant']}
|
||||
initialValue={filterToEdit?.isInstant || false}
|
||||
valuePropName="checked"
|
||||
colon={false}
|
||||
>
|
||||
<Checkbox data-test="apply-changes-instantly-checkbox">
|
||||
{t('Apply changes instantly')}
|
||||
</Checkbox>
|
||||
</StyledCheckboxFormItem>
|
||||
<ControlItems
|
||||
disabled={!showDefaultValue}
|
||||
filterToEdit={filterToEdit}
|
||||
formFilter={formFilter}
|
||||
filterId={filterId}
|
||||
form={form}
|
||||
forceUpdate={forceUpdate}
|
||||
/>
|
||||
{hasMetrics && (
|
||||
<StyledFormItem
|
||||
// don't show the column select unless we have a dataset
|
||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||
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();
|
||||
}
|
||||
}}
|
||||
<CleanFormItem
|
||||
name={['filters', filterId, 'defaultValueQueriesData']}
|
||||
hidden
|
||||
initialValue={null}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
{isCascadingFilter && (
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'parentFilter']}
|
||||
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
||||
initialValue={parentFilterOptions.find(
|
||||
({ value }) => value === filterToEdit?.cascadeParentIds[0],
|
||||
)}
|
||||
data-test="parent-filter-input"
|
||||
>
|
||||
<Select
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
isClearable
|
||||
/>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
<StyledContainer>
|
||||
<StyledFormItem className="bottom" label={<StyledLabel />}>
|
||||
{hasDataset && hasFilledDataset && (
|
||||
<Button onClick={refreshHandler}>
|
||||
{isDataDirty ? t('Populate') : t('Refresh')}
|
||||
</Button>
|
||||
)}
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'defaultDataMask']}
|
||||
initialValue={filterToEdit?.defaultDataMask}
|
||||
data-test="default-input"
|
||||
label={<StyledLabel>{t('Default Value')}</StyledLabel>}
|
||||
>
|
||||
{showDefaultValue ? (
|
||||
<DefaultValue
|
||||
setDataMask={dataMask => {
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
defaultDataMask: dataMask,
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
filterId={filterId}
|
||||
hasDataset={hasDataset}
|
||||
form={form}
|
||||
formData={newFormData}
|
||||
/>
|
||||
) : hasFilledDataset ? (
|
||||
t('Click "Populate" to get "Default Value" ->')
|
||||
) : (
|
||||
t('Fill all required fields to enable "Default Value"')
|
||||
)}
|
||||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
<StyledCheckboxFormItem
|
||||
name={['filters', filterId, 'isInstant']}
|
||||
initialValue={filterToEdit?.isInstant || false}
|
||||
valuePropName="checked"
|
||||
colon={false}
|
||||
>
|
||||
<Checkbox data-test="apply-changes-instantly-checkbox">
|
||||
{t('Apply changes instantly')}
|
||||
</Checkbox>
|
||||
</StyledCheckboxFormItem>
|
||||
<ControlItems
|
||||
disabled={!showDefaultValue}
|
||||
filterToEdit={filterToEdit}
|
||||
formFilter={formFilter}
|
||||
filterId={filterId}
|
||||
form={form}
|
||||
forceUpdate={forceUpdate}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
{((hasDataset && hasAdditionalFilters) || hasMetrics) && (
|
||||
<Collapse.Panel
|
||||
header={FilterPanels.advanced.name}
|
||||
key={FilterPanels.advanced.key}
|
||||
>
|
||||
{hasDataset && hasAdditionalFilters && (
|
||||
<>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'adhoc_filters']}
|
||||
initialValue={filterToEdit?.adhoc_filters}
|
||||
>
|
||||
<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={<StyledLabel>{t('Adhoc filters')}</StyledLabel>}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</>
|
||||
)}
|
||||
{hasMetrics && (
|
||||
<StyledFormItem
|
||||
// don't show the column select unless we have a dataset
|
||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||
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>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
)}
|
||||
</StyledCollapse>
|
||||
</TabPane>
|
||||
<TabPane
|
||||
tab={FilterTabs.scoping.name}
|
||||
|
|
@ -542,7 +604,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
|||
formScoping={formFilter?.scoping}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</StyledTabs>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,6 +37,13 @@ import FilterTabs from './FilterTabs';
|
|||
import FiltersConfigForm from './FiltersConfigForm/FiltersConfigForm';
|
||||
import { useOpenModal, useRemoveCurrentFilter } from './state';
|
||||
|
||||
const StyledModalWrapper = styled(StyledModal)`
|
||||
min-width: 700px;
|
||||
.ant-modal-body {
|
||||
padding: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const StyledModalBody = styled.div`
|
||||
display: flex;
|
||||
height: 500px;
|
||||
|
|
@ -205,11 +212,11 @@ export function FiltersConfigModal({
|
|||
};
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
<StyledModalWrapper
|
||||
visible={isOpen}
|
||||
maskClosable={false}
|
||||
title={t('Filters configuration and scoping')}
|
||||
width="55%"
|
||||
width="50%"
|
||||
destroyOnClose
|
||||
onCancel={handleCancel}
|
||||
onOk={handleSave}
|
||||
|
|
@ -269,6 +276,6 @@ export function FiltersConfigModal({
|
|||
</StyledForm>
|
||||
</StyledModalBody>
|
||||
</ErrorBoundary>
|
||||
</StyledModal>
|
||||
</StyledModalWrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue