feat(ui): add agent and channel dropdown filters to traces page

This commit is contained in:
viettranx committed 2026-03-12 10:38:20 +07:00
1 parent 050de16aa2
commit 225bd3d3b6
8 files changed
+73 -31

No files matched your search

+3
View File
@@ -71,6 +71,9 @@ func (h *TracesHandler) handleList(w http.ResponseWriter, r *http.Request) {
if v := r.URL.Query().Get("status"); v != "" {
opts.Status = v
}
if v := r.URL.Query().Get("channel"); v != "" {
opts.Channel = v
}
if v := r.URL.Query().Get("limit"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 200 {
opts.Limit = n
+5
View File
@@ -115,6 +115,11 @@ func buildTraceWhere(opts store.TraceListOpts) (string, []any) {
args = append(args, opts.Status)
argIdx++
}
if opts.Channel != "" {
conditions = append(conditions, fmt.Sprintf("channel = $%d", argIdx))
args = append(args, opts.Channel)
argIdx++
}
where := ""
if len(conditions) > 0 {
+7 -6
View File
@@ -96,12 +96,13 @@ type SpanData struct {
// TraceListOpts configures trace listing.
type TraceListOpts struct {
AgentID *uuid.UUID
UserID string
SessionKey string
Status string
Limit int
Offset int
AgentID *uuid.UUID
UserID string
SessionKey string
Status string
Channel string
Limit int
Offset int
}
// CostSummaryOpts configures cost aggregation queries.
+2
View File
@@ -3,6 +3,8 @@
"description": "LLM call traces and performance data",
"filterPlaceholder": "Filter by agent ID...",
"filter": "Filter",
"allAgents": "All agents",
"allChannels": "All channels",
"emptyTitle": "No traces",
"emptyDescription": "No traces found. Traces are recorded when agents process requests.",
"columns": {
+2
View File
@@ -3,6 +3,8 @@
"description": "Theo dõi lệnh gọi LLM và dữ liệu hiệu suất",
"filterPlaceholder": "Lọc theo agent ID...",
"filter": "Lọc",
"allAgents": "Tất cả agent",
"allChannels": "Tất cả kênh",
"emptyTitle": "Không có dữ liệu theo dõi",
"emptyDescription": "Không tìm thấy dữ liệu theo dõi. Theo dõi được ghi lại khi agent xử lý yêu cầu.",
"columns": {
+2
View File
@@ -30,6 +30,8 @@
},
"emptyDescription": "暂无追踪记录。当Agent处理请求时将自动记录追踪。",
"emptyTitle": "暂无追踪",
"allAgents": "所有Agent",
"allChannels": "所有渠道",
"filter": "筛选",
"filterPlaceholder": "按Agent ID 筛选...",
"span": {
@@ -10,6 +10,7 @@ export interface TraceFilters {
agentId?: string;
userId?: string;
status?: string;
channel?: string;
limit?: number;
offset?: number;
}
@@ -27,6 +28,7 @@ export function useTraces(filters: TraceFilters = {}) {
if (filters.agentId) params.agent_id = filters.agentId;
if (filters.userId) params.user_id = filters.userId;
if (filters.status) params.status = filters.status;
if (filters.channel) params.channel = filters.channel;
if (filters.limit) params.limit = String(filters.limit);
if (filters.offset !== undefined) params.offset = String(filters.offset);
+50 -25
View File
@@ -1,9 +1,15 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Activity, GitFork, RefreshCw, Search } from "lucide-react";
import { Activity, GitFork, RefreshCw } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { PageHeader } from "@/components/shared/page-header";
import { EmptyState } from "@/components/shared/empty-state";
import { Pagination } from "@/components/shared/pagination";
@@ -13,18 +19,24 @@ import { useTraces, type TraceData } from "./hooks/use-traces";
import { TraceDetailDialog } from "./trace-detail-dialog";
import { useMinLoading } from "@/hooks/use-min-loading";
import { useDeferredLoading } from "@/hooks/use-deferred-loading";
import { useAgents } from "@/pages/agents/hooks/use-agents";
import { useChannelInstances } from "@/pages/channels/hooks/use-channel-instances";
export function TracesPage() {
const { t } = useTranslation("traces");
const { t: tc } = useTranslation("common");
const [agentFilter, setAgentFilter] = useState("");
const [appliedAgentFilter, setAppliedAgentFilter] = useState("");
const [agentFilter, setAgentFilter] = useState<string>();
const [channelFilter, setChannelFilter] = useState<string>();
const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const { agents } = useAgents();
const { instances: channels } = useChannelInstances();
const { traces, total, loading, fetching, refresh, getTrace } = useTraces({
agentId: appliedAgentFilter || undefined,
agentId: agentFilter,
channel: channelFilter,
limit: pageSize,
offset: (page - 1) * pageSize,
});
@@ -33,12 +45,6 @@ export function TracesPage() {
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const handleFilterSubmit = (e: React.FormEvent) => {
e.preventDefault();
setAppliedAgentFilter(agentFilter);
setPage(1);
};
return (
<div className="p-4 sm:p-6">
<PageHeader
@@ -51,20 +57,39 @@ export function TracesPage() {
}
/>
<form onSubmit={handleFilterSubmit} className="mt-4 flex gap-2">
<div className="relative max-w-sm flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={agentFilter}
onChange={(e) => setAgentFilter(e.target.value)}
placeholder={t("filterPlaceholder")}
className="pl-9"
/>
</div>
<Button type="submit" variant="outline" size="sm">
{t("filter")}
</Button>
</form>
<div className="mt-4 flex flex-wrap items-center gap-2">
{/* Agent filter */}
<Select
value={agentFilter ?? "__all__"}
onValueChange={(v) => { setAgentFilter(v === "__all__" ? undefined : v); setPage(1); }}
>
<SelectTrigger className="h-8 w-44 text-xs">
<SelectValue placeholder={t("allAgents")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{t("allAgents")}</SelectItem>
{agents.map((a) => (
<SelectItem key={a.id} value={a.id}>{a.display_name || a.agent_key || a.id}</SelectItem>
))}
</SelectContent>
</Select>
{/* Channel filter */}
<Select
value={channelFilter ?? "__all__"}
onValueChange={(v) => { setChannelFilter(v === "__all__" ? undefined : v); setPage(1); }}
>
<SelectTrigger className="h-8 w-44 text-xs">
<SelectValue placeholder={t("allChannels")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">{t("allChannels")}</SelectItem>
{channels.map((ch) => (
<SelectItem key={ch.id} value={ch.name}>{ch.display_name || ch.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="mt-4">
{showSkeleton ? (