mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32:30 +08:00
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:
650
web/package-lock.json
generated
650
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -114,6 +114,7 @@
|
||||
"umi-request": "^1.4.0",
|
||||
"unist-util-visit-parents": "^6.0.1",
|
||||
"uuid": "^9.0.1",
|
||||
"xlsx": "^0.18.5",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
|
||||
@ -1,58 +1,66 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { useIsDarkTheme } from '../theme-provider';
|
||||
|
||||
type EmptyProps = {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
const EmptyIcon = () => (
|
||||
<svg
|
||||
width="184"
|
||||
height="152"
|
||||
viewBox="0 0 184 152"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>{t('common.noData')}</title>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g transform="translate(24 31.67)">
|
||||
<ellipse
|
||||
fillOpacity=".8"
|
||||
fill="#F5F5F7"
|
||||
cx="67.797"
|
||||
cy="106.89"
|
||||
rx="67.797"
|
||||
ry="12.668"
|
||||
></ellipse>
|
||||
const EmptyIcon = () => {
|
||||
const isDarkTheme = useIsDarkTheme();
|
||||
|
||||
return (
|
||||
<svg
|
||||
width="184"
|
||||
height="152"
|
||||
viewBox="0 0 184 152"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<title>{t('common.noData')}</title>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<g transform="translate(24 31.67)">
|
||||
<ellipse
|
||||
fillOpacity=".8"
|
||||
fill={isDarkTheme ? '#28282A' : '#F5F5F7'}
|
||||
cx="67.797"
|
||||
cy="106.89"
|
||||
rx="67.797"
|
||||
ry="12.668"
|
||||
></ellipse>
|
||||
<path
|
||||
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||
fill={isDarkTheme ? '#736960' : '#AEB8C2'}
|
||||
></path>
|
||||
<path
|
||||
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||
fill="url(#linearGradient-1)"
|
||||
transform="translate(13.56)"
|
||||
></path>
|
||||
<path
|
||||
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||
fill={isDarkTheme ? '#28282A' : '#F5F5F7'}
|
||||
></path>
|
||||
<path
|
||||
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||
fill={isDarkTheme ? '#45413A' : '#DCE0E6'}
|
||||
></path>
|
||||
</g>
|
||||
<path
|
||||
d="M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z"
|
||||
fill="#AEB8C2"
|
||||
></path>
|
||||
<path
|
||||
d="M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z"
|
||||
fill="url(#linearGradient-1)"
|
||||
transform="translate(13.56)"
|
||||
></path>
|
||||
<path
|
||||
d="M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"
|
||||
fill="#F5F5F7"
|
||||
></path>
|
||||
<path
|
||||
d="M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z"
|
||||
fill="#DCE0E6"
|
||||
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||
fill={isDarkTheme ? '#45413A' : '#DCE0E6'}
|
||||
></path>
|
||||
<g
|
||||
transform="translate(149.65 15.383)"
|
||||
fill={isDarkTheme ? '#222' : '#FFF'}
|
||||
>
|
||||
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"></ellipse>
|
||||
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"></path>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
d="M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z"
|
||||
fill="#DCE0E6"
|
||||
></path>
|
||||
<g transform="translate(149.65 15.383)" fill="#FFF">
|
||||
<ellipse cx="20.654" cy="3.167" rx="2.849" ry="2.815"></ellipse>
|
||||
<path d="M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const Empty = (props: EmptyProps) => {
|
||||
const { className, children } = props;
|
||||
|
||||
@ -1960,7 +1960,13 @@ Important structured information may include: names, dates, locations, events, k
|
||||
newRole: 'New Role',
|
||||
addNewRole: 'Add new role',
|
||||
roleName: 'Role name',
|
||||
roleNameRequired: 'Role name is required',
|
||||
resources: 'Resources',
|
||||
|
||||
editRoleDescription: 'Edit role description',
|
||||
deleteRole: 'Delete role',
|
||||
deleteRoleConfirmation:
|
||||
'Are you sure you want to delete this role? This action cannot be undone.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,7 +3,7 @@ import { IS_ENTERPRISE } from '../utils';
|
||||
export default function EnterpriseFeature({
|
||||
children,
|
||||
}: {
|
||||
children: () => React.ReactNode;
|
||||
children: React.ReactNode | (() => React.ReactNode);
|
||||
}) {
|
||||
return IS_ENTERPRISE
|
||||
? typeof children === 'function'
|
||||
|
||||
@ -15,7 +15,7 @@ const ThemeSwitch = forwardRef<
|
||||
return (
|
||||
<Root
|
||||
ref={ref}
|
||||
className={cn('relative rounded-full')}
|
||||
className={cn('relative rounded-full', className)}
|
||||
{...props}
|
||||
checked={isDark}
|
||||
onCheckedChange={(value) =>
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -2,7 +2,7 @@ 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 { logout } from '@/services/admin-service';
|
||||
import authorizationUtil from '@/utils/authorization-util';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import {
|
||||
@ -60,7 +60,7 @@ const AdminLayout = () => {
|
||||
const logoutMutation = useMutation({
|
||||
mutationKey: ['adminLogout'],
|
||||
mutationFn: async () => {
|
||||
await adminService.logout();
|
||||
await logout();
|
||||
|
||||
message.success(t('message.logout'));
|
||||
authorizationUtil.removeAll();
|
||||
|
||||
@ -2,7 +2,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
function AdminMonitoring() {
|
||||
return (
|
||||
<Card className="h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
|
||||
<Card className="!shadow-none h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
|
||||
<CardContent className="size-full p-0">
|
||||
<iframe />
|
||||
</CardContent>
|
||||
|
||||
@ -1,10 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { mapKeys } from 'lodash';
|
||||
|
||||
import { useId, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { LoadingButton } from '@/components/ui/loading-button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
@ -18,71 +29,134 @@ import {
|
||||
import { LucideEdit3, LucideTrash2, LucideUserPlus } from 'lucide-react';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
AdminService,
|
||||
assignRolePermissions,
|
||||
createRole,
|
||||
deleteRole,
|
||||
listResources,
|
||||
listRolesWithPermission,
|
||||
revokeRolePermissions,
|
||||
updateRoleDescription,
|
||||
} from '@/services/admin-service';
|
||||
|
||||
import { listRolesWithPermission } from '@/services/admin-service';
|
||||
|
||||
import useCreateRoleForm from './forms/role-form';
|
||||
|
||||
// #region FAKE DATA
|
||||
function _pickRandom<T extends unknown>(arr: T[]): T | void {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
const PSEUDO_TABLE_ITEMS = Array.from({ length: 20 }, () => ({
|
||||
id: Math.random().toString(36).slice(2, 8),
|
||||
name: 'Ahaha',
|
||||
description: 'Ahaha description',
|
||||
permissions: {
|
||||
dataset: {
|
||||
enable: _pickRandom([true, false]),
|
||||
read: _pickRandom([true, false]),
|
||||
write: _pickRandom([true, false]),
|
||||
share: _pickRandom([true, false]),
|
||||
},
|
||||
agent: {
|
||||
enable: _pickRandom([true, false]),
|
||||
read: _pickRandom([true, false]),
|
||||
write: _pickRandom([true, false]),
|
||||
share: _pickRandom([true, false]),
|
||||
},
|
||||
},
|
||||
}));
|
||||
// #endregion
|
||||
import Empty from '@/components/empty/empty';
|
||||
import useCreateRoleForm, { CreateRoleFormData } from './forms/role-form';
|
||||
import { PERMISSION_TYPES } from './utils';
|
||||
|
||||
function AdminRoles() {
|
||||
const { t } = useTranslation();
|
||||
const [isAddRoleModalOpen, setIsAddRoleModalOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
const createRoleForm = useCreateRoleForm();
|
||||
|
||||
const [isAddRoleModalOpen, setAddRoleModalOpen] = useState(false);
|
||||
|
||||
const editRoleDescriptionFormId = useId();
|
||||
const [isEditRoleDescriptionModalOpen, setEditRoleDescriptionModalOpen] =
|
||||
useState(false);
|
||||
const [roleDescription, setRoleDescription] = useState('');
|
||||
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [roleToMakeAction, setRoleToMakeAction] =
|
||||
useState<AdminService.ListRoleItemWithPermission | null>(null);
|
||||
|
||||
const { data: roleList } = useQuery({
|
||||
queryKey: ['admin/listRolesWithPermission'],
|
||||
queryFn: async () => (await listRolesWithPermission()).data.data.roles,
|
||||
queryFn: async () => (await listRolesWithPermission())?.data?.data?.roles,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const createRoleForm = useCreateRoleForm();
|
||||
const { data: resourceTypes } = useQuery({
|
||||
queryKey: ['admin/resourceTypes'],
|
||||
queryFn: async () => (await listResources()).data.data.resource_types,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const handleAddRole = (data: any) => {
|
||||
console.log('New role data:', data);
|
||||
// TODO: Implement actual role creation logic
|
||||
createRoleForm.form.reset();
|
||||
setIsAddRoleModalOpen(false);
|
||||
};
|
||||
const updateRoleDescriptionMutation = useMutation({
|
||||
mutationFn: (data: { name: string; description: string }) =>
|
||||
updateRoleDescription(data.name, data.description),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin/listRolesWithPermission'],
|
||||
});
|
||||
setEditRoleDescriptionModalOpen(false);
|
||||
setRoleToMakeAction(null);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const updateRolePermissionsMutation = useMutation({
|
||||
mutationFn: (data: {
|
||||
name: string;
|
||||
resourceName: string;
|
||||
permissionType: (typeof PERMISSION_TYPES)[number];
|
||||
value: boolean;
|
||||
}) => {
|
||||
const permissionDiffData = {
|
||||
[data.resourceName.toLowerCase()]: {
|
||||
[data.permissionType]: data.value,
|
||||
},
|
||||
};
|
||||
|
||||
return data.value
|
||||
? assignRolePermissions(data.name, permissionDiffData)
|
||||
: revokeRolePermissions(data.name, permissionDiffData);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin/listRolesWithPermission'],
|
||||
});
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const createRoleMutation = useMutation({
|
||||
mutationFn: async (data: CreateRoleFormData) => {
|
||||
const { data: { data: createdRoleDetail } = {} } = await createRole({
|
||||
roleName: data.name,
|
||||
description: data.description,
|
||||
});
|
||||
|
||||
if (!createdRoleDetail) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
await assignRolePermissions(
|
||||
data.name,
|
||||
mapKeys(data.permissions, (_, key) => key.toLowerCase()),
|
||||
);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin/listRolesWithPermission'],
|
||||
});
|
||||
createRoleForm.form.reset();
|
||||
setAddRoleModalOpen(false);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const deleteRoleMutation = useMutation({
|
||||
mutationFn: (roleName: string) => deleteRole(roleName),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin/listRolesWithPermission'],
|
||||
});
|
||||
setDeleteModalOpen(false);
|
||||
setRoleToMakeAction(null);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full border border-border-button bg-transparent rounded-xl">
|
||||
<Card className="!shadow-none w-full h-full border border-border-button bg-transparent rounded-xl">
|
||||
<ScrollArea className="size-full">
|
||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||
<CardTitle>{t('admin.roles')}</CardTitle>
|
||||
|
||||
<Button
|
||||
className="h-10 px-4"
|
||||
onClick={() => setIsAddRoleModalOpen(true)}
|
||||
onClick={() => setAddRoleModalOpen(true)}
|
||||
>
|
||||
<LucideUserPlus />
|
||||
{t('admin.newRole')}
|
||||
@ -90,20 +164,19 @@ function AdminRoles() {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
{roleList?.map((role) => {
|
||||
const resources = Object.entries(role.permissions);
|
||||
|
||||
return (
|
||||
{roleList?.length ? (
|
||||
roleList.map((role) => (
|
||||
<Card
|
||||
key={role.id}
|
||||
className="group border border-border-default bg-transparent hover:bg-bg-card transition-color duration-150"
|
||||
className="group border border-border-default bg-transparent dark:hover:bg-bg-card transition-color duration-150"
|
||||
>
|
||||
<CardHeader className="space-y-0 flex flex-row items-center border-b border-border-button">
|
||||
<div className="space-y-1.5">
|
||||
<CardHeader className="space-y-0 flex flex-row gap-4 items-center border-b border-border-button">
|
||||
<div className="space-y-1.5 w-0 flex-1">
|
||||
<CardTitle className="font-normal text-xl">
|
||||
{role.role_name}
|
||||
</CardTitle>
|
||||
<div className="text-sm text-text-secondary">
|
||||
|
||||
<div className="text-sm text-text-secondary break-words">
|
||||
{role.description || (
|
||||
<i className="text-muted-foreground">
|
||||
{t('admin.noDescription')}
|
||||
@ -113,6 +186,11 @@ function AdminRoles() {
|
||||
<Button
|
||||
variant="transparent"
|
||||
className="ml-2 p-0 border-0 size-[1em] align-middle opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
|
||||
onClick={() => {
|
||||
setEditRoleDescriptionModalOpen(true);
|
||||
setRoleToMakeAction(role);
|
||||
setRoleDescription(role.description || '');
|
||||
}}
|
||||
>
|
||||
<LucideEdit3 className="!size-[1em]" />
|
||||
</Button>
|
||||
@ -123,6 +201,11 @@ function AdminRoles() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="ml-auto opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
|
||||
disabled={deleteRoleMutation.isPending}
|
||||
onClick={() => {
|
||||
setDeleteModalOpen(true);
|
||||
setRoleToMakeAction(role);
|
||||
}}
|
||||
>
|
||||
<LucideTrash2 />
|
||||
</Button>
|
||||
@ -131,82 +214,95 @@ function AdminRoles() {
|
||||
<CardContent className="p-6">
|
||||
<Tabs
|
||||
className="h-full flex flex-col"
|
||||
defaultValue={resources[0]?.[0]}
|
||||
defaultValue={resourceTypes?.[0]}
|
||||
>
|
||||
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
|
||||
{resources.map(([name]) => (
|
||||
{resourceTypes?.map((resourceName) => (
|
||||
<TabsTrigger
|
||||
key={name}
|
||||
value={name}
|
||||
className="border-border-button dark:data-[state=active]:bg-bg-input"
|
||||
key={resourceName}
|
||||
value={resourceName}
|
||||
className="text-text-secondary !border-border-button data-[state=active]:bg-bg-card data-[state=active]:text-text-primary"
|
||||
>
|
||||
{t(`admin.resourceType.${name}`)}
|
||||
{t(
|
||||
`admin.resourceType.${resourceName.toLowerCase()}`,
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{resources.map(([name, permission]) => (
|
||||
<TabsContent key={name} value={name}>
|
||||
<div className="flex gap-8">
|
||||
<Label className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={!!permission.enable}
|
||||
onCheckedChange={console.log}
|
||||
/>
|
||||
{t('admin.enable')}
|
||||
</Label>
|
||||
{resourceTypes?.map((resourceName) => {
|
||||
const permission =
|
||||
role.permissions[resourceName.toLowerCase()];
|
||||
|
||||
<Label className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={!!permission.read}
|
||||
onCheckedChange={() => {}}
|
||||
/>
|
||||
{t('admin.read')}
|
||||
</Label>
|
||||
return (
|
||||
<TabsContent key={resourceName} value={resourceName}>
|
||||
<Card className="border-0 bg-bg-card !shadow-none">
|
||||
<CardContent className="p-6 flex gap-8">
|
||||
{PERMISSION_TYPES.map((permissionType) => (
|
||||
<Label
|
||||
key={permissionType}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{t(`admin.${permissionType}`)}
|
||||
|
||||
<Label className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={!!permission.write}
|
||||
onCheckedChange={() => {}}
|
||||
/>
|
||||
{t('admin.write')}
|
||||
</Label>
|
||||
|
||||
<Label className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={!!permission.share}
|
||||
onCheckedChange={() => {}}
|
||||
/>
|
||||
{t('admin.share')}
|
||||
</Label>
|
||||
</div>
|
||||
</TabsContent>
|
||||
))}
|
||||
<Switch
|
||||
disabled={
|
||||
updateRolePermissionsMutation.isPending
|
||||
}
|
||||
checked={!!permission?.[permissionType]}
|
||||
onCheckedChange={(value) =>
|
||||
updateRolePermissionsMutation.mutate({
|
||||
name: role.role_name,
|
||||
resourceName:
|
||||
resourceName.toLowerCase(),
|
||||
permissionType,
|
||||
value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Label>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
))
|
||||
) : (
|
||||
<Empty className="py-24" />
|
||||
)}
|
||||
</CardContent>
|
||||
</ScrollArea>
|
||||
</Card>
|
||||
|
||||
{/* Add Role Modal */}
|
||||
<Dialog open={isAddRoleModalOpen} onOpenChange={setIsAddRoleModalOpen}>
|
||||
<DialogContent className="max-w-2xl p-0 border-border-button">
|
||||
{/* Add role modal */}
|
||||
<Dialog open={isAddRoleModalOpen} onOpenChange={setAddRoleModalOpen}>
|
||||
<DialogContent
|
||||
className="max-w-2xl p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!isAddRoleModalOpen) {
|
||||
createRoleForm.form.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="p-6 border-b border-border-button">
|
||||
<DialogTitle>{t('admin.addNewRole')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<section className="px-12 py-4">
|
||||
<createRoleForm.FormComponent onSubmit={handleAddRole} />
|
||||
<createRoleForm.FormComponent
|
||||
onSubmit={createRoleMutation.mutate}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
|
||||
<Button
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => setIsAddRoleModalOpen(false)}
|
||||
onClick={() => setAddRoleModalOpen(false)}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
@ -222,6 +318,118 @@ function AdminRoles() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Modify role description modal */}
|
||||
<Dialog
|
||||
open={isEditRoleDescriptionModalOpen}
|
||||
onOpenChange={setEditRoleDescriptionModalOpen}
|
||||
>
|
||||
<DialogContent
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!isEditRoleDescriptionModalOpen) {
|
||||
setRoleToMakeAction(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="p-6 border-b border-border-button">
|
||||
<DialogTitle>{t('admin.editRoleDescription')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<section className="px-12 py-4">
|
||||
<form
|
||||
id={editRoleDescriptionFormId}
|
||||
onSubmit={(evt) => {
|
||||
evt.preventDefault();
|
||||
updateRoleDescriptionMutation.mutate({
|
||||
name: roleToMakeAction!?.role_name,
|
||||
description: roleDescription.trim(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
<div className="text-sm font-medium">
|
||||
{t('admin.description')}
|
||||
</div>
|
||||
|
||||
<Input
|
||||
className="mt-2 px-3 h-10 bg-bg-input border-border-button"
|
||||
value={roleDescription}
|
||||
onInput={(evt) => setRoleDescription(evt.currentTarget.value)}
|
||||
/>
|
||||
</Label>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
|
||||
<Button
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => setEditRoleDescriptionModalOpen(false)}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
|
||||
<LoadingButton
|
||||
type="submit"
|
||||
form={editRoleDescriptionFormId}
|
||||
className="px-4 h-10"
|
||||
>
|
||||
{t('admin.confirm')}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete role modal */}
|
||||
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
|
||||
<DialogContent
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!deleteModalOpen) {
|
||||
setRoleToMakeAction(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="p-6 border-b border-border-button">
|
||||
<DialogTitle>{t('admin.deleteRole')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<section className="px-12 py-4">
|
||||
<DialogDescription className="text-text-primary">
|
||||
{t('admin.deleteRoleConfirmation')}
|
||||
</DialogDescription>
|
||||
|
||||
<div className="rounded-lg mt-6 p-4 border border-border-button">
|
||||
{roleToMakeAction?.role_name}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
|
||||
<Button
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => setDeleteModalOpen(false)}
|
||||
disabled={deleteRoleMutation.isPending}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
|
||||
<LoadingButton
|
||||
className="px-4 h-10"
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
roleToMakeAction &&
|
||||
deleteRoleMutation.mutate(roleToMakeAction!?.role_name)
|
||||
}
|
||||
disabled={deleteRoleMutation.isPending}
|
||||
loading={deleteRoleMutation.isPending}
|
||||
>
|
||||
{t('admin.delete')}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ function ServiceDetail({ content }: ServiceDetailProps) {
|
||||
|
||||
if (isPlainObject(content)) {
|
||||
return (
|
||||
<dl className="grid grid-cols-[auto,1fr] border border-card rounded-xl overflow-hidden bg-bg-card">
|
||||
<dl className="text-sm text-text-primary grid grid-cols-[auto,1fr] border border-card rounded-xl overflow-hidden bg-bg-card">
|
||||
{Object.entries<any>(content).map(([key, value]) => (
|
||||
<div key={key} className="contents">
|
||||
<dt className="px-3 py-2 bg-bg-card">
|
||||
|
||||
@ -59,19 +59,13 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
|
||||
import {
|
||||
listServices,
|
||||
showServiceDetails,
|
||||
type AdminService,
|
||||
} from '@/services/admin-service';
|
||||
import { listServices, showServiceDetails } from '@/services/admin-service';
|
||||
|
||||
import {
|
||||
EMPTY_DATA,
|
||||
createColumnFilterFn,
|
||||
createFuzzySearchFn,
|
||||
getColumnFilter,
|
||||
getSortIcon,
|
||||
setColumnFilter,
|
||||
} from './utils';
|
||||
|
||||
import ServiceDetail from './service-detail';
|
||||
@ -97,22 +91,18 @@ function AdminServiceStatus() {
|
||||
const [itemToMakeAction, setItemToMakeAction] =
|
||||
useState<AdminService.ListServicesItem | null>(null);
|
||||
|
||||
const { data: servicesList, isPending } = useQuery({
|
||||
const { data: servicesList } = useQuery({
|
||||
queryKey: ['admin/listServices'],
|
||||
queryFn: async () => (await listServices()).data.data,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const {
|
||||
data: serviceDetails,
|
||||
isPending: isServiceDetailsPending,
|
||||
error: serviceDetailsError,
|
||||
} = useQuery({
|
||||
const { data: serviceDetails, error: serviceDetailsError } = useQuery({
|
||||
queryKey: ['admin/serviceDetails', itemToMakeAction?.id],
|
||||
queryFn: async () =>
|
||||
(await showServiceDetails(itemToMakeAction?.id!)).data.data,
|
||||
(await showServiceDetails(itemToMakeAction!?.id)).data.data,
|
||||
enabled: !!(itemToMakeAction && detailModalOpen),
|
||||
retry: false,
|
||||
refetchInterval: Infinity,
|
||||
});
|
||||
|
||||
const columnDefs = useMemo(
|
||||
@ -202,7 +192,7 @@ function AdminServiceStatus() {
|
||||
),
|
||||
}),
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
@ -225,7 +215,7 @@ function AdminServiceStatus() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full border border-border-button bg-transparent rounded-xl">
|
||||
<Card className="!shadow-none h-full border border-border-button bg-transparent rounded-xl">
|
||||
<ScrollArea className="size-full">
|
||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||
<CardTitle>{t('admin.serviceStatus')}</CardTitle>
|
||||
@ -254,11 +244,12 @@ function AdminServiceStatus() {
|
||||
|
||||
<RadioGroup
|
||||
value={
|
||||
(getColumnFilter(table, 'service_type')
|
||||
?.value as string) ?? ''
|
||||
table
|
||||
.getColumn('service_type')!
|
||||
?.getFilterValue() as string
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
setColumnFilter(table, 'service_type', value)
|
||||
onValueChange={
|
||||
table.getColumn('service_type')!?.setFilterValue
|
||||
}
|
||||
>
|
||||
<Label className="space-x-2">
|
||||
|
||||
@ -41,7 +41,6 @@ import {
|
||||
getUserDetails,
|
||||
listUserAgents,
|
||||
listUserDatasets,
|
||||
type AdminService,
|
||||
} from '@/services/admin-service';
|
||||
|
||||
import EnterpriseFeature from './components/enterprise-feature';
|
||||
@ -323,7 +322,7 @@ function AdminUserDetail() {
|
||||
</Button>
|
||||
</nav>
|
||||
|
||||
<Card className="h-0 basis-0 grow flex flex-col bg-transparent border dark:border-border-button overflow-hidden">
|
||||
<Card className="!shadow-none h-0 basis-0 grow flex flex-col bg-transparent border dark:border-border-button overflow-hidden">
|
||||
<CardHeader className="pb-10 border-b dark:border-border-button space-y-8">
|
||||
<section className="flex items-center gap-4 text-base">
|
||||
<Avatar className="justify-center items-center bg-bg-group uppercase">
|
||||
|
||||
@ -84,7 +84,6 @@ import {
|
||||
updateUserPassword,
|
||||
updateUserRole,
|
||||
updateUserStatus,
|
||||
type AdminService,
|
||||
} from '@/services/admin-service';
|
||||
|
||||
import {
|
||||
@ -130,7 +129,7 @@ function AdminUserManagement() {
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { data: usersList, isPending } = useQuery({
|
||||
const { data: usersList } = useQuery({
|
||||
queryKey: ['admin/listUsers'],
|
||||
queryFn: async () => (await listUsers()).data.data,
|
||||
retry: false,
|
||||
@ -341,13 +340,7 @@ function AdminUserManagement() {
|
||||
),
|
||||
}),
|
||||
],
|
||||
[
|
||||
roleList,
|
||||
t,
|
||||
navigate,
|
||||
updateUserStatusMutation.isPending,
|
||||
updateUserRoleMutation.isPending,
|
||||
],
|
||||
[t, updateUserRoleMutation, roleList, updateUserStatusMutation, navigate],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
@ -364,7 +357,7 @@ function AdminUserManagement() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
|
||||
<Card className="!shadow-none h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
|
||||
<ScrollArea className="size-full">
|
||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||
<CardTitle>{t('admin.userManagement')}</CardTitle>
|
||||
|
||||
@ -4,10 +4,35 @@ import {
|
||||
Row,
|
||||
RowData,
|
||||
SortDirection,
|
||||
Table,
|
||||
TransformFilterValueFn,
|
||||
} from '@tanstack/react-table';
|
||||
import { LucideSortAsc, LucideSortDesc } from 'lucide-react';
|
||||
import { DefaultValues } from 'react-hook-form';
|
||||
|
||||
type AsyncDefaultValues<TValues> = (payload?: unknown) => Promise<TValues>;
|
||||
|
||||
export function formMergeDefaultValues<T>(
|
||||
...parts: (DefaultValues<T> | AsyncDefaultValues<T> | undefined)[]
|
||||
): (payload?: unknown) => Promise<Required<T>> {
|
||||
return async (payload?: unknown) => {
|
||||
if (parts.length === 0) {
|
||||
return {} as DefaultValues<T>;
|
||||
}
|
||||
|
||||
if (parts.length === 1) {
|
||||
return typeof parts[0] === 'function'
|
||||
? await parts[0](payload)
|
||||
: parts[0];
|
||||
}
|
||||
|
||||
return Object.assign(
|
||||
// @ts-ignore
|
||||
...(await Promise.all(
|
||||
parts.map((p) => (typeof p === 'function' ? p(payload) : p)),
|
||||
)),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function parseBooleanish(value: any): boolean {
|
||||
return typeof value === 'string'
|
||||
@ -42,37 +67,6 @@ export function createColumnFilterFn<TData extends RowData>(
|
||||
return Object.assign(filterFn, options) as FilterFn<TData>;
|
||||
}
|
||||
|
||||
export function getColumnFilter<TData extends RowData>(
|
||||
table: Table<TData>,
|
||||
columnId: string,
|
||||
) {
|
||||
return table
|
||||
.getState()
|
||||
.columnFilters.find((filter) => filter.id === columnId);
|
||||
}
|
||||
|
||||
export function setColumnFilter<TData extends RowData>(
|
||||
table: Table<TData>,
|
||||
columnId: string,
|
||||
value?: unknown,
|
||||
) {
|
||||
const otherColumnFilters = table
|
||||
.getState()
|
||||
.columnFilters.filter((filter) => filter.id !== columnId);
|
||||
|
||||
if (value == null) {
|
||||
table.setColumnFilters(otherColumnFilters);
|
||||
} else {
|
||||
table.setColumnFilters([
|
||||
...otherColumnFilters,
|
||||
{
|
||||
id: columnId,
|
||||
value,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSortIcon(sorting: false | SortDirection) {
|
||||
return {
|
||||
asc: <LucideSortAsc />,
|
||||
@ -80,6 +74,7 @@ export function getSortIcon(sorting: false | SortDirection) {
|
||||
}[sorting as string];
|
||||
}
|
||||
|
||||
export const PERMISSION_TYPES = ['enable', 'read', 'write', 'share'] as const;
|
||||
export const EMPTY_DATA = Object.freeze<any[]>([]) as any[];
|
||||
export const IS_ENTERPRISE =
|
||||
process.env.UMI_APP_RAGFLOW_ENTERPRISE === 'RAGFLOW_ENTERPRISE';
|
||||
|
||||
@ -1,3 +1,27 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as XLSX from 'xlsx';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
import {
|
||||
LucideDownload,
|
||||
LucidePlus,
|
||||
LucideSearch,
|
||||
LucideTrash2,
|
||||
LucideUpload,
|
||||
LucideUserPen,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { TableEmpty } from '@/components/table-skeleton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@ -27,45 +51,27 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import {
|
||||
LucideDownload,
|
||||
LucidePlus,
|
||||
LucideSearch,
|
||||
LucideTrash2,
|
||||
LucideUpload,
|
||||
LucideUserPen,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
createWhitelistEntry,
|
||||
deleteWhitelistEntry,
|
||||
importWhitelistFromExcel,
|
||||
listWhitelist,
|
||||
updateWhitelistEntry,
|
||||
type AdminService,
|
||||
} from '@/services/admin-service';
|
||||
|
||||
import { EMPTY_DATA, createFuzzySearchFn, getSortIcon } from './utils';
|
||||
|
||||
import useCreateEmailForm from './forms/email-form';
|
||||
import useImportExcelForm from './forms/import-excel-form';
|
||||
import { EMPTY_DATA, createFuzzySearchFn } from './utils';
|
||||
import useImportExcelForm, {
|
||||
ImportExcelFormData,
|
||||
} from './forms/import-excel-form';
|
||||
|
||||
// #region FAKE DATA
|
||||
function _pickRandom<T extends unknown>(arr: T[]): T | void {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
const PSEUDO_TABLE_ITEMS = Array.from({ length: 20 }, () => ({
|
||||
id: Math.random().toString(36).slice(2, 8),
|
||||
email: `${Math.random().toString(36).slice(2, 6)}@example.com`,
|
||||
created_by: _pickRandom(['Alice', 'Bob', 'Carol', 'Dave']) || 'System',
|
||||
created_at: Date.now() - Math.floor(Math.random() * 1000 * 60 * 60 * 24 * 30),
|
||||
}));
|
||||
// #endregion
|
||||
|
||||
const columnHelper = createColumnHelper<(typeof PSEUDO_TABLE_ITEMS)[0]>();
|
||||
const globalFilterFn = createFuzzySearchFn<(typeof PSEUDO_TABLE_ITEMS)[0]>([
|
||||
const columnHelper = createColumnHelper<AdminService.ListWhitelistItem>();
|
||||
const globalFilterFn = createFuzzySearchFn<AdminService.ListWhitelistItem>([
|
||||
'email',
|
||||
]);
|
||||
|
||||
@ -74,104 +80,114 @@ function AdminWhitelist() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createEmailForm = useCreateEmailForm();
|
||||
const editEmailForm = useCreateEmailForm();
|
||||
const importExcelForm = useImportExcelForm();
|
||||
|
||||
const [emailToMakeAction, setEmailToMakeAction] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [itemToMakeAction, setItemToMakeAction] =
|
||||
useState<AdminService.ListWhitelistItem | null>(null);
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
|
||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||
|
||||
const { data: whitelist } = useQuery({
|
||||
queryKey: ['admin/listWhitelist'],
|
||||
queryFn: async () => (await listWhitelist())?.data?.data?.white_list,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
// Reset form when editing a different email
|
||||
useEffect(() => {
|
||||
if (emailToMakeAction && editModalOpen) {
|
||||
createEmailForm.form.setValue('email', emailToMakeAction);
|
||||
if (itemToMakeAction && editModalOpen) {
|
||||
editEmailForm.form.setValue('email', itemToMakeAction.email);
|
||||
}
|
||||
}, [emailToMakeAction, editModalOpen, createEmailForm.form]);
|
||||
}, [itemToMakeAction, editModalOpen, editEmailForm.form]);
|
||||
|
||||
const { isPending: isCreating, mutateAsync: createEmail } = useMutation({
|
||||
mutationFn: async (data: { email: string }) => {
|
||||
/* create email API call */
|
||||
},
|
||||
const createWhitelistEntryMutation = useMutation({
|
||||
mutationFn: (data: { email: string }) => createWhitelistEntry(data.email),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||
setCreateModalOpen(false);
|
||||
setEmailToMakeAction(null);
|
||||
createEmailForm.form.reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error creating email:', error);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const { isPending: isEditing, mutateAsync: updateEmail } = useMutation({
|
||||
mutationFn: async (data: { email: string }) => {
|
||||
/* update email API call */
|
||||
},
|
||||
const updateWhitelistEntryMutation = useMutation({
|
||||
mutationFn: (data: { id: number; email: string }) =>
|
||||
updateWhitelistEntry(data.id, data.email),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||
setEditModalOpen(false);
|
||||
setEmailToMakeAction(null);
|
||||
createEmailForm.form.reset();
|
||||
setItemToMakeAction(null);
|
||||
editEmailForm.form.reset();
|
||||
},
|
||||
});
|
||||
|
||||
const { isPending: isDeleting, mutateAsync: deleteEmail } = useMutation({
|
||||
mutationFn: async (data: { email: string }) => {
|
||||
/* delete email API call */
|
||||
},
|
||||
const deleteWhitelistEntryMutation = useMutation({
|
||||
mutationFn: (data: { email: string }) => deleteWhitelistEntry(data.email),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||
setDeleteModalOpen(false);
|
||||
setEmailToMakeAction(null);
|
||||
setItemToMakeAction(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error deleting email:', error);
|
||||
},
|
||||
});
|
||||
|
||||
const { isPending: isImporting, mutateAsync: importExcel } = useMutation({
|
||||
mutationFn: async (data: {
|
||||
file: FileList;
|
||||
overwriteExisting: boolean;
|
||||
}) => {
|
||||
/* import Excel API call */
|
||||
console.log(
|
||||
'Importing Excel file:',
|
||||
data.file[0]?.name,
|
||||
'Overwrite:',
|
||||
data.overwriteExisting,
|
||||
);
|
||||
},
|
||||
const importExcelMutation = useMutation({
|
||||
mutationFn: (data: ImportExcelFormData) =>
|
||||
importWhitelistFromExcel(data.file),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||
setImportModalOpen(false);
|
||||
importExcelForm.form.reset();
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('Error importing Excel:', error);
|
||||
},
|
||||
retry: false,
|
||||
});
|
||||
|
||||
const handleExportExcel = () => {
|
||||
const columnData = (whitelist ?? EMPTY_DATA).map((item) => ({
|
||||
email: item.email,
|
||||
}));
|
||||
|
||||
const now = new Date();
|
||||
const YYYY = String(now.getFullYear()).padStart(4, '0');
|
||||
const MM = String(now.getMonth()).padStart(2, '0');
|
||||
const dd = String(now.getDate()).padStart(2, '0');
|
||||
const HH = String(now.getHours()).padStart(2, '0');
|
||||
const mm = String(now.getMinutes()).padStart(2, '0');
|
||||
const ss = String(now.getSeconds()).padStart(2, '0');
|
||||
|
||||
const worksheet = XLSX.utils.json_to_sheet(columnData);
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
XLSX.writeFile(workbook, `whitelist_${YYYY}${MM}${dd}${HH}${mm}${ss}.xlsx`);
|
||||
};
|
||||
|
||||
const columnDefs = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor('email', {
|
||||
header: 'Email',
|
||||
header: t('admin.email'),
|
||||
enableSorting: false,
|
||||
}),
|
||||
columnHelper.accessor('created_by', {
|
||||
header: 'Created by',
|
||||
columnHelper.accessor('create_date', {
|
||||
header: t('admin.createDate'),
|
||||
}),
|
||||
columnHelper.accessor('created_at', {
|
||||
header: 'Created date',
|
||||
cell: ({ row }) =>
|
||||
new Date(row.getValue('created_at') as number).toLocaleString(),
|
||||
columnHelper.accessor('update_date', {
|
||||
header: t('admin.updateDate'),
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
header: t('admin.actions'),
|
||||
cell: ({ row }) => (
|
||||
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
|
||||
<Button
|
||||
@ -179,7 +195,7 @@ function AdminWhitelist() {
|
||||
size="icon"
|
||||
className="border-0 text-text-secondary"
|
||||
onClick={() => {
|
||||
setEmailToMakeAction(row.original.email);
|
||||
setItemToMakeAction(row.original);
|
||||
setEditModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@ -190,7 +206,7 @@ function AdminWhitelist() {
|
||||
size="icon"
|
||||
className="border-0 text-text-secondary"
|
||||
onClick={() => {
|
||||
setEmailToMakeAction(row.original.email);
|
||||
setItemToMakeAction(row.original);
|
||||
setDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@ -200,11 +216,11 @@ function AdminWhitelist() {
|
||||
),
|
||||
}),
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: PSEUDO_TABLE_ITEMS ?? EMPTY_DATA,
|
||||
data: whitelist ?? EMPTY_DATA,
|
||||
columns: columnDefs,
|
||||
|
||||
globalFilterFn,
|
||||
@ -217,7 +233,7 @@ function AdminWhitelist() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
|
||||
<Card className="!shadow-none h-full border border-border-button bg-transparent rounded-xl overflow-x-hidden overflow-y-auto">
|
||||
<ScrollArea className="size-full">
|
||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||
<CardTitle>{t('admin.whitelistManagement')}</CardTitle>
|
||||
@ -236,6 +252,7 @@ function AdminWhitelist() {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-10 px-4 dark:bg-bg-input dark:border-border-button text-text-secondary"
|
||||
onClick={handleExportExcel}
|
||||
>
|
||||
<LucideUpload />
|
||||
{t('admin.exportAsExcel')}
|
||||
@ -264,8 +281,8 @@ function AdminWhitelist() {
|
||||
<Table>
|
||||
<colgroup>
|
||||
<col />
|
||||
<col className="w-[20%]" />
|
||||
<col className="w-[30%]" />
|
||||
<col className="w-[25%]" />
|
||||
<col className="w-[25%]" />
|
||||
<col className="w-[12rem]" />
|
||||
</colgroup>
|
||||
|
||||
@ -274,12 +291,23 @@ function AdminWhitelist() {
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
{header.isPlaceholder ? null : header.column.getCanSort() ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
{getSortIcon(header.column.getIsSorted())}
|
||||
</Button>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
@ -328,7 +356,7 @@ function AdminWhitelist() {
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!deleteModalOpen) {
|
||||
setEmailToMakeAction(null);
|
||||
setItemToMakeAction(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -341,7 +369,7 @@ function AdminWhitelist() {
|
||||
{t('admin.deleteWhitelistEmailConfirmation')}
|
||||
|
||||
<div className="rounded-lg mt-6 p-4 border border-border-button">
|
||||
{emailToMakeAction}
|
||||
{itemToMakeAction?.email}
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</section>
|
||||
@ -351,7 +379,7 @@ function AdminWhitelist() {
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => setDeleteModalOpen(false)}
|
||||
disabled={isDeleting}
|
||||
disabled={deleteWhitelistEntryMutation.isPending}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
@ -360,10 +388,14 @@ function AdminWhitelist() {
|
||||
className="px-4 h-10"
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
deleteEmail({ email: emailToMakeAction! });
|
||||
if (itemToMakeAction) {
|
||||
deleteWhitelistEntryMutation.mutate({
|
||||
email: itemToMakeAction?.email,
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
loading={isDeleting}
|
||||
disabled={deleteWhitelistEntryMutation.isPending}
|
||||
loading={deleteWhitelistEntryMutation.isPending}
|
||||
>
|
||||
{t('admin.delete')}
|
||||
</LoadingButton>
|
||||
@ -372,14 +404,15 @@ function AdminWhitelist() {
|
||||
</Dialog>
|
||||
|
||||
{/* Create Email Modal */}
|
||||
<Dialog
|
||||
open={createModalOpen}
|
||||
onOpenChange={() => {
|
||||
setCreateModalOpen(false);
|
||||
createEmailForm.form.reset();
|
||||
}}
|
||||
>
|
||||
<DialogContent className="p-0 border-border-button">
|
||||
<Dialog open={createModalOpen} onOpenChange={setCreateModalOpen}>
|
||||
<DialogContent
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!createModalOpen) {
|
||||
createEmailForm.form.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="p-6 border-b border-border-button">
|
||||
<DialogTitle>{t('admin.createEmail')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -387,7 +420,7 @@ function AdminWhitelist() {
|
||||
<section className="px-12 py-4 text-text-secondary">
|
||||
<createEmailForm.FormComponent
|
||||
id={createEmailForm.id}
|
||||
onSubmit={createEmail}
|
||||
onSubmit={createWhitelistEntryMutation.mutate}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -395,11 +428,8 @@ function AdminWhitelist() {
|
||||
<Button
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setCreateModalOpen(false);
|
||||
createEmailForm.form.reset();
|
||||
}}
|
||||
disabled={isCreating}
|
||||
onClick={() => setCreateModalOpen(false)}
|
||||
disabled={createWhitelistEntryMutation.isPending}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
@ -409,8 +439,8 @@ function AdminWhitelist() {
|
||||
type="submit"
|
||||
className="px-4 h-10"
|
||||
variant="default"
|
||||
disabled={isCreating}
|
||||
loading={isCreating}
|
||||
disabled={createWhitelistEntryMutation.isPending}
|
||||
loading={createWhitelistEntryMutation.isPending}
|
||||
>
|
||||
{t('admin.confirm')}
|
||||
</LoadingButton>
|
||||
@ -419,20 +449,13 @@ function AdminWhitelist() {
|
||||
</Dialog>
|
||||
|
||||
{/* Edit Email Modal */}
|
||||
<Dialog
|
||||
open={editModalOpen}
|
||||
onOpenChange={() => {
|
||||
setEditModalOpen(false);
|
||||
setEmailToMakeAction(null);
|
||||
createEmailForm.form.reset();
|
||||
}}
|
||||
>
|
||||
<Dialog open={editModalOpen} onOpenChange={setEditModalOpen}>
|
||||
<DialogContent
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!editModalOpen) {
|
||||
setEmailToMakeAction(null);
|
||||
createEmailForm.form.reset();
|
||||
setItemToMakeAction(null);
|
||||
editEmailForm.form.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -441,9 +464,16 @@ function AdminWhitelist() {
|
||||
</DialogHeader>
|
||||
|
||||
<section className="px-12 py-4 text-text-secondary">
|
||||
<createEmailForm.FormComponent
|
||||
id={createEmailForm.id}
|
||||
onSubmit={updateEmail}
|
||||
<editEmailForm.FormComponent
|
||||
id={editEmailForm.id}
|
||||
onSubmit={(value) => {
|
||||
if (itemToMakeAction) {
|
||||
updateWhitelistEntryMutation.mutate({
|
||||
id: itemToMakeAction.id,
|
||||
email: value.email,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -451,23 +481,19 @@ function AdminWhitelist() {
|
||||
<Button
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditModalOpen(false);
|
||||
setEmailToMakeAction(null);
|
||||
createEmailForm.form.reset();
|
||||
}}
|
||||
disabled={isEditing}
|
||||
onClick={() => setEditModalOpen(false)}
|
||||
disabled={updateWhitelistEntryMutation.isPending}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
|
||||
<LoadingButton
|
||||
form={createEmailForm.id}
|
||||
form={editEmailForm.id}
|
||||
type="submit"
|
||||
className="px-4 h-10"
|
||||
variant="default"
|
||||
disabled={isEditing}
|
||||
loading={isEditing}
|
||||
disabled={updateWhitelistEntryMutation.isPending}
|
||||
loading={updateWhitelistEntryMutation.isPending}
|
||||
>
|
||||
{t('admin.confirm')}
|
||||
</LoadingButton>
|
||||
@ -477,7 +503,14 @@ function AdminWhitelist() {
|
||||
|
||||
{/* Import Excel Modal */}
|
||||
<Dialog open={importModalOpen} onOpenChange={setImportModalOpen}>
|
||||
<DialogContent className="p-0 border-border-button">
|
||||
<DialogContent
|
||||
className="p-0 border-border-button"
|
||||
onAnimationEnd={() => {
|
||||
if (!importModalOpen) {
|
||||
importExcelForm.form.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogHeader className="p-6 border-b border-border-button">
|
||||
<DialogTitle>{t('admin.importWhitelist')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
@ -485,7 +518,7 @@ function AdminWhitelist() {
|
||||
<section className="px-12 py-4 text-text-secondary">
|
||||
<importExcelForm.FormComponent
|
||||
id={importExcelForm.id}
|
||||
onSubmit={importExcel}
|
||||
onSubmit={importExcelMutation.mutate}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@ -493,11 +526,8 @@ function AdminWhitelist() {
|
||||
<Button
|
||||
className="px-4 h-10 dark:border-border-button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setImportModalOpen(false);
|
||||
importExcelForm.form.reset();
|
||||
}}
|
||||
disabled={isImporting}
|
||||
onClick={() => setImportModalOpen(false)}
|
||||
disabled={importExcelMutation.isPending}
|
||||
>
|
||||
{t('admin.cancel')}
|
||||
</Button>
|
||||
@ -507,8 +537,8 @@ function AdminWhitelist() {
|
||||
type="submit"
|
||||
className="px-4 h-10"
|
||||
variant="default"
|
||||
disabled={isImporting}
|
||||
loading={isImporting}
|
||||
disabled={importExcelMutation.isPending}
|
||||
loading={importExcelMutation.isPending}
|
||||
>
|
||||
{t('admin.import')}
|
||||
</LoadingButton>
|
||||
|
||||
@ -128,151 +128,20 @@ const {
|
||||
adminUpdateUserRole,
|
||||
|
||||
adminListResources,
|
||||
|
||||
adminListWhitelist,
|
||||
adminCreateWhitelistEntry,
|
||||
adminUpdateWhitelistEntry,
|
||||
adminDeleteWhitelistEntry,
|
||||
adminImportWhitelist,
|
||||
} = api;
|
||||
|
||||
type ResponseData<D = {}> = {
|
||||
type ResponseData<D = NonNullable<unknown>> = {
|
||||
code: number;
|
||||
message: string;
|
||||
data: D;
|
||||
};
|
||||
|
||||
export namespace AdminService {
|
||||
export type LoginData = {
|
||||
access_token: string;
|
||||
avatar: unknown;
|
||||
color_schema: 'Bright' | 'Dark';
|
||||
create_date: string;
|
||||
create_time: number;
|
||||
email: string;
|
||||
id: string;
|
||||
is_active: '0' | '1';
|
||||
is_anonymous: '0' | '1';
|
||||
is_authenticated: '0' | '1';
|
||||
is_superuser: boolean;
|
||||
language: string;
|
||||
last_login_time: string;
|
||||
login_channel: unknown;
|
||||
nickname: string;
|
||||
password: string;
|
||||
status: '0' | '1';
|
||||
timezone: string;
|
||||
update_date: [string];
|
||||
update_time: [number];
|
||||
};
|
||||
|
||||
export type ListUsersItem = {
|
||||
create_date: string;
|
||||
email: string;
|
||||
is_active: '0' | '1';
|
||||
is_superuser: boolean;
|
||||
role: string;
|
||||
nickname: string;
|
||||
};
|
||||
|
||||
export type UserDetail = {
|
||||
create_date: string;
|
||||
email: string;
|
||||
is_active: '0' | '1';
|
||||
is_anonymous: '0' | '1';
|
||||
is_superuser: boolean;
|
||||
language: string;
|
||||
last_login_time: string;
|
||||
login_channel: unknown;
|
||||
status: '0' | '1';
|
||||
update_date: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type ListUserDatasetItem = {
|
||||
chunk_num: number;
|
||||
create_date: string;
|
||||
doc_num: number;
|
||||
language: string;
|
||||
name: string;
|
||||
permission: string;
|
||||
status: '0' | '1';
|
||||
token_num: number;
|
||||
update_date: string;
|
||||
};
|
||||
|
||||
export type ListUserAgentItem = {
|
||||
canvas_category: 'agent';
|
||||
permission: 'string';
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type ListServicesItem = {
|
||||
extra: Record<string, unknown>;
|
||||
host: string;
|
||||
id: number;
|
||||
name: string;
|
||||
port: number;
|
||||
service_type: string;
|
||||
status: 'alive' | 'timeout' | 'fail';
|
||||
};
|
||||
|
||||
export type ServiceDetail = {
|
||||
service_name: string;
|
||||
status: 'alive' | 'timeout';
|
||||
message: string | Record<string, any> | Record<string, any>[];
|
||||
};
|
||||
|
||||
export type PermissionData = {
|
||||
enable: boolean;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
share: boolean;
|
||||
};
|
||||
|
||||
export type ListRoleItem = {
|
||||
id: string;
|
||||
role_name: string;
|
||||
description: string;
|
||||
create_date: string;
|
||||
update_date: string;
|
||||
};
|
||||
|
||||
export type ListRoleItemWithPermission = ListRoleItem & {
|
||||
permissions: Record<string, PermissionData>;
|
||||
};
|
||||
|
||||
export type RoleDetailWithPermission = {
|
||||
role: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
permissions: Record<string, PermissionData>;
|
||||
};
|
||||
|
||||
export type RoleDetail = {
|
||||
id: string;
|
||||
name: string;
|
||||
descrtiption: string;
|
||||
create_date: string;
|
||||
update_date: string;
|
||||
};
|
||||
|
||||
export type AssignRolePermissionsInput = Record<
|
||||
string,
|
||||
Partial<PermissionData>
|
||||
>;
|
||||
export type RevokeRolePermissionInput = AssignRolePermissionsInput;
|
||||
|
||||
export type UserDetailWithPermission = {
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
};
|
||||
role_permissions: Record<string, PermissionData>;
|
||||
};
|
||||
|
||||
export type ResourceType = {
|
||||
resource_types: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export const login = (params: { email: string; password: string }) =>
|
||||
request.post<ResponseData<AdminService.LoginData>>(adminLogin, params);
|
||||
export const logout = () => request.get<ResponseData<boolean>>(adminLogout);
|
||||
@ -363,15 +232,29 @@ export const getUserPermissions = (username: string) =>
|
||||
export const listResources = () =>
|
||||
request.get<ResponseData<AdminService.ResourceType>>(adminListResources);
|
||||
|
||||
export const whitelistImportFromExcel = (file: File) => {
|
||||
export const listWhitelist = () =>
|
||||
request.get<
|
||||
ResponseData<{
|
||||
total: number;
|
||||
white_list: AdminService.ListWhitelistItem[];
|
||||
}>
|
||||
>(adminListWhitelist);
|
||||
|
||||
export const createWhitelistEntry = (email: string) =>
|
||||
request.post<ResponseData<never>>(adminCreateWhitelistEntry, { email });
|
||||
|
||||
export const updateWhitelistEntry = (id: number, email: string) =>
|
||||
request.put<ResponseData<never>>(adminUpdateWhitelistEntry(id), { email });
|
||||
|
||||
export const deleteWhitelistEntry = (email: string) =>
|
||||
request.delete<ResponseData<never>>(adminDeleteWhitelistEntry(email));
|
||||
|
||||
export const importWhitelistFromExcel = (file: File) => {
|
||||
const fd = new FormData();
|
||||
|
||||
fd.append('file', file);
|
||||
|
||||
return request.post<ResponseData<never>>(
|
||||
'/api/v1/admin/whitelist/import',
|
||||
fd,
|
||||
);
|
||||
return request.post<ResponseData<never>>(adminImportWhitelist, fd);
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
145
web/src/services/admin.service.d.ts
vendored
Normal file
145
web/src/services/admin.service.d.ts
vendored
Normal file
@ -0,0 +1,145 @@
|
||||
declare module AdminService {
|
||||
export type LoginData = {
|
||||
access_token: string;
|
||||
avatar: unknown;
|
||||
color_schema: 'Bright' | 'Dark';
|
||||
create_date: string;
|
||||
create_time: number;
|
||||
email: string;
|
||||
id: string;
|
||||
is_active: '0' | '1';
|
||||
is_anonymous: '0' | '1';
|
||||
is_authenticated: '0' | '1';
|
||||
is_superuser: boolean;
|
||||
language: string;
|
||||
last_login_time: string;
|
||||
login_channel: unknown;
|
||||
nickname: string;
|
||||
password: string;
|
||||
status: '0' | '1';
|
||||
timezone: string;
|
||||
update_date: [string];
|
||||
update_time: [number];
|
||||
};
|
||||
|
||||
export type ListUsersItem = {
|
||||
create_date: string;
|
||||
email: string;
|
||||
is_active: '0' | '1';
|
||||
is_superuser: boolean;
|
||||
role: string;
|
||||
nickname: string;
|
||||
};
|
||||
|
||||
export type UserDetail = {
|
||||
create_date: string;
|
||||
email: string;
|
||||
is_active: '0' | '1';
|
||||
is_anonymous: '0' | '1';
|
||||
is_superuser: boolean;
|
||||
language: string;
|
||||
last_login_time: string;
|
||||
login_channel: unknown;
|
||||
status: '0' | '1';
|
||||
update_date: string;
|
||||
role: string;
|
||||
};
|
||||
|
||||
export type ListUserDatasetItem = {
|
||||
chunk_num: number;
|
||||
create_date: string;
|
||||
doc_num: number;
|
||||
language: string;
|
||||
name: string;
|
||||
permission: string;
|
||||
status: '0' | '1';
|
||||
token_num: number;
|
||||
update_date: string;
|
||||
};
|
||||
|
||||
export type ListUserAgentItem = {
|
||||
canvas_category: 'agent';
|
||||
permission: 'string';
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type ListServicesItem = {
|
||||
extra: Record<string, unknown>;
|
||||
host: string;
|
||||
id: number;
|
||||
name: string;
|
||||
port: number;
|
||||
service_type: string;
|
||||
status: 'alive' | 'timeout' | 'fail';
|
||||
};
|
||||
|
||||
export type ServiceDetail = {
|
||||
service_name: string;
|
||||
status: 'alive' | 'timeout';
|
||||
message: string | Record<string, any> | Record<string, any>[];
|
||||
};
|
||||
|
||||
export type PermissionData = {
|
||||
enable: boolean;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
share: boolean;
|
||||
};
|
||||
|
||||
export type ListRoleItem = {
|
||||
id: string;
|
||||
role_name: string;
|
||||
description: string;
|
||||
create_date: string;
|
||||
update_date: string;
|
||||
};
|
||||
|
||||
export type ListRoleItemWithPermission = ListRoleItem & {
|
||||
permissions: Record<string, PermissionData>;
|
||||
};
|
||||
|
||||
export type RoleDetailWithPermission = {
|
||||
role: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
permissions: Record<string, PermissionData>;
|
||||
};
|
||||
|
||||
export type RoleDetail = {
|
||||
id: string;
|
||||
name: string;
|
||||
descrtiption: string;
|
||||
create_date: string;
|
||||
update_date: string;
|
||||
};
|
||||
|
||||
export type AssignRolePermissionsInput = Record<
|
||||
string,
|
||||
Partial<PermissionData>
|
||||
>;
|
||||
export type RevokeRolePermissionInput = AssignRolePermissionsInput;
|
||||
|
||||
export type UserDetailWithPermission = {
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
role: string;
|
||||
};
|
||||
role_permissions: Record<string, PermissionData>;
|
||||
};
|
||||
|
||||
export type ResourceType = {
|
||||
resource_types: string[];
|
||||
};
|
||||
|
||||
export type ListWhitelistItem = {
|
||||
id: number;
|
||||
email: string;
|
||||
create_date: string;
|
||||
createt_time: number;
|
||||
update_date: string;
|
||||
update_time: number;
|
||||
};
|
||||
}
|
||||
@ -239,9 +239,9 @@ export default {
|
||||
adminGetRolePermissions: (roleName: string) =>
|
||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
|
||||
adminAssignRolePermissions: (roleName: string) =>
|
||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
|
||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permission`,
|
||||
adminRevokeRolePermissions: (roleName: string) =>
|
||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions/batch`,
|
||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permission`,
|
||||
adminCreateRole: `${ExternalApi}${api_host}/admin/roles`,
|
||||
adminDeleteRole: (roleName: string) =>
|
||||
`${ExternalApi}${api_host}/admin/roles/${roleName}`,
|
||||
@ -253,5 +253,13 @@ export default {
|
||||
adminGetUserPermissions: (username: string) =>
|
||||
`${ExternalApi}${api_host}/admin/users/${username}/permissions`,
|
||||
|
||||
adminListResources: `${ExternalApi}${api_host}/admin/roles/resources`,
|
||||
adminListResources: `${ExternalApi}${api_host}/admin/roles/resource`,
|
||||
|
||||
adminListWhitelist: `${ExternalApi}${api_host}/admin/whitelist`,
|
||||
adminCreateWhitelistEntry: `${ExternalApi}${api_host}/admin/whitelist/add`,
|
||||
adminUpdateWhitelistEntry: (id: number) =>
|
||||
`${ExternalApi}${api_host}/admin/whitelist/${id}`,
|
||||
adminDeleteWhitelistEntry: (email: string) =>
|
||||
`${ExternalApi}${api_host}/admin/whitelist/${email}`,
|
||||
adminImportWhitelist: `${ExternalApi}${api_host}/admin/whitelist/batch`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user