mirror of
https://github.com/apache/superset.git
synced 2024-09-16 02:29:39 -04:00
feat(filter-set): Filter set edge cases (#13576)
* 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 * feat: filter set edge cases * lint: fix lint * lint: fix TS * refactor: fix CR notes * fix: fix CR notes * fix: fix CR notes Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
parent
ae66f5fa78
commit
a35825de7b
@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { HandlerFunction, styled, t } from '@superset-ui/core';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import cx from 'classnames';
|
||||
@ -52,6 +52,11 @@ const BarWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const Bar = styled.div`
|
||||
& .ant-typography-edit-content {
|
||||
left: 0;
|
||||
margin-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@ -169,6 +174,11 @@ interface FiltersBarProps {
|
||||
directPathToChild?: string[];
|
||||
}
|
||||
|
||||
enum TabIds {
|
||||
AllFilters = 'allFilters',
|
||||
FilterSets = 'filterSets',
|
||||
}
|
||||
|
||||
const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
filtersOpen,
|
||||
toggleFiltersBar,
|
||||
@ -183,8 +193,10 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
const dispatch = useDispatch();
|
||||
const filterSets = useFilterSets();
|
||||
const filterSetFilterValues = Object.values(filterSets);
|
||||
const [isFilterSetChanged, setIsFilterSetChanged] = useState(false);
|
||||
const [tab, setTab] = useState(TabIds.AllFilters);
|
||||
const filters = useFilters();
|
||||
const filterValues = Object.values(filters);
|
||||
const filterValues = Object.values<Filter>(filters);
|
||||
const dataMaskApplied = useDataMask();
|
||||
const canEdit = useSelector<any, boolean>(
|
||||
({ dashboardInfo }) => dashboardInfo.dash_edit_perm,
|
||||
@ -212,8 +224,8 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
}
|
||||
const areFiltersInitialized = filterValues.every(filterValue =>
|
||||
areObjectsEqual(
|
||||
filterValue.defaultValue,
|
||||
dataMaskSelected[filterValue.id]?.currentState?.value,
|
||||
filterValue?.defaultValue,
|
||||
dataMaskSelected[filterValue?.id]?.currentState?.value,
|
||||
),
|
||||
);
|
||||
if (areFiltersInitialized) {
|
||||
@ -245,6 +257,7 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
filter: Pick<Filter, 'id'> & Partial<Filter>,
|
||||
dataMask: Partial<DataMaskState>,
|
||||
) => {
|
||||
setIsFilterSetChanged(tab !== TabIds.AllFilters);
|
||||
setDataMaskSelected(draft => {
|
||||
const children = cascadeChildren[filter.id] || [];
|
||||
// force instant updating on initialization or for parent filters
|
||||
@ -338,12 +351,13 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
{isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET) ? (
|
||||
<StyledTabs
|
||||
centered
|
||||
defaultActiveKey="allFilters"
|
||||
activeKey={editFilterSetId ? 'allFilters' : undefined}
|
||||
onChange={setTab as HandlerFunction}
|
||||
defaultActiveKey={TabIds.AllFilters}
|
||||
activeKey={editFilterSetId ? TabIds.AllFilters : undefined}
|
||||
>
|
||||
<Tabs.TabPane
|
||||
tab={t(`All Filters (${filterValues.length})`)}
|
||||
key="allFilters"
|
||||
key={TabIds.AllFilters}
|
||||
>
|
||||
{editFilterSetId && (
|
||||
<EditSection
|
||||
@ -358,12 +372,13 @@ const FilterBar: React.FC<FiltersBarProps> = ({
|
||||
<Tabs.TabPane
|
||||
disabled={!!editFilterSetId}
|
||||
tab={t(`Filter Sets (${filterSetFilterValues.length})`)}
|
||||
key="filterSets"
|
||||
key={TabIds.FilterSets}
|
||||
>
|
||||
<FilterSets
|
||||
onEditFilterSet={setEditFilterSetId}
|
||||
disabled={!isApplyDisabled}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
isFilterSetChanged={isFilterSetChanged}
|
||||
onFilterSelectionChange={handleFilterSelectionChange}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { HandlerFunction, styled, t } from '@superset-ui/core';
|
||||
import { Typography, Tooltip } from 'src/common/components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@ -25,8 +25,9 @@ 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 { useDataMask, useFilters, useFilterSets } from '../state';
|
||||
import { APPLY_FILTERS_HINT, findExistingFilterSet } from './utils';
|
||||
import { useFilterSetNameDuplicated } from './state';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: grid;
|
||||
@ -73,13 +74,26 @@ const EditSection: FC<EditSectionProps> = ({
|
||||
const dataMaskApplied = useDataMask();
|
||||
const dispatch = useDispatch();
|
||||
const filterSets = useFilterSets();
|
||||
const filters = useFilters();
|
||||
const filterSetFilterValues = Object.values(filterSets);
|
||||
|
||||
const [filterSetName, setFilterSetName] = useState(
|
||||
filterSets[filterSetId].name,
|
||||
);
|
||||
|
||||
const isFilterSetNameDuplicated = useFilterSetNameDuplicated(
|
||||
filterSetName,
|
||||
filterSets[filterSetId].name,
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
dispatch(
|
||||
setFilterSetsConfiguration(
|
||||
filterSetFilterValues.map(filterSet => {
|
||||
const newFilterSet = {
|
||||
...filterSet,
|
||||
name: filterSetName,
|
||||
nativeFilters: filters,
|
||||
dataMask: { nativeFilters: { ...dataMaskApplied } },
|
||||
};
|
||||
return filterSetId === filterSet.id ? newFilterSet : filterSet;
|
||||
@ -92,20 +106,30 @@ const EditSection: FC<EditSectionProps> = ({
|
||||
const foundFilterSet = useMemo(
|
||||
() =>
|
||||
findExistingFilterSet({
|
||||
dataMaskApplied,
|
||||
dataMaskSelected,
|
||||
filterSetFilterValues,
|
||||
}),
|
||||
[dataMaskApplied, dataMaskSelected, filterSetFilterValues],
|
||||
[dataMaskSelected, filterSetFilterValues],
|
||||
);
|
||||
|
||||
const isDuplicateFilterSet =
|
||||
foundFilterSet && foundFilterSet.id !== filterSetId;
|
||||
|
||||
const resultDisabled =
|
||||
disabled || isDuplicateFilterSet || isFilterSetNameDuplicated;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Title strong>{t('Editing filter set:')}</Title>
|
||||
<Title>{filterSets[filterSetId].name}</Title>
|
||||
<Title
|
||||
editable={{
|
||||
editing: true,
|
||||
icon: <span />,
|
||||
onChange: setFilterSetName,
|
||||
}}
|
||||
>
|
||||
{filterSetName}
|
||||
</Title>
|
||||
<ActionButtons>
|
||||
<Button
|
||||
ghost
|
||||
@ -117,15 +141,17 @@ const EditSection: FC<EditSectionProps> = ({
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="top"
|
||||
placement="right"
|
||||
title={
|
||||
(isFilterSetNameDuplicated &&
|
||||
t('Filter set with this name already exists')) ||
|
||||
(isDuplicateFilterSet && t('Filter set already exists')) ||
|
||||
(disabled && APPLY_FILTERS_HINT)
|
||||
}
|
||||
>
|
||||
<ActionButton disabled={disabled || isDuplicateFilterSet}>
|
||||
<ActionButton disabled={resultDisabled}>
|
||||
<Button
|
||||
disabled={disabled || isDuplicateFilterSet}
|
||||
disabled={resultDisabled}
|
||||
buttonStyle="primary"
|
||||
htmlType="submit"
|
||||
buttonSize="small"
|
||||
|
@ -22,8 +22,8 @@ import { FilterSet } from 'src/dashboard/reducers/types';
|
||||
import { DataMaskUnit } from 'src/dataMask/types';
|
||||
import { CheckOutlined, EllipsisOutlined } from '@ant-design/icons';
|
||||
import { HandlerFunction, styled, supersetTheme, t } from '@superset-ui/core';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import FiltersHeader from './FiltersHeader';
|
||||
import { Filter } from '../../types';
|
||||
|
||||
const TitleText = styled.div`
|
||||
display: flex;
|
||||
@ -41,36 +41,42 @@ const IconsBlock = styled.div`
|
||||
`;
|
||||
|
||||
type FilterSetUnitProps = {
|
||||
filters: Filter[];
|
||||
editMode?: boolean;
|
||||
isApplied?: boolean;
|
||||
filterSet?: FilterSet;
|
||||
filterSetName?: string;
|
||||
dataMaskApplied?: DataMaskUnit;
|
||||
dataMaskSelected?: DataMaskUnit;
|
||||
setFilterSetName?: (name: string) => void;
|
||||
onDelete?: HandlerFunction;
|
||||
onEdit?: HandlerFunction;
|
||||
onRebuild?: HandlerFunction;
|
||||
};
|
||||
|
||||
const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
||||
filters,
|
||||
editMode,
|
||||
setFilterSetName,
|
||||
onDelete,
|
||||
onEdit,
|
||||
filterSetName,
|
||||
dataMaskApplied,
|
||||
dataMaskSelected,
|
||||
filterSet,
|
||||
isApplied,
|
||||
onRebuild,
|
||||
}) => {
|
||||
const menu = (
|
||||
<Menu>
|
||||
<Menu.Item onClick={onEdit}>{t('Edit')}</Menu.Item>
|
||||
<Menu.Item onClick={onRebuild}>
|
||||
<Tooltip placement="right" title={t('Remove invalid filters')}>
|
||||
{t('Rebuild')}
|
||||
</Tooltip>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={onDelete} danger>
|
||||
{t('Delete')}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleText>
|
||||
@ -107,9 +113,8 @@ const FilterSetUnit: FC<FilterSetUnitProps> = ({
|
||||
</IconsBlock>
|
||||
</TitleText>
|
||||
<FiltersHeader
|
||||
expanded={!filterSet}
|
||||
dataMask={filterSet?.dataMask?.nativeFilters ?? dataMaskApplied}
|
||||
filters={filters}
|
||||
filterSet={filterSet}
|
||||
dataMask={filterSet?.dataMask?.nativeFilters ?? dataMaskSelected}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -22,7 +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 { FilterSet } from 'src/dashboard/reducers/types';
|
||||
import { Filters, FilterSet, FilterSets } from 'src/dashboard/reducers/types';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { findExistingFilterSet, generateFiltersSetId } from './utils';
|
||||
import { Filter } from '../../types';
|
||||
import { useFilters, useDataMask, useFilterSets } from '../state';
|
||||
@ -40,10 +41,6 @@ const FilterSetsWrapper = styled.div`
|
||||
& input {
|
||||
width: 100%;
|
||||
}
|
||||
& .ant-typography-edit-content {
|
||||
left: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const FilterSetUnitWrapper = styled.div<{
|
||||
@ -66,6 +63,7 @@ const FilterSetUnitWrapper = styled.div<{
|
||||
|
||||
type FilterSetsProps = {
|
||||
disabled: boolean;
|
||||
isFilterSetChanged: boolean;
|
||||
dataMaskSelected: DataMaskUnit;
|
||||
onEditFilterSet: (id: string) => void;
|
||||
onFilterSelectionChange: (
|
||||
@ -81,6 +79,7 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
onEditFilterSet,
|
||||
disabled,
|
||||
onFilterSelectionChange,
|
||||
isFilterSetChanged,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME);
|
||||
@ -88,30 +87,42 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
const dataMaskApplied = useDataMask();
|
||||
const filterSets = useFilterSets();
|
||||
const filterSetFilterValues = Object.values(filterSets);
|
||||
const filters = Object.values(useFilters());
|
||||
const filters = useFilters();
|
||||
const filterValues = Object.values(filters) as Filter[];
|
||||
const [selectedFiltersSetId, setSelectedFiltersSetId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFilterSetChanged) {
|
||||
return;
|
||||
}
|
||||
const foundFilterSet = findExistingFilterSet({
|
||||
dataMaskApplied,
|
||||
dataMaskSelected,
|
||||
filterSetFilterValues,
|
||||
});
|
||||
setSelectedFiltersSetId(foundFilterSet?.id ?? null);
|
||||
}, [dataMaskApplied, dataMaskSelected, filterSetFilterValues]);
|
||||
}, [isFilterSetChanged, dataMaskSelected, filterSetFilterValues]);
|
||||
|
||||
const isFilterMissingOrContainsInvalidMetadata = (
|
||||
id: string,
|
||||
filterSet?: FilterSet,
|
||||
) =>
|
||||
!filterValues.find(filter => filter?.id === id) ||
|
||||
!areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id]);
|
||||
|
||||
const takeFilterSet = (id: string, target?: HTMLElement) => {
|
||||
const ignoreSelector = 'ant-collapse-header';
|
||||
const ignoreSelectorHeader = 'ant-collapse-header';
|
||||
const ignoreSelectorDropdown = 'ant-dropdown-menu-item';
|
||||
if (
|
||||
target?.classList.contains(ignoreSelector) ||
|
||||
target?.parentElement?.classList.contains(ignoreSelector) ||
|
||||
target?.classList.contains(ignoreSelectorHeader) ||
|
||||
target?.classList.contains(ignoreSelectorDropdown) ||
|
||||
target?.parentElement?.classList.contains(ignoreSelectorHeader) ||
|
||||
target?.parentElement?.parentElement?.classList.contains(
|
||||
ignoreSelector,
|
||||
ignoreSelectorHeader,
|
||||
) ||
|
||||
target?.parentElement?.parentElement?.parentElement?.classList.contains(
|
||||
ignoreSelector,
|
||||
ignoreSelectorHeader,
|
||||
)
|
||||
) {
|
||||
// We don't want select filter set when user expand filters
|
||||
@ -121,10 +132,15 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const filtersSet = filterSets[id];
|
||||
Object.values(filtersSet.dataMask?.nativeFilters ?? []).forEach(
|
||||
|
||||
const filterSet = filterSets[id];
|
||||
|
||||
Object.values(filterSet?.dataMask?.nativeFilters ?? []).forEach(
|
||||
dataMask => {
|
||||
const { extraFormData, currentState, id } = dataMask as MaskWithId;
|
||||
if (isFilterMissingOrContainsInvalidMetadata(id, filterSet)) {
|
||||
return;
|
||||
}
|
||||
onFilterSelectionChange(
|
||||
{ id },
|
||||
{ nativeFilters: { extraFormData, currentState } },
|
||||
@ -133,21 +149,55 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const handleRebuild = (id: string) => {
|
||||
const filterSet = filterSets[id];
|
||||
// We need remove invalid filters from filter set
|
||||
const newFilters = Object.values(filterSet?.dataMask?.nativeFilters ?? [])
|
||||
.filter(dataMask => {
|
||||
const { id } = dataMask as MaskWithId;
|
||||
return !isFilterMissingOrContainsInvalidMetadata(id, filterSet);
|
||||
})
|
||||
.reduce((prev, next) => ({ ...prev, [next.id]: filters[next.id] }), {});
|
||||
|
||||
const updatedFilterSet: FilterSet = {
|
||||
...filterSet,
|
||||
nativeFilters: newFilters as Filters,
|
||||
dataMask: {
|
||||
nativeFilters: Object.keys(newFilters).reduce(
|
||||
(prev, nextFilterId) => ({
|
||||
...prev,
|
||||
[nextFilterId]: filterSet.dataMask?.nativeFilters?.[nextFilterId],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
};
|
||||
dispatch(
|
||||
setFilterSetsConfiguration(
|
||||
filterSetFilterValues.map(filterSetIt => {
|
||||
const isEquals = filterSetIt.id === updatedFilterSet.id;
|
||||
return isEquals ? updatedFilterSet : filterSetIt;
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
takeFilterSet(id);
|
||||
onEditFilterSet(id);
|
||||
};
|
||||
|
||||
const handleDeleteFilterSets = () => {
|
||||
const handleDeleteFilterSet = (filterSetId: string) => {
|
||||
dispatch(
|
||||
setFilterSetsConfiguration(
|
||||
filterSetFilterValues.filter(
|
||||
filtersSet => filtersSet.id !== selectedFiltersSetId,
|
||||
filtersSet => filtersSet.id !== filterSetId,
|
||||
),
|
||||
),
|
||||
);
|
||||
setFilterSetName(DEFAULT_FILTER_SET_NAME);
|
||||
setSelectedFiltersSetId(null);
|
||||
if (filterSetId === selectedFiltersSetId) {
|
||||
setSelectedFiltersSetId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
@ -159,8 +209,15 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
const newFilterSet: FilterSet = {
|
||||
name: filterSetName.trim(),
|
||||
id: generateFiltersSetId(),
|
||||
nativeFilters: filters,
|
||||
dataMask: {
|
||||
nativeFilters: dataMaskApplied,
|
||||
nativeFilters: Object.keys(filters).reduce(
|
||||
(prev, nextFilterId) => ({
|
||||
...prev,
|
||||
[nextFilterId]: dataMaskApplied[nextFilterId],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
};
|
||||
dispatch(
|
||||
@ -175,14 +232,13 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
{!selectedFiltersSetId && (
|
||||
<FilterSetUnitWrapper>
|
||||
<FilterSetUnit
|
||||
filters={filters}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
editMode={editMode}
|
||||
setFilterSetName={setFilterSetName}
|
||||
filterSetName={filterSetName}
|
||||
dataMaskApplied={dataMaskApplied}
|
||||
/>
|
||||
<Footer
|
||||
isApplyDisabled={!filterSetName.trim()}
|
||||
filterSetName={filterSetName.trim()}
|
||||
disabled={disabled}
|
||||
onCancel={handleCancel}
|
||||
editMode={editMode}
|
||||
@ -200,9 +256,10 @@ const FilterSets: React.FC<FilterSetsProps> = ({
|
||||
>
|
||||
<FilterSetUnit
|
||||
isApplied={filterSet.id === selectedFiltersSetId && !disabled}
|
||||
onDelete={handleDeleteFilterSets}
|
||||
onDelete={() => handleDeleteFilterSet(filterSet.id)}
|
||||
onEdit={() => handleEdit(filterSet.id)}
|
||||
filters={filters}
|
||||
onRebuild={() => handleRebuild(filterSet.id)}
|
||||
dataMaskSelected={dataMaskSelected}
|
||||
filterSet={filterSet}
|
||||
/>
|
||||
</FilterSetUnitWrapper>
|
||||
|
@ -18,11 +18,13 @@
|
||||
*/
|
||||
import React, { FC } from 'react';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import { Collapse, Typography } from 'src/common/components';
|
||||
import { Collapse, Typography, Tooltip } from 'src/common/components';
|
||||
import { DataMaskUnit } from 'src/dataMask/types';
|
||||
import { CaretDownOutlined } from '@ant-design/icons';
|
||||
import { areObjectsEqual } from 'src/reduxUtils';
|
||||
import { FilterSet } from 'src/dashboard/reducers/types';
|
||||
import { getFilterValueForDisplay } from './utils';
|
||||
import { Filter } from '../../types';
|
||||
import { useFilters } from '../state';
|
||||
|
||||
const FilterHeader = styled.div`
|
||||
display: flex;
|
||||
@ -53,45 +55,70 @@ const StyledCollapse = styled(Collapse)`
|
||||
`;
|
||||
|
||||
type FiltersHeaderProps = {
|
||||
filters: Filter[];
|
||||
dataMask?: DataMaskUnit;
|
||||
expanded: boolean;
|
||||
filterSet?: FilterSet;
|
||||
};
|
||||
|
||||
const FiltersHeader: FC<FiltersHeaderProps> = ({
|
||||
filters,
|
||||
dataMask,
|
||||
expanded,
|
||||
}) => {
|
||||
const FiltersHeader: FC<FiltersHeaderProps> = ({ dataMask, filterSet }) => {
|
||||
const filters = useFilters();
|
||||
const filterValues = Object.values(filters);
|
||||
|
||||
let resultFilters = filterValues ?? [];
|
||||
if (filterSet?.nativeFilters) {
|
||||
resultFilters = Object.values(filterSet?.nativeFilters);
|
||||
}
|
||||
|
||||
const getFiltersHeader = () => (
|
||||
<FilterHeader>
|
||||
<Typography.Text type="secondary">
|
||||
{t('Filters (%d)', filters.length)}
|
||||
{t('Filters (%d)', resultFilters.length)}
|
||||
</Typography.Text>
|
||||
</FilterHeader>
|
||||
);
|
||||
|
||||
const getFilterRow = ({ id, name }: { id: string; name: string }) => {
|
||||
const changedFilter =
|
||||
filterSet &&
|
||||
!areObjectsEqual(filters[id], filterSet?.nativeFilters?.[id]);
|
||||
const removedFilter = !Object.keys(filters).includes(id);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
(removedFilter &&
|
||||
t(
|
||||
"This filter doesn't exist in dashboard. It will not be applied.",
|
||||
)) ||
|
||||
(changedFilter &&
|
||||
t('Filter metadata changed in dashboard. It will not be applied.'))
|
||||
}
|
||||
placement="bottomLeft"
|
||||
>
|
||||
<div>
|
||||
<Typography.Text strong delete={removedFilter} mark={changedFilter}>
|
||||
{name}:
|
||||
</Typography.Text>
|
||||
<Typography.Text delete={removedFilter} mark={changedFilter}>
|
||||
{getFilterValueForDisplay(dataMask?.[id]?.currentState?.value) || (
|
||||
<Typography.Text type="secondary">{t('None')}</Typography.Text>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledCollapse
|
||||
ghost
|
||||
expandIconPosition="right"
|
||||
defaultActiveKey={expanded ? ['filters'] : undefined}
|
||||
defaultActiveKey={!filterSet ? ['filters'] : undefined}
|
||||
expandIcon={({ isActive }: { isActive: boolean }) => (
|
||||
<CaretDownOutlined rotate={isActive ? 0 : 180} />
|
||||
)}
|
||||
>
|
||||
<Collapse.Panel header={getFiltersHeader()} key="filters">
|
||||
{filters.map(({ id, name }) => (
|
||||
<div>
|
||||
<Typography.Text strong>{name}: </Typography.Text>
|
||||
<Typography.Text>
|
||||
{getFilterValueForDisplay(
|
||||
dataMask?.[id]?.currentState?.value,
|
||||
) || (
|
||||
<Typography.Text type="secondary">{t('None')}</Typography.Text>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
{resultFilters.map(getFilterRow)}
|
||||
</Collapse.Panel>
|
||||
</StyledCollapse>
|
||||
);
|
||||
|
@ -21,9 +21,10 @@ import React, { FC } from 'react';
|
||||
import Button from 'src/components/Button';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import { APPLY_FILTERS_HINT } from './utils';
|
||||
import { useFilterSetNameDuplicated } from './state';
|
||||
|
||||
type FooterProps = {
|
||||
isApplyDisabled: boolean;
|
||||
filterSetName: string;
|
||||
disabled: boolean;
|
||||
editMode: boolean;
|
||||
onCancel: () => void;
|
||||
@ -54,56 +55,65 @@ const Footer: FC<FooterProps> = ({
|
||||
onEdit,
|
||||
onCreate,
|
||||
disabled,
|
||||
isApplyDisabled,
|
||||
}) => (
|
||||
<>
|
||||
{editMode ? (
|
||||
<ActionButtons>
|
||||
<Button
|
||||
buttonStyle="tertiary"
|
||||
buttonSize="small"
|
||||
onClick={onCancel}
|
||||
data-test="filter-set-cancel-button"
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
title={
|
||||
(isApplyDisabled && t('Please filter set name')) ||
|
||||
(disabled && APPLY_FILTERS_HINT)
|
||||
}
|
||||
>
|
||||
filterSetName,
|
||||
}) => {
|
||||
const isFilterSetNameDuplicated = useFilterSetNameDuplicated(filterSetName);
|
||||
|
||||
const isCreateDisabled =
|
||||
!filterSetName || isFilterSetNameDuplicated || disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
{editMode ? (
|
||||
<ActionButtons>
|
||||
<Button
|
||||
buttonStyle="tertiary"
|
||||
buttonSize="small"
|
||||
onClick={onCancel}
|
||||
data-test="filter-set-cancel-button"
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Tooltip
|
||||
placement="right"
|
||||
title={
|
||||
(!filterSetName && t('Please filter set name')) ||
|
||||
(isFilterSetNameDuplicated &&
|
||||
t('Filter set with this name already exists')) ||
|
||||
(disabled && APPLY_FILTERS_HINT)
|
||||
}
|
||||
>
|
||||
<ActionButton disabled={isCreateDisabled}>
|
||||
<Button
|
||||
disabled={isCreateDisabled}
|
||||
buttonStyle="primary"
|
||||
htmlType="submit"
|
||||
buttonSize="small"
|
||||
onClick={onCreate}
|
||||
data-test="filter-set-create-button"
|
||||
>
|
||||
{t('Create')}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</ActionButtons>
|
||||
) : (
|
||||
<Tooltip placement="bottom" title={disabled && APPLY_FILTERS_HINT}>
|
||||
<ActionButton disabled={disabled}>
|
||||
<Button
|
||||
disabled={isApplyDisabled || disabled}
|
||||
buttonStyle="primary"
|
||||
htmlType="submit"
|
||||
disabled={disabled}
|
||||
buttonStyle="tertiary"
|
||||
buttonSize="small"
|
||||
onClick={onCreate}
|
||||
data-test="filter-set-create-button"
|
||||
data-test="filter-set-create-new-button"
|
||||
onClick={onEdit}
|
||||
>
|
||||
{t('Create')}
|
||||
{t('Create new filter set')}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</ActionButtons>
|
||||
) : (
|
||||
<Tooltip placement="bottom" title={disabled && APPLY_FILTERS_HINT}>
|
||||
<ActionButton disabled={disabled}>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
buttonStyle="tertiary"
|
||||
buttonSize="small"
|
||||
data-test="filter-set-create-new-button"
|
||||
onClick={onEdit}
|
||||
>
|
||||
{t('Create new filter set')}
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useFilterSets } from '../state';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const useFilterSetNameDuplicated = (
|
||||
filterSetName: string,
|
||||
ignoreName?: string,
|
||||
) => {
|
||||
const filterSets = useFilterSets();
|
||||
const filterSetFilterValues = Object.values(filterSets);
|
||||
const isFilterSetNameDuplicated = useMemo(
|
||||
() => !!filterSetFilterValues.find(({ name }) => name === filterSetName),
|
||||
[filterSetFilterValues, filterSetName],
|
||||
);
|
||||
if (ignoreName === filterSetName) {
|
||||
return false;
|
||||
}
|
||||
return isFilterSetNameDuplicated;
|
||||
};
|
@ -47,28 +47,22 @@ export const getFilterValueForDisplay = (
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
filterSetFilterValues.find(({ dataMask: dataMaskFromFilterSet }) => {
|
||||
if (dataMaskFromFilterSet?.nativeFilters) {
|
||||
const dataMaskSelectedEntries = Object.entries(dataMaskSelected);
|
||||
return dataMaskSelectedEntries.every(
|
||||
([id, filterFromSelectedFilters]) =>
|
||||
areObjectsEqual(
|
||||
filterFromSelectedFilters.currentState,
|
||||
dataMaskFromFilterSet?.nativeFilters?.[id]?.currentState,
|
||||
) &&
|
||||
dataMaskSelectedEntries.length ===
|
||||
Object.keys(dataMaskFromFilterSet?.nativeFilters ?? {}).length,
|
||||
);
|
||||
}
|
||||
return false;
|
||||
|
@ -70,7 +70,7 @@ const FilterValue: React.FC<FilterProps> = ({
|
||||
groupby,
|
||||
inputRef,
|
||||
});
|
||||
if (!areObjectsEqual(formData || {}, newFormData)) {
|
||||
if (!areObjectsEqual(formData, newFormData)) {
|
||||
setFormData(newFormData);
|
||||
if (!hasDataSource) {
|
||||
return;
|
||||
|
@ -18,12 +18,12 @@
|
||||
*/
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Filters,
|
||||
FilterSets as FilterSetsType,
|
||||
NativeFiltersState,
|
||||
} from 'src/dashboard/reducers/types';
|
||||
import { DataMaskUnitWithId } from 'src/dataMask/types';
|
||||
import { mergeExtraFormData } from '../utils';
|
||||
import { Filter } from '../types';
|
||||
|
||||
export const useFilterSets = () =>
|
||||
useSelector<any, FilterSetsType>(
|
||||
@ -31,7 +31,7 @@ export const useFilterSets = () =>
|
||||
);
|
||||
|
||||
export const useFilters = () =>
|
||||
useSelector<any, Filter>(state => state.nativeFilters.filters);
|
||||
useSelector<any, Filters>(state => state.nativeFilters.filters);
|
||||
|
||||
export const useDataMask = () =>
|
||||
useSelector<any, DataMaskUnitWithId>(state => state.dataMask.nativeFilters);
|
||||
|
@ -267,7 +267,7 @@ export const FiltersConfigForm: React.FC<FiltersConfigFormProps> = ({
|
||||
/>
|
||||
<StyledCheckboxFormItem
|
||||
name={['filters', filterId, 'isInstant']}
|
||||
initialValue={filterToEdit?.isInstant}
|
||||
initialValue={filterToEdit?.isInstant || false}
|
||||
valuePropName="checked"
|
||||
colon={false}
|
||||
>
|
||||
|
@ -70,6 +70,7 @@ export type LayoutItem = {
|
||||
export type FilterSet = {
|
||||
id: string;
|
||||
name: string;
|
||||
nativeFilters: Filters;
|
||||
dataMask: Partial<DataMaskStateWithId>;
|
||||
};
|
||||
|
||||
|
@ -16,16 +16,16 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { MaskWithId } from './types';
|
||||
import { DataMaskType, MaskWithId } from './types';
|
||||
import { FilterConfiguration } from '../dashboard/components/nativeFilters/types';
|
||||
|
||||
export const UPDATE_DATA_MASK = 'UPDATE_DATA_MASK';
|
||||
export interface UpdateDataMask {
|
||||
type: typeof UPDATE_DATA_MASK;
|
||||
filterId: string;
|
||||
nativeFilters?: Omit<MaskWithId, 'id'>;
|
||||
crossFilters?: Omit<MaskWithId, 'id'>;
|
||||
ownFilters?: Omit<MaskWithId, 'id'>;
|
||||
[DataMaskType.NativeFilters]?: Omit<MaskWithId, 'id'>;
|
||||
[DataMaskType.CrossFilters]?: Omit<MaskWithId, 'id'>;
|
||||
[DataMaskType.OwnFilters]?: Omit<MaskWithId, 'id'>;
|
||||
}
|
||||
|
||||
export const SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE =
|
||||
|
@ -20,7 +20,7 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
// <- When we work with Immer, we need reassign, so disabling lint
|
||||
import produce from 'immer';
|
||||
import { MaskWithId, DataMaskType, DataMaskStateWithId } from './types';
|
||||
import { MaskWithId, DataMaskType, DataMaskStateWithId, Mask } from './types';
|
||||
import {
|
||||
AnyDataMaskAction,
|
||||
SET_DATA_MASK_FOR_FILTER_CONFIG_COMPLETE,
|
||||
@ -43,8 +43,7 @@ const setUnitDataMask = (
|
||||
) => {
|
||||
if (action[unitName]) {
|
||||
dataMaskState[unitName][action.filterId] = {
|
||||
...dataMaskState[unitName][action.filterId],
|
||||
...action[unitName],
|
||||
...(action[unitName] as Mask),
|
||||
id: action.filterId,
|
||||
};
|
||||
}
|
||||
|
@ -169,9 +169,6 @@ export function areArraysShallowEqual(arr1: unknown[], arr2: unknown[]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areObjectsEqual(
|
||||
obj1: Record<string, any>,
|
||||
obj2: Record<string, any>,
|
||||
) {
|
||||
export function areObjectsEqual(obj1: any, obj2: any) {
|
||||
return isEqual(obj1, obj2);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user