Delete custom provider (#137)

This commit is contained in:
Will Chen
2025-05-12 17:31:03 -07:00
committed by GitHub
parent ea9301c771
commit f5a6a1abca
5 changed files with 182 additions and 5 deletions

View File

@@ -9,17 +9,36 @@ import { providerSettingsRoute } from "@/routes/settings/providers/$provider";
import type { LanguageModelProvider } from "@/ipc/ipc_types"; import type { LanguageModelProvider } from "@/ipc/ipc_types";
import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders"; import { useLanguageModelProviders } from "@/hooks/useLanguageModelProviders";
import { GiftIcon, PlusIcon } from "lucide-react"; import { useCustomLanguageModelProvider } from "@/hooks/useCustomLanguageModelProvider";
import { GiftIcon, PlusIcon, MoreVertical, Trash2 } from "lucide-react";
import { Skeleton } from "./ui/skeleton"; import { Skeleton } from "./ui/skeleton";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog"; import { CreateCustomProviderDialog } from "./CreateCustomProviderDialog";
export function ProviderSettingsGrid() { export function ProviderSettingsGrid() {
const navigate = useNavigate(); const navigate = useNavigate();
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [providerToDelete, setProviderToDelete] = useState<string | null>(null);
const { const {
data: providers, data: providers,
@@ -29,6 +48,8 @@ export function ProviderSettingsGrid() {
refetch, refetch,
} = useLanguageModelProviders(); } = useLanguageModelProviders();
const { deleteProvider, isDeleting } = useCustomLanguageModelProvider();
const handleProviderClick = (providerId: string) => { const handleProviderClick = (providerId: string) => {
navigate({ navigate({
to: providerSettingsRoute.id, to: providerSettingsRoute.id,
@@ -36,6 +57,14 @@ export function ProviderSettingsGrid() {
}); });
}; };
const handleDeleteProvider = async () => {
if (providerToDelete) {
await deleteProvider(providerToDelete);
setProviderToDelete(null);
refetch();
}
};
if (isLoading) { if (isLoading) {
return ( return (
<div className="p-6"> <div className="p-6">
@@ -74,13 +103,17 @@ export function ProviderSettingsGrid() {
<h2 className="text-2xl font-bold mb-6">AI Providers</h2> <h2 className="text-2xl font-bold mb-6">AI Providers</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{providers?.map((provider: LanguageModelProvider) => { {providers?.map((provider: LanguageModelProvider) => {
const isCustom = provider.type === "custom";
return ( return (
<Card <Card
key={provider.id} key={provider.id}
className="cursor-pointer transition-all hover:shadow-md border-border" className="relative transition-all hover:shadow-md border-border"
onClick={() => handleProviderClick(provider.id)}
> >
<CardHeader className="p-4"> <CardHeader
className="p-4 cursor-pointer"
onClick={() => handleProviderClick(provider.id)}
>
<CardTitle className="text-xl flex items-center justify-between"> <CardTitle className="text-xl flex items-center justify-between">
{provider.name} {provider.name}
{isProviderSetup(provider.id) ? ( {isProviderSetup(provider.id) ? (
@@ -102,6 +135,30 @@ export function ProviderSettingsGrid() {
)} )}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
{isCustom && (
<div
className="absolute top-2 right-2"
onClick={(e) => e.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger className="focus:outline-none">
<div className="p-1 hover:bg-muted rounded-full">
<MoreVertical className="h-4 w-4 text-muted-foreground" />
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => setProviderToDelete(provider.id)}
>
<Trash2 className="h-4 w-4 mr-2" />
Delete Provider
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Card> </Card>
); );
})} })}
@@ -131,6 +188,30 @@ export function ProviderSettingsGrid() {
refetch(); refetch();
}} }}
/> />
<AlertDialog
open={!!providerToDelete}
onOpenChange={(open) => !open && setProviderToDelete(null)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Custom Provider</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this custom provider and all its
associated models. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteProvider}
disabled={isDeleting}
>
{isDeleting ? "Deleting..." : "Delete Provider"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div> </div>
); );
} }

View File

@@ -40,15 +40,38 @@ export function useCustomLanguageModelProvider() {
}, },
}); });
const deleteProviderMutation = useMutation({
mutationFn: async (providerId: string): Promise<void> => {
if (!providerId) {
throw new Error("Provider ID is required");
}
return ipcClient.deleteCustomLanguageModelProvider(providerId);
},
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ["languageModelProviders"] });
},
onError: (error) => {
showError(error);
},
});
const createProvider = async ( const createProvider = async (
params: CreateCustomLanguageModelProviderParams, params: CreateCustomLanguageModelProviderParams,
): Promise<LanguageModelProvider> => { ): Promise<LanguageModelProvider> => {
return createProviderMutation.mutateAsync(params); return createProviderMutation.mutateAsync(params);
}; };
const deleteProvider = async (providerId: string): Promise<void> => {
return deleteProviderMutation.mutateAsync(providerId);
};
return { return {
createProvider, createProvider,
deleteProvider,
isCreating: createProviderMutation.isPending, isCreating: createProviderMutation.isPending,
error: createProviderMutation.error, isDeleting: deleteProviderMutation.isPending,
error: createProviderMutation.error || deleteProviderMutation.error,
}; };
} }

