mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 07:06:36 +08:00
perf(frontend): split admin bundles and tighten data fetching
Add next/dynamic lazy loading for database, logs, and settings heavy modules; migrate access-logs and task-executions to React Query; parallelize login auth-sources with public config; consolidate users table tooltips; enable lazy image decoding; and replace barrel @/lib/services imports with direct module paths.
This commit is contained in:
@@ -20,7 +20,7 @@ import {useCallback, useEffect, useState} from "react"
|
||||
import {BarChart3, Globe, RefreshCw, TrendingUp, Users, XCircle} from "lucide-react"
|
||||
import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from "recharts"
|
||||
|
||||
import {AdminService} from "@/lib/services"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
|
||||
@@ -16,12 +16,13 @@ limitations under the License.
|
||||
|
||||
"use client"
|
||||
|
||||
import {useCallback, useEffect, useState} from "react"
|
||||
import {useMemo, useState} from "react"
|
||||
import {useQuery} from "@tanstack/react-query"
|
||||
import {toast} from "sonner"
|
||||
import {format} from "date-fns"
|
||||
import {Activity, ChevronLeft, ChevronRight, Copy, Eye, RotateCcw, Search, XCircle} from "lucide-react"
|
||||
|
||||
import {AdminService} from "@/lib/services"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
@@ -47,6 +48,8 @@ interface AccessLog {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 15
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
if (!value) return "-"
|
||||
const date = new Date(value)
|
||||
@@ -66,15 +69,13 @@ function statusVariant(status: number) {
|
||||
return "secondary"
|
||||
}
|
||||
|
||||
export function AccessLogs() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [clickhouseDisabled, setClickhouseDisabled] = useState(false)
|
||||
function isClickhouseDisabledError(error: Error) {
|
||||
const errMsg = error.message || ""
|
||||
return errMsg.includes("ClickHouse") || errMsg.includes("未启用")
|
||||
}
|
||||
|
||||
const [logs, setLogs] = useState<AccessLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
export function AccessLogs() {
|
||||
const [page, setPage] = useState(1)
|
||||
const pageSize = 15
|
||||
|
||||
// 搜索过滤条件
|
||||
const [usernameFilter, setUsernameFilter] = useState("")
|
||||
@@ -93,12 +94,9 @@ export function AccessLogs() {
|
||||
const [selectedLog, setSelectedLog] = useState<AccessLog | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
|
||||
const fetchAccessLogs = useCallback(async (currentPage: number = page) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
// 转换起止时间格式
|
||||
const logsQuery = useQuery({
|
||||
queryKey: ["admin", "access-logs", page, searchParams],
|
||||
queryFn: async () => {
|
||||
let startISO = ""
|
||||
if (searchParams.start_time) {
|
||||
startISO = new Date(searchParams.start_time).toISOString()
|
||||
@@ -108,34 +106,25 @@ export function AccessLogs() {
|
||||
endISO = new Date(searchParams.end_time).toISOString()
|
||||
}
|
||||
|
||||
const data = await AdminService.getAccessLogs({
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
return AdminService.getAccessLogs({
|
||||
page,
|
||||
page_size: PAGE_SIZE,
|
||||
username: searchParams.username || undefined,
|
||||
path: searchParams.path || undefined,
|
||||
start_time: startISO || undefined,
|
||||
end_time: endISO || undefined,
|
||||
})
|
||||
},
|
||||
retry: false,
|
||||
})
|
||||
|
||||
setLogs(data.list || [])
|
||||
setTotal(data.total || 0)
|
||||
setClickhouseDisabled(false)
|
||||
} catch (err) {
|
||||
const errorInstance = err instanceof Error ? err : new Error("获取访问日志失败")
|
||||
const errMsg = errorInstance.message || ""
|
||||
if (errMsg.includes("ClickHouse") || errMsg.includes("未启用")) {
|
||||
setClickhouseDisabled(true)
|
||||
} else {
|
||||
setError(errorInstance)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [page, searchParams])
|
||||
const logs = logsQuery.data?.list ?? []
|
||||
const total = logsQuery.data?.total ?? 0
|
||||
const loading = logsQuery.isPending || logsQuery.isFetching
|
||||
const clickhouseDisabled = logsQuery.isError && isClickhouseDisabledError(logsQuery.error)
|
||||
const error = logsQuery.isError && !clickhouseDisabled ? logsQuery.error : null
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccessLogs()
|
||||
}, [fetchAccessLogs])
|
||||
const totalPages = useMemo(() => Math.max(1, Math.ceil(total / PAGE_SIZE)), [total])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -164,7 +153,6 @@ export function AccessLogs() {
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
setPage(newPage)
|
||||
fetchAccessLogs(newPage)
|
||||
}
|
||||
|
||||
const copyToClipboard = (text: string, subject: string) => {
|
||||
@@ -172,8 +160,6 @@ export function AccessLogs() {
|
||||
toast.success(`${subject}已复制到剪贴板`)
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / pageSize))
|
||||
|
||||
// Prettify Headers JSON string
|
||||
const getPrettyHeaders = (headersRaw?: string) => {
|
||||
if (!headersRaw) return "暂无头部数据"
|
||||
@@ -198,7 +184,7 @@ export function AccessLogs() {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorInline error={error} onRetry={() => fetchAccessLogs(1)} />
|
||||
return <ErrorInline error={error} onRetry={() => void logsQuery.refetch()} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -465,4 +451,4 @@ export function AccessLogs() {
|
||||
</Sheet>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import {useVirtualizer} from "@tanstack/react-virtual"
|
||||
import {toast} from "sonner"
|
||||
import {ArrowDown, ChevronUp, Loader2, Pause, Play} from "lucide-react"
|
||||
|
||||
import {AdminService} from "@/lib/services"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import type {CacheStatus} from "@/lib/services"
|
||||
import {AdminService} from "@/lib/services"
|
||||
import type {CacheStatus} from "@/lib/services/admin"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
|
||||
/**
|
||||
* 格式化字节大小
|
||||
|
||||
@@ -11,8 +11,8 @@ import {ArrowLeft, Play, RefreshCw, Terminal, Trash2,} from "lucide-react"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
|
||||
import type {ExecuteSQLResponse} from "@/lib/services"
|
||||
import services from "@/lib/services"
|
||||
import type {ExecuteSQLResponse} from "@/lib/services/db-manage"
|
||||
import {DbManageService} from "@/lib/services/db-manage"
|
||||
|
||||
/**
|
||||
* 格式化表格单元格内容
|
||||
@@ -81,7 +81,7 @@ export function SQLConsole({ dbType, onClose }: SQLConsoleProps) {
|
||||
setSqlResult(null)
|
||||
setSqlError(null)
|
||||
try {
|
||||
const result = await services.dbManage.executeSQL(sqlQuery)
|
||||
const result = await DbManageService.executeSQL(sqlQuery)
|
||||
setSqlResult(result)
|
||||
toast.success("SQL 执行成功")
|
||||
} catch (err) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import {Skeleton} from "@/components/ui/skeleton"
|
||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
|
||||
import {Button} from "@/components/ui/button"
|
||||
import type {TableDataResponse} from "@/lib/services"
|
||||
import services from "@/lib/services"
|
||||
import type {TableDataResponse} from "@/lib/services/db-manage"
|
||||
import {DbManageService} from "@/lib/services/db-manage"
|
||||
|
||||
/**
|
||||
* 格式化数字,每3位加逗号
|
||||
@@ -55,7 +55,7 @@ export function TableBrowser({ tables, loadingTables, refreshTrigger }: TableBro
|
||||
if (!tableName) return
|
||||
setLoadingData(true)
|
||||
try {
|
||||
const data = await services.dbManage.getTableData({
|
||||
const data = await DbManageService.getTableData({
|
||||
table: tableName,
|
||||
page: targetPage,
|
||||
pageSize: size,
|
||||
|
||||
@@ -292,6 +292,8 @@ export function FileList() {
|
||||
<img
|
||||
src={getFileUrl(file.id, "low") ?? undefined}
|
||||
alt={file.file_name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="size-full object-cover"
|
||||
onError={(e) => {
|
||||
;(e.currentTarget as HTMLImageElement).style.display = "none"
|
||||
@@ -408,6 +410,8 @@ export function FileList() {
|
||||
<img
|
||||
src={getFileUrl(detailTarget.id, "low") ?? undefined}
|
||||
alt={detailTarget.file_name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="max-h-full max-w-full object-contain rounded-lg shadow-sm"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -33,7 +33,8 @@ import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
|
||||
|
||||
import services from "@/lib/services"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
import {PushService} from "@/lib/services/push"
|
||||
import type {CreatePushEventRequest, PushEvent, UpdatePushEventRequest,} from "@/lib/services/push"
|
||||
|
||||
export function EventsTab() {
|
||||
@@ -42,7 +43,7 @@ export function EventsTab() {
|
||||
// --- 获取所有自定义消息通道 ---
|
||||
const channelsQuery = useQuery({
|
||||
queryKey: ["admin", "push-channels"],
|
||||
queryFn: () => services.push.listChannels(),
|
||||
queryFn: () => PushService.listChannels(),
|
||||
})
|
||||
|
||||
const availableChannels = React.useMemo(() => {
|
||||
@@ -55,23 +56,23 @@ export function EventsTab() {
|
||||
// --- 获取通知事件 ---
|
||||
const eventsQuery = useQuery({
|
||||
queryKey: ["admin", "push-events"],
|
||||
queryFn: () => services.push.listEvents(),
|
||||
queryFn: () => PushService.listEvents(),
|
||||
})
|
||||
|
||||
const builtInEventsQuery = useQuery({
|
||||
queryKey: ["admin", "push-builtin-events"],
|
||||
queryFn: () => services.push.listBuiltInEvents(),
|
||||
queryFn: () => PushService.listBuiltInEvents(),
|
||||
})
|
||||
|
||||
// --- 获取所有系统可调度任务类型 ---
|
||||
const taskTypesQuery = useQuery({
|
||||
queryKey: ["admin", "task-types"],
|
||||
queryFn: () => services.admin.getTaskTypes(),
|
||||
queryFn: () => AdminService.getTaskTypes(),
|
||||
})
|
||||
|
||||
// --- 修改保存事件 Mutation ---
|
||||
const updateEventMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => services.push.updateEvent(id, data),
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdatePushEventRequest }) => PushService.updateEvent(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success("事件更新成功")
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] })
|
||||
@@ -83,7 +84,7 @@ export function EventsTab() {
|
||||
})
|
||||
|
||||
const toggleEventMutation = useMutation({
|
||||
mutationFn: (id: number) => services.push.toggleEvent(id),
|
||||
mutationFn: (id: number) => PushService.toggleEvent(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] })
|
||||
},
|
||||
@@ -93,7 +94,7 @@ export function EventsTab() {
|
||||
})
|
||||
|
||||
const createEventMutation = useMutation({
|
||||
mutationFn: (data: CreatePushEventRequest) => services.push.createEvent(data),
|
||||
mutationFn: (data: CreatePushEventRequest) => PushService.createEvent(data),
|
||||
onSuccess: () => {
|
||||
toast.success("事件创建成功")
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] })
|
||||
@@ -110,7 +111,7 @@ export function EventsTab() {
|
||||
})
|
||||
|
||||
const deleteEventMutation = useMutation({
|
||||
mutationFn: (id: number) => services.push.deleteEvent(id),
|
||||
mutationFn: (id: number) => PushService.deleteEvent(id),
|
||||
onSuccess: () => {
|
||||
toast.success("配置删除成功")
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-events"] })
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
import services from "@/lib/services"
|
||||
import {PushService} from "@/lib/services/push"
|
||||
import type {PushHistory} from "@/lib/services/push"
|
||||
|
||||
function getLevelBadgeVariant(level: string): "outline" | "secondary" | "destructive" | "default" {
|
||||
@@ -56,7 +56,7 @@ export function HistoriesTab() {
|
||||
const historiesQuery = useQuery({
|
||||
queryKey: ["admin", "push-histories", historyPage, historySearch, historyStatus],
|
||||
queryFn: () =>
|
||||
services.push.listHistories({
|
||||
PushService.listHistories({
|
||||
page: historyPage,
|
||||
page_size: 10,
|
||||
event_key: historySearch || undefined,
|
||||
|
||||
@@ -29,7 +29,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
|
||||
import services from "@/lib/services"
|
||||
import {PushService} from "@/lib/services/push"
|
||||
import type {ChannelDefinition, CreateChannelRequest, PushChannel, UpdateChannelRequest} from "@/lib/services/push"
|
||||
|
||||
export function SettingsTab() {
|
||||
@@ -38,19 +38,19 @@ export function SettingsTab() {
|
||||
// --- 获取所有自定义消息通道 ---
|
||||
const channelsQuery = useQuery({
|
||||
queryKey: ["admin", "push-channels"],
|
||||
queryFn: () => services.push.listChannels(),
|
||||
queryFn: () => PushService.listChannels(),
|
||||
})
|
||||
|
||||
// --- 获取动态通道表单字段定义 ---
|
||||
const definitionsQuery = useQuery({
|
||||
queryKey: ["admin", "push-channels-definitions"],
|
||||
queryFn: () => services.push.listChannelDefinitions(),
|
||||
queryFn: () => PushService.listChannelDefinitions(),
|
||||
})
|
||||
|
||||
|
||||
// --- 消息通道 CRUD Mutations ---
|
||||
const createChannelMutation = useMutation({
|
||||
mutationFn: (data: CreateChannelRequest) => services.push.createChannel(data),
|
||||
mutationFn: (data: CreateChannelRequest) => PushService.createChannel(data),
|
||||
onSuccess: () => {
|
||||
toast.success("通道创建成功")
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-channels"] })
|
||||
@@ -63,7 +63,7 @@ export function SettingsTab() {
|
||||
|
||||
const updateChannelMutation = useMutation({
|
||||
mutationFn: ({ id, data }: { id: number; data: UpdateChannelRequest }) =>
|
||||
services.push.updateChannel(id, data),
|
||||
PushService.updateChannel(id, data),
|
||||
onSuccess: () => {
|
||||
toast.success("通道更新成功")
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-channels"] })
|
||||
@@ -75,7 +75,7 @@ export function SettingsTab() {
|
||||
})
|
||||
|
||||
const deleteChannelMutation = useMutation({
|
||||
mutationFn: (id: number) => services.push.deleteChannel(id),
|
||||
mutationFn: (id: number) => PushService.deleteChannel(id),
|
||||
onSuccess: () => {
|
||||
toast.success("通道删除成功")
|
||||
queryClient.invalidateQueries({ queryKey: ["admin", "push-channels"] })
|
||||
@@ -237,7 +237,7 @@ export function SettingsTab() {
|
||||
try {
|
||||
setIsTestingChannel(true)
|
||||
toast.info("正在发送测试推送...")
|
||||
await services.push.testChannel({
|
||||
await PushService.testChannel({
|
||||
name: testChannelName,
|
||||
target: testChannelTarget || undefined,
|
||||
})
|
||||
|
||||
@@ -11,8 +11,8 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Progress} from "@/components/ui/progress"
|
||||
import {Skeleton} from "@/components/ui/skeleton"
|
||||
import type {SystemStatus} from "@/lib/services"
|
||||
import services from "@/lib/services"
|
||||
import type {SystemStatus} from "@/lib/services/admin"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
|
||||
/**
|
||||
* 格式化数字,每3位加逗号
|
||||
@@ -40,7 +40,7 @@ export function SystemStatusManager() {
|
||||
const fetchStatus = useCallback(async (isSilent = false) => {
|
||||
if (!isSilent) setWavelet(true)
|
||||
try {
|
||||
const data = await services.admin.getSystemStatus()
|
||||
const data = await AdminService.getSystemStatus()
|
||||
|
||||
// 检测变化的字段用于微动画效果
|
||||
if (prevStatusRef.current) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import {useCallback, useEffect, useState} from "react"
|
||||
import {useMemo, useState} from "react"
|
||||
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query"
|
||||
import {toast} from "sonner"
|
||||
import {format} from "date-fns"
|
||||
import {Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw} from "lucide-react"
|
||||
|
||||
import {AdminService, TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services"
|
||||
import {AdminService} from "@/lib/services/admin"
|
||||
import type {TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services/admin"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
import {EmptyStateWithBorder} from "@/components/layout/empty"
|
||||
@@ -31,6 +33,8 @@ const TRIGGER_LABELS: Record<string, string> = {
|
||||
schedule: "定时",
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 10
|
||||
|
||||
function formatDateTime(value?: string | null) {
|
||||
if (!value) return "-"
|
||||
const date = new Date(value)
|
||||
@@ -51,108 +55,85 @@ function statusVariant(status: TaskExecutionStatus) {
|
||||
}
|
||||
|
||||
export function TaskExecutionsManager() {
|
||||
const [taskTypes, setTaskTypes] = useState<TaskMeta[]>([])
|
||||
const [executionsLoading, setExecutionsLoading] = useState(false)
|
||||
const [executionsError, setExecutionsError] = useState<Error | null>(null)
|
||||
const [executions, setExecutions] = useState<TaskExecution[]>([])
|
||||
const [executionsTotal, setExecutionsTotal] = useState(0)
|
||||
const queryClient = useQueryClient()
|
||||
const [executionsPage, setExecutionsPage] = useState(1)
|
||||
const [executionStatus, setExecutionStatus] = useState<TaskExecutionStatus | "all">("all")
|
||||
const [executionTaskType, setExecutionTaskType] = useState<string>("all")
|
||||
const [selectedExecution, setSelectedExecution] = useState<TaskExecution | null>(null)
|
||||
const [selectedExecutionId, setSelectedExecutionId] = useState<string | null>(null)
|
||||
const [executionPreview, setExecutionPreview] = useState<TaskExecution | null>(null)
|
||||
const [detailOpen, setDetailOpen] = useState(false)
|
||||
const [detailLoading, setDetailLoading] = useState(false)
|
||||
const [retrying, setRetrying] = useState(false)
|
||||
|
||||
const fetchTaskTypes = useCallback(async () => {
|
||||
try {
|
||||
const data = await AdminService.getTaskTypes()
|
||||
setTaskTypes(data)
|
||||
} catch {
|
||||
setTaskTypes([])
|
||||
}
|
||||
}, [])
|
||||
const taskTypesQuery = useQuery({
|
||||
queryKey: ["admin", "task-types"],
|
||||
queryFn: () => AdminService.getTaskTypes(),
|
||||
})
|
||||
|
||||
const fetchTaskExecutions = useCallback(async (overrides?: {
|
||||
page?: number
|
||||
status?: TaskExecutionStatus | "all"
|
||||
taskType?: string
|
||||
}) => {
|
||||
try {
|
||||
setExecutionsLoading(true)
|
||||
setExecutionsError(null)
|
||||
const page = overrides?.page ?? executionsPage
|
||||
const status = overrides?.status ?? executionStatus
|
||||
const taskType = overrides?.taskType ?? executionTaskType
|
||||
const data = await AdminService.listTaskExecutions({
|
||||
page,
|
||||
page_size: 10,
|
||||
status: status === "all" ? undefined : status,
|
||||
task_type: taskType === "all" ? undefined : taskType,
|
||||
})
|
||||
setExecutions(data.items || [])
|
||||
setExecutionsTotal(data.total || 0)
|
||||
} catch (err) {
|
||||
setExecutionsError(err instanceof Error ? err : new Error("加载任务执行记录失败"))
|
||||
} finally {
|
||||
setExecutionsLoading(false)
|
||||
}
|
||||
}, [executionStatus, executionTaskType, executionsPage])
|
||||
const executionsQuery = useQuery({
|
||||
queryKey: ["admin", "task-executions", executionsPage, executionStatus, executionTaskType],
|
||||
queryFn: () => AdminService.listTaskExecutions({
|
||||
page: executionsPage,
|
||||
page_size: PAGE_SIZE,
|
||||
status: executionStatus === "all" ? undefined : executionStatus,
|
||||
task_type: executionTaskType === "all" ? undefined : executionTaskType,
|
||||
}),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskTypes()
|
||||
}, [fetchTaskTypes])
|
||||
const detailQuery = useQuery({
|
||||
queryKey: ["admin", "task-execution", selectedExecutionId],
|
||||
queryFn: () => AdminService.getTaskExecution(selectedExecutionId!),
|
||||
enabled: detailOpen && !!selectedExecutionId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
fetchTaskExecutions()
|
||||
}, [fetchTaskExecutions])
|
||||
|
||||
useEffect(() => {
|
||||
setExecutionsPage(1)
|
||||
}, [executionStatus, executionTaskType])
|
||||
|
||||
const openExecutionDetail = async (execution: TaskExecution) => {
|
||||
setSelectedExecution(execution)
|
||||
setDetailOpen(true)
|
||||
try {
|
||||
setDetailLoading(true)
|
||||
const detail = await AdminService.getTaskExecution(execution.id)
|
||||
setSelectedExecution(detail)
|
||||
} catch (err) {
|
||||
toast.error("任务详情加载失败", {
|
||||
description: err instanceof Error ? err.message : "未知错误",
|
||||
})
|
||||
} finally {
|
||||
setDetailLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetryExecution = async () => {
|
||||
if (!selectedExecution) return
|
||||
try {
|
||||
setRetrying(true)
|
||||
const taskID = await AdminService.retryTaskExecution(selectedExecution.id)
|
||||
const retryMutation = useMutation({
|
||||
mutationFn: (id: string) => AdminService.retryTaskExecution(id),
|
||||
onSuccess: (taskID) => {
|
||||
toast.success("任务已重新下发", {
|
||||
description: `新任务 ID:${ taskID }`,
|
||||
})
|
||||
await fetchTaskExecutions()
|
||||
} catch (err) {
|
||||
void queryClient.invalidateQueries({ queryKey: ["admin", "task-executions"] })
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
toast.error("任务重试失败", {
|
||||
description: err instanceof Error ? err.message : "未知错误",
|
||||
description: err.message || "未知错误",
|
||||
})
|
||||
} finally {
|
||||
setRetrying(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const taskTypes: TaskMeta[] = taskTypesQuery.data ?? []
|
||||
const executions = executionsQuery.data?.items ?? []
|
||||
const executionsTotal = executionsQuery.data?.total ?? 0
|
||||
const executionsLoading = executionsQuery.isPending || executionsQuery.isFetching
|
||||
const executionsError = executionsQuery.isError ? executionsQuery.error : null
|
||||
const selectedExecution = detailQuery.data ?? executionPreview
|
||||
const detailLoading = detailQuery.isFetching
|
||||
|
||||
const totalPages = useMemo(
|
||||
() => Math.max(1, Math.ceil(executionsTotal / PAGE_SIZE)),
|
||||
[executionsTotal],
|
||||
)
|
||||
|
||||
const openExecutionDetail = (execution: TaskExecution) => {
|
||||
setExecutionPreview(execution)
|
||||
setSelectedExecutionId(execution.id)
|
||||
setDetailOpen(true)
|
||||
}
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(executionsTotal / 10))
|
||||
const handleStatusChange = (value: TaskExecutionStatus | "all") => {
|
||||
setExecutionStatus(value)
|
||||
setExecutionsPage(1)
|
||||
}
|
||||
|
||||
const handleTaskTypeChange = (value: string) => {
|
||||
setExecutionTaskType(value)
|
||||
setExecutionsPage(1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<br/>
|
||||
<div className="flex flex-col gap-3 border-b border-border pb-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Select value={executionStatus} onValueChange={(value) => setExecutionStatus(value as TaskExecutionStatus | "all")}>
|
||||
<Select value={executionStatus} onValueChange={(value) => handleStatusChange(value as TaskExecutionStatus | "all")}>
|
||||
<SelectTrigger size="sm" className="w-[120px]">
|
||||
<SelectValue placeholder="状态" />
|
||||
</SelectTrigger>
|
||||
@@ -164,7 +145,7 @@ export function TaskExecutionsManager() {
|
||||
<SelectItem value="failed">失败</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={executionTaskType} onValueChange={setExecutionTaskType}>
|
||||
<Select value={executionTaskType} onValueChange={handleTaskTypeChange}>
|
||||
<SelectTrigger size="sm" className="w-[180px]">
|
||||
<SelectValue placeholder="任务类型" />
|
||||
</SelectTrigger>
|
||||
@@ -175,7 +156,7 @@ export function TaskExecutionsManager() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="sm" onClick={() => fetchTaskExecutions()} disabled={executionsLoading}>
|
||||
<Button variant="outline" size="sm" onClick={() => void executionsQuery.refetch()} disabled={executionsLoading}>
|
||||
{executionsLoading ? <Spinner className="size-4" /> : <RefreshCw className="size-4" />}
|
||||
刷新
|
||||
</Button>
|
||||
@@ -184,7 +165,7 @@ export function TaskExecutionsManager() {
|
||||
|
||||
{executionsError ? (
|
||||
<div className="p-8 border border-dashed rounded-lg bg-card">
|
||||
<ErrorInline error={executionsError} onRetry={fetchTaskExecutions} className="justify-center" />
|
||||
<ErrorInline error={executionsError} onRetry={() => void executionsQuery.refetch()} className="justify-center" />
|
||||
</div>
|
||||
) : executionsLoading && executions.length === 0 ? (
|
||||
<LoadingStateWithBorder icon={Activity} description="加载任务执行记录中..." />
|
||||
@@ -375,13 +356,13 @@ export function TaskExecutionsManager() {
|
||||
</div>
|
||||
|
||||
<SheetFooter className="border-t">
|
||||
<Button variant="outline" onClick={() => selectedExecution && openExecutionDetail(selectedExecution)} disabled={!selectedExecution || detailLoading}>
|
||||
<Button variant="outline" onClick={() => void detailQuery.refetch()} disabled={!selectedExecutionId || detailLoading}>
|
||||
{detailLoading ? <Spinner className="size-4" /> : <RefreshCw className="size-4" />}
|
||||
刷新详情
|
||||
</Button>
|
||||
{selectedExecution && selectedExecution.status === "failed" && selectedExecution.retryable && (
|
||||
<Button onClick={handleRetryExecution} disabled={retrying}>
|
||||
{retrying ? <Spinner className="size-4" /> : <RotateCcw className="size-4" />}
|
||||
<Button onClick={() => retryMutation.mutate(selectedExecution.id)} disabled={retryMutation.isPending}>
|
||||
{retryMutation.isPending ? <Spinner className="size-4" /> : <RotateCcw className="size-4" />}
|
||||
重试任务
|
||||
</Button>
|
||||
)}
|
||||
@@ -390,4 +371,4 @@ export function TaskExecutionsManager() {
|
||||
</Sheet>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {Spinner} from "@/components/ui/spinner"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
|
||||
import {Calendar as CalendarIcon, Clock, Info, Layers, Play} from "lucide-react"
|
||||
|
||||
import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services"
|
||||
import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services/admin"
|
||||
import {buildTaskPayload} from "@/lib/task-param-utils"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
|
||||
@@ -12,7 +12,7 @@ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, Di
|
||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
|
||||
import {Clock, Edit2, Info, Plus, RefreshCw, Trash2} from "lucide-react"
|
||||
|
||||
import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services"
|
||||
import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services/admin"
|
||||
import {buildTaskPayload} from "@/lib/task-param-utils"
|
||||
import {ErrorInline} from "@/components/layout/error"
|
||||
import {LoadingStateWithBorder} from "@/components/layout/loading"
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Button} from "@/components/ui/button"
|
||||
import {Badge} from "@/components/ui/badge"
|
||||
import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar"
|
||||
import {Sheet, SheetContent, SheetTitle} from "@/components/ui/sheet"
|
||||
import type {AdminUser} from "@/lib/services"
|
||||
import type {AdminUser} from "@/lib/services/admin"
|
||||
import {cn, formatDateTime} from "@/lib/utils"
|
||||
|
||||
interface UserDetailSheetProps {
|
||||
|
||||
Reference in New Issue
Block a user