-
diff --git a/ui/app/workspace/dashboard/page.tsx b/ui/app/workspace/dashboard/page.tsx
index 0a4584cc0d..c7ac442f23 100644
--- a/ui/app/workspace/dashboard/page.tsx
+++ b/ui/app/workspace/dashboard/page.tsx
@@ -1,6 +1,8 @@
import { LogsFilterSidebar } from "@/components/filters/logsFilterSidebar";
+import { Button } from "@/components/ui/button";
import { DateTimePickerWithRange } from "@/components/ui/datePickerWithRange";
import { ScrollArea } from "@/components/ui/scrollArea";
+import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
useGetMCPAvailableFilterDataQuery,
@@ -35,6 +37,7 @@ import type {
} from "@/lib/types/logs";
import { dateUtils } from "@/lib/types/logs";
import UserRankingsTab from "@enterprise/components/user-rankings/userRankingsTab";
+import { Filter } from "lucide-react";
import { parseAsInteger, parseAsString, useQueryStates } from "nuqs";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { type ChartType } from "./components/charts/chartTypeToggle";
@@ -708,18 +711,39 @@ export default function DashboardPage() {
}, []);
return (
-
- {/* Sidebar Filters */}
-
+
+ {/* Sidebar Filters — hidden on mobile, use date picker + tab-level filters for filtering */}
+
+
+
{/* Main Content */}
-
+
{/* Header */}
-
+
Dashboard
-
+
+ {/* Mobile-only filter trigger — opens the sidebar in a Sheet (desktop shows the persistent sidebar) */}
+
+
+
+
+
+
+
+ Filters
+ Filter dashboard data by providers, models, status, and metadata.
+
+
+
{urlState.tab === "mcp" && mcpFilterData && (
-
+
{mcpFilterData.tool_names?.length > 0 && (
-
+
{/* Tabs */}
-
-
- Overview
-
-
- Provider Usage
-
-
- Model Rankings
-
-
- MCP usage
-
-
- User Rankings
-
-
+
+
+
+ Overview
+
+
+ Provider Usage
+
+
+ Model Rankings
+
+
+ MCP usage
+
+
+ User Rankings
+
+
+
{/* Overview Tab */}
diff --git a/ui/app/workspace/logs/page.tsx b/ui/app/workspace/logs/page.tsx
index af26b87b39..2262093bc6 100644
--- a/ui/app/workspace/logs/page.tsx
+++ b/ui/app/workspace/logs/page.tsx
@@ -10,7 +10,6 @@ import FullPageLoader from "@/components/fullPageLoader";
import { useColumnConfig } from "@/components/table";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Card, CardContent } from "@/components/ui/card";
-import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useWebSocket } from "@/hooks/useWebSocket";
import {
@@ -835,7 +834,13 @@ export default function LogsPage() {
},
{
title: "User Success Rate",
- value: fetchingStats ? : stats ? `${(stats.user_facing_success_rate ?? 0).toFixed(2)}%` : "-",
+ value: (
+
+ ),
icon: ,
description: "Success rate as perceived by the end user. It includes fallback chains as one request.",
},
@@ -868,10 +873,7 @@ export default function LogsPage() {
return Object.keys(filterData.metadata_keys).sort();
}, [filterData?.metadata_keys]);
- const columns = useMemo(
- () => createColumns(handleDelete, hasDeleteAccess, metadataKeys),
- [handleDelete, hasDeleteAccess, metadataKeys],
- );
+ const columns = useMemo(() => createColumns(handleDelete, hasDeleteAccess, metadataKeys), [handleDelete, hasDeleteAccess, metadataKeys]);
const columnIds = useMemo(
() => columns.map((col) => ("id" in col && col.id ? col.id : "accessorKey" in col ? String(col.accessorKey) : "")).filter(Boolean),
@@ -959,18 +961,20 @@ export default function LogsPage() {
);
return (
-
+
{initialLoading ? (
) : showEmptyState ? (
) : (
-
- {/* Sidebar Filters */}
-
+
+ {/* Sidebar Filters — hidden on small screens to free up width for the table */}
+
+
+
{/* Main Content */}
-
+
-
+
{statCards.map((card) => (
-
+
- {card.title}
+ {card.title}
{"description" in card && card.description && (
@@ -1010,7 +1014,7 @@ export default function LogsPage() {
)}
-
{card.value}
+
{card.value}
diff --git a/ui/app/workspace/logs/views/logsHeaderView.tsx b/ui/app/workspace/logs/views/logsHeaderView.tsx
index 848c49e342..8e5458e645 100644
--- a/ui/app/workspace/logs/views/logsHeaderView.tsx
+++ b/ui/app/workspace/logs/views/logsHeaderView.tsx
@@ -1,12 +1,14 @@
+import { LogsFilterSidebar } from "@/components/filters/logsFilterSidebar";
import { ColumnConfigDropdown, type ColumnConfigEntry } from "@/components/table";
import { Button } from "@/components/ui/button";
import { Command, CommandItem, CommandList } from "@/components/ui/command";
import { DateTimePickerWithRange } from "@/components/ui/datePickerWithRange";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { getErrorMessage, useRecalculateLogCostsMutation } from "@/lib/store";
import type { LogFilters as LogFiltersType } from "@/lib/types/logs";
-import { Calculator, MoreVertical, Pause, Play, Search } from "lucide-react";
+import { Calculator, Filter, MoreVertical, Pause, Play, Search } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
@@ -134,22 +136,64 @@ export function LogsHeaderView({
);
return (
-
-
onLiveToggle(!liveEnabled)}>
- {liveEnabled ? (
- <>
-
- Live updates
- >
- ) : (
- <>
-
- Live updates
- >
- )}
-
-
-
+
+ {/* Mobile row 1: icon buttons (filter, live) on the left + (more, columns) right-aligned.
+ On sm+ this wrapper becomes display:contents so its children flow inline with the rest. */}
+
+ {/* Filter sheet trigger — mobile only */}
+
+
+
+
+
+
+
+ Filters
+ Filter logs by status, providers, models, and metadata.
+
+
+
+
+
onLiveToggle(!liveEnabled)}>
+ {liveEnabled ? : }
+ Live updates
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Recalculate costs
+ For all logs that don't have a cost
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Search — mobile row 2 (full width); on desktop, slides between Live and Date as flex-1 */}
+
+
+ {/* Date picker — mobile row 3 (full width); auto width on desktop */}
{
setStartTime(p.from);
@@ -184,27 +231,6 @@ export function LogsHeaderView({
});
}}
/>
-
-
-
-
-
-
-
-
-
-
-
-
- Recalculate costs
- For all logs that don't have a cost
-
-
-
-
-
-
-
);
-}
+}
\ No newline at end of file
diff --git a/ui/app/workspace/mcp-logs/page.tsx b/ui/app/workspace/mcp-logs/page.tsx
index 3dcf993ded..e9041608ff 100644
--- a/ui/app/workspace/mcp-logs/page.tsx
+++ b/ui/app/workspace/mcp-logs/page.tsx
@@ -549,13 +549,15 @@ export default function MCPLogsPage() {
}
/>
) : (
-
- {/* Sidebar Filters */}
-
+
+ {/* Sidebar Filters — hidden on small screens; mobile access is via the Sheet trigger in the header */}
+
+
+
{/* Main Content */}
-
-
+
+
{/* Quick Stats */}
-
-
+
+
{statCards.map((card) => (
-
+
{card.title}
-
{card.value}
+
{card.value}
diff --git a/ui/app/workspace/mcp-logs/views/mcpHeaderView.tsx b/ui/app/workspace/mcp-logs/views/mcpHeaderView.tsx
index 4888e5afe3..67c53a1323 100644
--- a/ui/app/workspace/mcp-logs/views/mcpHeaderView.tsx
+++ b/ui/app/workspace/mcp-logs/views/mcpHeaderView.tsx
@@ -1,9 +1,11 @@
+import { MCPFilterSidebar } from "@/components/filters/mcpFilterSidebar";
import { ColumnConfigDropdown, type ColumnConfigEntry } from "@/components/table";
import { Button } from "@/components/ui/button";
import { DateTimePickerWithRange } from "@/components/ui/datePickerWithRange";
import { Input } from "@/components/ui/input";
+import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import type { MCPToolLogFilters } from "@/lib/types/logs";
-import { Pause, Play, Search } from "lucide-react";
+import { Filter, Pause, Play, Search } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
const LOG_TIME_PERIODS = [
@@ -107,22 +109,44 @@ export function McpHeaderView({
);
return (
-
-
onLiveToggle(!liveEnabled)}>
- {liveEnabled ? (
- <>
-
- Live updates
- >
- ) : (
- <>
-
- Live updates
- >
- )}
-
-
-
+
+ {/* Mobile row 1: filter + live on the left, column-config right-aligned.
+ On sm+ this wrapper becomes display:contents so its children flow inline with the rest. */}
+
+ {/* Filter sheet trigger — mobile only */}
+
+
+
+
+
+
+
+ Filters
+ Filter MCP logs by tool, server, status, and virtual key.
+
+
+
+
+
onLiveToggle(!liveEnabled)}>
+ {liveEnabled ? : }
+ Live updates
+
+
+
+
+
+ {/* Search — mobile row 2 (full width); on desktop, slides between Live and Date as flex-1 */}
+
+
handleSearchChange(e.target.value)}
/>
+
+ {/* Date picker — mobile row 3 (full width); auto width on desktop */}
{
setStartTime(p.from);
@@ -155,7 +183,6 @@ export function McpHeaderView({
});
}}
/>
-
);
}
diff --git a/ui/app/workspace/observability/fragments/maximFormFragment.tsx b/ui/app/workspace/observability/fragments/maximFormFragment.tsx
index 93f00d70fe..987ed211ce 100644
--- a/ui/app/workspace/observability/fragments/maximFormFragment.tsx
+++ b/ui/app/workspace/observability/fragments/maximFormFragment.tsx
@@ -110,7 +110,7 @@ export function MaximFormFragment({ initialConfig, onSave, onDelete, isDeleting
{/* Form Actions */}
-
+
)}
/>
-
+
{onDelete && (
@@ -144,6 +145,7 @@ export function MaximFormFragment({ initialConfig, onSave, onDelete, isDeleting
{
form.reset({
enabled: initialConfig?.enabled ?? true,
@@ -162,6 +164,7 @@ export function MaximFormFragment({ initialConfig, onSave, onDelete, isDeleting
diff --git a/ui/app/workspace/observability/fragments/otelFormFragment.tsx b/ui/app/workspace/observability/fragments/otelFormFragment.tsx
index f27438fb54..96eefcaec6 100644
--- a/ui/app/workspace/observability/fragments/otelFormFragment.tsx
+++ b/ui/app/workspace/observability/fragments/otelFormFragment.tsx
@@ -175,7 +175,7 @@ export function OtelFormFragment({
)}
/>
-
+
{/* Form Actions */}
-
+
)}
/>
-
+
{onDelete && (
@@ -405,6 +406,7 @@ export function OtelFormFragment({
{
form.reset({
enabled: initialConfig?.enabled ?? true,
@@ -431,6 +433,7 @@ export function OtelFormFragment({
diff --git a/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx b/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx
index 04ec5ca5b4..7086ad3072 100644
--- a/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx
+++ b/ui/app/workspace/observability/fragments/prometheusFormFragment.tsx
@@ -160,7 +160,7 @@ export function PrometheusFormFragment({
)}
/>
-
+
-
+
{/* Form Actions */}
-
+
)}
/>
-
+
{onDelete && (
@@ -343,6 +344,7 @@ export function PrometheusFormFragment({
{
form.reset({
enabled: initialConfig?.enabled ?? true,
@@ -366,6 +368,7 @@ export function PrometheusFormFragment({
diff --git a/ui/app/workspace/observability/page.tsx b/ui/app/workspace/observability/page.tsx
index fdace2035c..00ab72f0a6 100644
--- a/ui/app/workspace/observability/page.tsx
+++ b/ui/app/workspace/observability/page.tsx
@@ -2,7 +2,7 @@ import ObservabilityView from "./views/observabilityView";
export default function ObservabilityPage() {
return (
-
+
);
diff --git a/ui/app/workspace/observability/views/observabilityView.tsx b/ui/app/workspace/observability/views/observabilityView.tsx
index 2919f9b0fd..a03e0cb556 100644
--- a/ui/app/workspace/observability/views/observabilityView.tsx
+++ b/ui/app/workspace/observability/views/observabilityView.tsx
@@ -121,8 +121,43 @@ export default function ObservabilityView() {
}
return (
-
-
+
+ {/* Mobile: horizontal scroll of provider pills */}
+
+
Providers
+
+ {supportedPlatforms.map((tab) => (
+
{
+ if (tab.disabled) return;
+ setSelectedPluginId(tab.id ?? supportedPlatforms[0].id);
+ }}
+ >
+ {tab.icon}
+ {tab.name}
+ {tab.disabled && (
+
+ SOON
+
+ )}
+
+ ))}
+
+
+
+ {/* Desktop: left sidebar of providers */}
+
@@ -168,7 +203,8 @@ export default function ObservabilityView() {
-
+
+
{selectedPluginId === "prometheus" &&
}
{selectedPluginId === "otel" &&
}
{selectedPluginId === "maxim" &&
}
diff --git a/ui/components/filters/logsFilterSidebar.tsx b/ui/components/filters/logsFilterSidebar.tsx
index 84784a59d9..a620e1138a 100644
--- a/ui/components/filters/logsFilterSidebar.tsx
+++ b/ui/components/filters/logsFilterSidebar.tsx
@@ -20,17 +20,21 @@ const COLLAPSE_STORAGE_KEY = "logs-filter-sidebar-collapsed";
interface LogsSidebarProps {
filters: LogFilters;
onFiltersChange: (filters: LogFilters) => void;
+ /** When true, hide the collapse rail and always render expanded. Used inside the mobile Sheet. */
+ disableCollapse?: boolean;
+ className?: string;
}
-export function LogsFilterSidebar({ filters, onFiltersChange }: LogsSidebarProps) {
+export function LogsFilterSidebar({ filters, onFiltersChange, disableCollapse = false, className }: LogsSidebarProps) {
const [collapsed, setCollapsed] = useState(false);
// Load persisted collapsed state on mount
useEffect(() => {
+ if (disableCollapse) return;
if (typeof window === "undefined") return;
const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY);
if (stored === "true") setCollapsed(true);
- }, []);
+ }, [disableCollapse]);
const toggleCollapsed = useCallback(() => {
setCollapsed((prev) => {
@@ -63,7 +67,7 @@ export function LogsFilterSidebar({ filters, onFiltersChange }: LogsSidebarProps
}, [filters.start_time, filters.end_time, onFiltersChange]);
// Collapsed: thin rail with vertical "Filters" label — whole rail is clickable to expand
- if (collapsed) {
+ if (collapsed && !disableCollapse) {
return (
+
{/* Header */}
Filters
@@ -95,9 +99,11 @@ export function LogsFilterSidebar({ filters, onFiltersChange }: LogsSidebarProps
Reset
)}
-
-
-
+ {!disableCollapse && (
+
+
+
+ )}
diff --git a/ui/components/filters/mcpFilterSidebar.tsx b/ui/components/filters/mcpFilterSidebar.tsx
index 3da3178578..583a383938 100644
--- a/ui/components/filters/mcpFilterSidebar.tsx
+++ b/ui/components/filters/mcpFilterSidebar.tsx
@@ -20,17 +20,21 @@ const COLLAPSE_STORAGE_KEY = "mcp-filter-sidebar-collapsed";
interface MCPFilterSidebarProps {
filters: MCPToolLogFilters;
onFiltersChange: (filters: MCPToolLogFilters) => void;
+ /** When true, hide the collapse rail and always render expanded. Used inside the mobile Sheet. */
+ disableCollapse?: boolean;
+ className?: string;
}
-export function MCPFilterSidebar({ filters, onFiltersChange }: MCPFilterSidebarProps) {
+export function MCPFilterSidebar({ filters, onFiltersChange, disableCollapse = false, className }: MCPFilterSidebarProps) {
const [collapsed, setCollapsed] = useState(false);
// Load persisted collapsed state on mount
useEffect(() => {
+ if (disableCollapse) return;
if (typeof window === "undefined") return;
const stored = window.localStorage.getItem(COLLAPSE_STORAGE_KEY);
if (stored === "true") setCollapsed(true);
- }, []);
+ }, [disableCollapse]);
const toggleCollapsed = useCallback(() => {
setCollapsed((prev) => {
@@ -60,7 +64,7 @@ export function MCPFilterSidebar({ filters, onFiltersChange }: MCPFilterSidebarP
}, [filters.start_time, filters.end_time, onFiltersChange]);
// Collapsed: thin rail with vertical "Filters" label — whole rail is clickable to expand
- if (collapsed) {
+ if (collapsed && !disableCollapse) {
return (
+
{/* Header */}
Filters
@@ -92,9 +96,11 @@ export function MCPFilterSidebar({ filters, onFiltersChange }: MCPFilterSidebarP
Reset
)}
-
-
-
+ {!disableCollapse && (
+
+
+
+ )}
diff --git a/ui/components/header.tsx b/ui/components/header.tsx
index 4dc2790d60..eeb666604f 100644
--- a/ui/components/header.tsx
+++ b/ui/components/header.tsx
@@ -1,14 +1,34 @@
-import { ThemeToggle } from "./themeToggle";
-import { Separator } from "./ui/separator";
+import { useSidebar } from "@/components/ui/sidebar";
+import { Link } from "@tanstack/react-router";
+import { Menu, X } from "lucide-react";
+import { useTheme } from "next-themes";
+import { useEffect, useState } from "react";
+
+export default function Header() {
+ const { toggleSidebar, openMobile } = useSidebar();
+ const { resolvedTheme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const logoSrc = mounted && resolvedTheme === "dark" ? "/bifrost-logo-dark.webp" : "/bifrost-logo.webp";
-export default function Header({ title }: { title: string }) {
return (
-
+
+
+
+
+
+ {openMobile ? : }
+
+
);
-}
\ No newline at end of file
+}
diff --git a/ui/components/sidebar.tsx b/ui/components/sidebar.tsx
index b9c4cef00b..1dc5afd73e 100644
--- a/ui/components/sidebar.tsx
+++ b/ui/components/sidebar.tsx
@@ -1087,7 +1087,7 @@ export default function AppSidebar() {
return (
-
+
{/* Expanded state: horizontal layout */}
@@ -1109,7 +1109,7 @@ export default function AppSidebar() {
-
+
(dateTime);
const [timeValue, setTimeValue] = React.useState
({
from: dateTime?.from ? { hour: dateTime.from.getHours(), minute: dateTime.from.getMinutes() } : { hour: 0, minute: 0 },
@@ -144,9 +146,35 @@ export function DateTimePickerWithRange(props: DateTimePickerWithRangeProps) {
)}
-
-
-
+
+
+ {props.preDefinedPeriods && isMobile && (
+
+ {props.preDefinedPeriods.map((period) => (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ setPredefinedPeriod(period.value);
+ props.onPredefinedPeriodChange && props.onPredefinedPeriodChange(period.value);
+ }}
+ >
+ {period.label}
+
+ ))}
+
+ )}
+
-
+
- {props.preDefinedPeriods && (
+ {props.preDefinedPeriods && !isMobile && (
{props.preDefinedPeriods.map((period) => (
{children}