mirror of
https://github.com/logto-io/logto.git
synced 2024-12-23 20:33:16 -05:00
feat(console): user details connectors (#434)
* feat(console): user details connectors * fix: is submiting
This commit is contained in:
parent
4f41162ac3
commit
e8b4862843
6 changed files with 206 additions and 0 deletions
|
@ -0,0 +1,45 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.table {
|
||||
thead {
|
||||
th {
|
||||
padding: _.unit(2);
|
||||
}
|
||||
|
||||
th:first-child {
|
||||
padding-left: _.unit(4);
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
td {
|
||||
padding: _.unit(2);
|
||||
font: var(--font-body-2);
|
||||
|
||||
&:first-child {
|
||||
padding-left: _.unit(4);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-left: _.unit(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.connectorName {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: _.unit(2);
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: _.unit(3);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
import { Languages } from '@logto/phrases';
|
||||
import { ConnectorDTO, Identities } from '@logto/schemas';
|
||||
import { Optional } from '@silverhand/essentials';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import ImagePlaceholder from '@/components/ImagePlaceholder';
|
||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
|
||||
import * as styles from './UserConnectors.module.scss';
|
||||
|
||||
type Props = {
|
||||
userId: string;
|
||||
connectors: Identities;
|
||||
onDelete?: (connectorId: string) => void;
|
||||
};
|
||||
|
||||
type DisplayConnector = {
|
||||
id: string;
|
||||
userId?: string;
|
||||
logo: string;
|
||||
name: Record<Languages, string>;
|
||||
};
|
||||
|
||||
const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { data, error } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');
|
||||
const isLoading = !data && !error;
|
||||
const [isSubmiting, setIsSubmiting] = useState(false);
|
||||
|
||||
const handleDelete = async (connectorId: string) => {
|
||||
if (isSubmiting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmiting(true);
|
||||
|
||||
try {
|
||||
await api.delete(`/api/users/${userId}/identities/${connectorId}`);
|
||||
onDelete?.(connectorId);
|
||||
} finally {
|
||||
setIsSubmiting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const displayConnectors: Optional<DisplayConnector[]> = useMemo(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Object.keys(connectors).map((key) => {
|
||||
const connector = data.find(({ id }) => id === key);
|
||||
|
||||
if (!connector) {
|
||||
return {
|
||||
logo: '',
|
||||
name: {
|
||||
'zh-CN': '未知连接器',
|
||||
en: 'Unknown Connector',
|
||||
},
|
||||
id: key,
|
||||
userId: connectors[key]?.userId,
|
||||
};
|
||||
}
|
||||
|
||||
const { logo, name } = connector.metadata;
|
||||
|
||||
return {
|
||||
logo,
|
||||
name,
|
||||
id: key,
|
||||
userId: connectors[key]?.userId,
|
||||
};
|
||||
});
|
||||
}, [data, connectors]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isLoading && <div>Loading</div>}
|
||||
{error && error}
|
||||
{displayConnectors && (
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('user_details.connectors.connectors')}</th>
|
||||
<th>{t('user_details.connectors.user_id')}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayConnectors.length === 0 && (
|
||||
<tr>
|
||||
<td rowSpan={3}>{t('user_details.connectors.not_connected')}</td>
|
||||
</tr>
|
||||
)}
|
||||
{displayConnectors.map((connector) => (
|
||||
<tr key={connector.id}>
|
||||
<td>
|
||||
<div className={styles.connectorName}>
|
||||
<div>
|
||||
{connector.logo.startsWith('http') ? (
|
||||
<img src={connector.logo} />
|
||||
) : (
|
||||
<ImagePlaceholder size={32} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.name}>
|
||||
<UnnamedTrans resource={connector.name} />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>{connector.userId}</td>
|
||||
<td>
|
||||
<Button
|
||||
title="admin_console.user_details.connectors.remove"
|
||||
type="plain"
|
||||
onClick={() => {
|
||||
void handleDelete(connector.id);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserConnectors;
|
|
@ -27,6 +27,7 @@ import { safeParseJson } from '@/utilities/json';
|
|||
import CreateSuccess from './components/CreateSuccess';
|
||||
import DeleteForm from './components/DeleteForm';
|
||||
import ResetPasswordForm from './components/ResetPasswordForm';
|
||||
import UserConnectors from './components/UserConnectors';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type FormData = {
|
||||
|
@ -207,6 +208,18 @@ const UserDetails = () => {
|
|||
>
|
||||
<TextInput readOnly {...register('roles')} />
|
||||
</FormField>
|
||||
<FormField
|
||||
title="admin_console.user_details.field_connectors"
|
||||
className={styles.textField}
|
||||
>
|
||||
<UserConnectors
|
||||
userId={data.id}
|
||||
connectors={data.identities}
|
||||
onDelete={() => {
|
||||
void mutate();
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
isRequired
|
||||
title="admin_console.user_details.field_custom_data"
|
||||
|
|
|
@ -238,6 +238,12 @@ const translation = {
|
|||
custom_data_invalid: 'Custom data must be a valid JSON',
|
||||
save_changes: 'Save changes',
|
||||
saved: 'Saved!',
|
||||
connectors: {
|
||||
connectors: 'Connectors',
|
||||
user_id: 'User ID',
|
||||
remove: 'remove',
|
||||
not_connected: 'The user is not connected to any social connector.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -237,6 +237,12 @@ const translation = {
|
|||
custom_data_invalid: '自定义数据必须是有效的 JSON',
|
||||
save_changes: '保存设置',
|
||||
saved: '保存成功!',
|
||||
connectors: {
|
||||
connectors: '连接器',
|
||||
user_id: '用户ID',
|
||||
remove: '删除',
|
||||
not_connected: '该用户还没有绑定社交账号。',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ export const userInfoSelectFields = Object.freeze([
|
|||
'avatar',
|
||||
'roleNames',
|
||||
'customData',
|
||||
'identities',
|
||||
] as const);
|
||||
|
||||
export type UserInfo<Keys extends keyof CreateUser = typeof userInfoSelectFields[number]> = Pick<
|
||||
|
|
Loading…
Reference in a new issue