diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.ts new file mode 100644 index 00000000000..5abf9e03be2 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/projects/useDeleteProject.ts @@ -0,0 +1,54 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { + getProxyBaseUrl, + getGlobalLitellmHeaderName, + deriveErrorMessage, + handleError, +} from "@/components/networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { projectKeys } from "./useProjects"; + +// ── Fetch function ─────────────────────────────────────────────────────────── + +const deleteProjects = async ( + accessToken: string, + projectIds: string[], +): Promise => { + const baseUrl = getProxyBaseUrl(); + const url = `${baseUrl}/project/delete`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ project_ids: projectIds }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } +}; + +// ── Hook ───────────────────────────────────────────────────────────────────── + +export const useDeleteProject = () => { + const { accessToken } = useAuthorized(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (projectIds) => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return deleteProjects(accessToken, projectIds); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: projectKeys.all }); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx b/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx index 9c75e19ac4e..9a41b63d011 100644 --- a/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx +++ b/ui/litellm-dashboard/src/components/Projects/ProjectsPage.tsx @@ -1,3 +1,4 @@ +import { useDeleteProject } from "@/app/(dashboard)/hooks/projects/useDeleteProject"; import { useProjects, ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects"; import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { LoadingOutlined, PlusOutlined } from "@ant-design/icons"; @@ -8,6 +9,7 @@ import { Flex, Input, Layout, + message, Pagination, Space, Spin, @@ -20,6 +22,8 @@ import { import type { ColumnsType } from "antd/es/table"; import { LayersIcon, SearchIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; +import DeleteResourceModal from "../common_components/DeleteResourceModal"; +import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton"; import { CreateProjectModal } from "./ProjectModals/CreateProjectModal"; import { ProjectDetail } from "./ProjectDetailsPage"; @@ -31,8 +35,11 @@ export function ProjectsPage() { const { data: projects, isLoading } = useProjects(); const { data: teams, isLoading: isTeamsLoading } = useTeams(); + const deleteMutation = useDeleteProject(); + const [selectedProjectId, setSelectedProjectId] = useState(null); const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); + const [projectToDelete, setProjectToDelete] = useState(null); const [searchText, setSearchText] = useState(""); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; @@ -151,6 +158,18 @@ export function ProjectsPage() { responsive: ["xl"], render: (date: string) => new Date(date).toLocaleDateString(), }, + { + title: "Actions", + key: "actions", + width: 80, + render: (_: unknown, record: ProjectResponse) => ( + setProjectToDelete(record)} + /> + ), + }, ]; if (selectedProjectId) { @@ -231,6 +250,34 @@ export function ProjectsPage() { isOpen={isCreateModalVisible} onClose={() => setIsCreateModalVisible(false)} /> + + setProjectToDelete(null)} + onOk={() => { + if (!projectToDelete) return; + deleteMutation.mutate([projectToDelete.project_id], { + onSuccess: () => { + message.success("Project deleted successfully"); + setProjectToDelete(null); + }, + onError: (error) => { + message.error(error.message || "Failed to delete project"); + }, + }); + }} + confirmLoading={deleteMutation.isPending} + requiredConfirmation={projectToDelete?.project_alias ?? undefined} + /> ); }