Agent Usage UI

This commit is contained in:
yuneng-jiang 2025-12-10 15:46:22 -08:00
parent ccb6cd880a
commit 8ed1dfcb6a
8 changed files with 146 additions and 17 deletions

View file

@ -0,0 +1,15 @@
import { getAgentsList } from "@/components/networking";
import { AgentsResponse } from "@/components/agents/types";
import { useQuery } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
import { all_admin_roles } from "@/utils/roles";
const agentsKeys = createQueryKeys("agents");
export const useAgents = (accessToken: string | null, userRole: string | null) => {
return useQuery<AgentsResponse>({
queryKey: agentsKeys.list({}),
queryFn: async () => await getAgentsList(accessToken!),
enabled: Boolean(accessToken) && all_admin_roles.includes(userRole || ""),
});
};

View file

@ -1,13 +1,13 @@
import React, { useState } from "react";
import type { DateRangePickerValue } from "@tremor/react";
import { Button, Text } from "@tremor/react";
import { Select } from "antd";
import React, { useState } from "react";
import EntityUsageExportModal from "./EntityUsageExportModal";
import type { DateRangePickerValue } from "@tremor/react";
import type { EntitySpendData } from "./types";
import type { EntitySpendData, EntityType } from "./types";
interface UsageExportHeaderProps {
dateValue: DateRangePickerValue;
entityType: "tag" | "team" | "organization" | "customer";
entityType: EntityType;
spendData: EntitySpendData;
// Optional filter props
showFilters?: boolean;

View file

@ -2,7 +2,7 @@ import type { DateRangePickerValue } from "@tremor/react";
export type ExportFormat = "csv" | "json";
export type ExportScope = "daily" | "daily_with_models";
export type EntityType = "tag" | "team" | "organization" | "customer";
export type EntityType = "tag" | "team" | "organization" | "customer" | "agent";
export interface EntitySpendData {
results: any[];

View file

@ -19,6 +19,7 @@ vi.mock("./networking", () => ({
teamDailyActivityCall: vi.fn(),
organizationDailyActivityCall: vi.fn(),
customerDailyActivityCall: vi.fn(),
agentDailyActivityCall: vi.fn(),
}));
// Mock the child components to simplify testing
@ -44,6 +45,7 @@ describe("EntityUsage", () => {
const mockTeamDailyActivityCall = vi.mocked(networking.teamDailyActivityCall);
const mockOrganizationDailyActivityCall = vi.mocked(networking.organizationDailyActivityCall);
const mockCustomerDailyActivityCall = vi.mocked(networking.customerDailyActivityCall);
const mockAgentDailyActivityCall = vi.mocked(networking.agentDailyActivityCall);
const mockSpendData = {
results: [
@ -131,10 +133,12 @@ describe("EntityUsage", () => {
mockTeamDailyActivityCall.mockClear();
mockOrganizationDailyActivityCall.mockClear();
mockCustomerDailyActivityCall.mockClear();
mockAgentDailyActivityCall.mockClear();
mockTagDailyActivityCall.mockResolvedValue(mockSpendData);
mockTeamDailyActivityCall.mockResolvedValue(mockSpendData);
mockOrganizationDailyActivityCall.mockResolvedValue(mockSpendData);
mockCustomerDailyActivityCall.mockResolvedValue(mockSpendData);
mockAgentDailyActivityCall.mockResolvedValue(mockSpendData);
});
it("should render with tag entity type and display spend metrics", async () => {
@ -201,6 +205,21 @@ describe("EntityUsage", () => {
});
});
it("should render with agent entity type and call agent API", async () => {
render(<EntityUsage {...defaultProps} entityType="agent" />);
await waitFor(() => {
expect(mockAgentDailyActivityCall).toHaveBeenCalled();
});
expect(screen.getByText("Agent Spend Overview")).toBeInTheDocument();
await waitFor(() => {
const spendElements = screen.getAllByText("$100.50");
expect(spendElements.length).toBeGreaterThan(0);
});
});
it("should switch between tabs", async () => {
render(<EntityUsage {...defaultProps} />);

View file

@ -28,6 +28,7 @@ import {
tagDailyActivityCall,
teamDailyActivityCall,
customerDailyActivityCall,
agentDailyActivityCall,
} from "./networking";
import TopKeyView from "./top_key_view";
import { formatNumberWithCommas } from "@/utils/dataUtils";
@ -150,6 +151,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
selectedTags.length > 0 ? selectedTags : null,
);
setSpendData(data);
} else if (entityType === "agent") {
const data = await agentDailyActivityCall(
accessToken,
startTime,
endTime,
1,
selectedTags.length > 0 ? selectedTags : null,
);
setSpendData(data);
} else {
throw new Error("Invalid entity type");
}

View file

@ -1754,6 +1754,25 @@ export const customerDailyActivityCall = async (
});
};
export const agentDailyActivityCall = async (
accessToken: string,
startTime: Date,
endTime: Date,
page: number = 1,
agentIds: string[] | null = null,
) => {
return fetchDailyActivity({
accessToken,
endpoint: "/agent/daily/activity",
startTime,
endTime,
page,
extraQueryParams: {
agent_ids: agentIds,
},
});
};
export const getTotalSpendCall = async (accessToken: string) => {
/**
* Get all models on proxy

View file

@ -1,9 +1,10 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { render, screen, fireEvent, waitFor, act } 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";
import { useCustomers } from "@/app/(dashboard)/hooks/customers/useCustomers";
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
// Polyfill ResizeObserver for test environment
beforeAll(() => {
@ -58,10 +59,15 @@ vi.mock("@/app/(dashboard)/hooks/customers/useCustomers", () => ({
useCustomers: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/agents/useAgents", () => ({
useAgents: vi.fn(),
}));
describe("NewUsage", () => {
const mockUserDailyActivityAggregatedCall = vi.mocked(networking.userDailyActivityAggregatedCall);
const mockTagListCall = vi.mocked(networking.tagListCall);
const mockUseCustomers = vi.mocked(useCustomers);
const mockUseAgents = vi.mocked(useAgents);
const mockSpendData = {
results: [
@ -193,6 +199,13 @@ describe("NewUsage", () => {
},
];
const mockAgents = [
{
agent_id: "agent-123",
agent_name: "Test Agent",
},
];
const defaultProps = {
accessToken: "test-token",
userRole: "Admin",
@ -229,6 +242,11 @@ describe("NewUsage", () => {
isLoading: false,
error: null,
} as any);
mockUseAgents.mockReturnValue({
data: { agents: [] },
isLoading: false,
error: null,
} as any);
});
it("should render and fetch usage data on mount", async () => {
@ -278,7 +296,9 @@ describe("NewUsage", () => {
// Switch to Team Usage tab
const teamUsageTab = screen.getByText("Team Usage");
fireEvent.click(teamUsageTab);
act(() => {
fireEvent.click(teamUsageTab);
});
// Should render EntityUsage component
await waitFor(() => {
@ -288,7 +308,9 @@ describe("NewUsage", () => {
// Switch to Tag Usage tab (admin only)
const tagUsageTab = screen.getByText("Tag Usage");
fireEvent.click(tagUsageTab);
act(() => {
fireEvent.click(tagUsageTab);
});
// Should still render EntityUsage component for tags
await waitFor(() => {
@ -298,18 +320,20 @@ describe("NewUsage", () => {
});
it("should show organization usage banner and tab for admins", async () => {
const { getByText, getAllByText } = render(<NewUsagePage {...defaultProps} organizations={mockOrganizations} />);
render(<NewUsagePage {...defaultProps} organizations={mockOrganizations} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
const organizationTab = getByText("Organization Usage");
fireEvent.click(organizationTab);
const organizationTab = screen.getByText("Organization Usage");
act(() => {
fireEvent.click(organizationTab);
});
await waitFor(() => {
expect(getByText("Organization usage is a new feature.")).toBeInTheDocument();
const entityUsageElements = getAllByText("Entity Usage");
expect(screen.getByText("Organization usage is a new feature.")).toBeInTheDocument();
const entityUsageElements = screen.getAllByText("Entity Usage");
expect(entityUsageElements.length).toBeGreaterThan(0);
});
});
@ -321,17 +345,43 @@ describe("NewUsage", () => {
error: null,
} as any);
const { getByText, getAllByText } = render(<NewUsagePage {...defaultProps} />);
render(<NewUsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
const customerTab = getByText("Customer Usage");
fireEvent.click(customerTab);
const customerTab = screen.getByText("Customer Usage");
act(() => {
fireEvent.click(customerTab);
});
await waitFor(() => {
const entityUsageElements = getAllByText("Entity Usage");
const entityUsageElements = screen.getAllByText("Entity Usage");
expect(entityUsageElements.length).toBeGreaterThan(0);
});
});
it("should show agent usage tab for admins", async () => {
mockUseAgents.mockReturnValue({
data: { agents: mockAgents },
isLoading: false,
error: null,
} as any);
render(<NewUsagePage {...defaultProps} />);
await waitFor(() => {
expect(mockUserDailyActivityAggregatedCall).toHaveBeenCalled();
});
const agentTab = screen.getByText("Agent Usage");
act(() => {
fireEvent.click(agentTab);
});
await waitFor(() => {
const entityUsageElements = screen.getAllByText("Entity Usage");
expect(entityUsageElements.length).toBeGreaterThan(0);
});
});

View file

@ -49,6 +49,7 @@ import { DailyData, KeyMetricWithMetadata, MetricWithMetadata } from "./usage/ty
import { valueFormatterSpend } from "./usage/utils/value_formatters";
import UserAgentActivity from "./user_agent_activity";
import ViewUserSpend from "./view_user_spend";
import { useAgents } from "@/app/(dashboard)/hooks/agents/useAgents";
interface NewUsagePageProps {
accessToken: string | null;
@ -88,6 +89,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
const [allTags, setAllTags] = useState<EntityList[]>([]);
const { data: customers = [] } = useCustomers(accessToken, userRole);
const { data: agentsResponse } = useAgents(accessToken, userRole);
const [modelViewType, setModelViewType] = useState<"groups" | "individual">("groups");
const [isCloudZeroModalOpen, setIsCloudZeroModalOpen] = useState(false);
const [isGlobalExportModalOpen, setIsGlobalExportModalOpen] = useState(false);
@ -435,6 +437,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
<Tab>Team Usage</Tab>
{all_admin_roles.includes(userRole || "") ? <Tab>Customer Usage</Tab> : <></>}
{all_admin_roles.includes(userRole || "") ? <Tab>Tag Usage</Tab> : <></>}
{all_admin_roles.includes(userRole || "") ? <Tab>Agent Usage</Tab> : <></>}
{all_admin_roles.includes(userRole || "") ? <Tab>User Agent Activity</Tab> : <></>}
</TabList>
<AdvancedDatePicker value={dateValue} onValueChange={handleDateChange} />
@ -842,6 +845,19 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
dateValue={dateValue}
/>
</TabPanel>
<TabPanel>
<EntityUsage
accessToken={accessToken}
entityType="agent"
userID={userID}
userRole={userRole}
entityList={
agentsResponse?.agents?.map((agent) => ({ label: agent.agent_name, value: agent.agent_id })) || null
}
premiumUser={premiumUser}
dateValue={dateValue}
/>
</TabPanel>
{/* User Agent Activity Panel */}
<TabPanel>
<UserAgentActivity accessToken={accessToken} userRole={userRole} dateValue={dateValue} />