From e2f01e5531388f63c217b77a83bb931a7a55f52d Mon Sep 17 00:00:00 2001 From: Ethan Date: Wed, 27 May 2026 01:09:14 +1000 Subject: [PATCH] chore: prettify agents usage indicator (#25428) Restyles the Agents page usage trigger to match the new quota meter presentation by Tracy. The trigger now shows one compact row per section with the existing severity colors, the same spend icon used in settings, a server icon for workspace quota, and right-aligned counters. The related stories were updated to reflect the new trigger layout. **Before:** image **After:** image Relates to CODAGT-197 --- .../ChatsSidebar/chats/UserSidebarFooter.tsx | 6 +- .../components/UsageIndicator.stories.tsx | 66 ++++++++++++++++--- .../AgentsPage/components/UsageIndicator.tsx | 57 +++++++++++----- 3 files changed, 100 insertions(+), 29 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx b/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx index 2eb0ae1970..53533f5dbc 100644 --- a/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx +++ b/site/src/pages/AgentsPage/components/ChatsSidebar/chats/UserSidebarFooter.tsx @@ -16,11 +16,13 @@ export const UserSidebarFooter: FC = () => { return (
-
+ {/* This footer is resizable, so child sizing must follow its container width instead of the viewport. */} +
diff --git a/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx b/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx index 6f024a07fb..dbdb6c4bf4 100644 --- a/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx +++ b/site/src/pages/AgentsPage/components/UsageIndicator.stories.tsx @@ -1,4 +1,4 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Decorator, Meta, StoryObj } from "@storybook/react-vite"; import type { FC } from "react"; import { useQueryClient } from "react-query"; import { expect, userEvent, within } from "storybook/test"; @@ -57,11 +57,22 @@ const withUnavailableWorkspaceCount = (Story: FC) => { return ; }; -const withUsageIndicatorFrame = (Story: FC) => ( -
- -
-); +// Mirrors the sidebar footer wrapper: a fixed-width container with +// container-type set so the trigger inside reacts to the wrapper's width +// instead of the viewport's. +const withUsageIndicatorFrame = ( + widthClassName = "w-[320px]", + frameTestId?: string, +): Decorator => { + return (Story) => ( +
+ +
+ ); +}; const openUsageMenu = async (canvasElement: HTMLElement) => { const canvas = within(canvasElement); @@ -104,7 +115,7 @@ const meta: Meta = { decorators: [ withAuthProvider, withDashboardProvider, - withUsageIndicatorFrame, + withUsageIndicatorFrame(), ], parameters: { user: MockUserOwner, @@ -167,6 +178,9 @@ export const WorkspaceQuotaOnly: Story = { withWorkspaceCount(3), ], play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(canvas.getByText("30/100")).toBeVisible(); await openUsageMenu(canvasElement); }, }; @@ -181,12 +195,44 @@ export const UsageAndWorkspaceQuota: Story = { const canvas = within(canvasElement); const progressBars = canvas.getAllByRole("progressbar"); - expect(canvas.getByText("Usage")).toBeInTheDocument(); + expect(canvas.getByRole("button", { name: "Usage" })).toBeVisible(); + expect(canvas.getByText("$12.50")).toBeVisible(); + expect(canvas.getByText("30/100")).toBeVisible(); expect(progressBars.map((bar) => bar.getAttribute("aria-label"))).toEqual([ "Monthly spend usage", "Workspace quota usage", ]); - await userEvent.click(canvas.getByRole("button")); + await openUsageMenu(canvasElement); + }, +}; + +// The tiny story covers the responsive edge case where the trigger keeps +// the bars visible and drops the numeric details. +const expectTriggerContentFits = (canvasElement: HTMLElement) => { + const canvas = within(canvasElement); + const frame = canvas.getByTestId("usage-indicator-frame"); + + expect(canvas.getByRole("button", { name: "Usage" })).toBeVisible(); + expect(frame.scrollWidth).toBeLessThanOrEqual(frame.clientWidth); +}; + +const expectTriggerDetailsHidden = (canvasElement: HTMLElement) => { + const canvas = within(canvasElement); + + expect(canvas.getByText("$12.50")).not.toBeVisible(); + expect(canvas.getByText("30/100")).not.toBeVisible(); +}; + +export const TriggerTiny: Story = { + decorators: [ + withUsageIndicatorFrame("w-[240px]", "usage-indicator-frame"), + withUsageLimitStatus(limitedUsageStatus()), + withWorkspaceQuota(defaultWorkspaceQuota), + withWorkspaceCount(3), + ], + play: ({ canvasElement }) => { + expectTriggerContentFits(canvasElement); + expectTriggerDetailsHidden(canvasElement); }, }; @@ -220,7 +266,7 @@ export const WorkspaceQuotaWithoutBudget: Story = { name: "Workspace quota usage", }); - expect(canvas.getByText("Workspace quota")).toBeInTheDocument(); + expect(canvas.getByText("20")).toBeInTheDocument(); expect(progressbar).toHaveAttribute("aria-valuenow", "100"); await openUsageMenu(canvasElement); diff --git a/site/src/pages/AgentsPage/components/UsageIndicator.tsx b/site/src/pages/AgentsPage/components/UsageIndicator.tsx index 2a07878f12..00516e6260 100644 --- a/site/src/pages/AgentsPage/components/UsageIndicator.tsx +++ b/site/src/pages/AgentsPage/components/UsageIndicator.tsx @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { InfoIcon } from "lucide-react"; +import { CoinsIcon, InfoIcon, ServerIcon } from "lucide-react"; import { type FC, Fragment, type ReactNode } from "react"; import { useQuery } from "react-query"; import { Link } from "react-router"; @@ -36,6 +36,8 @@ type UsageSectionData = { progressLabel: string; percent: number; detail: ReactNode; + icon: ReactNode; + summaryValue: string; secondaryDetail?: ReactNode; tooltip?: ReactNode; severity?: UsageSeverity; @@ -79,6 +81,8 @@ export const UsageIndicator: FC = () => { progressLabel: `${periodLabel} spend usage`, percent: getPercent(currentSpend, spendLimit), severity: getSeverity(currentSpend, spendLimit), + icon: , + summaryValue: formatCostMicros(currentSpend), detail: ( <> {formatCostMicros(currentSpend)} of {formatCostMicros(spendLimit)}{" "} @@ -112,6 +116,11 @@ export const UsageIndicator: FC = () => { progressLabel: "Workspace quota usage", percent: getPercent(creditsConsumed, quota.budget), severity: getSeverity(creditsConsumed, quota.budget), + icon: , + summaryValue: + quota.budget > 0 + ? `${formatNumber(creditsConsumed)}/${formatNumber(quota.budget)}` + : formatNumber(creditsConsumed), detail: quotaDetail, tooltip: "Workspaces, stopped or running, may consume credits. Stop or delete unused ones to free quota.", @@ -128,7 +137,7 @@ export const UsageIndicator: FC = () => { const UsageMenu: FC<{ sections: readonly UsageSectionData[] }> = ({ sections, }) => { - const triggerLabel = + const triggerAriaLabel = sections.length > 1 ? "Usage" : (sections[0]?.title ?? "Usage"); return ( @@ -136,11 +145,9 @@ const UsageMenu: FC<{ sections: readonly UsageSectionData[] }> = ({ @@ -166,19 +173,35 @@ const UsageMenu: FC<{ sections: readonly UsageSectionData[] }> = ({ const UsageTriggerProgress: FC<{ sections: readonly UsageSectionData[] }> = ({ sections, }) => { - const size = sections.length > 1 ? "compact" : "default"; - return ( -
+
{sections.map((section) => ( - +
+ + + + {section.summaryValue} + +
))}
);