mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-14 00:09:55 +02:00
separate the users and roles tabs
This commit is contained in:
@@ -1941,7 +1941,6 @@
|
|||||||
"aiUsageTabOverview": "Overview",
|
"aiUsageTabOverview": "Overview",
|
||||||
"aiUsageTabProviders": "Providers",
|
"aiUsageTabProviders": "Providers",
|
||||||
"aiUsageTabResources": "Resources",
|
"aiUsageTabResources": "Resources",
|
||||||
"aiUsageTabUsersRoles": "Users & Roles",
|
|
||||||
"aiUsageFilterProvider": "Provider",
|
"aiUsageFilterProvider": "Provider",
|
||||||
"aiUsageFilterModel": "Model",
|
"aiUsageFilterModel": "Model",
|
||||||
"aiUsageFilterResource": "Resource",
|
"aiUsageFilterResource": "Resource",
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import { HorizontalTabs, type TabItem } from "./HorizontalTabs";
|
|||||||
import { OverviewTab } from "./ai-usage-analytics/OverviewTab";
|
import { OverviewTab } from "./ai-usage-analytics/OverviewTab";
|
||||||
import { ProvidersTab } from "./ai-usage-analytics/ProvidersTab";
|
import { ProvidersTab } from "./ai-usage-analytics/ProvidersTab";
|
||||||
import { ResourcesTab } from "./ai-usage-analytics/ResourcesTab";
|
import { ResourcesTab } from "./ai-usage-analytics/ResourcesTab";
|
||||||
import { UsersRolesTab } from "./ai-usage-analytics/UsersRolesTab";
|
import { RolesTab } from "./ai-usage-analytics/RolesTab";
|
||||||
|
import { UsersTab } from "./ai-usage-analytics/UsersTab";
|
||||||
|
|
||||||
export type AiUsageAnalyticsDataProps = {
|
export type AiUsageAnalyticsDataProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
@@ -144,7 +145,8 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
|
|||||||
{ title: t("aiUsageTabOverview"), href: "#" },
|
{ title: t("aiUsageTabOverview"), href: "#" },
|
||||||
{ title: t("aiUsageTabProviders"), href: "#" },
|
{ title: t("aiUsageTabProviders"), href: "#" },
|
||||||
{ title: t("aiUsageTabResources"), href: "#" },
|
{ title: t("aiUsageTabResources"), href: "#" },
|
||||||
{ title: t("aiUsageTabUsersRoles"), href: "#" }
|
{ title: t("aiUsageRolesTab"), href: "#" },
|
||||||
|
{ title: t("aiUsageUsersTab"), href: "#" }
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -255,7 +257,8 @@ export function AiUsageAnalyticsData(props: AiUsageAnalyticsDataProps) {
|
|||||||
<OverviewTab orgId={props.orgId} filters={filters} />
|
<OverviewTab orgId={props.orgId} filters={filters} />
|
||||||
<ProvidersTab orgId={props.orgId} filters={filters} />
|
<ProvidersTab orgId={props.orgId} filters={filters} />
|
||||||
<ResourcesTab orgId={props.orgId} filters={filters} />
|
<ResourcesTab orgId={props.orgId} filters={filters} />
|
||||||
<UsersRolesTab orgId={props.orgId} filters={filters} />
|
<RolesTab orgId={props.orgId} filters={filters} />
|
||||||
|
<UsersTab orgId={props.orgId} filters={filters} />
|
||||||
</HorizontalTabs>
|
</HorizontalTabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { aiUsageAnalyticsQueries } 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 { ToggleableTrendChart } from "./ToggleableTrendChart";
|
||||||
|
import { TopEntitiesList, type TopEntity } from "./TopEntitiesList";
|
||||||
|
import { buildSeriesFromData, formatCost } from "./shared";
|
||||||
|
|
||||||
|
type RolesTabProps = {
|
||||||
|
orgId: string;
|
||||||
|
filters: AiUsageAnalyticsFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RolesTab(props: RolesTabProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const { data, isLoading } = useQuery(
|
||||||
|
aiUsageAnalyticsQueries.usersRoles({
|
||||||
|
orgId: props.orgId,
|
||||||
|
filters: props.filters
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const roleNameByKey = new Map<string, string>();
|
||||||
|
for (const r of data?.topRoles ?? []) {
|
||||||
|
roleNameByKey.set(String(r.roleId), r.name ?? `Role #${r.roleId}`);
|
||||||
|
}
|
||||||
|
const roleLabelFor = (key: string) =>
|
||||||
|
roleNameByKey.get(key) ?? `Role #${key}`;
|
||||||
|
|
||||||
|
const roleCostSeries = buildSeriesFromData(
|
||||||
|
data?.roleCostPerDay ?? [],
|
||||||
|
roleLabelFor,
|
||||||
|
t("aiUsageOther")
|
||||||
|
);
|
||||||
|
const roleTokensSeries = buildSeriesFromData(
|
||||||
|
data?.roleTokensPerDay ?? [],
|
||||||
|
roleLabelFor,
|
||||||
|
t("aiUsageOther")
|
||||||
|
);
|
||||||
|
|
||||||
|
const topRoles: TopEntity[] = (data?.topRoles ?? []).map((r) => ({
|
||||||
|
key: String(r.roleId),
|
||||||
|
label: r.name ?? `Role #${r.roleId}`,
|
||||||
|
requests: r.requests,
|
||||||
|
totalTokens: r.totalTokens,
|
||||||
|
costUsd: r.costUsd
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h3 className="font-semibold">{t("aiUsageTopRoles")}</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<TopEntitiesList
|
||||||
|
entities={topRoles}
|
||||||
|
isLoading={isLoading}
|
||||||
|
nameColumnLabel={t("aiUsageFilterRole")}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<div className="grid lg:grid-cols-2 gap-5">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<ToggleableTrendChart
|
||||||
|
title={t("aiUsageRoleCost")}
|
||||||
|
data={data?.roleCostPerDay ?? []}
|
||||||
|
series={roleCostSeries}
|
||||||
|
isLoading={isLoading}
|
||||||
|
valueFormatter={(v) => formatCost(v)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<ToggleableTrendChart
|
||||||
|
title={t("aiUsageRoleTokenUsage")}
|
||||||
|
data={data?.roleTokensPerDay ?? []}
|
||||||
|
series={roleTokensSeries}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { aiUsageAnalyticsQueries } 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 { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs";
|
|
||||||
import { ToggleableTrendChart } from "./ToggleableTrendChart";
|
|
||||||
import { TopEntitiesList, type TopEntity } from "./TopEntitiesList";
|
|
||||||
import { buildSeriesFromData, formatCost } from "./shared";
|
|
||||||
|
|
||||||
type UsersRolesTabProps = {
|
|
||||||
orgId: string;
|
|
||||||
filters: AiUsageAnalyticsFilters;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UNKNOWN_USER_KEY = "unknown";
|
|
||||||
|
|
||||||
export function UsersRolesTab(props: UsersRolesTabProps) {
|
|
||||||
const t = useTranslations();
|
|
||||||
const { data, isLoading } = useQuery(
|
|
||||||
aiUsageAnalyticsQueries.usersRoles({
|
|
||||||
orgId: props.orgId,
|
|
||||||
filters: props.filters
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const roleNameByKey = new Map<string, string>();
|
|
||||||
for (const r of data?.topRoles ?? []) {
|
|
||||||
roleNameByKey.set(String(r.roleId), r.name ?? `Role #${r.roleId}`);
|
|
||||||
}
|
|
||||||
const roleLabelFor = (key: string) =>
|
|
||||||
roleNameByKey.get(key) ?? `Role #${key}`;
|
|
||||||
|
|
||||||
const userEmailByKey = new Map<string, string>();
|
|
||||||
for (const u of data?.topUsers ?? []) {
|
|
||||||
if (u.userId) {
|
|
||||||
userEmailByKey.set(u.userId, u.email ?? u.userId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const userLabelFor = (key: string) =>
|
|
||||||
key === UNKNOWN_USER_KEY
|
|
||||||
? t("aiUsageUnknownUser")
|
|
||||||
: (userEmailByKey.get(key) ?? key);
|
|
||||||
|
|
||||||
const roleCostSeries = buildSeriesFromData(
|
|
||||||
data?.roleCostPerDay ?? [],
|
|
||||||
roleLabelFor,
|
|
||||||
t("aiUsageOther")
|
|
||||||
);
|
|
||||||
const roleTokensSeries = buildSeriesFromData(
|
|
||||||
data?.roleTokensPerDay ?? [],
|
|
||||||
roleLabelFor,
|
|
||||||
t("aiUsageOther")
|
|
||||||
);
|
|
||||||
const userCostSeries = buildSeriesFromData(
|
|
||||||
data?.userCostPerDay ?? [],
|
|
||||||
userLabelFor,
|
|
||||||
t("aiUsageOther")
|
|
||||||
);
|
|
||||||
const userTokensSeries = buildSeriesFromData(
|
|
||||||
data?.userTokensPerDay ?? [],
|
|
||||||
userLabelFor,
|
|
||||||
t("aiUsageOther")
|
|
||||||
);
|
|
||||||
|
|
||||||
const topRoles: TopEntity[] = (data?.topRoles ?? []).map((r) => ({
|
|
||||||
key: String(r.roleId),
|
|
||||||
label: r.name ?? `Role #${r.roleId}`,
|
|
||||||
requests: r.requests,
|
|
||||||
totalTokens: r.totalTokens,
|
|
||||||
costUsd: r.costUsd
|
|
||||||
}));
|
|
||||||
|
|
||||||
const topUsers: TopEntity[] = (data?.topUsers ?? []).map((u) => ({
|
|
||||||
key: u.userId ?? UNKNOWN_USER_KEY,
|
|
||||||
label: u.email ?? u.userId ?? t("aiUsageUnknownUser"),
|
|
||||||
requests: u.requests,
|
|
||||||
totalTokens: u.totalTokens,
|
|
||||||
costUsd: u.costUsd
|
|
||||||
}));
|
|
||||||
|
|
||||||
const tabs: TabItem[] = [
|
|
||||||
{ title: t("aiUsageRolesTab"), href: "#" },
|
|
||||||
{ title: t("aiUsageUsersTab"), href: "#" }
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HorizontalTabs items={tabs} clientSide>
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<h3 className="font-semibold">
|
|
||||||
{t("aiUsageTopRoles")}
|
|
||||||
</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<TopEntitiesList
|
|
||||||
entities={topRoles}
|
|
||||||
isLoading={isLoading}
|
|
||||||
nameColumnLabel={t("aiUsageFilterRole")}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<div className="grid lg:grid-cols-2 gap-5">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<ToggleableTrendChart
|
|
||||||
title={t("aiUsageRoleCost")}
|
|
||||||
data={data?.roleCostPerDay ?? []}
|
|
||||||
series={roleCostSeries}
|
|
||||||
isLoading={isLoading}
|
|
||||||
valueFormatter={(v) => formatCost(v)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<ToggleableTrendChart
|
|
||||||
title={t("aiUsageRoleTokenUsage")}
|
|
||||||
data={data?.roleTokensPerDay ?? []}
|
|
||||||
series={roleTokensSeries}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<h3 className="font-semibold">
|
|
||||||
{t("aiUsageTopUsers")}
|
|
||||||
</h3>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<TopEntitiesList
|
|
||||||
entities={topUsers}
|
|
||||||
isLoading={isLoading}
|
|
||||||
nameColumnLabel={t("aiUsageFilterUser")}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<div className="grid lg:grid-cols-2 gap-5">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<ToggleableTrendChart
|
|
||||||
title={t("aiUsageUserCost")}
|
|
||||||
data={data?.userCostPerDay ?? []}
|
|
||||||
series={userCostSeries}
|
|
||||||
isLoading={isLoading}
|
|
||||||
valueFormatter={(v) => formatCost(v)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-6">
|
|
||||||
<ToggleableTrendChart
|
|
||||||
title={t("aiUsageUserTokenUsage")}
|
|
||||||
data={data?.userTokensPerDay ?? []}
|
|
||||||
series={userTokensSeries}
|
|
||||||
isLoading={isLoading}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HorizontalTabs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { aiUsageAnalyticsQueries } 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 { ToggleableTrendChart } from "./ToggleableTrendChart";
|
||||||
|
import { TopEntitiesList, type TopEntity } from "./TopEntitiesList";
|
||||||
|
import { buildSeriesFromData, formatCost } from "./shared";
|
||||||
|
|
||||||
|
type UsersTabProps = {
|
||||||
|
orgId: string;
|
||||||
|
filters: AiUsageAnalyticsFilters;
|
||||||
|
};
|
||||||
|
|
||||||
|
const UNKNOWN_USER_KEY = "unknown";
|
||||||
|
|
||||||
|
export function UsersTab(props: UsersTabProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const { data, isLoading } = useQuery(
|
||||||
|
aiUsageAnalyticsQueries.usersRoles({
|
||||||
|
orgId: props.orgId,
|
||||||
|
filters: props.filters
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const userEmailByKey = new Map<string, string>();
|
||||||
|
for (const u of data?.topUsers ?? []) {
|
||||||
|
if (u.userId) {
|
||||||
|
userEmailByKey.set(u.userId, u.email ?? u.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const userLabelFor = (key: string) =>
|
||||||
|
key === UNKNOWN_USER_KEY
|
||||||
|
? t("aiUsageUnknownUser")
|
||||||
|
: (userEmailByKey.get(key) ?? key);
|
||||||
|
|
||||||
|
const userCostSeries = buildSeriesFromData(
|
||||||
|
data?.userCostPerDay ?? [],
|
||||||
|
userLabelFor,
|
||||||
|
t("aiUsageOther")
|
||||||
|
);
|
||||||
|
const userTokensSeries = buildSeriesFromData(
|
||||||
|
data?.userTokensPerDay ?? [],
|
||||||
|
userLabelFor,
|
||||||
|
t("aiUsageOther")
|
||||||
|
);
|
||||||
|
|
||||||
|
const topUsers: TopEntity[] = (data?.topUsers ?? []).map((u) => ({
|
||||||
|
key: u.userId ?? UNKNOWN_USER_KEY,
|
||||||
|
label: u.email ?? u.userId ?? t("aiUsageUnknownUser"),
|
||||||
|
requests: u.requests,
|
||||||
|
totalTokens: u.totalTokens,
|
||||||
|
costUsd: u.costUsd
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<h3 className="font-semibold">{t("aiUsageTopUsers")}</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<TopEntitiesList
|
||||||
|
entities={topUsers}
|
||||||
|
isLoading={isLoading}
|
||||||
|
nameColumnLabel={t("aiUsageFilterUser")}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<div className="grid lg:grid-cols-2 gap-5">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<ToggleableTrendChart
|
||||||
|
title={t("aiUsageUserCost")}
|
||||||
|
data={data?.userCostPerDay ?? []}
|
||||||
|
series={userCostSeries}
|
||||||
|
isLoading={isLoading}
|
||||||
|
valueFormatter={(v) => formatCost(v)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<ToggleableTrendChart
|
||||||
|
title={t("aiUsageUserTokenUsage")}
|
||||||
|
data={data?.userTokensPerDay ?? []}
|
||||||
|
series={userTokensSeries}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user