This is an automated email from the ASF dual-hosted git repository.
kgabryje pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new d49fd01ff3 feat(CRUD): add new empty state (#19310)
d49fd01ff3 is described below
commit d49fd01ff3e3ee153e5e50352ec2151f028a5456
Author: Stephen Liu <[email protected]>
AuthorDate: Mon Apr 11 18:04:45 2022 +0800
feat(CRUD): add new empty state (#19310)
* feat(CRUD): add new empty state
* fix ci
* add svg license
---
.../src/assets/images/filter-results.svg | 34 +++++++++++++++++
superset-frontend/src/components/Button/index.tsx | 1 +
.../src/components/EmptyState/index.tsx | 19 ++++++++--
.../src/components/ListView/Filters/Base.ts | 4 ++
.../src/components/ListView/Filters/DateRange.tsx | 27 ++++++++++----
.../src/components/ListView/Filters/Search.tsx | 23 ++++++++----
.../src/components/ListView/Filters/Select.tsx | 36 ++++++++++++------
.../src/components/ListView/Filters/index.tsx | 36 ++++++++++++++----
.../src/components/ListView/ListView.tsx | 43 +++++++++++++++-------
superset-frontend/src/components/ListView/utils.ts | 1 +
.../src/views/CRUD/alert/AlertList.tsx | 17 ++++-----
.../src/views/CRUD/annotation/AnnotationList.tsx | 22 ++++-------
.../CRUD/annotationlayers/AnnotationLayersList.tsx | 20 +++-------
13 files changed, 196 insertions(+), 87 deletions(-)
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 0000000000..770a54b34f
--- /dev/null
+++ b/superset-frontend/src/assets/images/filter-results.svg
@@ -0,0 +1,34 @@
+<!--
+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.
+-->
+<svg width="120" height="150" viewBox="0 0 120 150" fill="none"
xmlns="http://www.w3.org/2000/svg">
+<path d="M100.133 19.8391L100.134 19.8402L119.5
40.6963V149.5H0.5V0.5H82.2811L100.133 19.8391Z" fill="#F7F7F7"
stroke="#D9D9D9"/>
+<path d="M82.5 0V42H120" stroke="#D9D9D9"/>
+<mask id="path-3-inside-1_738_30486" fill="white">
+<rect x="24" y="65" width="71.7778" height="9.44444" rx="0.5"/>
+</mask>
+<rect x="24" y="65" width="71.7778" height="9.44444" rx="0.5" fill="white"
stroke="#D9D9D9" stroke-width="2" mask="url(#path-3-inside-1_738_30486)"/>
+<mask id="path-4-inside-2_738_30486" fill="white">
+<rect x="39.1113" y="85.7778" width="41.5556" height="9.44444" rx="0.5"/>
+</mask>
+<rect x="39.1113" y="85.7778" width="41.5556" height="9.44444" rx="0.5"
fill="white" stroke="#D9D9D9" stroke-width="2"
mask="url(#path-4-inside-2_738_30486)"/>
+<mask id="path-5-inside-3_738_30486" fill="white">
+<rect x="50.4443" y="106.556" width="18.8889" height="9.44444" rx="0.5"/>
+</mask>
+<rect x="50.4443" y="106.556" width="18.8889" height="9.44444" rx="0.5"
fill="white" stroke="#D9D9D9" stroke-width="2"
mask="url(#path-5-inside-3_738_30486)"/>
+</svg>
diff --git a/superset-frontend/src/components/Button/index.tsx
b/superset-frontend/src/components/Button/index.tsx
index 30d4e3d9ac..b8e428d6ca 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 02c1d7c4a2..7ba54567e4 100644
--- a/superset-frontend/src/components/EmptyState/index.tsx
+++ b/superset-frontend/src/components/EmptyState/index.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React, { ReactNode } from 'react';
+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';
@@ -140,6 +140,11 @@ const ImageContainer = ({ image, size }:
ImageContainerProps) => (
/>
);
+const handleMouseDown = (e: SyntheticEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+};
+
export const EmptyStateBig = ({
title,
image,
@@ -159,7 +164,11 @@ export const EmptyStateBig = ({
<BigTitle>{title}</BigTitle>
{description && <BigDescription>{description}</BigDescription>}
{buttonAction && buttonText && (
- <ActionButton buttonStyle="primary" onClick={buttonAction}>
+ <ActionButton
+ buttonStyle="primary"
+ onClick={buttonAction}
+ onMouseDown={handleMouseDown}
+ >
{buttonText}
</ActionButton>
)}
@@ -186,7 +195,11 @@ export const EmptyStateMedium = ({
<Title>{title}</Title>
{description && <Description>{description}</Description>}
{buttonText && buttonAction && (
- <ActionButton buttonStyle="primary" onClick={buttonAction}>
+ <ActionButton
+ buttonStyle="primary"
+ onClick={buttonAction}
+ onMouseDown={handleMouseDown}
+ >
{buttonText}
</ActionButton>
)}
diff --git a/superset-frontend/src/components/ListView/Filters/Base.ts
b/superset-frontend/src/components/ListView/Filters/Base.ts
index 03d805a751..6baca649ff 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 c391d6ff67..4dfaf11f79 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, { useState, useMemo } 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<FilterHandler>,
+) {
const [value, setValue] = useState<ValueState | null>(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 (
<RangeFilterContainer>
<FormLabel>{Header}</FormLabel>
@@ -72,3 +83,5 @@ export default function DateRangeFilter({
</RangeFilterContainer>
);
}
+
+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 f327ac4b39..60cfe41bac 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 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<FilterHandler>,
+) {
const [value, setValue] = useState(initialValue || '');
const handleSubmit = () => {
if (value) {
@@ -61,6 +59,13 @@ export default function SearchFilter({
}
};
+ useImperativeHandle(ref, () => ({
+ clearFilter: () => {
+ setValue('');
+ onSubmit('');
+ },
+ }));
+
return (
<Container>
<FormLabel>{Header}</FormLabel>
@@ -78,3 +83,5 @@ export default function SearchFilter({
</Container>
);
}
+
+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 b2e5e639d4..525061fd27 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, { useState, useMemo } 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 { FilterContainer, BaseFilter } 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<FilterHandler>,
+) {
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,4 +102,4 @@ function SelectFilter({
</FilterContainer>
);
}
-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 5b630ebe9c..348ed3850d 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<FilterHandler>()),
+ [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 (
<SelectFilter
+ ref={filterRefs[index]}
Header={Header}
fetchSelects={fetchSelects}
initialValue={initialValue}
@@ -65,6 +85,7 @@ function UIFilters({
if (input === 'search' && typeof Header === 'string') {
return (
<SearchFilter
+ ref={filterRefs[index]}
Header={Header}
initialValue={initialValue}
key={id}
@@ -76,6 +97,7 @@ function UIFilters({
if (input === 'datetime_range') {
return (
<DateRangeFilter
+ ref={filterRefs[index]}
Header={Header}
initialValue={initialValue}
key={id}
@@ -91,4 +113,4 @@ function UIFilters({
);
}
-export default withTheme(UIFilters);
+export default withTheme(forwardRef(UIFilters));
diff --git a/superset-frontend/src/components/ListView/ListView.tsx
b/superset-frontend/src/components/ListView/ListView.tsx
index 4b979cbf56..e91626003f 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -17,10 +17,8 @@
* under the License.
*/
import { t, styled } from '@superset-ui/core';
-import React, { useEffect } from 'react';
-import { Empty } from 'src/components';
+import React, { useCallback, useEffect, useRef } from 'react';
import Alert from 'src/components/Alert';
-import EmptyImage from 'src/assets/images/empty.svg';
import cx from 'classnames';
import Button from 'src/components/Button';
import Icons from 'src/components/Icons';
@@ -38,6 +36,7 @@ import {
ViewModeType,
} from './types';
import { ListViewError, useListViewState } from './utils';
+import { EmptyStateBig, EmptyStateProps } from '../EmptyState';
const ListViewStyles = styled.div`
text-align: center;
@@ -223,10 +222,7 @@ export interface ListViewProps<T extends object = any> {
defaultViewMode?: ViewModeType;
highlightRowId?: number;
showThumbnails?: boolean;
- emptyState?: {
- message?: string;
- slot?: React.ReactNode;
- };
+ emptyState?: EmptyStateProps;
}
function ListView<T extends object = any>({
@@ -248,7 +244,7 @@ function ListView<T extends object = any>({
cardSortSelectOptions,
defaultViewMode = 'card',
highlightRowId,
- emptyState = {},
+ emptyState,
}: ListViewProps<T>) {
const {
getTableProps,
@@ -263,6 +259,7 @@ function ListView<T extends object = any>({
toggleAllRowsSelected,
setViewMode,
state: { pageIndex, pageSize, internalFilters, viewMode },
+ query,
} = useListViewState({
bulkSelectColumnConfig,
bulkSelectMode: bulkSelectEnabled && Boolean(bulkActions.length),
@@ -291,6 +288,14 @@ function ListView<T extends object = any>({
});
}
+ 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<T extends object = any>({
<div className="controls">
{filterable && (
<FilterControls
+ ref={filterControlsRef}
filters={filters}
internalFilters={internalFilters}
updateFilterValue={applyFilterValue}
@@ -394,12 +400,21 @@ function ListView<T extends object = any>({
)}
{!loading && rows.length === 0 && (
<EmptyWrapper className={viewMode}>
- <Empty
- image={<EmptyImage />}
- description={emptyState.message || t('No Data')}
- >
- {emptyState.slot || null}
- </Empty>
+ {query.filters ? (
+ <EmptyStateBig
+ title={t('No results match your filter criteria')}
+ description={t('Try different criteria to display results.')}
+ image="filter-results.svg"
+ buttonAction={() => handleClearFilterControls()}
+ buttonText={t('clear all filters')}
+ />
+ ) : (
+ <EmptyStateBig
+ {...emptyState}
+ title={emptyState?.title || t('No Data')}
+ image={emptyState?.image || 'filter-results.svg'}
+ />
+ )}
</EmptyWrapper>
)}
</div>
diff --git a/superset-frontend/src/components/ListView/utils.ts
b/superset-frontend/src/components/ListView/utils.ts
index 346bde0982..78873f51f1 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 2d84cb0b97..f0f9d7423b 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 { t, SupersetClient, makeApi, styled } 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, {
@@ -366,15 +365,15 @@ function AlertList({
});
}
- const EmptyStateButton = (
- <Button buttonStyle="primary" onClick={() => handleAlertEdit(null)}>
- <i className="fa fa-plus" /> {title}
- </Button>
- );
-
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 ? (
+ <>
+ <i className="fa fa-plus" /> {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 c91099a6d5..a4599b9ff5 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';
@@ -239,22 +238,17 @@ function AnnotationList({
hasHistory = false;
}
- const EmptyStateButton = (
- <Button
- buttonStyle="primary"
- onClick={() => {
- handleAnnotationEdit(null);
- }}
- >
+ const emptyState = {
+ title: t('No annotation yet'),
+ image: 'filter-results.svg',
+ buttonAction: () => {
+ handleAnnotationEdit(null);
+ },
+ buttonText: (
<>
<i className="fa fa-plus" /> {t('Annotation')}
</>
- </Button>
- );
-
- 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 b93e31d380..0265682dc7 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,
FilterOperator,
} 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';
@@ -311,22 +310,15 @@ function AnnotationLayersList({
[],
);
- const EmptyStateButton = (
- <Button
- buttonStyle="primary"
- onClick={() => {
- handleAnnotationLayerEdit(null);
- }}
- >
+ const emptyState = {
+ title: t('No annotation layers yet'),
+ image: 'filter-results.svg',
+ buttonAction: () => handleAnnotationLayerEdit(null),
+ buttonText: (
<>
<i className="fa fa-plus" /> {t('Annotation layer')}
</>
- </Button>
- );
-
- const emptyState = {
- message: t('No annotation layers yet'),
- slot: EmptyStateButton,
+ ),
};
const onLayerAdd = (id?: number) => {