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
+132 -25
View File
@@ -12,6 +12,7 @@
* so there's no phase filter — just date / scope / volume / user.
*/
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';
@@ -47,6 +48,29 @@ type Scope = 'self' | 'all';
type Metric = 'storage_gb_days' | 'storage_gb_hours';
type GroupKey = 'volume' | '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 StorageTab: React.FC = () => {
const access = useAccess();
const intl = useIntl();
@@ -96,15 +120,35 @@ const StorageTab: React.FC = () => {
]);
const [selectedUsers, setSelectedUsers] = useState<number[]>([]);
const [selectedVolumes, setSelectedVolumes] = 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>('storage_gb_days');
const [granularity, setGranularity] = useState<Granularity>('day');
// Optional trend group-by (split the chart into one series per group).
const [chartGroupBy, setChartGroupBy] = useState<GroupKey | null>(null);
const [activeTableTab, setActiveTableTab] = useState<GroupKey>('volume');
// Widened to ``string``: enterprise extra tabs use arbitrary keys.
const [activeTableTab, setActiveTableTab] = useState<string>('volume');
const { creators: userOptions, volumes: volumeOptions } =
useResourceMeta(scope);
const {
creators: userOptions,
volumes: volumeOptions,
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
);
// Bumped on any filter change to snap every mounted table back to page 1;
// each table owns its own page/sort state otherwise.
@@ -122,10 +166,19 @@ const StorageTab: React.FC = () => {
scope,
granularity,
filters:
selectedUsers.length || selectedVolumes.length
selectedUsers.length ||
selectedVolumes.length ||
selectedOrganizations.length ||
selectedUserGroups.length
? {
...(selectedUsers.length ? { creator_ids: selectedUsers } : {}),
...(selectedVolumes.length ? { volume_ids: selectedVolumes } : {})
...(selectedVolumes.length ? { volume_ids: selectedVolumes } : {}),
...(selectedOrganizations.length
? { organization_ids: selectedOrganizations }
: {}),
...(selectedUserGroups.length
? { user_group_ids: selectedUserGroups }
: {})
}
: undefined,
page: 1,
@@ -152,6 +205,8 @@ const StorageTab: React.FC = () => {
dateRange,
selectedUsers,
selectedVolumes,
selectedOrganizations,
selectedUserGroups,
granularity,
chartGroupBy,
refreshKey
@@ -378,6 +433,18 @@ const StorageTab: React.FC = () => {
},
placeholder: intl.formatMessage({ id: 'usage.filter.storage' })
}}
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}
@@ -415,26 +482,62 @@ const StorageTab: React.FC = () => {
<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 other.
forceRender: true,
children: (
<StorageBreakdownTable
key={t.key}
groupKey={t.key}
dateRange={dateRange}
scope={scope}
selectedUsers={selectedUsers}
selectedVolumes={selectedVolumes}
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 other.
forceRender: true,
children: (
<StorageBreakdownTable
key={t.key}
groupKey={t.key}
dateRange={dateRange}
scope={scope}
selectedUsers={selectedUsers}
selectedVolumes={selectedVolumes}
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="storage"
dateRange={dateRange}
scope={scope}
filters={{
...(selectedUsers.length
? { creator_ids: selectedUsers }
: {}),
...(selectedVolumes.length
? { volume_ids: selectedVolumes }
: {}),
...(selectedOrganizations.length
? { organization_ids: selectedOrganizations }
: {}),
...(selectedUserGroups.length
? { user_group_ids: selectedUserGroups }
: {})
}}
pageResetKey={pageResetKey}
refreshKey={refreshKey}
/>
)
}))
]}
/>
<ResourceExportData
@@ -457,6 +560,10 @@ const StorageTab: React.FC = () => {
initialDateRange={dateRange}
initialSelectedUsers={selectedUsers}
initialSelectedResources={selectedVolumes}
organizationOptions={organizations}
userGroupOptions={userGroups}
initialSelectedOrganizations={selectedOrganizations}
initialSelectedUserGroups={selectedUserGroups}
deletedNameFields={[
// The row's ``deleted`` is the grouped volume; the owner user
// carries its own ``user_deleted``.
@@ -28,6 +28,9 @@ interface Props {
scope: Scope;
selectedUsers: number[];
selectedVolumes: number[];
// Platform-wide "All" view only (enterprise-gated); empty otherwise.
selectedOrganizations?: number[];
selectedUserGroups?: number[];
// Bumped by the parent when a filter changes, so each mounted table snaps
// back to page 1 independently.
pageResetKey?: number;
@@ -46,6 +49,8 @@ const StorageBreakdownTable: React.FC<Props> = ({
scope,
selectedUsers,
selectedVolumes,
selectedOrganizations = [],
selectedUserGroups = [],
pageResetKey = 0,
refreshKey = 0
}) => {
@@ -105,10 +110,21 @@ const StorageBreakdownTable: React.FC<Props> = ({
end_date: dateRange[1].format('YYYY-MM-DD'),
scope,
filters:
selectedUsers.length || selectedVolumes.length
selectedUsers.length ||
selectedVolumes.length ||
selectedOrganizations.length ||
selectedUserGroups.length
? {
...(selectedUsers.length ? { creator_ids: selectedUsers } : {}),
...(selectedVolumes.length ? { volume_ids: selectedVolumes } : {})
...(selectedVolumes.length
? { volume_ids: selectedVolumes }
: {}),
...(selectedOrganizations.length
? { organization_ids: selectedOrganizations }
: {}),
...(selectedUserGroups.length
? { user_group_ids: selectedUserGroups }
: {})
}
: undefined,
group_by: [groupKey],
@@ -122,6 +138,8 @@ const StorageBreakdownTable: React.FC<Props> = ({
scope,
selectedUsers,
selectedVolumes,
selectedOrganizations,
selectedUserGroups,
queryParams.page,
queryParams.perPage,
queryParams.sort_by,