refactor: dashboard usage
This commit is contained in:
+1
-1
@@ -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",
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export const activeModelsAtom = atom<any[]>([]);
|
||||
@@ -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',
|
||||
|
||||
@@ -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 / メモリ',
|
||||
|
||||
@@ -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 / ОЗУ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '已分配显存 / 内存',
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user