feat(CRUD): add new empty state (#19310)

* feat(CRUD): add new empty state

* fix ci

* add svg license
This commit is contained in:
Stephen Liu 2022-04-11 18:04:45 +08:00 committed by GitHub
parent f21ba68a30
commit d49fd01ff3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 196 additions and 87 deletions

View File

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

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -56,6 +56,7 @@ export interface ButtonProps {
| 'rightTop'
| 'rightBottom';
onClick?: OnClickHandler;
onMouseDown?: OnClickHandler;
disabled?: boolean;
buttonStyle?: ButtonStyle;
buttonSize?: 'default' | 'small' | 'xsmall';

View File

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

View File

@ -31,3 +31,7 @@ export const FilterContainer = styled.div`
align-items: center;
width: ${SELECT_WIDTH}px;
`;
export type FilterHandler = {
clearFilter: () => void;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -378,6 +378,7 @@ export function useListViewState({
toggleAllRowsSelected,
applyFilterValue,
setViewMode,
query,
};
}

View File

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

View File

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

View File

@ -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) => {