- {lastRefreshed &&
Last Refreshed: {lastRefreshed}}
+
+ {lastRefreshed && Last Refreshed: {lastRefreshed}}
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx
index 34c1c3ca4b1..b7d4db26189 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx
@@ -1,8 +1,31 @@
import * as useAuthorizedModule from "@/app/(dashboard)/hooks/useAuthorized";
-import { renderWithProviders, screen, waitFor } from "../../../../../tests/test-utils";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { renderWithProviders } from "../../../../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AllModelsTab from "./AllModelsTab";
+// Mock modelDeleteCall
+const mockModelDeleteCall = vi.fn().mockResolvedValue({});
+vi.mock("@/components/networking", () => ({
+ modelDeleteCall: (...args: any[]) => mockModelDeleteCall(...args),
+}));
+
+// Mock NotificationsManager
+vi.mock("@/components/molecules/notifications_manager", () => ({
+ default: {
+ success: vi.fn(),
+ fromBackend: vi.fn(),
+ },
+}));
+
+// Mock react-query
+const mockInvalidateQueries = vi.fn();
+vi.mock("@tanstack/react-query", () => ({
+ useQueryClient: () => ({
+ invalidateQueries: mockInvalidateQueries,
+ }),
+}));
+
// Mock the useModelsInfo hook
const mockUseModelsInfo = vi.fn(() => ({
data: { data: [], total_count: 0, current_page: 1, total_pages: 1, size: 50 },
@@ -493,4 +516,101 @@ describe("AllModelsTab", () => {
const previousButton = screen.getByRole("button", { name: /previous/i });
expect(previousButton).toBeDisabled();
});
+
+ it("should pass setDeleteModalModelId to columns for delete functionality", async () => {
+ // This test verifies that the delete modal setter is passed to columns
+ // The actual modal rendering is handled by DeleteResourceModal component
+ mockUseTeams.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ refetch: vi.fn(),
+ });
+
+ mockUseModelCostMap.mockReturnValue(
+ createModelCostMapMock({
+ "gpt-4-delete-test": { litellm_provider: "openai" },
+ }),
+ );
+
+ const modelData = createPaginatedModelData([
+ {
+ model_name: "gpt-4-delete-test",
+ litellm_model_name: "gpt-4-delete-test",
+ provider: "openai",
+ model_info: {
+ id: "model-to-delete",
+ db_model: true,
+ direct_access: true,
+ access_via_team_ids: [],
+ access_groups: [],
+ created_by: "user-123",
+ created_at: "2024-01-01",
+ updated_at: "2024-01-01",
+ },
+ },
+ ], 1, 1, 1, 50);
+
+ mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(screen.getByText("gpt-4-delete-test")).toBeInTheDocument();
+ });
+
+ // Verify the DB Model badge is shown (indicating it can be deleted)
+ expect(screen.getByText("DB Model")).toBeInTheDocument();
+ });
+
+ it("should render clickable model ID that calls setSelectedModelId", async () => {
+ mockUseTeams.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ refetch: vi.fn(),
+ });
+
+ mockUseModelCostMap.mockReturnValue(
+ createModelCostMapMock({
+ "gpt-4-clickable": { litellm_provider: "openai" },
+ }),
+ );
+
+ const modelData = createPaginatedModelData([
+ {
+ model_name: "gpt-4-clickable",
+ litellm_model_name: "gpt-4-clickable",
+ provider: "openai",
+ model_info: {
+ id: "clickable-model-id",
+ db_model: true,
+ direct_access: true,
+ access_via_team_ids: [],
+ access_groups: [],
+ created_by: "user-123",
+ created_at: "2024-01-01",
+ updated_at: "2024-01-01",
+ },
+ },
+ ], 1, 1, 1, 50);
+
+ mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
+
+ render(
);
+
+ await waitFor(() => {
+ expect(screen.getByText("gpt-4-clickable")).toBeInTheDocument();
+ });
+
+ // Click on the Model ID cell which should call setSelectedModelId
+ const modelIdCell = screen.getByText("clickable-model-id");
+ expect(modelIdCell).toBeInTheDocument();
+
+ fireEvent.click(modelIdCell);
+
+ await waitFor(() => {
+ expect(mockSetSelectedModelId).toHaveBeenCalledWith("clickable-model-id");
+ });
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
index 36948630d8a..d7687def801 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
@@ -5,8 +5,12 @@ import { Team } from "@/components/key_team_helpers/key_list";
import { AllModelsDataTable } from "@/components/model_dashboard/all_models_table";
import { columns } from "@/components/molecules/models/columns";
import { getDisplayModelName } from "@/components/view_model/model_name_display";
+import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
+import NotificationsManager from "@/components/molecules/notifications_manager";
+import { modelDeleteCall } from "@/components/networking";
import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons";
import { PaginationState, SortingState } from "@tanstack/react-table";
+import { useQueryClient } from "@tanstack/react-query";
import { Grid, TabPanel } from "@tremor/react";
import { Badge, Button, Select, Skeleton, Space, Typography } from "antd";
import ModelSettingsModal from "@/components/model_dashboard/ModelSettingsModal/ModelSettingsModal";
@@ -35,8 +39,9 @@ const AllModelsTab = ({
setSelectedTeamId,
}: AllModelsTabProps) => {
const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap();
- const { userId, userRole, premiumUser } = useAuthorized();
+ const { accessToken, userId, userRole, premiumUser } = useAuthorized();
const { data: teams, isLoading: isLoadingTeams } = useTeams();
+ const queryClient = useQueryClient();
const [modelNameSearch, setModelNameSearch] = useState
("");
const [debouncedSearch, setDebouncedSearch] = useState("");
@@ -95,7 +100,7 @@ const AllModelsTab = ({
return sort.desc ? "desc" : "asc";
}, [sorting]);
- const { data: rawModelData, isLoading: isLoadingModelsInfo } = useModelsInfo(
+ const { data: rawModelData, isLoading: isLoadingModelsInfo, refetch: refetchModels } = useModelsInfo(
currentPage,
pageSize,
debouncedSearch || undefined,
@@ -120,6 +125,9 @@ const AllModelsTab = ({
return transformModelData(rawModelData, getProviderFromModel);
}, [rawModelData, modelCostMapData]);
+ const [deleteModalModelId, setDeleteModalModelId] = useState(null);
+ const [deleteLoading, setDeleteLoading] = useState(false);
+
// Get pagination metadata from the response
const paginationMeta = useMemo(() => {
if (!rawModelData) {
@@ -190,6 +198,28 @@ const AllModelsTab = ({
setSorting([]);
};
+ const modelToDelete = useMemo(() => {
+ if (!deleteModalModelId || !modelData?.data) return null;
+ return modelData.data.find((model: any) => model.model_info.id === deleteModalModelId);
+ }, [deleteModalModelId, modelData]);
+
+ const handleDeleteModel = async () => {
+ if (!accessToken || !deleteModalModelId) return;
+ try {
+ setDeleteLoading(true);
+ await modelDeleteCall(accessToken, deleteModalModelId);
+ NotificationsManager.success("Model deleted successfully");
+ queryClient.invalidateQueries({ queryKey: ["models", "list"] });
+ refetchModels();
+ } catch (error) {
+ console.error("Error deleting model:", error);
+ NotificationsManager.fromBackend(error);
+ } finally {
+ setDeleteLoading(false);
+ setDeleteModalModelId(null);
+ }
+ };
+
return (
@@ -504,6 +534,7 @@ const AllModelsTab = ({
() => { },
expandedRows,
setExpandedRows,
+ setDeleteModalModelId,
)}
data={filteredData}
isLoading={isLoadingModelsInfo}
@@ -512,10 +543,40 @@ const AllModelsTab = ({
pagination={pagination}
onPaginationChange={setPagination}
enablePagination={true}
+ onRowClick={(model: any) => setSelectedModelId(model.model_info.id)}
/>
+
+