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:
@@ -10,6 +10,34 @@ import useAPIKeysColumns from './use-apikeys-columns';
|
||||
import useModelsColumns from './use-models-columns';
|
||||
import useUsersColumns from './use-users-columns';
|
||||
|
||||
type ColumnLike = {
|
||||
title: any;
|
||||
dataIndex: string | string[];
|
||||
key?: string;
|
||||
};
|
||||
|
||||
const toFieldKey = (dataIndex: string | string[]): string =>
|
||||
Array.isArray(dataIndex) ? dataIndex.join('_') : dataIndex;
|
||||
|
||||
const buildSheetMeta = (columns: ColumnLike[]) => {
|
||||
const fields: string[] = [];
|
||||
const fieldLabels: Record<string, any> = {};
|
||||
const formatMap: Record<string, (raw: any, row: any) => any> = {};
|
||||
|
||||
columns.forEach((col) => {
|
||||
if (!col.dataIndex) return;
|
||||
const key = toFieldKey(col.dataIndex);
|
||||
fields.push(key);
|
||||
fieldLabels[key] = col.title;
|
||||
if (Array.isArray(col.dataIndex)) {
|
||||
const path = col.dataIndex;
|
||||
formatMap[key] = (_raw, row) => _.get(row, path);
|
||||
}
|
||||
});
|
||||
|
||||
return { fields, fieldLabels, formatMap };
|
||||
};
|
||||
|
||||
const useExportTable = () => {
|
||||
const store = useStore();
|
||||
const modelsColumns = useModelsColumns();
|
||||
@@ -20,53 +48,34 @@ const useExportTable = () => {
|
||||
const usersTableData = store.get(usersTableDataAtom);
|
||||
const apiKeysTableData = store.get(apiKeysTableDataAtom);
|
||||
const modelsTableData = store.get(modelsTableDataAtom);
|
||||
|
||||
const modelsMeta = buildSheetMeta(modelsColumns);
|
||||
const apiKeysMeta = buildSheetMeta(apiKeysColumns);
|
||||
const usersMeta = buildSheetMeta(usersColumns);
|
||||
|
||||
exportJsonToExcel({
|
||||
fileName: 'table_data.xlsx',
|
||||
sheets: [
|
||||
{
|
||||
jsonData: modelsTableData.dataList || [],
|
||||
sheetName: 'models',
|
||||
fields: modelsColumns
|
||||
.map((col: any) => _.join(col.dataIndex, '_'))
|
||||
.filter(Boolean) as string[],
|
||||
fieldLabels: modelsColumns.reduce(
|
||||
(map, col) => {
|
||||
map[_.join(col.dataIndex, '_')] = col.title;
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
),
|
||||
formatMap: {}
|
||||
fields: modelsMeta.fields,
|
||||
fieldLabels: modelsMeta.fieldLabels,
|
||||
formatMap: modelsMeta.formatMap
|
||||
},
|
||||
{
|
||||
jsonData: apiKeysTableData.dataList || [],
|
||||
sheetName: 'api_keys',
|
||||
fields: apiKeysColumns
|
||||
.map((col: any) => col.dataIndex)
|
||||
.filter(Boolean) as string[],
|
||||
fieldLabels: apiKeysColumns.reduce(
|
||||
(map, col) => {
|
||||
map[_.join(col.dataIndex, '_')] = col.title;
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
),
|
||||
formatMap: {}
|
||||
fields: apiKeysMeta.fields,
|
||||
fieldLabels: apiKeysMeta.fieldLabels,
|
||||
formatMap: apiKeysMeta.formatMap
|
||||
},
|
||||
{
|
||||
jsonData: usersTableData.dataList || [],
|
||||
sheetName: 'users',
|
||||
fields: usersColumns
|
||||
.map((col: any) => col.dataIndex)
|
||||
.filter(Boolean) as string[],
|
||||
fieldLabels: usersColumns.reduce(
|
||||
(map, col) => {
|
||||
map[_.join(col.dataIndex, '_')] = col.title;
|
||||
return map;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
),
|
||||
formatMap: {}
|
||||
fields: usersMeta.fields,
|
||||
fieldLabels: usersMeta.fieldLabels,
|
||||
formatMap: usersMeta.formatMap
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
// columns.ts
|
||||
import ProviderLogo from '@/pages/maas-provider/components/provider-logo';
|
||||
import { AutoTooltip } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Tag } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { BreakdownItem as ListItem } from '../config/types';
|
||||
|
||||
interface ColumnsHookProps {
|
||||
sortOrder: string[];
|
||||
}
|
||||
|
||||
const useModelsColumns = (): Array<{
|
||||
title: string;
|
||||
dataIndex: string | string[];
|
||||
@@ -21,8 +16,8 @@ const useModelsColumns = (): Array<{
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||
dataIndex: ['model', 'identity', 'value', 'model_name'],
|
||||
key: 'model_name',
|
||||
dataIndex: ['route', 'label'],
|
||||
key: 'route_name',
|
||||
render: (text: string, record: ListItem) => (
|
||||
<span className="flex items-center">
|
||||
<AutoTooltip
|
||||
@@ -32,7 +27,7 @@ const useModelsColumns = (): Array<{
|
||||
>
|
||||
<span className="text-primary">{text}</span>
|
||||
</AutoTooltip>
|
||||
{record.model?.deleted && (
|
||||
{record.route?.deleted && (
|
||||
<Tag
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
@@ -49,28 +44,6 @@ const useModelsColumns = (): Array<{
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.cluster' }),
|
||||
dataIndex: ['model', 'identity', 'value', 'cluster_name'],
|
||||
key: 'cluster_name',
|
||||
render: (text: string, record: ListItem) => (
|
||||
<AutoTooltip ghost>{text || '-'}</AutoTooltip>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.provider' }),
|
||||
dataIndex: ['model', 'identity', 'value', 'provider_type'],
|
||||
key: 'provider_type',
|
||||
render: (text: string, record: ListItem) => (
|
||||
<span className="flex-center gap-8">
|
||||
<ProviderLogo provider={text || 'deployments'} />
|
||||
<AutoTooltip ghost style={{ maxWidth: 400 }}>
|
||||
{text ||
|
||||
(!text && intl.formatMessage({ id: 'menu.models.deployment' }))}
|
||||
</AutoTooltip>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.filter.inputTokens' }),
|
||||
dataIndex: 'input_tokens',
|
||||
|
||||
@@ -12,6 +12,9 @@ const DefaultDateConfig = {
|
||||
type UserOptionType = UsageFilterItem & {
|
||||
value: string;
|
||||
};
|
||||
type RouteOptionType = UsageFilterItem & {
|
||||
value: string;
|
||||
};
|
||||
type ValueType = string | number | null;
|
||||
|
||||
type FilterOptionType = Omit<UsageFilterItem, 'label' | 'deleted'>;
|
||||
@@ -23,6 +26,7 @@ interface UseUsageFiltersParams {
|
||||
models?: GroupOptionType[];
|
||||
users?: UserOptionType[];
|
||||
api_keys?: GroupOptionType[];
|
||||
routes?: RouteOptionType[];
|
||||
};
|
||||
chartFilters: {
|
||||
metric: string;
|
||||
@@ -30,7 +34,7 @@ interface UseUsageFiltersParams {
|
||||
granularity: string;
|
||||
};
|
||||
initialState?: {
|
||||
activeModels?: ValueType[][];
|
||||
activeRoutes?: string[];
|
||||
activeApiKeys?: ValueType[][];
|
||||
users?: string[];
|
||||
start_date?: string;
|
||||
@@ -45,13 +49,13 @@ interface UseUsageFiltersParams {
|
||||
onFetchData?: (params: {
|
||||
chartFilters: UseUsageFiltersParams['chartFilters'];
|
||||
filters: {
|
||||
models?: FilterOptionType[];
|
||||
routes?: FilterOptionType[];
|
||||
users?: FilterOptionType[];
|
||||
api_keys?: FilterOptionType[];
|
||||
};
|
||||
commonFilters: {
|
||||
scope: string;
|
||||
models: string[];
|
||||
routes: string[];
|
||||
users: string[];
|
||||
api_keys: string[];
|
||||
start_date: string;
|
||||
@@ -70,7 +74,7 @@ export const useUsageFilters = ({
|
||||
onFetchData
|
||||
}: UseUsageFiltersParams) => {
|
||||
const {
|
||||
activeModels: initialActiveModels = [],
|
||||
activeRoutes: initialActiveRoutes = [],
|
||||
activeApiKeys: initialActiveApiKeys = [],
|
||||
users: initialUsers = [],
|
||||
start_date = '',
|
||||
@@ -93,7 +97,7 @@ export const useUsageFilters = ({
|
||||
|
||||
const [commonFilters, setCommonFilters] = useState({
|
||||
scope: initialScope,
|
||||
models: extractSelectedValues(initialActiveModels),
|
||||
routes: initialActiveRoutes,
|
||||
users: initialUsers || [],
|
||||
api_keys: extractSelectedValues(initialActiveApiKeys),
|
||||
start_date:
|
||||
@@ -104,11 +108,9 @@ export const useUsageFilters = ({
|
||||
end_date: end_date || dayjs().format('YYYY-MM-DD')
|
||||
});
|
||||
|
||||
const modelOptions = metaData?.models || [];
|
||||
const routeOptions = metaData?.routes || [];
|
||||
const userOptions = metaData?.users || [];
|
||||
const apiKeyOptions = metaData?.api_keys || [];
|
||||
const [activeModels, setActiveModels] =
|
||||
useState<ValueType[][]>(initialActiveModels);
|
||||
const [activeApiKeys, setActiveApiKeys] =
|
||||
useState<ValueType[][]>(initialActiveApiKeys);
|
||||
|
||||
@@ -117,12 +119,11 @@ export const useUsageFilters = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveModels(initialActiveModels);
|
||||
setActiveApiKeys(initialActiveApiKeys);
|
||||
setCommonFilters((prev) => ({
|
||||
...prev,
|
||||
scope: initialScope,
|
||||
models: extractSelectedValues(initialActiveModels),
|
||||
routes: initialActiveRoutes,
|
||||
users: initialUsers || [],
|
||||
api_keys: extractSelectedValues(initialActiveApiKeys),
|
||||
start_date:
|
||||
@@ -135,7 +136,7 @@ export const useUsageFilters = ({
|
||||
}, [
|
||||
end_date,
|
||||
initialActiveApiKeys,
|
||||
initialActiveModels,
|
||||
initialActiveRoutes,
|
||||
initialUsers,
|
||||
initialScope,
|
||||
initialState,
|
||||
@@ -144,17 +145,15 @@ export const useUsageFilters = ({
|
||||
|
||||
const buildFilters = (selected: typeof commonFilters) => {
|
||||
const filters: {
|
||||
models?: FilterOptionType[];
|
||||
routes?: FilterOptionType[];
|
||||
users?: FilterOptionType[];
|
||||
api_keys?: FilterOptionType[];
|
||||
} = {};
|
||||
|
||||
if (selected.models.length > 0) {
|
||||
const modelsSet = new Set(selected.models);
|
||||
filters.models = modelOptions
|
||||
.flatMap((group) =>
|
||||
group.children.filter((item) => modelsSet.has(item.value))
|
||||
)
|
||||
if (selected.routes.length > 0) {
|
||||
const routesSet = new Set(selected.routes);
|
||||
filters.routes = routeOptions
|
||||
.filter((item) => routesSet.has(item.value))
|
||||
.map((item) => ({
|
||||
identity: item.identity
|
||||
}));
|
||||
@@ -184,7 +183,7 @@ export const useUsageFilters = ({
|
||||
|
||||
const filters = useMemo(
|
||||
() => buildFilters(commonFilters),
|
||||
[commonFilters, modelOptions, userOptions, apiKeyOptions]
|
||||
[commonFilters, routeOptions, userOptions, apiKeyOptions]
|
||||
);
|
||||
|
||||
const fetchData = (
|
||||
@@ -233,16 +232,12 @@ export const useUsageFilters = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleActiveModelsChange = (value: ValueType[][]) => {
|
||||
setActiveModels(value);
|
||||
};
|
||||
|
||||
const handleActiveApiKeysChange = (value: ValueType[][]) => {
|
||||
setActiveApiKeys(value);
|
||||
};
|
||||
|
||||
const handleFilterChange = (
|
||||
type: 'models' | 'users' | 'api_keys',
|
||||
type: 'routes' | 'users' | 'api_keys',
|
||||
value: string[]
|
||||
) => {
|
||||
const selectedValues: string[] = value.map((item) => {
|
||||
@@ -267,7 +262,7 @@ export const useUsageFilters = ({
|
||||
const items = timeSeriesData?.items || [];
|
||||
const groupDim = chartFilters.group_by as
|
||||
| 'user'
|
||||
| 'model'
|
||||
| 'route'
|
||||
| 'api_key'
|
||||
| null;
|
||||
const metric = chartFilters.metric as keyof BreakdownItem;
|
||||
@@ -327,20 +322,18 @@ export const useUsageFilters = ({
|
||||
scope: commonFilters.scope,
|
||||
startDate: commonFilters.start_date,
|
||||
endDate: commonFilters.end_date,
|
||||
selectedModels: commonFilters.models,
|
||||
selectedRoutes: commonFilters.routes,
|
||||
selectedUsers: commonFilters.users,
|
||||
selectedApiKeys: commonFilters.api_keys,
|
||||
modelOptions,
|
||||
routeOptions,
|
||||
userOptions,
|
||||
apiKeyOptions,
|
||||
activeApiKeys,
|
||||
activeModels,
|
||||
handleSearch,
|
||||
handleActiveModelsChange,
|
||||
handleActiveApiKeysChange,
|
||||
onScopeChange: handleScopeChange,
|
||||
onDateChange: handleDateChange,
|
||||
onModelsChange: (value: string[]) => handleFilterChange('models', value),
|
||||
onRoutesChange: (value: string[]) => handleFilterChange('routes', value),
|
||||
onUsersChange: (value: string[]) => handleFilterChange('users', value),
|
||||
onApiKeysChange: (value: string[]) => handleFilterChange('api_keys', value),
|
||||
onExportChart: handleOnExportChart
|
||||
|
||||
Reference in New Issue
Block a user