Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions ui/app/workspace/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 = (() => {
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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<string, string>;
} catch {
return undefined;
}
}, [urlState.metadata_filters]);

// MCP filter arrays
const selectedMcpToolNames = useMemo(() => parseCsvParam(urlState.mcp_tool_names), [urlState.mcp_tool_names]);
Expand All @@ -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,
Expand All @@ -204,6 +241,7 @@ export default function DashboardPage() {
selectedRoutingRuleIds,
selectedRoutingEngines,
missingCostOnly,
metadataFilters,
],
);

Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -642,7 +710,7 @@ export default function DashboardPage() {
return (
<div id="dashboard-root" className="no-padding-parent no-border-parent bg-background flex h-[calc(100vh_-_16px)] w-full gap-3">
{/* Sidebar Filters */}
<LogsSidebar filters={filters} onFiltersChange={setFilters} />
<LogsFilterSidebar filters={filters} onFiltersChange={setFilters} />
Comment thread
greptile-apps[bot] marked this conversation as resolved.

{/* Main Content */}
<ScrollArea className="bg-card flex min-w-0 flex-1 flex-col gap-4 rounded-l-md">
Expand Down Expand Up @@ -692,6 +760,15 @@ export default function DashboardPage() {
)}
</div>
)}
<DateTimePickerWithRange
dateTime={dateRange}
onDateTimeUpdate={handleDateRangeChange}
preDefinedPeriods={TIME_PERIODS}
predefinedPeriod={urlState.period || undefined}
onPredefinedPeriodChange={handlePeriodChange}
triggerTestId="dashboard-filter-daterange"
popupAlignment="end"
/>
</div>
</div>

Expand Down
5 changes: 2 additions & 3 deletions ui/app/workspace/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -920,7 +920,7 @@ export default function LogsPage() {
) : (
<div className="bg-background flex h-full w-full grow gap-3">
{/* Sidebar Filters */}
<LogsSidebar filters={filters} onFiltersChange={setFilters} />
<LogsFilterSidebar filters={filters} onFiltersChange={setFilters} />

{/* Main Content */}
<div className="bg-card flex min-w-0 flex-1 flex-col gap-2 overflow-hidden rounded-l-md p-4 pb-2">
Expand Down Expand Up @@ -996,7 +996,6 @@ export default function LogsPage() {
isSocketConnected={isSocketConnected}
fetchLogs={fetchLogs}
fetchStats={fetchStats}
sidebarFilters
/>
</div>
</div>
Expand Down
120 changes: 35 additions & 85 deletions ui/app/workspace/logs/views/filters.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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" },
Expand Down Expand Up @@ -53,25 +51,23 @@ interface LogFiltersProps {
onLiveToggle: (enabled: boolean) => void;
fetchLogs: () => Promise<void>;
fetchStats: () => Promise<void>;
/** 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<NodeJS.Timeout | undefined>(undefined);
const filtersRef = useRef<LogFiltersType>(filters);
const [recalculateCosts] = useRecalculateLogCostsMutation();

const [startTime, setStartTime] = useState<Date | undefined>(filters.start_time ? new Date(filters.start_time) : undefined);
const [endTime, setEndTime] = useState<Date | undefined>(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;
Expand All @@ -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<Date | undefined>(filters.start_time ? new Date(filters.start_time) : undefined);
const [endTime, setEndTime] = useState<Date | undefined>(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 () => {
Expand Down Expand Up @@ -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 (
<div className="flex grow items-center justify-between space-x-2">
<Button variant={"outline"} size="sm" className="h-7.5" onClick={() => onLiveToggle(!liveEnabled)}>
Expand All @@ -182,44 +145,31 @@ export function LogFilters({
/>
</div>

{!hidePopoverFilters && (
<>
<DateTimePickerWithRange
triggerTestId="filter-date-range"
dateTime={{
from: startTime,
to: endTime,
}}
onDateTimeUpdate={(p) => {
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(),
});
}}
/>
<FilterPopover
filters={filters}
onFilterChange={handleFilterChange}
onMetadataFilterChange={handleMetadataFilterChange}
showMissingCost
/>
</>
)}
<DateTimePickerWithRange
triggerTestId="filter-date-range"
dateTime={{ from: startTime, to: endTime }}
onDateTimeUpdate={(p) => {
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(),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}}
/>
Comment thread
impoiler marked this conversation as resolved.
<Popover open={openMoreActionsPopover} onOpenChange={setOpenMoreActionsPopover}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="h-7.5 w-7.5">
Expand All @@ -242,4 +192,4 @@ export function LogFilters({
</Popover>
</div>
);
}
}
37 changes: 10 additions & 27 deletions ui/app/workspace/logs/views/logsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ interface DataTableProps {
onLiveToggle: (enabled: boolean) => void;
fetchLogs: () => Promise<void>;
fetchStats: () => Promise<void>;
/** When true, filters are rendered in a sidebar — hide them from the table header */
sidebarFilters?: boolean;
}

export function LogsDataTable({
Expand All @@ -63,7 +61,6 @@ export function LogsDataTable({
onLiveToggle,
fetchLogs,
fetchStats,
sidebarFilters = false,
}: DataTableProps) {
const [sorting, setSorting] = useState<SortingState>([{ id: pagination.sort_by, desc: pagination.order === "desc" }]);
const tableContainerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -168,30 +165,16 @@ export function LogsDataTable({
return (
<div className="flex h-full flex-col gap-2">
<div className="flex shrink-0 items-center gap-2">
{sidebarFilters ? (
<div className="flex flex-1 items-center gap-2">
<LogFiltersComponent
filters={filters}
onFiltersChange={onFiltersChange}
liveEnabled={liveEnabled}
onLiveToggle={onLiveToggle}
fetchLogs={fetchLogs}
fetchStats={fetchStats}
hidePopoverFilters
/>
</div>
) : (
<div className="flex-1">
<LogFiltersComponent
filters={filters}
onFiltersChange={onFiltersChange}
liveEnabled={liveEnabled}
onLiveToggle={onLiveToggle}
fetchLogs={fetchLogs}
fetchStats={fetchStats}
/>
</div>
)}
<div className="flex flex-1 items-center gap-2">
<LogFiltersComponent
filters={filters}
onFiltersChange={onFiltersChange}
liveEnabled={liveEnabled}
onLiveToggle={onLiveToggle}
fetchLogs={fetchLogs}
fetchStats={fetchStats}
/>
</div>
<ColumnConfigDropdown entries={entries} labels={columnLabels} onToggleVisibility={toggleVisibility} onReset={reset} />
</div>

Expand Down
Loading
Loading