fix: export data should contain all tables
This commit is contained in:
@@ -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 ?? [];
|
||||||
|
|||||||
@@ -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 ?? [];
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user