From 3b8929be84363a5ad3e26a3ab5e804e825c71f60 Mon Sep 17 00:00:00 2001
From: Susana Cardoso Ferreira
Date: Wed, 29 Jul 2026 18:03:59 +0000
Subject: [PATCH 1/5] fix: hide AI budget override controls without permission
---
.../src/pages/GroupsPage/GroupMembersPage.tsx | 6 +
.../pages/GroupsPage/GroupPage.stories.tsx | 53 +++++++-
.../UserAIBudgetOverrideDialog.stories.tsx | 36 ++++++
.../GroupsPage/UserAIBudgetOverrideDialog.tsx | 118 ++++++++++++++----
4 files changed, 189 insertions(+), 24 deletions(-)
diff --git a/site/src/pages/GroupsPage/GroupMembersPage.tsx b/site/src/pages/GroupsPage/GroupMembersPage.tsx
index ff4ee823f29..953a87c7cd6 100644
--- a/site/src/pages/GroupsPage/GroupMembersPage.tsx
+++ b/site/src/pages/GroupsPage/GroupMembersPage.tsx
@@ -47,6 +47,7 @@ import {
TableHeader,
TableRow,
} from "#/components/Table/Table";
+import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { isEveryoneGroup } from "#/modules/groups";
import { cn } from "#/utils/cn";
@@ -77,7 +78,11 @@ const GroupMembersPage: FC = () => {
const removeMemberMutation = useMutation(
removeMember(queryClient, organization),
);
+ const { permissions: sitePermissions } = useAuthenticated();
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
+ // Setting a user's AI budget override updates both the user and the group
+ // its spend is charged to, so it needs permission on both.
+ const canUpdateBudgetOverride = canUpdateGroup && sitePermissions.updateUsers;
const [budgetUser, setBudgetUser] = useState(null);
const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
@@ -232,6 +237,7 @@ const GroupMembersPage: FC = () => {
user={budgetUser}
currentGroup={groupData}
effectiveGroupId={budgetUser.spend?.effective_group_id}
+ canUpdate={canUpdateBudgetOverride}
/>
)}
diff --git a/site/src/pages/GroupsPage/GroupPage.stories.tsx b/site/src/pages/GroupsPage/GroupPage.stories.tsx
index 79f0f1956b2..7ed3ad8c0ef 100644
--- a/site/src/pages/GroupsPage/GroupPage.stories.tsx
+++ b/site/src/pages/GroupsPage/GroupPage.stories.tsx
@@ -37,15 +37,20 @@ import {
MockUserMember,
MockUserOwner,
} from "#/testHelpers/entities";
-import { withDashboardProvider } from "#/testHelpers/storybook";
+import {
+ withAuthProvider,
+ withDashboardProvider,
+} from "#/testHelpers/storybook";
import GroupMembersPage from "./GroupMembersPage";
import GroupPage from "./GroupPage";
const meta: Meta = {
title: "pages/OrganizationGroupsPage/GroupPage",
component: GroupPage,
- decorators: [withDashboardProvider],
+ decorators: [withDashboardProvider, withAuthProvider],
parameters: {
+ user: MockUserOwner,
+ permissions: { updateUsers: true },
reactRouter: reactRouterParameters({
location: {
pathParams: {
@@ -674,6 +679,50 @@ export const OpenAIBudgetForCurrentGroupMember: Story = {
},
};
+/** Group admins can read a member's budget without the site user permission. */
+export const AIBudgetReadOnlyWithoutUserPermission: Story = {
+ parameters: {
+ features: ["aibridge"],
+ permissions: { updateUsers: false },
+ queries: [
+ groupQuery(MockGroupWithoutMembers),
+ groupMembersQuery({
+ users: [MockUserOwner],
+ count: 1,
+ }),
+ membersSpendQuery([{ ...mockSpend, user_id: MockUserOwner.id }]),
+ permissionsQuery({ canUpdateGroup: true }),
+ { key: meAISpendKey, data: mockUserAISpend },
+ { key: getUserAIBudgetOverrideQueryKey(MockUserOwner.id), data: null },
+ {
+ key: getGroupsForUserQueryKey(
+ MockUserOwner.id,
+ MockGroupWithoutMembers.organization_id,
+ ),
+ data: [MockGroup2],
+ },
+ { key: groupAIBudget(MockGroupWithoutMembers.id).queryKey, data: null },
+ ],
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const body = within(document.body);
+
+ await userEvent.click(
+ canvas.getAllByRole("button", { name: "Open menu" })[0],
+ );
+ await userEvent.click(
+ await body.findByRole("menuitem", { name: "Manage AI budget" }),
+ );
+ await expect(
+ await body.findByText(
+ /To update this limit, contact your deployment administrator\./,
+ ),
+ ).toBeInTheDocument();
+ await expect(body.queryByRole("checkbox")).not.toBeInTheDocument();
+ },
+};
+
/** Unresolvable via getGroupById, standing in for another org's group. */
const unresolvedGroupId = "external-org-group";
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
index 3953bf5550d..bcffb96ef61 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
@@ -39,6 +39,7 @@ const meta: Meta = {
onOpenChange: () => undefined,
user: MockUserMember,
currentGroup: MockGroup,
+ canUpdate: true,
},
};
@@ -88,6 +89,41 @@ export const WithoutOverride: Story = {
},
};
+/** An existing override still hides the controls, so it can't be removed. */
+export const ReadOnlyWithoutPermission: Story = {
+ args: { canUpdate: false },
+ parameters: {
+ queries: [
+ {
+ key: getUserAIBudgetOverrideQueryKey(MockUserMember.id),
+ data: mockOverride,
+ },
+ ...groupQueries,
+ ],
+ },
+ play: async () => {
+ const body = within(document.body);
+ await expect(await body.findByText("$12,000 USD")).toBeInTheDocument();
+ await expect(
+ body.getByText(
+ /To update this limit, contact your deployment administrator\./,
+ ),
+ ).toBeInTheDocument();
+ await expect(body.queryByRole("checkbox")).not.toBeInTheDocument();
+ await expect(
+ body.queryByLabelText("Custom monthly budget"),
+ ).not.toBeInTheDocument();
+ await expect(
+ body.queryByRole("button", { name: "Budget assigned to" }),
+ ).not.toBeInTheDocument();
+ await expect(
+ body.queryByRole("button", { name: "Update" }),
+ ).not.toBeInTheDocument();
+ // Nothing to submit or cancel, so the dialog has no action buttons.
+ await expect(body.queryAllByRole("button")).toHaveLength(0);
+ },
+};
+
export const Uncapped: Story = {
parameters: {
queries: [
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
index 9b7a292e685..3b139e62da6 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
@@ -66,11 +66,20 @@ interface UserAIBudgetOverrideDialogProps {
user: ReducedUser;
currentGroup: Group;
effectiveGroupId?: string | null;
+ // When false, the budget is shown without the controls to change it.
+ canUpdate: boolean;
}
export const UserAIBudgetOverrideDialog: FC<
UserAIBudgetOverrideDialogProps
-> = ({ open, onOpenChange, user, currentGroup, effectiveGroupId }) => {
+> = ({
+ open,
+ onOpenChange,
+ user,
+ currentGroup,
+ effectiveGroupId,
+ canUpdate,
+}) => {
const queryClient = useQueryClient();
const budgetOverrideQuery = useQuery({
...userAIBudgetOverride(user.id),
@@ -135,7 +144,7 @@ export const UserAIBudgetOverrideDialog: FC<
Loading AI budget...
- ) : (
+ ) : canUpdate ? (
onOpenChange(false)}
/>
+ ) : (
+
)}
);
};
+interface BudgetSummaryProps {
+ user: ReducedUser;
+ currentGroup: Group;
+ override: UserAIBudgetOverride | null;
+ // The group the override charges spend to, when it can be resolved.
+ overrideGroup: Group | undefined;
+ groupBudget: GroupAIBudget | null;
+}
+
+/** The member's effective limit as a sentence, to place inside a paragraph. */
+const BudgetSummary: FC = ({
+ user,
+ currentGroup,
+ override,
+ overrideGroup,
+ groupBudget,
+}) =>
+ override ? (
+ <>
+ {user.username}'s custom monthly limit is{" "}
+ {formatUSD(override.spend_limit_micros)}, charged to{" "}
+
+ {overrideGroup ? groupDisplayName(overrideGroup) : "their group"}
+ {" "}
+ group.
+ >
+ ) : (
+ <>
+ {user.username}'s monthly limit is{" "}
+
+ {groupBudget ? formatUSD(groupBudget.spend_limit_micros) : "uncapped"}
+
+ , charged to {groupDisplayName(currentGroup)} group.
+ >
+ );
+
+interface ReadOnlyBudgetProps {
+ user: ReducedUser;
+ currentGroup: Group;
+ override: UserAIBudgetOverride | null;
+ groupBudget: GroupAIBudget | null;
+ userGroups: readonly Group[];
+}
+
+/**
+ * The budget without any editing controls. Setting an override requires
+ * updating both the user and the group it charges, so group admins can read a
+ * member's budget without being able to change it.
+ */
+const ReadOnlyBudget: FC = ({
+ user,
+ currentGroup,
+ override,
+ groupBudget,
+ userGroups,
+}) => (
+
+ group.id === override?.group_id,
+ )}
+ groupBudget={groupBudget}
+ />{" "}
+ To update this limit, contact your deployment administrator.
+
+);
+
interface OverrideFormProps {
user: ReducedUser;
currentGroup: Group;
@@ -266,26 +353,13 @@ const OverrideForm: FC = ({
return (
@@ -503,4 +486,15 @@ const Bold: FC<{ children: ReactNode }> = ({ children }) => (
const groupDisplayName = (group: Group): string =>
group.display_name || group.name;
+/**
+ * Finds a group among the ones this dialog knows about. Groups in another
+ * organization aren't fetchable here, so they resolve to undefined.
+ */
+const findGroup = (
+ currentGroup: Group,
+ userGroups: readonly Group[],
+ groupID: string,
+): Group | undefined =>
+ [currentGroup, ...userGroups].find((group) => group.id === groupID);
+
const formatUSD = (micros: number): string => `${formatBudgetUSD(micros)} USD`;
From 63e9058a232cc0384a1765c34489b51bfd7dcc7a Mon Sep 17 00:00:00 2001
From: Susana Cardoso Ferreira
Date: Thu, 30 Jul 2026 11:17:46 +0000
Subject: [PATCH 3/5] fix: name the correct effective group in the AI budget
dialog
---
.../UserAIBudgetOverrideDialog.stories.tsx | 37 +++++++++++-
.../GroupsPage/UserAIBudgetOverrideDialog.tsx | 56 ++++++++++++-------
2 files changed, 73 insertions(+), 20 deletions(-)
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
index a8efd3a8076..9ab17ad9539 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
@@ -5,7 +5,12 @@ import { groupAIBudget, groupsForUser } from "#/api/queries/groups";
import { getUserAIBudgetOverrideQueryKey } from "#/api/queries/users";
import type { GroupAIBudget, UserAIBudgetOverride } from "#/api/typesGenerated";
import { maxAIBudgetDollars } from "#/modules/groups";
-import { MockGroup, MockGroup2, MockUserMember } from "#/testHelpers/entities";
+import {
+ MockEveryoneGroup,
+ MockGroup,
+ MockGroup2,
+ MockUserMember,
+} from "#/testHelpers/entities";
import { UserAIBudgetOverrideDialog } from "./UserAIBudgetOverrideDialog";
const mockOverride: UserAIBudgetOverride = {
@@ -122,6 +127,36 @@ export const ReadOnlyWithoutPermission: Story = {
},
};
+/**
+ * No group the member belongs to sets a budget, so the backend attributes their
+ * spend to the Everyone group rather than the group being viewed.
+ */
+export const UnbudgetedGroupFallsBackToEveryone: Story = {
+ args: { effectiveGroupId: MockEveryoneGroup.id },
+ parameters: {
+ queries: [
+ { key: getUserAIBudgetOverrideQueryKey(MockUserMember.id), data: null },
+ {
+ key: groupsForUser(MockUserMember.id, MockGroup.organization_id)
+ .queryKey,
+ data: [MockGroup, MockEveryoneGroup],
+ },
+ { key: groupAIBudget(MockGroup.id).queryKey, data: null },
+ ],
+ },
+ play: async () => {
+ const body = within(document.body);
+ const summary = await body.findByText(/charged to/);
+ await expect(summary).toHaveTextContent(
+ "uncapped, charged to Everyone group.",
+ );
+ // The group being viewed must not be named as the charging group.
+ await expect(summary).not.toHaveTextContent(
+ MockGroup.display_name || MockGroup.name,
+ );
+ },
+};
+
/** The assigned group is in another organization, so it can't be named. */
export const OverrideWithUnresolvableGroup: Story = {
parameters: {
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
index 9e3b32ba4ab..320a1b8ea19 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
@@ -115,6 +115,7 @@ export const UserAIBudgetOverrideDialog: FC<
override: budgetOverrideQuery.data ?? null,
groupBudget: groupBudgetQuery.data ?? null,
userGroups: userGroupsQuery.data ?? [],
+ effectiveGroupId,
};
let body: ReactNode;
@@ -131,9 +132,6 @@ export const UserAIBudgetOverrideDialog: FC<
body = (
= ({
override,
groupBudget,
userGroups,
+ effectiveGroupId,
}) => {
if (!override) {
+ // The governing group is not necessarily the group being viewed.
+ const effectiveGroup =
+ effectiveGroupId === undefined
+ ? currentGroup
+ : findGroup(currentGroup, userGroups, effectiveGroupId);
return (
<>
{user.username}'s monthly limit is{" "}
{groupBudget ? formatUSD(groupBudget.spend_limit_micros) : "uncapped"}
- , charged to {groupDisplayName(currentGroup)} group.
+ ,
>
);
}
- const overrideGroup = findGroup(currentGroup, userGroups, override.group_id);
return (
<>
{user.username}'s custom monthly limit is{" "}
- {formatUSD(override.spend_limit_micros)}, charged to{" "}
- {overrideGroup ? (
- <>
- {groupDisplayName(overrideGroup)} group.
- >
- ) : (
- // The group is unresolvable here, so it can't be named.
- their group.
- )}
+ {formatUSD(override.spend_limit_micros)},{" "}
+
>
);
};
+const ChargedTo: FC<{ group: Group | undefined }> = ({ group }) =>
+ group ? (
+ <>
+ charged to {groupDisplayName(group)} group.
+ >
+ ) : (
+ // The group is unresolvable here, so it can't be named.
+ <>
+ charged to their group.
+ >
+ );
+
/**
* The budget without any editing controls. Setting an override requires
* updating both the user and the group it charges, so group admins can read a
@@ -235,8 +249,6 @@ const ReadOnlyBudget: FC = (props) => (
);
interface OverrideFormProps extends BudgetProps {
- // Group marked "(default)" in the picker; null marks none.
- defaultGroupId: string | null;
isSubmitting: boolean;
onSave: (request: UpsertUserAIBudgetOverrideRequest) => Promise;
onRemove: () => Promise;
@@ -247,15 +259,18 @@ interface OverrideFormProps extends BudgetProps {
const OverrideForm: FC = ({
user,
currentGroup,
- defaultGroupId,
override,
groupBudget,
userGroups,
+ effectiveGroupId,
isSubmitting,
onSave,
onRemove,
onClose,
}) => {
+ // Group marked "(default)" in the picker; null marks none.
+ const defaultGroupId =
+ effectiveGroupId === undefined ? currentGroup.id : effectiveGroupId;
const budgetId = useId();
const groupId = useId();
const overrideId = useId();
@@ -342,6 +357,7 @@ const OverrideForm: FC = ({
override={override}
groupBudget={groupBudget}
userGroups={userGroups}
+ effectiveGroupId={effectiveGroupId}
/>
@@ -493,8 +509,10 @@ const groupDisplayName = (group: Group): string =>
const findGroup = (
currentGroup: Group,
userGroups: readonly Group[],
- groupID: string,
+ groupID: string | null,
): Group | undefined =>
- [currentGroup, ...userGroups].find((group) => group.id === groupID);
+ groupID === null
+ ? undefined
+ : [currentGroup, ...userGroups].find((group) => group.id === groupID);
const formatUSD = (micros: number): string => `${formatBudgetUSD(micros)} USD`;
From 5b7428d0551c2598a98c4e79a7446da9a2043541 Mon Sep 17 00:00:00 2001
From: Susana Cardoso Ferreira
Date: Thu, 30 Jul 2026 11:38:28 +0000
Subject: [PATCH 4/5] revert: name the viewed group in the AI budget dialog
---
.../UserAIBudgetOverrideDialog.stories.tsx | 37 +-----------
.../GroupsPage/UserAIBudgetOverrideDialog.tsx | 56 +++++++------------
2 files changed, 20 insertions(+), 73 deletions(-)
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
index 9ab17ad9539..a8efd3a8076 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
@@ -5,12 +5,7 @@ import { groupAIBudget, groupsForUser } from "#/api/queries/groups";
import { getUserAIBudgetOverrideQueryKey } from "#/api/queries/users";
import type { GroupAIBudget, UserAIBudgetOverride } from "#/api/typesGenerated";
import { maxAIBudgetDollars } from "#/modules/groups";
-import {
- MockEveryoneGroup,
- MockGroup,
- MockGroup2,
- MockUserMember,
-} from "#/testHelpers/entities";
+import { MockGroup, MockGroup2, MockUserMember } from "#/testHelpers/entities";
import { UserAIBudgetOverrideDialog } from "./UserAIBudgetOverrideDialog";
const mockOverride: UserAIBudgetOverride = {
@@ -127,36 +122,6 @@ export const ReadOnlyWithoutPermission: Story = {
},
};
-/**
- * No group the member belongs to sets a budget, so the backend attributes their
- * spend to the Everyone group rather than the group being viewed.
- */
-export const UnbudgetedGroupFallsBackToEveryone: Story = {
- args: { effectiveGroupId: MockEveryoneGroup.id },
- parameters: {
- queries: [
- { key: getUserAIBudgetOverrideQueryKey(MockUserMember.id), data: null },
- {
- key: groupsForUser(MockUserMember.id, MockGroup.organization_id)
- .queryKey,
- data: [MockGroup, MockEveryoneGroup],
- },
- { key: groupAIBudget(MockGroup.id).queryKey, data: null },
- ],
- },
- play: async () => {
- const body = within(document.body);
- const summary = await body.findByText(/charged to/);
- await expect(summary).toHaveTextContent(
- "uncapped, charged to Everyone group.",
- );
- // The group being viewed must not be named as the charging group.
- await expect(summary).not.toHaveTextContent(
- MockGroup.display_name || MockGroup.name,
- );
- },
-};
-
/** The assigned group is in another organization, so it can't be named. */
export const OverrideWithUnresolvableGroup: Story = {
parameters: {
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
index 320a1b8ea19..9e3b32ba4ab 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.tsx
@@ -115,7 +115,6 @@ export const UserAIBudgetOverrideDialog: FC<
override: budgetOverrideQuery.data ?? null,
groupBudget: groupBudgetQuery.data ?? null,
userGroups: userGroupsQuery.data ?? [],
- effectiveGroupId,
};
let body: ReactNode;
@@ -132,6 +131,9 @@ export const UserAIBudgetOverrideDialog: FC<
body = (
= ({
override,
groupBudget,
userGroups,
- effectiveGroupId,
}) => {
if (!override) {
- // The governing group is not necessarily the group being viewed.
- const effectiveGroup =
- effectiveGroupId === undefined
- ? currentGroup
- : findGroup(currentGroup, userGroups, effectiveGroupId);
return (
<>
{user.username}'s monthly limit is{" "}
{groupBudget ? formatUSD(groupBudget.spend_limit_micros) : "uncapped"}
- ,
+ , charged to {groupDisplayName(currentGroup)} group.
>
);
}
+ const overrideGroup = findGroup(currentGroup, userGroups, override.group_id);
return (
<>
{user.username}'s custom monthly limit is{" "}
- {formatUSD(override.spend_limit_micros)},{" "}
-
+ {formatUSD(override.spend_limit_micros)}, charged to{" "}
+ {overrideGroup ? (
+ <>
+ {groupDisplayName(overrideGroup)} group.
+ >
+ ) : (
+ // The group is unresolvable here, so it can't be named.
+ their group.
+ )}
>
);
};
-const ChargedTo: FC<{ group: Group | undefined }> = ({ group }) =>
- group ? (
- <>
- charged to {groupDisplayName(group)} group.
- >
- ) : (
- // The group is unresolvable here, so it can't be named.
- <>
- charged to their group.
- >
- );
-
/**
* The budget without any editing controls. Setting an override requires
* updating both the user and the group it charges, so group admins can read a
@@ -249,6 +235,8 @@ const ReadOnlyBudget: FC = (props) => (
);
interface OverrideFormProps extends BudgetProps {
+ // Group marked "(default)" in the picker; null marks none.
+ defaultGroupId: string | null;
isSubmitting: boolean;
onSave: (request: UpsertUserAIBudgetOverrideRequest) => Promise;
onRemove: () => Promise;
@@ -259,18 +247,15 @@ interface OverrideFormProps extends BudgetProps {
const OverrideForm: FC = ({
user,
currentGroup,
+ defaultGroupId,
override,
groupBudget,
userGroups,
- effectiveGroupId,
isSubmitting,
onSave,
onRemove,
onClose,
}) => {
- // Group marked "(default)" in the picker; null marks none.
- const defaultGroupId =
- effectiveGroupId === undefined ? currentGroup.id : effectiveGroupId;
const budgetId = useId();
const groupId = useId();
const overrideId = useId();
@@ -357,7 +342,6 @@ const OverrideForm: FC = ({
override={override}
groupBudget={groupBudget}
userGroups={userGroups}
- effectiveGroupId={effectiveGroupId}
/>
@@ -509,10 +493,8 @@ const groupDisplayName = (group: Group): string =>
const findGroup = (
currentGroup: Group,
userGroups: readonly Group[],
- groupID: string | null,
+ groupID: string,
): Group | undefined =>
- groupID === null
- ? undefined
- : [currentGroup, ...userGroups].find((group) => group.id === groupID);
+ [currentGroup, ...userGroups].find((group) => group.id === groupID);
const formatUSD = (micros: number): string => `${formatBudgetUSD(micros)} USD`;
From 51fb7d1a6eec282af645b69627b011d2c7d087d1 Mon Sep 17 00:00:00 2001
From: Susana Cardoso Ferreira
Date: Thu, 30 Jul 2026 12:19:27 +0000
Subject: [PATCH 5/5] chore: minor improvements
---
.../UserAIBudgetOverrideDialog.stories.tsx | 26 +++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
index a8efd3a8076..4aa71c5fbf2 100644
--- a/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
+++ b/site/src/pages/GroupsPage/UserAIBudgetOverrideDialog.stories.tsx
@@ -89,8 +89,8 @@ export const WithoutOverride: Story = {
},
};
-/** An existing override still hides the controls, so it can't be removed. */
-export const ReadOnlyWithoutPermission: Story = {
+/** Without permission, an existing override can be read but not removed. */
+export const ReadOnlyWithOverride: Story = {
args: { canUpdate: false },
parameters: {
queries: [
@@ -122,6 +122,28 @@ export const ReadOnlyWithoutPermission: Story = {
},
};
+/** Without permission or an override, the group's budget is shown as-is. */
+export const ReadOnlyWithoutOverride: Story = {
+ args: { canUpdate: false },
+ parameters: {
+ queries: [
+ { key: getUserAIBudgetOverrideQueryKey(MockUserMember.id), data: null },
+ ...groupQueries,
+ ],
+ },
+ play: async () => {
+ const body = within(document.body);
+ await expect(await body.findByText("$5,000 USD")).toBeInTheDocument();
+ await expect(
+ body.getByText(/To update this limit, contact a Coder administrator\./),
+ ).toBeInTheDocument();
+ await expect(body.queryByRole("checkbox")).not.toBeInTheDocument();
+ await expect(
+ body.queryByRole("button", { name: "Update" }),
+ ).not.toBeInTheDocument();
+ },
+};
+
/** The assigned group is in another organization, so it can't be named. */
export const OverrideWithUnresolvableGroup: Story = {
parameters: {