0
Fork 0
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:
Wang Sijie 2022-03-23 11:48:42 +08:00 committed by GitHub
parent 4f41162ac3
commit e8b4862843
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 206 additions and 0 deletions

View file

@ -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);
}
}
}

View file

@ -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;

View file

@ -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"

View file

@ -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.',
},
},
},
};

View file

@ -237,6 +237,12 @@ const translation = {
custom_data_invalid: '自定义数据必须是有效的 JSON',
save_changes: '保存设置',
saved: '保存成功!',
connectors: {
connectors: '连接器',
user_id: '用户ID',
remove: '删除',
not_connected: '该用户还没有绑定社交账号。',
},
},
},
};

View file

@ -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<