Skip to content
Closed
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
56 changes: 46 additions & 10 deletions site/src/pages/AgentsPage/AgentAnalyticsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import dayjs, { type Dayjs } from "dayjs";
import { type FC, useState } from "react";
import { useQuery } from "react-query";
import { useSearchParams } from "react-router";
import { chatCostSummary } from "#/api/queries/chats";
import type { DateRangeValue } from "#/components/DateRangePicker/DateRangePicker";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import { useAuthContext } from "#/contexts/auth/AuthProvider";
import { AgentAnalyticsPageView } from "./AgentAnalyticsPageView";
import { AgentPageHeader } from "./components/AgentPageHeader";

const createDateRange = (now?: Dayjs) => {
const startDateSearchParam = "startDate";
const endDateSearchParam = "endDate";

const getDefaultDateRange = (now?: Dayjs): DateRangeValue => {
const end = now ?? dayjs();
const start = end.subtract(30, "day");
return {
startDate: start.toISOString(),
endDate: end.toISOString(),
rangeLabel: `${start.format("MMM D")} – ${end.format("MMM D, YYYY")}`,
startDate: end.subtract(30, "day").toDate(),
endDate: end.toDate(),
};
};

Expand All @@ -24,13 +27,44 @@ interface AgentAnalyticsPageProps {

const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
const { user } = useAuthContext();
const [anchor] = useState<Dayjs>(() => dayjs());
const dateRange = createDateRange(now ?? anchor);

const [searchParams, setSearchParams] = useSearchParams();
const startDateParam = searchParams.get(startDateSearchParam)?.trim() ?? "";
const endDateParam = searchParams.get(endDateSearchParam)?.trim() ?? "";
const [defaultDateRange] = useState(() => getDefaultDateRange(now));
let dateRange = defaultDateRange;
let hasExplicitDateRange = false;

if (startDateParam && endDateParam) {
const parsedStartDate = new Date(startDateParam);
const parsedEndDate = new Date(endDateParam);

if (
!Number.isNaN(parsedStartDate.getTime()) &&
!Number.isNaN(parsedEndDate.getTime()) &&
parsedStartDate.getTime() <= parsedEndDate.getTime()
) {
dateRange = {
startDate: parsedStartDate,
endDate: parsedEndDate,
};
hasExplicitDateRange = true;
}
}

const onDateRangeChange = (value: DateRangeValue) => {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set(startDateSearchParam, value.startDate.toISOString());
next.set(endDateSearchParam, value.endDate.toISOString());
return next;
});
};

const summaryQuery = useQuery({
...chatCostSummary(user?.id ?? "me", {
start_date: dateRange.startDate,
end_date: dateRange.endDate,
start_date: dateRange.startDate.toISOString(),
end_date: dateRange.endDate.toISOString(),
}),
enabled: Boolean(user?.id),
});
Expand All @@ -43,7 +77,9 @@ const AgentAnalyticsPage: FC<AgentAnalyticsPageProps> = ({ now }) => {
isLoading={summaryQuery.isLoading}
error={summaryQuery.error}
onRetry={() => void summaryQuery.refetch()}
rangeLabel={dateRange.rangeLabel}
dateRange={dateRange}
onDateRangeChange={onDateRangeChange}
hasExplicitDateRange={hasExplicitDateRange}
/>
</ScrollArea>
);
Expand Down
27 changes: 20 additions & 7 deletions site/src/pages/AgentsPage/AgentAnalyticsPageView.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
import { BarChart3Icon } from "lucide-react";
import type { FC } from "react";
import type { ChatCostSummary } from "#/api/typesGenerated";
import {
DateRangePicker,
type DateRangeValue,
} from "#/components/DateRangePicker/DateRangePicker";
import { ChatCostSummaryView } from "./components/ChatCostSummaryView";
import { SectionHeader } from "./components/SectionHeader";
import { toInclusiveDateRange } from "./utils/dateRange";

interface AgentAnalyticsPageViewProps {
summary: ChatCostSummary | undefined;
isLoading: boolean;
error: unknown;
onRetry: () => void;
rangeLabel: string;
dateRange: DateRangeValue;
onDateRangeChange: (value: DateRangeValue) => void;
hasExplicitDateRange: boolean;
}

