1import { ProcessSignalActions } from "./ProcessSignalActions";
2import { resolveUsageAccess } from "@t3tools/client-runtime/state/usage-access";
3import { environmentSession } from "../../state/session";
4import { AuthOrchestrationOperateScope } from "@t3tools/contracts";
5import { readEnvironmentScope, useEnvironmentScope } from "../../state/session";
6import { AuthEnvironmentMaintainScope } from "@t3tools/contracts";
7import { RefreshIcon } from "~/components/ui/refresh-icon";
8import { AlertTriangleIcon, CopyIcon, FolderOpenIcon, InfoIcon } from "lucide-react";
9import { ChevronDown, ChevronRight } from "lucide";
10import {
11 isAtomCommandInterrupted,
12 squashAtomCommandFailure,
13} from "@t3tools/client-runtime/state/runtime";
14import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
15import type {
16 ServerProcessDiagnosticsEntry,
17 ServerProcessResourceHistorySummary,
18 ServerProcessSignal,
19} from "@t3tools/contracts";
20import * as DateTime from "effect/DateTime";
21import * as Option from "effect/Option";
22
23import { cn } from "../../lib/utils";
24import { ensureLocalApi } from "../../localApi";
25import { useOpenInPreferredEditor } from "../../editorPreferences";
26import { formatRelativeTimeLabel, getRelativeTimeState } from "../../timestampFormat";
27import { useEnvironmentQuery } from "../../state/query";
28import { serverEnvironment } from "../../state/server";
29import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
30import { Button } from "../ui/button";
31import { MorphIcon } from "~/components/MorphIcon";
32import { ScrollArea } from "../ui/scroll-area";
33import { Toggle, ToggleGroup } from "../ui/toggle-group";
34import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
35import { toastManager } from "../ui/toast";
36import { ExpandableText } from "./ExpandableText";
37import { ResourceTelemetryDiagnostics } from "./ResourceTelemetryDiagnostics";
38import { SettingsPageContainer, SettingsSection, useRelativeTimeTick } from "./settingsLayout";
39import { useAtomCommand } from "../../state/use-atom-command";
40import { useSettingsScope } from "./SettingsScopeContext";
41import { TelemetryExportSettings } from "./TelemetryExportSettings";
42
43const NUMBER_FORMAT = new Intl.NumberFormat();
44
45function formatCount(value: number): string {
46 return NUMBER_FORMAT.format(value);
47}
48
49function formatDuration(value: number): string {
50 if (value < 1_000) return `${Math.round(value)} ms`;
51 return `${(value / 1_000).toFixed(value >= 10_000 ? 1 : 2)} s`;
52}
53
54function formatBytes(value: number): string {
55 if (value < 1024) return `${value} B`;
56 const units = ["KB", "MB", "GB"] as const;
57 let unitIndex = -1;
58 let next = value;
59 do {
60 next /= 1024;
61 unitIndex += 1;
62 } while (next >= 1024 && unitIndex < units.length - 1);
63 return `${next.toFixed(next >= 10 ? 1 : 2)} ${units[unitIndex]}`;
64}
65
66function formatRelative(value: DateTime.Utc | null): string {
67 if (!value) return "No trace records";
68 return formatRelativeTimeLabel(DateTime.formatIso(value));
69}
70
71function formatRelativeNoWrap(value: DateTime.Utc | null): string {
72 return formatRelative(value).replaceAll(" ", "\u00a0");
73}
74
75function shortenTraceId(traceId: string): string {
76 if (traceId.length <= 32) return traceId;
77 return `${traceId.slice(0, 18)}...${traceId.slice(-10)}`;
78}
79
80function isStaleProcessSignalMessage(message: string | undefined): boolean {
81 return message?.includes("not a live descendant") ?? false;
82}
83
84function StatBlock({
85 label,
86 value,
87 tooltip,
88 tone = "default",
89}: {
90 label: string;
91 value: string;
92 tooltip?: ReactNode;
93 tone?: "default" | "warning" | "danger";
94}) {
95 return (
96 <div className="min-w-0 border-border/60 px-4 py-3 sm:px-5">
97 <div className="flex min-w-0 items-center gap-1.5 text-2xs font-medium uppercase tracking-widest text-muted-foreground/70">
98 <span className="min-w-0 truncate">{label}</span>
99 {tooltip ? (
100 <Tooltip>
101 <TooltipTrigger
102 render={
103 <button
104 type="button"
105 className="cursor-pointer inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/60 hover:text-foreground"
106 aria-label={`${label} details`}
107 >
108 <InfoIcon className="size-3" />
109 </button>
110 }
111 />
112 <TooltipPopup side="top">{tooltip}</TooltipPopup>
113 </Tooltip>
114 ) : null}
115 </div>
116 <div
117 className={cn(
118 "mt-1 truncate font-mono text-lg font-semibold tabular-nums text-foreground",
119 tone === "warning" && "text-warning-foreground",
120 tone === "danger" && "text-destructive",
121 )}
122 >
123 {value}
124 </div>
125 </div>
126 );
127}
128
129function StatsGrid({ children }: { children: ReactNode }) {
130 return (
131 <div className="relative grid grid-cols-2 sm:grid-cols-4">
132 <span
133 className="pointer-events-none absolute inset-y-0 left-1/2 w-px bg-border/60"
134 aria-hidden
135 />
136 <span
137 className="pointer-events-none absolute inset-x-0 top-1/2 h-px bg-border/60 sm:hidden"
138 aria-hidden
139 />
140 <span
141 className="pointer-events-none absolute inset-y-0 left-1/4 hidden w-px bg-border/60 sm:block"
142 aria-hidden
143 />
144 <span
145 className="pointer-events-none absolute inset-y-0 left-3/4 hidden w-px bg-border/60 sm:block"
146 aria-hidden
147 />
148 {children}
149 </div>
150 );
151}
152
153function EmptyRows({ label }: { label: string }) {
154 return <div className="px-4 py-4 text-xs text-muted-foreground sm:px-5">{label}</div>;
155}
156
157function DiagnosticsTable({
158 headers,
159 children,
160 minTableWidth = "min-w-[640px]",
161 columnWidths,
162}: {
163 headers: ReadonlyArray<string>;
164 children: ReactNode;
165 minTableWidth?: string;
166 columnWidths?: ReadonlyArray<string>;
167}) {
168 return (
169 <ScrollArea
170 radius="none"
171 chainVerticalScroll
172 scrollFade
173 hideScrollbars
174 className="w-full max-w-full"
175 >
176 <table
177 className={cn("w-full text-left text-xs", minTableWidth, columnWidths && "table-fixed")}
178 >
179 {columnWidths ? (
180 <colgroup>
181 {headers.map((header, index) => (
182 <col key={header} className={columnWidths[index]} />
183 ))}
184 </colgroup>
185 ) : null}
186 <thead className="border-b border-border/60 text-2xs uppercase tracking-widest text-muted-foreground/70">
187 <tr>
188 {headers.map((header, index) => (
189 <th
190 key={header}
191 className={cn(
192 "whitespace-nowrap px-4 py-2.5 font-semibold first:sm:pl-5 last:sm:pr-5",
193 !columnWidths && index === headers.length - 1 && "w-px",
194 )}
195 >
196 {header.replaceAll(" ", "\u00a0")}
197 </th>
198 ))}
199 </tr>
200 </thead>
201 <tbody className="divide-y divide-border/60">{children}</tbody>
202 </table>
203 </ScrollArea>
204 );
205}
206
207function TraceIdCell({ traceId }: { traceId: string }) {
208 const { copyToClipboard, isCopied: copied } = useCopyToClipboard({
209 target: "trace ID",
210 timeout: 1_200,
211 });
212
213 return (
214 <div className="flex w-full min-w-0 max-w-full items-center gap-2">
215 <Tooltip>
216 <TooltipTrigger
217 render={
218 <span className="min-w-0 flex-1 truncate font-mono text-2xs">
219 {shortenTraceId(traceId)}
220 </span>
221 }
222 />
223 <TooltipPopup side="top" variant="code">
224 {traceId}
225 </TooltipPopup>
226 </Tooltip>
227 <Tooltip>
228 <TooltipTrigger
229 render={
230 <Button
231 size="icon-micro"
232 variant="ghost-muted"
233 aria-label={copied ? "Copied trace ID" : "Copy trace ID"}
234 onClick={() => copyToClipboard(traceId)}
235 >
236 <CopyIcon className="size-3" />
237 </Button>
238 }
239 />
240 <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup>
241 </Tooltip>
242 </div>
243 );
244}
245
246function formatProcessName(command: string): string {
247 const firstToken = command.trim().split(/\s+/)[0];
248 if (!firstToken) return command;
249 const normalized = firstToken.replace(/^['"]|['"]$/g, "");
250 const segments = normalized.split(/[\\/]/).filter(Boolean);
251 return segments.at(-1) ?? normalized;
252}
253
254function formatProcessType(process: ServerProcessDiagnosticsEntry): string {
255 if (process.depth > 0) return "Subprocess";
256 if (/\b(codex|claude|opencode|cursor)\b/i.test(process.command)) return "Agent";
257 return "Process";
258}
259
260function ProcessNameCell({
261 process,
262 isExpanded,
263 onToggle,
264}: {
265 process: ServerProcessDiagnosticsEntry;
266 isExpanded: boolean;
267 onToggle: (pid: number) => void;
268}) {
269 const name = formatProcessName(process.command);
270 const hasChildren = process.childPids.length > 0;
271
272 return (
273 <div
274 className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2"
275 style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }}
276 >
277 {hasChildren ? (
278 <Button
279 size="icon-micro"
280 variant="ghost-muted"
281 aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`}
282 onClick={() => onToggle(process.pid)}
283 >
284 <MorphIcon className="size-3.5" icon={isExpanded ? ChevronDown : ChevronRight} />
285 </Button>
286 ) : (
287 <span className="size-5 shrink-0" aria-hidden="true" />
288 )}
289 <span className="size-1.5 shrink-0 rounded-full bg-success/80" />
290 <Tooltip>
291 <TooltipTrigger
292 render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>}
293 />
294 <TooltipPopup side="top" variant="code">
295 {process.command}
296 </TooltipPopup>
297 </Tooltip>
298 </div>
299 );
300}
301
302function ProcessDiagnosticsTable({
303 processes,
304 canMaintainEnvironment,
305 signalingPid,
306 onSignal,
307 emptyLabel,
308}: {
309 processes: ReadonlyArray<ServerProcessDiagnosticsEntry>;
310 canMaintainEnvironment: boolean;
311 signalingPid: number | null;
312 onSignal: (pid: number, signal: ServerProcessSignal) => void;
313 emptyLabel?: string;
314}) {
315 const [collapsedPids, setCollapsedPids] = useState<ReadonlySet<number>>(() => new Set());
316 const visibleProcesses = useMemo(() => {
317 const visible: ServerProcessDiagnosticsEntry[] = [];
318 let hiddenChildDepth: number | null = null;
319
320 for (const process of processes) {
321 if (hiddenChildDepth !== null) {
322 if (process.depth > hiddenChildDepth) continue;
323 hiddenChildDepth = null;
324 }
325
326 visible.push(process);
327 if (collapsedPids.has(process.pid)) {
328 hiddenChildDepth = process.depth;
329 }
330 }
331
332 return visible;
333 }, [collapsedPids, processes]);
334
335 const toggleProcess = useCallback((pid: number) => {
336 setCollapsedPids((previous) => {
337 const next = new Set(previous);
338 if (next.has(pid)) {
339 next.delete(pid);
340 } else {
341 next.add(pid);
342 }
343 return next;
344 });
345 }, []);
346
347 return (
348 <div className="border-t border-border/60">
349 <ScrollArea
350 radius="none"
351 chainVerticalScroll
352 scrollFade
353 hideScrollbars
354 className="max-h-[min(64vh,44rem)] w-full max-w-full"
355 >
356 <table className="w-full min-w-[1040px] table-fixed text-left text-xs">
357 <colgroup>
358 <col className="w-[24%]" />
359 <col className="w-[8%]" />
360 <col className="w-[10%]" />
361 <col className="w-[33%]" />
362 <col className="w-[8%]" />
363 <col className="w-[11%]" />
364 <col className="w-[6%]" />
365 </colgroup>
366 <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-2xs uppercase tracking-widest text-muted-foreground/70">
367 <tr>
368 <th className="px-4 py-2 font-semibold sm:pl-5">Name</th>
369 <th className="px-3 py-2 text-right font-semibold">CPU</th>
370 <th className="px-3 py-2 text-right font-semibold">Memory</th>
371 <th className="px-3 py-2 font-semibold">Command</th>
372 <th className="px-3 py-2 text-right font-semibold">PID</th>
373 <th className="px-3 py-2 font-semibold">Type</th>
374 <th className="p-2 text-right font-semibold sm:pr-4">Kill</th>
375 </tr>
376 </thead>
377 <tbody className="divide-y divide-border/50">
378 {visibleProcesses.length === 0 ? (
379 <tr>
380 <td colSpan={7} className="px-4 py-4 text-xs text-muted-foreground sm:px-5">
381 {emptyLabel ?? "No live descendant processes found."}
382 </td>
383 </tr>
384 ) : null}
385 {visibleProcesses.map((process) => (
386 <tr key={process.pid} className="hover:bg-muted/20">
387 <td className="px-4 py-2 align-middle sm:pl-5">
388 <ProcessNameCell
389 process={process}
390 isExpanded={!collapsedPids.has(process.pid)}
391 onToggle={toggleProcess}
392 />
393 </td>
394 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
395 {process.cpuPercent.toFixed(1)}%
396 </td>
397 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
398 {formatBytes(process.rssBytes)}
399 </td>
400 <td className="px-3 py-2 align-middle text-muted-foreground">
401 <Tooltip>
402 <TooltipTrigger
403 render={<span className="block truncate">{process.command}</span>}
404 />
405 <TooltipPopup side="top" variant="code">
406 {process.command}
407 </TooltipPopup>
408 </Tooltip>
409 </td>
410 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums text-muted-foreground">
411 {process.pid}
412 </td>
413 <td className="truncate px-3 py-2 align-middle text-muted-foreground">
414 {formatProcessType(process)}
415 </td>
416 <td className="p-2 align-middle sm:pr-4">
417 <ProcessSignalActions
418 disabled={!canMaintainEnvironment || signalingPid === process.pid}
419 onSignal={(signal) => onSignal(process.pid, signal)}
420 />
421 </td>
422 </tr>
423 ))}
424 </tbody>
425 </table>
426 </ScrollArea>
427 </div>
428 );
429}
430
431const RESOURCE_HISTORY_WINDOWS = [
432 { label: "5m", windowMs: 5 * 60_000, bucketMs: 30_000 },
433 { label: "15m", windowMs: 15 * 60_000, bucketMs: 60_000 },
434 { label: "30m", windowMs: 30 * 60_000, bucketMs: 2 * 60_000 },
435 { label: "1h", windowMs: 60 * 60_000, bucketMs: 5 * 60_000 },
436] as const;
437
438function formatCpuTime(seconds: number): string {
439 if (seconds < 60) return `${seconds.toFixed(seconds >= 10 ? 1 : 2)}s`;
440 const minutes = seconds / 60;
441 if (minutes < 60) return `${minutes.toFixed(minutes >= 10 ? 1 : 2)}m`;
442 return `${(minutes / 60).toFixed(2)}h`;
443}
444
445function formatShortProcessName(command: string): string {
446 const name = formatProcessName(command);
447 return name.length > 42 ? `${name.slice(0, 39)}...` : name;
448}
449
450function ResourceHistoryProcessNameCell({
451 process,
452 visualDepth,
453}: {
454 process: ServerProcessResourceHistorySummary;
455 visualDepth: number;
456}) {
457 const name = formatShortProcessName(process.command);
458
459 return (
460 <div
461 className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2"
462 style={{ paddingLeft: `${Math.min(visualDepth, 6) * 10}px` }}
463 aria-label={`${process.isServerRoot ? "Root" : "Child"} process ${name}`}
464 >
465 <span className="size-5 shrink-0" aria-hidden="true" />
466 <span
467 className={cn(
468 "size-1.5 shrink-0 rounded-full",
469 process.isServerRoot ? "bg-warning/90" : "bg-success/80",
470 )}
471 />
472 <Tooltip>
473 <TooltipTrigger
474 render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>}
475 />
476 <TooltipPopup side="top" variant="code">
477 {process.command}
478 </TooltipPopup>
479 </Tooltip>
480 </div>
481 );
482}
483
484function ProcessResourceHistoryChart({
485 buckets,
486}: {
487 buckets: ReadonlyArray<{
488 readonly startedAt: DateTime.Utc;
489 readonly avgCpuPercent: number;
490 readonly maxCpuPercent: number;
491 }>;
492}) {
493 const maxCpuPercent = Math.max(1, ...buckets.map((bucket) => bucket.maxCpuPercent));
494
495 return (
496 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
497 <div className="flex h-28 items-end gap-1 overflow-hidden rounded-sm bg-muted/10 p-2">
498 {buckets.map((bucket) => {
499 const peakHeight = Math.max(2, (bucket.maxCpuPercent / maxCpuPercent) * 100);
500 const averageHeight = Math.max(2, (bucket.avgCpuPercent / maxCpuPercent) * 100);
501 return (
502 <Tooltip key={DateTime.formatIso(bucket.startedAt)}>
503 <TooltipTrigger
504 render={
505 <div className="flex h-full min-w-1 flex-1 items-end">
506 <div
507 className="relative h-full w-full"
508 aria-label={`Average CPU ${bucket.avgCpuPercent.toFixed(1)}%, peak CPU ${bucket.maxCpuPercent.toFixed(1)}%`}
509 >
510 <div
511 className="absolute inset-x-0 bottom-0 rounded-t-sm bg-foreground/15 transition-colors"
512 style={{ height: `${peakHeight}%` }}
513 />
514 <div
515 className="absolute inset-x-0 bottom-0 rounded-t-sm bg-foreground/60 transition-colors"
516 style={{ height: `${averageHeight}%` }}
517 />
518 </div>
519 </div>
520 }
521 />
522 <TooltipPopup side="top">
523 Avg {bucket.avgCpuPercent.toFixed(1)}%, peak {bucket.maxCpuPercent.toFixed(1)}%
524 </TooltipPopup>
525 </Tooltip>
526 );
527 })}
528 </div>
529 </div>
530 );
531}
532
533function ResourceHistoryWindowSelector({
534 selectedWindowMs,
535 onSelect,
536}: {
537 selectedWindowMs: number;
538 onSelect: (windowMs: number) => void;
539}) {
540 return (
541 <ToggleGroup
542 aria-label="Process history period"
543 variant="segmented"
544 value={[String(selectedWindowMs)]}
545 onValueChange={(next) => {
546 const selected = RESOURCE_HISTORY_WINDOWS.find(
547 (option) => String(option.windowMs) === next[0],
548 );
549 if (selected) onSelect(selected.windowMs);
550 }}
551 >
552 {RESOURCE_HISTORY_WINDOWS.map((option) => (
553 <Toggle key={option.windowMs} value={String(option.windowMs)}>
554 {option.label}
555 </Toggle>
556 ))}
557 </ToggleGroup>
558 );
559}
560
561function ProcessResourceHistoryTable({
562 processes,
563 emptyLabel,
564}: {
565 processes: ReadonlyArray<ServerProcessResourceHistorySummary>;
566 emptyLabel: string;
567}) {
568 const shallowestChildDepth = processes.reduce<number | null>((minDepth, process) => {
569 if (process.isServerRoot) return minDepth;
570 return minDepth === null ? process.depth : Math.min(minDepth, process.depth);
571 }, null);
572
573 return (
574 <div className="border-t border-border/60">
575 <ScrollArea
576 chainVerticalScroll
577 scrollFade
578 hideScrollbars
579 className="max-h-[min(64vh,44rem)] w-full max-w-full"
580 >
581 <table className="w-full min-w-[980px] table-fixed text-left text-xs">
582 <colgroup>
583 <col className="w-[24%]" />
584 <col className="w-[10%]" />
585 <col className="w-[10%]" />
586 <col className="w-[10%]" />
587 <col className="w-[10%]" />
588 <col className="w-[10%]" />
589 <col className="w-[16%]" />
590 <col className="w-[10%]" />
591 </colgroup>
592 <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-2xs uppercase tracking-widest text-muted-foreground/70">
593 <tr>
594 <th className="px-4 py-2 font-semibold sm:pl-5">Process</th>
595 <th className="px-3 py-2 text-right font-semibold">CPU Time</th>
596 <th className="px-3 py-2 text-right font-semibold">Current</th>
597 <th className="px-3 py-2 text-right font-semibold">Average</th>
598 <th className="px-3 py-2 text-right font-semibold">Peak</th>
599 <th className="px-3 py-2 text-right font-semibold">Max Mem</th>
600 <th className="px-3 py-2 font-semibold">Command</th>
601 <th className="px-3 py-2 text-right font-semibold sm:pr-5">PID</th>
602 </tr>
603 </thead>
604 <tbody className="divide-y divide-border/50">
605 {processes.length === 0 ? (
606 <tr>
607 <td colSpan={8} className="px-4 py-4 text-xs text-muted-foreground sm:px-5">
608 {emptyLabel}
609 </td>
610 </tr>
611 ) : null}
612 {processes.map((process) => (
613 <tr key={process.processKey} className="hover:bg-muted/20">
614 <td className="px-4 py-2 align-middle sm:pl-5">
615 <ResourceHistoryProcessNameCell
616 process={process}
617 visualDepth={
618 process.isServerRoot || shallowestChildDepth === null
619 ? 0
620 : Math.max(1, process.depth - shallowestChildDepth + 1)
621 }
622 />
623 </td>
624 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
625 {formatCpuTime(process.cpuSecondsApprox)}
626 </td>
627 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
628 {process.currentCpuPercent.toFixed(1)}%
629 </td>
630 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
631 {process.avgCpuPercent.toFixed(1)}%
632 </td>
633 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
634 {process.maxCpuPercent.toFixed(1)}%
635 </td>
636 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
637 {formatBytes(process.maxRssBytes)}
638 </td>
639 <td className="px-3 py-2 align-middle text-muted-foreground">
640 <Tooltip>
641 <TooltipTrigger
642 render={<span className="block truncate">{process.command}</span>}
643 />
644 <TooltipPopup side="top" variant="code">
645 {process.command}
646 </TooltipPopup>
647 </Tooltip>
648 </td>
649 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums text-muted-foreground sm:pr-5">
650 {process.pid}
651 </td>
652 </tr>
653 ))}
654 </tbody>
655 </table>
656 </ScrollArea>
657 </div>
658 );
659}
660
661function DiagnosticsLastChecked({ checkedAt }: { checkedAt: DateTime.Utc | null }) {
662 useRelativeTimeTick();
663 const relative = getRelativeTimeState(checkedAt ? DateTime.formatIso(checkedAt) : null);
664
665 if (relative.status === "missing") {
666 return <span className="text-2xs text-muted-foreground/50">Checking</span>;
667 }
668
669 if (relative.status === "invalid") {
670 return <span className="text-2xs text-muted-foreground/50">Checked unavailable</span>;
671 }
672
673 return (
674 <span className="text-2xs text-muted-foreground/60">
675 {relative.suffix ? (
676 <>
677 Checked <span className="font-mono tabular-nums">{relative.value}</span> {relative.suffix}
678 </>
679 ) : (
680 <>Checked {relative.value}</>
681 )}
682 </span>
683 );
684}
685
686function DiagnosticsRefreshButton({
687 isPending,
688 label,
689 onClick,
690}: {
691 isPending: boolean;
692 label: string;
693 onClick: () => void;
694}) {
695 return (
696 <Tooltip>
697 <TooltipTrigger
698 render={
699 <Button
700 size="icon-xs"
701 variant="ghost-muted"
702 disabled={isPending}
703 onClick={onClick}
704 aria-label={label}
705 >
706 <RefreshIcon refreshing={isPending} />
707 </Button>
708 }
709 />
710 <TooltipPopup side="top">{label}</TooltipPopup>
711 </Tooltip>
712 );
713}
714
715export function DiagnosticsSettingsPanel() {
716 const { environment } = useSettingsScope();
717 // The boundary only mounts this page when the selection resolves to one
718 // connected environment, so the representative is the one to inspect.
719 const environmentId = environment?.environmentId ?? null;
720 const canMaintainEnvironment = useEnvironmentScope(environmentId, AuthEnvironmentMaintainScope);
721 const observability = environment?.serverConfig?.observability;
722 const availableEditors = environment?.serverConfig?.availableEditors;
723 const canOpenHostEditor = useEnvironmentScope(environmentId, AuthOrchestrationOperateScope);
724 const session = useEnvironmentQuery(
725 environmentId === null ? null : environmentSession.sessionStateAtom(environmentId),
726 );
727 const diagnosticsAccess = resolveUsageAccess({
728 connectionPhase: environment?.connection.phase ?? "available",
729 session: session.data,
730 hasSessionError: session.error !== null,
731 });
732 const canReadDiagnostics = diagnosticsAccess.canReadDiagnostics;
733 const signalServerProcess = useAtomCommand(serverEnvironment.signalProcess, {
734 reportFailure: false,
735 });
736 const openInEditor = useOpenInPreferredEditor(environmentId, availableEditors ?? []);
737 const [resourceWindowMs, setResourceWindowMs] = useState(15 * 60_000);
738 const selectedResourceWindow =
739 RESOURCE_HISTORY_WINDOWS.find((option) => option.windowMs === resourceWindowMs) ??
740 RESOURCE_HISTORY_WINDOWS[1];
741 const { data, error, isPending, refresh } = useEnvironmentQuery(
742 environmentId === null || !canReadDiagnostics
743 ? null
744 : serverEnvironment.traceDiagnostics({ environmentId, input: {} }),
745 );
746 const {
747 data: processData,
748 error: processError,
749 isPending: isProcessPending,
750 refresh: refreshProcesses,
751 } = useEnvironmentQuery(
752 environmentId === null || !canReadDiagnostics
753 ? null
754 : serverEnvironment.processDiagnostics({ environmentId, input: {} }),
755 );
756 const {
757 data: resourceData,
758 error: resourceError,
759 isPending: isResourcePending,
760 refresh: refreshResources,
761 } = useEnvironmentQuery(
762 environmentId === null || !canReadDiagnostics
763 ? null
764 : serverEnvironment.processResourceHistory({
765 environmentId,
766 input: {
767 windowMs: selectedResourceWindow.windowMs,
768 bucketMs: selectedResourceWindow.bucketMs,
769 },
770 }),
771 );
772 const [isOpeningLogsDirectory, setIsOpeningLogsDirectory] = useState(false);
773 const [openLogsDirectoryError, setOpenLogsDirectoryError] = useState<string | null>(null);
774 const [signalingPid, setSignalingPid] = useState<number | null>(null);
775 const signalingPidRef = useRef<number | null>(null);
776 const environmentIdRef = useRef(environmentId);
777 const processDataRef = useRef(processData);
778 useEffect(() => {
779 processDataRef.current = processData;
780 }, [processData]);
781 useEffect(() => {
782 environmentIdRef.current = environmentId;
783 return () => {
784 environmentIdRef.current = null;
785 };
786 }, [environmentId]);
787
788 const openLogsDirectory = useCallback(() => {
789 const logsDirectoryPath = observability?.logsDirectoryPath ?? null;
790 if (!logsDirectoryPath) return;
791
792 if (
793 environmentId === null ||
794 !readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)
795 ) {
796 return;
797 }
798
799 setIsOpeningLogsDirectory(true);
800 setOpenLogsDirectoryError(null);
801 void (async () => {
802 const result = await openInEditor(logsDirectoryPath);
803 setIsOpeningLogsDirectory(false);
804 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
805 const error = squashAtomCommandFailure(result);
806 setOpenLogsDirectoryError(
807 error instanceof Error ? error.message : "Unable to open logs folder.",
808 );
809 }
810 })();
811 }, [environmentId, observability?.logsDirectoryPath, openInEditor]);
812
813 const isInitialLoading = isPending && data === null;
814 const isProcessInitialLoading = isProcessPending && processData === null;
815 const isResourceInitialLoading = isResourcePending && resourceData === null;
816 // The export form waits for each environment's first tables so a search target below them
817 // stays in view. Later loads, such as a new resource window, must not remount it and drop drafts.
818 const [exportFormEnvironmentId, setExportFormEnvironmentId] =
819 useState<typeof environmentId>(null);
820 if (
821 exportFormEnvironmentId !== environmentId &&
822 canReadDiagnostics &&
823 !isInitialLoading &&
824 !isProcessInitialLoading &&
825 !isResourceInitialLoading
826 ) {
827 setExportFormEnvironmentId(environmentId);
828 }
829 const signalProcess = useCallback(
830 async (pid: number, signal: ServerProcessSignal) => {
831 const targetEnvironmentId = environmentIdRef.current;
832 const process = processDataRef.current?.processes.find((entry) => entry.pid === pid);
833 if (
834 targetEnvironmentId === null ||
835 process === undefined ||
836 !readEnvironmentScope(targetEnvironmentId, AuthEnvironmentMaintainScope)
837 )
838 return;
839 if (signalingPidRef.current !== null) return;
840 signalingPidRef.current = pid;
841 setSignalingPid(pid);
842 const clearSignaling = () => {
843 signalingPidRef.current = null;
844 setSignalingPid(null);
845 };
846 if (signal === "SIGKILL") {
847 let confirmed = false;
848 try {
849 confirmed = await ensureLocalApi().dialogs.confirm(
850 `Send SIGKILL to process ${pid}? This cannot be handled by the process.`,
851 { variant: "destructive" },
852 );
853 } catch (error) {
854 clearSignaling();
855 toastManager.add({
856 type: "error",
857 title: "Could not confirm signal",
858 description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
859 });
860 return;
861 }
862 if (!confirmed) {
863 clearSignaling();
864 return;
865 }
866 }
867 const currentEnvironmentId = environmentIdRef.current;
868 if (
869 currentEnvironmentId === null ||
870 currentEnvironmentId !== targetEnvironmentId ||
871 !readEnvironmentScope(currentEnvironmentId, AuthEnvironmentMaintainScope)
872 ) {
873 clearSignaling();
874 return;
875 }
876 if (
877 processDataRef.current?.processes.find((entry) => entry.pid === pid)?.startTimeMs !==
878 process.startTimeMs
879 ) {
880 clearSignaling();
881 return;
882 }
883
884 try {
885 const result = await signalServerProcess({
886 environmentId: targetEnvironmentId,
887 input: { pid, startTimeMs: process.startTimeMs, signal },
888 });
889 if (result._tag === "Failure") {
890 if (!isAtomCommandInterrupted(result)) {
891 const error = squashAtomCommandFailure(result);
892 toastManager.add({
893 type: "error",
894 title: `Could not send ${signal}`,
895 description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
896 });
897 }
898 return;
899 }
900 if (!result.value.signaled) {
901 const message = Option.getOrUndefined(result.value.message);
902 refreshProcesses();
903 if (isStaleProcessSignalMessage(message)) {
904 toastManager.add({
905 type: "info",
906 title: "Process already exited",
907 description:
908 "The process is not a child of the T3 Server. It might already have exited.",
909 });
910 return;
911 }
912
913 toastManager.add({
914 type: "error",
915 title: `Could not send ${signal}`,
916 description: message ?? `Failed to send ${signal}.`,
917 });
918 return;
919 }
920 refreshProcesses();
921 } finally {
922 clearSignaling();
923 }
924 },
925 [refreshProcesses, signalServerProcess],
926 );
927
928 const processDiagnosticsError = processData ? Option.getOrNull(processData.error) : null;
929 const processResourceError = resourceData ? Option.getOrNull(resourceData.error) : null;
930 const traceDiagnosticsError = data ? Option.getOrNull(data.error) : null;
931 const traceDiagnosticsPartialFailure = data
932 ? Option.getOrElse(data.partialFailure, () => false)
933 : false;
934
935 if (!canReadDiagnostics) {
936 return (
937 <SettingsPageContainer>
938 <p className="text-sm text-muted-foreground">
939 {environmentId === null
940 ? "Connect an environment to see diagnostics."
941 : diagnosticsAccess.isPending
942 ? "Checking diagnostics access…"
943 : diagnosticsAccess.error}
944 </p>
945 </SettingsPageContainer>
946 );
947 }
948
949 return (
950 <SettingsPageContainer width="expanded" className="gap-10">
951 <ResourceTelemetryDiagnostics environmentId={environmentId} />
952
953 <SettingsSection
954 title="Live Processes"
955 headerAction={
956 <div className="flex items-center gap-1.5">
957 <DiagnosticsLastChecked checkedAt={processData?.readAt ?? null} />
958 <DiagnosticsRefreshButton
959 isPending={isProcessPending}
960 label="Refresh process diagnostics"
961 onClick={refreshProcesses}
962 />
963 </div>
964 }
965 >
966 <StatsGrid>
967 <StatBlock
968 label="Child Processes"
969 value={processData ? formatCount(processData.processCount) : "..."}
970 />
971 <StatBlock
972 label="CPU"
973 value={processData ? `${processData.totalCpuPercent.toFixed(1)}%` : "..."}
974 tooltip="Total CPU across live child processes of the current server process. The desktop shell and other parent processes are not included."
975 />
976 <StatBlock
977 label="Memory"
978 value={processData ? formatBytes(processData.totalRssBytes) : "..."}
979 tooltip="Total resident memory across live child processes of the current server process. The desktop shell and other parent processes are not included."
980 />
981 <StatBlock
982 label="Server PID"
983 value={processData ? String(processData.serverPid) : "..."}
984 />
985 </StatsGrid>
986 {processDiagnosticsError || processError ? (
987 <div className="space-y-2 border-t border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-5">
988 {processDiagnosticsError ? (
989 <div className="flex items-start gap-2 text-destructive">
990 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
991 <span>{processDiagnosticsError.message}</span>
992 </div>
993 ) : null}
994 {processError ? (
995 <div className="flex items-start gap-2 text-destructive">
996 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
997 <span>{processError}</span>
998 </div>
999 ) : null}
1000 </div>
1001 ) : null}
1002 <ProcessDiagnosticsTable
1003 processes={processData?.processes ?? []}
1004 canMaintainEnvironment={canMaintainEnvironment}
1005 signalingPid={signalingPid}
1006 onSignal={signalProcess}
1007 emptyLabel={
1008 isProcessInitialLoading
1009 ? "Loading live processes..."
1010 : "No live descendant processes found."
1011 }
1012 />
1013 </SettingsSection>
1014
1015 <SettingsSection
1016 title="Resource History"
1017 headerAction={
1018 <div className="flex items-center gap-1.5">
1019 <ResourceHistoryWindowSelector
1020 selectedWindowMs={resourceWindowMs}
1021 onSelect={setResourceWindowMs}
1022 />
1023 <DiagnosticsLastChecked checkedAt={resourceData?.readAt ?? null} />
1024 <DiagnosticsRefreshButton
1025 isPending={isResourcePending}
1026 label="Refresh resource history"
1027 onClick={refreshResources}
1028 />
1029 </div>
1030 }
1031 >
1032 <StatsGrid>
1033 <StatBlock
1034 label="CPU Time"
1035 value={resourceData ? formatCpuTime(resourceData.totalCpuSecondsApprox) : "..."}
1036 tooltip="Approximate active CPU time for the T3 server root process and its descendants during the selected window. It grows only while sampled processes use CPU and older samples leave as the window moves."
1037 />
1038 <StatBlock
1039 label="Samples"
1040 value={resourceData ? formatCount(resourceData.retainedSampleCount) : "..."}
1041 tooltip="In-memory process samples retained by the server. The server samples only while diagnostics is open, and this resets when the server restarts."
1042 />
1043 <StatBlock
1044 label="Interval"
1045 value={
1046 resourceData
1047 ? resourceData.sampleIntervalMs === 0
1048 ? "On demand"
1049 : formatDuration(resourceData.sampleIntervalMs)
1050 : "..."
1051 }
1052 />
1053 <StatBlock
1054 label="Processes"
1055 value={resourceData ? formatCount(resourceData.topProcesses.length) : "..."}
1056 />
1057 </StatsGrid>
1058 {processResourceError || resourceError ? (
1059 <div className="space-y-2 border-t border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-5">
1060 {processResourceError ? (
1061 <div className="flex items-start gap-2 text-destructive">
1062 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
1063 <span>{processResourceError.message}</span>
1064 </div>
1065 ) : null}
1066 {resourceError ? (
1067 <div className="flex items-start gap-2 text-destructive">
1068 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
1069 <span>{resourceError}</span>
1070 </div>
1071 ) : null}
1072 </div>
1073 ) : null}
1074 <ProcessResourceHistoryChart buckets={resourceData?.buckets ?? []} />
1075 <ProcessResourceHistoryTable
1076 processes={resourceData?.topProcesses ?? []}
1077 emptyLabel={
1078 isResourceInitialLoading
1079 ? "Collecting process resource samples..."
1080 : "No process resource samples found for this window."
1081 }
1082 />
1083 </SettingsSection>
1084
1085 <SettingsSection
1086 title="Trace Diagnostics"
1087 headerAction={
1088 <div className="flex items-center gap-1.5">
1089 <DiagnosticsLastChecked checkedAt={data?.readAt ?? null} />
1090 <Tooltip>
1091 <TooltipTrigger
1092 render={
1093 <Button
1094 size="icon-xs"
1095 variant="ghost-muted"
1096 disabled={
1097 !canOpenHostEditor ||
1098 !observability?.logsDirectoryPath ||
1099 isOpeningLogsDirectory
1100 }
1101 onClick={openLogsDirectory}
1102 aria-label="Open logs folder"
1103 >
1104 <FolderOpenIcon />
1105 </Button>
1106 }
1107 />
1108 <TooltipPopup side="top">Open logs folder</TooltipPopup>
1109 </Tooltip>
1110 <DiagnosticsRefreshButton
1111 isPending={isPending}
1112 label="Refresh trace diagnostics"
1113 onClick={refresh}
1114 />
1115 </div>
1116 }
1117 >
1118 <StatsGrid>
1119 <StatBlock label="Spans" value={data ? formatCount(data.recordCount) : "..."} />
1120 <StatBlock
1121 label="Failures"
1122 value={data ? formatCount(data.failureCount) : "..."}
1123 tone={data && data.failureCount > 0 ? "danger" : "default"}
1124 />
1125 <StatBlock
1126 label="Slow Spans"
1127 value={data ? formatCount(data.slowSpanCount) : "..."}
1128 tooltip={
1129 data
1130 ? `Spans with a duration of ${formatDuration(data.slowSpanThresholdMs)} or longer.`
1131 : "Spans at or above the configured slow-span threshold."
1132 }
1133 tone={data && data.slowSpanCount > 0 ? "warning" : "default"}
1134 />
1135 <StatBlock
1136 label="Parse Errors"
1137 value={data ? formatCount(data.parseErrorCount) : "..."}
1138 tone={data && data.parseErrorCount > 0 ? "warning" : "default"}
1139 />
1140 </StatsGrid>
1141 {openLogsDirectoryError || traceDiagnosticsError || error ? (
1142 <div className="space-y-2 border-t border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-5">
1143 {openLogsDirectoryError ? (
1144 <div className="flex items-start gap-2 text-destructive">
1145 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
1146 <span>{openLogsDirectoryError}</span>
1147 </div>
1148 ) : null}
1149 {traceDiagnosticsError ? (
1150 <div
1151 className={cn(
1152 "flex items-start gap-2",
1153 traceDiagnosticsPartialFailure ? "text-warning-foreground" : "text-destructive",
1154 )}
1155 >
1156 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
1157 <span>
1158 {traceDiagnosticsPartialFailure
1159 ? `Some trace files could not be read, so diagnostics may be incomplete. ${traceDiagnosticsError.message}`
1160 : traceDiagnosticsError.message}
1161 </span>
1162 </div>
1163 ) : null}
1164 {error ? (
1165 <div className="flex items-start gap-2 text-destructive">
1166 <AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0" />
1167 <span>{error}</span>
1168 </div>
1169 ) : null}
1170 </div>
1171 ) : null}
1172 </SettingsSection>
1173
1174 <SettingsSection title="Latest Failures">
1175 {data && data.latestFailures.length > 0 ? (
1176 <DiagnosticsTable headers={["Span", "Cause", "Duration", "Ended"]}>
1177 {data.latestFailures.map((failure) => (
1178 <tr key={`${failure.traceId}:${failure.spanId}`}>
1179 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1180 {failure.name}
1181 </td>
1182 <td className="max-w-[360px] px-4 py-3 align-top text-muted-foreground">
1183 <ExpandableText text={failure.cause} />
1184 </td>
1185 <td className="px-4 py-3 align-top font-mono tabular-nums">
1186 {formatDuration(failure.durationMs)}
1187 </td>
1188 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground last:sm:pr-5">
1189 {formatRelativeNoWrap(failure.endedAt)}
1190 </td>
1191 </tr>
1192 ))}
1193 </DiagnosticsTable>
1194 ) : (
1195 <EmptyRows label={isInitialLoading ? "Loading failures..." : "No failed spans found."} />
1196 )}
1197 </SettingsSection>
1198
1199 <SettingsSection title="Most Common Failures">
1200 {data && data.commonFailures.length > 0 ? (
1201 <DiagnosticsTable
1202 headers={["Span", "Count", "Cause", "Last Seen"]}
1203 minTableWidth="min-w-[760px]"
1204 >
1205 {data.commonFailures.map((failure) => (
1206 <tr key={`${failure.name}:${failure.cause}`}>
1207 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1208 {failure.name}
1209 </td>
1210 <td className="px-4 py-3 align-top font-mono tabular-nums">
1211 {formatCount(failure.count)}
1212 </td>
1213 <td className="max-w-[360px] px-4 py-3 align-top text-muted-foreground">
1214 <ExpandableText text={failure.cause} />
1215 </td>
1216 <td className="w-px whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground last:sm:pr-5">
1217 {formatRelativeNoWrap(failure.lastSeenAt)}
1218 </td>
1219 </tr>
1220 ))}
1221 </DiagnosticsTable>
1222 ) : (
1223 <EmptyRows
1224 label={isInitialLoading ? "Loading failure groups..." : "No repeated failures found."}
1225 />
1226 )}
1227 </SettingsSection>
1228
1229 <SettingsSection title="Slowest Spans">
1230 {data && data.slowestSpans.length > 0 ? (
1231 <DiagnosticsTable
1232 headers={["Span", "Duration", "Ended", "Trace"]}
1233 minTableWidth="min-w-[900px]"
1234 columnWidths={["w-[44%]", "w-[14%]", "w-[12%]", "w-[30%]"]}
1235 >
1236 {data.slowestSpans.map((span) => (
1237 <tr key={`${span.traceId}:${span.spanId}`}>
1238 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1239 {span.name}
1240 </td>
1241 <td className="px-4 py-3 align-top font-mono tabular-nums">
1242 {formatDuration(span.durationMs)}
1243 </td>
1244 <td className="w-px whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground">
1245 {formatRelativeNoWrap(span.endedAt)}
1246 </td>
1247 <td className="min-w-0 whitespace-nowrap px-4 py-3 align-top text-muted-foreground last:sm:pr-5">
1248 <TraceIdCell traceId={span.traceId} />
1249 </td>
1250 </tr>
1251 ))}
1252 </DiagnosticsTable>
1253 ) : (
1254 <EmptyRows label={isInitialLoading ? "Loading slow spans..." : "No spans found."} />
1255 )}
1256 </SettingsSection>
1257
1258 <SettingsSection title="Span Logs">
1259 {data && data.latestWarningAndErrorLogs.length > 0 ? (
1260 <ScrollArea
1261 radius="none"
1262 chainVerticalScroll
1263 scrollFade
1264 hideScrollbars
1265 className="w-full max-w-full"
1266 >
1267 <table className="w-full min-w-[920px] table-fixed text-left text-xs">
1268 <colgroup>
1269 <col className="w-[11%]" />
1270 <col className="w-[9%]" />
1271 <col className="w-[24%]" />
1272 <col className="w-[26%]" />
1273 <col className="w-[30%]" />
1274 </colgroup>
1275 <thead className="border-b border-border/60 text-2xs uppercase tracking-widest text-muted-foreground/70">
1276 <tr>
1277 <th className="whitespace-nowrap px-4 py-2.5 font-semibold sm:pl-5">Time</th>
1278 <th className="whitespace-nowrap px-4 py-2.5 font-semibold">Level</th>
1279 <th className="whitespace-nowrap px-4 py-2.5 font-semibold">Span</th>
1280 <th className="whitespace-nowrap px-4 py-2.5 font-semibold">Message</th>
1281 <th className="whitespace-nowrap px-4 py-2.5 font-semibold sm:pr-5">Trace</th>
1282 </tr>
1283 </thead>
1284 <tbody className="divide-y divide-border/60">
1285 {data.latestWarningAndErrorLogs.map((event) => (
1286 <tr
1287 key={`${event.traceId}:${event.spanId}:${DateTime.formatIso(event.seenAt)}:${event.message}`}
1288 className="hover:bg-muted/15"
1289 >
1290 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground sm:pl-5">
1291 {formatRelativeNoWrap(event.seenAt)}
1292 </td>
1293 <td className="px-4 py-3 align-top">
1294 <span className="inline-flex rounded bg-muted px-1.5 py-0.5 font-mono text-2xs font-medium uppercase text-foreground/80">
1295 {event.level}
1296 </span>
1297 </td>
1298 <td className="px-4 py-3 align-top">
1299 <div className="truncate font-medium text-foreground">{event.spanName}</div>
1300 </td>
1301 <td className="px-4 py-3 align-top text-muted-foreground">
1302 <ExpandableText
1303 collapsedClassName="line-clamp-2"
1304 expandLabel="Show full message"
1305 text={event.message}
1306 />
1307 </td>
1308 <td className="min-w-0 whitespace-nowrap px-4 py-3 align-top text-muted-foreground sm:pr-5">
1309 <TraceIdCell traceId={event.traceId} />
1310 </td>
1311 </tr>
1312 ))}
1313 </tbody>
1314 </table>
1315 </ScrollArea>
1316 ) : (
1317 <EmptyRows
1318 label={isInitialLoading ? "Loading recent logs..." : "No warnings or errors found."}
1319 />
1320 )}
1321 </SettingsSection>
1322
1323 <SettingsSection title="Top Span Names">
1324 {data && data.topSpansByCount.length > 0 ? (
1325 <DiagnosticsTable
1326 headers={["Span", "Count", "Failures", "Average", "Max"]}
1327 minTableWidth="min-w-[760px]"
1328 columnWidths={["w-[48%]", "w-[13%]", "w-[13%]", "w-[13%]", "w-[13%]"]}
1329 >
1330 {data.topSpansByCount.map((span) => (
1331 <tr key={span.name}>
1332 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1333 {span.name}
1334 </td>
1335 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums">
1336 {formatCount(span.count)}
1337 </td>
1338 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums">
1339 {formatCount(span.failureCount)}
1340 </td>
1341 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums">
1342 {formatDuration(span.averageDurationMs)}
1343 </td>
1344 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums last:sm:pr-5">
1345 {formatDuration(span.maxDurationMs)}
1346 </td>
1347 </tr>
1348 ))}
1349 </DiagnosticsTable>
1350 ) : (
1351 <EmptyRows label={isInitialLoading ? "Loading span names..." : "No spans found."} />
1352 )}
1353 </SettingsSection>
1354 {exportFormEnvironmentId === environmentId && <TelemetryExportSettings />}
1355 </SettingsPageContainer>
1356 );
1357}