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:
parent
3da6694fef
commit
3a6985a84e
10 changed files with 79 additions and 68 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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} />
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 />}
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -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`}>
|
||||
|
|
|
@ -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 }]}
|
||||
|
|
|
@ -81,16 +81,20 @@ function UserSocialIdentities({ userId, identities, onDelete }: Props) {
|
|||
});
|
||||
}, [connectorGroups, identities, t]);
|
||||
|
||||
const hasConnectors = Boolean(displayConnectors && displayConnectors.length > 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isLoading && !error && (
|
||||
<div className={styles.description}>
|
||||
{t(
|
||||
displayConnectors && displayConnectors.length > 0
|
||||
hasConnectors
|
||||
? 'user_details.connectors.connected'
|
||||
: 'user_details.connectors.not_connected'
|
||||
)}
|
||||
</div>
|
||||
{displayConnectors && displayConnectors.length > 0 && (
|
||||
)}
|
||||
{(isLoading || hasConnectors || error) && (
|
||||
<Table
|
||||
hasBorder
|
||||
rowGroups={[{ key: 'identities', data: displayConnectors }]}
|
||||
|
|
|
@ -55,10 +55,11 @@ function UserSsoIdentities({ ssoIdentities }: Props) {
|
|||
);
|
||||
}, [data, ssoIdentities]);
|
||||
|
||||
const hasLinkedSsoIdentities = displaySsoConnectors && displaySsoConnectors.length > 0;
|
||||
const hasLinkedSsoIdentities = Boolean(displaySsoConnectors && displaySsoConnectors.length > 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isLoading && !error && (
|
||||
<div className={styles.description}>
|
||||
{t(
|
||||
hasLinkedSsoIdentities
|
||||
|
@ -66,7 +67,8 @@ function UserSsoIdentities({ ssoIdentities }: Props) {
|
|||
: 'user_details.sso_connectors.not_connected'
|
||||
)}
|
||||
</div>
|
||||
{hasLinkedSsoIdentities && (
|
||||
)}
|
||||
{(isLoading || hasLinkedSsoIdentities || error) && (
|
||||
<Table
|
||||
hasBorder
|
||||
rowGroups={[{ key: 'ssoIdentity', data: displaySsoConnectors }]}
|
||||
|
|
Loading…
Add table
Reference in a new issue