View File

@@ -204,6 +204,72 @@ export function registerLanguageModelHandlers() {
}, },
); );
handle(
"delete-custom-language-model-provider",
async (
event: IpcMainInvokeEvent,
params: { providerId: string },
): Promise<void> => {
const { providerId } = params;
// Validation
if (!providerId) {
throw new Error("Provider ID is required");
}
logger.info(
`Handling delete-custom-language-model-provider for providerId: ${providerId}`,
);
// Check if the provider exists before attempting deletion
const existingProvider = await db
.select({ id: languageModelProvidersSchema.id })
.from(languageModelProvidersSchema)
.where(eq(languageModelProvidersSchema.id, providerId))
.get();
if (!existingProvider) {
// If the provider doesn't exist, maybe it was already deleted. Log and return.
logger.warn(
`Provider with ID "${providerId}" not found. It might have been deleted already.`,
);
// Optionally, throw new Error(`Provider with ID "${providerId}" not found`);
// Deciding to return gracefully instead of throwing an error if not found.
return;
}
// Use a transaction to ensure atomicity
await db.transaction(async (tx) => {
// 1. Delete associated models
const deleteModelsResult = await tx
.delete(languageModelsSchema)
.where(eq(languageModelsSchema.provider_id, providerId))
.run();
logger.info(
`Deleted ${deleteModelsResult.changes} model(s) associated with provider ${providerId}`,
);
// 2. Delete the provider
const deleteProviderResult = await tx
.delete(languageModelProvidersSchema)
.where(eq(languageModelProvidersSchema.id, providerId))
.run();
if (deleteProviderResult.changes === 0) {
// This case should ideally not happen if existingProvider check passed,
// but adding safety check within transaction.
logger.error(
`Failed to delete provider with ID "${providerId}" during transaction, although it was found initially. Rolling back.`,
);
throw new Error(
`Failed to delete provider with ID "${providerId}" which should have existed.`,
);
}
logger.info(`Successfully deleted provider with ID "${providerId}".`);
});
},
);
handle( handle(
"get-language-models", "get-language-models",
async ( async (

View File

@@ -774,6 +774,12 @@ export class IpcClient {
return this.ipcRenderer.invoke("delete-custom-model", params); return this.ipcRenderer.invoke("delete-custom-model", params);
} }
async deleteCustomLanguageModelProvider(providerId: string): Promise<void> {
return this.ipcRenderer.invoke("delete-custom-language-model-provider", {
providerId,
});
}
// --- End window control methods --- // --- End window control methods ---
// --- Language Model Operations --- // --- Language Model Operations ---

View File

@@ -8,6 +8,7 @@ const validInvokeChannels = [
"get-language-models", "get-language-models",
"create-custom-language-model", "create-custom-language-model",
"get-language-model-providers", "get-language-model-providers",
"delete-custom-language-model-provider",
"create-custom-language-model-provider", "create-custom-language-model-provider",
"delete-custom-language-model", "delete-custom-language-model",
"delete-custom-model", "delete-custom-model",