diff --git a/superset-frontend/src/assets/images/filter-results.svg b/superset-frontend/src/assets/images/filter-results.svg new file mode 100644 index 0000000000000..770a54b34f37f --- /dev/null +++ b/superset-frontend/src/assets/images/filter-results.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + diff --git a/superset-frontend/src/components/Button/index.tsx b/superset-frontend/src/components/Button/index.tsx index 3fb73238ce927..5afbccbde7a9c 100644 --- a/superset-frontend/src/components/Button/index.tsx +++ b/superset-frontend/src/components/Button/index.tsx @@ -56,6 +56,7 @@ export interface ButtonProps { | 'rightTop' | 'rightBottom'; onClick?: OnClickHandler; + onMouseDown?: OnClickHandler; disabled?: boolean; buttonStyle?: ButtonStyle; buttonSize?: 'default' | 'small' | 'xsmall'; diff --git a/superset-frontend/src/components/EmptyState/index.tsx b/superset-frontend/src/components/EmptyState/index.tsx index e79325e47f71d..7ba54567e438f 100644 --- a/superset-frontend/src/components/EmptyState/index.tsx +++ b/superset-frontend/src/components/EmptyState/index.tsx @@ -17,8 +17,8 @@ * under the License. */ -import React, { ReactNode } from 'react'; -import { css, styled, SupersetTheme } from '@superset-ui/core'; +import React, { ReactNode, SyntheticEvent } from 'react'; +import { styled, css, SupersetTheme } from '@superset-ui/core'; import { Empty } from 'src/components'; import Button from 'src/components/Button'; @@ -117,9 +117,7 @@ const ActionButton = styled(Button)` `; const getImage = (image: string | ReactNode) => - typeof image === 'string' - ? `${process.env.APP_PREFIX}/static/assets/images/${image}` - : image; + typeof image === 'string' ? `/static/assets/images/${image}` : image; const getImageHeight = (size: EmptyStateSize) => { switch (size) { @@ -142,6 +140,11 @@ const ImageContainer = ({ image, size }: ImageContainerProps) => ( /> ); +const handleMouseDown = (e: SyntheticEvent) => { + e.preventDefault(); + e.stopPropagation(); +}; + export const EmptyStateBig = ({ title, image, @@ -161,7 +164,11 @@ export const EmptyStateBig = ({ {title} {description && {description}} {buttonAction && buttonText && ( - + {buttonText} )} @@ -188,7 +195,11 @@ export const EmptyStateMedium = ({ {title} {description && {description}} {buttonText && buttonAction && ( - + {buttonText} )} diff --git a/superset-frontend/src/components/ListView/Filters/Base.ts b/superset-frontend/src/components/ListView/Filters/Base.ts index 03d805a751986..6baca649ffa16 100644 --- a/superset-frontend/src/components/ListView/Filters/Base.ts +++ b/superset-frontend/src/components/ListView/Filters/Base.ts @@ -31,3 +31,7 @@ export const FilterContainer = styled.div` align-items: center; width: ${SELECT_WIDTH}px; `; + +export type FilterHandler = { + clearFilter: () => void; +}; diff --git a/superset-frontend/src/components/ListView/Filters/DateRange.tsx b/superset-frontend/src/components/ListView/Filters/DateRange.tsx index b190ccedcd8dd..4dfaf11f79fdf 100644 --- a/superset-frontend/src/components/ListView/Filters/DateRange.tsx +++ b/superset-frontend/src/components/ListView/Filters/DateRange.tsx @@ -16,12 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo, useState } from 'react'; +import React, { + useState, + useMemo, + forwardRef, + useImperativeHandle, +} from 'react'; import moment, { Moment } from 'moment'; import { styled } from '@superset-ui/core'; import { RangePicker } from 'src/components/DatePicker'; import { FormLabel } from 'src/components/Form'; -import { BaseFilter } from './Base'; +import { BaseFilter, FilterHandler } from './Base'; interface DateRangeFilterProps extends BaseFilter { onSubmit: (val: number[]) => void; @@ -38,17 +43,23 @@ const RangeFilterContainer = styled.div` width: 360px; `; -export default function DateRangeFilter({ - Header, - initialValue, - onSubmit, -}: DateRangeFilterProps) { +function DateRangeFilter( + { Header, initialValue, onSubmit }: DateRangeFilterProps, + ref: React.RefObject, +) { const [value, setValue] = useState(initialValue ?? null); const momentValue = useMemo((): [Moment, Moment] | null => { if (!value || (Array.isArray(value) && !value.length)) return null; return [moment(value[0]), moment(value[1])]; }, [value]); + useImperativeHandle(ref, () => ({ + clearFilter: () => { + setValue(null); + onSubmit([]); + }, + })); + return ( {Header} @@ -72,3 +83,5 @@ export default function DateRangeFilter({ ); } + +export default forwardRef(DateRangeFilter); diff --git a/superset-frontend/src/components/ListView/Filters/Search.tsx b/superset-frontend/src/components/ListView/Filters/Search.tsx index 7d233d9afc46c..60cfe41bac070 100644 --- a/superset-frontend/src/components/ListView/Filters/Search.tsx +++ b/superset-frontend/src/components/ListView/Filters/Search.tsx @@ -16,13 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState } from 'react'; -import { styled, t } from '@superset-ui/core'; +import React, { forwardRef, useImperativeHandle, useState } from 'react'; +import { t, styled } from '@superset-ui/core'; import Icons from 'src/components/Icons'; import { AntdInput } from 'src/components'; import { SELECT_WIDTH } from 'src/components/ListView/utils'; import { FormLabel } from 'src/components/Form'; -import { BaseFilter } from './Base'; +import { BaseFilter, FilterHandler } from './Base'; interface SearchHeaderProps extends BaseFilter { Header: string; @@ -42,12 +42,10 @@ const StyledInput = styled(AntdInput)` border-radius: ${({ theme }) => theme.gridUnit}px; `; -export default function SearchFilter({ - Header, - name, - initialValue, - onSubmit, -}: SearchHeaderProps) { +function SearchFilter( + { Header, name, initialValue, onSubmit }: SearchHeaderProps, + ref: React.RefObject, +) { const [value, setValue] = useState(initialValue || ''); const handleSubmit = () => { if (value) { @@ -61,6 +59,13 @@ export default function SearchFilter({ } }; + useImperativeHandle(ref, () => ({ + clearFilter: () => { + setValue(''); + onSubmit(''); + }, + })); + return ( {Header} @@ -78,3 +83,5 @@ export default function SearchFilter({ ); } + +export default forwardRef(SearchFilter); diff --git a/superset-frontend/src/components/ListView/Filters/Select.tsx b/superset-frontend/src/components/ListView/Filters/Select.tsx index 74dcad5d76c28..525061fd27411 100644 --- a/superset-frontend/src/components/ListView/Filters/Select.tsx +++ b/superset-frontend/src/components/ListView/Filters/Select.tsx @@ -16,12 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useMemo, useState } from 'react'; +import React, { + useState, + useMemo, + forwardRef, + useImperativeHandle, +} from 'react'; import { t } from '@superset-ui/core'; import { Select } from 'src/components'; import { Filter, SelectOption } from 'src/components/ListView/types'; import { FormLabel } from 'src/components/Form'; -import { BaseFilter, FilterContainer } from './Base'; +import { FilterContainer, BaseFilter, FilterHandler } from './Base'; interface SelectFilterProps extends BaseFilter { fetchSelects?: Filter['fetchSelects']; @@ -31,14 +36,17 @@ interface SelectFilterProps extends BaseFilter { selects: Filter['selects']; } -function SelectFilter({ - Header, - name, - fetchSelects, - initialValue, - onSelect, - selects = [], -}: SelectFilterProps) { +function SelectFilter( + { + Header, + name, + fetchSelects, + initialValue, + onSelect, + selects = [], + }: SelectFilterProps, + ref: React.RefObject, +) { const [selectedOption, setSelectedOption] = useState(initialValue); const onChange = (selected: SelectOption) => { @@ -53,6 +61,12 @@ function SelectFilter({ setSelectedOption(undefined); }; + useImperativeHandle(ref, () => ({ + clearFilter: () => { + onClear(); + }, + })); + const fetchAndFormatSelects = useMemo( () => async (inputValue: string, page: number, pageSize: number) => { if (fetchSelects) { @@ -88,5 +102,4 @@ function SelectFilter({ ); } - -export default SelectFilter; +export default forwardRef(SelectFilter); diff --git a/superset-frontend/src/components/ListView/Filters/index.tsx b/superset-frontend/src/components/ListView/Filters/index.tsx index 39fec41c2bfe5..7f05fe9bd5b6b 100644 --- a/superset-frontend/src/components/ListView/Filters/index.tsx +++ b/superset-frontend/src/components/ListView/Filters/index.tsx @@ -16,7 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { + createRef, + forwardRef, + useImperativeHandle, + useMemo, +} from 'react'; import { withTheme } from '@superset-ui/core'; import { @@ -28,6 +33,7 @@ import { import SearchFilter from './Search'; import SelectFilter from './Select'; import DateRangeFilter from './DateRange'; +import { FilterHandler } from './Base'; interface UIFiltersProps { filters: Filters; @@ -35,11 +41,24 @@ interface UIFiltersProps { updateFilterValue: (id: number, value: FilterValue['value']) => void; } -function UIFilters({ - filters, - internalFilters = [], - updateFilterValue, -}: UIFiltersProps) { +function UIFilters( + { filters, internalFilters = [], updateFilterValue }: UIFiltersProps, + ref: React.RefObject<{ clearFilters: () => void }>, +) { + const filterRefs = useMemo( + () => + Array.from({ length: filters.length }, () => createRef()), + [filters.length], + ); + + useImperativeHandle(ref, () => ({ + clearFilters: () => { + filterRefs.forEach((filter: any) => { + filter.current?.clearFilter?.(); + }); + }, + })); + return ( <> {filters.map( @@ -49,6 +68,7 @@ function UIFilters({ if (input === 'select') { return ( { defaultViewMode?: ViewModeType; highlightRowId?: number; showThumbnails?: boolean; - emptyState?: { - message?: string; - slot?: React.ReactNode; - }; + emptyState?: EmptyStateProps; } function ListView({ @@ -248,7 +244,7 @@ function ListView({ cardSortSelectOptions, defaultViewMode = 'card', highlightRowId, - emptyState = {}, + emptyState, }: ListViewProps) { const { getTableProps, @@ -263,6 +259,7 @@ function ListView({ toggleAllRowsSelected, setViewMode, state: { pageIndex, pageSize, internalFilters, viewMode }, + query, } = useListViewState({ bulkSelectColumnConfig, bulkSelectMode: bulkSelectEnabled && Boolean(bulkActions.length), @@ -291,6 +288,14 @@ function ListView({ }); } + const filterControlsRef = useRef<{ clearFilters: () => void }>(null); + + const handleClearFilterControls = useCallback(() => { + if (query.filters) { + filterControlsRef.current?.clearFilters(); + } + }, [query.filters]); + const cardViewEnabled = Boolean(renderCard); useEffect(() => { @@ -308,6 +313,7 @@ function ListView({
{filterable && ( ({ )} {!loading && rows.length === 0 && ( - } - description={emptyState.message || t('No Data')} - > - {emptyState.slot || null} - + {query.filters ? ( + handleClearFilterControls()} + buttonText={t('clear all filters')} + /> + ) : ( + + )} )}
diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts index 0cbd6db2bcdc9..7a7f97d142a85 100644 --- a/superset-frontend/src/components/ListView/utils.ts +++ b/superset-frontend/src/components/ListView/utils.ts @@ -378,6 +378,7 @@ export function useListViewState({ toggleAllRowsSelected, applyFilterValue, setViewMode, + query, }; } diff --git a/superset-frontend/src/views/CRUD/alert/AlertList.tsx b/superset-frontend/src/views/CRUD/alert/AlertList.tsx index 803566cf356b7..a27e441ae9e79 100644 --- a/superset-frontend/src/views/CRUD/alert/AlertList.tsx +++ b/superset-frontend/src/views/CRUD/alert/AlertList.tsx @@ -22,7 +22,6 @@ import { useHistory } from 'react-router-dom'; import { makeApi, styled, SupersetClient, t } from '@superset-ui/core'; import moment from 'moment'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; -import Button from 'src/components/Button'; import FacePile from 'src/components/FacePile'; import { Tooltip } from 'src/components/Tooltip'; import ListView, { @@ -367,15 +366,15 @@ function AlertList({ }); } - const EmptyStateButton = ( - - ); - const emptyState = { - message: t('No %s yet', titlePlural), - slot: canCreate ? EmptyStateButton : null, + title: t('No %s yet', titlePlural), + image: 'filter-results.svg', + buttonAction: () => handleAlertEdit(null), + buttonText: canCreate ? ( + <> + {title}{' '} + + ) : null, }; const filters: Filters = useMemo( diff --git a/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx b/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx index aa018418802b1..44f99039cf646 100644 --- a/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx +++ b/superset-frontend/src/views/CRUD/annotation/AnnotationList.tsx @@ -24,7 +24,6 @@ import moment from 'moment'; import rison from 'rison'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; -import Button from 'src/components/Button'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import DeleteModal from 'src/components/DeleteModal'; import ListView, { ListViewProps } from 'src/components/ListView'; @@ -241,22 +240,17 @@ function AnnotationList({ hasHistory = false; } - const EmptyStateButton = ( - - ); - - const emptyState = { - message: t('No annotation yet'), - slot: EmptyStateButton, + ), }; return ( diff --git a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx index 26b3072fd7f0c..9443eb2f7452c 100644 --- a/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx +++ b/superset-frontend/src/views/CRUD/annotationlayers/AnnotationLayersList.tsx @@ -32,7 +32,6 @@ import ListView, { Filters, ListViewProps, } from 'src/components/ListView'; -import Button from 'src/components/Button'; import DeleteModal from 'src/components/DeleteModal'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import AnnotationLayerModal from './AnnotationLayerModal'; @@ -323,22 +322,15 @@ function AnnotationLayersList({ [], ); - const EmptyStateButton = ( - - ); - - const emptyState = { - message: t('No annotation layers yet'), - slot: EmptyStateButton, + ), }; const onLayerAdd = (id?: number) => {