From 4b14fa6ca8c7b4bfac6802146d4af5eec65dba89 Mon Sep 17 00:00:00 2001 From: Yuxing Deng Date: Fri, 22 May 2026 17:30:20 +0800 Subject: [PATCH] feat(usage): plugin slot for Models tab extra columns Lets enterprise plugins contribute per-route columns and a global mount point on the Usage page's Models tab. Mirrors the existing modelRoutes pattern: `usage.modelsExtraColumns` for cells, `` for the page-level data lifecycle (receives the visible route ids so a plugin can bulk-fetch per-row data in one call). --- src/pages/usage/hooks/use-export-table.tsx | 6 ++- src/pages/usage/hooks/use-models-columns.tsx | 41 ++++++++++++++++++-- src/pages/usage/tables/models-table.tsx | 24 +++++++++++- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/pages/usage/hooks/use-export-table.tsx b/src/pages/usage/hooks/use-export-table.tsx index 82b90281..b68297c7 100644 --- a/src/pages/usage/hooks/use-export-table.tsx +++ b/src/pages/usage/hooks/use-export-table.tsx @@ -12,7 +12,11 @@ import useUsersColumns from './use-users-columns'; type ColumnLike = { title: any; - dataIndex: string | string[]; + // Plugin-contributed columns may omit `dataIndex` — they render via a + // bound component and aren't tied to a single row field. The export + // path skips those entries; an exported sheet has no place for a + // computed cell anyway. + dataIndex?: string | string[]; key?: string; }; diff --git a/src/pages/usage/hooks/use-models-columns.tsx b/src/pages/usage/hooks/use-models-columns.tsx index 0f47f4ac..2c063a37 100644 --- a/src/pages/usage/hooks/use-models-columns.tsx +++ b/src/pages/usage/hooks/use-models-columns.tsx @@ -1,18 +1,50 @@ // columns.ts +import { getGPUStackPlugin } from '@/plugins'; 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'; -const useModelsColumns = (): Array<{ - title: string; - dataIndex: string | string[]; +// Plugin slot: enterprise plugins can contribute extra columns to the +// usage breakdown Models table via `usage.modelsExtraColumns`. Each +// entry renders a per-row cell keyed off `record.route.identity.current +// .route_id`. `placement` decides where in the existing column order +// the column lands (currently only `before-last-active` is supported, +// which is the natural spot for per-route quota / usage indicators). +export type UsageModelsPluginColumn = { key: string; -}> => { + titleId: string; + placement?: 'before-last-active'; + render: (record: ListItem) => React.ReactNode; +}; + +type ColumnDef = { + title: React.ReactNode; + dataIndex?: string | string[]; + key: string; + sorter?: boolean; + ellipsis?: { showTitle: boolean }; + render?: (text: any, record: ListItem) => React.ReactNode; +}; + +const useModelsColumns = (): ColumnDef[] => { const intl = useIntl(); return useMemo(() => { + const pluginColumns: UsageModelsPluginColumn[] = + getGPUStackPlugin()?.usage?.modelsExtraColumns ?? []; + + const pluginCols: ColumnDef[] = pluginColumns.map((c) => ({ + title: intl.formatMessage({ id: c.titleId }), + key: c.key, + render: (_text: any, record: ListItem) => c.render(record) + })); + const beforeLastActive = pluginCols.filter( + (_c, i) => + (pluginColumns[i].placement ?? 'before-last-active') === + 'before-last-active' + ); return [ { title: intl.formatMessage({ id: 'common.table.name' }), @@ -95,6 +127,7 @@ const useModelsColumns = (): Array<{ }, render: (text: number) => {text} }, + ...beforeLastActive, { title: intl.formatMessage({ id: 'usage.table.lastActive' }), dataIndex: 'last_active', diff --git a/src/pages/usage/tables/models-table.tsx b/src/pages/usage/tables/models-table.tsx index 3fc333cf..bb2f5bef 100644 --- a/src/pages/usage/tables/models-table.tsx +++ b/src/pages/usage/tables/models-table.tsx @@ -1,10 +1,11 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import { TABLE_SORT_DIRECTIONS } from '@/config/settings'; import PageBox from '@/pages/_components/page-box'; import { IconFont, NoResult } from '@gpustack/core-ui'; import { useIntl } from '@umijs/max'; import { Table } from 'antd'; import _ from 'lodash'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { BreakdownItem, FilterOptionType } from '../config/types'; import useModelsColumns from '../hooks/use-models-columns'; import useQueryBreakdownList from '../services/use-query-breakdown-list'; @@ -107,6 +108,22 @@ const Models: React.FC<{ scope ]); + // Route ids currently visible in the Models tab. Mirrored to the + // plugin slot below so enterprise plugins can bulk-fetch per-route + // data (e.g. the caller's quota / usage on each route) in one call + // rather than firing N round-trips from each cell. `refreshToken` + // bumps with every successful fetch so plugins re-pull even when the + // id set hasn't changed. + const pluginContext = useMemo( + () => ({ + routeIds: (dataSource.dataList || []) + .map((item) => item.route?.identity?.current?.route_id) + .filter((id): id is number => id != null), + refreshToken: refreshKey + }), + [dataSource.dataList, refreshKey] + ); + return ( <> @@ -134,6 +151,11 @@ const Models: React.FC<{ }} > + {/* Page-level data lifecycle for plugin-contributed extra + columns on this tab. Receives the visible route ids so the + plugin can bulk-fetch per-route data in one call; renders + nothing when no plugin is registered. */} + ); };