Files
gpustack-ui/src/pages/usage/components/token-tab.tsx
T
micheliaandmichela feng 00008abf03 feat(usage): add the tabbed Usage page
A Tabs shell hosting Tokens / GPU Instances / Storage / Summary /
Resource Events:
- token-tab: the original token view, wrapped as a tab.
- gpu-instances-tab & storage-tab: per-instance / per-volume breakdowns
  with KPI cards, a metric+granularity trend chart, and grouped tables.
- summary-tab: three-domain (Tokens / Compute / Storage) overview with
  headline stats, donut, and trend.
- resource-events: the lifecycle audit log (Started / Stopped).
- resource-filter-bar + metric-chart-card: shared filter / chart controls
  mirroring the Tokens tab.
- resource-export-data: a preview modal (filter + paginated preview, then
  download) backing the Export Chart Data / Export Table Data actions.
2026-06-03 17:10:51 +08:00

245 lines
7.3 KiB
TypeScript

import { baseColorMap } from '@/pages/dashboard/config';
import { formatLargeNumber } from '@/utils';
import { SimpleCard } from '@gpustack/core-ui';
import { useAccess, useIntl } from '@umijs/max';
import React, { useEffect, useMemo, useState } from 'react';
import useExportTable from '../hooks/use-export-table';
import { useUsageFilters } from '../hooks/use-usage-filters';
import useQueryUsageMetaData from '../services/use-query-meta-data';
import BreakdownTabs from './breakdown-tabs';
import DailyUsage from './daily-usage';
import ExportData from './export-data';
import FilterBar from './filter-bar';
type DateType = 'date' | 'week' | 'month' | 'quarter' | 'year';
/**
* Token Usage tab — the original ``/usage`` page wrapped so the parent
* route can host it inside a ``Tabs`` shell alongside Resource / GPU
* Instances / Storage / Summary panes.
*
* Body identical to the previous ``pages/usage/index.tsx``; only the
* relative paths (``./components/...`` → ``./`` / ``./hooks`` →
* ``../hooks`` / ``./services`` → ``../services``) were rewritten.
*/
const TokenTab: React.FC = () => {
const intl = useIntl();
const access = useAccess();
const { exportTable } = useExportTable();
const [openExportModal, setOpenExportModal] = useState(false);
const [breakdownRefreshKey, setBreakdownRefreshKey] = useState(0);
const [breakdownPageResetKey, setBreakdownPageResetKey] = useState(0);
const summaryColumns = [
{
title: intl.formatMessage({ id: 'usage.filter.inputTokens' }),
dataIndex: 'input_tokens',
key: 'input_tokens'
},
{
title: intl.formatMessage({ id: 'usage.filter.outputTokens' }),
dataIndex: 'output_tokens',
key: 'output_tokens'
},
{
title: intl.formatMessage({ id: 'usage.filter.totalTokens' }),
dataIndex: 'total_tokens',
key: 'total_tokens'
},
{
title: intl.formatMessage({ id: 'usage.filter.apiRequests' }),
dataIndex: 'api_requests',
key: 'api_requests'
},
{
title: intl.formatMessage({ id: 'usage.filter.modelsUsed' }),
dataIndex: 'models_called',
key: 'models_called'
}
];
const [chartFilters, setChartFilters] = useState<{
metric: string;
group_by: string | null;
granularity: string;
}>({
metric: 'total_tokens',
group_by: null,
granularity: 'day'
});
const { detailData: metaData, fetchData: fetchMetaData } =
useQueryUsageMetaData();
const { filters, commonFilters, fetchData, timeSeriesData, filterBar } =
useUsageFilters({
initialScope: access.canSeeOrgAdmin ? 'all' : 'self',
metaData,
chartFilters,
summaryColumns
});
useEffect(() => {
fetchMetaData();
fetchData(commonFilters, chartFilters);
}, []);
const handleChartFilterChange = (type: string, value: string) => {
setChartFilters((prev) => ({ ...prev, [type]: value }));
fetchData(commonFilters, { ...chartFilters, [type]: value });
};
const summaryCards = useMemo(() => {
const summary = timeSeriesData?.summary || {
input_tokens: 0,
output_tokens: 0,
total_tokens: 0,
api_requests: 0,
models_called: 0
};
return [
{
label: formatLargeNumber(summary.input_tokens) as string,
value: intl.formatMessage({ id: 'usage.filter.inputTokens' }),
color: baseColorMap.baseR3
},
{
label: formatLargeNumber(summary.output_tokens) as string,
value: intl.formatMessage({ id: 'usage.filter.outputTokens' }),
color: baseColorMap.base
},
{
label: formatLargeNumber(summary.total_tokens) as string,
value: intl.formatMessage({ id: 'usage.filter.totalTokens' }),
color: baseColorMap.baseL1
},
{
label: formatLargeNumber(summary.api_requests) as string,
value: intl.formatMessage({ id: 'usage.filter.apiRequests' }),
color: baseColorMap.baseR1
},
{
label: summary.models_called.toString(),
value: intl.formatMessage({ id: 'usage.filter.modelsUsed' }),
color: baseColorMap.baseR2
}
];
}, [timeSeriesData.summary]);
const breakdownDateRange = useMemo(
() => ({
start_date: commonFilters.start_date,
end_date: commonFilters.end_date
}),
[commonFilters.end_date, commonFilters.start_date]
);
const handlePickerChange = (picker: DateType) => {
setChartFilters((prev) => ({
...prev,
granularity: picker === 'date' ? 'day' : picker
}));
};
const handleExportChart = () => {
setOpenExportModal(true);
};
const handleSearch = () => {
filterBar.handleSearch();
setBreakdownRefreshKey((prev) => prev + 1);
};
const handleBreakdownPageReset = () => {
setBreakdownPageResetKey((prev) => prev + 1);
};
return (
<div>
<FilterBar
{...filterBar}
onScopeChange={(value) => {
filterBar.onScopeChange(value);
handleBreakdownPageReset();
}}
onDateChange={(dates, dateStrings) => {
filterBar.onDateChange(dates, dateStrings);
handleBreakdownPageReset();
}}
onRoutesChange={(value) => {
filterBar.onRoutesChange(value);
handleBreakdownPageReset();
}}
onUsersChange={(value) => {
filterBar.onUsersChange(value);
handleBreakdownPageReset();
}}
onApiKeysChange={(value) => {
filterBar.onApiKeysChange(value);
handleBreakdownPageReset();
}}
handleSearch={handleSearch}
handlePickerChange={handlePickerChange}
onExportTable={exportTable}
onExportChart={handleExportChart}
/>
<div
style={{
marginBlock: 24
}}
>
<SimpleCard
dataList={summaryCards}
height={80}
styles={{
item: {
backgroundColor: 'var(--ant-color-fill-quaternary)',
borderRadius: '6px'
}
}}
/>
</div>
<DailyUsage
timeSeriesData={timeSeriesData}
metric={chartFilters.metric}
groupBy={chartFilters.group_by}
granularity={chartFilters.granularity}
startDate={commonFilters.start_date}
endDate={commonFilters.end_date}
onMetricChange={(value) => handleChartFilterChange('metric', value)}
onGroupByChange={(value) =>
handleChartFilterChange('group_by', value as string)
}
onGranularityChange={(value) =>
handleChartFilterChange('granularity', value)
}
/>
<BreakdownTabs
filters={filters}
dateRange={breakdownDateRange}
scope={commonFilters.scope}
pageResetKey={breakdownPageResetKey}
refreshKey={breakdownRefreshKey}
></BreakdownTabs>
<ExportData
metaData={metaData}
granularity={chartFilters.granularity}
initialScope={commonFilters.scope}
commonFilters={commonFilters}
open={openExportModal}
handlePickerChange={handlePickerChange}
onCancel={() => setOpenExportModal(false)}
initialState={{
activeRoutes: filterBar.selectedRoutes,
activeApiKeys: filterBar.activeApiKeys,
users: commonFilters.users,
start_date: commonFilters.start_date,
end_date: commonFilters.end_date
}}
></ExportData>
</div>
);
};
export default TokenTab;