From 5f2140eb0fff9ba5af39eb6c683d339946072d06 Mon Sep 17 00:00:00 2001 From: Xiao Yijun Date: Tue, 17 Jan 2023 12:30:25 +0800 Subject: [PATCH] refactor(console): add empty state to transfers (#2956) --- .../components/DataEmpty/index.module.scss | 19 +++++++ .../src/components/DataEmpty/index.tsx | 39 +++++++++++++++ .../SourceScopesBox/index.module.scss | 5 ++ .../components/SourceScopesBox/index.tsx | 49 ++++++++++++++----- .../SourceUsersBox/index.module.scss | 5 ++ .../components/SourceUsersBox/index.tsx | 43 +++++++++------- .../components/Table/TableEmpty.module.scss | 20 -------- .../src/components/Table/TableEmpty.tsx | 39 ++++----------- .../SourceRolesBox/index.module.scss | 5 ++ .../components/SourceRolesBox/index.tsx | 44 ++++++++++------- .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + .../translation/admin-console/role-details.ts | 2 + .../translation/admin-console/user-details.ts | 1 + 26 files changed, 195 insertions(+), 97 deletions(-) create mode 100644 packages/console/src/components/DataEmpty/index.module.scss create mode 100644 packages/console/src/components/DataEmpty/index.tsx diff --git a/packages/console/src/components/DataEmpty/index.module.scss b/packages/console/src/components/DataEmpty/index.module.scss new file mode 100644 index 000000000..534df66cc --- /dev/null +++ b/packages/console/src/components/DataEmpty/index.module.scss @@ -0,0 +1,19 @@ +@use '@/scss/underscore' as _; + +.empty { + display: flex; + flex-direction: column; + align-items: center; + padding: _.unit(4) 0; + + .title { + font: var(--font-subhead-2); + margin-bottom: _.unit(2); + } + + .description { + font: var(--font-body-medium); + color: var(--color-neutral-50); + margin-bottom: _.unit(2); + } +} diff --git a/packages/console/src/components/DataEmpty/index.tsx b/packages/console/src/components/DataEmpty/index.tsx new file mode 100644 index 000000000..949cbf84e --- /dev/null +++ b/packages/console/src/components/DataEmpty/index.tsx @@ -0,0 +1,39 @@ +import { AppearanceMode } from '@logto/schemas'; +import type { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import EmptyDark from '@/assets/images/table-empty-dark.svg'; +import Empty from '@/assets/images/table-empty.svg'; +import { useTheme } from '@/hooks/use-theme'; + +import * as styles from './index.module.scss'; + +export type Props = { + title?: string; + description?: string; + image?: ReactNode; + children?: ReactNode; + imageClassName?: string; +}; + +const DataEmpty = ({ title, description, image, imageClassName, children }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + + const theme = useTheme(); + + return ( +
+ {image ?? + (theme === AppearanceMode.LightMode ? ( + + ) : ( + + ))} +
{title ?? t('errors.empty')}
+ {description &&
{description}
} + {children} +
+ ); +}; + +export default DataEmpty; diff --git a/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.module.scss b/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.module.scss index b6084458b..622fdba2e 100644 --- a/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.module.scss +++ b/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.module.scss @@ -5,3 +5,8 @@ .icon { color: var(--color-text-secondary); } + +.emptyImage { + width: 128px; + height: 128px; +} diff --git a/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx b/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx index d706cb477..16fe9d8e4 100644 --- a/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx +++ b/packages/console/src/components/RoleScopesTransfer/components/SourceScopesBox/index.tsx @@ -6,8 +6,10 @@ import { useTranslation } from 'react-i18next'; import useSWR from 'swr'; import Search from '@/assets/images/search.svg'; +import DataEmpty from '@/components/DataEmpty'; import type { DetailedResourceResponse } from '@/components/RoleScopesTransfer/types'; import TextInput from '@/components/TextInput'; +import type { RequestError } from '@/hooks/use-api'; import * as transferLayout from '@/scss/transfer.module.scss'; import ResourceItem from '../ResourceItem'; @@ -21,8 +23,20 @@ type Props = { const SourceScopesBox = ({ roleId, selectedScopes, onChange }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data = [] } = useSWR('/api/resources?includeScopes=true'); - const { data: roleScopes = [] } = useSWR(roleId && `/api/roles/${roleId}/scopes`); + + const { data: allResources, error: fetchAllResourcesError } = useSWR< + ResourceResponse[], + RequestError + >('/api/resources?includeScopes=true'); + + const { data: roleScopes, error: fetchRoleScopesError } = useSWR( + roleId && `/api/roles/${roleId}/scopes` + ); + + const isLoading = + (!allResources && !fetchAllResourcesError) || (!roleScopes && !fetchRoleScopesError); + + const hasError = Boolean(fetchAllResourcesError) || Boolean(fetchRoleScopesError); const [keyword, setKeyword] = useState(''); @@ -58,9 +72,13 @@ const SourceScopesBox = ({ roleId, selectedScopes, onChange }: Props) => { scopes.filter((scope) => selectedScopes.findIndex(({ id }) => id === scope.id) >= 0); const resources: DetailedResourceResponse[] = useMemo(() => { + if (!allResources || !roleScopes) { + return []; + } + const excludeScopeIds = new Set(roleScopes.map(({ id }) => id)); - return data + return allResources .filter(({ scopes }) => scopes.some(({ id }) => !excludeScopeIds.has(id))) .map(({ scopes, ...resource }) => ({ ...resource, @@ -71,7 +89,7 @@ const SourceScopesBox = ({ roleId, selectedScopes, onChange }: Props) => { resource, })), })); - }, [data, roleScopes]); + }, [allResources, roleScopes]); const dataSource = useMemo(() => { const lowerCasedKeyword = keyword.toLowerCase(); @@ -110,15 +128,22 @@ const SourceScopesBox = ({ roleId, selectedScopes, onChange }: Props) => { />
- {dataSource.map((resource) => ( - - ))} + )} + {dataSource.length > 0 && + dataSource.map((resource) => ( + + ))}
); diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.module.scss b/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.module.scss index b6084458b..622fdba2e 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.module.scss +++ b/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.module.scss @@ -5,3 +5,8 @@ .icon { color: var(--color-text-secondary); } + +.emptyImage { + width: 128px; + height: 128px; +} diff --git a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx b/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx index 2d446e4ea..a26fd2132 100644 --- a/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx +++ b/packages/console/src/components/RoleUsersTransfer/components/SourceUsersBox/index.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import useSWR from 'swr'; import Search from '@/assets/images/search.svg'; +import DataEmpty from '@/components/DataEmpty'; import Pagination from '@/components/Pagination'; import TextInput from '@/components/TextInput'; import { defaultPageSize } from '@/consts'; @@ -40,7 +41,9 @@ const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => { ...conditional(keyword && { search: formatKeyword(keyword) }), }); - const { data } = useSWR<[User[], number], RequestError>(url); + const { data, error } = useSWR<[User[], number], RequestError>(url); + + const isLoading = !data && !error; const [dataSource = [], totalCount] = data ?? []; @@ -64,24 +67,28 @@ const SourceUsersBox = ({ roleId, selectedUsers, onChange }: Props) => { />
- {dataSource.map((user) => { - const isSelected = isUserAdded(user); + {!isLoading && !error && dataSource.length === 0 && ( + + )} + {dataSource.length > 0 && + dataSource.map((user) => { + const isSelected = isUserAdded(user); - return ( - { - onChange( - isSelected - ? selectedUsers.filter(({ id }) => user.id !== id) - : [user, ...selectedUsers] - ); - }} - /> - ); - })} + return ( + { + onChange( + isSelected + ? selectedUsers.filter(({ id }) => user.id !== id) + : [user, ...selectedUsers] + ); + }} + /> + ); + })}
{ - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const theme = useTheme(); - - return ( - - -
- {image ?? (theme === AppearanceMode.LightMode ? : )} -
{title ?? t('errors.empty')}
- {description &&
{description}
} - {children} -
- - - ); -}; +const TableEmpty = ({ columns, ...emptyProps }: Props) => ( + + + + + +); export default TableEmpty; diff --git a/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.module.scss b/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.module.scss index b6084458b..622fdba2e 100644 --- a/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.module.scss +++ b/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.module.scss @@ -5,3 +5,8 @@ .icon { color: var(--color-text-secondary); } + +.emptyImage { + width: 128px; + height: 128px; +} diff --git a/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.tsx b/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.tsx index 49ff2e904..a1fc72e47 100644 --- a/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.tsx +++ b/packages/console/src/components/UserRolesTransfer/components/SourceRolesBox/index.tsx @@ -6,8 +6,10 @@ import { useTranslation } from 'react-i18next'; import useSWR from 'swr'; import Search from '@/assets/images/search.svg'; +import DataEmpty from '@/components/DataEmpty'; import Pagination from '@/components/Pagination'; import TextInput from '@/components/TextInput'; +import type { RequestError } from '@/hooks/use-api'; import useDebounce from '@/hooks/use-debounce'; import * as transferLayout from '@/scss/transfer.module.scss'; import { buildUrl } from '@/utilities/url'; @@ -39,7 +41,9 @@ const SourceRolesBox = ({ userId, selectedRoles, onChange }: Props) => { ...conditional(keyword && { search: `%${keyword}%` }), }); - const { data } = useSWR<[RoleResponse[], number]>(url); + const { data, error } = useSWR<[RoleResponse[], number], RequestError>(url); + + const isLoading = !data && !error; const [dataSource = [], totalCount] = data ?? []; @@ -64,24 +68,28 @@ const SourceRolesBox = ({ userId, selectedRoles, onChange }: Props) => { />
- {dataSource.map((role) => { - const isSelected = isRoleSelected(role); + {!isLoading && !error && dataSource.length === 0 && ( + + )} + {dataSource.length > 0 && + dataSource.map((role) => { + const isSelected = isRoleSelected(role); - return ( - { - onChange( - isSelected - ? selectedRoles.filter(({ id }) => id !== role.id) - : [role, ...selectedRoles] - ); - }} - /> - ); - })} + return ( + { + onChange( + isSelected + ? selectedRoles.filter(({ id }) => id !== role.id) + : [role, ...selectedRoles] + ); + }} + /> + ); + })}