mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Fix several admin UI issues (#10869)
### What problem does this PR solve? - Fix login card will overlap title in admin login page. - Disable unnecessary `listRoles()` query in user management page and create user form - Disable admin UI API queries and mutations retry mechanism - Fix page not redirect to login page automatically if API reports unauthorized (401) - Fix change password form not reset when change password modal close - Resolve admin UI content (mostly long texts) may break layout main box issue ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
@ -24,7 +24,9 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { listRoles } from '@/services/admin-service';
|
||||
|
||||
import EnterpriseFeature from '../components/enterprise-feature';
|
||||
import { IS_ENTERPRISE } from '../utils';
|
||||
|
||||
interface CreateUserFormData {
|
||||
email: string;
|
||||
@ -49,6 +51,8 @@ export const CreateUserForm = ({
|
||||
const { data: roleList } = useQuery({
|
||||
queryKey: ['admin/listRoles'],
|
||||
queryFn: async () => (await listRoles()).data.data.roles,
|
||||
enabled: IS_ENTERPRISE,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -1,3 +1,16 @@
|
||||
import { type AxiosResponseHeaders } from 'axios';
|
||||
import { useEffect, useId, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'umi';
|
||||
|
||||
import { LucideEye, LucideEyeOff } from 'lucide-react';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import Spotlight from '@/components/spotlight';
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardFooter } from '@/components/ui/card';
|
||||
@ -11,22 +24,17 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
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 { login } from '@/services/admin-service';
|
||||
|
||||
import { BgSvg } from '../login-next/bg';
|
||||
import ThemeSwitch from './components/theme-switch';
|
||||
|
||||
@ -37,13 +45,19 @@ function AdminLogin() {
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const { isPending: signLoading, mutateAsync: login } = useMutation({
|
||||
const loginMutation = useMutation({
|
||||
mutationKey: ['adminLogin'],
|
||||
mutationFn: async (params: { email: string; password: string }) => {
|
||||
const request = await adminService.login(params);
|
||||
const rsaPassWord = rsaPsw(params.password) as string;
|
||||
return await login({
|
||||
email: params.email,
|
||||
password: rsaPassWord,
|
||||
});
|
||||
},
|
||||
onSuccess: (request) => {
|
||||
const { data: req, headers } = request;
|
||||
|
||||
if (req.code === 0) {
|
||||
if (req?.code === 0) {
|
||||
const authorization = (headers as AxiosResponseHeaders)?.get(
|
||||
Authorization,
|
||||
);
|
||||
@ -60,13 +74,17 @@ function AdminLogin() {
|
||||
Token: token,
|
||||
userInfo: JSON.stringify(userInfo),
|
||||
});
|
||||
}
|
||||
|
||||
return req;
|
||||
navigate('/admin/services');
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('Failed:', error);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const loading = signLoading;
|
||||
const loading = loginMutation.isPending;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLogin) {
|
||||
@ -93,172 +111,159 @@ function AdminLogin() {
|
||||
resolver: zodResolver(FormSchema),
|
||||
});
|
||||
|
||||
const onCheck: SubmitHandler<z.infer<typeof FormSchema>> = 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 (
|
||||
<div className="relative w-screen h-screen">
|
||||
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
|
||||
<Spotlight
|
||||
opcity={0.3}
|
||||
coverage={12}
|
||||
X="10%"
|
||||
Y="-10%"
|
||||
color="rgb(128, 255, 248)"
|
||||
/>
|
||||
<Spotlight
|
||||
opcity={0.3}
|
||||
coverage={12}
|
||||
X="90%"
|
||||
Y="-10%"
|
||||
color="rgb(128, 255, 248)"
|
||||
/>
|
||||
<ScrollArea className="w-screen h-screen">
|
||||
<div className="relative">
|
||||
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
|
||||
<Spotlight
|
||||
opcity={0.3}
|
||||
coverage={12}
|
||||
X="10%"
|
||||
Y="-10%"
|
||||
color="rgb(128, 255, 248)"
|
||||
/>
|
||||
<Spotlight
|
||||
opcity={0.3}
|
||||
coverage={12}
|
||||
X="90%"
|
||||
Y="-10%"
|
||||
color="rgb(128, 255, 248)"
|
||||
/>
|
||||
|
||||
<BgSvg />
|
||||
<BgSvg />
|
||||
|
||||
<div className="absolute top-3 left-0 w-full">
|
||||
<div className="absolute mt-12 ml-12 flex items-center">
|
||||
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
|
||||
<span className="text-xl font-bold">RAGFlow</span>
|
||||
<div className="absolute top-3 left-0 w-full">
|
||||
<div className="absolute mt-12 ml-12 flex items-center">
|
||||
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
|
||||
<span className="text-xl font-bold">RAGFlow</span>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-[6.5rem] text-4xl font-medium text-center mb-12">
|
||||
{t('loginTitle', { keyPrefix: 'admin' })}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<h1 className="mt-[6.5rem] text-4xl font-medium text-center mb-12">
|
||||
{t('loginTitle', { keyPrefix: 'admin' })}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center w-screen h-screen">
|
||||
<div className="w-full max-w-[540px]">
|
||||
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
|
||||
<CardContent className="px-10 pt-14 pb-10">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
className="space-y-8 text-text-primary"
|
||||
onSubmit={form.handleSubmit(onCheck)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
className="h-10 px-2.5"
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
<div className="flex items-center justify-center w-screen min-h-[1050px]">
|
||||
<div className="w-full max-w-[540px]">
|
||||
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
|
||||
<CardContent className="px-10 pt-14 pb-10">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id={formId}
|
||||
className="space-y-8 text-text-primary"
|
||||
onSubmit={form.handleSubmit((data) =>
|
||||
loginMutation.mutate(data),
|
||||
)}
|
||||
/>
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('emailLabel')}</FormLabel>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<FormControl>
|
||||
<Input
|
||||
className="h-10 px-2.5"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete="password"
|
||||
placeholder={t('emailPlaceholder')}
|
||||
autoComplete="email"
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<LucideEyeOff className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<LucideEye className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
render={({ field }) => (
|
||||
<FormItem className="!mt-5">
|
||||
<FormLabel
|
||||
className={cn(
|
||||
'flex items-center hover:text-text-primary',
|
||||
field.value
|
||||
? 'text-text-primary'
|
||||
: 'text-text-disabled',
|
||||
)}
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<span className="ml-2">{t('rememberMe')}</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<CardFooter className="px-10 pt-8 pb-14">
|
||||
<ButtonLoading
|
||||
form={formId}
|
||||
size="lg"
|
||||
className="
|
||||
w-full h-10
|
||||
bg-metallic-gradient border-b-[#00BEB4] border-b-2
|
||||
hover:bg-metallic-gradient hover:border-b-[#02bcdd]
|
||||
"
|
||||
type="submit"
|
||||
loading={loading}
|
||||
>
|
||||
{t('login')}
|
||||
</ButtonLoading>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required>{t('passwordLabel')}</FormLabel>
|
||||
|
||||
<div className="mt-8 flex justify-center">
|
||||
<ThemeSwitch />
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="h-10 px-2.5"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
autoComplete="password"
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<LucideEyeOff className="h-4 w-4 text-gray-500" />
|
||||
) : (
|
||||
<LucideEye className="h-4 w-4 text-gray-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="remember"
|
||||
render={({ field }) => (
|
||||
<FormItem className="!mt-5">
|
||||
<FormLabel
|
||||
className={cn(
|
||||
'flex items-center hover:text-text-primary',
|
||||
field.value
|
||||
? 'text-text-primary'
|
||||
: 'text-text-disabled',
|
||||
)}
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<span className="ml-2">{t('rememberMe')}</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="px-10 pt-8 pb-14">
|
||||
<ButtonLoading
|
||||
form={formId}
|
||||
size="lg"
|
||||
className="
|
||||
w-full h-10
|
||||
bg-metallic-gradient border-b-[#00BEB4] border-b-2
|
||||
hover:bg-metallic-gradient hover:border-b-[#02bcdd]
|
||||
"
|
||||
type="submit"
|
||||
loading={loading}
|
||||
>
|
||||
{t('login')}
|
||||
</ButtonLoading>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8 flex justify-center">
|
||||
<ThemeSwitch />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -14,13 +14,12 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavLink, Outlet, useLocation, useNavigate } from 'umi';
|
||||
import { NavLink, Outlet, 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(
|
||||
@ -58,11 +57,7 @@ const AdminLayout = () => {
|
||||
[t],
|
||||
);
|
||||
|
||||
const {
|
||||
data,
|
||||
isPending,
|
||||
mutateAsync: logout,
|
||||
} = useMutation({
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ['adminLogout'],
|
||||
mutationFn: async () => {
|
||||
await adminService.logout();
|
||||
@ -71,11 +66,12 @@ const AdminLayout = () => {
|
||||
authorizationUtil.removeAll();
|
||||
navigate(Routes.Admin);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="w-screen h-screen flex flex-row px-6 pt-12 pb-6 dark:*:focus-visible:ring-white">
|
||||
<aside className="w-[28rem] mr-6 flex flex-col gap-6">
|
||||
<aside className="w-72 mr-6 flex flex-col gap-6">
|
||||
<div className="flex items-center mb-6">
|
||||
<img className="size-8 mr-5" src="/logo.svg" alt="logo" />
|
||||
<span className="text-xl font-bold">{t('admin.title')}</span>
|
||||
@ -87,17 +83,18 @@ const AdminLayout = () => {
|
||||
<li key={it.path}>
|
||||
<NavLink
|
||||
to={it.path}
|
||||
className={cn(
|
||||
'px-4 py-3 rounded-lg',
|
||||
'text-base w-full flex items-center justify-start text-text-secondary',
|
||||
'hover:bg-bg-card focus:bg-bg-card focus-visible:bg-bg-card',
|
||||
'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
|
||||
'active:text-text-primary',
|
||||
{
|
||||
'bg-bg-card text-text-primary':
|
||||
it.path && pathname.startsWith(it.path),
|
||||
},
|
||||
)}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'px-4 py-3 rounded-lg',
|
||||
'text-base w-full flex items-center justify-start text-text-secondary',
|
||||
'hover:bg-bg-card focus:bg-bg-card focus-visible:bg-bg-card',
|
||||
'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
|
||||
'active:text-text-primary',
|
||||
{
|
||||
'bg-bg-card text-text-primary': isActive,
|
||||
},
|
||||
)
|
||||
}
|
||||
>
|
||||
{it.icon}
|
||||
<span className="ml-3">{it.name}</span>
|
||||
@ -116,14 +113,14 @@ const AdminLayout = () => {
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
className="block w-full dark:border-border-button"
|
||||
onClick={() => logout()}
|
||||
onClick={() => logoutMutation.mutate()}
|
||||
>
|
||||
{t('header.logout')}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section className="w-full h-full">
|
||||
<section className="flex-1 h-full">
|
||||
<Outlet />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@ -307,6 +307,7 @@ function AdminUserDetail() {
|
||||
};
|
||||
},
|
||||
enabled: !!id,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -126,11 +126,14 @@ function AdminUserManagement() {
|
||||
const { data: roleList } = useQuery({
|
||||
queryKey: ['admin/listRoles'],
|
||||
queryFn: async () => (await listRoles()).data.data.roles,
|
||||
enabled: IS_ENTERPRISE,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: usersList, isPending } = useQuery({
|
||||
queryKey: ['admin/listUsers'],
|
||||
queryFn: async () => (await listUsers()).data.data,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Delete user mutation
|
||||
@ -142,17 +145,19 @@ function AdminUserManagement() {
|
||||
setDeleteModalOpen(false);
|
||||
setUserToMakeAction(null);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Change password mutation
|
||||
const changePasswordMutation = useMutation({
|
||||
mutationFn: ({ email, password }: { email: string; password: string }) =>
|
||||
updateUserPassword(email, password),
|
||||
updateUserPassword(email, rsaPsw(password) as string),
|
||||
onSuccess: () => {
|
||||
// message.success(t('admin.passwordChangedSuccessfully'));
|
||||
setPasswordModalOpen(false);
|
||||
setUserToMakeAction(null);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Update user role mutation
|
||||
@ -162,6 +167,7 @@ function AdminUserManagement() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Create user mutation
|
||||
@ -175,7 +181,7 @@ function AdminUserManagement() {
|
||||
password: string;
|
||||
role?: string;
|
||||
}) => {
|
||||
await createUser(email, password);
|
||||
await createUser(email, rsaPsw(password) as string);
|
||||
|
||||
if (IS_ENTERPRISE && role) {
|
||||
await updateUserRoleMutation.mutateAsync({ email, role });
|
||||
@ -187,6 +193,7 @@ function AdminUserManagement() {
|
||||
setCreateUserModalOpen(false);
|
||||
createUserForm.form.reset();
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Update user status mutation
|
||||
@ -196,6 +203,7 @@ function AdminUserManagement() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const columnDefs = useMemo(
|
||||
@ -573,7 +581,8 @@ function AdminUserManagement() {
|
||||
className="px-4 h-10"
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
deleteUserMutation.mutate(userToMakeAction?.email || '')
|
||||
userToMakeAction &&
|
||||
deleteUserMutation.mutate(userToMakeAction?.email)
|
||||
}
|
||||
disabled={deleteUserMutation.isPending}
|
||||
loading={deleteUserMutation.isPending}
|
||||
@ -586,7 +595,14 @@ function AdminUserManagement() {
|
||||
|
||||
{/* Change Password Modal */}
|
||||
<Dialog open={passwordModalOpen} onOpenChange={setPasswordModalOpen}>
|
||||
<DialogContent className="p-0 border-border-button">
|
||||
<DialogContent
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!passwordModalOpen) {
|
||||
changePasswordForm.form.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="p-6 border-b border-border-button">
|
||||
<DialogTitle>{t('admin.changePassword')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -599,7 +615,7 @@ function AdminUserManagement() {
|
||||
if (userToMakeAction) {
|
||||
changePasswordMutation.mutate({
|
||||
email: userToMakeAction.email,
|
||||
password: rsaPsw(newPassword) as string,
|
||||
password: newPassword,
|
||||
});
|
||||
}
|
||||
}}
|
||||
@ -649,12 +665,7 @@ function AdminUserManagement() {
|
||||
<section className="px-12 py-4">
|
||||
<createUserForm.FormComponent
|
||||
id={createUserForm.id}
|
||||
onSubmit={({ email, password }) => {
|
||||
createUserMutation.mutate({
|
||||
email: email,
|
||||
password: rsaPsw(password) as string,
|
||||
});
|
||||
}}
|
||||
onSubmit={createUserMutation.mutate}
|
||||
/>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user