Add User Usage UI components and integration

Implement complete UI for admin user usage analytics:
- UserUsage: Main component with state management and data fetching
- UserUsageBarChart: Horizontal bar chart for top users (10/25/50)
- UserUsageSummary: Five summary metric cards (total, spend, avg, power users, low users)
- UserUsageFilters: Tag filters, min/max spend, sort controls
- UserUsageTable: Paginated table with sortable columns
- types.ts: TypeScript interfaces for all components

Integration:
- Added "User Usage" option to UsageViewSelect (admin-only)
- Integrated UserUsage component into UsagePageView
- Connects to /admin/users/daily/activity endpoint via adminUsersDailyActivityCall()

Features:
- Server-side pagination for 1K+ users
- Tag-based filtering (User-Agent:claude-code, etc.)
- Min/max spend thresholds
- Sort by spend/requests/tokens
- Success rate badges with color coding
- Dynamic top N users selector
- Responsive layout with Tremor and Ant Design
This commit is contained in:
Claude 2026-01-21 20:20:14 +00:00
parent 2659061235
commit b89ad6d296
No known key found for this signature in database
8 changed files with 929 additions and 1 deletions

View file

@ -47,6 +47,7 @@ import AdvancedDatePicker from "../../shared/advanced_date_picker";
import { ChartLoader } from "../../shared/chart_loader";
import { Tag } from "../../tag_management/types";
import UserAgentActivity from "../../user_agent_activity";
import UserUsage from "./UserUsage/UserUsage";
import ViewUserSpend from "../../view_user_spend";
import { DailyData, KeyMetricWithMetadata, MetricWithMetadata } from "../types";
import { valueFormatterSpend } from "../utils/value_formatters";
@ -886,6 +887,8 @@ const UsagePage: React.FC<UsagePageProps> = ({ teams, organizations }) => {
{usageView === "user-agent-activity" && (
<UserAgentActivity accessToken={accessToken} userRole={userRole} dateValue={dateValue} />
)}
{/* User Usage Panel */}
{usageView === "user-usage" && <UserUsage accessToken={accessToken} dateValue={dateValue} />}
</div>
</div>

View file

@ -7,10 +7,11 @@ import {
ShoppingCartOutlined,
TagsOutlined,
TeamOutlined,
UserOutlined,
} from "@ant-design/icons";
import { Badge, Select } from "antd";
import React from "react";
export type UsageOption = "global" | "organization" | "team" | "customer" | "tag" | "agent" | "user-agent-activity";
export type UsageOption = "global" | "organization" | "team" | "customer" | "tag" | "agent" | "user-agent-activity" | "user-usage";
export interface UsageViewSelectProps {
value: UsageOption;
onChange: (value: UsageOption) => void;
@ -86,6 +87,13 @@ const OPTIONS: OptionConfig[] = [
icon: <LineChartOutlined style={{ fontSize: "16px" }} />,
adminOnly: true,
},
{
value: "user-usage",
label: "User Usage",
description: "View usage metrics per user with filtering",
icon: <UserOutlined style={{ fontSize: "16px" }} />,
adminOnly: true,
},
];
export const UsageViewSelect: React.FC<UsageViewSelectProps> = ({
value,

View file

@ -0,0 +1,137 @@
/**
* User Usage Component
*
* Admin view for user usage analytics with filtering, sorting, and pagination.
* Shows top users bar chart, summary cards, and paginated user table.
*/
import { DateRangePickerValue } from "@tremor/react";
import React, { useCallback, useEffect, useState } from "react";
import { adminUsersDailyActivityCall } from "../../../networking";
import { UserUsageBarChart } from "./UserUsageBarChart";
import { UserUsageFilters } from "./UserUsageFilters";
import { UserUsageSummary } from "./UserUsageSummary";
import { UserUsageTable } from "./UserUsageTable";
import {
UserUsageFiltersState,
UserUsageResponse,
} from "./types";
interface UserUsageProps {
accessToken: string | null;
dateValue: DateRangePickerValue;
}
const UserUsage: React.FC<UserUsageProps> = ({ accessToken, dateValue }) => {
const [data, setData] = useState<UserUsageResponse | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [topN, setTopN] = useState(10);
const [filters, setFilters] = useState<UserUsageFiltersState>({
tagFilters: [],
minSpend: null,
maxSpend: null,
sortBy: "spend",
sortOrder: "desc",
});
const fetchData = useCallback(async () => {
if (!accessToken || !dateValue.from || !dateValue.to) return;
setLoading(true);
try {
const response = await adminUsersDailyActivityCall(
accessToken,
dateValue.from,
dateValue.to,
page,
filters.tagFilters.length > 0 ? filters.tagFilters : null,
filters.minSpend,
filters.maxSpend,
filters.sortBy,
filters.sortOrder,
topN
);
setData(response);
} catch (error) {
console.error("Error fetching user usage data:", error);
} finally {
setLoading(false);
}
}, [
accessToken,
dateValue.from,
dateValue.to,
page,
pageSize,
topN,
filters,
]);
useEffect(() => {
fetchData();
}, [fetchData]);
const handleFiltersChange = (newFilters: Partial<UserUsageFiltersState>) => {
setFilters((prev) => ({ ...prev, ...newFilters }));
setPage(1); // Reset to first page when filters change
};
const handlePageChange = (newPage: number) => {
setPage(newPage);
};
const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize);
setPage(1); // Reset to first page when page size changes
};
const handleTopNChange = (newTopN: number) => {
setTopN(newTopN);
};
return (
<div className="space-y-6">
{/* Filters */}
<UserUsageFilters
filters={filters}
onFiltersChange={handleFiltersChange}
loading={loading}
/>
{/* Top Users Bar Chart */}
<UserUsageBarChart
topUsers={data?.top_users || []}
loading={loading}
topN={topN}
onTopNChange={handleTopNChange}
sortBy={filters.sortBy}
/>
{/* Summary Cards */}
<UserUsageSummary summary={data?.summary || null} loading={loading} />
{/* Paginated User Table */}
<UserUsageTable
users={data?.users || []}
pagination={data?.pagination || null}
loading={loading}
page={page}
pageSize={pageSize}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
sortBy={filters.sortBy}
sortOrder={filters.sortOrder}
onSortChange={(sortBy, sortOrder) =>
handleFiltersChange({ sortBy, sortOrder })
}
/>
</div>
);
};
export default UserUsage;

View file

@ -0,0 +1,118 @@
/**
* User Usage Bar Chart Component
*
* Displays top N users in a horizontal bar chart
*/
import { BarChart, Card, Title } from "@tremor/react";
import { Segmented } from "antd";
import React from "react";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { ChartLoader } from "../../../shared/chart_loader";
import { UserMetrics } from "./types";
interface UserUsageBarChartProps {
topUsers: UserMetrics[];
loading: boolean;
topN: number;
onTopNChange: (topN: number) => void;
sortBy: "spend" | "requests" | "tokens";
}
const valueFormatters = {
spend: (value: number) => `$${formatNumberWithCommas(value, 2)}`,
requests: (value: number) => formatNumberWithCommas(value, 0),
tokens: (value: number) => formatNumberWithCommas(value, 0),
};
export const UserUsageBarChart: React.FC<UserUsageBarChartProps> = ({
topUsers,
loading,
topN,
onTopNChange,
sortBy,
}) => {
const chartData = topUsers.map((user) => ({
name: user.user_email || user.user_id,
value: user[sortBy],
}));
const valueFormatter = valueFormatters[sortBy];
const getTitle = () => {
const sortLabel = sortBy === "spend" ? "Spend" : sortBy === "requests" ? "Requests" : "Tokens";
return `Top ${topN} Users by ${sortLabel}`;
};
return (
<Card>
<div className="flex justify-between items-center mb-4">
<Title>{getTitle()}</Title>
<Segmented
options={[
{ label: "10", value: 10 },
{ label: "25", value: 25 },
{ label: "50", value: 50 },
]}
value={topN}
onChange={(value) => onTopNChange(value as number)}
/>
</div>
{loading ? (
<ChartLoader isDateChanging={loading} />
) : chartData.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No user data available for the selected date range and filters.
</div>
) : (
<BarChart
data={chartData}
index="name"
categories={["value"]}
colors={["cyan"]}
valueFormatter={valueFormatter}
layout="vertical"
yAxisWidth={250}
showLegend={false}
style={{ height: Math.min(chartData.length * 52, 600) }}
customTooltip={({ payload, active }) => {
if (!active || !payload?.[0]) return null;
const data = payload[0].payload;
const user = topUsers.find(
(u) => (u.user_email || u.user_id) === data.name
);
if (!user) return null;
return (
<div className="bg-white p-4 shadow-lg rounded-lg border">
<p className="font-bold">{user.user_email || user.user_id}</p>
<p className="text-cyan-500">
Spend: ${formatNumberWithCommas(user.spend, 2)}
</p>
<p className="text-gray-600">
Requests: {formatNumberWithCommas(user.requests, 0)}
</p>
<p className="text-green-600">
Successful: {formatNumberWithCommas(user.successful_requests, 0)}
</p>
<p className="text-red-600">
Failed: {formatNumberWithCommas(user.failed_requests, 0)}
</p>
<p className="text-gray-600">
Tokens: {formatNumberWithCommas(user.tokens, 0)}
</p>
<p className="text-gray-600">Days Active: {user.days_active}</p>
{user.tags && user.tags.length > 0 && (
<p className="text-gray-600 text-sm mt-2">
Tags: {user.tags.join(", ")}
</p>
)}
</div>
);
}}
/>
)}
</Card>
);
};

View file

@ -0,0 +1,187 @@
/**
* User Usage Filters Component
*
* Filter controls for tag selection, spend thresholds, and sorting
*/
import { Button, Card } from "@tremor/react";
import { Input, Select } from "antd";
import React, { useState } from "react";
import { UserUsageFiltersState } from "./types";
interface UserUsageFiltersProps {
filters: UserUsageFiltersState;
onFiltersChange: (filters: Partial<UserUsageFiltersState>) => void;
loading: boolean;
}
const COMMON_TAGS = [
"User-Agent:claude-code",
"User-Agent:claude-code-max",
"User-Agent:cursor",
"User-Agent:windsurf",
];
export const UserUsageFilters: React.FC<UserUsageFiltersProps> = ({
filters,
onFiltersChange,
loading,
}) => {
const [localMinSpend, setLocalMinSpend] = useState<string>(
filters.minSpend?.toString() || ""
);
const [localMaxSpend, setLocalMaxSpend] = useState<string>(
filters.maxSpend?.toString() || ""
);
const handleApplySpendFilters = () => {
onFiltersChange({
minSpend: localMinSpend ? parseFloat(localMinSpend) : null,
maxSpend: localMaxSpend ? parseFloat(localMaxSpend) : null,
});
};
const handleResetFilters = () => {
setLocalMinSpend("");
setLocalMaxSpend("");
onFiltersChange({
tagFilters: [],
minSpend: null,
maxSpend: null,
sortBy: "spend",
sortOrder: "desc",
});
};
return (
<Card>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Filters</h3>
<Button
size="xs"
variant="secondary"
onClick={handleResetFilters}
disabled={loading}
>
Reset All
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Tag Filters */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
🏷️ User Agent Tags
</label>
<Select
mode="multiple"
style={{ width: "100%" }}
placeholder="Select tags..."
value={filters.tagFilters}
onChange={(value) => onFiltersChange({ tagFilters: value })}
disabled={loading}
options={COMMON_TAGS.map((tag) => ({
label: tag,
value: tag,
}))}
allowClear
/>
</div>
{/* Min Spend */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
💰 Min Spend ($)
</label>
<Input
type="number"
placeholder="e.g., 200"
value={localMinSpend}
onChange={(e) => setLocalMinSpend(e.target.value)}
onPressEnter={handleApplySpendFilters}
disabled={loading}
min={0}
step={10}
/>
</div>
{/* Max Spend */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
💰 Max Spend ($)
</label>
<Input
type="number"
placeholder="e.g., 1000"
value={localMaxSpend}
onChange={(e) => setLocalMaxSpend(e.target.value)}
onPressEnter={handleApplySpendFilters}
disabled={loading}
min={0}
step={10}
/>
</div>
{/* Sort By */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
📊 Sort By
</label>
<div className="flex gap-2">
<Select
style={{ flex: 1 }}
value={filters.sortBy}
onChange={(value) => onFiltersChange({ sortBy: value })}
disabled={loading}
options={[
{ label: "Spend", value: "spend" },
{ label: "Requests", value: "requests" },
{ label: "Tokens", value: "tokens" },
]}
/>
<Select
style={{ width: 100 }}
value={filters.sortOrder}
onChange={(value) => onFiltersChange({ sortOrder: value })}
disabled={loading}
options={[
{ label: "↓ Desc", value: "desc" },
{ label: "↑ Asc", value: "asc" },
]}
/>
</div>
</div>
</div>
{/* Apply Button for Spend Filters */}
{(localMinSpend !== (filters.minSpend?.toString() || "") ||
localMaxSpend !== (filters.maxSpend?.toString() || "")) && (
<div className="flex justify-end">
<Button onClick={handleApplySpendFilters} disabled={loading}>
Apply Spend Filters
</Button>
</div>
)}
{/* Active Filters Summary */}
{(filters.tagFilters.length > 0 ||
filters.minSpend !== null ||
filters.maxSpend !== null) && (
<div className="text-sm text-gray-600">
<strong>Active filters:</strong>{" "}
{filters.tagFilters.length > 0 && (
<span>Tags: {filters.tagFilters.length} selected; </span>
)}
{filters.minSpend !== null && (
<span>Min spend: ${filters.minSpend}; </span>
)}
{filters.maxSpend !== null && (
<span>Max spend: ${filters.maxSpend}; </span>
)}
</div>
)}
</div>
</Card>
);
};

View file

@ -0,0 +1,89 @@
/**
* User Usage Summary Component
*
* Displays summary cards with total users, spend, requests, etc.
*/
import { Card, Col, Grid, Text, Title } from "@tremor/react";
import React from "react";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { UserUsageSummary as UserUsageSummaryType } from "./types";
interface UserUsageSummaryProps {
summary: UserUsageSummaryType | null;
loading: boolean;
}
export const UserUsageSummary: React.FC<UserUsageSummaryProps> = ({
summary,
loading,
}) => {
if (loading || !summary) {
return (
<Grid numItems={5} className="gap-4">
{[...Array(5)].map((_, i) => (
<Card key={i}>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-24 mb-2"></div>
<div className="h-8 bg-gray-200 rounded w-16"></div>
</div>
</Card>
))}
</Grid>
);
}
return (
<Grid numItems={5} className="gap-4">
{/* Total Users */}
<Card>
<Title>Total Users</Title>
<Text className="text-2xl font-bold mt-2">
{formatNumberWithCommas(summary.total_users, 0)}
</Text>
</Card>
{/* Total Spend */}
<Card>
<Title>Total Spend</Title>
<Text className="text-2xl font-bold mt-2 text-cyan-600">
${formatNumberWithCommas(summary.total_spend, 2)}
</Text>
</Card>
{/* Average per User */}
<Card>
<Title>Avg per User</Title>
<Text className="text-2xl font-bold mt-2">
${formatNumberWithCommas(summary.avg_spend_per_user, 2)}
</Text>
</Card>
{/* Power Users */}
<Card>
<Title>Power Users (&gt;$200)</Title>
<Text className="text-2xl font-bold mt-2 text-green-600">
{formatNumberWithCommas(summary.power_users_count, 0)}
</Text>
<Text className="text-sm text-gray-500 mt-1">
{summary.total_users > 0
? `${((summary.power_users_count / summary.total_users) * 100).toFixed(1)}% of total`
: "0%"}
</Text>
</Card>
{/* Low Users */}
<Card>
<Title>Low Users (&lt;$10)</Title>
<Text className="text-2xl font-bold mt-2 text-orange-600">
{formatNumberWithCommas(summary.low_users_count, 0)}
</Text>
<Text className="text-sm text-gray-500 mt-1">
{summary.total_users > 0
? `${((summary.low_users_count / summary.total_users) * 100).toFixed(1)}% of total`
: "0%"}
</Text>
</Card>
</Grid>
);
};

View file

@ -0,0 +1,332 @@
/**
* User Usage Table Component
*
* Displays paginated user list with sorting and pagination controls
*/
import {
Badge,
Card,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
Title,
} from "@tremor/react";
import { Select } from "antd";
import React from "react";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { UserMetrics, UserUsagePagination } from "./types";
interface UserUsageTableProps {
users: UserMetrics[];
pagination: UserUsagePagination | null;
loading: boolean;
page: number;
pageSize: number;
onPageChange: (page: number) => void;
onPageSizeChange: (pageSize: number) => void;
sortBy: "spend" | "requests" | "tokens";
sortOrder: "asc" | "desc";
onSortChange: (
sortBy: "spend" | "requests" | "tokens",
sortOrder: "asc" | "desc"
) => void;
}
export const UserUsageTable: React.FC<UserUsageTableProps> = ({
users,
pagination,
loading,
page,
pageSize,
onPageChange,
onPageSizeChange,
sortBy,
sortOrder,
onSortChange,
}) => {
const handleSortClick = (column: "spend" | "requests" | "tokens") => {
if (sortBy === column) {
// Toggle sort order
onSortChange(column, sortOrder === "asc" ? "desc" : "asc");
} else {
// Change sort column (default to desc)
onSortChange(column, "desc");
}
};
const getSortIcon = (column: "spend" | "requests" | "tokens") => {
if (sortBy !== column) return "⇅";
return sortOrder === "asc" ? "↑" : "↓";
};
const renderPagination = () => {
if (!pagination) return null;
const { page: currentPage, total_pages, total_count } = pagination;
const startItem = (currentPage - 1) * pageSize + 1;
const endItem = Math.min(currentPage * pageSize, total_count);
const pages = [];
const maxPagesToShow = 7;
const halfRange = Math.floor(maxPagesToShow / 2);
let startPage = Math.max(1, currentPage - halfRange);
let endPage = Math.min(total_pages, currentPage + halfRange);
// Adjust if we're near the start or end
if (currentPage <= halfRange) {
endPage = Math.min(total_pages, maxPagesToShow);
} else if (currentPage + halfRange >= total_pages) {
startPage = Math.max(1, total_pages - maxPagesToShow + 1);
}
// Add first page if not visible
if (startPage > 1) {
pages.push(
<button
key={1}
onClick={() => onPageChange(1)}
className="px-3 py-1 border rounded hover:bg-gray-50"
>
1
</button>
);
if (startPage > 2) {
pages.push(
<span key="ellipsis1" className="px-2">
...
</span>
);
}
}
// Add page numbers
for (let i = startPage; i <= endPage; i++) {
pages.push(
<button
key={i}
onClick={() => onPageChange(i)}
className={`px-3 py-1 border rounded ${
i === currentPage
? "bg-cyan-500 text-white"
: "hover:bg-gray-50"
}`}
>
{i}
</button>
);
}
// Add last page if not visible
if (endPage < total_pages) {
if (endPage < total_pages - 1) {
pages.push(
<span key="ellipsis2" className="px-2">
...
</span>
);
}
pages.push(
<button
key={total_pages}
onClick={() => onPageChange(total_pages)}
className="px-3 py-1 border rounded hover:bg-gray-50"
>
{total_pages}
</button>
);
}
return (
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-gray-600">
Showing {startItem}-{endItem} of {total_count} users
</div>
<div className="flex items-center gap-4">
{/* Page Size Selector */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Items per page:</span>
<Select
value={pageSize}
onChange={onPageSizeChange}
style={{ width: 80 }}
options={[
{ label: "25", value: 25 },
{ label: "50", value: 50 },
{ label: "100", value: 100 },
]}
/>
</div>
{/* Pagination Buttons */}
<div className="flex items-center gap-2">
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
← Previous
</button>
<div className="flex gap-1">{pages}</div>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === total_pages}
className="px-3 py-1 border rounded hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next →
</button>
</div>
</div>
</div>
);
};
return (
<Card>
<div className="flex justify-between items-center mb-4">
<Title>👥 All Users</Title>
{pagination && (
<span className="text-sm text-gray-600">
{pagination.total_count} total users
</span>
)}
</div>
{loading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-cyan-500 mx-auto"></div>
<p className="text-gray-500 mt-4">Loading users...</p>
</div>
) : users.length === 0 ? (
<div className="text-center py-12 text-gray-500">
No users found for the selected filters.
</div>
) : (
<>
<div className="overflow-x-auto">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>User Email</TableHeaderCell>
<TableHeaderCell>
<button
onClick={() => handleSortClick("spend")}
className="flex items-center gap-1 hover:text-cyan-600"
>
Spend {getSortIcon("spend")}
</button>
</TableHeaderCell>
<TableHeaderCell>
<button
onClick={() => handleSortClick("requests")}
className="flex items-center gap-1 hover:text-cyan-600"
>
Requests {getSortIcon("requests")}
</button>
</TableHeaderCell>
<TableHeaderCell>
<button
onClick={() => handleSortClick("tokens")}
className="flex items-center gap-1 hover:text-cyan-600"
>
Tokens {getSortIcon("tokens")}
</button>
</TableHeaderCell>
<TableHeaderCell>Success Rate</TableHeaderCell>
<TableHeaderCell>Days Active</TableHeaderCell>
<TableHeaderCell>Tags</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => {
const successRate =
user.requests > 0
? (user.successful_requests / user.requests) * 100
: 0;
return (
<TableRow key={user.user_id}>
<TableCell>
<div>
<div className="font-medium">
{user.user_email || user.user_id}
</div>
{user.user_email && (
<div className="text-xs text-gray-500">
{user.user_id}
</div>
)}
</div>
</TableCell>
<TableCell>
<span className="font-medium text-cyan-600">
${formatNumberWithCommas(user.spend, 2)}
</span>
</TableCell>
<TableCell>
{formatNumberWithCommas(user.requests, 0)}
<div className="text-xs text-gray-500">
<span className="text-green-600">
{formatNumberWithCommas(user.successful_requests, 0)}
</span>
{" / "}
<span className="text-red-600">
{formatNumberWithCommas(user.failed_requests, 0)}
</span>
</div>
</TableCell>
<TableCell>
{formatNumberWithCommas(user.tokens, 0)}
</TableCell>
<TableCell>
<Badge
color={
successRate >= 99
? "green"
: successRate >= 95
? "yellow"
: "red"
}
>
{successRate.toFixed(1)}%
</Badge>
</TableCell>
<TableCell>{user.days_active}</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{user.tags && user.tags.length > 0 ? (
user.tags.slice(0, 2).map((tag, idx) => (
<Badge key={idx} size="xs">
{tag.replace("User-Agent:", "")}
</Badge>
))
) : (
<span className="text-gray-400 text-sm">-</span>
)}
{user.tags && user.tags.length > 2 && (
<Badge size="xs" color="gray">
+{user.tags.length - 2}
</Badge>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
{renderPagination()}
</>
)}
</Card>
);
};

View file

@ -0,0 +1,54 @@
/**
* TypeScript types for Admin User Usage components
*/
export interface UserUsageSummary {
total_users: number;
total_spend: number;
total_requests: number;
total_successful_requests: number;
total_failed_requests: number;
total_tokens: number;
avg_spend_per_user: number;
power_users_count: number;
low_users_count: number;
}
export interface UserMetrics {
user_id: string;
user_email: string;
spend: number;
requests: number;
successful_requests: number;
failed_requests: number;
prompt_tokens: number;
completion_tokens: number;
tokens: number;
days_active: number;
first_request_date: string;
last_request_date: string;
tags: string[];
models_used: string[];
}
export interface UserUsagePagination {
page: number;
page_size: number;
total_count: number;
total_pages: number;
}
export interface UserUsageResponse {
summary: UserUsageSummary;
top_users: UserMetrics[];
users: UserMetrics[];
pagination: UserUsagePagination;
}
export interface UserUsageFiltersState {
tagFilters: string[];
minSpend: number | null;
maxSpend: number | null;
sortBy: "spend" | "requests" | "tokens";
sortOrder: "asc" | "desc";
}