Compare commits
2
Commits
7a325bebb9
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a19fea0c6c | ||
|
|
a516e6ce72 |
@@ -225,29 +225,77 @@ export const toUsagePieData = (
|
||||
return aggregateUsageByGroup(data, groupBy, metric);
|
||||
};
|
||||
|
||||
export const toUsageRankData = (
|
||||
export type UsageTokenMetric = 'input_tokens' | 'output_tokens';
|
||||
|
||||
export interface UsageTokenSeriesDef {
|
||||
name: string;
|
||||
key: UsageTokenMetric;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// Aggregates each group's input/output tokens into a stacked HBarChart shape,
|
||||
// so a single bar shows prompt (input) and completion (output) tokens side by
|
||||
// side. Rows sharing a group (e.g. the same user across dates) are summed, then
|
||||
// ranked by combined tokens and capped at the top 10.
|
||||
export const toUsageTokenBreakdownData = (
|
||||
data: UsageBreakdownResponse | null | undefined,
|
||||
groupBy: UsageGroupBy,
|
||||
seriesName: string,
|
||||
color: string
|
||||
seriesDefs: UsageTokenSeriesDef[]
|
||||
) => {
|
||||
const items = aggregateUsageByGroup(data, groupBy, 'total_tokens');
|
||||
const names = items.map((item) => item.name);
|
||||
const itemMap = new Map<string, { total: number; values: number[] }>();
|
||||
const items = getUsageResponseItems(data);
|
||||
|
||||
items.forEach((item: BreakdownItem) => {
|
||||
const name = buildUsageLabel(item, groupBy);
|
||||
const entry = itemMap.get(name) || {
|
||||
total: 0,
|
||||
values: seriesDefs.map(() => 0)
|
||||
};
|
||||
|
||||
seriesDefs.forEach((def, index) => {
|
||||
const value = Number((item as any)?.[def.key] ?? 0);
|
||||
entry.values[index] += value;
|
||||
entry.total += value;
|
||||
});
|
||||
|
||||
itemMap.set(name, entry);
|
||||
});
|
||||
|
||||
const ranked = Array.from(itemMap.entries())
|
||||
.filter(([, entry]) => entry.total > 0)
|
||||
.sort((a, b) => b[1].total - a[1].total)
|
||||
.slice(0, 10);
|
||||
|
||||
const names = ranked.map(([name]) => name);
|
||||
|
||||
// Round only the outer ends of the stacked bar; the seam where the input and
|
||||
// output segments meet stays square ([topLeft, topRight, bottomRight, bottomLeft]).
|
||||
const radius = 2;
|
||||
const getBorderRadius = (index: number, count: number) => {
|
||||
if (count <= 1) {
|
||||
return [radius, radius, radius, radius];
|
||||
}
|
||||
if (index === 0) {
|
||||
return [radius, 0, 0, radius];
|
||||
}
|
||||
if (index === count - 1) {
|
||||
return [0, radius, radius, 0];
|
||||
}
|
||||
return [0, 0, 0, 0];
|
||||
};
|
||||
|
||||
return {
|
||||
names,
|
||||
series: [
|
||||
{
|
||||
name: seriesName,
|
||||
color,
|
||||
data: items.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
itemStyle: {
|
||||
borderRadius: [2, 2, 2, 2]
|
||||
}
|
||||
}))
|
||||
}
|
||||
]
|
||||
series: seriesDefs.map((def, index) => ({
|
||||
name: def.name,
|
||||
color: def.color,
|
||||
data: ranked.map(([name, entry]) => ({
|
||||
name,
|
||||
value: entry.values[index],
|
||||
itemStyle: {
|
||||
borderRadius: getBorderRadius(index, seriesDefs.length)
|
||||
}
|
||||
}))
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import {
|
||||
baseColorMap,
|
||||
DashboardUsageCommonParams,
|
||||
toUsageRankData
|
||||
toUsageTokenBreakdownData
|
||||
} from '../config';
|
||||
|
||||
export default function useTopTokenUsageByUser(
|
||||
commonParams: DashboardUsageCommonParams
|
||||
) {
|
||||
const intl = useIntl();
|
||||
const query = useQueryTimeSeriesData({
|
||||
key: 'topTokenUsageByUserData'
|
||||
});
|
||||
const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' });
|
||||
|
||||
useEffect(() => {
|
||||
query
|
||||
@@ -31,13 +28,19 @@ export default function useTopTokenUsageByUser(
|
||||
|
||||
const rankData = useMemo(
|
||||
() =>
|
||||
toUsageRankData(
|
||||
query.detailData,
|
||||
'user',
|
||||
tokenUsageText,
|
||||
baseColorMap.base
|
||||
),
|
||||
[query.detailData, tokenUsageText]
|
||||
toUsageTokenBreakdownData(query.detailData, 'user', [
|
||||
{
|
||||
name: 'Prompt Tokens',
|
||||
key: 'input_tokens',
|
||||
color: baseColorMap.base
|
||||
},
|
||||
{
|
||||
name: 'Completion Tokens',
|
||||
key: 'output_tokens',
|
||||
color: baseColorMap.baseR3
|
||||
}
|
||||
]),
|
||||
[query.detailData]
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -96,3 +96,12 @@ export interface UsageMeta {
|
||||
}
|
||||
|
||||
export type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
|
||||
|
||||
// The full breakdown filter set (route / user / api_key). Every breakdown
|
||||
// table sends all active dimensions — matching the trend chart — so e.g. a
|
||||
// user filter narrows the Models table too, not only the Users table.
|
||||
export type BreakdownFilters = {
|
||||
routes?: FilterOptionType[];
|
||||
users?: FilterOptionType[];
|
||||
api_keys?: FilterOptionType[];
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { exportJsonToExcel } from '@gpustack/core-ui/excel';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { GroupOption } from '../config';
|
||||
import { BreakdownItem, UsageFilterItem } from '../config/types';
|
||||
import useQueryTimeSeriesData from '../services/use-query-timeseries-data';
|
||||
@@ -181,10 +182,17 @@ export const useUsageFilters = ({
|
||||
return filters;
|
||||
};
|
||||
|
||||
const filters = useMemo(
|
||||
() => buildFilters(commonFilters),
|
||||
[commonFilters, routeOptions, userOptions, apiKeyOptions]
|
||||
);
|
||||
// Keep a stable reference while the content is unchanged. ``buildFilters``
|
||||
// returns a fresh object every render — and again when the meta options
|
||||
// resolve after mount — which would otherwise retrigger every breakdown
|
||||
// table's fetch effect a second time on first load. Only a real selection
|
||||
// change (or options resolving a previously-selected id) should swap it.
|
||||
const filtersRef = useRef<ReturnType<typeof buildFilters>>({});
|
||||
const nextFilters = buildFilters(commonFilters);
|
||||
if (!_.isEqual(nextFilters, filtersRef.current)) {
|
||||
filtersRef.current = nextFilters;
|
||||
}
|
||||
const filters = filtersRef.current;
|
||||
|
||||
const fetchData = (
|
||||
currentSelectedFilters = commonFilters,
|
||||
|
||||
@@ -31,7 +31,9 @@ import { Col, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import ResourceFilterBar from '../components/resource-filter-bar';
|
||||
import useResourceMeta from '../hooks/use-resource-meta';
|
||||
import { FilterOptionType } from '../config/types';
|
||||
import useResourceMeta, { SelectOption } from '../hooks/use-resource-meta';
|
||||
import useQueryUsageMetaData from '../services/use-query-meta-data';
|
||||
import {
|
||||
bucketKey,
|
||||
generateBucketRange,
|
||||
@@ -231,7 +233,38 @@ const SummaryTab: React.FC = () => {
|
||||
selectedUsers: []
|
||||
});
|
||||
const { start, end, selectedUsers } = queryParams;
|
||||
const { creators: userOptions } = useResourceMeta(scope);
|
||||
const { creators: resourceUsers } = useResourceMeta(scope);
|
||||
const { detailData: tokenMeta, fetchData: fetchTokenMeta } =
|
||||
useQueryUsageMetaData();
|
||||
|
||||
// The user filter unions two sources: resource creators (GPU / storage
|
||||
// usage) and the token-usage users (/usage/meta) — a user may appear in only
|
||||
// one. Deduped by user id. The token meta also carries the per-user identity
|
||||
// the token-series endpoint filters on (see ``tokenUserById``).
|
||||
const userOptions = useMemo<SelectOption[]>(() => {
|
||||
const map = new Map<number, SelectOption>();
|
||||
resourceUsers.forEach((u) =>
|
||||
map.set(u.value, { value: u.value, label: u.label, deleted: u.deleted })
|
||||
);
|
||||
(tokenMeta?.users || []).forEach((u) => {
|
||||
const id = u.identity.current?.user_id;
|
||||
if (id != null && !map.has(id)) {
|
||||
map.set(id, { value: id, label: u.label });
|
||||
}
|
||||
});
|
||||
return Array.from(map.values());
|
||||
}, [resourceUsers, tokenMeta]);
|
||||
|
||||
// user id → the identity object the token series filters by. Built from the
|
||||
// token meta so the trend's ``users`` filter carries the real identity.
|
||||
const tokenUserById = useMemo(() => {
|
||||
const map = new Map<number, FilterOptionType>();
|
||||
(tokenMeta?.users || []).forEach((u) => {
|
||||
const id = u.identity.current?.user_id;
|
||||
if (id != null) map.set(id, { identity: u.identity });
|
||||
});
|
||||
return map;
|
||||
}, [tokenMeta]);
|
||||
|
||||
const {
|
||||
detailData: summary,
|
||||
@@ -285,6 +318,24 @@ const SummaryTab: React.FC = () => {
|
||||
? { creator_ids: currentParams.selectedUsers }
|
||||
: undefined;
|
||||
|
||||
// The token series hits /usage/breakdown, which filters users by identity
|
||||
// rather than the creator_ids the resource endpoints take — so the token
|
||||
// trend honors the user filter like the totals do. Resolve each id to its
|
||||
// token-meta identity, falling back to a minimal current.user_id object for
|
||||
// users present only in the resource meta.
|
||||
const tokenUserFilter: { users?: FilterOptionType[] } = currentParams
|
||||
.selectedUsers.length
|
||||
? {
|
||||
users: currentParams.selectedUsers.map(
|
||||
(id) =>
|
||||
tokenUserById.get(id) ??
|
||||
({
|
||||
identity: { current: { user_id: id } }
|
||||
} as unknown as FilterOptionType)
|
||||
)
|
||||
}
|
||||
: {};
|
||||
|
||||
await Promise.all([
|
||||
fetchSummary({
|
||||
...commonParams,
|
||||
@@ -308,7 +359,7 @@ const SummaryTab: React.FC = () => {
|
||||
group_by: ['date'],
|
||||
granularity,
|
||||
page: -1,
|
||||
filters: {}
|
||||
filters: tokenUserFilter
|
||||
}),
|
||||
|
||||
fetchComputeBreakdown({
|
||||
@@ -418,6 +469,7 @@ const SummaryTab: React.FC = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTokenMeta();
|
||||
fetchAll();
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tabs } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { UsageFilterItem } from '../../config/types';
|
||||
import { BreakdownFilters } from '../../config/types';
|
||||
import ApiKeysTable from '../tables/apikeys-table';
|
||||
import ModelsTable from '../tables/models-table';
|
||||
import UsersTable from '../tables/users-table';
|
||||
|
||||
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
|
||||
const EMPTY_FILTERS: FilterOptionType[] = [];
|
||||
|
||||
const BreakdownTabs: React.FC<{
|
||||
dateRange: {
|
||||
start_date: string;
|
||||
@@ -17,16 +14,9 @@ const BreakdownTabs: React.FC<{
|
||||
scope: string;
|
||||
pageResetKey?: number;
|
||||
refreshKey?: number;
|
||||
filters: {
|
||||
routes?: FilterOptionType[];
|
||||
users?: FilterOptionType[];
|
||||
api_keys?: FilterOptionType[];
|
||||
};
|
||||
filters: BreakdownFilters;
|
||||
}> = ({ filters, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
const intl = useIntl();
|
||||
const routes = filters.routes || EMPTY_FILTERS;
|
||||
const users = filters.users || EMPTY_FILTERS;
|
||||
const apiKeys = filters.api_keys || EMPTY_FILTERS;
|
||||
|
||||
const items = useMemo(() => {
|
||||
return [
|
||||
@@ -37,7 +27,7 @@ const BreakdownTabs: React.FC<{
|
||||
children: (
|
||||
<ModelsTable
|
||||
key="models"
|
||||
routes={routes}
|
||||
filters={filters}
|
||||
dateRange={dateRange}
|
||||
scope={scope}
|
||||
pageResetKey={pageResetKey}
|
||||
@@ -52,7 +42,7 @@ const BreakdownTabs: React.FC<{
|
||||
children: (
|
||||
<UsersTable
|
||||
key="users"
|
||||
users={users}
|
||||
filters={filters}
|
||||
dateRange={dateRange}
|
||||
scope={scope}
|
||||
pageResetKey={pageResetKey}
|
||||
@@ -67,7 +57,7 @@ const BreakdownTabs: React.FC<{
|
||||
children: (
|
||||
<ApiKeysTable
|
||||
key="api_keys"
|
||||
apiKeys={apiKeys}
|
||||
filters={filters}
|
||||
dateRange={dateRange}
|
||||
scope={scope}
|
||||
pageResetKey={pageResetKey}
|
||||
@@ -81,7 +71,7 @@ const BreakdownTabs: React.FC<{
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [apiKeys, dateRange, routes, pageResetKey, refreshKey, scope, users]);
|
||||
}, [filters, dateRange, pageResetKey, refreshKey, scope]);
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
|
||||
@@ -3,18 +3,18 @@ import PageBox from '@/pages/_components/page-box';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Table } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FilterOptionType } from '../../config/types';
|
||||
import { BreakdownFilters } from '../../config/types';
|
||||
import useAPIKeys from '../../hooks/use-apikeys-columns';
|
||||
import useQueryBreakdownList from '../../services/use-query-breakdown-list';
|
||||
import getBreakdownRowKey from '../../utils/get-breakdown-row-key';
|
||||
|
||||
const APIKeys: React.FC<{
|
||||
apiKeys: FilterOptionType[];
|
||||
filters: BreakdownFilters;
|
||||
dateRange: { start_date: string; end_date: string };
|
||||
scope: string;
|
||||
pageResetKey?: number;
|
||||
refreshKey?: number;
|
||||
}> = ({ apiKeys, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
}> = ({ filters, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { loading, dataSource, fetchData } = useQueryBreakdownList({
|
||||
@@ -69,16 +69,16 @@ const APIKeys: React.FC<{
|
||||
fetchData({
|
||||
...queryParams,
|
||||
group_by: ['api_key'],
|
||||
filters: {
|
||||
api_keys: apiKeys
|
||||
},
|
||||
// Send the full filter set (route / user / api_key), not just the
|
||||
// table's own dimension, so the breakdown matches the trend chart.
|
||||
filters,
|
||||
scope: scope,
|
||||
...dateRange
|
||||
});
|
||||
}, [
|
||||
apiKeys,
|
||||
dateRange.end_date,
|
||||
dateRange.start_date,
|
||||
filters,
|
||||
queryParams.page,
|
||||
queryParams.perPage,
|
||||
queryParams.sort_by,
|
||||
|
||||
@@ -4,18 +4,18 @@ import PageBox from '@/pages/_components/page-box';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Table } from 'antd';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BreakdownItem, FilterOptionType } from '../../config/types';
|
||||
import { BreakdownFilters, BreakdownItem } from '../../config/types';
|
||||
import useModelsColumns from '../../hooks/use-models-columns';
|
||||
import useQueryBreakdownList from '../../services/use-query-breakdown-list';
|
||||
import getBreakdownRowKey from '../../utils/get-breakdown-row-key';
|
||||
|
||||
const Models: React.FC<{
|
||||
routes: FilterOptionType[];
|
||||
filters: BreakdownFilters;
|
||||
dateRange: { start_date: string; end_date: string };
|
||||
scope: string;
|
||||
pageResetKey?: number;
|
||||
refreshKey?: number;
|
||||
}> = ({ routes, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
}> = ({ filters, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { loading, dataSource, fetchData } = useQueryBreakdownList({
|
||||
@@ -33,7 +33,6 @@ const Models: React.FC<{
|
||||
const pendingPageResetRef = useRef(false);
|
||||
|
||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||
console.log('pagination, filters, sorter: ', pagination, filters, sorter);
|
||||
const sort_by =
|
||||
sorter.order === 'descend' ? `-${sorter.field}` : sorter.field;
|
||||
setQueryParams((prev) => ({
|
||||
@@ -72,16 +71,16 @@ const Models: React.FC<{
|
||||
fetchData({
|
||||
...queryParams,
|
||||
group_by: ['route'],
|
||||
filters: {
|
||||
routes
|
||||
},
|
||||
// Send the full filter set (route / user / api_key), not just the
|
||||
// table's own dimension, so the breakdown matches the trend chart.
|
||||
filters,
|
||||
scope: scope,
|
||||
...dateRange
|
||||
});
|
||||
}, [
|
||||
dateRange.end_date,
|
||||
dateRange.start_date,
|
||||
routes,
|
||||
filters,
|
||||
queryParams.page,
|
||||
queryParams.perPage,
|
||||
queryParams.sort_by,
|
||||
|
||||
@@ -3,18 +3,18 @@ import PageBox from '@/pages/_components/page-box';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Table } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FilterOptionType } from '../../config/types';
|
||||
import { BreakdownFilters } from '../../config/types';
|
||||
import useUsersColumns from '../../hooks/use-users-columns';
|
||||
import useQueryBreakdownList from '../../services/use-query-breakdown-list';
|
||||
import getBreakdownRowKey from '../../utils/get-breakdown-row-key';
|
||||
|
||||
const Users: React.FC<{
|
||||
users: FilterOptionType[];
|
||||
filters: BreakdownFilters;
|
||||
dateRange: { start_date: string; end_date: string };
|
||||
scope: string;
|
||||
pageResetKey?: number;
|
||||
refreshKey?: number;
|
||||
}> = ({ users, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
}> = ({ filters, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { loading, dataSource, fetchData } = useQueryBreakdownList({
|
||||
@@ -71,9 +71,9 @@ const Users: React.FC<{
|
||||
fetchData({
|
||||
...queryParams,
|
||||
group_by: ['user'],
|
||||
filters: {
|
||||
users
|
||||
},
|
||||
// Send the full filter set (route / user / api_key), not just the
|
||||
// table's own dimension, so the breakdown matches the trend chart.
|
||||
filters,
|
||||
scope: scope,
|
||||
...dateRange
|
||||
});
|
||||
@@ -81,12 +81,12 @@ const Users: React.FC<{
|
||||
}, [
|
||||
dateRange.end_date,
|
||||
dateRange.start_date,
|
||||
filters,
|
||||
queryParams.page,
|
||||
queryParams.perPage,
|
||||
queryParams.sort_by,
|
||||
refreshKey,
|
||||
scope,
|
||||
users
|
||||
scope
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user