159 lines
5.1 KiB
TypeScript
159 lines
5.1 KiB
TypeScript
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 useQueryStorageBreakdown from '../services/use-query-storage-breakdown';
|
|
import useStorageColumns from './use-storage-columns';
|
|
|
|
type Scope = 'self' | 'all';
|
|
type Metric = 'storage_gb_days' | 'storage_gb_hours';
|
|
type GroupKey = 'volume' | 'user';
|
|
|
|
// The frontend metric keys (storage_gb_days/hours) map to the server's
|
|
// breakdown metric keys (gb_days/gb_hours).
|
|
const ORDER_BY_KEY: Record<Metric, string> = {
|
|
storage_gb_days: 'gb_days',
|
|
storage_gb_hours: 'gb_hours'
|
|
};
|
|
|
|
const PER_PAGE = 50;
|
|
// sort_by encodes order as a string (`-` prefix = descending), so the fetch
|
|
// effect dedupes naturally on the primitive. Default: GB-Days descending.
|
|
const DEFAULT_SORT = '-gb_days';
|
|
|
|
interface Props {
|
|
groupKey: GroupKey;
|
|
dateRange: [dayjs.Dayjs, dayjs.Dayjs];
|
|
scope: Scope;
|
|
selectedUsers: number[];
|
|
selectedVolumes: number[];
|
|
// Bumped by the parent when a filter changes, so each mounted table snaps
|
|
// back to page 1 independently.
|
|
pageResetKey?: number;
|
|
refreshKey?: number;
|
|
}
|
|
|
|
/**
|
|
* One storage breakdown table, owning its own page/sort/data. Both tabs mount
|
|
* an instance (volume / user) and keep it alive (Tabs `forceRender`), so each
|
|
* keeps its own pagination + sort and switching tabs neither refetches nor
|
|
* resets the other.
|
|
*/
|
|
const StorageBreakdownTable: React.FC<Props> = ({
|
|
groupKey,
|
|
dateRange,
|
|
scope,
|
|
selectedUsers,
|
|
selectedVolumes,
|
|
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 } = useQueryStorageBreakdown({
|
|
key: `storageBreakdown-${groupKey}`
|
|
});
|
|
|
|
const columns = useStorageColumns(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 = ORDER_BY_KEY[s?.field as Metric];
|
|
// Cleared (3rd click) or an unknown column → default GB-Days descending.
|
|
const sort_by =
|
|
!field || !s?.order
|
|
? 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 || selectedVolumes.length
|
|
? {
|
|
...(selectedUsers.length ? { creator_ids: selectedUsers } : {}),
|
|
...(selectedVolumes.length ? { volume_ids: selectedVolumes } : {})
|
|
}
|
|
: 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,
|
|
selectedVolumes,
|
|
queryParams.page,
|
|
queryParams.perPage,
|
|
queryParams.sort_by,
|
|
refreshKey,
|
|
fetchData
|
|
]);
|
|
|
|
const rows: ResourceBreakdownItem[] = detailData?.items ?? [];
|
|
|
|
return (
|
|
<Table
|
|
rowKey={(row) =>
|
|
`${row.volume_id ?? ''}|${row.user_id ?? ''}|${row.volume_name ?? ''}`
|
|
}
|
|
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 StorageBreakdownTable;
|