From 40db4527f709b92eea4217234e804ad4daca5d0a Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Sat, 15 Nov 2025 16:23:48 -0800 Subject: [PATCH] [Feature] UI - Organization Usage in Usage Tab (#16614) * Organization Usage UI * Resolved build issues * Added a test --- .../src/app/(dashboard)/usage/page.tsx | 1 + ui/litellm-dashboard/src/app/page.tsx | 1 + .../EntityUsageExportModal.tsx | 2 +- .../EntityUsageExport/ExportTypeSelector.tsx | 3 +- .../EntityUsageExport/UsageExportHeader.tsx | 2 +- .../src/components/EntityUsageExport/types.ts | 3 +- .../src/components/EntityUsageExport/utils.ts | 11 +- .../common_components/default_org.tsx | 2 +- .../src/components/entity_usage.test.tsx | 18 +- .../src/components/entity_usage.tsx | 39 +++- .../src/components/networking.tsx | 205 ++++++++++-------- .../src/components/new_usage.test.tsx | 39 ++++ .../src/components/new_usage.tsx | 105 ++++++--- 13 files changed, 288 insertions(+), 143 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx index e4b44e5a450..d77b947df36 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/page.tsx @@ -15,6 +15,7 @@ const UsagePage = () => { userID={userId} teams={teams ?? []} premiumUser={premiumUser} + organizations={[]} /> ); }; diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index f547ab7d057..56c35804df9 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -480,6 +480,7 @@ export default function CreateKeyPage() { userRole={userRole} accessToken={accessToken} teams={(teams as Team[]) ?? []} + organizations={(organizations as Organization[]) ?? []} premiumUser={premiumUser} /> ) : ( diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx index 104e446cb38..672643f2ad9 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/EntityUsageExportModal.tsx @@ -22,7 +22,7 @@ const EntityUsageExportModal: React.FC = ({ const [exportScope, setExportScope] = useState("daily"); const [isExporting, setIsExporting] = useState(false); - const entityLabel = entityType === "tag" ? "Tag" : "Team"; + const entityLabel = entityType.charAt(0).toUpperCase() + entityType.slice(1); const modalTitle = customTitle || `Export ${entityLabel} Usage`; const handleExportCSV = () => { diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx index 83e719032ce..43e6f986dfb 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.tsx @@ -5,7 +5,7 @@ import type { ExportScope } from "./types"; interface ExportTypeSelectorProps { value: ExportScope; onChange: (value: ExportScope) => void; - entityType: "tag" | "team"; + entityType: "tag" | "team" | "organization"; } const ExportTypeSelector: React.FC = ({ value, onChange, entityType }) => { @@ -36,4 +36,3 @@ const ExportTypeSelector: React.FC = ({ value, onChange }; export default ExportTypeSelector; - diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx index 1f61ea260ef..3547d65379e 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.tsx @@ -7,7 +7,7 @@ import type { EntitySpendData } from "./types"; interface UsageExportHeaderProps { dateValue: DateRangePickerValue; - entityType: "tag" | "team"; + entityType: "tag" | "team" | "organization"; spendData: EntitySpendData; // Optional filter props showFilters?: boolean; diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts index b7ac41c6f33..ea11701f7ee 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/types.ts @@ -17,7 +17,7 @@ export interface EntitySpendData { export interface EntityUsageExportModalProps { isOpen: boolean; onClose: () => void; - entityType: "tag" | "team"; + entityType: "tag" | "team" | "organization"; spendData: EntitySpendData; dateRange: DateRangePickerValue; selectedFilters: string[]; @@ -59,4 +59,3 @@ export interface EntityBreakdown { id: string; }; } - diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts index a63a60e5cb4..87ca860657f 100644 --- a/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/utils.ts @@ -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", + entityType: "tag" | "team" | "organization", dateRange: { from?: Date; to?: Date }, selectedFilters: string[], exportScope: ExportScope, @@ -159,4 +159,3 @@ export const generateMetadata = ( total_tokens: spendData.metadata.total_tokens, }, }); - diff --git a/ui/litellm-dashboard/src/components/common_components/default_org.tsx b/ui/litellm-dashboard/src/components/common_components/default_org.tsx index 8bb5fdaad44..f50c59a556d 100644 --- a/ui/litellm-dashboard/src/components/common_components/default_org.tsx +++ b/ui/litellm-dashboard/src/components/common_components/default_org.tsx @@ -1,6 +1,6 @@ import { Organization } from "../networking"; export const defaultOrg = { - organization_id: null, + organization_id: "default_organization", organization_alias: "Default Organization", } as Organization; diff --git a/ui/litellm-dashboard/src/components/entity_usage.test.tsx b/ui/litellm-dashboard/src/components/entity_usage.test.tsx index a52e63a391c..d5cc503fd10 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.test.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.test.tsx @@ -3,7 +3,6 @@ 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 { @@ -18,6 +17,7 @@ beforeAll(() => { vi.mock("./networking", () => ({ tagDailyActivityCall: vi.fn(), teamDailyActivityCall: vi.fn(), + organizationDailyActivityCall: vi.fn(), })); // Mock the child components to simplify testing @@ -41,6 +41,7 @@ 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: [ @@ -126,8 +127,10 @@ 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 () => { @@ -164,6 +167,19 @@ describe("EntityUsage", () => { }); }); + it("should render with organization entity type and call organization API", async () => { + const { getByText, getAllByText } = render(); + + await waitFor(() => { + expect(mockOrganizationDailyActivityCall).toHaveBeenCalled(); + }); + + expect(getByText("Organization Spend Overview")).toBeInTheDocument(); + + const spendElements = getAllByText("$100.50"); + expect(spendElements.length).toBeGreaterThan(0); + }); + it("should switch between tabs", async () => { render(); diff --git a/ui/litellm-dashboard/src/components/entity_usage.tsx b/ui/litellm-dashboard/src/components/entity_usage.tsx index fc1d03a3729..a5789b7dbac 100644 --- a/ui/litellm-dashboard/src/components/entity_usage.tsx +++ b/ui/litellm-dashboard/src/components/entity_usage.tsx @@ -23,7 +23,7 @@ import { } from "@tremor/react"; import { ActivityMetrics, processActivityData } from "./activity_metrics"; import { DailyData, BreakdownMetrics, KeyMetricWithMetadata, EntityMetricWithMetadata, TagUsage } from "./usage/types"; -import { tagDailyActivityCall, teamDailyActivityCall } from "./networking"; +import { organizationDailyActivityCall, 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"; + entityType: "tag" | "team" | "organization"; entityId?: string | null; userID: string | null; userRole: string | null; @@ -126,6 +126,15 @@ const EntityUsage: React.FC = ({ 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"); } @@ -325,6 +334,16 @@ const EntityUsage: React.FC = ({ })); }; + 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 (
= ({ entityType={entityType} spendData={spendData} showFilters={entityList !== null && entityList.length > 0} - filterLabel={`Filter by ${entityType === "tag" ? "Tags" : "Teams"}`} - filterPlaceholder={`Select ${entityType === "tag" ? "tags" : "teams"} to filter...`} + filterLabel={getFilterLabel(entityType)} + filterPlaceholder={getFilterPlaceholder(entityType)} selectedFilters={selectedTags} onFiltersChange={setSelectedTags} filterOptions={getAllTags() || undefined} @@ -350,7 +369,7 @@ const EntityUsage: React.FC = ({ {/* Total Spend Card */} - {entityType === "tag" ? "Tag" : "Team"} Spend Overview + {capitalizedEntityLabel} Spend Overview Total Spend @@ -413,10 +432,10 @@ const EntityUsage: React.FC = ({

Failed: {data.metrics.failed_requests}

Total Tokens: {data.metrics.total_tokens}

- {entityType === "tag" ? "Total Tags" : "Total Teams"}: {entityCount} + Total {capitalizedEntityLabel}s: {entityCount}

-

Spend by {entityType === "tag" ? "Tag" : "Team"}:

+

Spend by {capitalizedEntityLabel}:

{Object.entries(data.breakdown.entities || {}) .sort(([, a], [, b]) => { const spendA = (a as EntityMetrics).metrics.spend; @@ -449,10 +468,10 @@ const EntityUsage: React.FC = ({
- Spend Per {entityType === "tag" ? "Tag" : "Team"} + Spend Per {capitalizedEntityLabel} Showing Top 5 by Spend
- Get Started by Tracking cost per {entityType} + Get Started by Tracking cost per {capitalizedEntityLabel} = ({ - {entityType === "tag" ? "Tag" : "Team"} + {capitalizedEntityLabel} Spend Successful Failed diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 87c8563e467..14763930aaf 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -143,7 +143,7 @@ export interface ListPromptsResponse { } export interface Organization { - organization_id: string | null; + organization_id: string; organization_alias: string; budget_id: string; metadata: Record; @@ -1570,21 +1570,70 @@ export const transformRequestCall = async (accessToken: string, request: object) } }; -export const userDailyActivityCall = async (accessToken: string, startTime: Date, endTime: Date, page: number = 1) => { - /** - * Get daily user activity on proxy - */ - try { - 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}`; +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, +) => { + 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; +}; + +const fetchDailyActivity = async ({ + accessToken, + endpoint, + startTime, + endTime, + page = 1, + extraQueryParams, +}: DailyActivityCallOptions) => { + try { + const url = buildDailyActivityUrl(endpoint, startTime, endTime, page, extraQueryParams); const response = await fetch(url, { method: "GET", @@ -1604,11 +1653,24 @@ export const userDailyActivityCall = async (accessToken: string, startTime: Date const data = await response.json(); return data; } catch (error) { - console.error("Failed to create key:", error); + console.error(`Failed to fetch daily activity (${endpoint}):`, 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, @@ -1619,42 +1681,16 @@ export const tagDailyActivityCall = async ( /** * Get daily user activity on proxy */ - 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; - } + return fetchDailyActivity({ + accessToken, + endpoint: "/tag/daily/activity", + startTime, + endTime, + page, + extraQueryParams: { + tags, + }, + }); }; export const teamDailyActivityCall = async ( @@ -1667,43 +1703,36 @@ export const teamDailyActivityCall = async ( /** * Get daily user activity on proxy */ - 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}`; - } + return fetchDailyActivity({ + accessToken, + endpoint: "/team/daily/activity", + startTime, + endTime, + page, + extraQueryParams: { + team_ids: teamIds, + exclude_team_ids: "litellm-dashboard", + }, + }); +}; - 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 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, + }, + }); }; export const getTotalSpendCall = async (accessToken: string) => { diff --git a/ui/litellm-dashboard/src/components/new_usage.test.tsx b/ui/litellm-dashboard/src/components/new_usage.test.tsx index 6c426401c54..a4969124f1f 100644 --- a/ui/litellm-dashboard/src/components/new_usage.test.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.test.tsx @@ -1,6 +1,7 @@ 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 @@ -153,6 +154,26 @@ 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", @@ -175,6 +196,7 @@ describe("NewUsage", () => { members_with_roles: [], }, ], + organizations: [], premiumUser: true, }; @@ -250,4 +272,21 @@ describe("NewUsage", () => { expect(entityUsageElements.length).toBeGreaterThan(0); }); }); + + it("should show organization usage banner and tab for admins", async () => { + const { getByText, getAllByText } = render(); + + 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); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/new_usage.tsx b/ui/litellm-dashboard/src/components/new_usage.tsx index 95d721ce9ba..4794a7f091d 100644 --- a/ui/litellm-dashboard/src/components/new_usage.tsx +++ b/ui/litellm-dashboard/src/components/new_usage.tsx @@ -6,58 +6,66 @@ * Works at 1m+ spend logs, by querying an aggregate table instead. */ -import React, { useState, useEffect, useMemo, useCallback } from "react"; import { BarChart, Card, - Title, - Text, - Grid, Col, - TabGroup, - TabList, - Tab, - TabPanel, - TabPanels, + DateRangePickerValue, DonutChart, + Grid, + Tab, + TabGroup, Table, - TableHead, - TableRow, - TableHeaderCell, TableBody, TableCell, - DateRangePickerValue, + TableHead, + TableHeaderCell, + TableRow, + TabList, + TabPanel, + TabPanels, + Text, + Title, } from "@tremor/react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Alert } from "antd"; -import { userDailyActivityCall, userDailyActivityAggregatedCall, tagListCall } from "./networking"; -import { Tag } from "./tag_management/types"; -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"; +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 { 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"; interface NewUsagePageProps { accessToken: string | null; userRole: string | null; userID: string | null; teams: Team[]; + organizations: Organization[]; premiumUser: boolean; } -const NewUsagePage: React.FC = ({ accessToken, userRole, userID, teams, premiumUser }) => { +const NewUsagePage: React.FC = ({ + accessToken, + userRole, + userID, + teams, + organizations, + premiumUser, +}) => { const [userSpendData, setUserSpendData] = useState<{ results: DailyData[]; metadata: any; @@ -81,6 +89,7 @@ const NewUsagePage: React.FC = ({ accessToken, userRole, user 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) { @@ -415,6 +424,11 @@ const NewUsagePage: React.FC = ({ accessToken, userRole, user
{all_admin_roles.includes(userRole || "") ? Global Usage : Your Usage} + {all_admin_roles.includes(userRole || "") ? ( + Organization Usage + ) : ( + Your Organization Usage + )} Team Usage {all_admin_roles.includes(userRole || "") ? Tag Usage : <>} {all_admin_roles.includes(userRole || "") ? User Agent Activity : <>} @@ -737,6 +751,35 @@ const NewUsagePage: React.FC = ({ accessToken, userRole, user + {/* Organization Usage Panel */} + + {showOrganizationBanner && ( + setShowOrganizationBanner(false)} + className="mb-5" + /> + )} + ({ + label: organization.organization_alias, + value: organization.organization_id, + })) || null + } + premiumUser={premiumUser} + /> + + {/* Team Usage Panel */}