feat: 工具配置增强、站点拨测与日志中心多项改进

- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖
- 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败
- Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS
- 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率
- 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示
- 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
This commit is contained in:
Logan committed 2026-07-02 07:47:33 +08:00
1 parent cde7bbe540
commit 7e1c8f583c
84 files changed
+7729 -1507

No files matched your search

+57 -33
View File
@@ -1,5 +1,6 @@
// 站点拨测 — 任务列表 + 详情可观测 // Logan.Li
// 站点拨测 — 任务列表 + 详情页可观测 // Logan.Li
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -14,17 +15,18 @@ import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import {
Plus, Trash2, Pencil, Loader2, RefreshCw, Activity, ShieldAlert, ChevronRight,
Plus, Trash2, Pencil, Loader2, RefreshCw, Activity, ShieldAlert, LineChart,
} from 'lucide-react';
import { toast } from 'sonner';
import { siteApi } from '@/api';
import { useConfirm } from '@/components/ui/confirm-dialog';
import type { JsonData } from '@/types/common';
import { SiteProtocolPieChart } from './site-monitor/SiteProtocolPieChart';
import { SiteTaskDetailDialog } from './site-monitor/SiteTaskDetailDialog';
import { SiteTaskDetailPage } from './site-monitor/SiteTaskDetailPage';
import {
PROBE_TYPE_OPTIONS, type ProbeType, type SiteStats, type SiteTask,
probeTypeMeta, fmtAvailResponse, fmtFrequency, isSslProbeTask, isSslExpiringSoon, normalizeProbeType,
probeTypeMeta, probeFrequencyHint, defaultProbeFrequency, MIN_PROBE_FREQUENCY, fmtAvailResponse, fmtFrequency,
isSslProbeTask, isSslExpiringSoon, normalizeProbeType,
} from './site-monitor/helpers';
interface CreateTaskForm {
@@ -34,7 +36,7 @@ interface CreateTaskForm {
const emptyForm: CreateTaskForm = {
name: '', url: '', type: 'HTTP',
frequency_sec: 60, timeout_sec: 10, expected_status_code: 200,
frequency_sec: defaultProbeFrequency('HTTP'), timeout_sec: 10, expected_status_code: 200,
};
const FILTER_TYPES: Array<ProbeType | 'ALL'> = ['ALL', ...PROBE_TYPE_OPTIONS];
@@ -47,6 +49,8 @@ function alertStatusBadge(status?: string) {
}
export function SiteMonitor() {
const [searchParams, setSearchParams] = useSearchParams();
const taskIdParam = searchParams.get('task');
const [stats, setStats] = useState<SiteStats | null>(null);
const [tasks, setTasks] = useState<SiteTask[]>([]);
const [initialLoading, setInitialLoading] = useState(true);
@@ -56,10 +60,15 @@ export function SiteMonitor() {
const [form, setForm] = useState<CreateTaskForm>({ ...emptyForm });
const [saving, setSaving] = useState(false);
const [selectedType, setSelectedType] = useState<ProbeType | 'ALL'>('ALL');
const [detailTask, setDetailTask] = useState<SiteTask | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const _confirm = useConfirm();
const detailTask = useMemo(() => {
if (!taskIdParam) return null;
const id = Number(taskIdParam);
if (!Number.isFinite(id)) return null;
return tasks.find((t) => t.id === id) || null;
}, [taskIdParam, tasks]);
const loadStats = useCallback(async () => {
try { setStats(await siteApi.getStats() as SiteStats); } catch { /* ignore */ }
}, []);
@@ -70,10 +79,6 @@ export function SiteMonitor() {
const data = await siteApi.getTasks();
const list = (data as JsonData)?.list as SiteTask[] || [];
setTasks(list);
setDetailTask((prev) => {
if (!prev) return null;
return list.find((t) => t.id === prev.id) || null;
});
} catch { /* ignore */ } finally {
setRefreshing(false);
setInitialLoading(false);
@@ -105,7 +110,8 @@ export function SiteMonitor() {
const openCreate = (type?: ProbeType) => {
setEditingTaskId(null);
setForm({ ...emptyForm, type: type || (selectedType !== 'ALL' ? selectedType : 'HTTP') });
const t = type || (selectedType !== 'ALL' ? selectedType : 'HTTP');
setForm({ ...emptyForm, type: t, frequency_sec: defaultProbeFrequency(t) });
setFormOpen(true);
};
@@ -122,8 +128,11 @@ export function SiteMonitor() {
};
const openDetail = (task: SiteTask) => {
setDetailTask(task);
setDetailOpen(true);
setSearchParams({ tab: 'site', task: String(task.id) });
};
const closeDetail = () => {
setSearchParams({ tab: 'site' });
};
const handleSave = async () => {
@@ -150,9 +159,8 @@ export function SiteMonitor() {
if (!ok) return;
try {
await siteApi.deleteTask(task.id);
if (detailTask?.id === task.id) {
setDetailTask(null);
setDetailOpen(false);
if (taskIdParam === String(task.id)) {
closeDetail();
}
toast.success('任务已删除');
loadTasks(); loadStats();
@@ -179,6 +187,24 @@ export function SiteMonitor() {
);
}
if (taskIdParam) {
if (!detailTask) {
return (
<div className="space-y-4 text-center py-16">
<p className="text-sm text-[var(--dg-text-muted)]">拨测任务不存在或已删除</p>
<Button variant="outline" size="sm" onClick={closeDetail}>返回列表</Button>
</div>
);
}
return (
<SiteTaskDetailPage
task={detailTask}
onBack={closeDetail}
onProbed={() => { loadTasks(); loadStats(); }}
/>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between flex-wrap gap-2">
@@ -219,7 +245,7 @@ export function SiteMonitor() {
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-[var(--dg-text-muted)]" />
<h3 className="text-sm font-medium">拨测任务</h3>
<span className="text-[10px] text-[var(--dg-text-muted)]">点击行查看详情</span>
<span className="text-[10px] text-[var(--dg-text-muted)]">点击可观测进入详情页</span>
</div>
<div className="flex flex-wrap gap-1">
{FILTER_TYPES.map((type) => {
@@ -265,8 +291,7 @@ export function SiteMonitor() {
{filteredTasks.map((task) => (
<TableRow
key={task.id}
className="border-[var(--dg-border)] cursor-pointer hover:bg-[var(--dg-bg-tertiary)]/60"
onClick={() => openDetail(task)}
className="border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/60"
>
<TableCell className="py-2">
<div className="min-w-0">
@@ -290,11 +315,13 @@ export function SiteMonitor() {
<TableCell className="py-2" onClick={(e) => e.stopPropagation()}>
<Switch checked={task.enabled} onCheckedChange={() => handleToggle(task)} />
</TableCell>
<TableCell className="py-2" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-0.5">
<TableCell className="py-2">
<div className="flex items-center justify-end gap-0.5">
<Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => openDetail(task)}>
<LineChart className="h-3 w-3 mr-1" />可观测
</Button>
<Button variant="ghost" size="icon-sm" onClick={() => openEdit(task)}><Pencil className="h-3.5 w-3.5" /></Button>
<Button variant="ghost" size="icon-sm" className="dg-text-red" onClick={() => handleDelete(task)}><Trash2 className="h-3.5 w-3.5" /></Button>
<ChevronRight className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />
</div>
</TableCell>
</TableRow>
@@ -312,13 +339,6 @@ export function SiteMonitor() {
</div>
</div>
<SiteTaskDetailDialog
task={detailTask}
open={detailOpen}
onOpenChange={setDetailOpen}
onProbed={() => { loadTasks(); loadStats(); }}
/>
<Dialog open={formOpen} onOpenChange={(open) => { setFormOpen(open); if (!open) setEditingTaskId(null); }}>
<DialogContent className="sm:max-w-md bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<DialogHeader>
@@ -332,7 +352,10 @@ export function SiteMonitor() {
</div>
<div className="grid gap-1.5">
<Label>协议类型</Label>
<Select value={form.type} onValueChange={(v) => setForm({ ...form, type: v as ProbeType })}>
<Select value={form.type} onValueChange={(v) => {
const t = v as ProbeType;
setForm({ ...form, type: t, frequency_sec: editingTaskId ? form.frequency_sec : defaultProbeFrequency(t) });
}}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{PROBE_TYPE_OPTIONS.map((t) => <SelectItem key={t} value={t}>{t}</SelectItem>)}
@@ -347,8 +370,9 @@ export function SiteMonitor() {
<div className="grid grid-cols-2 gap-3">
<div className="grid gap-1.5">
<Label>检测间隔 (秒)</Label>
<Input type="number" min={10} max={86400} value={form.frequency_sec}
onChange={(e) => setForm({ ...form, frequency_sec: Number(e.target.value) || 60 })} />
<Input type="number" min={MIN_PROBE_FREQUENCY[form.type]} max={86400} value={form.frequency_sec}
onChange={(e) => setForm({ ...form, frequency_sec: Number(e.target.value) || defaultProbeFrequency(form.type) })} />
<p className="text-[10px] text-[var(--dg-text-muted)]">{probeFrequencyHint(form.type)}</p>
</div>
<div className="grid gap-1.5">
<Label>超时 (秒)</Label>