From 6af18275c4f17d08c7ebe6dc5a90a937db7876b1 Mon Sep 17 00:00:00 2001 From: gitlawr Date: Mon, 25 May 2026 10:38:50 +0800 Subject: [PATCH] feat(usage): treat org owners as managers on the Usage page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Usage page gated cross-user features (default ``scope=all``, the user filter, the per-user breakdown dimension) on ``currentUser.is_admin`` alone. The backend's ``_can_use_all_scope`` already grants the same surface to Org owners — the page just never asked. Result: a non-admin Org owner landed on ``scope=self`` with no user filter and no per-user group-by, even though the API would have happily served them the org-wide picture. Switch the three sites to ``useAccess().canSeeOrgAdmin``, which already encodes "platform admin OR owner of the selected (non-Personal) Org". One predicate, no new helper. Sites: - ``index.tsx``: ``initialScope`` defaults to ``all`` for managers. - ``filter-bar.tsx``: the user filter + nested API-key cascader surface (vs the flat API-key picker for members). - ``daily-usage.tsx``: the ``user`` group-by option in the chart. --- src/pages/usage/components/daily-usage.tsx | 12 ++++++++---- src/pages/usage/components/filter-bar.tsx | 15 +++++++++------ src/pages/usage/index.tsx | 11 ++++++----- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/pages/usage/components/daily-usage.tsx b/src/pages/usage/components/daily-usage.tsx index def1cd58..dc0e3280 100644 --- a/src/pages/usage/components/daily-usage.tsx +++ b/src/pages/usage/components/daily-usage.tsx @@ -1,7 +1,7 @@ import useCoolColors from '@/hooks/use-cool-colors'; import BarChart from '@/pages/_components/bar-chart'; import { BaseSelect, CardWrapper } from '@gpustack/core-ui'; -import { useIntl, useModel } from '@umijs/max'; +import { useAccess, useIntl } from '@umijs/max'; import { Segmented } from 'antd'; import dayjs from 'dayjs'; import React, { useMemo } from 'react'; @@ -80,8 +80,12 @@ const DailyUsage: React.FC = (props) => { onGranularityChange } = props; const generateCoolColors = useCoolColors(); - const { initialState } = useModel('@@initialState'); - const { currentUser } = initialState || {}; + // ``canSeeOrgAdmin`` widens to Org owners of the selected Org + // (Personal Org excluded). Mirrors the BE's + // ``_can_use_all_scope`` gate for the per-user breakdown + // dimension. + const access = useAccess(); + const canGroupByUser = !!access.canSeeOrgAdmin; const labelFormatter = (v: any) => { if (granularity === 'month') { @@ -253,7 +257,7 @@ const DailyUsage: React.FC = (props) => { value: item.value })) .filter((option) => { - if (currentUser?.is_admin) { + if (canGroupByUser) { return true; } return option.value !== 'user'; diff --git a/src/pages/usage/components/filter-bar.tsx b/src/pages/usage/components/filter-bar.tsx index 1d8500da..c909fef9 100644 --- a/src/pages/usage/components/filter-bar.tsx +++ b/src/pages/usage/components/filter-bar.tsx @@ -1,5 +1,4 @@ import useRangePickerPreset from '@/pages/dashboard/hooks/use-rangepicker-preset'; -import { useModel } from '@@/plugin-model'; import { DownloadOutlined, SyncOutlined } from '@ant-design/icons'; import { AutoTooltip, @@ -7,7 +6,7 @@ import { IconFont, SimpleSelect } from '@gpustack/core-ui'; -import { useIntl } from '@umijs/max'; +import { useAccess, useIntl } from '@umijs/max'; import { Button, DatePicker, Dropdown, MenuProps } from 'antd'; import dayjs from 'dayjs'; import React from 'react'; @@ -119,8 +118,12 @@ const FilterBar: React.FC = (props) => { ] }); - const initialInfo = useModel('@@initialState'); - const { initialState } = initialInfo || {}; + // ``canSeeOrgAdmin`` already encodes "platform admin OR owner of the + // selected (non-Personal) Org" via the access seam. Mirrors the + // backend's ``_can_use_all_scope`` predicate for the user-filter + // drill-down surface. + const access = useAccess(); + const canManageUsers = !!access.canSeeOrgAdmin; const exportMenuItems: MenuProps['items'] = [ { @@ -252,7 +255,7 @@ const FilterBar: React.FC = (props) => { optionLabelRender={singleOptionRender} onChange={onRoutesChange} /> - {initialState?.currentUser?.is_admin && ( + {canManageUsers && ( <> = (props) => { )} - {!initialState?.currentUser?.is_admin && ( + {!canManageUsers && ( { const intl = useIntl(); - const initialInfo = useModel('@@initialState'); - const { initialState } = initialInfo || {}; + const access = useAccess(); const { exportTable } = useExportTable(); const [openExportModal, setOpenExportModal] = useState(false); const [breakdownRefreshKey, setBreakdownRefreshKey] = useState(0); @@ -66,7 +64,10 @@ const Usage: React.FC = () => { const { filters, commonFilters, fetchData, timeSeriesData, filterBar } = useUsageFilters({ - initialScope: initialState?.currentUser?.is_admin ? 'all' : 'self', + // ``canSeeOrgAdmin`` widens to Org owners of the selected Org in + // the enterprise build (Personal Org excluded). Mirrors the BE's + // ``_can_use_all_scope`` gate one-to-one. + initialScope: access.canSeeOrgAdmin ? 'all' : 'self', metaData, chartFilters, summaryColumns