mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
refactor(console): list pages (#3753)
This commit is contained in:
parent
467c6d8321
commit
858854b9b1
13 changed files with 392 additions and 367 deletions
59
packages/console/src/components/ListPage/index.tsx
Normal file
59
packages/console/src/components/ListPage/index.tsx
Normal 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;
|
|
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { mainTitle } from '@/consts/tenants';
|
import { mainTitle } from '@/consts/tenants';
|
||||||
|
|
||||||
type Props = {
|
export type Props = {
|
||||||
titleKey: AdminConsoleKey | AdminConsoleKey[];
|
titleKey: AdminConsoleKey | AdminConsoleKey[];
|
||||||
// eslint-disable-next-line react/boolean-prop-naming
|
// eslint-disable-next-line react/boolean-prop-naming
|
||||||
trackPageView?: boolean;
|
trackPageView?: boolean;
|
||||||
|
|
|
@ -15,7 +15,7 @@ import TableLoading from './TableLoading';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
import type { Column, RowGroup } from './types';
|
import type { Column, RowGroup } from './types';
|
||||||
|
|
||||||
type Props<
|
export type Props<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
> = {
|
> = {
|
||||||
|
|
|
@ -3,7 +3,3 @@
|
||||||
.icon {
|
.icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: _.unit(4);
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,22 +9,16 @@ import useSWR from 'swr';
|
||||||
|
|
||||||
import ApiResourceDark from '@/assets/images/api-resource-dark.svg';
|
import ApiResourceDark from '@/assets/images/api-resource-dark.svg';
|
||||||
import ApiResource from '@/assets/images/api-resource.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 CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||||
import ItemPreview from '@/components/ItemPreview';
|
import ItemPreview from '@/components/ItemPreview';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import ListPage from '@/components/ListPage';
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import { defaultPageSize } from '@/consts';
|
import { defaultPageSize } from '@/consts';
|
||||||
import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
|
import { ApiResourceDetailsTabs } from '@/consts/page-tabs';
|
||||||
import type { RequestError } from '@/hooks/use-api';
|
import type { RequestError } from '@/hooks/use-api';
|
||||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||||
import useTheme from '@/hooks/use-theme';
|
import useTheme from '@/hooks/use-theme';
|
||||||
import * as modalStyles from '@/scss/modal.module.scss';
|
import * as modalStyles from '@/scss/modal.module.scss';
|
||||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
|
||||||
import { buildUrl } from '@/utils/url';
|
import { buildUrl } from '@/utils/url';
|
||||||
|
|
||||||
import CreateForm from './components/CreateForm';
|
import CreateForm from './components/CreateForm';
|
||||||
|
@ -60,22 +54,61 @@ function ApiResources() {
|
||||||
const ResourceIcon = theme === Theme.Light ? ApiResource : ApiResourceDark;
|
const ResourceIcon = theme === Theme.Light ? ApiResource : ApiResourceDark;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={resourcesStyles.container}>
|
<ListPage
|
||||||
<PageMeta titleKey="api_resources.page_title" />
|
title={{
|
||||||
<div className={resourcesStyles.headline}>
|
title: 'api_resources.title',
|
||||||
<CardTitle title="api_resources.title" subtitle="api_resources.subtitle" />
|
subtitle: 'api_resources.subtitle',
|
||||||
<Button
|
}}
|
||||||
title="api_resources.create"
|
pageMeta={{ titleKey: 'api_resources.page_title' }}
|
||||||
type="primary"
|
createButton={{
|
||||||
size="large"
|
title: 'api_resources.create',
|
||||||
icon={<Plus />}
|
onClick: () => {
|
||||||
onClick={() => {
|
navigate({
|
||||||
navigate({
|
pathname: createApiResourcePathname,
|
||||||
pathname: createApiResourcePathname,
|
search,
|
||||||
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
|
<Modal
|
||||||
shouldCloseOnEsc
|
shouldCloseOnEsc
|
||||||
isOpen={isCreateNew}
|
isOpen={isCreateNew}
|
||||||
|
@ -105,49 +138,8 @@ function ApiResources() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,6 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: _.unit(4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.applicationName {
|
.applicationName {
|
||||||
width: 360px;
|
width: 360px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,19 +5,13 @@ import { useTranslation } from 'react-i18next';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import Plus from '@/assets/images/plus.svg';
|
|
||||||
import ApplicationIcon from '@/components/ApplicationIcon';
|
import ApplicationIcon from '@/components/ApplicationIcon';
|
||||||
import Button from '@/components/Button';
|
|
||||||
import CardTitle from '@/components/CardTitle';
|
|
||||||
import CopyToClipboard from '@/components/CopyToClipboard';
|
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
import ItemPreview from '@/components/ItemPreview';
|
import ItemPreview from '@/components/ItemPreview';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import ListPage from '@/components/ListPage';
|
||||||
import Pagination from '@/components/Pagination';
|
|
||||||
import Table from '@/components/Table';
|
|
||||||
import { defaultPageSize } from '@/consts';
|
import { defaultPageSize } from '@/consts';
|
||||||
import type { RequestError } from '@/hooks/use-api';
|
import type { RequestError } from '@/hooks/use-api';
|
||||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
|
||||||
import { applicationTypeI18nKey } from '@/types/applications';
|
import { applicationTypeI18nKey } from '@/types/applications';
|
||||||
import { buildUrl } from '@/utils/url';
|
import { buildUrl } from '@/utils/url';
|
||||||
|
|
||||||
|
@ -63,30 +57,27 @@ function Applications() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={resourcesStyles.container}>
|
<ListPage
|
||||||
<PageMeta titleKey="applications.title" />
|
title={{
|
||||||
<div className={resourcesStyles.headline}>
|
title: 'applications.title',
|
||||||
<CardTitle title="applications.title" subtitle="applications.subtitle" />
|
subtitle: 'applications.subtitle',
|
||||||
<Button
|
}}
|
||||||
icon={<Plus />}
|
pageMeta={{ titleKey: 'applications.title' }}
|
||||||
title="applications.create"
|
createButton={{
|
||||||
type="primary"
|
title: 'applications.create',
|
||||||
size="large"
|
onClick: () => {
|
||||||
onClick={() => {
|
navigate({
|
||||||
navigate({
|
pathname: createApplicationPathname,
|
||||||
pathname: createApplicationPathname,
|
search,
|
||||||
search,
|
});
|
||||||
});
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
table={{
|
||||||
</div>
|
rowGroups: [{ key: 'applications', data: applications }],
|
||||||
<Table
|
rowIndexKey: 'id',
|
||||||
className={resourcesStyles.table}
|
isLoading,
|
||||||
rowGroups={[{ key: 'applications', data: applications }]}
|
errorMessage: error?.body?.message ?? error?.message,
|
||||||
rowIndexKey="id"
|
columns: [
|
||||||
isLoading={isLoading}
|
|
||||||
errorMessage={error?.body?.message ?? error?.message}
|
|
||||||
columns={[
|
|
||||||
{
|
{
|
||||||
title: t('applications.application_name'),
|
title: t('applications.application_name'),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
@ -106,44 +97,45 @@ function Applications() {
|
||||||
colSpan: 10,
|
colSpan: 10,
|
||||||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
||||||
},
|
},
|
||||||
]}
|
],
|
||||||
placeholder={
|
placeholder: (
|
||||||
<ApplicationsPlaceholder
|
<ApplicationsPlaceholder
|
||||||
onCreate={async (newApp) => {
|
onCreate={async (newApp) => {
|
||||||
await mutateApplicationList(newApp);
|
await mutateApplicationList(newApp);
|
||||||
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
|
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
rowClickHandler={({ id }) => {
|
rowClickHandler: ({ id }) => {
|
||||||
navigate(buildDetailsPathname(id));
|
navigate(buildDetailsPathname(id));
|
||||||
}}
|
},
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry: async () => mutate(undefined, true),
|
||||||
/>
|
pagination: {
|
||||||
<Pagination
|
page,
|
||||||
page={page}
|
totalCount,
|
||||||
totalCount={totalCount}
|
pageSize,
|
||||||
pageSize={pageSize}
|
onChange: (page) => {
|
||||||
className={styles.pagination}
|
updateSearchParameters({ page });
|
||||||
onChange={(page) => {
|
},
|
||||||
updateSearchParameters({ page });
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
widgets={
|
||||||
<CreateForm
|
<CreateForm
|
||||||
isOpen={isShowingCreationForm}
|
isOpen={isShowingCreationForm}
|
||||||
onClose={async (newApp) => {
|
onClose={async (newApp) => {
|
||||||
if (newApp) {
|
if (newApp) {
|
||||||
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
|
navigate(buildNavigatePathPostAppCreation(newApp), { replace: true });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigate({
|
navigate({
|
||||||
pathname: applicationsPathname,
|
pathname: applicationsPathname,
|
||||||
search,
|
search,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,16 +3,16 @@ import { withAppInsights } from '@logto/app-insights/react';
|
||||||
import AuditLogTable from '@/components/AuditLogTable';
|
import AuditLogTable from '@/components/AuditLogTable';
|
||||||
import CardTitle from '@/components/CardTitle';
|
import CardTitle from '@/components/CardTitle';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import PageMeta from '@/components/PageMeta';
|
||||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
import * as pageLayout from '@/scss/page-layout.module.scss';
|
||||||
|
|
||||||
function AuditLogs() {
|
function AuditLogs() {
|
||||||
return (
|
return (
|
||||||
<div className={resourcesStyles.container}>
|
<div className={pageLayout.container}>
|
||||||
<PageMeta titleKey="logs.page_title" />
|
<PageMeta titleKey="logs.page_title" />
|
||||||
<div className={resourcesStyles.headline}>
|
<div className={pageLayout.headline}>
|
||||||
<CardTitle title="logs.title" subtitle="logs.subtitle" />
|
<CardTitle title="logs.title" subtitle="logs.subtitle" />
|
||||||
</div>
|
</div>
|
||||||
<AuditLogTable className={resourcesStyles.table} />
|
<AuditLogTable className={pageLayout.table} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { withAppInsights } from '@logto/app-insights/react';
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType } from '@logto/schemas';
|
||||||
import type { ConnectorFactoryResponse } from '@logto/schemas';
|
import type { ConnectorFactoryResponse } from '@logto/schemas';
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
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 SocialConnectorEmptyDark from '@/assets/images/social-connector-empty-dark.svg';
|
||||||
import SocialConnectorEmpty from '@/assets/images/social-connector-empty.svg';
|
import SocialConnectorEmpty from '@/assets/images/social-connector-empty.svg';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CardTitle from '@/components/CardTitle';
|
import ListPage from '@/components/ListPage';
|
||||||
import PageMeta from '@/components/PageMeta';
|
|
||||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||||
import Table from '@/components/Table';
|
|
||||||
import TablePlaceholder from '@/components/Table/TablePlaceholder';
|
import TablePlaceholder from '@/components/Table/TablePlaceholder';
|
||||||
import { defaultEmailConnectorGroup, defaultSmsConnectorGroup } from '@/consts';
|
import { defaultEmailConnectorGroup, defaultSmsConnectorGroup } from '@/consts';
|
||||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
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 useConnectorGroups from '@/hooks/use-connector-groups';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import DemoConnectorNotice from '@/onboarding/components/DemoConnectorNotice';
|
import DemoConnectorNotice from '@/onboarding/components/DemoConnectorNotice';
|
||||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
|
||||||
|
|
||||||
import ConnectorDeleteButton from './components/ConnectorDeleteButton';
|
import ConnectorDeleteButton from './components/ConnectorDeleteButton';
|
||||||
import ConnectorName from './components/ConnectorName';
|
import ConnectorName from './components/ConnectorName';
|
||||||
|
@ -102,126 +98,130 @@ function Connectors() {
|
||||||
}, [factoryId, factories]);
|
}, [factoryId, factories]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ListPage
|
||||||
<PageMeta titleKey="connectors.page_title" />
|
className={styles.container}
|
||||||
<div className={classNames(resourcesStyles.container, styles.container)}>
|
title={{
|
||||||
<div className={resourcesStyles.headline}>
|
title: 'connectors.title',
|
||||||
<CardTitle title="connectors.title" subtitle="connectors.subtitle" />
|
subtitle: 'connectors.subtitle',
|
||||||
{isSocial && (
|
}}
|
||||||
<Button
|
pageMeta={{ titleKey: 'connectors.page_title' }}
|
||||||
title="connectors.create"
|
createButton={conditional(
|
||||||
type="primary"
|
isSocial && {
|
||||||
size="large"
|
title: 'connectors.create',
|
||||||
icon={<Plus />}
|
onClick: () => {
|
||||||
onClick={() => {
|
navigate(buildCreatePathname(ConnectorType.Social));
|
||||||
navigate(buildCreatePathname(ConnectorType.Social));
|
},
|
||||||
}}
|
}
|
||||||
/>
|
)}
|
||||||
)}
|
subHeader={
|
||||||
</div>
|
<>
|
||||||
<SignInExperienceSetupNotice />
|
<SignInExperienceSetupNotice />
|
||||||
<TabNav className={styles.tabs}>
|
<TabNav className={styles.tabs}>
|
||||||
<TabNavItem href={passwordlessPathname}>{t('connectors.tab_email_sms')}</TabNavItem>
|
<TabNavItem href={passwordlessPathname}>{t('connectors.tab_email_sms')}</TabNavItem>
|
||||||
<TabNavItem href={socialPathname}>{t('connectors.tab_social')}</TabNavItem>
|
<TabNavItem href={socialPathname}>{t('connectors.tab_social')}</TabNavItem>
|
||||||
</TabNav>
|
</TabNav>
|
||||||
{hasDemoConnector && <DemoConnectorNotice />}
|
{hasDemoConnector && <DemoConnectorNotice />}
|
||||||
<Table
|
</>
|
||||||
className={resourcesStyles.table}
|
}
|
||||||
rowIndexKey="id"
|
table={{
|
||||||
rowGroups={[{ key: 'connectors', data: connectors }]}
|
rowIndexKey: 'id',
|
||||||
columns={[
|
rowGroups: [{ key: 'connectors', data: connectors }],
|
||||||
{
|
columns: [
|
||||||
title: t('connectors.connector_name'),
|
{
|
||||||
dataIndex: 'name',
|
title: t('connectors.connector_name'),
|
||||||
colSpan: 6,
|
dataIndex: 'name',
|
||||||
render: (connectorGroup) => (
|
colSpan: 6,
|
||||||
<ConnectorName connectorGroup={connectorGroup} isDemo={connectorGroup.isDemo} />
|
render: (connectorGroup) => (
|
||||||
),
|
<ConnectorName connectorGroup={connectorGroup} isDemo={connectorGroup.isDemo} />
|
||||||
},
|
),
|
||||||
{
|
},
|
||||||
title: t('connectors.connector_type'),
|
{
|
||||||
dataIndex: 'type',
|
title: t('connectors.connector_type'),
|
||||||
colSpan: 5,
|
dataIndex: 'type',
|
||||||
render: (connectorGroup) => <ConnectorTypeColumn connectorGroup={connectorGroup} />,
|
colSpan: 5,
|
||||||
},
|
render: (connectorGroup) => <ConnectorTypeColumn connectorGroup={connectorGroup} />,
|
||||||
{
|
},
|
||||||
title: <ConnectorStatusField />,
|
{
|
||||||
dataIndex: 'status',
|
title: <ConnectorStatusField />,
|
||||||
colSpan: 4,
|
dataIndex: 'status',
|
||||||
render: (connectorGroup) => <ConnectorStatus connectorGroup={connectorGroup} />,
|
colSpan: 4,
|
||||||
},
|
render: (connectorGroup) => <ConnectorStatus connectorGroup={connectorGroup} />,
|
||||||
{
|
},
|
||||||
title: null,
|
{
|
||||||
dataIndex: 'delete',
|
title: null,
|
||||||
colSpan: 1,
|
dataIndex: 'delete',
|
||||||
render: (connectorGroup) =>
|
colSpan: 1,
|
||||||
connectorGroup.isDemo ? (
|
render: (connectorGroup) =>
|
||||||
<ConnectorDeleteButton connectorGroup={connectorGroup} />
|
connectorGroup.isDemo ? (
|
||||||
) : null,
|
<ConnectorDeleteButton connectorGroup={connectorGroup} />
|
||||||
},
|
) : null,
|
||||||
]}
|
},
|
||||||
isRowClickable={({ connectors }) => Boolean(connectors[0]) && !connectors[0]?.isDemo}
|
],
|
||||||
rowClickHandler={({ connectors }) => {
|
isRowClickable: ({ connectors }) => Boolean(connectors[0]) && !connectors[0]?.isDemo,
|
||||||
const firstConnector = connectors[0];
|
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 });
|
|
||||||
|
|
||||||
|
if (!firstConnector) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigate(`${basePathname}/${tab}`);
|
|
||||||
}}
|
const { type, id } = firstConnector;
|
||||||
/>
|
|
||||||
<Guide
|
navigate(
|
||||||
connector={connectorToShowInGuide}
|
`${type === ConnectorType.Social ? socialPathname : passwordlessPathname}/${id}`
|
||||||
onClose={() => {
|
);
|
||||||
navigate(`${basePathname}/${tab}`);
|
},
|
||||||
}}
|
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}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import { useStaticApi } from '@/hooks/use-api';
|
||||||
import useCurrentUser from '@/hooks/use-current-user';
|
import useCurrentUser from '@/hooks/use-current-user';
|
||||||
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
||||||
import useUserAssetsService from '@/hooks/use-user-assets-service';
|
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 BasicUserInfoSection from './components/BasicUserInfoSection';
|
||||||
import CardContent from './components/CardContent';
|
import CardContent from './components/CardContent';
|
||||||
|
@ -43,9 +43,9 @@ function Profile() {
|
||||||
const showLoadingSkeleton = isLoadingUser || isLoadingConnectors || isUserAssetServiceLoading;
|
const showLoadingSkeleton = isLoadingUser || isLoadingConnectors || isUserAssetServiceLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={resourcesStyles.container}>
|
<div className={pageLayout.container}>
|
||||||
<PageMeta titleKey="profile.page_title" />
|
<PageMeta titleKey="profile.page_title" />
|
||||||
<div className={resourcesStyles.headline}>
|
<div className={pageLayout.headline}>
|
||||||
<CardTitle title="profile.title" subtitle="profile.description" />
|
<CardTitle title="profile.title" subtitle="profile.description" />
|
||||||
</div>
|
</div>
|
||||||
{showLoadingSkeleton && <Skeleton />}
|
{showLoadingSkeleton && <Skeleton />}
|
||||||
|
|
|
@ -9,17 +9,14 @@ import Plus from '@/assets/images/plus.svg';
|
||||||
import RolesEmptyDark from '@/assets/images/roles-empty-dark.svg';
|
import RolesEmptyDark from '@/assets/images/roles-empty-dark.svg';
|
||||||
import RolesEmpty from '@/assets/images/roles-empty.svg';
|
import RolesEmpty from '@/assets/images/roles-empty.svg';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CardTitle from '@/components/CardTitle';
|
|
||||||
import ItemPreview from '@/components/ItemPreview';
|
import ItemPreview from '@/components/ItemPreview';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import ListPage from '@/components/ListPage';
|
||||||
import Search from '@/components/Search';
|
import Search from '@/components/Search';
|
||||||
import Table from '@/components/Table';
|
|
||||||
import TablePlaceholder from '@/components/Table/TablePlaceholder';
|
import TablePlaceholder from '@/components/Table/TablePlaceholder';
|
||||||
import { defaultPageSize } from '@/consts';
|
import { defaultPageSize } from '@/consts';
|
||||||
import type { RequestError } from '@/hooks/use-api';
|
import type { RequestError } from '@/hooks/use-api';
|
||||||
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
import useDocumentationUrl from '@/hooks/use-documentation-url';
|
||||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||||
import * as pageStyles from '@/scss/resources.module.scss';
|
|
||||||
import { buildUrl, formatSearchKeyword } from '@/utils/url';
|
import { buildUrl, formatSearchKeyword } from '@/utils/url';
|
||||||
|
|
||||||
import AssignedUsers from './components/AssignedUsers';
|
import AssignedUsers from './components/AssignedUsers';
|
||||||
|
@ -57,31 +54,26 @@ function Roles() {
|
||||||
const [roles, totalCount] = data ?? [];
|
const [roles, totalCount] = data ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pageStyles.container}>
|
<ListPage
|
||||||
<PageMeta titleKey="roles.page_title" />
|
title={{
|
||||||
<div className={pageStyles.headline}>
|
title: 'roles.title',
|
||||||
<CardTitle
|
subtitle: 'roles.subtitle',
|
||||||
title="roles.title"
|
learnMoreLink:
|
||||||
subtitle="roles.subtitle"
|
'https://docs.logto.io/docs/recipes/rbac/manage-permissions-and-roles#manage-roles',
|
||||||
learnMoreLink="https://docs.logto.io/docs/recipes/rbac/manage-permissions-and-roles#manage-roles"
|
}}
|
||||||
/>
|
pageMeta={{ titleKey: 'roles.page_title' }}
|
||||||
<Button
|
createButton={{
|
||||||
icon={<Plus />}
|
title: 'roles.create',
|
||||||
title="roles.create"
|
onClick: () => {
|
||||||
type="primary"
|
navigate({ pathname: createRolePathname, search });
|
||||||
size="large"
|
},
|
||||||
onClick={() => {
|
}}
|
||||||
navigate({ pathname: createRolePathname, search });
|
table={{
|
||||||
}}
|
rowGroups: [{ key: 'roles', data: roles }],
|
||||||
/>
|
rowIndexKey: 'id',
|
||||||
</div>
|
isLoading,
|
||||||
<Table
|
errorMessage: error?.body?.message ?? error?.message,
|
||||||
className={pageStyles.table}
|
columns: [
|
||||||
rowGroups={[{ key: 'roles', data: roles }]}
|
|
||||||
rowIndexKey="id"
|
|
||||||
isLoading={isLoading}
|
|
||||||
errorMessage={error?.body?.message ?? error?.message}
|
|
||||||
columns={[
|
|
||||||
{
|
{
|
||||||
title: t('roles.role_name'),
|
title: t('roles.role_name'),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
@ -102,11 +94,11 @@ function Roles() {
|
||||||
<AssignedUsers users={featuredUsers} count={usersCount} />
|
<AssignedUsers users={featuredUsers} count={usersCount} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
],
|
||||||
rowClickHandler={({ id }) => {
|
rowClickHandler: ({ id }) => {
|
||||||
navigate(buildDetailsPathname(id));
|
navigate(buildDetailsPathname(id));
|
||||||
}}
|
},
|
||||||
filter={
|
filter: (
|
||||||
<Search
|
<Search
|
||||||
inputClassName={styles.search}
|
inputClassName={styles.search}
|
||||||
placeholder={t('roles.search')}
|
placeholder={t('roles.search')}
|
||||||
|
@ -119,16 +111,16 @@ function Roles() {
|
||||||
updateSearchParameters({ keyword: '', page: 1 });
|
updateSearchParameters({ keyword: '', page: 1 });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
pagination={{
|
pagination: {
|
||||||
page,
|
page,
|
||||||
totalCount,
|
totalCount,
|
||||||
pageSize,
|
pageSize,
|
||||||
onChange: (page) => {
|
onChange: (page) => {
|
||||||
updateSearchParameters({ page });
|
updateSearchParameters({ page });
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
placeholder={
|
placeholder: (
|
||||||
<TablePlaceholder
|
<TablePlaceholder
|
||||||
image={<RolesEmpty />}
|
image={<RolesEmpty />}
|
||||||
imageDark={<RolesEmptyDark />}
|
imageDark={<RolesEmptyDark />}
|
||||||
|
@ -149,17 +141,19 @@ function Roles() {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry: async () => mutate(undefined, true),
|
||||||
/>
|
}}
|
||||||
{isOnCreatePage && (
|
widgets={
|
||||||
<CreateRoleModal
|
isOnCreatePage && (
|
||||||
onClose={() => {
|
<CreateRoleModal
|
||||||
navigate({ pathname: rolesPathname, search });
|
onClose={() => {
|
||||||
}}
|
navigate({ pathname: rolesPathname, search });
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
</div>
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,19 +10,16 @@ import UsersEmptyDark from '@/assets/images/users-empty-dark.svg';
|
||||||
import UsersEmpty from '@/assets/images/users-empty.svg';
|
import UsersEmpty from '@/assets/images/users-empty.svg';
|
||||||
import ApplicationName from '@/components/ApplicationName';
|
import ApplicationName from '@/components/ApplicationName';
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import CardTitle from '@/components/CardTitle';
|
|
||||||
import DateTime from '@/components/DateTime';
|
import DateTime from '@/components/DateTime';
|
||||||
import ItemPreview from '@/components/ItemPreview';
|
import ItemPreview from '@/components/ItemPreview';
|
||||||
import PageMeta from '@/components/PageMeta';
|
import ListPage from '@/components/ListPage';
|
||||||
import Search from '@/components/Search';
|
import Search from '@/components/Search';
|
||||||
import Table from '@/components/Table';
|
|
||||||
import TablePlaceholder from '@/components/Table/TablePlaceholder';
|
import TablePlaceholder from '@/components/Table/TablePlaceholder';
|
||||||
import UserAvatar from '@/components/UserAvatar';
|
import UserAvatar from '@/components/UserAvatar';
|
||||||
import { defaultPageSize } from '@/consts';
|
import { defaultPageSize } from '@/consts';
|
||||||
import { UserDetailsTabs } from '@/consts/page-tabs';
|
import { UserDetailsTabs } from '@/consts/page-tabs';
|
||||||
import type { RequestError } from '@/hooks/use-api';
|
import type { RequestError } from '@/hooks/use-api';
|
||||||
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
|
||||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
|
||||||
import { buildUrl, formatSearchKeyword } from '@/utils/url';
|
import { buildUrl, formatSearchKeyword } from '@/utils/url';
|
||||||
import { getUserTitle, getUserSubtitle } from '@/utils/user';
|
import { getUserTitle, getUserSubtitle } from '@/utils/user';
|
||||||
|
|
||||||
|
@ -57,43 +54,27 @@ function Users() {
|
||||||
const [users, totalCount] = data ?? [];
|
const [users, totalCount] = data ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={resourcesStyles.container}>
|
<ListPage
|
||||||
<PageMeta titleKey="users.page_title" />
|
title={{
|
||||||
<div className={resourcesStyles.headline}>
|
title: 'users.title',
|
||||||
<CardTitle title="users.title" subtitle="users.subtitle" />
|
subtitle: 'users.subtitle',
|
||||||
<Button
|
}}
|
||||||
title="users.create"
|
pageMeta={{ titleKey: 'users.page_title' }}
|
||||||
size="large"
|
createButton={{
|
||||||
type="primary"
|
title: 'users.create',
|
||||||
icon={<Plus />}
|
onClick: () => {
|
||||||
onClick={() => {
|
navigate({
|
||||||
navigate({
|
pathname: createUserPathname,
|
||||||
pathname: createUserPathname,
|
search,
|
||||||
search,
|
});
|
||||||
});
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
table={{
|
||||||
{isCreateNew && (
|
rowGroups: [{ key: 'users', data: users }],
|
||||||
<CreateForm
|
rowIndexKey: 'id',
|
||||||
onClose={() => {
|
isLoading,
|
||||||
navigate({
|
errorMessage: error?.body?.message ?? error?.message,
|
||||||
pathname: usersPathname,
|
columns: [
|
||||||
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={[
|
|
||||||
{
|
{
|
||||||
title: t('users.user_name'),
|
title: t('users.user_name'),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
|
@ -125,8 +106,8 @@ function Users() {
|
||||||
colSpan: 5,
|
colSpan: 5,
|
||||||
render: ({ lastSignInAt }) => <DateTime>{lastSignInAt}</DateTime>,
|
render: ({ lastSignInAt }) => <DateTime>{lastSignInAt}</DateTime>,
|
||||||
},
|
},
|
||||||
]}
|
],
|
||||||
filter={
|
filter: (
|
||||||
<Search
|
<Search
|
||||||
inputClassName={styles.searchInput}
|
inputClassName={styles.searchInput}
|
||||||
placeholder={t('users.search')}
|
placeholder={t('users.search')}
|
||||||
|
@ -139,8 +120,8 @@ function Users() {
|
||||||
updateSearchParameters({ keyword: '', page: 1 });
|
updateSearchParameters({ keyword: '', page: 1 });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
placeholder={
|
placeholder: (
|
||||||
<TablePlaceholder
|
<TablePlaceholder
|
||||||
image={<UsersEmpty />}
|
image={<UsersEmpty />}
|
||||||
imageDark={<UsersEmptyDark />}
|
imageDark={<UsersEmptyDark />}
|
||||||
|
@ -161,21 +142,36 @@ function Users() {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
),
|
||||||
rowClickHandler={({ id }) => {
|
rowClickHandler: ({ id }) => {
|
||||||
navigate(buildDetailsPathname(id));
|
navigate(buildDetailsPathname(id));
|
||||||
}}
|
},
|
||||||
pagination={{
|
pagination: {
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
totalCount,
|
totalCount,
|
||||||
onChange: (page) => {
|
onChange: (page) => {
|
||||||
updateSearchParameters({ page });
|
updateSearchParameters({ page });
|
||||||
},
|
},
|
||||||
}}
|
},
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry: async () => mutate(undefined, true),
|
||||||
/>
|
}}
|
||||||
</div>
|
widgets={
|
||||||
|
isCreateNew && (
|
||||||
|
<CreateForm
|
||||||
|
onClose={() => {
|
||||||
|
navigate({
|
||||||
|
pathname: usersPathname,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onCreate={() => {
|
||||||
|
void mutate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue