fix(ptu-ui): read flag from ui_settings, gate leftnav on it

The useIsPtuCostAttributionEnabled hook was reading /config/list?config_type=general_settings, which is built from the fixed ConfigGeneralSettings schema and does not contain enable_ptu_cost_attribution. The flag lives in the ui_settings surface (/get/ui_settings) and is synced into the in-process general_settings dict at runtime. Point the hook at useUISettings so the panel and Usage Flat Cost column reflect the flag state.

Thread enablePtuCostAttribution through SidebarProvider so the PTU Reservations nav item appears and disappears with the flag, matching the enable_chat_ui / enable_projects_ui pattern.
This commit is contained in:
Yucheng Zhu 2026-07-15 14:11:08 -07:00
parent 06d8b45a91
commit b69f3601f8
5 changed files with 64 additions and 7 deletions

View file

@ -26,6 +26,7 @@ const SidebarProvider = ({
const [allowAgentsForTeamAdmins, setAllowAgentsForTeamAdmins] = useState<boolean>(false);
const [disableVectorStoresForInternalUsers, setDisableVectorStoresForInternalUsers] = useState<boolean>(false);
const [allowVectorStoresForTeamAdmins, setAllowVectorStoresForTeamAdmins] = useState<boolean>(false);
const [enablePtuCostAttribution, setEnablePtuCostAttribution] = useState<boolean>(false);
useEffect(() => {
const fetchUISettings = async () => {
@ -65,6 +66,10 @@ const SidebarProvider = ({
if (settings?.values?.allow_vector_stores_for_team_admins !== undefined) {
setAllowVectorStoresForTeamAdmins(Boolean(settings.values.allow_vector_stores_for_team_admins));
}
if (settings?.values?.enable_ptu_cost_attribution !== undefined) {
setEnablePtuCostAttribution(Boolean(settings.values.enable_ptu_cost_attribution));
}
} catch (error) {
console.error("[SidebarProvider] Failed to fetch UI settings:", error);
}
@ -86,6 +91,7 @@ const SidebarProvider = ({
allowAgentsForTeamAdmins={allowAgentsForTeamAdmins}
disableVectorStoresForInternalUsers={disableVectorStoresForInternalUsers}
allowVectorStoresForTeamAdmins={allowVectorStoresForTeamAdmins}
enablePtuCostAttribution={enablePtuCostAttribution}
/>
);
};

View file

@ -0,0 +1,42 @@
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useIsPtuCostAttributionEnabled } from "./useIsPtuCostAttributionEnabled";
const mockUseUISettings = vi.hoisted(() => vi.fn());
vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: mockUseUISettings,
}));
describe("useIsPtuCostAttributionEnabled", () => {
it("reads enable_ptu_cost_attribution from /get/ui_settings values (regression: v1 read the wrong endpoint)", () => {
mockUseUISettings.mockReturnValue({
data: { values: { enable_ptu_cost_attribution: true } },
isLoading: false,
});
const { result } = renderHook(() => useIsPtuCostAttributionEnabled());
expect(result.current.enabled).toBe(true);
expect(result.current.isLoading).toBe(false);
});
it("returns enabled=false when the flag is not set in ui_settings values", () => {
mockUseUISettings.mockReturnValue({ data: { values: {} }, isLoading: false });
const { result } = renderHook(() => useIsPtuCostAttributionEnabled());
expect(result.current.enabled).toBe(false);
});
it("returns enabled=false when ui_settings data is undefined (still loading or missing)", () => {
mockUseUISettings.mockReturnValue({ data: undefined, isLoading: true });
const { result } = renderHook(() => useIsPtuCostAttributionEnabled());
expect(result.current.enabled).toBe(false);
expect(result.current.isLoading).toBe(true);
});
it("returns enabled=false when the flag value is explicitly false", () => {
mockUseUISettings.mockReturnValue({
data: { values: { enable_ptu_cost_attribution: false } },
isLoading: false,
});
const { result } = renderHook(() => useIsPtuCostAttributionEnabled());
expect(result.current.enabled).toBe(false);
});
});

View file

@ -1,17 +1,13 @@
import { useMemo } from "react";
import { ConfigType, useProxyConfig } from "@/app/(dashboard)/hooks/proxyConfig/useProxyConfig";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
const FLAG_FIELD = "enable_ptu_cost_attribution";
export const useIsPtuCostAttributionEnabled = (): { enabled: boolean; isLoading: boolean } => {
const { data, isLoading } = useProxyConfig(ConfigType.GENERAL_SETTINGS);
const { data, isLoading } = useUISettings();
const enabled = useMemo(() => {
if (!data) {
return false;
}
const entry = data.find((item) => item.field_name === FLAG_FIELD);
return Boolean(entry?.field_value);
return Boolean(data?.values?.[FLAG_FIELD]);
}, [data]);
return { enabled, isLoading };

View file

@ -122,6 +122,16 @@ describe("Sidebar (leftnav)", () => {
expect(screen.getByText("Chat")).toBeInTheDocument();
});
it("hides PTU Reservations by default", () => {
renderWithProviders(<Sidebar {...defaultProps} />);
expect(screen.queryByText("PTU Reservations")).not.toBeInTheDocument();
});
it("shows PTU Reservations when enablePtuCostAttribution is true", () => {
renderWithProviders(<Sidebar {...defaultProps} enablePtuCostAttribution />);
expect(screen.getByText("PTU Reservations")).toBeInTheDocument();
});
it("expands a nested tab to reveal its children (Tools > Search Tools)", async () => {
renderWithProviders(<Sidebar {...defaultProps} />);

View file

@ -93,6 +93,7 @@ interface SidebarProps {
allowAgentsForTeamAdmins?: boolean;
disableVectorStoresForInternalUsers?: boolean;
allowVectorStoresForTeamAdmins?: boolean;
enablePtuCostAttribution?: boolean;
}
interface MenuItem {
@ -401,6 +402,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
allowAgentsForTeamAdmins,
disableVectorStoresForInternalUsers,
allowVectorStoresForTeamAdmins,
enablePtuCostAttribution,
}) => {
const { userId, accessToken, userRole } = useAuthorized();
const { data: organizations } = useOrganizations();
@ -452,6 +454,7 @@ const Sidebar_: React.FC<SidebarProps> = ({
}
if (item.key === "projects" && !enableProjectsUI) return false;
if (item.key === "chat" && !enableChatUI) return false;
if (item.key === "ptu-reservations" && !enablePtuCostAttribution) return false;
if (
!isAdmin &&
item.key === "agents" &&