feat(site): show disabled state on AI provider rows (#28558)

Updates the AI providers list to make the disabled state visible on the
row itself. Disabled providers show a `Disabled` badge beside the name,
a muted avatar, and disabled text for the name and base URL. The
`Enabled` badge is removed from the status column, which now holds only
issue badges (`Not supported in Agents` and `Warning`).

Also fixes tooltip inconsistency in the status column: the `Not
supported in Agents` badge used a native `title` attribute while the
warning used the shared Tooltip component, so they looked different.
Both now use the shared tooltip, with a max width so long warnings wrap
instead of rendering as one long line.

Storybook interaction coverage: new `Disabled`/`Enabled` stories, and
`NotSupportedInAgents` now hovers the badge and asserts the shared
tooltip content.

---

Created by Coder Agents on behalf of @tracyjohnsonux.
This commit is contained in:
TJ
2026-08-26 12:00:47 -07:00
committed by GitHub
parent bba0ad97d2
commit c0e681863a
3 changed files with 73 additions and 28 deletions
@@ -103,7 +103,9 @@ const ProvidersPageView: React.FC<ProvidersPageViewProps> = ({
<TableRow>
<TableHead className="w-1/3">Name</TableHead>
<TableHead className="w-1/3">Base URL</TableHead>
<TableHead className="w-22">Status</TableHead>
<TableHead className="w-22">
<span className="sr-only">Status</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody size="lg">
@@ -23,12 +23,17 @@ const meta: Meta<typeof ProviderRow> = {
},
decorators: [
(Story) => (
<Table aria-label="AI providers">
<Table className="table-fixed" aria-label="AI providers">
<TableHeader>
<TableRow>
<TableHead className="w-1/3">Name</TableHead>
<TableHead className="w-1/3">Base URL</TableHead>
<TableHead className="w-22">Status</TableHead>
<TableHead className="w-[42%]">Name</TableHead>
<TableHead className="w-[38%]">Base URL</TableHead>
<TableHead className="w-20 text-center">
<span className="sr-only">Status</span>
</TableHead>
<TableHead className="w-12">
<span className="sr-only">Open provider</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -77,19 +82,36 @@ export const NotSupportedInAgents: Story = {
args: {
provider: MockAIProviderCopilot,
},
play: async ({ canvasElement }) => {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const badge = canvas.getByRole("button", {
name: "Not supported in Agents",
});
await expect(badge).toBeInTheDocument();
await userEvent.hover(badge);
const tooltip = await within(canvasElement.ownerDocument.body).findByRole(
"tooltip",
);
const tooltip = await within(document.body).findByRole("tooltip");
await expect(tooltip).toHaveTextContent(
"This provider works with the AI Gateway Proxy but Coder Agents can't use it.",
);
// Activation must not navigate the row.
badge.focus();
await userEvent.keyboard("{Enter}");
await userEvent.keyboard(" ");
await userEvent.click(badge);
await expect(args.onClick).not.toHaveBeenCalled();
},
};
export const Disabled: Story = {
args: {
provider: { ...MockAIProviderOpenAI, enabled: false },
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Disabled")).toBeInTheDocument();
await expect(canvas.getByText("OpenAI")).toBeInTheDocument();
},
};
@@ -103,6 +125,7 @@ export const SupportedHasNoAgentsLabel: Story = {
await expect(
canvas.queryByText("Not supported in Agents"),
).not.toBeInTheDocument();
await expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
},
};
@@ -120,19 +143,15 @@ export const WithHostnameCollisionWarning: Story = {
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const badge = canvas.getByLabelText(/^Warning: Hostname/);
const badge = canvas.getByRole("button", { name: /^Warning:/ });
await expect(badge).toBeInTheDocument();
await expect(badge).toHaveAttribute(
"aria-label",
await expect(badge).toHaveAccessibleName(
expect.stringContaining("api.openai.com"),
);
await expect(badge).toHaveAttribute("tabIndex", "0");
// Hover shows the tooltip with the warning text.
await userEvent.hover(badge);
const tooltip = await within(canvasElement.ownerDocument.body).findByRole(
"tooltip",
);
const tooltip = await within(document.body).findByRole("tooltip");
await expect(tooltip).toHaveTextContent("api.openai.com");
// Keyboard and mouse activation must not navigate the row.
@@ -13,6 +13,7 @@ import {
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { cn } from "#/utils/cn";
import { ProviderIcon } from "./ProviderIcon";
import { getProviderDisplayType } from "./providerFormApiMap";
@@ -29,6 +30,7 @@ export const ProviderRow: React.FC<ProviderRowProps> = ({
onClick: () => onClick?.(),
});
const displayName = provider.display_name || provider.name;
const disabled = !provider.enabled;
// Stop activation from bubbling to a parent `useClickableTableRow`
// row, which navigates on click, Enter (onKeyDown), and Space
@@ -42,11 +44,27 @@ export const ProviderRow: React.FC<ProviderRowProps> = ({
<TableRow key={provider.name} {...clickableProps}>
<TableCell className="min-w-0 px-4 py-3">
<AvatarData
title={displayName}
title={
<span className="flex items-center gap-2">
<span
className={cn("truncate", disabled && "text-content-secondary")}
>
{displayName}
</span>
{disabled && (
<Badge asChild size="sm" variant="default">
<span>Disabled</span>
</Badge>
)}
</span>
}
avatar={
<Avatar
size="lg"
className="flex shrink-0 items-center justify-center"
className={cn(
"flex shrink-0 items-center justify-center",
disabled && "opacity-50 grayscale",
)}
>
<ProviderIcon
provider={getProviderDisplayType(provider)}
@@ -58,7 +76,10 @@ export const ProviderRow: React.FC<ProviderRowProps> = ({
</TableCell>
<TableCell className="min-w-0">
<span
className="block truncate text-content-secondary"
className={cn(
"block truncate",
disabled ? "text-content-disabled" : "text-content-secondary",
)}
title={provider.base_url}
>
{provider.base_url}
@@ -66,19 +87,17 @@ export const ProviderRow: React.FC<ProviderRowProps> = ({
</TableCell>
<TableCell>
<div className="flex flex-wrap items-center gap-1">
{provider.enabled && <Badge variant="default">Enabled</Badge>}
{AgentsUnsupportedProviderTypes.some((t) => t === provider.type) && (
<Tooltip>
<TooltipTrigger asChild>
<Badge
asChild
variant="info"
role="button"
tabIndex={0}
onClick={stopPropagation}
onKeyDown={stopPropagation}
onKeyUp={stopPropagation}
>
Not supported in Agents
<button type="button">Not supported in Agents</button>
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
@@ -91,20 +110,25 @@ export const ProviderRow: React.FC<ProviderRowProps> = ({
<Tooltip>
<TooltipTrigger asChild>
<Badge
asChild
variant="warning"
role="button"
tabIndex={0}
aria-label={`Warning: ${provider.status.warnings.join("; ")}`}
onClick={stopPropagation}
onKeyDown={stopPropagation}
onKeyUp={stopPropagation}
>
Warning
<button
type="button"
aria-label={`Warning: ${provider.status.warnings.join("; ")}`}
>
Warning
</button>
</Badge>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
{provider.status.warnings.map((warning) => (
<p key={warning}>{warning}</p>
<p key={warning} className="break-words">
{warning}
</p>
))}
</TooltipContent>
</Tooltip>