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 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 (
<div>
@@ -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 = () => {
</div>
{/* Daily trend chart */}
<div style={{ marginBottom: 24 }}>
<div style={{ marginBottom: 16 }}>
<MetricChartCard
metric={metric}
metricOptions={METRIC_OPTIONS}
@@ -393,14 +438,7 @@ const GpuInstancesTab: React.FC = () => {
<ResourceExportData
open={exportMode !== null}
onCancel={() => 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}
@@ -123,7 +123,8 @@ const InstancesBreakdownTable: React.FC<Props> = ({
queryParams.page,
queryParams.perPage,
queryParams.sort_by,
refreshKey
refreshKey,
fetchData
]);
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 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 (
<div>
@@ -322,7 +361,7 @@ const StorageTab: React.FC = () => {
}}
onRefresh={() => setRefreshKey((k) => k + 1)}
onExportChart={() => setExportMode('chart')}
onExportTable={() => setExportMode('table')}
onExportTable={handleExportTable}
/>
<div style={{ height: 24 }} />
@@ -382,14 +421,7 @@ const StorageTab: React.FC = () => {
<ResourceExportData
open={exportMode !== null}
onCancel={() => 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}
@@ -125,7 +125,8 @@ const StorageBreakdownTable: React.FC<Props> = ({
queryParams.page,
queryParams.perPage,
queryParams.sort_by,
refreshKey
refreshKey,
fetchData
]);
const rows: ResourceBreakdownItem[] = detailData?.items ?? [];
+25 -10
View File
@@ -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);
};