refactor(ui): migrate cost-tracking to shadcn (#36631)

* refactor(ui): migrate cost-tracking helpers to shadcn

* fix(ui): restore export menu keyboard navigation
This commit is contained in:
yuneng-jiang 2026-08-12 12:40:12 -07:00 • committed by GitHub
parent 2b9e3db6b0
commit 7d12f21e31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 127 additions and 127 deletions

View file

@ -234,9 +234,6 @@
"src/app/(dashboard)/cost-tracking/_components/how_it_works.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/index.tsx": {
@ -260,17 +257,9 @@
"count": 2
}
},
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.test.tsx": {
"unused-imports/no-unused-imports": {
"count": 1
}
},
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/multi_export_dropdown.tsx": {
"local/filename-pascal-case": {
"count": 1
},
"no-restricted-imports": {
"count": 1
}
},
"src/app/(dashboard)/cost-tracking/_components/pricing_calculator/use_multi_cost_estimate.ts": {

View file

@ -1,6 +1,7 @@
import React, { useState, useMemo } from "react";
import { Text, TextInput } from "@tremor/react";
import CodeBlock from "@/components/CodeBlock";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const HowItWorks: React.FC = () => {
const [responseCost, setResponseCost] = useState("");
@ -9,8 +10,10 @@ const HowItWorks: React.FC = () => {
const calculatedDiscount = useMemo(() => {
const cost = parseFloat(responseCost);
const discount = parseFloat(discountAmount);
const hasInvalidCost = isNaN(cost) || cost === 0;
const hasInvalidDiscount = isNaN(discount) || discount === 0;
if (isNaN(cost) || isNaN(discount) || cost === 0 || discount === 0) {
if (hasInvalidCost || hasInvalidDiscount) {
return null;
}
@ -28,30 +31,30 @@ const HowItWorks: React.FC = () => {
return (
<div className="space-y-4 pt-2">
<div>
<Text className="font-medium text-gray-900 text-sm mb-1">Cost Calculation</Text>
<Text className="text-xs text-gray-600">
<h3 className="mb-1 text-sm font-medium text-foreground">Cost Calculation</h3>
<p className="text-xs text-muted-foreground">
Discounts are applied to provider costs:{" "}
<code className="bg-gray-100 px-1.5 py-0.5 rounded-sm text-xs">
<code className="rounded-sm bg-muted px-1.5 py-0.5 text-xs text-foreground">
final_cost = base_cost × (1 - discount%/100)
</code>
</Text>
</p>
</div>
<div>
<Text className="font-medium text-gray-900 text-sm mb-1">Example</Text>
<Text className="text-xs text-gray-600">
<h3 className="mb-1 text-sm font-medium text-foreground">Example</h3>
<p className="text-xs text-muted-foreground">
A 5% discount on a $10.00 request results in: $10.00 × (1 - 0.05) = $9.50
</Text>
</p>
</div>
<div>
<Text className="font-medium text-gray-900 text-sm mb-1">Valid Range</Text>
<Text className="text-xs text-gray-600">Discount percentages must be between 0% and 100%</Text>
<h3 className="mb-1 text-sm font-medium text-foreground">Valid Range</h3>
<p className="text-xs text-muted-foreground">Discount percentages must be between 0% and 100%</p>
</div>
<div className="pt-4 border-t border-gray-200">
<Text className="font-medium text-gray-900 text-sm mb-2">Validating Discounts</Text>
<Text className="text-xs text-gray-600 mb-3">
<div className="border-t border-border pt-4">
<h3 className="mb-2 text-sm font-medium text-foreground">Validating Discounts</h3>
<p className="mb-3 text-xs text-muted-foreground">
Make a test request and check the response headers to verify discounts are applied:
</Text>
</p>
<CodeBlock
language="bash"
code={`curl -X POST -i http://your-proxy:4000/chat/completions \\
@ -62,78 +65,80 @@ const HowItWorks: React.FC = () => {
"messages": [{"role": "user", "content": "Hello"}]
}'`}
/>
<Text className="text-xs text-gray-600 mt-3 mb-2">Look for these headers in the response:</Text>
<p className="mb-2 mt-3 text-xs text-muted-foreground">Look for these headers in the response:</p>
<div className="space-y-1.5">
<div className="flex items-start gap-3">
<code className="bg-gray-100 px-2 py-1 rounded-sm text-xs font-mono text-gray-800 whitespace-nowrap">
<code className="whitespace-nowrap rounded-sm bg-muted px-2 py-1 font-mono text-xs text-foreground">
x-litellm-response-cost
</code>
<Text className="text-xs text-gray-600">Final cost after discount</Text>
<p className="text-xs text-muted-foreground">Final cost after discount</p>
</div>
<div className="flex items-start gap-3">
<code className="bg-gray-100 px-2 py-1 rounded-sm text-xs font-mono text-gray-800 whitespace-nowrap">
<code className="whitespace-nowrap rounded-sm bg-muted px-2 py-1 font-mono text-xs text-foreground">
x-litellm-response-cost-original
</code>
<Text className="text-xs text-gray-600">Original cost before discount</Text>
<p className="text-xs text-muted-foreground">Original cost before discount</p>
</div>
<div className="flex items-start gap-3">
<code className="bg-gray-100 px-2 py-1 rounded-sm text-xs font-mono text-gray-800 whitespace-nowrap">
<code className="whitespace-nowrap rounded-sm bg-muted px-2 py-1 font-mono text-xs text-foreground">
x-litellm-response-cost-discount-amount
</code>
<Text className="text-xs text-gray-600">Amount discounted</Text>
<p className="text-xs text-muted-foreground">Amount discounted</p>
</div>
</div>
</div>
<div className="pt-4 border-t border-gray-200">
<Text className="font-medium text-gray-900 text-sm mb-3">Discount Calculator</Text>
<Text className="text-xs text-gray-600 mb-3">
<div className="border-t border-border pt-4">
<h3 className="mb-3 text-sm font-medium text-foreground">Discount Calculator</h3>
<p className="mb-3 text-xs text-muted-foreground">
Enter values from your response headers to verify the discount:
</Text>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
</p>
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
<Label htmlFor="response-cost" className="mb-1 block text-xs">
Response Cost (x-litellm-response-cost)
</label>
<TextInput
</Label>
<Input
id="response-cost"
placeholder="0.0171938125"
value={responseCost}
onValueChange={setResponseCost}
onChange={(event) => setResponseCost(event.target.value)}
className="text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">
<Label htmlFor="discount-amount" className="mb-1 block text-xs">
Discount Amount (x-litellm-response-cost-discount-amount)
</label>
<TextInput
</Label>
<Input
id="discount-amount"
placeholder="0.0009049375"
value={discountAmount}
onValueChange={setDiscountAmount}
onChange={(event) => setDiscountAmount(event.target.value)}
className="text-sm"
/>
</div>
</div>
{calculatedDiscount && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<Text className="text-sm font-medium text-blue-900 mb-2">Calculated Results</Text>
<div className="rounded-lg border border-border bg-muted/50 p-4">
<p className="mb-2 text-sm font-medium text-foreground">Calculated Results</p>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Text className="text-xs text-blue-800">Original Cost:</Text>
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.originalCost}</code>
<p className="text-xs text-muted-foreground">Original Cost:</p>
<code className="font-mono text-xs text-foreground">${calculatedDiscount.originalCost}</code>
</div>
<div className="flex items-center justify-between">
<Text className="text-xs text-blue-800">Final Cost:</Text>
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.finalCost}</code>
<p className="text-xs text-muted-foreground">Final Cost:</p>
<code className="font-mono text-xs text-foreground">${calculatedDiscount.finalCost}</code>
</div>
<div className="flex items-center justify-between">
<Text className="text-xs text-blue-800">Discount Amount:</Text>
<code className="text-xs font-mono text-blue-900">${calculatedDiscount.discountAmount}</code>
<p className="text-xs text-muted-foreground">Discount Amount:</p>
<code className="font-mono text-xs text-foreground">${calculatedDiscount.discountAmount}</code>
</div>
<div className="flex items-center justify-between pt-2 border-t border-blue-300">
<Text className="text-xs font-semibold text-blue-900">Discount Applied:</Text>
<Text className="text-sm font-bold text-blue-900">{calculatedDiscount.discountPercentage}%</Text>
<div className="flex items-center justify-between border-t border-border pt-2">
<p className="text-xs font-semibold text-foreground">Discount Applied:</p>
<p className="text-sm font-bold text-foreground">{calculatedDiscount.discountPercentage}%</p>
</div>
</div>
</div>

View file

@ -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(<MultiExportDropdown multiResult={makeMultiResult(true)} />);
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(<MultiExportDropdown multiResult={makeMultiResult(true)} />);
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(<MultiExportDropdown multiResult={makeMultiResult(true)} />);
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(<MultiExportDropdown multiResult={multiResult} />);
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", () => {
</div>,
);
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(<MultiExportDropdown multiResult={makeMultiResult(true)} />);
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(<MultiExportDropdown multiResult={makeMultiResult(true)} />);
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();
});
});

View file

@ -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<MultiExportDropdownProps> = ({ multiResult }) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(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 (
<div className="relative inline-block" ref={menuRef}>
<Button size="xs" variant="secondary" icon={DownloadOutlined} onClick={() => setIsOpen(!isOpen)}>
<DropdownMenu>
<DropdownMenuTrigger className={buttonVariants({ variant: "secondary", size: "xs" })}>
<Download />
Export
</Button>
{isOpen && (
<div className="absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
exportMultiToPDF(multiResult);
setIsOpen(false);
}}
>
<FilePdfOutlined className="mr-3 text-red-500" />
Export as PDF
</button>
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
exportMultiToCSV(multiResult);
setIsOpen(false);
}}
>
<FileExcelOutlined className="mr-3 text-green-600" />
Export as CSV
</button>
</div>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem onClick={() => exportMultiToPDF(multiResult)}>
<FileText />
Export as PDF
</DropdownMenuItem>
<DropdownMenuItem onClick={() => exportMultiToCSV(multiResult)}>
<FileSpreadsheet />
Export as CSV
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};