0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

refactor(console): list pages (#3753)

This commit is contained in:
Xiao Yijun 2023-04-28 12:05:43 +08:00 committed by GitHub
parent 467c6d8321
commit 858854b9b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 392 additions and 367 deletions

View file

@ -0,0 +1,59 @@
import classNames from 'classnames';
import { type ReactNode } from 'react';
import { type FieldValues, type FieldPath } from 'react-hook-form';
import Plus from '@/assets/images/plus.svg';
import { type Props as ButtonProps } from '@/components/Button';
import { type Props as CardTitleProps } from '@/components/CardTitle';
import PageMeta, { type Props as PageMetaProps } from '@/components/PageMeta';
import Table, { type Props as TableProps } from '@/components/Table';
import * as pageLayout from '@/scss/page-layout.module.scss';
import Button from '../Button';
import CardTitle from '../CardTitle';
type CreateButtonProps = {
title: ButtonProps['title'];
onClick: ButtonProps['onClick'];
};
type Props<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
title: CardTitleProps;
pageMeta?: PageMetaProps;
createButton?: CreateButtonProps;
subHeader?: ReactNode;
table: TableProps<TFieldValues, TName>;
widgets: ReactNode;
className?: string;
};
function ListPage<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
title,
pageMeta,
createButton,
subHeader,
table,
widgets,
className,
}: Props<TFieldValues, TName>) {
return (
<div className={classNames(pageLayout.container, className)}>
{pageMeta && <PageMeta {...pageMeta} />}
<div className={pageLayout.headline}>
<CardTitle {...title} />
{createButton && <Button icon={<Plus />} type="primary" size="large" {...createButton} />}
</div>
{subHeader}
<Table className={pageLayout.table} {...table} />
{widgets}
</div>
);
}
export default ListPage;

