diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index e963ef23d0131..5dd8aff418961 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -59,8 +59,8 @@ "fontsource-inter": "^3.0.5", "geolib": "^2.0.24", "global-box": "^1.2.0", - "immer": "^8.0.1", "html-webpack-plugin": "^4.5.1", + "immer": "^8.0.1", "immutable": "^4.0.0-rc.12", "interweave": "^11.2.0", "jquery": "^3.5.1", diff --git a/superset-frontend/src/common/components/index.tsx b/superset-frontend/src/common/components/index.tsx index bc0eb71a055f3..c3ce3f39e8cbc 100644 --- a/superset-frontend/src/common/components/index.tsx +++ b/superset-frontend/src/common/components/index.tsx @@ -58,6 +58,7 @@ export { FormInstance } from 'antd/lib/form'; export { RadioChangeEvent } from 'antd/lib/radio'; export { default as Badge } from './Badge'; +export { default as Collapse } from './Collapse'; export { default as Progress } from './ProgressBar'; export const MenuItem = styled(AntdMenu.Item)` diff --git a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx index c68477daec2dd..0cdfffee78e52 100644 --- a/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx +++ b/superset-frontend/src/dashboard/components/FiltersBadge/DetailsPanel.tsx @@ -37,6 +37,7 @@ import { FilterValue, } from './Styles'; import { Indicator } from './selectors'; +import { getFilterValueForDisplay } from '../nativeFilters/FilterBar/FilterSets/utils'; export interface IndicatorProps { indicator: Indicator; @@ -46,18 +47,21 @@ export interface IndicatorProps { const Indicator = ({ indicator: { column, name, value = [], path }, onClick, -}: IndicatorProps) => ( - onClick([...path, `LABEL-${column}`])}> - - <ItemIcon> - <SearchOutlined /> - </ItemIcon> - {name} - {value.length ? ': ' : ''} - - {value.length ? value.join(', ') : ''} - -); +}: IndicatorProps) => { + const resultValue = getFilterValueForDisplay(value); + return ( + onClick([...path, `LABEL-${column}`])}> + + <ItemIcon> + <SearchOutlined /> + </ItemIcon> + {name} + {resultValue ? ': ' : ''} + + {resultValue} + + ); +}; export interface DetailsPanelProps { appliedIndicators: Indicator[]; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx index 92ea57ba5243a..f2964a122a39b 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.tsx @@ -36,11 +36,11 @@ import { useImmer } from 'use-immer'; import { getInitialMask } from 'src/dataMask/reducer'; import { areObjectsEqual } from 'src/reduxUtils'; import FilterConfigurationLink from './FilterConfigurationLink'; -import { useFilterConfiguration } from '../state'; import { Filter } from '../types'; import { buildCascadeFiltersTree, mapParentFiltersToChildren } from './utils'; import CascadePopover from './CascadePopover'; -import FilterSets from './FilterSets'; +import FilterSets from './FilterSets/FilterSets'; +import { useFilters, useFilterSets } from './state'; const barWidth = `250px`; @@ -128,14 +128,6 @@ const TitleArea = styled.h4` & > span { flex-grow: 1; } - - & :not(:first-child) { - margin-left: ${({ theme }) => theme.gridUnit}px; - - &:hover { - cursor: pointer; - } - } `; const StyledTabs = styled(Tabs)` @@ -153,6 +145,9 @@ const StyledTabs = styled(Tabs)` const ActionButtons = styled.div` display: grid; flex-direction: row; + justify-content: center; + align-items: center; + grid-gap: 10px; grid-template-columns: 1fr 1fr; ${({ theme }) => `padding: 0 ${theme.gridUnit * 2}px ${theme.gridUnit * 2}px`}; @@ -164,6 +159,9 @@ const ActionButtons = styled.div` const FilterControls = styled.div` padding: 0 ${({ theme }) => theme.gridUnit * 4}px; + &:hover { + cursor: pointer; + } `; interface FiltersBarProps { @@ -183,48 +181,67 @@ const FilterBar: React.FC = ({ setLastAppliedFilterData, ] = useImmer({}); const dispatch = useDispatch(); + const filterSets = useFilterSets(); + const filterSetsArray = Object.values(filterSets); + const filters = useFilters(); + const filtersArray = Object.values(filters); const dataMaskState = useSelector( state => state.dataMask.nativeFilters ?? {}, ); - const filterConfigs = useFilterConfiguration(); const canEdit = useSelector( ({ dashboardInfo }) => dashboardInfo.dash_edit_perm, ); const [visiblePopoverId, setVisiblePopoverId] = useState(null); const [isInitialized, setIsInitialized] = useState(false); + const handleApply = () => { + const filterIds = Object.keys(filterData); + filterIds.forEach(filterId => { + if (filterData[filterId]) { + dispatch( + updateDataMask(filterId, { + nativeFilters: filterData[filterId], + }), + ); + } + }); + setLastAppliedFilterData(() => filterData); + }; + useEffect(() => { if (isInitialized) { return; } - const areFiltersInitialized = filterConfigs.every( - filterConfig => - filterConfig.defaultValue === + const areFiltersInitialized = filtersArray.every(filterConfig => + areObjectsEqual( + filterConfig.defaultValue, filterData[filterConfig.id]?.currentState?.value, + ), ); if (areFiltersInitialized) { + handleApply(); setIsInitialized(true); } - }, [filterConfigs, filterData, isInitialized]); + }, [filtersArray, filterData, isInitialized]); useEffect(() => { - if (filterConfigs.length === 0 && filtersOpen) { + if (filtersArray.length === 0 && filtersOpen) { toggleFiltersBar(false); } - }, [filterConfigs]); + }, [filtersArray.length]); const cascadeChildren = useMemo( - () => mapParentFiltersToChildren(filterConfigs), - [filterConfigs], + () => mapParentFiltersToChildren(filtersArray), + [filtersArray], ); const cascadeFilters = useMemo(() => { - const filtersWithValue = filterConfigs.map(filter => ({ + const filtersWithValue = filtersArray.map(filter => ({ ...filter, currentValue: filterData[filter.id]?.currentState?.value, })); return buildCascadeFiltersTree(filtersWithValue); - }, [filterConfigs, filterData]); + }, [filtersArray, filterData]); const handleFilterSelectionChange = ( filter: Pick & Partial, @@ -243,35 +260,15 @@ const FilterBar: React.FC = ({ }); }; - const handleApply = () => { - const filterIds = Object.keys(filterData); - filterIds.forEach(filterId => { - if (filterData[filterId]) { - dispatch( - updateDataMask(filterId, { - nativeFilters: filterData[filterId], - }), - ); - } - }); - setLastAppliedFilterData(() => filterData); - }; - - useEffect(() => { - if (isInitialized) { - handleApply(); - } - }, [isInitialized]); - const handleClearAll = () => { - filterConfigs.forEach(filter => { + filtersArray.forEach(filter => { setFilterData(draft => { draft[filter.id] = getInitialMask(filter.id); }); }); }; - const isClearAllDisabled = !Object.values(dataMaskState).every( + const isClearAllDisabled = Object.values(dataMaskState).every( filter => filterData[filter.id]?.currentState?.value === null || (!filterData[filter.id] && filter.currentState?.value === null), @@ -295,6 +292,9 @@ const FilterBar: React.FC = ({ ); + const isApplyDisabled = + !isInitialized || areObjectsEqual(filterData, lastAppliedFilterData); + return ( = ({ {t('Filters')} {canEdit && ( @@ -318,7 +318,7 @@ const FilterBar: React.FC = ({ - -
{t('Name')}
- ) => { - setFiltersSetName(value); - }} - /> -
- - - - ); -}; - -export default FilterSets; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx new file mode 100644 index 0000000000000..825267777dbf0 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterSets/FilterSets.tsx @@ -0,0 +1,181 @@ +/** + * 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 { Select, Typography } from 'src/common/components'; +import Button from 'src/components/Button'; +import React, { useState } from 'react'; +import { styled, t, tn } from '@superset-ui/core'; +import { useDispatch } from 'react-redux'; +import { + DataMaskState, + DataMaskUnitWithId, + MaskWithId, +} from 'src/dataMask/types'; +import { setFilterSetsConfiguration } from 'src/dashboard/actions/nativeFilters'; +import { generateFiltersSetId } from './utils'; +import { Filter } from '../../types'; +import { useFilters, useDataMask, useFilterSets } from '../state'; +import Footer from './Footer'; +import FiltersHeader from './FiltersHeader'; + +const FilterSet = styled.div` + display: grid; + align-items: center; + justify-content: center; + grid-template-columns: 1fr; + grid-gap: ${({ theme }) => theme.gridUnit}px; + ${({ theme }) => + `padding: 0 ${theme.gridUnit * 4}px ${theme.gridUnit * 4}px`}; + border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + & button.superset-button { + margin-left: 0; + } + & input { + width: 100%; + } + & .ant-typography-edit-content { + left: 0; + margin-top: 0; + } +`; + +type FilterSetsProps = { + disabled: boolean; + dataMaskState: DataMaskUnitWithId; + onFilterSelectionChange: ( + filter: Pick & Partial, + dataMask: Partial, + ) => void; +}; + +const DEFAULT_FILTER_SET_NAME = t('New filter set'); + +const FilterSets: React.FC = ({ + disabled, + onFilterSelectionChange, + dataMaskState, +}) => { + const dispatch = useDispatch(); + const [filterSetName, setFilterSetName] = useState(DEFAULT_FILTER_SET_NAME); + const [editMode, setEditMode] = useState(false); + const filterSets = useFilterSets(); + const filterSetsArray = Object.values(filterSets); + const dataMask = useDataMask(); + const filters = Object.values(useFilters()); + const [selectedFiltersSetId, setSelectedFiltersSetId] = useState< + string | null + >(null); + + const takeFilterSet = (value: string) => { + setSelectedFiltersSetId(value); + if (!value) { + return; + } + const filtersSet = filterSets[value]; + Object.values(filtersSet.dataMask?.nativeFilters ?? []).forEach( + dataMask => { + const { extraFormData, currentState, id } = dataMask as MaskWithId; + onFilterSelectionChange( + { id }, + { nativeFilters: { extraFormData, currentState } }, + ); + }, + ); + }; + + const handleDeleteFilterSets = () => { + dispatch( + setFilterSetsConfiguration( + filterSetsArray.filter( + filtersSet => filtersSet.id !== selectedFiltersSetId, + ), + ), + ); + setFilterSetName(DEFAULT_FILTER_SET_NAME); + setSelectedFiltersSetId(null); + }; + + const handleCancel = () => { + setEditMode(false); + setFilterSetName(DEFAULT_FILTER_SET_NAME); + }; + + const handleCreateFilterSet = () => { + dispatch( + setFilterSetsConfiguration( + filterSetsArray.concat([ + { + name: filterSetName.trim(), + id: generateFiltersSetId(), + dataMask: { + nativeFilters: dataMaskState, + }, + }, + ]), + ), + ); + setEditMode(false); + setFilterSetName(DEFAULT_FILTER_SET_NAME); + }; + + return ( + + , + onChange: setFilterSetName, + }} + > + {filterSetName} + + +