mirror of
https://github.com/logto-io/logto.git
synced 2025-01-06 20:40:08 -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 CreateSuccess from './components/CreateSuccess';
|
||||||
import DeleteForm from './components/DeleteForm';
|
import DeleteForm from './components/DeleteForm';
|
||||||
import ResetPasswordForm from './components/ResetPasswordForm';
|
import ResetPasswordForm from './components/ResetPasswordForm';
|
||||||
|
import UserConnectors from './components/UserConnectors';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type FormData = {
|
type FormData = {
|
||||||
|
@ -207,6 +208,18 @@ const UserDetails = () => {
|
||||||
>
|
>
|
||||||
<TextInput readOnly {...register('roles')} />
|
<TextInput readOnly {...register('roles')} />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
<FormField
|
||||||
|
title="admin_console.user_details.field_connectors"
|
||||||
|
className={styles.textField}
|
||||||
|
>
|
||||||
|
<UserConnectors
|
||||||
|
userId={data.id}
|
||||||
|
connectors={data.identities}
|
||||||
|
onDelete={() => {
|
||||||
|
void mutate();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
<FormField
|
<FormField
|
||||||
isRequired
|
isRequired
|
||||||
title="admin_console.user_details.field_custom_data"
|
title="admin_console.user_details.field_custom_data"
|
||||||
|
|
|
@ -238,6 +238,12 @@ const translation = {
|
||||||
custom_data_invalid: 'Custom data must be a valid JSON',
|
custom_data_invalid: 'Custom data must be a valid JSON',
|
||||||
save_changes: 'Save changes',
|
save_changes: 'Save changes',
|
||||||
saved: 'Saved!',
|
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',
|
custom_data_invalid: '自定义数据必须是有效的 JSON',
|
||||||
save_changes: '保存设置',
|
save_changes: '保存设置',
|
||||||
saved: '保存成功!',
|
saved: '保存成功!',
|
||||||
|
connectors: {
|
||||||
|
connectors: '连接器',
|
||||||
|
user_id: '用户ID',
|
||||||
|
remove: '删除',
|
||||||
|
not_connected: '该用户还没有绑定社交账号。',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const userInfoSelectFields = Object.freeze([
|
||||||
'avatar',
|
'avatar',
|
||||||
'roleNames',
|
'roleNames',
|
||||||
'customData',
|
'customData',
|
||||||
|
'identities',
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
export type UserInfo<Keys extends keyof CreateUser = typeof userInfoSelectFields[number]> = Pick<
|
export type UserInfo<Keys extends keyof CreateUser = typeof userInfoSelectFields[number]> = Pick<
|
||||||
|
|
Loading…
Reference in a new issue