mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -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);
|
||||
}
|
||||
|
||||
.content {
|
||||
.description {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-neutral-50);
|
||||
margin-bottom: _.unit(2);
|
||||
|
|
|
@ -10,13 +10,13 @@ import * as styles from './TableEmpty.module.scss';
|
|||
|
||||
type Props = {
|
||||
title?: string;
|
||||
content?: string;
|
||||
description?: string;
|
||||
image?: ReactNode;
|
||||
children?: ReactNode;
|
||||
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 theme = useTheme();
|
||||
|
||||
|
@ -26,7 +26,7 @@ const TableEmpty = ({ title, content, image, children, columns }: Props) => {
|
|||
<div className={styles.tableEmpty}>
|
||||
{image ?? (theme === AppearanceMode.LightMode ? <Empty /> : <EmptyDark />)}
|
||||
<div className={styles.title}>{title ?? t('errors.empty')}</div>
|
||||
{content && <div className={styles.content}>{content}</div>}
|
||||
{description && <div className={styles.description}>{description}</div>}
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
@ -9,6 +10,13 @@ import TableLoading from './TableLoading';
|
|||
import * as styles from './index.module.scss';
|
||||
import type { Column, RowGroup } from './types';
|
||||
|
||||
export type TablePlaceholder = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: ReactNode;
|
||||
content?: ReactNode;
|
||||
};
|
||||
|
||||
type Props<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
|
@ -16,12 +24,13 @@ type Props<
|
|||
rowGroups: Array<RowGroup<TFieldValues>>;
|
||||
columns: Array<Column<TFieldValues>>;
|
||||
rowIndexKey: TName;
|
||||
onClickRow?: (row: TFieldValues) => void;
|
||||
isRowClickable?: (row: TFieldValues) => boolean;
|
||||
rowClickHandler?: (row: TFieldValues) => void;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
bodyClassName?: string;
|
||||
isLoading?: boolean;
|
||||
placeholder?: ReactNode;
|
||||
placeholder?: TablePlaceholder;
|
||||
errorMessage?: string;
|
||||
onRetry?: () => void;
|
||||
};
|
||||
|
@ -33,7 +42,8 @@ const Table = <
|
|||
rowGroups,
|
||||
columns,
|
||||
rowIndexKey,
|
||||
onClickRow,
|
||||
rowClickHandler,
|
||||
isRowClickable = () => Boolean(rowClickHandler),
|
||||
className,
|
||||
headerClassName,
|
||||
bodyClassName,
|
||||
|
@ -69,7 +79,14 @@ const Table = <
|
|||
<TableError columns={columns.length} content={errorMessage} onRetry={onRetry} />
|
||||
)}
|
||||
{!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 }) => (
|
||||
<Fragment key={key}>
|
||||
|
@ -80,21 +97,31 @@ const Table = <
|
|||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{data?.map((row) => (
|
||||
<tr
|
||||
key={row[rowIndexKey]}
|
||||
className={classNames(onClickRow && styles.clickable)}
|
||||
onClick={() => {
|
||||
onClickRow?.(row);
|
||||
}}
|
||||
>
|
||||
{columns.map(({ dataIndex, colSpan, className, render }) => (
|
||||
<td key={dataIndex} colSpan={colSpan} className={className}>
|
||||
{render(row)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{data?.map((row) => {
|
||||
const rowClickable = isRowClickable(row);
|
||||
|
||||
const onClick = conditional(
|
||||
rowClickable &&
|
||||
rowClickHandler &&
|
||||
(() => {
|
||||
rowClickHandler(row);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<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>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { ConnectorPlatform, ConnectorType } from '@logto/schemas';
|
|||
|
||||
import EmailConnector from '@/assets/images/connector-email.svg';
|
||||
import SmsConnectorIcon from '@/assets/images/connector-sms.svg';
|
||||
import type { ConnectorGroup } from '@/types/connector';
|
||||
|
||||
type TitlePlaceHolder = {
|
||||
[key in ConnectorType]: AdminConsoleKey;
|
||||
|
@ -32,3 +33,25 @@ export const connectorPlaceholderIcon: ConnectorPlaceholderIcon = Object.freeze(
|
|||
[ConnectorType.Sms]: SmsConnectorIcon,
|
||||
[ConnectorType.Email]: EmailConnector,
|
||||
} 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 =
|
||||
type === ConnectorType.Email ? SignInIdentifier.Email : SignInIdentifier.Sms;
|
||||
|
||||
return (
|
||||
data.signIn.methods.some(
|
||||
({ identifier, verificationCode }) => verificationCode && identifier === relatedIdentifier
|
||||
) ||
|
||||
(data.signUp.identifiers.includes(relatedIdentifier) && data.signUp.verify)
|
||||
const usedInSignUp =
|
||||
data.signUp.identifiers.includes(relatedIdentifier) && data.signUp.verify;
|
||||
|
||||
const usedInSignIn = data.signIn.methods.some(
|
||||
({ identifier, verificationCode }) => verificationCode && identifier === relatedIdentifier
|
||||
);
|
||||
|
||||
return usedInSignUp || usedInSignIn;
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
|
|
@ -118,19 +118,21 @@ const ApiResources = () => {
|
|||
render: ({ indicator }) => <CopyToClipboard value={indicator} variant="text" />,
|
||||
},
|
||||
]}
|
||||
placeholder={
|
||||
<Button
|
||||
title="api_resources.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApiResourcePathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClickRow={({ id }) => {
|
||||
placeholder={{
|
||||
content: (
|
||||
<Button
|
||||
title="api_resources.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApiResourcePathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
rowClickHandler={({ id }) => {
|
||||
navigate(buildDetailsPathname(id));
|
||||
}}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
|
|
|
@ -112,19 +112,21 @@ const Applications = () => {
|
|||
render: ({ id }) => <CopyToClipboard value={id} variant="text" />,
|
||||
},
|
||||
]}
|
||||
placeholder={
|
||||
<Button
|
||||
title="applications.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApplicationPathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClickRow={({ id }) => {
|
||||
placeholder={{
|
||||
content: (
|
||||
<Button
|
||||
title="applications.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate({
|
||||
pathname: createApplicationPathname,
|
||||
search,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
rowClickHandler={({ id }) => {
|
||||
navigate(buildDetailsPathname(id));
|
||||
}}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { ConnectorResponse } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ConnectorLogo from '@/components/ConnectorLogo';
|
||||
|
@ -13,18 +13,19 @@ import {
|
|||
} from '@/consts/connectors';
|
||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||
import ConnectorPlatformIcon from '@/icons/ConnectorPlatformIcon';
|
||||
import type { ConnectorGroup } from '@/types/connector';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
type: ConnectorType;
|
||||
connectors: ConnectorResponse[];
|
||||
onClickSetup?: () => void;
|
||||
connectorGroup: ConnectorGroup;
|
||||
};
|
||||
|
||||
const ConnectorName = ({ type, connectors, onClickSetup }: Props) => {
|
||||
const ConnectorName = ({ connectorGroup }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { type, connectors } = connectorGroup;
|
||||
const connector = connectors[0];
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!connector) {
|
||||
const PlaceholderIcon = connectorPlaceholderIcon[type];
|
||||
|
@ -35,7 +36,12 @@ const ConnectorName = ({ type, connectors, onClickSetup }: Props) => {
|
|||
<div className={styles.previewTitle}>
|
||||
<div>{t(connectorTitlePlaceHolder[type])}</div>
|
||||
{type !== ConnectorType.Social && (
|
||||
<Button title="general.set_up" onClick={onClickSetup} />
|
||||
<Button
|
||||
title="general.set_up"
|
||||
onClick={() => {
|
||||
navigate(`/connectors/${ConnectorsTabs.Passwordless}/create/${type}`);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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 CardTitle from '@/components/CardTitle';
|
||||
import TabNav, { TabNavItem } from '@/components/TabNav';
|
||||
import TableEmpty from '@/components/Table/TableEmpty';
|
||||
import TableError from '@/components/Table/TableError';
|
||||
import TableLoading from '@/components/Table/TableLoading';
|
||||
import type { TablePlaceholder } from '@/components/Table';
|
||||
import Table from '@/components/Table';
|
||||
import { defaultEmailConnectorGroup, defaultSmsConnectorGroup } from '@/consts';
|
||||
import { ConnectorsTabs } from '@/consts/page-tabs';
|
||||
import useConnectorGroups from '@/hooks/use-connector-groups';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
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 ConnectorTypeColumn from './components/ConnectorTypeColumn';
|
||||
import CreateForm from './components/CreateForm';
|
||||
import SignInExperienceSetupNotice from './components/SignInExperienceSetupNotice';
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -53,25 +54,39 @@ const Connectors = () => {
|
|||
const theme = useTheme();
|
||||
const isLightMode = theme === AppearanceMode.LightMode;
|
||||
|
||||
const emailConnector = useMemo(() => {
|
||||
const emailConnectorGroup = data?.find(({ type }) => type === ConnectorType.Email);
|
||||
const passwordlessConnectors = useMemo(() => {
|
||||
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]);
|
||||
|
||||
const smsConnector = useMemo(() => {
|
||||
const smsConnectorGroup = data?.find(({ type }) => type === ConnectorType.Sms);
|
||||
const socialConnectors = useMemo(
|
||||
() => data?.filter(({ type }) => type === ConnectorType.Social),
|
||||
[data]
|
||||
);
|
||||
|
||||
return smsConnectorGroup?.connectors[0];
|
||||
}, [data]);
|
||||
const connectors = isSocial ? socialConnectors : passwordlessConnectors;
|
||||
|
||||
const socialConnectorGroups = useMemo(() => {
|
||||
if (!isSocial) {
|
||||
return;
|
||||
const placeholder: TablePlaceholder | undefined = conditional(
|
||||
isSocial && {
|
||||
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 (
|
||||
<>
|
||||
|
@ -95,73 +110,49 @@ const Connectors = () => {
|
|||
<TabNavItem href={passwordlessPathname}>{t('connectors.tab_email_sms')}</TabNavItem>
|
||||
<TabNavItem href={socialPathname}>{t('connectors.tab_social')}</TabNavItem>
|
||||
</TabNav>
|
||||
<div className={resourcesStyles.table}>
|
||||
<div className={tableStyles.scrollable}>
|
||||
<table className={classNames(!data && tableStyles.empty)}>
|
||||
<colgroup>
|
||||
<col className={styles.connectorName} />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('connectors.connector_name')}</th>
|
||||
<th>{t('connectors.connector_type')}</th>
|
||||
<th>
|
||||
<ConnectorStatusField />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!data && error && (
|
||||
<TableError
|
||||
columns={3}
|
||||
content={error.body?.message ?? error.message}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
/>
|
||||
)}
|
||||
{isLoading && <TableLoading columns={3} />}
|
||||
{socialConnectorGroups?.length === 0 && (
|
||||
<TableEmpty
|
||||
columns={3}
|
||||
title={t('connectors.type.social')}
|
||||
content={t('connectors.social_connector_eg')}
|
||||
image={isLightMode ? <SocialConnectorEmpty /> : <SocialConnectorEmptyDark />}
|
||||
>
|
||||
<Button
|
||||
title="connectors.create"
|
||||
type="outline"
|
||||
onClick={() => {
|
||||
navigate(buildCreatePathname(ConnectorType.Social));
|
||||
}}
|
||||
/>
|
||||
</TableEmpty>
|
||||
)}
|
||||
{!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>
|
||||
<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} />,
|
||||
},
|
||||
{
|
||||
title: t('connectors.connector_type'),
|
||||
dataIndex: 'type',
|
||||
colSpan: 5,
|
||||
render: (connectorGroup) => <ConnectorTypeColumn connectorGroup={connectorGroup} />,
|
||||
},
|
||||
{
|
||||
title: <ConnectorStatusField />,
|
||||
dataIndex: 'status',
|
||||
colSpan: 5,
|
||||
render: (connectorGroup) => <ConnectorStatus connectorGroup={connectorGroup} />,
|
||||
},
|
||||
]}
|
||||
isRowClickable={({ connectors }) => Boolean(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={placeholder}
|
||||
onRetry={async () => mutate(undefined, true)}
|
||||
/>
|
||||
</div>
|
||||
{Boolean(createConnectorType) && (
|
||||
<CreateForm
|
||||
|
|
Loading…
Reference in a new issue