refactor: dashboard usage

This commit is contained in:
jialin
2026-05-06 12:27:22 +08:00
committed by jialin
parent 705b33e307
commit 898c7f1be2
23 changed files with 901 additions and 35 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
"@ant-design/icons": "^6.1.0",
"@ant-design/pro-components": "3.1.0-0",
"@braintree/sanitize-url": "^7.1.1",
"@gpustack/core-ui": "^1.0.5",
"@gpustack/core-ui": "^1.0.6",
"@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6",
+4 -4
View File
@@ -18,8 +18,8 @@ dependencies:
specifier: ^7.1.1
version: 7.1.1
'@gpustack/core-ui':
specifier: ^1.0.5
version: 1.0.5(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0)
specifier: ^1.0.6
version: 1.0.6(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0)
'@huggingface/gguf':
specifier: ^0.1.7
version: 0.1.18
@@ -2905,8 +2905,8 @@ packages:
resolution: {integrity: sha512-KWk80UPIzPmUg+P0rKh6TqspRw0G6eux1PuJr+zz47ftMaZ9QDwbGzHZbtzWkl5hgayM/qrKRutllRC7D/vVXQ==, tarball: https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.3.0.tgz}
deprecated: the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package
/@gpustack/core-ui@1.0.5(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0):
resolution: {integrity: sha512-IFFKXi1xIY6WWySasmKlRip/74eX/yb/iNH1iQ1sigzMWEV3j4V3VQvtBOUxsBxXmRViBSRrklIa0KDjtCwneQ==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.5.tgz}
/@gpustack/core-ui@1.0.6(@ant-design/icons@6.1.0)(@monaco-editor/react@4.6.0)(@types/react@18.3.27)(ahooks@3.9.6)(antd-style@3.7.1)(antd@6.3.3)(axios@1.13.2)(echarts@5.6.0)(file-saver@2.0.5)(monaco-editor@0.30.1)(monaco-yaml@4.0.0)(overlayscrollbars-react@0.5.6)(react-dom@18.3.1)(react@18.3.1)(styled-components@6.2.0):
resolution: {integrity: sha512-P7xs4Dh8dUKpN6YO9hNHgZUdEQvqU7TRMw+yJR5/SiIBAn04bhOZWoBMWR84TcSsT9pEt3IkTDIEmtBPtoQVnw==, tarball: https://registry.npmjs.org/@gpustack/core-ui/-/core-ui-1.0.6.tgz}
peerDependencies:
'@ant-design/icons': '>=6.0.0'
'@monaco-editor/react': ^4.6.0
+3
View File
@@ -0,0 +1,3 @@
import { atom } from 'jotai';
export const activeModelsAtom = atom<any[]>([]);
+5
View File
@@ -20,6 +20,11 @@ export default {
'dashboard.tokens': 'Token Usage',
'dashboard.topusers': 'Top Users',
'dashboard.activeModels': 'Active Models',
'dashboard.activeUsers': 'Active Users',
'dashboard.tokenUsageByModel': 'Token Usage by Model',
'dashboard.apiRequestsByModel': 'API Requests by Model',
'dashboard.topTokenUsageByUser': 'Top 10 Token Usage by User',
'dashboard.topTokenUsageByApiKey': 'Top 10 Token Usage by API Key',
'dashboard.runninginstances': 'Running Instances',
'dashboard.activeModels.name': 'Model Name',
'dashboard.allocatevram': 'Allocated VRAM / RAM',
+5
View File
@@ -19,6 +19,11 @@ export default {
'dashboard.tokens': 'トークン使用量',
'dashboard.topusers': 'トップユーザー',
'dashboard.activeModels': 'アクティブなモデル',
'dashboard.activeUsers': 'アクティブユーザー',
'dashboard.tokenUsageByModel': 'モデル別トークン使用量',
'dashboard.apiRequestsByModel': 'モデル別APIリクエスト',
'dashboard.topTokenUsageByUser': 'ユーザー別トークン使用量トップ10',
'dashboard.topTokenUsageByApiKey': 'APIキー別トークン使用量トップ10',
'dashboard.runninginstances': '稼働中のインスタンス',
'dashboard.activeModels.name': 'モデル名',
'dashboard.allocatevram': '割り当て済みVRAM / メモリ',
+5
View File
@@ -19,6 +19,11 @@ export default {
'dashboard.tokens': 'Использование токенов',
'dashboard.topusers': 'Топ пользователей',
'dashboard.activeModels': 'Активные модели',
'dashboard.activeUsers': 'Активные пользователи',
'dashboard.tokenUsageByModel': 'Использование токенов по моделям',
'dashboard.apiRequestsByModel': 'API-запросы по моделям',
'dashboard.topTokenUsageByUser': 'Топ-10 пользователей по токенам',
'dashboard.topTokenUsageByApiKey': 'Топ-10 API-ключей по токенам',
'dashboard.runninginstances': 'Запущенные инстансы',
'dashboard.activeModels.name': 'Название модели',
'dashboard.allocatevram': 'Выделено VRAM / ОЗУ',
+6
View File
@@ -20,6 +20,12 @@ export default {
'dashboard.tokens': 'Token Kullanımı',
'dashboard.topusers': 'En Aktif Kullanıcılar',
'dashboard.activeModels': 'Aktif Modeller',
'dashboard.activeUsers': 'Aktif Kullanıcılar',
'dashboard.tokenUsageByModel': 'Modele Göre Token Kullanımı',
'dashboard.apiRequestsByModel': 'Modele Göre API İstekleri',
'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.runninginstances': 'Çalışan Örnekler',
'dashboard.activeModels.name': 'Model Adı',
'dashboard.allocatevram': 'Ayrılan VRAM / RAM',
+5
View File
@@ -20,6 +20,11 @@ export default {
'dashboard.tokens': 'Token 使用量',
'dashboard.topusers': '用户排行',
'dashboard.activeModels': '活跃模型',
'dashboard.activeUsers': '活跃用户',
'dashboard.tokenUsageByModel': '按模型统计 Token 使用量',
'dashboard.apiRequestsByModel': '按模型统计 API 请求数',
'dashboard.topTokenUsageByUser': '用户 Token 使用量 Top 10',
'dashboard.topTokenUsageByApiKey': 'API 密钥 Token 使用量 Top 10',
'dashboard.activeModels.name': '模型名称',
'dashboard.runninginstances': '运行实例',
'dashboard.allocatevram': '已分配显存 / 内存',
+192
View File
@@ -0,0 +1,192 @@
import { Chart } from '@gpustack/core-ui';
import { formatLargeNumber } from '@gpustack/core-ui/utils';
import { Empty, Spin, theme } from 'antd';
import React, { useMemo, useRef } from 'react';
import { generateCoolColors } from '../bar-chart';
export interface PieChartItem {
name: string;
value: number;
}
interface PieChartProps {
data: PieChartItem[];
height?: number | string;
width?: number | string;
loading?: boolean;
colorOffset?: number;
total?: number;
totalLabel?: string;
}
const PieChart: React.FC<PieChartProps> = ({
data,
height = 300,
width = '100%',
loading = false,
colorOffset = 0,
total,
totalLabel
}) => {
const { token } = theme.useToken();
const chartRef = useRef<{ chart: any } | null>(null);
const colors = useMemo(() => {
const generatedColors = generateCoolColors(data.length + colorOffset);
return generatedColors.slice(colorOffset);
}, [colorOffset, data.length]);
const options = useMemo(
() => ({
color: colors,
tooltip: {
trigger: 'item',
backgroundColor: token.colorBgElevated,
borderColor: 'transparent',
formatter: (params: any) => {
return `<div class="tooltip-wrapper">
<span class="tooltip-item">
<span class="tooltip-item-name">
<span class="tooltip-item-dot" style="border-radius:50%;background-color:${params.color};"></span>
<span class="tooltip-item-title">${params.name}</span>:
</span>
<span class="tooltip-value">${formatLargeNumber(params.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: 180
},
pageTextStyle: { color: token.colorTextTertiary },
pageIconColor: token.colorTextTertiary,
pageIconInactiveColor: token.colorTextDisabled,
data: data.map((item) => item.name),
show: data.length > 0
},
series: [
{
type: 'pie',
radius: ['52%', '72%'],
center: ['34%', '50%'],
avoidLabelOverlap: true,
label: {
show: false
},
emphasis: {
label: {
show: true,
formatter: (params: any) => `${params.percent}%`,
fontSize: 16,
fontWeight: 600,
color: token.colorText
}
},
labelLine: {
show: false
},
data
}
]
}),
[colors, data, token]
);
if (!data.length) {
return (
<div
style={{
width,
height,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{loading ? (
<Spin size="middle" />
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</div>
);
}
const displayTotal = total ?? data.reduce((sum, item) => sum + item.value, 0);
return (
<div style={{ width, height, position: 'relative' }}>
<Chart
ref={chartRef as any}
options={options as any}
height={height}
width={width}
/>
<div
style={{
position: 'absolute',
left: '34%',
top: '50%',
transform: 'translate(-50%, -50%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
textAlign: 'center'
}}
>
<span
style={{
fontSize: 20,
fontWeight: 600,
color: token.colorText,
lineHeight: 1.2
}}
>
{formatLargeNumber(displayTotal)}
</span>
{totalLabel && (
<span
style={{
fontSize: 12,
color: token.colorTextTertiary,
marginTop: 4
}}
>
{totalLabel}
</span>
)}
</div>
{loading && (
<div
style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--ant-color-bg-container)',
opacity: 0.6,
pointerEvents: 'none'
}}
>
<Spin size="middle" />
</div>
)}
</div>
);
};
export default PieChart;
@@ -80,17 +80,17 @@ const Instance: React.FC = () => {
{
key: '1',
label: intl.formatMessage({
id: 'models.instance.params.configured'
id: 'models.form.backend_parameters'
}),
children: renderParams(instanceData?.backend_parameters || [])
},
{
key: '1-1',
label: intl.formatMessage({
id: 'models.instance.params.autoInjected'
}),
children: renderParams(instanceData?.injected_backend_parameters || [])
},
// {
// key: '1-1',
// label: intl.formatMessage({
// id: 'models.instance.params.autoInjected'
// }),
// children: renderParams(instanceData?.injected_backend_parameters || [])
// },
{
key: '3',
label: intl.formatMessage({ id: 'benchmark.detail.kvCache' }),
@@ -1,9 +1,11 @@
import { activeModelsAtom } from '@/atoms/dashboard';
import { modelCategoriesMap } from '@/pages/llmodels/config';
import { convertFileSize } from '@/utils';
import { AutoTooltip, PageTools } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Col, Row, Table } from 'antd';
import { useContext } from 'react';
import { useSetAtom } from 'jotai';
import { useContext, useEffect } from 'react';
import { DashboardContext } from '../config/dashboard-context';
const NACategories = [
modelCategoriesMap.llm,
@@ -14,6 +16,12 @@ const NACategories = [
const ActiveTable = () => {
const intl = useIntl();
const data = useContext(DashboardContext).active_models || [];
const setActiveModels = useSetAtom(activeModelsAtom);
useEffect(() => {
setActiveModels(data);
}, [data, setActiveModels]);
const modelColumns = [
{
title: intl.formatMessage({ id: 'common.table.name' }),
@@ -0,0 +1,157 @@
import { activeModelsAtom } from '@/atoms/dashboard';
import useQueryBreakdownList from '@/pages/usage/services/use-query-breakdown-list';
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
import { formatLargeNumber } from '@/utils';
import { PageTools } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Col, Row } from 'antd';
import dayjs from 'dayjs';
import { useAtomValue } from 'jotai';
import { useContext, useEffect, useMemo } from 'react';
import styled from 'styled-components';
import {
baseColorMap,
DashboardUsageCommonParams,
getUsageSummary
} from '../config';
import { DashboardContext } from '../config/dashboard-context';
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';
const Section = styled.div`
margin-top: 16px;
`;
const NewUsage = () => {
const intl = useIntl();
const activeModelsFromAtom = useAtomValue(activeModelsAtom);
const { active_models: activeModelsFromContext = [] } =
useContext(DashboardContext);
const tokenByModelQuery = useQueryTimeSeriesData({
key: 'tokenUsageByModelData'
});
const activeUsersQuery = useQueryBreakdownList({
key: 'dashboardActiveUsersData'
});
const dateRange = useMemo(
() => ({
start_date: dayjs().subtract(29, 'days').format('YYYY-MM-DD'),
end_date: dayjs().format('YYYY-MM-DD')
}),
[]
);
const commonParams = useMemo<DashboardUsageCommonParams>(
() => ({
...dateRange,
scope: 'all',
granularity: 'day',
filters: {}
}),
[dateRange]
);
const activeModels = activeModelsFromAtom.length
? activeModelsFromAtom
: activeModelsFromContext;
useEffect(() => {
const fetchData = async () => {
await Promise.all([
tokenByModelQuery.fetchData({
...commonParams,
metric: 'total_tokens',
group_by: ['date', 'model'],
sort_by: '-total_tokens'
}),
activeUsersQuery.fetchData({
...dateRange,
scope: 'all',
group_by: ['user'],
page: 1,
perPage: 10,
sort_by: '-total_tokens',
filters: {}
})
]);
};
fetchData().catch(() => {});
}, [commonParams, dateRange]);
const summaryCards = useMemo(() => {
const summary = getUsageSummary(tokenByModelQuery.detailData) || {
total_tokens: 0,
api_requests: 0
};
return [
{
label: formatLargeNumber(summary.total_tokens) as string,
value: intl.formatMessage({ id: 'dashboard.tokens' }),
color: baseColorMap.base
},
{
label: formatLargeNumber(summary.api_requests) as string,
value: intl.formatMessage({ id: 'dashboard.apirequest' }),
color: baseColorMap.baseR1
},
{
label: formatLargeNumber(activeModels.length) as string,
value: intl.formatMessage({ id: 'dashboard.activeModels' }),
color: baseColorMap.baseR2
},
{
label: formatLargeNumber(activeUsersQuery.dataSource.total) as string,
value: intl.formatMessage({ id: 'dashboard.activeUsers' }),
color: baseColorMap.baseR3
}
];
}, [
activeModels.length,
activeUsersQuery.dataSource.total,
intl,
tokenByModelQuery.detailData
]);
return (
<>
<PageTools
style={{ margin: '24px 0 0' }}
left={
<span className="font-700">
{intl.formatMessage({ id: 'dashboard.usage' })}
</span>
}
/>
<Section>
<Row gutter={[20, 20]}>
<Col xs={24} sm={24} md={24} lg={12}>
<TokenUsageByModel
data={tokenByModelQuery.detailData}
loading={tokenByModelQuery.loading}
/>
</Col>
<Col xs={24} sm={24} md={24} lg={12}>
<ApiRequestsByModel commonParams={commonParams} />
</Col>
</Row>
</Section>
<Section>
<Row gutter={[20, 20]}>
<Col xs={24} sm={24} md={24} lg={12}>
<TopTokenUsageByUser commonParams={commonParams} height={450} />
</Col>
<Col xs={24} sm={24} md={24} lg={12}>
<TopTokenUsageByApiKey commonParams={commonParams} height={450} />
</Col>
</Row>
</Section>
</>
);
};
export default NewUsage;
@@ -0,0 +1,63 @@
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', 'model'],
page: 1,
perPage: 10,
sort_by: '-api_requests'
})
.catch(() => {});
}, [commonParams]);
const chartData = useMemo(
() => toUsagePieData(query.detailData, 'model', '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={1}
total={total}
/>
</UsageChartCard>
);
};
export default ApiRequestsByModel;
@@ -0,0 +1,42 @@
import { CardWrapper } from '@gpustack/core-ui';
import { Spin } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { usageChartCardHeight, usageChartHeight } from '../../config';
export const ChartTitle = styled.div`
font-weight: var(--font-weight-bold);
margin-bottom: 12px;
`;
const UsageChartCard: React.FC<{
title: React.ReactNode;
children: React.ReactNode;
height?: number | string;
}> = ({ title, children, height = usageChartCardHeight }) => {
return (
<CardWrapper style={{ height }}>
<ChartTitle>{title}</ChartTitle>
{children}
</CardWrapper>
);
};
export default UsageChartCard;
export const ChartLoadingBox: React.FC<{ height?: number | string }> = ({
height = usageChartHeight
}) => {
return (
<div
style={{
height,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Spin size="middle" />
</div>
);
};
@@ -0,0 +1,48 @@
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, 'model', '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={3}
total={total}
/>
</UsageChartCard>
);
};
export default TokenUsageByModel;
@@ -0,0 +1,76 @@
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
import { HBarChart } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useEffect, useMemo } from 'react';
import {
baseColorMap,
DashboardUsageCommonParams,
toUsageRankData,
usageChartHeight
} from '../../config';
import UsageChartCard, { ChartLoadingBox } from './shared';
interface TopTokenUsageByApiKeyProps {
commonParams: DashboardUsageCommonParams;
height?: number;
}
const TopTokenUsageByApiKey: React.FC<TopTokenUsageByApiKeyProps> = ({
commonParams,
height = usageChartHeight
}) => {
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 (
<UsageChartCard
title={intl.formatMessage({ id: 'dashboard.topTokenUsageByApiKey' })}
height={height + 52}
>
{query.loading && rankData.names.length === 0 ? (
<ChartLoadingBox height={height} />
) : (
<HBarChart
seriesData={rankData.series}
xAxisData={rankData.names}
height={height}
maxItems={10}
/>
)}
</UsageChartCard>
);
};
export default TopTokenUsageByApiKey;
@@ -0,0 +1,71 @@
import useQueryTimeSeriesData from '@/pages/usage/services/use-query-timeseries-data';
import { HBarChart } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { useEffect, useMemo } from 'react';
import {
baseColorMap,
DashboardUsageCommonParams,
toUsageRankData,
usageChartHeight
} from '../../config';
import UsageChartCard, { ChartLoadingBox } from './shared';
interface TopTokenUsageByUserProps {
commonParams: DashboardUsageCommonParams;
height?: number;
}
const TopTokenUsageByUser: React.FC<TopTokenUsageByUserProps> = ({
commonParams,
height = usageChartHeight
}) => {
const intl = useIntl();
const query = useQueryTimeSeriesData({
key: 'topTokenUsageByUserData'
});
const tokenUsageText = intl.formatMessage({ id: 'dashboard.tokens' });
useEffect(() => {
query
.fetchData({
...commonParams,
metric: 'total_tokens',
group_by: ['date', 'user'],
page: 1,
perPage: 10,
sort_by: '-total_tokens'
})
.catch(() => {});
}, [commonParams]);
const rankData = useMemo(
() =>
toUsageRankData(
query.detailData,
'user',
tokenUsageText,
baseColorMap.base
),
[query.detailData, tokenUsageText]
);
return (
<UsageChartCard
title={intl.formatMessage({ id: 'dashboard.topTokenUsageByUser' })}
height={height + 52}
>
{query.loading && rankData.names.length === 0 ? (
<ChartLoadingBox height={height} />
) : (
<HBarChart
seriesData={rankData.series}
xAxisData={rankData.names}
height={height}
maxItems={10}
/>
)}
</UsageChartCard>
);
};
export default TopTokenUsageByUser;
+2 -13
View File
@@ -1,18 +1,7 @@
import breakpoints from '@/config/breakpoints';
import useWindowResize from '@/hooks/use-window-resize';
import { useEffect, useState } from 'react';
import UserInner from './usage-inner';
import NewUsage from './new-usage';
const Usage = () => {
const {
size: { width }
} = useWindowResize();
const [maxWdith, setMaxWidth] = useState<number>(breakpoints.xl);
useEffect(() => {
setMaxWidth(width);
}, [width]);
return <UserInner maxWidth={maxWdith}></UserInner>;
return <NewUsage />;
};
export default Usage;
+189
View File
@@ -1,3 +1,8 @@
import {
BreakdownItem,
UsageBreakdownResponse
} from '@/pages/usage/config/types';
export const overviewConfigs = [
{
key: 'cluster_count',
@@ -35,3 +40,187 @@ export const baseColorMap = {
baseR2: 'rgba(48,0,255,0.8)',
baseR3: 'rgba(85,167,255,0.8)'
};
export type UsageGroupBy = 'model' | 'user' | 'api_key';
export type UsageMetric = 'total_tokens' | 'api_requests';
export interface UsageChartDatum {
name: string;
value: number;
}
export interface DashboardUsageCommonParams {
start_date: string;
end_date: string;
scope: string;
granularity: string;
filters: Record<string, any>;
}
export const usageChartHeight = 300;
export const usageChartCardHeight = usageChartHeight + 52;
export const buildUsageLabel = (item: BreakdownItem, groupBy: UsageGroupBy) => {
const rawItem = item as any;
const groupItem = rawItem[groupBy];
const identityValue = groupItem?.identity?.value;
const groupValue = groupItem?.value;
if (typeof groupItem === 'string') {
return groupItem;
}
if (groupBy === 'model') {
const providerName =
identityValue?.provider_name || groupValue?.provider_name;
const modelName =
identityValue?.model_name ||
groupValue?.model_name ||
rawItem.model_name ||
rawItem.model;
if (providerName && modelName) {
return `${providerName}/${modelName}`;
}
return groupItem?.label || modelName || '-';
}
return (
groupItem?.label ||
identityValue?.user_name ||
identityValue?.api_key_name ||
identityValue?.access_key ||
groupValue?.user_name ||
groupValue?.api_key_name ||
groupValue?.access_key ||
rawItem.user_name ||
rawItem.api_key_name ||
rawItem.access_key ||
rawItem[groupBy] ||
'-'
);
};
export const getUsageResponseItems = (
data: UsageBreakdownResponse | null | undefined
) => {
const response = data as any;
return response?.items || response?.data?.items || [];
};
export const getUsageResponseSeries = (
data: UsageBreakdownResponse | null | undefined
) => {
const response = data as any;
return response?.series || response?.data?.series || [];
};
export const getUsageMetricValue = (item: any, metric: UsageMetric) => {
return Number(
item?.[metric] ??
item?.metrics?.[metric] ??
item?.summary?.[metric] ??
item?.value ??
0
);
};
export const aggregateUsageByGroup = (
data: UsageBreakdownResponse | null | undefined,
groupBy: UsageGroupBy,
metric: UsageMetric
): UsageChartDatum[] => {
const itemMap = new Map<string, number>();
const items = getUsageResponseItems(data);
items.forEach((item: BreakdownItem) => {
const name = buildUsageLabel(item, groupBy);
itemMap.set(
name,
(itemMap.get(name) || 0) + getUsageMetricValue(item, metric)
);
});
if (!items.length) {
getUsageResponseSeries(data).forEach((item: any) => {
const name = item.label || buildUsageLabel(item, groupBy);
const value = (item.timeline || []).reduce(
(sum: number, point: any) => {
return sum + getUsageMetricValue(point, metric);
},
getUsageMetricValue(item, metric)
);
itemMap.set(name, (itemMap.get(name) || 0) + value);
});
}
if (!itemMap.size) {
return [];
}
const hasPositiveValue = Array.from(itemMap.values()).some(
(value) => value > 0
);
if (!hasPositiveValue) {
return [];
}
return Array.from(itemMap.entries())
.filter(([, value]) => value > 0)
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value)
.slice(0, 10);
};
export const getUsageSummaryData = (
dataList: Array<UsageBreakdownResponse | null | undefined>
) => {
return dataList.find((item) => {
const summary = item?.summary || (item as any)?.data?.summary;
return summary;
});
};
export const getUsageSummary = (
data: UsageBreakdownResponse | null | undefined
) => {
return data?.summary || (data as any)?.data?.summary;
};
export const toUsagePieData = (
data: UsageBreakdownResponse | null | undefined,
groupBy: UsageGroupBy,
metric: UsageMetric
): UsageChartDatum[] => {
return aggregateUsageByGroup(data, groupBy, metric);
};
export const toUsageRankData = (
data: UsageBreakdownResponse | null | undefined,
groupBy: UsageGroupBy,
seriesName: string,
color: string
) => {
const items = aggregateUsageByGroup(data, groupBy, 'total_tokens');
const names = items.map((item) => item.name);
return {
names,
series: [
{
name: seriesName,
color,
data: items.map((item) => ({
name: item.name,
value: item.value,
itemStyle: {
borderRadius: [2, 2, 2, 2]
}
}))
}
]
};
};
@@ -95,7 +95,7 @@ const InjectedParamsCell: React.FC<InjectedParamsCellProps> = ({
if (!injectedBackendParameters.length) {
return null;
}
return null;
return (
<Tooltip
color="var(--ant-color-bg-elevated)"
@@ -2,7 +2,6 @@
import { tableSorter } from '@/config/settings';
import { ListItem as workerListItem } from '@/pages/resources/config/types';
import { convertFileSize } from '@/utils';
import { ThunderboltFilled } from '@ant-design/icons';
import { AutoTooltip, IconFont } from '@gpustack/core-ui';
import { useIntl } from '@umijs/max';
import { Flex } from 'antd';
@@ -107,7 +106,7 @@ const useInstancesColumns = (options: {
<>
<Flex gap={8} wrap="wrap">
<NameCell
showWorkerInfo={false}
showWorkerInfo={true}
record={record}
modelData={{
backend: record.backend,
@@ -121,7 +120,7 @@ const useInstancesColumns = (options: {
></NameCell>
<InjectedParamsCell record={record}></InjectedParamsCell>
</Flex>
<div className="flex-center">
{/* <div className="flex-center">
<ThunderboltFilled
className="m-r-5 text-quaternary"
style={{ fontSize: 12, position: 'relative', top: 2 }}
@@ -132,7 +131,7 @@ const useInstancesColumns = (options: {
? `(${record.backend_version || record?.backend_version})`
: ''}
</span>
</div>
</div> */}
</>
)
},
@@ -153,7 +152,7 @@ const useInstancesColumns = (options: {
dataIndex: 'worker_id',
render: (text, record) => (
<div className="flex-center gap-8">
<AutoTooltip ghost>{renderWorkerCell(text, record)}</AutoTooltip>
<AutoTooltip ghost>{record.worker_name}</AutoTooltip>
<DistributeInfoCell
record={record}
workerList={workerList}
+3
View File
@@ -30,6 +30,9 @@ export async function queryUsageTimeSeriesData(
metric: string;
group_by: string[];
granularity: string;
page?: number;
perPage?: number;
sort_by?: string;
filters: {
models?: FilterOptionType[];
users?: FilterOptionType[];
@@ -2,11 +2,11 @@ import { useQueryData } from '@/hooks/use-query-data-list';
import { queryUsageTimeSeriesData } from '../apis';
import { UsageBreakdownResponse } from '../config/types';
export default function useQueryTimeSeriesData() {
export default function useQueryTimeSeriesData(option?: { key?: string }) {
const { detailData, loading, cancelRequest, fetchData } =
useQueryData<UsageBreakdownResponse>({
fetchDetail: queryUsageTimeSeriesData,
key: 'timeSeriesData'
key: option?.key || 'timeSeriesData'
});
return {
detailData,