diff --git a/src/pages/usage/instances-tab/index.tsx b/src/pages/usage/instances-tab/index.tsx index 48583c6b..7de4009d 100644 --- a/src/pages/usage/instances-tab/index.tsx +++ b/src/pages/usage/instances-tab/index.tsx @@ -25,6 +25,10 @@ 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 { + exportBreakdownSheets, + toExportColumns +} from '../utils/export-breakdown'; import { bucketKey, generateBucketRange, @@ -245,18 +249,67 @@ const GpuInstancesTab: React.FC = () => { [TABLE_TABS] ); - // 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); + // Columns for each bottom-table grouping — same factory the tables render + // with — used to build the export sheets below. + const gpuTypeColumns = useInstancesColumns('gpu_type'); + const instanceColumns = useInstancesColumns('instance'); + const userColumns = useInstancesColumns('user'); - // Export opens a preview modal (matches the Tokens tab): re-filter + preview - // the rows, then download. "Chart" = the by-date trend, "Table" = the active - // bottom-table grouping. - const [exportMode, setExportMode] = useState<'chart' | 'table' | null>(null); + // "Export Table Data" writes every bottom table at once — one sheet per + // grouping (Instance Types / Instances / Users) — straight to a workbook, + // no preview dialog (mirrors the Tokens tab's `useExportTable`). The User + // sheet is included only when the org-wide view is available. + const tableExportGroups = useMemo(() => { + const groups = [ + { + key: 'gpu_type' as GroupKey, + columns: gpuTypeColumns, + sheetName: intl.formatMessage({ id: 'usage.table.instanceTypes' }) + }, + { + key: 'instance' as GroupKey, + columns: instanceColumns, + sheetName: intl.formatMessage({ id: 'usage.table.instances' }) + } + ]; + if (canManageUsers) { + groups.push({ + key: 'user' as GroupKey, + columns: userColumns, + sheetName: intl.formatMessage({ id: 'usage.table.users' }) + }); + } + return groups; + }, [gpuTypeColumns, instanceColumns, userColumns, canManageUsers, intl]); + + // Chart export still opens the preview modal (matches the Tokens tab); the + // table export is direct, so this only ever holds 'chart'. + const [exportMode, setExportMode] = useState<'chart' | null>(null); const dateSuffix = `${dateRange[0].format('YYYY-MM-DD')}_${dateRange[1].format( 'YYYY-MM-DD' )}`; + const handleExportTable = async () => { + const results = await Promise.all( + tableExportGroups.map((g) => + queryGpuInstancesBreakdown({ + ...baseRequest(), + group_by: [g.key], + // A breakdown export is the full filtered set, not a page. + perPage: 10000 + }) + ) + ); + exportBreakdownSheets( + tableExportGroups.map((g, i) => ({ + rows: results[i]?.items ?? [], + columns: toExportColumns(g.columns), + sheetName: g.sheetName + })), + `gpu-instances_tables_${dateSuffix}.xlsx` + ); + }; + const chartExportColumns = [ { title: intl.formatMessage({ id: 'usage.table.date' }), @@ -287,21 +340,13 @@ const GpuInstancesTab: React.FC = () => { } ]; - const tabLabel = TABLE_TABS.find((t) => t.key === activeTableTab)?.label; - const exportConfig = - exportMode === 'chart' - ? { - groupBy: ['date'], - columns: chartExportColumns, - fileName: `gpu-instances_chart_${dateSuffix}.xlsx`, - sheetName: intl.formatMessage({ id: 'usage.tabs.gpuInstances' }) - } - : { - groupBy: [activeTableTab], - columns: exportTableColumns, - fileName: `gpu-instances_${activeTableTab}_${dateSuffix}.xlsx`, - sheetName: tabLabel || 'gpu-instances' - }; + // The preview modal now only backs the by-date chart export. + const exportConfig = { + groupBy: ['date'], + columns: chartExportColumns, + fileName: `gpu-instances_chart_${dateSuffix}.xlsx`, + sheetName: intl.formatMessage({ id: 'usage.tabs.gpuInstances' }) + }; return (
@@ -330,7 +375,7 @@ const GpuInstancesTab: React.FC = () => { }} onRefresh={() => setRefreshKey((k) => k + 1)} onExportChart={() => setExportMode('chart')} - onExportTable={() => setExportMode('table')} + onExportTable={handleExportTable} /> {/* KPI cards */} @@ -349,7 +394,7 @@ const GpuInstancesTab: React.FC = () => {
{/* Daily trend chart */} -
+
{ setExportMode(null)} - title={ - exportMode === 'chart' - ? intl.formatMessage({ id: 'usage.export.chart' }) - : intl.formatMessage( - { id: 'usage.export.tableNamed' }, - { name: tabLabel } - ) - } + title={intl.formatMessage({ id: 'usage.export.chart' })} queryFn={queryGpuInstancesBreakdown} groupBy={exportConfig.groupBy} columns={exportConfig.columns} diff --git a/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx b/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx index 4f05b39d..83aaf020 100644 --- a/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx +++ b/src/pages/usage/instances-tab/tables/instances-breakdown-table.tsx @@ -123,7 +123,8 @@ const InstancesBreakdownTable: React.FC = ({ queryParams.page, queryParams.perPage, queryParams.sort_by, - refreshKey + refreshKey, + fetchData ]); const rows: ResourceBreakdownItem[] = detailData?.items ?? []; diff --git a/src/pages/usage/storage-tab/index.tsx b/src/pages/usage/storage-tab/index.tsx index 28fd0f50..b613b9fb 100644 --- a/src/pages/usage/storage-tab/index.tsx +++ b/src/pages/usage/storage-tab/index.tsx @@ -28,6 +28,10 @@ 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 { + exportBreakdownSheets, + toExportColumns +} from '../utils/export-breakdown'; import { bucketKey, generateBucketRange, @@ -238,18 +242,61 @@ const StorageTab: React.FC = () => { [TABLE_TABS, scope] ); - // 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 = useStorageColumns(activeTableTab); + // Columns for each bottom-table grouping — same factory the tables render + // with — used to build the export sheets below. + const volumeColumns = useStorageColumns('volume'); + const userColumns = useStorageColumns('user'); - // Export opens a preview modal (matches the Tokens tab): re-filter + preview - // the rows, then download. "Chart" = the by-date trend, "Table" = the active - // bottom-table grouping. - const [exportMode, setExportMode] = useState<'chart' | 'table' | null>(null); + // "Export Table Data" writes every bottom table at once — one sheet per + // grouping (Storage / Users) — straight to a workbook, no preview dialog + // (mirrors the Tokens tab's `useExportTable`). The User sheet is included + // only when the org-wide view is available. + const tableExportGroups = useMemo(() => { + const groups = [ + { + key: 'volume' as GroupKey, + columns: volumeColumns, + sheetName: intl.formatMessage({ id: 'usage.tabs.storage' }) + } + ]; + if (canManageUsers) { + groups.push({ + key: 'user' as GroupKey, + columns: userColumns, + sheetName: intl.formatMessage({ id: 'usage.table.users' }) + }); + } + return groups; + }, [volumeColumns, userColumns, canManageUsers, intl]); + + // Chart export still opens the preview modal (matches the Tokens tab); the + // table export is direct, so this only ever holds 'chart'. + const [exportMode, setExportMode] = useState<'chart' | null>(null); const dateSuffix = `${dateRange[0].format('YYYY-MM-DD')}_${dateRange[1].format( 'YYYY-MM-DD' )}`; + const handleExportTable = async () => { + const results = await Promise.all( + tableExportGroups.map((g) => + queryStorageBreakdown({ + ...baseRequest(), + group_by: [g.key], + // A breakdown export is the full filtered set, not a page. + perPage: 10000 + }) + ) + ); + exportBreakdownSheets( + tableExportGroups.map((g, i) => ({ + rows: results[i]?.items ?? [], + columns: toExportColumns(g.columns), + sheetName: g.sheetName + })), + `storage_tables_${dateSuffix}.xlsx` + ); + }; + const chartExportColumns = [ { title: intl.formatMessage({ id: 'usage.table.date' }), @@ -280,21 +327,13 @@ const StorageTab: React.FC = () => { } ]; - const tabLabel = TABLE_TABS.find((t) => t.key === activeTableTab)?.label; - const exportConfig = - exportMode === 'chart' - ? { - groupBy: ['date'], - columns: chartExportColumns, - fileName: `storage_chart_${dateSuffix}.xlsx`, - sheetName: intl.formatMessage({ id: 'usage.tabs.storage' }) - } - : { - groupBy: [activeTableTab], - columns: exportTableColumns, - fileName: `storage_${activeTableTab}_${dateSuffix}.xlsx`, - sheetName: tabLabel || 'storage' - }; + // The preview modal now only backs the by-date chart export. + const exportConfig = { + groupBy: ['date'], + columns: chartExportColumns, + fileName: `storage_chart_${dateSuffix}.xlsx`, + sheetName: intl.formatMessage({ id: 'usage.tabs.storage' }) + }; return (
@@ -322,7 +361,7 @@ const StorageTab: React.FC = () => { }} onRefresh={() => setRefreshKey((k) => k + 1)} onExportChart={() => setExportMode('chart')} - onExportTable={() => setExportMode('table')} + onExportTable={handleExportTable} />
@@ -382,14 +421,7 @@ const StorageTab: React.FC = () => { setExportMode(null)} - title={ - exportMode === 'chart' - ? intl.formatMessage({ id: 'usage.export.chart' }) - : intl.formatMessage( - { id: 'usage.export.tableNamed' }, - { name: tabLabel } - ) - } + title={intl.formatMessage({ id: 'usage.export.chart' })} queryFn={queryStorageBreakdown} groupBy={exportConfig.groupBy} columns={exportConfig.columns} diff --git a/src/pages/usage/storage-tab/tables/storage-breakdown-table.tsx b/src/pages/usage/storage-tab/tables/storage-breakdown-table.tsx index 13ce224d..9c4a9ded 100644 --- a/src/pages/usage/storage-tab/tables/storage-breakdown-table.tsx +++ b/src/pages/usage/storage-tab/tables/storage-breakdown-table.tsx @@ -125,7 +125,8 @@ const StorageBreakdownTable: React.FC = ({ queryParams.page, queryParams.perPage, queryParams.sort_by, - refreshKey + refreshKey, + fetchData ]); const rows: ResourceBreakdownItem[] = detailData?.items ?? []; diff --git a/src/pages/usage/utils/export-breakdown.ts b/src/pages/usage/utils/export-breakdown.ts index 93dcc30d..6cb77a39 100644 --- a/src/pages/usage/utils/export-breakdown.ts +++ b/src/pages/usage/utils/export-breakdown.ts @@ -26,12 +26,13 @@ export const toExportColumns = (columns: any[]): ExportColumn[] => dataIndex: c.dataIndex as string })); -export const exportBreakdownRows = ( - rows: any[], - columns: ExportColumn[], - fileName: string, - sheetName = 'usage' -): void => { +export interface ExportSheet { + rows: any[]; + columns: ExportColumn[]; + sheetName: string; +} + +const buildSheet = ({ rows, columns, sheetName }: ExportSheet) => { const fields = columns.map((c) => c.dataIndex); const fieldLabels = Object.fromEntries( columns.map((c) => [c.dataIndex, c.title]) @@ -43,8 +44,22 @@ export const exportBreakdownRows = ( }); return o; }); - exportJsonToExcel({ - fileName, - sheets: [{ jsonData, sheetName, fields, fieldLabels, formatMap: {} }] - }); + return { jsonData, sheetName, fields, fieldLabels, formatMap: {} }; +}; + +// Write one or more breakdown result sets to a single workbook, one sheet each. +export const exportBreakdownSheets = ( + sheets: ExportSheet[], + fileName: string +): void => { + exportJsonToExcel({ fileName, sheets: sheets.map(buildSheet) }); +}; + +export const exportBreakdownRows = ( + rows: any[], + columns: ExportColumn[], + fileName: string, + sheetName = 'usage' +): void => { + exportBreakdownSheets([{ rows, columns, sheetName }], fileName); };