mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
test(site): cover provider delete cascade
This commit is contained in:
+175
-5
@@ -1,7 +1,16 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, screen, userEvent, within } from "storybook/test";
|
||||
import {
|
||||
expect,
|
||||
screen,
|
||||
spyOn,
|
||||
userEvent,
|
||||
waitFor,
|
||||
within,
|
||||
} from "storybook/test";
|
||||
import { reactRouterParameters } from "storybook-addon-remix-react-router";
|
||||
import type { AIProvider } from "#/api/typesGenerated";
|
||||
import { API } from "#/api/api";
|
||||
import { chatModelConfigs } from "#/api/queries/chats";
|
||||
import type { AIProvider, ChatModelConfig } from "#/api/typesGenerated";
|
||||
import {
|
||||
MockAIProviderAnthropic,
|
||||
MockAIProviderBedrock,
|
||||
@@ -19,8 +28,58 @@ const routingFor = (path: string) =>
|
||||
],
|
||||
});
|
||||
|
||||
const seed = (provider: AIProvider) => ({
|
||||
queries: [{ key: ["ai", "providers", provider.name], data: provider }],
|
||||
const model = (
|
||||
overrides: Partial<ChatModelConfig> &
|
||||
Pick<ChatModelConfig, "id" | "provider" | "model">,
|
||||
): ChatModelConfig => ({
|
||||
id: overrides.id,
|
||||
provider: overrides.provider,
|
||||
ai_provider_id: overrides.ai_provider_id,
|
||||
model: overrides.model,
|
||||
display_name: overrides.display_name ?? overrides.model,
|
||||
enabled: overrides.enabled ?? true,
|
||||
is_default: overrides.is_default ?? false,
|
||||
context_limit: overrides.context_limit ?? 200000,
|
||||
compression_threshold: overrides.compression_threshold ?? 70,
|
||||
model_config: overrides.model_config,
|
||||
created_at: overrides.created_at ?? "2026-05-14T10:00:00Z",
|
||||
updated_at: overrides.updated_at ?? "2026-05-14T10:00:00Z",
|
||||
});
|
||||
|
||||
const seed = (
|
||||
provider: AIProvider,
|
||||
models: readonly ChatModelConfig[] = [],
|
||||
) => ({
|
||||
queries: [
|
||||
{ key: ["ai", "providers", provider.name], data: provider },
|
||||
{ key: chatModelConfigs().queryKey, data: models },
|
||||
],
|
||||
});
|
||||
|
||||
const openAIAssociatedModels = [
|
||||
model({
|
||||
id: "model-openai-default",
|
||||
provider: "openai",
|
||||
ai_provider_id: MockAIProviderOpenAI.id,
|
||||
model: "gpt-4o",
|
||||
display_name: "GPT-4o",
|
||||
is_default: true,
|
||||
}),
|
||||
model({
|
||||
id: "model-openai-secondary",
|
||||
provider: "openai",
|
||||
ai_provider_id: MockAIProviderOpenAI.id,
|
||||
model: "gpt-4o-mini",
|
||||
display_name: "GPT-4o Mini",
|
||||
}),
|
||||
] satisfies readonly ChatModelConfig[];
|
||||
|
||||
const anthropicFallbackModel = model({
|
||||
id: "model-anthropic-fallback",
|
||||
provider: "anthropic",
|
||||
ai_provider_id: MockAIProviderAnthropic.id,
|
||||
model: "claude-sonnet-4",
|
||||
display_name: "Claude Sonnet 4",
|
||||
});
|
||||
|
||||
const meta: Meta<typeof UpdateProviderPageView> = {
|
||||
@@ -71,9 +130,120 @@ export const DeleteDialogOpen: Story = {
|
||||
name: /^delete$/i,
|
||||
});
|
||||
await userEvent.click(deleteButton);
|
||||
// DeleteDialog renders via Radix portal, so search the document, not
|
||||
// The dialog renders via Radix portal, so search the document, not
|
||||
// just the story canvas.
|
||||
await expect(await screen.findByRole("dialog")).toBeInTheDocument();
|
||||
await expect(await screen.findByText(/irreversible/i)).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const DeleteDialogWithAssociatedModels: Story = {
|
||||
parameters: {
|
||||
reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
|
||||
...seed(MockAIProviderOpenAI, openAIAssociatedModels),
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByRole("button", { name: /^delete$/i }),
|
||||
);
|
||||
|
||||
await expect(
|
||||
await screen.findByText(/Deleting this provider will also disable/i),
|
||||
).toBeInTheDocument();
|
||||
await expect(screen.getByText("2 models")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const DeleteDialogCascadeConfirmed: Story = {
|
||||
parameters: {
|
||||
reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
|
||||
...seed(MockAIProviderOpenAI, [
|
||||
...openAIAssociatedModels,
|
||||
anthropicFallbackModel,
|
||||
]),
|
||||
},
|
||||
beforeEach: () => {
|
||||
spyOn(API.experimental, "updateChatModelConfig").mockResolvedValue(
|
||||
anthropicFallbackModel,
|
||||
);
|
||||
spyOn(API, "deleteAIProvider").mockResolvedValue(undefined);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByRole("button", { name: /^delete$/i }),
|
||||
);
|
||||
await userEvent.type(
|
||||
await screen.findByLabelText(
|
||||
`Type ${MockAIProviderOpenAI.name} to confirm`,
|
||||
),
|
||||
MockAIProviderOpenAI.name,
|
||||
);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "Delete provider" }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.experimental.updateChatModelConfig).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
expect(API.experimental.updateChatModelConfig).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"model-openai-default",
|
||||
{ enabled: false },
|
||||
);
|
||||
expect(API.experimental.updateChatModelConfig).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"model-openai-secondary",
|
||||
{ enabled: false },
|
||||
);
|
||||
expect(API.experimental.updateChatModelConfig).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"model-anthropic-fallback",
|
||||
{ is_default: true },
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(API.deleteAIProvider).toHaveBeenCalledWith(
|
||||
MockAIProviderOpenAI.name,
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const DeleteDialogCascadeFailure: Story = {
|
||||
parameters: {
|
||||
reactRouter: routingFor(`/ai/settings/${MockAIProviderOpenAI.name}`),
|
||||
...seed(MockAIProviderOpenAI, [
|
||||
...openAIAssociatedModels,
|
||||
anthropicFallbackModel,
|
||||
]),
|
||||
},
|
||||
beforeEach: () => {
|
||||
spyOn(API.experimental, "updateChatModelConfig")
|
||||
.mockResolvedValueOnce(anthropicFallbackModel)
|
||||
.mockRejectedValueOnce(new Error("Failed to disable model."));
|
||||
spyOn(API, "deleteAIProvider").mockResolvedValue(undefined);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
await canvas.findByRole("button", { name: /^delete$/i }),
|
||||
);
|
||||
await userEvent.type(
|
||||
await screen.findByLabelText(
|
||||
`Type ${MockAIProviderOpenAI.name} to confirm`,
|
||||
),
|
||||
MockAIProviderOpenAI.name,
|
||||
);
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: "Delete provider" }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(API.experimental.updateChatModelConfig).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
expect(API.deleteAIProvider).not.toHaveBeenCalled();
|
||||
await expect(await screen.findByRole("dialog")).toBeInTheDocument();
|
||||
await expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
|
||||
},
|
||||
};
|
||||
|
||||
+11
-28
@@ -21,6 +21,7 @@ import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -30,6 +31,7 @@ import { Loader } from "#/components/Loader/Loader";
|
||||
import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { Switch } from "#/components/Switch/Switch";
|
||||
import { cascadeDisableProviderModels } from "#/pages/AISettingsPage/utils/providerDelete";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { ProviderForm } from "../components/ProviderForm";
|
||||
import { getProviderIcon } from "../components/ProviderIcon";
|
||||
@@ -247,11 +249,11 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
<DialogContent variant="destructive">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete provider</DialogTitle>
|
||||
<DialogDescription>
|
||||
Deleting this provider is irreversible!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-3 text-sm text-content-secondary">
|
||||
<p className="m-0 font-medium text-content-destructive">
|
||||
Deleting this provider is irreversible!
|
||||
</p>
|
||||
{associatedModelCount > 0 && (
|
||||
<ul className="m-0 pl-5">
|
||||
<li>
|
||||
@@ -306,31 +308,12 @@ const UpdateProviderPageView: React.FC = () => {
|
||||
const deleteAll = async () => {
|
||||
setIsCascadeDeleting(true);
|
||||
try {
|
||||
const hadDefault = associatedModels.some(
|
||||
(mc) => mc.is_default,
|
||||
);
|
||||
const disabledIds = new Set(
|
||||
associatedModels.map((mc) => mc.id),
|
||||
);
|
||||
for (const mc of associatedModels) {
|
||||
await API.experimental.updateChatModelConfig(mc.id, {
|
||||
enabled: false,
|
||||
});
|
||||
}
|
||||
if (hadDefault) {
|
||||
const allModels = modelConfigsQuery.data ?? [];
|
||||
const newDefault = allModels.find(
|
||||
(mc) => mc.enabled && !disabledIds.has(mc.id),
|
||||
);
|
||||
if (newDefault) {
|
||||
await API.experimental.updateChatModelConfig(
|
||||
newDefault.id,
|
||||
{
|
||||
is_default: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
await cascadeDisableProviderModels({
|
||||
associatedModels,
|
||||
allModels: modelConfigsQuery.data ?? [],
|
||||
updateModelConfig:
|
||||
API.experimental.updateChatModelConfig,
|
||||
});
|
||||
await invalidateChatConfigurationQueries(queryClient);
|
||||
deleteMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { cascadeDisableProviderModels } from "./providerDelete";
|
||||
|
||||
const model = (
|
||||
overrides: Partial<TypesGen.ChatModelConfig> &
|
||||
Pick<TypesGen.ChatModelConfig, "id" | "provider" | "model">,
|
||||
): TypesGen.ChatModelConfig => ({
|
||||
id: overrides.id,
|
||||
provider: overrides.provider,
|
||||
ai_provider_id: overrides.ai_provider_id,
|
||||
model: overrides.model,
|
||||
display_name: overrides.display_name ?? overrides.model,
|
||||
enabled: overrides.enabled ?? true,
|
||||
is_default: overrides.is_default ?? false,
|
||||
context_limit: overrides.context_limit ?? 200000,
|
||||
compression_threshold: overrides.compression_threshold ?? 70,
|
||||
model_config: overrides.model_config,
|
||||
created_at: overrides.created_at ?? "2026-02-18T12:00:00.000Z",
|
||||
updated_at: overrides.updated_at ?? "2026-02-18T12:00:00.000Z",
|
||||
});
|
||||
|
||||
describe("cascadeDisableProviderModels", () => {
|
||||
it("disables associated models before reassigning the default", async () => {
|
||||
const associatedModels = [
|
||||
model({
|
||||
id: "model-associated-default",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
is_default: true,
|
||||
}),
|
||||
model({
|
||||
id: "model-associated-secondary",
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
}),
|
||||
];
|
||||
const allModels = [
|
||||
...associatedModels,
|
||||
model({
|
||||
id: "model-next-default",
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4",
|
||||
}),
|
||||
];
|
||||
const updateModelConfig = vi.fn(async () => undefined);
|
||||
|
||||
await cascadeDisableProviderModels({
|
||||
associatedModels,
|
||||
allModels,
|
||||
updateModelConfig,
|
||||
});
|
||||
|
||||
expect(updateModelConfig).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"model-associated-default",
|
||||
{ enabled: false },
|
||||
);
|
||||
expect(updateModelConfig).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"model-associated-secondary",
|
||||
{ enabled: false },
|
||||
);
|
||||
expect(updateModelConfig).toHaveBeenNthCalledWith(3, "model-next-default", {
|
||||
is_default: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not reassign the default when the deleted provider had no default", async () => {
|
||||
const updateModelConfig = vi.fn(async () => undefined);
|
||||
|
||||
await cascadeDisableProviderModels({
|
||||
associatedModels: [
|
||||
model({
|
||||
id: "model-associated",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
}),
|
||||
],
|
||||
allModels: [
|
||||
model({
|
||||
id: "model-associated",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
}),
|
||||
model({
|
||||
id: "model-other-default",
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4",
|
||||
is_default: true,
|
||||
}),
|
||||
],
|
||||
updateModelConfig,
|
||||
});
|
||||
|
||||
expect(updateModelConfig).toHaveBeenCalledTimes(1);
|
||||
expect(updateModelConfig).toHaveBeenCalledWith("model-associated", {
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("stops before reassigning the default when a model disable fails", async () => {
|
||||
const error = new Error("failed to disable model");
|
||||
const updateModelConfig = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(error);
|
||||
|
||||
await expect(
|
||||
cascadeDisableProviderModels({
|
||||
associatedModels: [
|
||||
model({
|
||||
id: "model-associated-default",
|
||||
provider: "openai",
|
||||
model: "gpt-4o",
|
||||
is_default: true,
|
||||
}),
|
||||
model({
|
||||
id: "model-associated-secondary",
|
||||
provider: "openai",
|
||||
model: "gpt-4o-mini",
|
||||
}),
|
||||
],
|
||||
allModels: [
|
||||
model({
|
||||
id: "model-next-default",
|
||||
provider: "anthropic",
|
||||
model: "claude-sonnet-4",
|
||||
}),
|
||||
],
|
||||
updateModelConfig,
|
||||
}),
|
||||
).rejects.toThrow(error);
|
||||
|
||||
expect(updateModelConfig).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
|
||||
type UpdateModelConfig = (
|
||||
modelConfigId: string,
|
||||
req: TypesGen.UpdateChatModelConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
|
||||
export const cascadeDisableProviderModels = async ({
|
||||
associatedModels,
|
||||
allModels,
|
||||
updateModelConfig,
|
||||
}: {
|
||||
associatedModels: readonly TypesGen.ChatModelConfig[];
|
||||
allModels: readonly TypesGen.ChatModelConfig[];
|
||||
updateModelConfig: UpdateModelConfig;
|
||||
}) => {
|
||||
const disabledIds = new Set(associatedModels.map((model) => model.id));
|
||||
const hadDefault = associatedModels.some((model) => model.is_default);
|
||||
|
||||
for (const model of associatedModels) {
|
||||
await updateModelConfig(model.id, { enabled: false });
|
||||
}
|
||||
|
||||
if (!hadDefault) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newDefault = allModels.find(
|
||||
(model) => model.enabled && !disabledIds.has(model.id),
|
||||
);
|
||||
if (newDefault) {
|
||||
await updateModelConfig(newDefault.id, { is_default: true });
|
||||
}
|
||||
};
|
||||
+70
-2
@@ -2264,6 +2264,23 @@ export const ProviderDeleteConfirmation: Story = {
|
||||
has_api_key: true,
|
||||
}),
|
||||
],
|
||||
modelConfigsData: [
|
||||
createModelConfig({
|
||||
id: "model-openai-default",
|
||||
provider: "openai",
|
||||
ai_provider_id: "provider-openai",
|
||||
model: "gpt-4o",
|
||||
display_name: "GPT-4o",
|
||||
is_default: true,
|
||||
}),
|
||||
createModelConfig({
|
||||
id: "model-openai-secondary",
|
||||
provider: "openai",
|
||||
ai_provider_id: "provider-openai",
|
||||
model: "gpt-4o-mini",
|
||||
display_name: "GPT-4o Mini",
|
||||
}),
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const body = within(canvasElement.ownerDocument.body);
|
||||
@@ -2278,8 +2295,9 @@ export const ProviderDeleteConfirmation: Story = {
|
||||
// The confirmation dialog should appear - leave it visible
|
||||
// so the Chromatic snapshot captures this state.
|
||||
await expect(
|
||||
await body.findByText(/Are you sure you want to delete this provider/i),
|
||||
await body.findByText(/Deleting this provider will also disable/i),
|
||||
).toBeInTheDocument();
|
||||
expect(body.getByText(/2 models/i)).toBeInTheDocument();
|
||||
await expect(body.getByRole("dialog")).toBeInTheDocument();
|
||||
await expect(
|
||||
body.getByRole("button", { name: "Delete provider" }),
|
||||
@@ -2336,6 +2354,37 @@ export const ProviderDeleteConfirmed: Story = {
|
||||
source: "database",
|
||||
has_api_key: true,
|
||||
}),
|
||||
createProviderConfig({
|
||||
id: "provider-anthropic",
|
||||
provider: "anthropic",
|
||||
display_name: "Anthropic",
|
||||
source: "database",
|
||||
has_api_key: true,
|
||||
}),
|
||||
],
|
||||
modelConfigsData: [
|
||||
createModelConfig({
|
||||
id: "model-openai-default",
|
||||
provider: "openai",
|
||||
ai_provider_id: "provider-openai",
|
||||
model: "gpt-4o",
|
||||
display_name: "GPT-4o",
|
||||
is_default: true,
|
||||
}),
|
||||
createModelConfig({
|
||||
id: "model-openai-secondary",
|
||||
provider: "openai",
|
||||
ai_provider_id: "provider-openai",
|
||||
model: "gpt-4o-mini",
|
||||
display_name: "GPT-4o Mini",
|
||||
}),
|
||||
createModelConfig({
|
||||
id: "model-anthropic-fallback",
|
||||
provider: "anthropic",
|
||||
ai_provider_id: "provider-anthropic",
|
||||
model: "claude-sonnet-4",
|
||||
display_name: "Claude Sonnet 4",
|
||||
}),
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
@@ -2348,7 +2397,26 @@ export const ProviderDeleteConfirmed: Story = {
|
||||
await body.findByRole("button", { name: "Delete provider" }),
|
||||
);
|
||||
|
||||
// The delete callback should have been called.
|
||||
await waitFor(() => {
|
||||
expect(args.onUpdateModel).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
expect(args.onUpdateModel).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"model-openai-default",
|
||||
{
|
||||
enabled: false,
|
||||
},
|
||||
);
|
||||
expect(args.onUpdateModel).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"model-openai-secondary",
|
||||
{ enabled: false },
|
||||
);
|
||||
expect(args.onUpdateModel).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
"model-anthropic-fallback",
|
||||
{ is_default: true },
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(args.onDeleteProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -324,7 +324,7 @@ export const ChatModelAdminPanel: FC<ChatModelAdminPanelProps> = ({
|
||||
onCreateProvider={onCreateProvider}
|
||||
onUpdateProvider={onUpdateProvider}
|
||||
onDeleteProvider={onDeleteProvider}
|
||||
onDisableModel={onUpdateModel}
|
||||
onUpdateModel={onUpdateModel}
|
||||
allModelConfigs={modelConfigs}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -7,9 +7,11 @@ import {
|
||||
useId,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useQueryClient } from "react-query";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { getErrorMessage } from "#/api/errors";
|
||||
import { invalidateChatConfigurationQueries } from "#/api/queries/chats";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "#/components/Tooltip/Tooltip";
|
||||
import { cascadeDisableProviderModels } from "#/pages/AISettingsPage/utils/providerDelete";
|
||||
import { formatProviderLabel } from "../../utils/modelOptions";
|
||||
import { BackButton } from "../BackButton";
|
||||
import { ConfirmDeleteDialog } from "../ConfirmDeleteDialog";
|
||||
@@ -47,7 +50,7 @@ interface ProviderFormProps {
|
||||
req: TypesGen.UpdateChatProviderConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
onDeleteProvider: (providerConfigId: string) => Promise<void>;
|
||||
onDisableModel: (
|
||||
onUpdateModel: (
|
||||
modelConfigId: string,
|
||||
req: TypesGen.UpdateChatModelConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
@@ -62,11 +65,12 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
onCreateProvider,
|
||||
onUpdateProvider,
|
||||
onDeleteProvider,
|
||||
onDisableModel,
|
||||
onUpdateModel,
|
||||
allModelConfigs,
|
||||
onBack,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const { provider, providerConfig, baseURL, isEnvPreset } = providerState;
|
||||
|
||||
const apiKeyInputId = useId();
|
||||
@@ -383,36 +387,25 @@ export const ProviderForm: FC<ProviderFormProps> = ({
|
||||
entity="provider"
|
||||
description={deleteProviderDescription}
|
||||
onConfirm={() => {
|
||||
setIsCascadeDeleting(true);
|
||||
const disabledIds = new Set(
|
||||
providerState.modelConfigs.map((mc) => mc.id),
|
||||
);
|
||||
const hadDefault = providerState.modelConfigs.some(
|
||||
(mc) => mc.is_default,
|
||||
);
|
||||
const chain = providerState.modelConfigs.reduce<Promise<unknown>>(
|
||||
(prev, mc) =>
|
||||
prev.then(() => onDisableModel(mc.id, { enabled: false })),
|
||||
Promise.resolve(),
|
||||
);
|
||||
chain
|
||||
.then(() => {
|
||||
if (hadDefault) {
|
||||
const newDefault = allModelConfigs.find(
|
||||
(mc) => mc.enabled && !disabledIds.has(mc.id),
|
||||
);
|
||||
if (newDefault) {
|
||||
return onDisableModel(newDefault.id, { is_default: true });
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(() => onDeleteProvider(providerConfig.id))
|
||||
.catch((error: unknown) => {
|
||||
const deleteProvider = async () => {
|
||||
setIsCascadeDeleting(true);
|
||||
try {
|
||||
await cascadeDisableProviderModels({
|
||||
associatedModels: providerState.modelConfigs,
|
||||
allModels: allModelConfigs,
|
||||
updateModelConfig: onUpdateModel,
|
||||
});
|
||||
await onDeleteProvider(providerConfig.id);
|
||||
setIsCascadeDeleting(false);
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
getErrorMessage(error, "Failed to delete provider."),
|
||||
);
|
||||
})
|
||||
.then(() => setIsCascadeDeleting(false));
|
||||
setIsCascadeDeleting(false);
|
||||
await invalidateChatConfigurationQueries(queryClient);
|
||||
}
|
||||
};
|
||||
void deleteProvider();
|
||||
}}
|
||||
isPending={isCascadeDeleting || isProviderMutationPending}
|
||||
open={confirmingDelete}
|
||||
|
||||
@@ -81,7 +81,7 @@ interface ProvidersSectionProps {
|
||||
req: UpdateChatProviderConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
onDeleteProvider: (providerConfigId: string) => Promise<void>;
|
||||
onDisableModel: (
|
||||
onUpdateModel: (
|
||||
modelConfigId: string,
|
||||
req: UpdateChatModelConfigRequest,
|
||||
) => Promise<unknown>;
|
||||
@@ -97,7 +97,7 @@ export const ProvidersSection: FC<ProvidersSectionProps> = ({
|
||||
onCreateProvider,
|
||||
onUpdateProvider,
|
||||
onDeleteProvider,
|
||||
onDisableModel,
|
||||
onUpdateModel,
|
||||
allModelConfigs,
|
||||
}) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
@@ -207,7 +207,7 @@ export const ProvidersSection: FC<ProvidersSectionProps> = ({
|
||||
}
|
||||
}}
|
||||
onBack={clearProviderView}
|
||||
onDisableModel={onDisableModel}
|
||||
onUpdateModel={onUpdateModel}
|
||||
allModelConfigs={allModelConfigs}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user