feat(ui): admin-configurable default Usage date range

Adds the default_usage_date_range UI setting (today, last_7_days, last_30_days,
month_to_date, year_to_date) so a proxy admin can pick what range the Usage page
opens with for every user. Unset keeps the trailing 7 days. Presets are computed
in the viewer's browser timezone and shared with the date picker. Manual range
changes stay local to the page and never write the setting back.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-09-03 02:45:13 +00:00
parent 8065ede40b
commit 24a17e8353
10 changed files with 556 additions and 59 deletions

View file

@ -7,6 +7,7 @@ from collections.abc import Mapping, Sequence
from types import MappingProxyType
from typing import (
Final,
Literal,
NamedTuple,
Protocol,
cast, # noqa: TID251 # prisma types Json columns as fields.Json but de-serializes them to plain python on read
@ -212,6 +213,9 @@ class UIThemeSettingsResponse(SettingsResponse):
"""Response model for UI theme settings"""
UsageDateRangePreset = Literal["today", "last_7_days", "last_30_days", "month_to_date", "year_to_date"]
class UISettings(BaseModel):
"""Configuration for UI-specific flags"""
@ -304,6 +308,16 @@ class UISettings(BaseModel):
description="If true, shows the Chat page in the UI sidebar, letting users chat with an LLM and connect their own MCP server credentials via OAuth.",
)
default_usage_date_range: UsageDateRangePreset | None = Field(
default=None,
description=(
"Date range the Usage page opens with for every user. Boundaries are computed in the viewer's browser "
"timezone: month_to_date starts at 00:00 on the 1st of the current calendar month and ends at the end of "
"today. Unset falls back to the last 7 days. Users can still pick any other range; doing so does not "
"change this setting."
),
)
class UISettingsResponse(SettingsResponse):
"""Response model for UI settings"""
@ -326,6 +340,7 @@ ALLOWED_UI_SETTINGS_FIELDS: Final = {
"disable_custom_api_keys",
"disable_key_generate_for_org_admin",
"enable_chat_ui",
"default_usage_date_range",
}
ENABLE_PTU_COST_ATTRIBUTION_UI_SETTING: Final = "enable_ptu_cost_attribution"

View file

