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 }] },
|
||||
},
|
||||
{
|
||||
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}"],
|
||||
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 noNoopHoverVariant from "./no-noop-hover-variant.mjs";
|
||||
import noAdHocZIndex from "./no-ad-hoc-z-index.mjs";
|
||||
import noArbitraryDesignValue from "./no-arbitrary-design-value.mjs";
|
||||
|
||||
const plugin = {
|
||||
rules: {
|
||||
|
|
@ -13,6 +14,7 @@ const plugin = {
|
|||
"filename-pascal-case": filenamePascalCase,
|
||||
"no-noop-hover-variant": noNoopHoverVariant,
|
||||
"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 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 utility = utilityOf(token);
|
||||
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 { 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 =
|
||||
"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 }) {
|
||||
const status = lens.jobs[0]?.status;
|
||||
const className = "size-4 shrink-0";
|
||||
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 === "cancelled")
|
||||
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 (
|
||||
<span className="flex min-w-0 flex-col gap-0.5">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
@ -87,7 +87,7 @@ export function InvestigationList({
|
|||
<Input
|
||||
aria-label="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}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
|
@ -149,7 +149,7 @@ export function InvestigationList({
|
|||
</td>
|
||||
<td
|
||||
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",
|
||||
)}
|
||||
>
|
||||
|
|
@ -159,14 +159,14 @@ export function InvestigationList({
|
|||
{findings.length > 0 && (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
)}
|
||||
</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}
|
||||
>
|
||||
{latest ? agoLabel(Date.parse(latest.created_at), now) : "never run"}
|
||||
|
|
@ -227,12 +227,12 @@ export function InvestigationList({
|
|||
/>
|
||||
</span>
|
||||
</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"}
|
||||
</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)}
|
||||
>
|
||||
{agoLabel(Date.parse(finding.last_seen), now)}
|
||||
|
|
@ -249,12 +249,10 @@ export function InvestigationList({
|
|||
</tbody>
|
||||
</table>
|
||||
{!shown.length && (
|
||||
<div className="py-16 text-center text-[12px] text-muted-foreground">
|
||||
No investigations match your search.
|
||||
</div>
|
||||
<div className="py-16 text-center text-xs text-muted-foreground">No investigations match your search.</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"} ·{" "}
|
||||
{lenses.filter((l) => l.settings.enabled).length} watching
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ import type { Job } from "../model/types";
|
|||
const STEP_MARK = { model: "✓", stage: "▸", error: "!" } as const;
|
||||
const STEP_TONE = {
|
||||
model: "text-foreground",
|
||||
stage: "text-[#0011b3] dark:text-[#8b9bff]",
|
||||
error: "text-[#e5484d]",
|
||||
stage: "text-info",
|
||||
error: "text-destructive",
|
||||
} as const;
|
||||
|
||||
export function StepFeed({ job }: { job: Job }) {
|
||||
|
|
@ -21,7 +21,7 @@ export function StepFeed({ job }: { job: Job }) {
|
|||
const models = modelsUsed(steps);
|
||||
const calls = steps.filter((s) => s.kind === "model").length;
|
||||
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
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<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}`}>
|
||||
<span className="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
Time <ArrowDown className="size-2.5" />
|
||||
|
|
@ -128,14 +128,14 @@ export function AgentTracesTable({
|
|||
onClick={() => onOpenTrace(run)}
|
||||
aria-selected={selectedKey === (run.trace_ref || run.trace_id)}
|
||||
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)
|
||||
? "bg-trace-row-selected shadow-[inset_2px_0_0_var(--trace-brand)]"
|
||||
: "hover:bg-trace-row-hover",
|
||||
)}
|
||||
>
|
||||
<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)}
|
||||
>
|
||||
{formatRunTimestamp(run.start_time)}
|
||||
|
|
@ -149,13 +149,13 @@ export function AgentTracesTable({
|
|||
</span>
|
||||
{run.resolution_limited && (
|
||||
<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"
|
||||
>
|
||||
Partial totals
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -170,7 +170,7 @@ export function AgentTracesTable({
|
|||
{run.error_count > 0 ? (
|
||||
<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>
|
||||
|
|
@ -182,9 +182,9 @@ export function AgentTracesTable({
|
|||
PLACEHOLDER_ROWS.map((row) => <PlaceholderRow key={row} rowRef={row === 0 ? tailRef : undefined} />)}
|
||||
</tbody>
|
||||
</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 && (
|
||||
<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>
|
||||
{traces.length ? "Could not load more runs" : "Could not load runs"}: {error.message}
|
||||
</span>
|
||||
|
|
@ -196,16 +196,14 @@ export function AgentTracesTable({
|
|||
</div>
|
||||
)}
|
||||
{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>
|
||||
<Button size="xs" variant="outline" disabled={isFetching} onClick={onLoadMore}>
|
||||
Load older runs
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isEmpty && (
|
||||
<div className="py-16 text-center text-[12px] text-muted-foreground">No runs match these filters.</div>
|
||||
)}
|
||||
{isEmpty && <div className="py-16 text-center text-xs text-muted-foreground">No runs match these filters.</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ interface AttributesDetailProps {
|
|||
export function DetailGroup({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<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>
|
||||
</section>
|
||||
);
|
||||
|
|
@ -38,7 +38,7 @@ export function AttributesDetail({ traceId, span, attributes, isLoading }: Attri
|
|||
<KeyValueRows entries={attributeEntries} />
|
||||
</DetailGroup>
|
||||
)}
|
||||
{isLoading && <div className="text-[13px] text-trace-duration">Loading attributes…</div>}
|
||||
{isLoading && <div className="text-sm text-trace-duration">Loading attributes…</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 TRACEBACK_MARKER = "Traceback (most recent call last):";
|
||||
const STATUS_TEXT = "px-5 py-2 text-[13px] tracking-[-0.26px] text-muted-foreground";
|
||||
const PAYLOAD_PRE =
|
||||
"font-mono text-[13px] leading-[1.5] tracking-[-0.26px] break-words whitespace-pre-wrap text-foreground";
|
||||
const STATUS_TEXT = "px-3 py-2 text-sm text-muted-foreground";
|
||||
const PAYLOAD_PRE = "font-mono text-sm leading-normal break-words whitespace-pre-wrap text-foreground";
|
||||
|
||||
/** Exporters record `repr(exc)` + traceback with no separator; keep the exception line. */
|
||||
export const errorHeadline = (error: string): string =>
|
||||
|
|
@ -46,9 +45,9 @@ export function ErrorBlock({ span }: { span: Span }) {
|
|||
return (
|
||||
<section
|
||||
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" />
|
||||
{ERROR_SOURCE_LABEL[source]} · {errorReason(headline)}
|
||||
</div>
|
||||
|
|
@ -210,7 +209,7 @@ export function DetailContent({ accessToken, traceId, traceRef, span }: DetailCo
|
|||
: parseMessages(detail?.input ?? "")?.length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col px-2 pt-1 pb-4">
|
||||
<div className="flex flex-col pt-1 pb-4">
|
||||
<ErrorBlock span={span} />
|
||||
{span.error && (
|
||||
<DiagnosticContent
|
||||
|
|
@ -239,9 +238,7 @@ export function DetailContent({ accessToken, traceId, traceRef, span }: DetailCo
|
|||
</Section>
|
||||
) : null}
|
||||
{empty && span.status !== "error" && (
|
||||
<div className="py-12 text-center text-[13px] tracking-[-0.26px] text-muted-foreground">
|
||||
No content recorded for this span.
|
||||
</div>
|
||||
<div className="py-12 text-center text-sm text-muted-foreground">No content recorded for this span.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { AttributesDetail } from "./AttributesDetail";
|
|||
import { CopyButton } from "./CopyButton";
|
||||
import { DetailContent, errorHeadline, useSpanDetail } from "./DetailContent";
|
||||
import { IdChip } from "./IdChip";
|
||||
import { PaneBar } from "./PaneBar";
|
||||
import { RequestDetail } from "./RequestDetail";
|
||||
import { SpanIcon } from "./SpanIcon";
|
||||
import { useTracesApi } from "./tracesApi";
|
||||
|
|
@ -47,7 +48,7 @@ function PaneHeader({
|
|||
onClose: () => void;
|
||||
}) {
|
||||
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">
|
||||
<span className="mr-2 shrink-0">
|
||||
<SpanIcon type={type} model={model} error={failed} size="md" />
|
||||
|
|
@ -64,20 +65,16 @@ function PaneHeader({
|
|||
size="icon-xs"
|
||||
onClick={onClose}
|
||||
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" />
|
||||
</Button>
|
||||
</div>
|
||||
</PaneBar>
|
||||
);
|
||||
}
|
||||
|
||||
function PaneFooter({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <div className="flex h-10 shrink-0 items-center gap-3 border-t bg-background px-3">{children}</div>;
|
||||
}
|
||||
|
||||
function Meta({ label, value }: { label: string; value: string }) {
|
||||
|
|
@ -112,10 +109,7 @@ function SpanPane({
|
|||
);
|
||||
const tokens = span.input_tokens + span.output_tokens;
|
||||
return (
|
||||
<aside
|
||||
className="flex h-full min-w-0 flex-col bg-background pt-4 text-[13px] text-foreground"
|
||||
aria-label="Span details"
|
||||
>
|
||||
<aside className="flex h-full min-w-0 flex-col bg-background text-sm text-foreground" aria-label="Span details">
|
||||
<PaneHeader
|
||||
type={span.type}
|
||||
model={span.model}
|
||||
|
|
@ -125,15 +119,15 @@ function SpanPane({
|
|||
onClose={onClose}
|
||||
/>
|
||||
<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">
|
||||
<TabsList variant="line" aria-label="Span detail sections" className="h-10 gap-4 px-0">
|
||||
<PaneBar>
|
||||
<TabsList variant="line" aria-label="Span detail sections" className="h-full gap-4 px-0">
|
||||
{SPAN_TABS.map((id) => (
|
||||
<TabsTrigger key={id} value={id} className="flex-none px-0 text-xs">
|
||||
{TAB_LABELS[id]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
</PaneBar>
|
||||
<TabsContent value="content" className="min-h-0 overflow-auto pt-3">
|
||||
<DetailContent accessToken={accessToken} traceId={traceId} traceRef={trace.summary.trace_ref} span={span} />
|
||||
</TabsContent>
|
||||
|
|
@ -163,8 +157,8 @@ function SpanPane({
|
|||
function GroupMetric({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="border-b border-border/60 py-3">
|
||||
<div className="text-[13px] font-medium text-muted-foreground">{label}</div>
|
||||
<div className="mt-1 text-[13px] text-foreground tabular-nums">{value}</div>
|
||||
<div className="text-sm font-medium text-muted-foreground">{label}</div>
|
||||
<div className="mt-1 text-sm text-foreground tabular-nums">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -186,10 +180,7 @@ function GroupPane({
|
|||
const sample = (firstFailure ?? row.members[0]).span_id;
|
||||
const handoff = useTracesApi(accessToken).handoff(trace.summary.trace_id, sample, trace.summary.trace_ref);
|
||||
return (
|
||||
<aside
|
||||
className="flex h-full min-w-0 flex-col bg-background pt-4 text-[13px] text-foreground"
|
||||
aria-label="Group details"
|
||||
>
|
||||
<aside className="flex h-full min-w-0 flex-col bg-background text-sm text-foreground" aria-label="Group details">
|
||||
<PaneHeader
|
||||
type={row.type}
|
||||
model={row.members[0]?.model ?? null}
|
||||
|
|
@ -201,7 +192,7 @@ function GroupPane({
|
|||
}
|
||||
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">
|
||||
<GroupMetric label="Invocations" value={row.members.length.toLocaleString()} />
|
||||
<GroupMetric label="Failed" value={row.failedCount.toLocaleString()} />
|
||||
|
|
@ -211,9 +202,9 @@ function GroupPane({
|
|||
<GroupMetric label="Type" value={row.type} />
|
||||
</div>
|
||||
{firstFailure?.error && (
|
||||
<section className="mt-3 rounded-[4px] border border-destructive/30 px-3.5 py-3">
|
||||
<div className="text-[12px] font-medium text-destructive">Failure pattern</div>
|
||||
<p className="mt-2 font-mono text-[12px] leading-5 text-foreground">{errorHeadline(firstFailure.error)}</p>
|
||||
<section className="mt-3 rounded-sm border border-destructive/30 px-3.5 py-3">
|
||||
<div className="text-xs font-medium text-destructive">Failure pattern</div>
|
||||
<p className="mt-2 font-mono text-xs leading-5 text-foreground">{errorHeadline(firstFailure.error)}</p>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -228,7 +219,7 @@ function GroupPane({
|
|||
export function DetailPane({ trace, row, accessToken, spanTab, onSpanTabChange, onClose }: DetailPaneProps) {
|
||||
if (!row || row.kind === "load-more") {
|
||||
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.
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function KeyValueRow({ entry, mono }: { entry: KeyValue; mono: boolean }) {
|
|||
const [key, value] = entry;
|
||||
const [open, setOpen] = useState(false);
|
||||
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",
|
||||
);
|
||||
|
||||
|
|
@ -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">
|
||||
{head}
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ const markdownComponents: Components = {
|
|||
<pre
|
||||
className={cn(
|
||||
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}
|
||||
/>
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
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 }) {
|
||||
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";
|
||||
const LABEL = "min-w-0 truncate text-xs leading-5 font-medium text-foreground";
|
||||
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";
|
||||
const INLINE_COPY = "size-4 shrink-0 rounded-[3px] text-muted-foreground";
|
||||
"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-sm text-muted-foreground";
|
||||
|
||||
export function ToolCallBlock({ call }: { call: TraceToolCall }) {
|
||||
const argEntries = objectEntries(call.args);
|
||||
|
|
@ -111,7 +111,7 @@ export function MessageCard({ message }: { message: TraceMessage; model: string
|
|||
const expanded = open && hasBody;
|
||||
return (
|
||||
<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)} />
|
||||
<span className={LABEL}>{label}</span>
|
||||
<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";
|
||||
return (
|
||||
<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} />
|
||||
<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>
|
||||
|
|
@ -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)}
|
||||
>
|
||||
<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>
|
||||
) : (
|
||||
<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} />
|
||||
</div>
|
||||
{open && (
|
||||
<pre
|
||||
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,
|
||||
)}
|
||||
>
|
||||
|
|
@ -197,10 +197,10 @@ export function Section({
|
|||
type="button"
|
||||
aria-expanded={open}
|
||||
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" />
|
||||
<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 && (
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{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;
|
||||
|
||||
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[] = [
|
||||
|
|
@ -57,15 +57,15 @@ export function RequestDetail({ span, accessToken, traceStartMs }: RequestDetail
|
|||
<Button
|
||||
variant="outline"
|
||||
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)}
|
||||
>
|
||||
Open request log <ArrowUpRight className="size-3.5" />
|
||||
</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>
|
||||
) : (
|
||||
<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>
|
||||
<LogDetailsDrawer
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export function RunsToolbar({
|
|||
onChange={(e) => onQueryChange(e.target.value)}
|
||||
placeholder="Search input or trace ID"
|
||||
aria-label="Search runs"
|
||||
className="h-7 pl-8 text-[12px]"
|
||||
className="h-7 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
|
|
@ -56,7 +56,7 @@ export function RunsToolbar({
|
|||
value={agent}
|
||||
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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
@ -72,7 +72,7 @@ export function RunsToolbar({
|
|||
value={status}
|
||||
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 />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
|
|||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[12px] text-trace-duration">
|
||||
<span className="text-xs text-trace-duration">
|
||||
{TYPE_LABEL[facts.type]}
|
||||
{facts.model ? ` · ${facts.model}` : ""}
|
||||
</span>
|
||||
|
|
@ -150,7 +150,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
|
|||
<FactSection title="Tags">
|
||||
<ul className="flex flex-wrap gap-1">
|
||||
{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}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -158,7 +158,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
|
|||
</FactSection>
|
||||
)}
|
||||
{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)}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -183,7 +183,7 @@ export function SpanHoverCard({ facts, traceStartMs, children }: SpanHoverCardPr
|
|||
sideOffset={0}
|
||||
alignOffset={0}
|
||||
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} />
|
||||
</HoverCardContent>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ import { useSpanProvider } from "./spanProvider";
|
|||
import type { SpanType } from "./traceTypes";
|
||||
|
||||
const SIZE = {
|
||||
sm: "size-4 rounded-[2px]",
|
||||
card: "size-[18px] rounded-[3px]",
|
||||
sm: "size-4 rounded-sm",
|
||||
card: "size-[18px] rounded-sm",
|
||||
md: "size-5 rounded-full",
|
||||
lg: "size-5 rounded-full",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { cn } from "@/lib/cva.config";
|
||||
|
||||
import { FoldChevron } from "./Collapse";
|
||||
import { PaneBar } from "./PaneBar";
|
||||
import { groupFacts, SpanHoverCard, spanFacts } from "./SpanHoverCard";
|
||||
import { SpanIcon } from "./SpanIcon";
|
||||
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",
|
||||
);
|
||||
|
||||
const LINE = "border-dashed border-border [border-width:0] [border-left-width:1px]";
|
||||
const ELBOW = "border-dashed border-border [border-width:0] [border-left-width:1px] [border-bottom-width:1px]";
|
||||
const NAME = "line-clamp-2 break-words text-[13px] leading-[1.2] font-medium";
|
||||
const LINE = "border-l border-dashed border-border";
|
||||
const ELBOW = "border-b border-l border-dashed border-border";
|
||||
const NAME = "line-clamp-2 break-words text-sm leading-tight font-medium";
|
||||
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. */
|
||||
|
|
@ -99,7 +100,7 @@ function Caret({ open, onToggle, label }: { open: boolean; onToggle: () => void;
|
|||
role="button"
|
||||
tabIndex={-1}
|
||||
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) => {
|
||||
event.stopPropagation();
|
||||
onToggle();
|
||||
|
|
@ -295,79 +296,78 @@ export function SpanTree({
|
|||
|
||||
return (
|
||||
<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">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-medium">
|
||||
Steps{" "}
|
||||
<span className="ml-1 text-xs font-normal text-muted-foreground">
|
||||
{summary.span_count.toLocaleString()}
|
||||
</span>
|
||||
<PaneBar className="justify-between">
|
||||
<span className="text-sm font-medium">
|
||||
Steps{" "}
|
||||
<span className="ml-1 text-xs font-normal text-muted-foreground">
|
||||
{filtering && `${rows.length.toLocaleString()} of `}
|
||||
{summary.span_count.toLocaleString()}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{onOpenDetails && (
|
||||
<Button variant="ghost" size="icon-xs" onClick={onOpenDetails} aria-label="Show details">
|
||||
<PanelRightOpen className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" aria-label="Step display options" />}>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuCheckboxItem checked={hideFramework} onCheckedChange={onToggleHideFramework}>
|
||||
Hide framework spans
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onCollapseAll}>Collapse branches</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{onOpenDetails && (
|
||||
<Button variant="ghost" size="icon-xs" onClick={onOpenDetails} aria-label="Show details">
|
||||
<PanelRightOpen className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" aria-label="Step display options" />}>
|
||||
<MoreHorizontal className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuCheckboxItem checked={hideFramework} onCheckedChange={onToggleHideFramework}>
|
||||
Hide framework spans
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onCollapseAll}>Collapse branches</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute top-2.5 left-2.5 size-3.5 text-muted-foreground" />
|
||||
</PaneBar>
|
||||
<PaneBar>
|
||||
<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
|
||||
aria-label="Search steps"
|
||||
placeholder="Search steps"
|
||||
value={query}
|
||||
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 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
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("")}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant={!errorsOnly ? "secondary" : "ghost"}
|
||||
size="xs"
|
||||
aria-pressed={!errorsOnly}
|
||||
onClick={() => onErrorsOnlyChange(false)}
|
||||
>
|
||||
All steps
|
||||
</Button>
|
||||
<Button
|
||||
variant={errorsOnly ? "secondary" : "ghost"}
|
||||
size="xs"
|
||||
aria-pressed={errorsOnly}
|
||||
onClick={() => onErrorsOnlyChange(true)}
|
||||
>
|
||||
Errors <span className="ml-1 text-muted-foreground">{summary.error_count}</span>
|
||||
</Button>
|
||||
{filtering && (
|
||||
<span className="ml-auto text-xs text-muted-foreground" role="status">
|
||||
{rows.length} found
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant={!errorsOnly ? "secondary" : "ghost"}
|
||||
size="xs"
|
||||
aria-pressed={!errorsOnly}
|
||||
onClick={() => onErrorsOnlyChange(false)}
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
variant={errorsOnly ? "secondary" : "ghost"}
|
||||
size="xs"
|
||||
aria-pressed={errorsOnly}
|
||||
onClick={() => onErrorsOnlyChange(true)}
|
||||
>
|
||||
Errors <span className="ml-1 text-muted-foreground">{summary.error_count}</span>
|
||||
</Button>
|
||||
{filtering && (
|
||||
<span role="status" className="sr-only">
|
||||
{rows.length} found
|
||||
</span>
|
||||
)}
|
||||
</PaneBar>
|
||||
<div ref={scrollRef} className="min-h-0 flex-1 overflow-auto py-2">
|
||||
{rows.length === 0 && (
|
||||
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
|
|
@ -385,7 +385,7 @@ export function SpanTree({
|
|||
key={row.id}
|
||||
type="button"
|
||||
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]} />
|
||||
<span className="ml-0.5 flex items-center gap-2">
|
||||
|
|
@ -413,7 +413,7 @@ export function SpanTree({
|
|||
})}
|
||||
</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 ? (
|
||||
<>
|
||||
<span className="whitespace-nowrap">
|
||||
|
|
@ -441,7 +441,7 @@ export function SpanTree({
|
|||
|
||||
function Kbd({ children }: { children: React.ReactNode }) {
|
||||
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}
|
||||
</kbd>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function StatusMark({ status, count, subtle = false }: StatusMarkProps) {
|
|||
if (status === "error") {
|
||||
return (
|
||||
<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"}`}
|
||||
>
|
||||
<AlertCircle className="size-3" />
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const RANGE_LABEL_FORMAT = "MMM D, h:mm A";
|
|||
export const rangeLabel = (range: TimeWindow): string =>
|
||||
`${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 {
|
||||
range: TimeWindow;
|
||||
|
|
@ -79,7 +79,7 @@ export function TimeRangeControls({
|
|||
onValueChange={(value: string) => onRangeHoursChange(Number(value))}
|
||||
>
|
||||
{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}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ function CopyForAgent({ handoff }: { handoff: TraceHandoff }) {
|
|||
<Button
|
||||
variant="outline"
|
||||
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))}
|
||||
>
|
||||
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||
|
|
@ -416,15 +416,15 @@ export function RunView({ traceId, traceRef, selection, accessToken, onBack, emb
|
|||
}
|
||||
if (!trace) {
|
||||
return (
|
||||
<div className="p-6 text-[12px]" data-testid="run-view-error">
|
||||
<div className="p-6 text-xs" data-testid="run-view-error">
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</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>
|
||||
<Button variant="outline" size="sm" className="ml-3" onClick={() => void refreshTrace()}>
|
||||
Retry
|
||||
|
|
|
|||
|
|
@ -227,11 +227,11 @@ function FieldHeader({ runs, range }: { runs: readonly TraceSummary[]; range: Ti
|
|||
const lastMs = latestStart(runs);
|
||||
const live = isReceiving(lastMs, now);
|
||||
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">
|
||||
<span className="flex items-center gap-2" role="status" data-testid="traces-last-seen">
|
||||
<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"}
|
||||
</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{" "}
|
||||
<span className="font-semibold text-foreground">{agents.length}</span>{" "}
|
||||
{agents.length === 1 ? "agent" : "agents"}
|
||||
{failed > 0 && <span className="text-[#e5484d]"> · {failed} failed</span>}
|
||||
{failed > 0 && <span className="text-destructive"> · {failed} failed</span>}
|
||||
</span>
|
||||
</div>
|
||||
<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 }) {
|
||||
return (
|
||||
<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)) }}
|
||||
role="tooltip"
|
||||
>
|
||||
|
|
@ -288,7 +288,7 @@ function BucketTooltip({ bucket, index }: { bucket: Bucket; index: number }) {
|
|||
{bucket.runs} {bucket.runs === 1 ? "run" : "runs"}
|
||||
{bucket.failed > 0 && `, ${bucket.failed} failed`}
|
||||
</div>
|
||||
<div className="text-[#0011b3] dark:text-[#8b9bff]">drag to zoom</div>
|
||||
<div className="text-info">drag to zoom</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 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";
|
||||
const edgeLabel =
|
||||
"pointer-events-none absolute -bottom-5 font-mono text-[10px] whitespace-nowrap text-[#0011b3] tabular-nums dark:text-[#8b9bff]";
|
||||
const edgeLabel = "pointer-events-none absolute -bottom-5 font-mono text-xs whitespace-nowrap text-info tabular-nums";
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -372,7 +371,7 @@ function SelectionBracket({
|
|||
onPointerDown={onHandleDown("resize-hi")}
|
||||
/>
|
||||
{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)}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -404,7 +403,7 @@ function TickAxis({ range }: { range: TimeWindow }) {
|
|||
style={{ left: pct(t), transform: tickShift(t) }}
|
||||
>
|
||||
<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)}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ function CodeBlock({
|
|||
}
|
||||
|
||||
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>({
|
||||
|
|
@ -155,7 +155,7 @@ function LineTabs<T extends string>({
|
|||
aria-selected={value === option}
|
||||
onClick={() => onChange(option)}
|
||||
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
|
||||
? "border-foreground text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground",
|
||||
|
|
@ -362,7 +362,7 @@ function EndpointValue({ value }: { value: string }) {
|
|||
type="button"
|
||||
onClick={() => void copy()}
|
||||
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>
|
||||
{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">
|
||||
{otlpEndpoints(proxyUrl).map(([label, value, copyable]) => (
|
||||
<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">
|
||||
{copyable ? <EndpointValue value={value} /> : <span className="text-[12.5px]">{value}</span>}
|
||||
{copyable ? <EndpointValue value={value} /> : <span className="text-xs">{value}</span>}
|
||||
</dd>
|
||||
</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