diff --git a/site/src/components/AIBudgetUsage/AIBudgetUsage.stories.tsx b/site/src/components/AIBudgetUsage/AIBudgetUsage.stories.tsx
index e51d53c4aa4..f9d0582749d 100644
--- a/site/src/components/AIBudgetUsage/AIBudgetUsage.stories.tsx
+++ b/site/src/components/AIBudgetUsage/AIBudgetUsage.stories.tsx
@@ -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");
+ },
+};
diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx b/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx
index 98ae0ceda1c..758c8b66929 100644
--- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx
+++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.stories.tsx
@@ -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("Everyone (not allocated)"),
+ ).toBeInTheDocument();
+ },
+};
+
+// The Everyone group's own budget governs; the badge drops "(not allocated)".
+export const EveryoneGroupWithBudget: Story = {
+ args: {
+ group: MockEveryoneGroup,
+ spend: {
+ ...mockSpend,
+ group_spend_micros: 1_250_000_000,
+ effective_group_id: MockEveryoneGroup.id,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ 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();
},
};
-export const None: Story = {
+// 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" },
- effective_group_id: group.organization_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("$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.",
);
},
};
diff --git a/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx b/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx
index 9efe4690424..eb692206162 100644
--- a/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx
+++ b/site/src/pages/GroupsPage/GroupMemberBudgetCells.tsx
@@ -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 = Everyone (not allocated);
+ // A populated budget means the Everyone group's own budget applies,
+ // so it isn't the unallocated fallback.
+ budgetGroup = (
+
+ {spend?.group_budget
+ ? badgeName("Everyone")
+ : "Everyone (not allocated)"}
+
+ );
break;
case "this":
budgetGroup = {badgeName(groupName)};
@@ -91,11 +100,11 @@ export const GroupMemberBudgetCells: FC<{
- None of this user's spend counts against the{" "}
+ The amount shown is this user's spend in the{" "}
{groupName}
{" "}
- group. It is managed by the{" "}
+ group. Their AI budget is currently managed by the{" "}
{effectiveGroupName}
{" "}
@@ -105,7 +114,7 @@ export const GroupMemberBudgetCells: FC<{
/>
- Not attributed to this group
+ Budget managed by another group
);
@@ -116,18 +125,15 @@ export const GroupMemberBudgetCells: FC<{
// The effective group has no budget, so no limit applies.
budget = (
+ }
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 = (
-
- );
} else {
const limitLabel =
spend.group_budget?.limit_source === "user_override"
diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts
index cf6674b2ed5..95158700332 100644
--- a/site/src/testHelpers/entities.ts
+++ b/site/src/testHelpers/entities.ts
@@ -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,