mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04:00
feat(filter-set): Update existing filter set (#13545)
* refactor(native-filters): move data mask to root reducer * refactor: update rest stuff for dataMask * refactor: add ownCrrentState to explore * fix: fix immer reducer * fix: merge with master * refactor: support explore dataMask * refactor: support explore dataMask * docs: add comment * refactor: remove json stringify * fix: fix failed cases * feat: filter bat buttons start * fix: fix CR notes * fix: fix cascade filters * fix: fix CR notes * refactor: add clear all * fix: fix CR notes * fix: fix CR notes * fix: fix CR notes * feat: buttons in filter bar * lint: update imports * feat: add tabs for filter sets * feat: add buttons to filter set * feat: first phase add filter sets * fix: undo FF * refactor: continue filter sets * fix: fix CR notes * refactor: header * fix: fix CR notes * fix: fix CR notes * refactor: continue filter sets * lint: fix lint * refactor: continue filter sets * fix: fix filter bar opening * refactor: continue filter sets * refactor: continue filter sets * refactor: continue filter sets * feat: filters sets history * feat: filters sets history * fix: filter set name * refactor: fix expand filters case * fix: fix CR notes * refactor: filter sets * refactor: filter sets * refactor: filter sets * refactor: filter sets * refactor: update sets * feat: edit filter set * refactor: add warning icon * fix: fix CR notes * Update superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/EditSection.tsx Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * fix: fix CR notes Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
parent
1d1a1cdc20
commit
d509b157fd
@ -30,6 +30,7 @@ export interface ButtonProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
tooltip?: string;
|
||||
ghost?: boolean;
|
||||
placement?:
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
|
@ -134,14 +134,15 @@ export const setFilterSetsConfiguration = (
|
||||
filter_sets_configuration: filterSetsConfig,
|
||||
}),
|
||||
});
|
||||
const newMetadata = JSON.parse(response.result.json_metadata);
|
||||
dispatch(
|
||||
dashboardInfoChanged({
|
||||
metadata: JSON.parse(response.result.json_metadata),
|
||||
metadata: newMetadata,
|
||||
}),
|
||||
);
|
||||
dispatch({
|
||||
type: SET_FILTER_SETS_CONFIG_COMPLETE,
|
||||
filterSetsConfig,
|
||||
filterSetsConfig: newMetadata?.filter_sets_configuration,
|
||||
});
|
||||
} catch (err) {
|
||||
dispatch({ type: SET_FILTER_SETS_CONFIG_FAIL, filterSetsConfig });
|
||||
|
@ -37,6 +37,7 @@ import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils';
|
||||
import CascadePopover from './CascadePopover';
|
||||
import FilterSets from './FilterSets/FilterSets';
|
||||
import { useDataMask, useFilters, useFilterSets } from './state';
|
||||
import EditSection from './FilterSets/EditSection';
|
||||
|
||||
const barWidth = `250px`;
|
||||
|
||||
@ -173,6 +174,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
toggleFiltersBar,
|
||||
directPathToChild,
|
||||
}) => {
|
||||
const [editFilterSetId, setEditFilterSetId] = useState<string | null>(null);
|
||||
const [dataMaskSelected, setDataMaskSelected] = useImmer<DataMaskUnit>({});
|
||||
const [
|
||||
lastAppliedFilterData,
|
||||
@ -337,19 +339,29 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
<StyledTabs
|
||||
centered
|
||||
defaultActiveKey="allFilters"
|
||||
onChange={() => {}}
|
||||
activeKey={editFilterSetId ? 'allFilters' : undefined}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
tab={t(`All Filters (${filterValues.length})`)}
|
||||
key="allFilters"
|
||||
>
|
||||
{editFilterSetId && (
|
||||
<EditSection
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
disabled={!isApplyDisabled}
|
||||
onCancel={() => setEditFilterSetId(null)}
|
||||
filterSetId={editFilterSetId}
|
||||
/>
|
||||
)}
|
||||
{getFilterControls()}
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane
|
||||
disabled={!!editFilterSetId}
|
||||
tab={t(`Filter Sets (${filterSetFilterValues.length})`)}
|
||||
key="filterSets"
|
||||
>
|
||||
<FilterSets
|
||||
onEditFilterSet={setEditFilterSetId}
|
||||
disabled={!isApplyDisabled}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
onFilterSelectionChange={handleFilterSelectionChange}
|
||||
|
@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { HandlerFunction, styled, t } from '@superset-ui/core';
|
||||
import { Typography, Tooltip } from 'src/common/components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Button from 'src/components/Button';
|
||||
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters';
|
||||
import { DataMaskUnit } from 'src/dataMask/types';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
import { ActionButtons } from './Footer';
|
||||
import { useDataMask, useFilterSets } from '../state';
|
||||
import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
grid-gap: ${({ theme }) => theme.gridUnit}px;
|
||||
background: ${({ theme }) => theme.colors.primary.light4};
|
||||
padding: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
`;
|
||||
|
||||
const Title = styled(Typography.Text)`
|
||||
color: ${({ theme }) => theme.colors.primary.dark2};
|
||||
`;
|
||||
|
||||
const Warning = styled(Typography.Text)`
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
& .anticon {
|
||||
padding: ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButton = styled.div<{ disabled?: boolean }>`
|
||||
display: flex;
|
||||
& button {
|
||||
${({ disabled }) => `pointer-events: ${disabled ? 'none' : 'all'}`};
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
type EditSectionProps = {
|
||||
filterSetId: string;
|
||||
dataMaskSelected: DataMaskUnit;
|
||||
onCancel: HandlerFunction;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
const EditSection: FC<EditSectionProps> = ({
|
||||
filterSetId,
|
||||
onCancel,
|
||||
dataMaskSelected,
|
||||
disabled,
|
||||
}) => {
|
||||
const dataMaskApplied = useDataMask();
|
||||
const dispatch = useDispatch();
|
||||
const filterSets = useFilterSets();
|
||||
const filterSetFilterValues = Object.values(filterSets);
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
setFilterSetsConfiguration(
|
||||
filterSetFilterValues.map(filterSet => {
|
||||
const newFilterSet = {
|
||||
...filterSet,
|
||||
dataMask: { nativeFilters: { ...dataMaskApplied } },
|
||||
};
|
||||
return filterSetId === filterSet.id ? newFilterSet : filterSet;
|
||||
}),
|
||||
),
|
||||
);
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const foundFilterSet = useMemo(
|
||||
() =>
|
||||
findExistingFilterSet({
|
||||
dataMaskApplied,
|
||||
dataMaskSelected,
|
||||
filterSetFilterValues,
|
||||
}),
|
||||
[dataMaskApplied, dataMaskSelected, filterSetFilterValues],
|
||||
);
|
||||
|
||||
const isDuplicateFilterSet =
|
||||
foundFilterSet && foundFilterSet.id !== filterSetId;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title strong>{t('Editing filter set:')}</Title>
|
||||
<Title>{filterSets[filterSetId].name}</Title>
|
||||
<ActionButtons>
|
||||
<Button
|
||||
ghost
|
||||
buttonStyle="tertiary"
|
||||
buttonSize="small"
|
||||
onClick={onCancel}
|
||||
data-test="filter-set-edit-cancel"
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
title={
|
||||
(isDuplicateFilterSet && t('Filter set already exists')) ||
|
||||
(disabled && APPLY_FILTERS_HINT)
|
||||
}
|
||||
>
|
||||
<ActionButton disabled={disabled || isDuplicateFilterSet}>
|
||||
<Button
|
||||
disabled={disabled || isDuplicateFilterSet}
|
||||
buttonStyle="primary"
|
||||
htmlType="submit"
|
||||
buttonSize="small"
|
||||
onClick={handleSave}
|
||||
data-test="filter-set-edit-save"
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</ActionButtons>
|
||||
{isDuplicateFilterSet && (
|
||||
<Warning mark>
|
||||
<WarningOutlined />
|
||||
{t('This filter set is identical to: "%s"', foundFilterSet?.name)}
|
||||
</Warning>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSection;
|
@ -19,7 +19,7 @@
|
||||
import { Typography, Dropdown, Menu } from 'src/common/components';
|
||||
import React, { FC } from 'react';
|
||||
import { FilterSet } from 'src/dashboard/reducers/types';
|
||||
import { DataMaskUnitWithId } from 'src/dataMask/types';
|
||||
import { DataMaskUnit } from 'src/dataMask/types';
|
||||
import { CheckOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||
import { HandlerFunction, styled, supersetTheme, t } from '@superset-ui/core';
|
||||
import FiltersHeader from './FiltersHeader';
|
||||
@ -46,9 +46,10 @@ type FilterSetUnitProps = {
|
||||
isApplied?: boolean;
|
||||
filterSet?: FilterSet;
|
||||
filterSetName?: string;
|
||||
dataMaskApplied: DataMaskUnitWithId;
|
||||
dataMaskApplied?: DataMaskUnit;
|
||||
setFilterSetName?: (name: string) => void;
|
||||
onDelete?: HandlerFunction;
|
||||
onEdit?: HandlerFunction;
|
||||
};
|
||||
|
||||
const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
||||
@ -56,6 +57,7 @@ const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
||||
editMode,
|
||||
setFilterSetName,
|
||||
onDelete,
|
||||
onEdit,
|
||||
filterSetName,
|
||||
dataMaskApplied,
|
||||
filterSet,
|
||||
@ -63,7 +65,10 @@ const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
||||
}) => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item onClick={onDelete}>{t('Delete')}</Menu.Item>
|
||||
<Menu.Item onClick={onEdit}>{t('Edit')}</Menu.Item>
|
||||
<Menu.Item onClick={onDelete} danger>
|
||||
{t('Delete')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
|
@ -22,9 +22,8 @@ import { HandlerFunction, styled, t } from '@superset-ui/core';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { DataMaskState, DataMaskUnit, MaskWithId } from 'src/dataMask/types';
|
||||
import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { FilterSet } from 'src/dashboard/reducers/types';
|
||||
import { generateFiltersSetId } from './utils';
|
||||
import { findExistingFilterSet, generateFiltersSetId } from './utils';
|
||||
import { Filter } from '../../types';
|
||||
import { useFilters, useDataMask, useFilterSets } from '../state';
|
||||
import Footer from './Footer';
|
||||
@ -68,6 +67,7 @@ const FilterSetUnitWrapper = styled.div<{
|
||||
type FilterSetsProps = {
|
||||
disabled: boolean;
|
||||
dataMaskSelected: DataMaskUnit;
|
||||
onEditFilterSet: (id: string) => void;
|
||||
onFilterSelectionChange: (
|
||||
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
||||
dataMask: Partial<DataMaskState>,
|
||||
@ -78,6 +78,7 @@ const DEFAULT_FILTER_SET_NAME = t('New filter set');
|
||||
|
||||
const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
dataMaskSelected,
|
||||
onEditFilterSet,
|
||||
disabled,
|
||||
onFilterSelectionChange,
|
||||
}) => {
|
||||
@ -93,34 +94,25 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const foundFilterSet = filterSetFilterValues.find(({ dataMask }) => {
|
||||
if (dataMask?.nativeFilters) {
|
||||
return Object.values(dataMask?.nativeFilters).every(
|
||||
filterFromFilterSet => {
|
||||
let currentValueFromFiltersTab =
|
||||
dataMaskApplied[filterFromFilterSet.id]?.currentState ?? {};
|
||||
if (dataMaskSelected[filterFromFilterSet.id]) {
|
||||
currentValueFromFiltersTab =
|
||||
dataMaskSelected[filterFromFilterSet.id]?.currentState;
|
||||
}
|
||||
return areObjectsEqual(
|
||||
filterFromFilterSet.currentState ?? {},
|
||||
currentValueFromFiltersTab,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return false;
|
||||
const foundFilterSet = findExistingFilterSet({
|
||||
dataMaskApplied,
|
||||
dataMaskSelected,
|
||||
filterSetFilterValues,
|
||||
});
|
||||
setSelectedFiltersSetId(foundFilterSet?.id ?? null);
|
||||
}, [dataMaskApplied, dataMaskSelected, filterSetFilterValues]);
|
||||
|
||||
const takeFilterSet = (target: HTMLElement, id: string) => {
|
||||
const takeFilterSet = (id: string, target?: HTMLElement) => {
|
||||
const ignoreSelector = 'ant-collapse-header';
|
||||
if (
|
||||
target.classList.contains(ignoreSelector) ||
|
||||
target.parentElement?.classList.contains(ignoreSelector) ||
|
||||
target.parentElement?.parentElement?.classList.contains(ignoreSelector)
|
||||
target?.classList.contains(ignoreSelector) ||
|
||||
target?.parentElement?.classList.contains(ignoreSelector) ||
|
||||
target?.parentElement?.parentElement?.classList.contains(
|
||||
ignoreSelector,
|
||||
) ||
|
||||
target?.parentElement?.parentElement?.parentElement?.classList.contains(
|
||||
ignoreSelector,
|
||||
)
|
||||
) {
|
||||
// We don't want select filter set when user expand filters
|
||||
return;
|
||||
@ -141,6 +133,11 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
takeFilterSet(id);
|
||||
onEditFilterSet(id);
|
||||
};
|
||||
|
||||
const handleDeleteFilterSets = () => {
|
||||
dispatch(
|
||||
setFilterSetsConfiguration(
|
||||
@ -198,14 +195,14 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
<FilterSetUnitWrapper
|
||||
selected={filterSet.id === selectedFiltersSetId}
|
||||
onClick={(e: MouseEvent<HTMLElement>) =>
|
||||
takeFilterSet(e.target as HTMLElement, filterSet.id)
|
||||
takeFilterSet(filterSet.id, e.target as HTMLElement)
|
||||
}
|
||||
>
|
||||
<FilterSetUnit
|
||||
isApplied={filterSet.id === selectedFiltersSetId && !disabled}
|
||||
onDelete={handleDeleteFilterSets}
|
||||
onEdit={() => handleEdit(filterSet.id)}
|
||||
filters={filters}
|
||||
dataMaskApplied={dataMaskApplied}
|
||||
filterSet={filterSet}
|
||||
/>
|
||||
</FilterSetUnitWrapper>
|
||||
|
@ -19,7 +19,7 @@
|
||||
import React, { FC } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { Collapse, Typography } from 'src/common/components';
|
||||
import { DataMaskUnitWithId } from 'src/dataMask/types';
|
||||
import { DataMaskUnit } from 'src/dataMask/types';
|
||||
import { CaretDownOutlined } from '@ant-design/icons';
|
||||
import { getFilterValueForDisplay } from './utils';
|
||||
import { Filter } from '../../types';
|
||||
@ -54,7 +54,7 @@ const StyledCollapse = styled(Collapse)`
|
||||
|
||||
type FiltersHeaderProps = {
|
||||
filters: Filter[];
|
||||
dataMask: DataMaskUnitWithId;
|
||||
dataMask?: DataMaskUnit;
|
||||
expanded: boolean;
|
||||
};
|
||||
|
||||
@ -84,7 +84,9 @@ const FiltersHeader: FC<FiltersHeaderProps> = ({
|
||||
<div>
|
||||
<Typography.Text strong>{name}: </Typography.Text>
|
||||
<Typography.Text>
|
||||
{getFilterValueForDisplay(dataMask[id]?.currentState?.value) || (
|
||||
{getFilterValueForDisplay(
|
||||
dataMask?.[id]?.currentState?.value,
|
||||
) || (
|
||||
<Typography.Text type="secondary">{t('None')}</Typography.Text>
|
||||
)}
|
||||
</Typography.Text>
|
||||
|
@ -20,6 +20,7 @@ import { t, styled } from '@superset-ui/core';
|
||||
import React, { FC } from 'react';
|
||||
import Button from 'src/components/Button';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import { APPLY_FILTERS_HINT } from './utils';
|
||||
|
||||
type FooterProps = {
|
||||
isApplyDisabled: boolean;
|
||||
@ -32,14 +33,13 @@ type FooterProps = {
|
||||
|
||||
const ActionButton = styled.div<{ disabled: boolean }>`
|
||||
display: flex;
|
||||
padding: 1px;
|
||||
& button {
|
||||
${({ disabled }) => `pointer-events: ${disabled ? 'none' : 'all'}`};
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const ActionButtons = styled.div`
|
||||
export const ActionButtons = styled.div`
|
||||
display: grid;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
@ -48,8 +48,6 @@ const ActionButtons = styled.div`
|
||||
grid-template-columns: 1fr 1fr;
|
||||
`;
|
||||
|
||||
const APPLY_FILTERS = t('Please apply filter changes');
|
||||
|
||||
const Footer: FC<FooterProps> = ({
|
||||
onCancel,
|
||||
editMode,
|
||||
@ -73,7 +71,7 @@ const Footer: FC<FooterProps> = ({
|
||||
placement="bottom"
|
||||
title={
|
||||
(isApplyDisabled && t('Please filter set name')) ||
|
||||
(disabled && APPLY_FILTERS)
|
||||
(disabled && APPLY_FILTERS_HINT)
|
||||
}
|
||||
>
|
||||
<ActionButton disabled={disabled}>
|
||||
@ -91,7 +89,7 @@ const Footer: FC<FooterProps> = ({
|
||||
</Tooltip>
|
||||
</ActionButtons>
|
||||
) : (
|
||||
<Tooltip placement="bottom" title={disabled && APPLY_FILTERS}>
|
||||
<Tooltip placement="bottom" title={disabled && APPLY_FILTERS_HINT}>
|
||||
<ActionButton disabled={disabled}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
|
@ -19,9 +19,14 @@
|
||||
|
||||
import shortid from 'shortid';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { DataMaskUnit } from 'src/dataMask/types';
|
||||
import { FilterSet } from 'src/dashboard/reducers/types';
|
||||
|
||||
export const generateFiltersSetId = () => `FILTERS_SET-${shortid.generate()}`;
|
||||
|
||||
export const APPLY_FILTERS_HINT = t('Please apply filter changes');
|
||||
|
||||
export const getFilterValueForDisplay = (
|
||||
value?: string[] | null | string | number | object,
|
||||
): string => {
|
||||
@ -39,3 +44,32 @@ export const getFilterValueForDisplay = (
|
||||
}
|
||||
return t('Unknown value');
|
||||
};
|
||||
|
||||
export const findExistingFilterSet = ({
|
||||
filterSetFilterValues,
|
||||
dataMaskApplied,
|
||||
dataMaskSelected,
|
||||
}: {
|
||||
filterSetFilterValues: FilterSet[];
|
||||
dataMaskApplied: DataMaskUnit;
|
||||
dataMaskSelected: DataMaskUnit;
|
||||
}) =>
|
||||
filterSetFilterValues.find(({ dataMask }) => {
|
||||
if (dataMask?.nativeFilters) {
|
||||
return Object.values(dataMask?.nativeFilters).every(
|
||||
filterFromFilterSet => {
|
||||
let currentValueFromFiltersTab =
|
||||
dataMaskApplied[filterFromFilterSet.id]?.currentState ?? {};
|
||||
if (dataMaskSelected[filterFromFilterSet.id]) {
|
||||
currentValueFromFiltersTab =
|
||||
dataMaskSelected[filterFromFilterSet.id]?.currentState;
|
||||
}
|
||||
return areObjectsEqual(
|
||||
filterFromFilterSet.currentState ?? {},
|
||||
currentValueFromFiltersTab,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user