@ -3266,3 +3266,114 @@ class TestPtuCostAttributionUISetting:
assert response.status_code == 400
assert "enable_ptu_cost_attribution" in str(response.json()["detail"])
assert not mock_prisma.db.litellm_uisettings.upsert.called
class TestDefaultUsageDateRangeUISetting:
"""``default_usage_date_range`` is what the Usage page opens with. It is persisted
through the same allowlisted PATCH/GET pair as the other UI settings."""
@staticmethod
def _mock_prisma(monkeypatch, stored=None):
from unittest.mock import AsyncMock, MagicMock
mock_prisma = MagicMock()
mock_record = None
if stored is not None:
mock_record = MagicMock()
mock_record.ui_settings = json.dumps(stored)
mock_prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=mock_record)
mock_prisma.db.litellm_uisettings.upsert = AsyncMock()
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
return mock_prisma
@staticmethod
def _override_auth(role):
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_id="test-user-123", user_role=role)
def test_get_reports_the_setting_as_unset_and_documents_the_fallback(self, mock_auth, monkeypatch):
self._mock_prisma(monkeypatch)
response = client.get("/get/ui_settings")
assert response.status_code == 200
body = response.json()
assert body["values"]["default_usage_date_range"] is None
assert "Unset falls back to the last 7 days" in body["field_schema"]["properties"]["default_usage_date_range"]["description"]
def test_proxy_admin_can_persist_a_preset_and_read_it_back(self, mock_auth, monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
mock_prisma = self._mock_prisma(monkeypatch, stored={"enable_chat_ui": True})
self._override_auth(LitellmUserRoles.PROXY_ADMIN)
try:
response = client.patch("/update/ui_settings", json={"default_usage_date_range": "month_to_date"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 200, response.text
upsert_data = mock_prisma.db.litellm_uisettings.upsert.call_args.kwargs["data"]
persisted = json.loads(upsert_data["update"]["ui_settings"])
assert persisted == {"enable_chat_ui": True, "default_usage_date_range": "month_to_date"}
self._mock_prisma(monkeypatch, stored=persisted)
read_back = client.get("/get/ui_settings")
assert read_back.json()["values"]["default_usage_date_range"] == "month_to_date"
def test_proxy_admin_can_clear_the_setting_back_to_the_fallback(self, mock_auth, monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
mock_prisma = self._mock_prisma(monkeypatch, stored={"default_usage_date_range": "month_to_date"})
self._override_auth(LitellmUserRoles.PROXY_ADMIN)
try:
response = client.patch("/update/ui_settings", json={"default_usage_date_range": None})
finally:
app.dependency_overrides.clear()
assert response.status_code == 200, response.text
upsert_data = mock_prisma.db.litellm_uisettings.upsert.call_args.kwargs["data"]
assert json.loads(upsert_data["update"]["ui_settings"])["default_usage_date_range"] is None
def test_patch_rejects_a_value_outside_the_presets(self, mock_auth, monkeypatch):
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
mock_prisma = self._mock_prisma(monkeypatch)
self._override_auth(LitellmUserRoles.PROXY_ADMIN)
try:
response = client.patch("/update/ui_settings", json={"default_usage_date_range": "last_90_days"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 422
assert not mock_prisma.db.litellm_uisettings.upsert.called
@pytest.mark.parametrize(
"role",
[LitellmUserRoles.INTERNAL_USER_VIEW_ONLY, LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY],
)
def test_non_proxy_admins_cannot_change_the_setting(self, mock_auth, monkeypatch, role):
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
mock_prisma = self._mock_prisma(monkeypatch, stored={"default_usage_date_range": "month_to_date"})
self._override_auth(role)
try:
response = client.patch("/update/ui_settings", json={"default_usage_date_range": "last_30_days"})
finally:
app.dependency_overrides.clear()
assert response.status_code == 403
assert not mock_prisma.db.litellm_uisettings.upsert.called
def test_read_only_user_can_read_the_setting(self, mock_auth, monkeypatch):
self._mock_prisma(monkeypatch, stored={"default_usage_date_range": "month_to_date"})
self._override_auth(LitellmUserRoles.INTERNAL_USER_VIEW_ONLY)
try:
response = client.get("/get/ui_settings")
finally:
app.dependency_overrides.clear()
assert response.status_code == 200
assert response.json()["values"]["default_usage_date_range"] == "month_to_date"

View file

@ -1,5 +1,6 @@
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
import { useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin";
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
@ -29,6 +30,7 @@ vi.mock("@/components/networking", () => ({
userDailyActivityAggregatedCall: vi.fn(),
gatewayDailyActivityCall: vi.fn(),
tagListCall: vi.fn(),
updateUiSettings: vi.fn(),
}));
// Mock child components to simplify testing
@ -48,8 +50,22 @@ vi.mock("@/components/UsagePage/components/EntityUsage/TopKeyView", () => ({
}));
vi.mock("./EntityUsage/EntityUsage", () => ({
default: ({ entityType, entityList }: { entityType: string; entityList: unknown }) => (
<div data-testid="entity-usage" data-entity-type={entityType} data-entity-list={JSON.stringify(entityList ?? null)}>
default: ({
entityType,
entityList,
dateValue,
}: {
entityType: string;
entityList: unknown;
dateValue: { from?: Date; to?: Date };
}) => (
<div
data-testid="entity-usage"
data-entity-type={entityType}
data-entity-list={JSON.stringify(entityList ?? null)}
data-date-from={dateValue.from?.toISOString()}
data-date-to={dateValue.to?.toISOString()}
>
Entity Usage
</div>
),
@ -133,6 +149,10 @@ vi.mock("@/app/(dashboard)/hooks/customers/useCustomers", () => ({
useCustomers: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/agents/useAgents", () => ({
useAgents: vi.fn(),
}));
@ -166,6 +186,16 @@ describe("UsagePage", () => {
const mockUseAuthorized = vi.mocked(useAuthorized);
const mockUseCurrentUser = vi.mocked(useCurrentUser);
const mockUseInfiniteUsers = vi.mocked(useInfiniteUsers);
const mockUseUISettings = vi.mocked(useUISettings);
const mockUpdateUiSettings = vi.mocked(networking.updateUiSettings);
type UISettingsResult = ReturnType<typeof useUISettings>;
const uiSettingsResolvedWith = (values: Record<string, unknown>): UISettingsResult =>
({
data: { values, field_schema: { properties: {} } },
isLoading: false,
isError: false,
}) as unknown as UISettingsResult;
const mockSpendData = {
results: [
@ -373,6 +403,8 @@ describe("UsagePage", () => {
isLoading: false,
error: null,
} as any);
mockUseUISettings.mockReturnValue(uiSettingsResolvedWith({}));
mockUpdateUiSettings.mockClear();
mockUserDailyActivityAggregatedCall.mockClear();
mockUserDailyActivityCall.mockClear();
mockTagListCall.mockClear();
@ -1025,6 +1057,120 @@ describe("UsagePage", () => {
});
});
describe("admin-configured default date range", () => {
const firstFetchedRange = () => {
const [, from, to] = mockUserDailyActivityAggregatedCall.mock.calls[0] as [string, Date, Date, ...unknown[]];
return { from, to };
};
const startOfThisMonth = () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1);
};
const endOfToday = () => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
};
it("opens on the trailing 7 days when the admin has not set a default", async () => {
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalledTimes(1);
});
const { from, to } = firstFetchedRange();
expect(to.getTime() - from.getTime()).toBe(7 * 24 * 60 * 60 * 1000);
expect(Date.now() - to.getTime()).toBeLessThan(60 * 1000);
});
it("opens a read-only user's first view on the configured month-to-date range", async () => {
mockUseAuthorized.mockReturnValue(nonAdminSession);
mockUseUISettings.mockReturnValue(uiSettingsResolvedWith({ default_usage_date_range: "month_to_date" }));
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalledTimes(1);
});
expect(firstFetchedRange()).toEqual({ from: startOfThisMonth(), to: endOfToday() });
});
it("applies the configured range to every request an admin's first view makes", async () => {
mockUseUISettings.mockReturnValue(uiSettingsResolvedWith({ default_usage_date_range: "month_to_date" }));
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockGatewayDailyActivityCall).toHaveBeenCalledWith("test-token", startOfThisMonth(), endOfToday());
});
expect(firstFetchedRange()).toEqual({ from: startOfThisMonth(), to: endOfToday() });
});
it("carries the configured range into the entity usage views the user navigates to", async () => {
mockUseUISettings.mockReturnValue(uiSettingsResolvedWith({ default_usage_date_range: "month_to_date" }));
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
act(() => {
fireEvent.change(screen.getByTestId("usage-view-select"), { target: { value: "team" } });
});
const entityUsage = await screen.findByTestId("entity-usage");
expect(entityUsage).toHaveAttribute("data-entity-type", "team");
expect(entityUsage).toHaveAttribute("data-date-from", startOfThisMonth().toISOString());
expect(entityUsage).toHaveAttribute("data-date-to", endOfToday().toISOString());
});
it("does not fetch with the fallback range while the setting is still loading", async () => {
mockUseUISettings.mockReturnValue({
data: undefined,
isLoading: true,
isError: false,
} as unknown as UISettingsResult);
const { rerender } = renderWithProviders(<UsagePage {...defaultProps} />);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
});
expect(mockUserDailyActivityAggregatedCall).not.toHaveBeenCalled();
mockUseUISettings.mockReturnValue(uiSettingsResolvedWith({ default_usage_date_range: "month_to_date" }));
rerender(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalledTimes(1);
});
expect(firstFetchedRange()).toEqual({ from: startOfThisMonth(), to: endOfToday() });
});
it("lets the user pick another range without writing the admin default back", async () => {
mockUseAuthorized.mockReturnValue(nonAdminSession);
mockUseUISettings.mockReturnValue(uiSettingsResolvedWith({ default_usage_date_range: "month_to_date" }));
renderWithProviders(<UsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalledTimes(1);
});
await act(async () => {
fireEvent.click(screen.getByTestId("pick-a-different-range"));
});
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalledTimes(2);
});
expect(mockUserDailyActivityAggregatedCall).toHaveBeenLastCalledWith(
"test-token",
new Date("2024-01-01T00:00:00Z"),
new Date("2024-01-08T00:00:00Z"),
"user-123",
);
expect(mockUpdateUiSettings).not.toHaveBeenCalled();
});
});
describe("aggregated endpoint fallback", () => {
it("should fall back to paginated calls when aggregated endpoint fails", async () => {
mockUserDailyActivityAggregatedCall.mockRejectedValue(new Error("Aggregated endpoint not available"));

View file

@ -20,6 +20,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
import { useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import useIsOrgAdmin from "@/app/(dashboard)/hooks/useIsOrgAdmin";
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
@ -63,6 +64,7 @@ import { TOP_MODEL_LIMITS } from "./EntityUsage/TopModelView";
import TopKeyView from "@/components/UsagePage/components/EntityUsage/TopKeyView";
import UsageAIChatPanel from "./UsageAIChatPanel";
import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect";
import { DEFAULT_USAGE_DATE_RANGE_SETTING_KEY, resolveDefaultUsageDateRange } from "./defaultUsageDateRange";
interface UsagePageProps {
teams: Team[];
@ -86,15 +88,14 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
// Separate loading states for better UX
const [isDateChanging, setIsDateChanging] = useState(false);
// Create initial dates outside of state to prevent recreation
const initialFromDate = useMemo(() => new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), []);
const initialToDate = useMemo(() => new Date(), []);
// Single date state that directly triggers data fetching
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
from: initialFromDate,
to: initialToDate,
});
const { data: uiSettings, isLoading: isUISettingsLoading } = useUISettings();
const configuredDefaultRange: unknown = uiSettings?.values?.[DEFAULT_USAGE_DATE_RANGE_SETTING_KEY];
const defaultDateValue = useMemo<DateRangePickerValue | null>(
() => (isUISettingsLoading ? null : resolveDefaultUsageDateRange(configuredDefaultRange)),
[isUISettingsLoading, configuredDefaultRange],
);
const [pickedDateValue, setPickedDateValue] = useState<DateRangePickerValue | null>(null);
const dateValue: DateRangePickerValue = pickedDateValue ?? defaultDateValue ?? {};
const [fetchedTags, setFetchedTags] = useState<FetchedForRange<EntityList[]> | null>(null);
// No [] default: an unresolved query must stay undefined so the customer
@ -260,7 +261,7 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
setIsDateChanging(true);
// Update date immediately for UI responsiveness
setDateValue(newValue);
setPickedDateValue(newValue);
}, []);
// Derived states from userSpendData

View file

@ -0,0 +1,65 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { resolveDefaultUsageDateRange } from "./defaultUsageDateRange";
const local = (y: number, m: number, d: number, time = "00:00:00.000") =>
new Date(`${y}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}T${time}`);
const END_OF_DAY = "23:59:59.999";
describe("resolveDefaultUsageDateRange", () => {
describe("fallback when the admin has not configured a default", () => {
it.each([undefined, null, "", "last_90_days", 7, { id: "month_to_date" }])(
"opens on the trailing 7 days for %j",
(setting) => {
const now = local(2026, 3, 15, "10:30:00.000");
const range = resolveDefaultUsageDateRange(setting, now);
expect(range.from).toEqual(local(2026, 3, 8, "10:30:00.000"));
expect(range.to).toEqual(now);
},
);
});
describe("month_to_date", () => {
afterEach(() => {
vi.unstubAllEnvs();
});
it("starts at local midnight on the 1st of the current month and ends at the end of today", () => {
const range = resolveDefaultUsageDateRange("month_to_date", local(2026, 3, 15, "10:30:00.000"));
expect(range.from).toEqual(local(2026, 3, 1));
expect(range.to).toEqual(local(2026, 3, 15, END_OF_DAY));
});
it("rolls over to the new month as soon as the 1st begins", () => {
const lastOfMarch = resolveDefaultUsageDateRange("month_to_date", local(2026, 3, 31, END_OF_DAY));
const firstOfApril = resolveDefaultUsageDateRange("month_to_date", local(2026, 4, 1, "00:00:00.001"));
expect(lastOfMarch.from).toEqual(local(2026, 3, 1));
expect(firstOfApril.from).toEqual(local(2026, 4, 1));
expect(firstOfApril.to).toEqual(local(2026, 4, 1, END_OF_DAY));
});
it("keeps the month boundary in the viewer's local timezone rather than UTC", () => {
vi.stubEnv("TZ", "Pacific/Kiritimati");
const range = resolveDefaultUsageDateRange("month_to_date", new Date("2026-01-31T10:30:00.000Z"));
expect(range.from.toISOString()).toBe("2026-01-31T10:00:00.000Z");
expect(range.to.toISOString()).toBe("2026-02-01T09:59:59.999Z");
});
});
it.each([
["today", local(2026, 3, 15), local(2026, 3, 15, END_OF_DAY)],
["last_7_days", local(2026, 3, 8), local(2026, 3, 15, END_OF_DAY)],
["last_30_days", local(2026, 2, 13), local(2026, 3, 15, END_OF_DAY)],
["year_to_date", local(2026, 1, 1), local(2026, 3, 15, END_OF_DAY)],
])("resolves %s to the same window the date picker preset uses", (preset, from, to) => {
const range = resolveDefaultUsageDateRange(preset, local(2026, 3, 15, "10:30:00.000"));
expect(range.from).toEqual(from);
expect(range.to).toEqual(to);
});
});

View file

@ -0,0 +1,10 @@
import { isDateRangePresetId, resolveDateRangePreset } from "@/components/shared/date_range_presets";
export const DEFAULT_USAGE_DATE_RANGE_SETTING_KEY = "default_usage_date_range";
const FALLBACK_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
export const resolveDefaultUsageDateRange = (setting: unknown, now: Date = new Date()): { from: Date; to: Date } =>
isDateRangePresetId(setting)
? resolveDateRangePreset(setting, now)
: { from: new Date(now.getTime() - FALLBACK_WINDOW_MS), to: now };

View file

@ -1,5 +1,7 @@
import { render, screen, fireEvent, act } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { chooseSelectOption } from "@/../tests/test-utils";
import UISettings from "./UISettings";
import { toast } from "@/lib/toast";
@ -155,4 +157,70 @@ describe("UISettings", () => {
);
expect(toast.success).toHaveBeenCalledWith("UI settings updated successfully");
});
describe("default Usage date range", () => {
const selectWithValue = (value: unknown) => {
const response = buildSettingsResponse();
mockUseUISettings.mockReturnValue({
...response,
data: { ...response.data, values: { ...response.data.values, default_usage_date_range: value } },
});
};
it("shows the 7-day fallback when the admin has not set a default", () => {
selectWithValue(null);
render(<UISettings />);
expect(screen.getByRole("combobox", { name: "Default Usage date range" })).toHaveTextContent(
"Last 7 days (default)",
);
});
it("shows the persisted preset", () => {
selectWithValue("month_to_date");
render(<UISettings />);
expect(screen.getByRole("combobox", { name: "Default Usage date range" })).toHaveTextContent("Month to date");
});
it("persists the chosen preset under default_usage_date_range", async () => {
const mutateMock = vi.fn((_settings, options) => {
options?.onSuccess?.();
});
mockUseUpdateUISettings.mockReturnValue({ mutate: mutateMock, isPending: false, error: null });
selectWithValue(null);
const user = userEvent.setup();
render(<UISettings />);
await chooseSelectOption(
user,
screen.getByRole("combobox", { name: "Default Usage date range" }),
"Month to date",
);
expect(mutateMock).toHaveBeenCalledWith(
{ default_usage_date_range: "month_to_date" },
expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }),
);
expect(toast.success).toHaveBeenCalledWith("UI settings updated successfully");
});
it("clears the setting back to the fallback with null", async () => {
const mutateMock = vi.fn();
mockUseUpdateUISettings.mockReturnValue({ mutate: mutateMock, isPending: false, error: null });
selectWithValue("month_to_date");
const user = userEvent.setup();
render(<UISettings />);
await chooseSelectOption(
user,
screen.getByRole("combobox", { name: "Default Usage date range" }),
"Last 7 days (default)",
);
expect(mutateMock).toHaveBeenCalledWith({ default_usage_date_range: null }, expect.anything());
});
});
});

