mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +08:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user