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:
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';
|
||||
|
||||
type Props = {
|
||||
export type Props = {
|
||||
titleKey: AdminConsoleKey | AdminConsoleKey[];
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
trackPageView?: boolean;
|
||||
|
|
|
@ -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>
|
||||
> = {
|
||||
|
|
|
@ -3,7 +3,3 @@
|
|||
.icon {
|
||||
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 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,10 +4,6 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: _.unit(4);
|
||||
}
|
||||
|
||||
.applicationName {
|
||||
width: 360px;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 />}
|
||||
|
|
|
@ -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 });
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue