translate keys and restructure roles and users

This commit is contained in:
Owen
2026-08-12 10:37:19 -04:00
parent 49020fa6ea
commit 8a04f13dd4
11 changed files with 241 additions and 150 deletions
+56
View File
@@ -1936,6 +1936,62 @@
"aiResourceAliasRequired": "Alias is required for inference resources", "aiResourceAliasRequired": "Alias is required for inference resources",
"aiResourceDomainConfiguration": "Domain configuration", "aiResourceDomainConfiguration": "Domain configuration",
"aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this inference resource.", "aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this inference resource.",
"aiUsageAnalyticsTitle": "AI Usage Analytics",
"aiUsageAnalyticsDescription": "Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users",
"aiUsageTabOverview": "Overview",
"aiUsageTabProviders": "Providers",
"aiUsageTabResources": "Resources",
"aiUsageTabUsersRoles": "Users & Roles",
"aiUsageFilterProvider": "Provider",
"aiUsageFilterModel": "Model",
"aiUsageFilterResource": "Resource",
"aiUsageFilterRole": "Role",
"aiUsageFilterUser": "User",
"aiUsageFilterAllProviders": "All Providers",
"aiUsageFilterAllModels": "All Models",
"aiUsageFilterAllResources": "All Resources",
"aiUsageFilterAllRoles": "All Roles",
"aiUsageFilterAllUsers": "All Users",
"aiUsageResetFilters": "Reset Filters",
"aiUsageRefresh": "Refresh",
"aiUsageTokenTypePrompt": "Prompt",
"aiUsageTokenTypeCacheRead": "Cache read",
"aiUsageTokenTypeCacheWrite": "Cache write",
"aiUsageTokenTypeCompletion": "Completion",
"aiUsageTokenTypeReasoning": "Reasoning",
"aiUsageRequests": "Requests",
"aiUsageCost": "Cost",
"aiUsageTokens": "Tokens",
"aiUsageOther": "Other",
"aiUsageTotalRequests": "Total Requests",
"aiUsageTotalTokens": "Total Tokens",
"aiUsageTotalCost": "Total Cost",
"aiUsageEstimated": "Estimated",
"aiUsageRequestVolume": "Request Volume",
"aiUsageTokenUsage": "Token Usage",
"aiUsageModelCost": "Model Cost",
"aiUsageModelTokens": "Model Tokens",
"aiUsageTopModels": "Top Models",
"aiUsageTopProviders": "Top Providers",
"aiUsageProviderCost": "Provider Cost",
"aiUsageProviderTokenUsage": "Provider Token Usage",
"aiUsageTopResources": "Top Resources",
"aiUsageResourceCost": "Resource Cost",
"aiUsageResourceTokenUsage": "Resource Token Usage",
"aiUsageResourceTypePublic": "Resource",
"aiUsageResourceTypeSite": "Site resource",
"aiUsageNoResource": "No resource",
"aiUsageRolesTab": "Roles",
"aiUsageUsersTab": "Users",
"aiUsageTopRoles": "Top Roles",
"aiUsageRoleCost": "Role Cost",
"aiUsageRoleTokenUsage": "Role Token Usage",
"aiUsageTopUsers": "Top Users",
"aiUsageUserCost": "User Cost",
"aiUsageUserTokenUsage": "User Token Usage",
"aiUsageUnknownUser": "Unknown user",
"aiUsageLoading": "Loading...",
"aiUsageNoData": "No data",
"resourceBudgetSettings": "Budget", "resourceBudgetSettings": "Budget",
"resourceBudgetSettingsDescription": "Configure how this inference resource restricts usage based on spending or token limits", "resourceBudgetSettingsDescription": "Configure how this inference resource restricts usage based on spending or token limits",
"sidebarApiKeys": "API Keys", "sidebarApiKeys": "API Keys",
@@ -1,10 +1,14 @@
import { AiUsageAnalyticsData } from "@app/components/AiUsageAnalyticsData"; import { AiUsageAnalyticsData } from "@app/components/AiUsageAnalyticsData";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
export const metadata: Metadata = { export async function generateMetadata(): Promise<Metadata> {
title: "AI Usage Analytics" const t = await getTranslations();
}; return {
title: t("aiUsageAnalyticsTitle")
};
}
export interface AiUsageAnalyticsPageProps { export interface AiUsageAnalyticsPageProps {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -14,12 +18,13 @@ export default async function AiUsageAnalyticsPage(
props: AiUsageAnalyticsPageProps props: AiUsageAnalyticsPageProps
) { ) {
const orgId = (await props.params).orgId; const orgId = (await props.params).orgId;
const t = await getTranslations();
return ( return (
<> <>
<SettingsSectionTitle <SettingsSectionTitle
title="AI Usage Analytics" title={t("aiUsageAnalyticsTitle")}
description="Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users" description={t("aiUsageAnalyticsDescription")}
/> />
<div className="container mx-auto max-w-12xl"> <div className="container mx-auto max-w-12xl">
+18 -16
View File
@@ -8,6 +8,7 @@ import {
} from "@app/lib/queries"; } from "@app/lib/queries";
import { useIsFetching, useQuery, useQueryClient } from "@tanstack/react-query"; import { useIsFetching, useQuery, useQueryClient } from "@tanstack/react-query";
import { RefreshCw, XIcon } from "lucide-react"; import { RefreshCw, XIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { DateRangePicker, type DateTimeValue } from "./DateTimePicker"; import { DateRangePicker, type DateTimeValue } from "./DateTimePicker";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
@@ -35,6 +36,7 @@ export type AiUsageAnalyticsDataProps = {
const AI_USAGE_ANALYTICS_QUERY_PREFIX = ["AI_USAGE_ANALYTICS"]; const AI_USAGE_ANALYTICS_QUERY_PREFIX = ["AI_USAGE_ANALYTICS"];
export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) { export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
const t = useTranslations();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const path = usePathname(); const path = usePathname();
const router = useRouter(); const router = useRouter();
@@ -139,10 +141,10 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
})); }));
const tabs: TabItem[] = [ const tabs: TabItem[] = [
{ title: "Overview", href: "#" }, { title: t("aiUsageTabOverview"), href: "#" },
{ title: "Provider Usage", href: "#" }, { title: t("aiUsageTabProviders"), href: "#" },
{ title: "Resources", href: "#" }, { title: t("aiUsageTabResources"), href: "#" },
{ title: "Users & Roles", href: "#" } { title: t("aiUsageTabUsersRoles"), href: "#" }
]; ];
return ( return (
@@ -172,42 +174,42 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
<div className="flex flex-wrap items-end gap-2"> <div className="flex flex-wrap items-end gap-2">
<FilterSelect <FilterSelect
id="providerId" id="providerId"
label="Provider" label={t("aiUsageFilterProvider")}
value={filters.providerId?.toString()} value={filters.providerId?.toString()}
options={providerOptions} options={providerOptions}
placeholder="All Providers" placeholder={t("aiUsageFilterAllProviders")}
onValueChange={(v) => setFilter("providerId", v)} onValueChange={(v) => setFilter("providerId", v)}
/> />
<FilterSelect <FilterSelect
id="model" id="model"
label="Model" label={t("aiUsageFilterModel")}
value={filters.model} value={filters.model}
options={modelOptions} options={modelOptions}
placeholder="All Models" placeholder={t("aiUsageFilterAllModels")}
onValueChange={(v) => setFilter("model", v)} onValueChange={(v) => setFilter("model", v)}
/> />
<FilterSelect <FilterSelect
id="resourceId" id="resourceId"
label="Resource" label={t("aiUsageFilterResource")}
value={filters.resourceId?.toString()} value={filters.resourceId?.toString()}
options={resourceOptions} options={resourceOptions}
placeholder="All Resources" placeholder={t("aiUsageFilterAllResources")}
onValueChange={(v) => setFilter("resourceId", v)} onValueChange={(v) => setFilter("resourceId", v)}
/> />
<FilterSelect <FilterSelect
id="roleId" id="roleId"
label="Role" label={t("aiUsageFilterRole")}
value={filters.roleId?.toString()} value={filters.roleId?.toString()}
options={roleOptions} options={roleOptions}
placeholder="All Roles" placeholder={t("aiUsageFilterAllRoles")}
onValueChange={(v) => setFilter("roleId", v)} onValueChange={(v) => setFilter("roleId", v)}
/> />
<FilterSelect <FilterSelect
id="userId" id="userId"
label="User" label={t("aiUsageFilterUser")}
value={filters.userId} value={filters.userId}
options={userOptions} options={userOptions}
placeholder="All Users" placeholder={t("aiUsageFilterAllUsers")}
onValueChange={(v) => setFilter("userId", v)} onValueChange={(v) => setFilter("userId", v)}
/> />
@@ -218,7 +220,7 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
className="gap-2" className="gap-2"
> >
<XIcon className="size-4" /> <XIcon className="size-4" />
Reset Filters {t("aiUsageResetFilters")}
</Button> </Button>
)} )}
</div> </div>
@@ -243,7 +245,7 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
isFetching && "animate-spin" isFetching && "animate-spin"
)} )}
/> />
Refresh {t("aiUsageRefresh")}
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
@@ -3,6 +3,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import { aiUsageAnalyticsQueries } from "@app/lib/queries";
import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { Card, CardContent, CardHeader } from "@app/components/ui/card";
import { import {
InfoSection, InfoSection,
@@ -24,15 +25,8 @@ type OverviewTabProps = {
filters: AiUsageAnalyticsFilters; filters: AiUsageAnalyticsFilters;
}; };
const TOKEN_TYPE_LABELS: Record<string, string> = {
promptTokens: "Prompt",
cacheReadTokens: "Cache read",
cacheWriteTokens: "Cache write",
completionTokens: "Completion",
reasoningTokens: "Reasoning"
};
export function OverviewTab(props: OverviewTabProps) { export function OverviewTab(props: OverviewTabProps) {
const t = useTranslations();
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery(
aiUsageAnalyticsQueries.overview({ aiUsageAnalyticsQueries.overview({
orgId: props.orgId, orgId: props.orgId,
@@ -40,8 +34,16 @@ export function OverviewTab(props: OverviewTabProps) {
}) })
); );
const TOKEN_TYPE_LABELS: Record<string, string> = {
promptTokens: t("aiUsageTokenTypePrompt"),
cacheReadTokens: t("aiUsageTokenTypeCacheRead"),
cacheWriteTokens: t("aiUsageTokenTypeCacheWrite"),
completionTokens: t("aiUsageTokenTypeCompletion"),
reasoningTokens: t("aiUsageTokenTypeReasoning")
};
const requestsSeries = [ const requestsSeries = [
{ key: "requests", label: "Requests", color: SERIES_COLORS[0] } { key: "requests", label: t("aiUsageRequests"), color: SERIES_COLORS[0] }
]; ];
const tokensSeries = Object.keys(TOKEN_TYPE_LABELS).map((key, i) => ({ const tokensSeries = Object.keys(TOKEN_TYPE_LABELS).map((key, i) => ({
key, key,
@@ -49,16 +51,18 @@ export function OverviewTab(props: OverviewTabProps) {
color: SERIES_COLORS[i % SERIES_COLORS.length] color: SERIES_COLORS[i % SERIES_COLORS.length]
})); }));
const costSeries = [ const costSeries = [
{ key: "cost", label: "Cost", color: SERIES_COLORS[0] } { key: "cost", label: t("aiUsageCost"), color: SERIES_COLORS[0] }
]; ];
const modelCostSeries = buildSeriesFromData( const modelCostSeries = buildSeriesFromData(
data?.modelCostPerDay ?? [], data?.modelCostPerDay ?? [],
(key) => key (key) => key,
t("aiUsageOther")
); );
const modelTokensSeries = buildSeriesFromData( const modelTokensSeries = buildSeriesFromData(
data?.modelTokensPerDay ?? [], data?.modelTokensPerDay ?? [],
(key) => key (key) => key,
t("aiUsageOther")
); );
const topModels: TopEntity[] = (data?.topModels ?? []).map((m) => ({ const topModels: TopEntity[] = (data?.topModels ?? []).map((m) => ({
@@ -75,7 +79,9 @@ export function OverviewTab(props: OverviewTabProps) {
<CardHeader> <CardHeader>
<InfoSections cols={4}> <InfoSections cols={4}>
<InfoSection> <InfoSection>
<InfoSectionTitle>Total Requests</InfoSectionTitle> <InfoSectionTitle>
{t("aiUsageTotalRequests")}
</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{data {data
? compactNumberFormatter.format( ? compactNumberFormatter.format(
@@ -85,7 +91,9 @@ export function OverviewTab(props: OverviewTabProps) {
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle>Total Tokens</InfoSectionTitle> <InfoSectionTitle>
{t("aiUsageTotalTokens")}
</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{data {data
? compactNumberFormatter.format( ? compactNumberFormatter.format(
@@ -95,13 +103,17 @@ export function OverviewTab(props: OverviewTabProps) {
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle>Total Cost</InfoSectionTitle> <InfoSectionTitle>
{t("aiUsageTotalCost")}
</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{data ? formatCost(data.totalCost) : "--"} {data ? formatCost(data.totalCost) : "--"}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
<InfoSectionTitle>Estimated</InfoSectionTitle> <InfoSectionTitle>
{t("aiUsageEstimated")}
</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{data {data
? `${Math.round(data.estimatedPercent)}%` ? `${Math.round(data.estimatedPercent)}%`
@@ -114,11 +126,9 @@ export function OverviewTab(props: OverviewTabProps) {
<div className="grid lg:grid-cols-3 gap-5"> <div className="grid lg:grid-cols-3 gap-5">
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Request Volume</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageRequestVolume")}
data={data?.requestsPerDay ?? []} data={data?.requestsPerDay ?? []}
series={requestsSeries} series={requestsSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -126,11 +136,9 @@ export function OverviewTab(props: OverviewTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Token Usage</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageTokenUsage")}
data={data?.tokensPerDay ?? []} data={data?.tokensPerDay ?? []}
series={tokensSeries} series={tokensSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -138,11 +146,9 @@ export function OverviewTab(props: OverviewTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Cost</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageCost")}
data={data?.costPerDay ?? []} data={data?.costPerDay ?? []}
series={costSeries} series={costSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -154,11 +160,9 @@ export function OverviewTab(props: OverviewTabProps) {
<div className="grid lg:grid-cols-2 gap-5"> <div className="grid lg:grid-cols-2 gap-5">
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Model Cost</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageModelCost")}
data={data?.modelCostPerDay ?? []} data={data?.modelCostPerDay ?? []}
series={modelCostSeries} series={modelCostSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -167,11 +171,9 @@ export function OverviewTab(props: OverviewTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Model Tokens</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageModelTokens")}
data={data?.modelTokensPerDay ?? []} data={data?.modelTokensPerDay ?? []}
series={modelTokensSeries} series={modelTokensSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -182,13 +184,13 @@ export function OverviewTab(props: OverviewTabProps) {
<Card> <Card>
<CardHeader> <CardHeader>
<h3 className="font-semibold">Top Models</h3> <h3 className="font-semibold">{t("aiUsageTopModels")}</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TopEntitiesList <TopEntitiesList
entities={topModels} entities={topModels}
isLoading={isLoading} isLoading={isLoading}
nameColumnLabel="Model" nameColumnLabel={t("aiUsageFilterModel")}
/> />
</CardContent> </CardContent>
</Card> </Card>
@@ -3,6 +3,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import { aiUsageAnalyticsQueries } from "@app/lib/queries";
import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { Card, CardContent, CardHeader } from "@app/components/ui/card";
import { ToggleableTrendChart } from "./ToggleableTrendChart"; import { ToggleableTrendChart } from "./ToggleableTrendChart";
import { TopEntitiesList, type TopEntity } from "./TopEntitiesList"; import { TopEntitiesList, type TopEntity } from "./TopEntitiesList";
@@ -14,6 +15,7 @@ type ProvidersTabProps = {
}; };
export function ProvidersTab(props: ProvidersTabProps) { export function ProvidersTab(props: ProvidersTabProps) {
const t = useTranslations();
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery(
aiUsageAnalyticsQueries.providers({ aiUsageAnalyticsQueries.providers({
orgId: props.orgId, orgId: props.orgId,
@@ -29,11 +31,13 @@ export function ProvidersTab(props: ProvidersTabProps) {
const costSeries = buildSeriesFromData( const costSeries = buildSeriesFromData(
data?.providerCostPerDay ?? [], data?.providerCostPerDay ?? [],
labelFor labelFor,
t("aiUsageOther")
); );
const tokensSeries = buildSeriesFromData( const tokensSeries = buildSeriesFromData(
data?.providerTokensPerDay ?? [], data?.providerTokensPerDay ?? [],
labelFor labelFor,
t("aiUsageOther")
); );
const topProviders: TopEntity[] = (data?.topProviders ?? []).map((p) => ({ const topProviders: TopEntity[] = (data?.topProviders ?? []).map((p) => ({
@@ -48,24 +52,22 @@ export function ProvidersTab(props: ProvidersTabProps) {
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<Card> <Card>
<CardHeader> <CardHeader>
<h3 className="font-semibold">Top Providers</h3> <h3 className="font-semibold">{t("aiUsageTopProviders")}</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TopEntitiesList <TopEntitiesList
entities={topProviders} entities={topProviders}
isLoading={isLoading} isLoading={isLoading}
nameColumnLabel="Provider" nameColumnLabel={t("aiUsageFilterProvider")}
/> />
</CardContent> </CardContent>
</Card> </Card>
<div className="grid lg:grid-cols-2 gap-5"> <div className="grid lg:grid-cols-2 gap-5">
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Provider Cost</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageProviderCost")}
data={data?.providerCostPerDay ?? []} data={data?.providerCostPerDay ?? []}
series={costSeries} series={costSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -74,11 +76,9 @@ export function ProvidersTab(props: ProvidersTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Provider Token Usage</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageProviderTokenUsage")}
data={data?.providerTokensPerDay ?? []} data={data?.providerTokensPerDay ?? []}
series={tokensSeries} series={tokensSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -3,6 +3,7 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import { aiUsageAnalyticsQueries } from "@app/lib/queries";
import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { Card, CardContent, CardHeader } from "@app/components/ui/card";
import { ToggleableTrendChart } from "./ToggleableTrendChart"; import { ToggleableTrendChart } from "./ToggleableTrendChart";
import { TopEntitiesList, type TopEntity } from "./TopEntitiesList"; import { TopEntitiesList, type TopEntity } from "./TopEntitiesList";
@@ -13,13 +14,15 @@ type ResourcesTabProps = {
filters: AiUsageAnalyticsFilters; filters: AiUsageAnalyticsFilters;
}; };
function resourceTypeLabel(type: "public" | "site" | null) {
if (type === "public") return "Resource";
if (type === "site") return "Site resource";
return undefined;
}
export function ResourcesTab(props: ResourcesTabProps) { export function ResourcesTab(props: ResourcesTabProps) {
const t = useTranslations();
function resourceTypeLabel(type: "public" | "site" | null) {
if (type === "public") return t("aiUsageResourceTypePublic");
if (type === "site") return t("aiUsageResourceTypeSite");
return undefined;
}
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery(
aiUsageAnalyticsQueries.resources({ aiUsageAnalyticsQueries.resources({
orgId: props.orgId, orgId: props.orgId,
@@ -32,20 +35,22 @@ export function ResourcesTab(props: ResourcesTabProps) {
nameByKey.set(r.key, r.name ?? r.key); nameByKey.set(r.key, r.name ?? r.key);
} }
const labelFor = (key: string) => const labelFor = (key: string) =>
key === "none" ? "No resource" : (nameByKey.get(key) ?? key); key === "none" ? t("aiUsageNoResource") : (nameByKey.get(key) ?? key);
const costSeries = buildSeriesFromData( const costSeries = buildSeriesFromData(
data?.resourceCostPerDay ?? [], data?.resourceCostPerDay ?? [],
labelFor labelFor,
t("aiUsageOther")
); );
const tokensSeries = buildSeriesFromData( const tokensSeries = buildSeriesFromData(
data?.resourceTokensPerDay ?? [], data?.resourceTokensPerDay ?? [],
labelFor labelFor,
t("aiUsageOther")
); );
const topResources: TopEntity[] = (data?.topResources ?? []).map((r) => ({ const topResources: TopEntity[] = (data?.topResources ?? []).map((r) => ({
key: r.key, key: r.key,
label: r.name ?? (r.key === "none" ? "No resource" : r.key), label: r.name ?? (r.key === "none" ? t("aiUsageNoResource") : r.key),
sublabel: resourceTypeLabel(r.type), sublabel: resourceTypeLabel(r.type),
requests: r.requests, requests: r.requests,
totalTokens: r.totalTokens, totalTokens: r.totalTokens,
@@ -56,24 +61,22 @@ export function ResourcesTab(props: ResourcesTabProps) {
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<Card> <Card>
<CardHeader> <CardHeader>
<h3 className="font-semibold">Top Resources</h3> <h3 className="font-semibold">{t("aiUsageTopResources")}</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TopEntitiesList <TopEntitiesList
entities={topResources} entities={topResources}
isLoading={isLoading} isLoading={isLoading}
nameColumnLabel="Resource" nameColumnLabel={t("aiUsageFilterResource")}
/> />
</CardContent> </CardContent>
</Card> </Card>
<div className="grid lg:grid-cols-2 gap-5"> <div className="grid lg:grid-cols-2 gap-5">
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Resource Cost</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageResourceCost")}
data={data?.resourceCostPerDay ?? []} data={data?.resourceCostPerDay ?? []}
series={costSeries} series={costSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -82,11 +85,9 @@ export function ResourcesTab(props: ResourcesTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Resource Token Usage</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageResourceTokenUsage")}
data={data?.resourceTokensPerDay ?? []} data={data?.resourceTokensPerDay ?? []}
series={tokensSeries} series={tokensSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -2,6 +2,7 @@
import { useState } from "react"; import { useState } from "react";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { useTranslations } from "next-intl";
import { BarChart3, LineChart as LineChartIcon, LoaderIcon } from "lucide-react"; import { BarChart3, LineChart as LineChartIcon, LoaderIcon } from "lucide-react";
import { import {
Bar, Bar,
@@ -34,6 +35,7 @@ export interface TrendChartRow {
} }
type ToggleableTrendChartProps = { type ToggleableTrendChartProps = {
title: string;
data: TrendChartRow[]; data: TrendChartRow[];
series: TrendSeries[]; series: TrendSeries[];
isLoading?: boolean; isLoading?: boolean;
@@ -48,6 +50,7 @@ const compactFormatter = new Intl.NumberFormat(undefined, {
}); });
export function ToggleableTrendChart(props: ToggleableTrendChartProps) { export function ToggleableTrendChart(props: ToggleableTrendChartProps) {
const t = useTranslations();
const [chartType, setChartType] = useState<"bar" | "line">("bar"); const [chartType, setChartType] = useState<"bar" | "line">("bar");
const valueFormatter = props.valueFormatter ?? compactFormatter.format; const valueFormatter = props.valueFormatter ?? compactFormatter.format;
@@ -61,25 +64,28 @@ export function ToggleableTrendChart(props: ToggleableTrendChartProps) {
return ( return (
<div className={cn("relative flex flex-col gap-2", props.className)}> <div className={cn("relative flex flex-col gap-2", props.className)}>
<div className="flex justify-end gap-1"> <div className="flex items-center justify-between gap-2">
<Button <h3 className="font-semibold">{props.title}</h3>
type="button" <div className="flex gap-1">
size="sm" <Button
variant={chartType === "bar" ? "secondary" : "ghost"} type="button"
onClick={() => setChartType("bar")} size="sm"
className="gap-1.5 px-2" variant={chartType === "bar" ? "secondary" : "ghost"}
> onClick={() => setChartType("bar")}
<BarChart3 className="size-3.5" /> className="gap-1.5 px-2"
</Button> >
<Button <BarChart3 className="size-3.5" />
type="button" </Button>
size="sm" <Button
variant={chartType === "line" ? "secondary" : "ghost"} type="button"
onClick={() => setChartType("line")} size="sm"
className="gap-1.5 px-2" variant={chartType === "line" ? "secondary" : "ghost"}
> onClick={() => setChartType("line")}
<LineChartIcon className="size-3.5" /> className="gap-1.5 px-2"
</Button> >
<LineChartIcon className="size-3.5" />
</Button>
</div>
</div> </div>
{!hasData ? ( {!hasData ? (
@@ -87,10 +93,10 @@ export function ToggleableTrendChart(props: ToggleableTrendChartProps) {
{props.isLoading ? ( {props.isLoading ? (
<> <>
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
Loading... {t("aiUsageLoading")}
</> </>
) : ( ) : (
"No data" t("aiUsageNoData")
)} )}
</div> </div>
) : ( ) : (
@@ -1,6 +1,7 @@
"use client"; "use client";
import { LoaderIcon } from "lucide-react"; import { LoaderIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { compactNumberFormatter, formatCost } from "./shared"; import { compactNumberFormatter, formatCost } from "./shared";
export type TopEntity = { export type TopEntity = {
@@ -20,6 +21,7 @@ type TopEntitiesListProps = {
}; };
export function TopEntitiesList(props: TopEntitiesListProps) { export function TopEntitiesList(props: TopEntitiesListProps) {
const t = useTranslations();
const totalCost = props.entities.reduce( const totalCost = props.entities.reduce(
(sum, e) => sum + (e.costUsd ?? 0), (sum, e) => sum + (e.costUsd ?? 0),
0 0
@@ -30,9 +32,15 @@ export function TopEntitiesList(props: TopEntitiesListProps) {
{props.entities.length > 0 && ( {props.entities.length > 0 && (
<div className="grid grid-cols-12 text-sm text-muted-foreground font-semibold h-4"> <div className="grid grid-cols-12 text-sm text-muted-foreground font-semibold h-4">
<div className="col-span-5">{props.nameColumnLabel}</div> <div className="col-span-5">{props.nameColumnLabel}</div>
<div className="col-span-2 text-end">Requests</div> <div className="col-span-2 text-end">
<div className="col-span-2 text-end">Tokens</div> {t("aiUsageRequests")}
<div className="col-span-2 text-end">Cost</div> </div>
<div className="col-span-2 text-end">
{t("aiUsageTokens")}
</div>
<div className="col-span-2 text-end">
{t("aiUsageCost")}
</div>
<div className="col-span-1 text-end">%</div> <div className="col-span-1 text-end">%</div>
</div> </div>
)} )}
@@ -42,10 +50,10 @@ export function TopEntitiesList(props: TopEntitiesListProps) {
{props.isLoading ? ( {props.isLoading ? (
<> <>
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
Loading... {t("aiUsageLoading")}
</> </>
) : ( ) : (
(props.emptyLabel ?? "No data") (props.emptyLabel ?? t("aiUsageNoData"))
)} )}
</div> </div>
)} )}
@@ -3,7 +3,9 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { aiUsageAnalyticsQueries } from "@app/lib/queries"; import { aiUsageAnalyticsQueries } from "@app/lib/queries";
import type { AiUsageAnalyticsFilters } from "@app/lib/queries"; import type { AiUsageAnalyticsFilters } from "@app/lib/queries";
import { useTranslations } from "next-intl";
import { Card, CardContent, CardHeader } from "@app/components/ui/card"; import { Card, CardContent, CardHeader } from "@app/components/ui/card";
import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs";
import { ToggleableTrendChart } from "./ToggleableTrendChart"; import { ToggleableTrendChart } from "./ToggleableTrendChart";
import { TopEntitiesList, type TopEntity } from "./TopEntitiesList"; import { TopEntitiesList, type TopEntity } from "./TopEntitiesList";
import { buildSeriesFromData, formatCost } from "./shared"; import { buildSeriesFromData, formatCost } from "./shared";
@@ -16,6 +18,7 @@ type UsersRolesTabProps = {
const UNKNOWN_USER_KEY = "unknown"; const UNKNOWN_USER_KEY = "unknown";
export function UsersRolesTab(props: UsersRolesTabProps) { export function UsersRolesTab(props: UsersRolesTabProps) {
const t = useTranslations();
const { data, isLoading } = useQuery( const { data, isLoading } = useQuery(
aiUsageAnalyticsQueries.usersRoles({ aiUsageAnalyticsQueries.usersRoles({
orgId: props.orgId, orgId: props.orgId,
@@ -38,24 +41,28 @@ export function UsersRolesTab(props: UsersRolesTabProps) {
} }
const userLabelFor = (key: string) => const userLabelFor = (key: string) =>
key === UNKNOWN_USER_KEY key === UNKNOWN_USER_KEY
? "Unknown user" ? t("aiUsageUnknownUser")
: (userEmailByKey.get(key) ?? key); : (userEmailByKey.get(key) ?? key);
const roleCostSeries = buildSeriesFromData( const roleCostSeries = buildSeriesFromData(
data?.roleCostPerDay ?? [], data?.roleCostPerDay ?? [],
roleLabelFor roleLabelFor,
t("aiUsageOther")
); );
const roleTokensSeries = buildSeriesFromData( const roleTokensSeries = buildSeriesFromData(
data?.roleTokensPerDay ?? [], data?.roleTokensPerDay ?? [],
roleLabelFor roleLabelFor,
t("aiUsageOther")
); );
const userCostSeries = buildSeriesFromData( const userCostSeries = buildSeriesFromData(
data?.userCostPerDay ?? [], data?.userCostPerDay ?? [],
userLabelFor userLabelFor,
t("aiUsageOther")
); );
const userTokensSeries = buildSeriesFromData( const userTokensSeries = buildSeriesFromData(
data?.userTokensPerDay ?? [], data?.userTokensPerDay ?? [],
userLabelFor userLabelFor,
t("aiUsageOther")
); );
const topRoles: TopEntity[] = (data?.topRoles ?? []).map((r) => ({ const topRoles: TopEntity[] = (data?.topRoles ?? []).map((r) => ({
@@ -68,35 +75,39 @@ export function UsersRolesTab(props: UsersRolesTabProps) {
const topUsers: TopEntity[] = (data?.topUsers ?? []).map((u) => ({ const topUsers: TopEntity[] = (data?.topUsers ?? []).map((u) => ({
key: u.userId ?? UNKNOWN_USER_KEY, key: u.userId ?? UNKNOWN_USER_KEY,
label: u.email ?? u.userId ?? "Unknown user", label: u.email ?? u.userId ?? t("aiUsageUnknownUser"),
requests: u.requests, requests: u.requests,
totalTokens: u.totalTokens, totalTokens: u.totalTokens,
costUsd: u.costUsd costUsd: u.costUsd
})); }));
const tabs: TabItem[] = [
{ title: t("aiUsageRolesTab"), href: "#" },
{ title: t("aiUsageUsersTab"), href: "#" }
];
return ( return (
<div className="flex flex-col gap-8"> <HorizontalTabs items={tabs} clientSide>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<h3 className="font-semibold text-muted-foreground">Roles</h3>
<Card> <Card>
<CardHeader> <CardHeader>
<h3 className="font-semibold">Top Roles</h3> <h3 className="font-semibold">
{t("aiUsageTopRoles")}
</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TopEntitiesList <TopEntitiesList
entities={topRoles} entities={topRoles}
isLoading={isLoading} isLoading={isLoading}
nameColumnLabel="Role" nameColumnLabel={t("aiUsageFilterRole")}
/> />
</CardContent> </CardContent>
</Card> </Card>
<div className="grid lg:grid-cols-2 gap-5"> <div className="grid lg:grid-cols-2 gap-5">
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Role Cost</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageRoleCost")}
data={data?.roleCostPerDay ?? []} data={data?.roleCostPerDay ?? []}
series={roleCostSeries} series={roleCostSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -105,11 +116,9 @@ export function UsersRolesTab(props: UsersRolesTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">Role Token Usage</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageRoleTokenUsage")}
data={data?.roleTokensPerDay ?? []} data={data?.roleTokensPerDay ?? []}
series={roleTokensSeries} series={roleTokensSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -120,26 +129,25 @@ export function UsersRolesTab(props: UsersRolesTabProps) {
</div> </div>
<div className="flex flex-col gap-5"> <div className="flex flex-col gap-5">
<h3 className="font-semibold text-muted-foreground">Users</h3>
<Card> <Card>
<CardHeader> <CardHeader>
<h3 className="font-semibold">Top Users</h3> <h3 className="font-semibold">
{t("aiUsageTopUsers")}
</h3>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TopEntitiesList <TopEntitiesList
entities={topUsers} entities={topUsers}
isLoading={isLoading} isLoading={isLoading}
nameColumnLabel="User" nameColumnLabel={t("aiUsageFilterUser")}
/> />
</CardContent> </CardContent>
</Card> </Card>
<div className="grid lg:grid-cols-2 gap-5"> <div className="grid lg:grid-cols-2 gap-5">
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">User Cost</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageUserCost")}
data={data?.userCostPerDay ?? []} data={data?.userCostPerDay ?? []}
series={userCostSeries} series={userCostSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -148,11 +156,9 @@ export function UsersRolesTab(props: UsersRolesTabProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader> <CardContent className="p-6">
<h3 className="font-semibold">User Token Usage</h3>
</CardHeader>
<CardContent>
<ToggleableTrendChart <ToggleableTrendChart
title={t("aiUsageUserTokenUsage")}
data={data?.userTokensPerDay ?? []} data={data?.userTokensPerDay ?? []}
series={userTokensSeries} series={userTokensSeries}
isLoading={isLoading} isLoading={isLoading}
@@ -161,6 +167,6 @@ export function UsersRolesTab(props: UsersRolesTabProps) {
</Card> </Card>
</div> </div>
</div> </div>
</div> </HorizontalTabs>
); );
} }
+3 -2
View File
@@ -18,7 +18,8 @@ export const OTHER_KEY = "other";
// top-list needed. Assigns one categorical color per key, "other" last. // top-list needed. Assigns one categorical color per key, "other" last.
export function buildSeriesFromData( export function buildSeriesFromData(
data: Array<Record<string, number | string>>, data: Array<Record<string, number | string>>,
labelFor: (key: string) => string labelFor: (key: string) => string,
otherLabel = "Other"
): TrendSeries[] { ): TrendSeries[] {
const keys = new Set<string>(); const keys = new Set<string>();
for (const row of data) { for (const row of data) {
@@ -37,7 +38,7 @@ export function buildSeriesFromData(
const hasOther = data.some((row) => OTHER_KEY in row); const hasOther = data.some((row) => OTHER_KEY in row);
if (hasOther) { if (hasOther) {
series.push({ key: OTHER_KEY, label: "Other", color: OTHER_COLOR }); series.push({ key: OTHER_KEY, label: otherLabel, color: OTHER_COLOR });
} }
return series; return series;
+5 -1
View File
@@ -57,7 +57,11 @@ const ChartContainer = React.forwardRef<
{...props} {...props}
> >
<ChartStyle id={chartId} config={config} /> <ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer minWidth={0} minHeight={0}> <RechartsPrimitive.ResponsiveContainer
minWidth={0}
minHeight={0}
initialDimension={{ width: 520, height: 256 }}
>
{children} {children}
</RechartsPrimitive.ResponsiveContainer> </RechartsPrimitive.ResponsiveContainer>
</div> </div>