diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx index 58c518a998..8350304389 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx @@ -225,7 +225,8 @@ export const AISpendZeroLimit: Story = { }, }; -// Dropdown closed to isolate the avatar border, which reflects spend severity. +// Dropdown closed to isolate the avatar and its severity badge, which +// indicates AI spend limit severity. export const AvatarBorderDisabled: Story = { parameters: { @@ -238,6 +239,14 @@ export const AvatarBorderNormal: Story = { ...aiCostControl, queries: [{ key: meAISpendKey, data: mockAISpend }], }, + play: async ({ canvasElement, step }) => { + await step("shows no severity indicator for normal spend", async () => { + const canvas = within(canvasElement); + expect( + canvas.getByRole("button", { name: "User menu" }), + ).toBeInTheDocument(); + }); + }, }; export const AvatarBorderWarning: Story = { @@ -250,6 +259,14 @@ export const AvatarBorderWarning: Story = { }, ], }, + play: async ({ canvasElement, step }) => { + await step("labels the trigger with the warning state", async () => { + const canvas = within(canvasElement); + await canvas.findByRole("button", { + name: "User menu. AI spend is nearing its limit", + }); + }); + }, }; export const AvatarBorderExceeded: Story = { @@ -262,6 +279,14 @@ export const AvatarBorderExceeded: Story = { }, ], }, + play: async ({ canvasElement, step }) => { + await step("labels the trigger with the exceeded state", async () => { + const canvas = within(canvasElement); + await canvas.findByRole("button", { + name: "User menu. AI spend limit exceeded", + }); + }); + }, }; export const AISpendHiddenOnInvalidData: Story = { diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx index 36d08b3284..3a9f52a5c7 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -1,4 +1,5 @@ -import type { FC } from "react"; +import { OctagonAlertIcon, TriangleAlertIcon } from "lucide-react"; +import type { FC, JSX } from "react"; import { useQuery } from "react-query"; import { meAISpend } from "#/api/queries/users"; import type * as TypesGen from "#/api/typesGenerated"; @@ -11,14 +12,25 @@ import { } from "#/components/DropdownMenu/DropdownMenu"; import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility"; import { getSeverity, type UsageSeverity } from "#/utils/budget"; +import { cn } from "#/utils/cn"; import { UserDropdownAISpend } from "./UserDropdownAISpend"; import { UserDropdownContent } from "./UserDropdownContent"; -const severityBorderClasses = { - normal: "border-content-secondary", - warning: "border-content-warning", - exceeded: "border-content-destructive", -} as const satisfies Record; +// Elevated states show a corner badge with a distinct icon per state. +const severityIndicators: Partial< + Record +> = { + warning: { + badge: "bg-surface-orange text-highlight-orange", + icon: , + label: "AI spend is nearing its limit", + }, + exceeded: { + badge: "bg-surface-red text-highlight-red", + icon: , + label: "AI spend limit exceeded", + }, +}; interface UserDropdownProps { user: TypesGen.User; @@ -58,20 +70,28 @@ export const UserDropdown: FC = ({ spend && spend.spendLimit !== null ? getSeverity(spend.currentSpend, spend.spendLimit) : "normal"; + const indicator = spend ? severityIndicators[severity] : undefined; return (