From 8fe02a0059189518ecfa88485c26c98329bbf3a7 Mon Sep 17 00:00:00 2001 From: Charles Zhao Date: Mon, 30 Jan 2023 12:23:56 +0800 Subject: [PATCH] refactor(console): user social identities table (#2992) Co-authored-by: Xiao Yijun --- .../ImageWithErrorFallback/index.tsx | 2 +- .../src/components/Table/index.module.scss | 97 ++++++---- .../console/src/components/Table/index.tsx | 4 +- .../UserConnectors/index.module.scss | 71 -------- .../components/UserConnectors/index.tsx | 170 ------------------ .../UserSocialIdentities/index.module.scss | 35 ++++ .../components/UserSocialIdentities/index.tsx | 162 +++++++++++++++++ .../pages/UserDetails/UserSettings/index.tsx | 6 +- .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + .../translation/admin-console/connectors.ts | 1 + 16 files changed, 276 insertions(+), 279 deletions(-) delete mode 100644 packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.module.scss delete mode 100644 packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.tsx create mode 100644 packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss create mode 100644 packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.tsx diff --git a/packages/console/src/components/ImageWithErrorFallback/index.tsx b/packages/console/src/components/ImageWithErrorFallback/index.tsx index 4eb014ced..ba2c5df9e 100644 --- a/packages/console/src/components/ImageWithErrorFallback/index.tsx +++ b/packages/console/src/components/ImageWithErrorFallback/index.tsx @@ -15,7 +15,7 @@ const ImageWithErrorFallback = ({ src, alt, className }: ImgHTMLAttributes; } diff --git a/packages/console/src/components/Table/index.module.scss b/packages/console/src/components/Table/index.module.scss index bef497acb..f31800843 100644 --- a/packages/console/src/components/Table/index.module.scss +++ b/packages/console/src/components/Table/index.module.scss @@ -55,45 +55,76 @@ background-color: var(--color-layer-1); border-radius: 0 0 12px 12px; - table { - tbody { - tr { - td { - font: var(--font-body-medium); - border-top: 1px solid var(--color-divider); - border-bottom: unset; - padding: _.unit(3); - } - - &.clickable { - cursor: pointer; - } + tbody { + tr { + td { + font: var(--font-body-medium); + border-top: 1px solid var(--color-divider); + border-bottom: unset; + padding: _.unit(3); } - tr.hoverEffect:hover { - background: var(--color-hover); - - td { - border-top: 1px solid transparent; - } - - + tr { - td { - border-top: 1px solid transparent; - } - } - - td:first-child { - border-radius: 8px 0 0 8px; - } - - td:last-child { - border-radius: 0 8px 8px 0; - } + &.clickable { + cursor: pointer; } } } } + + tr.hoverEffect:hover { + background: var(--color-hover); + + td { + border-top: 1px solid transparent; + } + + + tr { + td { + border-top: 1px solid transparent; + } + } + + td:first-child { + border-radius: 8px 0 0 8px; + } + + td:last-child { + border-radius: 0 8px 8px 0; + } + } + + &.hasBorder { + .filterContainer { + border: 1px solid var(--color-divider); + border-bottom: unset; + + .filter { + border-bottom: unset; + } + } + + .headerTable { + padding: 0; + border: 1px solid var(--color-divider); + } + + .bodyTable { + padding: 0; + border: 1px solid var(--color-divider); + border-top: unset; + + tr:first-child td { + border-top: 1px solid transparent; + } + } + + tr.hoverEffect:hover { + td:first-child, + td:last-child { + border-radius: 0; + } + } + } } .pagination { diff --git a/packages/console/src/components/Table/index.tsx b/packages/console/src/components/Table/index.tsx index 8827032e0..951da096d 100644 --- a/packages/console/src/components/Table/index.tsx +++ b/packages/console/src/components/Table/index.tsx @@ -38,6 +38,7 @@ type Props< pagination?: PaginationProps; placeholder?: TablePlaceholder; errorMessage?: string; + hasBorder?: boolean; onRetry?: () => void; }; @@ -59,6 +60,7 @@ const Table = < pagination, placeholder, errorMessage, + hasBorder, onRetry, }: Props) => { const totalColspan = columns.reduce((result, { colSpan }) => { @@ -69,7 +71,7 @@ const Table = < return (
-
+
{filter && (
{filter}
diff --git a/packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.module.scss b/packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.module.scss deleted file mode 100644 index 20638cb43..000000000 --- a/packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.module.scss +++ /dev/null @@ -1,71 +0,0 @@ -@use '@/scss/underscore' as _; - -.empty { - color: var(--color-text-secondary); - font: var(--font-body-medium); -} - -.table { - thead { - th { - padding: _.unit(2); - } - - th:first-child { - padding-left: _.unit(4); - } - } - - tbody { - tr { - td { - padding: _.unit(3) _.unit(2); - font: var(--font-body-medium); - - &:first-child { - padding-left: _.unit(4); - } - - &:last-child { - padding-left: _.unit(4); - } - } - - &:last-child { - td { - border-bottom: none; - } - } - } - } - - .connectorName { - display: flex; - align-items: center; - - img { - width: 32px; - height: 32px; - border-radius: _.unit(2); - } - - .name { - margin-left: _.unit(3); - } - } - - .connectorId { - display: flex; - align-items: center; - font: var(--font-body-medium); - font-family: 'Roboto Mono', monospace; - line-height: 32px; - - span { - max-width: 220px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - } -} diff --git a/packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.tsx b/packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.tsx deleted file mode 100644 index 4d1320d59..000000000 --- a/packages/console/src/pages/UserDetails/UserSettings/components/UserConnectors/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import type { Identities, ConnectorResponse } from '@logto/schemas'; -import type { Optional } from '@silverhand/essentials'; -import { conditional } from '@silverhand/essentials'; -import { useMemo, useState } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import useSWR from 'swr'; - -import Button from '@/components/Button'; -import CopyToClipboard from '@/components/CopyToClipboard'; -import DeleteConfirmModal from '@/components/DeleteConfirmModal'; -import TableError from '@/components/Table/TableError'; -import UnnamedTrans from '@/components/UnnamedTrans'; -import type { RequestError } from '@/hooks/use-api'; -import useApi from '@/hooks/use-api'; -import { getConnectorGroups } from '@/pages/Connectors/utils'; - -import * as styles from './index.module.scss'; - -type Props = { - userId: string; - connectors: Identities; - onDelete?: (connectorId: string) => void; -}; - -type DisplayConnector = Pick & { userId?: string }; - -const UserConnectors = ({ userId, connectors, onDelete }: Props) => { - const api = useApi(); - const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data, error, mutate } = useSWR('/api/connectors'); - const [deletingConnector, setDeletingConnector] = useState(); - const connectorGroups = conditional(data && getConnectorGroups(data)); - const isLoading = !connectorGroups && !error; - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleDelete = async (target: string) => { - if (isSubmitting) { - return; - } - - setIsSubmitting(true); - - try { - await api.delete(`/api/users/${userId}/identities/${target}`); - onDelete?.(target); - } finally { - setIsSubmitting(false); - } - }; - - const displayConnectors: Optional = useMemo(() => { - if (!connectorGroups) { - return; - } - - return Object.keys(connectors).map((key) => { - const connector = connectorGroups.find(({ target }) => target === key); - - if (!connector) { - return { - logo: '', - name: { - 'zh-CN': '未知连接器', - en: 'Unknown Connector', - 'tr-TR': 'Bilinmeyen connector.', - ko: '알수없는 연동', - }, - target: key, - userId: connectors[key]?.userId, - }; - } - - const { logo, name } = connector; - - return { - logo, - name, - target: key, - userId: connectors[key]?.userId, - }; - }); - }, [connectorGroups, connectors]); - - if (Object.keys(connectors).length === 0) { - return
{t('user_details.connectors.not_connected')}
; - } - - return ( -
- {isLoading &&
Loading
} - {displayConnectors && ( - - - - - - - - - - - - - - {!connectorGroups && error && ( - mutate(undefined, true)} - /> - )} - {displayConnectors.map((connector) => { - const { target, userId = '', name, logo } = connector; - - return ( - - - - - - ); - })} - -
{t('user_details.connectors.connectors')}{t('user_details.connectors.user_id')} -
-
- logo -
- -
-
-
- {userId || '-'} - - -
- )} - { - setDeletingConnector(undefined); - }} - onConfirm={async () => { - if (deletingConnector !== undefined) { - await handleDelete(deletingConnector.target); - setDeletingConnector(undefined); - } - }} - > - {deletingConnector && ( - }} - /> - )} - -
- ); -}; - -export default UserConnectors; diff --git a/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss b/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss new file mode 100644 index 000000000..71d131494 --- /dev/null +++ b/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.module.scss @@ -0,0 +1,35 @@ +@use '@/scss/underscore' as _; + +.empty { + color: var(--color-text-secondary); + font: var(--font-body-medium); +} + +.connectorName { + display: flex; + align-items: center; + + .icon { + width: 32px; + height: 32px; + border-radius: _.unit(2); + flex-shrink: 0; + } + + .name { + margin-left: _.unit(3); + } +} + +.connectorId { + display: flex; + align-items: center; + font: var(--font-body-medium); + font-family: 'Roboto Mono', monospace; + line-height: 32px; + + span { + display: inline-block; + @include _.text-ellipsis; + } +} diff --git a/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.tsx b/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.tsx new file mode 100644 index 000000000..7e49f3040 --- /dev/null +++ b/packages/console/src/pages/UserDetails/UserSettings/components/UserSocialIdentities/index.tsx @@ -0,0 +1,162 @@ +import type { Identities, ConnectorResponse } from '@logto/schemas'; +import { useMemo, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import useSWR from 'swr'; + +import Button from '@/components/Button'; +import CopyToClipboard from '@/components/CopyToClipboard'; +import DeleteConfirmModal from '@/components/DeleteConfirmModal'; +import ImageWithErrorFallback from '@/components/ImageWithErrorFallback'; +import Table from '@/components/Table'; +import UnnamedTrans from '@/components/UnnamedTrans'; +import type { RequestError } from '@/hooks/use-api'; +import useApi from '@/hooks/use-api'; +import { getConnectorGroups } from '@/pages/Connectors/utils'; + +import * as styles from './index.module.scss'; + +type Props = { + userId: string; + identities: Identities; + onDelete?: (connectorId: string) => void; +}; + +type DisplayConnector = { + target: ConnectorResponse['target']; + userId?: string; + logo?: ConnectorResponse['logo']; + name: ConnectorResponse['name'] | string; +}; + +const ConnectorName = ({ name }: { name: DisplayConnector['name'] }) => + typeof name === 'string' ? {name} : ; + +const UserSocialIdentities = ({ userId, identities, onDelete }: Props) => { + const api = useApi(); + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { data, error, mutate } = useSWR('/api/connectors'); + const [deletingConnector, setDeletingConnector] = useState(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const connectorGroups = useMemo(() => { + if (!data?.length) { + return; + } + + return getConnectorGroups(data); + }, [data]); + + const isLoading = !connectorGroups && !error; + + const handleDelete = async (target: string) => { + if (isSubmitting) { + return; + } + + setIsSubmitting(true); + + try { + await api.delete(`/api/users/${userId}/identities/${target}`); + onDelete?.(target); + } finally { + setIsSubmitting(false); + } + }; + + const displayConnectors = useMemo(() => { + if (!connectorGroups) { + return; + } + + return Object.keys(identities).map((key): DisplayConnector => { + const { logo, name } = connectorGroups.find((group) => group.target === key) ?? {}; + const socialUserId = identities[key]?.userId; + + return { logo, name: name ?? t('connectors.unknown'), target: key, userId: socialUserId }; + }); + }, [connectorGroups, identities, t]); + + if (Object.keys(identities).length === 0) { + return
{t('user_details.connectors.not_connected')}
; + } + + return ( +
+ {displayConnectors && ( + ( +
+ +
+ +
+
+ ), + }, + { + title: t('user_details.connectors.user_id'), + dataIndex: 'userId', + colSpan: 8, + render: ({ userId = '' }) => ( +
+ {userId || '-'} + {userId && } +
+ ), + }, + { + title: null, + dataIndex: 'action', + colSpan: 3, + render: (connector) => ( +