chore: Improves the native filters UI/UX - iteration 2 (#14753)

This commit is contained in:
Michael S. Molina 2021-05-24 09:22:05 -03:00 committed by GitHub
parent 2b2a8c4bcd
commit bee6f3ba8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 274 additions and 172 deletions

View File

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

View File

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

View File

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

View File

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