Add task executor bar chart, add system version string (#11155)

### What problem does this PR solve?

- Add task executor bar chart
- Add read version string

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
Jimmy Ben Klieve
2025-11-11 15:20:37 +08:00
committed by GitHub
parent 26cf5131c9
commit 7dd9758056
16 changed files with 315 additions and 78 deletions

View File

@ -38,7 +38,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
'outline-0 fixed left-[50%] top-[50%] z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4 border bg-bg-base p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', 'outline-0 fixed left-[50%] top-[50%] rounded-lg z-50 grid w-full max-w-xl translate-x-[-50%] translate-y-[-50%] gap-4',
'border-0.5 border-border-button bg-bg-base p-6 shadow-lg duration-200 sm:rounded-lg',
'data-[state=open]:animate-in data-[state=closed]:animate-out',
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]',
'data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
className, className,
)} )}
{...props} {...props}
@ -66,6 +72,7 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
'-mx-6 -mt-6 p-6 border-b-0.5 border-border-button',
'flex flex-col space-y-1.5 text-center sm:text-left', 'flex flex-col space-y-1.5 text-center sm:text-left',
className, className,
)} )}
@ -80,6 +87,7 @@ const DialogFooter = ({
}: React.HTMLAttributes<HTMLDivElement>) => ( }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div
className={cn( className={cn(
// '-mx-6 -mb-6 px-12 pt-4 pb-8',
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4', 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-4',
className, className,
)} )}

View File

