mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -05:00
refactor(console): connectors table (#2813)
This commit is contained in:
parent
45e72f173c
commit
be69a81478
12 changed files with 268 additions and 230 deletions
|
@ -15,7 +15,7 @@
|
||||||
margin-bottom: _.unit(2);
|
margin-bottom: _.unit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.description {
|
||||||
font: var(--font-body-medium);
|
font: var(--font-body-medium);
|
||||||
color: var(--color-neutral-50);
|
color: var(--color-neutral-50);
|
||||||
margin-bottom: _.unit(2);
|
margin-bottom: _.unit(2);
|
||||||
|
|
|
@ -10,13 +10,13 @@ import * as styles from './TableEmpty.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string;
|
title?: string;
|
||||||
content?: string;
|
description?: string;
|
||||||
image?: ReactNode;
|
image?: ReactNode;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
columns: number;
|
columns: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableEmpty = ({ title, content, image, children, columns }: Props) => {
|
const TableEmpty = ({ title, description, image, children, columns }: Props) => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ const TableEmpty = ({ title, content, image, children, columns }: Props) => {
|
||||||
<div className={styles.tableEmpty}>
|
<div className={styles.tableEmpty}>
|
||||||
{image ?? (theme === AppearanceMode.LightMode ? <Empty /> : <EmptyDark />)}
|
{image ?? (theme === AppearanceMode.LightMode ? <Empty /> : <EmptyDark />)}
|
||||||
<div className={styles.title}>{title ?? t('errors.empty')}</div>
|
<div className={styles.title}>{title ?? t('errors.empty')}</div>
|
||||||
{content && <div className={styles.content}>{content}</div>}
|
{description && <div className={styles.description}>{description}</div>}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { conditional } from '@silverhand/essentials';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
@ -9,6 +10,13 @@ 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';
|
||||||
|
|
||||||
|
export type TablePlaceholder = {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
image?: ReactNode;
|
||||||
|
content?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
type Props<
|
type Props<
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
@ -16,12 +24,13 @@ type Props<
|
||||||
rowGroups: Array<RowGroup<TFieldValues>>;
|
rowGroups: Array<RowGroup<TFieldValues>>;
|
||||||
columns: Array<Column<TFieldValues>>;
|
columns: Array<Column<TFieldValues>>;
|
||||||
rowIndexKey: TName;
|
rowIndexKey: TName;
|
||||||
onClickRow?: (row: TFieldValues) => void;
|
isRowClickable?: (row: TFieldValues) => boolean;
|
||||||
|
rowClickHandler?: (row: TFieldValues) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
headerClassName?: string;
|
headerClassName?: string;
|
||||||
bodyClassName?: string;
|
bodyClassName?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
placeholder?: ReactNode;
|
placeholder?: TablePlaceholder;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
};
|
};
|
||||||
|
@ -33,7 +42,8 @@ const Table = <
|
||||||
rowGroups,
|
rowGroups,
|
||||||
columns,
|
columns,
|
||||||
rowIndexKey,
|
rowIndexKey,
|
||||||
onClickRow,
|
rowClickHandler,
|
||||||
|
isRowClickable = () => Boolean(rowClickHandler),
|
||||||
className,
|
className,
|
||||||
headerClassName,
|
headerClassName,
|
||||||
bodyClassName,
|
bodyClassName,
|
||||||
|
@ -69,7 +79,14 @@ const Table = <
|
||||||
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />
|
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />
|
||||||
)}
|
)}
|
||||||
{!isLoading && !hasData && placeholder && (
|
{!isLoading && !hasData && placeholder && (
|
||||||
<TableEmpty columns={columns.length}>{placeholder}</TableEmpty>
|
<TableEmpty
|
||||||
|
columns={columns.length}
|
||||||
|
title={placeholder.title}
|
||||||
|
description={placeholder.description}
|
||||||
|
image={placeholder.image}
|
||||||
|
>
|
||||||
|
{placeholder.content}
|
||||||
|
</TableEmpty>
|
||||||
)}
|
)}
|
||||||
{rowGroups.map(({ key, label, labelClassName, data }) => (
|
{rowGroups.map(({ key, label, labelClassName, data }) => (
|
||||||
<Fragment key={key}>
|
<Fragment key={key}>
|
||||||
|
@ -80,21 +97,31 @@ const Table = <
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{data?.map((row) => (
|
{data?.map((row) => {
|
||||||
<tr
|
const rowClickable = isRowClickable(row);
|
||||||
key={row[rowIndexKey]}
|
|
||||||
className={classNames(onClickRow && styles.clickable)}
|
const onClick = conditional(
|
||||||
onClick={() => {
|
rowClickable &&
|
||||||
onClickRow?.(row);
|
rowClickHandler &&
|
||||||
}}
|
(() => {
|
||||||
>
|
rowClickHandler(row);
|
||||||
{columns.map(({ dataIndex, colSpan, className, render }) => (
|
})
|
||||||
<td key={dataIndex} colSpan={colSpan} className={className}>
|
);
|
||||||
{render(row)}
|
|
||||||
</td>
|
return (
|
||||||
))}
|
<tr
|
||||||
</tr>
|
key={row[rowIndexKey]}
|
||||||
))}
|
className={classNames(rowClickable && styles.clickable)}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{columns.map(({ dataIndex, colSpan, className, render }) => (
|
||||||
|
<td key={dataIndex} colSpan={colSpan} className={className}>
|
||||||
|
{render(row)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { ConnectorPlatform, ConnectorType } from '@logto/schemas';
|
||||||
|
|
||||||
import EmailConnector from '@/assets/images/connector-email.svg';
|
import EmailConnector from '@/assets/images/connector-email.svg';
|
||||||
import SmsConnectorIcon from '@/assets/images/connector-sms.svg';
|
import SmsConnectorIcon from '@/assets/images/connector-sms.svg';
|
||||||
|
import type { ConnectorGroup } from '@/types/connector';
|
||||||
|
|
||||||
type TitlePlaceHolder = {
|
type TitlePlaceHolder = {
|
||||||
[key in ConnectorType]: AdminConsoleKey;
|
[key in ConnectorType]: AdminConsoleKey;
|
||||||
|
@ -32,3 +33,25 @@ export const connectorPlaceholderIcon: ConnectorPlaceholderIcon = Object.freeze(
|
||||||
[ConnectorType.Sms]: SmsConnectorIcon,
|
[ConnectorType.Sms]: SmsConnectorIcon,
|
||||||
[ConnectorType.Email]: EmailConnector,
|
[ConnectorType.Email]: EmailConnector,
|
||||||
} as const);
|
} as const);
|
||||||
|
|
||||||
|
export const defaultSmsConnectorGroup: ConnectorGroup = {
|
||||||
|
id: 'default-sms-connector',
|
||||||
|
type: ConnectorType.Sms,
|
||||||
|
connectors: [],
|
||||||
|
name: { en: '' },
|
||||||
|
description: { en: '' },
|
||||||
|
logo: '',
|
||||||
|
logoDark: null,
|
||||||
|
target: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultEmailConnectorGroup: ConnectorGroup = {
|
||||||
|
id: 'default-email-connector',
|
||||||
|
type: ConnectorType.Email,
|
||||||
|
connectors: [],
|
||||||
|
name: { en: '' },
|
||||||
|
description: { en: '' },
|
||||||
|
logo: '',
|
||||||
|
logoDark: null,
|
||||||
|
target: '',
|
||||||
|
};
|
||||||
|
|
|
@ -23,12 +23,14 @@ const useConnectorInUse = () => {
|
||||||
const relatedIdentifier =
|
const relatedIdentifier =
|
||||||
type === ConnectorType.Email ? SignInIdentifier.Email : SignInIdentifier.Sms;
|
type === ConnectorType.Email ? SignInIdentifier.Email : SignInIdentifier.Sms;
|
||||||
|
|
||||||
return (
|
const usedInSignUp =
|
||||||
data.signIn.methods.some(
|
data.signUp.identifiers.includes(relatedIdentifier) && data.signUp.verify;
|
||||||
({ identifier, verificationCode }) => verificationCode && identifier === relatedIdentifier
|
|
||||||
) ||
|
const usedInSignIn = data.signIn.methods.some(
|
||||||
(data.signUp.identifiers.includes(relatedIdentifier) && data.signUp.verify)
|
({ identifier, verificationCode }) => verificationCode && identifier === relatedIdentifier
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return usedInSignUp || usedInSignIn;
|
||||||
},
|
},
|
||||||
[data]
|
[data]
|
||||||
);
|
);
|
||||||
|
|
|
@ -118,19 +118,21 @@ const ApiResources = () => {
|
||||||
render: ({ indicator }) => <CopyToClipboard value={indicator} variant="text" />,
|
render: ({ indicator }) => <CopyToClipboard value={indicator} variant="text" />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
placeholder={
|
placeholder={{
|
||||||
<Button
|
content: (
|
||||||
title="api_resources.create"
|
<Button
|
||||||
type="outline"
|
title="api_resources.create"
|
||||||
onClick={() => {
|
type="outline"
|
||||||
navigate({
|
onClick={() => {
|
||||||
pathname: createApiResourcePathname,
|
navigate({
|
||||||
search,
|
pathname: createApiResourcePathname,
|
||||||
});
|
search,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
}
|
/>
|
||||||
onClickRow={({ id }) => {
|
),
|
||||||
|
}}
|
||||||
|
rowClickHandler={({ id }) => {
|
||||||
navigate(buildDetailsPathname(id));
|
navigate(buildDetailsPathname(id));
|
||||||
}}
|
}}
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry={async () => mutate(undefined, true)}
|
||||||
|
|
|
@ -112,19 +112,21 @@ const Applications = () => {
|
||||||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
placeholder={
|
placeholder={{
|
||||||
<Button
|
content: (
|
||||||
title="applications.create"
|
<Button
|
||||||
type="outline"
|
title="applications.create"
|
||||||
onClick={() => {
|
type="outline"
|
||||||
navigate({
|
onClick={() => {
|
||||||
pathname: createApplicationPathname,
|
navigate({
|
||||||
search,
|
pathname: createApplicationPathname,
|
||||||
});
|
search,
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
}
|
/>
|
||||||
onClickRow={({ id }) => {
|
),
|
||||||
|
}}
|
||||||
|
rowClickHandler={({ id }) => {
|
||||||
navigate(buildDetailsPathname(id));
|
navigate(buildDetailsPathname(id));
|
||||||
}}
|
}}
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry={async () => mutate(undefined, true)}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { ConnectorResponse } from '@logto/schemas';
|
|
||||||
import { ConnectorType } from '@logto/schemas';
|
import { ConnectorType } from '@logto/schemas';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import ConnectorLogo from '@/components/ConnectorLogo';
|
import ConnectorLogo from '@/components/ConnectorLogo';
|
||||||
|
@ -13,18 +13,19 @@ import {
|
||||||
} from '@/consts/connectors';
|
} from '@/consts/connectors';
|
||||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||||
import ConnectorPlatformIcon from '@/icons/ConnectorPlatformIcon';
|
import ConnectorPlatformIcon from '@/icons/ConnectorPlatformIcon';
|
||||||
|
import type { ConnectorGroup } from '@/types/connector';
|
||||||
|
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type: ConnectorType;
|
connectorGroup: ConnectorGroup;
|
||||||
connectors: ConnectorResponse[];
|
|
||||||
onClickSetup?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConnectorName = ({ type, connectors, onClickSetup }: Props) => {
|
const ConnectorName = ({ connectorGroup }: Props) => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
const { type, connectors } = connectorGroup;
|
||||||
const connector = connectors[0];
|
const connector = connectors[0];
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
if (!connector) {
|
if (!connector) {
|
||||||
const PlaceholderIcon = connectorPlaceholderIcon[type];
|
const PlaceholderIcon = connectorPlaceholderIcon[type];
|
||||||
|
@ -35,7 +36,12 @@ const ConnectorName = ({ type, connectors, onClickSetup }: Props) => {
|
||||||
<div className={styles.previewTitle}>
|
<div className={styles.previewTitle}>
|
||||||
<div>{t(connectorTitlePlaceHolder[type])}</div>
|
<div>{t(connectorTitlePlaceHolder[type])}</div>
|
||||||
{type !== ConnectorType.Social && (
|
{type !== ConnectorType.Social && (
|
||||||
<Button title="general.set_up" onClick={onClickSetup} />
|
<Button
|
||||||
|
title="general.set_up"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/connectors/${ConnectorsTabs.Passwordless}/create/${type}`);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
|
|
||||||
import { ConnectorType } from '@logto/schemas';
|
|
||||||
import { conditional } from '@silverhand/essentials';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
|
|
||||||
import Status from '@/components/Status';
|
|
||||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
|
||||||
import { connectorTitlePlaceHolder } from '@/consts/connectors';
|
|
||||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
|
||||||
import useConnectorInUse from '@/hooks/use-connector-in-use';
|
|
||||||
import * as tableStyles from '@/scss/table.module.scss';
|
|
||||||
|
|
||||||
import ConnectorName from '../ConnectorName';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
type: ConnectorType;
|
|
||||||
connectors: ConnectorResponse[];
|
|
||||||
onClickSetup?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ConnectorRow = ({ type, connectors, onClickSetup }: Props) => {
|
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
|
||||||
const firstConnector = connectors[0];
|
|
||||||
const { isConnectorInUse } = useConnectorInUse();
|
|
||||||
const inUse = isConnectorInUse(firstConnector);
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const showSetupButton = type !== ConnectorType.Social && !firstConnector;
|
|
||||||
|
|
||||||
const standardConnectors = connectors.filter(({ isStandard }) => isStandard);
|
|
||||||
|
|
||||||
if (standardConnectors.length > 1) {
|
|
||||||
throw new Error('More than one standard connectors with the same target is not supported.');
|
|
||||||
}
|
|
||||||
const firstStandardConnector = standardConnectors[0];
|
|
||||||
const { data: connectorFactory } = useSWR<ConnectorFactoryResponse>(
|
|
||||||
firstStandardConnector && `/api/connector-factories/${firstStandardConnector.connectorId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickRow = () => {
|
|
||||||
if (showSetupButton || !firstConnector) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate(
|
|
||||||
`/connectors/${
|
|
||||||
firstConnector.type === ConnectorType.Social
|
|
||||||
? ConnectorsTabs.Social
|
|
||||||
: ConnectorsTabs.Passwordless
|
|
||||||
}/${firstConnector.id}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const connectorTypeColumn = useMemo(() => {
|
|
||||||
if (!firstStandardConnector) {
|
|
||||||
return t(connectorTitlePlaceHolder[type]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return connectorFactory && <UnnamedTrans resource={connectorFactory.name} />;
|
|
||||||
}, [type, connectorFactory, t, firstStandardConnector]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr className={conditional(!showSetupButton && tableStyles.clickable)} onClick={handleClickRow}>
|
|
||||||
<td>
|
|
||||||
<ConnectorName type={type} connectors={connectors} onClickSetup={onClickSetup} />
|
|
||||||
</td>
|
|
||||||
<td>{connectorTypeColumn}</td>
|
|
||||||
<td>
|
|
||||||
{conditional(
|
|
||||||
firstConnector && (
|
|
||||||
<Status status={inUse ? 'enabled' : 'disabled'}>
|
|
||||||
{t('connectors.connector_status', {
|
|
||||||
context: inUse ? 'in_use' : 'not_in_use',
|
|
||||||
})}
|
|
||||||
</Status>
|
|
||||||
)
|
|
||||||
) ?? '-'}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectorRow;
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Status from '@/components/Status';
|
||||||
|
import useConnectorInUse from '@/hooks/use-connector-in-use';
|
||||||
|
import type { ConnectorGroup } from '@/types/connector';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
connectorGroup: ConnectorGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectorStatus = ({ connectorGroup }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
const { connectors } = connectorGroup;
|
||||||
|
const { isConnectorInUse } = useConnectorInUse();
|
||||||
|
|
||||||
|
const firstConnector = connectors[0];
|
||||||
|
const inUse = isConnectorInUse(firstConnector);
|
||||||
|
|
||||||
|
return firstConnector ? (
|
||||||
|
<Status status={inUse ? 'enabled' : 'disabled'}>
|
||||||
|
{t('connectors.connector_status', {
|
||||||
|
context: inUse ? 'in_use' : 'not_in_use',
|
||||||
|
})}
|
||||||
|
</Status>
|
||||||
|
) : (
|
||||||
|
<span>-</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectorStatus;
|
|
@ -0,0 +1,39 @@
|
||||||
|
import type { ConnectorFactoryResponse } from '@logto/schemas';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||||
|
import { connectorTitlePlaceHolder } from '@/consts/connectors';
|
||||||
|
import type { ConnectorGroup } from '@/types/connector';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
connectorGroup: ConnectorGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ConnectorTypeColumn = ({ connectorGroup: { type, connectors } }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
const standardConnectors = connectors.filter(({ isStandard }) => isStandard);
|
||||||
|
|
||||||
|
if (standardConnectors.length > 1) {
|
||||||
|
throw new Error('More than one standard connectors with the same target is not supported.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstStandardConnector = standardConnectors[0];
|
||||||
|
|
||||||
|
const { data: connectorFactory } = useSWR<ConnectorFactoryResponse>(
|
||||||
|
firstStandardConnector && `/api/connector-factories/${firstStandardConnector.connectorId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!firstStandardConnector) {
|
||||||
|
return <>{t(connectorTitlePlaceHolder[type])}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!connectorFactory) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <UnnamedTrans resource={connectorFactory.name} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConnectorTypeColumn;
|
|
@ -11,17 +11,18 @@ 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 CardTitle from '@/components/CardTitle';
|
||||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||||
import TableEmpty from '@/components/Table/TableEmpty';
|
import type { TablePlaceholder } from '@/components/Table';
|
||||||
import TableError from '@/components/Table/TableError';
|
import Table from '@/components/Table';
|
||||||
import TableLoading from '@/components/Table/TableLoading';
|
import { defaultEmailConnectorGroup, defaultSmsConnectorGroup } from '@/consts';
|
||||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||||
import useConnectorGroups from '@/hooks/use-connector-groups';
|
import useConnectorGroups from '@/hooks/use-connector-groups';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import * as resourcesStyles from '@/scss/resources.module.scss';
|
import * as resourcesStyles from '@/scss/resources.module.scss';
|
||||||
import * as tableStyles from '@/scss/table.module.scss';
|
|
||||||
|
|
||||||
import ConnectorRow from './components/ConnectorRow';
|
import ConnectorName from './components/ConnectorName';
|
||||||
|
import ConnectorStatus from './components/ConnectorStatus';
|
||||||
import ConnectorStatusField from './components/ConnectorStatusField';
|
import ConnectorStatusField from './components/ConnectorStatusField';
|
||||||
|
import ConnectorTypeColumn from './components/ConnectorTypeColumn';
|
||||||
import CreateForm from './components/CreateForm';
|
import CreateForm from './components/CreateForm';
|
||||||
import SignInExperienceSetupNotice from './components/SignInExperienceSetupNotice';
|
import SignInExperienceSetupNotice from './components/SignInExperienceSetupNotice';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
@ -53,25 +54,39 @@ const Connectors = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isLightMode = theme === AppearanceMode.LightMode;
|
const isLightMode = theme === AppearanceMode.LightMode;
|
||||||
|
|
||||||
const emailConnector = useMemo(() => {
|
const passwordlessConnectors = useMemo(() => {
|
||||||
const emailConnectorGroup = data?.find(({ type }) => type === ConnectorType.Email);
|
const smsConnector =
|
||||||
|
data?.find(({ type }) => type === ConnectorType.Sms) ?? defaultSmsConnectorGroup;
|
||||||
|
|
||||||
return emailConnectorGroup?.connectors[0];
|
const emailConnector =
|
||||||
|
data?.find(({ type }) => type === ConnectorType.Email) ?? defaultEmailConnectorGroup;
|
||||||
|
|
||||||
|
return [smsConnector, emailConnector];
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const smsConnector = useMemo(() => {
|
const socialConnectors = useMemo(
|
||||||
const smsConnectorGroup = data?.find(({ type }) => type === ConnectorType.Sms);
|
() => data?.filter(({ type }) => type === ConnectorType.Social),
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
return smsConnectorGroup?.connectors[0];
|
const connectors = isSocial ? socialConnectors : passwordlessConnectors;
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const socialConnectorGroups = useMemo(() => {
|
const placeholder: TablePlaceholder | undefined = conditional(
|
||||||
if (!isSocial) {
|
isSocial && {
|
||||||
return;
|
title: t('connectors.type.social'),
|
||||||
|
description: t('connectors.social_connector_eg'),
|
||||||
|
image: isLightMode ? <SocialConnectorEmpty /> : <SocialConnectorEmptyDark />,
|
||||||
|
content: (
|
||||||
|
<Button
|
||||||
|
title="connectors.create"
|
||||||
|
type="outline"
|
||||||
|
onClick={() => {
|
||||||
|
navigate(buildCreatePathname(ConnectorType.Social));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
);
|
||||||
return data?.filter(({ type }) => type === ConnectorType.Social);
|
|
||||||
}, [data, isSocial]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -95,73 +110,49 @@ const Connectors = () => {
|
||||||
<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>
|
||||||
<div className={resourcesStyles.table}>
|
<Table
|
||||||
<div className={tableStyles.scrollable}>
|
className={resourcesStyles.table}
|
||||||
<table className={classNames(!data && tableStyles.empty)}>
|
rowIndexKey="id"
|
||||||
<colgroup>
|
rowGroups={[{ key: 'connectors', data: connectors }]}
|
||||||
<col className={styles.connectorName} />
|
columns={[
|
||||||
<col />
|
{
|
||||||
<col />
|
title: t('connectors.connector_name'),
|
||||||
</colgroup>
|
dataIndex: 'name',
|
||||||
<thead>
|
colSpan: 6,
|
||||||
<tr>
|
render: (connectorGroup) => <ConnectorName connectorGroup={connectorGroup} />,
|
||||||
<th>{t('connectors.connector_name')}</th>
|
},
|
||||||
<th>{t('connectors.connector_type')}</th>
|
{
|
||||||
<th>
|
title: t('connectors.connector_type'),
|
||||||
<ConnectorStatusField />
|
dataIndex: 'type',
|
||||||
</th>
|
colSpan: 5,
|
||||||
</tr>
|
render: (connectorGroup) => <ConnectorTypeColumn connectorGroup={connectorGroup} />,
|
||||||
</thead>
|
},
|
||||||
<tbody>
|
{
|
||||||
{!data && error && (
|
title: <ConnectorStatusField />,
|
||||||
<TableError
|
dataIndex: 'status',
|
||||||
columns={3}
|
colSpan: 5,
|
||||||
content={error.body?.message ?? error.message}
|
render: (connectorGroup) => <ConnectorStatus connectorGroup={connectorGroup} />,
|
||||||
onRetry={async () => mutate(undefined, true)}
|
},
|
||||||
/>
|
]}
|
||||||
)}
|
isRowClickable={({ connectors }) => Boolean(connectors[0])}
|
||||||
{isLoading && <TableLoading columns={3} />}
|
rowClickHandler={({ connectors }) => {
|
||||||
{socialConnectorGroups?.length === 0 && (
|
const firstConnector = connectors[0];
|
||||||
<TableEmpty
|
|
||||||
columns={3}
|
if (!firstConnector) {
|
||||||
title={t('connectors.type.social')}
|
return;
|
||||||
content={t('connectors.social_connector_eg')}
|
}
|
||||||
image={isLightMode ? <SocialConnectorEmpty /> : <SocialConnectorEmptyDark />}
|
|
||||||
>
|
const { type, id } = firstConnector;
|
||||||
<Button
|
|
||||||
title="connectors.create"
|
navigate(
|
||||||
type="outline"
|
`${type === ConnectorType.Social ? socialPathname : passwordlessPathname}/${id}`
|
||||||
onClick={() => {
|
);
|
||||||
navigate(buildCreatePathname(ConnectorType.Social));
|
}}
|
||||||
}}
|
isLoading={isLoading}
|
||||||
/>
|
errorMessage={error?.body?.message ?? error?.message}
|
||||||
</TableEmpty>
|
placeholder={placeholder}
|
||||||
)}
|
onRetry={async () => mutate(undefined, true)}
|
||||||
{!isLoading && !isSocial && (
|
/>
|
||||||
<ConnectorRow
|
|
||||||
connectors={smsConnector ? [smsConnector] : []}
|
|
||||||
type={ConnectorType.Sms}
|
|
||||||
onClickSetup={() => {
|
|
||||||
navigate(buildCreatePathname(ConnectorType.Sms));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!isLoading && !isSocial && (
|
|
||||||
<ConnectorRow
|
|
||||||
connectors={emailConnector ? [emailConnector] : []}
|
|
||||||
type={ConnectorType.Email}
|
|
||||||
onClickSetup={() => {
|
|
||||||
navigate(buildCreatePathname(ConnectorType.Email));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{socialConnectorGroups?.map(({ connectors, id }) => (
|
|
||||||
<ConnectorRow key={id} connectors={connectors} type={ConnectorType.Social} />
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{Boolean(createConnectorType) && (
|
{Boolean(createConnectorType) && (
|
||||||
<CreateForm
|
<CreateForm
|
||||||
|
|
Loading…
Reference in a new issue