mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat(site): warn admins about ineffective AI provider env config
Add an admin-only dashboard banner that surfaces when deprecated CODER_AIBRIDGE_* env configuration has drifted from the database (appearance.ai_providers_env_drift_detected). It stacks beneath the license banner inside the existing deployment-config gate and links to the AI gateway setup docs. Presentation lives in a pure subcomponent covered by a Storybook story.
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, within } from "storybook/test";
|
||||
import { chromatic } from "#/testHelpers/chromatic";
|
||||
import {
|
||||
MockAppearanceConfig,
|
||||
MockBuildInfo,
|
||||
MockDefaultOrganization,
|
||||
MockEntitlements,
|
||||
MockExperiments,
|
||||
} from "#/testHelpers/entities";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { DashboardContext, type DashboardValue } from "../DashboardProvider";
|
||||
import {
|
||||
AIProviderEnvDriftBanner,
|
||||
AIProviderEnvDriftBannerView,
|
||||
} from "./AIProviderEnvDriftBanner";
|
||||
|
||||
const meta: Meta<typeof AIProviderEnvDriftBannerView> = {
|
||||
title: "modules/dashboard/AIProviderEnvDriftBanner",
|
||||
parameters: { chromatic },
|
||||
component: AIProviderEnvDriftBannerView,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AIProviderEnvDriftBannerView>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
docsHref: docs("/ai-coder/ai-gateway/setup"),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(canvas.getByRole("status")).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("link", { name: /View setup docs/i }),
|
||||
).toHaveAttribute("href", docs("/ai-coder/ai-gateway/setup"));
|
||||
},
|
||||
};
|
||||
|
||||
const renderBannerWithDrift = (driftDetected: boolean) => {
|
||||
const value: DashboardValue = {
|
||||
entitlements: MockEntitlements,
|
||||
experiments: MockExperiments,
|
||||
appearance: {
|
||||
...MockAppearanceConfig,
|
||||
ai_providers_env_drift_detected: driftDetected,
|
||||
},
|
||||
buildInfo: MockBuildInfo,
|
||||
organizations: [MockDefaultOrganization],
|
||||
showOrganizations: false,
|
||||
canViewOrganizationSettings: false,
|
||||
};
|
||||
return (
|
||||
<DashboardContext.Provider value={value}>
|
||||
<AIProviderEnvDriftBanner />
|
||||
</DashboardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const DriftDetected: Story = {
|
||||
render: () => renderBannerWithDrift(true),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(canvas.getByRole("status")).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("link", { name: /View setup docs/i }),
|
||||
).toHaveAttribute("href", docs("/ai-coder/ai-gateway/setup"));
|
||||
},
|
||||
};
|
||||
|
||||
export const NoDrift: Story = {
|
||||
render: () => renderBannerWithDrift(false),
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(canvas.queryByRole("status")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { TriangleAlertIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { useDashboard } from "#/modules/dashboard/useDashboard";
|
||||
import { docs } from "#/utils/docs";
|
||||
|
||||
interface AIProviderEnvDriftBannerViewProps {
|
||||
docsHref: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AIProviderEnvDriftBannerView is the presentational banner. It is pure
|
||||
* (props only) so it can be exercised in Storybook without dashboard
|
||||
* context. Styling mirrors the single-message LicenseBannerView so the
|
||||
* two banners stack consistently.
|
||||
*/
|
||||
export const AIProviderEnvDriftBannerView: FC<
|
||||
AIProviderEnvDriftBannerViewProps
|
||||
> = ({ docsHref }) => {
|
||||
return (
|
||||
<div role="status" className="flex items-center p-3 bg-surface-secondary">
|
||||
<div className="flex min-w-0 flex-1 items-start gap-2">
|
||||
<div className="flex h-6 items-center">
|
||||
<TriangleAlertIcon className="size-4 text-content-warning" />
|
||||
</div>
|
||||
<div className="flex min-h-6 min-w-0 flex-1 items-center text-xs leading-4 text-content-primary">
|
||||
<span>
|
||||
Changes to the deprecated AI provider environment variables
|
||||
(CODER_AIBRIDGE_*) are ineffective. Manage AI providers through the
|
||||
dashboard, which is the source of truth.{" "}
|
||||
<Link
|
||||
className="text-xs font-medium !text-content-link"
|
||||
href={docsHref}
|
||||
target="_blank"
|
||||
>
|
||||
View setup docs
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* AIProviderEnvDriftBanner warns admins when deprecated CODER_AIBRIDGE_*
|
||||
* env configuration drifts from the AI provider rows in the database,
|
||||
* which makes those env changes ineffective. It renders nothing when no
|
||||
* drift was detected at startup.
|
||||
*/
|
||||
export const AIProviderEnvDriftBanner: FC = () => {
|
||||
const { appearance } = useDashboard();
|
||||
|
||||
if (!appearance.ai_providers_env_drift_detected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AIProviderEnvDriftBannerView
|
||||
docsHref={docs("/ai-coder/ai-gateway/setup")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { Outlet } from "react-router";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { AIProviderEnvDriftBanner } from "#/modules/dashboard/AIProviderEnvDriftBanner/AIProviderEnvDriftBanner";
|
||||
import { AnnouncementBanners } from "#/modules/dashboard/AnnouncementBanners/AnnouncementBanners";
|
||||
import { LicenseBanner } from "#/modules/dashboard/LicenseBanner/LicenseBanner";
|
||||
import { cn } from "#/utils/cn";
|
||||
@@ -22,6 +23,7 @@ export const DashboardLayout: FC = () => {
|
||||
return (
|
||||
<>
|
||||
{canViewDeployment && <LicenseBanner />}
|
||||
{canViewDeployment && <AIProviderEnvDriftBanner />}
|
||||
<AnnouncementBanners />
|
||||
|
||||
<div className="flex flex-col min-h-screen justify-between">
|
||||
|
||||
Reference in New Issue
Block a user