修复负载UI问题
This commit is contained in:
1 parent
6b4ee11cbf
commit
aededd1362
3 files changed
+42
-91
No files matched your search
@@ -127,7 +127,7 @@ function HomeView() {
|
||||
}
|
||||
|
||||
function DetailView() {
|
||||
const { config, goHome, liveMetrics, loading, selectedId, servers } = useApp();
|
||||
const { config, goHome, loading, selectedId, servers } = useApp();
|
||||
const locale = config.locale;
|
||||
const selected = servers.find((server) => server.id === selectedId) ?? null;
|
||||
|
||||
@@ -136,7 +136,6 @@ function DetailView() {
|
||||
<Suspense fallback={<div className="chart-loading">{ui(locale, "正在加载节点", "Loading server")}</div>}>
|
||||
<NodeDetails
|
||||
server={selected}
|
||||
liveLatencyResults={liveMetrics[selected.id]?.latencyResults ?? []}
|
||||
threshold={config.offline_threshold_seconds}
|
||||
retentionDays={config.history_retention_days}
|
||||
locale={locale}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { chartGapLimit, chartTimeLabel, chartTimeRange, insertTimelineGaps } fro
|
||||
import { demoHistory, demoLatencyHistory, demoLatencyTasks } from "../demo";
|
||||
import { averageOf } from "../latency";
|
||||
import { displayGpuDevices, formatBytes, formatCpuName, formatSpeed, formatUptime, isOnline, number } from "../format";
|
||||
import type { HistoryPoint, LatencySample, LatencyTestPoint, LiveLatencyResult, Server } from "../types";
|
||||
import type { HistoryPoint, LatencySample, LatencyTestPoint, Server } from "../types";
|
||||
import { Flag, regionDisplayName } from "./Flag";
|
||||
import { OSIcon } from "./OSIcon";
|
||||
import { ui } from "../locale";
|
||||
@@ -186,9 +186,8 @@ const tooltipStyle = {
|
||||
fontSize: 12,
|
||||
};
|
||||
|
||||
export function NodeDetails({ server, liveLatencyResults, threshold, retentionDays, locale, demo = false, onClose }: {
|
||||
export function NodeDetails({ server, threshold, retentionDays, locale, demo = false, onClose }: {
|
||||
server: Server;
|
||||
liveLatencyResults: LiveLatencyResult[];
|
||||
threshold: number;
|
||||
retentionDays: number;
|
||||
locale: "zh-CN" | "en";
|
||||
@@ -247,50 +246,13 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa
|
||||
api.latencyHistory(server.id, latencyHours)
|
||||
.then((latency) => { if (active) {
|
||||
setLatencyTasks(latency.tasks);
|
||||
setLatencyPoints((current) => mergeLatencySamples(
|
||||
current,
|
||||
latency.points,
|
||||
latencyHours,
|
||||
latency.tasks.length,
|
||||
));
|
||||
setLatencyPoints(mergeLatencySamples([], latency.points, latencyHours, latency.tasks.length));
|
||||
} })
|
||||
.catch((reason) => { if (active) { setLatencyTasks([]); setLatencyPoints([]); setLatencyError(reason instanceof Error ? reason.message : ui(locale, "延迟数据加载失败", "Unable to load latency")); } })
|
||||
.finally(() => { if (active) setLatencyLoading(false); });
|
||||
return () => { active = false; };
|
||||
}, [server.id, latencyHours, demo, locale]);
|
||||
|
||||
useEffect(() => {
|
||||
if (demo || !server.latency.length) return;
|
||||
setLatencyPoints((current) => mergeLatencySamples(
|
||||
current,
|
||||
server.latency,
|
||||
latencyHours,
|
||||
Math.max(latencyTasks.length, server.latency.length),
|
||||
));
|
||||
}, [demo, latencyHours, latencyTasks.length, server.latency]);
|
||||
|
||||
useEffect(() => {
|
||||
if (demo || !liveLatencyResults.length || !latencyTasks.length) return;
|
||||
const definitions = new Map(latencyTasks.map((task) => [task.id, task]));
|
||||
const samples = liveLatencyResults.flatMap((result): LatencySample[] => {
|
||||
const task = definitions.get(result.task_id);
|
||||
if (!task || !Number.isFinite(result.timestamp) || result.timestamp <= 0) return [];
|
||||
return [{
|
||||
task_id: task.id,
|
||||
server_id: server.id,
|
||||
name: task.name,
|
||||
task_type: task.task_type,
|
||||
target: task.target,
|
||||
port: task.port,
|
||||
timestamp: result.timestamp,
|
||||
latency_ms: result.latency_ms,
|
||||
packet_loss: result.packet_loss,
|
||||
}];
|
||||
});
|
||||
if (!samples.length) return;
|
||||
setLatencyPoints((current) => mergeLatencySamples(current, samples, latencyHours, latencyTasks.length));
|
||||
}, [demo, latencyHours, latencyTasks, liveLatencyResults, server.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setHiddenLatencyTaskIds((current) => {
|
||||
const available = new Set(latencyTasks.map((task) => task.id));
|
||||
@@ -317,8 +279,8 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa
|
||||
() => insertLoadGaps(points, loadChartHours),
|
||||
[loadChartHours, points],
|
||||
);
|
||||
// 有时间范围时钉住整段窗口,部分数据也保持真实比例;实时档跟随数据自适应
|
||||
// (与延迟图、Komari 原版一致)。points 进 deps 是为了在数据落位后重算
|
||||
// 有时间范围时钉住整段窗口,部分数据也保持真实比例;实时档用 dataMin/dataMax
|
||||
// 贴合数据(与延迟图、Komari 原版一致)。points 进 deps 是为了在数据落位后重算
|
||||
// domain,慢查询时末端不会被裁掉。
|
||||
const loadTimeRange = useMemo(
|
||||
() => (loadHours === 0 ? null : chartTimeRange(loadChartHours)),
|
||||
@@ -350,14 +312,15 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa
|
||||
[hiddenLatencyTaskIds, latencySeries],
|
||||
);
|
||||
const latencyData = useMemo(() => {
|
||||
// 共享时间轴取全部任务(含已隐藏),全部隐藏时图表仍渲染空轴 + 覆盖提示。
|
||||
const timestamps = new Set<number>();
|
||||
for (const series of visibleLatencySeries) {
|
||||
for (const series of latencySeries) {
|
||||
for (const point of series.points) timestamps.add(point.timestamp);
|
||||
}
|
||||
return Array.from(timestamps)
|
||||
.sort((left, right) => left - right)
|
||||
.map((timestamp) => ({ timestamp, label: chartTimeLabel(timestamp, hours, locale) }));
|
||||
}, [hours, locale, visibleLatencySeries]);
|
||||
}, [hours, locale, latencySeries]);
|
||||
const latencyTimeRange = useMemo(() => chartTimeRange(latencyHours), [latencyHours, latencyPoints]);
|
||||
const last = data[data.length - 1];
|
||||
const pingEnabled = latencyTasks.length > 0;
|
||||
@@ -416,53 +379,53 @@ export function NodeDetails({ server, liveLatencyResults, threshold, retentionDa
|
||||
{chartType === "load" && loadLoading ? <div className="chart-loading">{ui(locale, "正在读取历史数据", "Loading history")}</div> : chartType === "latency" && latencyLoading ? <div className="chart-loading">{ui(locale, "正在读取延迟数据", "Loading latency")}</div> : chartType === "load" && !data.length ? <div className="chart-loading">{ui(locale, "暂无历史数据", "No history")}</div> : chartType === "load" ? (
|
||||
<div className="detail-charts-grid">
|
||||
<ChartCard title="CPU" value={`${number(last.cpu).toFixed(1)}%`}>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : undefined} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis domain={[0, 100]} tick={{ fontSize: 10 }} width={36} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [`${Number(value).toFixed(1)}%`, "CPU"]} /><Area type="monotone" dataKey="cpu" stroke="var(--danger-bar)" fill="var(--danger-bar)" fillOpacity={0.15} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : ["dataMin", "dataMax"]} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis domain={[0, 100]} tick={{ fontSize: 10 }} width={36} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [`${Number(value).toFixed(1)}%`, "CPU"]} /><Area type="monotone" dataKey="cpu" stroke="var(--danger-bar)" fill="var(--danger-bar)" fillOpacity={0.15} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
</ChartCard>
|
||||
<ChartCard title={ui(locale, "内存", "Memory")} value={`${formatBytes(last.mem_used)} / ${formatBytes(last.mem_total)}`}>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : undefined} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis domain={[0, last.mem_total]} ticks={resourceTicks(last.mem_total)} tick={{ fontSize: 10 }} width={56} tickFormatter={(value) => formatBytes(Number(value), 0)} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "内存"]} /><Area type="monotone" dataKey="mem_used" stroke="var(--primary)" fill="var(--primary)" fillOpacity={0.15} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : ["dataMin", "dataMax"]} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis domain={[0, last.mem_total]} ticks={resourceTicks(last.mem_total)} tick={{ fontSize: 10 }} width={56} tickFormatter={(value) => formatBytes(Number(value), 0)} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "内存"]} /><Area type="monotone" dataKey="mem_used" stroke="var(--primary)" fill="var(--primary)" fillOpacity={0.15} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
</ChartCard>
|
||||
<ChartCard title={ui(locale, "硬盘", "Disk")} value={`${formatBytes(last.disk_used)} / ${formatBytes(last.disk_total)}`}>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : undefined} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis domain={[0, last.disk_total]} ticks={resourceTicks(last.disk_total)} tick={{ fontSize: 10 }} width={56} tickFormatter={(value) => formatBytes(Number(value), 0)} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "硬盘"]} /><Area type="monotone" dataKey="disk_used" stroke="var(--warning-bar)" fill="var(--warning-bar)" fillOpacity={0.15} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : ["dataMin", "dataMax"]} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis domain={[0, last.disk_total]} ticks={resourceTicks(last.disk_total)} tick={{ fontSize: 10 }} width={56} tickFormatter={(value) => formatBytes(Number(value), 0)} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value) => [formatBytes(Number(value)), "硬盘"]} /><Area type="monotone" dataKey="disk_used" stroke="var(--warning-bar)" fill="var(--warning-bar)" fillOpacity={0.15} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
</ChartCard>
|
||||
<ChartCard title={ui(locale, "网络", "Network")} value={`↑ ${formatSpeed(last.net_out)} ↓ ${formatSpeed(last.net_in)}`}>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : undefined} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis tick={{ fontSize: 10 }} width={48} tickFormatter={(value) => formatBytes(Number(value), 0)} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value, name) => [formatSpeed(Number(value)), name === "net_out" ? "上行" : "下行"]} /><Area type="monotone" dataKey="net_out" stroke="var(--success-bar)" fill="var(--success-bar)" fillOpacity={0.12} strokeWidth={2} dot={false} isAnimationActive={false} /><Area type="monotone" dataKey="net_in" stroke="var(--info)" fill="var(--info)" fillOpacity={0.1} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
<ResponsiveContainer width="100%" height="100%"><AreaChart data={data}><CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" /><XAxis dataKey="timestamp" type="number" domain={loadTimeRange ? loadTimeRange.domain : ["dataMin", "dataMax"]} ticks={loadTimeRange?.ticks} allowDataOverflow={loadHours > 0} tick={{ fontSize: 10 }} minTickGap={30} tickFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} /><YAxis tick={{ fontSize: 10 }} width={48} tickFormatter={(value) => formatBytes(Number(value), 0)} /><Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), loadChartHours, locale)} formatter={(value, name) => [formatSpeed(Number(value)), name === "net_out" ? "上行" : "下行"]} /><Area type="monotone" dataKey="net_out" stroke="var(--success-bar)" fill="var(--success-bar)" fillOpacity={0.12} strokeWidth={2} dot={false} isAnimationActive={false} /><Area type="monotone" dataKey="net_in" stroke="var(--info)" fill="var(--info)" fillOpacity={0.1} strokeWidth={2} dot={false} isAnimationActive={false} /></AreaChart></ResponsiveContainer>
|
||||
</ChartCard>
|
||||
</div>
|
||||
) : (
|
||||
<section className="latency-overview glass-panel">
|
||||
<div className="latency-summary-grid" style={{ gridTemplateColumns: `repeat(${Math.max(1, latencySeries.length)}, minmax(180px, 1fr))` }}>
|
||||
{latencySeries.map((series) => <div className={`latency-summary ${hiddenLatencyTaskIds.has(series.id) ? "hidden" : ""}`} key={series.id}>
|
||||
<span><i style={{ backgroundColor: series.color }} />{series.name}</span>
|
||||
<strong>{series.latency == null ? "--" : `${series.latency.toFixed(2)} ms`}</strong>
|
||||
<small>{series.loss == null ? "--" : `${series.loss.toFixed(2)}%`} 平均丢包</small>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="latency-task-legend" role="group" aria-label={ui(locale, "延迟任务", "Latency tasks")}>
|
||||
{latencySeries.length ? <div className="latency-task-legend" role="group" aria-label={ui(locale, "延迟任务", "Latency tasks")}>
|
||||
{latencySeries.map((series) => {
|
||||
const hidden = hiddenLatencyTaskIds.has(series.id);
|
||||
const values = `${ui(locale, "延迟", "Latency")} ${series.latency == null ? "--" : `${series.latency.toFixed(1)} ms`} · ${ui(locale, "丢包", "Loss")} ${series.loss == null ? "--" : `${series.loss.toFixed(1)}%`}`;
|
||||
return <button
|
||||
className={`latency-task-toggle ${hidden ? "hidden" : ""}`}
|
||||
key={series.id}
|
||||
type="button"
|
||||
aria-pressed={!hidden}
|
||||
title={hidden ? ui(locale, "显示此任务", "Show this task") : ui(locale, "隐藏此任务", "Hide this task")}
|
||||
aria-label={`${hidden ? ui(locale, "显示", "Show") : ui(locale, "隐藏", "Hide")} ${series.name}`}
|
||||
title={`${hidden ? ui(locale, "点击显示", "Click to show") : ui(locale, "点击隐藏", "Click to hide")} ${series.name}\n${values}`}
|
||||
onClick={() => toggleLatencyTask(series.id)}
|
||||
>
|
||||
<i style={{ backgroundColor: series.color }} />
|
||||
<span>{series.name}</span>
|
||||
<em>{series.latency == null ? "--" : `${series.latency.toFixed(1)} ms`}</em>
|
||||
<em>{ui(locale, "丢包", "Loss")} {series.loss == null ? "--" : `${series.loss.toFixed(1)}%`}</em>
|
||||
</button>;
|
||||
})}
|
||||
</div>
|
||||
</div> : null}
|
||||
<div className="latency-line-chart">
|
||||
{latencyData.length && visibleLatencySeries.length ? <ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={latencyData} margin={{ top: 10, right: 12, left: 4, bottom: 2 }}>
|
||||
<CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="timestamp" type="number" domain={latencyTimeRange.domain} ticks={latencyTimeRange.ticks} allowDataOverflow tick={{ fontSize: 10 }} minTickGap={40} tickFormatter={(value) => chartTimeLabel(Number(value), latencyHours, locale)} />
|
||||
<YAxis tick={{ fontSize: 10 }} width={48} unit="ms" />
|
||||
<Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), latencyHours, locale)} formatter={(value, name) => [value == null ? "--" : `${Number(value).toFixed(1)} ms`, name]} />
|
||||
{visibleLatencySeries.map((series) => <Line key={series.id} data={series.points} type="monotone" dataKey="value" name={series.name} stroke={series.color} strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer> : <div className="latency-empty">{visibleLatencySeries.length ? ui(locale, "暂无延迟数据", "No latency data") : ui(locale, "已隐藏全部任务", "All tasks are hidden")}</div>}
|
||||
{latencyData.length ? <>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={latencyData} margin={{ top: 10, right: 12, left: 4, bottom: 2 }}>
|
||||
<CartesianGrid stroke="var(--chart-grid)" strokeDasharray="3 3" />
|
||||
<XAxis dataKey="timestamp" type="number" domain={latencyTimeRange.domain} ticks={latencyTimeRange.ticks} allowDataOverflow tick={{ fontSize: 10 }} minTickGap={40} tickFormatter={(value) => chartTimeLabel(Number(value), latencyHours, locale)} />
|
||||
<YAxis tick={{ fontSize: 10 }} width={48} unit="ms" />
|
||||
<Tooltip contentStyle={tooltipStyle} labelFormatter={(value) => chartTimeLabel(Number(value), latencyHours, locale)} formatter={(value, name) => [value == null ? "--" : `${Number(value).toFixed(1)} ms`, name]} />
|
||||
{visibleLatencySeries.map((series) => <Line key={series.id} data={series.points} type="monotone" dataKey="value" name={series.name} stroke={series.color} strokeWidth={2} dot={false} connectNulls={false} isAnimationActive={false} />)}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{visibleLatencySeries.length ? null : <div className="latency-empty-overlay">{ui(locale, "已隐藏全部延迟任务", "All latency tasks are hidden")}</div>}
|
||||
</> : <div className="latency-empty">{ui(locale, "暂无延迟数据", "No latency data")}</div>}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -186,23 +186,18 @@ html[data-blur="on"] .node-offline-overlay { backdrop-filter: blur(2px); }
|
||||
.detail-chart header span { overflow: hidden; color: var(--muted-foreground); font-size: 12px; font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.detail-chart > div { width: 100%; height: 208px; }
|
||||
.latency-overview { min-width: 0; overflow: hidden; border-radius: 8px; }
|
||||
.latency-summary-grid { display: grid; min-width: 100%; overflow-x: auto; border-bottom: 1px solid var(--border); }
|
||||
.latency-summary { min-width: 180px; border-right: 1px solid var(--border); padding: 17px 20px; }
|
||||
.latency-summary:last-child { border-right: 0; }
|
||||
.latency-summary > span { display: flex; min-width: 0; align-items: center; gap: 7px; overflow: hidden; color: var(--muted-foreground); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.latency-summary > span i { width: 9px; height: 9px; flex: 0 0 auto; border-radius: 2px; }
|
||||
.latency-summary strong { display: block; margin-top: 6px; font-size: 22px; font-weight: 650; font-variant-numeric: tabular-nums; letter-spacing: 0; }
|
||||
.latency-summary small { display: block; margin-top: 5px; color: var(--muted-foreground); font-size: 12px; font-variant-numeric: tabular-nums; }
|
||||
.latency-summary.hidden { opacity: .42; }
|
||||
.latency-task-legend { display: flex; min-width: 0; align-items: center; gap: 6px; overflow-x: auto; border-bottom: 1px solid var(--border); padding: 10px 16px; }
|
||||
.latency-task-toggle { display: inline-flex; min-width: 0; min-height: 28px; flex: 0 0 auto; align-items: center; gap: 6px; border: 1px solid transparent; border-radius: 6px; padding: 4px 8px; color: var(--muted-foreground); font-size: 11px; white-space: nowrap; }
|
||||
.latency-task-toggle:hover { color: var(--foreground); background: color-mix(in srgb, var(--foreground) 6%, transparent); }
|
||||
.latency-task-legend { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 6px 20px; border-bottom: 1px solid var(--border); padding: 10px 16px; }
|
||||
.latency-task-toggle { display: inline-flex; min-width: 0; max-width: 100%; min-height: 28px; flex: 0 0 auto; align-items: center; gap: 7px; border: 0; border-radius: 6px; padding: 4px 6px; color: var(--foreground); font-size: 12px; white-space: nowrap; transition: opacity 150ms ease; }
|
||||
.latency-task-toggle:hover { background: color-mix(in srgb, var(--foreground) 6%, transparent); }
|
||||
.latency-task-toggle:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }
|
||||
.latency-task-toggle i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 2px; }
|
||||
.latency-task-toggle.hidden { border-color: var(--border); color: var(--muted-foreground); opacity: .58; }
|
||||
.latency-task-toggle.hidden i { opacity: .38; }
|
||||
.latency-line-chart { width: 100%; height: 320px; padding: 16px 18px 10px 10px; }
|
||||
.latency-task-toggle i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; }
|
||||
.latency-task-toggle span { min-width: 0; overflow: hidden; max-width: 160px; font-weight: 500; text-overflow: ellipsis; }
|
||||
.latency-task-toggle em { flex: 0 0 auto; color: var(--muted-foreground); font-style: normal; font-variant-numeric: tabular-nums; }
|
||||
.latency-task-toggle.hidden { opacity: .35; }
|
||||
.latency-task-toggle.hidden:hover { opacity: .65; }
|
||||
.latency-line-chart { position: relative; width: 100%; height: 288px; padding: 16px 18px 10px 10px; }
|
||||
.latency-empty { display: grid; width: 100%; height: 100%; place-items: center; color: var(--muted-foreground); font-size: 13px; }
|
||||
.latency-empty-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--muted-foreground); font-size: 12px; pointer-events: none; }
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.brand strong { font-size: 18px; }
|
||||
@@ -245,11 +240,5 @@ html[data-blur="on"] .node-offline-overlay { backdrop-filter: blur(2px); }
|
||||
.range-control { width: 100%; }
|
||||
.range-control button { flex: 0 0 auto; }
|
||||
.detail-chart > div { height: 192px; }
|
||||
.latency-summary { padding: 14px 16px; }
|
||||
.latency-summary strong { font-size: 19px; }
|
||||
.latency-summary-grid { overflow: visible; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
|
||||
.latency-summary { min-width: 0; }
|
||||
.latency-summary:nth-child(2n) { border-right: 0; }
|
||||
.latency-summary:nth-child(n+3) { border-top: 1px solid var(--border); }
|
||||
.latency-line-chart { height: 270px; padding: 12px 10px 8px 4px; }
|
||||
}
|
||||
Reference in new issue
Block a user