### 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)tags/v0.13.0
| @@ -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 }; | |||
| }; | |||
| @@ -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; | |||
| } | |||
| @@ -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!', | |||
| @@ -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: '註冊成功', | |||
| @@ -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: '注册成功', | |||
| @@ -30,3 +30,9 @@ export const LocalLlmFactories = [ | |||
| 'OpenRouter', | |||
| 'HuggingFace', | |||
| ]; | |||
| export enum TenantRole { | |||
| Owner = 'owner', | |||
| Invite = 'invite', | |||
| Normal = 'normal', | |||
| } | |||
| @@ -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; | |||
| @@ -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 }; | |||
| }; | |||
| @@ -1,5 +1,8 @@ | |||
| .teamWrapper { | |||
| width: 100%; | |||
| display: flex; | |||
| flex-direction: column; | |||
| gap: 20px; | |||
| .teamCard { | |||
| // width: 100%; | |||
| } | |||
| @@ -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> | |||
| ); | |||
| }; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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`, | |||
| @@ -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 = () => {}; | |||