fix: export data should contain all tables

This commit is contained in:
jialin
2026-06-16 22:28:46 +08:00
committed by jialin
parent c8e14dade5
commit e68bb0bc4e
5 changed files with 162 additions and 75 deletions
+70 -32
View File
@@ -25,6 +25,10 @@ import MetricLabel from '../components/metric-label';
import ResourceExportData from '../components/resource-export-data'; import ResourceExportData from '../components/resource-export-data';
import ResourceFilterBar from '../components/resource-filter-bar'; import ResourceFilterBar from '../components/resource-filter-bar';
import useResourceMeta from '../hooks/use-resource-meta'; import useResourceMeta from '../hooks/use-resource-meta';
import {
exportBreakdownSheets,
toExportColumns
} from '../utils/export-breakdown';
import { import {
bucketKey, bucketKey,
generateBucketRange, generateBucketRange,
@@ -245,18 +249,67 @@ const GpuInstancesTab: React.FC = () => {
[TABLE_TABS] [TABLE_TABS]
); );
// Columns for the export preview of the active tab (sort arrows omitted — // Columns for each bottom-table grouping — same factory the tables render
// the in-tab table owns its own sort state). Same factory the tables use. // with — used to build the export sheets below.
const exportTableColumns = useInstancesColumns(activeTableTab); 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 // "Export Table Data" writes every bottom table at once — one sheet per
// the rows, then download. "Chart" = the by-date trend, "Table" = the active // grouping (Instance Types / Instances / Users) — straight to a workbook,
// bottom-table grouping. // no preview dialog (mirrors the Tokens tab's `useExportTable`). The User
const [exportMode, setExportMode] = useState<'chart' | 'table' | null>(null); // 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( const dateSuffix = `${dateRange[0].format('YYYY-MM-DD')}_${dateRange[1].format(
'YYYY-MM-DD' '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 = [ const chartExportColumns = [
{ {
title: intl.formatMessage({ id: 'usage.table.date' }), 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; // The preview modal now only backs the by-date chart export.
const exportConfig = const exportConfig = {
exportMode === 'chart' groupBy: ['date'],
? { columns: chartExportColumns,
groupBy: ['date'], fileName: `gpu-instances_chart_${dateSuffix}.xlsx`,
columns: chartExportColumns, sheetName: intl.formatMessage({ id: 'usage.tabs.gpuInstances' })
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'
};
return ( return (
<div> <div>
@@ -330,7 +375,7 @@ const GpuInstancesTab: React.FC = () => {
}} }}
onRefresh={() => setRefreshKey((k) => k + 1)} onRefresh={() => setRefreshKey((k) => k + 1)}
onExportChart={() => setExportMode('chart')} onExportChart={() => setExportMode('chart')}
onExportTable={() => setExportMode('table')} onExportTable={handleExportTable}
/> />
{/* KPI cards */} {/* KPI cards */}
@@ -349,7 +394,7 @@ const GpuInstancesTab: React.FC = () => {
</div> </div>
{/* Daily trend chart */} {/* Daily trend chart */}
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 16 }}>
<MetricChartCard <MetricChartCard
metric={metric} metric={metric}
metricOptions={METRIC_OPTIONS} metricOptions={METRIC_OPTIONS}
@@ -393,14 +438,7 @@ const GpuInstancesTab: React.FC = () => {
<ResourceExportData <ResourceExportData
open={exportMode !== null} open={exportMode !== null}
onCancel={() => setExportMode(null)} onCancel={() => setExportMode(null)}
title={ title={intl.formatMessage({ id: 'usage.export.chart' })}
exportMode === 'chart'
? intl.formatMessage({ id: 'usage.export.chart' })
: intl.formatMessage(
{ id: 'usage.export.tableNamed' },
{ name: tabLabel }
)
}
queryFn={queryGpuInstancesBreakdown} queryFn={queryGpuInstancesBreakdown}
groupBy={exportConfig.groupBy} groupBy={exportConfig.groupBy}
columns={exportConfig.columns} columns={exportConfig.columns}
@@ -123,7 +123,8 @@ const InstancesBreakdownTable: React.FC<Props> = ({
queryParams.page, queryParams.page,
queryParams.perPage, queryParams.perPage,
queryParams.sort_by, queryParams.sort_by,
refreshKey refreshKey,
fetchData
]); ]);
const rows: ResourceBreakdownItem[] = detailData?.items ?? []; const rows: ResourceBreakdownItem[] = detailData?.items ?? [];
+63 -31
View File
@@ -28,6 +28,10 @@ import MetricLabel from '../components/metric-label';
import ResourceExportData from '../components/resource-export-data'; import ResourceExportData from '../components/resource-export-data';
import ResourceFilterBar from '../components/resource-filter-bar'; import ResourceFilterBar from '../components/resource-filter-bar';
import useResourceMeta from '../hooks/use-resource-meta'; import useResourceMeta from '../hooks/use-resource-meta';
import {
exportBreakdownSheets,
toExportColumns
} from '../utils/export-breakdown';
import { import {
bucketKey, bucketKey,
generateBucketRange, generateBucketRange,
@@ -238,18 +242,61 @@ const StorageTab: React.FC = () => {
[TABLE_TABS, scope] [TABLE_TABS, scope]
); );
// Columns for the export preview of the active tab (sort arrows omitted — // Columns for each bottom-table grouping — same factory the tables render
// the in-tab table owns its own sort state). Same factory the tables use. // with — used to build the export sheets below.
const exportTableColumns = useStorageColumns(activeTableTab); const volumeColumns = useStorageColumns('volume');
const userColumns = useStorageColumns('user');
// Export opens a preview modal (matches the Tokens tab): re-filter + preview // "Export Table Data" writes every bottom table at once — one sheet per
// the rows, then download. "Chart" = the by-date trend, "Table" = the active // grouping (Storage / Users) — straight to a workbook, no preview dialog
// bottom-table grouping. // (mirrors the Tokens tab's `useExportTable`). The User sheet is included
const [exportMode, setExportMode] = useState<'chart' | 'table' | null>(null); // 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( const dateSuffix = `${dateRange[0].format('YYYY-MM-DD')}_${dateRange[1].format(
'YYYY-MM-DD' '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 = [ const chartExportColumns = [
{ {
title: intl.formatMessage({ id: 'usage.table.date' }), 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; // The preview modal now only backs the by-date chart export.
const exportConfig = const exportConfig = {
exportMode === 'chart' groupBy: ['date'],
? { columns: chartExportColumns,
groupBy: ['date'], fileName: `storage_chart_${dateSuffix}.xlsx`,
columns: chartExportColumns, sheetName: intl.formatMessage({ id: 'usage.tabs.storage' })
fileName: `storage_chart_${dateSuffix}.xlsx`, };
sheetName: intl.formatMessage({ id: 'usage.tabs.storage' })
}
: {
groupBy: [activeTableTab],
columns: exportTableColumns,
fileName: `storage_${activeTableTab}_${dateSuffix}.xlsx`,
sheetName: tabLabel || 'storage'
};
return ( return (
<div> <div>
@@ -322,7 +361,7 @@ const StorageTab: React.FC = () => {
}} }}
onRefresh={() => setRefreshKey((k) => k + 1)} onRefresh={() => setRefreshKey((k) => k + 1)}
onExportChart={() => setExportMode('chart')} onExportChart={() => setExportMode('chart')}
onExportTable={() => setExportMode('table')} onExportTable={handleExportTable}
/> />
<div style={{ height: 24 }} /> <div style={{ height: 24 }} />
@@ -382,14 +421,7 @@ const StorageTab: React.FC = () => {
<ResourceExportData <ResourceExportData
open={exportMode !== null} open={exportMode !== null}
onCancel={() => setExportMode(null)} onCancel={() => setExportMode(null)}
title={ title={intl.formatMessage({ id: 'usage.export.chart' })}
exportMode === 'chart'
? intl.formatMessage({ id: 'usage.export.chart' })
: intl.formatMessage(
{ id: 'usage.export.tableNamed' },
{ name: tabLabel }
)
}
queryFn={queryStorageBreakdown} queryFn={queryStorageBreakdown}
groupBy={exportConfig.groupBy} groupBy={exportConfig.groupBy}
columns={exportConfig.columns} columns={exportConfig.columns}
@@ -125,7 +125,8 @@ const StorageBreakdownTable: React.FC<Props> = ({
queryParams.page, queryParams.page,
queryParams.perPage, queryParams.perPage,
queryParams.sort_by, queryParams.sort_by,
refreshKey refreshKey,
fetchData
]); ]);
const rows: ResourceBreakdownItem[] = detailData?.items ?? []; const rows: ResourceBreakdownItem[] = detailData?.items ?? [];
+25 -10
View File
@@ -26,12 +26,13 @@ export const toExportColumns = (columns: any[]): ExportColumn[] =>
dataIndex: c.dataIndex as string dataIndex: c.dataIndex as string
})); }));
export const exportBreakdownRows = ( export interface ExportSheet {
rows: any[], rows: any[];
columns: ExportColumn[], columns: ExportColumn[];
fileName: string, sheetName: string;
sheetName = 'usage' }
): void => {
const buildSheet = ({ rows, columns, sheetName }: ExportSheet) => {
const fields = columns.map((c) => c.dataIndex); const fields = columns.map((c) => c.dataIndex);
const fieldLabels = Object.fromEntries( const fieldLabels = Object.fromEntries(
columns.map((c) => [c.dataIndex, c.title]) columns.map((c) => [c.dataIndex, c.title])
@@ -43,8 +44,22 @@ export const exportBreakdownRows = (
}); });
return o; return o;
}); });
exportJsonToExcel({ return { jsonData, sheetName, fields, fieldLabels, formatMap: {} };
fileName, };
sheets: [{ 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);
}; };