mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
Merge pull request #18798 from BerriAI/litellm_ui_endpoint_usage
[Feature] UI - Endpoint Activity in Usage
This commit is contained in:
commit
b7a61d469c
16 changed files with 633 additions and 41 deletions
|
|
@ -0,0 +1,25 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import EndpointUsage from "./EndpointUsage";
|
||||
|
||||
vi.mock("./components/EndpointUsageBarChart", () => ({
|
||||
default: () => <div>Endpoint Usage Bar Chart</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./components/EndpointUsageLineChart", () => ({
|
||||
default: () => <div>Endpoint Usage Line Chart</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./components/EndpointUsageTable", () => ({
|
||||
default: () => <div>Endpoint Usage Table</div>,
|
||||
}));
|
||||
|
||||
describe("EndpointUsage", () => {
|
||||
it("should render", () => {
|
||||
render(<EndpointUsage />);
|
||||
|
||||
expect(screen.getByText("Endpoint Usage Table")).toBeInTheDocument();
|
||||
expect(screen.getByText("Endpoint Usage Bar Chart")).toBeInTheDocument();
|
||||
expect(screen.getByText("Endpoint Usage Line Chart")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import React, { useMemo } from "react";
|
||||
import EndpointUsageBarChart from "./components/EndpointUsageBarChart";
|
||||
import EndpointUsageLineChart from "./components/EndpointUsageLineChart";
|
||||
import EndpointUsageTable from "./components/EndpointUsageTable";
|
||||
import { DailyData, MetricWithMetadata } from "../../types";
|
||||
|
||||
interface EndpointUsageProps {
|
||||
userSpendData?: {
|
||||
results: DailyData[];
|
||||
metadata: any;
|
||||
};
|
||||
}
|
||||
|
||||
const EndpointUsage: React.FC<EndpointUsageProps> = ({ userSpendData }) => {
|
||||
// Aggregate endpoints data from all days
|
||||
const endpointData = useMemo(() => {
|
||||
const aggregatedEndpoints: Record<string, MetricWithMetadata> = {};
|
||||
|
||||
if (!userSpendData?.results) {
|
||||
return aggregatedEndpoints;
|
||||
}
|
||||
|
||||
userSpendData.results.forEach((day) => {
|
||||
Object.entries(day.breakdown.endpoints || {}).forEach(([endpoint, metrics]) => {
|
||||
if (!aggregatedEndpoints[endpoint]) {
|
||||
aggregatedEndpoints[endpoint] = {
|
||||
metrics: {
|
||||
spend: 0,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: 0,
|
||||
total_tokens: 0,
|
||||
api_requests: 0,
|
||||
successful_requests: 0,
|
||||
failed_requests: 0,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
metadata: metrics.metadata || {},
|
||||
api_key_breakdown: {},
|
||||
};
|
||||
}
|
||||
aggregatedEndpoints[endpoint].metrics.spend += metrics.metrics.spend;
|
||||
aggregatedEndpoints[endpoint].metrics.prompt_tokens += metrics.metrics.prompt_tokens;
|
||||
aggregatedEndpoints[endpoint].metrics.completion_tokens += metrics.metrics.completion_tokens;
|
||||
aggregatedEndpoints[endpoint].metrics.total_tokens += metrics.metrics.total_tokens;
|
||||
aggregatedEndpoints[endpoint].metrics.api_requests += metrics.metrics.api_requests;
|
||||
aggregatedEndpoints[endpoint].metrics.successful_requests += metrics.metrics.successful_requests || 0;
|
||||
aggregatedEndpoints[endpoint].metrics.failed_requests += metrics.metrics.failed_requests || 0;
|
||||
aggregatedEndpoints[endpoint].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0;
|
||||
aggregatedEndpoints[endpoint].metrics.cache_creation_input_tokens +=
|
||||
metrics.metrics.cache_creation_input_tokens || 0;
|
||||
});
|
||||
});
|
||||
|
||||
return aggregatedEndpoints;
|
||||
}, [userSpendData]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<EndpointUsageTable endpointData={endpointData} />
|
||||
<EndpointUsageBarChart endpointData={endpointData} />
|
||||
<EndpointUsageLineChart dailyData={userSpendData} endpointData={endpointData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndpointUsage;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import EndpointUsageBarChart from "./EndpointUsageBarChart";
|
||||
|
||||
vi.mock("@tremor/react", async () => {
|
||||
const React = await import("react");
|
||||
|
||||
function Card({ children }: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-card" }, children);
|
||||
}
|
||||
(Card as any).displayName = "Card";
|
||||
|
||||
function Title({ children }: any) {
|
||||
return React.createElement("h2", { "data-testid": "tremor-title" }, children);
|
||||
}
|
||||
(Title as any).displayName = "Title";
|
||||
|
||||
function BarChart(_props: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-bar-chart" }, "Bar Chart");
|
||||
}
|
||||
(BarChart as any).displayName = "BarChart";
|
||||
|
||||
return { Card, Title, BarChart };
|
||||
});
|
||||
|
||||
vi.mock("@/components/common_components/chartUtils", () => ({
|
||||
CustomLegend: ({ categories }: any) => (
|
||||
<div data-testid="custom-legend">{categories.join(", ")}</div>
|
||||
),
|
||||
CustomTooltip: () => <div data-testid="custom-tooltip">Tooltip</div>,
|
||||
}));
|
||||
|
||||
describe("EndpointUsageBarChart", () => {
|
||||
it("should render", () => {
|
||||
render(<EndpointUsageBarChart />);
|
||||
|
||||
expect(screen.getByTestId("tremor-card")).toBeInTheDocument();
|
||||
expect(screen.getByText("Success vs Failed Requests by Endpoint")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tremor-bar-chart")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import React from "react";
|
||||
import { BarChart, Card, Title } from "@tremor/react";
|
||||
import { CustomLegend, CustomTooltip } from "@/components/common_components/chartUtils";
|
||||
import { MetricWithMetadata } from "../../../types";
|
||||
|
||||
interface EndpointUsageBarChartProps {
|
||||
endpointData?: Record<string, MetricWithMetadata>;
|
||||
}
|
||||
|
||||
const EndpointUsageBarChart: React.FC<EndpointUsageBarChartProps> = ({ endpointData }) => {
|
||||
const dataToUse = endpointData || {};
|
||||
|
||||
// Transform endpoint data into chart format
|
||||
const chartData = React.useMemo(() => {
|
||||
return Object.entries(dataToUse).map(([endpoint, data]) => ({
|
||||
endpoint,
|
||||
"metrics.successful_requests": data.metrics.successful_requests,
|
||||
"metrics.failed_requests": data.metrics.failed_requests,
|
||||
metrics: {
|
||||
successful_requests: data.metrics.successful_requests,
|
||||
failed_requests: data.metrics.failed_requests,
|
||||
},
|
||||
}));
|
||||
}, [dataToUse]);
|
||||
|
||||
const valueFormatter = (value: number) => value.toLocaleString();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex justify-between items-center">
|
||||
<Title>Success vs Failed Requests by Endpoint</Title>
|
||||
<CustomLegend
|
||||
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||
colors={["green", "red"]}
|
||||
/>
|
||||
</div>
|
||||
<BarChart
|
||||
className="mt-4"
|
||||
data={chartData}
|
||||
index="endpoint"
|
||||
categories={["metrics.successful_requests", "metrics.failed_requests"]}
|
||||
colors={["green", "red"]}
|
||||
valueFormatter={valueFormatter}
|
||||
customTooltip={CustomTooltip}
|
||||
showLegend={false}
|
||||
stack={true}
|
||||
yAxisWidth={60}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default EndpointUsageBarChart;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import EndpointUsageLineChart from "./EndpointUsageLineChart";
|
||||
|
||||
vi.mock("@tremor/react", async () => {
|
||||
const React = await import("react");
|
||||
|
||||
function Card({ children }: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-card" }, children);
|
||||
}
|
||||
(Card as any).displayName = "Card";
|
||||
|
||||
function Title({ children }: any) {
|
||||
return React.createElement("h2", { "data-testid": "tremor-title" }, children);
|
||||
}
|
||||
(Title as any).displayName = "Title";
|
||||
|
||||
function LineChart(_props: any) {
|
||||
return React.createElement("div", { "data-testid": "tremor-line-chart" }, "Line Chart");
|
||||
}
|
||||
(LineChart as any).displayName = "LineChart";
|
||||
|
||||
return { Card, Title, LineChart };
|
||||
});
|
||||
|
||||
vi.mock("antd", async () => {
|
||||
const React = await import("react");
|
||||
|
||||
function Segmented({ options, value, onChange }: any) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ "data-testid": "antd-segmented", "data-value": value },
|
||||
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 = "Segmented";
|
||||
|
||||
return { Segmented };
|
||||
});
|
||||
|
||||
describe("EndpointUsageLineChart", () => {
|
||||
it("should render", () => {
|
||||
render(<EndpointUsageLineChart />);
|
||||
|
||||
expect(screen.getByTestId("tremor-card")).toBeInTheDocument();
|
||||
expect(screen.getByText("Endpoint Usage Trends")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("antd-segmented")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("tremor-line-chart")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { Card, LineChart, Title } from "@tremor/react";
|
||||
import { Segmented } from "antd";
|
||||
import { DailyData } from "../../../types";
|
||||
|
||||
interface EndpointUsageLineChartProps {
|
||||
dailyData?: { results: DailyData[] };
|
||||
endpointData?: Record<string, any>;
|
||||
}
|
||||
|
||||
type TimePeriod = "7" | "30" | "90";
|
||||
|
||||
|
||||
// Transform daily data into chart format
|
||||
function transformDailyDataToChart(dailyData: DailyData[], days: number): Array<Record<string, string | number>> {
|
||||
const chartData: Array<Record<string, string | number>> = [];
|
||||
|
||||
// Get all unique endpoint names
|
||||
const endpointNames = new Set<string>();
|
||||
dailyData.forEach((day) => {
|
||||
if (day.breakdown.endpoints) {
|
||||
Object.keys(day.breakdown.endpoints).forEach((name) => endpointNames.add(name));
|
||||
}
|
||||
});
|
||||
|
||||
// Filter to the last N days based on time period
|
||||
const filteredData = dailyData.slice(-days);
|
||||
|
||||
filteredData.forEach((day) => {
|
||||
const date = new Date(day.date);
|
||||
const dateStr = date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const dataPoint: Record<string, string | number> = {
|
||||
date: dateStr,
|
||||
};
|
||||
|
||||
endpointNames.forEach((endpointName) => {
|
||||
const endpoint = day.breakdown.endpoints?.[endpointName];
|
||||
dataPoint[endpointName] = endpoint?.metrics.api_requests || 0;
|
||||
});
|
||||
|
||||
chartData.push(dataPoint);
|
||||
});
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
export function EndpointUsageLineChart({ dailyData, endpointData }: EndpointUsageLineChartProps) {
|
||||
const [timePeriod, setTimePeriod] = useState<TimePeriod>("7");
|
||||
const days = parseInt(timePeriod);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!dailyData?.results || dailyData.results.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return transformDailyDataToChart(dailyData.results, days);
|
||||
}, [dailyData, days]);
|
||||
|
||||
// Get endpoint names from chart data
|
||||
const categories = useMemo(() => {
|
||||
if (chartData.length === 0) return [];
|
||||
const keys = Object.keys(chartData[0]).filter((key) => key !== "date");
|
||||
return keys;
|
||||
}, [chartData]);
|
||||
|
||||
// Tremor color palette for multiple lines
|
||||
const colors = ["blue", "cyan", "indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange"];
|
||||
|
||||
return (
|
||||
<Card className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Title>Endpoint Usage Trends</Title>
|
||||
<Segmented
|
||||
options={[
|
||||
{
|
||||
label: "7 Days",
|
||||
value: "7",
|
||||
},
|
||||
{
|
||||
label: "30 Days",
|
||||
value: "30",
|
||||
},
|
||||
{
|
||||
label: "90 Days",
|
||||
value: "90",
|
||||
},
|
||||
]}
|
||||
value={timePeriod}
|
||||
onChange={(value) => setTimePeriod(value as TimePeriod)}
|
||||
style={{
|
||||
backgroundColor: "#f3f4f6",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<LineChart
|
||||
className="h-80"
|
||||
data={chartData}
|
||||
index="date"
|
||||
categories={categories}
|
||||
colors={colors.slice(0, categories.length)}
|
||||
valueFormatter={(value) => value.toLocaleString()}
|
||||
showLegend={true}
|
||||
showGridLines={true}
|
||||
yAxisWidth={60}
|
||||
connectNulls={true}
|
||||
curveType="natural"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default EndpointUsageLineChart;
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import EndpointUsageTable from "./EndpointUsageTable";
|
||||
|
||||
vi.mock("antd", async () => {
|
||||
const React = await import("react");
|
||||
|
||||
function Table({ columns, dataSource }: any) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ "data-testid": "antd-table" },
|
||||
columns?.map((col: any) =>
|
||||
React.createElement("div", { key: col.key, "data-testid": `column-${col.key}` }, col.title),
|
||||
),
|
||||
dataSource?.map((row: any) =>
|
||||
React.createElement(
|
||||
"div",
|
||||
{ key: row.key, "data-testid": `row-${row.key}` },
|
||||
React.createElement("div", null, row.endpoint),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
(Table as any).displayName = "Table";
|
||||
|
||||
function Progress({ percent }: any) {
|
||||
return React.createElement("div", { "data-testid": "antd-progress", "data-percent": percent });
|
||||
}
|
||||
(Progress as any).displayName = "Progress";
|
||||
|
||||
return { Table, Progress };
|
||||
});
|
||||
|
||||
vi.mock("@/utils/dataUtils", () => ({
|
||||
formatNumberWithCommas: (value: number, decimals?: number) => {
|
||||
return value.toFixed(decimals || 0);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("EndpointUsageTable", () => {
|
||||
it("should render", () => {
|
||||
const mockEndpointData = {
|
||||
"endpoint-1": {
|
||||
metrics: {
|
||||
spend: 100.5,
|
||||
prompt_tokens: 5000,
|
||||
completion_tokens: 3000,
|
||||
total_tokens: 8000,
|
||||
api_requests: 100,
|
||||
successful_requests: 95,
|
||||
failed_requests: 5,
|
||||
cache_read_input_tokens: 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
},
|
||||
metadata: {},
|
||||
api_key_breakdown: {},
|
||||
},
|
||||
};
|
||||
|
||||
render(<EndpointUsageTable endpointData={mockEndpointData} />);
|
||||
|
||||
expect(screen.getByTestId("antd-table")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-endpoint")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-requests")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-api_requests")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-successRate")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-total_tokens")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("column-spend")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React from "react";
|
||||
import { Table, Progress } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { MetricWithMetadata } from "../../../types";
|
||||
|
||||
interface EndpointUsageTableProps {
|
||||
endpointData: Record<string, MetricWithMetadata>;
|
||||
}
|
||||
|
||||
interface EndpointRow {
|
||||
key: string;
|
||||
endpoint: string;
|
||||
successful_requests: number;
|
||||
failed_requests: number;
|
||||
api_requests: number;
|
||||
total_tokens: number;
|
||||
spend: number;
|
||||
successRate: number;
|
||||
}
|
||||
|
||||
const EndpointUsageTable: React.FC<EndpointUsageTableProps> = ({ endpointData }) => {
|
||||
const calculateSuccessRate = (successful: number, total: number): number => {
|
||||
if (total === 0) return 0;
|
||||
return (successful / total) * 100;
|
||||
};
|
||||
|
||||
const dataSource: EndpointRow[] = Object.entries(endpointData).map(([endpoint, data]) => ({
|
||||
key: endpoint,
|
||||
endpoint,
|
||||
successful_requests: data.metrics.successful_requests,
|
||||
failed_requests: data.metrics.failed_requests,
|
||||
api_requests: data.metrics.api_requests,
|
||||
total_tokens: data.metrics.total_tokens,
|
||||
spend: data.metrics.spend,
|
||||
successRate: calculateSuccessRate(data.metrics.successful_requests, data.metrics.api_requests),
|
||||
}));
|
||||
|
||||
const columns: ColumnsType<EndpointRow> = [
|
||||
{
|
||||
title: "Endpoint",
|
||||
dataIndex: "endpoint",
|
||||
key: "endpoint",
|
||||
render: (text: string) => <span className="font-medium">{text}</span>,
|
||||
},
|
||||
{
|
||||
title: "Successful / Failed",
|
||||
key: "requests",
|
||||
render: (_: any, record: EndpointRow) => {
|
||||
const successPercentage =
|
||||
record.api_requests > 0 ? (record.successful_requests / record.api_requests) * 100 : 0;
|
||||
const failurePercentage = record.api_requests > 0 ? (record.failed_requests / record.api_requests) * 100 : 0;
|
||||
const totalPercentage = successPercentage + failurePercentage;
|
||||
|
||||
const strokeColorConfig: Record<string, string> = {
|
||||
"0%": "#22c55e",
|
||||
};
|
||||
if (successPercentage > 0 && successPercentage < 100) {
|
||||
strokeColorConfig[`${successPercentage}%`] = "#22c55e";
|
||||
strokeColorConfig[`${successPercentage + 0.01}%`] = "#ef4444";
|
||||
}
|
||||
strokeColorConfig["100%"] = failurePercentage > 0 ? "#ef4444" : "#22c55e";
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1 relative">
|
||||
<Progress percent={totalPercentage} size="small" strokeColor={strokeColorConfig} showInfo={false} />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-sm min-w-[100px]">
|
||||
<span className="text-green-600 font-medium">{record.successful_requests.toLocaleString()}</span>
|
||||
<span className="text-gray-400">/</span>
|
||||
<span className="text-red-600 font-medium">{record.failed_requests.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Total Request",
|
||||
dataIndex: "api_requests",
|
||||
key: "api_requests",
|
||||
render: (value: number) => value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "Success Rate",
|
||||
dataIndex: "successRate",
|
||||
key: "successRate",
|
||||
render: (value: number) => {
|
||||
const successRateStr = value.toFixed(2);
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
value >= 95
|
||||
? "text-green-600 font-medium"
|
||||
: value >= 80
|
||||
? "text-yellow-600 font-medium"
|
||||
: "text-red-600 font-medium"
|
||||
}
|
||||
>
|
||||
{successRateStr}%
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Total Tokens",
|
||||
dataIndex: "total_tokens",
|
||||
key: "total_tokens",
|
||||
render: (value: number) => value.toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: "Spend",
|
||||
dataIndex: "spend",
|
||||
key: "spend",
|
||||
render: (value: number) => `$${formatNumberWithCommas(value, 2)}`,
|
||||
},
|
||||
];
|
||||
|
||||
return <Table columns={columns} dataSource={dataSource} pagination={false} />;
|
||||
};
|
||||
|
||||
export default EndpointUsageTable;
|
||||
|
|
@ -38,6 +38,8 @@ import { valueFormatterSpend } from "../../utils/value_formatters";
|
|||
import TopKeyView from "./TopKeyView";
|
||||
import TopModelView from "./TopModelView";
|
||||
import useTeams from "@/app/(dashboard)/hooks/useTeams";
|
||||
import EndpointUsage from "../EndpointUsage/EndpointUsage";
|
||||
import NewBadge from "../../../common_components/NewBadge";
|
||||
|
||||
interface EntityMetrics {
|
||||
metrics: {
|
||||
|
|
@ -399,11 +401,14 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
filterOptions={getAllTags() || undefined}
|
||||
/>
|
||||
<TabGroup>
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>{entityType === "agent" ? "Request / Token Consumption" : "Model Activity"}</Tab>
|
||||
<Tab>Key Activity</Tab>
|
||||
</TabList>
|
||||
<NewBadge>
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>{entityType === "agent" ? "Request / Token Consumption" : "Model Activity"}</Tab>
|
||||
<Tab>Key Activity</Tab>
|
||||
<Tab>Endpoint Activity</Tab>
|
||||
</TabList>
|
||||
</NewBadge>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<Grid numItems={2} className="gap-2 w-full">
|
||||
|
|
@ -682,6 +687,9 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={keyMetrics} hidePromptCachingMetrics={entityType === "agent"} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<EndpointUsage userSpendData={spendData} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -70,8 +70,9 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
|||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("antd", async () => {
|
||||
vi.mock("antd", async (importOriginal) => {
|
||||
const React = await import("react");
|
||||
const actual = await importOriginal<typeof import("antd")>();
|
||||
|
||||
function Select(props: any) {
|
||||
const { value, onChange, options, ...rest } = props;
|
||||
|
|
@ -111,7 +112,34 @@ vi.mock("antd", async () => {
|
|||
}
|
||||
(Badge as any).displayName = "AntdBadge";
|
||||
|
||||
return { Select, Alert, Badge };
|
||||
function Table({ columns, dataSource, ...rest }: any) {
|
||||
return React.createElement(
|
||||
"div",
|
||||
{ ...rest, "data-testid": "antd-table" },
|
||||
columns?.map((col: any) =>
|
||||
React.createElement("div", { key: col.key, "data-testid": `column-${col.key}` }, col.title),
|
||||
),
|
||||
dataSource?.map((row: any) =>
|
||||
React.createElement(
|
||||
"div",
|
||||
{ key: row.key, "data-testid": `row-${row.key}` },
|
||||
columns?.map((col: any) => {
|
||||
const value = col.render ? col.render(row[col.dataIndex], row) : row[col.dataIndex];
|
||||
return React.createElement("div", { key: col.key }, value);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
(Table as any).displayName = "Table";
|
||||
|
||||
return {
|
||||
...actual,
|
||||
Select,
|
||||
Alert,
|
||||
Badge,
|
||||
Table,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@ant-design/icons", async () => {
|
||||
|
|
@ -341,9 +369,11 @@ describe("NewUsage", () => {
|
|||
});
|
||||
|
||||
// Check that key metrics are displayed
|
||||
expect(screen.getByText("Total Requests")).toBeInTheDocument();
|
||||
const totalRequestElements = screen.getAllByText("Total Requests");
|
||||
expect(totalRequestElements.length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("1,500")).toBeInTheDocument();
|
||||
expect(screen.getByText("Successful Requests")).toBeInTheDocument();
|
||||
const successfulRequestLabelElements = screen.getAllByText("Successful Requests");
|
||||
expect(successfulRequestLabelElements.length).toBeGreaterThan(0);
|
||||
// Use getAllByText since this value appears in multiple places (metrics card + table)
|
||||
const successfulRequestElements = screen.getAllByText("1,450");
|
||||
expect(successfulRequestElements.length).toBeGreaterThan(0);
|
||||
|
|
@ -357,10 +387,14 @@ describe("NewUsage", () => {
|
|||
});
|
||||
|
||||
// Check for usage metrics cards
|
||||
expect(screen.getByText("Total Requests")).toBeInTheDocument();
|
||||
expect(screen.getByText("Successful Requests")).toBeInTheDocument();
|
||||
expect(screen.getByText("Failed Requests")).toBeInTheDocument();
|
||||
expect(screen.getByText("Total Tokens")).toBeInTheDocument();
|
||||
const totalRequestElements = screen.getAllByText("Total Requests");
|
||||
expect(totalRequestElements.length).toBeGreaterThan(0);
|
||||
const successfulRequestElements = screen.getAllByText("Successful Requests");
|
||||
expect(successfulRequestElements.length).toBeGreaterThan(0);
|
||||
const failedRequestElements = screen.getAllByText("Failed Requests");
|
||||
expect(failedRequestElements.length).toBeGreaterThan(0);
|
||||
const totalTokensElements = screen.getAllByText("Total Tokens");
|
||||
expect(totalTokensElements.length).toBeGreaterThan(0);
|
||||
|
||||
// Check for chart titles
|
||||
expect(screen.getByText("Daily Spend")).toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage";
|
|||
import TopKeyView from "./EntityUsage/TopKeyView";
|
||||
import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect";
|
||||
import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser";
|
||||
import EndpointUsage from "./EndpointUsage/EndpointUsage";
|
||||
import NewBadge from "../../common_components/NewBadge";
|
||||
|
||||
interface UsagePageProps {
|
||||
teams: Team[];
|
||||
|
|
@ -434,12 +436,15 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
|
|||
{usageView === "global" && (
|
||||
<TabGroup>
|
||||
<div className="flex justify-between items-center">
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>Model Activity</Tab>
|
||||
<Tab>Key Activity</Tab>
|
||||
<Tab>MCP Server Activity</Tab>
|
||||
</TabList>
|
||||
<NewBadge>
|
||||
<TabList variant="solid" className="mt-1">
|
||||
<Tab>Cost</Tab>
|
||||
<Tab>Model Activity</Tab>
|
||||
<Tab>Key Activity</Tab>
|
||||
<Tab>MCP Server Activity</Tab>
|
||||
<Tab>Endpoint Activity</Tab>
|
||||
</TabList>
|
||||
</NewBadge>
|
||||
<Button
|
||||
onClick={() => setIsGlobalExportModalOpen(true)}
|
||||
icon={() => (
|
||||
|
|
@ -728,6 +733,9 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
|
|||
<TabPanel>
|
||||
<ActivityMetrics modelMetrics={mcpServerMetrics} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<EndpointUsage userSpendData={userSpendData} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -109,13 +109,4 @@ describe("UsageViewSelect", () => {
|
|||
|
||||
expect(mockOnChange).toHaveBeenCalledWith("team");
|
||||
});
|
||||
|
||||
it("should render badge when option has badgeText", () => {
|
||||
render(<UsageViewSelect value="agent" onChange={mockOnChange} isAdmin={true} />);
|
||||
|
||||
const badge = screen.getByTestId("antd-badge");
|
||||
expect(badge).toBeInTheDocument();
|
||||
expect(badge).toHaveAttribute("data-color", "blue");
|
||||
expect(screen.getByTestId("antd-badge-count")).toHaveTextContent("New");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ const OPTIONS: OptionConfig[] = [
|
|||
description: "View usage by AI agents",
|
||||
icon: <RobotOutlined style={{ fontSize: "16px" }} />,
|
||||
adminOnly: true,
|
||||
badgeText: "New",
|
||||
},
|
||||
{
|
||||
value: "user-agent-activity",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface BreakdownMetrics {
|
|||
providers: { [key: string]: MetricWithMetadata };
|
||||
api_keys: { [key: string]: KeyMetricWithMetadata };
|
||||
entities: { [key: string]: MetricWithMetadata };
|
||||
endpoints?: { [key: string]: MetricWithMetadata };
|
||||
}
|
||||
|
||||
export interface MetricWithMetadata {
|
||||
|
|
|
|||
|
|
@ -152,7 +152,11 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
page: "new_usage",
|
||||
icon: <BarChartOutlined />,
|
||||
roles: [...all_admin_roles, ...internalUserRoles],
|
||||
label: <span className="flex items-center gap-4">Usage</span>,
|
||||
label: (
|
||||
<span className="flex items-center gap-4">
|
||||
Usage <NewBadge />
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "logs",
|
||||
|
|
@ -260,11 +264,7 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
|
|||
{
|
||||
key: "settings",
|
||||
page: "settings",
|
||||
label: (
|
||||
<span className="flex items-center gap-4">
|
||||
Settings <NewBadge />
|
||||
</span>
|
||||
),
|
||||
label: <span className="flex items-center gap-4">Settings</span>,
|
||||
icon: <SettingOutlined />,
|
||||
roles: all_admin_roles,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ const { Title, Paragraph } = Typography;
|
|||
|
||||
import FormItem from "antd/es/form/FormItem";
|
||||
import AlertingSettings from "./alerting/alerting_settings";
|
||||
import CloudZeroCostTracking from "./CloudZeroCostTracking/CloudZeroCostTracking";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import {
|
||||
deleteCallback,
|
||||
getCallbackConfigsCall,
|
||||
|
|
@ -38,9 +40,6 @@ import {
|
|||
import { LoggingCallbacksTable } from "./Settings/LoggingAndAlerts/LoggingCallbacks/LoggingCallbacksTable";
|
||||
import { AlertingObject } from "./Settings/LoggingAndAlerts/LoggingCallbacks/types";
|
||||
import { parseErrorMessage } from "./shared/errorUtils";
|
||||
import DeleteResourceModal from "./common_components/DeleteResourceModal";
|
||||
import CloudZeroCostTracking from "./CloudZeroCostTracking/CloudZeroCostTracking";
|
||||
import NewBadge from "./common_components/NewBadge";
|
||||
interface SettingsPageProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
|
|
@ -570,9 +569,7 @@ const Settings: React.FC<SettingsPageProps> = ({ accessToken, userRole, userID,
|
|||
<TabGroup>
|
||||
<TabList variant="line" defaultValue="1">
|
||||
<Tab value="1">Logging Callbacks</Tab>
|
||||
<Tab value="2">
|
||||
CloudZero Cost Tracking <NewBadge />
|
||||
</Tab>
|
||||
<Tab value="2">CloudZero Cost Tracking</Tab>
|
||||
<Tab value="2">Alerting Types</Tab>
|
||||
<Tab value="3">Alerting Settings</Tab>
|
||||
<Tab value="4">Email Alerts</Tab>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue