From e4c1a3f5c29dba4e83a2f067a181e763b26f7db0 Mon Sep 17 00:00:00 2001 From: jialin Date: Tue, 16 Jun 2026 17:56:16 +0800 Subject: [PATCH] refactor: usage instance tab, split tables --- src/pages/usage/instances-tab/index.tsx | 297 ++++-------------- .../use-query-gpu-instances-breakdown.ts | 31 ++ .../tables/instances-breakdown-table.tsx | 155 +++++++++ .../tables/use-instances-columns.tsx | 116 +++++++ src/pages/usage/storage-tab/index.tsx | 30 +- .../usage/token-tab/tables/apikeys-table.tsx | 1 + .../usage/token-tab/tables/models-table.tsx | 1 + .../usage/token-tab/tables/users-table.tsx | 1 + 8 files changed, 387 insertions(+), 245 deletions(-) create mode 100644 src/pages/usage/instances-tab/services/use-query-gpu-instances-breakdown.ts create mode 100644 src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx create mode 100644 src/pages/usage/instances-tab/tables/use-instances-columns.tsx diff --git a/src/pages/usage/instances-tab/index.tsx b/src/pages/usage/instances-tab/index.tsx index 7b1a556c..48583c6b 100644 --- a/src/pages/usage/instances-tab/index.tsx +++ b/src/pages/usage/instances-tab/index.tsx @@ -10,35 +10,30 @@ * Talks to the new ``/usage/gpu-instances/{meta,breakdown}`` endpoints. */ import useCoolColors from '@/hooks/use-cool-colors'; -import { - buildInstanceTypeRecordFromMiB, - renderInstanceType -} from '@/pages/gpu-service/instances/utils/render-instance-type'; import { formatLargeNumber } from '@/utils'; import { SimpleCard } from '@gpustack/core-ui'; import { useAccess, useIntl } from '@umijs/max'; -import { Table, Tabs } from 'antd'; +import { Tabs } from 'antd'; import dayjs from 'dayjs'; import React, { useEffect, useMemo, useState } from 'react'; import { queryGpuInstancesBreakdown, - ResourceBreakdownItem, - ResourceBreakdownRequest, - ResourceBreakdownResponse + ResourceBreakdownRequest } from '../apis/resource'; import MetricChartCard from '../components/metric-chart-card'; import MetricLabel from '../components/metric-label'; import ResourceExportData from '../components/resource-export-data'; import ResourceFilterBar from '../components/resource-filter-bar'; import useResourceMeta from '../hooks/use-resource-meta'; -import { instanceTypeLabel } from '../utils/format-instance-type'; import { bucketKey, generateBucketRange, - Granularity, - parseRollup + Granularity } from '../utils/time-buckets'; import { buildTrendSeries } from '../utils/trend-series'; +import useQueryGpuInstancesBreakdown from './services/use-query-gpu-instances-breakdown'; +import InstancesBreakdownTable from './tables/instances-breakdown-table'; +import useInstancesColumns from './tables/use-instances-columns'; type Scope = 'self' | 'all'; type Metric = 'gpu_hours' | 'instance_hours'; @@ -62,20 +57,26 @@ const GpuInstancesTab: React.FC = () => { [intl] ); - const TABLE_TABS: { key: GroupKey; label: string }[] = useMemo( - () => [ + const TABLE_TABS: { key: GroupKey; label: string }[] = useMemo(() => { + const tabs = [ { - key: 'gpu_type', + key: 'gpu_type' as GroupKey, label: intl.formatMessage({ id: 'usage.table.instanceTypes' }) }, { - key: 'instance', + key: 'instance' as GroupKey, label: intl.formatMessage({ id: 'usage.table.instances' }) - }, - { key: 'user', label: intl.formatMessage({ id: 'usage.table.users' }) } - ], - [intl] - ); + } + ]; + // Managers see the org-wide User breakdown; members only their own rows. + if (access.canSeeOrgAdmin) { + tabs.push({ + key: 'user' as GroupKey, + label: intl.formatMessage({ id: 'usage.table.users' }) + }); + } + return tabs; + }, [intl, access.canSeeOrgAdmin]); // ``useCoolColors`` returns a memoized factory; resolve a fixed 5-slot // palette for the KPI cards, and keep the factory for the grouped trend // (sized to the group count). @@ -105,21 +106,16 @@ const GpuInstancesTab: React.FC = () => { const { creators: userOptions, instances: instanceOptions } = useResourceMeta(scope); - // Two independent fetches: one for the daily chart (group_by=date), - // one for the table (group_by=tab key). Both reuse the same date / - // scope filters so the views stay in sync. - const [chartData, setChartData] = useState( - null - ); - const [tableData, setTableData] = useState( - null - ); - const [tablePage, setTablePage] = useState(1); - // Server-side sort for the bottom tables; default GPU Hours, descending. - const [tableSort, setTableSort] = useState<{ - field: Metric; - order: 'ascend' | 'descend'; - }>({ field: 'gpu_hours', order: 'descend' }); + // The daily chart fetches group_by=date here; each bottom table owns its own + // fetch (group_by=tab key) inside InstancesBreakdownTable. Bumped on any + // filter change to snap every mounted table back to page 1. + const [pageResetKey, setPageResetKey] = useState(0); + + const { + detailData: chartData, + loading: chartLoading, + fetchData: fetchChartData + } = useQueryGpuInstancesBreakdown({ key: 'gpuInstancesBreakdownChart' }); const baseRequest = (): Omit => ({ start_date: dateRange[0].format('YYYY-MM-DD'), @@ -139,39 +135,17 @@ const GpuInstancesTab: React.FC = () => { perPage: 50 }); - const fetchChart = async () => { - try { - const data = await queryGpuInstancesBreakdown({ - ...baseRequest(), - // Split each bucket by the chosen dimension when grouping. - group_by: chartGroupBy ? ['date', chartGroupBy] : ['date'], - // A trend is a time series, not a paginated table: always fetch the - // whole range. The default order is metric-desc, so partial (current/ - // recent) buckets have smaller values and would be pushed onto later - // pages — dropping the newest hours from the chart under a small page. - perPage: 10000 - }); - setChartData(data); - } catch { - // Network/auth errors surface via the global request interceptor; - // keep the previous chart so the UI doesn't flash empty. - } - }; - - const fetchTable = async () => { - try { - const data = await queryGpuInstancesBreakdown({ - ...baseRequest(), - group_by: [activeTableTab], - page: tablePage, - order_by: tableSort.field, - descending: tableSort.order === 'descend' - }); - setTableData(data); - } catch { - // Same rationale as fetchChart. - } - }; + const fetchChart = () => + fetchChartData({ + ...baseRequest(), + // Split each bucket by the chosen dimension when grouping. + group_by: chartGroupBy ? ['date', chartGroupBy] : ['date'], + // A trend is a time series, not a paginated table: always fetch the + // whole range. The default order is metric-desc, so partial (current/ + // recent) buckets have smaller values and would be pushed onto later + // pages — dropping the newest hours from the chart under a small page. + perPage: 10000 + }); useEffect(() => { fetchChart(); @@ -184,18 +158,6 @@ const GpuInstancesTab: React.FC = () => { refreshKey ]); - useEffect(() => { - fetchTable(); - }, [ - dateRange, - selectedUsers, - selectedInstances, - activeTableTab, - tablePage, - tableSort, - refreshKey - ]); - // KPI summary cards — pull from the chart summary since both queries // return the same scope-wide totals. const summary = chartData?.summary; @@ -273,116 +235,19 @@ const GpuInstancesTab: React.FC = () => { ); // Group-by options for the trend = the same dimensions as the bottom tables - // (Users only when org-wide, matching the table tabs). + // (TABLE_TABS already gates Users to org admins). const chartGroupByOptions = useMemo( () => - TABLE_TABS.filter((t) => t.key !== 'user' || scope === 'all').map( - (t) => ({ - value: t.key, - label: t.label - }) - ), - [TABLE_TABS, scope] + TABLE_TABS.map((t) => ({ + value: t.key, + label: t.label + })), + [TABLE_TABS] ); - // Table columns adapt to the active tab. - const tableColumns = useMemo(() => { - const baseValueCols = [ - { - title: intl.formatMessage({ id: 'usage.metric.gpuHours' }), - dataIndex: 'gpu_hours', - key: 'gpu_hours', - sorter: true, - sortOrder: tableSort.field === 'gpu_hours' ? tableSort.order : null, - render: (v: number) => (v ?? 0).toFixed(2) - }, - { - title: intl.formatMessage({ id: 'usage.metric.instanceHours' }), - dataIndex: 'instance_hours', - key: 'instance_hours', - sorter: true, - sortOrder: - tableSort.field === 'instance_hours' ? tableSort.order : null, - render: (v: number) => (v ?? 0).toFixed(2) - } - ]; - // Instance Types breakdown: just the pretty product name (or flavor slug - // for older rows) — no spec sub-line. - const instanceTypeColType = { - title: intl.formatMessage({ id: 'usage.table.instanceType' }), - dataIndex: 'gpu_type', - key: 'gpu_type', - render: (_v: string, row: ResourceBreakdownItem) => instanceTypeLabel(row) - }; - // Instances breakdown: render through the canonical GPU Instances list - // renderer so the label + spec popover are identical. The breakdown row - // carries flat MiB fields, so adapt it into the ListItem shape first. - const instanceTypeColInstance = { - title: intl.formatMessage({ id: 'usage.table.instanceType' }), - dataIndex: 'gpu_type', - key: 'gpu_type', - render: (_v: string, row: ResourceBreakdownItem) => - renderInstanceType( - buildInstanceTypeRecordFromMiB({ - name: row.instance_name, - product: row.product || row.gpu_type, - gpuCount: row.gpu_count, - unitCpuMilli: row.unit_cpu_milli, - unitMemoryMib: row.unit_memory_mib, - vramMib: row.vram_mib, - localStorageMib: row.local_storage_mib, - ephemeralMib: row.ephemeral_mib, - persistentMib: row.persistent_mib - }), - { intl } - ) - }; - // Last Active = the last active day. The backend sends a rollup-tz instant - // with its offset; parseRollup keeps that wall clock (no browser-tz convert), - // consistent with the trend chart buckets. Shown date-only. - const lastActiveCol = { - title: intl.formatMessage({ id: 'usage.table.lastActive' }), - dataIndex: 'last_active', - key: 'last_active', - render: (v?: string) => (v ? parseRollup(v).format('YYYY-MM-DD') : '-') - }; - if (activeTableTab === 'gpu_type') { - return [ - instanceTypeColType, - ...baseValueCols, - { - title: intl.formatMessage({ id: 'usage.metric.activeInstances' }), - dataIndex: 'active_instances', - key: 'active_instances' - }, - lastActiveCol - ]; - } - if (activeTableTab === 'instance') { - return [ - { - title: intl.formatMessage({ id: 'usage.table.instance' }), - dataIndex: 'instance_name', - key: 'instance_name' - }, - instanceTypeColInstance, - ...baseValueCols, - lastActiveCol - ]; - } - // user tab - return [ - { - title: intl.formatMessage({ id: 'usage.table.user' }), - dataIndex: 'user_name', - key: 'user_name' - }, - ...baseValueCols, - lastActiveCol - ]; - }, [activeTableTab, tableSort, intl]); - - const tableRows: ResourceBreakdownItem[] = tableData?.items ?? []; + // Columns for the export preview of the active tab (sort arrows omitted — + // the in-tab table owns its own sort state). Same factory the tables use. + const exportTableColumns = useInstancesColumns(activeTableTab); // Export opens a preview modal (matches the Tokens tab): re-filter + preview // the rows, then download. "Chart" = the by-date trend, "Table" = the active @@ -433,7 +298,7 @@ const GpuInstancesTab: React.FC = () => { } : { groupBy: [activeTableTab], - columns: tableColumns, + columns: exportTableColumns, fileName: `gpu-instances_${activeTableTab}_${dateSuffix}.xlsx`, sheetName: tabLabel || 'gpu-instances' }; @@ -445,21 +310,21 @@ const GpuInstancesTab: React.FC = () => { value={dateRange} onChange={(dates) => { setDateRange(dates); - setTablePage(1); + setPageResetKey((k) => k + 1); }} canManageUsers={canManageUsers} userOptions={userOptions} selectedUsers={selectedUsers} onUsersChange={(ids) => { setSelectedUsers(ids); - setTablePage(1); + setPageResetKey((k) => k + 1); }} resourceFilter={{ options: instanceOptions, value: selectedInstances, onChange: (ids) => { setSelectedInstances(ids); - setTablePage(1); + setPageResetKey((k) => k + 1); }, placeholder: intl.formatMessage({ id: 'usage.filter.instance' }) }} @@ -496,54 +361,30 @@ const GpuInstancesTab: React.FC = () => { groupBy={chartGroupBy} groupByOptions={chartGroupByOptions} onGroupByChange={(v) => setChartGroupBy(v as GroupKey | null)} + loading={chartLoading} /> {/* Bottom tabs + table */} { - setActiveTableTab(k as GroupKey); - setTablePage(1); - }} - items={TABLE_TABS.filter( - (t) => t.key !== 'user' || scope === 'all' - ).map((t) => ({ + 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: ( - - `${row.gpu_type ?? ''}|${row.instance_id ?? ''}|${row.user_id ?? ''}` - } + { - const s = Array.isArray(sorter) ? sorter[0] : sorter; - // Sort changed: reset to page 1. Cleared (3rd click) → default - // back to GPU Hours descending. - const next = s?.order - ? { - field: (s.columnKey as Metric) ?? 'gpu_hours', - order: s.order as 'ascend' | 'descend' - } - : { field: 'gpu_hours' as Metric, order: 'descend' as const }; - if ( - next.field !== tableSort.field || - next.order !== tableSort.order - ) { - setTableSort(next); - setTablePage(1); - } - }} - pagination={{ - size: 'middle', - current: tablePage, - pageSize: tableData?.pagination.perPage ?? 50, - total: tableData?.pagination.total ?? 0, - onChange: (p) => setTablePage(p) - }} + groupKey={t.key} + dateRange={dateRange} + scope={scope} + selectedUsers={selectedUsers} + selectedInstances={selectedInstances} + pageResetKey={pageResetKey} + refreshKey={refreshKey} /> ) }))} diff --git a/src/pages/usage/instances-tab/services/use-query-gpu-instances-breakdown.ts b/src/pages/usage/instances-tab/services/use-query-gpu-instances-breakdown.ts new file mode 100644 index 00000000..8a13b7ee --- /dev/null +++ b/src/pages/usage/instances-tab/services/use-query-gpu-instances-breakdown.ts @@ -0,0 +1,31 @@ +import { useQueryData } from '@/hooks/use-query-data-list'; +import { + queryGpuInstancesBreakdown, + ResourceBreakdownRequest, + ResourceBreakdownResponse +} from '../../apis/resource'; + +/** + * Wraps the `queryGpuInstancesBreakdown` request with shared loading state and + * in-flight cancellation (the previous request is cancelled on each new fetch + * and on unmount), so rapid filter/page/sort changes can't race a stale + * response onto the chart or a table. + */ +export default function useQueryGpuInstancesBreakdown(option?: { + key?: string; +}) { + const { detailData, loading, cancelRequest, fetchData } = useQueryData< + ResourceBreakdownResponse, + ResourceBreakdownRequest + >({ + fetchDetail: queryGpuInstancesBreakdown, + key: option?.key || 'gpuInstancesBreakdown' + }); + + return { + detailData, + loading, + cancelRequest, + fetchData + }; +} diff --git a/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx b/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx new file mode 100644 index 00000000..4f05b39d --- /dev/null +++ b/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx @@ -0,0 +1,155 @@ +import { TABLE_SORT_DIRECTIONS } from '@/config/settings'; +import { Table } from 'antd'; +import dayjs from 'dayjs'; +import React, { useEffect, useRef, useState } from 'react'; +import { ResourceBreakdownItem } from '../../apis/resource'; +import useQueryGpuInstancesBreakdown from '../services/use-query-gpu-instances-breakdown'; +import useInstancesColumns from './use-instances-columns'; + +type Scope = 'self' | 'all'; +type Metric = 'gpu_hours' | 'instance_hours'; +type GroupKey = 'gpu_type' | 'instance' | 'user'; + +const SORTABLE: Metric[] = ['gpu_hours', 'instance_hours']; + +const PER_PAGE = 50; +// sort_by encodes order as a string (`-` prefix = descending), so the fetch +// effect dedupes naturally on the primitive. The breakdown order_by takes the +// metric key directly. Default: GPU Hours descending. +const DEFAULT_SORT = '-gpu_hours'; + +interface Props { + groupKey: GroupKey; + dateRange: [dayjs.Dayjs, dayjs.Dayjs]; + scope: Scope; + selectedUsers: number[]; + selectedInstances: number[]; + // Bumped by the parent when a filter changes, so each mounted table snaps + // back to page 1 independently. + pageResetKey?: number; + refreshKey?: number; +} + +/** + * One GPU-instances breakdown table, owning its own page/sort/data. Each tab + * (GPU Type / Instance / User) mounts an instance and keeps it alive (Tabs + * `forceRender`), so each keeps its own pagination + sort and switching tabs + * neither refetches nor resets the others. + */ +const InstancesBreakdownTable: React.FC = ({ + groupKey, + dateRange, + scope, + selectedUsers, + selectedInstances, + pageResetKey = 0, + refreshKey = 0 +}) => { + const [queryParams, setQueryParams] = useState<{ + page: number; + perPage: number; + sort_by: string; + }>({ page: 1, perPage: PER_PAGE, sort_by: DEFAULT_SORT }); + const pendingPageResetRef = useRef(false); + + const { detailData, loading, fetchData } = useQueryGpuInstancesBreakdown({ + key: `gpuInstancesBreakdown-${groupKey}` + }); + + const columns = useInstancesColumns(groupKey); + + // Sort only — paging goes through handlePageChange, so this stays + // page-agnostic and the two compose (matches the Tokens tab tables). + const handleTableChange = (_pagination: any, _filters: any, sorter: any) => { + const s = Array.isArray(sorter) ? sorter[0] : sorter; + const field = s?.field as Metric; + // Cleared (3rd click) or an unsortable column → default GPU Hours desc. + const sort_by = + !s?.order || !SORTABLE.includes(field) + ? DEFAULT_SORT + : s.order === 'ascend' + ? field + : `-${field}`; + // Only a real sort change snaps back to page 1; an unchanged sort_by + // (e.g. the onChange that fires alongside a page click) is a no-op, so it + // doesn't fight handlePageChange. + setQueryParams((prev) => + prev.sort_by === sort_by ? prev : { ...prev, sort_by, page: 1 } + ); + }; + + const handlePageChange = (page: number, pageSize: number) => { + setQueryParams((prev) => ({ ...prev, page, perPage: pageSize })); + }; + + // Filters changed upstream → snap back to page 1. Defer the fetch until the + // page-1 render so we don't fire a stale page-N request first. + useEffect(() => { + if (queryParams.page !== 1) { + pendingPageResetRef.current = true; + setQueryParams((prev) => ({ ...prev, page: 1 })); + } + }, [pageResetKey]); + + useEffect(() => { + if (pendingPageResetRef.current && queryParams.page !== 1) return; + pendingPageResetRef.current = false; + + const descending = queryParams.sort_by.startsWith('-'); + fetchData({ + start_date: dateRange[0].format('YYYY-MM-DD'), + end_date: dateRange[1].format('YYYY-MM-DD'), + scope, + filters: + selectedUsers.length || selectedInstances.length + ? { + ...(selectedUsers.length ? { creator_ids: selectedUsers } : {}), + ...(selectedInstances.length + ? { instance_ids: selectedInstances } + : {}) + } + : undefined, + group_by: [groupKey], + page: queryParams.page, + perPage: queryParams.perPage, + order_by: descending ? queryParams.sort_by.slice(1) : queryParams.sort_by, + descending + }); + }, [ + dateRange, + scope, + selectedUsers, + selectedInstances, + queryParams.page, + queryParams.perPage, + queryParams.sort_by, + refreshKey + ]); + + const rows: ResourceBreakdownItem[] = detailData?.items ?? []; + + return ( +
+ `${row.gpu_type ?? ''}|${row.instance_id ?? ''}|${row.user_id ?? ''}` + } + dataSource={rows} + columns={columns as any} + loading={{ spinning: loading, size: 'middle' }} + sortDirections={TABLE_SORT_DIRECTIONS} + showSorterTooltip={false} + onChange={handleTableChange} + pagination={{ + size: 'middle', + current: queryParams.page, + pageSize: detailData?.pagination?.perPage ?? queryParams.perPage, + total: detailData?.pagination?.total ?? 0, + showSizeChanger: true, + hideOnSinglePage: queryParams.perPage === PER_PAGE, + onChange: handlePageChange + }} + /> + ); +}; + +export default InstancesBreakdownTable; diff --git a/src/pages/usage/instances-tab/tables/use-instances-columns.tsx b/src/pages/usage/instances-tab/tables/use-instances-columns.tsx new file mode 100644 index 00000000..c869071c --- /dev/null +++ b/src/pages/usage/instances-tab/tables/use-instances-columns.tsx @@ -0,0 +1,116 @@ +import { + buildInstanceTypeRecordFromMiB, + renderInstanceType +} from '@/pages/gpu-service/instances/utils/render-instance-type'; +import { useIntl } from '@umijs/max'; +import { useMemo } from 'react'; +import { ResourceBreakdownItem } from '../../apis/resource'; +import { instanceTypeLabel } from '../../utils/format-instance-type'; +import { parseRollup } from '../../utils/time-buckets'; + +type GroupKey = 'gpu_type' | 'instance' | 'user'; + +/** + * Column factory for the GPU-instances breakdown tables, shared by the in-tab + * table and the export preview. Columns adapt to the active grouping. Sort + * indicators are uncontrolled (antd manages the header arrows); the table + * reports changes through its `onChange`. + */ +const useInstancesColumns = (groupKey: GroupKey) => { + const intl = useIntl(); + + return useMemo(() => { + const baseValueCols = [ + { + title: intl.formatMessage({ id: 'usage.metric.gpuHours' }), + dataIndex: 'gpu_hours', + key: 'gpu_hours', + sorter: true, + render: (v: number) => (v ?? 0).toFixed(2) + }, + { + title: intl.formatMessage({ id: 'usage.metric.instanceHours' }), + dataIndex: 'instance_hours', + key: 'instance_hours', + sorter: true, + render: (v: number) => (v ?? 0).toFixed(2) + } + ]; + // Instance Types breakdown: just the pretty product name (or flavor slug + // for older rows) — no spec sub-line. + const instanceTypeColType = { + title: intl.formatMessage({ id: 'usage.table.instanceType' }), + dataIndex: 'gpu_type', + key: 'gpu_type', + render: (_v: string, row: ResourceBreakdownItem) => instanceTypeLabel(row) + }; + // Instances breakdown: render through the canonical GPU Instances list + // renderer so the label + spec popover are identical. The breakdown row + // carries flat MiB fields, so adapt it into the ListItem shape first. + const instanceTypeColInstance = { + title: intl.formatMessage({ id: 'usage.table.instanceType' }), + dataIndex: 'gpu_type', + key: 'gpu_type', + render: (_v: string, row: ResourceBreakdownItem) => + renderInstanceType( + buildInstanceTypeRecordFromMiB({ + name: row.instance_name, + product: row.product || row.gpu_type, + gpuCount: row.gpu_count, + unitCpuMilli: row.unit_cpu_milli, + unitMemoryMib: row.unit_memory_mib, + vramMib: row.vram_mib, + localStorageMib: row.local_storage_mib, + ephemeralMib: row.ephemeral_mib, + persistentMib: row.persistent_mib + }), + { intl } + ) + }; + // Last Active = the last active day. The backend sends a rollup-tz instant + // with its offset; parseRollup keeps that wall clock (no browser-tz convert), + // consistent with the trend chart buckets. Shown date-only. + const lastActiveCol = { + title: intl.formatMessage({ id: 'usage.table.lastActive' }), + dataIndex: 'last_active', + key: 'last_active', + render: (v?: string) => (v ? parseRollup(v).format('YYYY-MM-DD') : '-') + }; + if (groupKey === 'gpu_type') { + return [ + instanceTypeColType, + ...baseValueCols, + { + title: intl.formatMessage({ id: 'usage.metric.activeInstances' }), + dataIndex: 'active_instances', + key: 'active_instances' + }, + lastActiveCol + ]; + } + if (groupKey === 'instance') { + return [ + { + title: intl.formatMessage({ id: 'usage.table.instance' }), + dataIndex: 'instance_name', + key: 'instance_name' + }, + instanceTypeColInstance, + ...baseValueCols, + lastActiveCol + ]; + } + // user tab + return [ + { + title: intl.formatMessage({ id: 'usage.table.user' }), + dataIndex: 'user_name', + key: 'user_name' + }, + ...baseValueCols, + lastActiveCol + ]; + }, [groupKey, intl]); +}; + +export default useInstancesColumns; diff --git a/src/pages/usage/storage-tab/index.tsx b/src/pages/usage/storage-tab/index.tsx index 33d55efd..28fd0f50 100644 --- a/src/pages/usage/storage-tab/index.tsx +++ b/src/pages/usage/storage-tab/index.tsx @@ -65,23 +65,19 @@ const StorageTab: React.FC = () => { ); const TABLE_TABS: { key: GroupKey; label: string }[] = useMemo(() => { - return access.canSeeOrgAdmin - ? [ - { - key: 'volume', - label: intl.formatMessage({ id: 'usage.tabs.storage' }) - }, - { - key: 'user', - label: intl.formatMessage({ id: 'usage.table.users' }) - } - ] - : [ - { - key: 'volume', - label: intl.formatMessage({ id: 'usage.tabs.storage' }) - } - ]; + const tabs = [ + { + key: 'volume' as GroupKey, + label: intl.formatMessage({ id: 'usage.tabs.storage' }) + } + ]; + if (access.canSeeOrgAdmin) { + tabs.push({ + key: 'user' as GroupKey, + label: intl.formatMessage({ id: 'usage.table.users' }) + }); + } + return tabs; }, [intl, access.canSeeOrgAdmin]); // No All/My dropdown (matches the Tokens tab): managers see the org-wide diff --git a/src/pages/usage/token-tab/tables/apikeys-table.tsx b/src/pages/usage/token-tab/tables/apikeys-table.tsx index de115dca..c1d94e8a 100644 --- a/src/pages/usage/token-tab/tables/apikeys-table.tsx +++ b/src/pages/usage/token-tab/tables/apikeys-table.tsx @@ -36,6 +36,7 @@ const APIKeys: React.FC<{ sorter.order === 'descend' ? `-${sorter.field}` : sorter.field; setQueryParams((prev) => ({ ...prev, + page: 1, sort_by })); }; diff --git a/src/pages/usage/token-tab/tables/models-table.tsx b/src/pages/usage/token-tab/tables/models-table.tsx index f15ee997..fb3fea46 100644 --- a/src/pages/usage/token-tab/tables/models-table.tsx +++ b/src/pages/usage/token-tab/tables/models-table.tsx @@ -38,6 +38,7 @@ const Models: React.FC<{ sorter.order === 'descend' ? `-${sorter.field}` : sorter.field; setQueryParams((prev) => ({ ...prev, + page: 1, sort_by })); }; diff --git a/src/pages/usage/token-tab/tables/users-table.tsx b/src/pages/usage/token-tab/tables/users-table.tsx index 1b5793c1..d1a43687 100644 --- a/src/pages/usage/token-tab/tables/users-table.tsx +++ b/src/pages/usage/token-tab/tables/users-table.tsx @@ -36,6 +36,7 @@ const Users: React.FC<{ sorter.order === 'descend' ? `-${sorter.field}` : sorter.field; setQueryParams((prev) => ({ ...prev, + page: 1, sort_by })); };