Merge pull request #18798 from BerriAI/litellm_ui_endpoint_usage

[Feature] UI - Endpoint Activity in Usage
This commit is contained in:
yuneng-jiang 2026-01-10 09:42:21 -08:00 • committed by GitHub
commit b7a61d469c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 633 additions and 41 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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>

View file

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

View file

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

View file

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

View file

@ -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",

View file

@ -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 {

View file

@ -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: [

View file

@ -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>