refactor(dashboard): collapse usage section into a single row
Merge the two "by Model" pies (Token Usage + API Requests) into one
dual-pie card with a shared right-side vertical legend and per-donut
center totals, drop the standalone API-Key Top 10 chart, and put the
combined chart side-by-side with the User Top 10 ranking in one row
with the same 16/8 column ratio used by the System Load section above.
* New component usage-charts/usage-by-model.tsx owns both metric queries
(group_by=['date','route']), aggregates a shared route→color map so
the legend toggles both donuts together, and renders two pies at
centers ('20%','50%') and ('60%','50%') with radius ['50%','70%'].
* new-usage.tsx no longer mounts ApiRequestsByModel / TokenUsageByModel
/ TopTokenUsageByApiKey; layout collapses to two columns sized
(lg=24 xl=16) and (lg=24 xl=8).
* Delete unused token-usage-by-model.tsx, api-requests-by-model.tsx,
top-token-usage-by-api-key.tsx, and the use-top-token-usage-by-api-key
hook.
* Strip the now-unreferenced dashboard.tokenUsageByModel /
apiRequestsByModel / topTokenUsageByApiKey locale keys across 5
locales, add the new dashboard.usageByModel title; locale parity
check passes.
This commit is contained in:
@@ -14,10 +14,8 @@ export default {
|
|||||||
'dashboard.tokens': 'Token Usage',
|
'dashboard.tokens': 'Token Usage',
|
||||||
'dashboard.topusers': 'Top Users',
|
'dashboard.topusers': 'Top Users',
|
||||||
'dashboard.activeDeployments': 'Active Deployments',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.tokenUsageByModel': 'Token Usage by Model',
|
'dashboard.usageByModel': 'Usage by Model',
|
||||||
'dashboard.apiRequestsByModel': 'API Requests by Model',
|
|
||||||
'dashboard.topTokenUsageByUser': 'Top 10 Token Usage by User',
|
'dashboard.topTokenUsageByUser': 'Top 10 Token Usage by User',
|
||||||
'dashboard.topTokenUsageByApiKey': 'Top 10 Token Usage by API Key',
|
|
||||||
'dashboard.allocatevram': 'Allocated VRAM / RAM',
|
'dashboard.allocatevram': 'Allocated VRAM / RAM',
|
||||||
'dashboard.usage.selectuser': 'Select users',
|
'dashboard.usage.selectuser': 'Select users',
|
||||||
'dashboard.usage.selectmodel': 'Select models',
|
'dashboard.usage.selectmodel': 'Select models',
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ export default {
|
|||||||
'dashboard.tokens': 'トークン使用量',
|
'dashboard.tokens': 'トークン使用量',
|
||||||
'dashboard.topusers': 'トップユーザー',
|
'dashboard.topusers': 'トップユーザー',
|
||||||
'dashboard.activeDeployments': 'Active Deployments',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.tokenUsageByModel': 'モデル別トークン使用量',
|
'dashboard.usageByModel': 'モデル別使用量',
|
||||||
'dashboard.apiRequestsByModel': 'モデル別APIリクエスト',
|
|
||||||
'dashboard.topTokenUsageByUser': 'ユーザー別トークン使用量トップ10',
|
'dashboard.topTokenUsageByUser': 'ユーザー別トークン使用量トップ10',
|
||||||
'dashboard.topTokenUsageByApiKey': 'APIキー別トークン使用量トップ10',
|
|
||||||
'dashboard.allocatevram': '割り当て済みVRAM / メモリ',
|
'dashboard.allocatevram': '割り当て済みVRAM / メモリ',
|
||||||
'dashboard.usage.selectuser': 'Select users',
|
'dashboard.usage.selectuser': 'Select users',
|
||||||
'dashboard.usage.selectmodel': 'Select models',
|
'dashboard.usage.selectmodel': 'Select models',
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ export default {
|
|||||||
'dashboard.tokens': 'Использование токенов',
|
'dashboard.tokens': 'Использование токенов',
|
||||||
'dashboard.topusers': 'Топ пользователей',
|
'dashboard.topusers': 'Топ пользователей',
|
||||||
'dashboard.activeDeployments': 'Active Deployments',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.tokenUsageByModel': 'Использование токенов по моделям',
|
'dashboard.usageByModel': 'Использование по моделям',
|
||||||
'dashboard.apiRequestsByModel': 'API-запросы по моделям',
|
|
||||||
'dashboard.topTokenUsageByUser': 'Топ-10 пользователей по токенам',
|
'dashboard.topTokenUsageByUser': 'Топ-10 пользователей по токенам',
|
||||||
'dashboard.topTokenUsageByApiKey': 'Топ-10 API-ключей по токенам',
|
|
||||||
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ',
|
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ',
|
||||||
'dashboard.usage.selectuser': 'Выбрать пользователей',
|
'dashboard.usage.selectuser': 'Выбрать пользователей',
|
||||||
'dashboard.usage.selectmodel': 'Выбрать модели',
|
'dashboard.usage.selectmodel': 'Выбрать модели',
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ export default {
|
|||||||
'dashboard.tokens': 'Token Kullanımı',
|
'dashboard.tokens': 'Token Kullanımı',
|
||||||
'dashboard.topusers': 'En Aktif Kullanıcılar',
|
'dashboard.topusers': 'En Aktif Kullanıcılar',
|
||||||
'dashboard.activeDeployments': 'Active Deployments',
|
'dashboard.activeDeployments': 'Active Deployments',
|
||||||
'dashboard.tokenUsageByModel': 'Modele Göre Token Kullanımı',
|
'dashboard.usageByModel': 'Modele Göre Kullanım',
|
||||||
'dashboard.apiRequestsByModel': 'Modele Göre API İstekleri',
|
|
||||||
'dashboard.topTokenUsageByUser': 'Kullanıcıya Göre İlk 10 Token Kullanımı',
|
'dashboard.topTokenUsageByUser': 'Kullanıcıya Göre İlk 10 Token Kullanımı',
|
||||||
'dashboard.topTokenUsageByApiKey':
|
|
||||||
'API Anahtarına Göre İlk 10 Token Kullanımı',
|
|
||||||
'dashboard.allocatevram': 'Ayrılan VRAM / RAM',
|
'dashboard.allocatevram': 'Ayrılan VRAM / RAM',
|
||||||
'dashboard.usage.selectuser': 'Kullanıcı seçin',
|
'dashboard.usage.selectuser': 'Kullanıcı seçin',
|
||||||
'dashboard.usage.selectmodel': 'Model seçin',
|
'dashboard.usage.selectmodel': 'Model seçin',
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ export default {
|
|||||||
'dashboard.tokens': 'Token 使用量',
|
'dashboard.tokens': 'Token 使用量',
|
||||||
'dashboard.topusers': '用户排行',
|
'dashboard.topusers': '用户排行',
|
||||||
'dashboard.activeDeployments': '活跃部署',
|
'dashboard.activeDeployments': '活跃部署',
|
||||||
'dashboard.tokenUsageByModel': '按模型统计 Token 使用量',
|
'dashboard.usageByModel': '按模型统计使用量',
|
||||||
'dashboard.apiRequestsByModel': '按模型统计 API 请求数',
|
|
||||||
'dashboard.topTokenUsageByUser': '用户 Token 使用量 Top 10',
|
'dashboard.topTokenUsageByUser': '用户 Token 使用量 Top 10',
|
||||||
'dashboard.topTokenUsageByApiKey': 'API 密钥 Token 使用量 Top 10',
|
|
||||||
'dashboard.allocatevram': '已分配显存 / 内存',
|
'dashboard.allocatevram': '已分配显存 / 内存',
|
||||||
'dashboard.usage.selectuser': '选择用户',
|
'dashboard.usage.selectuser': '选择用户',
|
||||||
'dashboard.usage.selectmodel': '选择模型',
|
'dashboard.usage.selectmodel': '选择模型',
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
|
||||||
import { PageTools } from '@gpustack/core-ui';
|
import { PageTools } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Col, Row } from 'antd';
|
import { Col, Row } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
DashboardUsageCommonParams,
|
DashboardUsageCommonParams,
|
||||||
getUsageRankHeight,
|
getUsageRankHeight,
|
||||||
getUsageRankSlotCount
|
getUsageRankSlotCount
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import useTopTokenUsageByApiKey from '../hooks/use-top-token-usage-by-api-key';
|
|
||||||
import useTopTokenUsageByUser from '../hooks/use-top-token-usage-by-user';
|
import useTopTokenUsageByUser from '../hooks/use-top-token-usage-by-user';
|
||||||
import ApiRequestsByModel from './usage-charts/api-requests-by-model';
|
|
||||||
import TokenUsageByModel from './usage-charts/token-usage-by-model';
|
|
||||||
import TopTokenUsageByApiKey from './usage-charts/top-token-usage-by-api-key';
|
|
||||||
import TopTokenUsageByUser from './usage-charts/top-token-usage-by-user';
|
import TopTokenUsageByUser from './usage-charts/top-token-usage-by-user';
|
||||||
|
import UsageByModel from './usage-charts/usage-by-model';
|
||||||
|
|
||||||
const Section = styled.div`
|
const Section = styled.div`
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -23,9 +19,6 @@ const Section = styled.div`
|
|||||||
|
|
||||||
const NewUsage = () => {
|
const NewUsage = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const tokenByModelQuery = useQueryTimeSeriesData({
|
|
||||||
key: 'tokenUsageByModelData'
|
|
||||||
});
|
|
||||||
|
|
||||||
const dateRange = useMemo(
|
const dateRange = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -45,37 +38,11 @@ const NewUsage = () => {
|
|||||||
[dateRange]
|
[dateRange]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
await Promise.all([
|
|
||||||
tokenByModelQuery.fetchData({
|
|
||||||
...commonParams,
|
|
||||||
metric: 'total_tokens',
|
|
||||||
group_by: ['date', 'route'],
|
|
||||||
sort_by: '-total_tokens'
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData().catch(() => {});
|
|
||||||
}, [commonParams, dateRange]);
|
|
||||||
|
|
||||||
const userUsage = useTopTokenUsageByUser(commonParams);
|
const userUsage = useTopTokenUsageByUser(commonParams);
|
||||||
const apiKeyUsage = useTopTokenUsageByApiKey(commonParams);
|
|
||||||
|
|
||||||
const userCount = userUsage.rankData.names.length;
|
const userCount = userUsage.rankData.names.length;
|
||||||
const apiKeyCount = apiKeyUsage.rankData.names.length;
|
const rankHeight = getUsageRankHeight(userCount);
|
||||||
|
const rankMaxItems = Math.max(getUsageRankSlotCount(rankHeight), userCount);
|
||||||
const rankHeight = Math.max(
|
|
||||||
getUsageRankHeight(userCount),
|
|
||||||
getUsageRankHeight(apiKeyCount)
|
|
||||||
);
|
|
||||||
|
|
||||||
const rankMaxItems = Math.max(
|
|
||||||
getUsageRankSlotCount(rankHeight),
|
|
||||||
userCount,
|
|
||||||
apiKeyCount
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -89,20 +56,10 @@ const NewUsage = () => {
|
|||||||
/>
|
/>
|
||||||
<Section>
|
<Section>
|
||||||
<Row gutter={[20, 20]}>
|
<Row gutter={[20, 20]}>
|
||||||
<Col xs={24} sm={24} md={24} lg={12}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={16}>
|
||||||
<TokenUsageByModel
|
<UsageByModel commonParams={commonParams} height={rankHeight} />
|
||||||
data={tokenByModelQuery.detailData}
|
|
||||||
loading={tokenByModelQuery.loading}
|
|
||||||
/>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={12}>
|
<Col xs={24} sm={24} md={24} lg={24} xl={8}>
|
||||||
<ApiRequestsByModel commonParams={commonParams} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Section>
|
|
||||||
<Section>
|
|
||||||
<Row gutter={[20, 20]}>
|
|
||||||
<Col xs={24} sm={24} md={24} lg={12}>
|
|
||||||
<TopTokenUsageByUser
|
<TopTokenUsageByUser
|
||||||
rankData={userUsage.rankData}
|
rankData={userUsage.rankData}
|
||||||
loading={userUsage.loading}
|
loading={userUsage.loading}
|
||||||
@@ -110,14 +67,6 @@ const NewUsage = () => {
|
|||||||
maxItems={rankMaxItems}
|
maxItems={rankMaxItems}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={24} md={24} lg={12}>
|
|
||||||
<TopTokenUsageByApiKey
|
|
||||||
rankData={apiKeyUsage.rankData}
|
|
||||||
loading={apiKeyUsage.loading}
|
|
||||||
height={rankHeight}
|
|
||||||
maxItems={rankMaxItems}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import PieChart from '@/pages/_components/pie-chart';
|
|
||||||
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
DashboardUsageCommonParams,
|
|
||||||
getUsageSummary,
|
|
||||||
toUsagePieData,
|
|
||||||
usageChartHeight
|
|
||||||
} from '../../config';
|
|
||||||
import UsageChartCard from './shared';
|
|
||||||
|
|
||||||
interface ApiRequestsByModelProps {
|
|
||||||
commonParams: DashboardUsageCommonParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ApiRequestsByModel: React.FC<ApiRequestsByModelProps> = ({
|
|
||||||
commonParams
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const query = useQueryTimeSeriesData({
|
|
||||||
key: 'apiRequestsByModelData'
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
query
|
|
||||||
.fetchData({
|
|
||||||
...commonParams,
|
|
||||||
metric: 'api_requests',
|
|
||||||
group_by: ['date', 'route'],
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
sort_by: '-api_requests'
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, [commonParams]);
|
|
||||||
|
|
||||||
const chartData = useMemo(
|
|
||||||
() => toUsagePieData(query.detailData, 'route', 'api_requests'),
|
|
||||||
[query.detailData]
|
|
||||||
);
|
|
||||||
|
|
||||||
const total = useMemo(() => {
|
|
||||||
const summary = getUsageSummary(query.detailData);
|
|
||||||
return Number(summary?.api_requests ?? 0);
|
|
||||||
}, [query.detailData]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UsageChartCard
|
|
||||||
title={intl.formatMessage({ id: 'dashboard.apiRequestsByModel' })}
|
|
||||||
>
|
|
||||||
<PieChart
|
|
||||||
data={chartData}
|
|
||||||
height={usageChartHeight}
|
|
||||||
loading={query.loading}
|
|
||||||
colorOffset={5}
|
|
||||||
total={total}
|
|
||||||
/>
|
|
||||||
</UsageChartCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiRequestsByModel;
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import PieChart from '@/pages/_components/pie-chart';
|
|
||||||
import { UsageBreakdownResponse } from '@/pages/usage/config/types';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
getUsageSummary,
|
|
||||||
toUsagePieData,
|
|
||||||
usageChartHeight
|
|
||||||
} from '../../config';
|
|
||||||
import UsageChartCard from './shared';
|
|
||||||
|
|
||||||
interface TokenUsageByModelProps {
|
|
||||||
data: UsageBreakdownResponse | null | undefined;
|
|
||||||
loading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TokenUsageByModel: React.FC<TokenUsageByModelProps> = ({
|
|
||||||
data,
|
|
||||||
loading = false
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const chartData = useMemo(
|
|
||||||
() => toUsagePieData(data, 'route', 'total_tokens'),
|
|
||||||
[data]
|
|
||||||
);
|
|
||||||
|
|
||||||
const total = useMemo(() => {
|
|
||||||
const summary = getUsageSummary(data);
|
|
||||||
return Number(summary?.total_tokens ?? 0);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UsageChartCard
|
|
||||||
title={intl.formatMessage({ id: 'dashboard.tokenUsageByModel' })}
|
|
||||||
>
|
|
||||||
<PieChart
|
|
||||||
data={chartData}
|
|
||||||
height={usageChartHeight}
|
|
||||||
loading={loading}
|
|
||||||
colorOffset={2}
|
|
||||||
total={total}
|
|
||||||
/>
|
|
||||||
</UsageChartCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TokenUsageByModel;
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { HBarChart } from '@gpustack/core-ui';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { usageChartHeight, usageRankMaxItems } from '../../config';
|
|
||||||
import type useTopTokenUsageByApiKey from '../../hooks/use-top-token-usage-by-api-key';
|
|
||||||
import UsageChartCard, { ChartLoadingBox } from './shared';
|
|
||||||
|
|
||||||
type RankData = ReturnType<typeof useTopTokenUsageByApiKey>['rankData'];
|
|
||||||
|
|
||||||
interface TopTokenUsageByApiKeyProps {
|
|
||||||
rankData: RankData;
|
|
||||||
loading: boolean;
|
|
||||||
height?: number;
|
|
||||||
maxItems?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TopTokenUsageByApiKey: React.FC<TopTokenUsageByApiKeyProps> = ({
|
|
||||||
rankData,
|
|
||||||
loading,
|
|
||||||
height = usageChartHeight,
|
|
||||||
maxItems = usageRankMaxItems
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UsageChartCard
|
|
||||||
title={intl.formatMessage({ id: 'dashboard.topTokenUsageByApiKey' })}
|
|
||||||
height={height + 52}
|
|
||||||
>
|
|
||||||
{loading && rankData.names.length === 0 ? (
|
|
||||||
<ChartLoadingBox height={height} />
|
|
||||||
) : (
|
|
||||||
<HBarChart
|
|
||||||
seriesData={rankData.series}
|
|
||||||
xAxisData={rankData.names}
|
|
||||||
height={height}
|
|
||||||
maxItems={maxItems}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</UsageChartCard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TopTokenUsageByApiKey;
|
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
import useCoolColors from '@/hooks/use-cool-colors';
|
||||||
|
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
||||||
|
import { Chart } from '@gpustack/core-ui';
|
||||||
|
import { formatLargeNumber } from '@gpustack/core-ui/utils';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import type { GlobalToken } from 'antd';
|
||||||
|
import { Empty, Spin, theme } from 'antd';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
DashboardUsageCommonParams,
|
||||||
|
getUsageSummary,
|
||||||
|
toUsagePieData,
|
||||||
|
UsageChartDatum,
|
||||||
|
usageChartHeight
|
||||||
|
} from '../../config';
|
||||||
|
import UsageChartCard from './shared';
|
||||||
|
|
||||||
|
const HTML_ESCAPES: Record<string, string> = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tooltip strings come from route names (user-controlled via the API) and
|
||||||
|
// translated metric labels, so they must be escaped before being interpolated
|
||||||
|
// into raw HTML for ECharts' tooltip formatter.
|
||||||
|
const escapeHtml = (value: unknown): string =>
|
||||||
|
String(value ?? '').replace(/[&<>"']/g, (c) => HTML_ESCAPES[c] ?? c);
|
||||||
|
|
||||||
|
interface UsageByModelProps {
|
||||||
|
commonParams: DashboardUsageCommonParams;
|
||||||
|
height?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UsageByModel: React.FC<UsageByModelProps> = ({
|
||||||
|
commonParams,
|
||||||
|
height = usageChartHeight
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const generateCoolColors = useCoolColors();
|
||||||
|
|
||||||
|
const tokenQuery = useQueryTimeSeriesData({ key: 'tokenUsageByModelData' });
|
||||||
|
const apiQuery = useQueryTimeSeriesData({ key: 'apiRequestsByModelData' });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
tokenQuery
|
||||||
|
.fetchData({
|
||||||
|
...commonParams,
|
||||||
|
metric: 'total_tokens',
|
||||||
|
group_by: ['date', 'route'],
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
sort_by: '-total_tokens'
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
apiQuery
|
||||||
|
.fetchData({
|
||||||
|
...commonParams,
|
||||||
|
metric: 'api_requests',
|
||||||
|
group_by: ['date', 'route'],
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
sort_by: '-api_requests'
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [commonParams]);
|
||||||
|
|
||||||
|
const tokenData = useMemo<UsageChartDatum[]>(
|
||||||
|
() => toUsagePieData(tokenQuery.detailData, 'route', 'total_tokens'),
|
||||||
|
[tokenQuery.detailData]
|
||||||
|
);
|
||||||
|
const apiData = useMemo<UsageChartDatum[]>(
|
||||||
|
() => toUsagePieData(apiQuery.detailData, 'route', 'api_requests'),
|
||||||
|
[apiQuery.detailData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const tokenTotal = useMemo(() => {
|
||||||
|
const summary = getUsageSummary(tokenQuery.detailData);
|
||||||
|
return Number(summary?.total_tokens ?? 0);
|
||||||
|
}, [tokenQuery.detailData]);
|
||||||
|
|
||||||
|
const apiTotal = useMemo(() => {
|
||||||
|
const summary = getUsageSummary(apiQuery.detailData);
|
||||||
|
return Number(summary?.api_requests ?? 0);
|
||||||
|
}, [apiQuery.detailData]);
|
||||||
|
|
||||||
|
// Build a shared name->color map so legend toggles both donuts and
|
||||||
|
// matching slices read the same color across the pair.
|
||||||
|
const { legendNames, colorMap } = useMemo(() => {
|
||||||
|
const names: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
[...tokenData, ...apiData].forEach((d) => {
|
||||||
|
if (!seen.has(d.name)) {
|
||||||
|
seen.add(d.name);
|
||||||
|
names.push(d.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const colors = generateCoolColors(names.length);
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
names.forEach((name, i) => {
|
||||||
|
map[name] = colors[i];
|
||||||
|
});
|
||||||
|
return { legendNames: names, colorMap: map };
|
||||||
|
}, [tokenData, apiData, generateCoolColors]);
|
||||||
|
|
||||||
|
const tokenLabel = intl.formatMessage({ id: 'usage.filter.totalTokens' });
|
||||||
|
const apiLabel = intl.formatMessage({ id: 'usage.filter.apiRequests' });
|
||||||
|
|
||||||
|
const isEmpty = tokenData.length === 0 && apiData.length === 0;
|
||||||
|
const isLoading = tokenQuery.loading || apiQuery.loading;
|
||||||
|
|
||||||
|
const options = useMemo(() => {
|
||||||
|
const paintData = (rows: UsageChartDatum[]) =>
|
||||||
|
rows.map((row) => ({
|
||||||
|
name: row.name,
|
||||||
|
value: row.value,
|
||||||
|
itemStyle: { color: colorMap[row.name] }
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'item',
|
||||||
|
backgroundColor: token.colorBgElevated,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const seriesName = escapeHtml(params.seriesName);
|
||||||
|
const name = escapeHtml(params.name);
|
||||||
|
const value = escapeHtml(formatLargeNumber(params.value));
|
||||||
|
return `<div class="tooltip-wrapper">
|
||||||
|
<span class="tooltip-item">
|
||||||
|
<span class="tooltip-item-name">
|
||||||
|
${params.marker}
|
||||||
|
<span class="tooltip-item-title">${seriesName} · ${name}</span>:
|
||||||
|
</span>
|
||||||
|
<span class="tooltip-value">${value}</span>
|
||||||
|
</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
type: 'scroll',
|
||||||
|
orient: 'vertical',
|
||||||
|
right: 0,
|
||||||
|
top: 18,
|
||||||
|
bottom: 18,
|
||||||
|
itemWidth: 8,
|
||||||
|
itemHeight: 8,
|
||||||
|
itemGap: 12,
|
||||||
|
textStyle: {
|
||||||
|
color: token.colorTextTertiary,
|
||||||
|
overflow: 'truncate',
|
||||||
|
width: 140
|
||||||
|
},
|
||||||
|
pageTextStyle: { color: token.colorTextTertiary },
|
||||||
|
pageIconColor: token.colorTextTertiary,
|
||||||
|
pageIconInactiveColor: token.colorTextDisabled,
|
||||||
|
data: legendNames,
|
||||||
|
show: legendNames.length > 0
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: tokenLabel,
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['50%', '70%'],
|
||||||
|
center: ['20%', '50%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
label: { show: false },
|
||||||
|
labelLine: { show: false },
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: (params: any) => `${params.percent}%`,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: token.colorText
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: paintData(tokenData)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: apiLabel,
|
||||||
|
type: 'pie',
|
||||||
|
radius: ['50%', '70%'],
|
||||||
|
center: ['60%', '50%'],
|
||||||
|
avoidLabelOverlap: true,
|
||||||
|
label: { show: false },
|
||||||
|
labelLine: { show: false },
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
formatter: (params: any) => `${params.percent}%`,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: token.colorText
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: paintData(apiData)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}, [tokenData, apiData, colorMap, legendNames, token, tokenLabel, apiLabel]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UsageChartCard
|
||||||
|
title={intl.formatMessage({ id: 'dashboard.usageByModel' })}
|
||||||
|
height={height + 52}
|
||||||
|
>
|
||||||
|
<div style={{ position: 'relative', width: '100%', height }}>
|
||||||
|
{isEmpty && !isLoading ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Chart options={options as any} height={height} width="100%" />
|
||||||
|
<DonutCenter
|
||||||
|
left="20%"
|
||||||
|
top="50%"
|
||||||
|
total={tokenTotal}
|
||||||
|
label={tokenLabel}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
<DonutCenter
|
||||||
|
left="60%"
|
||||||
|
top="50%"
|
||||||
|
total={apiTotal}
|
||||||
|
label={apiLabel}
|
||||||
|
token={token}
|
||||||
|
/>
|
||||||
|
{isLoading && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
opacity: 0.6,
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spin size="middle" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</UsageChartCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DonutCenter: React.FC<{
|
||||||
|
left: string;
|
||||||
|
top: string;
|
||||||
|
total: number;
|
||||||
|
label: string;
|
||||||
|
token: GlobalToken;
|
||||||
|
}> = ({ left, top, total, label, token }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: token.colorText,
|
||||||
|
lineHeight: 1.2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatLargeNumber(total)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: token.colorTextTertiary,
|
||||||
|
marginTop: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UsageByModel;
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
|
|
||||||
import { useIntl } from '@umijs/max';
|
|
||||||
import { useEffect, useMemo } from 'react';
|
|
||||||
import {
|
|
||||||
baseColorMap,
|
|
||||||
DashboardUsageCommonParams,
|
|
||||||
toUsageRankData
|
|
||||||
} from '../config';
|
|
||||||
|
|
||||||
export default function useTopTokenUsageByApiKey(
|
|
||||||
commonParams: DashboardUsageCommonParams
|
|
||||||
) {
|
|
||||||
const intl = useIntl();
|
|
||||||
const query = useQueryTimeSeriesData({
|
|
||||||
key: 'topTokenUsageByApiKeyData'
|
|
||||||
});
|
|
||||||
const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
query
|
|
||||||
.fetchData({
|
|
||||||
...commonParams,
|
|
||||||
metric: 'total_tokens',
|
|
||||||
group_by: ['date', 'api_key'],
|
|
||||||
page: 1,
|
|
||||||
perPage: 10,
|
|
||||||
sort_by: '-total_tokens'
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}, [commonParams]);
|
|
||||||
|
|
||||||
const rankData = useMemo(() => {
|
|
||||||
const data = toUsageRankData(
|
|
||||||
query.detailData,
|
|
||||||
'api_key',
|
|
||||||
tokenUsageText,
|
|
||||||
baseColorMap.baseR3
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
names: data.names.filter((name) => name !== '-'),
|
|
||||||
series: data.series.map((item) => ({
|
|
||||||
...item,
|
|
||||||
data: item.data.filter((point) => point.name !== '-')
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}, [query.detailData, tokenUsageText]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
rankData,
|
|
||||||
loading: query.loading
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user