// 站点拨测 — 按协议可观测图表(拖拽选区 + 暗黑 Tooltip)// Logan.Li
import {
Area, Bar, BarChart, CartesianGrid, Cell, ComposedChart, Line,
ReferenceArea, ReferenceLine, Tooltip, XAxis, YAxis,
} from 'recharts';
import { useTranslation } from 'react-i18next';
import { Globe, Network, Radio, Shield, Wifi } from 'lucide-react';
import type { SiteResult, SiteTask } from '../helpers';
import { fmtMs, fmtSslDays, normalizeProbeType } from '../helpers';
import {
analyzeDnsResults, analyzeHttpResults, analyzePingResults,
analyzeSslResults, analyzeTcpResults,
} from './analytics';
import { SiteEventTimeline } from './SiteEventTimeline';
import { SiteChartCard } from '../SiteChartCard';
import { MonitorRangeChartFrame } from '../../shared/MonitorRangeChartFrame';
import { MonitorChartTooltip, MONITOR_CHART_GRID, MONITOR_CHART_TICK } from '../../shared/monitorChartTheme';
interface Props {
task: SiteTask;
results: SiteResult[];
rangeLabel?: string;
}
function SelectionArea({ left, right }: { left: string; right: string }) {
if (!left || !right || left === right) return null;
return (
);
}
function HttpObs({ task, results, rangeLabel }: Props) {
const { t } = useTranslation();
const rl = rangeLabel ?? t('monitor.siteProtocol.rangeDefault');
const s = analyzeHttpResults(results, task);
return (
{[
{ label: `${t('monitor.siteProtocol.http.expectedPrefix')} ${s.expectedCode}`, value: s.okCount, tone: 'dg-text-green' },
{ label: '4xx', value: s.err4xx, tone: 'dg-text-yellow' },
{ label: '5xx', value: s.err5xx, tone: 'dg-text-red' },
{ label: t('monitor.siteProtocol.http.timeoutOther'), value: s.otherErr, tone: 'dg-text-red' },
].map((item) => (
{item.label}
{item.value}
))}
}
title={`${t('monitor.siteProtocol.http.statusCodeTitle')}(${rl})`}
empty={s.chart.length === 0}
>
{(h) => (
{({ displayData, chartHandlers, refLeft, refRight }) => (
`HTTP ${v}`} nameFormatter={() => t('monitor.siteProtocol.http.statusCodeName')} />} />
{displayData.map((d, i) => (
= 500 ? 'var(--dg-red)' : d.code >= 400 ? '#eab308' : 'var(--dg-red)'} />
))}
|
)}
)}
);
}
function SslObs({ task, results }: Props) {
const { t } = useTranslation();
const s = analyzeSslResults(results, task);
const sslInfo = fmtSslDays(s.days);
return (
{[
{ label: t('monitor.siteProtocol.ssl.expiry'), value: s.expiry || '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.ssl.remainingDays'), value: sslInfo.text, tone: sslInfo.tone },
{ label: t('monitor.siteProtocol.ssl.issuer'), value: s.issuer ? (s.issuer.length > 16 ? `${s.issuer.slice(0, 16)}…` : s.issuer) : '--', tone: 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.ssl.handshakeLatency'), value: fmtMs(s.handshakeMs), tone: 'text-[var(--dg-text-primary)]' },
].map((item) => (
{item.label}
{item.value}
))}
}
title={t('monitor.siteProtocol.ssl.daysTrendTitle')}
height={220}
empty={s.chart.length === 0}
>
{(h) => (
{({ displayData, chartHandlers, refLeft, refRight }) => (
`${v} ${t('monitor.siteProtocol.ssl.daysUnit')}`} nameFormatter={() => t('monitor.siteProtocol.ssl.daysName')} />} />
)}
)}
);
}
function LatencyObs({ results, mode, rangeLabel }: Props & { mode: 'PING' | 'DNS' }) {
const { t } = useTranslation();
const rl = rangeLabel ?? t('monitor.siteProtocol.rangeDefault');
const s = mode === 'PING' ? analyzePingResults(results) : analyzeDnsResults(results);
const label = mode === 'PING' ? t('monitor.siteProtocol.latency.pingLabel') : t('monitor.siteProtocol.latency.dnsLabel');
const Icon = mode === 'PING' ? Wifi : Radio;
return (
{[
{ label: t('monitor.siteProtocol.latency.avg'), value: fmtMs(s.avg) },
{ label: t('monitor.siteProtocol.latency.min'), value: fmtMs(s.min) },
{ label: t('monitor.siteProtocol.latency.max'), value: fmtMs(s.max) },
{ label: 'P95', value: fmtMs(s.p95) },
{ label: t('monitor.siteProtocol.latency.spikeCount'), value: String(s.spikeCount), tone: s.spikeCount > 0 ? 'dg-text-yellow' : undefined },
].map((item) => (
{item.label}
{item.value}
))}
}
title={`${label}(${rl})`}
empty={s.chart.length === 0}
>
{(h) => (
{({ displayData, chartHandlers, refLeft, refRight }) => (
(v >= 1000 ? `${(v / 1000).toFixed(1)}s` : `${v}ms`)} />
(name === 'latency' ? fmtMs(v) : String(v))} nameFormatter={(name) => (name === 'latency' ? t('monitor.siteProtocol.latency.latencyName') : name)} />} />
)}
)}
);
}
function TcpObs({ results, rangeLabel }: Props) {
const { t } = useTranslation();
const rl = rangeLabel ?? t('monitor.siteProtocol.rangeDefault');
const s = analyzeTcpResults(results);
return (
{[
{ label: `${t('monitor.siteProtocol.tcp.upRate')}(${rl})`, value: s.upRate, tone: 'dg-text-green' },
{ label: t('monitor.siteProtocol.tcp.failCount'), value: String(s.failCount), tone: s.failCount > 0 ? 'dg-text-red' : 'text-[var(--dg-text-primary)]' },
{ label: t('monitor.siteProtocol.tcp.lastFail'), value: s.lastFailAt || '--', tone: 'text-[var(--dg-text-primary)]' },
].map((item) => (
{item.label}
{item.value}
))}
}
title={t('monitor.siteProtocol.tcp.statusTitle')}
height={200}
empty={s.chart.length === 0}
>
{(h) => (
{({ displayData, chartHandlers, refLeft, refRight }) => (
(v >= 100 ? t('monitor.siteProtocol.tcp.connected') : t('monitor.siteProtocol.tcp.disconnected'))} nameFormatter={() => t('monitor.siteProtocol.tcp.statusName')} />} />
)}
)}
);
}
export function SiteProtocolObs({ task, results, rangeLabel }: Props) {
const type = normalizeProbeType(task.type);
if (type === 'SSL') return ;
if (type === 'PING') return ;
if (type === 'DNS') return ;
if (type === 'TCP') return ;
return ;
}