+ {/* Sidebar Filters */}
+
+
+ {/* Main Content */}
+
{statCards.map((card) => (
@@ -990,6 +996,7 @@ export default function LogsPage() {
isSocketConnected={isSocketConnected}
fetchLogs={fetchLogs}
fetchStats={fetchStats}
+ sidebarFilters
/>
diff --git a/ui/app/workspace/logs/sheets/logDetailView.tsx b/ui/app/workspace/logs/sheets/logDetailView.tsx
index 338688b876..4d52f726df 100644
--- a/ui/app/workspace/logs/sheets/logDetailView.tsx
+++ b/ui/app/workspace/logs/sheets/logDetailView.tsx
@@ -33,8 +33,8 @@ import {
} from "@/lib/constants/logs";
import { LogEntry } from "@/lib/types/logs";
import { Link } from "@tanstack/react-router";
-import { Clipboard, Loader2, MoreVertical, Trash2 } from "lucide-react";
import { addMilliseconds, format } from "date-fns";
+import { Clipboard, Loader2, MoreVertical, Trash2 } from "lucide-react";
import type { ReactNode } from "react";
import { toast } from "sonner";
import BlockHeader from "../views/blockHeader";
diff --git a/ui/app/workspace/logs/views/emptyState.tsx b/ui/app/workspace/logs/views/emptyState.tsx
index 1f0d0082d3..036cb87af5 100644
--- a/ui/app/workspace/logs/views/emptyState.tsx
+++ b/ui/app/workspace/logs/views/emptyState.tsx
@@ -3,8 +3,8 @@ import { Button } from "@/components/ui/button";
import { CodeEditor } from "@/components/ui/codeEditor";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
-import { getExampleBaseUrl } from "@/lib/utils/port";
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard";
+import { getExampleBaseUrl } from "@/lib/utils/port";
import { AlertTriangle, Copy } from "lucide-react";
import { useMemo, useState } from "react";
@@ -251,7 +251,7 @@ const result = await chain.invoke({ input: "What is LangChain?" });`,
)}
-
+
Integrate under 60 seconds
diff --git a/ui/app/workspace/logs/views/filters.tsx b/ui/app/workspace/logs/views/filters.tsx
index 68df53c49d..8ec7ad7318 100644
--- a/ui/app/workspace/logs/views/filters.tsx
+++ b/ui/app/workspace/logs/views/filters.tsx
@@ -53,9 +53,19 @@ interface LogFiltersProps {
onLiveToggle: (enabled: boolean) => void;
fetchLogs: () => Promise
;
fetchStats: () => Promise;
+ /** When true, hide FilterPopover and DateTimePicker (they live in the sidebar instead) */
+ hidePopoverFilters?: boolean;
}
-export function LogFilters({ filters, onFiltersChange, liveEnabled, onLiveToggle, fetchLogs, fetchStats }: LogFiltersProps) {
+export function LogFilters({
+ filters,
+ onFiltersChange,
+ liveEnabled,
+ onLiveToggle,
+ fetchLogs,
+ fetchStats,
+ hidePopoverFilters,
+}: LogFiltersProps) {
const [openMoreActionsPopover, setOpenMoreActionsPopover] = useState(false);
const [localSearch, setLocalSearch] = useState(filters.content_search || "");
const searchTimeoutRef = useRef(undefined);
@@ -147,7 +157,7 @@ export function LogFilters({ filters, onFiltersChange, liveEnabled, onLiveToggle
);
return (
-
+
-
{
- setStartTime(p.from);
- setEndTime(p.to);
- onFiltersChange({
- ...filters,
- start_time: p.from?.toISOString(),
- end_time: p.to?.toISOString(),
- });
- }}
- preDefinedPeriods={LOG_TIME_PERIODS}
- onPredefinedPeriodChange={(periodValue) => {
- if (!periodValue) return;
- const { from, to } = getRangeForPeriod(periodValue);
- setStartTime(from);
- setEndTime(to);
- onFiltersChange({
- ...filters,
- start_time: from.toISOString(),
- end_time: to.toISOString(),
- });
- }}
- />
-
+ {!hidePopoverFilters && (
+ <>
+ {
+ setStartTime(p.from);
+ setEndTime(p.to);
+ onFiltersChange({
+ ...filters,
+ start_time: p.from?.toISOString(),
+ end_time: p.to?.toISOString(),
+ });
+ }}
+ preDefinedPeriods={LOG_TIME_PERIODS}
+ onPredefinedPeriodChange={(periodValue) => {
+ if (!periodValue) return;
+ const { from, to } = getRangeForPeriod(periodValue);
+ setStartTime(from);
+ setEndTime(to);
+ onFiltersChange({
+ ...filters,
+ start_time: from.toISOString(),
+ end_time: to.toISOString(),
+ });
+ }}
+ />
+
+ >
+ )}
-
diff --git a/ui/app/workspace/logs/views/logsSidebar.tsx b/ui/app/workspace/logs/views/logsSidebar.tsx
new file mode 100644
index 0000000000..8faaf1f9d8
--- /dev/null
+++ b/ui/app/workspace/logs/views/logsSidebar.tsx
@@ -0,0 +1,735 @@
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { Input } from "@/components/ui/input";
+import { ScrollArea } from "@/components/ui/scrollArea";
+import { Skeleton } from "@/components/ui/skeleton";
+import { RequestTypeLabels, RequestTypes, RoutingEngineUsedLabels, Statuses } from "@/lib/constants/logs";
+import { useGetAvailableFilterDataQuery, useGetProvidersQuery } from "@/lib/store";
+import type { LogFilters } from "@/lib/types/logs";
+import { cn } from "@/lib/utils";
+import { ChevronDown, RotateCcw } from "lucide-react";
+import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
+
+// ---------------------------------------------------------------------------
+// LogsSidebar – orchestrator
+// ---------------------------------------------------------------------------
+
+interface LogsSidebarProps {
+ filters: LogFilters;
+ onFiltersChange: (filters: LogFilters) => void;
+}
+
+export function LogsSidebar({ filters, onFiltersChange }: LogsSidebarProps) {
+ const activeFilterCount = useMemo(() => {
+ const excludedKeys = ["start_time", "end_time", "content_search", "metadata_filters"];
+ let count = Object.entries(filters).reduce((c, [key, value]) => {
+ if (excludedKeys.includes(key)) return c;
+ if (Array.isArray(value)) return c + value.length;
+ return c + (value ? 1 : 0);
+ }, 0);
+ if (filters.metadata_filters) {
+ count += Object.keys(filters.metadata_filters).length;
+ }
+ return count;
+ }, [filters]);
+
+ const handleReset = useCallback(() => {
+ onFiltersChange({
+ start_time: filters.start_time,
+ end_time: filters.end_time,
+ });
+ }, [filters.start_time, filters.end_time, onFiltersChange]);
+
+ return (
+
+ {/* Header */}
+
+ Filters
+ {activeFilterCount > 0 && (
+
+
+ Reset
+
+ )}
+
+
+ {/* Scrollable filter sections */}
+
+
+ {/* First 2 open by default */}
+
+
+ {/* Rest closed unless they have active filters */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Shared helpers & primitives
+// ---------------------------------------------------------------------------
+
+function groupByName(items: { name: string; id: string }[]) {
+ const map = new Map();
+ for (const item of items) {
+ const ids = map.get(item.name) || [];
+ ids.push(item.id);
+ map.set(item.name, ids);
+ }
+ return map;
+}
+
+function dedup(items: { name: string }[]) {
+ return [...new Map(items.map((i) => [i.name, i])).values()].map((i) => i.name);
+}
+
+/** Shared props every individual filter component receives. */
+interface FilterComponentProps {
+ filters: LogFilters;
+ onFiltersChange: (filters: LogFilters) => void;
+ defaultOpen?: boolean;
+}
+
+// ---------------------------------------------------------------------------
+// FilterSection – collapsible wrapper
+// ---------------------------------------------------------------------------
+
+function FilterSectionSkeleton({ rows = 3 }: { rows?: number }) {
+ return (
+ <>
+ {Array.from({ length: rows }).map((_, i) => (
+
+
+
+
+ ))}
+ >
+ );
+}
+
+function FilterSection({
+ title,
+ children,
+ defaultOpen = false,
+ loading = false,
+ onOpenChange,
+ testId,
+}: {
+ title: string;
+ children: React.ReactNode;
+ defaultOpen?: boolean;
+ loading?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ testId?: string;
+}) {
+ const [open, setOpen] = useState(defaultOpen);
+
+ // Force open when defaultOpen flips to true (e.g. a filter in this section becomes active)
+ useEffect(() => {
+ if (defaultOpen) setOpen(true);
+ }, [defaultOpen]);
+
+ const handleOpenChange = (next: boolean) => {
+ setOpen(next);
+ onOpenChange?.(next);
+ };
+
+ return (
+
+
+
+ {title}
+
+
+ {loading ? : children}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// CheckboxFilterItem – single checkbox row
+// ---------------------------------------------------------------------------
+
+function CheckboxFilterItem({
+ label,
+ checked,
+ onCheckedChange,
+ labelClassName,
+ testId,
+}: {
+ label: string;
+ checked: boolean;
+ onCheckedChange: (checked: boolean) => void;
+ labelClassName?: string;
+ testId?: string;
+}) {
+ return (
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// SearchableCheckboxList – list of checkbox rows with a search input.
+// Caller passes `inputRef` to control focus (see `useAutoFocusOnOpen`).
+// ---------------------------------------------------------------------------
+
+function useAutoFocusOnOpen(isOpen: boolean) {
+ const ref = useRef(null);
+ useEffect(() => {
+ if (isOpen) ref.current?.focus({ preventScroll: true });
+ }, [isOpen]);
+ return ref;
+}
+
+function SearchableCheckboxList({
+ items,
+ isSelected,
+ onToggle,
+ placeholder = "Search...",
+ inputRef,
+ testIdPrefix,
+}: {
+ items: { key: string; label: string }[];
+ isSelected: (key: string) => boolean;
+ onToggle: (key: string) => void;
+ placeholder?: string;
+ inputRef?: Ref;
+ testIdPrefix?: string;
+}) {
+ const [query, setQuery] = useState("");
+ const normalized = query.trim().toLowerCase();
+ const filtered = normalized ? items.filter((item) => item.label.toLowerCase().includes(normalized)) : items;
+
+ return (
+ <>
+
+ setQuery(e.target.value)}
+ placeholder={placeholder}
+ className="h-8 border-0 text-xs"
+ data-testid={testIdPrefix ? `${testIdPrefix}-search` : undefined}
+ />
+
+ {filtered.map((item) => (
+ onToggle(item.key)}
+ testId={testIdPrefix ? `${testIdPrefix}-checkbox-${item.key}` : undefined}
+ />
+ ))}
+ {filtered.length === 0 && No results
}
+ >
+ );
+}
+
+// ---------------------------------------------------------------------------
+// StatusFilter
+// ---------------------------------------------------------------------------
+
+function StatusFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.status || []).length > 0;
+ return (
+
+ {Statuses.map((status) => (
+ {
+ const current = filters.status || [];
+ const next = current.includes(status) ? current.filter((s) => s !== status) : [...current, status];
+ onFiltersChange({ ...filters, status: next });
+ }}
+ testId={`status-filter-checkbox-${status}`}
+ />
+ ))}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// ProvidersFilter – fetches providers internally
+// ---------------------------------------------------------------------------
+
+function ProvidersFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.providers || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: providersData, isUninitialized, isLoading } = useGetProvidersQuery(undefined, { skip: !opened && !hasActive });
+ const availableProviders = providersData || [];
+
+ // Hide only if data was fetched (not loading) and came back empty
+ if (!isUninitialized && !isLoading && availableProviders.length === 0 && !hasActive) return null;
+
+ return (
+
+ ({ key: p.name, label: p.name }))}
+ isSelected={(name) => (filters.providers || []).includes(name)}
+ onToggle={(name) => {
+ const current = filters.providers || [];
+ const next = current.includes(name) ? current.filter((p) => p !== name) : [...current, name];
+ onFiltersChange({ ...filters, providers: next });
+ }}
+ testIdPrefix="providers-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// TypeFilter
+// ---------------------------------------------------------------------------
+
+function TypeFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.objects || []).length > 0;
+ return (
+
+ {RequestTypes.map((type) => {
+ const label = RequestTypeLabels[type as keyof typeof RequestTypeLabels] ?? type;
+ return (
+ {
+ const current = filters.objects || [];
+ const next = current.includes(type) ? current.filter((t) => t !== type) : [...current, type];
+ onFiltersChange({ ...filters, objects: next });
+ }}
+ testId={`type-filter-checkbox-${type}`}
+ />
+ );
+ })}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// ModelsFilter – fetches available models internally
+// ---------------------------------------------------------------------------
+
+function ModelsFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.models || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableModels = filterData?.models || [];
+
+ if (!isUninitialized && !isLoading && availableModels.length === 0 && !hasActive) return null;
+
+ return (
+
+ ({ key: m, label: m }))}
+ isSelected={(model) => (filters.models || []).includes(model)}
+ onToggle={(model) => {
+ const current = filters.models || [];
+ const next = current.includes(model) ? current.filter((m) => m !== model) : [...current, model];
+ onFiltersChange({ ...filters, models: next });
+ }}
+ testIdPrefix="models-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// AliasesFilter – fetches available aliases internally
+// ---------------------------------------------------------------------------
+
+function AliasesFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.aliases || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableAliases = filterData?.aliases || [];
+
+ if (!isUninitialized && !isLoading && availableAliases.length === 0 && !hasActive) return null;
+
+ return (
+
+ ({ key: a, label: a }))}
+ isSelected={(alias) => (filters.aliases || []).includes(alias)}
+ onToggle={(alias) => {
+ const current = filters.aliases || [];
+ const next = current.includes(alias) ? current.filter((a) => a !== alias) : [...current, alias];
+ onFiltersChange({ ...filters, aliases: next });
+ }}
+ testIdPrefix="aliases-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// SelectedKeysFilter – fetches keys, resolves name→IDs for deduplication
+// ---------------------------------------------------------------------------
+
+function SelectedKeysFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.selected_key_ids || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableSelectedKeys = filterData?.selected_keys || [];
+ const nameToIds = useMemo(() => groupByName(availableSelectedKeys), [availableSelectedKeys]);
+
+ if (!isUninitialized && !isLoading && availableSelectedKeys.length === 0 && !hasActive) return null;
+
+ const toggle = (name: string) => {
+ const resolvedIds = nameToIds.get(name) || [name];
+ const current = filters.selected_key_ids || [];
+ const allSelected = resolvedIds.every((id) => current.includes(id));
+ const next = allSelected
+ ? current.filter((v) => !resolvedIds.includes(v))
+ : [...current, ...resolvedIds.filter((id) => !current.includes(id))];
+ onFiltersChange({ ...filters, selected_key_ids: next });
+ };
+
+ const isSelected = (name: string) => {
+ const resolvedIds = nameToIds.get(name) || [name];
+ const current = filters.selected_key_ids || [];
+ return resolvedIds.every((id) => current.includes(id));
+ };
+
+ return (
+
+ ({ key: name, label: name }))}
+ isSelected={isSelected}
+ onToggle={toggle}
+ testIdPrefix="selected-keys-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// VirtualKeysFilter
+// ---------------------------------------------------------------------------
+
+function VirtualKeysFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.virtual_key_ids || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableVirtualKeys = filterData?.virtual_keys || [];
+ const nameToIds = useMemo(() => groupByName(availableVirtualKeys), [availableVirtualKeys]);
+
+ if (!isUninitialized && !isLoading && availableVirtualKeys.length === 0 && !hasActive) return null;
+
+ const toggle = (name: string) => {
+ const resolvedIds = nameToIds.get(name) || [name];
+ const current = filters.virtual_key_ids || [];
+ const allSelected = resolvedIds.every((id) => current.includes(id));
+ const next = allSelected
+ ? current.filter((v) => !resolvedIds.includes(v))
+ : [...current, ...resolvedIds.filter((id) => !current.includes(id))];
+ onFiltersChange({ ...filters, virtual_key_ids: next });
+ };
+
+ const isSelected = (name: string) => {
+ const resolvedIds = nameToIds.get(name) || [name];
+ const current = filters.virtual_key_ids || [];
+ return resolvedIds.every((id) => current.includes(id));
+ };
+
+ return (
+
+ ({ key: name, label: name }))}
+ isSelected={isSelected}
+ onToggle={toggle}
+ testIdPrefix="virtual-keys-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// RoutingEnginesFilter
+// ---------------------------------------------------------------------------
+
+function RoutingEnginesFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.routing_engine_used || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableRoutingEngines = filterData?.routing_engines || [];
+
+ if (!isUninitialized && !isLoading && availableRoutingEngines.length === 0 && !hasActive) return null;
+
+ return (
+
+ ({
+ key: engine,
+ label: RoutingEngineUsedLabels[engine as keyof typeof RoutingEngineUsedLabels] ?? engine,
+ }))}
+ isSelected={(engine) => (filters.routing_engine_used || []).includes(engine)}
+ onToggle={(engine) => {
+ const current = filters.routing_engine_used || [];
+ const next = current.includes(engine) ? current.filter((e) => e !== engine) : [...current, engine];
+ onFiltersChange({ ...filters, routing_engine_used: next });
+ }}
+ testIdPrefix="routing-engines-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// RoutingRulesFilter
+// ---------------------------------------------------------------------------
+
+function RoutingRulesFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = (filters.routing_rule_ids || []).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const searchInputRef = useAutoFocusOnOpen(opened);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableRoutingRules = filterData?.routing_rules || [];
+ const nameToIds = useMemo(() => groupByName(availableRoutingRules), [availableRoutingRules]);
+
+ if (!isUninitialized && !isLoading && availableRoutingRules.length === 0 && !hasActive) return null;
+
+ const toggle = (name: string) => {
+ const resolvedIds = nameToIds.get(name) || [name];
+ const current = filters.routing_rule_ids || [];
+ const allSelected = resolvedIds.every((id) => current.includes(id));
+ const next = allSelected
+ ? current.filter((v) => !resolvedIds.includes(v))
+ : [...current, ...resolvedIds.filter((id) => !current.includes(id))];
+ onFiltersChange({ ...filters, routing_rule_ids: next });
+ };
+
+ const isSelected = (name: string) => {
+ const resolvedIds = nameToIds.get(name) || [name];
+ const current = filters.routing_rule_ids || [];
+ return resolvedIds.every((id) => current.includes(id));
+ };
+
+ return (
+
+ ({ key: name, label: name }))}
+ isSelected={isSelected}
+ onToggle={toggle}
+ testIdPrefix="routing-rules-filter"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// SessionFilter
+// ---------------------------------------------------------------------------
+
+function SessionFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = !!filters.parent_request_id;
+ return (
+
+
+ onFiltersChange({ ...filters, parent_request_id: e.target.value })}
+ placeholder="Parent request ID"
+ className="h-8 text-sm"
+ data-testid="session-filter-input"
+ />
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// CostFilter
+// ---------------------------------------------------------------------------
+
+function CostFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = !!filters.missing_cost_only;
+ return (
+
+ onFiltersChange({ ...filters, missing_cost_only: !!checked })}
+ testId="cost-filter-missing-only-checkbox"
+ />
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// MetadataFilters – fetches metadata keys internally
+// ---------------------------------------------------------------------------
+
+function MetadataFilters({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
+ const hasActive = !!filters.metadata_filters && Object.keys(filters.metadata_filters).length > 0;
+ const [opened, setOpened] = useState(defaultOpen || hasActive);
+ const { data: filterData, isUninitialized, isLoading } = useGetAvailableFilterDataQuery(undefined, { skip: !opened && !hasActive });
+ const availableMetadataKeys = filterData?.metadata_keys || {};
+ const [customInputs, setCustomInputs] = useState>({});
+
+ const handleChange = useCallback(
+ (metadataKey: string, value: string | undefined) => {
+ const current = { ...(filters.metadata_filters || {}) };
+ if (value === undefined) {
+ delete current[metadataKey];
+ } else {
+ current[metadataKey] = value;
+ }
+ onFiltersChange({
+ ...filters,
+ metadata_filters: Object.keys(current).length > 0 ? current : undefined,
+ });
+ },
+ [filters, onFiltersChange],
+ );
+
+ const entries = Object.entries(availableMetadataKeys);
+ const isEmpty = !isUninitialized && !isLoading && entries.length === 0 && !hasActive;
+
+ return (
+
+ {isEmpty ? (
+ No metadata keys
+ ) : (
+ entries.map(([metadataKey, values]) => (
+
+
{metadataKey}
+ {values.map((value: string) => (
+
{
+ const currentValue = filters.metadata_filters?.[metadataKey];
+ handleChange(metadataKey, currentValue === value ? undefined : value);
+ }}
+ testId={`metadata-${metadataKey}-filter-checkbox-${value}`}
+ />
+ ))}
+
+ {
+ const newVal = e.target.value;
+ setCustomInputs((prev) => ({ ...prev, [metadataKey]: newVal }));
+ if (newVal === "" && filters.metadata_filters?.[metadataKey]) {
+ handleChange(metadataKey, undefined);
+ }
+ }}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && customInputs[metadataKey]?.trim()) {
+ handleChange(metadataKey, customInputs[metadataKey].trim());
+ }
+ }}
+ data-testid={`metadata-${metadataKey}-filter-custom-input`}
+ />
+
+
+ ))
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/ui/app/workspace/logs/views/logsTable.tsx b/ui/app/workspace/logs/views/logsTable.tsx
index e94a0f120b..7253a6f128 100644
--- a/ui/app/workspace/logs/views/logsTable.tsx
+++ b/ui/app/workspace/logs/views/logsTable.tsx
@@ -44,6 +44,8 @@ interface DataTableProps {
onLiveToggle: (enabled: boolean) => void;
fetchLogs: () => Promise;
fetchStats: () => Promise;
+ /** When true, filters are rendered in a sidebar — hide them from the table header */
+ sidebarFilters?: boolean;
}
export function LogsDataTable({
@@ -61,6 +63,7 @@ export function LogsDataTable({
onLiveToggle,
fetchLogs,
fetchStats,
+ sidebarFilters = false,
}: DataTableProps) {
const [sorting, setSorting] = useState([{ id: pagination.sort_by, desc: pagination.order === "desc" }]);
const tableContainerRef = useRef(null);
@@ -165,16 +168,30 @@ export function LogsDataTable({
return (
-
-
-
+ {sidebarFilters ? (
+
+
+
+ ) : (
+
+
+
+ )}
diff --git a/ui/app/workspace/logs/views/logsVolumeChart.tsx b/ui/app/workspace/logs/views/logsVolumeChart.tsx
index 424f45746d..7257b1175b 100644
--- a/ui/app/workspace/logs/views/logsVolumeChart.tsx
+++ b/ui/app/workspace/logs/views/logsVolumeChart.tsx
@@ -318,7 +318,7 @@ export function LogsVolumeChart({
-
+
diff --git a/ui/components/table/columnConfigDropdown.tsx b/ui/components/table/columnConfigDropdown.tsx
index 04f32566fb..dc093bb58a 100644
--- a/ui/components/table/columnConfigDropdown.tsx
+++ b/ui/components/table/columnConfigDropdown.tsx
@@ -1,8 +1,8 @@
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import type { ColumnConfigEntry } from "./hooks/useColumnConfig";
import { Columns3, RotateCcw } from "lucide-react";
+import type { ColumnConfigEntry } from "./hooks/useColumnConfig";
interface ColumnConfigDropdownProps {
entries: ColumnConfigEntry[];
@@ -22,7 +22,7 @@ export function ColumnConfigDropdown({ entries, labels = {}, onToggleVisibility,
return (
-
+