mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 00:09:55 +02:00
translate keys and restructure roles and users
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user