View file

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import { mainTitle } from '@/consts/tenants';
type Props = {
export type Props = {
titleKey: AdminConsoleKey | AdminConsoleKey[];
// eslint-disable-next-line react/boolean-prop-naming
trackPageView?: boolean;

View file

@ -15,7 +15,7 @@ import TableLoading from './TableLoading';
import * as styles from './index.module.scss';
import type { Column, RowGroup } from './types';
type Props<
export type Props<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {

View file

@ -3,7 +3,3 @@
.icon {
flex-shrink: 0;
}
.pagination {
margin-top: _.unit(4);
}

View file

@ -9,22 +9,16 @@ import useSWR from 'swr';
import ApiResourceDark from '@/assets/images/api-resource-dark.svg';
import ApiResource from '@/assets/images/api-resource.svg';
import Plus from '@/assets/images/plus.svg';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import CopyToClipboard from '@/components/CopyToClipboard';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import ItemPreview from '@/components/ItemPreview';
import PageMeta from '@/components/PageMeta';
import Pagination from '@/components/Pagination';
import Table from '@/components/Table';
import ListPage from '@/components/ListPage';
import { defaultPageSize } from '@/consts';
import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import useTheme from '@/hooks/use-theme';
import * as modalStyles from '@/scss/modal.module.scss';
import * as resourcesStyles from '@/scss/resources.module.scss';
import { buildUrl } from '@/utils/url';
import CreateForm from './components/CreateForm';
@ -60,22 +54,61 @@ function ApiResources() {
const ResourceIcon = theme === Theme.Light ? ApiResource : ApiResourceDark;
return (
<div className={resourcesStyles.container}>
<PageMeta titleKey="api_resources.page_title" />
<div className={resourcesStyles.headline}>
<CardTitle title="api_resources.title" subtitle="api_resources.subtitle" />
<Button
title="api_resources.create"
type="primary"
size="large"
icon={<Plus />}
onClick={() => {
navigate({
pathname: createApiResourcePathname,
search,
});
}}
/>
<ListPage
title={{
title: 'api_resources.title',
subtitle: 'api_resources.subtitle',
}}
pageMeta={{ titleKey: 'api_resources.page_title' }}
createButton={{
title: 'api_resources.create',
onClick: () => {
navigate({
pathname: createApiResourcePathname,
search,
});
},
}}
table={{
rowGroups: [{ key: 'apiResources', data: apiResources }],
rowIndexKey: 'id',
isLoading,
errorMessage: error?.body?.message ?? error?.message,
columns: [
{
title: t('api_resources.api_name'),
dataIndex: 'name',
colSpan: 6,
render: ({ id, name }) => (
<ItemPreview
title={name}
icon={<ResourceIcon className={styles.icon} />}
to={buildDetailsPathname(id)}
/>
),
},
{
title: t('api_resources.api_identifier'),
dataIndex: 'indicator',
colSpan: 10,
render: ({ indicator }) => <CopyToClipboard value={indicator} variant="text" />,
},
],
placeholder: <EmptyDataPlaceholder />,
rowClickHandler: ({ id }) => {
navigate(buildDetailsPathname(id));
},
onRetry: async () => mutate(undefined, true),
pagination: {
page,
totalCount,
pageSize,
onChange: (page) => {
updateSearchParameters({ page });
},
},
}}
widgets={
<Modal
shouldCloseOnEsc
isOpen={isCreateNew}
@ -105,49 +138,8 @@ function ApiResources() {
}}
/>
</Modal>
</div>
<Table
className={resourcesStyles.table}
rowGroups={[{ key: 'apiResources', data: apiResources }]}
rowIndexKey="id"
isLoading={isLoading}
errorMessage={error?.body?.message ?? error?.message}
columns={[
{
title: t('api_resources.api_name'),
dataIndex: 'name',
colSpan: 6,
render: ({ id, name }) => (
<ItemPreview
title={name}
icon={<ResourceIcon className={styles.icon} />}
to={buildDetailsPathname(id)}
/>
),
},
{
title: t('api_resources.api_identifier'),
dataIndex: 'indicator',
colSpan: 10,
render: ({ indicator }) => <CopyToClipboard value={indicator} variant="text" />,
},
]}
placeholder={<EmptyDataPlaceholder />}
rowClickHandler={({ id }) => {
navigate(buildDetailsPathname(id));
}}
onRetry={async () => mutate(undefined, true)}
/>
<Pagination
page={page}
totalCount={totalCount}
pageSize={pageSize}
className={styles.pagination}
onChange={(page) => {
updateSearchParameters({ page });
}}
/>
</div>
}
/>
);
}

View file

@ -4,10 +4,6 @@
flex-shrink: 0;
}
.pagination {
margin-top: _.unit(4);
}
.applicationName {
width: 360px;
}

View file

@ -5,19 +5,13 @@ import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import useSWR from 'swr';
import Plus from '@/assets/images/plus.svg';
import ApplicationIcon from '@/components/ApplicationIcon';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import CopyToClipboard from '@/components/CopyToClipboard';
import ItemPreview from '@/components/ItemPreview';
import PageMeta from '@/components/PageMeta';
import Pagination from '@/components/Pagination';
import Table from '@/components/Table';
import ListPage from '@/components/ListPage';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as resourcesStyles from '@/scss/resources.module.scss';
import { applicationTypeI18nKey } from '@/types/applications';
import { buildUrl } from '@/utils/url';
@ -63,30 +57,27 @@ function Applications() {
};
return (
<div className={resourcesStyles.container}>
<PageMeta titleKey="applications.title" />
<div className={resourcesStyles.headline}>
<CardTitle title="applications.title" subtitle="applications.subtitle" />
<Button
icon={<Plus />}
title="applications.create"
type="primary"
size="large"
onClick={() => {
navigate({
pathname: createApplicationPathname,
search,
});
}}
/>
</div>
<Table
className={resourcesStyles.table}
rowGroups={[{ key: 'applications', data: applications }]}
rowIndexKey="id"
isLoading={isLoading}
errorMessage={error?.body?.message ?? error?.message}
columns={[
<ListPage
title={{
title: 'applications.title',
subtitle: 'applications.subtitle',
}}
pageMeta={{ titleKey: 'applications.title' }}
createButton={{
title: 'applications.create',
onClick: () => {
navigate({
pathname: createApplicationPathname,
search,
});
},
}}
table={{
rowGroups: [{ key: 'applications', data: applications }],
rowIndexKey: 'id',
isLoading,
errorMessage: error?.body?.message ?? error?.message,
columns: [
{
title: t('applications.application_name'),
dataIndex: 'name',
@ -106,44 +97,45 @@ function Applications() {
colSpan: 10,
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
},
]}
placeholder={
],
placeholder: (
<ApplicationsPlaceholder
onCreate={async (newApp) => {
await mutateApplicationList(newApp);
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
}}
/>
}
rowClickHandler={({ id }) => {
),
rowClickHandler: ({ id }) => {
navigate(buildDetailsPathname(id));
}}
onRetry={async () => mutate(undefined, true)}
/>
<Pagination
page={page}
totalCount={totalCount}
pageSize={pageSize}
className={styles.pagination}
onChange={(page) => {
updateSearchParameters({ page });
}}
/>
<CreateForm
isOpen={isShowingCreationForm}
onClose={async (newApp) => {
if (newApp) {
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
},
onRetry: async () => mutate(undefined, true),
pagination: {
page,
totalCount,
pageSize,
onChange: (page) => {
updateSearchParameters({ page });
},
},
}}
widgets={
<CreateForm
isOpen={isShowingCreationForm}
onClose={async (newApp) => {
if (newApp) {
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
return;
}
navigate({
pathname: applicationsPathname,
search,
});
}}
/>
</div>
return;
}
navigate({
pathname: applicationsPathname,
search,
});
}}
/>
}
/>
);
}

View file

@ -3,16 +3,16 @@ import { withAppInsights } from '@logto/app-insights/react';
import AuditLogTable from '@/components/AuditLogTable';
import CardTitle from '@/components/CardTitle';
import PageMeta from '@/components/PageMeta';
import * as resourcesStyles from '@/scss/resources.module.scss';
import * as pageLayout from '@/scss/page-layout.module.scss';
function AuditLogs() {
return (
<div className={resourcesStyles.container}>
<div className={pageLayout.container}>
<PageMeta titleKey="logs.page_title" />
<div className={resourcesStyles.headline}>
<div className={pageLayout.headline}>
<CardTitle title="logs.title" subtitle="logs.subtitle" />
</div>
<AuditLogTable className={resourcesStyles.table} />
<AuditLogTable className={pageLayout.table} />
</div>
);
}

View file

@ -2,7 +2,6 @@ import { withAppInsights } from '@logto/app-insights/react';
import { ConnectorType } from '@logto/schemas';
import type { ConnectorFactoryResponse } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams } from 'react-router-dom';
@ -12,10 +11,8 @@ import Plus from '@/assets/images/plus.svg';
import SocialConnectorEmptyDark from '@/assets/images/social-connector-empty-dark.svg';
import SocialConnectorEmpty from '@/assets/images/social-connector-empty.svg';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import PageMeta from '@/components/PageMeta';
import ListPage from '@/components/ListPage';
import TabNav, { TabNavItem } from '@/components/TabNav';
import Table from '@/components/Table';
import TablePlaceholder from '@/components/Table/TablePlaceholder';
import { defaultEmailConnectorGroup, defaultSmsConnectorGroup } from '@/consts';
import { ConnectorsTabs } from '@/consts/page-tabs';
@ -23,7 +20,6 @@ import type { RequestError } from '@/hooks/use-api';
import useConnectorGroups from '@/hooks/use-connector-groups';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import DemoConnectorNotice from '@/onboarding/components/DemoConnectorNotice';
import * as resourcesStyles from '@/scss/resources.module.scss';
import ConnectorDeleteButton from './components/ConnectorDeleteButton';
import ConnectorName from './components/ConnectorName';
@ -102,126 +98,130 @@ function Connectors() {
}, [factoryId, factories]);
return (
<>
<PageMeta titleKey="connectors.page_title" />
<div className={classNames(resourcesStyles.container, styles.container)}>
<div className={resourcesStyles.headline}>
<CardTitle title="connectors.title" subtitle="connectors.subtitle" />
{isSocial && (
<Button
title="connectors.create"
type="primary"
size="large"
icon={<Plus />}
onClick={() => {
navigate(buildCreatePathname(ConnectorType.Social));
}}
/>
)}
</div>
<SignInExperienceSetupNotice />
<TabNav className={styles.tabs}>
<TabNavItem href={passwordlessPathname}>{t('connectors.tab_email_sms')}</TabNavItem>
<TabNavItem href={socialPathname}>{t('connectors.tab_social')}</TabNavItem>
</TabNav>
{hasDemoConnector && <DemoConnectorNotice />}
<Table
className={resourcesStyles.table}
rowIndexKey="id"
rowGroups={[{ key: 'connectors', data: connectors }]}
columns={[
{
title: t('connectors.connector_name'),
dataIndex: 'name',
colSpan: 6,
render: (connectorGroup) => (
<ConnectorName connectorGroup={connectorGroup} isDemo={connectorGroup.isDemo} />
),
},
{
title: t('connectors.connector_type'),
dataIndex: 'type',
colSpan: 5,
render: (connectorGroup) => <ConnectorTypeColumn connectorGroup={connectorGroup} />,
},
{
title: <ConnectorStatusField />,
dataIndex: 'status',
colSpan: 4,
render: (connectorGroup) => <ConnectorStatus connectorGroup={connectorGroup} />,
},
{
title: null,
dataIndex: 'delete',
colSpan: 1,
render: (connectorGroup) =>
connectorGroup.isDemo ? (
<ConnectorDeleteButton connectorGroup={connectorGroup} />
) : null,
},
]}
isRowClickable={({ connectors }) => Boolean(connectors[0]) && !connectors[0]?.isDemo}
rowClickHandler={({ connectors }) => {
const firstConnector = connectors[0];
if (!firstConnector) {
return;
}
const { type, id } = firstConnector;
navigate(
`${type === ConnectorType.Social ? socialPathname : passwordlessPathname}/${id}`
);
}}
isLoading={isLoading}
errorMessage={error?.body?.message ?? error?.message}
placeholder={
isSocial && (
<TablePlaceholder
image={<SocialConnectorEmpty />}
imageDark={<SocialConnectorEmptyDark />}
title="connectors.placeholder_title"
description="connectors.placeholder_description"
learnMoreLink={getDocumentationUrl(
'/docs/recipes/configure-connectors/configure-social-connector'
)}
action={
<Button
title="connectors.create"
type="primary"
size="large"
icon={<Plus />}
onClick={() => {
navigate(buildCreatePathname(ConnectorType.Social));
}}
/>
}
/>
)
}
onRetry={async () => mutate(undefined, true)}
/>
</div>
<CreateForm
isOpen={Boolean(createConnectorType)}
type={createConnectorType}
onClose={(id) => {
if (createConnectorType && id) {
navigate(buildGuidePathname(createConnectorType, id), { replace: true });
<ListPage
className={styles.container}
title={{
title: 'connectors.title',
subtitle: 'connectors.subtitle',
}}
pageMeta={{ titleKey: 'connectors.page_title' }}
createButton={conditional(
isSocial && {
title: 'connectors.create',
onClick: () => {
navigate(buildCreatePathname(ConnectorType.Social));
},
}
)}
subHeader={
<>
<SignInExperienceSetupNotice />
<TabNav className={styles.tabs}>
<TabNavItem href={passwordlessPathname}>{t('connectors.tab_email_sms')}</TabNavItem>
<TabNavItem href={socialPathname}>{t('connectors.tab_social')}</TabNavItem>
</TabNav>
{hasDemoConnector && <DemoConnectorNotice />}
</>
}
table={{
rowIndexKey: 'id',
rowGroups: [{ key: 'connectors', data: connectors }],
columns: [
{
title: t('connectors.connector_name'),
dataIndex: 'name',
colSpan: 6,
render: (connectorGroup) => (
<ConnectorName connectorGroup={connectorGroup} isDemo={connectorGroup.isDemo} />
),
},
{
title: t('connectors.connector_type'),
dataIndex: 'type',
colSpan: 5,
render: (connectorGroup) => <ConnectorTypeColumn connectorGroup={connectorGroup} />,
},
{
title: <ConnectorStatusField />,
dataIndex: 'status',
colSpan: 4,
render: (connectorGroup) => <ConnectorStatus connectorGroup={connectorGroup} />,
},
{
title: null,
dataIndex: 'delete',
colSpan: 1,
render: (connectorGroup) =>
connectorGroup.isDemo ? (
<ConnectorDeleteButton connectorGroup={connectorGroup} />
) : null,
},
],
isRowClickable: ({ connectors }) => Boolean(connectors[0]) && !connectors[0]?.isDemo,
rowClickHandler: ({ connectors }) => {
const firstConnector = connectors[0];
if (!firstConnector) {
return;
}
navigate(`${basePathname}/${tab}`);
}}
/>
<Guide
connector={connectorToShowInGuide}
onClose={() => {
navigate(`${basePathname}/${tab}`);
}}
/>
</>
const { type, id } = firstConnector;
navigate(
`${type === ConnectorType.Social ? socialPathname : passwordlessPathname}/${id}`
);
},
isLoading,
errorMessage: error?.body?.message ?? error?.message,
placeholder: conditional(
isSocial && (
<TablePlaceholder
image={<SocialConnectorEmpty />}
imageDark={<SocialConnectorEmptyDark />}
title="connectors.placeholder_title"
description="connectors.placeholder_description"
learnMoreLink={getDocumentationUrl(
'/docs/recipes/configure-connectors/configure-social-connector'
)}
action={
<Button
title="connectors.create"
type="primary"
size="large"
icon={<Plus />}
onClick={() => {
navigate(buildCreatePathname(ConnectorType.Social));
}}
/>
}
/>
)
),
onRetry: async () => mutate(undefined, true),
}}
widgets={
<>
<CreateForm
isOpen={Boolean(createConnectorType)}
type={createConnectorType}
onClose={(id) => {
if (createConnectorType && id) {
navigate(buildGuidePathname(createConnectorType, id), { replace: true });
return;
}
navigate(`${basePathname}/${tab}`);
}}
/>
<Guide
connector={connectorToShowInGuide}
onClose={() => {
navigate(`${basePathname}/${tab}`);
}}
/>
</>
}
/>
);
}

