refactor(usage): switch breakdown dimension from model to route
Align with gpustack backend commits 7b57cd3b (add route dimension to
usage breakdown) and 51fd25c8 (use model_route_id as models_called):
* group_by, filters, and BreakdownItem now use the `route` dimension;
`models` filter cascader is replaced with a flat `routes` selector
(routes have no provider/cluster grouping).
* Tab / group_by option / column header keep the user-facing "模型 / Model"
wording — only the underlying data dimension changed.
* Drop cluster_name / provider_type columns from the breakdown table;
drop api_keys_used column per design.
* Fix pre-existing excel export bug in use-export-table — string
dataIndex was lodash-joined char-by-char ("input_tokens" →
"i_n_p_u_t..."), array dataIndex was used as a flat key against nested
rows. Rebuilt around buildSheetMeta that emits a stable field key plus
a formatMap using _.get(row, path) for nested values. Sheet name kept
as `models` to match the visible column label.
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tabs } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { GroupOption } from '../config';
|
||||
import { UsageFilterItem } from '../config/types';
|
||||
import ApiKeysTable from '../tables/apikeys-table';
|
||||
import ModelsTable from '../tables/models-table';
|
||||
import UsersTable from '../tables/users-table';
|
||||
|
||||
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
|
||||
type GroupOptionType = GroupOption<UsageFilterItem>;
|
||||
const EMPTY_FILTERS: FilterOptionType[] = [];
|
||||
|
||||
const BreakdownTabs: React.FC<{
|
||||
@@ -20,13 +18,13 @@ const BreakdownTabs: React.FC<{
|
||||
pageResetKey?: number;
|
||||
refreshKey?: number;
|
||||
filters: {
|
||||
models?: FilterOptionType[];
|
||||
routes?: FilterOptionType[];
|
||||
users?: FilterOptionType[];
|
||||
api_keys?: FilterOptionType[];
|
||||
};
|
||||
}> = ({ filters, dateRange, scope, pageResetKey = 0, refreshKey = 0 }) => {
|
||||
const intl = useIntl();
|
||||
const models = filters.models || EMPTY_FILTERS;
|
||||
const routes = filters.routes || EMPTY_FILTERS;
|
||||
const users = filters.users || EMPTY_FILTERS;
|
||||
const apiKeys = filters.api_keys || EMPTY_FILTERS;
|
||||
|
||||
@@ -39,7 +37,7 @@ const BreakdownTabs: React.FC<{
|
||||
children: (
|
||||
<ModelsTable
|
||||
key="models"
|
||||
models={models}
|
||||
routes={routes}
|
||||
dateRange={dateRange}
|
||||
scope={scope}
|
||||
pageResetKey={pageResetKey}
|
||||
@@ -83,7 +81,7 @@ const BreakdownTabs: React.FC<{
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [apiKeys, dateRange, models, pageResetKey, refreshKey, scope, users]);
|
||||
}, [apiKeys, dateRange, routes, pageResetKey, refreshKey, scope, users]);
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
|
||||
@@ -104,7 +104,7 @@ const DailyUsage: React.FC<DailyUsageProps> = (props) => {
|
||||
};
|
||||
}
|
||||
|
||||
const groupDim = groupBy as 'user' | 'model' | 'api_key' | null;
|
||||
const groupDim = groupBy as 'user' | 'route' | 'api_key' | null;
|
||||
const isCached = metric === CACHED_METRIC;
|
||||
|
||||
const dateSet = new Set<string>(
|
||||
|
||||
@@ -22,7 +22,7 @@ const ExportData: React.FC<{
|
||||
metaData: any;
|
||||
granularity: string;
|
||||
initialState: {
|
||||
activeModels: ValueType[][];
|
||||
activeRoutes: string[];
|
||||
activeApiKeys: ValueType[][];
|
||||
users: string[];
|
||||
start_date: string;
|
||||
@@ -32,7 +32,7 @@ const ExportData: React.FC<{
|
||||
scope: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
models: string[];
|
||||
routes: string[];
|
||||
users: string[];
|
||||
api_keys: string[];
|
||||
};
|
||||
@@ -84,7 +84,7 @@ const ExportData: React.FC<{
|
||||
...pageParams,
|
||||
granularity: 'day',
|
||||
sort_by: '-date',
|
||||
group_by: ['date', 'user', 'model', 'api_key'],
|
||||
group_by: ['date', 'user', 'route', 'api_key'],
|
||||
filters: nextFilters,
|
||||
scope: initialScope,
|
||||
start_date: nextCommonFilters.start_date,
|
||||
@@ -113,25 +113,12 @@ const ExportData: React.FC<{
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.cluster' }),
|
||||
dataIndex: ['model', 'identity', 'value', 'cluster_name'],
|
||||
title: intl.formatMessage({ id: 'usage.filter.group.model' }),
|
||||
dataIndex: ['route', 'label'],
|
||||
render: (text: string) => {
|
||||
return <AutoTooltip ghost>{text}</AutoTooltip>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
|
||||
dataIndex: ['model', 'identity', 'value', 'model_name'],
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
<AutoTooltip ghost>
|
||||
{record?.model?.identity?.value?.provider_name
|
||||
? `${record?.model?.identity?.value?.provider_name}/${text}`
|
||||
: text}
|
||||
</AutoTooltip>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
|
||||
dataIndex: ['user', 'label'],
|
||||
@@ -213,8 +200,7 @@ const ExportData: React.FC<{
|
||||
jsonData: (dataSource.dataList || []).map((item: any) => ({
|
||||
date: item?.date?.label,
|
||||
user: item?.user?.label,
|
||||
cluster: item?.model?.identity?.value?.cluster_name,
|
||||
model: item?.model?.identity?.value?.model_name,
|
||||
route: item?.route?.label,
|
||||
api_key: item?.api_key?.label,
|
||||
input_tokens: item?.input_tokens,
|
||||
input_cached_tokens: item?.input_cached_tokens,
|
||||
@@ -226,7 +212,7 @@ const ExportData: React.FC<{
|
||||
fields: [
|
||||
'date',
|
||||
'user',
|
||||
'model',
|
||||
'route',
|
||||
'api_key',
|
||||
'input_tokens',
|
||||
'input_cached_tokens',
|
||||
@@ -237,8 +223,7 @@ const ExportData: React.FC<{
|
||||
fieldLabels: {
|
||||
date: intl.formatMessage({ id: 'dashboard.usage.export.date' }),
|
||||
user: intl.formatMessage({ id: 'dashboard.usage.export.user' }),
|
||||
cluster: intl.formatMessage({ id: 'usage.table.cluster' }),
|
||||
model: intl.formatMessage({ id: 'dashboard.usage.export.model' }),
|
||||
route: intl.formatMessage({ id: 'usage.filter.group.model' }),
|
||||
api_key: intl.formatMessage({ id: 'usage.table.provider' }),
|
||||
input_tokens: intl.formatMessage({
|
||||
id: 'usage.filter.inputTokens'
|
||||
@@ -286,7 +271,7 @@ const ExportData: React.FC<{
|
||||
fetchExportData({
|
||||
...INITIAL_PAGE_PARAMS,
|
||||
granularity: 'day',
|
||||
group_by: ['date', 'user', 'model', 'api_key'],
|
||||
group_by: ['date', 'user', 'route', 'api_key'],
|
||||
filters,
|
||||
sort_by: '-date',
|
||||
scope: initialScope,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import useRangePickerPreset from '@/pages/dashboard/hooks/use-rangepicker-preset';
|
||||
import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
|
||||
import { useModel } from '@@/plugin-model';
|
||||
import { DownloadOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
@@ -32,21 +31,19 @@ interface FilterBarProps {
|
||||
scope: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedModels: string[];
|
||||
selectedRoutes: string[];
|
||||
selectedUsers: string[];
|
||||
selectedApiKeys: string[];
|
||||
modelOptions: GroupOption<UsageFilterItem>[];
|
||||
routeOptions: OptionType[];
|
||||
userOptions: OptionType[];
|
||||
apiKeyOptions: GroupOption<UsageFilterItem>[];
|
||||
activeModels: valueType[][];
|
||||
activeApiKeys: valueType[][];
|
||||
handlePickerChange: (picker: DateType) => void;
|
||||
onScopeChange: (value: string) => void;
|
||||
onDateChange: (dates: any, dateStrings: [string, string]) => void;
|
||||
onModelsChange: (value: string[]) => void;
|
||||
onRoutesChange: (value: string[]) => void;
|
||||
onUsersChange: (value: string[]) => void;
|
||||
onApiKeysChange: (value: string[]) => void;
|
||||
handleActiveModelsChange: (value: valueType[][]) => void;
|
||||
handleActiveApiKeysChange: (value: valueType[][]) => void;
|
||||
onExport?: () => void;
|
||||
handleSearch?: () => void;
|
||||
@@ -56,7 +53,7 @@ interface FilterBarProps {
|
||||
scope: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
models: string[];
|
||||
routes: string[];
|
||||
users: string[];
|
||||
api_keys: string[];
|
||||
};
|
||||
@@ -67,18 +64,17 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
pageType = 'page',
|
||||
startDate,
|
||||
endDate,
|
||||
selectedRoutes,
|
||||
selectedUsers,
|
||||
selectedApiKeys,
|
||||
modelOptions,
|
||||
routeOptions,
|
||||
userOptions,
|
||||
apiKeyOptions,
|
||||
activeModels,
|
||||
activeApiKeys,
|
||||
handleActiveModelsChange,
|
||||
handleActiveApiKeysChange,
|
||||
handlePickerChange,
|
||||
onDateChange,
|
||||
onModelsChange,
|
||||
onRoutesChange,
|
||||
onUsersChange,
|
||||
onApiKeysChange,
|
||||
onExportChart,
|
||||
@@ -157,17 +153,6 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleOnModelsChange = (value: valueType[][], selectedOptions: any) => {
|
||||
const selectedValues: string[] = value.map((item) => {
|
||||
if (Array.isArray(item)) {
|
||||
return item[item.length - 1] as string; // Get the last value in the array
|
||||
}
|
||||
return item as string;
|
||||
});
|
||||
handleActiveModelsChange(value);
|
||||
onModelsChange(selectedValues);
|
||||
};
|
||||
|
||||
const handleOnApiKeysChange = (
|
||||
value: valueType[][],
|
||||
selectedOptions: any
|
||||
@@ -182,55 +167,6 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
onApiKeysChange(selectedValues);
|
||||
};
|
||||
|
||||
const displayRender = (labels: any[], option: any) => {
|
||||
return (
|
||||
<AutoTooltip
|
||||
ghost
|
||||
maxWidth={150}
|
||||
title={
|
||||
<span>
|
||||
{labels[0]} / {labels[1]}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{labels[0]} / {labels[1]}
|
||||
</AutoTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const optionRender = (option: any) => {
|
||||
const { data } = option;
|
||||
if (!data.isParent) {
|
||||
return (
|
||||
<span className="flex-center gap-4">
|
||||
<AutoTooltip ghost>{data.label}</AutoTooltip>
|
||||
{data.deleted &&
|
||||
renderTag(intl.formatMessage({ id: 'usage.table.deleted' }))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (data.type === 'deployments') {
|
||||
return (
|
||||
<span className={FilterBarCss.optionsWrapper}>
|
||||
<ProviderLogo provider={data.type as string} />
|
||||
<AutoTooltip ghost>
|
||||
{intl.formatMessage({ id: 'menu.models.deployment' })}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={FilterBarCss.optionsWrapper}>
|
||||
<ProviderLogo provider={data.type as string} />
|
||||
<AutoTooltip ghost>
|
||||
<span>{data.label}</span>
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const apiKeyOptionRender = (option: any) => {
|
||||
const { data } = option;
|
||||
if (!data.isParent) {
|
||||
@@ -303,47 +239,19 @@ const FilterBar: React.FC<FilterBarProps> = (props) => {
|
||||
style={{ width: 240 }}
|
||||
onChange={onDateChange}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: 400,
|
||||
flex: 1,
|
||||
minWidth: 200
|
||||
<SimpleSelect
|
||||
allowClear
|
||||
showSearch
|
||||
mode="multiple"
|
||||
options={routeOptions}
|
||||
placeholder={intl.formatMessage({ id: 'usage.filter.model' })}
|
||||
styles={{
|
||||
wrapper: { flex: 1, maxWidth: 400, minWidth: 200 }
|
||||
}}
|
||||
>
|
||||
<Cascader
|
||||
showSearch
|
||||
multiple={true}
|
||||
classNames={{
|
||||
popup: {
|
||||
root: 'cascader-popup-wrapper gpu-selector'
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
root: {
|
||||
width: '100%'
|
||||
},
|
||||
popup: {
|
||||
list: {
|
||||
flex: 1
|
||||
},
|
||||
listItem: {
|
||||
padding: '5px 10px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
maxTagCount={1}
|
||||
size="small"
|
||||
isInFormItems={false}
|
||||
placeholder={intl.formatMessage({ id: 'usage.filter.model' })}
|
||||
options={modelOptions}
|
||||
showCheckedStrategy="SHOW_CHILD"
|
||||
displayRender={displayRender}
|
||||
optionNode={optionRender}
|
||||
value={activeModels}
|
||||
onChange={handleOnModelsChange}
|
||||
getPopupContainer={(triggerNode) => triggerNode.parentNode}
|
||||
></Cascader>
|
||||
</div>
|
||||
value={selectedRoutes}
|
||||
optionLabelRender={singleOptionRender}
|
||||
onChange={onRoutesChange}
|
||||
/>
|
||||
{initialState?.currentUser?.is_admin && (
|
||||
<>
|
||||
<SimpleSelect
|
||||
|
||||
Reference in New Issue
Block a user