first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,795 @@
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, PanelLeftClose, PanelLeftOpen, RotateCcw } from "lucide-react";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
const COLLAPSE_STORAGE_KEY = "logs-filter-sidebar-collapsed";
// ---------------------------------------------------------------------------
// LogsSidebar orchestrator
// ---------------------------------------------------------------------------
interface LogsSidebarProps {
filters: LogFilters;
onFiltersChange: (filters: LogFilters) => void;
}
export function LogsFilterSidebar({ filters, onFiltersChange }: LogsSidebarProps) {
const [collapsed, setCollapsed] = useState(false);
// Load persisted collapsed state on mount
useEffect(() => {
if (typeof window === "undefined") return;
const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY);
if (stored === "true") setCollapsed(true);
}, []);
const toggleCollapsed = useCallback(() => {
setCollapsed((prev) => {
const next = !prev;
if (typeof window !== "undefined") {
window.localStorage.setItem(COLLAPSE_STORAGE_KEY, String(next));
}
return next;
});
}, []);
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]);
// Collapsed: thin rail with vertical "Filters" label — whole rail is clickable to expand
if (collapsed) {
return (
<button
type="button"
onClick={toggleCollapsed}
className="bg-card group flex h-full w-10 shrink-0 cursor-pointer flex-col items-center gap-3 rounded-r-md py-3 text-sm font-medium"
title="Show filters"
aria-label="Show filters"
>
<PanelLeftOpen className="text-muted-foreground group-hover:text-foreground size-4 transition-colors" />
<span className="rotate-180 select-none [writing-mode:vertical-rl]">Filters</span>
{activeFilterCount > 0 && (
<span className="bg-primary/10 text-primary flex size-6 items-center justify-center rounded-full text-xs font-medium">
{activeFilterCount}
</span>
)}
</button>
);
}
return (
<div className="bg-card flex h-full w-64 shrink-0 flex-col rounded-r-md">
{/* Header */}
<div className="flex h-11 items-center justify-between border-b pr-2 pl-5">
<span className="text-sm font-semibold">Filters</span>
<div className="flex items-center gap-1">
{activeFilterCount > 0 && (
<Button variant="outline" size="sm" className="text-muted-foreground h-7 px-2 text-xs" onClick={handleReset}>
<RotateCcw className="size-3" />
Reset
</Button>
)}
<Button variant="ghost" size="icon" className="size-7" onClick={toggleCollapsed} title="Hide filters" aria-label="Hide filters">
<PanelLeftClose className="size-4" />
</Button>
</div>
</div>
{/* Scrollable filter sections */}
<ScrollArea className="flex flex-1 overflow-y-auto p-2 pb-0" viewportClassName="no-table">
<div className="flex grow flex-col gap-1">
{/* First 2 open by default */}
<StatusFilter filters={filters} onFiltersChange={onFiltersChange} defaultOpen />
<ModelsFilter filters={filters} onFiltersChange={onFiltersChange} defaultOpen />
{/* Rest closed unless they have active filters */}
<SelectedKeysFilter filters={filters} onFiltersChange={onFiltersChange} />
<VirtualKeysFilter filters={filters} onFiltersChange={onFiltersChange} />
<ProvidersFilter filters={filters} onFiltersChange={onFiltersChange} />
<TypeFilter filters={filters} onFiltersChange={onFiltersChange} />
<AliasesFilter filters={filters} onFiltersChange={onFiltersChange} />
<RoutingEnginesFilter filters={filters} onFiltersChange={onFiltersChange} />
<RoutingRulesFilter filters={filters} onFiltersChange={onFiltersChange} />
<UserFilter filters={filters} onFiltersChange={onFiltersChange} />
<SessionFilter filters={filters} onFiltersChange={onFiltersChange} />
<CostFilter filters={filters} onFiltersChange={onFiltersChange} />
<MetadataFilters filters={filters} onFiltersChange={onFiltersChange} />
</div>
</ScrollArea>
</div>
);
}
// ---------------------------------------------------------------------------
// Shared helpers & primitives
// ---------------------------------------------------------------------------
function groupByName(items: { name: string; id: string }[]) {
const map = new Map<string, string[]>();
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) => (
<div key={i} className="flex items-center gap-2.5 px-3 py-2">
<Skeleton className="size-4 shrink-0 rounded-[4px]" />
<Skeleton className="h-3.5 w-full rounded" />
</div>
))}
</>
);
}
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 (
<Collapsible open={open} onOpenChange={handleOpenChange} className="last:pb-2">
<CollapsibleTrigger
className="flex h-8 w-full cursor-pointer items-center gap-1.5 px-2 py-2 text-sm font-medium hover:opacity-80"
data-testid={testId}
>
<ChevronDown className={cn("size-3.5 transition-transform", open ? "rotate-0" : "-rotate-90")} />
<span>{title}</span>
</CollapsibleTrigger>
<CollapsibleContent className="pt-1">
<div className="divide-border divide-y overflow-hidden rounded-sm border">{loading ? <FilterSectionSkeleton /> : children}</div>
</CollapsibleContent>
</Collapsible>
);
}
// ---------------------------------------------------------------------------
// CheckboxFilterItem single checkbox row
// ---------------------------------------------------------------------------
function CheckboxFilterItem({
label,
checked,
onCheckedChange,
labelClassName,
testId,
}: {
label: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
labelClassName?: string;
testId?: string;
}) {
return (
<label className="hover:bg-muted/50 flex cursor-pointer items-center gap-2.5 px-3 py-2 text-sm" data-testid={testId}>
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
<span className={cn("truncate", labelClassName)}>{label}</span>
</label>
);
}
// ---------------------------------------------------------------------------
// SearchableCheckboxList list of checkbox rows with a search input.
// Caller passes `inputRef` to control focus (see `useAutoFocusOnOpen`).
// ---------------------------------------------------------------------------
function useAutoFocusOnOpen(isOpen: boolean) {
const ref = useRef<HTMLInputElement>(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<HTMLInputElement>;
testIdPrefix?: string;
}) {
const [query, setQuery] = useState("");
const normalized = query.trim().toLowerCase();
const filtered = normalized ? items.filter((item) => item.label.toLowerCase().includes(normalized)) : items;
return (
<>
<div className="border-b">
<Input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
className="h-8 border-0 text-xs"
data-testid={testIdPrefix ? `${testIdPrefix}-search` : undefined}
/>
</div>
{filtered.map((item) => (
<CheckboxFilterItem
key={item.key}
label={item.label}
checked={isSelected(item.key)}
onCheckedChange={() => onToggle(item.key)}
testId={testIdPrefix ? `${testIdPrefix}-checkbox-${item.key}` : undefined}
/>
))}
{filtered.length === 0 && <div className="text-muted-foreground flex h-9 items-center px-3 text-xs">No results</div>}
</>
);
}
// ---------------------------------------------------------------------------
// StatusFilter
// ---------------------------------------------------------------------------
function StatusFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = (filters.status || []).length > 0;
return (
<FilterSection title="Status" defaultOpen={defaultOpen || hasActive} testId="status-filter-toggle">
{Statuses.map((status) => (
<CheckboxFilterItem
key={status}
labelClassName="capitalize"
label={status}
checked={(filters.status || []).includes(status)}
onCheckedChange={() => {
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}`}
/>
))}
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Providers"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="providers-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search providers"
items={availableProviders.map((p) => ({ 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"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// TypeFilter
// ---------------------------------------------------------------------------
function TypeFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = (filters.objects || []).length > 0;
return (
<FilterSection title="Type" defaultOpen={defaultOpen || hasActive} testId="type-filter-toggle">
{RequestTypes.map((type) => {
const label = RequestTypeLabels[type as keyof typeof RequestTypeLabels] ?? type;
return (
<CheckboxFilterItem
key={type}
label={label}
checked={(filters.objects || []).includes(type)}
onCheckedChange={() => {
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}`}
/>
);
})}
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Models"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="models-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search models"
items={availableModels.map((m) => ({ 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"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Aliases"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="aliases-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search aliases"
items={availableAliases.map((a) => ({ 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"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Selected Keys"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="selected-keys-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search keys"
items={dedup(availableSelectedKeys).map((name) => ({ key: name, label: name }))}
isSelected={isSelected}
onToggle={toggle}
testIdPrefix="selected-keys-filter"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Virtual Keys"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="virtual-keys-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search virtual keys"
items={dedup(availableVirtualKeys).map((name) => ({ key: name, label: name }))}
isSelected={isSelected}
onToggle={toggle}
testIdPrefix="virtual-keys-filter"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Routing Engines"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="routing-engines-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search engines"
items={availableRoutingEngines.map((engine) => ({
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"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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 (
<FilterSection
title="Routing Rules"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="routing-rules-filter-toggle"
>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search rules"
items={dedup(availableRoutingRules).map((name) => ({ key: name, label: name }))}
isSelected={isSelected}
onToggle={toggle}
testIdPrefix="routing-rules-filter"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// SessionFilter
// ---------------------------------------------------------------------------
function SessionFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = !!filters.parent_request_id;
return (
<FilterSection title="Session" defaultOpen={defaultOpen || hasActive} testId="session-filter-toggle">
<Input
value={filters.parent_request_id || ""}
onChange={(e) => onFiltersChange({ ...filters, parent_request_id: e.target.value })}
placeholder="Parent request ID"
className="h-8 border-0 text-sm"
data-testid="session-filter-input"
autoFocus
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// UserFilter
// ---------------------------------------------------------------------------
function UserFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = !!filters.user_ids?.length;
return (
<FilterSection title="User" defaultOpen={defaultOpen || hasActive} testId="user-filter-toggle">
<Input
value={filters.user_ids?.[0] || ""}
onChange={(e) => onFiltersChange({ ...filters, user_ids: e.target.value ? [e.target.value] : [] })}
placeholder="User ID"
className="h-8 border-0 text-sm"
data-testid="user-id-filter-input"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// CostFilter
// ---------------------------------------------------------------------------
function CostFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = !!filters.missing_cost_only;
return (
<FilterSection title="Cost" defaultOpen={defaultOpen || hasActive} testId="cost-filter-toggle">
<CheckboxFilterItem
label="Show missing cost only"
checked={!!filters.missing_cost_only}
onCheckedChange={(checked) => onFiltersChange({ ...filters, missing_cost_only: !!checked })}
testId="cost-filter-missing-only-checkbox"
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// 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<Record<string, string>>({});
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 (
<FilterSection
title="Metadata"
defaultOpen={defaultOpen || hasActive}
loading={isLoading}
onOpenChange={setOpened}
testId="metadata-filter-toggle"
>
{isEmpty ? (
<div className="text-muted-foreground px-3 py-2 text-xs">No metadata keys</div>
) : (
entries.map(([metadataKey, values]) => (
<div key={metadataKey} data-testid={`metadata-${metadataKey}-filter-group`}>
<div className="text-muted-foreground px-3 pt-2 pb-1 text-xs font-medium">{metadataKey}</div>
{values.map((value: string) => (
<CheckboxFilterItem
key={value}
label={value}
checked={filters.metadata_filters?.[metadataKey] === value}
onCheckedChange={() => {
const currentValue = filters.metadata_filters?.[metadataKey];
handleChange(metadataKey, currentValue === value ? undefined : value);
}}
testId={`metadata-${metadataKey}-filter-checkbox-${value}`}
/>
))}
<div className="px-3 py-2.5">
<Input
className="placeholder:text-muted-foreground h-7 w-full rounded border bg-transparent px-2 text-sm"
placeholder="Custom value..."
value={
customInputs[metadataKey] ??
(filters.metadata_filters?.[metadataKey] && !values.includes(filters.metadata_filters[metadataKey])
? filters.metadata_filters[metadataKey]
: "")
}
onChange={(e) => {
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`}
/>
</div>
</div>
))
)}
</FilterSection>
);
}

View File

@@ -0,0 +1,375 @@
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 { Statuses } from "@/lib/constants/logs";
import { useGetMCPLogsFilterDataQuery } from "@/lib/store";
import type { MCPToolLogFilters } from "@/lib/types/logs";
import { cn } from "@/lib/utils";
import { ChevronDown, PanelLeftClose, PanelLeftOpen, RotateCcw } from "lucide-react";
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
const COLLAPSE_STORAGE_KEY = "mcp-filter-sidebar-collapsed";
// ---------------------------------------------------------------------------
// MCPFilterSidebar orchestrator
// ---------------------------------------------------------------------------
interface MCPFilterSidebarProps {
filters: MCPToolLogFilters;
onFiltersChange: (filters: MCPToolLogFilters) => void;
}
export function MCPFilterSidebar({ filters, onFiltersChange }: MCPFilterSidebarProps) {
const [collapsed, setCollapsed] = useState(false);
// Load persisted collapsed state on mount
useEffect(() => {
if (typeof window === "undefined") return;
const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY);
if (stored === "true") setCollapsed(true);
}, []);
const toggleCollapsed = useCallback(() => {
setCollapsed((prev) => {
const next = !prev;
if (typeof window !== "undefined") {
window.localStorage.setItem(COLLAPSE_STORAGE_KEY, String(next));
}
return next;
});
}, []);
const activeFilterCount = useMemo(() => {
const excludedKeys = ["start_time", "end_time", "content_search"];
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);
return count;
}, [filters]);
const handleReset = useCallback(() => {
onFiltersChange({
start_time: filters.start_time,
end_time: filters.end_time,
});
}, [filters.start_time, filters.end_time, onFiltersChange]);
// Collapsed: thin rail with vertical "Filters" label — whole rail is clickable to expand
if (collapsed) {
return (
<button
type="button"
onClick={toggleCollapsed}
className="bg-card group flex h-full w-10 shrink-0 cursor-pointer flex-col items-center gap-3 rounded-r-md py-3 text-sm font-medium"
title="Show filters"
aria-label="Show filters"
>
<PanelLeftOpen className="text-muted-foreground group-hover:text-foreground size-4 transition-colors" />
<span className="rotate-180 select-none [writing-mode:vertical-rl]">Filters</span>
{activeFilterCount > 0 && (
<span className="bg-primary/10 text-primary flex size-6 items-center justify-center rounded-full text-xs font-medium">
{activeFilterCount}
</span>
)}
</button>
);
}
return (
<div className="bg-card flex h-full w-64 shrink-0 flex-col rounded-r-md">
{/* Header */}
<div className="flex h-11 items-center justify-between border-b pr-2 pl-5">
<span className="text-sm font-semibold">Filters</span>
<div className="flex items-center gap-1">
{activeFilterCount > 0 && (
<Button variant="outline" size="sm" className="text-muted-foreground h-7 px-2 text-xs" onClick={handleReset}>
<RotateCcw className="size-3" />
Reset
</Button>
)}
<Button variant="ghost" size="icon" className="size-7" onClick={toggleCollapsed} title="Hide filters" aria-label="Hide filters">
<PanelLeftClose className="size-4" />
</Button>
</div>
</div>
{/* Scrollable filter sections */}
<ScrollArea className="flex flex-1 overflow-y-auto p-2 pb-0" viewportClassName="no-table">
<div className="flex grow flex-col gap-1">
{/* First 2 open by default */}
<StatusFilter filters={filters} onFiltersChange={onFiltersChange} defaultOpen />
<ToolNamesFilter filters={filters} onFiltersChange={onFiltersChange} defaultOpen />
{/* Rest closed unless they have active filters */}
<ServersFilter filters={filters} onFiltersChange={onFiltersChange} />
<VirtualKeysFilter filters={filters} onFiltersChange={onFiltersChange} />
</div>
</ScrollArea>
</div>
);
}
// ---------------------------------------------------------------------------
// Shared helpers & primitives
// ---------------------------------------------------------------------------
interface FilterComponentProps {
filters: MCPToolLogFilters;
onFiltersChange: (filters: MCPToolLogFilters) => void;
defaultOpen?: boolean;
}
// ---------------------------------------------------------------------------
// FilterSection collapsible wrapper
// ---------------------------------------------------------------------------
function FilterSectionSkeleton({ rows = 3 }: { rows?: number }) {
return (
<>
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex items-center gap-2.5 px-3 py-2">
<Skeleton className="size-4 shrink-0 rounded-[4px]" />
<Skeleton className="h-3.5 w-full rounded" />
</div>
))}
</>
);
}
function FilterSection({
title,
children,
defaultOpen = false,
loading = false,
onOpenChange,
}: {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
loading?: boolean;
onOpenChange?: (open: boolean) => void;
}) {
const [open, setOpen] = useState(defaultOpen);
useEffect(() => {
if (defaultOpen) setOpen(true);
}, [defaultOpen]);
const handleOpenChange = (next: boolean) => {
setOpen(next);
onOpenChange?.(next);
};
return (
<Collapsible open={open} onOpenChange={handleOpenChange} className="last:pb-2">
<CollapsibleTrigger className="flex h-8 w-full cursor-pointer items-center gap-1.5 px-2 py-2 text-sm font-medium hover:opacity-80">
<ChevronDown className={cn("size-3.5 transition-transform", open ? "rotate-0" : "-rotate-90")} />
<span>{title}</span>
</CollapsibleTrigger>
<CollapsibleContent className="pt-1">
<div className="divide-border divide-y overflow-hidden rounded-sm border">{loading ? <FilterSectionSkeleton /> : children}</div>
</CollapsibleContent>
</Collapsible>
);
}
// ---------------------------------------------------------------------------
// CheckboxFilterItem
// ---------------------------------------------------------------------------
function CheckboxFilterItem({
label,
checked,
onCheckedChange,
labelClassName,
}: {
label: string;
checked: boolean;
onCheckedChange: (checked: boolean) => void;
labelClassName?: string;
}) {
return (
<label className="hover:bg-muted/50 flex cursor-pointer items-center gap-2.5 px-3 py-2 text-sm">
<Checkbox checked={checked} onCheckedChange={onCheckedChange} />
<span className={cn("truncate", labelClassName)}>{label}</span>
</label>
);
}
// ---------------------------------------------------------------------------
// SearchableCheckboxList list of checkbox rows with a search input.
// Caller passes `inputRef` to control focus (see `useAutoFocusOnOpen`).
// ---------------------------------------------------------------------------
function useAutoFocusOnOpen(isOpen: boolean) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isOpen) ref.current?.focus({ preventScroll: true });
}, [isOpen]);
return ref;
}
function SearchableCheckboxList({
items,
isSelected,
onToggle,
placeholder = "Search...",
inputRef,
}: {
items: { key: string; label: string }[];
isSelected: (key: string) => boolean;
onToggle: (key: string) => void;
placeholder?: string;
inputRef?: Ref<HTMLInputElement>;
}) {
const [query, setQuery] = useState("");
const normalized = query.trim().toLowerCase();
const filtered = normalized ? items.filter((item) => item.label.toLowerCase().includes(normalized)) : items;
return (
<>
<div className="border-b">
<Input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={placeholder}
className="h-8 border-0 text-xs"
/>
</div>
{filtered.map((item) => (
<CheckboxFilterItem key={item.key} label={item.label} checked={isSelected(item.key)} onCheckedChange={() => onToggle(item.key)} />
))}
{filtered.length === 0 && <div className="text-muted-foreground flex h-9 items-center px-3 text-xs">No results</div>}
</>
);
}
// ---------------------------------------------------------------------------
// StatusFilter
// ---------------------------------------------------------------------------
function StatusFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = (filters.status || []).length > 0;
return (
<FilterSection title="Status" defaultOpen={defaultOpen || hasActive}>
{Statuses.map((status) => (
<CheckboxFilterItem
key={status}
labelClassName="capitalize"
label={status}
checked={(filters.status || []).includes(status)}
onCheckedChange={() => {
const current = filters.status || [];
const next = current.includes(status) ? current.filter((s) => s !== status) : [...current, status];
onFiltersChange({ ...filters, status: next });
}}
/>
))}
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// ToolNamesFilter fetches tool names; skips while closed & inactive
// ---------------------------------------------------------------------------
function ToolNamesFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = (filters.tool_names || []).length > 0;
const [opened, setOpened] = useState(defaultOpen || hasActive);
const searchInputRef = useAutoFocusOnOpen(opened);
const { data: filterData, isUninitialized, isLoading } = useGetMCPLogsFilterDataQuery(undefined, { skip: !opened && !hasActive });
const availableToolNames = filterData?.tool_names || [];
if (!isUninitialized && !isLoading && availableToolNames.length === 0 && !hasActive) return null;
return (
<FilterSection title="Tool Names" defaultOpen={defaultOpen || hasActive} loading={isLoading} onOpenChange={setOpened}>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search tools"
items={availableToolNames.map((name) => ({ key: name, label: name }))}
isSelected={(name) => (filters.tool_names || []).includes(name)}
onToggle={(name) => {
const current = filters.tool_names || [];
const next = current.includes(name) ? current.filter((n) => n !== name) : [...current, name];
onFiltersChange({ ...filters, tool_names: next });
}}
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// ServersFilter fetches server labels; skips while closed & inactive
// ---------------------------------------------------------------------------
function ServersFilter({ filters, onFiltersChange, defaultOpen }: FilterComponentProps) {
const hasActive = (filters.server_labels || []).length > 0;
const [opened, setOpened] = useState(defaultOpen || hasActive);
const searchInputRef = useAutoFocusOnOpen(opened);
const { data: filterData, isUninitialized, isLoading } = useGetMCPLogsFilterDataQuery(undefined, { skip: !opened && !hasActive });
const availableServerLabels = filterData?.server_labels || [];
if (!isUninitialized && !isLoading && availableServerLabels.length === 0 && !hasActive) return null;
return (
<FilterSection title="Servers" defaultOpen={defaultOpen || hasActive} loading={isLoading} onOpenChange={setOpened}>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search servers"
items={availableServerLabels.map((label) => ({ key: label, label }))}
isSelected={(label) => (filters.server_labels || []).includes(label)}
onToggle={(label) => {
const current = filters.server_labels || [];
const next = current.includes(label) ? current.filter((l) => l !== label) : [...current, label];
onFiltersChange({ ...filters, server_labels: next });
}}
/>
</FilterSection>
);
}
// ---------------------------------------------------------------------------
// VirtualKeysFilter fetches virtual keys; maps name→ID
// ---------------------------------------------------------------------------
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 } = useGetMCPLogsFilterDataQuery(undefined, { skip: !opened && !hasActive });
const availableVirtualKeys = filterData?.virtual_keys || [];
const nameToId = useMemo(() => new Map(availableVirtualKeys.map((key) => [key.name, key.id])), [availableVirtualKeys]);
if (!isUninitialized && !isLoading && availableVirtualKeys.length === 0 && !hasActive) return null;
const isSelected = (name: string) => {
const id = nameToId.get(name) || name;
return (filters.virtual_key_ids || []).includes(id);
};
const toggle = (name: string) => {
const id = nameToId.get(name) || name;
const current = filters.virtual_key_ids || [];
const next = current.includes(id) ? current.filter((v) => v !== id) : [...current, id];
onFiltersChange({ ...filters, virtual_key_ids: next });
};
return (
<FilterSection title="Virtual Keys" defaultOpen={defaultOpen || hasActive} loading={isLoading} onOpenChange={setOpened}>
<SearchableCheckboxList
inputRef={searchInputRef}
placeholder="Search virtual keys"
items={availableVirtualKeys.map((key) => ({ key: key.name, label: key.name }))}
isSelected={isSelected}
onToggle={toggle}
/>
</FilterSection>
);
}