View file

@ -16,7 +16,7 @@ import { useStaticApi } from '@/hooks/use-api';
import useCurrentUser from '@/hooks/use-current-user';
import useSwrFetcher from '@/hooks/use-swr-fetcher';
import useUserAssetsService from '@/hooks/use-user-assets-service';
import * as resourcesStyles from '@/scss/resources.module.scss';
import * as pageLayout from '@/scss/page-layout.module.scss';
import BasicUserInfoSection from './components/BasicUserInfoSection';
import CardContent from './components/CardContent';
@ -43,9 +43,9 @@ function Profile() {
const showLoadingSkeleton = isLoadingUser || isLoadingConnectors || isUserAssetServiceLoading;
return (
<div className={resourcesStyles.container}>
<div className={pageLayout.container}>
<PageMeta titleKey="profile.page_title" />
<div className={resourcesStyles.headline}>
<div className={pageLayout.headline}>
<CardTitle title="profile.title" subtitle="profile.description" />
</div>
{showLoadingSkeleton && <Skeleton />}

View file

@ -9,17 +9,14 @@ import Plus from '@/assets/images/plus.svg';
import RolesEmptyDark from '@/assets/images/roles-empty-dark.svg';
import RolesEmpty from '@/assets/images/roles-empty.svg';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import ItemPreview from '@/components/ItemPreview';
import PageMeta from '@/components/PageMeta';
import ListPage from '@/components/ListPage';
import Search from '@/components/Search';
import Table from '@/components/Table';
import TablePlaceholder from '@/components/Table/TablePlaceholder';
import { defaultPageSize } from '@/consts';
import type { RequestError } from '@/hooks/use-api';
import useDocumentationUrl from '@/hooks/use-documentation-url';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as pageStyles from '@/scss/resources.module.scss';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import AssignedUsers from './components/AssignedUsers';
@ -57,31 +54,26 @@ function Roles() {
const [roles, totalCount] = data ?? [];
return (
<div className={pageStyles.container}>
<PageMeta titleKey="roles.page_title" />
<div className={pageStyles.headline}>
<CardTitle
title="roles.title"
subtitle="roles.subtitle"
learnMoreLink="https://docs.logto.io/docs/recipes/rbac/manage-permissions-and-roles#manage-roles"
/>
<Button
icon={<Plus />}
title="roles.create"
type="primary"
size="large"
onClick={() => {
navigate({ pathname: createRolePathname, search });
}}
/>
</div>
<Table
className={pageStyles.table}
rowGroups={[{ key: 'roles', data: roles }]}
rowIndexKey="id"
isLoading={isLoading}
errorMessage={error?.body?.message ?? error?.message}
columns={[
<ListPage
title={{
title: 'roles.title',
subtitle: 'roles.subtitle',
learnMoreLink:
'https://docs.logto.io/docs/recipes/rbac/manage-permissions-and-roles#manage-roles',
}}
pageMeta={{ titleKey: 'roles.page_title' }}
createButton={{
title: 'roles.create',
onClick: () => {
navigate({ pathname: createRolePathname, search });
},
}}
table={{
rowGroups: [{ key: 'roles', data: roles }],
rowIndexKey: 'id',
isLoading,
errorMessage: error?.body?.message ?? error?.message,
columns: [
{
title: t('roles.role_name'),
dataIndex: 'name',
@ -102,11 +94,11 @@ function Roles() {
<AssignedUsers users={featuredUsers} count={usersCount} />
),
},
]}
rowClickHandler={({ id }) => {
],
rowClickHandler: ({ id }) => {
navigate(buildDetailsPathname(id));
}}
filter={
},
filter: (
<Search
inputClassName={styles.search}
placeholder={t('roles.search')}
@ -119,16 +111,16 @@ function Roles() {
updateSearchParameters({ keyword: '', page: 1 });
}}
/>
}
pagination={{
),
pagination: {
page,
totalCount,
pageSize,
onChange: (page) => {
updateSearchParameters({ page });
},
}}
placeholder={
},
placeholder: (
<TablePlaceholder
image={<RolesEmpty />}
imageDark={<RolesEmptyDark />}
@ -149,17 +141,19 @@ function Roles() {
/>
}
/>
}
onRetry={async () => mutate(undefined, true)}
/>
{isOnCreatePage && (
<CreateRoleModal
onClose={() => {
navigate({ pathname: rolesPathname, search });
}}
/>
)}
</div>
),
onRetry: async () => mutate(undefined, true),
}}
widgets={
isOnCreatePage && (
<CreateRoleModal
onClose={() => {
navigate({ pathname: rolesPathname, search });
}}
/>
)
}
/>
);
}

View file

@ -10,19 +10,16 @@ import UsersEmptyDark from '@/assets/images/users-empty-dark.svg';
import UsersEmpty from '@/assets/images/users-empty.svg';
import ApplicationName from '@/components/ApplicationName';
import Button from '@/components/Button';
import CardTitle from '@/components/CardTitle';
import DateTime from '@/components/DateTime';
import ItemPreview from '@/components/ItemPreview';
import PageMeta from '@/components/PageMeta';
import ListPage from '@/components/ListPage';
import Search from '@/components/Search';
import Table from '@/components/Table';
import TablePlaceholder from '@/components/Table/TablePlaceholder';
import UserAvatar from '@/components/UserAvatar';
import { defaultPageSize } from '@/consts';
import { UserDetailsTabs } from '@/consts/page-tabs';
import type { RequestError } from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import * as resourcesStyles from '@/scss/resources.module.scss';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import { getUserTitle, getUserSubtitle } from '@/utils/user';
@ -57,43 +54,27 @@ function Users() {
const [users, totalCount] = data ?? [];
return (
<div className={resourcesStyles.container}>
<PageMeta titleKey="users.page_title" />
<div className={resourcesStyles.headline}>
<CardTitle title="users.title" subtitle="users.subtitle" />
<Button
title="users.create"
size="large"
type="primary"
icon={<Plus />}
onClick={() => {
navigate({
pathname: createUserPathname,
search,
});
}}
/>
{isCreateNew && (
<CreateForm
onClose={() => {
navigate({
pathname: usersPathname,
search,
});
}}
onCreate={() => {
void mutate();
}}
/>
)}
</div>
<Table
className={resourcesStyles.table}
rowGroups={[{ key: 'users', data: users }]}
rowIndexKey="id"
isLoading={isLoading}
errorMessage={error?.body?.message ?? error?.message}
columns={[
<ListPage
title={{
title: 'users.title',
subtitle: 'users.subtitle',
}}
pageMeta={{ titleKey: 'users.page_title' }}
createButton={{
title: 'users.create',
onClick: () => {
navigate({
pathname: createUserPathname,
search,
});
},
}}
table={{
rowGroups: [{ key: 'users', data: users }],
rowIndexKey: 'id',
isLoading,
errorMessage: error?.body?.message ?? error?.message,
columns: [
{
title: t('users.user_name'),
dataIndex: 'name',
@ -125,8 +106,8 @@ function Users() {
colSpan: 5,
render: ({ lastSignInAt }) => <DateTime>{lastSignInAt}</DateTime>,
},
]}
filter={
],
filter: (
<Search
inputClassName={styles.searchInput}
placeholder={t('users.search')}
@ -139,8 +120,8 @@ function Users() {
updateSearchParameters({ keyword: '', page: 1 });
}}
/>
}
placeholder={
),
placeholder: (
<TablePlaceholder
image={<UsersEmpty />}
imageDark={<UsersEmptyDark />}
@ -161,21 +142,36 @@ function Users() {
/>
}
/>
}
rowClickHandler={({ id }) => {
),
rowClickHandler: ({ id }) => {
navigate(buildDetailsPathname(id));
}}
pagination={{
},
pagination: {
page,
pageSize,
totalCount,
onChange: (page) => {
updateSearchParameters({ page });
},
}}
onRetry={async () => mutate(undefined, true)}
/>
</div>
},
onRetry: async () => mutate(undefined, true),
}}
widgets={
isCreateNew && (
<CreateForm
onClose={() => {
navigate({
pathname: usersPathname,
search,
});
}}
onCreate={() => {
void mutate();
}}
/>
)
}
/>
);
}