feat: Bind data to TenantTable #2846 (#2883)

### What problem does this PR solve?

feat: Bind data to TenantTable #2846
feat: Add TenantTable

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2024-10-18 09:21:01 +08:00 committed by GitHub
parent 791afbba15
commit cf3106040a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 548 additions and 11 deletions

View File

@ -2,8 +2,19 @@ import { LanguageTranslationMap } from '@/constants/common';
import { ResponseGetType } from '@/interfaces/database/base';
import { IToken } from '@/interfaces/database/chat';
import { ITenantInfo } from '@/interfaces/database/knowledge';
import { ISystemStatus, IUserInfo } from '@/interfaces/database/user-setting';
import userService from '@/services/user-service';
import {
ISystemStatus,
ITenant,
ITenantUser,
IUserInfo,
} from '@/interfaces/database/user-setting';
import userService, {
addTenantUser,
agreeTenant,
deleteTenantUser,
listTenant,
listTenantUser,
} from '@/services/user-service';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Modal, message } from 'antd';
import DOMPurify from 'dompurify';
@ -215,3 +226,125 @@ export const useCreateSystemToken = () => {
return { data, loading, createToken: mutateAsync };
};
export const useListTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITenantUser[]>({
queryKey: ['listTenantUser', tenantId],
initialData: [],
gcTime: 0,
enabled: !!tenantId,
queryFn: async () => {
const { data } = await listTenantUser(tenantId);
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useAddTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['addTenantUser'],
mutationFn: async (email: string) => {
const { data } = await addTenantUser(tenantInfo.tenant_id, email);
if (data.retcode === 0) {
queryClient.invalidateQueries({ queryKey: ['listTenantUser'] });
}
return data?.retcode;
},
});
return { data, loading, addTenantUser: mutateAsync };
};
export const useDeleteTenantUser = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['deleteTenantUser'],
mutationFn: async ({
userId,
tenantId,
}: {
userId: string;
tenantId?: string;
}) => {
const { data } = await deleteTenantUser({
tenantId: tenantId ?? tenantInfo.tenant_id,
userId,
});
if (data.retcode === 0) {
message.success(t('message.deleted'));
queryClient.invalidateQueries({ queryKey: ['listTenantUser'] });
queryClient.invalidateQueries({ queryKey: ['listTenant'] });
}
return data?.data ?? [];
},
});
return { data, loading, deleteTenantUser: mutateAsync };
};
export const useListTenant = () => {
const { data: tenantInfo } = useFetchTenantInfo();
const tenantId = tenantInfo.tenant_id;
const {
data,
isFetching: loading,
refetch,
} = useQuery<ITenant[]>({
queryKey: ['listTenant', tenantId],
initialData: [],
gcTime: 0,
enabled: !!tenantId,
queryFn: async () => {
const { data } = await listTenant();
return data?.data ?? [];
},
});
return { data, loading, refetch };
};
export const useAgreeTenant = () => {
const queryClient = useQueryClient();
const { t } = useTranslation();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['agreeTenant'],
mutationFn: async (tenantId: string) => {
const { data } = await agreeTenant(tenantId);
if (data.retcode === 0) {
message.success(t('message.operated'));
queryClient.invalidateQueries({ queryKey: ['listTenant'] });
}
return data?.data ?? [];
},
});
return { data, loading, agreeTenant: mutateAsync };
};

View File

@ -60,3 +60,28 @@ interface Es {
number_of_nodes: number;
active_shards: number;
}
export interface ITenantUser {
avatar: null;
delta_seconds: number;
email: string;
is_active: string;
is_anonymous: string;
is_authenticated: string;
is_superuser: boolean;
nickname: string;
role: string;
status: string;
update_date: string;
user_id: string;
}
export interface ITenant {
avatar: string;
delta_seconds: number;
email: string;
nickname: string;
role: string;
tenant_id: string;
update_date: string;
}

View File

