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, `<PluginExtraFields name="UsageModelsPageGlobal" />` for the page-level data lifecycle (receives the visible route ids so a plugin can bulk-fetch per-row data in one call).
This commit is contained in:
@@ -12,7 +12,11 @@ import useUsersColumns from './use-users-columns';
|
|||||||
|
|
||||||
type ColumnLike = {
|
type ColumnLike = {
|
||||||
title: any;
|
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;
|
key?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,50 @@
|
|||||||
// columns.ts
|
// columns.ts
|
||||||
|
import { getGPUStackPlugin } from '@/plugins';
|
||||||
import { AutoTooltip } from '@gpustack/core-ui';
|
import { AutoTooltip } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Tag } from 'antd';
|
import { Tag } from 'antd';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { BreakdownItem as ListItem } from '../config/types';
|
import { BreakdownItem as ListItem } from '../config/types';
|
||||||
|
|
||||||
const useModelsColumns = (): Array<{
|
// Plugin slot: enterprise plugins can contribute extra columns to the
|
||||||
title: string;
|
// usage breakdown Models table via `usage.modelsExtraColumns`. Each
|
||||||
dataIndex: string | string[];
|
// 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;
|
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();
|
const intl = useIntl();
|
||||||
|
|
||||||
return useMemo(() => {
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'common.table.name' }),
|
title: intl.formatMessage({ id: 'common.table.name' }),
|
||||||
@@ -95,6 +127,7 @@ const useModelsColumns = (): Array<{
|
|||||||
},
|
},
|
||||||
render: (text: number) => <AutoTooltip ghost>{text}</AutoTooltip>
|
render: (text: number) => <AutoTooltip ghost>{text}</AutoTooltip>
|
||||||
},
|
},
|
||||||
|
...beforeLastActive,
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||||
dataIndex: 'last_active',
|
dataIndex: 'last_active',
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||||
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||||
import PageBox from '@/pages/_components/page-box';
|
import PageBox from '@/pages/_components/page-box';
|
||||||
import { IconFont, NoResult } from '@gpustack/core-ui';
|
import { IconFont, NoResult } from '@gpustack/core-ui';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { BreakdownItem, FilterOptionType } from '../config/types';
|
import { BreakdownItem, FilterOptionType } from '../config/types';
|
||||||
import useModelsColumns from '../hooks/use-models-columns';
|
import useModelsColumns from '../hooks/use-models-columns';
|
||||||
import useQueryBreakdownList from '../services/use-query-breakdown-list';
|
import useQueryBreakdownList from '../services/use-query-breakdown-list';
|
||||||
@@ -107,6 +108,22 @@ const Models: React.FC<{
|
|||||||
scope
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageBox>
|
<PageBox>
|
||||||
@@ -134,6 +151,11 @@ const Models: React.FC<{
|
|||||||
}}
|
}}
|
||||||
></Table>
|
></Table>
|
||||||
</PageBox>
|
</PageBox>
|
||||||
|
{/* 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. */}
|
||||||
|
<PluginExtraFields name="UsageModelsPageGlobal" context={pluginContext} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user