@ -40,6 +40,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref} ref={ref}
type={isPasswordInput && showPassword ? 'text' : type} type={isPasswordInput && showPassword ? 'text' : type}
className={cn( className={cn(
'peer/input',
'flex h-8 w-full rounded-md border-0.5 border-input bg-bg-input px-3 py-2 outline-none text-sm text-text-primary', 'flex h-8 w-full rounded-md border-0.5 border-input bg-bg-input px-3 py-2 outline-none text-sm text-text-primary',
'file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled', 'file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-text-disabled',
'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary', 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-primary',
@ -79,7 +80,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<Button <Button
variant="transparent" variant="transparent"
type="button" type="button"
className="border-0 absolute right-1 top-[50%] translate-y-[-50%]" className="
absolute border-0 right-1 top-[50%] translate-y-[-50%]
dark:peer-autofill/input:text-text-secondary-inverse
dark:peer-autofill/input:hover:text-text-primary-inverse
dark:peer-autofill/input:focus-visible:text-text-primary-inverse
"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
> >
{showPassword ? ( {showPassword ? (

View File

@ -13,7 +13,7 @@ const Switch = React.forwardRef<
className={cn( className={cn(
'group/switch inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full', 'group/switch inline-flex h-4 w-7 shrink-0 cursor-pointer items-center rounded-full',
'border-2 border-transparent overflow-hidden transition-colors', 'border-2 border-transparent overflow-hidden transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary',
'disabled:cursor-not-allowed disabled:opacity-50', 'disabled:cursor-not-allowed disabled:opacity-50',
'data-[state=checked]:bg-accent-primary data-[state=unchecked]:bg-text-sub-title', 'data-[state=checked]:bg-accent-primary data-[state=unchecked]:bg-text-sub-title',
className, className,

View File

@ -1911,7 +1911,7 @@ Important structured information may include: names, dates, locations, events, k
processing: 'Processing', processing: 'Processing',
}, },
admin: { admin: {
loginTitle: 'RAGFlow ADMIN', loginTitle: 'Admin Console',
title: 'RAGFlow admin', title: 'RAGFlow admin',
confirm: 'Confirm', confirm: 'Confirm',
close: 'Close', close: 'Close',
@ -1998,6 +1998,7 @@ Important structured information may include: names, dates, locations, events, k
extraInfo: 'Extra information', extraInfo: 'Extra information',
serviceDetail: `Service {{name}} detail`, serviceDetail: `Service {{name}} detail`,
taskExecutorDetail: 'Task executor detail',
whitelistManagement: 'Whitelist management', whitelistManagement: 'Whitelist management',
exportAsExcel: 'Export Excel', exportAsExcel: 'Export Excel',

View File

@ -2,7 +2,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { NavLink, Outlet, useNavigate } from 'umi'; import { NavLink, Outlet, useNavigate } from 'umi';
import { useMutation } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { import {
LucideMonitor, LucideMonitor,
@ -15,7 +15,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Routes } from '@/routes'; import { Routes } from '@/routes';
import { logout } from '@/services/admin-service'; import { getSystemVersion, logout } from '@/services/admin-service';
import authorizationUtil from '@/utils/authorization-util'; import authorizationUtil from '@/utils/authorization-util';
@ -26,6 +26,11 @@ const AdminNavigationLayout = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { data: version } = useQuery({
queryKey: ['admin/version'],
queryFn: async () => (await getSystemVersion())?.data?.data?.version,
});
const navItems = useMemo( const navItems = useMemo(
() => [ () => [
{ {
@ -109,8 +114,8 @@ const AdminNavigationLayout = () => {
<div className="mt-auto space-y-4"> <div className="mt-auto space-y-4">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-accent-primary"> <span className="leading-none text-xs text-accent-primary">
vmm.ss.rr-nnn-commithash {version}
</span> </span>
<ThemeSwitch /> <ThemeSwitch />

View File

@ -280,24 +280,24 @@ function AdminRoles() {
{/* Add role modal */} {/* Add role modal */}
<Dialog open={isAddRoleModalOpen} onOpenChange={setAddRoleModalOpen}> <Dialog open={isAddRoleModalOpen} onOpenChange={setAddRoleModalOpen}>
<DialogContent <DialogContent
className="max-w-2xl p-0 border-border-button" className="max-w-2xl"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!isAddRoleModalOpen) { if (!isAddRoleModalOpen) {
createRoleForm.form.reset(); createRoleForm.form.reset();
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.addNewRole')}</DialogTitle> <DialogTitle>{t('admin.addNewRole')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4"> <section className="px-6">
<createRoleForm.FormComponent <createRoleForm.FormComponent
onSubmit={createRoleMutation.mutate} onSubmit={createRoleMutation.mutate}
/> />
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -324,18 +324,17 @@ function AdminRoles() {
onOpenChange={setEditRoleDescriptionModalOpen} onOpenChange={setEditRoleDescriptionModalOpen}
> >
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!isEditRoleDescriptionModalOpen) { if (!isEditRoleDescriptionModalOpen) {
setRoleToMakeAction(null); setRoleToMakeAction(null);
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.editRoleDescription')}</DialogTitle> <DialogTitle>{t('admin.editRoleDescription')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4"> <section className="px-6">
<form <form
id={editRoleDescriptionFormId} id={editRoleDescriptionFormId}
onSubmit={(evt) => { onSubmit={(evt) => {
@ -360,7 +359,7 @@ function AdminRoles() {
</form> </form>
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -383,18 +382,17 @@ function AdminRoles() {
{/* Delete role modal */} {/* Delete role modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}> <Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!deleteModalOpen) { if (!deleteModalOpen) {
setRoleToMakeAction(null); setRoleToMakeAction(null);
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.deleteRole')}</DialogTitle> <DialogTitle>{t('admin.deleteRole')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4"> <section className="px-6">
<DialogDescription className="text-text-primary"> <DialogDescription className="text-text-primary">
{t('admin.deleteRoleConfirmation')} {t('admin.deleteRoleConfirmation')}
</DialogDescription> </DialogDescription>
@ -404,7 +402,7 @@ function AdminRoles() {
</div> </div>
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"

View File

@ -68,7 +68,7 @@ function ServiceDetail({ content }: ServiceDetailProps) {
if (typeof content === 'string') { if (typeof content === 'string') {
return ( return (
<div className="rounded-lg p-4 bg-bg-card text-sm text-text-primary"> <div className="rounded-xl p-4 bg-bg-card text-sm text-text-primary">
<pre> <pre>
<code> <code>
{typeof content === 'string' {typeof content === 'string'

View File

@ -34,6 +34,7 @@ import {
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
@ -66,7 +67,9 @@ import {
getSortIcon, getSortIcon,
} from './utils'; } from './utils';
import JsonView from 'react18-json-view';
import ServiceDetail from './service-detail'; import ServiceDetail from './service-detail';
import TaskExecutorDetail from './task-executor-detail';
const columnHelper = createColumnHelper<AdminService.ListServicesItem>(); const columnHelper = createColumnHelper<AdminService.ListServicesItem>();
const globalFilterFn = createFuzzySearchFn<AdminService.ListServicesItem>([ const globalFilterFn = createFuzzySearchFn<AdminService.ListServicesItem>([
@ -381,7 +384,7 @@ function AdminServiceStatus() {
{/* Extra info modal*/} {/* Extra info modal*/}
<Dialog open={extraInfoModalOpen} onOpenChange={setExtraInfoModalOpen}> <Dialog open={extraInfoModalOpen} onOpenChange={setExtraInfoModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button" className="flex flex-col max-h-[calc(100vh-4rem)] p-0 overflow-hidden"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!extraInfoModalOpen) { if (!extraInfoModalOpen) {
setItemToMakeAction(null); setItemToMakeAction(null);
@ -392,15 +395,16 @@ function AdminServiceStatus() {
<DialogTitle>{t('admin.extraInfo')}</DialogTitle> <DialogTitle>{t('admin.extraInfo')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 pt-6 pb-4"> <DialogDescription className="sr-only" />
<div className="rounded-lg p-4 bg-bg-input">
<pre className="text-sm"> <ScrollArea className="h-0 flex-1 grid">
<code> <div className="px-12">
{JSON.stringify(itemToMakeAction?.extra ?? {}, null, 2)} <JsonView
</code> src={itemToMakeAction?.extra ?? {}}
</pre> className="rounded-lg p-4 bg-bg-card break-words text-text-secondary"
/>
</div> </div>
</section> </ScrollArea>
<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
@ -417,7 +421,7 @@ function AdminServiceStatus() {
{/* Service details modal */} {/* Service details modal */}
<Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}> <Dialog open={detailModalOpen} onOpenChange={setDetailModalOpen}>
<DialogContent <DialogContent
className="flex flex-col max-h-[calc(100vh-4rem)] max-w-6xl p-0 border-border-button" className="flex flex-col max-h-[calc(100vh-4rem)] max-w-6xl p-0 overflow-hidden"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!detailModalOpen) { if (!detailModalOpen) {
setItemToMakeAction(null); setItemToMakeAction(null);
@ -426,14 +430,30 @@ function AdminServiceStatus() {
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader className="p-6 border-b-0.5 border-border-button">
<DialogTitle> <DialogTitle>
<Trans i18nKey="admin.serviceDetail"> {itemToMakeAction?.service_type === 'task_executor' ? (
{{ name: itemToMakeAction?.name }} t('admin.taskExecutorDetail')
</Trans> ) : (
<Trans i18nKey="admin.serviceDetail">
{{ name: itemToMakeAction?.name }}
</Trans>
)}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<ScrollArea className="pt-6 pb-4 px-12 h-0 flex-1 text-text-secondary flex flex-col"> <DialogDescription className="sr-only" />
<ServiceDetail content={serviceDetails?.message} />
<ScrollArea className="h-0 flex-1 text-text-secondary grid">
<div className="px-12">
{itemToMakeAction?.service_type === 'task_executor' ? (
<TaskExecutorDetail
content={
serviceDetails?.message as AdminService.TaskExecutorInfo
}
/>
) : (
<ServiceDetail content={serviceDetails?.message} />
)}
</div>
</ScrollArea> </ScrollArea>
<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">

View File

@ -0,0 +1,162 @@
import dayjs from 'dayjs';
import JsonView from 'react18-json-view';
import {
Bar,
BarChart,
CartesianGrid,
Legend,
Rectangle,
ResponsiveContainer,
Tooltip,
XAxis,
} from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { formatDate, formatTime } from '@/utils/date';
interface TaskExecutorDetailProps {
content?: AdminService.TaskExecutorInfo;
}
function CustomTooltip({ active, payload, ...restProps }: any) {
if (active && payload && payload.length) {
const item = payload.at(0)?.payload ?? {};
return (
<Card className="border bg-bg-base overflow-hidden shadow-xl">
<CardHeader className="px-3 py-2 text-text-primary bg-bg-title">
<CardTitle className="text-lg">
{formatDate(restProps.label)}
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<ScrollArea className="px-3">
<div className="max-h-56">
<JsonView
src={item}
displaySize={30}
className="py-2 break-words text-text-secondary"
/>
</div>
</ScrollArea>
</CardContent>
</Card>
);
}
return null;
}
function CustomAxisTick({ x, y, payload }: any) {
return (
<g transform={`translate(${x},${y})`}>
<text
x={0}
y={0}
dy={8}
transform="rotate(60)"
textAnchor="start"
fill="rgb(var(--text-secondary))"
>
{formatTime(payload.value)}
</text>
</g>
);
}
function TaskExecutorDetail({ content }: TaskExecutorDetailProps) {
return (
<section className="space-y-8">
{Object.entries(content ?? {}).map(([name, data]) => {
const items = data.map((x) => ({
...x,
done: Math.floor(Math.random() * 100),
failed: Math.floor(Math.random() * 100),
now: dayjs(x.now).valueOf(),
}));
const lastItem = items.at(-1);
return (
<Card key={name} className="border-0">
<CardHeader className="text-text-primary">
<CardTitle>{name}</CardTitle>
<div className="text-sm text-text-secondary space-x-4">
<span>Lag: {lastItem?.lag}</span>
<span>Pending: {lastItem?.pending}</span>
</div>
</CardHeader>
<CardContent>
<ResponsiveContainer className="min-h-40 w-full">
<BarChart
data={items}
margin={{ bottom: 32 }}
className="text-sm text-text-secondary"
>
<XAxis
dataKey="now"
type="number"
scale="time"
domain={['dataMin', 'dataMax']}
tick={CustomAxisTick}
interval="equidistantPreserveStart"
angle={60}
minTickGap={16}
allowDataOverflow
padding={{ left: 24, right: 24 }}
/>
{/* <YAxis
type="number"
tick={{ fill: 'rgb(var(--text-secondary))' }}
/> */}
<CartesianGrid strokeDasharray="3 3" />
<Tooltip
trigger="click"
content={CustomTooltip}
wrapperStyle={{
pointerEvents: 'auto',
}}
/>
<Legend wrapperStyle={{ bottom: 0 }} />
<Bar
dataKey="done"
fill="rgb(var(--state-success))"
activeBar={
<Rectangle
fill="rgb(var(--state-success))"
stroke="var(--bg-base)"
/>
}
/>
<Bar
dataKey="failed"
fill="rgb(var(--state-error))"
activeBar={
<Rectangle
fill="rgb(var(--state-error))"
stroke="var(--bg-base)"
/>
}
/>
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
);
})}
</section>
);
}
export default TaskExecutorDetail;

View File

@ -91,6 +91,7 @@ import {
parseBooleanish, parseBooleanish,
} from './utils'; } from './utils';
import { DialogDescription } from '@radix-ui/react-dialog';
import EnterpriseFeature from './components/enterprise-feature'; import EnterpriseFeature from './components/enterprise-feature';
const columnHelper = createColumnHelper<AdminService.ListUsersItem>(); const columnHelper = createColumnHelper<AdminService.ListUsersItem>();
@ -210,8 +211,8 @@ function AdminUserManagement() {
columnHelper.accessor('nickname', { columnHelper.accessor('nickname', {
header: t('admin.nickname'), header: t('admin.nickname'),
cell: ({ row, cell }) => ( cell: ({ row, cell }) => (
<div className="flex items-center gap-2"> <div className="flex items-center">
<span>{cell.getValue()}</span> <span className="mr-2 empty:hidden">{cell.getValue()}</span>
{row.original.is_superuser ? ( {row.original.is_superuser ? (
<Badge variant="secondary">{t('admin.superuser')}</Badge> <Badge variant="secondary">{t('admin.superuser')}</Badge>
@ -553,20 +554,22 @@ function AdminUserManagement() {
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}> <Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent className="p-0 border-border-button"> <DialogContent>
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.deleteUser')}</DialogTitle> <DialogTitle>{t('admin.deleteUser')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4 text-text-primary text-sm"> <section className="px-6">
{t('admin.deleteUserConfirmation')} <DialogDescription>
{t('admin.deleteUserConfirmation')}
</DialogDescription>
<div className="rounded-lg mt-6 p-4 border-0.5 border-border-button"> <div className="rounded-lg mt-6 p-4 border-0.5 border-border-button">
{userToMakeAction?.email} {userToMakeAction?.email}
</div> </div>
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -595,18 +598,17 @@ function AdminUserManagement() {
{/* Change Password Modal */} {/* Change Password Modal */}
<Dialog open={passwordModalOpen} onOpenChange={setPasswordModalOpen}> <Dialog open={passwordModalOpen} onOpenChange={setPasswordModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!passwordModalOpen) { if (!passwordModalOpen) {
changePasswordForm.form.reset(); changePasswordForm.form.reset();
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.changePassword')}</DialogTitle> <DialogTitle>{t('admin.changePassword')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4 text-text-secondary"> <section className="px-6">
<changePasswordForm.FormComponent <changePasswordForm.FormComponent
key="changePasswordForm" key="changePasswordForm"
email={userToMakeAction?.email || ''} email={userToMakeAction?.email || ''}
@ -621,7 +623,7 @@ function AdminUserManagement() {
/> />
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -656,19 +658,19 @@ function AdminUserManagement() {
createUserForm.form.reset(); createUserForm.form.reset();
}} }}
> >
<DialogContent className="p-0 border-border-button"> <DialogContent>
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.createNewUser')}</DialogTitle> <DialogTitle>{t('admin.createNewUser')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4"> <section className="px-6">
<createUserForm.FormComponent <createUserForm.FormComponent
id={createUserForm.id} id={createUserForm.id}
onSubmit={createUserMutation.mutate} onSubmit={createUserMutation.mutate}
/> />
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"

View File

@ -349,18 +349,17 @@ function AdminWhitelist() {
{/* Delete Confirmation Modal */} {/* Delete Confirmation Modal */}
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}> <Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!deleteModalOpen) { if (!deleteModalOpen) {
setItemToMakeAction(null); setItemToMakeAction(null);
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.deleteEmail')}</DialogTitle> <DialogTitle>{t('admin.deleteEmail')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4"> <section className="px-6">
<DialogDescription className="text-text-primary"> <DialogDescription className="text-text-primary">
{t('admin.deleteWhitelistEmailConfirmation')} {t('admin.deleteWhitelistEmailConfirmation')}
</DialogDescription> </DialogDescription>
@ -370,7 +369,7 @@ function AdminWhitelist() {
</div> </div>
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -402,25 +401,24 @@ function AdminWhitelist() {
{/* Create Email Modal */} {/* Create Email Modal */}
<Dialog open={createModalOpen} onOpenChange={setCreateModalOpen}> <Dialog open={createModalOpen} onOpenChange={setCreateModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!createModalOpen) { if (!createModalOpen) {
createEmailForm.form.reset(); createEmailForm.form.reset();
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.createEmail')}</DialogTitle> <DialogTitle>{t('admin.createEmail')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4 text-text-secondary"> <section className="px-6">
<createEmailForm.FormComponent <createEmailForm.FormComponent
id={createEmailForm.id} id={createEmailForm.id}
onSubmit={createWhitelistEntryMutation.mutate} onSubmit={createWhitelistEntryMutation.mutate}
/> />
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -447,7 +445,6 @@ function AdminWhitelist() {
{/* Edit Email Modal */} {/* Edit Email Modal */}
<Dialog open={editModalOpen} onOpenChange={setEditModalOpen}> <Dialog open={editModalOpen} onOpenChange={setEditModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!editModalOpen) { if (!editModalOpen) {
setItemToMakeAction(null); setItemToMakeAction(null);
@ -455,11 +452,11 @@ function AdminWhitelist() {
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.editEmail')}</DialogTitle> <DialogTitle>{t('admin.editEmail')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4 text-text-secondary"> <section className="px-6">
<editEmailForm.FormComponent <editEmailForm.FormComponent
id={editEmailForm.id} id={editEmailForm.id}
onSubmit={(value) => { onSubmit={(value) => {
@ -473,7 +470,7 @@ function AdminWhitelist() {
/> />
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"
@ -500,25 +497,24 @@ function AdminWhitelist() {
{/* Import Excel Modal */} {/* Import Excel Modal */}
<Dialog open={importModalOpen} onOpenChange={setImportModalOpen}> <Dialog open={importModalOpen} onOpenChange={setImportModalOpen}>
<DialogContent <DialogContent
className="p-0 border-border-button"
onAnimationEnd={() => { onAnimationEnd={() => {
if (!importModalOpen) { if (!importModalOpen) {
importExcelForm.form.reset(); importExcelForm.form.reset();
} }
}} }}
> >
<DialogHeader className="p-6 border-b-0.5 border-border-button"> <DialogHeader>
<DialogTitle>{t('admin.importWhitelist')}</DialogTitle> <DialogTitle>{t('admin.importWhitelist')}</DialogTitle>
</DialogHeader> </DialogHeader>
<section className="px-12 py-4 text-text-secondary"> <section className="px-6">
<importExcelForm.FormComponent <importExcelForm.FormComponent
id={importExcelForm.id} id={importExcelForm.id}
onSubmit={importExcelMutation.mutate} onSubmit={importExcelMutation.mutate}
/> />
</section> </section>
<DialogFooter className="flex justify-end gap-4 px-12 pt-4 pb-8"> <DialogFooter className="gap-4 px-6 py-4">
<Button <Button
className="px-4 h-10 dark:border-border-button" className="px-4 h-10 dark:border-border-button"
variant="outline" variant="outline"

View File

@ -101,8 +101,6 @@ request.interceptors.response.use(
); );
const { const {
getSystemVersion: _getSystemVersion,
adminLogin, adminLogin,
adminLogout, adminLogout,
adminListUsers, adminListUsers,
@ -136,6 +134,8 @@ const {
adminUpdateWhitelistEntry, adminUpdateWhitelistEntry,
adminDeleteWhitelistEntry, adminDeleteWhitelistEntry,
adminImportWhitelist, adminImportWhitelist,
adminGetSystemVersion,
} = api; } = api;
type ResponseData<D = NonNullable<unknown>> = { type ResponseData<D = NonNullable<unknown>> = {
@ -260,4 +260,4 @@ export const importWhitelistFromExcel = (file: File) => {
}; };
export const getSystemVersion = () => export const getSystemVersion = () =>
request.get<ResponseData<string>>(_getSystemVersion); request.get<ResponseData<{ version: string }>>(adminGetSystemVersion);

View File

@ -66,6 +66,21 @@ declare module AdminService {
title: string; title: string;
}; };
export type TaskExectorHeartbeatItem = {
name: string;
boot_at: string;
now: string;
ip_address: string;
current: Record<string, object>;
done: number;
failed: number;
lag: number;
pending: number;
pid: number;
};
export type TaskExecutorInfo = Record<string, TaskExectorHeartbeatItem[]>;
export type ListServicesItem = { export type ListServicesItem = {
extra: Record<string, unknown>; extra: Record<string, unknown>;
host: string; host: string;
@ -76,11 +91,17 @@ declare module AdminService {
status: 'alive' | 'timeout' | 'fail'; status: 'alive' | 'timeout' | 'fail';
}; };
export type ServiceDetail = { export type ServiceDetail =
service_name: string; | {
status: 'alive' | 'timeout'; service_name: string;
message: string | Record<string, any> | Record<string, any>[]; status: 'alive' | 'timeout';
}; message: string | Record<string, any> | Record<string, any>[];
}
| {
service_name: 'task_executor';
status: 'alive' | 'timeout';
message: AdminService.TaskExecutorInfo;
};
export type PermissionData = { export type PermissionData = {
enable: boolean; enable: boolean;

View File

@ -271,4 +271,6 @@ export default {
adminDeleteWhitelistEntry: (email: string) => adminDeleteWhitelistEntry: (email: string) =>
`${ExternalApi}${api_host}/admin/whitelist/${email}`, `${ExternalApi}${api_host}/admin/whitelist/${email}`,
adminImportWhitelist: `${ExternalApi}${api_host}/admin/whitelist/batch`, adminImportWhitelist: `${ExternalApi}${api_host}/admin/whitelist/batch`,
adminGetSystemVersion: `${ExternalApi}${api_host}/admin/version`,
}; };

View File

@ -81,7 +81,15 @@ module.exports = {
'text-primary': { 'text-primary': {
DEFAULT: 'rgb(var(--text-primary) / <alpha-value>)', DEFAULT: 'rgb(var(--text-primary) / <alpha-value>)',
}, },
'text-secondary': 'var(--text-secondary)', 'text-primary-inverse': {
DEFAULT: 'rgb(var(--text-primary-inverse) / <alpha-value>)',
},
'text-secondary': {
DEFAULT: 'rgb(var(--text-secondary) / <alpha-value>)',
},
'text-secondary-inverse': {
DEFAULT: 'rgb(var(--text-secondary-inverse) / <alpha-value>)',
},
'text-disabled': 'var(--text-disabled)', 'text-disabled': 'var(--text-disabled)',
'text-input-tip': 'var(--text-input-tip)', 'text-input-tip': 'var(--text-input-tip)',
'border-default': 'var(--border-default)', 'border-default': 'var(--border-default)',

View File

@ -102,7 +102,7 @@
/* #161618 */ /* #161618 */
--text-primary: 22 22 24; --text-primary: 22 22 24;
--text-secondary: #75787a; --text-secondary: 117 120 122;
--text-disabled: #b2b5b7; --text-disabled: #b2b5b7;
/* input placeholder color */ /* input placeholder color */
--text-input-tip: #b2b5b7; --text-input-tip: #b2b5b7;
@ -128,6 +128,10 @@
--bg-group: rgba(90, 183, 126, 0.1); --bg-group: rgba(90, 183, 126, 0.1);
--bg-member: rgba(92, 150, 200, 0.1); --bg-member: rgba(92, 150, 200, 0.1);
--bg-department: rgba(136, 102, 211, 0.1); --bg-department: rgba(136, 102, 211, 0.1);
/* --*-inverse: colors in dark mode */
--text-primary-inverse: 246 246 247;
--text-secondary-inverse: 178 181 183;
} }
.dark { .dark {
@ -236,12 +240,16 @@
/* #f6f6f7 */ /* #f6f6f7 */
--text-primary: 246 246 247; --text-primary: 246 246 247;
--text-secondary: #b2b5b7; --text-secondary: 178 181 183;
--text-disabled: #75787a; --text-disabled: #75787a;
--text-input-tip: #75787a; --text-input-tip: #75787a;
--border-default: rgba(255, 255, 255, 0.2); --border-default: rgba(255, 255, 255, 0.2);
--border-accent: #ffffff; --border-accent: #ffffff;
--border-button: rgba(255, 255, 255, 0.1); --border-button: rgba(255, 255, 255, 0.1);
/* *-inverse: colors in light mode */
--text-primary-inverse: 22 22 24;
--text-secondary-inverse: 117 120 122;
} }
} }