From 4aea2df98617249681c140640811cbcb306cf135 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 27 Nov 2025 16:19:50 -0800 Subject: [PATCH] Various Text, button state, and test changes --- .../EntityUsageExportModal.test.tsx | 20 ++++---- .../EntityUsageExportModal.tsx | 42 ++------------- .../src/components/EntityUsageExport/utils.ts | 51 ++++++++++++++++++- .../components/guardrails/pii_components.tsx | 16 +++--- .../src/components/mcp_tools/mcp_servers.tsx | 1 + 5 files changed, 71 insertions(+), 59 deletions(-) diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.test.tsx index dae758e7e71..d881e225273 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.test.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.test.tsx @@ -17,6 +17,8 @@ import EntityUsageExportModal from "./EntityUsageExportModal"; // Mock utilities that format/export data so tests stay fast and deterministic vi.mock("./utils", () => { return { + handleExportCSV: vi.fn(), + handleExportJSON: vi.fn(), generateExportData: vi.fn(() => [{ Date: "2025-10-01" }]), generateMetadata: vi.fn(() => ({ meta: true })), }; @@ -66,11 +68,11 @@ describe("EntityUsageExportModal", () => { it("renders default state and exports CSV (daily) successfully", async () => { /** * Tests the happy path: user opens modal and exports with defaults. - * Verifies that generateExportData is called with 'daily' scope + * Verifies that handleExportCSV is called with correct parameters * and modal closes after export completes. */ const user = userEvent.setup(); - const { generateExportData } = await import("./utils"); + const { handleExportCSV } = await import("./utils"); const { getByRole } = render(); @@ -80,10 +82,8 @@ describe("EntityUsageExportModal", () => { // Click export await user.click(getByRole("button", { name: /Export CSV/i })); - // Verifies export pipeline was invoked with default scope 'daily' - expect(generateExportData).toHaveBeenCalled(); - const callArgs = (generateExportData as any).mock.calls[0]; - expect(callArgs[1]).toBe("daily"); + // Verifies export function was invoked with correct parameters + expect(handleExportCSV).toHaveBeenCalledWith(baseProps.spendData, "daily", "Tag", "tag"); // Modal closes after export expect(baseProps.onClose).toHaveBeenCalled(); @@ -92,11 +92,11 @@ describe("EntityUsageExportModal", () => { it("exports with 'day-by-day by tag and model' scope when selected", async () => { /** * Tests that user can change export type (scope). - * Verifies generateExportData receives 'daily_with_models' scope + * Verifies handleExportCSV receives 'daily_with_models' scope * when the second radio option is selected. */ const user = userEvent.setup(); - const { generateExportData } = await import("./utils"); + const { handleExportCSV } = await import("./utils"); const { getByText, getByRole } = render(); @@ -109,9 +109,7 @@ describe("EntityUsageExportModal", () => { await user.click(exportBtn); // Ensure the selected scope flowed through - expect(generateExportData).toHaveBeenCalled(); - const callArgs = (generateExportData as any).mock.calls.at(-1); - expect(callArgs[1]).toBe("daily_with_models"); + expect(handleExportCSV).toHaveBeenCalledWith(baseProps.spendData, "daily_with_models", "Tag", "tag"); // Modal closes after export expect(baseProps.onClose).toHaveBeenCalled(); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx index 672643f2ad9..5b40d761983 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx @@ -1,12 +1,11 @@ import React, { useState } from "react"; import { Button } from "@tremor/react"; import { Modal } from "antd"; -import Papa from "papaparse"; import NotificationsManager from "../molecules/notifications_manager"; import ExportSummary from "./ExportSummary"; import ExportTypeSelector from "./ExportTypeSelector"; import ExportFormatSelector from "./ExportFormatSelector"; -import { generateExportData, generateMetadata } from "./utils"; +import { handleExportCSV, handleExportJSON } from "./utils"; import type { EntityUsageExportModalProps, ExportFormat, ExportScope } from "./types"; const EntityUsageExportModal: React.FC = ({ @@ -25,50 +24,15 @@ const EntityUsageExportModal: React.FC = ({ const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1); const modalTitle = customTitle || `Export ${entityLabel} Usage`; - const handleExportCSV = () => { - const data = generateExportData(spendData, exportScope, entityLabel); - const csv = Papa.unparse(data); - const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - const fileName = `${entityType}_usage_${exportScope}_${new Date().toISOString().split("T")[0]}.csv`; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }; - - const handleExportJSON = () => { - const data = generateExportData(spendData, exportScope, entityLabel); - const metadata = generateMetadata(entityType, dateRange, selectedFilters, exportScope, spendData); - const exportObject = { - metadata, - data, - }; - const jsonString = JSON.stringify(exportObject, null, 2); - const blob = new Blob([jsonString], { type: "application/json" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - const fileName = `${entityType}_usage_${exportScope}_${new Date().toISOString().split("T")[0]}.json`; - a.download = fileName; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }; - const handleExport = async (format?: ExportFormat) => { const formatToUse = format || exportFormat; setIsExporting(true); try { if (formatToUse === "csv") { - handleExportCSV(); + handleExportCSV(spendData, exportScope, entityLabel, entityType); NotificationsManager.success(`${entityLabel} usage data exported successfully as CSV`); } else { - handleExportJSON(); + handleExportJSON(spendData, exportScope, entityLabel, entityType, dateRange, selectedFilters); NotificationsManager.success(`${entityLabel} usage data exported successfully as JSON`); } onClose(); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts index 87ca860657f..1327e158a6e 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts @@ -1,5 +1,7 @@ import { formatNumberWithCommas } from "@/utils/dataUtils"; +import Papa from "papaparse"; import type { EntitySpendData, EntityBreakdown, ExportMetadata, ExportScope } from "./types"; +import type { DateRangePickerValue } from "@tremor/react"; export const getEntityBreakdown = (spendData: EntitySpendData): EntityBreakdown[] => { const entitySpend: { [key: string]: EntityBreakdown } = {}; @@ -138,7 +140,7 @@ export const generateExportData = ( export const generateMetadata = ( entityType: "tag" | "team" | "organization", - dateRange: { from?: Date; to?: Date }, + dateRange: DateRangePickerValue, selectedFilters: string[], exportScope: ExportScope, spendData: EntitySpendData, @@ -159,3 +161,50 @@ export const generateMetadata = ( total_tokens: spendData.metadata.total_tokens, }, }); + +export const handleExportCSV = ( + spendData: EntitySpendData, + exportScope: ExportScope, + entityLabel: string, + entityType: "tag" | "team" | "organization", +): void => { + const data = generateExportData(spendData, exportScope, entityLabel); + const csv = Papa.unparse(data); + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const fileName = `${entityType}_usage_${exportScope}_${new Date().toISOString().split("T")[0]}.csv`; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); +}; + +export const handleExportJSON = ( + spendData: EntitySpendData, + exportScope: ExportScope, + entityLabel: string, + entityType: "tag" | "team" | "organization", + dateRange: DateRangePickerValue, + selectedFilters: string[], +): void => { + const data = generateExportData(spendData, exportScope, entityLabel); + const metadata = generateMetadata(entityType, dateRange, selectedFilters, exportScope, spendData); + const exportObject = { + metadata, + data, + }; + const jsonString = JSON.stringify(exportObject, null, 2); + const blob = new Blob([jsonString], { type: "application/json" }); + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + const fileName = `${entityType}_usage_${exportScope}_${new Date().toISOString().split("T")[0]}.json`; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + window.URL.revokeObjectURL(url); +}; diff --git a/ui/litellm-dashboard/src/components/guardrails/pii_components.tsx b/ui/litellm-dashboard/src/components/guardrails/pii_components.tsx index e3b3926b994..3365e866aa9 100644 --- a/ui/litellm-dashboard/src/components/guardrails/pii_components.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/pii_components.tsx @@ -82,31 +82,31 @@ export const QuickActions: React.FC = ({ onSelectAll, onUnsel