From 2c0035dcea71c89446b7d717f811a8360dd79d8b Mon Sep 17 00:00:00 2001 From: UN1C0DE Date: Tue, 28 Oct 2025 22:25:43 +0800 Subject: [PATCH] Feat: Admin UI (#10857) ### What problem does this PR solve? Add admin UI for RAGFlow ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/.umirc.ts | 7 + web/src/components/ui/scroll-area.tsx | 3 +- web/src/locales/en.ts | 132 +++- web/src/pages/404.jsx | 14 +- .../admin/components/enterprise-feature.tsx | 13 + .../pages/admin/components/theme-switch.tsx | 47 ++ .../admin/forms/change-password-form.tsx | 150 ++++ web/src/pages/admin/forms/email-form.tsx | 102 +++ .../pages/admin/forms/import-excel-form.tsx | 155 ++++ web/src/pages/admin/forms/role-form.tsx | 207 ++++++ web/src/pages/admin/forms/user-form.tsx | 215 ++++++ web/src/pages/admin/index.tsx | 265 +++++++ web/src/pages/admin/layout.tsx | 133 ++++ web/src/pages/admin/monitoring.tsx | 13 + web/src/pages/admin/roles.tsx | 229 ++++++ web/src/pages/admin/service-detail.tsx | 86 +++ web/src/pages/admin/service-status.tsx | 460 ++++++++++++ web/src/pages/admin/user-detail.tsx | 433 +++++++++++ web/src/pages/admin/users.tsx | 691 ++++++++++++++++++ web/src/pages/admin/utils.tsx | 85 +++ web/src/pages/admin/whitelist.tsx | 522 +++++++++++++ web/src/routes.ts | 58 ++ web/src/services/admin-service.ts | 379 ++++++++++ web/src/utils/api.ts | 43 ++ web/src/wrappers/authAdmin.tsx | 9 + 25 files changed, 4442 insertions(+), 9 deletions(-) create mode 100644 web/src/pages/admin/components/enterprise-feature.tsx create mode 100644 web/src/pages/admin/components/theme-switch.tsx create mode 100644 web/src/pages/admin/forms/change-password-form.tsx create mode 100644 web/src/pages/admin/forms/email-form.tsx create mode 100644 web/src/pages/admin/forms/import-excel-form.tsx create mode 100644 web/src/pages/admin/forms/role-form.tsx create mode 100644 web/src/pages/admin/forms/user-form.tsx create mode 100644 web/src/pages/admin/index.tsx create mode 100644 web/src/pages/admin/layout.tsx create mode 100644 web/src/pages/admin/monitoring.tsx create mode 100644 web/src/pages/admin/roles.tsx create mode 100644 web/src/pages/admin/service-detail.tsx create mode 100644 web/src/pages/admin/service-status.tsx create mode 100644 web/src/pages/admin/user-detail.tsx create mode 100644 web/src/pages/admin/users.tsx create mode 100644 web/src/pages/admin/utils.tsx create mode 100644 web/src/pages/admin/whitelist.tsx create mode 100644 web/src/services/admin-service.ts create mode 100644 web/src/wrappers/authAdmin.tsx diff --git a/web/.umirc.ts b/web/.umirc.ts index 7cf7f16b8..044ea3036 100644 --- a/web/.umirc.ts +++ b/web/.umirc.ts @@ -38,6 +38,13 @@ export default defineConfig({ { from: 'node_modules/monaco-editor/min/vs/', to: 'dist/vs/' }, ], proxy: [ + { + context: ['/api/v1/admin'], + target: 'http://127.0.0.1:9381/', + changeOrigin: true, + ws: true, + logger: console, + }, { context: ['/api', '/v1'], target: 'http://127.0.0.1:9380/', diff --git a/web/src/components/ui/scroll-area.tsx b/web/src/components/ui/scroll-area.tsx index 141463d83..e8f989ee2 100644 --- a/web/src/components/ui/scroll-area.tsx +++ b/web/src/components/ui/scroll-area.tsx @@ -39,7 +39,8 @@ const ScrollArea = React.forwardRef< {children} - + + )); diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 88c17b1e1..718a867be 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -278,8 +278,8 @@ export default { tocExtractionTip: " For existing chunks, generate a hierarchical table of contents (one directory per file). During queries, when Directory Enhancement is activated, the system will use a large model to determine which directory items are relevant to the user's question, thereby identifying the relevant chunks.", deleteGenerateModalContent: ` -

Deleting the generated {{type}} results - will remove all derived entities and relationships from this dataset. +

Deleting the generated {{type}} results + will remove all derived entities and relationships from this dataset. Your original files will remain intact.


Do you want to continue? @@ -1813,15 +1813,15 @@ Important structured information may include: names, dates, locations, events, k `, changeStepModalTitle: 'Step Switch Warning', changeStepModalContent: ` -

You are currently editing the results of this stage.

-

If you switch to a later stage, your changes will be lost.

+

You are currently editing the results of this stage.

+

If you switch to a later stage, your changes will be lost.

To keep them, please click Rerun to re-run the current stage.

`, changeStepModalConfirmText: 'Switch Anyway', changeStepModalCancelText: 'Cancel', unlinkPipelineModalTitle: 'Unlink Ingestion pipeline', unlinkPipelineModalContent: ` -

Once unlinked, this Dataset will no longer be connected to the current Ingestion pipeline.

-

Files that are already being parsed will continue until completion

+

Once unlinked, this Dataset will no longer be connected to the current Ingestion pipeline.

+

Files that are already being parsed will continue until completion

Files that are not yet parsed will no longer be processed


Are you sure you want to proceed?

`, unlinkPipelineModalConfirmText: 'Unlink', @@ -1837,5 +1837,125 @@ Important structured information may include: names, dates, locations, events, k processingFailedTip: 'Total failed processes', processing: 'Processing', }, + admin: { + loginTitle: 'RAGFlow ADMIN', + title: 'RAGFlow admin', + confirm: 'Confirm', + close: 'Close', + yes: 'Yes', + no: 'No', + delete: 'Delete', + cancel: 'Cancel', + reset: 'Reset', + import: 'Import', + description: 'Description', + noDescription: 'No description', + + resourceType: { + dataset: 'Dataset', + chat: 'Chat', + agent: 'Agent', + search: 'Search', + file: 'File', + team: 'Team', + memory: 'Memory', + }, + + permissionType: { + enable: 'Enable', + read: 'Read', + write: 'Write', + share: 'Share', + }, + + serviceStatus: 'Service status', + userManagement: 'User management', + registrationWhitelist: 'Registration whitelist', + roles: 'Roles', + monitoring: 'Monitoring', + + active: 'Active', + inactive: 'Inactive', + enable: 'Enable', + disable: 'Disable', + all: 'All', + actions: 'Actions', + newUser: 'New User', + email: 'Email', + name: 'Name', + nickname: 'Nickname', + status: 'Status', + id: 'ID', + serviceType: 'Service type', + host: 'Host', + port: 'Port', + + role: 'Role', + user: 'User', + superuser: 'Superuser', + + createTime: 'Create time', + lastLoginTime: 'Last login time', + lastUpdateTime: 'Last update time', + + isAnonymous: 'Is Anonymous', + + deleteUser: 'Delete user', + deleteUserConfirmation: 'Are you sure you want to delete this user?', + + createNewUser: 'Create new user', + changePassword: 'Change password', + newPassword: 'New password', + confirmNewPassword: 'Confirm new password', + password: 'Password', + confirmPassword: 'Confirm password', + + invalidEmail: 'Please input a valid email address!', + passwordRequired: 'Please input your password!', + passwordMinLength: 'Password must be more than 8 characters.', + confirmPasswordRequired: 'Please confirm your password!', + confirmPasswordDoNotMatch: 'The password that you entered do not match!', + + read: 'Read', + write: 'Write', + share: 'Share', + create: 'Create', + + extraInfo: 'Extra information', + serviceDetail: `Service {{name}} detail`, + + whitelistManagement: 'Whitelist management', + exportAsExcel: 'Export Excel', + importFromExcel: 'Import Excel', + createEmail: 'Create email', + deleteEmail: 'Delete email', + editEmail: 'Edit email', + deleteWhitelistEmailConfirmation: + 'Are you sure you want to delete this email from whitelist? This action cannot be undone.', + + importWhitelist: 'Import whitelist (excel)', + importSelectExcelFile: 'Excel file (.xlsx)', + importOverwriteExistingEmails: 'Overwrite existing emails', + importInvalidExcelFile: 'Please select a valid Excel file', + importFileRequired: 'Please select a file to import', + importFileTips: + 'File must contain a single header column named email.', + + chunkNum: 'Chunks', + docNum: 'Documents', + tokenNum: 'Tokens used', + language: 'Language', + createDate: 'Create date', + updateDate: 'Update date', + permission: 'Permission', + + agentTitle: 'Agent title', + canvasCategory: 'Canvas category', + + newRole: 'New Role', + addNewRole: 'Add new role', + roleName: 'Role name', + resources: 'Resources', + }, }, }; diff --git a/web/src/pages/404.jsx b/web/src/pages/404.jsx index db9ee69c8..7ca998ed7 100644 --- a/web/src/pages/404.jsx +++ b/web/src/pages/404.jsx @@ -1,14 +1,24 @@ +import { Routes } from '@/routes'; import { Button, Result } from 'antd'; -import { history } from 'umi'; +import { history, useLocation } from 'umi'; const NoFoundPage = () => { + const location = useLocation(); + return ( history.push('/')}> + } diff --git a/web/src/pages/admin/components/enterprise-feature.tsx b/web/src/pages/admin/components/enterprise-feature.tsx new file mode 100644 index 000000000..6b92d2281 --- /dev/null +++ b/web/src/pages/admin/components/enterprise-feature.tsx @@ -0,0 +1,13 @@ +import { IS_ENTERPRISE } from '../utils'; + +export default function EnterpriseFeature({ + children, +}: { + children: () => React.ReactNode; +}) { + return IS_ENTERPRISE + ? typeof children === 'function' + ? children() + : children + : null; +} diff --git a/web/src/pages/admin/components/theme-switch.tsx b/web/src/pages/admin/components/theme-switch.tsx new file mode 100644 index 000000000..6ba97e3f9 --- /dev/null +++ b/web/src/pages/admin/components/theme-switch.tsx @@ -0,0 +1,47 @@ +import { useIsDarkTheme, useTheme } from '@/components/theme-provider'; +import { ThemeEnum } from '@/constants/common'; +import { cn } from '@/lib/utils'; +import { Root, Thumb } from '@radix-ui/react-switch'; +import { LucideMoon, LucideSun } from 'lucide-react'; +import { forwardRef } from 'react'; + +const ThemeSwitch = forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { setTheme } = useTheme(); + const isDark = useIsDarkTheme(); + + return ( + + setTheme(value ? ThemeEnum.Dark : ThemeEnum.Light) + } + > +
+
+ + +
+
+ + +
+ ); +}); + +export default ThemeSwitch; diff --git a/web/src/pages/admin/forms/change-password-form.tsx b/web/src/pages/admin/forms/change-password-form.tsx new file mode 100644 index 000000000..a0627c916 --- /dev/null +++ b/web/src/pages/admin/forms/change-password-form.tsx @@ -0,0 +1,150 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useCallback, useId, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +interface ChangePasswordFormData { + newPassword: string; + confirmPassword: string; +} + +interface ChangePasswordFormProps { + id: string; + form: ReturnType>; + email?: string; + onSubmit?: (data: ChangePasswordFormData) => void; +} + +export const ChangePasswordForm = ({ + id, + form, + email, + onSubmit = () => {}, +}: ChangePasswordFormProps) => { + const { t } = useTranslation(); + + return ( +
+ + {/* Email field (readonly) */} +
+ + {t('admin.email')} + + +
+ + {/* New password field */} + ( + + + {t('admin.newPassword')} + + + + + + + + )} + /> + + {/* Confirm password field */} + ( + + + {t('admin.confirmNewPassword')} + + + + + + + )} + /> + + + ); +}; + +// Export the form validation state for parent component +function useChangePasswordForm() { + const { t } = useTranslation(); + const id = useId(); + + const schema = useMemo(() => { + return z + .object({ + newPassword: z + .string() + .min(8, { message: t('admin.passwordMinLength') }), + confirmPassword: z + .string() + .min(8, { message: t('admin.confirmPasswordRequired') }), + }) + .refine((data) => data.newPassword === data.confirmPassword, { + message: t('admin.confirmPasswordDoNotMatch'), + path: ['confirmPassword'], + }); + }, [t]); + + const form = useForm({ + defaultValues: { + newPassword: '', + confirmPassword: '', + }, + resolver: zodResolver(schema), + }); + + const FormComponent = useCallback( + (props: Partial) => ( + + ), + [id, form], + ); + + return { + schema, + id, + form, + FormComponent, + }; +} + +export default useChangePasswordForm; diff --git a/web/src/pages/admin/forms/email-form.tsx b/web/src/pages/admin/forms/email-form.tsx new file mode 100644 index 000000000..e5fc2636a --- /dev/null +++ b/web/src/pages/admin/forms/email-form.tsx @@ -0,0 +1,102 @@ +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useCallback, useId, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +interface CreateEmailFormData { + email: string; +} + +interface CreateEmailFormProps { + id: string; + form: ReturnType>; + onSubmit?: (data: CreateEmailFormData) => void; +} + +export const CreateEmailForm = ({ + id, + form, + onSubmit = () => {}, +}: CreateEmailFormProps) => { + const { t } = useTranslation(); + + return ( +
+ + {/* Email field */} + ( + + + {t('admin.email')} + + + + + + + )} + /> + + + ); +}; + +// Export the form validation state for parent component +function useCreateEmailForm(props?: { + defaultValues: Partial; +}) { + const { t } = useTranslation(); + const id = useId(); + + const schema = useMemo(() => { + return z.object({ + email: z.string().email({ message: t('admin.invalidEmail') }), + }); + }, [t]); + + const form = useForm({ + defaultValues: { + email: '', + ...(props?.defaultValues ?? {}), + }, + resolver: zodResolver(schema), + }); + + const FormComponent = useCallback( + (props: Partial) => ( + + ), + [id, form], + ); + + return { + schema, + id, + form, + FormComponent, + }; +} + +export default useCreateEmailForm; diff --git a/web/src/pages/admin/forms/import-excel-form.tsx b/web/src/pages/admin/forms/import-excel-form.tsx new file mode 100644 index 000000000..67c1b9033 --- /dev/null +++ b/web/src/pages/admin/forms/import-excel-form.tsx @@ -0,0 +1,155 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useCallback, useId, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +interface ImportExcelFormData { + file: FileList; + overwriteExisting: boolean; +} + +interface ImportExcelFormProps { + id: string; + form: ReturnType>; + onSubmit?: (data: ImportExcelFormData) => void; +} + +export const ImportExcelForm = ({ + id, + form, + onSubmit = () => {}, +}: ImportExcelFormProps) => { + const { t } = useTranslation(); + + return ( +
+ + {/* File input field */} + ( + + + {t('admin.importSelectExcelFile')} + + + + { + const files = e.target.files; + onChange(files); + }} + {...field} + /> + + + + )} + /> + + {/* Overwrite checkbox */} + ( + + + + + + + {t('admin.importOverwriteExistingEmails')} + + + )} + /> + +

+ }} + /> +

+ + + ); +}; + +// Export the form validation state for parent component +function useImportExcelForm() { + const { t } = useTranslation(); + const id = useId(); + + const schema = useMemo(() => { + return z.object({ + file: z + .any() + .refine((files) => files && files.length > 0, { + message: t('admin.importFileRequired'), + }) + .refine( + (files) => { + if (!files || files.length === 0) return false; + const [file] = files; + return ( + file.type === + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || + // || file.type === 'application/vnd.ms-excel' + file.name.endsWith('.xlsx') + ); + // || file.name.endsWith('.xls'); + }, + { + message: t('admin.invalidExcelFile'), + }, + ), + overwriteExisting: z.boolean().optional(), + }); + }, [t]); + + const form = useForm({ + defaultValues: { + file: undefined, + overwriteExisting: false, + }, + resolver: zodResolver(schema), + }); + + const FormComponent = useCallback( + (props: Partial) => ( + + ), + [id, form], + ); + + return { + schema, + id, + form, + FormComponent, + }; +} + +export default useImportExcelForm; diff --git a/web/src/pages/admin/forms/role-form.tsx b/web/src/pages/admin/forms/role-form.tsx new file mode 100644 index 000000000..dd236bc31 --- /dev/null +++ b/web/src/pages/admin/forms/role-form.tsx @@ -0,0 +1,207 @@ +import { Card, CardContent } from '@/components/ui/card'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@/components/ui/tabs-underlined'; +import { AdminService, listResources } from '@/services/admin-service'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useId, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +interface CreateRoleFormData { + name: string; + description: string; + permissions: Record; +} + +interface CreateRoleFormProps { + id: string; + form: ReturnType>; + onSubmit?: (data: CreateRoleFormData) => void; +} + +const PERMISSION_TYPES = ['enable', 'read', 'write', 'share'] as const; + +export const CreateRoleForm = ({ + id, + form, + onSubmit = () => {}, +}: CreateRoleFormProps) => { + const { t } = useTranslation(); + + const { data: resourceTypes } = useQuery({ + queryKey: ['admin/resourceTypes'], + queryFn: async () => (await listResources()).data.data.resource_types, + }); + + return ( +
+ + {/* Role name field */} + ( + + + {t('admin.roleName')} + + + + + + + )} + /> + + {/* Role description field */} + ( + + + {t('admin.description')} + + + + + + )} + /> + + {/* Permissions section */} +
+ + + + + {resourceTypes?.map((resourceType) => ( + + {t(`admin.resourceType.${resourceType}`)} + + ))} + + + {resourceTypes?.map((resourceType) => ( + + + +
+ {PERMISSION_TYPES.map((permissionType) => ( + ( + + + {t(`admin.permissionType.${permissionType}`)} + + + + + + )} + /> + ))} +
+
+
+
+ ))} +
+
+ + + ); +}; + +// Export the form validation state for parent component +function useCreateRoleForm(props?: { + defaultValues: Partial; +}) { + const { t } = useTranslation(); + const id = useId(); + + const schema = useMemo(() => { + return z.object({ + name: z.string().min(1, { message: 'Role name is required' }), + description: z.string().optional(), + permissions: z.record( + z.string(), + z.object({ + enable: z.boolean(), + read: z.boolean(), + write: z.boolean(), + share: z.boolean(), + }), + ), + }); + }, [t]); + + const form = useForm({ + defaultValues: { + name: '', + description: '', + permissions: {}, + ...(props?.defaultValues ?? {}), + }, + resolver: zodResolver(schema), + }); + + const FormComponent = useCallback( + (props: Partial) => ( + + ), + [form], + ); + + return { + schema, + id, + form, + FormComponent, + }; +} + +export default useCreateRoleForm; diff --git a/web/src/pages/admin/forms/user-form.tsx b/web/src/pages/admin/forms/user-form.tsx new file mode 100644 index 000000000..2ffe16a26 --- /dev/null +++ b/web/src/pages/admin/forms/user-form.tsx @@ -0,0 +1,215 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useId, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { z } from 'zod'; + +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { listRoles } from '@/services/admin-service'; +import EnterpriseFeature from '../components/enterprise-feature'; + +interface CreateUserFormData { + email: string; + password: string; + confirmPassword: string; + role?: string; +} + +interface CreateUserFormProps { + id: string; + form: ReturnType>; + onSubmit?: (data: CreateUserFormData) => void; +} + +export const CreateUserForm = ({ + id, + form, + onSubmit = () => {}, +}: CreateUserFormProps) => { + const { t } = useTranslation(); + + const { data: roleList } = useQuery({ + queryKey: ['admin/listRoles'], + queryFn: async () => (await listRoles()).data.data.roles, + }); + + return ( +
+ + {/* Email field (editable) */} + ( + + + {t('admin.email')} + + + + + + + )} + /> + + {/* Password field */} + ( + + + {t('admin.password')} + + + + + + + )} + /> + + {/* Confirm password field */} + ( + + + {t('admin.confirmPassword')} + + + + + + + )} + /> + + + {/* Role field */} + {() => ( + ( + + + {t('admin.role')} + + + + + + )} + /> + )} + + + + ); +}; + +// Export the form validation state for parent component +function useCreateUserForm(props?: { + defaultValues: Partial; +}) { + const { t } = useTranslation(); + const id = useId(); + + const schema = useMemo(() => { + return z + .object({ + email: z.string().email({ message: t('admin.invalidEmail') }), + password: z.string().min(6, { message: t('admin.passwordMinLength') }), + confirmPassword: z + .string() + .min(1, { message: t('admin.confirmPasswordRequired') }), + role: z.string().optional(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: t('admin.confirmPasswordDoNotMatch'), + path: ['confirmPassword'], + }); + }, [t]); + + const form = useForm({ + defaultValues: { + email: '', + password: '', + confirmPassword: '', + ...(props?.defaultValues ?? {}), + }, + resolver: zodResolver(schema), + }); + + const FormComponent = useCallback( + (props: Partial) => ( + + ), + [id, form], + ); + + return { + schema, + id, + form, + FormComponent, + }; +} + +export default useCreateUserForm; diff --git a/web/src/pages/admin/index.tsx b/web/src/pages/admin/index.tsx new file mode 100644 index 000000000..9009de635 --- /dev/null +++ b/web/src/pages/admin/index.tsx @@ -0,0 +1,265 @@ +import Spotlight from '@/components/spotlight'; +import { ButtonLoading } from '@/components/ui/button'; +import { Card, CardContent, CardFooter } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Authorization } from '@/constants/authorization'; +import { useAuth } from '@/hooks/auth-hooks'; +import { cn } from '@/lib/utils'; +import { Routes } from '@/routes'; +import adminService from '@/services/admin-service'; +import { rsaPsw } from '@/utils'; +import authorizationUtil from '@/utils/authorization-util'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { AxiosResponseHeaders } from 'axios'; +import { LucideEye, LucideEyeOff } from 'lucide-react'; +import { useEffect, useId, useState } from 'react'; +import { SubmitHandler, useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'umi'; +import { z } from 'zod'; +import { BgSvg } from '../login-next/bg'; +import ThemeSwitch from './components/theme-switch'; + +function AdminLogin() { + const navigate = useNavigate(); + const { t } = useTranslation('translation', { keyPrefix: 'login' }); + const { isLogin } = useAuth(); + + const [showPassword, setShowPassword] = useState(false); + + const { isPending: signLoading, mutateAsync: login } = useMutation({ + mutationKey: ['adminLogin'], + mutationFn: async (params: { email: string; password: string }) => { + const request = await adminService.login(params); + const { data: req, headers } = request; + + if (req.code === 0) { + const authorization = (headers as AxiosResponseHeaders)?.get( + Authorization, + ); + const token = req.data.access_token; + + const userInfo = { + avatar: req.data.avatar, + name: req.data.nickname, + email: req.data.email, + }; + + authorizationUtil.setItems({ + Authorization: authorization as string, + Token: token, + userInfo: JSON.stringify(userInfo), + }); + } + + return req; + }, + }); + + const loading = signLoading; + + useEffect(() => { + if (isLogin) { + navigate(Routes.AdminServices); + } + }, [isLogin, navigate]); + + const FormSchema = z.object({ + email: z + .string() + .email() + .min(1, { message: t('emailPlaceholder') }), + password: z.string().min(1, { message: t('passwordPlaceholder') }), + remember: z.boolean().optional(), + }); + + const formId = useId(); + const form = useForm({ + defaultValues: { + email: '', + password: '', + remember: false, + }, + resolver: zodResolver(FormSchema), + }); + + const onCheck: SubmitHandler> = async (params) => { + try { + const rsaPassWord = rsaPsw(params.password) as string; + + const { code } = await login({ + email: `${params.email}`.trim(), + password: rsaPassWord, + }); + + if (code === 0) { + navigate('/admin/services'); + } + } catch (errorInfo) { + console.log('Failed:', errorInfo); + } + }; + + return ( +
+ + + + + + +
+
+ logo + RAGFlow +
+ +

+ {t('loginTitle', { keyPrefix: 'admin' })} +

+
+ +
+
+ + +
+ + ( + + {t('emailLabel')} + + + + + + + + )} + /> + + ( + + {t('passwordLabel')} + + +
+ + +
+
+ + +
+ )} + /> + + ( + + + + + + + {t('rememberMe')} + + + )} + /> + + +
+ + + + {t('login')} + + +
+ +
+ +
+
+
+
+ ); +} + +export default AdminLogin; diff --git a/web/src/pages/admin/layout.tsx b/web/src/pages/admin/layout.tsx new file mode 100644 index 000000000..5ee2cf895 --- /dev/null +++ b/web/src/pages/admin/layout.tsx @@ -0,0 +1,133 @@ +import { Button } from '@/components/ui/button'; +import message from '@/components/ui/message'; +import { cn } from '@/lib/utils'; +import { Routes } from '@/routes'; +import adminService from '@/services/admin-service'; +import authorizationUtil from '@/utils/authorization-util'; +import { useMutation } from '@tanstack/react-query'; +import { + LucideMonitor, + LucideServerCrash, + LucideSquareUserRound, + LucideUserCog, + LucideUserStar, +} from 'lucide-react'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { NavLink, Outlet, useLocation, useNavigate } from 'umi'; +import ThemeSwitch from './components/theme-switch'; +import { IS_ENTERPRISE } from './utils'; + +const AdminLayout = () => { + const { t } = useTranslation(); + const { pathname } = useLocation(); + const navigate = useNavigate(); + + const navItems = useMemo( + () => [ + { + path: Routes.AdminServices, + name: t('admin.serviceStatus'), + icon: , + }, + { + path: Routes.AdminUserManagement, + name: t('admin.userManagement'), + icon: , + }, + ...(IS_ENTERPRISE + ? [ + { + path: Routes.AdminWhitelist, + name: t('admin.registrationWhitelist'), + icon: , + }, + { + path: Routes.AdminRoles, + name: t('admin.roles'), + icon: , + }, + { + path: Routes.AdminMonitoring, + name: t('admin.monitoring'), + icon: , + }, + ] + : []), + ], + [t], + ); + + const { + data, + isPending, + mutateAsync: logout, + } = useMutation({ + mutationKey: ['adminLogout'], + mutationFn: async () => { + await adminService.logout(); + + message.success(t('message.logout')); + authorizationUtil.removeAll(); + navigate(Routes.Admin); + }, + }); + + return ( +
+ + +
+ +
+
+ ); +}; + +export default AdminLayout; diff --git a/web/src/pages/admin/monitoring.tsx b/web/src/pages/admin/monitoring.tsx new file mode 100644 index 000000000..60af64545 --- /dev/null +++ b/web/src/pages/admin/monitoring.tsx @@ -0,0 +1,13 @@ +import { Card, CardContent } from '@/components/ui/card'; + +function AdminMonitoring() { + return ( + + +