Feat: Admin UI whitelist management and role management (#10910)

### What problem does this PR solve?

Add whitelist management and role management in Admin UI

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Jimmy Ben Klieve
2025-11-03 09:52:23 +08:00
committed by GitHub
parent 685311814f
commit 7ec587fa9e
20 changed files with 1037 additions and 1004 deletions

View File

@@ -1,4 +1,3 @@
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
@@ -14,8 +13,8 @@ import { useForm } from 'react-hook-form';
import { Trans, useTranslation } from 'react-i18next';
import { z } from 'zod';
interface ImportExcelFormData {
file: FileList;
export interface ImportExcelFormData {
file: File;
overwriteExisting: boolean;
}
@@ -43,6 +42,7 @@ export const ImportExcelForm = ({
<FormField
control={form.control}
name="file"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
render={({ field: { onChange, value, ...field } }) => (
<FormItem>
<FormLabel className="text-sm font-medium">
@@ -56,7 +56,7 @@ export const ImportExcelForm = ({
className="mt-2 px-3 h-10 bg-bg-input border-border-button file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-bg-accent file:text-text-primary hover:file:bg-bg-accent/80"
onChange={(e) => {
const files = e.target.files;
onChange(files);
onChange(files?.[0]);
}}
{...field}
/>
@@ -66,27 +66,7 @@ export const ImportExcelForm = ({
)}
/>
{/* Overwrite checkbox */}
<FormField
control={form.control}
name="overwriteExisting"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2 text-sm font-medium">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
{t('admin.importOverwriteExistingEmails')}
</FormLabel>
</FormItem>
)}
/>
<p className="text-xs text-text-secondary">
<p className="text-sm text-text-secondary">
<Trans
i18nKey="admin.importFileTips"
components={{ code: <code /> }}
@@ -105,21 +85,14 @@ function useImportExcelForm() {
const schema = useMemo(() => {
return z.object({
file: z
.any()
.refine((files) => files && files.length > 0, {
message: t('admin.importFileRequired'),
})
.instanceof(File, { message: t('admin.importFileRequired') })
.refine(
(files) => {
if (!files || files.length === 0) return false;
const [file] = files;
(file) => {
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'),

View File

@@ -1,3 +1,11 @@
import { useCallback, useId, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { zodResolver } from '@hookform/resolvers/zod';
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
import { Card, CardContent } from '@/components/ui/card';
import {
Form,
@@ -16,15 +24,11 @@ import {
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 {
import { listResources } from '@/services/admin-service';
import { PERMISSION_TYPES, formMergeDefaultValues } from '../utils';
export interface CreateRoleFormData {
name: string;
description: string;
permissions: Record<string, AdminService.PermissionData>;
@@ -36,8 +40,6 @@ interface CreateRoleFormProps {
onSubmit?: (data: CreateRoleFormData) => void;
}
const PERMISSION_TYPES = ['enable', 'read', 'write', 'share'] as const;
export const CreateRoleForm = ({
id,
form,
@@ -48,6 +50,7 @@ export const CreateRoleForm = ({
const { data: resourceTypes } = useQuery({
queryKey: ['admin/resourceTypes'],
queryFn: async () => (await listResources()).data.data.resource_types,
retry: false,
});
return (
@@ -108,9 +111,9 @@ export const CreateRoleForm = ({
<TabsTrigger
key={resourceType}
value={resourceType}
className="text-text-secondary border-border-button dark:data-[state=active]:bg-bg-input"
className="text-text-secondary !border-border-button data-[state=active]:bg-bg-card data-[state=active]:text-text-primary"
>
{t(`admin.resourceType.${resourceType}`)}
{t(`admin.resourceType.${resourceType.toLowerCase()}`)}
</TabsTrigger>
))}
</TabsList>
@@ -121,7 +124,7 @@ export const CreateRoleForm = ({
value={resourceType}
className="space-y-4"
>
<Card className="border-0 bg-bg-card">
<Card className="border-0 bg-bg-card !shadow-none">
<CardContent className="p-6">
<div className="grid grid-cols-4 gap-4">
{PERMISSION_TYPES.map((permissionType) => (
@@ -129,8 +132,8 @@ export const CreateRoleForm = ({
key={permissionType}
name={`permissions.${resourceType}.${permissionType}`}
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center gap-2">
<FormItem className="space-y-0 inline-flex items-center gap-2">
<FormLabel>
{t(`admin.permissionType.${permissionType}`)}
</FormLabel>
<FormControl>
@@ -158,42 +161,46 @@ export const CreateRoleForm = ({
// Export the form validation state for parent component
function useCreateRoleForm(props?: {
defaultValues: Partial<CreateRoleFormData>;
defaultValues:
| Partial<CreateRoleFormData>
| (() => Promise<CreateRoleFormData>);
}) {
const { t } = useTranslation();
const id = useId();
const schema = useMemo(() => {
return z.object({
name: z.string().min(1, { message: 'Role name is required' }),
name: z.string().min(1, { message: t('admin.roleNameRequired') }),
description: z.string().optional(),
permissions: z.record(
z.string(),
z.object({
enable: z.boolean(),
read: z.boolean(),
write: z.boolean(),
share: z.boolean(),
enable: z.boolean().optional(),
read: z.boolean().optional(),
write: z.boolean().optional(),
share: z.boolean().optional(),
}),
),
});
}, [t]);
const form = useForm<CreateRoleFormData>({
defaultValues: {
name: '',
description: '',
permissions: {},
...(props?.defaultValues ?? {}),
},
defaultValues: formMergeDefaultValues(
{
name: '',
description: '',
permissions: {},
},
props?.defaultValues,
),
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial<CreateRoleFormProps>) => (
<CreateRoleForm id="create-role-form" form={form} {...props} />
<CreateRoleForm id={id} form={form} {...props} />
),
[form],
[id, form],
);
return {