fix(ui): improve traces table layout and readability

Compact columns: status as icon-only, merge time+duration into one column.
Truncate long user IDs, clean <media:*> tags from preview, move badges to second line.
This commit is contained in:
viettranx committed 2026-04-06 10:42:47 +07:00
1 parent 6ddc112940
commit 85d44e8471
2 files changed
+53 -37

No files matched your search

+6 -2
View File
@@ -25,7 +25,11 @@ export function formatUserLabel(userId: string, resolve?: Resolver): string {
}
}
// Fallback: prefix numeric IDs with #, string IDs with @
if (/^\d+$/.test(userId)) return `#${userId}`;
// Fallback: prefix numeric IDs with #
if (/^-?\d+$/.test(userId)) return `#${userId}`;
// Truncate long opaque IDs (e.g. oc_295eb80d325c976cbeb4a779e2010518)
if (userId.length > 20) return `${userId.slice(0, 10)}…${userId.slice(-4)}`;
return userId;
}
+47 -35
View File
@@ -1,6 +1,6 @@
import { useState, useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Activity, GitFork, RefreshCw, Square, Bot, User, Users, Clock, Network, Globe } from "lucide-react";
import { Activity, GitFork, RefreshCw, Square, Bot, User, Users, Clock, Network, Globe, CheckCircle2, XCircle, Loader2, CircleDot, CircleDashed } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
@@ -28,6 +28,12 @@ import { useWs } from "@/hooks/use-ws";
import { Methods } from "@/api/protocol";
import { toast } from "@/stores/use-toast-store";
/** Strip media placeholder tags like <media:image> from preview text */
function cleanPreview(text: string): string {
if (!text) return text;
return text.replace(/<media:\w+>/g, "[media]");
}
/** Parse session_key to extract source type: Direct, Group, Cron, Team, WS */
function parseSourceType(sessionKey: string): { type: string; topic?: string } {
if (!sessionKey) return { type: "unknown" };
@@ -168,15 +174,14 @@ export function TracesPage() {
/>
) : (
<div className="rounded-md border overflow-x-auto">
<table className="w-full min-w-[700px] text-sm">
<table className="w-full min-w-[600px] text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="px-4 py-3 text-left font-medium">{t("columns.name")}</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.status")}</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.duration")}</th>
<th className="px-3 py-3 text-center font-medium w-10"></th>
<th className="px-4 py-3 text-left font-medium">{t("columns.tokens")}</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.spans")}</th>
<th className="px-4 py-3 text-left font-medium">{t("columns.time")}</th>
<th className="px-4 py-3 text-center font-medium">{t("columns.spans")}</th>
<th className="px-4 py-3 text-right font-medium">{t("columns.time")}</th>
</tr>
</thead>
<tbody>
@@ -192,14 +197,20 @@ export function TracesPage() {
className="cursor-pointer border-b last:border-0 hover:bg-muted/30"
onClick={() => setSelectedTraceId(trace.id)}
>
<td className="max-w-[360px] px-4 py-2.5">
<div className="flex items-center gap-1.5 text-sm font-medium">
<td className="px-4 py-2.5">
<div className="flex items-center gap-1.5 text-sm font-medium min-w-0">
{trace.parent_trace_id && (
<GitFork className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
)}
<span className="truncate">{agentName || trace.name || t("unnamed")}</span>
<span className="shrink-0 text-muted-foreground">·</span>
<span className="shrink-0 text-xs text-muted-foreground">{userLabel}</span>
{userLabel && (
<>
<span className="shrink-0 text-muted-foreground">·</span>
<span className="truncate text-xs text-muted-foreground max-w-[140px]">{userLabel}</span>
</>
)}
</div>
<div className="mt-0.5 flex items-center gap-1">
<Badge variant="outline" className="shrink-0 gap-0.5 text-[10px] px-1.5 py-0">
<SourceIcon className="h-2.5 w-2.5" />
{t(`source.${source.type}`)}
@@ -210,16 +221,16 @@ export function TracesPage() {
{trace.channel}
</Badge>
)}
{trace.input_preview && (
<span className="truncate text-xs text-muted-foreground">
{cleanPreview(trace.input_preview)}
</span>
)}
</div>
{trace.input_preview && (
<p className="mt-0.5 truncate text-xs text-muted-foreground">
{trace.input_preview}
</p>
)}
</td>
<td className="px-4 py-2.5">
<div className="flex items-center gap-1.5">
<StatusBadge status={trace.status} />
<td className="px-3 py-2.5 text-center">
<div className="flex items-center justify-center gap-1">
<StatusIcon status={trace.status} />
{(trace.status === "running") && (
<Button
variant="destructive"
@@ -233,9 +244,6 @@ export function TracesPage() {
)}
</div>
</td>
<td className="px-4 py-2.5 text-muted-foreground">
{formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))}
</td>
<td className="px-4 py-2.5 text-muted-foreground">
<div>{formatTokens(trace.total_input_tokens)} / {formatTokens(trace.total_output_tokens)}</div>
{(trace.metadata?.total_cache_read_tokens ?? 0) > 0 && (
@@ -244,11 +252,12 @@ export function TracesPage() {
</div>
)}
</td>
<td className="px-4 py-2.5 text-muted-foreground">
<td className="px-4 py-2.5 text-center text-muted-foreground">
{trace.span_count}
</td>
<td className="px-4 py-2.5 text-muted-foreground">
{formatDate(trace.start_time, tz)}
<td className="px-4 py-2.5 text-right text-muted-foreground whitespace-nowrap">
<div>{formatDate(trace.start_time, tz)}</div>
<div className="text-xs">{formatDuration(trace.duration_ms || computeDurationMs(trace.start_time, trace.end_time))}</div>
</td>
</tr>
);
@@ -280,15 +289,18 @@ export function TracesPage() {
);
}
function StatusBadge({ status }: { status: string }) {
const variant =
status === "ok" || status === "success" || status === "completed"
? "success"
: status === "error" || status === "failed"
? "destructive"
: status === "running" || status === "pending"
? "info"
: "secondary";
return <Badge variant={variant} className="text-xs">{status || "unknown"}</Badge>;
function StatusIcon({ status }: { status: string }) {
if (status === "ok" || status === "success" || status === "completed") {
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
}
if (status === "error" || status === "failed") {
return <XCircle className="h-4 w-4 text-destructive" />;
}
if (status === "running") {
return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
}
if (status === "pending") {
return <CircleDashed className="h-4 w-4 text-muted-foreground" />;
}
return <CircleDot className="h-4 w-4 text-muted-foreground" />;
}