View file

@ -5,7 +5,13 @@ import { useUpdateUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUpdat
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { toast } from "@/lib/toast";
import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert";
import {
DATE_RANGE_PRESETS,
isDateRangePresetId,
type DateRangePresetId,
} from "@/components/shared/date_range_presets";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
@ -45,6 +51,13 @@ function SettingRow({
);
}
const FALLBACK_USAGE_DATE_RANGE_LABEL = "Last 7 days (default)";
const usageDateRangeItems = [
{ value: null, label: FALLBACK_USAGE_DATE_RANGE_LABEL },
...DATE_RANGE_PRESETS.map((preset) => ({ value: preset.id, label: preset.label })),
];
export default function UISettings() {
const { accessToken } = useAuthorized();
const { data, isLoading, isError, error } = useUISettings();
@ -65,7 +78,11 @@ export default function UISettings() {
const allowVectorStoresTeamAdminsProperty = schema?.properties?.allow_vector_stores_for_team_admins;
const scopeUserSearchProperty = schema?.properties?.scope_user_search_to_org;
const disableCustomApiKeysProperty = schema?.properties?.disable_custom_api_keys;
const defaultUsageDateRangeProperty = schema?.properties?.default_usage_date_range;
const values = data?.values ?? {};
const defaultUsageDateRange: DateRangePresetId | null = isDateRangePresetId(values.default_usage_date_range)
? values.default_usage_date_range
: null;
const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users);
const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user);
const isAgentsDisabled = Boolean(values.disable_agents_for_internal_users);
@ -252,6 +269,20 @@ export default function UISettings() {
);
};
const handleChangeDefaultUsageDateRange = (value: DateRangePresetId | null) => {
updateSettings(
{ default_usage_date_range: value },
{
onSuccess: () => {
toast.success("UI settings updated successfully");
},
onError: (error) => {
toast.fromError(error);
},
},
);
};
const handleToggleDisableCustomApiKeys = (checked: boolean) => {
updateSettings(
{ disable_custom_api_keys: checked },
@ -432,6 +463,32 @@ export default function UISettings() {
}
/>
<Separator />
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">Default Usage date range</p>
<p className="text-sm text-muted-foreground">
{defaultUsageDateRangeProperty?.description ??
"Date range the Usage page opens with for every user, in the viewer's browser timezone. Unset falls back to the last 7 days."}
</p>
<Select
value={defaultUsageDateRange}
items={usageDateRangeItems}
onValueChange={handleChangeDefaultUsageDateRange}
disabled={isUpdating}
>
<SelectTrigger aria-label="Default Usage date range" className="w-56">
<SelectValue placeholder={FALLBACK_USAGE_DATE_RANGE_LABEL} />
</SelectTrigger>
<SelectContent>
{usageDateRangeItems.map((item) => (
<SelectItem key={item.value ?? "unset"} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Separator />
<PageVisibilitySettings
enabledPagesInternalUsers={values.enabled_ui_pages_internal_users}

View file

@ -2,6 +2,7 @@ import { Calendar, Clock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/cva.config";
import type { DateRangePickerValue } from "./date_picker_types";
import { DATE_RANGE_PRESETS, type DateRangePreset } from "./date_range_presets";
import moment from "moment";
import React, { useCallback, useEffect, useRef, useState } from "react";
@ -14,54 +15,9 @@ interface AdvancedDatePickerProps {
align?: "left" | "right";
}
interface RelativeTimeOption {
label: string;
shortLabel: string;
getValue: () => { from: Date; to: Date };
}
type RelativeTimeOption = DateRangePreset;
const relativeTimeOptions: RelativeTimeOption[] = [
{
label: "Today",
shortLabel: "today",
getValue: () => ({
from: moment().startOf("day").toDate(),
to: moment().endOf("day").toDate(),
}),
},
{
label: "Last 7 days",
shortLabel: "7d",
getValue: () => ({
from: moment().subtract(7, "days").startOf("day").toDate(),
to: moment().endOf("day").toDate(),
}),
},
{
label: "Last 30 days",
shortLabel: "30d",
getValue: () => ({
from: moment().subtract(30, "days").startOf("day").toDate(),
to: moment().endOf("day").toDate(),
}),
},
{
label: "Month to date",
shortLabel: "MTD",
getValue: () => ({
from: moment().startOf("month").toDate(),
to: moment().endOf("day").toDate(),
}),
},
{
label: "Year to date",
shortLabel: "YTD",
getValue: () => ({
from: moment().startOf("year").toDate(),
to: moment().endOf("day").toDate(),
}),
},
];
const relativeTimeOptions: readonly RelativeTimeOption[] = DATE_RANGE_PRESETS;
/**
* Advanced Date Range Picker with dropdown, relative times, and custom inputs

View file

@ -0,0 +1,68 @@
import moment from "moment";
export const DATE_RANGE_PRESET_IDS = ["today", "last_7_days", "last_30_days", "month_to_date", "year_to_date"] as const;
export type DateRangePresetId = (typeof DATE_RANGE_PRESET_IDS)[number];
export interface DateRangePreset {
id: DateRangePresetId;
label: string;
shortLabel: string;
getValue: (now?: Date) => { from: Date; to: Date };
}
export const isDateRangePresetId = (value: unknown): value is DateRangePresetId =>
typeof value === "string" && (DATE_RANGE_PRESET_IDS as readonly string[]).includes(value);
const PRESETS_BY_ID: Record<DateRangePresetId, DateRangePreset> = {
today: {
id: "today",
label: "Today",
shortLabel: "today",
getValue: (now = new Date()) => ({
from: moment(now).startOf("day").toDate(),
to: moment(now).endOf("day").toDate(),
}),
},
last_7_days: {
id: "last_7_days",
label: "Last 7 days",
shortLabel: "7d",
getValue: (now = new Date()) => ({
from: moment(now).subtract(7, "days").startOf("day").toDate(),
to: moment(now).endOf("day").toDate(),
}),
},
last_30_days: {
id: "last_30_days",
label: "Last 30 days",
shortLabel: "30d",
getValue: (now = new Date()) => ({
from: moment(now).subtract(30, "days").startOf("day").toDate(),
to: moment(now).endOf("day").toDate(),
}),
},
month_to_date: {
id: "month_to_date",
label: "Month to date",
shortLabel: "MTD",
getValue: (now = new Date()) => ({
from: moment(now).startOf("month").toDate(),
to: moment(now).endOf("day").toDate(),
}),
},
year_to_date: {
id: "year_to_date",
label: "Year to date",
shortLabel: "YTD",
getValue: (now = new Date()) => ({
from: moment(now).startOf("year").toDate(),
to: moment(now).endOf("day").toDate(),
}),
},
};
export const DATE_RANGE_PRESETS: readonly DateRangePreset[] = DATE_RANGE_PRESET_IDS.map((id) => PRESETS_BY_ID[id]);
export const resolveDateRangePreset = (id: DateRangePresetId, now: Date = new Date()): { from: Date; to: Date } =>
PRESETS_BY_ID[id].getValue(now);