feat(usage): add plugin injection points for org / user-group filters and org breakdown tab

- token & resource filter bars render a plugin slot (UsageFilterBar /
  ResourceUsageFilterBar) for the organization / user-group filters
- breakdown tabs read plugin-provided extra sub-tabs (breakdownExtraTabs /
  resourceBreakdownExtraTabs) so the enterprise plugin can inject the
  Organization sub-tab
- thread the org / user-group filter state through the token filter hook and
  the resource tabs / breakdown tables / summary / export dialog
- standardize the organization label on the principal name; resource export
  date formatted to date-only
This commit is contained in:
michelia
2026-07-14 14:35:23 +08:00
committed by jialin
parent d0b6498bda
commit efb63335e7
15 changed files with 725 additions and 91 deletions
+131 -24
View File
@@ -10,6 +10,7 @@
* Talks to the new ``/usage/gpu-instances/{meta,breakdown}`` endpoints.
*/
import useCoolColors from '@/hooks/use-cool-colors';
import { getGPUStackPlugin } from '@/plugins';
import { formatLargeNumber } from '@/utils';
import { SimpleCard } from '@gpustack/core-ui';
import { useAccess, useIntl } from '@umijs/max';
@@ -44,6 +45,29 @@ type Scope = 'self' | 'all';
type Metric = 'gpu_hours' | 'instance_hours';
type GroupKey = 'gpu_type' | 'instance' | 'user';
// Enterprise-provided extra bottom sub-tab (e.g. the Organization breakdown).
// Registered on the enterprise plugin under ``usage.resourceBreakdownExtraTabs``;
// empty when no plugin is loaded, so the OSS build renders nothing extra.
interface ResourceBreakdownExtraTab {
key: string;
labelId: string;
useVisible?: () => boolean;
Component: React.ComponentType<{
tab: 'gpu-instances' | 'storage';
dateRange: [dayjs.Dayjs, dayjs.Dayjs];
scope: Scope;
filters: {
creator_ids?: number[];
instance_ids?: number[];
volume_ids?: number[];
organization_ids?: number[];
user_group_ids?: number[];
};
pageResetKey?: number;
refreshKey?: number;
}>;
}
const GpuInstancesTab: React.FC = () => {
const access = useAccess();
const intl = useIntl();
@@ -99,6 +123,11 @@ const GpuInstancesTab: React.FC = () => {
]);
const [selectedUsers, setSelectedUsers] = useState<number[]>([]);
const [selectedInstances, setSelectedInstances] = useState<number[]>([]);
// Platform-wide "All" view only (enterprise-gated); empty otherwise.
const [selectedOrganizations, setSelectedOrganizations] = useState<number[]>(
[]
);
const [selectedUserGroups, setSelectedUserGroups] = useState<number[]>([]);
const [refreshKey, setRefreshKey] = useState(0);
const [metric, setMetric] = useState<Metric>('gpu_hours');
const [granularity, setGranularity] = useState<Granularity>('day');
@@ -106,10 +135,25 @@ const GpuInstancesTab: React.FC = () => {
const [chartGroupBy, setChartGroupBy] = useState<GroupKey | null>(null);
// ``null`` group_by = no row grouping, just the summary KPIs.
// The chart needs the ``date`` group; tables use the active table tab.
const [activeTableTab, setActiveTableTab] = useState<GroupKey>('instance');
// Widened to ``string``: enterprise extra tabs use arbitrary keys.
const [activeTableTab, setActiveTableTab] = useState<string>('instance');
const { creators: userOptions, instances: instanceOptions } =
useResourceMeta(scope);
const {
creators: userOptions,
instances: instanceOptions,
organizations,
user_groups: userGroups
} = useResourceMeta(scope);
// Enterprise-provided extra bottom sub-tabs (empty in the OSS build). Call
// each descriptor's ``useVisible`` in a stable order — the descriptor list is
// registered once at plugin init, so its length never changes (rules of
// hooks).
const extraBreakdownTabs: ResourceBreakdownExtraTab[] =
getGPUStackPlugin()?.usage?.resourceBreakdownExtraTabs ?? [];
const extraTabVisible = extraBreakdownTabs.map(
(t) => t.useVisible?.() ?? true
);
// The daily chart fetches group_by=date here; each bottom table owns its own
// fetch (group_by=tab key) inside InstancesBreakdownTable. Bumped on any
@@ -128,11 +172,20 @@ const GpuInstancesTab: React.FC = () => {
scope,
granularity,
filters:
selectedUsers.length || selectedInstances.length
selectedUsers.length ||
selectedInstances.length ||
selectedOrganizations.length ||
selectedUserGroups.length
? {
...(selectedUsers.length ? { creator_ids: selectedUsers } : {}),
...(selectedInstances.length
? { instance_ids: selectedInstances }
: {}),
...(selectedOrganizations.length
? { organization_ids: selectedOrganizations }
: {}),
...(selectedUserGroups.length
? { user_group_ids: selectedUserGroups }
: {})
}
: undefined,
@@ -159,6 +212,8 @@ const GpuInstancesTab: React.FC = () => {
dateRange,
selectedUsers,
selectedInstances,
selectedOrganizations,
selectedUserGroups,
granularity,
chartGroupBy,
refreshKey
@@ -395,6 +450,18 @@ const GpuInstancesTab: React.FC = () => {
},
placeholder: intl.formatMessage({ id: 'usage.filter.instance' })
}}
organizationOptions={organizations}
userGroupOptions={userGroups}
selectedOrganizations={selectedOrganizations}
selectedUserGroups={selectedUserGroups}
onOrganizationsChange={(ids) => {
setSelectedOrganizations(ids);
setPageResetKey((k) => k + 1);
}}
onUserGroupsChange={(ids) => {
setSelectedUserGroups(ids);
setPageResetKey((k) => k + 1);
}}
onRefresh={() => setRefreshKey((k) => k + 1)}
onExportChart={() => setExportMode('chart')}
onExportTable={handleExportTable}
@@ -435,26 +502,62 @@ const GpuInstancesTab: React.FC = () => {
{/* Bottom tabs + table */}
<Tabs
activeKey={activeTableTab}
onChange={(k) => setActiveTableTab(k as GroupKey)}
items={TABLE_TABS.map((t) => ({
key: t.key,
label: t.label,
// Keep every pane mounted so each table holds its own page/sort and
// switching tabs neither refetches nor resets the others.
forceRender: true,
children: (
<InstancesBreakdownTable
key={t.key}
groupKey={t.key}
dateRange={dateRange}
scope={scope}
selectedUsers={selectedUsers}
selectedInstances={selectedInstances}
pageResetKey={pageResetKey}
refreshKey={refreshKey}
/>
)
}))}
onChange={(k) => setActiveTableTab(k)}
items={[
...TABLE_TABS.map((t) => ({
key: t.key,
label: t.label,
// Keep every pane mounted so each table holds its own page/sort and
// switching tabs neither refetches nor resets the others.
forceRender: true,
children: (
<InstancesBreakdownTable
key={t.key}
groupKey={t.key}
dateRange={dateRange}
scope={scope}
selectedUsers={selectedUsers}
selectedInstances={selectedInstances}
selectedOrganizations={selectedOrganizations}
selectedUserGroups={selectedUserGroups}
pageResetKey={pageResetKey}
refreshKey={refreshKey}
/>
)
})),
// Enterprise Organization breakdown sub-tab(s) — appended after the
// built-in tabs; nothing here in the OSS build.
...extraBreakdownTabs
.filter((_, i) => extraTabVisible[i])
.map((t) => ({
key: t.key,
label: intl.formatMessage({ id: t.labelId }),
forceRender: true,
children: (
<t.Component
tab="gpu-instances"
dateRange={dateRange}
scope={scope}
filters={{
...(selectedUsers.length
? { creator_ids: selectedUsers }
: {}),
...(selectedInstances.length
? { instance_ids: selectedInstances }
: {}),
...(selectedOrganizations.length
? { organization_ids: selectedOrganizations }
: {}),
...(selectedUserGroups.length
? { user_group_ids: selectedUserGroups }
: {})
}}
pageResetKey={pageResetKey}
refreshKey={refreshKey}
/>
)
}))
]}
/>
<ResourceExportData
@@ -477,6 +580,10 @@ const GpuInstancesTab: React.FC = () => {
initialDateRange={dateRange}
initialSelectedUsers={selectedUsers}
initialSelectedResources={selectedInstances}
organizationOptions={organizations}
userGroupOptions={userGroups}
initialSelectedOrganizations={selectedOrganizations}
initialSelectedUserGroups={selectedUserGroups}
deletedNameFields={[
// The row's ``deleted`` is the grouped instance; the owner user
// carries its own ``user_deleted``.