mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
refactor(ui): theme-scale type and shape in trace views, aligned pane bars
Add local/no-arbitrary-design-value, scoped to TraceView and Lens, banning arbitrary font size, tracking, leading, radius, border and CSS property values. Map the Figma-export values onto the theme scale and replace hex colors with info/destructive tokens. Add PaneBar, a fixed-height bordered row, and build the step tree and span detail headers from it so their borders line up across the split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
07eb0dd10f
commit
e7c3092571
26 changed files with 270 additions and 498 deletions
|
|
@ -99,6 +99,11 @@ const eslintConfig = [
|
||||||
],
|
],
|
||||||
rules: { "local/no-ad-hoc-z-index": ["error", { allowPopupLayer: true }] },
|
rules: { "local/no-ad-hoc-z-index": ["error", { allowPopupLayer: true }] },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ["src/components/view_logs/TraceView/**/*.tsx", "src/components/lens/**/*.tsx"],
|
||||||
|
ignores: ["src/**/*.test.tsx"],
|
||||||
|
rules: { "local/no-arbitrary-design-value": "error" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
|
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
|
||||||
rules: { "local/no-noop-hover-variant": "off", "local/no-ad-hoc-z-index": "off" },
|
rules: { "local/no-noop-hover-variant": "off", "local/no-ad-hoc-z-index": "off" },
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
|
||||||
import filenamePascalCase from "./filename-pascal-case.mjs";
|
import filenamePascalCase from "./filename-pascal-case.mjs";
|
||||||
import noNoopHoverVariant from "./no-noop-hover-variant.mjs";
|
import noNoopHoverVariant from "./no-noop-hover-variant.mjs";
|
||||||
import noAdHocZIndex from "./no-ad-hoc-z-index.mjs";
|
import noAdHocZIndex from "./no-ad-hoc-z-index.mjs";
|
||||||
|
import noArbitraryDesignValue from "./no-arbitrary-design-value.mjs";
|
||||||
|
|
||||||
const plugin = {
|
const plugin = {
|
||||||
rules: {
|
rules: {
|
||||||
|
|
@ -13,6 +14,7 @@ const plugin = {
|
||||||
"filename-pascal-case": filenamePascalCase,
|
"filename-pascal-case": filenamePascalCase,
|
||||||
"no-noop-hover-variant": noNoopHoverVariant,
|
"no-noop-hover-variant": noNoopHoverVariant,
|
||||||
"no-ad-hoc-z-index": noAdHocZIndex,
|
"no-ad-hoc-z-index": noAdHocZIndex,
|
||||||
|
"no-arbitrary-design-value": noArbitraryDesignValue,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,7 @@
|
||||||
|
import { utilityOf } from "./tailwind-utility.mjs";
|
||||||
|
|
||||||
const AD_HOC_Z = /^-?z-(?:\d+|\[[^\]]*\]|\([^)]*\))$/;
|
const AD_HOC_Z = /^-?z-(?:\d+|\[[^\]]*\]|\([^)]*\))$/;
|
||||||
|
|
||||||
const OPENERS = { "[": "]", "(": ")" };
|
|
||||||
|
|
||||||
const utilityOf = (token) => {
|
|
||||||
const closers = [];
|
|
||||||
const lastTopLevelColon = [...token].reduce((found, ch, i) => {
|
|
||||||
if (closers.length > 0 && ch === closers[closers.length - 1]) {
|
|
||||||
closers.pop();
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
if (ch in OPENERS) {
|
|
||||||
closers.push(OPENERS[ch]);
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
return ch === ":" && closers.length === 0 ? i : found;
|
|
||||||
}, -1);
|
|
||||||
return token
|
|
||||||
.slice(lastTopLevelColon + 1)
|
|
||||||
.replace(/^!/, "")
|
|
||||||
.replace(/!$/, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
const classify = (token, allowPopupLayer) => {
|
const classify = (token, allowPopupLayer) => {
|
||||||
const utility = utilityOf(token);
|
const utility = utilityOf(token);
|
||||||
if (AD_HOC_Z.test(utility)) return "adHoc";
|
if (AD_HOC_Z.test(utility)) return "adHoc";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { utilityOf } from "./tailwind-utility.mjs";
|
||||||
|
|
||||||
|
const ARBITRARY_SCALE = /^(?:text|tracking|leading|rounded(?:-[a-z]+)?|border(?:-[a-z]+)?)-\[/;
|
||||||
|
const ARBITRARY_PROPERTY = /^\[[a-z-]+:/;
|
||||||
|
|
||||||
|
const isOffending = (token) => {
|
||||||
|
const utility = utilityOf(token);
|
||||||
|
return ARBITRARY_SCALE.test(utility) || ARBITRARY_PROPERTY.test(utility);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
meta: {
|
||||||
|
type: "problem",
|
||||||
|
docs: {
|
||||||
|
description:
|
||||||
|
"Disallow arbitrary font size, tracking, leading, radius and border values, and arbitrary CSS properties. Use the theme scale so surfaces share one type and shape system.",
|
||||||
|
},
|
||||||
|
schema: [],
|
||||||
|
messages: {
|
||||||
|
arbitrary:
|
||||||
|
"`{{token}}` bypasses the theme scale. Use a scale utility (text-xs/sm, leading-*, tracking-*, rounded-sm/md/lg, border/border-2) instead.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create(context) {
|
||||||
|
const check = (node, value) => {
|
||||||
|
if (typeof value !== "string" || !value.includes("[")) return;
|
||||||
|
for (const token of value.split(/\s+/).filter(isOffending)) {
|
||||||
|
context.report({ node, messageId: "arbitrary", data: { token } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
Literal(node) {
|
||||||
|
check(node, node.value);
|
||||||
|
},
|
||||||
|
TemplateElement(node) {
|
||||||
|
check(node, node.value.cooked);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default rule;
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
const OPENERS = { "[": "]", "(": ")" };
|
||||||
|
|
||||||
|
export const utilityOf = (token) => {
|
||||||
|
const closers = [];
|
||||||
|
const lastTopLevelColon = [...token].reduce((found, ch, i) => {
|
||||||
|
if (closers.length > 0 && ch === closers[closers.length - 1]) {
|
||||||
|
closers.pop();
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
if (ch in OPENERS) {
|
||||||
|
closers.push(OPENERS[ch]);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
return ch === ":" && closers.length === 0 ? i : found;
|
||||||
|
}, -1);
|
||||||
|
return token
|
||||||
|
.slice(lastTopLevelColon + 1)
|
||||||
|
.replace(/^!/, "")
|
||||||
|
.replace(/!$/, "");
|
||||||
|
};
|
||||||
|
|
@ -26,16 +26,16 @@ import { scopeLabel } from "../model/format";
|
||||||
import { type Finding, type Lens } from "../model/types";
|
import { type Finding, type Lens } from "../model/types";
|
||||||
import { useListSearchRoute } from "../route";
|
import { useListSearchRoute } from "../route";
|
||||||
|
|
||||||
const PRIORITY_COLOR = { high: "text-[#e5484d]", medium: "text-amber-500", low: "text-muted-foreground" } as const;
|
const PRIORITY_COLOR = { high: "text-destructive", medium: "text-amber-500", low: "text-muted-foreground" } as const;
|
||||||
const ROW =
|
const ROW =
|
||||||
"border-b border-border/60 transition-colors duration-150 hover:bg-trace-row-hover motion-reduce:transition-none";
|
"border-b border-border/60 transition-colors duration-150 hover:bg-trace-row-hover motion-reduce:transition-none";
|
||||||
const META = "truncate text-[11px] text-muted-foreground";
|
const META = "truncate text-xs text-muted-foreground";
|
||||||
|
|
||||||
function JobIcon({ lens }: { lens: Lens }) {
|
function JobIcon({ lens }: { lens: Lens }) {
|
||||||
const status = lens.jobs[0]?.status;
|
const status = lens.jobs[0]?.status;
|
||||||
const className = "size-4 shrink-0";
|
const className = "size-4 shrink-0";
|
||||||
if (status === "queued" || status === "running")
|
if (status === "queued" || status === "running")
|
||||||
return <CircleDashed aria-hidden="true" className={cn(className, "text-[#3b5bfd]")} />;
|
return <CircleDashed aria-hidden="true" className={cn(className, "text-info")} />;
|
||||||
if (status === "failed") return <CircleX aria-hidden="true" className={cn(className, "text-destructive")} />;
|
if (status === "failed") return <CircleX aria-hidden="true" className={cn(className, "text-destructive")} />;
|
||||||
if (status === "cancelled")
|
if (status === "cancelled")
|
||||||
return <CircleSlash aria-hidden="true" className={cn(className, "text-muted-foreground")} />;
|
return <CircleSlash aria-hidden="true" className={cn(className, "text-muted-foreground")} />;
|
||||||
|
|
@ -47,7 +47,7 @@ function TwoLine({ meta, title, className }: { meta: ReactNode; title: ReactNode
|
||||||
return (
|
return (
|
||||||
<span className="flex min-w-0 flex-col gap-0.5">
|
<span className="flex min-w-0 flex-col gap-0.5">
|
||||||
<span className={META}>{meta}</span>
|
<span className={META}>{meta}</span>
|
||||||
<span className={cn("truncate text-[13px] text-foreground", className)}>{title}</span>
|
<span className={cn("truncate text-sm text-foreground", className)}>{title}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ export function InvestigationList({
|
||||||
<Input
|
<Input
|
||||||
aria-label="Search investigations"
|
aria-label="Search investigations"
|
||||||
placeholder="Search investigations"
|
placeholder="Search investigations"
|
||||||
className="h-7 bg-background pl-8 text-[12px]"
|
className="h-7 bg-background pl-8 text-xs"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -149,7 +149,7 @@ export function InvestigationList({
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[180px] truncate px-3 text-right text-[12px]",
|
"w-[180px] truncate px-3 text-right text-xs",
|
||||||
latest?.status === "failed" ? "text-destructive" : "text-muted-foreground",
|
latest?.status === "failed" ? "text-destructive" : "text-muted-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -159,14 +159,14 @@ export function InvestigationList({
|
||||||
{findings.length > 0 && (
|
{findings.length > 0 && (
|
||||||
<span
|
<span
|
||||||
title={`${findings.length} open ${findings.length === 1 ? "finding" : "findings"}`}
|
title={`${findings.length} open ${findings.length === 1 ? "finding" : "findings"}`}
|
||||||
className="inline-flex min-w-5 justify-center rounded-full bg-muted px-1.5 font-mono text-[11px] tabular-nums text-foreground"
|
className="inline-flex min-w-5 justify-center rounded-full bg-muted px-1.5 font-mono text-xs tabular-nums text-foreground"
|
||||||
>
|
>
|
||||||
{findings.length}
|
{findings.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="w-[120px] px-3 text-right text-[12px] text-muted-foreground"
|
className="w-[120px] px-3 text-right text-xs text-muted-foreground"
|
||||||
title={latest ? formatActivityTimestamp(latest.created_at) : undefined}
|
title={latest ? formatActivityTimestamp(latest.created_at) : undefined}
|
||||||
>
|
>
|
||||||
{latest ? agoLabel(Date.parse(latest.created_at), now) : "never run"}
|
{latest ? agoLabel(Date.parse(latest.created_at), now) : "never run"}
|
||||||
|
|
@ -227,12 +227,12 @@ export function InvestigationList({
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 text-right text-[12px] text-muted-foreground">
|
<td className="px-3 text-right text-xs text-muted-foreground">
|
||||||
{runs} {runs === 1 ? "run" : "runs"}
|
{runs} {runs === 1 ? "run" : "runs"}
|
||||||
</td>
|
</td>
|
||||||
<td />
|
<td />
|
||||||
<td
|
<td
|
||||||
className="px-3 text-right text-[12px] text-muted-foreground"
|
className="px-3 text-right text-xs text-muted-foreground"
|
||||||
title={formatActivityTimestamp(finding.last_seen)}
|
title={formatActivityTimestamp(finding.last_seen)}
|
||||||
>
|
>
|
||||||
{agoLabel(Date.parse(finding.last_seen), now)}
|
{agoLabel(Date.parse(finding.last_seen), now)}
|
||||||
|
|
@ -249,12 +249,10 @@ export function InvestigationList({
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{!shown.length && (
|
{!shown.length && (
|
||||||
<div className="py-16 text-center text-[12px] text-muted-foreground">
|
<div className="py-16 text-center text-xs text-muted-foreground">No investigations match your search.</div>
|
||||||
No investigations match your search.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<footer className="flex h-8 shrink-0 items-center border-t border-border bg-muted/40 px-3 font-mono text-[11px] text-muted-foreground">
|
<footer className="flex h-8 shrink-0 items-center border-t border-border bg-muted/40 px-3 font-mono text-xs text-muted-foreground">
|
||||||
{shown.length} {shown.length === 1 ? "investigation" : "investigations"} ·{" "}
|
{shown.length} {shown.length === 1 ? "investigation" : "investigations"} ·{" "}
|
||||||
{lenses.filter((l) => l.settings.enabled).length} watching
|
{lenses.filter((l) => l.settings.enabled).length} watching
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ import type { Job } from "../model/types";
|
||||||
const STEP_MARK = { model: "✓", stage: "▸", error: "!" } as const;
|
const STEP_MARK = { model: "✓", stage: "▸", error: "!" } as const;
|
||||||
const STEP_TONE = {
|
const STEP_TONE = {
|
||||||
model: "text-foreground",
|
model: "text-foreground",
|
||||||
stage: "text-[#0011b3] dark:text-[#8b9bff]",
|
stage: "text-info",
|
||||||
error: "text-[#e5484d]",
|
error: "text-destructive",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function StepFeed({ job }: { job: Job }) {
|
export function StepFeed({ job }: { job: Job }) {
|
||||||
|
|
@ -21,7 +21,7 @@ export function StepFeed({ job }: { job: Job }) {
|
||||||
const models = modelsUsed(steps);
|
const models = modelsUsed(steps);
|
||||||
const calls = steps.filter((s) => s.kind === "model").length;
|
const calls = steps.filter((s) => s.kind === "model").length;
|
||||||
return (
|
return (
|
||||||
<section aria-label="Live investigation steps" className="rounded-md border bg-background font-mono text-[11px]">
|
<section aria-label="Live investigation steps" className="rounded-md border bg-background font-mono text-xs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
|
|
|
||||||
|
|
@ -1,303 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import moment from "moment";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { HeaderActions } from "@/components/lens/HeaderActions";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
import { AgentTracesTable } from "./AgentTracesTable";
|
|
||||||
import { RunDrawer } from "./RunDrawer";
|
|
||||||
import { ALL_AGENTS, RunsToolbar, type RunStatusFilter } from "./RunsToolbar";
|
|
||||||
import {
|
|
||||||
type TraceRef,
|
|
||||||
traceKey,
|
|
||||||
traceRefOf,
|
|
||||||
useOpenTraceRouting,
|
|
||||||
useRunFilterRouting,
|
|
||||||
useZoomRouting,
|
|
||||||
} from "./traceRouting";
|
|
||||||
import type { TraceSummary } from "./traceTypes";
|
|
||||||
import { previewText, traceAgentNames } from "./traceUtils";
|
|
||||||
import { TimeRangeControls } from "./TimeRangeControls";
|
|
||||||
import { TracesTimeline, type TimeWindow } from "./TracesTimeline";
|
|
||||||
import { ActiveDot } from "./ActiveDot";
|
|
||||||
import { TracingSetupCard } from "./TracingSetupCard";
|
|
||||||
import { useTracesLive } from "./tracesApi";
|
|
||||||
import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces";
|
|
||||||
|
|
||||||
/** Client-side search (input text or trace id) plus agent / status filters over the loaded runs. */
|
|
||||||
export function filterRuns(
|
|
||||||
runs: TraceSummary[],
|
|
||||||
query: string,
|
|
||||||
agent: string,
|
|
||||||
status: RunStatusFilter,
|
|
||||||
): TraceSummary[] {
|
|
||||||
const q = query.trim().toLowerCase();
|
|
||||||
return runs.filter((run) => {
|
|
||||||
const haystack = [run.trace_id, previewText(run.input_preview), run.name].map((s) => s.toLowerCase());
|
|
||||||
const matchesQuery = !q || haystack.some((text) => text.includes(q));
|
|
||||||
const matchesAgent = agent === ALL_AGENTS || traceAgentNames(run).includes(agent);
|
|
||||||
const failed = run.error_count > 0;
|
|
||||||
const matchesStatus = status === "all" || (status === "error" ? failed : !failed);
|
|
||||||
return matchesQuery && matchesAgent && matchesStatus;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterByWindow = (runs: TraceSummary[], range: TimeWindow): TraceSummary[] =>
|
|
||||||
runs.filter((run) => {
|
|
||||||
const t = moment(run.start_time).valueOf();
|
|
||||||
return t >= range.startMs && t < range.endMs;
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface TimeControls {
|
|
||||||
rangeHours: number;
|
|
||||||
onRangeHoursChange: (hours: number) => void;
|
|
||||||
onLiveChange: (live: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AgentTracesSectionProps {
|
|
||||||
accessToken: string;
|
|
||||||
isActive: boolean;
|
|
||||||
startTime: string;
|
|
||||||
endTime: string;
|
|
||||||
isCustomDate: boolean;
|
|
||||||
isLiveTail: boolean;
|
|
||||||
/** Page-owned time range + live state; when given, the toolbar shows the range / Live control group. */
|
|
||||||
timeControls?: TimeControls;
|
|
||||||
readOnly?: boolean;
|
|
||||||
canMintTracingKey?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useTracingSetup(traces: AgentTracesResult, isActive: boolean, rangeChanged: boolean) {
|
|
||||||
const [setupResult, setSetupResult] = useState<{ detail: string | null } | null>(null);
|
|
||||||
const waitingForFirstTrace = traces.traces.length === 0 && !rangeChanged;
|
|
||||||
const settledResponse = isActive && !traces.isFetching && !traces.error;
|
|
||||||
const rememberSetup = waitingForFirstTrace || setupResult !== null;
|
|
||||||
if (settledResponse && rememberSetup && setupResult?.detail !== traces.notEnabledDetail) {
|
|
||||||
setSetupResult({ detail: traces.notEnabledDetail });
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabledDetail = traces.notEnabledDetail ?? (traces.isFetching ? setupResult?.detail : null);
|
|
||||||
const loadingFirstPage = traces.isLoading && setupResult === null;
|
|
||||||
const isEmpty = !loadingFirstPage && !traces.error && traces.traces.length === 0;
|
|
||||||
return { disabledDetail, isEmpty, received: setupResult !== null && traces.traces.length > 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function TraceHistoryError({ history }: { history: ReturnType<typeof useTraceAvailability> }) {
|
|
||||||
if (!history.error) return null;
|
|
||||||
return (
|
|
||||||
<div role="alert" className="flex items-center justify-between gap-4 border-b px-3 py-3 text-sm">
|
|
||||||
<p>Could not check earlier traces. {history.error.message}</p>
|
|
||||||
<Button variant="outline" size="sm" disabled={history.isFetching} onClick={() => void history.refetch()}>
|
|
||||||
Retry trace check
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The Runs view: filters, the runs table and footer — or one run, in place, once a row is clicked. */
|
|
||||||
export function AgentTracesSection({
|
|
||||||
accessToken,
|
|
||||||
isActive,
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
isCustomDate,
|
|
||||||
isLiveTail,
|
|
||||||
timeControls,
|
|
||||||
readOnly = false,
|
|
||||||
canMintTracingKey = false,
|
|
||||||
}: AgentTracesSectionProps) {
|
|
||||||
const live = useTracesLive();
|
|
||||||
const { trace: openTrace, openTrace: openRun, selection, fullScreen, setFullScreen } = useOpenTraceRouting();
|
|
||||||
const { query, agent, status, setQuery, setAgent, setStatus } = useRunFilterRouting();
|
|
||||||
const [showSetup, setShowSetup] = useState(false);
|
|
||||||
const [zoom, setZoom] = useZoomRouting();
|
|
||||||
const [rangeChanged, setRangeChanged] = useState(false);
|
|
||||||
const traceQuery = { accessToken, startTime, endTime, isCustomDate, isLiveTail, enabled: isActive };
|
|
||||||
const traces = useAgentTraces(traceQuery);
|
|
||||||
const setup = useTracingSetup(traces, isActive, rangeChanged);
|
|
||||||
const checkHistory = setup.isEmpty && !rangeChanged;
|
|
||||||
const history = useTraceAvailability(accessToken, isActive && checkHistory && setup.disabledDetail == null);
|
|
||||||
|
|
||||||
const checkTraces = () => {
|
|
||||||
traces.refetch();
|
|
||||||
if (setup.disabledDetail == null) void history.refetch();
|
|
||||||
};
|
|
||||||
|
|
||||||
const agents = useMemo(() => Array.from(new Set(traces.traces.flatMap(traceAgentNames))).sort(), [traces.traces]);
|
|
||||||
// Relative ranges end "now" (the list query uses Date.now() too); round to the minute so the histogram is stable.
|
|
||||||
const endMs = isCustomDate ? moment(endTime).valueOf() : moment().endOf("minute").valueOf();
|
|
||||||
const range = useMemo(
|
|
||||||
() => ({ startMs: traceWindowStartMs(startTime, endTime, isCustomDate, endMs), endMs }),
|
|
||||||
[startTime, endTime, isCustomDate, endMs],
|
|
||||||
);
|
|
||||||
const filtered = useMemo(
|
|
||||||
() => filterRuns(traces.traces, query, agent, status),
|
|
||||||
[traces.traces, query, agent, status],
|
|
||||||
);
|
|
||||||
const runs = useMemo(() => (zoom ? filterByWindow(filtered, zoom) : filtered), [filtered, zoom]);
|
|
||||||
|
|
||||||
const changeRange = (hours: number, apply: (hours: number) => void) => {
|
|
||||||
setZoom(null);
|
|
||||||
setRangeChanged(true);
|
|
||||||
apply(hours);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSentTrace = (trace: TraceSummary) => {
|
|
||||||
setShowSetup(false);
|
|
||||||
setRangeChanged(true);
|
|
||||||
checkTraces();
|
|
||||||
openRun(traceRefOf(trace));
|
|
||||||
};
|
|
||||||
const setupProps = {
|
|
||||||
accessToken,
|
|
||||||
readOnly,
|
|
||||||
canMintTracingKey,
|
|
||||||
onOpenTrace: openSentTrace,
|
|
||||||
onCheck: checkTraces,
|
|
||||||
checking: traces.isFetching,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (setup.disabledDetail != null) return <TracingSetupCard detail={setup.disabledDetail} {...setupProps} />;
|
|
||||||
// Onboarding only on the first, default view; an empty range the user picked keeps its controls.
|
|
||||||
if (checkHistory && !history.error && history.data === false)
|
|
||||||
return <TracingSetupCard detail={null} {...setupProps} />;
|
|
||||||
if (showSetup) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowSetup(false)}
|
|
||||||
className="mb-3 text-[13px] text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
← Back to traces
|
|
||||||
</button>
|
|
||||||
<TracingSetupCard detail={null} connected {...setupProps} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleRun = (trace: TraceSummary | null) => {
|
|
||||||
const ref: TraceRef | null = trace && traceRefOf(trace);
|
|
||||||
openRun(ref !== null && openTrace !== null && traceKey(ref) === traceKey(openTrace) ? null : ref);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[560px] flex-1 flex-col overflow-hidden bg-card">
|
|
||||||
{checkHistory && <TraceHistoryError history={history} />}
|
|
||||||
<TracesReceived received={setup.received} />
|
|
||||||
<TracingSetupAction live={live} isActive={isActive} onClick={() => setShowSetup(true)} />
|
|
||||||
<RunDrawer
|
|
||||||
trace={openTrace}
|
|
||||||
runs={runs}
|
|
||||||
accessToken={accessToken}
|
|
||||||
selection={selection}
|
|
||||||
onSelect={openRun}
|
|
||||||
fullScreen={fullScreen}
|
|
||||||
onFullScreenChange={setFullScreen}
|
|
||||||
/>
|
|
||||||
<RunsToolbar
|
|
||||||
query={query}
|
|
||||||
agent={agent}
|
|
||||||
status={status}
|
|
||||||
agents={agents}
|
|
||||||
onQueryChange={setQuery}
|
|
||||||
onAgentChange={setAgent}
|
|
||||||
onStatusChange={setStatus}
|
|
||||||
>
|
|
||||||
{timeControls && (
|
|
||||||
<TimeRangeControls
|
|
||||||
range={zoom ?? range}
|
|
||||||
rangeHours={timeControls.rangeHours}
|
|
||||||
onRangeHoursChange={(hours) => changeRange(hours, timeControls.onRangeHoursChange)}
|
|
||||||
live={isLiveTail}
|
|
||||||
showLive={live}
|
|
||||||
onLiveChange={timeControls.onLiveChange}
|
|
||||||
onRefresh={() => {
|
|
||||||
setZoom(null);
|
|
||||||
checkTraces();
|
|
||||||
}}
|
|
||||||
refreshing={traces.isFetching}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</RunsToolbar>
|
|
||||||
<TracesTimeline runs={filtered} range={range} selection={zoom} onSelect={setZoom} />
|
|
||||||
<AgentTracesTable
|
|
||||||
traces={runs}
|
|
||||||
isLoading={traces.isLoading || (checkHistory && history.isLoading)}
|
|
||||||
error={traces.error}
|
|
||||||
hasMore={traces.hasMore}
|
|
||||||
isFetching={traces.isFetching}
|
|
||||||
onRetry={traces.hasMore ? traces.loadMore : traces.refetch}
|
|
||||||
onLoadMore={traces.loadMore}
|
|
||||||
onOpenTrace={toggleRun}
|
|
||||||
selectedKey={openTrace === null ? null : traceKey(openTrace)}
|
|
||||||
/>
|
|
||||||
<RunsFooter
|
|
||||||
count={runs.length}
|
|
||||||
zoom={zoom}
|
|
||||||
isFetching={traces.isFetching}
|
|
||||||
failed={!!traces.error}
|
|
||||||
onResetZoom={() => setZoom(null)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TracingSetupAction({ live, isActive, onClick }: { live: boolean; isActive: boolean; onClick: () => void }) {
|
|
||||||
if (!live || !isActive) return null;
|
|
||||||
return (
|
|
||||||
<HeaderActions>
|
|
||||||
<Button variant="outline" size="sm" onClick={onClick} className="shrink-0 gap-1.5">
|
|
||||||
<ActiveDot />
|
|
||||||
Set up tracing
|
|
||||||
</Button>
|
|
||||||
</HeaderActions>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TracesReceived({ received }: { received: boolean }) {
|
|
||||||
if (!received) return null;
|
|
||||||
return (
|
|
||||||
<p role="status" className="border-b px-3 py-3 text-sm text-emerald-700 dark:text-emerald-400">
|
|
||||||
Traces received. Select a run to inspect it.
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RunsFooter({
|
|
||||||
count,
|
|
||||||
zoom,
|
|
||||||
isFetching,
|
|
||||||
failed,
|
|
||||||
onResetZoom,
|
|
||||||
}: {
|
|
||||||
count: number;
|
|
||||||
zoom: TimeWindow | null;
|
|
||||||
isFetching: boolean;
|
|
||||||
failed: boolean;
|
|
||||||
onResetZoom: () => void;
|
|
||||||
}) {
|
|
||||||
const settled = failed ? "Update failed" : "Updated just now";
|
|
||||||
const status = isFetching ? "Updating…" : settled;
|
|
||||||
return (
|
|
||||||
<footer
|
|
||||||
data-testid="runs-footer"
|
|
||||||
className="flex h-8 shrink-0 items-center border-t border-border bg-muted/40 px-3 font-mono text-[11px] text-muted-foreground"
|
|
||||||
>
|
|
||||||
{count} {count === 1 ? "run" : "runs"}
|
|
||||||
{zoom && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onResetZoom()}
|
|
||||||
aria-label="Clear time zoom"
|
|
||||||
className="ml-3 rounded border border-info/40 bg-info/10 px-1.5 text-info hover:bg-info/20"
|
|
||||||
>
|
|
||||||
{moment(zoom.startMs).format("MMM DD, HH:mm")} to {moment(zoom.endMs).format("MMM DD, HH:mm")} ×
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<span className="ml-auto">{status}</span>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -100,7 +100,7 @@ export function AgentTracesTable({
|
||||||
className="w-full min-w-[900px] table-fixed border-collapse text-left"
|
className="w-full min-w-[900px] table-fixed border-collapse text-left"
|
||||||
>
|
>
|
||||||
<thead className="sticky top-0 z-sticky bg-[color-mix(in_oklab,var(--muted)_40%,var(--card))]">
|
<thead className="sticky top-0 z-sticky bg-[color-mix(in_oklab,var(--muted)_40%,var(--card))]">
|
||||||
<tr className="h-8 border-b border-border text-[10px] tracking-[0.08em] text-muted-foreground uppercase">
|
<tr className="h-8 border-b border-border text-xs tracking-wider text-muted-foreground uppercase">
|
||||||
<th className={`w-[170px] ${TH}`}>
|
<th className={`w-[170px] ${TH}`}>
|
||||||
<span className="inline-flex items-center gap-1 whitespace-nowrap">
|
<span className="inline-flex items-center gap-1 whitespace-nowrap">
|
||||||
Time <ArrowDown className="size-2.5" />
|
Time <ArrowDown className="size-2.5" />
|
||||||
|
|
@ -128,14 +128,14 @@ export function AgentTracesTable({
|
||||||
onClick={() => onOpenTrace(run)}
|
onClick={() => onOpenTrace(run)}
|
||||||
aria-selected={selectedKey === (run.trace_ref || run.trace_id)}
|
aria-selected={selectedKey === (run.trace_ref || run.trace_id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 cursor-pointer border-b border-border/60 text-[12px] transition-colors duration-150 motion-reduce:transition-none",
|
"h-9 cursor-pointer border-b border-border/60 text-xs transition-colors duration-150 motion-reduce:transition-none",
|
||||||
selectedKey === (run.trace_ref || run.trace_id)
|
selectedKey === (run.trace_ref || run.trace_id)
|
||||||
? "bg-trace-row-selected shadow-[inset_2px_0_0_var(--trace-brand)]"
|
? "bg-trace-row-selected shadow-[inset_2px_0_0_var(--trace-brand)]"
|
||||||
: "hover:bg-trace-row-hover",
|
: "hover:bg-trace-row-hover",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
className="px-3 font-mono text-[11px] whitespace-nowrap tabular-nums text-muted-foreground"
|
className="px-3 font-mono text-xs whitespace-nowrap tabular-nums text-muted-foreground"
|
||||||
title={formatActivityTimestamp(run.start_time)}
|
title={formatActivityTimestamp(run.start_time)}
|
||||||
>
|
>
|
||||||
{formatRunTimestamp(run.start_time)}
|
{formatRunTimestamp(run.start_time)}
|
||||||
|
|
@ -149,13 +149,13 @@ export function AgentTracesTable({
|
||||||
</span>
|
</span>
|
||||||
{run.resolution_limited && (
|
{run.resolution_limited && (
|
||||||
<span
|
<span
|
||||||
className="shrink-0 text-[10px] text-muted-foreground"
|
className="shrink-0 text-xs text-muted-foreground"
|
||||||
title="This run is too large to calculate all totals in this view"
|
title="This run is too large to calculate all totals in this view"
|
||||||
>
|
>
|
||||||
Partial totals
|
Partial totals
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="hidden shrink-0 font-mono text-[10px] text-muted-foreground 2xl:inline">
|
<span className="hidden shrink-0 font-mono text-xs text-muted-foreground 2xl:inline">
|
||||||
{run.trace_id}
|
{run.trace_id}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -170,7 +170,7 @@ export function AgentTracesTable({
|
||||||
{run.error_count > 0 ? (
|
{run.error_count > 0 ? (
|
||||||
<StatusMark status="error" count={run.error_count} />
|
<StatusMark status="error" count={run.error_count} />
|
||||||
) : (
|
) : (
|
||||||
<span className="font-mono text-[11px] text-muted-foreground/60">0</span>
|
<span className="font-mono text-xs text-muted-foreground/60">0</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -182,9 +182,9 @@ export function AgentTracesTable({
|
||||||
PLACEHOLDER_ROWS.map((row) => <PlaceholderRow key={row} rowRef={row === 0 ? tailRef : undefined} />)}
|
PLACEHOLDER_ROWS.map((row) => <PlaceholderRow key={row} rowRef={row === 0 ? tailRef : undefined} />)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{isLoading && <div className="py-16 text-center text-[12px] text-muted-foreground">Loading runs…</div>}
|
{isLoading && <div className="py-16 text-center text-xs text-muted-foreground">Loading runs…</div>}
|
||||||
{error && (
|
{error && (
|
||||||
<div role="alert" className="flex items-center justify-center gap-3 py-6 text-[12px] text-muted-foreground">
|
<div role="alert" className="flex items-center justify-center gap-3 py-6 text-xs text-muted-foreground">
|
||||||
<span>
|
<span>
|
||||||
{traces.length ? "Could not load more runs" : "Could not load runs"}: {error.message}
|
{traces.length ? "Could not load more runs" : "Could not load runs"}: {error.message}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -196,16 +196,14 @@ export function AgentTracesTable({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{canContinue && traces.length === 0 && (
|
{canContinue && traces.length === 0 && (
|
||||||
<div className="flex items-center justify-center gap-3 py-16 text-[12px] text-muted-foreground">
|
<div className="flex items-center justify-center gap-3 py-16 text-xs text-muted-foreground">
|
||||||
<span>No loaded runs match these filters.</span>
|
<span>No loaded runs match these filters.</span>
|
||||||
<Button size="xs" variant="outline" disabled={isFetching} onClick={onLoadMore}>
|
<Button size="xs" variant="outline" disabled={isFetching} onClick={onLoadMore}>
|
||||||
Load older runs
|
Load older runs
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isEmpty && (
|
{isEmpty && <div className="py-16 text-center text-xs text-muted-foreground">No runs match these filters.</div>}
|
||||||
<div className="py-16 text-center text-[12px] text-muted-foreground">No runs match these filters.</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface AttributesDetailProps {
|
||||||
export function DetailGroup({ title, children }: { title: string; children: React.ReactNode }) {
|
export function DetailGroup({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<section aria-label={title} className="flex flex-col gap-2">
|
<section aria-label={title} className="flex flex-col gap-2">
|
||||||
<h3 className="py-1 text-[13px] font-medium tracking-[-0.26px] text-trace-text">{title}</h3>
|
<h3 className="py-1 text-sm font-medium text-trace-text">{title}</h3>
|
||||||
<Card className="px-3 py-2.5">{children}</Card>
|
<Card className="px-3 py-2.5">{children}</Card>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
@ -38,7 +38,7 @@ export function AttributesDetail({ traceId, span, attributes, isLoading }: Attri
|
||||||
<KeyValueRows entries={attributeEntries} />
|
<KeyValueRows entries={attributeEntries} />
|
||||||
</DetailGroup>
|
</DetailGroup>
|
||||||
)}
|
)}
|
||||||
{isLoading && <div className="text-[13px] text-trace-duration">Loading attributes…</div>}
|
{isLoading && <div className="text-sm text-trace-duration">Loading attributes…</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,8 @@ import { errorSource, parseJson, parseMessages, prettyPayload } from "./traceUti
|
||||||
|
|
||||||
const ERROR_SOURCE_LABEL: Record<ErrorSource, string> = { tool: "Tool", model: "Model", litellm: "LiteLLM" };
|
const ERROR_SOURCE_LABEL: Record<ErrorSource, string> = { tool: "Tool", model: "Model", litellm: "LiteLLM" };
|
||||||
const TRACEBACK_MARKER = "Traceback (most recent call last):";
|
const TRACEBACK_MARKER = "Traceback (most recent call last):";
|
||||||
const STATUS_TEXT = "px-5 py-2 text-[13px] tracking-[-0.26px] text-muted-foreground";
|
const STATUS_TEXT = "px-3 py-2 text-sm text-muted-foreground";
|
||||||
const PAYLOAD_PRE =
|
const PAYLOAD_PRE = "font-mono text-sm leading-normal break-words whitespace-pre-wrap text-foreground";
|
||||||
"font-mono text-[13px] leading-[1.5] tracking-[-0.26px] break-words whitespace-pre-wrap text-foreground";
|
|
||||||
|
|
||||||
/** Exporters record `repr(exc)` + traceback with no separator; keep the exception line. */
|
/** Exporters record `repr(exc)` + traceback with no separator; keep the exception line. */
|
||||||
export const errorHeadline = (error: string): string =>
|
export const errorHeadline = (error: string): string =>
|
||||||
|
|
@ -46,9 +45,9 @@ export function ErrorBlock({ span }: { span: Span }) {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-label="Error"
|
aria-label="Error"
|
||||||
className="mx-3 mb-3 rounded-[4px] border-[0.67px] border-destructive/40 bg-destructive/5 px-3 py-2.5"
|
className="mx-3 mb-3 rounded-sm border border-destructive/40 bg-destructive/5 px-3 py-2.5"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 text-[13px] leading-[1.2] font-medium tracking-[-0.26px] text-destructive">
|
<div className="flex items-center gap-2 text-sm leading-tight font-medium text-destructive">
|
||||||
<AlertTriangle className="size-3.5" />
|
<AlertTriangle className="size-3.5" />
|
||||||
{ERROR_SOURCE_LABEL[source]} · {errorReason(headline)}
|
{ERROR_SOURCE_LABEL[source]} · {errorReason(headline)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -210,7 +209,7 @@ export function DetailContent({ accessToken, traceId, traceRef, span }: DetailCo
|
||||||
: parseMessages(detail?.input ?? "")?.length;
|
: parseMessages(detail?.input ?? "")?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col px-2 pt-1 pb-4">
|
<div className="flex flex-col pt-1 pb-4">
|
||||||
<ErrorBlock span={span} />
|
<ErrorBlock span={span} />
|
||||||
{span.error && (
|
{span.error && (
|
||||||
<DiagnosticContent
|
<DiagnosticContent
|
||||||
|
|
@ -239,9 +238,7 @@ export function DetailContent({ accessToken, traceId, traceRef, span }: DetailCo
|
||||||
</Section>
|
</Section>
|
||||||
) : null}
|
) : null}
|
||||||
{empty && span.status !== "error" && (
|
{empty && span.status !== "error" && (
|
||||||
<div className="py-12 text-center text-[13px] tracking-[-0.26px] text-muted-foreground">
|
<div className="py-12 text-center text-sm text-muted-foreground">No content recorded for this span.</div>
|
||||||
No content recorded for this span.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { AttributesDetail } from "./AttributesDetail";
|
||||||
import { CopyButton } from "./CopyButton";
|
import { CopyButton } from "./CopyButton";
|
||||||
import { DetailContent, errorHeadline, useSpanDetail } from "./DetailContent";
|
import { DetailContent, errorHeadline, useSpanDetail } from "./DetailContent";
|
||||||
import { IdChip } from "./IdChip";
|
import { IdChip } from "./IdChip";
|
||||||
|
import { PaneBar } from "./PaneBar";
|
||||||
import { RequestDetail } from "./RequestDetail";
|
import { RequestDetail } from "./RequestDetail";
|
||||||
import { SpanIcon } from "./SpanIcon";
|
import { SpanIcon } from "./SpanIcon";
|
||||||
import { useTracesApi } from "./tracesApi";
|
import { useTracesApi } from "./tracesApi";
|
||||||
|
|
@ -47,7 +48,7 @@ function PaneHeader({
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-7 shrink-0 items-center justify-between gap-3 px-5">
|
<PaneBar className="justify-between gap-3">
|
||||||
<div className="flex min-w-0 items-center">
|
<div className="flex min-w-0 items-center">
|
||||||
<span className="mr-2 shrink-0">
|
<span className="mr-2 shrink-0">
|
||||||
<SpanIcon type={type} model={model} error={failed} size="md" />
|
<SpanIcon type={type} model={model} error={failed} size="md" />
|
||||||
|
|
@ -64,20 +65,16 @@ function PaneHeader({
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
aria-label="Close details"
|
aria-label="Close details"
|
||||||
className="size-6 shrink-0 rounded-[4px] text-muted-foreground"
|
className="size-6 shrink-0 rounded-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
<PanelRightClose className="size-4" />
|
<PanelRightClose className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</PaneBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaneFooter({ children }: { children: React.ReactNode }) {
|
function PaneFooter({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return <div className="flex h-10 shrink-0 items-center gap-3 border-t bg-background px-3">{children}</div>;
|
||||||
<div className="flex min-h-10 shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-t bg-background px-5 py-2">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Meta({ label, value }: { label: string; value: string }) {
|
function Meta({ label, value }: { label: string; value: string }) {
|
||||||
|
|
@ -112,10 +109,7 @@ function SpanPane({
|
||||||
);
|
);
|
||||||
const tokens = span.input_tokens + span.output_tokens;
|
const tokens = span.input_tokens + span.output_tokens;
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside className="flex h-full min-w-0 flex-col bg-background text-sm text-foreground" aria-label="Span details">
|
||||||
className="flex h-full min-w-0 flex-col bg-background pt-4 text-[13px] text-foreground"
|
|
||||||
aria-label="Span details"
|
|
||||||
>
|
|
||||||
<PaneHeader
|
<PaneHeader
|
||||||
type={span.type}
|
type={span.type}
|
||||||
model={span.model}
|
model={span.model}
|
||||||
|
|
@ -125,15 +119,15 @@ function SpanPane({
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
<Tabs value={tab} onValueChange={(value) => onSpanTabChange(value as SpanTab)} className="min-h-0 flex-1 gap-0">
|
<Tabs value={tab} onValueChange={(value) => onSpanTabChange(value as SpanTab)} className="min-h-0 flex-1 gap-0">
|
||||||
<div className="shrink-0 border-b px-5">
|
<PaneBar>
|
||||||
<TabsList variant="line" aria-label="Span detail sections" className="h-10 gap-4 px-0">
|
<TabsList variant="line" aria-label="Span detail sections" className="h-full gap-4 px-0">
|
||||||
{SPAN_TABS.map((id) => (
|
{SPAN_TABS.map((id) => (
|
||||||
<TabsTrigger key={id} value={id} className="flex-none px-0 text-xs">
|
<TabsTrigger key={id} value={id} className="flex-none px-0 text-xs">
|
||||||
{TAB_LABELS[id]}
|
{TAB_LABELS[id]}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</PaneBar>
|
||||||
<TabsContent value="content" className="min-h-0 overflow-auto pt-3">
|
<TabsContent value="content" className="min-h-0 overflow-auto pt-3">
|
||||||
<DetailContent accessToken={accessToken} traceId={traceId} traceRef={trace.summary.trace_ref} span={span} />
|
<DetailContent accessToken={accessToken} traceId={traceId} traceRef={trace.summary.trace_ref} span={span} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
@ -163,8 +157,8 @@ function SpanPane({
|
||||||
function GroupMetric({ label, value }: { label: string; value: string }) {
|
function GroupMetric({ label, value }: { label: string; value: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border/60 py-3">
|
<div className="border-b border-border/60 py-3">
|
||||||
<div className="text-[13px] font-medium text-muted-foreground">{label}</div>
|
<div className="text-sm font-medium text-muted-foreground">{label}</div>
|
||||||
<div className="mt-1 text-[13px] text-foreground tabular-nums">{value}</div>
|
<div className="mt-1 text-sm text-foreground tabular-nums">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -186,10 +180,7 @@ function GroupPane({
|
||||||
const sample = (firstFailure ?? row.members[0]).span_id;
|
const sample = (firstFailure ?? row.members[0]).span_id;
|
||||||
const handoff = useTracesApi(accessToken).handoff(trace.summary.trace_id, sample, trace.summary.trace_ref);
|
const handoff = useTracesApi(accessToken).handoff(trace.summary.trace_id, sample, trace.summary.trace_ref);
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside className="flex h-full min-w-0 flex-col bg-background text-sm text-foreground" aria-label="Group details">
|
||||||
className="flex h-full min-w-0 flex-col bg-background pt-4 text-[13px] text-foreground"
|
|
||||||
aria-label="Group details"
|
|
||||||
>
|
|
||||||
<PaneHeader
|
<PaneHeader
|
||||||
type={row.type}
|
type={row.type}
|
||||||
model={row.members[0]?.model ?? null}
|
model={row.members[0]?.model ?? null}
|
||||||
|
|
@ -201,7 +192,7 @@ function GroupPane({
|
||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
<div className="min-h-0 flex-1 overflow-auto px-5 py-3">
|
<div className="min-h-0 flex-1 overflow-auto px-3 py-3">
|
||||||
<div className="grid grid-cols-2 gap-x-5">
|
<div className="grid grid-cols-2 gap-x-5">
|
||||||
<GroupMetric label="Invocations" value={row.members.length.toLocaleString()} />
|
<GroupMetric label="Invocations" value={row.members.length.toLocaleString()} />
|
||||||
<GroupMetric label="Failed" value={row.failedCount.toLocaleString()} />
|
<GroupMetric label="Failed" value={row.failedCount.toLocaleString()} />
|
||||||
|
|
@ -211,9 +202,9 @@ function GroupPane({
|
||||||
<GroupMetric label="Type" value={row.type} />
|
<GroupMetric label="Type" value={row.type} />
|
||||||
</div>
|
</div>
|
||||||
{firstFailure?.error && (
|
{firstFailure?.error && (
|
||||||
<section className="mt-3 rounded-[4px] border border-destructive/30 px-3.5 py-3">
|
<section className="mt-3 rounded-sm border border-destructive/30 px-3.5 py-3">
|
||||||
<div className="text-[12px] font-medium text-destructive">Failure pattern</div>
|
<div className="text-xs font-medium text-destructive">Failure pattern</div>
|
||||||
<p className="mt-2 font-mono text-[12px] leading-5 text-foreground">{errorHeadline(firstFailure.error)}</p>
|
<p className="mt-2 font-mono text-xs leading-5 text-foreground">{errorHeadline(firstFailure.error)}</p>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -228,7 +219,7 @@ function GroupPane({
|
||||||
export function DetailPane({ trace, row, accessToken, spanTab, onSpanTabChange, onClose }: DetailPaneProps) {
|
export function DetailPane({ trace, row, accessToken, spanTab, onSpanTabChange, onClose }: DetailPaneProps) {
|
||||||
if (!row || row.kind === "load-more") {
|
if (!row || row.kind === "load-more") {
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full place-items-center bg-background text-[13px] text-muted-foreground">
|
<div className="grid h-full place-items-center bg-background text-sm text-muted-foreground">
|
||||||
Select a span to inspect it.
|
Select a span to inspect it.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ function KeyValueRow({ entry, mono }: { entry: KeyValue; mono: boolean }) {
|
||||||
const [key, value] = entry;
|
const [key, value] = entry;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const valueClass = cn(
|
const valueClass = cn(
|
||||||
"min-w-0 flex-1 break-words text-left text-[13px] leading-5 text-foreground",
|
"min-w-0 flex-1 break-words text-left text-sm leading-5 text-foreground",
|
||||||
mono || ID_KEY.test(key) ? "font-mono" : "font-sans",
|
mono || ID_KEY.test(key) ? "font-mono" : "font-sans",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -72,7 +72,7 @@ function KeyValueRow({ entry, mono }: { entry: KeyValue; mono: boolean }) {
|
||||||
<button {...toggle} className="flex shrink-0 cursor-pointer items-baseline gap-2.5 text-left">
|
<button {...toggle} className="flex shrink-0 cursor-pointer items-baseline gap-2.5 text-left">
|
||||||
{head}
|
{head}
|
||||||
</button>
|
</button>
|
||||||
<pre className={cn(valueClass, "leading-[1.5] break-words whitespace-pre-wrap")}>{value}</pre>
|
<pre className={cn(valueClass, "leading-normal break-words whitespace-pre-wrap")}>{value}</pre>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<button {...toggle} className="flex min-w-0 flex-1 cursor-pointer items-baseline gap-2.5 text-left">
|
<button {...toggle} className="flex min-w-0 flex-1 cursor-pointer items-baseline gap-2.5 text-left">
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const markdownComponents: Components = {
|
||||||
<pre
|
<pre
|
||||||
className={cn(
|
className={cn(
|
||||||
BLOCK_GAP,
|
BLOCK_GAP,
|
||||||
"max-w-full overflow-x-auto rounded-[4px] bg-muted/40 p-2.5 font-mono text-[13px] leading-[1.5] tracking-normal",
|
"max-w-full overflow-x-auto rounded-sm bg-muted/40 p-2.5 font-mono text-sm leading-normal tracking-normal",
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -55,7 +55,7 @@ export function Card({ children, className }: { children: React.ReactNode; class
|
||||||
return <section className={cn("rounded-md border border-border bg-background", className)}>{children}</section>;
|
return <section className={cn("rounded-md border border-border bg-background", className)}>{children}</section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TILE = "grid size-4 shrink-0 place-items-center rounded-[2px] p-0.5";
|
const TILE = "grid size-4 shrink-0 place-items-center rounded-sm p-0.5";
|
||||||
|
|
||||||
function RoleTile({ failed = false }: { role: string; failed?: boolean }) {
|
function RoleTile({ failed = false }: { role: string; failed?: boolean }) {
|
||||||
return <Wrench className={cn("size-3.5 shrink-0", failed ? "text-destructive" : "text-muted-foreground")} />;
|
return <Wrench className={cn("size-3.5 shrink-0", failed ? "text-destructive" : "text-muted-foreground")} />;
|
||||||
|
|
@ -83,8 +83,8 @@ const HEADER =
|
||||||
"group/header flex min-w-0 items-center gap-2 border border-border bg-background py-2 pr-2 pl-3 transition-colors hover:bg-muted/30";
|
"group/header flex min-w-0 items-center gap-2 border border-border bg-background py-2 pr-2 pl-3 transition-colors hover:bg-muted/30";
|
||||||
const LABEL = "min-w-0 truncate text-xs leading-5 font-medium text-foreground";
|
const LABEL = "min-w-0 truncate text-xs leading-5 font-medium text-foreground";
|
||||||
const CARD_COPY =
|
const CARD_COPY =
|
||||||
"ml-auto size-5 shrink-0 rounded-[3px] text-muted-foreground opacity-60 transition-opacity duration-150 group-hover/header:opacity-100 focus-visible:opacity-100 motion-reduce:transition-none [&_svg]:size-4";
|
"ml-auto size-5 shrink-0 rounded-sm text-muted-foreground opacity-60 transition-opacity duration-150 group-hover/header:opacity-100 focus-visible:opacity-100 motion-reduce:transition-none [&_svg]:size-4";
|
||||||
const INLINE_COPY = "size-4 shrink-0 rounded-[3px] text-muted-foreground";
|
const INLINE_COPY = "size-4 shrink-0 rounded-sm text-muted-foreground";
|
||||||
|
|
||||||
export function ToolCallBlock({ call }: { call: TraceToolCall }) {
|
export function ToolCallBlock({ call }: { call: TraceToolCall }) {
|
||||||
const argEntries = objectEntries(call.args);
|
const argEntries = objectEntries(call.args);
|
||||||
|
|
@ -111,7 +111,7 @@ export function MessageCard({ message }: { message: TraceMessage; model: string
|
||||||
const expanded = open && hasBody;
|
const expanded = open && hasBody;
|
||||||
return (
|
return (
|
||||||
<article className="rounded-md">
|
<article className="rounded-md">
|
||||||
<div className={cn(HEADER, "sticky top-10 z-sticky", expanded ? "rounded-t-[4px] border-b-0" : "rounded-[4px]")}>
|
<div className={cn(HEADER, "sticky top-10 z-sticky", expanded ? "rounded-t-sm border-b-0" : "rounded-sm")}>
|
||||||
<FoldTile label={label} open={open} onToggle={() => setOpen((v) => !v)} />
|
<FoldTile label={label} open={open} onToggle={() => setOpen((v) => !v)} />
|
||||||
<span className={LABEL}>{label}</span>
|
<span className={LABEL}>{label}</span>
|
||||||
<CopyButton value={copyValue} label={`Copy ${label}`} iconOnly className={CARD_COPY} />
|
<CopyButton value={copyValue} label={`Copy ${label}`} iconOnly className={CARD_COPY} />
|
||||||
|
|
@ -142,7 +142,7 @@ export function ToolResultCard({ name, result, failed = false }: { name: string;
|
||||||
const tone = failed ? "text-destructive" : "text-muted-foreground";
|
const tone = failed ? "text-destructive" : "text-muted-foreground";
|
||||||
return (
|
return (
|
||||||
<article className="flex flex-col">
|
<article className="flex flex-col">
|
||||||
<div className={cn(HEADER, open ? "rounded-t-[4px]" : "rounded-[4px]")}>
|
<div className={cn(HEADER, open ? "rounded-t-sm" : "rounded-sm")}>
|
||||||
<RoleTile role="tool" failed={failed} />
|
<RoleTile role="tool" failed={failed} />
|
||||||
<span className="flex min-w-0 max-w-[50%] shrink-0 items-center gap-1.5">
|
<span className="flex min-w-0 max-w-[50%] shrink-0 items-center gap-1.5">
|
||||||
<span className={cn(LABEL, failed && "text-destructive")}>{name}</span>
|
<span className={cn(LABEL, failed && "text-destructive")}>{name}</span>
|
||||||
|
|
@ -157,17 +157,17 @@ export function ToolResultCard({ name, result, failed = false }: { name: string;
|
||||||
className={cn("flex min-w-0 cursor-pointer items-center gap-1 text-left", tone)}
|
className={cn("flex min-w-0 cursor-pointer items-center gap-1 text-left", tone)}
|
||||||
>
|
>
|
||||||
<FoldChevron open={open} className="size-3 shrink-0" />
|
<FoldChevron open={open} className="size-3 shrink-0" />
|
||||||
{!open && <span className="min-w-0 truncate text-[13px] leading-[1.2]">{result}</span>}
|
{!open && <span className="min-w-0 truncate text-sm leading-tight">{result}</span>}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<span className={cn("min-w-0 break-words text-[13px] leading-5", tone)}>{result || "No output"}</span>
|
<span className={cn("min-w-0 break-words text-sm leading-5", tone)}>{result || "No output"}</span>
|
||||||
)}
|
)}
|
||||||
<CopyButton value={result} label={`Copy ${name} result`} iconOnly className={CARD_COPY} />
|
<CopyButton value={result} label={`Copy ${name} result`} iconOnly className={CARD_COPY} />
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<pre
|
<pre
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-h-80 overflow-auto rounded-b-[4px] border-[0.67px] border-t-0 border-border bg-muted/40 px-3 py-2 font-mono text-[12px] leading-[1.5] break-words whitespace-pre-wrap",
|
"max-h-80 overflow-auto rounded-b-sm border border-t-0 border-border bg-muted/40 px-3 py-2 font-mono text-xs leading-normal break-words whitespace-pre-wrap",
|
||||||
tone,
|
tone,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -197,10 +197,10 @@ export function Section({
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
onClick={() => setOpen((prev) => !prev)}
|
onClick={() => setOpen((prev) => !prev)}
|
||||||
className="sticky top-0 z-sticky-pinned mb-2 flex h-10 w-full cursor-pointer items-center gap-2.5 bg-background py-1 pr-3 pl-2 text-left transition-colors duration-150 hover:bg-muted/40 motion-reduce:transition-none"
|
className="sticky top-0 z-sticky-pinned mb-2 flex h-10 w-full cursor-pointer items-center gap-2.5 bg-background px-3 text-left transition-colors duration-150 hover:bg-muted/40 motion-reduce:transition-none"
|
||||||
>
|
>
|
||||||
<FoldChevron open={open} className="size-3 shrink-0 text-foreground" />
|
<FoldChevron open={open} className="size-3 shrink-0 text-foreground" />
|
||||||
<span className="text-[13px] leading-[1.2] font-medium tracking-[-0.26px] text-foreground">{title}</span>{" "}
|
<span className="text-sm leading-tight font-medium text-foreground">{title}</span>{" "}
|
||||||
{count !== undefined && (
|
{count !== undefined && (
|
||||||
<span className="text-xs font-normal text-muted-foreground">
|
<span className="text-xs font-normal text-muted-foreground">
|
||||||
{count} {count === 1 ? "message" : "messages"}
|
{count} {count === 1 ? "message" : "messages"}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { cn } from "@/lib/cva.config";
|
||||||
|
|
||||||
|
/** Fixed-height pane row; every pane in the run view stacks these so borders line up across the split. */
|
||||||
|
export function PaneBar({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||||
|
return <div className={cn("flex h-10 shrink-0 items-center gap-2 border-b px-3", className)}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ export function RequestDetail({ span, accessToken, traceStartMs }: RequestDetail
|
||||||
const logNotFound = lookupDone && logQuery.data === null;
|
const logNotFound = lookupDone && logQuery.data === null;
|
||||||
|
|
||||||
if (span.type !== "llm") {
|
if (span.type !== "llm") {
|
||||||
return <div className="py-16 text-center text-[13px] text-trace-duration">This span is not a model request.</div>;
|
return <div className="py-16 text-center text-sm text-trace-duration">This span is not a model request.</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const usage: KeyValue[] = [
|
const usage: KeyValue[] = [
|
||||||
|
|
@ -57,15 +57,15 @@ export function RequestDetail({ span, accessToken, traceStartMs }: RequestDetail
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
className="self-start border-trace-border text-[13px] text-trace-text-2 shadow-trace-xs"
|
className="self-start border-trace-border text-sm text-trace-text-2 shadow-trace-xs"
|
||||||
onClick={() => setDrawerOpen(true)}
|
onClick={() => setDrawerOpen(true)}
|
||||||
>
|
>
|
||||||
Open request log <ArrowUpRight className="size-3.5" />
|
Open request log <ArrowUpRight className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
{logNotFound && <p className="text-[13px] text-trace-duration">No request log found for this call.</p>}
|
{logNotFound && <p className="text-sm text-trace-duration">No request log found for this call.</p>}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-[13px] text-trace-duration">Not linked to a LiteLLM request</p>
|
<p className="text-sm text-trace-duration">Not linked to a LiteLLM request</p>
|
||||||
)}
|
)}
|
||||||
</DetailGroup>
|
</DetailGroup>
|
||||||
<LogDetailsDrawer
|
<LogDetailsDrawer
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ export function RunsToolbar({
|
||||||
onChange={(e) => onQueryChange(e.target.value)}
|
onChange={(e) => onQueryChange(e.target.value)}
|
||||||
placeholder="Search input or trace ID"
|
placeholder="Search input or trace ID"
|
||||||
aria-label="Search runs"
|
aria-label="Search runs"
|
||||||
className="h-7 pl-8 text-[12px]"
|
className="h-7 pl-8 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -56,7 +56,7 @@ export function RunsToolbar({
|
||||||
value={agent}
|
value={agent}
|
||||||
onValueChange={(value: string | null) => value !== null && onAgentChange(value)}
|
onValueChange={(value: string | null) => value !== null && onAgentChange(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger size="sm" className="h-7 min-w-[130px] text-[12px]" aria-label="Filter by agent">
|
<SelectTrigger size="sm" className="h-7 min-w-[130px] text-xs" aria-label="Filter by agent">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -72,7 +72,7 @@ export function RunsToolbar({
|
||||||
value={status}
|
value={status}
|
||||||
onValueChange={(value: RunStatusFilter | null) => value !== null && onStatusChange(value)}
|
onValueChange={(value: RunStatusFilter | null) => value !== null && onStatusChange(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger size="sm" className="h-7 min-w-[110px] text-[12px]" aria-label="Filter by status">
|
<SelectTrigger size="sm" className="h-7 min-w-[110px] text-xs" aria-label="Filter by status">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[12px] text-trace-duration">
|
<span className="text-xs text-trace-duration">
|
||||||
{TYPE_LABEL[facts.type]}
|
{TYPE_LABEL[facts.type]}
|
||||||
{facts.model ? ` · ${facts.model}` : ""}
|
{facts.model ? ` · ${facts.model}` : ""}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -150,7 +150,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
|
||||||
<FactSection title="Tags">
|
<FactSection title="Tags">
|
||||||
<ul className="flex flex-wrap gap-1">
|
<ul className="flex flex-wrap gap-1">
|
||||||
{facts.tags.map((tag) => (
|
{facts.tags.map((tag) => (
|
||||||
<li key={tag} className="rounded-[3px] bg-trace-tag px-1 py-0.5 text-trace-duration">
|
<li key={tag} className="rounded-sm bg-trace-tag px-1 py-0.5 text-trace-duration">
|
||||||
{tag}
|
{tag}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -158,7 +158,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
|
||||||
</FactSection>
|
</FactSection>
|
||||||
)}
|
)}
|
||||||
{facts.error && (
|
{facts.error && (
|
||||||
<div className="rounded-[4px] bg-destructive/10 px-2 py-1.5 font-mono text-[12px] break-words text-destructive">
|
<div className="rounded-sm bg-destructive/10 px-2 py-1.5 font-mono text-xs break-words text-destructive">
|
||||||
{errorHeadline(facts.error)}
|
{errorHeadline(facts.error)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -183,7 +183,7 @@ export function SpanHoverCard({ facts, traceStartMs, children }: SpanHoverCardPr
|
||||||
sideOffset={0}
|
sideOffset={0}
|
||||||
alignOffset={0}
|
alignOffset={0}
|
||||||
data-testid="span-hover-card"
|
data-testid="span-hover-card"
|
||||||
className="max-h-[min(520px,calc(100vh-16px))] w-auto max-w-[420px] overflow-y-auto rounded-[6px] border border-trace-border bg-trace-surface p-3 text-[13px] leading-[1.2] tracking-[-0.26px] text-trace-text shadow-trace-md ring-0 transition-opacity duration-150 ease-[cubic-bezier(0,0,0.2,1)] data-closed:animate-none data-ending-style:opacity-0 data-open:animate-none motion-reduce:transition-none"
|
className="max-h-[min(520px,calc(100vh-16px))] w-auto max-w-[420px] overflow-y-auto rounded-md border border-trace-border bg-trace-surface p-3 text-sm leading-tight text-trace-text shadow-trace-md ring-0 transition-opacity duration-150 ease-[cubic-bezier(0,0,0.2,1)] data-closed:animate-none data-ending-style:opacity-0 data-open:animate-none motion-reduce:transition-none"
|
||||||
>
|
>
|
||||||
<HoverCardBody facts={facts} traceStartMs={traceStartMs} />
|
<HoverCardBody facts={facts} traceStartMs={traceStartMs} />
|
||||||
</HoverCardContent>
|
</HoverCardContent>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ import { useSpanProvider } from "./spanProvider";
|
||||||
import type { SpanType } from "./traceTypes";
|
import type { SpanType } from "./traceTypes";
|
||||||
|
|
||||||
const SIZE = {
|
const SIZE = {
|
||||||
sm: "size-4 rounded-[2px]",
|
sm: "size-4 rounded-sm",
|
||||||
card: "size-[18px] rounded-[3px]",
|
card: "size-[18px] rounded-sm",
|
||||||
md: "size-5 rounded-full",
|
md: "size-5 rounded-full",
|
||||||
lg: "size-5 rounded-full",
|
lg: "size-5 rounded-full",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
import { cn } from "@/lib/cva.config";
|
import { cn } from "@/lib/cva.config";
|
||||||
|
|
||||||
import { FoldChevron } from "./Collapse";
|
import { FoldChevron } from "./Collapse";
|
||||||
|
import { PaneBar } from "./PaneBar";
|
||||||
import { groupFacts, SpanHoverCard, spanFacts } from "./SpanHoverCard";
|
import { groupFacts, SpanHoverCard, spanFacts } from "./SpanHoverCard";
|
||||||
import { SpanIcon } from "./SpanIcon";
|
import { SpanIcon } from "./SpanIcon";
|
||||||
import type { GroupRowData, SpanRowData, TreeRow } from "./traceTree";
|
import type { GroupRowData, SpanRowData, TreeRow } from "./traceTree";
|
||||||
|
|
@ -50,9 +51,9 @@ const rowClass = (selected: boolean): string =>
|
||||||
selected ? "border-l-primary bg-accent" : "border-l-transparent hover:bg-muted/60",
|
selected ? "border-l-primary bg-accent" : "border-l-transparent hover:bg-muted/60",
|
||||||
);
|
);
|
||||||
|
|
||||||
const LINE = "border-dashed border-border [border-width:0] [border-left-width:1px]";
|
const LINE = "border-l border-dashed border-border";
|
||||||
const ELBOW = "border-dashed border-border [border-width:0] [border-left-width:1px] [border-bottom-width:1px]";
|
const ELBOW = "border-b border-l border-dashed border-border";
|
||||||
const NAME = "line-clamp-2 break-words text-[13px] leading-[1.2] font-medium";
|
const NAME = "line-clamp-2 break-words text-sm leading-tight font-medium";
|
||||||
const META = "text-xs leading-4 text-muted-foreground tabular-nums";
|
const META = "text-xs leading-4 text-muted-foreground tabular-nums";
|
||||||
|
|
||||||
/** One 20px column per ancestor: pass-through rails for open branches, an elbow into this row's tile. */
|
/** One 20px column per ancestor: pass-through rails for open branches, an elbow into this row's tile. */
|
||||||
|
|
@ -99,7 +100,7 @@ function Caret({ open, onToggle, label }: { open: boolean; onToggle: () => void;
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
className="grid size-6 shrink-0 place-items-center self-start rounded-[4px] p-0.5 text-muted-foreground transition-colors duration-150 hover:bg-muted motion-reduce:transition-none"
|
className="grid size-6 shrink-0 place-items-center self-start rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 hover:bg-muted motion-reduce:transition-none"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onToggle();
|
onToggle();
|
||||||
|
|
@ -295,11 +296,11 @@ export function SpanTree({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex h-full min-h-0 min-w-0 flex-col border-r border-border bg-card" aria-label="Run spans">
|
<section className="flex h-full min-h-0 min-w-0 flex-col border-r border-border bg-card" aria-label="Run spans">
|
||||||
<div className="shrink-0 space-y-3 border-b px-3 py-3">
|
<PaneBar className="justify-between">
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Steps{" "}
|
Steps{" "}
|
||||||
<span className="ml-1 text-xs font-normal text-muted-foreground">
|
<span className="ml-1 text-xs font-normal text-muted-foreground">
|
||||||
|
{filtering && `${rows.length.toLocaleString()} of `}
|
||||||
{summary.span_count.toLocaleString()}
|
{summary.span_count.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -322,36 +323,36 @@ export function SpanTree({
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PaneBar>
|
||||||
<div className="relative">
|
<PaneBar>
|
||||||
<Search className="pointer-events-none absolute top-2.5 left-2.5 size-3.5 text-muted-foreground" />
|
<div className="relative min-w-0 flex-1">
|
||||||
|
<Search className="pointer-events-none absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
aria-label="Search steps"
|
aria-label="Search steps"
|
||||||
placeholder="Search steps"
|
placeholder="Search steps"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => onQueryChange(event.target.value)}
|
onChange={(event) => onQueryChange(event.target.value)}
|
||||||
className="h-8 pr-8 pl-8 text-xs shadow-none md:text-xs"
|
className="h-7 pr-7 pl-7 text-xs shadow-none md:text-xs"
|
||||||
/>
|
/>
|
||||||
{query && (
|
{query && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
aria-label="Clear step search"
|
aria-label="Clear step search"
|
||||||
className="absolute top-1 right-1"
|
className="absolute top-1/2 right-0.5 -translate-y-1/2"
|
||||||
onClick={() => onQueryChange("")}
|
onClick={() => onQueryChange("")}
|
||||||
>
|
>
|
||||||
<X className="size-3" />
|
<X className="size-3" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button
|
<Button
|
||||||
variant={!errorsOnly ? "secondary" : "ghost"}
|
variant={!errorsOnly ? "secondary" : "ghost"}
|
||||||
size="xs"
|
size="xs"
|
||||||
aria-pressed={!errorsOnly}
|
aria-pressed={!errorsOnly}
|
||||||
onClick={() => onErrorsOnlyChange(false)}
|
onClick={() => onErrorsOnlyChange(false)}
|
||||||
>
|
>
|
||||||
All steps
|
All
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={errorsOnly ? "secondary" : "ghost"}
|
variant={errorsOnly ? "secondary" : "ghost"}
|
||||||
|
|
@ -362,12 +363,11 @@ export function SpanTree({
|
||||||
Errors <span className="ml-1 text-muted-foreground">{summary.error_count}</span>
|
Errors <span className="ml-1 text-muted-foreground">{summary.error_count}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{filtering && (
|
{filtering && (
|
||||||
<span className="ml-auto text-xs text-muted-foreground" role="status">
|
<span role="status" className="sr-only">
|
||||||
{rows.length} found
|
{rows.length} found
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PaneBar>
|
||||||
</div>
|
|
||||||
<div ref={scrollRef} className="min-h-0 flex-1 overflow-auto py-2">
|
<div ref={scrollRef} className="min-h-0 flex-1 overflow-auto py-2">
|
||||||
{rows.length === 0 && (
|
{rows.length === 0 && (
|
||||||
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||||
|
|
@ -385,7 +385,7 @@ export function SpanTree({
|
||||||
key={row.id}
|
key={row.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onLoadMore(row.groupId)}
|
onClick={() => onLoadMore(row.groupId)}
|
||||||
className={cn(rowClass(false), "h-8 text-[13px] tracking-[-0.26px] text-muted-foreground")}
|
className={cn(rowClass(false), "h-8 text-sm text-muted-foreground")}
|
||||||
>
|
>
|
||||||
<Gutters depth={row.depth} guide={guides[i]} />
|
<Gutters depth={row.depth} guide={guides[i]} />
|
||||||
<span className="ml-0.5 flex items-center gap-2">
|
<span className="ml-0.5 flex items-center gap-2">
|
||||||
|
|
@ -413,7 +413,7 @@ export function SpanTree({
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-8 shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-t border-border px-3 py-2 text-[10px] text-muted-foreground">
|
<div className="flex h-10 shrink-0 items-center gap-x-3 overflow-hidden border-t border-border px-3 text-xs text-muted-foreground">
|
||||||
{embedded ? (
|
{embedded ? (
|
||||||
<>
|
<>
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
|
|
@ -441,7 +441,7 @@ export function SpanTree({
|
||||||
|
|
||||||
function Kbd({ children }: { children: React.ReactNode }) {
|
function Kbd({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<kbd className="rounded-[3px] border border-border border-b-2 bg-muted px-[3px] font-mono text-muted-foreground">
|
<kbd className="rounded-sm border border-border border-b-2 bg-muted px-[3px] font-mono text-muted-foreground">
|
||||||
{children}
|
{children}
|
||||||
</kbd>
|
</kbd>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export function StatusMark({ status, count, subtle = false }: StatusMarkProps) {
|
||||||
if (status === "error") {
|
if (status === "error") {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="inline-flex items-center gap-1 text-[11px] text-destructive"
|
className="inline-flex items-center gap-1 text-xs text-destructive"
|
||||||
aria-label={`${count ?? 1} error${count === 1 ? "" : "s"}`}
|
aria-label={`${count ?? 1} error${count === 1 ? "" : "s"}`}
|
||||||
>
|
>
|
||||||
<AlertCircle className="size-3" />
|
<AlertCircle className="size-3" />
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const RANGE_LABEL_FORMAT = "MMM D, h:mm A";
|
||||||
export const rangeLabel = (range: TimeWindow): string =>
|
export const rangeLabel = (range: TimeWindow): string =>
|
||||||
`${moment(range.startMs).format(RANGE_LABEL_FORMAT)} to ${moment(range.endMs).format(RANGE_LABEL_FORMAT)}`;
|
`${moment(range.startMs).format(RANGE_LABEL_FORMAT)} to ${moment(range.endMs).format(RANGE_LABEL_FORMAT)}`;
|
||||||
|
|
||||||
const SEGMENT = "inline-flex h-7 items-center gap-1.5 px-2.5 text-[13px] outline-none focus-visible:bg-accent";
|
const SEGMENT = "inline-flex h-7 items-center gap-1.5 px-2.5 text-sm outline-none focus-visible:bg-accent";
|
||||||
|
|
||||||
interface TimeRangeControlsProps {
|
interface TimeRangeControlsProps {
|
||||||
range: TimeWindow;
|
range: TimeWindow;
|
||||||
|
|
@ -79,7 +79,7 @@ export function TimeRangeControls({
|
||||||
onValueChange={(value: string) => onRangeHoursChange(Number(value))}
|
onValueChange={(value: string) => onRangeHoursChange(Number(value))}
|
||||||
>
|
>
|
||||||
{RANGE_PRESETS.map((preset) => (
|
{RANGE_PRESETS.map((preset) => (
|
||||||
<DropdownMenuRadioItem key={preset.hours} value={String(preset.hours)} className="text-[13px]">
|
<DropdownMenuRadioItem key={preset.hours} value={String(preset.hours)} className="text-sm">
|
||||||
{preset.label}
|
{preset.label}
|
||||||
</DropdownMenuRadioItem>
|
</DropdownMenuRadioItem>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ function CopyForAgent({ handoff }: { handoff: TraceHandoff }) {
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
className="h-7 shrink-0 gap-1.5 rounded-md text-[12px] shadow-none"
|
className="h-7 shrink-0 gap-1.5 rounded-md text-xs shadow-none"
|
||||||
onClick={async () => setCopied(await copyToClipboard(handoff.text, handoff.copied))}
|
onClick={async () => setCopied(await copyToClipboard(handoff.text, handoff.copied))}
|
||||||
>
|
>
|
||||||
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||||
|
|
@ -416,15 +416,15 @@ export function RunView({ traceId, traceRef, selection, accessToken, onBack, emb
|
||||||
}
|
}
|
||||||
if (!trace) {
|
if (!trace) {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 text-[12px]" data-testid="run-view-error">
|
<div className="p-6 text-xs" data-testid="run-view-error">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
className="mb-4 inline-flex items-center gap-1.5 text-[12px] text-muted-foreground hover:text-foreground"
|
className="mb-4 inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="size-3.5" /> Back to traces
|
<ArrowLeft className="size-3.5" /> Back to traces
|
||||||
</button>
|
</button>
|
||||||
<h1 className="mb-2 text-[13px] font-medium">Could not load trace</h1>
|
<h1 className="mb-2 text-sm font-medium">Could not load trace</h1>
|
||||||
<span className="text-muted-foreground">{traceQuery.error?.message ?? "Unknown error"}</span>
|
<span className="text-muted-foreground">{traceQuery.error?.message ?? "Unknown error"}</span>
|
||||||
<Button variant="outline" size="sm" className="ml-3" onClick={() => void refreshTrace()}>
|
<Button variant="outline" size="sm" className="ml-3" onClick={() => void refreshTrace()}>
|
||||||
Retry
|
Retry
|
||||||
|
|
|
||||||
|
|
@ -227,11 +227,11 @@ function FieldHeader({ runs, range }: { runs: readonly TraceSummary[]; range: Ti
|
||||||
const lastMs = latestStart(runs);
|
const lastMs = latestStart(runs);
|
||||||
const live = isReceiving(lastMs, now);
|
const live = isReceiving(lastMs, now);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 font-mono text-[11px] leading-none">
|
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 font-mono text-xs leading-none">
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground tabular-nums">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground tabular-nums">
|
||||||
<span className="flex items-center gap-2" role="status" data-testid="traces-last-seen">
|
<span className="flex items-center gap-2" role="status" data-testid="traces-last-seen">
|
||||||
<LivePulse live={live} />
|
<LivePulse live={live} />
|
||||||
<span className={cn("font-semibold", live ? "text-[#0011b3] dark:text-[#8b9bff]" : "text-foreground")}>
|
<span className={cn("font-semibold", live ? "text-info" : "text-foreground")}>
|
||||||
{live ? "receiving" : "idle"}
|
{live ? "receiving" : "idle"}
|
||||||
</span>
|
</span>
|
||||||
{lastMs !== null && <span>last trace {agoLabel(lastMs, now)}</span>}
|
{lastMs !== null && <span>last trace {agoLabel(lastMs, now)}</span>}
|
||||||
|
|
@ -241,7 +241,7 @@ function FieldHeader({ runs, range }: { runs: readonly TraceSummary[]; range: Ti
|
||||||
{runs.length === 1 ? "run" : "runs"} from{" "}
|
{runs.length === 1 ? "run" : "runs"} from{" "}
|
||||||
<span className="font-semibold text-foreground">{agents.length}</span>{" "}
|
<span className="font-semibold text-foreground">{agents.length}</span>{" "}
|
||||||
{agents.length === 1 ? "agent" : "agents"}
|
{agents.length === 1 ? "agent" : "agents"}
|
||||||
{failed > 0 && <span className="text-[#e5484d]"> · {failed} failed</span>}
|
{failed > 0 && <span className="text-destructive"> · {failed} failed</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center justify-end gap-x-3 gap-y-1 text-muted-foreground">
|
<div className="flex flex-wrap items-center justify-end gap-x-3 gap-y-1 text-muted-foreground">
|
||||||
|
|
@ -277,7 +277,7 @@ function NowEdge() {
|
||||||
function BucketTooltip({ bucket, index }: { bucket: Bucket; index: number }) {
|
function BucketTooltip({ bucket, index }: { bucket: Bucket; index: number }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="pointer-events-none absolute top-full z-floating mt-1 rounded-md border border-border bg-popover px-2.5 py-1.5 font-mono text-[11px] text-popover-foreground shadow-md"
|
className="pointer-events-none absolute top-full z-floating mt-1 rounded-md border border-border bg-popover px-2.5 py-1.5 font-mono text-xs text-popover-foreground shadow-md"
|
||||||
style={{ left: pct(Math.min(0.8, index / BUCKETS)) }}
|
style={{ left: pct(Math.min(0.8, index / BUCKETS)) }}
|
||||||
role="tooltip"
|
role="tooltip"
|
||||||
>
|
>
|
||||||
|
|
@ -288,7 +288,7 @@ function BucketTooltip({ bucket, index }: { bucket: Bucket; index: number }) {
|
||||||
{bucket.runs} {bucket.runs === 1 ? "run" : "runs"}
|
{bucket.runs} {bucket.runs === 1 ? "run" : "runs"}
|
||||||
{bucket.failed > 0 && `, ${bucket.failed} failed`}
|
{bucket.failed > 0 && `, ${bucket.failed} failed`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[#0011b3] dark:text-[#8b9bff]">drag to zoom</div>
|
<div className="text-info">drag to zoom</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -351,8 +351,7 @@ function SelectionBracket({
|
||||||
const handle = "absolute inset-y-0 w-[2px] cursor-ew-resize bg-[#0011b3] dark:bg-[#8b9bff]";
|
const handle = "absolute inset-y-0 w-[2px] cursor-ew-resize bg-[#0011b3] dark:bg-[#8b9bff]";
|
||||||
const tick =
|
const tick =
|
||||||
"before:absolute before:top-0 before:h-[2px] before:w-2 before:bg-inherit after:absolute after:bottom-0 after:h-[2px] after:w-2 after:bg-inherit";
|
"before:absolute before:top-0 before:h-[2px] before:w-2 before:bg-inherit after:absolute after:bottom-0 after:h-[2px] after:w-2 after:bg-inherit";
|
||||||
const edgeLabel =
|
const edgeLabel = "pointer-events-none absolute -bottom-5 font-mono text-xs whitespace-nowrap text-info tabular-nums";
|
||||||
"pointer-events-none absolute -bottom-5 font-mono text-[10px] whitespace-nowrap text-[#0011b3] tabular-nums dark:text-[#8b9bff]";
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
@ -372,7 +371,7 @@ function SelectionBracket({
|
||||||
onPointerDown={onHandleDown("resize-hi")}
|
onPointerDown={onHandleDown("resize-hi")}
|
||||||
/>
|
/>
|
||||||
{widthPx >= MIN_DURATION_LABEL_PX && (
|
{widthPx >= MIN_DURATION_LABEL_PX && (
|
||||||
<span className="pointer-events-none absolute inset-x-0 top-2 text-center font-mono text-[11px] font-semibold text-[#0011b3] dark:text-[#8b9bff]">
|
<span className="pointer-events-none absolute inset-x-0 top-2 text-center font-mono text-xs font-semibold text-info">
|
||||||
{formatSpan(window.endMs - window.startMs)}
|
{formatSpan(window.endMs - window.startMs)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -404,7 +403,7 @@ function TickAxis({ range }: { range: TimeWindow }) {
|
||||||
style={{ left: pct(t), transform: tickShift(t) }}
|
style={{ left: pct(t), transform: tickShift(t) }}
|
||||||
>
|
>
|
||||||
<span className="h-1 w-px bg-border" />
|
<span className="h-1 w-px bg-border" />
|
||||||
<span className="font-mono text-[10px] whitespace-nowrap text-muted-foreground tabular-nums">
|
<span className="font-mono text-xs whitespace-nowrap text-muted-foreground tabular-nums">
|
||||||
{moment(range.startMs + (range.endMs - range.startMs) * t).format(format)}
|
{moment(range.startMs + (range.endMs - range.startMs) * t).format(format)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ function CodeBlock({
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileLabel({ children }: { children: React.ReactNode }) {
|
function FileLabel({ children }: { children: React.ReactNode }) {
|
||||||
return <span className="text-[12.5px] text-foreground">{children}</span>;
|
return <span className="text-xs text-foreground">{children}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function LineTabs<T extends string>({
|
function LineTabs<T extends string>({
|
||||||
|
|
@ -155,7 +155,7 @@ function LineTabs<T extends string>({
|
||||||
aria-selected={value === option}
|
aria-selected={value === option}
|
||||||
onClick={() => onChange(option)}
|
onClick={() => onChange(option)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"-mb-px inline-flex h-9 items-center gap-1.5 border-b-2 text-[12.5px]",
|
"-mb-px inline-flex h-9 items-center gap-1.5 border-b-2 text-xs",
|
||||||
value === option
|
value === option
|
||||||
? "border-foreground text-foreground"
|
? "border-foreground text-foreground"
|
||||||
: "border-transparent text-muted-foreground hover:text-foreground",
|
: "border-transparent text-muted-foreground hover:text-foreground",
|
||||||
|
|
@ -362,7 +362,7 @@ function EndpointValue({ value }: { value: string }) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void copy()}
|
onClick={() => void copy()}
|
||||||
aria-label={`Copy ${value}`}
|
aria-label={`Copy ${value}`}
|
||||||
className="group inline-flex min-w-0 items-center gap-2 text-left font-mono text-[12.5px] text-foreground"
|
className="group inline-flex min-w-0 items-center gap-2 text-left font-mono text-xs text-foreground"
|
||||||
>
|
>
|
||||||
<span className="break-all">{value}</span>
|
<span className="break-all">{value}</span>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
|
|
@ -387,9 +387,9 @@ function Endpoints({ proxyUrl, children }: { proxyUrl: string; children?: React.
|
||||||
<dl className="mt-3 grid gap-x-6 gap-y-3 rounded-md border bg-muted/30 p-4 md:grid-cols-2">
|
<dl className="mt-3 grid gap-x-6 gap-y-3 rounded-md border bg-muted/30 p-4 md:grid-cols-2">
|
||||||
{otlpEndpoints(proxyUrl).map(([label, value, copyable]) => (
|
{otlpEndpoints(proxyUrl).map(([label, value, copyable]) => (
|
||||||
<div key={label} className={cn("min-w-0", label === "Traces endpoint" && "md:col-span-2")}>
|
<div key={label} className={cn("min-w-0", label === "Traces endpoint" && "md:col-span-2")}>
|
||||||
<dt className="text-[12px] text-muted-foreground">{label}</dt>
|
<dt className="text-xs text-muted-foreground">{label}</dt>
|
||||||
<dd className="mt-0.5">
|
<dd className="mt-0.5">
|
||||||
{copyable ? <EndpointValue value={value} /> : <span className="text-[12.5px]">{value}</span>}
|
{copyable ? <EndpointValue value={value} /> : <span className="text-xs">{value}</span>}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { RuleTester } from "eslint";
|
||||||
|
import rule from "../../scripts/eslint-rules/no-arbitrary-design-value.mjs";
|
||||||
|
|
||||||
|
const ruleTester = new RuleTester({
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const arbitrary = (code: string, token: string) => ({ code, errors: [{ messageId: "arbitrary", data: { token } }] });
|
||||||
|
|
||||||
|
ruleTester.run("no-arbitrary-design-value", rule as never, {
|
||||||
|
valid: [
|
||||||
|
'const c = "text-xs leading-tight tracking-wider rounded-sm border";',
|
||||||
|
'const c = "grid-cols-[minmax(0,1fr)_auto] w-[180px] left-[calc(50%-0.5px)]";',
|
||||||
|
'const c = "data-[side=right]:sm:max-w-[90vw] [&>p]:mb-0";',
|
||||||
|
'const c = "ease-[cubic-bezier(0.4,0,0.2,1)]";',
|
||||||
|
'const el = <div className="text-sm text-muted-foreground" />;',
|
||||||
|
],
|
||||||
|
invalid: [
|
||||||
|
arbitrary('const c = "flex text-[13px]";', "text-[13px]"),
|
||||||
|
arbitrary('const c = "text-[#e5484d]";', "text-[#e5484d]"),
|
||||||
|
arbitrary('const c = "tracking-[-0.26px]";', "tracking-[-0.26px]"),
|
||||||
|
arbitrary('const c = "leading-[1.2]";', "leading-[1.2]"),
|
||||||
|
arbitrary('const c = "rounded-[4px]";', "rounded-[4px]"),
|
||||||
|
arbitrary('const c = "rounded-t-[4px]";', "rounded-t-[4px]"),
|
||||||
|
arbitrary('const c = "border-[0.67px]";', "border-[0.67px]"),
|
||||||
|
arbitrary('const c = "[border-left-width:1px]";', "[border-left-width:1px]"),
|
||||||
|
arbitrary('const c = "dark:text-[#8b9bff]";', "dark:text-[#8b9bff]"),
|
||||||
|
arbitrary('const c = "data-[side=top]:text-[11px]";', "data-[side=top]:text-[11px]"),
|
||||||
|
arbitrary("const c = `px-2 ${x} text-[10px]`;", "text-[10px]"),
|
||||||
|
arbitrary('const el = <div className="text-[12px]" />;', "text-[12px]"),
|
||||||
|
],
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue