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:
1 parent
cde7bbe540
commit
7e1c8f583c
84 files changed
+7729
-1507
No files matched your search
@@ -26,6 +26,13 @@ import type { OperationLog, OptionItem } from '@/types';
|
||||
|
||||
// Normalize backend log to frontend type
|
||||
function normalizeLog(raw: Record<string, unknown>): OperationLog {
|
||||
const status = raw.status as number | undefined;
|
||||
const result: OperationLog['result'] =
|
||||
status === 0 ? 'failure' : status === 1 ? 'success' : raw.result === 'failure' ? 'failure' : 'success';
|
||||
const request = String(raw.request ?? '');
|
||||
const response = String(raw.response ?? '');
|
||||
const error = String(raw.error ?? '');
|
||||
const message = error || response || request || String(raw.message ?? raw.detail ?? '');
|
||||
return {
|
||||
id: String(raw.id ?? ''),
|
||||
userId: String(raw.user_id ?? raw.userId ?? ''),
|
||||
@@ -35,8 +42,11 @@ function normalizeLog(raw: Record<string, unknown>): OperationLog {
|
||||
resource: String(raw.resource ?? ''),
|
||||
ip: String(raw.ip ?? ''),
|
||||
userAgent: String(raw.user_agent ?? raw.userAgent ?? ''),
|
||||
result: raw.result === 'failure' ? 'failure' : 'success',
|
||||
message: String(raw.message ?? raw.detail ?? ''),
|
||||
result,
|
||||
message,
|
||||
request,
|
||||
response,
|
||||
error,
|
||||
createdAt: String(raw.createdAt ?? raw.created_at ?? ''),
|
||||
};
|
||||
}
|
||||
@@ -338,7 +348,7 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={onClose} />
|
||||
<div className="relative dg-card rounded-lg p-6 max-w-lg w-full mx-4 shadow-2xl">
|
||||
<div className="relative dg-card rounded-lg p-6 max-w-2xl w-full mx-4 shadow-2xl max-h-[85vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--dg-text-primary)]">{t('audit.operations.detailTitle')}</h3>
|
||||
<Button
|
||||
@@ -376,7 +386,12 @@ function LogDetailDialog({ log, onClose }: { log: OperationLog | null; onClose:
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
{log.message && <DetailRow label={t('audit.operations.detailCol.message')} value={log.message} mono />}
|
||||
{log.request && <DetailRow label={t('audit.operations.detailCol.request', { defaultValue: '请求参数' })} value={log.request} mono />}
|
||||
{log.response && <DetailRow label={t('audit.operations.detailCol.response', { defaultValue: '执行输出' })} value={log.response} mono />}
|
||||
{log.error && <DetailRow label={t('audit.operations.detailCol.error', { defaultValue: '错误信息' })} value={log.error} mono />}
|
||||
{!log.request && !log.response && log.message && (
|
||||
<DetailRow label={t('audit.operations.detailCol.message')} value={log.message} mono />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// 站点拨测图表容器 — 详情页全宽展示,无放大弹框 // Logan.Li
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
icon?: ReactNode;
|
||||
height?: number;
|
||||
empty?: boolean;
|
||||
children: (height: number) => ReactNode;
|
||||
}
|
||||
|
||||
export function SiteChartCard({
|
||||
title,
|
||||
icon,
|
||||
height = 260,
|
||||
empty = false,
|
||||
children,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="rounded-lg bg-[var(--dg-bg-tertiary)] p-3">
|
||||
<div className="flex items-center gap-2 mb-2 min-w-0">
|
||||
{icon}
|
||||
<span className="text-xs text-[var(--dg-text-muted)] truncate">{title}</span>
|
||||
</div>
|
||||
{empty ? (
|
||||
<p className="text-xs text-center py-12 text-[var(--dg-text-muted)]">暂无数据</p>
|
||||
) : (
|
||||
children(height)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -23,8 +23,8 @@ import { SiteProtocolObs } from './observability/SiteProtocolObs';
|
||||
interface Props {
|
||||
task: SiteTask | null;
|
||||
onProbed: () => void;
|
||||
/** 嵌入详情弹框时去掉外层 Card // Logan.Li */
|
||||
embedded?: boolean;
|
||||
/** 详情页全宽模式 // Logan.Li */
|
||||
fullPage?: boolean;
|
||||
}
|
||||
|
||||
function statusBadge(status: string) {
|
||||
@@ -46,7 +46,7 @@ function httpCodeBadge(code?: number, expected = 200) {
|
||||
return <span className={`font-mono text-xs font-medium ${tone}`}>{code}</span>;
|
||||
}
|
||||
|
||||
export function SiteResultsPanel({ task, onProbed, embedded = false }: Props) {
|
||||
export function SiteResultsPanel({ task, onProbed, fullPage = false }: Props) {
|
||||
const [results, setResults] = useState<SiteResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [probing, setProbing] = useState(false);
|
||||
@@ -98,7 +98,7 @@ export function SiteResultsPanel({ task, onProbed, embedded = false }: Props) {
|
||||
选择任务查看协议专属可观测指标
|
||||
</div>
|
||||
);
|
||||
return embedded ? empty : (
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent>{empty}</CardContent>
|
||||
</Card>
|
||||
@@ -207,7 +207,7 @@ export function SiteResultsPanel({ task, onProbed, embedded = false }: Props) {
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{!embedded && (
|
||||
{!fullPage && (
|
||||
<>
|
||||
<Activity className="h-4 w-4 text-[var(--dg-text-muted)] shrink-0" />
|
||||
<h3 className="text-sm font-medium text-[var(--dg-text-primary)] truncate">可观测 · {task.name}</h3>
|
||||
@@ -256,7 +256,7 @@ export function SiteResultsPanel({ task, onProbed, embedded = false }: Props) {
|
||||
) : (
|
||||
<>
|
||||
<SiteProtocolObs task={task} results={results} rangeLabel={rangeLabel} />
|
||||
<div className="mt-4 overflow-x-auto max-h-56">
|
||||
<div className={`mt-4 overflow-x-auto ${fullPage ? 'max-h-80' : 'max-h-56'}`}>
|
||||
{results.length === 0 ? (
|
||||
<p className="text-center py-6 text-sm text-[var(--dg-text-muted)]">该时段暂无拨测记录</p>
|
||||
) : (
|
||||
@@ -271,13 +271,9 @@ export function SiteResultsPanel({ task, onProbed, embedded = false }: Props) {
|
||||
</>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return <div className="p-1">{body}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
||||
<CardContent className="p-4">{body}</CardContent>
|
||||
<CardContent className={fullPage ? 'p-5' : 'p-4'}>{body}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// 拨测任务详情弹框 — 可观测指标与历史记录 // Logan.Li
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { SiteTask } from './helpers';
|
||||
import { fmtFrequency, normalizeProbeType } from './helpers';
|
||||
import { SiteResultsPanel } from './SiteResultsPanel';
|
||||
|
||||
const dialogClass =
|
||||
'!max-w-[min(96vw,88rem)] sm:!max-w-[min(96vw,88rem)] w-[min(96vw,88rem)] max-h-[92vh] flex flex-col gap-2 p-4 sm:p-5 bg-[var(--dg-bg-primary)] border-[var(--dg-border)] overflow-hidden';
|
||||
|
||||
interface Props {
|
||||
task: SiteTask | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onProbed: () => void;
|
||||
}
|
||||
|
||||
export function SiteTaskDetailDialog({ task, open, onOpenChange, onProbed }: Props) {
|
||||
if (!task) return null;
|
||||
|
||||
const probeType = normalizeProbeType(task.type);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className={dialogClass}>
|
||||
<DialogHeader className="shrink-0 space-y-1 pr-8">
|
||||
<DialogTitle className="text-base truncate">{task.name}</DialogTitle>
|
||||
<DialogDescription className="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span className="truncate max-w-full font-mono text-[var(--dg-text-muted)]">{task.url}</span>
|
||||
<Badge variant="outline" className="text-[10px]">{probeType}</Badge>
|
||||
<span className="text-[var(--dg-text-muted)]">频率 {fmtFrequency(task.frequency_sec)}</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<SiteResultsPanel task={task} onProbed={onProbed} embedded />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 拨测任务详情页 — 可观测指标与历史记录(非弹框)// Logan.Li
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { SiteTask } from './helpers';
|
||||
import { fmtFrequency, normalizeProbeType } from './helpers';
|
||||
import { SiteResultsPanel } from './SiteResultsPanel';
|
||||
|
||||
interface Props {
|
||||
task: SiteTask;
|
||||
onBack: () => void;
|
||||
onProbed: () => void;
|
||||
}
|
||||
|
||||
export function SiteTaskDetailPage({ task, onBack, onProbed }: Props) {
|
||||
const probeType = normalizeProbeType(task.type);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3 flex-wrap">
|
||||
<Button variant="outline" size="sm" className="h-8 shrink-0" onClick={onBack}>
|
||||
<ArrowLeft className="h-3.5 w-3.5 mr-1" />返回列表
|
||||
</Button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h2 className="text-lg font-semibold text-[var(--dg-text-primary)] truncate">{task.name}</h2>
|
||||
<Badge variant="outline" className="text-[10px] shrink-0">{probeType}</Badge>
|
||||
<span className="text-[10px] text-[var(--dg-text-muted)] shrink-0">频率 {fmtFrequency(task.frequency_sec)}</span>
|
||||
</div>
|
||||
<p className="text-xs font-mono text-[var(--dg-text-muted)] truncate mt-0.5">{task.url}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SiteResultsPanel task={task} onProbed={onProbed} fullPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,14 +62,41 @@ export const SITE_TIME_RANGE_LABELS: Record<SiteTimeRange, string> = {
|
||||
'7d': '近 7 天',
|
||||
};
|
||||
|
||||
/** 各协议推荐默认拨测间隔(秒)// Logan.Li */
|
||||
export const DEFAULT_PROBE_FREQUENCY: Record<ProbeType, number> = {
|
||||
HTTP: 60,
|
||||
SSL: 3600,
|
||||
TCP: 120,
|
||||
DNS: 300,
|
||||
PING: 120,
|
||||
};
|
||||
|
||||
/** 各协议最小拨测间隔(秒)// Logan.Li */
|
||||
export const MIN_PROBE_FREQUENCY: Record<ProbeType, number> = {
|
||||
HTTP: 30,
|
||||
SSL: 600,
|
||||
TCP: 30,
|
||||
DNS: 60,
|
||||
PING: 30,
|
||||
};
|
||||
|
||||
export function defaultProbeFrequency(type: ProbeType): number {
|
||||
return DEFAULT_PROBE_FREQUENCY[type] ?? 60;
|
||||
}
|
||||
|
||||
export const probeTypeMeta: Record<ProbeType, { label: string; placeholder: string; hint: string }> = {
|
||||
HTTP: { label: '目标 URL', placeholder: 'https://example.com/health', hint: 'Web 请求(支持 http/https),检测状态码与响应时间' },
|
||||
SSL: { label: 'SSL 地址', placeholder: 'example.com:443 或 https://example.com', hint: 'TLS 握手检测证书(支持 host:port / https:// 域名,不发起 HTTP 请求)' },
|
||||
TCP: { label: '主机:端口', placeholder: '192.168.1.1:8080 或 example.com:443', hint: 'TCP 端口连通性' },
|
||||
DNS: { label: '域名', placeholder: 'example.com', hint: 'DNS 解析延迟与结果' },
|
||||
PING: { label: '主机地址', placeholder: '192.168.1.1 或 example.com', hint: 'ICMP 连通性与延迟' },
|
||||
PING: { label: '主机地址', placeholder: '192.168.1.1 或 example.com', hint: 'ICMP 连通性与延迟(默认 2 分钟/次)' },
|
||||
};
|
||||
|
||||
export function probeFrequencyHint(type: ProbeType): string {
|
||||
const sec = defaultProbeFrequency(type);
|
||||
return `推荐 ${fmtFrequency(sec)},最小 ${fmtFrequency(MIN_PROBE_FREQUENCY[type])}`;
|
||||
}
|
||||
|
||||
/** 拨测频率展示 // Logan.Li */
|
||||
export function fmtFrequency(sec?: number): string {
|
||||
if (!sec) return '--';
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
analyzeSslResults, analyzeTcpResults,
|
||||
} from './analytics';
|
||||
import { SiteEventTimeline } from './SiteEventTimeline';
|
||||
import { ChartExpandShell } from '../../shared/ChartExpandShell';
|
||||
import { SiteChartCard } from '../SiteChartCard';
|
||||
import { MonitorRangeChartFrame } from '../../shared/MonitorRangeChartFrame';
|
||||
import { MonitorChartTooltip, MONITOR_CHART_GRID, MONITOR_CHART_TICK } from '../../shared/monitorChartTheme';
|
||||
|
||||
@@ -52,7 +52,7 @@ function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ChartExpandShell
|
||||
<SiteChartCard
|
||||
icon={<Globe className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
|
||||
title={`HTTP 状态码时序(${rangeLabel})`}
|
||||
empty={s.chart.length === 0}
|
||||
@@ -76,7 +76,7 @@ function HttpObs({ task, results, rangeLabel = '近 6 小时' }: Props) {
|
||||
)}
|
||||
</MonitorRangeChartFrame>
|
||||
)}
|
||||
</ChartExpandShell>
|
||||
</SiteChartCard>
|
||||
<SiteEventTimeline title="4xx / 5xx / 超时事件" events={s.events} />
|
||||
</div>
|
||||
);
|
||||
@@ -100,10 +100,10 @@ function SslObs({ task, results }: Props) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ChartExpandShell
|
||||
<SiteChartCard
|
||||
icon={<Shield className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
|
||||
title="证书剩余天数趋势"
|
||||
compactHeight={160}
|
||||
height={220}
|
||||
empty={s.chart.length === 0}
|
||||
>
|
||||
{(h) => (
|
||||
@@ -122,7 +122,7 @@ function SslObs({ task, results }: Props) {
|
||||
)}
|
||||
</MonitorRangeChartFrame>
|
||||
)}
|
||||
</ChartExpandShell>
|
||||
</SiteChartCard>
|
||||
<SiteEventTimeline title="证书 / 握手异常" events={s.events} />
|
||||
</div>
|
||||
);
|
||||
@@ -148,7 +148,7 @@ function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mo
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ChartExpandShell
|
||||
<SiteChartCard
|
||||
icon={<Icon className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
|
||||
title={`${label}(${rangeLabel})`}
|
||||
empty={s.chart.length === 0}
|
||||
@@ -168,7 +168,7 @@ function LatencyObs({ results, mode, rangeLabel = '近 6 小时' }: Props & { mo
|
||||
)}
|
||||
</MonitorRangeChartFrame>
|
||||
)}
|
||||
</ChartExpandShell>
|
||||
</SiteChartCard>
|
||||
<SiteEventTimeline title="延迟尖峰 / 失败事件" events={s.events} />
|
||||
</div>
|
||||
);
|
||||
@@ -190,10 +190,10 @@ function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ChartExpandShell
|
||||
<SiteChartCard
|
||||
icon={<Network className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />}
|
||||
title="TCP 连通状态时序"
|
||||
compactHeight={120}
|
||||
height={200}
|
||||
empty={s.chart.length === 0}
|
||||
>
|
||||
{(h) => (
|
||||
@@ -209,7 +209,7 @@ function TcpObs({ results, rangeLabel = '近 6 小时' }: Props) {
|
||||
)}
|
||||
</MonitorRangeChartFrame>
|
||||
)}
|
||||
</ChartExpandShell>
|
||||
</SiteChartCard>
|
||||
<SiteEventTimeline title="TCP 不通时间点" events={s.events} emptyText="近期 TCP 均连通" />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user