[Feature] UI - Projects: Add delete project action to projects table

Add the ability to delete projects from the projects list page:
- New useDeleteProject hook calling DELETE /project/delete
- Actions column with TableIconActionButton (Delete variant)
- DeleteResourceModal with confirmation requiring project name input
- Alert warns that keys must be unlinked before deletion

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-02-28 09:40:15 -08:00
parent 13ea00acdd
commit 926d95cd19
2 changed files with 101 additions and 0 deletions

View file

@ -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<void> => {
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<void, Error, string[]>({
mutationFn: async (projectIds) => {
if (!accessToken) {
throw new Error("Access token is required");
}
return deleteProjects(accessToken, projectIds);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: projectKeys.all });
},
});
};

View file

@ -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<string | null>(null);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<ProjectResponse | null>(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) => (
<TableIconActionButton
variant="Delete"
tooltipText="Delete project"
onClick={() => setProjectToDelete(record)}
/>
),
},
];
if (selectedProjectId) {
@ -231,6 +250,34 @@ export function ProjectsPage() {
isOpen={isCreateModalVisible}
onClose={() => setIsCreateModalVisible(false)}
/>
<DeleteResourceModal
isOpen={projectToDelete !== null}
title="Delete Project"
alertMessage="This action is irreversible. All keys must be unlinked from this project before it can be deleted."
message="Are you sure you want to delete this project?"
resourceInformationTitle="Project Information"
resourceInformation={[
{ label: "Name", value: projectToDelete?.project_alias || "—" },
{ label: "Project ID", value: projectToDelete?.project_id, code: true },
{ label: "Team", value: teamAliasMap.get(projectToDelete?.team_id ?? "") || projectToDelete?.team_id || "—" },
]}
onCancel={() => 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}
/>
</Content>
);
}