-
Calculated Results
+
+
Calculated Results
-
Original Cost:
-
${calculatedDiscount.originalCost}
+
Original Cost:
+
${calculatedDiscount.originalCost}
-
Final Cost:
-
${calculatedDiscount.finalCost}
+
Final Cost:
+
${calculatedDiscount.finalCost}
-
Discount Amount:
-
${calculatedDiscount.discountAmount}
+
Discount Amount:
+
${calculatedDiscount.discountAmount}
-
-
Discount Applied:
-
{calculatedDiscount.discountPercentage}%
+
+
Discount Applied:
+
{calculatedDiscount.discountPercentage}%
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx
index e40fe7dbbca..1c6800de7d2 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx
@@ -1,8 +1,7 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
-import { renderWithProviders } from "../../../../../../tests/test-utils";
+import { renderWithProviders, screen, waitFor } from "../../../../../../tests/test-utils";
import MultiExportDropdown from "./multi_export_dropdown";
import type { MultiModelResult } from "./types";
@@ -78,41 +77,44 @@ describe("MultiExportDropdown", () => {
await user.click(screen.getByRole("button", { name: /^export$/i }));
- expect(screen.getByText("Export as PDF")).toBeInTheDocument();
- expect(screen.getByText("Export as CSV")).toBeInTheDocument();
+ expect(await screen.findByRole("menuitem", { name: "Export as PDF" })).toBeInTheDocument();
+ expect(screen.getByRole("menuitem", { name: "Export as CSV" })).toBeInTheDocument();
});
it("should hide the export menu when the Export button is clicked again", async () => {
const user = userEvent.setup();
renderWithProviders(
);
- await user.click(screen.getByRole("button", { name: /^export$/i }));
- expect(screen.getByText("Export as PDF")).toBeInTheDocument();
+ const trigger = screen.getByRole("button", { name: /^export$/i });
+ await user.click(trigger);
+ await screen.findByRole("menuitem", { name: "Export as PDF" });
- await user.click(screen.getByRole("button", { name: /^export$/i }));
- expect(screen.queryByText("Export as PDF")).not.toBeInTheDocument();
+ await user.click(trigger);
+ await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "false"));
});
it("should call exportMultiToPDF and close the menu when Export as PDF is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
);
- await user.click(screen.getByRole("button", { name: /^export$/i }));
- await user.click(screen.getByText("Export as PDF"));
+ const trigger = screen.getByRole("button", { name: /^export$/i });
+ await user.click(trigger);
+ await user.click(await screen.findByRole("menuitem", { name: "Export as PDF" }));
expect(exportMultiToPDF).toHaveBeenCalledTimes(1);
- expect(screen.queryByText("Export as PDF")).not.toBeInTheDocument();
+ await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "false"));
});
it("should call exportMultiToCSV and close the menu when Export as CSV is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(
);
- await user.click(screen.getByRole("button", { name: /^export$/i }));
- await user.click(screen.getByText("Export as CSV"));
+ const trigger = screen.getByRole("button", { name: /^export$/i });
+ await user.click(trigger);
+ await user.click(await screen.findByRole("menuitem", { name: "Export as CSV" }));
expect(exportMultiToCSV).toHaveBeenCalledTimes(1);
- expect(screen.queryByText("Export as CSV")).not.toBeInTheDocument();
+ await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "false"));
});
it("should pass the multiResult to the export functions", async () => {
@@ -121,7 +123,7 @@ describe("MultiExportDropdown", () => {
renderWithProviders(
);
await user.click(screen.getByRole("button", { name: /^export$/i }));
- await user.click(screen.getByText("Export as PDF"));
+ await user.click(await screen.findByRole("menuitem", { name: "Export as PDF" }));
expect(exportMultiToPDF).toHaveBeenCalledWith(multiResult);
});
@@ -135,10 +137,41 @@ describe("MultiExportDropdown", () => {
,
);
- await user.click(screen.getByRole("button", { name: /^export$/i }));
- expect(screen.getByText("Export as PDF")).toBeInTheDocument();
+ const trigger = screen.getByRole("button", { name: /^export$/i });
+ await user.click(trigger);
+ await screen.findByRole("menuitem", { name: "Export as PDF" });
- fireEvent.mouseDown(screen.getByTestId("outside"));
- expect(screen.queryByText("Export as PDF")).not.toBeInTheDocument();
+ await user.click(screen.getByTestId("outside"));
+ await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "false"));
+ });
+
+ it("should focus and navigate export options with the keyboard", async () => {
+ const user = userEvent.setup();
+ renderWithProviders(
);
+
+ const trigger = screen.getByRole("button", { name: /^export$/i });
+ trigger.focus();
+ await user.keyboard("{ArrowDown}");
+
+ const pdfOption = await screen.findByRole("menuitem", { name: "Export as PDF" });
+ await waitFor(() => expect(pdfOption).toHaveFocus());
+
+ await user.keyboard("{ArrowDown}");
+ expect(screen.getByRole("menuitem", { name: "Export as CSV" })).toHaveFocus();
+ });
+
+ it("should close the menu and restore trigger focus when Escape is pressed", async () => {
+ const user = userEvent.setup();
+ renderWithProviders(
);
+
+ const trigger = screen.getByRole("button", { name: /^export$/i });
+ trigger.focus();
+ await user.keyboard("{ArrowDown}");
+ await screen.findByRole("menuitem", { name: "Export as PDF" });
+
+ await user.keyboard("{Escape}");
+
+ await waitFor(() => expect(trigger).toHaveAttribute("aria-expanded", "false"));
+ expect(trigger).toHaveFocus();
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx
index af60b590165..3174df0b951 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx
@@ -1,6 +1,12 @@
-import React, { useState, useRef, useEffect } from "react";
-import { Button } from "@tremor/react";
-import { DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons";
+import React from "react";
+import { Download, FileSpreadsheet, FileText } from "lucide-react";
+import { buttonVariants } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
import { MultiModelResult } from "./types";
import { exportMultiToPDF, exportMultiToCSV } from "./multi_export_utils";
@@ -9,62 +15,29 @@ interface MultiExportDropdownProps {
}
const MultiExportDropdown: React.FC
= ({ multiResult }) => {
- const [isOpen, setIsOpen] = useState(false);
- const menuRef = useRef(null);
-
const hasResults = multiResult.entries.some((e) => e.result !== null);
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
- setIsOpen(false);
- }
- };
-
- if (isOpen) {
- document.addEventListener("mousedown", handleClickOutside);
- }
-
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- };
- }, [isOpen]);
-
if (!hasResults) {
return null;
}
return (
-
-
-
- {isOpen && (
-
-
-
-
- )}
-
+
+
+ exportMultiToPDF(multiResult)}>
+
+ Export as PDF
+
+ exportMultiToCSV(multiResult)}>
+
+ Export as CSV
+
+
+
);
};