Various Text, button state, and test changes

This commit is contained in:
yuneng-jiang 2025-11-27 16:19:50 -08:00
parent 634ec91acc
commit 4aea2df986
5 changed files with 71 additions and 59 deletions

View file

@ -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(<EntityUsageExportModal {...baseProps} />);
@ -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(<EntityUsageExportModal {...baseProps} />);
@ -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();

View file

@ -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<EntityUsageExportModalProps> = ({
@ -25,50 +24,15 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
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();

View file

@ -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);
};

View file

@ -82,31 +82,31 @@ export const QuickActions: React.FC<QuickActionsProps> = ({ onSelectAll, onUnsel
</Tooltip>
</div>
<Button
type="default"
danger
color="danger"
variant="outlined"
onClick={onUnselectAll}
disabled={!hasSelectedEntities}
icon={<CloseOutlined />}
className="border-gray-300 hover:text-red-600 hover:border-red-300"
>
Unselect All
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<Button
type="default"
color="primary"
variant="outlined"
onClick={() => onSelectAll("MASK")}
className="flex items-center justify-center h-10 border-blue-200 hover:border-blue-300 hover:text-blue-700 bg-blue-50 hover:bg-blue-100 text-blue-600"
className="h-10"
block
icon={<EyeInvisibleOutlined />}
>
Select All & Mask
</Button>
<Button
type="default"
danger
color="danger"
variant="outlined"
onClick={() => onSelectAll("BLOCK")}
className="flex items-center justify-center h-10 border-red-200 hover:border-red-300 hover:text-red-700 bg-red-50 hover:bg-red-100 text-red-600"
className="h-10 hover:bg-red-100"
block
icon={<StopOutlined />}
>

View file

@ -292,6 +292,7 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
getRowCanExpand={() => false}
isLoading={isLoadingServers}
noDataMessage="No MCP servers configured"
loadingMessage="🚅 Loading MCP servers..."
/>
</div>
</div>