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:
simcha90 2021-03-10 15:43:24 +02:00 committed by GitHub
parent 1d1a1cdc20
commit d509b157fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 241 additions and 41 deletions

View File

@ -30,6 +30,7 @@ export interface ButtonProps {
id?: string;
className?: string;
tooltip?: string;
ghost?: boolean;
placement?:
| 'bottom'
| 'left'

View File

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

View File

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

View File

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

View File

@ -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 (

View File

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

View File

@ -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}:&nbsp;</Typography.Text>
<Typography.Text>
{getFilterValueForDisplay(dataMask[id]?.currentState?.value) || (
{getFilterValueForDisplay(
dataMask?.[id]?.currentState?.value,
) || (
<Typography.Text type="secondary">{t('None')}</Typography.Text>
)}
</Typography.Text>

View File

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

View File

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