Delete custom provider (#137)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user