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:
Jimmy Ben Klieve
2025-10-30 20:13:39 +08:00
committed by GitHub
parent 5059d3db18
commit 361c74ab42
6 changed files with 251 additions and 224 deletions

View File

@ -24,7 +24,9 @@ import {
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { listRoles } from '@/services/admin-service'; import { listRoles } from '@/services/admin-service';
import EnterpriseFeature from '../components/enterprise-feature'; import EnterpriseFeature from '../components/enterprise-feature';
import { IS_ENTERPRISE } from '../utils';
interface CreateUserFormData { interface CreateUserFormData {
email: string; email: string;
@ -49,6 +51,8 @@ export const CreateUserForm = ({
const { data: roleList } = useQuery({ const { data: roleList } = useQuery({
queryKey: ['admin/listRoles'], queryKey: ['admin/listRoles'],
queryFn: async () => (await listRoles()).data.data.roles, queryFn: async () => (await listRoles()).data.data.roles,
enabled: IS_ENTERPRISE,
retry: false,
}); });
return ( return (

View File

@ -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 Spotlight from '@/components/spotlight';
import { ButtonLoading } from '@/components/ui/button'; import { ButtonLoading } from '@/components/ui/button';
import { Card, CardContent, CardFooter } from '@/components/ui/card'; import { Card, CardContent, CardFooter } from '@/components/ui/card';
@ -11,22 +24,17 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Authorization } from '@/constants/authorization'; import { Authorization } from '@/constants/authorization';
import { useAuth } from '@/hooks/auth-hooks'; import { useAuth } from '@/hooks/auth-hooks';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import adminService from '@/services/admin-service';
import { rsaPsw } from '@/utils'; import { rsaPsw } from '@/utils';
import authorizationUtil from '@/utils/authorization-util'; import authorizationUtil from '@/utils/authorization-util';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query'; import { login } from '@/services/admin-service';
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 { BgSvg } from '../login-next/bg';
import ThemeSwitch from './components/theme-switch'; import ThemeSwitch from './components/theme-switch';
@ -37,13 +45,19 @@ function AdminLogin() {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const { isPending: signLoading, mutateAsync: login } = useMutation({ const loginMutation = useMutation({
mutationKey: ['adminLogin'], mutationKey: ['adminLogin'],
mutationFn: async (params: { email: string; password: string }) => { 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; const { data: req, headers } = request;
if (req.code === 0) { if (req?.code === 0) {
const authorization = (headers as AxiosResponseHeaders)?.get( const authorization = (headers as AxiosResponseHeaders)?.get(
Authorization, Authorization,
); );
@ -60,13 +74,17 @@ function AdminLogin() {
Token: token, Token: token,
userInfo: JSON.stringify(userInfo), 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(() => { useEffect(() => {
if (isLogin) { if (isLogin) {
@ -93,25 +111,9 @@ function AdminLogin() {
resolver: zodResolver(FormSchema), 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 ( return (
<div className="relative w-screen h-screen"> <ScrollArea className="w-screen h-screen">
<div className="relative">
<Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" /> <Spotlight opcity={0.4} coverage={60} color="rgb(128, 255, 248)" />
<Spotlight <Spotlight
opcity={0.3} opcity={0.3}
@ -141,7 +143,7 @@ function AdminLogin() {
</h1> </h1>
</div> </div>
<div className="flex items-center justify-center w-screen h-screen"> <div className="flex items-center justify-center w-screen min-h-[1050px]">
<div className="w-full max-w-[540px]"> <div className="w-full max-w-[540px]">
<Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button"> <Card className="w-full bg-bg-component backdrop-blur-sm rounded-2xl border border-border-button">
<CardContent className="px-10 pt-14 pb-10"> <CardContent className="px-10 pt-14 pb-10">
@ -149,7 +151,9 @@ function AdminLogin() {
<form <form
id={formId} id={formId}
className="space-y-8 text-text-primary" className="space-y-8 text-text-primary"
onSubmit={form.handleSubmit(onCheck)} onSubmit={form.handleSubmit((data) =>
loginMutation.mutate(data),
)}
> >
<FormField <FormField
control={form.control} control={form.control}
@ -259,6 +263,7 @@ function AdminLogin() {
</div> </div>
</div> </div>
</div> </div>
</ScrollArea>
); );
} }

View File

@ -14,13 +14,12 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; 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 ThemeSwitch from './components/theme-switch';
import { IS_ENTERPRISE } from './utils'; import { IS_ENTERPRISE } from './utils';
const AdminLayout = () => { const AdminLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pathname } = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const navItems = useMemo( const navItems = useMemo(
@ -58,11 +57,7 @@ const AdminLayout = () => {
[t], [t],
); );
const { const logoutMutation = useMutation({
data,
isPending,
mutateAsync: logout,
} = useMutation({
mutationKey: ['adminLogout'], mutationKey: ['adminLogout'],
mutationFn: async () => { mutationFn: async () => {
await adminService.logout(); await adminService.logout();
@ -71,11 +66,12 @@ const AdminLayout = () => {
authorizationUtil.removeAll(); authorizationUtil.removeAll();
navigate(Routes.Admin); navigate(Routes.Admin);
}, },
retry: false,
}); });
return ( return (
<main className="w-screen h-screen flex flex-row px-6 pt-12 pb-6 dark:*:focus-visible:ring-white"> <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"> <div className="flex items-center mb-6">
<img className="size-8 mr-5" src="/logo.svg" alt="logo" /> <img className="size-8 mr-5" src="/logo.svg" alt="logo" />
<span className="text-xl font-bold">{t('admin.title')}</span> <span className="text-xl font-bold">{t('admin.title')}</span>
@ -87,17 +83,18 @@ const AdminLayout = () => {
<li key={it.path}> <li key={it.path}>
<NavLink <NavLink
to={it.path} to={it.path}
className={cn( className={({ isActive }) =>
cn(
'px-4 py-3 rounded-lg', 'px-4 py-3 rounded-lg',
'text-base w-full flex items-center justify-start text-text-secondary', '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: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', 'hover:text-text-primary focus:text-text-primary focus-visible:text-text-primary',
'active:text-text-primary', 'active:text-text-primary',
{ {
'bg-bg-card text-text-primary': 'bg-bg-card text-text-primary': isActive,
it.path && pathname.startsWith(it.path),
}, },
)} )
}
> >
{it.icon} {it.icon}
<span className="ml-3">{it.name}</span> <span className="ml-3">{it.name}</span>
@ -116,14 +113,14 @@ const AdminLayout = () => {
size="lg" size="lg"
variant="transparent" variant="transparent"
className="block w-full dark:border-border-button" className="block w-full dark:border-border-button"
onClick={() => logout()} onClick={() => logoutMutation.mutate()}
> >
{t('header.logout')} {t('header.logout')}
</Button> </Button>
</div> </div>
</aside> </aside>
<section className="w-full h-full"> <section className="flex-1 h-full">
<Outlet /> <Outlet />
</section> </section>
</main> </main>

View File

@ -307,6 +307,7 @@ function AdminUserDetail() {
}; };
}, },
enabled: !!id, enabled: !!id,
retry: false,
}); });
return ( return (

View File

@ -126,11 +126,14 @@ function AdminUserManagement() {
const { data: roleList } = useQuery({ const { data: roleList } = useQuery({
queryKey: ['admin/listRoles'], queryKey: ['admin/listRoles'],
queryFn: async () => (await listRoles()).data.data.roles, queryFn: async () => (await listRoles()).data.data.roles,
enabled: IS_ENTERPRISE,
retry: false,
}); });
const { data: usersList, isPending } = useQuery({ const { data: usersList, isPending } = useQuery({
queryKey: ['admin/listUsers'], queryKey: ['admin/listUsers'],
queryFn: async () => (await listUsers()).data.data, queryFn: async () => (await listUsers()).data.data,
retry: false,
}); });
// Delete user mutation // Delete user mutation
@ -142,17 +145,19 @@ function AdminUserManagement() {
setDeleteModalOpen(false); setDeleteModalOpen(false);
setUserToMakeAction(null); setUserToMakeAction(null);
}, },
retry: false,
}); });
// Change password mutation // Change password mutation
const changePasswordMutation = useMutation({ const changePasswordMutation = useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) => mutationFn: ({ email, password }: { email: string; password: string }) =>
updateUserPassword(email, password), updateUserPassword(email, rsaPsw(password) as string),
onSuccess: () => { onSuccess: () => {
// message.success(t('admin.passwordChangedSuccessfully')); // message.success(t('admin.passwordChangedSuccessfully'));
setPasswordModalOpen(false); setPasswordModalOpen(false);
setUserToMakeAction(null); setUserToMakeAction(null);
}, },
retry: false,
}); });
// Update user role mutation // Update user role mutation
@ -162,6 +167,7 @@ function AdminUserManagement() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] }); queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
}, },
retry: false,
}); });
// Create user mutation // Create user mutation
@ -175,7 +181,7 @@ function AdminUserManagement() {
password: string; password: string;
role?: string; role?: string;
}) => { }) => {
await createUser(email, password); await createUser(email, rsaPsw(password) as string);
if (IS_ENTERPRISE && role) { if (IS_ENTERPRISE && role) {
await updateUserRoleMutation.mutateAsync({ email, role }); await updateUserRoleMutation.mutateAsync({ email, role });
@ -187,6 +193,7 @@ function AdminUserManagement() {
setCreateUserModalOpen(false); setCreateUserModalOpen(false);
createUserForm.form.reset(); createUserForm.form.reset();
}, },
retry: false,
}); });
// Update user status mutation // Update user status mutation
@ -196,6 +203,7 @@ function AdminUserManagement() {
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] }); queryClient.invalidateQueries({ queryKey: ['admin/listUsers'] });
}, },
retry: false,
}); });
const columnDefs = useMemo( const columnDefs = useMemo(
@ -573,7 +581,8 @@ function AdminUserManagement() {
className="px-4 h-10" className="px-4 h-10"
variant="destructive" variant="destructive"
onClick={() => onClick={() =>
deleteUserMutation.mutate(userToMakeAction?.email || '') userToMakeAction &&
deleteUserMutation.mutate(userToMakeAction?.email)
} }
disabled={deleteUserMutation.isPending} disabled={deleteUserMutation.isPending}
loading={deleteUserMutation.isPending} loading={deleteUserMutation.isPending}
@ -586,7 +595,14 @@ function AdminUserManagement() {
{/* Change Password Modal */} {/* Change Password Modal */}
<Dialog open={passwordModalOpen} onOpenChange={setPasswordModalOpen}> <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"> <DialogHeader className="p-6 border-b border-border-button">
<DialogTitle>{t('admin.changePassword')}</DialogTitle> <DialogTitle>{t('admin.changePassword')}</DialogTitle>
</DialogHeader> </DialogHeader>
@ -599,7 +615,7 @@ function AdminUserManagement() {
if (userToMakeAction) { if (userToMakeAction) {
changePasswordMutation.mutate({ changePasswordMutation.mutate({
email: userToMakeAction.email, email: userToMakeAction.email,
password: rsaPsw(newPassword) as string, password: newPassword,
}); });
} }
}} }}
@ -649,12 +665,7 @@ function AdminUserManagement() {
<section className="px-12 py-4"> <section className="px-12 py-4">
<createUserForm.FormComponent <createUserForm.FormComponent
id={createUserForm.id} id={createUserForm.id}
onSubmit={({ email, password }) => { onSubmit={createUserMutation.mutate}
createUserMutation.mutate({
email: email,
password: rsaPsw(password) as string,
});
}}
/> />
</section> </section>

View File

@ -1,6 +1,6 @@
import { message, notification } from 'antd'; import { message, notification } from 'antd';
import axios from 'axios'; import axios from 'axios';
import { Navigate } from 'umi'; import { history } from 'umi';
import { Authorization } from '@/constants/authorization'; import { Authorization } from '@/constants/authorization';
import i18n from '@/locales/config'; import i18n from '@/locales/config';
@ -48,7 +48,7 @@ request.interceptors.response.use(
}); });
authorizationUtil.removeAll(); authorizationUtil.removeAll();
Navigate({ to: Routes.Admin }); history.push(Routes.Admin);
} else if (data?.code && data.code !== 0) { } else if (data?.code && data.code !== 0) {
notification.error({ notification.error({
message: `${i18n.t('message.hint')}: ${data?.code}`, message: `${i18n.t('message.hint')}: ${data?.code}`,
@ -70,15 +70,16 @@ request.interceptors.response.use(
}); });
} else if (data?.code === 100) { } else if (data?.code === 100) {
message.error(data?.message); message.error(data?.message);
} else if (data?.code === 401) { } else if (response.status === 401 || data?.code === 401) {
notification.error({ notification.error({
message: data?.message, message: data?.message || response.statusText,
description: data?.message, description:
data?.message || RetcodeMessage[response?.status as ResultCode],
duration: 3, duration: 3,
}); });
authorizationUtil.removeAll(); authorizationUtil.removeAll();
Navigate({ to: Routes.Admin }); history.push(Routes.Admin);
} else if (data?.code && data.code !== 0) { } else if (data?.code && data.code !== 0) {
notification.error({ notification.error({
message: `${i18n.t('message.hint')}: ${data?.code}`, message: `${i18n.t('message.hint')}: ${data?.code}`,
@ -93,17 +94,9 @@ request.interceptors.response.use(
}); });
} else if (response.status === 413 || response?.status === 504) { } else if (response.status === 413 || response?.status === 504) {
message.error(RetcodeMessage[response?.status as ResultCode]); message.error(RetcodeMessage[response?.status as ResultCode]);
} else if (response.status === 401) {
notification.error({
message: response.data.message,
description: response.data.message,
duration: 3,
});
authorizationUtil.removeAll();
window.location.href = location.origin + '/admin';
} }
return error; throw error;
}, },
); );
@ -112,7 +105,7 @@ const {
adminLogout, adminLogout,
adminListUsers, adminListUsers,
adminCreateUser, adminCreateUser,
adminGetUserDetails: adminShowUserDetails, adminGetUserDetails,
adminUpdateUserStatus, adminUpdateUserStatus,
adminUpdateUserPassword, adminUpdateUserPassword,
adminDeleteUser, adminDeleteUser,
@ -260,11 +253,11 @@ export namespace AdminService {
update_date: string; update_date: string;
}; };
export type AssignRolePermissionInput = { export type AssignRolePermissionsInput = Record<
permissions: Record<string, Partial<PermissionData>>; string,
}; Partial<PermissionData>
>;
export type RevokeRolePermissionInput = AssignRolePermissionInput; export type RevokeRolePermissionInput = AssignRolePermissionsInput;
export type UserDetailWithPermission = { export type UserDetailWithPermission = {
user: { user: {
@ -293,7 +286,7 @@ export const createUser = (email: string, password: string) =>
}); });
export const getUserDetails = (email: string) => export const getUserDetails = (email: string) =>
request.get<ResponseData<[AdminService.UserDetail]>>( request.get<ResponseData<[AdminService.UserDetail]>>(
adminShowUserDetails(email), adminGetUserDetails(email),
); );
export const listUserDatasets = (email: string) => export const listUserDatasets = (email: string) =>
request.get<ResponseData<AdminService.ListUserDatasetItem[]>>( request.get<ResponseData<AdminService.ListUserDatasetItem[]>>(
@ -317,7 +310,10 @@ export const showServiceDetails = (serviceId: number) =>
adminShowServiceDetails(String(serviceId)), adminShowServiceDetails(String(serviceId)),
); );
export const createRole = (params: { roleName: string; description: string }) => export const createRole = (params: {
roleName: string;
description?: string;
}) =>
request.post<ResponseData<AdminService.RoleDetail>>(adminCreateRole, params); request.post<ResponseData<AdminService.RoleDetail>>(adminCreateRole, params);
export const updateRoleDescription = (role: string, description: string) => export const updateRoleDescription = (role: string, description: string) =>
request.put<ResponseData<AdminService.RoleDetail>>( request.put<ResponseData<AdminService.RoleDetail>>(
@ -343,15 +339,17 @@ export const getRolePermissions = (role: string) =>
); );
export const assignRolePermissions = ( export const assignRolePermissions = (
role: string, role: string,
params: AdminService.AssignRolePermissionInput, permissions: Partial<AdminService.AssignRolePermissionsInput>,
) => ) =>
request.post<ResponseData<never>>(adminAssignRolePermissions(role), params); request.post<ResponseData<never>>(adminAssignRolePermissions(role), {
new_permissions: permissions,
});
export const revokeRolePermissions = ( export const revokeRolePermissions = (
role: string, role: string,
params: AdminService.RevokeRolePermissionInput, permissions: Partial<AdminService.RevokeRolePermissionInput>,
) => ) =>
request.delete<ResponseData<never>>(adminRevokeRolePermissions(role), { request.delete<ResponseData<never>>(adminRevokeRolePermissions(role), {
data: params, data: { revoke_permissions: permissions },
}); });
export const updateUserRole = (username: string, role: string) => export const updateUserRole = (username: string, role: string) =>
@ -365,12 +363,23 @@ export const getUserPermissions = (username: string) =>
export const listResources = () => export const listResources = () =>
request.get<ResponseData<AdminService.ResourceType>>(adminListResources); request.get<ResponseData<AdminService.ResourceType>>(adminListResources);
export const whitelistImportFromExcel = (file: File) => {
const fd = new FormData();
fd.append('file', file);
return request.post<ResponseData<never>>(
'/api/v1/admin/whitelist/import',
fd,
);
};
export default { export default {
login, login,
logout, logout,
listUsers, listUsers,
createUser, createUser,
showUserDetails: getUserDetails, getUserDetails,
updateUserStatus, updateUserStatus,
updateUserPassword, updateUserPassword,
deleteUser, deleteUser,