From 0a4a7408c7542671f4f433ed8fd14cc991f6a435 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 17 Dec 2025 22:32:39 -0800 Subject: [PATCH] UI Cloudzero delete route --- .../hooks/cloudzero/useCloudZeroSettings.ts | 51 +++++++++++++- .../CloudZeroIntegrationSettings.test.tsx | 15 +++- .../CloudZeroIntegrationSettings.tsx | 69 ++++++++++++++----- 3 files changed, 114 insertions(+), 21 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings.ts index 2ef23e28247..5ccbe244e60 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings.ts @@ -1,7 +1,7 @@ -import { getProxyBaseUrl } from "@/components/networking"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { createQueryKeys } from "../common/queryKeysFactory"; import { CloudZeroSettings } from "@/components/CloudZeroCostTracking/types"; +import { getProxyBaseUrl } from "@/components/networking"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; const cloudZeroSettingsKeys = createQueryKeys("cloudZeroSettings"); @@ -54,6 +54,11 @@ interface UpdateResponse { status: string; } +interface DeleteResponse { + message: string; + status: string; +} + const updateCloudZeroSettings = async (accessToken: string, params: UpdateParams): Promise => { const proxyBaseUrl = getProxyBaseUrl(); const url = proxyBaseUrl ? `${proxyBaseUrl}/cloudzero/settings` : `/cloudzero/settings`; @@ -98,3 +103,43 @@ export const useCloudZeroUpdateSettings = (accessToken: string) => { }, }); }; + +const deleteCloudZeroSettings = async (accessToken: string): Promise => { + const proxyBaseUrl = getProxyBaseUrl(); + const url = proxyBaseUrl ? `${proxyBaseUrl}/cloudzero/delete` : `/cloudzero/delete`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + const errorMessage = + errorData?.error?.message || errorData?.message || errorData?.detail || "Failed to delete CloudZero settings"; + throw new Error(errorMessage); + } + + const data = await response.json(); + return data; +}; + +export const useCloudZeroDeleteSettings = (accessToken: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { + if (!accessToken) { + throw new Error("Access token is required"); + } + return await deleteCloudZeroSettings(accessToken); + }, + onSuccess: () => { + // Invalidate the settings query to refetch updated data + queryClient.invalidateQueries({ queryKey: cloudZeroSettingsKeys.list({}) }); + }, + }); +}; diff --git a/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.test.tsx b/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.test.tsx index 350c0a3c4da..51179f4014f 100644 --- a/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.test.tsx +++ b/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.test.tsx @@ -1,6 +1,6 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { CloudZeroIntegrationSettings } from "./CloudZeroIntegrationSettings"; import { CloudZeroSettings } from "./types"; @@ -68,4 +68,15 @@ describe("CloudZeroIntegrationSettings", () => { expect(screen.getByText("Connection ID")).toBeInTheDocument(); expect(screen.getByText("Timezone")).toBeInTheDocument(); }); + + it("should display the correct values from settings", () => { + render( + + + , + ); + + expect(screen.getByText(mockSettings.api_key_masked)).toBeInTheDocument(); + expect(screen.getByText(mockSettings.connection_id)).toBeInTheDocument(); + }); }); diff --git a/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.tsx b/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.tsx index 6afe3d65f83..780fa83652a 100644 --- a/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.tsx +++ b/ui/litellm-dashboard/src/components/CloudZeroCostTracking/CloudZeroIntegrationSettings.tsx @@ -1,6 +1,8 @@ import { useCloudZeroDryRun } from "@/app/(dashboard)/hooks/cloudzero/useCloudZeroDryRun"; import { useCloudZeroExport } from "@/app/(dashboard)/hooks/cloudzero/useCloudZeroExport"; +import { useCloudZeroDeleteSettings } from "@/app/(dashboard)/hooks/cloudzero/useCloudZeroSettings"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; import { Alert, Button, Card, Descriptions, Divider, message, Popconfirm, Tag } from "antd"; import { CheckCircle, Edit, Play, Trash2, Upload } from "lucide-react"; import { useState } from "react"; @@ -15,9 +17,11 @@ interface CloudZeroIntegrationSettingsProps { export function CloudZeroIntegrationSettings({ settings, onSettingsUpdated }: CloudZeroIntegrationSettingsProps) { const { accessToken } = useAuthorized(); const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const dryRunMutation = useCloudZeroDryRun(accessToken || ""); const exportMutation = useCloudZeroExport(accessToken || ""); + const deleteMutation = useCloudZeroDeleteSettings(accessToken || ""); const handleDryRun = () => { if (!accessToken) return; @@ -66,10 +70,27 @@ export function CloudZeroIntegrationSettings({ settings, onSettingsUpdated }: Cl setIsEditModalOpen(false); }; - const handleDelete = async () => { - // Note: Delete functionality is not yet implemented in the backend API - // This would require a DELETE endpoint at /cloudzero/settings - message.warning("Delete functionality is not yet available. Please contact support."); + const handleDeleteClick = () => { + setIsDeleteModalOpen(true); + }; + + const handleDeleteConfirm = () => { + if (!accessToken) return; + + deleteMutation.mutate(undefined, { + onSuccess: () => { + message.success("CloudZero integration deleted successfully"); + setIsDeleteModalOpen(false); + onSettingsUpdated(); + }, + onError: (error) => { + message.error(error?.message || "Failed to delete CloudZero integration"); + }, + }); + }; + + const handleDeleteCancel = () => { + setIsDeleteModalOpen(false); }; return ( @@ -89,19 +110,14 @@ export function CloudZeroIntegrationSettings({ settings, onSettingsUpdated }: Cl - } + onClick={handleDeleteClick} + className="flex items-center gap-2" > - - + Delete + } className="shadow-sm" @@ -187,6 +203,27 @@ export function CloudZeroIntegrationSettings({ settings, onSettingsUpdated }: Cl onCancel={handleEditModalCancel} settings={settings} /> + + ); }