export const AgentAnalyticsPageView: FC<AgentAnalyticsPageViewProps> = ({
summary,
isLoading,
error,
onRetry,
rangeLabel,
dateRange,
onDateRangeChange,
hasExplicitDateRange,
}) => {
const displayDateRange = toInclusiveDateRange(
dateRange,
hasExplicitDateRange,
);

return (
<div className="flex flex-col p-4 pt-8">
<div className="mx-auto w-full max-w-3xl">
<SectionHeader
label="Analytics"
description="Review your personal Coder Agents usage and cost breakdowns."
action={
<div className="flex items-center gap-2 text-xs text-content-secondary">
<BarChart3Icon className="h-4 w-4" />
<span>{rangeLabel}</span>
</div>
<DateRangePicker
value={displayDateRange}
onChange={onDateRangeChange}
/>
}
/>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export const ChatModelAdminPanel: FC<ChatModelAdminPanelProps> = ({
const modelConfigsUnavailable = modelConfigsData === null;

return (
<div className={cn("flex min-h-full flex-col space-y-3", className)}>
<div className={cn("flex min-h-full flex-col space-y-6", className)}>
{isLoading && (
<div className="flex items-center gap-1.5 text-xs text-content-secondary">
<Spinner className="h-4 w-4" loading />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ export const MCPServerAdminPanel: FC<MCPServerAdminPanelProps> = ({
}

return (
<div className="flex min-h-full flex-col space-y-3">
<div className="flex min-h-full flex-col space-y-6">
{!isFormView ? (
<ServerList
servers={servers}
Expand Down
25 changes: 12 additions & 13 deletions site/src/pages/AgentsPage/components/PRInsightsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {
} from "#/components/Table/Table";
import { cn } from "#/utils/cn";
import { formatCostMicros } from "#/utils/currency";
import { AdminBadge } from "./AdminBadge";
import { PrStateIcon } from "./GitPanel/GitPanel";
import { SectionHeader } from "./SectionHeader";

dayjs.extend(relativeTime);

Expand Down Expand Up @@ -295,19 +297,16 @@ export const PRInsightsView: FC<PRInsightsViewProps> = ({
const isEmpty = summary.total_prs_created === 0;

return (
<div className="space-y-8">
<div className="space-y-6">
{/* ── Header ── */}
<div className="flex items-end justify-between">
<div>
<h2 className="m-0 text-xl font-semibold tracking-tight text-content-primary">
Pull Request Insights
</h2>
<p className="m-0 mt-1 text-[13px] text-content-secondary">
Code changes detected by Agents.
</p>
</div>
<TimeRangeFilter value={timeRange} onChange={onTimeRangeChange} />
</div>
<SectionHeader
label="Pull Request Insights"
description="Code changes detected by Agents."
badge={<AdminBadge />}
action={
<TimeRangeFilter value={timeRange} onChange={onTimeRangeChange} />
}
/>

{isEmpty ? (
<EmptyState />
Expand Down Expand Up @@ -355,7 +354,7 @@ export const PRInsightsView: FC<PRInsightsViewProps> = ({
</section>

{/* ── Model breakdown + Recent PRs side by side ── */}
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
<div className="grid grid-cols-1 gap-6">
{/* ── Model performance (simplified) ── */}
{by_model.length > 0 && (
<section>
Expand Down
35 changes: 35 additions & 0 deletions site/src/pages/AgentsPage/utils/dateRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { DateRangeValue } from "#/components/DateRangePicker/DateRangePicker";

/**
* Returns true when the given date falls exactly on midnight
* (00:00:00.000). Date-range pickers use midnight of the *following*
* day as the exclusive upper bound for a full-day selection. Detecting
* this lets call sites subtract 1 ms (or 1 day) so the UI shows the
* inclusive end date instead.
*/
function isMidnight(date: Date): boolean {
return (
date.getHours() === 0 &&
date.getMinutes() === 0 &&
date.getSeconds() === 0 &&
date.getMilliseconds() === 0
);
}

/**
* When the user picks an explicit date range whose end boundary is
* midnight of the following day, adjust it by βˆ’1 ms so the
* DateRangePicker highlights the inclusive end date.
*/
export function toInclusiveDateRange(
dateRange: DateRangeValue,
hasExplicitDateRange: boolean,
): DateRangeValue {
if (hasExplicitDateRange && isMidnight(dateRange.endDate)) {
return {
startDate: dateRange.startDate,
endDate: new Date(dateRange.endDate.getTime() - 1),
};
}
return dateRange;
}
Loading