diff --git a/src/pages/usage/apis/resource.ts b/src/pages/usage/apis/resource.ts index 68fe4694..f65e6213 100644 --- a/src/pages/usage/apis/resource.ts +++ b/src/pages/usage/apis/resource.ts @@ -20,6 +20,10 @@ export interface ResourceUsageFilters { instance_ids?: number[]; gpu_types?: string[]; volume_ids?: number[]; + // Platform-wide "All" view only (backend-gated): consumer-Org ids and + // user-group ids (expanded server-side to the groups' direct members). + organization_ids?: number[]; + user_group_ids?: number[]; } export interface ResourceBreakdownRequest { @@ -65,6 +69,10 @@ export interface ResourceBreakdownItem extends ResourceBreakdownSummary { volume_name?: string; user_id?: number; user_name?: string; + // Organization grouping (platform-wide "All" view). ``organization_name`` + // is resolved live server-side; a gone Org sets ``deleted``. + organization_id?: number; + organization_name?: string; // The grouped entity (instance / volume / user) no longer exists. The name // fields keep the clean (stale) name; the tables show a DeletedTag off this // flag plus the id, matching the Tokens tab. @@ -261,6 +269,7 @@ const GROUP_BY_MAP: Record = { instance: 'instance', volume: 'volume', user: 'user', + organization: 'organization', date: 'date' }; @@ -334,6 +343,10 @@ function flattenItem( flat.user_name = rawKey; flat.user_id = id; break; + case 'organization': + flat.organization_name = rawKey; + flat.organization_id = id; + break; default: break; } @@ -393,7 +406,13 @@ function flattenResponse( function toServerRequest(data: ResourceBreakdownRequest) { const groupByList = data.group_by?.length ? data.group_by : ['resource_type']; - const { creator_ids, instance_ids, volume_ids } = data.filters ?? {}; + const { + creator_ids, + instance_ids, + volume_ids, + organization_ids, + user_group_ids + } = data.filters ?? {}; // The non-date dimension drives response flattening into the right field. const dim = groupByList.find((g) => g !== 'date'); return { @@ -408,6 +427,8 @@ function toServerRequest(data: ResourceBreakdownRequest) { ...(creator_ids?.length ? { creator_ids } : {}), ...(instance_ids?.length ? { instance_ids } : {}), ...(volume_ids?.length ? { volume_ids } : {}), + ...(organization_ids?.length ? { organization_ids } : {}), + ...(user_group_ids?.length ? { user_group_ids } : {}), ...(data.order_by ? { order_by: data.order_by } : {}), ...(data.descending !== undefined ? { descending: data.descending } : {}), page: data.page ?? 1, @@ -477,6 +498,8 @@ export async function queryResourceEvents( options?: { skipErrorHandler?: boolean; token?: any } ): Promise { const creatorIds = data.filters?.creator_ids; + const organizationIds = data.filters?.organization_ids; + const userGroupIds = data.filters?.user_group_ids; return request(URL.EVENTS, { params: { start_date: data.start_date, @@ -486,6 +509,12 @@ export async function queryResourceEvents( // GET endpoints take list params as CSV strings (avoids axios array // serialization quirks); the server splits them back into lists. ...(creatorIds?.length ? { creator_ids: creatorIds.join(',') } : {}), + ...(organizationIds?.length + ? { organization_ids: organizationIds.join(',') } + : {}), + ...(userGroupIds?.length + ? { user_group_ids: userGroupIds.join(',') } + : {}), ...(data.event_types?.length ? { event_types: data.event_types.join(',') } : {}), @@ -503,12 +532,18 @@ export interface ResourceFilterOption { id: number; label: string; deleted?: boolean; + // ``org`` / ``user`` / ``group`` — only set on organization options so the + // filter dropdown can tag a personal (USER) consumer. + kind?: string; } export interface ResourceFilterMeta { creators: ResourceFilterOption[]; instances: ResourceFilterOption[]; volumes: ResourceFilterOption[]; + // Platform-wide "All" view only (backend returns them empty otherwise). + organizations: ResourceFilterOption[]; + user_groups: ResourceFilterOption[]; } export async function queryResourceFilterMeta( @@ -521,7 +556,9 @@ export async function queryResourceFilterMeta( return { creators: res.creators || [], instances: res.instances || [], - volumes: res.volumes || [] + volumes: res.volumes || [], + organizations: res.organizations || [], + user_groups: res.user_groups || [] }; } @@ -531,10 +568,12 @@ export async function queryUsageSummary( end_date: string; scope?: 'self' | 'all'; creator_ids?: number[]; + organization_ids?: number[]; + user_group_ids?: number[]; }, options?: { token?: any } ): Promise { - const { creator_ids, ...rest } = params; + const { creator_ids, organization_ids, user_group_ids, ...rest } = params; const res = await request<{ total_tokens: number; input_tokens: number; @@ -548,7 +587,13 @@ export async function queryUsageSummary( params: { ...rest, scope: params.scope ?? 'all', - ...(creator_ids?.length ? { creator_ids: creator_ids.join(',') } : {}) + ...(creator_ids?.length ? { creator_ids: creator_ids.join(',') } : {}), + ...(organization_ids?.length + ? { organization_ids: organization_ids.join(',') } + : {}), + ...(user_group_ids?.length + ? { user_group_ids: user_group_ids.join(',') } + : {}) }, method: 'GET', cancelToken: options?.token @@ -565,7 +610,17 @@ export async function queryUsageSummary( end_date: params.end_date, scope: params.scope ?? 'all', group_by: ['gpu_type'], - ...(creator_ids?.length ? { filters: { creator_ids } } : {}), + ...(creator_ids?.length || + organization_ids?.length || + user_group_ids?.length + ? { + filters: { + ...(creator_ids?.length ? { creator_ids } : {}), + ...(organization_ids?.length ? { organization_ids } : {}), + ...(user_group_ids?.length ? { user_group_ids } : {}) + } + } + : {}), page: 1, perPage: 100 }, diff --git a/src/pages/usage/components/filter-bar.tsx b/src/pages/usage/components/filter-bar.tsx index 55e26f47..1474868e 100644 --- a/src/pages/usage/components/filter-bar.tsx +++ b/src/pages/usage/components/filter-bar.tsx @@ -1,3 +1,4 @@ +import PluginExtraFields from '@/components/plugin-extra-fields'; import useRangePickerPreset from '@/pages/dashboard/hooks/use-rangepicker-preset'; import { DownloadOutlined, SyncOutlined } from '@ant-design/icons'; import { @@ -37,6 +38,14 @@ interface FilterBarProps { routeOptions: OptionType[]; userOptions: OptionType[]; apiKeyOptions: GroupOption[]; + // Platform-wide "All" view only; empty otherwise (backend-gated). Rendered + // by the enterprise ``UsageFilterBar`` slot. + organizationOptions?: OptionType[]; + userGroupOptions?: OptionType[]; + selectedOrganizations?: string[]; + selectedUserGroups?: string[]; + onOrganizationsChange?: (value: string[]) => void; + onUserGroupsChange?: (value: string[]) => void; activeApiKeys: valueType[][]; handlePickerChange: (picker: DateType) => void; onScopeChange: (value: string) => void; @@ -77,6 +86,12 @@ const FilterBar: React.FC = (props) => { onRoutesChange, onUsersChange, onApiKeysChange, + organizationOptions, + userGroupOptions, + selectedOrganizations, + selectedUserGroups, + onOrganizationsChange, + onUserGroupsChange, onExportChart, onExportTable, handleSearch @@ -345,6 +360,21 @@ const FilterBar: React.FC = (props) => { onChange={onApiKeysChange} /> )} + {/* Enterprise-only Organization / User Group filters (platform-wide + "All" view). Renders nothing when no plugin is registered or the + backend returned no options. */} +