@ -27,7 +27,8 @@ export default {
close: 'Close',
preview: 'Preview',
move: 'Move',
warn: '提醒',
warn: 'Warn',
action: 'Action',
},
login: {
login: 'Sign in',
@ -584,6 +585,14 @@ The above is the content you need to summarize.`,
'Please add both embedding model and LLM in <b>Settings > Model providers</b> firstly.',
apiVersion: 'API-Version',
apiVersionMessage: 'Please input API version',
add: 'Add',
updateDate: 'Update Date',
role: 'Role',
invite: 'Invite',
agree: 'Agree',
refuse: 'Refuse',
teamMembers: 'Team Members',
joinedTeams: 'Joined Teams',
},
message: {
registered: 'Registered!',

View File

@ -28,6 +28,7 @@ export default {
preview: '預覽',
move: '移動',
warn: '提醒',
action: '操作',
},
login: {
login: '登入',
@ -540,6 +541,14 @@ export default {
GoogleRegionMessage: '請輸入 Google Cloud 區域',
modelProvidersWarn:
'請先在 <b>「設定」>「模型提供者」</b> 中新增嵌入模型和LLM。',
add: '添加',
updateDate: '更新日期',
role: '角色',
invite: '邀請',
agree: '同意',
refuse: '拒絕',
teamMembers: '團隊成員',
joinedTeams: '加入的團隊',
},
message: {
registered: '註冊成功',

View File

@ -28,6 +28,7 @@ export default {
preview: '预览',
move: '移动',
warn: '提醒',
action: '操作',
},
login: {
login: '登录',
@ -559,6 +560,14 @@ export default {
'请首先在 <b>设置 > 模型提供商</b> 中添加嵌入模型和 LLM。',
apiVersion: 'API版本',
apiVersionMessage: '请输入API版本!',
add: '添加',
updateDate: '更新日期',
role: '角色',
invite: '邀请',
agree: '同意',
refuse: '拒绝',
teamMembers: '团队成员',
joinedTeams: '加入的团队',
},
message: {
registered: '注册成功',

View File

@ -30,3 +30,9 @@ export const LocalLlmFactories = [
'OpenRouter',
'HuggingFace',
];
export enum TenantRole {
Owner = 'owner',
Invite = 'invite',
Normal = 'normal',
}

View File

@ -0,0 +1,52 @@
import { IModalProps } from '@/interfaces/common';
import { Form, Input, Modal } from 'antd';
import { useTranslation } from 'react-i18next';
const AddingUserModal = ({
visible,
hideModal,
loading,
onOk,
}: IModalProps<string>) => {
const [form] = Form.useForm();
const { t } = useTranslation();
type FieldType = {
email?: string;
};
const handleOk = async () => {
const ret = await form.validateFields();
return onOk?.(ret.email);
};
return (
<Modal
title={t('setting.add')}
open={visible}
onOk={handleOk}
onCancel={hideModal}
okButtonProps={{ loading }}
confirmLoading={loading}
>
<Form
name="basic"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
autoComplete="off"
form={form}
>
<Form.Item<FieldType>
label={t('setting.email')}
name="email"
rules={[{ required: true, message: t('namePlaceholder') }]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
};
export default AddingUserModal;

View File

@ -0,0 +1,68 @@
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import {
useAddTenantUser,
useAgreeTenant,
useDeleteTenantUser,
useFetchUserInfo,
} from '@/hooks/user-setting-hooks';
import { useCallback } from 'react';
export const useAddUser = () => {
const { addTenantUser } = useAddTenantUser();
const {
visible: addingTenantModalVisible,
hideModal: hideAddingTenantModal,
showModal: showAddingTenantModal,
} = useSetModalState();
const handleAddUserOk = useCallback(
async (email: string) => {
const retcode = await addTenantUser(email);
if (retcode === 0) {
hideAddingTenantModal();
}
},
[addTenantUser, hideAddingTenantModal],
);
return {
addingTenantModalVisible,
hideAddingTenantModal,
showAddingTenantModal,
handleAddUserOk,
};
};
export const useHandleDeleteUser = () => {
const { deleteTenantUser, loading } = useDeleteTenantUser();
const showDeleteConfirm = useShowDeleteConfirm();
const handleDeleteTenantUser = (userId: string) => () => {
showDeleteConfirm({
onOk: async () => {
const retcode = await deleteTenantUser({ userId });
if (retcode === 0) {
}
return;
},
});
};
return { handleDeleteTenantUser, loading };
};
export const useHandleAgreeTenant = () => {
const { agreeTenant } = useAgreeTenant();
const { deleteTenantUser } = useDeleteTenantUser();
const { data: user } = useFetchUserInfo();
const handleAgree = (tenantId: string, isAgree: boolean) => () => {
if (isAgree) {
agreeTenant(tenantId);
} else {
deleteTenantUser({ tenantId, userId: user.id });
}
};
return { handleAgree };
};

View File

@ -1,5 +1,8 @@
.teamWrapper {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
.teamCard {
// width: 100%;
}

View File

@ -1,25 +1,70 @@
import { Button, Card, Flex } from 'antd';
import {
useFetchUserInfo,
useListTenantUser,
} from '@/hooks/user-setting-hooks';
import { Button, Card, Flex, Space } from 'antd';
import { useTranslation } from 'react-i18next';
import { useTranslate } from '@/hooks/common-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { TeamOutlined, UserAddOutlined, UserOutlined } from '@ant-design/icons';
import AddingUserModal from './add-user-modal';
import { useAddUser } from './hooks';
import styles from './index.less';
import TenantTable from './tenant-table';
import UserTable from './user-table';
const iconStyle = { fontSize: 20, color: '#1677ff' };
const UserSettingTeam = () => {
const { data: userInfo } = useFetchUserInfo();
const { t } = useTranslate('setting');
const { t } = useTranslation();
useListTenantUser();
const {
addingTenantModalVisible,
hideAddingTenantModal,
showAddingTenantModal,
handleAddUserOk,
} = useAddUser();
return (
<div className={styles.teamWrapper}>
<Card className={styles.teamCard}>
<Flex align="center" justify={'space-between'}>
<span>
{userInfo.nickname} {t('workspace')}
{userInfo.nickname} {t('setting.workspace')}
</span>
<Button type="primary" disabled>
{t('upgrade')}
<Button type="primary" onClick={showAddingTenantModal}>
<UserAddOutlined />
{t('setting.invite')}
</Button>
</Flex>
</Card>
<Card
title={
<Space>
<UserOutlined style={iconStyle} /> {t('setting.teamMembers')}
</Space>
}
bordered={false}
>
<UserTable></UserTable>
</Card>
<Card
title={
<Space>
<TeamOutlined style={iconStyle} /> {t('setting.joinedTeams')}
</Space>
}
bordered={false}
>
<TenantTable></TenantTable>
</Card>
{addingTenantModalVisible && (
<AddingUserModal
visible
hideModal={hideAddingTenantModal}
onOk={handleAddUserOk}
></AddingUserModal>
)}
</div>
);
};

View File

@ -0,0 +1,65 @@
import { useListTenant } from '@/hooks/user-setting-hooks';
import { ITenant } from '@/interfaces/database/user-setting';
import { formatDate } from '@/utils/date';
import type { TableProps } from 'antd';
import { Button, Space, Table } from 'antd';
import { useTranslation } from 'react-i18next';
import { TenantRole } from '../constants';
import { useHandleAgreeTenant } from './hooks';
const TenantTable = () => {
const { t } = useTranslation();
const { data, loading } = useListTenant();
const { handleAgree } = useHandleAgreeTenant();
const columns: TableProps<ITenant>['columns'] = [
{
title: t('common.name'),
dataIndex: 'nickname',
key: 'nickname',
},
{
title: t('setting.email'),
dataIndex: 'email',
key: 'email',
},
{
title: t('setting.updateDate'),
dataIndex: 'update_date',
key: 'update_date',
render(value) {
return formatDate(value);
},
},
{
title: t('common.action'),
key: 'action',
render: (_, { role, tenant_id }) => {
if (role === TenantRole.Invite) {
return (
<Space>
<Button type="link" onClick={handleAgree(tenant_id, true)}>
{t(`setting.agree`)}
</Button>
<Button type="link" onClick={handleAgree(tenant_id, false)}>
{t(`setting.refuse`)}
</Button>
</Space>
);
}
},
},
];
return (
<Table<ITenant>
columns={columns}
dataSource={data}
rowKey={'tenant_id'}
loading={loading}
pagination={false}
/>
);
};
export default TenantTable;

View File

@ -0,0 +1,73 @@
import { useListTenantUser } from '@/hooks/user-setting-hooks';
import { ITenantUser } from '@/interfaces/database/user-setting';
import { formatDate } from '@/utils/date';
import { DeleteOutlined } from '@ant-design/icons';
import type { TableProps } from 'antd';
import { Button, Table, Tag } from 'antd';
import { useTranslation } from 'react-i18next';
import { TenantRole } from '../constants';
import { useHandleDeleteUser } from './hooks';
const ColorMap = {
[TenantRole.Normal]: 'green',
[TenantRole.Invite]: 'orange',
[TenantRole.Owner]: 'red',
};
const UserTable = () => {
const { data, loading } = useListTenantUser();
const { handleDeleteTenantUser } = useHandleDeleteUser();
const { t } = useTranslation();
const columns: TableProps<ITenantUser>['columns'] = [
{
title: t('common.name'),
dataIndex: 'nickname',
key: 'nickname',
},
{
title: t('setting.email'),
dataIndex: 'email',
key: 'email',
},
{
title: t('setting.role'),
dataIndex: 'role',
key: 'role',
render(value, { role }) {
return (
<Tag color={ColorMap[role as keyof typeof ColorMap]}>{role}</Tag>
);
},
},
{
title: t('setting.updateDate'),
dataIndex: 'update_date',
key: 'update_date',
render(value) {
return formatDate(value);
},
},
{
title: t('common.action'),
key: 'action',
render: (_, record) => (
<Button type="text" onClick={handleDeleteTenantUser(record.user_id)}>
<DeleteOutlined size={20} />
</Button>
),
},
];
return (
<Table<ITenantUser>
rowKey={'user_id'}
columns={columns}
dataSource={data}
loading={loading}
pagination={false}
/>
);
};
export default UserTable;

View File

@ -1,6 +1,6 @@
import api from '@/utils/api';
import registerServer from '@/utils/register-server';
import request from '@/utils/request';
import request, { post } from '@/utils/request';
const {
login,
@ -105,4 +105,23 @@ const methods = {
const userService = registerServer<keyof typeof methods>(methods, request);
export const listTenantUser = (tenantId: string) =>
request.get(api.listTenantUser(tenantId));
export const addTenantUser = (tenantId: string, email: string) =>
post(api.addTenantUser(tenantId), { email });
export const deleteTenantUser = ({
tenantId,
userId,
}: {
tenantId: string;
userId: string;
}) => request.delete(api.deleteTenantUser(tenantId, userId));
export const listTenant = () => request.get(api.listTenant);
export const agreeTenant = (tenantId: string) =>
request.put(api.agreeTenant(tenantId));
export default userService;

View File

@ -12,6 +12,15 @@ export default {
tenant_info: `${api_host}/user/tenant_info`,
set_tenant_info: `${api_host}/user/set_tenant_info`,
// team
addTenantUser: (tenantId: string) => `${api_host}/tenant/${tenantId}/user`,
listTenantUser: (tenantId: string) =>
`${api_host}/tenant/${tenantId}/user/list`,
deleteTenantUser: (tenantId: string, userId: string) =>
`${api_host}/tenant/${tenantId}/user/${userId}`,
listTenant: `${api_host}/tenant/list`,
agreeTenant: (tenantId: string) => `${api_host}/tenant/agree/${tenantId}`,
// llm model
factories_list: `${api_host}/llm/factories`,
llm_list: `${api_host}/llm/list`,

View File

@ -135,3 +135,15 @@ request.interceptors.response.use(async (response: any, options) => {
});
export default request;
export const get = (url: string) => {
return request.get(url);
};
export const post = (url: string, body: any) => {
return request.post(url, { data: body });
};
export const drop = () => {};
export const put = () => {};