feat(site/src): show spend for unlimited and zero AI budgets (#27458)

The group members table hid a member's spend behind a bare "Unlimited"
label when their budget resolves to a group with no limit, and rendered
a $0 budget as a special "None" label. Spend now always shows: as
"$X / Unlimited USD" for unlimited budgets, and as a normal limit row
("Group limit $0", exceeded color once spend is above zero) for $0
budgets.

The Everyone badge drops "(not allocated)" when the Everyone group's
own budget or a user override applies, showing "Everyone" or
"Everyone (individual)" instead. The not-attributed tooltip now states
that the amount is the user's spend in the viewed group and that their
AI budget is managed by another group, replacing the misleading
"Not attributed to this group" wording.
This commit is contained in:
Ehab Younes
2026-07-23 17:38:05 +00:00
committed by GitHub
parent 3c7a1d33e3
commit 10624122c5
4 changed files with 114 additions and 33 deletions
@@ -50,3 +50,11 @@ export const ZeroBudget: Story = {
await expect(canvasElement).toHaveTextContent("$5 / $0 USD");
},
};
// Zero budget with no spend: shown as-is in the normal color.
export const ZeroBudgetNoSpend: Story = {
args: { currentSpend: 0, spendLimit: 0 },
play: async ({ canvasElement }) => {
await expect(canvasElement).toHaveTextContent("$0 / $0 USD");
},
};
@@ -10,7 +10,11 @@ import {
TableHeader,
TableRow,
} from "#/components/Table/Table";
import { MockGroup2, MockGroupWithoutMembers } from "#/testHelpers/entities";
import {
MockEveryoneGroup,
MockGroup2,
MockGroupWithoutMembers,
} from "#/testHelpers/entities";
import { GroupMemberBudgetCells } from "./GroupMemberBudgetCells";
const group = MockGroupWithoutMembers;
@@ -74,6 +78,7 @@ export const Unlimited: Story = {
args: {
spend: {
...mockSpend,
group_spend_micros: 1_250_000_000,
group_budget: null,
effective_group_id: group.organization_id,
},
@@ -81,7 +86,7 @@ export const Unlimited: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByTestId(testId)).toHaveTextContent(
"Unlimited",
"$1,250 / Unlimited USD",
);
await expect(
canvas.getByText("Everyone (not allocated)"),
@@ -91,38 +96,100 @@ export const Unlimited: Story = {
},
};
// Unlimited where the viewed group itself is the effective group.
export const UnlimitedThisGroup: Story = {
// Only the Everyone group can be an effective group without a budget.
export const UnlimitedEveryoneGroup: Story = {
args: {
group: MockEveryoneGroup,
spend: {
...mockSpend,
group_budget: null,
effective_group_id: MockEveryoneGroup.id,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByTestId(testId)).toHaveTextContent(
"Unlimited",
"$0 / Unlimited USD",
);
await expect(canvas.getByText("Front-End")).toBeInTheDocument();
await expect(
canvas.getByText("Everyone (not allocated)"),
).toBeInTheDocument();
},
};
export const None: Story = {
// The Everyone group's own budget governs; the badge drops "(not allocated)".
export const EveryoneGroupWithBudget: Story = {
args: {
group: MockEveryoneGroup,
spend: {
...mockSpend,
group_budget: { spend_limit_micros: 0, limit_source: "group" },
effective_group_id: group.organization_id,
group_spend_micros: 1_250_000_000,
effective_group_id: MockEveryoneGroup.id,
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByTestId(testId)).toHaveTextContent("None");
const body = await openInfo(canvasElement);
await expect(
await body.findByText(/no AI spending allowance/),
).toBeInTheDocument();
const cell = await canvas.findByTestId(testId);
await expect(cell).toHaveTextContent("$1,250 USD");
await expect(cell).toHaveTextContent("Group limit $7,000");
await expect(canvas.getByText("Everyone")).toBeInTheDocument();
await expect(canvas.queryByText(/not allocated/)).not.toBeInTheDocument();
},
};
// A user override resolving to the Everyone group shows as individual.
export const EveryoneGroupIndividual: Story = {
args: {
group: MockEveryoneGroup,
spend: {
...mockSpend,
group_spend_micros: 1_250_000_000,
effective_group_id: MockEveryoneGroup.id,
group_budget: {
spend_limit_micros: 9_000_000_000,
limit_source: "user_override",
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const cell = await canvas.findByTestId(testId);
await expect(cell).toHaveTextContent("Custom limit $9,000");
await expect(canvas.getByText("Everyone (individual)")).toBeInTheDocument();
},
};
// A $0 budget renders like any other limit; no spend keeps the normal color.
export const ZeroBudget: Story = {
args: {
spend: {
...mockSpend,
group_budget: { spend_limit_micros: 0, limit_source: "group" },
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const cell = await canvas.findByTestId(testId);
await expect(cell).toHaveTextContent("$0 USD");
await expect(cell).toHaveTextContent("Group limit $0");
await expect(canvas.getByText("Front-End")).toBeInTheDocument();
},
};
// Visual variant of ZeroBudget: spend over a $0 budget takes the exceeded color.
export const ZeroBudgetExceeded: Story = {
args: {
spend: {
...mockSpend,
group_spend_micros: 100_000_000,
group_budget: { spend_limit_micros: 0, limit_source: "group" },
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const cell = await canvas.findByTestId(testId);
await expect(cell).toHaveTextContent("$100 USD");
await expect(cell).toHaveTextContent("Group limit $0");
},
};
@@ -188,13 +255,13 @@ export const NotAttributed: Story = {
const canvas = within(canvasElement);
const cell = await canvas.findByTestId(testId);
await expect(cell).toHaveTextContent("$456 USD");
await expect(cell).toHaveTextContent("Not attributed to this group");
await expect(cell).toHaveTextContent("Budget managed by another group");
await expect(await canvas.findByText("developer")).toBeInTheDocument();
const body = await openInfo(canvasElement);
await expect(
await body.findByText(/None of this user's spend counts against/),
await body.findByText(/this user's spend in the/),
).toHaveTextContent(
"None of this user's spend counts against the Front-End group. It is managed by the developer group.",
"The amount shown is this user's spend in the Front-End group. Their AI budget is currently managed by the developer group.",
);
},
};
@@ -3,6 +3,7 @@ import { useQuery } from "react-query";
import { groupById } from "#/api/queries/groups";
import type { Group, GroupMemberAISpend } from "#/api/typesGenerated";
import { AIBudgetAmount } from "#/components/AIBudgetAmount/AIBudgetAmount";
import { AIBudgetUsage } from "#/components/AIBudgetUsage/AIBudgetUsage";
import { Badge } from "#/components/Badge/Badge";
import { Spinner } from "#/components/Spinner/Spinner";
import { TableCell } from "#/components/Table/Table";
@@ -16,8 +17,8 @@ const OTHER_ORG_MESSAGE =
"This user's AI budget is managed by a group in another organization and isn't visible here.";
/**
* The AI budget and Budget group cells for a group member. Spend only counts
* against the viewed group; another group's budget shows as unattributed.
* The AI budget and Budget group cells for a group member. Spend is scoped to
* the viewed group; the limit comes from the member's effective group.
*/
export const GroupMemberBudgetCells: FC<{
group: Group;
@@ -50,7 +51,15 @@ export const GroupMemberBudgetCells: FC<{
budgetGroup = EM_DASH;
break;
case "everyone":
budgetGroup = <Badge size="sm">Everyone (not allocated)</Badge>;
// A populated budget means the Everyone group's own budget applies,
// so it isn't the unallocated fallback.
budgetGroup = (
<Badge size="sm">
{spend?.group_budget
? badgeName("Everyone")
: "Everyone (not allocated)"}
</Badge>
);
break;
case "this":
budgetGroup = <Badge size="sm">{badgeName(groupName)}</Badge>;
@@ -91,11 +100,11 @@ export const GroupMemberBudgetCells: FC<{
<StatusIconTooltip
message={
<>
None of this user's spend counts against the{" "}
The amount shown is this user's spend in the{" "}
<span className="font-medium text-content-primary">
{groupName}
</span>{" "}
group. It is managed by the{" "}
group. Their AI budget is currently managed by the{" "}
<span className="font-medium text-content-primary">
{effectiveGroupName}
</span>{" "}
@@ -105,7 +114,7 @@ export const GroupMemberBudgetCells: FC<{
/>
</span>
<span className="text-xs text-content-secondary">
Not attributed to this group
Budget managed by another group
</span>
</div>
);
@@ -116,18 +125,15 @@ export const GroupMemberBudgetCells: FC<{
// The effective group has no budget, so no limit applies.
budget = (
<LabelWithInfo
label="Unlimited"
label={
<AIBudgetUsage
currentSpend={spend.group_spend_micros}
spendLimit={null}
/>
}
message="None of this user's groups have an AI budget configured, so their AI usage isn't restricted."
/>
);
} else if (limit === 0) {
// A $0 budget disables spending, distinct from no budget configured.
budget = (
<LabelWithInfo
label="None"
message="This user's group(s) have an AI budget of $0, so they have no AI spending allowance."
/>
);
} else {
const limitLabel =
spend.group_budget?.limit_source === "user_override"
+1 -1
View File
@@ -3243,7 +3243,7 @@ export const MockGroup3: TypesGen.Group = {
total_member_count: 2,
};
const MockEveryoneGroup: TypesGen.Group = {
export const MockEveryoneGroup: TypesGen.Group = {
// The "Everyone" group must have the same ID as a the organization it belongs
// to.
id: MockOrganization.id,