From 68687d99aa4b5113a979d777bdaeab6b3d2dcb27 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 25 Aug 2026 20:11:59 +0000 Subject: [PATCH 1/2] fix(site): distinguish cross-org budgets --- .../GroupMemberBudgetCells.stories.tsx | 15 ++----- .../GroupsPage/GroupMemberBudgetCells.test.ts | 6 ++- .../GroupsPage/GroupMemberBudgetCells.tsx | 44 +++++++++++-------- .../src/pages/GroupsPage/GroupMembersPage.tsx | 6 +-- .../pages/GroupsPage/GroupPage.stories.tsx | 10 ++--- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx b/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx index 2ca857b8e779e..deec091a5907c 100644 --- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx +++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx @@ -287,29 +287,20 @@ export const ResolvingGroupName: Story = { }, }; -/** An effective group that can't be resolved, standing in for another org's. */ -export const NotAttributedUnknownGroup: Story = { +export const NotAttributedOtherOrganization: Story = { args: { spend: { ...mockSpend, group_spend_micros: 456_000_000, - effective_group_id: "external-group", + effective_group_id: null, + group_budget: null, }, }, - parameters: { - queries: [ - { - key: getGroupByIdQueryKey("external-group", { exclude_members: true }), - data: null, - }, - ], - }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); const cell = await canvas.findByTestId(testId); await expect(cell).toHaveTextContent("\u2014"); await expect(cell).not.toHaveTextContent("$456"); - // The group cell shows an em-dash + info instead of naming the group. const groupCell = canvas.getAllByRole("cell")[1]; await expect(groupCell).toHaveTextContent("\u2014"); await userEvent.click( diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts b/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts index 2a93a22928407..c7862e0c2826a 100644 --- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts +++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts @@ -16,8 +16,10 @@ describe("effectiveBudgetGroup", () => { expect(effectiveBudgetGroup(undefined, group)).toEqual({ kind: "none" }); }); - it("is other without a governing group (budget in another org)", () => { - expect(effectiveBudgetGroup(mockSpend, group)).toEqual({ kind: "other" }); + it("is other organization when the effective group is masked", () => { + expect(effectiveBudgetGroup(mockSpend, group)).toEqual({ + kind: "otherOrganization", + }); }); it("is everyone for the org-wide Everyone group", () => { diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx b/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx index a449b966128ee..5a4f9772b664e 100644 --- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx +++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx @@ -64,30 +64,32 @@ export const GroupMemberBudgetCells: FC<{ case "this": budgetGroup = {badgeName(groupName)}; break; - case "other": { + case "other": // Wait for the name to resolve rather than flashing the fallback. if (isResolvingGroupName) { budgetGroup = ; - } else if (effectiveGroupName) { - budgetGroup = {badgeName(effectiveGroupName)}; } else { - // The group can't be resolved (another org), so it can't be named. - budgetGroup = ( - + budgetGroup = effectiveGroupName ? ( + {badgeName(effectiveGroupName)} + ) : ( + EM_DASH ); } break; - } + case "otherOrganization": + budgetGroup = ( + + ); + break; } let budget: ReactNode = EM_DASH; - if (spend && fromOtherGroup) { + if (spend && effective.kind === "otherOrganization") { + budget = ; + } else if (spend && effective.kind === "other") { if (isResolvingGroupName) { budget = ; - } else if (!effectiveGroupName) { - // The spend hides entirely when the governing group can't be resolved. - budget = ; - } else { + } else if (effectiveGroupName) { budget = (
@@ -171,13 +173,17 @@ type EffectiveBudgetGroup = | { kind: "none" } | { kind: "everyone" } | { kind: "this" } - | { kind: "other" }; + | { kind: "other" } + | { kind: "otherOrganization" }; /** - * Resolves which group governs a member's AI budget. "none" means no budget - * data loaded; "everyone" is the org-wide fallback when no named group sets a - * budget. A null effective group means the budget resolves to a group in - * another organization, so it can't be shown here. + * Resolves which group governs a member's AI budget: + * + * - "none": spend data is not loaded. + * - "everyone": the Everyone group governs the budget. + * - "this": the viewed group governs the budget. + * - "other": another group in this organization governs the budget. + * - "otherOrganization": a group in another organization governs the budget. */ export function effectiveBudgetGroup( spend: GroupMemberAISpend | undefined, @@ -185,7 +191,9 @@ export function effectiveBudgetGroup( ): EffectiveBudgetGroup { const groupId = spend?.effective_group_id ?? null; if (groupId === null) { - return spend === undefined ? { kind: "none" } : { kind: "other" }; + return spend === undefined + ? { kind: "none" } + : { kind: "otherOrganization" }; } // Everyone shares the org's id; checked first so it wins when the viewed // group is Everyone itself. diff --git a/site/src/pages/GroupsPage/GroupMembersPage.tsx b/site/src/pages/GroupsPage/GroupMembersPage.tsx index 48727a29cabb2..f1780c9a31485 100644 --- a/site/src/pages/GroupsPage/GroupMembersPage.tsx +++ b/site/src/pages/GroupsPage/GroupMembersPage.tsx @@ -238,8 +238,8 @@ const GroupMemberRow: FC = ({ onManageAIBudget, onRemove, }) => { - const budgetFromOtherGroup = - effectiveBudgetGroup(member.spend, group).kind === "other"; + const budgetFromOtherOrganization = + effectiveBudgetGroup(member.spend, group).kind === "otherOrganization"; return ( @@ -288,7 +288,7 @@ const GroupMemberRow: FC = ({ {showAIBudget && ( Manage AI budget diff --git a/site/src/pages/GroupsPage/GroupPage.stories.tsx b/site/src/pages/GroupsPage/GroupPage.stories.tsx index 023d03f27773b..55ebed0f7cc22 100644 --- a/site/src/pages/GroupsPage/GroupPage.stories.tsx +++ b/site/src/pages/GroupsPage/GroupPage.stories.tsx @@ -439,7 +439,7 @@ export const WithoutMemberAIBudgetColumn: Story = { }, }; -export const AIBudgetActionDisabledForOtherGroup: Story = { +export const AIBudgetActionEnabledForOtherGroup: Story = { parameters: { features: ["aibridge"], queries: [ @@ -489,7 +489,7 @@ export const AIBudgetActionDisabledForOtherGroup: Story = { const menuItem = await body.findByRole("menuitem", { name: "Manage AI budget", }); - await expect(menuItem).toHaveAttribute("aria-disabled", "true"); + await expect(menuItem).not.toHaveAttribute("aria-disabled", "true"); }, }; @@ -929,14 +929,14 @@ export const AIBudgetShowcase: Story = { await expect(manageItem).not.toHaveAttribute("aria-disabled", "true"); await userEvent.keyboard("{Escape}"); - // Another named group does disable it. + // Another group in the same organization keeps the action enabled. const otherGroupMenu = await canvas.findAllByRole("button", { name: "Open menu", }); await userEvent.click(otherGroupMenu[7]); - const disabledItem = await body.findByRole("menuitem", { + const otherGroupItem = await body.findByRole("menuitem", { name: "Manage AI budget", }); - await expect(disabledItem).toHaveAttribute("aria-disabled", "true"); + await expect(otherGroupItem).not.toHaveAttribute("aria-disabled", "true"); }, }; From fd63c62caf022ea7a24db7603ce93ef0a5b8c20b Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 26 Aug 2026 11:53:31 +0000 Subject: [PATCH 2/2] fix(site): clarify effective budget group names --- .../GroupsPage/GroupMemberBudgetCells.test.ts | 4 ++-- .../GroupsPage/GroupMemberBudgetCells.tsx | 24 +++++++++---------- .../src/pages/GroupsPage/GroupMembersPage.tsx | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts b/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts index c7862e0c2826a..d48f57b120fc3 100644 --- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts +++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.test.ts @@ -18,7 +18,7 @@ describe("effectiveBudgetGroup", () => { it("is other organization when the effective group is masked", () => { expect(effectiveBudgetGroup(mockSpend, group)).toEqual({ - kind: "otherOrganization", + kind: "otherOrg", }); }); @@ -55,6 +55,6 @@ describe("effectiveBudgetGroup", () => { { ...mockSpend, effective_group_id: "group-2" }, group, ), - ).toEqual({ kind: "other" }); + ).toEqual({ kind: "otherGroup" }); }); }); diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx b/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx index 5a4f9772b664e..d7c60295a5c8e 100644 --- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx +++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx @@ -26,7 +26,7 @@ export const GroupMemberBudgetCells: FC<{ spend: GroupMemberAISpend | undefined; }> = ({ group, userID, spend }) => { const effective = effectiveBudgetGroup(spend, group); - const fromOtherGroup = effective.kind === "other"; + const fromOtherGroup = effective.kind === "otherGroup"; // A null effective_group_id is a group in another org that can't be // fetched, so only resolve the name when an ID exists. @@ -64,7 +64,7 @@ export const GroupMemberBudgetCells: FC<{ case "this": budgetGroup = {badgeName(groupName)}; break; - case "other": + case "otherGroup": // Wait for the name to resolve rather than flashing the fallback. if (isResolvingGroupName) { budgetGroup = ; @@ -76,7 +76,7 @@ export const GroupMemberBudgetCells: FC<{ ); } break; - case "otherOrganization": + case "otherOrg": budgetGroup = ( ); @@ -84,9 +84,9 @@ export const GroupMemberBudgetCells: FC<{ } let budget: ReactNode = EM_DASH; - if (spend && effective.kind === "otherOrganization") { + if (spend && effective.kind === "otherOrg") { budget = ; - } else if (spend && effective.kind === "other") { + } else if (spend && effective.kind === "otherGroup") { if (isResolvingGroupName) { budget = ; } else if (effectiveGroupName) { @@ -173,8 +173,8 @@ type EffectiveBudgetGroup = | { kind: "none" } | { kind: "everyone" } | { kind: "this" } - | { kind: "other" } - | { kind: "otherOrganization" }; + | { kind: "otherGroup" } + | { kind: "otherOrg" }; /** * Resolves which group governs a member's AI budget: @@ -182,8 +182,8 @@ type EffectiveBudgetGroup = * - "none": spend data is not loaded. * - "everyone": the Everyone group governs the budget. * - "this": the viewed group governs the budget. - * - "other": another group in this organization governs the budget. - * - "otherOrganization": a group in another organization governs the budget. + * - "otherGroup": another group in this organization governs the budget. + * - "otherOrg": a group in another organization governs the budget. */ export function effectiveBudgetGroup( spend: GroupMemberAISpend | undefined, @@ -191,9 +191,7 @@ export function effectiveBudgetGroup( ): EffectiveBudgetGroup { const groupId = spend?.effective_group_id ?? null; if (groupId === null) { - return spend === undefined - ? { kind: "none" } - : { kind: "otherOrganization" }; + return spend === undefined ? { kind: "none" } : { kind: "otherOrg" }; } // Everyone shares the org's id; checked first so it wins when the viewed // group is Everyone itself. @@ -203,7 +201,7 @@ export function effectiveBudgetGroup( if (groupId === group.id) { return { kind: "this" }; } - return { kind: "other" }; + return { kind: "otherGroup" }; } const LabelWithInfo: FC<{ label: ReactNode; message: ReactNode }> = ({ diff --git a/site/src/pages/GroupsPage/GroupMembersPage.tsx b/site/src/pages/GroupsPage/GroupMembersPage.tsx index f1780c9a31485..0620f914fe2e3 100644 --- a/site/src/pages/GroupsPage/GroupMembersPage.tsx +++ b/site/src/pages/GroupsPage/GroupMembersPage.tsx @@ -239,7 +239,7 @@ const GroupMemberRow: FC = ({ onRemove, }) => { const budgetFromOtherOrganization = - effectiveBudgetGroup(member.spend, group).kind === "otherOrganization"; + effectiveBudgetGroup(member.spend, group).kind === "otherOrg"; return (