mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-18 14:23:37 +00:00
Merge pull request #20539 from BerriAI/litellm_usage_failed_req
[Refactor] UI - Usage Page: Spend By Provider
This commit is contained in:
+239
@@ -0,0 +1,239 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import SpendByProvider from "./SpendByProvider";
|
||||
|
||||
vi.mock("../../../shared/chart_loader", () => ({
|
||||
ChartLoader: ({ isDateChanging }: { isDateChanging: boolean }) => (
|
||||
<div data-testid="chart-loader">
|
||||
{isDateChanging ? "Processing date selection..." : "Loading chart data..."}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../../../molecules/models/ProviderLogo", () => ({
|
||||
ProviderLogo: ({ provider }: { provider: string }) => (
|
||||
<div data-testid={`provider-logo-${provider}`}>{provider}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("SpendByProvider", () => {
|
||||
const mockProviderSpend = [
|
||||
{
|
||||
provider: "openai",
|
||||
spend: 150.5,
|
||||
requests: 100,
|
||||
successful_requests: 95,
|
||||
failed_requests: 5,
|
||||
tokens: 50000,
|
||||
},
|
||||
{
|
||||
provider: "anthropic",
|
||||
spend: 200.75,
|
||||
requests: 120,
|
||||
successful_requests: 115,
|
||||
failed_requests: 5,
|
||||
tokens: 75000,
|
||||
},
|
||||
{
|
||||
provider: "unknown",
|
||||
spend: 0,
|
||||
requests: 10,
|
||||
successful_requests: 0,
|
||||
failed_requests: 10,
|
||||
tokens: 0,
|
||||
},
|
||||
{
|
||||
provider: "google",
|
||||
spend: 0,
|
||||
requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
tokens: 0,
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
|
||||
expect(screen.getByText("Spend by Provider")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display the title", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
|
||||
expect(screen.getByText("Spend by Provider")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Show Zero Spend toggle", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
|
||||
expect(screen.getByText("Show Zero Spend")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("switch")[0]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Show Unknown toggle", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
|
||||
expect(screen.getByText("Show Unknown")).toBeInTheDocument();
|
||||
expect(screen.getAllByRole("switch")[1]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display table headers", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByText("Provider")).toBeInTheDocument();
|
||||
expect(screen.getByText("Spend")).toBeInTheDocument();
|
||||
expect(screen.getByText("Successful")).toBeInTheDocument();
|
||||
expect(screen.getByText("Failed")).toBeInTheDocument();
|
||||
expect(screen.getByText("Tokens")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display provider data in table", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getAllByText("openai").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("anthropic").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("$150.50")).toBeInTheDocument();
|
||||
expect(screen.getByText("$200.75")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display formatted spend values with two decimal places", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByText("$150.50")).toBeInTheDocument();
|
||||
expect(screen.getByText("$200.75")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display successful requests with locale formatting", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByText("95")).toBeInTheDocument();
|
||||
expect(screen.getByText("115")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display failed requests with locale formatting", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getAllByText("5").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should display tokens with locale formatting", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByText("50,000")).toBeInTheDocument();
|
||||
expect(screen.getByText("75,000")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display provider logos", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByTestId("provider-logo-openai")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("provider-logo-anthropic")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter out providers with zero spend by default", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getAllByText("openai").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("anthropic").length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText("google")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter out unknown provider by default", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.queryByText("unknown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display ChartLoader when loading is true", () => {
|
||||
render(<SpendByProvider loading={true} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByTestId("chart-loader")).toBeInTheDocument();
|
||||
expect(screen.getByText("Loading chart data...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display ChartLoader with date changing message when isDateChanging is true", () => {
|
||||
render(<SpendByProvider loading={true} isDateChanging={true} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getByTestId("chart-loader")).toBeInTheDocument();
|
||||
expect(screen.getByText("Processing date selection...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display table when loading is true", () => {
|
||||
render(<SpendByProvider loading={true} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.queryByText("Provider")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty provider spend array", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={[]} />);
|
||||
expect(screen.getByText("Provider")).toBeInTheDocument();
|
||||
expect(screen.getByText("Spend")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle provider with null provider name", () => {
|
||||
const providerSpendWithNull = [
|
||||
{
|
||||
provider: null as unknown as string,
|
||||
spend: 100,
|
||||
requests: 50,
|
||||
successful_requests: 45,
|
||||
failed_requests: 5,
|
||||
tokens: 25000,
|
||||
},
|
||||
];
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithNull} />);
|
||||
expect(screen.getByText("$100.00")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle provider with empty string provider name", () => {
|
||||
const providerSpendWithEmpty = [
|
||||
{
|
||||
provider: "",
|
||||
spend: 100,
|
||||
requests: 50,
|
||||
successful_requests: 45,
|
||||
failed_requests: 5,
|
||||
tokens: 25000,
|
||||
},
|
||||
];
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithEmpty} />);
|
||||
expect(screen.getByText("$100.00")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display large token numbers with comma formatting", () => {
|
||||
const providerSpendWithLargeTokens = [
|
||||
{
|
||||
provider: "openai",
|
||||
spend: 1000,
|
||||
requests: 1000,
|
||||
successful_requests: 950,
|
||||
failed_requests: 50,
|
||||
tokens: 1234567,
|
||||
},
|
||||
];
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithLargeTokens} />);
|
||||
expect(screen.getByText("1,234,567")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should filter data correctly when both toggles are off", () => {
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={mockProviderSpend} />);
|
||||
expect(screen.getAllByText("openai").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("anthropic").length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText("google")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("unknown")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should include all providers with spend greater than zero by default", () => {
|
||||
const providerSpendWithMixed = [
|
||||
{
|
||||
provider: "provider1",
|
||||
spend: 0.01,
|
||||
requests: 10,
|
||||
successful_requests: 10,
|
||||
failed_requests: 0,
|
||||
tokens: 1000,
|
||||
},
|
||||
{
|
||||
provider: "provider2",
|
||||
spend: 0,
|
||||
requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
tokens: 0,
|
||||
},
|
||||
];
|
||||
render(<SpendByProvider loading={false} isDateChanging={false} providerSpend={providerSpendWithMixed} />);
|
||||
expect(screen.getAllByText("provider1").length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText("provider2")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Card,
|
||||
Col,
|
||||
DonutChart,
|
||||
Grid,
|
||||
Switch,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Title,
|
||||
} from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { ProviderLogo } from "../../../molecules/models/ProviderLogo";
|
||||
import { ChartLoader } from "../../../shared/chart_loader";
|
||||
|
||||
interface ProviderSpendData {
|
||||
provider: string;
|
||||
spend: number;
|
||||
requests: number;
|
||||
successful_requests: number;
|
||||
failed_requests: number;
|
||||
tokens: number;
|
||||
}
|
||||
|
||||
interface SpendByProviderProps {
|
||||
loading: boolean;
|
||||
isDateChanging: boolean;
|
||||
providerSpend: ProviderSpendData[];
|
||||
}
|
||||
|
||||
const SpendByProvider: React.FC<SpendByProviderProps> = ({ loading, isDateChanging, providerSpend }) => {
|
||||
const [includeZeroSpend, setIncludeZeroSpend] = useState(false);
|
||||
const [includeUnknown, setIncludeUnknown] = useState(false);
|
||||
|
||||
const filteredProviderSpend = providerSpend.filter((provider) => {
|
||||
const isUnknown = provider.provider?.toLowerCase() === "unknown";
|
||||
|
||||
// If includeUnknown is true, always include unknown provider
|
||||
if (isUnknown) {
|
||||
return includeUnknown;
|
||||
}
|
||||
|
||||
// If includeZeroSpend is true, include all providers (including those with 0 spend)
|
||||
// Otherwise, only include providers with spend > 0
|
||||
if (includeZeroSpend) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return provider.spend > 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Spend by Provider</Title>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-700">Show Zero Spend</label>
|
||||
<Switch checked={includeZeroSpend} onChange={setIncludeZeroSpend} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<label className="text-sm text-gray-700">Show Unknown</label>
|
||||
<Tooltip title="Requests that failed to route to a provider">
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Switch checked={includeUnknown} onChange={setIncludeUnknown} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<Grid numItems={2}>
|
||||
<Col numColSpan={1}>
|
||||
<DonutChart
|
||||
className="mt-4 h-40"
|
||||
data={filteredProviderSpend}
|
||||
index="provider"
|
||||
category="spend"
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
colors={["cyan"]}
|
||||
/>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Spend</TableHeaderCell>
|
||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{filteredProviderSpend.map((provider) => (
|
||||
<TableRow key={provider.provider}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{provider.provider && <ProviderLogo provider={provider.provider} className="w-4 h-4" />}
|
||||
<span>{provider.provider}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
|
||||
<TableCell className="text-green-600">
|
||||
{provider.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-red-600">
|
||||
{provider.failed_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Grid>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpendByProvider;
|
||||
@@ -1,20 +1,21 @@
|
||||
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
|
||||
import { useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
|
||||
import { act, fireEvent, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "../../../../tests/test-utils";
|
||||
import type { Organization } from "../../networking";
|
||||
import * as networking from "../../networking";
|
||||
import NewUsagePage from "./UsagePageView";
|
||||
import UsagePage from "./UsagePageView";
|
||||
|
||||
// Polyfill ResizeObserver for test environment
|
||||
beforeAll(() => {
|
||||
if (typeof window !== "undefined" && !window.ResizeObserver) {
|
||||
window.ResizeObserver = class ResizeObserver {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
observe() { }
|
||||
unobserve() { }
|
||||
disconnect() { }
|
||||
} as any;
|
||||
}
|
||||
});
|
||||
@@ -45,6 +46,43 @@ vi.mock("./EntityUsage/EntityUsage", () => ({
|
||||
EntityList: [],
|
||||
}));
|
||||
|
||||
vi.mock("./EntityUsage/SpendByProvider", () => ({
|
||||
default: () => <div>Spend By Provider</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./EndpointUsage/EndpointUsage", () => ({
|
||||
default: () => <div>Endpoint Usage</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./UsageViewSelect/UsageViewSelect", () => ({
|
||||
UsageViewSelect: ({ value, onChange }: any) => {
|
||||
const React = require("react");
|
||||
return React.createElement(
|
||||
"select",
|
||||
{
|
||||
value,
|
||||
onChange: (e: any) => onChange?.(e.target.value),
|
||||
role: "combobox",
|
||||
"data-testid": "usage-view-select",
|
||||
},
|
||||
React.createElement("option", { value: "global" }, "Global Usage"),
|
||||
React.createElement("option", { value: "team" }, "Team Usage"),
|
||||
React.createElement("option", { value: "organization" }, "Organization Usage"),
|
||||
React.createElement("option", { value: "customer" }, "Customer Usage"),
|
||||
React.createElement("option", { value: "tag" }, "Tag Usage"),
|
||||
React.createElement("option", { value: "agent" }, "Agent Usage"),
|
||||
React.createElement("option", { value: "user-agent-activity" }, "User Agent Activity"),
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../shared/advanced_date_picker", () => ({
|
||||
default: ({ value, onValueChange }: any) => {
|
||||
const React = require("react");
|
||||
return React.createElement("div", { "data-testid": "advanced-date-picker" }, "Date Picker");
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../../user_agent_activity", () => ({
|
||||
default: () => <div>User Agent Activity</div>,
|
||||
}));
|
||||
@@ -70,6 +108,10 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({
|
||||
useCurrentUser: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("antd", async (importOriginal) => {
|
||||
const React = await import("react");
|
||||
const actual = await importOriginal<typeof import("antd")>();
|
||||
@@ -133,12 +175,40 @@ vi.mock("antd", async (importOriginal) => {
|
||||
}
|
||||
(Table as any).displayName = "Table";
|
||||
|
||||
function Segmented(props: any) {
|
||||
const { value, onChange, options, ...rest } = props;
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ ...rest, "data-testid": "antd-segmented" },
|
||||
options?.map((opt: any) =>
|
||||
React.createElement(
|
||||
"button",
|
||||
{
|
||||
key: opt.value,
|
||||
onClick: () => onChange?.(opt.value),
|
||||
"data-selected": value === opt.value,
|
||||
},
|
||||
opt.label,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
(Segmented as any).displayName = "AntdSegmented";
|
||||
|
||||
function Tooltip(props: any) {
|
||||
const { title, children, ...rest } = props;
|
||||
return React.createElement("div", { ...rest, "data-testid": "antd-tooltip", title }, children);
|
||||
}
|
||||
(Tooltip as any).displayName = "AntdTooltip";
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Select,
|
||||
Alert,
|
||||
Badge,
|
||||
Table,
|
||||
Segmented,
|
||||
Tooltip,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -160,15 +230,97 @@ vi.mock("@ant-design/icons", async () => {
|
||||
BarChartOutlined: Icon,
|
||||
ClockCircleOutlined: Icon,
|
||||
CalendarOutlined: Icon,
|
||||
InfoCircleOutlined: Icon,
|
||||
};
|
||||
});
|
||||
|
||||
describe("NewUsage", () => {
|
||||
// Mock Tremor components
|
||||
vi.mock("@tremor/react", async () => {
|
||||
const React = await import("react");
|
||||
const actual = await import("@tremor/react");
|
||||
|
||||
function TabGroup({ children }: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-tab-group" }, children);
|
||||
}
|
||||
|
||||
function TabList({ children }: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-tab-list" }, children);
|
||||
}
|
||||
|
||||
function Tab({ children, ...props }: any) {
|
||||
return React.createElement("button", { ...props, "data-testid": "tremor-tab" }, children);
|
||||
}
|
||||
|
||||
function TabPanels({ children }: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-tab-panels" }, children);
|
||||
}
|
||||
|
||||
function TabPanel({ children }: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-tab-panel" }, children);
|
||||
}
|
||||
|
||||
function Card({ children, ...props }: any) {
|
||||
return React.createElement("div", { ...props, "data-testid": "tremor-card" }, children);
|
||||
}
|
||||
|
||||
function Grid({ children, numItems, ...props }: any) {
|
||||
return React.createElement("div", { ...props, "data-testid": "tremor-grid" }, children);
|
||||
}
|
||||
|
||||
function Col({ children, numColSpan, ...props }: any) {
|
||||
return React.createElement("div", { ...props, "data-testid": "tremor-col" }, children);
|
||||
}
|
||||
|
||||
function Title({ children, ...props }: any) {
|
||||
return React.createElement("h2", { ...props, "data-testid": "tremor-title" }, children);
|
||||
}
|
||||
|
||||
function Text({ children, ...props }: any) {
|
||||
return React.createElement("p", { ...props, "data-testid": "tremor-text" }, children);
|
||||
}
|
||||
|
||||
function BarChart({ data, valueFormatter, yAxisWidth, showLegend, customTooltip, ...props }: any) {
|
||||
return React.createElement("div", { ...props, "data-testid": "tremor-bar-chart" }, "Bar Chart");
|
||||
}
|
||||
|
||||
function DonutChart({ data, ...props }: any) {
|
||||
return React.createElement("div", { ...props, "data-testid": "tremor-donut-chart" }, "Donut Chart");
|
||||
}
|
||||
|
||||
function Button({ children, icon, onClick, ...props }: any) {
|
||||
return React.createElement(
|
||||
"button",
|
||||
{ ...props, onClick, "data-testid": "tremor-button" },
|
||||
icon && React.createElement("span", { "data-testid": "tremor-button-icon" }),
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...actual,
|
||||
TabGroup,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
Card,
|
||||
Grid,
|
||||
Col,
|
||||
Title,
|
||||
Text,
|
||||
BarChart,
|
||||
DonutChart,
|
||||
Button,
|
||||
};
|
||||
});
|
||||
|
||||
describe("UsagePage", () => {
|
||||
const mockUserDailyActivityAggregatedCall = vi.mocked(networking.userDailyActivityAggregatedCall);
|
||||
const mockTagListCall = vi.mocked(networking.tagListCall);
|
||||
const mockUseCustomers = vi.mocked(useCustomers);
|
||||
const mockUseAgents = vi.mocked(useAgents);
|
||||
const mockUseAuthorized = vi.mocked(useAuthorized);
|
||||
const mockUseCurrentUser = vi.mocked(useCurrentUser);
|
||||
|
||||
const mockSpendData = {
|
||||
results: [
|
||||
@@ -308,9 +460,6 @@ describe("NewUsage", () => {
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
accessToken: "test-token",
|
||||
userRole: "Admin",
|
||||
userID: "user-123",
|
||||
teams: [
|
||||
{
|
||||
team_id: "team-1",
|
||||
@@ -330,20 +479,27 @@ describe("NewUsage", () => {
|
||||
},
|
||||
],
|
||||
organizations: [],
|
||||
premiumUser: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseAuthorized.mockReturnValue({
|
||||
token: "mock-token",
|
||||
accessToken: defaultProps.accessToken,
|
||||
userId: defaultProps.userID,
|
||||
accessToken: "test-token",
|
||||
userId: "user-123",
|
||||
userEmail: "test@example.com",
|
||||
userRole: defaultProps.userRole,
|
||||
premiumUser: defaultProps.premiumUser,
|
||||
userRole: "Admin",
|
||||
premiumUser: true,
|
||||
disabledPersonalKeyCreation: false,
|
||||
showSSOBanner: false,
|
||||
});
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
data: {
|
||||
user_id: "user-123",
|
||||
max_budget: null,
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as any);
|
||||
mockUserDailyActivityAggregatedCall.mockClear();
|
||||
mockTagListCall.mockClear();
|
||||
mockUserDailyActivityAggregatedCall.mockResolvedValue(mockSpendData);
|
||||
@@ -361,7 +517,7 @@ describe("NewUsage", () => {
|
||||
});
|
||||
|
||||
it("should render and fetch usage data on mount", async () => {
|
||||
renderWithProviders(<NewUsagePage {...defaultProps} />);
|
||||
renderWithProviders(<UsagePage {...defaultProps} />);
|
||||
|
||||
// Wait for data to be fetched
|
||||
await waitFor(() => {
|
||||
@@ -380,7 +536,7 @@ describe("NewUsage", () => {
|
||||
});
|
||||
|
||||
it("should display usage metrics and charts", async () => {
|
||||
renderWithProviders(<NewUsagePage {...defaultProps} />);
|
||||
renderWithProviders(<UsagePage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
|
||||
@@ -396,13 +552,13 @@ describe("NewUsage", () => {
|
||||
const totalTokensElements = screen.getAllByText("Total Tokens");
|
||||
expect(totalTokensElements.length).toBeGreaterThan(0);
|
||||
|
||||
// Check for chart titles
|
||||
// Check for chart titles (these are in the Cost tab)
|
||||
expect(screen.getByText("Daily Spend")).toBeInTheDocument();
|
||||
expect(screen.getByText("Top Virtual Keys")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should switch between usage views correctly", async () => {
|
||||
renderWithProviders(<NewUsagePage {...defaultProps} />);
|
||||
renderWithProviders(<UsagePage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
|
||||
@@ -412,7 +568,7 @@ describe("NewUsage", () => {
|
||||
expect(screen.getByText("Daily Spend")).toBeInTheDocument();
|
||||
|
||||
// Switch to Team Usage view
|
||||
const usageSelect = screen.getByRole("combobox");
|
||||
const usageSelect = screen.getByTestId("usage-view-select");
|
||||
act(() => {
|
||||
fireEvent.change(usageSelect, { target: { value: "team" } });
|
||||
});
|
||||
@@ -436,13 +592,13 @@ describe("NewUsage", () => {
|
||||
});
|
||||
|
||||
it("should show organization usage banner and view for admins", async () => {
|
||||
renderWithProviders(<NewUsagePage {...defaultProps} organizations={mockOrganizations} />);
|
||||
renderWithProviders(<UsagePage {...defaultProps} organizations={mockOrganizations} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const usageSelect = screen.getByRole("combobox");
|
||||
const usageSelect = screen.getByTestId("usage-view-select");
|
||||
act(() => {
|
||||
fireEvent.change(usageSelect, { target: { value: "organization" } });
|
||||
});
|
||||
@@ -461,13 +617,13 @@ describe("NewUsage", () => {
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<NewUsagePage {...defaultProps} />);
|
||||
renderWithProviders(<UsagePage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const usageSelect = screen.getByRole("combobox");
|
||||
const usageSelect = screen.getByTestId("usage-view-select");
|
||||
act(() => {
|
||||
fireEvent.change(usageSelect, { target: { value: "customer" } });
|
||||
});
|
||||
@@ -485,13 +641,13 @@ describe("NewUsage", () => {
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<NewUsagePage {...defaultProps} />);
|
||||
renderWithProviders(<UsagePage {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const usageSelect = screen.getByRole("combobox");
|
||||
const usageSelect = screen.getByTestId("usage-view-select");
|
||||
act(() => {
|
||||
fireEvent.change(usageSelect, { target: { value: "agent" } });
|
||||
});
|
||||
|
||||
@@ -6,28 +6,22 @@
|
||||
* Works at 1m+ spend logs, by querying an aggregate table instead.
|
||||
*/
|
||||
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
BarChart,
|
||||
Card,
|
||||
Col,
|
||||
DateRangePickerValue,
|
||||
DonutChart,
|
||||
Grid,
|
||||
Tab,
|
||||
TabGroup,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Text,
|
||||
Title,
|
||||
Title
|
||||
} from "@tremor/react";
|
||||
import { Alert, Segmented } from "antd";
|
||||
import { Alert, Segmented, Tooltip } from "antd";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
|
||||
@@ -42,7 +36,6 @@ import CloudZeroExportModal from "../../cloudzero_export_modal";
|
||||
import EntityUsageExportModal from "../../EntityUsageExport";
|
||||
import { Team } from "../../key_team_helpers/key_list";
|
||||
import { Organization, tagListCall, userDailyActivityAggregatedCall, userDailyActivityCall } from "../../networking";
|
||||
import { getProviderLogoAndName } from "../../provider_info_helpers";
|
||||
import AdvancedDatePicker from "../../shared/advanced_date_picker";
|
||||
import { ChartLoader } from "../../shared/chart_loader";
|
||||
import { Tag } from "../../tag_management/types";
|
||||
@@ -52,6 +45,7 @@ import { DailyData, KeyMetricWithMetadata, MetricWithMetadata } from "../types";
|
||||
import { valueFormatterSpend } from "../utils/value_formatters";
|
||||
import EndpointUsage from "./EndpointUsage/EndpointUsage";
|
||||
import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage";
|
||||
import SpendByProvider from "./EntityUsage/SpendByProvider";
|
||||
import TopKeyView from "./EntityUsage/TopKeyView";
|
||||
import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect";
|
||||
|
||||
@@ -509,7 +503,12 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
|
||||
</Text>
|
||||
</Card>
|
||||
<Card>
|
||||
<Title>Failed Requests</Title>
|
||||
<div className="flex items-center gap-2">
|
||||
<Title>Failed Requests</Title>
|
||||
<Tooltip title="Includes requests that failed to route to a provider, tool usage failures, and other request errors where the provider cannot be determined.">
|
||||
<InfoCircleOutlined className="text-gray-400 hover:text-gray-600" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Text className="text-2xl font-bold mt-2 text-red-600">
|
||||
{userSpendData.metadata?.total_failed_requests?.toLocaleString() || 0}
|
||||
</Text>
|
||||
@@ -669,79 +668,11 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
|
||||
|
||||
{/* Spend by Provider */}
|
||||
<Col numColSpan={2}>
|
||||
<Card className="h-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>Spend by Provider</Title>
|
||||
</div>
|
||||
{loading ? (
|
||||
<ChartLoader isDateChanging={isDateChanging} />
|
||||
) : (
|
||||
<Grid numItems={2}>
|
||||
<Col numColSpan={1}>
|
||||
<DonutChart
|
||||
className="mt-4 h-40"
|
||||
data={getProviderSpend()}
|
||||
index="provider"
|
||||
category="spend"
|
||||
valueFormatter={(value) => `$${formatNumberWithCommas(value, 2)}`}
|
||||
colors={["cyan"]}
|
||||
/>
|
||||
</Col>
|
||||
<Col numColSpan={1}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Provider</TableHeaderCell>
|
||||
<TableHeaderCell>Spend</TableHeaderCell>
|
||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{getProviderSpend()
|
||||
.filter((provider) => provider.spend > 0)
|
||||
.map((provider) => (
|
||||
<TableRow key={provider.provider}>
|
||||
<TableCell>
|
||||
<div className="flex items-center space-x-2">
|
||||
{provider.provider && (
|
||||
<img
|
||||
src={getProviderLogoAndName(provider.provider).logo}
|
||||
alt={`${provider.provider} logo`}
|
||||
className="w-4 h-4"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
const parent = target.parentElement;
|
||||
if (parent) {
|
||||
const fallbackDiv = document.createElement("div");
|
||||
fallbackDiv.className =
|
||||
"w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs";
|
||||
fallbackDiv.textContent = provider.provider?.charAt(0) || "-";
|
||||
parent.replaceChild(fallbackDiv, target);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span>{provider.provider}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>${formatNumberWithCommas(provider.spend, 2)}</TableCell>
|
||||
<TableCell className="text-green-600">
|
||||
{provider.successful_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-red-600">
|
||||
{provider.failed_requests.toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Col>
|
||||
</Grid>
|
||||
)}
|
||||
</Card>
|
||||
<SpendByProvider
|
||||
loading={loading}
|
||||
isDateChanging={isDateChanging}
|
||||
providerSpend={getProviderSpend()}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/* Usage Metrics */}
|
||||
|
||||
Reference in New Issue
Block a user