From cf3106040a95e94eedc5ee91a43899f9c79d2588 Mon Sep 17 00:00:00 2001 From: balibabu Date: Fri, 18 Oct 2024 09:21:01 +0800 Subject: [PATCH] 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) --- web/src/hooks/user-setting-hooks.tsx | 137 +++++++++++++++++- web/src/interfaces/database/user-setting.ts | 25 ++++ web/src/locales/en.ts | 11 +- web/src/locales/zh-traditional.ts | 9 ++ web/src/locales/zh.ts | 9 ++ web/src/pages/user-setting/constants.tsx | 6 + .../setting-team/add-user-modal.tsx | 52 +++++++ .../pages/user-setting/setting-team/hooks.ts | 68 +++++++++ .../user-setting/setting-team/index.less | 3 + .../pages/user-setting/setting-team/index.tsx | 59 +++++++- .../setting-team/tenant-table.tsx | 65 +++++++++ .../user-setting/setting-team/user-table.tsx | 73 ++++++++++ web/src/services/user-service.ts | 21 ++- web/src/utils/api.ts | 9 ++ web/src/utils/request.ts | 12 ++ 15 files changed, 548 insertions(+), 11 deletions(-) create mode 100644 web/src/pages/user-setting/setting-team/add-user-modal.tsx create mode 100644 web/src/pages/user-setting/setting-team/hooks.ts create mode 100644 web/src/pages/user-setting/setting-team/tenant-table.tsx create mode 100644 web/src/pages/user-setting/setting-team/user-table.tsx diff --git a/web/src/hooks/user-setting-hooks.tsx b/web/src/hooks/user-setting-hooks.tsx index 68cd9e482..9b0288cfd 100644 --- a/web/src/hooks/user-setting-hooks.tsx +++ b/web/src/hooks/user-setting-hooks.tsx @@ -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({ + 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({ + 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 }; +}; diff --git a/web/src/interfaces/database/user-setting.ts b/web/src/interfaces/database/user-setting.ts index 61e16c0ea..17e59a217 100644 --- a/web/src/interfaces/database/user-setting.ts +++ b/web/src/interfaces/database/user-setting.ts @@ -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; +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 247e586db..d843eabda 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -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 Settings > Model providers 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!', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 9b57dd75a..fc2eda46d 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -28,6 +28,7 @@ export default { preview: '預覽', move: '移動', warn: '提醒', + action: '操作', }, login: { login: '登入', @@ -540,6 +541,14 @@ export default { GoogleRegionMessage: '請輸入 Google Cloud 區域', modelProvidersWarn: '請先在 「設定」>「模型提供者」 中新增嵌入模型和LLM。', + add: '添加', + updateDate: '更新日期', + role: '角色', + invite: '邀請', + agree: '同意', + refuse: '拒絕', + teamMembers: '團隊成員', + joinedTeams: '加入的團隊', }, message: { registered: '註冊成功', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 687cc9cd9..dc59ac43e 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -28,6 +28,7 @@ export default { preview: '预览', move: '移动', warn: '提醒', + action: '操作', }, login: { login: '登录', @@ -559,6 +560,14 @@ export default { '请首先在 设置 > 模型提供商 中添加嵌入模型和 LLM。', apiVersion: 'API版本', apiVersionMessage: '请输入API版本!', + add: '添加', + updateDate: '更新日期', + role: '角色', + invite: '邀请', + agree: '同意', + refuse: '拒绝', + teamMembers: '团队成员', + joinedTeams: '加入的团队', }, message: { registered: '注册成功', diff --git a/web/src/pages/user-setting/constants.tsx b/web/src/pages/user-setting/constants.tsx index 1fa4c859e..597839ac0 100644 --- a/web/src/pages/user-setting/constants.tsx +++ b/web/src/pages/user-setting/constants.tsx @@ -30,3 +30,9 @@ export const LocalLlmFactories = [ 'OpenRouter', 'HuggingFace', ]; + +export enum TenantRole { + Owner = 'owner', + Invite = 'invite', + Normal = 'normal', +} diff --git a/web/src/pages/user-setting/setting-team/add-user-modal.tsx b/web/src/pages/user-setting/setting-team/add-user-modal.tsx new file mode 100644 index 000000000..5adc43e61 --- /dev/null +++ b/web/src/pages/user-setting/setting-team/add-user-modal.tsx @@ -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) => { + const [form] = Form.useForm(); + const { t } = useTranslation(); + + type FieldType = { + email?: string; + }; + + const handleOk = async () => { + const ret = await form.validateFields(); + + return onOk?.(ret.email); + }; + + return ( + +
+ + label={t('setting.email')} + name="email" + rules={[{ required: true, message: t('namePlaceholder') }]} + > + + + +
+ ); +}; + +export default AddingUserModal; diff --git a/web/src/pages/user-setting/setting-team/hooks.ts b/web/src/pages/user-setting/setting-team/hooks.ts new file mode 100644 index 000000000..88e2dabed --- /dev/null +++ b/web/src/pages/user-setting/setting-team/hooks.ts @@ -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 }; +}; diff --git a/web/src/pages/user-setting/setting-team/index.less b/web/src/pages/user-setting/setting-team/index.less index bca9dc2e4..12c572af9 100644 --- a/web/src/pages/user-setting/setting-team/index.less +++ b/web/src/pages/user-setting/setting-team/index.less @@ -1,5 +1,8 @@ .teamWrapper { width: 100%; + display: flex; + flex-direction: column; + gap: 20px; .teamCard { // width: 100%; } diff --git a/web/src/pages/user-setting/setting-team/index.tsx b/web/src/pages/user-setting/setting-team/index.tsx index 098f4bf65..a1eaa495e 100644 --- a/web/src/pages/user-setting/setting-team/index.tsx +++ b/web/src/pages/user-setting/setting-team/index.tsx @@ -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 (
- {userInfo.nickname} {t('workspace')} + {userInfo.nickname} {t('setting.workspace')} - + + {t('setting.teamMembers')} + + } + bordered={false} + > + + + + {t('setting.joinedTeams')} + + } + bordered={false} + > + + + {addingTenantModalVisible && ( + + )}
); }; diff --git a/web/src/pages/user-setting/setting-team/tenant-table.tsx b/web/src/pages/user-setting/setting-team/tenant-table.tsx new file mode 100644 index 000000000..cc7a4e2d5 --- /dev/null +++ b/web/src/pages/user-setting/setting-team/tenant-table.tsx @@ -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['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 ( + + + + + ); + } + }, + }, + ]; + + return ( + + columns={columns} + dataSource={data} + rowKey={'tenant_id'} + loading={loading} + pagination={false} + /> + ); +}; + +export default TenantTable; diff --git a/web/src/pages/user-setting/setting-team/user-table.tsx b/web/src/pages/user-setting/setting-team/user-table.tsx new file mode 100644 index 000000000..8dbbc9c3e --- /dev/null +++ b/web/src/pages/user-setting/setting-team/user-table.tsx @@ -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['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 ( + {role} + ); + }, + }, + { + title: t('setting.updateDate'), + dataIndex: 'update_date', + key: 'update_date', + render(value) { + return formatDate(value); + }, + }, + { + title: t('common.action'), + key: 'action', + render: (_, record) => ( + + ), + }, + ]; + + return ( + + rowKey={'user_id'} + columns={columns} + dataSource={data} + loading={loading} + pagination={false} + /> + ); +}; + +export default UserTable; diff --git a/web/src/services/user-service.ts b/web/src/services/user-service.ts index 99617bae0..c9739bb15 100644 --- a/web/src/services/user-service.ts +++ b/web/src/services/user-service.ts @@ -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(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; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 99639f4b5..7fe6c6a29 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -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`, diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 2627362bd..52ecc5f9c 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -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 = () => {};