diff --git a/ui/app/workspace/dashboard/page.tsx b/ui/app/workspace/dashboard/page.tsx index dbac77de44..0a4584cc0d 100644 --- a/ui/app/workspace/dashboard/page.tsx +++ b/ui/app/workspace/dashboard/page.tsx @@ -1,4 +1,4 @@ -import { LogsSidebar } from "@/app/workspace/logs/views/logsSidebar"; +import { LogsFilterSidebar } from "@/components/filters/logsFilterSidebar"; import { DateTimePickerWithRange } from "@/components/ui/datePickerWithRange"; import { ScrollArea } from "@/components/ui/scrollArea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -48,6 +48,33 @@ import { ProviderUsageTab } from "./components/providerUsageTab"; // Type-safe parser for chart type URL state const toChartType = (value: string): ChartType => (value === "line" ? "line" : "bar"); +// Predefined time periods +const TIME_PERIODS = [ + { label: "Last hour", value: "1h" }, + { label: "Last 6 hours", value: "6h" }, + { label: "Last 24 hours", value: "24h" }, + { label: "Last 7 days", value: "7d" }, + { label: "Last 30 days", value: "30d" }, +]; + +function getTimeRangeFromPeriod(period: string): { start: number; end: number } { + const now = Math.floor(Date.now() / 1000); + switch (period) { + case "1h": + return { start: now - 3600, end: now }; + case "6h": + return { start: now - 6 * 3600, end: now }; + case "24h": + return { start: now - 24 * 3600, end: now }; + case "7d": + return { start: now - 7 * 24 * 3600, end: now }; + case "30d": + return { start: now - 30 * 24 * 3600, end: now }; + default: + return { start: now - 24 * 3600, end: now }; + } +} + // Calculate default timestamps once at module level const DEFAULT_END_TIME = Math.floor(Date.now() / 1000); const DEFAULT_START_TIME = (() => { @@ -138,6 +165,7 @@ export default function DashboardPage() { routing_rule_ids: parseAsString.withDefault(""), routing_engine_used: parseAsString.withDefault(""), missing_cost_only: parseAsString.withDefault("false"), + metadata_filters: parseAsString.withDefault(""), volume_chart: parseAsString.withDefault("bar"), token_chart: parseAsString.withDefault("bar"), cost_chart: parseAsString.withDefault("bar"), @@ -172,6 +200,14 @@ export default function DashboardPage() { const selectedRoutingRuleIds = useMemo(() => parseCsvParam(urlState.routing_rule_ids), [urlState.routing_rule_ids]); const selectedRoutingEngines = useMemo(() => parseCsvParam(urlState.routing_engine_used), [urlState.routing_engine_used]); const missingCostOnly = useMemo(() => urlState.missing_cost_only === "true", [urlState.missing_cost_only]); + const metadataFilters = useMemo(() => { + if (!urlState.metadata_filters) return undefined; + try { + return JSON.parse(urlState.metadata_filters) as Record; + } catch { + return undefined; + } + }, [urlState.metadata_filters]); // MCP filter arrays const selectedMcpToolNames = useMemo(() => parseCsvParam(urlState.mcp_tool_names), [urlState.mcp_tool_names]); @@ -191,6 +227,7 @@ export default function DashboardPage() { ...(selectedRoutingRuleIds.length > 0 && { routing_rule_ids: selectedRoutingRuleIds }), ...(selectedRoutingEngines.length > 0 && { routing_engine_used: selectedRoutingEngines }), ...(missingCostOnly && { missing_cost_only: true }), + ...(metadataFilters && Object.keys(metadataFilters).length > 0 && { metadata_filters: metadataFilters }), }), [ urlState.start_time, @@ -204,6 +241,7 @@ export default function DashboardPage() { selectedRoutingRuleIds, selectedRoutingEngines, missingCostOnly, + metadataFilters, ], ); @@ -501,6 +539,36 @@ export default function DashboardPage() { [setUrlState], ); + // Date range for picker + const dateRange = useMemo( + () => ({ + from: dateUtils.fromUnixTimestamp(urlState.start_time), + to: dateUtils.fromUnixTimestamp(urlState.end_time), + }), + [urlState.start_time, urlState.end_time], + ); + + const handlePeriodChange = useCallback( + (period: string | undefined) => { + if (!period) return; + const { start, end } = getTimeRangeFromPeriod(period); + setUrlState({ start_time: start, end_time: end, period }); + }, + [setUrlState], + ); + + const handleDateRangeChange = useCallback( + (range: { from?: Date; to?: Date }) => { + if (!range.from || !range.to) return; + setUrlState({ + start_time: dateUtils.toUnixTimestamp(range.from), + end_time: dateUtils.toUnixTimestamp(range.to), + period: "", + }); + }, + [setUrlState], + ); + const handleProviderCostChartToggle = useCallback((type: ChartType) => setUrlState({ provider_cost_chart: type }), [setUrlState]); const handleProviderTokenChartToggle = useCallback((type: ChartType) => setUrlState({ provider_token_chart: type }), [setUrlState]); const handleProviderLatencyChartToggle = useCallback((type: ChartType) => setUrlState({ provider_latency_chart: type }), [setUrlState]); @@ -642,7 +710,7 @@ export default function DashboardPage() { return (
{/* Sidebar Filters */} - + {/* Main Content */} @@ -692,6 +760,15 @@ export default function DashboardPage() { )}
)} + diff --git a/ui/app/workspace/logs/page.tsx b/ui/app/workspace/logs/page.tsx index e65cef71ea..eb20ac6c43 100644 --- a/ui/app/workspace/logs/page.tsx +++ b/ui/app/workspace/logs/page.tsx @@ -2,7 +2,7 @@ import { LogDetailSheet } from "@/app/workspace/logs/sheets/logDetailsSheet"; import { SessionDetailsSheet } from "@/app/workspace/logs/sheets/sessionDetailsSheet"; import { createColumns } from "@/app/workspace/logs/views/columns"; import { EmptyState } from "@/app/workspace/logs/views/emptyState"; -import { LogsSidebar } from "@/app/workspace/logs/views/logsSidebar"; +import { LogsFilterSidebar } from "@/components/filters/logsFilterSidebar"; import { LogsDataTable } from "@/app/workspace/logs/views/logsTable"; import { LogsVolumeChart } from "@/app/workspace/logs/views/logsVolumeChart"; import FullPageLoader from "@/components/fullPageLoader"; @@ -920,7 +920,7 @@ export default function LogsPage() { ) : (
{/* Sidebar Filters */} - + {/* Main Content */}
@@ -996,7 +996,6 @@ export default function LogsPage() { isSocketConnected={isSocketConnected} fetchLogs={fetchLogs} fetchStats={fetchStats} - sidebarFilters />
diff --git a/ui/app/workspace/logs/views/filters.tsx b/ui/app/workspace/logs/views/filters.tsx index 8ec7ad7318..036101e532 100644 --- a/ui/app/workspace/logs/views/filters.tsx +++ b/ui/app/workspace/logs/views/filters.tsx @@ -1,4 +1,3 @@ -import { FilterPopover } from "@/components/filters/filterPopover"; import { Button } from "@/components/ui/button"; import { Command, CommandItem, CommandList } from "@/components/ui/command"; import { DateTimePickerWithRange } from "@/components/ui/datePickerWithRange"; @@ -12,7 +11,6 @@ import { toast } from "sonner"; export { dateToRfc3339Local } from "@/lib/utils/date"; -/** Predefined time periods for the logs date range picker (matches E2E test labels) */ const LOG_TIME_PERIODS = [ { label: "Last hour", value: "1h" }, { label: "Last 6 hours", value: "6h" }, @@ -53,25 +51,23 @@ 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, - hidePopoverFilters, -}: LogFiltersProps) { +export function LogFilters({ filters, onFiltersChange, liveEnabled, onLiveToggle, fetchLogs, fetchStats }: LogFiltersProps) { const [openMoreActionsPopover, setOpenMoreActionsPopover] = useState(false); const [localSearch, setLocalSearch] = useState(filters.content_search || ""); const searchTimeoutRef = useRef(undefined); const filtersRef = useRef(filters); const [recalculateCosts] = useRecalculateLogCostsMutation(); + const [startTime, setStartTime] = useState(filters.start_time ? new Date(filters.start_time) : undefined); + const [endTime, setEndTime] = useState(filters.end_time ? new Date(filters.end_time) : undefined); + + useEffect(() => { + setStartTime(filters.start_time ? new Date(filters.start_time) : undefined); + setEndTime(filters.end_time ? new Date(filters.end_time) : undefined); + }, [filters.start_time, filters.end_time]); + // Keep filtersRef in sync so debounced search always merges with latest filters (search within filtered results) useEffect(() => { filtersRef.current = filters; @@ -82,16 +78,6 @@ export function LogFilters({ setLocalSearch(filters.content_search || ""); }, [filters.content_search]); - // Convert ISO strings from filters to Date objects for the DateTimePicker - const [startTime, setStartTime] = useState(filters.start_time ? new Date(filters.start_time) : undefined); - const [endTime, setEndTime] = useState(filters.end_time ? new Date(filters.end_time) : undefined); - - // Sync local date state when filters change from URL - useEffect(() => { - setStartTime(filters.start_time ? new Date(filters.start_time) : undefined); - setEndTime(filters.end_time ? new Date(filters.end_time) : undefined); - }, [filters.start_time, filters.end_time]); - // Cleanup timeout on unmount useEffect(() => { return () => { @@ -133,29 +119,6 @@ export function LogFilters({ [onFiltersChange], ); - const handleFilterChange = useCallback( - (key: keyof LogFiltersType, values: string[] | boolean | string) => { - onFiltersChange({ ...filters, [key]: values }); - }, - [filters, onFiltersChange], - ); - - const handleMetadataFilterChange = 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], - ); - return (
- {!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(), - }); - }} - /> - - - )} + { + 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(), + }); + }} + /> - - - - - - No filters found. - {showParentRequestIdFilter && ( - -
- onFilterChange("parent_request_id", e.target.value)} - onKeyDown={(e) => e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - placeholder="Parent request ID" - className="h-8" - data-testid="session-parent-request-id-input" - /> -
-
- )} - {showMissingCost && ( - - - onFilterChange("missing_cost_only", checked)} - /> - Show missing cost - - - )} - {Object.entries(FILTER_OPTIONS) - .filter(([category, values]) => values.length > 0 || isCategoryLoading(category)) - .map(([category, values]) => ( - - {isCategoryLoading(category) && values.length === 0 ? ( - -
-
-
- Loading... - - ) : ( - values.map((value: string) => { - const selected = isSelected(category, value); - return ( - handleFilterSelect(category, value)} - > -
- -
- - {category === "Type" - ? RequestTypeLabels[value as keyof typeof RequestTypeLabels] - : category === "Routing Engines" - ? (RoutingEngineUsedLabels[value as keyof typeof RoutingEngineUsedLabels] ?? value) - : value} - -
- ); - }) - )} - {category.startsWith("Metadata: ") && - (() => { - const metadataKey = category.replace("Metadata: ", ""); - const activeValue = filters.metadata_filters?.[metadataKey]; - const isCustom = activeValue && !values.includes(activeValue); - const displayValue = customMetadataInputs[category] ?? (isCustom ? activeValue : ""); - return ( -
- { - const newVal = e.target.value; - setCustomMetadataInputs((prev) => ({ ...prev, [category]: newVal })); - if (newVal === "" && isCustom) { - onMetadataFilterChange?.(metadataKey, undefined); - } - }} - onKeyDown={(e) => { - if (e.key === "Enter" && customMetadataInputs[category]?.trim()) { - handleFilterSelect(category, customMetadataInputs[category].trim()); - } - e.stopPropagation(); - }} - onClick={(e) => e.stopPropagation()} - /> -
- ); - })()} - - ))} - - - - - ); -} \ No newline at end of file diff --git a/ui/app/workspace/logs/views/logsSidebar.tsx b/ui/components/filters/logsFilterSidebar.tsx similarity index 99% rename from ui/app/workspace/logs/views/logsSidebar.tsx rename to ui/components/filters/logsFilterSidebar.tsx index 8faaf1f9d8..3464a9fdac 100644 --- a/ui/app/workspace/logs/views/logsSidebar.tsx +++ b/ui/components/filters/logsFilterSidebar.tsx @@ -20,7 +20,7 @@ interface LogsSidebarProps { onFiltersChange: (filters: LogFilters) => void; } -export function LogsSidebar({ filters, onFiltersChange }: LogsSidebarProps) { +export function LogsFilterSidebar({ 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]) => {