feat(site): add AI Gateway keys management page (#25817)

> Vibe-coded using Coder Agents, author with limited frontend knowledge,
manually tested.

Adds an `AI Gateway Keys` page under `Admin settings > AI > AI Gateway
Keys` for key management of keys used by standalone AI Gateway replicas
to authenticate into `coderd`.
The page is shown to users with `viewAIGatewayKeys` permission and a
Premium license with AI Gateway enabled.
Adds Storybook coverage.

---------

Co-authored-by: Jake Howell <jacob@coder.com>
This commit is contained in:
Paweł Banaszewski
2026-06-15 18:26:58 +02:00
committed by GitHub
co-authored by Jake Howell
parent 86bdedb0a9
commit e019210f4b
13 changed files with 819 additions and 6 deletions
+4
View File
@@ -107,6 +107,10 @@
"object": { "resource_type": "ai_provider" },
"action": "read"
},
"viewAIGatewayKeys": {
"object": { "resource_type": "ai_gateway_key" },
"action": "read"
},
"createOAuth2App": {
"object": { "resource_type": "oauth2_app" },
"action": "create"
+21
View File
@@ -3121,6 +3121,27 @@ class ApiMethods {
`/api/v2/ai/providers/${encodeURIComponent(idOrName)}`,
);
};
getAIGatewayKeys = async (): Promise<TypesGen.AIGatewayKey[]> => {
const response = await this.axios.get<TypesGen.AIGatewayKey[]>(
"/api/v2/aibridge/keys",
);
return response.data;
};
createAIGatewayKey = async (
req: TypesGen.CreateAIGatewayKeyRequest,
): Promise<TypesGen.CreateAIGatewayKeyResponse> => {
const response = await this.axios.post<TypesGen.CreateAIGatewayKeyResponse>(
"/api/v2/aibridge/keys",
req,
);
return response.data;
};
deleteAIGatewayKey = async (id: string): Promise<void> => {
await this.axios.delete(`/api/v2/aibridge/keys/${encodeURIComponent(id)}`);
};
}
export type TaskFeedbackRating = "good" | "okay" | "bad";
+30
View File
@@ -0,0 +1,30 @@
import type { QueryClient } from "react-query";
import { API } from "#/api/api";
import type {
AIGatewayKey,
CreateAIGatewayKeyRequest,
CreateAIGatewayKeyResponse,
} from "#/api/typesGenerated";
const aiGatewayKeysListKey = ["ai", "gatewayKeys"] as const;
export const aiGatewayKeysList = () => ({
queryKey: aiGatewayKeysListKey,
queryFn: (): Promise<AIGatewayKey[]> => API.getAIGatewayKeys(),
});
export const createAIGatewayKeyMutation = (queryClient: QueryClient) => ({
mutationFn: (
request: CreateAIGatewayKeyRequest,
): Promise<CreateAIGatewayKeyResponse> => API.createAIGatewayKey(request),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: aiGatewayKeysListKey });
},
});
export const deleteAIGatewayKeyMutation = (queryClient: QueryClient) => ({
mutationFn: (id: string): Promise<void> => API.deleteAIGatewayKey(id),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: aiGatewayKeysListKey });
},
});
+2 -1
View File
@@ -27,7 +27,8 @@ export const Navbar: FC = () => {
featureVisibility.connection_log && permissions.viewAnyConnectionLog;
const canViewAIBridge =
featureVisibility.aibridge && permissions.viewAnyAIBridgeInterception;
const canViewAISettings = permissions.viewAnyAIProvider;
const canViewAISettings =
permissions.viewAnyAIProvider || permissions.viewAIGatewayKeys;
const canCreateChat = permissions.createChat;
const uniqueLinks = new Map<string, LinkConfig>();
@@ -22,9 +22,16 @@ const AISettingsSidebarView: FC<AISettingsSidebarViewProps> = ({
AI Governance
</SidebarNavItem>
)}
<SidebarNavItem href="/ai/settings" end>
Providers
</SidebarNavItem>
{permissions.viewAnyAIProvider && (
<SidebarNavItem href="/ai/settings" end>
Providers
</SidebarNavItem>
)}
{permissions.viewAIGatewayKeys && (
<SidebarNavItem href="/ai/settings/gateway-keys">
AI Gateway Keys
</SidebarNavItem>
)}
{permissions.editDeploymentConfig && (
<SidebarNavItem href="/agents/settings/agents">
<div className="flex flex-row items-center gap-1">
+2 -1
View File
@@ -26,7 +26,8 @@ export const canViewDeploymentSettings = (
permissions.viewAnyGroup ||
permissions.viewNotificationTemplate ||
permissions.viewOrganizationIDPSyncSettings ||
permissions.viewAnyAIProvider)
permissions.viewAnyAIProvider ||
permissions.viewAIGatewayKeys)
);
};
@@ -0,0 +1,226 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import { expect, fn, userEvent, within } from "storybook/test";
import type { CreateAIGatewayKeyResponse } from "#/api/typesGenerated";
import {
MockCreateAIGatewayKeyResponse,
mockApiError,
} from "#/testHelpers/entities";
import { CreateGatewayKeyDialog } from "./CreateGatewayKeyDialog";
const meta: Meta<typeof CreateGatewayKeyDialog> = {
title: "pages/AISettingsPage/CreateGatewayKeyDialog",
component: CreateGatewayKeyDialog,
args: {
open: true,
onClose: fn(),
onCreate: fn(),
},
};
export default meta;
type Story = StoryObj<typeof CreateGatewayKeyDialog>;
export const Form: Story = {};
export const CreateAndReveal: Story = {
render: (args) => {
const [createdKey, setCreatedKey] = useState<
CreateAIGatewayKeyResponse | undefined
>(undefined);
return (
<CreateGatewayKeyDialog
{...args}
createdKey={createdKey}
onCreate={(name) => {
args.onCreate(name);
setCreatedKey(MockCreateAIGatewayKeyResponse);
}}
/>
);
},
play: async ({ args, canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const nameField = await body.findByLabelText("Name");
await userEvent.type(nameField, "new-gateway");
const createButton = await body.findByRole("button", { name: "Create" });
await userEvent.click(createButton);
await expect(args.onCreate).toHaveBeenCalledWith("new-gateway");
await body.findByText(MockCreateAIGatewayKeyResponse.key);
},
};
export const InvalidName: Story = {
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const nameField = await body.findByLabelText("Name");
await userEvent.type(nameField, "UPPER_CASE");
const createButton = await body.findByRole("button", { name: "Create" });
await userEvent.click(createButton);
await expect(
body.findByText(
"Use lowercase letters and numbers with optional single hyphens between words.",
),
).resolves.toBeVisible();
},
};
export const CreateError: Story = {
render: (args) => {
const [submitError, setSubmitError] = useState<unknown>(undefined);
const error = mockApiError({
message: "Key name must be unique.",
validations: [
{
field: "name",
detail: "A key with this name already exists.",
},
],
});
return (
<CreateGatewayKeyDialog
{...args}
submitError={submitError}
onCreate={(name) => {
args.onCreate(name);
setSubmitError(error);
}}
/>
);
},
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const nameField = await body.findByLabelText("Name");
await userEvent.type(nameField, "dup-key");
const createButton = await body.findByRole("button", { name: "Create" });
await userEvent.click(createButton);
await expect(
body.findByText("A key with this name already exists."),
).resolves.toBeVisible();
},
};
export const GeneralCreateError: Story = {
render: (args) => {
const [submitError, setSubmitError] = useState<unknown>(undefined);
const error = mockApiError({ message: "Failed to create key." });
return (
<CreateGatewayKeyDialog
{...args}
submitError={submitError}
onCreate={(name) => {
args.onCreate(name);
setSubmitError(error);
}}
/>
);
},
play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const nameField = await body.findByLabelText("Name");
await userEvent.type(nameField, "new-gateway");
const createButton = await body.findByRole("button", { name: "Create" });
await userEvent.click(createButton);
await expect(
body.findByText("Failed to create key."),
).resolves.toBeVisible();
},
};
export const EscapeDoesNotDismissCreatedKey: Story = {
render: (args) => {
const [createdKey, setCreatedKey] = useState<
CreateAIGatewayKeyResponse | undefined
>(undefined);
return (
<CreateGatewayKeyDialog
{...args}
createdKey={createdKey}
onCreate={(name) => {
args.onCreate(name);
setCreatedKey(MockCreateAIGatewayKeyResponse);
}}
/>
);
},
play: async ({ args, canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const nameField = await body.findByLabelText("Name");
await userEvent.type(nameField, "new-gateway");
const createButton = await body.findByRole("button", { name: "Create" });
await userEvent.click(createButton);
await expect(args.onCreate).toHaveBeenCalledWith("new-gateway");
await body.findByText(MockCreateAIGatewayKeyResponse.key);
await userEvent.keyboard("{Escape}");
await expect(
body.findByText(MockCreateAIGatewayKeyResponse.key),
).resolves.toBeVisible();
},
};
export const ReopenAfterCreate: Story = {
render: (args) => {
const [open, setOpen] = useState(args.open);
const [createdKey, setCreatedKey] = useState<
CreateAIGatewayKeyResponse | undefined
>(undefined);
return (
<div>
<button type="button" onClick={() => setOpen(true)}>
Open dialog
</button>
<CreateGatewayKeyDialog
{...args}
open={open}
createdKey={createdKey}
onCreate={(name) => {
args.onCreate(name);
setCreatedKey(MockCreateAIGatewayKeyResponse);
}}
onClose={() => {
args.onClose();
setCreatedKey(undefined);
setOpen(false);
}}
/>
</div>
);
},
play: async ({ args, canvasElement }) => {
const body = within(canvasElement.ownerDocument.body);
const nameField = await body.findByLabelText("Name");
await userEvent.type(nameField, "new-gateway");
const createButton = await body.findByRole("button", { name: "Create" });
await userEvent.click(createButton);
await expect(args.onCreate).toHaveBeenCalledWith("new-gateway");
await body.findByText(MockCreateAIGatewayKeyResponse.key);
const doneButton = await body.findByRole("button", { name: "Done" });
await userEvent.click(doneButton);
const canvas = within(canvasElement);
const openButton = await canvas.findByRole("button", {
name: "Open dialog",
});
await userEvent.click(openButton);
await expect(body.findByLabelText("Name")).resolves.toHaveValue("");
await expect(
body.queryByText(MockCreateAIGatewayKeyResponse.key),
).not.toBeInTheDocument();
},
};
@@ -0,0 +1,140 @@
import { useFormik } from "formik";
import type { FC } from "react";
import * as Yup from "yup";
import { isApiValidationError } from "#/api/errors";
import type { CreateAIGatewayKeyResponse } from "#/api/typesGenerated";
import { Alert, AlertDescription } from "#/components/Alert/Alert";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { CodeExample } from "#/components/CodeExample/CodeExample";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "#/components/Dialog/Dialog";
import { FormField } from "#/components/FormField/FormField";
import { Spinner } from "#/components/Spinner/Spinner";
import { getFormHelpers } from "#/utils/formUtils";
interface CreateGatewayKeyFormValues {
name: string;
}
const validationSchema = Yup.object({
name: Yup.string()
.required("Name is required.")
.matches(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
excludeEmptyString: true,
message:
"Use lowercase letters and numbers with optional single hyphens between words.",
})
.max(64, "Name cannot be longer than 64 characters."),
});
interface CreateGatewayKeyDialogProps {
open: boolean;
onClose: () => void;
onCreate: (name: string) => void;
createdKey?: CreateAIGatewayKeyResponse;
submitError?: unknown;
isSubmitting?: boolean;
}
export const CreateGatewayKeyDialog: FC<CreateGatewayKeyDialogProps> = ({
open,
onClose,
onCreate,
createdKey,
submitError,
isSubmitting = false,
}) => {
const form = useFormik<CreateGatewayKeyFormValues>({
initialValues: { name: "" },
validationSchema,
onSubmit: (values) => {
onCreate(values.name);
},
});
const getFieldHelpers = getFormHelpers(form, submitError);
const closeDialog = () => {
form.resetForm();
onClose();
};
const isBusy = isSubmitting;
const showSubmitError =
Boolean(submitError) && !isApiValidationError(submitError);
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen && !isBusy && !createdKey) {
closeDialog();
}
}}
>
<DialogContent
className="max-h-[90vh] overflow-y-auto"
aria-describedby={undefined}
>
<DialogHeader>
<DialogTitle>
{createdKey ? "Save your AI Gateway key" : "Create AI Gateway key"}
</DialogTitle>
</DialogHeader>
{createdKey ? (
<div className="flex flex-col gap-5">
<Alert severity="warning">
<AlertDescription>
Copy this key now. For security reasons it cannot be shown
again.
</AlertDescription>
</Alert>
<CodeExample
secret={false}
code={createdKey.key}
className="min-h-0 select-all w-full"
/>
<DialogFooter>
<Button onClick={closeDialog}>Done</Button>
</DialogFooter>
</div>
) : (
<div className="flex flex-col gap-5">
{showSubmitError && <ErrorAlert error={submitError} />}
<form onSubmit={form.handleSubmit} className="flex flex-col gap-5">
<FormField
field={getFieldHelpers("name", {
helperText:
"Lowercase letters and numbers with optional single hyphens between words. Maximum 64 characters.",
})}
label="Name"
placeholder="primary-gateway"
autoFocus
autoComplete="off"
/>
<DialogFooter>
<Button
variant="outline"
disabled={isBusy}
onClick={closeDialog}
>
Cancel
</Button>
<Button type="submit" disabled={isBusy || !form.dirty}>
<Spinner loading={isBusy} />
Create
</Button>
</DialogFooter>
</form>
</div>
)}
</DialogContent>
</Dialog>
);
};
@@ -0,0 +1,97 @@
import { type FC, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "react-query";
import { toast } from "sonner";
import { getErrorMessage } from "#/api/errors";
import {
aiGatewayKeysList,
createAIGatewayKeyMutation,
deleteAIGatewayKeyMutation,
} from "#/api/queries/aiGatewayKeys";
import type { AIGatewayKey } from "#/api/typesGenerated";
import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { pageTitle } from "#/utils/page";
import { CreateGatewayKeyDialog } from "./CreateGatewayKeyDialog";
import { GatewayKeysPageView } from "./GatewayKeysPageView";
const GatewayKeysPage: FC = () => {
const { permissions } = useAuthenticated();
const featureVisibility = useFeatureVisibility();
const showPaywall = !featureVisibility.aibridge;
const queryClient = useQueryClient();
const keysQuery = useQuery({
...aiGatewayKeysList(),
enabled: !showPaywall,
});
const createMutation = useMutation(createAIGatewayKeyMutation(queryClient));
const deleteMutation = useMutation(deleteAIGatewayKeyMutation(queryClient));
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [keyToDelete, setKeyToDelete] = useState<AIGatewayKey | undefined>(
undefined,
);
return (
<RequirePermission isFeatureVisible={permissions.viewAIGatewayKeys}>
<title>{pageTitle("AI Gateway Keys")}</title>
<GatewayKeysPageView
keys={keysQuery.data ?? []}
isLoading={keysQuery.isLoading}
error={keysQuery.error}
showPaywall={showPaywall}
onCreateKey={() => setIsCreateOpen(true)}
onDeleteKey={setKeyToDelete}
/>
<CreateGatewayKeyDialog
open={isCreateOpen}
onClose={() => {
createMutation.reset();
setIsCreateOpen(false);
}}
onCreate={(name) => createMutation.mutate({ name })}
createdKey={createMutation.data}
submitError={createMutation.error}
isSubmitting={createMutation.isPending}
/>
<ConfirmDialog
type="delete"
title="Delete AI Gateway key"
description={
<>
Are you sure you want to permanently delete key{" "}
<strong>{keyToDelete?.name}</strong>? Any AI Gateway replica using
it will no longer be able to authenticate.
</>
}
open={Boolean(keyToDelete)}
confirmLoading={deleteMutation.isPending}
onConfirm={() => {
if (!keyToDelete) {
return;
}
const name = keyToDelete.name;
deleteMutation.mutate(keyToDelete.id, {
onSuccess: () => {
toast.success(`Deleted AI Gateway key "${name}" successfully.`);
setKeyToDelete(undefined);
},
onError: (error) => {
toast.error(
getErrorMessage(error, "Failed to delete AI Gateway key."),
);
},
});
}}
onClose={() => setKeyToDelete(undefined)}
/>
</RequirePermission>
);
};
export default GatewayKeysPage;
@@ -0,0 +1,92 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { MockAIGatewayKeys, mockApiError } from "#/testHelpers/entities";
import { GatewayKeysPageView } from "./GatewayKeysPageView";
const meta: Meta<typeof GatewayKeysPageView> = {
title: "pages/AISettingsPage/GatewayKeysPageView",
component: GatewayKeysPageView,
args: {
keys: MockAIGatewayKeys,
isLoading: false,
error: null,
showPaywall: false,
onCreateKey: fn(),
onDeleteKey: fn(),
},
};
export default meta;
type Story = StoryObj<typeof GatewayKeysPageView>;
export const Default: Story = {};
export const Loading: Story = {
args: {
isLoading: true,
keys: [],
},
};
export const Empty: Story = {
args: {
keys: [],
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const [, emptyCta] = await canvas.findAllByRole("button", {
name: /create key/i,
});
await userEvent.click(emptyCta);
await expect(args.onCreateKey).toHaveBeenCalledTimes(1);
},
};
export const LoadError: Story = {
args: {
keys: [],
error: mockApiError({ message: "Failed to load AI Gateway keys" }),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(
canvas.findByText("Failed to load AI Gateway keys"),
).resolves.toBeVisible();
await expect(
canvas.findByText("Failed to fetch AI Gateway keys"),
).resolves.toBeVisible();
await expect(
canvas.queryByText("No AI Gateway keys"),
).not.toBeInTheDocument();
await expect(
canvas.getAllByRole("button", { name: /create key/i }),
).toHaveLength(1);
},
};
export const Paywall: Story = {
args: {
showPaywall: true,
keys: [],
},
};
export const ClickCreate: Story = {
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const button = await canvas.findByRole("button", { name: /create key/i });
await userEvent.click(button);
await expect(args.onCreateKey).toHaveBeenCalledTimes(1);
},
};
export const ClickDelete: Story = {
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const button = await canvas.findByRole("button", {
name: `Delete ${MockAIGatewayKeys[0].name}`,
});
await userEvent.click(button);
await expect(args.onDeleteKey).toHaveBeenCalledWith(MockAIGatewayKeys[0]);
},
};
@@ -0,0 +1,145 @@
import { PlusIcon, TrashIcon } from "lucide-react";
import type { FC } from "react";
import type { AIGatewayKey } from "#/api/typesGenerated";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Button } from "#/components/Button/Button";
import { Link } from "#/components/Link/Link";
import { PaywallPremium } from "#/components/Paywall/PaywallPremium";
import {
SettingsHeader,
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "#/components/Table/Table";
import { TableEmpty } from "#/components/TableEmpty/TableEmpty";
import { TableLoader } from "#/components/TableLoader/TableLoader";
import { docs } from "#/utils/docs";
import { relativeTime } from "#/utils/time";
interface GatewayKeysPageViewProps {
keys: AIGatewayKey[];
isLoading: boolean;
error: unknown;
showPaywall: boolean;
onCreateKey: () => void;
onDeleteKey: (key: AIGatewayKey) => void;
}
export const GatewayKeysPageView: FC<GatewayKeysPageViewProps> = ({
keys,
isLoading,
error,
showPaywall,
onCreateKey,
onDeleteKey,
}) => {
return (
<div>
<SettingsHeader
actions={
!showPaywall && (
<Button variant="outline" onClick={onCreateKey}>
<PlusIcon />
Create key
</Button>
)
}
>
<SettingsHeaderTitle>AI Gateway Keys</SettingsHeaderTitle>
<SettingsHeaderDescription>
Keys authenticate standalone AI Gateway replicas to this deployment.
The key value is shown only once when created.{" "}
<Link href={docs("/ai-coder/ai-gateway")}>View docs</Link>
</SettingsHeaderDescription>
</SettingsHeader>
{showPaywall && (
<PaywallPremium
message="AI Gateway"
description="Authenticate standalone AI Gateway replicas to your deployment. You need a Premium license with AI Gateway enabled to use this feature."
documentationLink={docs("/ai-coder/ai-gateway")}
/>
)}
{!showPaywall && Boolean(error) && (
<div className="mb-4">
<ErrorAlert error={error} />
</div>
)}
{!showPaywall && (
<Table aria-label="AI Gateway keys">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Key prefix</TableHead>
<TableHead>Last used</TableHead>
<TableHead>Created</TableHead>
<TableHead className="w-8" />
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableLoader />
) : error ? (
<TableEmpty message="Failed to fetch AI Gateway keys" />
) : keys.length === 0 ? (
<TableEmpty
message="No AI Gateway keys"
description="Create a key to authenticate a standalone AI Gateway replica."
cta={
<Button variant="outline" onClick={onCreateKey}>
<PlusIcon />
Create key
</Button>
}
/>
) : (
keys.map((key) => (
<TableRow key={key.id}>
<TableCell>{key.name}</TableCell>
<TableCell>
<span className="font-mono text-content-secondary">
{key.key_prefix}
</span>
</TableCell>
<TableCell>
{key.last_used_at ? (
<span className="block first-letter:uppercase">
{relativeTime(new Date(key.last_used_at))}
</span>
) : (
<span className="text-content-disabled">Never</span>
)}
</TableCell>
<TableCell>
<span className="block first-letter:uppercase">
{relativeTime(new Date(key.created_at))}
</span>
</TableCell>
<TableCell>
<Button
variant="destructive"
size="icon"
aria-label={`Delete ${key.name}`}
onClick={() => onDeleteKey(key)}
>
<TrashIcon className="size-icon-sm" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
)}
</div>
);
};
+23 -1
View File
@@ -11,6 +11,7 @@ import {
import { GlobalErrorBoundary } from "./components/ErrorBoundary/GlobalErrorBoundary";
import { Loader } from "./components/Loader/Loader";
import { RequireAuth } from "./contexts/auth/RequireAuth";
import { useAuthenticated } from "./hooks/useAuthenticated";
import { DashboardLayout } from "./modules/dashboard/DashboardLayout";
import AuditPage from "./pages/AuditPage/AuditPage";
import ConnectionLogPage from "./pages/ConnectionLogPage/ConnectionLogPage";
@@ -446,6 +447,23 @@ const AISettingsAddProviderPage = lazy(
"./pages/AISettingsPage/ProvidersPage/AddProviderPage/AddProviderPage"
),
);
const AISettingsGatewayKeysPage = lazy(
() => import("./pages/AISettingsPage/GatewayKeysPage/GatewayKeysPage"),
);
const AISettingsIndexPage = () => {
const { permissions } = useAuthenticated();
if (permissions.viewAnyAIProvider) {
return <AISettingsProvidersPage />;
}
if (permissions.viewAIGatewayKeys) {
return <Navigate to="/ai/settings/gateway-keys" replace />;
}
return <AISettingsProvidersPage />;
};
const GlobalLayout = () => {
return (
@@ -697,7 +715,11 @@ export const router = createBrowserRouter(
<Route element={<DeploymentConfigProvider />}>
<Route path="governance" element={<AIGovernanceSettingsPage />} />
</Route>
<Route index element={<AISettingsProvidersPage />} />
<Route
path="gateway-keys"
element={<AISettingsGatewayKeysPage />}
/>
<Route index element={<AISettingsIndexPage />} />
<Route path="add" element={<AISettingsAddProviderPage />} />
<Route
path=":providerId"
+27
View File
@@ -3302,6 +3302,7 @@ export const MockPermissions: Permissions = {
viewAnyMembers: true,
viewAnyAIBridgeInterception: true,
viewAnyAIProvider: true,
viewAIGatewayKeys: true,
createOAuth2App: true,
editOAuth2App: true,
deleteOAuth2App: true,
@@ -3337,6 +3338,7 @@ export const MockNoPermissions: Permissions = {
viewAnyMembers: false,
viewAnyAIBridgeInterception: true,
viewAnyAIProvider: false,
viewAIGatewayKeys: false,
createOAuth2App: false,
editOAuth2App: false,
deleteOAuth2App: false,
@@ -5521,3 +5523,28 @@ export const MockAIProviders: TypesGen.AIProvider[] = [
MockAIProviderBedrock,
MockAIProviderCopilot,
];
export const MockAIGatewayKeys: TypesGen.AIGatewayKey[] = [
{
id: "1c2e6f4a-8b3d-4f1a-9c7e-2a5b8d6f0e11",
name: "primary-gateway",
key_prefix: "a1B2c3D4e5F",
created_at: "2024-05-01T14:00:00Z",
last_used_at: "2024-05-20T09:30:00Z",
},
{
id: "2d3f7a5b-9c4e-4a2b-8d6f-3b6c9e7f1a22",
name: "backup-gateway",
key_prefix: "z9Y8x7W6v5U",
created_at: "2024-05-10T08:15:00Z",
},
];
export const MockCreateAIGatewayKeyResponse: TypesGen.CreateAIGatewayKeyResponse =
{
id: "3e4a8b6c-0d5f-4b3c-9e7a-4c7d0f8a2b33",
name: "new-gateway",
key: "K3mNp7qRs2TvW9xY4zA6bC1dE8fG5hJ0kL3nM7pQ2rS",
key_prefix: "K3mNp7qRs2T",
created_at: "2024-05-28T12:00:00Z",
};