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:**
<img width="318" height="332" alt="image"
src="https://github.com/user-attachments/assets/4c6087bd-7c14-4cb6-b2e7-26bb7a1d3e70"
/>

**After:**
<img width="323" height="353" alt="image"
src="https://github.com/user-attachments/assets/95bfb992-cab8-473e-838d-1dcbe246fe3d"
/>




Relates to CODAGT-197
This commit is contained in:
Ethan
2026-05-27 01:09:14 +10:00
committed by GitHub
parent 90d8f56cc2
commit e2f01e5531
3 changed files with 100 additions and 29 deletions
@@ -16,11 +16,13 @@ export const UserSidebarFooter: FC = () => {
return (
<div className="hidden border-0 border-t border-solid sm:block">
<div className="flex items-stretch">
{/* This footer is resizable, so child sizing must follow its container width instead of the viewport. */}
<div className="flex min-w-0 items-stretch [container-type:inline-size]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
aria-label={`Account menu for ${user.name || user.username}`}
className="flex min-w-0 flex-1 items-center gap-2 bg-transparent border-0 cursor-pointer px-3 py-3 text-left hover:bg-surface-tertiary/50 transition-colors"
>
<Avatar
@@ -28,7 +30,7 @@ export const UserSidebarFooter: FC = () => {
src={user.avatar_url}
size="sm"
/>
<span className="truncate text-sm text-content-secondary">
<span className="min-w-0 flex-1 truncate text-sm text-content-secondary">
{user.name || user.username}
</span>
</button>
@@ -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 <Story />;
};
const withUsageIndicatorFrame = (Story: FC) => (
<div className="flex h-12 w-[260px] items-stretch justify-end rounded-md bg-surface-secondary">
<Story />
</div>
);
// 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) => (
<div
data-testid={frameTestId}
className={`flex h-12 min-w-0 items-stretch justify-end rounded-md bg-surface-secondary [container-type:inline-size] ${widthClassName}`}
>
<Story />
</div>
);
};
const openUsageMenu = async (canvasElement: HTMLElement) => {
const canvas = within(canvasElement);
@@ -104,7 +115,7 @@ const meta: Meta<typeof UsageIndicator> = {
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);
@@ -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: <CoinsIcon className="size-3.5" />,
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: <ServerIcon className="size-3.5" />,
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[] }> = ({
<DropdownMenuTrigger asChild>
<button
type="button"
className="ml-auto flex self-stretch flex-col items-center justify-center gap-1 border-none bg-transparent px-3 cursor-pointer select-none transition-colors text-content-secondary hover:bg-surface-tertiary/50 outline-none text-[13px]"
aria-label={triggerAriaLabel}
className="flex shrink-0 self-stretch items-center justify-center border-none bg-transparent px-3 cursor-pointer select-none transition-colors hover:bg-surface-tertiary/50 outline-none"
>
<span className="shrink-0 whitespace-nowrap text-center">
{triggerLabel}
</span>
<UsageTriggerProgress sections={sections} />
</button>
</DropdownMenuTrigger>
@@ -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 (
<div className="flex w-24 shrink-0 flex-col gap-0.5">
<div className="flex shrink-0 flex-col gap-1">
{sections.map((section) => (
<UsageProgress
key={section.id}
ariaLabel={section.progressLabel}
percent={section.percent}
severity={section.severity}
size={size}
className="w-full"
/>
<div key={section.id} className="flex items-center gap-2">
<span
aria-hidden="true"
className={cn(
"flex shrink-0 items-center justify-center",
getTextClassName(section.severity),
)}
>
{section.icon}
</span>
<UsageProgress
ariaLabel={section.progressLabel}
percent={section.percent}
severity={section.severity}
size="compact"
className="w-20 shrink-0 [@container_(min-width:300px)]:w-24 [@container_(min-width:420px)]:w-32 [@container_(min-width:560px)]:w-40"
/>
<span
className={cn(
"hidden shrink-0 whitespace-nowrap text-xs tabular-nums [@container_(min-width:300px)]:inline",
getTextClassName(section.severity),
)}
>
{section.summaryValue}
</span>
</div>
))}
</div>
);