0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-24 22:05:56 -05:00

refactor(console): improve table loading experience (#4920)

This commit is contained in:
Charles Zhao 2023-11-22 01:27:35 +08:00 committed by GitHub
parent 3da6694fef
commit 3a6985a84e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 79 additions and 68 deletions

View file

@ -1,6 +1,12 @@
@use '@/scss/underscore' as _;
.loading {
.rect {
@include _.shimmering-animation;
height: 26px;
max-width: 344px;
}
.row {
.itemPreview {
display: flex;
align-items: center;
@ -31,8 +37,6 @@
}
.rect {
@include _.shimmering-animation;
height: 32px;
max-width: 344px;
}
}

View file

@ -1,15 +1,34 @@
import * as styles from './TableLoading.module.scss';
import * as styles from './index.module.scss';
type Props = {
columnSpans: number[];
/** For the compact inline style table */
isCompact?: boolean;
};
function TableLoading({ columnSpans }: Props) {
function Skeleton({ columnSpans, isCompact }: Props) {
if (isCompact) {
return (
<>
{Array.from({ length: 2 }).map((_, rowIndex) => (
// eslint-disable-next-line react/no-array-index-key
<tr key={`row-${rowIndex}`}>
{columnSpans.map((colSpan, columnIndex) => (
// eslint-disable-next-line react/no-array-index-key
<td key={columnIndex} colSpan={colSpan}>
<div className={styles.rect} />
</td>
))}
</tr>
))}
</>
);
}
return (
<>
{Array.from({ length: 8 }).map((_, rowIndex) => (
// eslint-disable-next-line react/no-array-index-key
<tr key={`row-${rowIndex}`} className={styles.loading}>
<tr key={`row-${rowIndex}`} className={styles.row}>
<td colSpan={columnSpans[0]}>
<div className={styles.itemPreview}>
<div className={styles.avatar} />
@ -31,4 +50,4 @@ function TableLoading({ columnSpans }: Props) {
);
}
export default TableLoading;
export default Skeleton;

View file

@ -9,9 +9,9 @@ import Pagination from '@/ds-components/Pagination';
import OverlayScrollbar from '../OverlayScrollbar';
import Skeleton from './Skeleton';
import TableEmptyWrapper from './TableEmptyWrapper';
import TableError from './TableError';
import TableLoading from './TableLoading';
import * as styles from './index.module.scss';
import type { Column, RowGroup } from './types';
@ -35,6 +35,7 @@ export type Props<
placeholder?: ReactNode;
loadingSkeleton?: ReactNode;
errorMessage?: string;
/** The inline style table that is usually embedded in other card containers, has rounded-corner border */
hasBorder?: boolean;
onRetry?: () => void;
/** A footer that will be rendered on the bottom-left of the table. */
@ -110,7 +111,10 @@ function Table<
<tbody>
{isLoading &&
(loadingSkeleton ?? (
<TableLoading columnSpans={columns.map(({ colSpan }) => colSpan ?? 1)} />
<Skeleton
isCompact={hasBorder}
columnSpans={columns.map(({ colSpan }) => colSpan ?? 1)}
/>
))}
{hasError && (
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />

View file

@ -27,10 +27,11 @@ const pathname = '/organizations';
const apiPathname = 'api/organizations';
type Props = {
isLoading: boolean;
onCreate: () => void;
};
function OrganizationsTable({ onCreate }: Props) {
function OrganizationsTable({ isLoading, onCreate }: Props) {
const [keyword, setKeyword] = useState('');
const [page, setPage] = useState(1);
const { data: response, error } = useSWR<[OrganizationWithFeatured[], number], RequestError>(
@ -42,14 +43,14 @@ function OrganizationsTable({ onCreate }: Props) {
})
);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const isLoading = !response && !error;
const isTableLoading = isLoading || (!response && !error);
const [data, totalCount] = response ?? [[], 0];
const { navigate } = useTenantPathname();
return (
<Table
className={pageLayout.table}
isLoading={isLoading}
isLoading={isTableLoading}
placeholder={<EmptyDataPlaceholder onCreate={onCreate} />}
rowGroups={[{ key: 'data', data }]}
rowClickHandler={({ id }) => {

View file

@ -1,14 +1,11 @@
import { type AdminConsoleKey } from '@logto/phrases';
import classNames from 'classnames';
import { type FieldValues, type FieldPath } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import CirclePlus from '@/assets/icons/circle-plus.svg';
import Plus from '@/assets/icons/plus.svg';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
import { Ring as Spinner } from '@/ds-components/Spinner';
import Table from '@/ds-components/Table';
import { type Column } from '@/ds-components/Table/types';
@ -49,12 +46,8 @@ function TemplateTable<
isLoading,
onPageChange,
}: Props<TFieldValues, TName>) {
const hasData = !isLoading && data.length > 0;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
if (isLoading) {
<Spinner className={styles.spinner} />;
}
const noData = !isLoading && data.length === 0;
return (
<section className={styles.section}>
@ -63,10 +56,19 @@ function TemplateTable<
<DynamicT forKey={name} interpolation={{ count: 2 }} />
</header>
)}
{hasData && (
{onAdd && noData && (
<>
{name && (
<div className={styles.empty}>
{t('organizations.empty_placeholder', { entity: String(t(name)).toLowerCase() })}
</div>
)}
<Button icon={<Plus />} title="general.add" onClick={onAdd} />
</>
)}
{!noData && (
<Table
hasBorder
placeholder={<EmptyDataPlaceholder />}
isLoading={isLoading}
rowGroups={[
{
@ -94,16 +96,6 @@ function TemplateTable<
}
/>
)}
{onAdd && !hasData && (
<>
{name && (
<div className={classNames(styles.empty)}>
{t('organizations.empty_placeholder', { entity: String(t(name)).toLowerCase() })}
</div>
)}
<Button icon={<Plus />} title="general.add" onClick={onAdd} />
</>
)}
</section>
);
}

View file

@ -31,8 +31,8 @@ function Organizations({ tab }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { navigate } = useTenantPathname();
const [isCreating, setIsCreating] = useState(false);
const { configs } = useConfigs();
const isInitialSetup = !configs?.organizationCreated;
const { configs, isLoading: isLoadingConfigs } = useConfigs();
const isInitialSetup = !isLoadingConfigs && !configs?.organizationCreated;
const handleCreate = useCallback(() => {
if (isInitialSetup) {
@ -85,7 +85,7 @@ function Organizations({ tab }: Props) {
{t('organizations.organization_template')}
</TabNavItem>
</TabNav>
{!tab && <OrganizationsTable onCreate={handleCreate} />}
{!tab && <OrganizationsTable isLoading={isLoadingConfigs} onCreate={handleCreate} />}
{tab === 'template' && <Settings />}
</>
)}

View file

@ -124,21 +124,6 @@ function SigningKeys() {
rowIndexKey="id"
rowGroups={[{ key: 'signing_keys', data }]}
columns={tableColumns}
loadingSkeleton={
<>
{Array.from({ length: 2 }).map((_, rowIndex) => (
// eslint-disable-next-line react/no-array-index-key
<tr key={`skeleton-row-${rowIndex}`}>
{tableColumns.map(({ colSpan }, columnIndex) => (
// eslint-disable-next-line react/no-array-index-key
<td key={columnIndex} colSpan={colSpan}>
<div className={styles.bone} />
</td>
))}
</tr>
))}
</>
}
/>
</FormField>
<FormField title={`tenants.signing_keys.rotate_${isPrivateKey ? 'private' : 'cookie'}_keys`}>

View file

@ -61,7 +61,7 @@ function UserMfaVerifications({ userId }: Props) {
{t(mfaVerifications?.length ? 'field_description' : 'field_description_empty')}
</div>
)}
{(Boolean(mfaVerifications?.length) || error) && (
{(isLoading || Boolean(mfaVerifications?.length) || error) && (
<Table
hasBorder
rowGroups={[{ key: 'mfaVerifications', data: mfaVerifications }]}

View file

@ -81,16 +81,20 @@ function UserSocialIdentities({ userId, identities, onDelete }: Props) {
});
}, [connectorGroups, identities, t]);
const hasConnectors = Boolean(displayConnectors && displayConnectors.length > 0);
return (
<div>
<div className={styles.description}>
{t(
displayConnectors && displayConnectors.length > 0
? 'user_details.connectors.connected'
: 'user_details.connectors.not_connected'
)}
</div>
{displayConnectors && displayConnectors.length > 0 && (
{!isLoading && !error && (
<div className={styles.description}>
{t(
hasConnectors
? 'user_details.connectors.connected'
: 'user_details.connectors.not_connected'
)}
</div>
)}
{(isLoading || hasConnectors || error) && (
<Table
hasBorder
rowGroups={[{ key: 'identities', data: displayConnectors }]}

View file

@ -55,18 +55,20 @@ function UserSsoIdentities({ ssoIdentities }: Props) {
);
}, [data, ssoIdentities]);
const hasLinkedSsoIdentities = displaySsoConnectors && displaySsoConnectors.length > 0;
const hasLinkedSsoIdentities = Boolean(displaySsoConnectors && displaySsoConnectors.length > 0);
return (
<div>
<div className={styles.description}>
{t(
hasLinkedSsoIdentities
? 'user_details.sso_connectors.connected'
: 'user_details.sso_connectors.not_connected'
)}
</div>
{hasLinkedSsoIdentities && (
{!isLoading && !error && (
<div className={styles.description}>
{t(
hasLinkedSsoIdentities
? 'user_details.sso_connectors.connected'
: 'user_details.sso_connectors.not_connected'
)}
</div>
)}
{(isLoading || hasLinkedSsoIdentities || error) && (
<Table
hasBorder
rowGroups={[{ key: 'ssoIdentity', data: displaySsoConnectors }]}