chore: Improves the native filters UI/UX - iteration 6 (#14932)

This commit is contained in:
Michael S. Molina 2021-06-02 03:03:13 -03:00 committed by GitHub
parent 06945ccbcf
commit b6f00e69e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 84 deletions

View File

@ -24,7 +24,7 @@ import Icon from 'src/components/Icon';
import { FilterRemoval } from './types'; import { FilterRemoval } from './types';
import { REMOVAL_DELAY_SECS } from './utils'; import { REMOVAL_DELAY_SECS } from './utils';
export const FILTER_WIDTH = 200; export const FILTER_WIDTH = 180;
export const StyledSpan = styled.span` export const StyledSpan = styled.span`
cursor: pointer; cursor: pointer;
@ -115,6 +115,7 @@ const FilterTabsContainer = styled(LineEditableTabs)`
padding-right: ${theme.gridUnit}px; padding-right: ${theme.gridUnit}px;
padding-bottom: ${theme.gridUnit * 3}px; padding-bottom: ${theme.gridUnit * 3}px;
padding-left: ${theme.gridUnit * 3}px; padding-left: ${theme.gridUnit * 3}px;
width: 270px;
} }
// extra selector specificity: // extra selector specificity:
@ -185,6 +186,8 @@ const FilterTabs: FC<FilterTabsProps> = ({
children, children,
}) => ( }) => (
<FilterTabsContainer <FilterTabsContainer
id="native-filters-tabs"
type="editable-card"
tabPosition="left" tabPosition="left"
onChange={onChange} onChange={onChange}
activeKey={currentFilterId} activeKey={currentFilterId}
@ -193,7 +196,20 @@ const FilterTabs: FC<FilterTabsProps> = ({
tabBarExtraContent={{ tabBarExtraContent={{
left: <StyledHeader>{t('Filters')}</StyledHeader>, left: <StyledHeader>{t('Filters')}</StyledHeader>,
right: ( right: (
<StyledAddFilterBox onClick={() => onEdit('', 'add')}> <StyledAddFilterBox
onClick={() => {
onEdit('', 'add');
setTimeout(() => {
const element = document.getElementById('native-filters-tabs');
if (element) {
const navList = element.getElementsByClassName(
'ant-tabs-nav-list',
)[0];
navList.scrollTop = navList.scrollHeight;
}
}, 0);
}}
>
<PlusOutlined />{' '} <PlusOutlined />{' '}
<span data-test="add-filter-button" aria-label="Add filter"> <span data-test="add-filter-button" aria-label="Add filter">
{t('Add filter')} {t('Add filter')}

View File

@ -229,6 +229,7 @@ const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect']; const BASIC_CONTROL_ITEMS = ['enableEmptyFilter', 'multiSelect'];
// TODO: Rename the filter plugins and remove this mapping
const FILTER_TYPE_NAME_MAPPING = { const FILTER_TYPE_NAME_MAPPING = {
[t('Select filter')]: t('Value'), [t('Select filter')]: t('Value'),
[t('Range filter')]: t('Numerical range'), [t('Range filter')]: t('Numerical range'),
@ -254,7 +255,12 @@ const FiltersConfigForm = (
ref: React.RefObject<any>, ref: React.RefObject<any>,
) => { ) => {
const [metrics, setMetrics] = useState<Metric[]>([]); const [metrics, setMetrics] = useState<Metric[]>([]);
const [activeTabKey, setActiveTabKey] = useState<string | undefined>(); const [activeTabKey, setActiveTabKey] = useState<string>(
FilterTabs.configuration.key,
);
const [activeFilterPanelKey, setActiveFilterPanelKey] = useState<
string | string[]
>(FilterPanels.basic.key);
const [hasDefaultValue, setHasDefaultValue] = useState( const [hasDefaultValue, setHasDefaultValue] = useState(
!!filterToEdit?.defaultDataMask?.filterState?.value, !!filterToEdit?.defaultDataMask?.filterState?.value,
); );
@ -410,10 +416,6 @@ const FiltersConfigForm = (
[], [],
); );
if (removed) {
return <RemovedFilter onClick={() => restoreFilter(filterId)} />;
}
const parentFilterOptions = parentFilters.map(filter => ({ const parentFilterOptions = parentFilters.map(filter => ({
value: filter.id, value: filter.id,
label: filter.title, label: filter.title,
@ -423,6 +425,14 @@ const FiltersConfigForm = (
({ value }) => value === filterToEdit?.cascadeParentIds[0], ({ value }) => value === filterToEdit?.cascadeParentIds[0],
); );
const hasParentFilter = !!parentFilter;
const hasPreFilter =
!!filterToEdit?.adhoc_filters || !!filterToEdit?.time_range;
const hasSorting =
typeof filterToEdit?.controlValues?.sortAscending === 'boolean';
const showDefaultValue = !hasDataset || (!isDataDirty && hasFilledDataset); const showDefaultValue = !hasDataset || (!isDataDirty && hasFilledDataset);
const controlItems = formFilter const controlItems = formFilter
@ -447,9 +457,27 @@ const FiltersConfigForm = (
forceUpdate(); forceUpdate();
}; };
let hasCheckedAdvancedControl = hasParentFilter || hasPreFilter || hasSorting;
if (!hasCheckedAdvancedControl) {
hasCheckedAdvancedControl = Object.keys(controlItems)
.filter(key => !BASIC_CONTROL_ITEMS.includes(key))
.some(key => controlItems[key].checked);
}
useEffect(() => {
const activeFilterPanelKey = [FilterPanels.basic.key];
if (hasCheckedAdvancedControl) {
activeFilterPanelKey.push(FilterPanels.advanced.key);
}
setActiveFilterPanelKey(activeFilterPanelKey);
}, [hasCheckedAdvancedControl]);
if (removed) {
return <RemovedFilter onClick={() => restoreFilter(filterId)} />;
}
return ( return (
<StyledTabs <StyledTabs
defaultActiveKey={FilterTabs.configuration.key}
activeKey={activeTabKey} activeKey={activeTabKey}
onChange={activeKey => setActiveTabKey(activeKey)} onChange={activeKey => setActiveTabKey(activeKey)}
centered centered
@ -559,7 +587,8 @@ const FiltersConfigForm = (
</StyledRowContainer> </StyledRowContainer>
)} )}
<StyledCollapse <StyledCollapse
defaultActiveKey={FilterPanels.basic.key} activeKey={activeFilterPanelKey}
onChange={key => setActiveFilterPanelKey(key)}
expandIconPosition="right" expandIconPosition="right"
> >
<Collapse.Panel <Collapse.Panel
@ -630,7 +659,7 @@ const FiltersConfigForm = (
</CollapsibleControl> </CollapsibleControl>
{Object.keys(controlItems) {Object.keys(controlItems)
.filter(key => BASIC_CONTROL_ITEMS.includes(key)) .filter(key => BASIC_CONTROL_ITEMS.includes(key))
.map(key => controlItems[key])} .map(key => controlItems[key].element)}
<StyledRowFormItem <StyledRowFormItem
name={['filters', filterId, 'isInstant']} name={['filters', filterId, 'isInstant']}
initialValue={filterToEdit?.isInstant || false} initialValue={filterToEdit?.isInstant || false}
@ -650,7 +679,7 @@ const FiltersConfigForm = (
{isCascadingFilter && ( {isCascadingFilter && (
<CollapsibleControl <CollapsibleControl
title={t('Filter is hierarchical')} title={t('Filter is hierarchical')}
checked={!!parentFilter} checked={hasParentFilter}
onChange={checked => { onChange={checked => {
if (checked) { if (checked) {
// execute after render // execute after render
@ -687,13 +716,11 @@ const FiltersConfigForm = (
)} )}
{Object.keys(controlItems) {Object.keys(controlItems)
.filter(key => !BASIC_CONTROL_ITEMS.includes(key)) .filter(key => !BASIC_CONTROL_ITEMS.includes(key))
.map(key => controlItems[key])} .map(key => controlItems[key].element)}
{hasDataset && hasAdditionalFilters && ( {hasDataset && hasAdditionalFilters && (
<CollapsibleControl <CollapsibleControl
title={t('Pre-filter available values')} title={t('Pre-filter available values')}
checked={ checked={hasPreFilter}
!!filterToEdit?.adhoc_filters || !!filterToEdit?.time_range
}
onChange={checked => { onChange={checked => {
if (checked) { if (checked) {
// execute after render // execute after render
@ -757,72 +784,71 @@ const FiltersConfigForm = (
</StyledRowFormItem> </StyledRowFormItem>
</CollapsibleControl> </CollapsibleControl>
)} )}
<CollapsibleControl {formFilter?.filterType !== 'filter_range' && (
title={t('Sort filter values')} <CollapsibleControl
onChange={checked => onSortChanged(checked || undefined)} title={t('Sort filter values')}
checked={ onChange={checked => onSortChanged(checked || undefined)}
typeof filterToEdit?.controlValues?.sortAscending === checked={hasSorting}
'boolean' >
} <StyledRowContainer>
>
<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={[
{
value: true,
label: t('Sort ascending'),
},
{
value: false,
label: t('Sort descending'),
},
]}
onChange={({ value }: { value: boolean }) =>
onSortChanged(value)
}
/>
</StyledFormItem>
{hasMetrics && (
<StyledFormItem <StyledFormItem
name={['filters', filterId, 'sortMetric']} name={[
initialValue={filterToEdit?.sortMetric} 'filters',
label={<StyledLabel>{t('Sort Metric')}</StyledLabel>} filterId,
data-test="field-input" 'controlValues',
'sortAscending',
]}
initialValue={filterToEdit?.controlValues?.sortAscending}
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
> >
<SelectControl <Select
form={form} form={form}
filterId={filterId} filterId={filterId}
name="sortMetric" name="sortAscending"
options={metrics.map((metric: Metric) => ({ options={[
value: metric.metric_name, {
label: metric.verbose_name ?? metric.metric_name, value: true,
}))} label: t('Sort ascending'),
onChange={(value: string | null): void => { },
if (value !== undefined) { {
setNativeFilterFieldValues(form, filterId, { value: false,
sortMetric: value, label: t('Sort descending'),
}); },
forceUpdate(); ]}
} onChange={({ value }: { value: boolean }) =>
}} onSortChanged(value)
}
/> />
</StyledFormItem> </StyledFormItem>
)} {hasMetrics && (
</StyledRowContainer> <StyledFormItem
</CollapsibleControl> 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>
)}
</StyledRowContainer>
</CollapsibleControl>
)}
</Collapse.Panel> </Collapse.Panel>
)} )}
</StyledCollapse> </StyledCollapse>

View File

@ -83,8 +83,12 @@ beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
function renderControlItems(controlItemsMap: {}): any { function renderControlItems(
return render(<>{Object.values(controlItemsMap).map(value => value)}</>); controlItemsMap: ReturnType<typeof getControlItemsMap>,
) {
return render(
<>{Object.values(controlItemsMap).map(value => value.element)}</>,
);
} }
test('Should render null when has no "formFilter"', () => { test('Should render null when has no "formFilter"', () => {

View File

@ -50,7 +50,10 @@ export default function getControlItemsMap({
const controlPanelRegistry = getChartControlPanelRegistry(); const controlPanelRegistry = getChartControlPanelRegistry();
const controlItems = const controlItems =
getControlItems(controlPanelRegistry.get(filterType)) ?? []; getControlItems(controlPanelRegistry.get(filterType)) ?? [];
const map = {}; const map: Record<
string,
{ element: React.ReactNode; checked: boolean }
> = {};
controlItems controlItems
.filter( .filter(
@ -59,6 +62,9 @@ export default function getControlItemsMap({
controlItem.name !== 'sortAscending', controlItem.name !== 'sortAscending',
) )
.forEach(controlItem => { .forEach(controlItem => {
const initialValue =
filterToEdit?.controlValues?.[controlItem.name] ??
controlItem?.config?.default;
const element = ( const element = (
<Tooltip <Tooltip
key={controlItem.name} key={controlItem.name}
@ -72,10 +78,7 @@ export default function getControlItemsMap({
<StyledRowFormItem <StyledRowFormItem
key={controlItem.name} key={controlItem.name}
name={['filters', filterId, 'controlValues', controlItem.name]} name={['filters', filterId, 'controlValues', controlItem.name]}
initialValue={ initialValue={initialValue}
filterToEdit?.controlValues?.[controlItem.name] ??
controlItem?.config?.default
}
valuePropName="checked" valuePropName="checked"
colon={false} colon={false}
> >
@ -104,7 +107,7 @@ export default function getControlItemsMap({
</StyledRowFormItem> </StyledRowFormItem>
</Tooltip> </Tooltip>
); );
map[controlItem.name] = element; map[controlItem.name] = { element, checked: initialValue };
}); });
return map; return map;
} }

View File

@ -183,7 +183,11 @@ export function FiltersConfigModal({
filterIds filterIds
.filter(filterId => filterId !== id && !removedFilters[filterId]) .filter(filterId => filterId !== id && !removedFilters[filterId])
.filter(filterId => .filter(filterId =>
CASCADING_FILTERS.includes(formValues.filters[filterId]?.filterType), CASCADING_FILTERS.includes(
formValues.filters[filterId]
? formValues.filters[filterId].filterType
: filterConfigMap[filterId]?.filterType,
),
) )
.map(id => ({ .map(id => ({
id, id,