(
-
-
-
-
-
-
- {t('admin.importOverwriteExistingEmails')}
-
-
- )}
- />
-
-
+
}}
@@ -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'),
diff --git a/web/src/pages/admin/forms/role-form.tsx b/web/src/pages/admin/forms/role-form.tsx
index dd236bc31..13b52f5ab 100644
--- a/web/src/pages/admin/forms/role-form.tsx
+++ b/web/src/pages/admin/forms/role-form.tsx
@@ -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;
@@ -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 = ({
- {t(`admin.resourceType.${resourceType}`)}
+ {t(`admin.resourceType.${resourceType.toLowerCase()}`)}
))}
@@ -121,7 +124,7 @@ export const CreateRoleForm = ({
value={resourceType}
className="space-y-4"
>
-
+
{PERMISSION_TYPES.map((permissionType) => (
@@ -129,8 +132,8 @@ export const CreateRoleForm = ({
key={permissionType}
name={`permissions.${resourceType}.${permissionType}`}
render={({ field }) => (
-
-
+
+
{t(`admin.permissionType.${permissionType}`)}
@@ -158,42 +161,46 @@ export const CreateRoleForm = ({
// Export the form validation state for parent component
function useCreateRoleForm(props?: {
- defaultValues: Partial;
+ defaultValues:
+ | Partial
+ | (() => Promise);
}) {
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({
- defaultValues: {
- name: '',
- description: '',
- permissions: {},
- ...(props?.defaultValues ?? {}),
- },
+ defaultValues: formMergeDefaultValues(
+ {
+ name: '',
+ description: '',
+ permissions: {},
+ },
+ props?.defaultValues,
+ ),
resolver: zodResolver(schema),
});
const FormComponent = useCallback(
(props: Partial) => (
-
+
),
- [form],
+ [id, form],
);
return {
diff --git a/web/src/pages/admin/layout.tsx b/web/src/pages/admin/layout.tsx
index f019fa22c..77fd75dc0 100644
--- a/web/src/pages/admin/layout.tsx
+++ b/web/src/pages/admin/layout.tsx
@@ -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();
diff --git a/web/src/pages/admin/monitoring.tsx b/web/src/pages/admin/monitoring.tsx
index 60af64545..36ab16aaa 100644
--- a/web/src/pages/admin/monitoring.tsx
+++ b/web/src/pages/admin/monitoring.tsx
@@ -2,7 +2,7 @@ import { Card, CardContent } from '@/components/ui/card';
function AdminMonitoring() {
return (
-
+
diff --git a/web/src/pages/admin/roles.tsx b/web/src/pages/admin/roles.tsx
index 8d69a50f8..dcee2d363 100644
--- a/web/src/pages/admin/roles.tsx
+++ b/web/src/pages/admin/roles.tsx
@@ -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(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(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 (
<>
-
+
{t('admin.roles')}
- {roleList?.map((role) => {
- const resources = Object.entries(role.permissions);
-
- return (
+ {roleList?.length ? (
+ roleList.map((role) => (
-
-
+
+
{role.role_name}
-
+
+
{role.description || (
{t('admin.noDescription')}
@@ -113,6 +186,11 @@ function AdminRoles() {
@@ -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);
+ }}
>
@@ -131,82 +214,95 @@ function AdminRoles() {
- {resources.map(([name]) => (
+ {resourceTypes?.map((resourceName) => (
- {t(`admin.resourceType.${name}`)}
+ {t(
+ `admin.resourceType.${resourceName.toLowerCase()}`,
+ )}
))}
- {resources.map(([name, permission]) => (
-
-
-
+ {resourceTypes?.map((resourceName) => {
+ const permission =
+ role.permissions[resourceName.toLowerCase()];
-
+ return (
+
+
+
+ {PERMISSION_TYPES.map((permissionType) => (
+
-
- ))}
+
+ updateRolePermissionsMutation.mutate({
+ name: role.role_name,
+ resourceName:
+ resourceName.toLowerCase(),
+ permissionType,
+ value,
+ })
+ }
+ />
+
+ ))}
+
+
+
+ );
+ })}
- );
- })}
+ ))
+ ) : (
+
+ )}
- {/* Add Role Modal */}
-