mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-21 08:26:34 +00:00
[Fix] Revert UI - Organization Usage (#16980)
* Revert "[Feature] UI - Organization Usage in Usage Tab (#16614)"
This reverts commit 4de182c98f.
* Networking Changes
This commit is contained in:
@@ -15,7 +15,6 @@ const UsagePage = () => {
|
||||
userID={userId}
|
||||
teams={teams ?? []}
|
||||
premiumUser={premiumUser}
|
||||
organizations={[]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -480,7 +480,6 @@ export default function CreateKeyPage() {
|
||||
userRole={userRole}
|
||||
accessToken={accessToken}
|
||||
teams={(teams as Team[]) ?? []}
|
||||
organizations={(organizations as Organization[]) ?? []}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -22,7 +22,7 @@ const EntityUsageExportModal: React.FC<EntityUsageExportModalProps> = ({
|
||||
const [exportScope, setExportScope] = useState<ExportScope>("daily");
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
|
||||
const entityLabel = entityType === "tag" ? "Tag" : "Team";
|
||||
const modalTitle = customTitle || `Export ${entityLabel} Usage`;
|
||||
|
||||
const handleExportCSV = () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ExportScope } from "./types";
|
||||
interface ExportTypeSelectorProps {
|
||||
value: ExportScope;
|
||||
onChange: (value: ExportScope) => void;
|
||||
entityType: "tag" | "team" | "organization";
|
||||
entityType: "tag" | "team";
|
||||
}
|
||||
|
||||
const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange, entityType }) => {
|
||||
@@ -36,3 +36,4 @@ const ExportTypeSelector: React.FC<ExportTypeSelectorProps> = ({ value, onChange
|
||||
};
|
||||
|
||||
export default ExportTypeSelector;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { EntitySpendData } from "./types";
|
||||
|
||||
interface UsageExportHeaderProps {
|
||||
dateValue: DateRangePickerValue;
|
||||
entityType: "tag" | "team" | "organization";
|
||||
entityType: "tag" | "team";
|
||||
spendData: EntitySpendData;
|
||||
// Optional filter props
|
||||
showFilters?: boolean;
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface EntitySpendData {
|
||||
export interface EntityUsageExportModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
entityType: "tag" | "team" | "organization";
|
||||
entityType: "tag" | "team";
|
||||
spendData: EntitySpendData;
|
||||
dateRange: DateRangePickerValue;
|
||||
selectedFilters: string[];
|
||||
@@ -59,3 +59,4 @@ export interface EntityBreakdown {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export const generateDailyData = (spendData: EntitySpendData, entityLabel: strin
|
||||
[entityLabel]: data.metadata?.team_alias || entity,
|
||||
[`${entityLabel} ID`]: entity,
|
||||
"Spend ($)": formatNumberWithCommas(data.metrics.spend, 4),
|
||||
Requests: data.metrics.api_requests,
|
||||
"Requests": data.metrics.api_requests,
|
||||
"Successful Requests": data.metrics.successful_requests,
|
||||
"Failed Requests": data.metrics.failed_requests,
|
||||
"Total Tokens": data.metrics.total_tokens,
|
||||
@@ -109,9 +109,9 @@ export const generateDailyWithModelsData = (spendData: EntitySpendData, entityLa
|
||||
[`${entityLabel} ID`]: entity,
|
||||
Model: model,
|
||||
"Spend ($)": formatNumberWithCommas(metrics.spend, 4),
|
||||
Requests: metrics.requests,
|
||||
Successful: metrics.successful,
|
||||
Failed: metrics.failed,
|
||||
"Requests": metrics.requests,
|
||||
"Successful": metrics.successful,
|
||||
"Failed": metrics.failed,
|
||||
"Total Tokens": metrics.tokens,
|
||||
});
|
||||
});
|
||||
@@ -137,7 +137,7 @@ export const generateExportData = (
|
||||
};
|
||||
|
||||
export const generateMetadata = (
|
||||
entityType: "tag" | "team" | "organization",
|
||||
entityType: "tag" | "team",
|
||||
dateRange: { from?: Date; to?: Date },
|
||||
selectedFilters: string[],
|
||||
exportScope: ExportScope,
|
||||
@@ -159,3 +159,4 @@ export const generateMetadata = (
|
||||
total_tokens: spendData.metadata.total_tokens,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Organization } from "../networking";
|
||||
|
||||
export const defaultOrg = {
|
||||
organization_id: "default_organization",
|
||||
organization_id: null,
|
||||
organization_alias: "Default Organization",
|
||||
} as Organization;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import EntityUsage from "./entity_usage";
|
||||
import * as networking from "./networking";
|
||||
|
||||
// Polyfill ResizeObserver for test environment
|
||||
beforeAll(() => {
|
||||
if (typeof window !== "undefined" && !window.ResizeObserver) {
|
||||
window.ResizeObserver = class ResizeObserver {
|
||||
@@ -17,7 +18,6 @@ beforeAll(() => {
|
||||
vi.mock("./networking", () => ({
|
||||
tagDailyActivityCall: vi.fn(),
|
||||
teamDailyActivityCall: vi.fn(),
|
||||
organizationDailyActivityCall: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the child components to simplify testing
|
||||
@@ -41,7 +41,6 @@ vi.mock("./EntityUsageExport", () => ({
|
||||
describe("EntityUsage", () => {
|
||||
const mockTagDailyActivityCall = vi.mocked(networking.tagDailyActivityCall);
|
||||
const mockTeamDailyActivityCall = vi.mocked(networking.teamDailyActivityCall);
|
||||
const mockOrganizationDailyActivityCall = vi.mocked(networking.organizationDailyActivityCall);
|
||||
|
||||
const mockSpendData = {
|
||||
results: [
|
||||
@@ -127,10 +126,8 @@ describe("EntityUsage", () => {
|
||||
beforeEach(() => {
|
||||
mockTagDailyActivityCall.mockClear();
|
||||
mockTeamDailyActivityCall.mockClear();
|
||||
mockOrganizationDailyActivityCall.mockClear();
|
||||
mockTagDailyActivityCall.mockResolvedValue(mockSpendData);
|
||||
mockTeamDailyActivityCall.mockResolvedValue(mockSpendData);
|
||||
mockOrganizationDailyActivityCall.mockResolvedValue(mockSpendData);
|
||||
});
|
||||
|
||||
it("should render with tag entity type and display spend metrics", async () => {
|
||||
@@ -167,21 +164,6 @@ describe("EntityUsage", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render with organization entity type and call organization API", async () => {
|
||||
const { getByText, getAllByText } = render(<EntityUsage {...defaultProps} entityType="organization" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOrganizationDailyActivityCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(getByText("Organization Spend Overview")).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
const spendElements = getAllByText("$100.50");
|
||||
expect(spendElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should switch between tabs", async () => {
|
||||
render(<EntityUsage {...defaultProps} />);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
} from "@tremor/react";
|
||||
import { ActivityMetrics, processActivityData } from "./activity_metrics";
|
||||
import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata, TagUsage } from "./usage/types";
|
||||
import { organizationDailyActivityCall, tagDailyActivityCall, teamDailyActivityCall } from "./networking";
|
||||
import { tagDailyActivityCall, teamDailyActivityCall } from "./networking";
|
||||
import TopKeyView from "./top_key_view";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { valueFormatterSpend } from "./usage/utils/value_formatters";
|
||||
@@ -68,7 +68,7 @@ export interface EntityList {
|
||||
|
||||
interface EntityUsageProps {
|
||||
accessToken: string | null;
|
||||
entityType: "tag" | "team" | "organization";
|
||||
entityType: "tag" | "team";
|
||||
entityId?: string | null;
|
||||
userID: string | null;
|
||||
userRole: string | null;
|
||||
@@ -126,15 +126,6 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
selectedTags.length > 0 ? selectedTags : null,
|
||||
);
|
||||
setSpendData(data);
|
||||
} else if (entityType === "organization") {
|
||||
const data = await organizationDailyActivityCall(
|
||||
accessToken,
|
||||
startTime,
|
||||
endTime,
|
||||
1,
|
||||
selectedTags.length > 0 ? selectedTags : null,
|
||||
);
|
||||
setSpendData(data);
|
||||
} else {
|
||||
throw new Error("Invalid entity type");
|
||||
}
|
||||
@@ -334,16 +325,6 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
}));
|
||||
};
|
||||
|
||||
const getFilterLabel = (entityType: string) => {
|
||||
return `Filter by ${entityType}`;
|
||||
};
|
||||
|
||||
const getFilterPlaceholder = (entityType: string) => {
|
||||
return `Select ${entityType} to filter...`;
|
||||
};
|
||||
|
||||
const capitalizedEntityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%" }} className="relative">
|
||||
<UsageExportHeader
|
||||
@@ -351,8 +332,8 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
entityType={entityType}
|
||||
spendData={spendData}
|
||||
showFilters={entityList !== null && entityList.length > 0}
|
||||
filterLabel={getFilterLabel(entityType)}
|
||||
filterPlaceholder={getFilterPlaceholder(entityType)}
|
||||
filterLabel={`Filter by ${entityType === "tag" ? "Tags" : "Teams"}`}
|
||||
filterPlaceholder={`Select ${entityType === "tag" ? "tags" : "teams"} to filter...`}
|
||||
selectedFilters={selectedTags}
|
||||
onFiltersChange={setSelectedTags}
|
||||
filterOptions={getAllTags() || undefined}
|
||||
@@ -369,7 +350,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
{/* Total Spend Card */}
|
||||
<Col numColSpan={2}>
|
||||
<Card>
|
||||
<Title>{capitalizedEntityLabel} Spend Overview</Title>
|
||||
<Title>{entityType === "tag" ? "Tag" : "Team"} Spend Overview</Title>
|
||||
<Grid numItems={5} className="gap-4 mt-4">
|
||||
<Card>
|
||||
<Title>Total Spend</Title>
|
||||
@@ -432,10 +413,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
<p className="text-gray-600">Failed: {data.metrics.failed_requests}</p>
|
||||
<p className="text-gray-600">Total Tokens: {data.metrics.total_tokens}</p>
|
||||
<p className="text-gray-600">
|
||||
Total {capitalizedEntityLabel}s: {entityCount}
|
||||
{entityType === "tag" ? "Total Tags" : "Total Teams"}: {entityCount}
|
||||
</p>
|
||||
<div className="mt-2 border-t pt-2">
|
||||
<p className="font-semibold">Spend by {capitalizedEntityLabel}:</p>
|
||||
<p className="font-semibold">Spend by {entityType === "tag" ? "Tag" : "Team"}:</p>
|
||||
{Object.entries(data.breakdown.entities || {})
|
||||
.sort(([, a], [, b]) => {
|
||||
const spendA = (a as EntityMetrics).metrics.spend;
|
||||
@@ -468,10 +449,10 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
<Card>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Title>Spend Per {capitalizedEntityLabel}</Title>
|
||||
<Title>Spend Per {entityType === "tag" ? "Tag" : "Team"}</Title>
|
||||
<Subtitle className="text-xs">Showing Top 5 by Spend</Subtitle>
|
||||
<div className="flex items-center text-sm text-gray-500">
|
||||
<span>Get Started by Tracking cost per {capitalizedEntityLabel} </span>
|
||||
<span>Get Started by Tracking cost per {entityType} </span>
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
|
||||
className="text-blue-500 hover:text-blue-700 ml-1"
|
||||
@@ -515,7 +496,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>{capitalizedEntityLabel}</TableHeaderCell>
|
||||
<TableHeaderCell>{entityType === "tag" ? "Tag" : "Team"}</TableHeaderCell>
|
||||
<TableHeaderCell>Spend</TableHeaderCell>
|
||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||
|
||||
@@ -143,7 +143,7 @@ export interface ListPromptsResponse {
|
||||
}
|
||||
|
||||
export interface Organization {
|
||||
organization_id: string;
|
||||
organization_id: string | null;
|
||||
organization_alias: string;
|
||||
budget_id: string;
|
||||
metadata: Record<string, any>;
|
||||
@@ -1570,70 +1570,21 @@ export const transformRequestCall = async (accessToken: string, request: object)
|
||||
}
|
||||
};
|
||||
|
||||
type DailyActivityQueryValue = string | number | string[] | null | undefined;
|
||||
|
||||
const DEFAULT_DAILY_ACTIVITY_PAGE_SIZE = "1000";
|
||||
|
||||
const appendDailyActivityQueryParam = (params: URLSearchParams, key: string, value: DailyActivityQueryValue) => {
|
||||
if (value === null || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
params.append(key, value.join(","));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
params.append(key, `${value}`);
|
||||
};
|
||||
|
||||
const buildDailyActivityUrl = (
|
||||
endpoint: string,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
page: number,
|
||||
extraQueryParams?: Record<string, DailyActivityQueryValue>,
|
||||
) => {
|
||||
const resolvedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
||||
const baseUrl = proxyBaseUrl ? `${proxyBaseUrl}${resolvedEndpoint}` : resolvedEndpoint;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append("start_date", formatDate(startTime));
|
||||
params.append("end_date", formatDate(endTime));
|
||||
params.append("page_size", DEFAULT_DAILY_ACTIVITY_PAGE_SIZE);
|
||||
params.append("page", page.toString());
|
||||
|
||||
if (extraQueryParams) {
|
||||
Object.entries(extraQueryParams).forEach(([key, value]) => {
|
||||
appendDailyActivityQueryParam(params, key, value);
|
||||
});
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
return queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||||
};
|
||||
|
||||
type DailyActivityCallOptions = {
|
||||
accessToken: string;
|
||||
endpoint: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
page?: number;
|
||||
extraQueryParams?: Record<string, DailyActivityQueryValue>;
|
||||
};
|
||||
|
||||
const fetchDailyActivity = async ({
|
||||
accessToken,
|
||||
endpoint,
|
||||
startTime,
|
||||
endTime,
|
||||
page = 1,
|
||||
extraQueryParams,
|
||||
}: DailyActivityCallOptions) => {
|
||||
export const userDailyActivityCall = async (accessToken: string, startTime: Date, endTime: Date, page: number = 1) => {
|
||||
/**
|
||||
* Get daily user activity on proxy
|
||||
*/
|
||||
try {
|
||||
const url = buildDailyActivityUrl(endpoint, startTime, endTime, page, extraQueryParams);
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/user/daily/activity` : `/user/daily/activity`;
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("start_date", formatDate(startTime));
|
||||
queryParams.append("end_date", formatDate(endTime));
|
||||
queryParams.append("page_size", "1000");
|
||||
queryParams.append("page", page.toString());
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
@@ -1653,24 +1604,11 @@ const fetchDailyActivity = async ({
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch daily activity (${endpoint}):`, error);
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const userDailyActivityCall = async (accessToken: string, startTime: Date, endTime: Date, page: number = 1) => {
|
||||
/**
|
||||
* Get daily user activity on proxy
|
||||
*/
|
||||
return fetchDailyActivity({
|
||||
accessToken,
|
||||
endpoint: "/user/daily/activity",
|
||||
startTime,
|
||||
endTime,
|
||||
page,
|
||||
});
|
||||
};
|
||||
|
||||
export const tagDailyActivityCall = async (
|
||||
accessToken: string,
|
||||
startTime: Date,
|
||||
@@ -1681,16 +1619,42 @@ export const tagDailyActivityCall = async (
|
||||
/**
|
||||
* Get daily user activity on proxy
|
||||
*/
|
||||
return fetchDailyActivity({
|
||||
accessToken,
|
||||
endpoint: "/tag/daily/activity",
|
||||
startTime,
|
||||
endTime,
|
||||
page,
|
||||
extraQueryParams: {
|
||||
tags,
|
||||
},
|
||||
});
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/tag/daily/activity` : `/tag/daily/activity`;
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("start_date", formatDate(startTime));
|
||||
queryParams.append("end_date", formatDate(endTime));
|
||||
queryParams.append("page_size", "1000");
|
||||
queryParams.append("page", page.toString());
|
||||
if (tags) {
|
||||
queryParams.append("tags", tags.join(","));
|
||||
}
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const teamDailyActivityCall = async (
|
||||
@@ -1703,36 +1667,43 @@ export const teamDailyActivityCall = async (
|
||||
/**
|
||||
* Get daily user activity on proxy
|
||||
*/
|
||||
return fetchDailyActivity({
|
||||
accessToken,
|
||||
endpoint: "/team/daily/activity",
|
||||
startTime,
|
||||
endTime,
|
||||
page,
|
||||
extraQueryParams: {
|
||||
team_ids: teamIds,
|
||||
exclude_team_ids: "litellm-dashboard",
|
||||
},
|
||||
});
|
||||
};
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/team/daily/activity` : `/team/daily/activity`;
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.append("start_date", formatDate(startTime));
|
||||
queryParams.append("end_date", formatDate(endTime));
|
||||
queryParams.append("page_size", "1000");
|
||||
queryParams.append("page", page.toString());
|
||||
if (teamIds) {
|
||||
queryParams.append("team_ids", teamIds.join(","));
|
||||
}
|
||||
queryParams.append("exclude_team_ids", "litellm-dashboard");
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
|
||||
export const organizationDailyActivityCall = async (
|
||||
accessToken: string,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
page: number = 1,
|
||||
organizationIds: string[] | null = null,
|
||||
) => {
|
||||
return fetchDailyActivity({
|
||||
accessToken,
|
||||
endpoint: "/organization/daily/activity",
|
||||
startTime,
|
||||
endTime,
|
||||
page,
|
||||
extraQueryParams: {
|
||||
organization_ids: organizationIds,
|
||||
},
|
||||
});
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getTotalSpendCall = async (accessToken: string) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll } from "vitest";
|
||||
import NewUsagePage from "./new_usage";
|
||||
import type { Organization } from "./networking";
|
||||
import * as networking from "./networking";
|
||||
|
||||
// Polyfill ResizeObserver for test environment
|
||||
@@ -154,26 +153,6 @@ describe("NewUsage", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockOrganizations: Organization[] = [
|
||||
{
|
||||
organization_id: "org-123",
|
||||
organization_alias: "Acme Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: [],
|
||||
spend: 0,
|
||||
model_spend: {},
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
created_by: "user-123",
|
||||
updated_at: "2025-01-02T00:00:00Z",
|
||||
updated_by: "user-123",
|
||||
litellm_budget_table: null,
|
||||
teams: null,
|
||||
users: null,
|
||||
members: null,
|
||||
},
|
||||
];
|
||||
|
||||
const defaultProps = {
|
||||
accessToken: "test-token",
|
||||
userRole: "Admin",
|
||||
@@ -196,7 +175,6 @@ describe("NewUsage", () => {
|
||||
members_with_roles: [],
|
||||
},
|
||||
],
|
||||
organizations: [],
|
||||
premiumUser: true,
|
||||
};
|
||||
|
||||
@@ -272,21 +250,4 @@ describe("NewUsage", () => {
|
||||
expect(entityUsageElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show organization usage banner and tab for admins", async () => {
|
||||
const { getByText, getAllByText } = render(<NewUsagePage {...defaultProps} organizations={mockOrganizations} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const organizationTab = getByText("Organization Usage");
|
||||
fireEvent.click(organizationTab);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByText("Organization usage is a new feature.")).toBeInTheDocument();
|
||||
const entityUsageElements = getAllByText("Entity Usage");
|
||||
expect(entityUsageElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,66 +6,58 @@
|
||||
* Works at 1m+ spend logs, by querying an aggregate table instead.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo, useCallback } from "react";
|
||||
import {
|
||||
BarChart,
|
||||
Card,
|
||||
Col,
|
||||
DateRangePickerValue,
|
||||
DonutChart,
|
||||
Title,
|
||||
Text,
|
||||
Grid,
|
||||
Tab,
|
||||
Col,
|
||||
TabGroup,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Text,
|
||||
Title,
|
||||
DonutChart,
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
DateRangePickerValue,
|
||||
} from "@tremor/react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Alert } from "antd";
|
||||
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { Button } from "@tremor/react";
|
||||
import { all_admin_roles } from "../utils/roles";
|
||||
import { ActivityMetrics, processActivityData } from "./activity_metrics";
|
||||
import CloudZeroExportModal from "./cloudzero_export_modal";
|
||||
import EntityUsage, { EntityList } from "./entity_usage";
|
||||
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 { userDailyActivityCall, userDailyActivityAggregatedCall, tagListCall } from "./networking";
|
||||
import { Tag } from "./tag_management/types";
|
||||
import TopKeyView from "./top_key_view";
|
||||
import { DailyData, KeyMetricWithMetadata, MetricWithMetadata } from "./usage/types";
|
||||
import { valueFormatterSpend } from "./usage/utils/value_formatters";
|
||||
import UserAgentActivity from "./user_agent_activity";
|
||||
import ViewUserSpend from "./view_user_spend";
|
||||
import TopKeyView from "./top_key_view";
|
||||
import { ActivityMetrics, processActivityData } from "./activity_metrics";
|
||||
import UserAgentActivity from "./user_agent_activity";
|
||||
import { DailyData, MetricWithMetadata, KeyMetricWithMetadata } from "./usage/types";
|
||||
import EntityUsage from "./entity_usage";
|
||||
import { all_admin_roles } from "../utils/roles";
|
||||
import { Team } from "./key_team_helpers/key_list";
|
||||
import { EntityList } from "./entity_usage";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { valueFormatterSpend } from "./usage/utils/value_formatters";
|
||||
import CloudZeroExportModal from "./cloudzero_export_modal";
|
||||
import { ChartLoader } from "./shared/chart_loader";
|
||||
import { getProviderLogoAndName } from "./provider_info_helpers";
|
||||
import EntityUsageExportModal from "./EntityUsageExport";
|
||||
import AdvancedDatePicker from "./shared/advanced_date_picker";
|
||||
import { Button } from "@tremor/react";
|
||||
|
||||
interface NewUsagePageProps {
|
||||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
teams: Team[];
|
||||
organizations: Organization[];
|
||||
premiumUser: boolean;
|
||||
}
|
||||
|
||||
const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||
accessToken,
|
||||
userRole,
|
||||
userID,
|
||||
teams,
|
||||
organizations,
|
||||
premiumUser,
|
||||
}) => {
|
||||
const NewUsagePage: React.FC<NewUsagePageProps> = ({ accessToken, userRole, userID, teams, premiumUser }) => {
|
||||
const [userSpendData, setUserSpendData] = useState<{
|
||||
results: DailyData[];
|
||||
metadata: any;
|
||||
@@ -89,7 +81,6 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||
const [modelViewType, setModelViewType] = useState<"groups" | "individual">("groups");
|
||||
const [isCloudZeroModalOpen, setIsCloudZeroModalOpen] = useState(false);
|
||||
const [isGlobalExportModalOpen, setIsGlobalExportModalOpen] = useState(false);
|
||||
const [showOrganizationBanner, setShowOrganizationBanner] = useState(true);
|
||||
|
||||
const getAllTags = async () => {
|
||||
if (!accessToken) {
|
||||
@@ -424,11 +415,6 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||
<div className="flex items-end justify-start gap-6 mb-6">
|
||||
<TabList variant="solid">
|
||||
{all_admin_roles.includes(userRole || "") ? <Tab>Global Usage</Tab> : <Tab>Your Usage</Tab>}
|
||||
{all_admin_roles.includes(userRole || "") ? (
|
||||
<Tab>Organization Usage</Tab>
|
||||
) : (
|
||||
<Tab>Your Organization Usage</Tab>
|
||||
)}
|
||||
<Tab>Team Usage</Tab>
|
||||
{all_admin_roles.includes(userRole || "") ? <Tab>Tag Usage</Tab> : <></>}
|
||||
{all_admin_roles.includes(userRole || "") ? <Tab>User Agent Activity</Tab> : <></>}
|
||||
@@ -751,35 +737,6 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||
</TabGroup>
|
||||
</TabPanel>
|
||||
|
||||
{/* Organization Usage Panel */}
|
||||
<TabPanel>
|
||||
{showOrganizationBanner && (
|
||||
<Alert
|
||||
banner
|
||||
type="info"
|
||||
message="Organization usage is a new feature."
|
||||
description="Spend is tracked from feature launch and previous data isn't backfilled, so only future usage appears here."
|
||||
closable
|
||||
onClose={() => setShowOrganizationBanner(false)}
|
||||
className="mb-5"
|
||||
/>
|
||||
)}
|
||||
<EntityUsage
|
||||
accessToken={accessToken}
|
||||
entityType="organization"
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
dateValue={dateValue}
|
||||
entityList={
|
||||
organizations?.map((organization) => ({
|
||||
label: organization.organization_alias,
|
||||
value: organization.organization_id,
|
||||
})) || null
|
||||
}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
{/* Team Usage Panel */}
|
||||
<TabPanel>
|
||||
<EntityUsage
|
||||
|
||||
Reference in New Issue
Block a user