Skip to content

Commit 1e957fa

Browse files
committed
fix(site/src/pages/GroupsPage): gate AI budget behind experiment
Gate the AI budget UI behind the ai-gateway-cost-control experiment while the feature is in development. Also give the group patch and budget save separate error handling so a budget failure is not reported as a group update failure.
1 parent 499b7c5 commit 1e957fa

3 files changed

Lines changed: 51 additions & 17 deletions

File tree

site/src/pages/GroupsPage/GroupSettingsPage.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ import {
1010
} from "#/api/queries/groups";
1111
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
1212
import { Spinner } from "#/components/Spinner/Spinner";
13+
import { useDashboard } from "#/modules/dashboard/useDashboard";
1314
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
1415
import { dollarsToMicros, microsToDollars } from "#/utils/currency";
1516
import type { GroupPageOutletContext } from "./GroupPage";
1617
import GroupSettingsPageView from "./GroupSettingsPageView";
1718

18-
// Empty is uncapped (no budget row); otherwise the budget in micros.
1919
const budgetFromInput = (dollars: string): number | null =>
2020
dollars.trim() === "" ? null : dollarsToMicros(dollars);
2121

@@ -29,8 +29,12 @@ const GroupSettingsPage: FC = () => {
2929
const patchGroupMutation = useMutation(patchGroup(queryClient, organization));
3030
const navigate = useNavigate();
3131

32-
// Budget routes are gated on aibridge; useFeatureVisibility is {} unlicensed.
33-
const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
32+
const { experiments } = useDashboard();
33+
// TODO(AIGOV-443): remove the ai-gateway-cost-control experiment gate once
34+
// the cost-control feature is stable.
35+
const aibridgeVisible =
36+
Boolean(useFeatureVisibility().aibridge) &&
37+
experiments.includes("ai-gateway-cost-control");
3438
const budgetQuery = useQuery({
3539
...groupAIBudget(groupData.id),
3640
enabled: aibridgeVisible,
@@ -39,7 +43,6 @@ const GroupSettingsPage: FC = () => {
3943
saveGroupAIBudget(queryClient, groupData.id),
4044
);
4145

42-
// Load the budget before rendering so the form initializes with it.
4346
if (aibridgeVisible && budgetQuery.isLoading) {
4447
return (
4548
<div className="flex items-center justify-center p-10">
@@ -69,20 +72,28 @@ const GroupSettingsPage: FC = () => {
6972
add_users: [],
7073
remove_users: [],
7174
});
72-
73-
// Save only when the budget changed (0 disables, empty uncaps).
74-
const next = budgetFromInput(monthly_budget_per_member);
75-
if (aibridgeVisible && next !== currentBudgetMicros) {
76-
await saveBudgetMutation.mutateAsync(next);
77-
}
78-
79-
navigate(`/organizations/${organization}/groups/${data.name}`);
8075
} catch (error) {
8176
toast.error(
8277
getErrorMessage(error, `Failed to update group "${groupName}".`),
8378
{ description: getErrorDetail(error) },
8479
);
80+
return;
8581
}
82+
83+
const next = budgetFromInput(monthly_budget_per_member);
84+
if (aibridgeVisible && next !== currentBudgetMicros) {
85+
try {
86+
await saveBudgetMutation.mutateAsync(next);
87+
} catch (error) {
88+
toast.error(
89+
getErrorMessage(error, "Failed to update the AI budget."),
90+
{ description: getErrorDetail(error) },
91+
);
92+
return;
93+
}
94+
}
95+
96+
navigate(`/organizations/${organization}/groups/${data.name}`);
8697
}}
8798
group={groupData}
8899
showAISettings={aibridgeVisible}

site/src/pages/GroupsPage/GroupSettingsPageView.stories.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const WithAIBudget: Story = {
3939
await expect(canvas.getByText("AI budget")).toBeInTheDocument();
4040
const helper = canvas.getByText(/month maximum/i);
4141
await expect(helper).toHaveTextContent(
42-
"$7,000.00/month maximum, based on 7 members.",
42+
"$7,000/month maximum, based on 7 members.",
4343
);
4444
},
4545
};
@@ -68,7 +68,23 @@ export const AIBudgetDisabled: Story = {
6868
// A budget of 0 is valid and reads as disabled spend.
6969
const helper = canvas.getByText(/month maximum/i);
7070
await expect(helper).toHaveTextContent(
71-
"$0.00/month maximum, based on 7 members.",
71+
"$0/month maximum, based on 7 members.",
72+
);
73+
},
74+
};
75+
76+
export const AIBudgetDecimal: Story = {
77+
args: {
78+
showAISettings: true,
79+
group: { ...MockGroup, total_member_count: 1 },
80+
initialBudgetDollars: 99.99,
81+
},
82+
play: async ({ canvasElement }) => {
83+
const canvas = within(canvasElement);
84+
// Cents are kept when the amount is not a whole dollar.
85+
const helper = canvas.getByText(/month maximum/i);
86+
await expect(helper).toHaveTextContent(
87+
"$99.99/month maximum, based on 1 member.",
7288
);
7389
},
7490
};

site/src/pages/GroupsPage/GroupSettingsPageView.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ import { Input } from "#/components/Input/Input";
99
import { Label } from "#/components/Label/Label";
1010
import { Spinner } from "#/components/Spinner/Spinner";
1111
import { isEveryoneGroup } from "#/modules/groups";
12-
import { dollarsToMicros, formatCostMicros } from "#/utils/currency";
1312
import {
1413
getFormHelpers,
1514
nameValidator,
1615
onChangeTrimmed,
1716
} from "#/utils/formUtils";
1817

18+
// Drops the cents when the amount is a whole dollar (the common case).
19+
const usdMaximumFormatter = new Intl.NumberFormat("en-US", {
20+
style: "currency",
21+
currency: "USD",
22+
minimumFractionDigits: 0,
23+
maximumFractionDigits: 2,
24+
});
25+
1926
type FormData = {
2027
name: string;
2128
display_name: string;
@@ -79,8 +86,8 @@ const UpdateGroupForm: FC<UpdateGroupFormProps> = ({
7986
const budgetField = getFieldHelpers("monthly_budget_per_member");
8087
const budgetDollars = form.values.monthly_budget_per_member;
8188
const memberCount = group.total_member_count;
82-
const monthlyMaximum = formatCostMicros(
83-
dollarsToMicros(budgetDollars) * memberCount,
89+
const monthlyMaximum = usdMaximumFormatter.format(
90+
Number(budgetDollars) * memberCount,
8491
);
8592

8693
return (

0 commit comments

Comments
 (0)