feat: events queue page + restructure under 系统运维
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 7s
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 7s
This commit is contained in:
parent
31fa5d836a
commit
af76de4ff7
@ -99,6 +99,10 @@ function App() {
|
|||||||
<Suspense fallback={<PageLoading />}><GiteaEmbed /></Suspense>
|
<Suspense fallback={<PageLoading />}><GiteaEmbed /></Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="events"
|
||||||
|
element={<PermissionGuard requiredRole="SUPER_ADMIN"><Suspense fallback={<PageLoading />}><EventsPage /></Suspense></PermissionGuard>}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="servers"
|
path="servers"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@ -29,7 +29,10 @@ export const adminMenuItems: AdminMenuItem[] = [
|
|||||||
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
{ path: '/audit', name: '审计日志', icon: <SafetyOutlined />, requiredRole: 'ADMIN' },
|
||||||
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
{ path: '/billing', name: 'API 用量', icon: <DollarOutlined />, requiredRole: 'SUPER_ADMIN' },
|
||||||
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> },
|
{ path: '/git', name: '代码仓库', icon: <CodeOutlined /> },
|
||||||
{ path: '/servers', name: '服务器运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN' },
|
{ path: '/ops', name: '系统运维', icon: <CloudServerOutlined />, requiredRole: 'SUPER_ADMIN', children: [
|
||||||
|
{ path: '/servers', name: '服务器' },
|
||||||
|
{ path: '/events', name: '事件队列' },
|
||||||
|
]},
|
||||||
{ path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' },
|
{ path: '/settings', name: '系统配置', icon: <SettingOutlined />, requiredRole: 'ADMIN' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ const breadcrumbMap: Record<string, string> = {
|
|||||||
'/billing': 'API 用量',
|
'/billing': 'API 用量',
|
||||||
'/git': '代码仓库',
|
'/git': '代码仓库',
|
||||||
'/servers': '服务器运维',
|
'/servers': '服务器运维',
|
||||||
|
'/events': '事件队列',
|
||||||
'/audit': '审计日志',
|
'/audit': '审计日志',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
71
src/pages/Events.tsx
Normal file
71
src/pages/Events.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Table, Tag, Button, Typography, App, Space, Badge } from 'antd'
|
||||||
|
import { ReloadOutlined, RetweetOutlined, CloudServerOutlined } from '@ant-design/icons'
|
||||||
|
import { getQueueOverview, getFailedJobs, retryJob } from '@/services/events-api'
|
||||||
|
|
||||||
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
|
function EventsPage() {
|
||||||
|
const { message } = App.useApp()
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const [selectedQueue, setSelectedQueue] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const { data: overview } = useQuery({ queryKey: ['events', 'overview'], queryFn: getQueueOverview, staleTime: 10_000 })
|
||||||
|
const { data: failed } = useQuery({
|
||||||
|
queryKey: ['events', 'failed', selectedQueue],
|
||||||
|
queryFn: () => selectedQueue ? getFailedJobs(selectedQueue) : null,
|
||||||
|
enabled: !!selectedQueue,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRetry = async (queue: string, jobId: string) => {
|
||||||
|
await retryJob(queue, jobId)
|
||||||
|
message.success('已重试')
|
||||||
|
qc.invalidateQueries({ queryKey: ['events'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const overviewColumns = [
|
||||||
|
{ title: '队列', dataIndex: 'name', width: 160 },
|
||||||
|
{ title: '总计', dataIndex: 'total', width: 60, align: 'center' as const },
|
||||||
|
{ title: '等待', dataIndex: 'waiting', width: 60, align: 'center' as const, render: (v: number) => <Badge count={v} showZero color="blue" /> },
|
||||||
|
{ title: '进行中', dataIndex: 'active', width: 70, align: 'center' as const, render: (v: number) => <Badge count={v} showZero color="processing" /> },
|
||||||
|
{ title: '完成', dataIndex: 'completed', width: 60, align: 'center' as const, render: (v: number) => <Text type="success">{v}</Text> },
|
||||||
|
{ title: '失败', dataIndex: 'failed', width: 60, align: 'center' as const, render: (v: number, r: any) => (
|
||||||
|
v > 0 ? <Button type="link" size="small" danger onClick={() => setSelectedQueue(r.name)}>{v}</Button> : <Text type="secondary">0</Text>
|
||||||
|
)},
|
||||||
|
{ title: '延迟', dataIndex: 'delayed', width: 60, align: 'center' as const },
|
||||||
|
]
|
||||||
|
|
||||||
|
const failedColumns = [
|
||||||
|
{ title: 'Job ID', dataIndex: 'id', width: 160, ellipsis: true },
|
||||||
|
{ title: '名称', dataIndex: 'name', width: 150 },
|
||||||
|
{ title: '重试', dataIndex: 'attemptsMade', width: 60, align: 'center' as const },
|
||||||
|
{ title: '失败原因', dataIndex: 'failedReason', ellipsis: true },
|
||||||
|
{ title: '操作', width: 80, render: (_: any, r: any) => (
|
||||||
|
<Button type="link" size="small" icon={<RetweetOutlined />} onClick={() => handleRetry(selectedQueue!, r.id)}>重试</Button>
|
||||||
|
)},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Title level={5} style={{ margin: 0 }}><CloudServerOutlined /> 事件队列</Title>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['events'] })}>刷新</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table dataSource={overview?.queues || []} columns={overviewColumns} rowKey="name" pagination={false} style={{ marginBottom: 24 }} />
|
||||||
|
|
||||||
|
{selectedQueue && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||||
|
<Title level={5} style={{ margin: 0, fontSize: 14 }}>失败任务 · {selectedQueue}</Title>
|
||||||
|
<Button size="small" onClick={() => setSelectedQueue(null)}>关闭</Button>
|
||||||
|
</div>
|
||||||
|
<Table dataSource={failed?.jobs || []} columns={failedColumns} rowKey="id" pagination={false} size="small" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventsPage
|
||||||
10
src/services/events-api.ts
Normal file
10
src/services/events-api.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { api } from './http-client'
|
||||||
|
|
||||||
|
export interface QueueInfo { name: string; waiting: number; active: number; completed: number; failed: number; delayed: number; total: number }
|
||||||
|
export interface FailedJob { id: string; name: string; timestamp: number; attemptsMade: number; failedReason?: string }
|
||||||
|
export interface JobDetail extends FailedJob { state: string; data: any; stacktrace?: string[] }
|
||||||
|
|
||||||
|
export function getQueueOverview(): Promise<{ queues: QueueInfo[] }> { return api.get('/admin-api/events') }
|
||||||
|
export function getFailedJobs(queue: string): Promise<{ jobs: FailedJob[] }> { return api.get(`/admin-api/events/${queue}/failed`) }
|
||||||
|
export function getJobDetail(queue: string, jobId: string): Promise<JobDetail> { return api.get(`/admin-api/events/${queue}/jobs/${jobId}`) }
|
||||||
|
export function retryJob(queue: string, jobId: string): Promise<any> { return api.post(`/admin-api/events/${queue}/jobs/${jobId}/retry`) }
|
||||||
Loading…
x
Reference in New Issue
Block a user