diff --git a/src/pages/usage/apis/resource.ts b/src/pages/usage/apis/resource.ts index 221afb7d..c1302970 100644 --- a/src/pages/usage/apis/resource.ts +++ b/src/pages/usage/apis/resource.ts @@ -10,7 +10,8 @@ * (cpu/memory/ephemeral hours, dangling volumes) are left at 0 — the * whole-machine SKU model meters runtime, not decomposed components. */ -import { request } from '@umijs/max'; +import { getIntl, request } from '@umijs/max'; +import { withDeletedMark } from '../utils/deleted-label'; import { instanceTypeSeriesLabel } from '../utils/format-instance-type'; export interface ResourceUsageFilters { @@ -283,10 +284,12 @@ function flattenItem( const id = it.id ?? undefined; const deleted = !!it.deleted; const rawKey = it.key ?? undefined; - // The chart series legend keeps the "(Deleted)" suffix (a legend can't render - // a tag); the tables show a DeletedTag off ``flat.deleted`` + the id and so - // use the clean name. - const key = deleted && rawKey != null ? `${rawKey} (Deleted)` : rawKey; + // The chart series legend can't render a tag, so it carries the deleted + // marker as text (" [Deleted.]"); the tables render a DeletedTag off + // ``flat.deleted`` + the id and so keep the clean name. + const deletedWord = getIntl().formatMessage({ id: 'usage.table.deleted' }); + const key = + rawKey != null ? withDeletedMark(rawKey, deleted, deletedWord, id) : rawKey; // Generic group label — for a compound (date + dim) trend row the key is the // sub-group value (the switch below targets single-dimension table rows). if (rawKey != null) flat.group = key; diff --git a/src/pages/usage/components/daily-usage.tsx b/src/pages/usage/components/daily-usage.tsx index 387711a5..67ec3124 100644 --- a/src/pages/usage/components/daily-usage.tsx +++ b/src/pages/usage/components/daily-usage.tsx @@ -12,6 +12,15 @@ import { UsageBreakdownResponse, UsageFilterItem } from '../config/types'; +import { withDeletedMark } from '../utils/deleted-label'; + +// group dimension → the id field inside ``identity.current`` (the backend nulls +// it for deleted entities, so the marker falls back to just "[Deleted]"). +const GROUP_ID_KEY: Record = { + route: 'route_id', + user: 'user_id', + api_key: 'api_key_id' +}; const ControlsWrapper = styled.div` display: flex; @@ -124,12 +133,22 @@ const DailyUsage: React.FC = (props) => { (a, b) => dayjs(a).valueOf() - dayjs(b).valueOf() ); + const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' }); + const groupOrder: string[] = []; const groupItemsMap = new Map>(); items.forEach((item) => { + const groupEntity = groupDim + ? (item[groupDim] as UsageFilterItem) + : undefined; const groupLabel = groupDim - ? ((item[groupDim] as UsageFilterItem)?.label ?? '-') + ? withDeletedMark( + groupEntity?.label ?? '-', + groupEntity?.deleted, + deletedWord, + groupEntity?.identity?.current?.[GROUP_ID_KEY[groupDim]] + ) : '__total__'; if (!groupItemsMap.has(groupLabel)) { diff --git a/src/pages/usage/hooks/use-export-table.tsx b/src/pages/usage/hooks/use-export-table.tsx index b68297c7..2c602da9 100644 --- a/src/pages/usage/hooks/use-export-table.tsx +++ b/src/pages/usage/hooks/use-export-table.tsx @@ -4,12 +4,24 @@ import { usersTableDataAtom } from '@/atoms/usage'; import { exportJsonToExcel } from '@gpustack/core-ui/excel'; +import { useIntl } from '@umijs/max'; import { useStore } from 'jotai'; import _ from 'lodash'; +import { withDeletedMark } from '../utils/deleted-label'; import useAPIKeysColumns from './use-apikeys-columns'; import useModelsColumns from './use-models-columns'; import useUsersColumns from './use-users-columns'; +// The name column's ``dataIndex`` starts with the embedded entity object +// (route / user / api_key); that entity carries ``deleted`` + the id used for +// the "[Deleted.]" export marker. +const ID_KEY_BY_ENTITY: Record = + { + route: 'route_id', + user: 'user_id', + api_key: 'api_key_id' + }; + type ColumnLike = { title: any; // Plugin-contributed columns may omit `dataIndex` — they render via a @@ -23,11 +35,14 @@ type ColumnLike = { const toFieldKey = (dataIndex: string | string[]): string => Array.isArray(dataIndex) ? dataIndex.join('_') : dataIndex; -const buildSheetMeta = (columns: ColumnLike[]) => { +const buildSheetMeta = (columns: ColumnLike[], deletedWord: string) => { const fields: string[] = []; const fieldLabels: Record = {}; const formatMap: Record any> = {}; + // The first entity-backed column is the name column; its exported value gets + // the " [Deleted.]" marker for deleted rows. + let nameMarked = false; columns.forEach((col) => { if (!col.dataIndex) return; const key = toFieldKey(col.dataIndex); @@ -35,7 +50,22 @@ const buildSheetMeta = (columns: ColumnLike[]) => { fieldLabels[key] = col.title; if (Array.isArray(col.dataIndex)) { const path = col.dataIndex; - formatMap[key] = (_raw, row) => _.get(row, path); + if (!nameMarked) { + nameMarked = true; + const entityKey = path[0]; + const idKey = ID_KEY_BY_ENTITY[entityKey]; + formatMap[key] = (_raw, row) => { + const entity = row?.[entityKey]; + return withDeletedMark( + _.get(row, path), + entity?.deleted, + deletedWord, + idKey ? entity?.identity?.current?.[idKey] : undefined + ); + }; + } else { + formatMap[key] = (_raw, row) => _.get(row, path); + } } }); @@ -44,18 +74,20 @@ const buildSheetMeta = (columns: ColumnLike[]) => { const useExportTable = () => { const store = useStore(); + const intl = useIntl(); const modelsColumns = useModelsColumns(); const apiKeysColumns = useAPIKeysColumns(); const usersColumns = useUsersColumns(); const exportTable = () => { + const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' }); 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); + const modelsMeta = buildSheetMeta(modelsColumns, deletedWord); + const apiKeysMeta = buildSheetMeta(apiKeysColumns, deletedWord); + const usersMeta = buildSheetMeta(usersColumns, deletedWord); exportJsonToExcel({ fileName: 'table_data.xlsx', diff --git a/src/pages/usage/hooks/use-usage-filters.ts b/src/pages/usage/hooks/use-usage-filters.ts index 759977e9..e1332fc2 100644 --- a/src/pages/usage/hooks/use-usage-filters.ts +++ b/src/pages/usage/hooks/use-usage-filters.ts @@ -1,10 +1,20 @@ import { exportJsonToExcel } from '@gpustack/core-ui/excel'; +import { useIntl } from '@umijs/max'; import dayjs from 'dayjs'; import _ from 'lodash'; import { useEffect, useRef, useState } from 'react'; import { GroupOption } from '../config'; import { BreakdownItem, UsageFilterItem } from '../config/types'; import useQueryTimeSeriesData from '../services/use-query-timeseries-data'; +import { withDeletedMark } from '../utils/deleted-label'; + +// group dimension → the id field inside ``identity.current`` (null for deleted +// entities on the Tokens tab, so the marker degrades to just "[Deleted]"). +const GROUP_ID_KEY: Record = { + route: 'route_id', + user: 'user_id', + api_key: 'api_key_id' +}; const DefaultDateConfig = { defaultRange: 29 @@ -74,6 +84,7 @@ export const useUsageFilters = ({ autoFetchOnFilterChange = true, onFetchData }: UseUsageFiltersParams) => { + const intl = useIntl(); const { activeRoutes: initialActiveRoutes = [], activeApiKeys: initialActiveApiKeys = [], @@ -288,12 +299,21 @@ export const useUsageFilters = ({ const dateMap: Record> = {}; const groupLabels = new Set(); + const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' }); items.forEach((item) => { const date = item.date?.value; if (!date) return; + const groupEntity = groupDim + ? (item[groupDim] as UsageFilterItem) + : undefined; const groupLabel = groupDim - ? ((item[groupDim] as UsageFilterItem)?.label ?? '-') + ? withDeletedMark( + groupEntity?.label ?? '-', + groupEntity?.deleted, + deletedWord, + groupEntity?.identity?.current?.[GROUP_ID_KEY[groupDim]] + ) : metric; groupLabels.add(groupLabel); if (!dateMap[date]) dateMap[date] = { date }; diff --git a/src/pages/usage/instances-tab/index.tsx b/src/pages/usage/instances-tab/index.tsx index bc83126b..89df42bb 100644 --- a/src/pages/usage/instances-tab/index.tsx +++ b/src/pages/usage/instances-tab/index.tsx @@ -27,6 +27,7 @@ import ResourceFilterBar from '../components/resource-filter-bar'; import useResourceMeta from '../hooks/use-resource-meta'; import { exportBreakdownSheets, + markDeletedNames, toExportColumns } from '../utils/export-breakdown'; import { @@ -302,9 +303,13 @@ const GpuInstancesTab: React.FC = () => { }) ) ); + const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' }); exportBreakdownSheets( tableExportGroups.map((g, i) => ({ - rows: results[i]?.items ?? [], + // The Instance Types sheet's name (``gpu_type``) is already marked by + // the adapter; the instance / user sheets keep the clean name (the + // table renders a tag) so mark it here for the export only. + rows: markDeletedNames(results[i]?.items ?? [], g.key, deletedWord), columns: toExportColumns(g.columns), sheetName: g.sheetName })), diff --git a/src/pages/usage/storage-tab/index.tsx b/src/pages/usage/storage-tab/index.tsx index ded6badf..2f8fb389 100644 --- a/src/pages/usage/storage-tab/index.tsx +++ b/src/pages/usage/storage-tab/index.tsx @@ -30,6 +30,7 @@ import ResourceFilterBar from '../components/resource-filter-bar'; import useResourceMeta from '../hooks/use-resource-meta'; import { exportBreakdownSheets, + markDeletedNames, toExportColumns } from '../utils/export-breakdown'; import { @@ -289,9 +290,10 @@ const StorageTab: React.FC = () => { }) ) ); + const deletedWord = intl.formatMessage({ id: 'usage.table.deleted' }); exportBreakdownSheets( tableExportGroups.map((g, i) => ({ - rows: results[i]?.items ?? [], + rows: markDeletedNames(results[i]?.items ?? [], g.key, deletedWord), columns: toExportColumns(g.columns), sheetName: g.sheetName })), diff --git a/src/pages/usage/utils/deleted-label.ts b/src/pages/usage/utils/deleted-label.ts new file mode 100644 index 00000000..baca7951 --- /dev/null +++ b/src/pages/usage/utils/deleted-label.ts @@ -0,0 +1,17 @@ +/** + * Text form of the deleted marker for the usage CHARTS and EXPORTS, where a + * Tag component can't be used (chart legends, Excel cells). Renders + * "