mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42: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",
|
"umi-request": "^1.4.0",
|
||||||
"unist-util-visit-parents": "^6.0.1",
|
"unist-util-visit-parents": "^6.0.1",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,58 +1,66 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { t } from 'i18next';
|
import { t } from 'i18next';
|
||||||
|
import { useIsDarkTheme } from '../theme-provider';
|
||||||
|
|
||||||
type EmptyProps = {
|
type EmptyProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmptyIcon = () => (
|
const EmptyIcon = () => {
|
||||||
<svg
|
const isDarkTheme = useIsDarkTheme();
|
||||||
width="184"
|
|
||||||
height="152"
|
return (
|
||||||
viewBox="0 0 184 152"
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
width="184"
|
||||||
>
|
height="152"
|
||||||
<title>{t('common.noData')}</title>
|
viewBox="0 0 184 152"
|
||||||
<g fill="none" fillRule="evenodd">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
<g transform="translate(24 31.67)">
|
>
|
||||||
<ellipse
|
<title>{t('common.noData')}</title>
|
||||||
fillOpacity=".8"
|
<g fill="none" fillRule="evenodd">
|
||||||
fill="#F5F5F7"
|
<g transform="translate(24 31.67)">
|
||||||
cx="67.797"
|
<ellipse
|
||||||
cy="106.89"
|
fillOpacity=".8"
|
||||||
rx="67.797"
|
fill={isDarkTheme ? '#28282A' : '#F5F5F7'}
|
||||||
ry="12.668"
|
cx="67.797"
|
||||||
></ellipse>
|
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
|
<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"
|
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="#AEB8C2"
|
fill={isDarkTheme ? '#45413A' : '#DCE0E6'}
|
||||||
></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"
|
|
||||||
></path>
|
></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>
|
</g>
|
||||||
<path
|
</svg>
|
||||||
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>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Empty = (props: EmptyProps) => {
|
const Empty = (props: EmptyProps) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
@ -1960,7 +1960,13 @@ Important structured information may include: names, dates, locations, events, k
|
|||||||
newRole: 'New Role',
|
newRole: 'New Role',
|
||||||
addNewRole: 'Add new role',
|
addNewRole: 'Add new role',
|
||||||
roleName: 'Role name',
|
roleName: 'Role name',
|
||||||
|
roleNameRequired: 'Role name is required',
|
||||||
resources: 'Resources',
|
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({
|
export default function EnterpriseFeature({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: () => React.ReactNode;
|
children: React.ReactNode | (() => React.ReactNode);
|
||||||
}) {
|
}) {
|
||||||
return IS_ENTERPRISE
|
return IS_ENTERPRISE
|
||||||
? typeof children === 'function'
|
? typeof children === 'function'
|
||||||
|
|||||||
@ -15,7 +15,7 @@ const ThemeSwitch = forwardRef<
|
|||||||
return (
|
return (
|
||||||
<Root
|
<Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('relative rounded-full')}
|
className={cn('relative rounded-full', className)}
|
||||||
{...props}
|
{...props}
|
||||||
checked={isDark}
|
checked={isDark}
|
||||||
onCheckedChange={(value) =>
|
onCheckedChange={(value) =>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -14,8 +13,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
interface ImportExcelFormData {
|
export interface ImportExcelFormData {
|
||||||
file: FileList;
|
file: File;
|
||||||
overwriteExisting: boolean;
|
overwriteExisting: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +42,7 @@ export const ImportExcelForm = ({
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="file"
|
name="file"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
render={({ field: { onChange, value, ...field } }) => (
|
render={({ field: { onChange, value, ...field } }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="text-sm font-medium">
|
<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"
|
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) => {
|
onChange={(e) => {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
onChange(files);
|
onChange(files?.[0]);
|
||||||
}}
|
}}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
@ -66,27 +66,7 @@ export const ImportExcelForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Overwrite checkbox */}
|
<p className="text-sm text-text-secondary">
|
||||||
<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">
|
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="admin.importFileTips"
|
i18nKey="admin.importFileTips"
|
||||||
components={{ code: <code /> }}
|
components={{ code: <code /> }}
|
||||||
@ -105,21 +85,14 @@ function useImportExcelForm() {
|
|||||||
const schema = useMemo(() => {
|
const schema = useMemo(() => {
|
||||||
return z.object({
|
return z.object({
|
||||||
file: z
|
file: z
|
||||||
.any()
|
.instanceof(File, { message: t('admin.importFileRequired') })
|
||||||
.refine((files) => files && files.length > 0, {
|
|
||||||
message: t('admin.importFileRequired'),
|
|
||||||
})
|
|
||||||
.refine(
|
.refine(
|
||||||
(files) => {
|
(file) => {
|
||||||
if (!files || files.length === 0) return false;
|
|
||||||
const [file] = files;
|
|
||||||
return (
|
return (
|
||||||
file.type ===
|
file.type ===
|
||||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
|
||||||
// || file.type === 'application/vnd.ms-excel'
|
|
||||||
file.name.endsWith('.xlsx')
|
file.name.endsWith('.xlsx')
|
||||||
);
|
);
|
||||||
// || file.name.endsWith('.xls');
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: t('admin.invalidExcelFile'),
|
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 { Card, CardContent } from '@/components/ui/card';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -16,15 +24,11 @@ import {
|
|||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from '@/components/ui/tabs-underlined';
|
} 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;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
permissions: Record<string, AdminService.PermissionData>;
|
permissions: Record<string, AdminService.PermissionData>;
|
||||||
@ -36,8 +40,6 @@ interface CreateRoleFormProps {
|
|||||||
onSubmit?: (data: CreateRoleFormData) => void;
|
onSubmit?: (data: CreateRoleFormData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PERMISSION_TYPES = ['enable', 'read', 'write', 'share'] as const;
|
|
||||||
|
|
||||||
export const CreateRoleForm = ({
|
export const CreateRoleForm = ({
|
||||||
id,
|
id,
|
||||||
form,
|
form,
|
||||||
@ -48,6 +50,7 @@ export const CreateRoleForm = ({
|
|||||||
const { data: resourceTypes } = useQuery({
|
const { data: resourceTypes } = useQuery({
|
||||||
queryKey: ['admin/resourceTypes'],
|
queryKey: ['admin/resourceTypes'],
|
||||||
queryFn: async () => (await listResources()).data.data.resource_types,
|
queryFn: async () => (await listResources()).data.data.resource_types,
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -108,9 +111,9 @@ export const CreateRoleForm = ({
|
|||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={resourceType}
|
key={resourceType}
|
||||||
value={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>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@ -121,7 +124,7 @@ export const CreateRoleForm = ({
|
|||||||
value={resourceType}
|
value={resourceType}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
<Card className="border-0 bg-bg-card">
|
<Card className="border-0 bg-bg-card !shadow-none">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{PERMISSION_TYPES.map((permissionType) => (
|
{PERMISSION_TYPES.map((permissionType) => (
|
||||||
@ -129,8 +132,8 @@ export const CreateRoleForm = ({
|
|||||||
key={permissionType}
|
key={permissionType}
|
||||||
name={`permissions.${resourceType}.${permissionType}`}
|
name={`permissions.${resourceType}.${permissionType}`}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="space-y-0 inline-flex items-center gap-2">
|
||||||
<FormLabel className="flex items-center gap-2">
|
<FormLabel>
|
||||||
{t(`admin.permissionType.${permissionType}`)}
|
{t(`admin.permissionType.${permissionType}`)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@ -158,42 +161,46 @@ export const CreateRoleForm = ({
|
|||||||
|
|
||||||
// Export the form validation state for parent component
|
// Export the form validation state for parent component
|
||||||
function useCreateRoleForm(props?: {
|
function useCreateRoleForm(props?: {
|
||||||
defaultValues: Partial<CreateRoleFormData>;
|
defaultValues:
|
||||||
|
| Partial<CreateRoleFormData>
|
||||||
|
| (() => Promise<CreateRoleFormData>);
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const id = useId();
|
const id = useId();
|
||||||
|
|
||||||
const schema = useMemo(() => {
|
const schema = useMemo(() => {
|
||||||
return z.object({
|
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(),
|
description: z.string().optional(),
|
||||||
permissions: z.record(
|
permissions: z.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
z.object({
|
z.object({
|
||||||
enable: z.boolean(),
|
enable: z.boolean().optional(),
|
||||||
read: z.boolean(),
|
read: z.boolean().optional(),
|
||||||
write: z.boolean(),
|
write: z.boolean().optional(),
|
||||||
share: z.boolean(),
|
share: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const form = useForm<CreateRoleFormData>({
|
const form = useForm<CreateRoleFormData>({
|
||||||
defaultValues: {
|
defaultValues: formMergeDefaultValues(
|
||||||
name: '',
|
{
|
||||||
description: '',
|
name: '',
|
||||||
permissions: {},
|
description: '',
|
||||||
...(props?.defaultValues ?? {}),
|
permissions: {},
|
||||||
},
|
},
|
||||||
|
props?.defaultValues,
|
||||||
|
),
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const FormComponent = useCallback(
|
const FormComponent = useCallback(
|
||||||
(props: Partial<CreateRoleFormProps>) => (
|
(props: Partial<CreateRoleFormProps>) => (
|
||||||
<CreateRoleForm id="create-role-form" form={form} {...props} />
|
<CreateRoleForm id={id} form={form} {...props} />
|
||||||
),
|
),
|
||||||
[form],
|
[id, form],
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import message from '@/components/ui/message';
|
import message from '@/components/ui/message';
|
||||||
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 { logout } from '@/services/admin-service';
|
||||||
import authorizationUtil from '@/utils/authorization-util';
|
import authorizationUtil from '@/utils/authorization-util';
|
||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
@ -60,7 +60,7 @@ const AdminLayout = () => {
|
|||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
mutationKey: ['adminLogout'],
|
mutationKey: ['adminLogout'],
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await adminService.logout();
|
await logout();
|
||||||
|
|
||||||
message.success(t('message.logout'));
|
message.success(t('message.logout'));
|
||||||
authorizationUtil.removeAll();
|
authorizationUtil.removeAll();
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
|
|
||||||
function AdminMonitoring() {
|
function AdminMonitoring() {
|
||||||
return (
|
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">
|
<CardContent className="size-full p-0">
|
||||||
<iframe />
|
<iframe />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
import { useState } from 'react';
|
import { mapKeys } from 'lodash';
|
||||||
|
|
||||||
|
import { useId, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { Label } from '@/components/ui/label';
|
||||||
import { LoadingButton } from '@/components/ui/loading-button';
|
import { LoadingButton } from '@/components/ui/loading-button';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
@ -18,71 +29,134 @@ import {
|
|||||||
import { LucideEdit3, LucideTrash2, LucideUserPlus } from 'lucide-react';
|
import { LucideEdit3, LucideTrash2, LucideUserPlus } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
AdminService,
|
||||||
DialogContent,
|
assignRolePermissions,
|
||||||
DialogFooter,
|
createRole,
|
||||||
DialogHeader,
|
deleteRole,
|
||||||
DialogTitle,
|
listResources,
|
||||||
} from '@/components/ui/dialog';
|
listRolesWithPermission,
|
||||||
|
revokeRolePermissions,
|
||||||
|
updateRoleDescription,
|
||||||
|
} from '@/services/admin-service';
|
||||||
|
|
||||||
import { listRolesWithPermission } from '@/services/admin-service';
|
import Empty from '@/components/empty/empty';
|
||||||
|
import useCreateRoleForm, { CreateRoleFormData } from './forms/role-form';
|
||||||
import useCreateRoleForm from './forms/role-form';
|
import { PERMISSION_TYPES } from './utils';
|
||||||
|
|
||||||
// #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
|
|
||||||
|
|
||||||
function AdminRoles() {
|
function AdminRoles() {
|
||||||
const { t } = useTranslation();
|
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({
|
const { data: roleList } = useQuery({
|
||||||
queryKey: ['admin/listRolesWithPermission'],
|
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) => {
|
const updateRoleDescriptionMutation = useMutation({
|
||||||
console.log('New role data:', data);
|
mutationFn: (data: { name: string; description: string }) =>
|
||||||
// TODO: Implement actual role creation logic
|
updateRoleDescription(data.name, data.description),
|
||||||
createRoleForm.form.reset();
|
onSuccess: () => {
|
||||||
setIsAddRoleModalOpen(false);
|
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 (
|
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">
|
<ScrollArea className="size-full">
|
||||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||||
<CardTitle>{t('admin.roles')}</CardTitle>
|
<CardTitle>{t('admin.roles')}</CardTitle>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="h-10 px-4"
|
className="h-10 px-4"
|
||||||
onClick={() => setIsAddRoleModalOpen(true)}
|
onClick={() => setAddRoleModalOpen(true)}
|
||||||
>
|
>
|
||||||
<LucideUserPlus />
|
<LucideUserPlus />
|
||||||
{t('admin.newRole')}
|
{t('admin.newRole')}
|
||||||
@ -90,20 +164,19 @@ function AdminRoles() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
{roleList?.map((role) => {
|
{roleList?.length ? (
|
||||||
const resources = Object.entries(role.permissions);
|
roleList.map((role) => (
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
<Card
|
||||||
key={role.id}
|
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">
|
<CardHeader className="space-y-0 flex flex-row gap-4 items-center border-b border-border-button">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5 w-0 flex-1">
|
||||||
<CardTitle className="font-normal text-xl">
|
<CardTitle className="font-normal text-xl">
|
||||||
{role.role_name}
|
{role.role_name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="text-sm text-text-secondary">
|
|
||||||
|
<div className="text-sm text-text-secondary break-words">
|
||||||
{role.description || (
|
{role.description || (
|
||||||
<i className="text-muted-foreground">
|
<i className="text-muted-foreground">
|
||||||
{t('admin.noDescription')}
|
{t('admin.noDescription')}
|
||||||
@ -113,6 +186,11 @@ function AdminRoles() {
|
|||||||
<Button
|
<Button
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
className="ml-2 p-0 border-0 size-[1em] align-middle opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
|
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]" />
|
<LucideEdit3 className="!size-[1em]" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -123,6 +201,11 @@ function AdminRoles() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="ml-auto opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
|
className="ml-auto opacity-0 group-hover:opacity-100 group-focus-within:opacity-100"
|
||||||
|
disabled={deleteRoleMutation.isPending}
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteModalOpen(true);
|
||||||
|
setRoleToMakeAction(role);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<LucideTrash2 />
|
<LucideTrash2 />
|
||||||
</Button>
|
</Button>
|
||||||
@ -131,82 +214,95 @@ function AdminRoles() {
|
|||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<Tabs
|
<Tabs
|
||||||
className="h-full flex flex-col"
|
className="h-full flex flex-col"
|
||||||
defaultValue={resources[0]?.[0]}
|
defaultValue={resourceTypes?.[0]}
|
||||||
>
|
>
|
||||||
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
|
<TabsList className="p-0 mb-2 gap-4 bg-transparent">
|
||||||
{resources.map(([name]) => (
|
{resourceTypes?.map((resourceName) => (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={name}
|
key={resourceName}
|
||||||
value={name}
|
value={resourceName}
|
||||||
className="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.${name}`)}
|
{t(
|
||||||
|
`admin.resourceType.${resourceName.toLowerCase()}`,
|
||||||
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{resources.map(([name, permission]) => (
|
{resourceTypes?.map((resourceName) => {
|
||||||
<TabsContent key={name} value={name}>
|
const permission =
|
||||||
<div className="flex gap-8">
|
role.permissions[resourceName.toLowerCase()];
|
||||||
<Label className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={!!permission.enable}
|
|
||||||
onCheckedChange={console.log}
|
|
||||||
/>
|
|
||||||
{t('admin.enable')}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Label className="flex items-center gap-2">
|
return (
|
||||||
<Switch
|
<TabsContent key={resourceName} value={resourceName}>
|
||||||
checked={!!permission.read}
|
<Card className="border-0 bg-bg-card !shadow-none">
|
||||||
onCheckedChange={() => {}}
|
<CardContent className="p-6 flex gap-8">
|
||||||
/>
|
{PERMISSION_TYPES.map((permissionType) => (
|
||||||
{t('admin.read')}
|
<Label
|
||||||
</Label>
|
key={permissionType}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{t(`admin.${permissionType}`)}
|
||||||
|
|
||||||
<Label className="flex items-center gap-2">
|
<Switch
|
||||||
<Switch
|
disabled={
|
||||||
checked={!!permission.write}
|
updateRolePermissionsMutation.isPending
|
||||||
onCheckedChange={() => {}}
|
}
|
||||||
/>
|
checked={!!permission?.[permissionType]}
|
||||||
{t('admin.write')}
|
onCheckedChange={(value) =>
|
||||||
</Label>
|
updateRolePermissionsMutation.mutate({
|
||||||
|
name: role.role_name,
|
||||||
<Label className="flex items-center gap-2">
|
resourceName:
|
||||||
<Switch
|
resourceName.toLowerCase(),
|
||||||
checked={!!permission.share}
|
permissionType,
|
||||||
onCheckedChange={() => {}}
|
value,
|
||||||
/>
|
})
|
||||||
{t('admin.share')}
|
}
|
||||||
</Label>
|
/>
|
||||||
</div>
|
</Label>
|
||||||
</TabsContent>
|
))}
|
||||||
))}
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
))
|
||||||
})}
|
) : (
|
||||||
|
<Empty className="py-24" />
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Add Role Modal */}
|
{/* Add role modal */}
|
||||||
<Dialog open={isAddRoleModalOpen} onOpenChange={setIsAddRoleModalOpen}>
|
<Dialog open={isAddRoleModalOpen} onOpenChange={setAddRoleModalOpen}>
|
||||||
<DialogContent className="max-w-2xl p-0 border-border-button">
|
<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">
|
<DialogHeader className="p-6 border-b border-border-button">
|
||||||
<DialogTitle>{t('admin.addNewRole')}</DialogTitle>
|
<DialogTitle>{t('admin.addNewRole')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<section className="px-12 py-4">
|
<section className="px-12 py-4">
|
||||||
<createRoleForm.FormComponent onSubmit={handleAddRole} />
|
<createRoleForm.FormComponent
|
||||||
|
onSubmit={createRoleMutation.mutate}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
|
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8">
|
||||||
<Button
|
<Button
|
||||||
className="px-4 h-10 dark:border-border-button"
|
className="px-4 h-10 dark:border-border-button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setIsAddRoleModalOpen(false)}
|
onClick={() => setAddRoleModalOpen(false)}
|
||||||
>
|
>
|
||||||
{t('admin.cancel')}
|
{t('admin.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -222,6 +318,118 @@ function AdminRoles() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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)) {
|
if (isPlainObject(content)) {
|
||||||
return (
|
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]) => (
|
{Object.entries<any>(content).map(([key, value]) => (
|
||||||
<div key={key} className="contents">
|
<div key={key} className="contents">
|
||||||
<dt className="px-3 py-2 bg-bg-card">
|
<dt className="px-3 py-2 bg-bg-card">
|
||||||
|
|||||||
@ -59,19 +59,13 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
|
|
||||||
import {
|
import { listServices, showServiceDetails } from '@/services/admin-service';
|
||||||
listServices,
|
|
||||||
showServiceDetails,
|
|
||||||
type AdminService,
|
|
||||||
} from '@/services/admin-service';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EMPTY_DATA,
|
EMPTY_DATA,
|
||||||
createColumnFilterFn,
|
createColumnFilterFn,
|
||||||
createFuzzySearchFn,
|
createFuzzySearchFn,
|
||||||
getColumnFilter,
|
|
||||||
getSortIcon,
|
getSortIcon,
|
||||||
setColumnFilter,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
import ServiceDetail from './service-detail';
|
import ServiceDetail from './service-detail';
|
||||||
@ -97,22 +91,18 @@ function AdminServiceStatus() {
|
|||||||
const [itemToMakeAction, setItemToMakeAction] =
|
const [itemToMakeAction, setItemToMakeAction] =
|
||||||
useState<AdminService.ListServicesItem | null>(null);
|
useState<AdminService.ListServicesItem | null>(null);
|
||||||
|
|
||||||
const { data: servicesList, isPending } = useQuery({
|
const { data: servicesList } = useQuery({
|
||||||
queryKey: ['admin/listServices'],
|
queryKey: ['admin/listServices'],
|
||||||
queryFn: async () => (await listServices()).data.data,
|
queryFn: async () => (await listServices()).data.data,
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const { data: serviceDetails, error: serviceDetailsError } = useQuery({
|
||||||
data: serviceDetails,
|
|
||||||
isPending: isServiceDetailsPending,
|
|
||||||
error: serviceDetailsError,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ['admin/serviceDetails', itemToMakeAction?.id],
|
queryKey: ['admin/serviceDetails', itemToMakeAction?.id],
|
||||||
queryFn: async () =>
|
queryFn: async () =>
|
||||||
(await showServiceDetails(itemToMakeAction?.id!)).data.data,
|
(await showServiceDetails(itemToMakeAction!?.id)).data.data,
|
||||||
enabled: !!(itemToMakeAction && detailModalOpen),
|
enabled: !!(itemToMakeAction && detailModalOpen),
|
||||||
retry: false,
|
retry: false,
|
||||||
refetchInterval: Infinity,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const columnDefs = useMemo(
|
const columnDefs = useMemo(
|
||||||
@ -202,7 +192,7 @@ function AdminServiceStatus() {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
@ -225,7 +215,7 @@ function AdminServiceStatus() {
|
|||||||
|
|
||||||
return (
|
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">
|
<ScrollArea className="size-full">
|
||||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||||
<CardTitle>{t('admin.serviceStatus')}</CardTitle>
|
<CardTitle>{t('admin.serviceStatus')}</CardTitle>
|
||||||
@ -254,11 +244,12 @@ function AdminServiceStatus() {
|
|||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={
|
value={
|
||||||
(getColumnFilter(table, 'service_type')
|
table
|
||||||
?.value as string) ?? ''
|
.getColumn('service_type')!
|
||||||
|
?.getFilterValue() as string
|
||||||
}
|
}
|
||||||
onValueChange={(value) =>
|
onValueChange={
|
||||||
setColumnFilter(table, 'service_type', value)
|
table.getColumn('service_type')!?.setFilterValue
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Label className="space-x-2">
|
<Label className="space-x-2">
|
||||||
|
|||||||
@ -41,7 +41,6 @@ import {
|
|||||||
getUserDetails,
|
getUserDetails,
|
||||||
listUserAgents,
|
listUserAgents,
|
||||||
listUserDatasets,
|
listUserDatasets,
|
||||||
type AdminService,
|
|
||||||
} from '@/services/admin-service';
|
} from '@/services/admin-service';
|
||||||
|
|
||||||
import EnterpriseFeature from './components/enterprise-feature';
|
import EnterpriseFeature from './components/enterprise-feature';
|
||||||
@ -323,7 +322,7 @@ function AdminUserDetail() {
|
|||||||
</Button>
|
</Button>
|
||||||
</nav>
|
</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">
|
<CardHeader className="pb-10 border-b dark:border-border-button space-y-8">
|
||||||
<section className="flex items-center gap-4 text-base">
|
<section className="flex items-center gap-4 text-base">
|
||||||
<Avatar className="justify-center items-center bg-bg-group uppercase">
|
<Avatar className="justify-center items-center bg-bg-group uppercase">
|
||||||
|
|||||||
@ -84,7 +84,6 @@ import {
|
|||||||
updateUserPassword,
|
updateUserPassword,
|
||||||
updateUserRole,
|
updateUserRole,
|
||||||
updateUserStatus,
|
updateUserStatus,
|
||||||
type AdminService,
|
|
||||||
} from '@/services/admin-service';
|
} from '@/services/admin-service';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -130,7 +129,7 @@ function AdminUserManagement() {
|
|||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: usersList, isPending } = useQuery({
|
const { data: usersList } = useQuery({
|
||||||
queryKey: ['admin/listUsers'],
|
queryKey: ['admin/listUsers'],
|
||||||
queryFn: async () => (await listUsers()).data.data,
|
queryFn: async () => (await listUsers()).data.data,
|
||||||
retry: false,
|
retry: false,
|
||||||
@ -341,13 +340,7 @@ function AdminUserManagement() {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[
|
[t, updateUserRoleMutation, roleList, updateUserStatusMutation, navigate],
|
||||||
roleList,
|
|
||||||
t,
|
|
||||||
navigate,
|
|
||||||
updateUserStatusMutation.isPending,
|
|
||||||
updateUserRoleMutation.isPending,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
@ -364,7 +357,7 @@ function AdminUserManagement() {
|
|||||||
|
|
||||||
return (
|
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">
|
<ScrollArea className="size-full">
|
||||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||||
<CardTitle>{t('admin.userManagement')}</CardTitle>
|
<CardTitle>{t('admin.userManagement')}</CardTitle>
|
||||||
|
|||||||
@ -4,10 +4,35 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
RowData,
|
RowData,
|
||||||
SortDirection,
|
SortDirection,
|
||||||
Table,
|
|
||||||
TransformFilterValueFn,
|
TransformFilterValueFn,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { LucideSortAsc, LucideSortDesc } from 'lucide-react';
|
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 {
|
export function parseBooleanish(value: any): boolean {
|
||||||
return typeof value === 'string'
|
return typeof value === 'string'
|
||||||
@ -42,37 +67,6 @@ export function createColumnFilterFn<TData extends RowData>(
|
|||||||
return Object.assign(filterFn, options) as FilterFn<TData>;
|
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) {
|
export function getSortIcon(sorting: false | SortDirection) {
|
||||||
return {
|
return {
|
||||||
asc: <LucideSortAsc />,
|
asc: <LucideSortAsc />,
|
||||||
@ -80,6 +74,7 @@ export function getSortIcon(sorting: false | SortDirection) {
|
|||||||
}[sorting as string];
|
}[sorting as string];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PERMISSION_TYPES = ['enable', 'read', 'write', 'share'] as const;
|
||||||
export const EMPTY_DATA = Object.freeze<any[]>([]) as any[];
|
export const EMPTY_DATA = Object.freeze<any[]>([]) as any[];
|
||||||
export const IS_ENTERPRISE =
|
export const IS_ENTERPRISE =
|
||||||
process.env.UMI_APP_RAGFLOW_ENTERPRISE === 'RAGFLOW_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 { TableEmpty } from '@/components/table-skeleton';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -27,45 +51,27 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import {
|
import {
|
||||||
createColumnHelper,
|
createWhitelistEntry,
|
||||||
flexRender,
|
deleteWhitelistEntry,
|
||||||
getCoreRowModel,
|
importWhitelistFromExcel,
|
||||||
getFilteredRowModel,
|
listWhitelist,
|
||||||
getPaginationRowModel,
|
updateWhitelistEntry,
|
||||||
getSortedRowModel,
|
type AdminService,
|
||||||
useReactTable,
|
} from '@/services/admin-service';
|
||||||
} from '@tanstack/react-table';
|
|
||||||
import {
|
import { EMPTY_DATA, createFuzzySearchFn, getSortIcon } from './utils';
|
||||||
LucideDownload,
|
|
||||||
LucidePlus,
|
|
||||||
LucideSearch,
|
|
||||||
LucideTrash2,
|
|
||||||
LucideUpload,
|
|
||||||
LucideUserPen,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import useCreateEmailForm from './forms/email-form';
|
import useCreateEmailForm from './forms/email-form';
|
||||||
import useImportExcelForm from './forms/import-excel-form';
|
import useImportExcelForm, {
|
||||||
import { EMPTY_DATA, createFuzzySearchFn } from './utils';
|
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
|
// #endregion
|
||||||
|
|
||||||
const columnHelper = createColumnHelper<(typeof PSEUDO_TABLE_ITEMS)[0]>();
|
const columnHelper = createColumnHelper<AdminService.ListWhitelistItem>();
|
||||||
const globalFilterFn = createFuzzySearchFn<(typeof PSEUDO_TABLE_ITEMS)[0]>([
|
const globalFilterFn = createFuzzySearchFn<AdminService.ListWhitelistItem>([
|
||||||
'email',
|
'email',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -74,104 +80,114 @@ function AdminWhitelist() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const createEmailForm = useCreateEmailForm();
|
const createEmailForm = useCreateEmailForm();
|
||||||
|
const editEmailForm = useCreateEmailForm();
|
||||||
const importExcelForm = useImportExcelForm();
|
const importExcelForm = useImportExcelForm();
|
||||||
|
|
||||||
const [emailToMakeAction, setEmailToMakeAction] = useState<string | null>(
|
const [itemToMakeAction, setItemToMakeAction] =
|
||||||
null,
|
useState<AdminService.ListWhitelistItem | null>(null);
|
||||||
);
|
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
||||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
|
||||||
const [importModalOpen, setImportModalOpen] = 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
|
// Reset form when editing a different email
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (emailToMakeAction && editModalOpen) {
|
if (itemToMakeAction && editModalOpen) {
|
||||||
createEmailForm.form.setValue('email', emailToMakeAction);
|
editEmailForm.form.setValue('email', itemToMakeAction.email);
|
||||||
}
|
}
|
||||||
}, [emailToMakeAction, editModalOpen, createEmailForm.form]);
|
}, [itemToMakeAction, editModalOpen, editEmailForm.form]);
|
||||||
|
|
||||||
const { isPending: isCreating, mutateAsync: createEmail } = useMutation({
|
const createWhitelistEntryMutation = useMutation({
|
||||||
mutationFn: async (data: { email: string }) => {
|
mutationFn: (data: { email: string }) => createWhitelistEntry(data.email),
|
||||||
/* create email API call */
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||||
setCreateModalOpen(false);
|
setCreateModalOpen(false);
|
||||||
setEmailToMakeAction(null);
|
|
||||||
createEmailForm.form.reset();
|
createEmailForm.form.reset();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Error creating email:', error);
|
console.error('Error creating email:', error);
|
||||||
},
|
},
|
||||||
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isPending: isEditing, mutateAsync: updateEmail } = useMutation({
|
const updateWhitelistEntryMutation = useMutation({
|
||||||
mutationFn: async (data: { email: string }) => {
|
mutationFn: (data: { id: number; email: string }) =>
|
||||||
/* update email API call */
|
updateWhitelistEntry(data.id, data.email),
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||||
setEditModalOpen(false);
|
setEditModalOpen(false);
|
||||||
setEmailToMakeAction(null);
|
setItemToMakeAction(null);
|
||||||
createEmailForm.form.reset();
|
editEmailForm.form.reset();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isPending: isDeleting, mutateAsync: deleteEmail } = useMutation({
|
const deleteWhitelistEntryMutation = useMutation({
|
||||||
mutationFn: async (data: { email: string }) => {
|
mutationFn: (data: { email: string }) => deleteWhitelistEntry(data.email),
|
||||||
/* delete email API call */
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||||
setDeleteModalOpen(false);
|
setDeleteModalOpen(false);
|
||||||
setEmailToMakeAction(null);
|
setItemToMakeAction(null);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Error deleting email:', error);
|
console.error('Error deleting email:', error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isPending: isImporting, mutateAsync: importExcel } = useMutation({
|
const importExcelMutation = useMutation({
|
||||||
mutationFn: async (data: {
|
mutationFn: (data: ImportExcelFormData) =>
|
||||||
file: FileList;
|
importWhitelistFromExcel(data.file),
|
||||||
overwriteExisting: boolean;
|
|
||||||
}) => {
|
|
||||||
/* import Excel API call */
|
|
||||||
console.log(
|
|
||||||
'Importing Excel file:',
|
|
||||||
data.file[0]?.name,
|
|
||||||
'Overwrite:',
|
|
||||||
data.overwriteExisting,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin/whitelist'] });
|
queryClient.invalidateQueries({ queryKey: ['admin/listWhitelist'] });
|
||||||
setImportModalOpen(false);
|
setImportModalOpen(false);
|
||||||
importExcelForm.form.reset();
|
importExcelForm.form.reset();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
console.error('Error importing Excel:', 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(
|
const columnDefs = useMemo(
|
||||||
() => [
|
() => [
|
||||||
columnHelper.accessor('email', {
|
columnHelper.accessor('email', {
|
||||||
header: 'Email',
|
header: t('admin.email'),
|
||||||
|
enableSorting: false,
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('created_by', {
|
columnHelper.accessor('create_date', {
|
||||||
header: 'Created by',
|
header: t('admin.createDate'),
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('created_at', {
|
columnHelper.accessor('update_date', {
|
||||||
header: 'Created date',
|
header: t('admin.updateDate'),
|
||||||
cell: ({ row }) =>
|
|
||||||
new Date(row.getValue('created_at') as number).toLocaleString(),
|
|
||||||
}),
|
}),
|
||||||
columnHelper.display({
|
columnHelper.display({
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
header: 'Actions',
|
header: t('admin.actions'),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
|
<div className="opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity duration-100">
|
||||||
<Button
|
<Button
|
||||||
@ -179,7 +195,7 @@ function AdminWhitelist() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="border-0 text-text-secondary"
|
className="border-0 text-text-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEmailToMakeAction(row.original.email);
|
setItemToMakeAction(row.original);
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -190,7 +206,7 @@ function AdminWhitelist() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="border-0 text-text-secondary"
|
className="border-0 text-text-secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEmailToMakeAction(row.original.email);
|
setItemToMakeAction(row.original);
|
||||||
setDeleteModalOpen(true);
|
setDeleteModalOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -200,11 +216,11 @@ function AdminWhitelist() {
|
|||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
[],
|
[t],
|
||||||
);
|
);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: PSEUDO_TABLE_ITEMS ?? EMPTY_DATA,
|
data: whitelist ?? EMPTY_DATA,
|
||||||
columns: columnDefs,
|
columns: columnDefs,
|
||||||
|
|
||||||
globalFilterFn,
|
globalFilterFn,
|
||||||
@ -217,7 +233,7 @@ function AdminWhitelist() {
|
|||||||
|
|
||||||
return (
|
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">
|
<ScrollArea className="size-full">
|
||||||
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
<CardHeader className="space-y-0 flex flex-row justify-between items-center">
|
||||||
<CardTitle>{t('admin.whitelistManagement')}</CardTitle>
|
<CardTitle>{t('admin.whitelistManagement')}</CardTitle>
|
||||||
@ -236,6 +252,7 @@ function AdminWhitelist() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-10 px-4 dark:bg-bg-input dark:border-border-button text-text-secondary"
|
className="h-10 px-4 dark:bg-bg-input dark:border-border-button text-text-secondary"
|
||||||
|
onClick={handleExportExcel}
|
||||||
>
|
>
|
||||||
<LucideUpload />
|
<LucideUpload />
|
||||||
{t('admin.exportAsExcel')}
|
{t('admin.exportAsExcel')}
|
||||||
@ -264,8 +281,8 @@ function AdminWhitelist() {
|
|||||||
<Table>
|
<Table>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col />
|
<col />
|
||||||
<col className="w-[20%]" />
|
<col className="w-[25%]" />
|
||||||
<col className="w-[30%]" />
|
<col className="w-[25%]" />
|
||||||
<col className="w-[12rem]" />
|
<col className="w-[12rem]" />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
|
|
||||||
@ -274,12 +291,23 @@ function AdminWhitelist() {
|
|||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead key={header.id}>
|
<TableHead key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder ? null : header.column.getCanSort() ? (
|
||||||
? null
|
<Button
|
||||||
: flexRender(
|
variant="ghost"
|
||||||
|
onClick={header.column.getToggleSortingHandler()}
|
||||||
|
>
|
||||||
|
{flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext(),
|
header.getContext(),
|
||||||
)}
|
)}
|
||||||
|
{getSortIcon(header.column.getIsSorted())}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)
|
||||||
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@ -328,7 +356,7 @@ function AdminWhitelist() {
|
|||||||
className="p-0 border-border-button"
|
className="p-0 border-border-button"
|
||||||
onAnimationEnd={() => {
|
onAnimationEnd={() => {
|
||||||
if (!deleteModalOpen) {
|
if (!deleteModalOpen) {
|
||||||
setEmailToMakeAction(null);
|
setItemToMakeAction(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -341,7 +369,7 @@ function AdminWhitelist() {
|
|||||||
{t('admin.deleteWhitelistEmailConfirmation')}
|
{t('admin.deleteWhitelistEmailConfirmation')}
|
||||||
|
|
||||||
<div className="rounded-lg mt-6 p-4 border border-border-button">
|
<div className="rounded-lg mt-6 p-4 border border-border-button">
|
||||||
{emailToMakeAction}
|
{itemToMakeAction?.email}
|
||||||
</div>
|
</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</section>
|
</section>
|
||||||
@ -351,7 +379,7 @@ function AdminWhitelist() {
|
|||||||
className="px-4 h-10 dark:border-border-button"
|
className="px-4 h-10 dark:border-border-button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setDeleteModalOpen(false)}
|
onClick={() => setDeleteModalOpen(false)}
|
||||||
disabled={isDeleting}
|
disabled={deleteWhitelistEntryMutation.isPending}
|
||||||
>
|
>
|
||||||
{t('admin.cancel')}
|
{t('admin.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -360,10 +388,14 @@ function AdminWhitelist() {
|
|||||||
className="px-4 h-10"
|
className="px-4 h-10"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteEmail({ email: emailToMakeAction! });
|
if (itemToMakeAction) {
|
||||||
|
deleteWhitelistEntryMutation.mutate({
|
||||||
|
email: itemToMakeAction?.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isDeleting}
|
disabled={deleteWhitelistEntryMutation.isPending}
|
||||||
loading={isDeleting}
|
loading={deleteWhitelistEntryMutation.isPending}
|
||||||
>
|
>
|
||||||
{t('admin.delete')}
|
{t('admin.delete')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
@ -372,14 +404,15 @@ function AdminWhitelist() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Create Email Modal */}
|
{/* Create Email Modal */}
|
||||||
<Dialog
|
<Dialog open={createModalOpen} onOpenChange={setCreateModalOpen}>
|
||||||
open={createModalOpen}
|
<DialogContent
|
||||||
onOpenChange={() => {
|
className="p-0 border-border-button"
|
||||||
setCreateModalOpen(false);
|
onAnimationEnd={() => {
|
||||||
createEmailForm.form.reset();
|
if (!createModalOpen) {
|
||||||
}}
|
createEmailForm.form.reset();
|
||||||
>
|
}
|
||||||
<DialogContent className="p-0 border-border-button">
|
}}
|
||||||
|
>
|
||||||
<DialogHeader className="p-6 border-b border-border-button">
|
<DialogHeader className="p-6 border-b border-border-button">
|
||||||
<DialogTitle>{t('admin.createEmail')}</DialogTitle>
|
<DialogTitle>{t('admin.createEmail')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -387,7 +420,7 @@ function AdminWhitelist() {
|
|||||||
<section className="px-12 py-4 text-text-secondary">
|
<section className="px-12 py-4 text-text-secondary">
|
||||||
<createEmailForm.FormComponent
|
<createEmailForm.FormComponent
|
||||||
id={createEmailForm.id}
|
id={createEmailForm.id}
|
||||||
onSubmit={createEmail}
|
onSubmit={createWhitelistEntryMutation.mutate}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -395,11 +428,8 @@ function AdminWhitelist() {
|
|||||||
<Button
|
<Button
|
||||||
className="px-4 h-10 dark:border-border-button"
|
className="px-4 h-10 dark:border-border-button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => setCreateModalOpen(false)}
|
||||||
setCreateModalOpen(false);
|
disabled={createWhitelistEntryMutation.isPending}
|
||||||
createEmailForm.form.reset();
|
|
||||||
}}
|
|
||||||
disabled={isCreating}
|
|
||||||
>
|
>
|
||||||
{t('admin.cancel')}
|
{t('admin.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -409,8 +439,8 @@ function AdminWhitelist() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 h-10"
|
className="px-4 h-10"
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={isCreating}
|
disabled={createWhitelistEntryMutation.isPending}
|
||||||
loading={isCreating}
|
loading={createWhitelistEntryMutation.isPending}
|
||||||
>
|
>
|
||||||
{t('admin.confirm')}
|
{t('admin.confirm')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
@ -419,20 +449,13 @@ function AdminWhitelist() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Edit Email Modal */}
|
{/* Edit Email Modal */}
|
||||||
<Dialog
|
<Dialog open={editModalOpen} onOpenChange={setEditModalOpen}>
|
||||||
open={editModalOpen}
|
|
||||||
onOpenChange={() => {
|
|
||||||
setEditModalOpen(false);
|
|
||||||
setEmailToMakeAction(null);
|
|
||||||
createEmailForm.form.reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="p-0 border-border-button"
|
className="p-0 border-border-button"
|
||||||
onAnimationEnd={() => {
|
onAnimationEnd={() => {
|
||||||
if (!editModalOpen) {
|
if (!editModalOpen) {
|
||||||
setEmailToMakeAction(null);
|
setItemToMakeAction(null);
|
||||||
createEmailForm.form.reset();
|
editEmailForm.form.reset();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -441,9 +464,16 @@ function AdminWhitelist() {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<section className="px-12 py-4 text-text-secondary">
|
<section className="px-12 py-4 text-text-secondary">
|
||||||
<createEmailForm.FormComponent
|
<editEmailForm.FormComponent
|
||||||
id={createEmailForm.id}
|
id={editEmailForm.id}
|
||||||
onSubmit={updateEmail}
|
onSubmit={(value) => {
|
||||||
|
if (itemToMakeAction) {
|
||||||
|
updateWhitelistEntryMutation.mutate({
|
||||||
|
id: itemToMakeAction.id,
|
||||||
|
email: value.email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -451,23 +481,19 @@ function AdminWhitelist() {
|
|||||||
<Button
|
<Button
|
||||||
className="px-4 h-10 dark:border-border-button"
|
className="px-4 h-10 dark:border-border-button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => setEditModalOpen(false)}
|
||||||
setEditModalOpen(false);
|
disabled={updateWhitelistEntryMutation.isPending}
|
||||||
setEmailToMakeAction(null);
|
|
||||||
createEmailForm.form.reset();
|
|
||||||
}}
|
|
||||||
disabled={isEditing}
|
|
||||||
>
|
>
|
||||||
{t('admin.cancel')}
|
{t('admin.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
form={createEmailForm.id}
|
form={editEmailForm.id}
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 h-10"
|
className="px-4 h-10"
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={isEditing}
|
disabled={updateWhitelistEntryMutation.isPending}
|
||||||
loading={isEditing}
|
loading={updateWhitelistEntryMutation.isPending}
|
||||||
>
|
>
|
||||||
{t('admin.confirm')}
|
{t('admin.confirm')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
@ -477,7 +503,14 @@ function AdminWhitelist() {
|
|||||||
|
|
||||||
{/* Import Excel Modal */}
|
{/* Import Excel Modal */}
|
||||||
<Dialog open={importModalOpen} onOpenChange={setImportModalOpen}>
|
<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">
|
<DialogHeader className="p-6 border-b border-border-button">
|
||||||
<DialogTitle>{t('admin.importWhitelist')}</DialogTitle>
|
<DialogTitle>{t('admin.importWhitelist')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@ -485,7 +518,7 @@ function AdminWhitelist() {
|
|||||||
<section className="px-12 py-4 text-text-secondary">
|
<section className="px-12 py-4 text-text-secondary">
|
||||||
<importExcelForm.FormComponent
|
<importExcelForm.FormComponent
|
||||||
id={importExcelForm.id}
|
id={importExcelForm.id}
|
||||||
onSubmit={importExcel}
|
onSubmit={importExcelMutation.mutate}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -493,11 +526,8 @@ function AdminWhitelist() {
|
|||||||
<Button
|
<Button
|
||||||
className="px-4 h-10 dark:border-border-button"
|
className="px-4 h-10 dark:border-border-button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => setImportModalOpen(false)}
|
||||||
setImportModalOpen(false);
|
disabled={importExcelMutation.isPending}
|
||||||
importExcelForm.form.reset();
|
|
||||||
}}
|
|
||||||
disabled={isImporting}
|
|
||||||
>
|
>
|
||||||
{t('admin.cancel')}
|
{t('admin.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -507,8 +537,8 @@ function AdminWhitelist() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 h-10"
|
className="px-4 h-10"
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={isImporting}
|
disabled={importExcelMutation.isPending}
|
||||||
loading={isImporting}
|
loading={importExcelMutation.isPending}
|
||||||
>
|
>
|
||||||
{t('admin.import')}
|
{t('admin.import')}
|
||||||
</LoadingButton>
|
</LoadingButton>
|
||||||
|
|||||||
@ -128,151 +128,20 @@ const {
|
|||||||
adminUpdateUserRole,
|
adminUpdateUserRole,
|
||||||
|
|
||||||
adminListResources,
|
adminListResources,
|
||||||
|
|
||||||
|
adminListWhitelist,
|
||||||
|
adminCreateWhitelistEntry,
|
||||||
|
adminUpdateWhitelistEntry,
|
||||||
|
adminDeleteWhitelistEntry,
|
||||||
|
adminImportWhitelist,
|
||||||
} = api;
|
} = api;
|
||||||
|
|
||||||
type ResponseData<D = {}> = {
|
type ResponseData<D = NonNullable<unknown>> = {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
data: D;
|
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 }) =>
|
export const login = (params: { email: string; password: string }) =>
|
||||||
request.post<ResponseData<AdminService.LoginData>>(adminLogin, params);
|
request.post<ResponseData<AdminService.LoginData>>(adminLogin, params);
|
||||||
export const logout = () => request.get<ResponseData<boolean>>(adminLogout);
|
export const logout = () => request.get<ResponseData<boolean>>(adminLogout);
|
||||||
@ -363,15 +232,29 @@ 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) => {
|
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();
|
const fd = new FormData();
|
||||||
|
|
||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
|
|
||||||
return request.post<ResponseData<never>>(
|
return request.post<ResponseData<never>>(adminImportWhitelist, fd);
|
||||||
'/api/v1/admin/whitelist/import',
|
|
||||||
fd,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
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) =>
|
adminGetRolePermissions: (roleName: string) =>
|
||||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
|
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
|
||||||
adminAssignRolePermissions: (roleName: string) =>
|
adminAssignRolePermissions: (roleName: string) =>
|
||||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions`,
|
`${ExternalApi}${api_host}/admin/roles/${roleName}/permission`,
|
||||||
adminRevokeRolePermissions: (roleName: string) =>
|
adminRevokeRolePermissions: (roleName: string) =>
|
||||||
`${ExternalApi}${api_host}/admin/roles/${roleName}/permissions/batch`,
|
`${ExternalApi}${api_host}/admin/roles/${roleName}/permission`,
|
||||||
adminCreateRole: `${ExternalApi}${api_host}/admin/roles`,
|
adminCreateRole: `${ExternalApi}${api_host}/admin/roles`,
|
||||||
adminDeleteRole: (roleName: string) =>
|
adminDeleteRole: (roleName: string) =>
|
||||||
`${ExternalApi}${api_host}/admin/roles/${roleName}`,
|
`${ExternalApi}${api_host}/admin/roles/${roleName}`,
|
||||||
@ -253,5 +253,13 @@ export default {
|
|||||||
adminGetUserPermissions: (username: string) =>
|
adminGetUserPermissions: (username: string) =>
|
||||||
`${ExternalApi}${api_host}/admin/users/${username}/permissions`,
|
`${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