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 6 (#14932)
This commit is contained in:
parent
06945ccbcf
commit
b6f00e69e1
@ -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')}
|
||||||
|
@ -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>
|
||||||
|
@ -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"', () => {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user