refactor: usage storage tab, split tables
This commit is contained in:
@@ -40,6 +40,7 @@ interface MetricOption {
|
||||
}
|
||||
|
||||
interface MetricChartCardProps {
|
||||
loading?: boolean;
|
||||
metric: string;
|
||||
metricOptions: MetricOption[];
|
||||
granularity: string;
|
||||
@@ -55,6 +56,7 @@ interface MetricChartCardProps {
|
||||
}
|
||||
|
||||
const MetricChartCard: React.FC<MetricChartCardProps> = ({
|
||||
loading,
|
||||
metric,
|
||||
metricOptions,
|
||||
granularity,
|
||||
@@ -129,6 +131,7 @@ const MetricChartCard: React.FC<MetricChartCardProps> = ({
|
||||
/>
|
||||
</ControlsWrapper>
|
||||
<BarChart
|
||||
loading={loading}
|
||||
seriesData={seriesData}
|
||||
xAxisData={xAxisData}
|
||||
height={280}
|
||||
|
||||
@@ -15,12 +15,11 @@ import useCoolColors from '@/hooks/use-cool-colors';
|
||||
import { formatLargeNumber } from '@/utils';
|
||||
import { SimpleCard } from '@gpustack/core-ui';
|
||||
import { useAccess, useIntl } from '@umijs/max';
|
||||
import { Table, Tabs } from 'antd';
|
||||
import { Tabs } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
queryStorageBreakdown,
|
||||
ResourceBreakdownItem,
|
||||
ResourceBreakdownRequest,
|
||||
ResourceBreakdownResponse
|
||||
} from '../apis/resource';
|
||||
@@ -32,10 +31,11 @@ import useResourceMeta from '../hooks/use-resource-meta';
|
||||
import {
|
||||
bucketKey,
|
||||
generateBucketRange,
|
||||
Granularity,
|
||||
parseRollup
|
||||
Granularity
|
||||
} from '../utils/time-buckets';
|
||||
import { buildTrendSeries } from '../utils/trend-series';
|
||||
import StorageBreakdownTable from './tables/storage-breakdown-table';
|
||||
import useStorageColumns from './tables/use-storage-columns';
|
||||
|
||||
type Scope = 'self' | 'all';
|
||||
type Metric = 'storage_gb_days' | 'storage_gb_hours';
|
||||
@@ -63,16 +63,25 @@ const StorageTab: React.FC = () => {
|
||||
[intl]
|
||||
);
|
||||
|
||||
const TABLE_TABS: { key: GroupKey; label: string }[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'volume',
|
||||
label: intl.formatMessage({ id: 'usage.tabs.storage' })
|
||||
},
|
||||
{ key: 'user', label: intl.formatMessage({ id: 'usage.table.users' }) }
|
||||
],
|
||||
[intl]
|
||||
);
|
||||
const TABLE_TABS: { key: GroupKey; label: string }[] = useMemo(() => {
|
||||
return access.canSeeOrgAdmin
|
||||
? [
|
||||
{
|
||||
key: 'volume',
|
||||
label: intl.formatMessage({ id: 'usage.tabs.storage' })
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: intl.formatMessage({ id: 'usage.table.users' })
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'volume',
|
||||
label: intl.formatMessage({ id: 'usage.tabs.storage' })
|
||||
}
|
||||
];
|
||||
}, [intl, access.canSeeOrgAdmin]);
|
||||
|
||||
// No All/My dropdown (matches the Tokens tab): managers see the org-wide
|
||||
// view and narrow it with the user filter, others only their own rows.
|
||||
@@ -98,15 +107,9 @@ const StorageTab: React.FC = () => {
|
||||
const [chartData, setChartData] = useState<ResourceBreakdownResponse | null>(
|
||||
null
|
||||
);
|
||||
const [tableData, setTableData] = useState<ResourceBreakdownResponse | null>(
|
||||
null
|
||||
);
|
||||
const [tablePage, setTablePage] = useState(1);
|
||||
// Server-side sort for the bottom tables; default GB-Days, descending.
|
||||
const [tableSort, setTableSort] = useState<{
|
||||
field: Metric;
|
||||
order: 'ascend' | 'descend';
|
||||
}>({ field: 'storage_gb_days', order: 'descend' });
|
||||
// Bumped on any filter change to snap every mounted table back to page 1;
|
||||
// each table owns its own page/sort state otherwise.
|
||||
const [pageResetKey, setPageResetKey] = useState(0);
|
||||
|
||||
const baseRequest = (): Omit<ResourceBreakdownRequest, 'group_by'> => ({
|
||||
start_date: dateRange[0].format('YYYY-MM-DD'),
|
||||
@@ -142,28 +145,6 @@ const StorageTab: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// The frontend metric keys (storage_gb_days/hours) map to the server's
|
||||
// breakdown metric keys (gb_days/gb_hours) for order_by.
|
||||
const ORDER_BY_KEY: Record<Metric, string> = {
|
||||
storage_gb_days: 'gb_days',
|
||||
storage_gb_hours: 'gb_hours'
|
||||
};
|
||||
|
||||
const fetchTable = async () => {
|
||||
try {
|
||||
const data = await queryStorageBreakdown({
|
||||
...baseRequest(),
|
||||
group_by: [activeTableTab],
|
||||
page: tablePage,
|
||||
order_by: ORDER_BY_KEY[tableSort.field],
|
||||
descending: tableSort.order === 'descend'
|
||||
});
|
||||
setTableData(data);
|
||||
} catch {
|
||||
// Same rationale.
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchChart();
|
||||
}, [
|
||||
@@ -175,18 +156,6 @@ const StorageTab: React.FC = () => {
|
||||
refreshKey
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTable();
|
||||
}, [
|
||||
dateRange,
|
||||
selectedUsers,
|
||||
selectedVolumes,
|
||||
activeTableTab,
|
||||
tablePage,
|
||||
tableSort,
|
||||
refreshKey
|
||||
]);
|
||||
|
||||
const summary = chartData?.summary;
|
||||
const summaryCards = useMemo(
|
||||
() => [
|
||||
@@ -267,86 +236,16 @@ const StorageTab: React.FC = () => {
|
||||
|
||||
const chartGroupByOptions = useMemo(
|
||||
() =>
|
||||
TABLE_TABS.filter((t) => t.key !== 'user' || scope === 'all').map(
|
||||
(t) => ({
|
||||
value: t.key,
|
||||
label: t.label
|
||||
})
|
||||
),
|
||||
TABLE_TABS.map((t) => ({
|
||||
value: t.key,
|
||||
label: t.label
|
||||
})),
|
||||
[TABLE_TABS, scope]
|
||||
);
|
||||
|
||||
const tableColumns = useMemo(() => {
|
||||
const valueCols = [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbDays' }),
|
||||
dataIndex: 'storage_gb_days',
|
||||
key: 'storage_gb_days',
|
||||
sorter: true,
|
||||
sortOrder:
|
||||
tableSort.field === 'storage_gb_days' ? tableSort.order : null,
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbHours' }),
|
||||
dataIndex: 'storage_gb_hours',
|
||||
key: 'storage_gb_hours',
|
||||
sorter: true,
|
||||
sortOrder:
|
||||
tableSort.field === 'storage_gb_hours' ? tableSort.order : null,
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
}
|
||||
];
|
||||
// Last Active = the last active day. The backend sends a rollup-tz instant
|
||||
// with its offset; parseRollup keeps that wall clock (no browser-tz convert),
|
||||
// consistent with the trend chart buckets. Shown date-only.
|
||||
const lastActiveCol = {
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active',
|
||||
render: (v?: string) => (v ? parseRollup(v).format('YYYY-MM-DD') : '-')
|
||||
};
|
||||
if (activeTableTab === 'volume') {
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.tabs.storage' }),
|
||||
dataIndex: 'volume_name',
|
||||
key: 'volume_name'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.type' }),
|
||||
dataIndex: 'storage_type',
|
||||
key: 'storage_type',
|
||||
render: (_v: string, row: ResourceBreakdownItem) =>
|
||||
row.storage_type || row.gpu_type || '-'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.capacity' }),
|
||||
dataIndex: 'capacity_mib',
|
||||
key: 'capacity_mib',
|
||||
render: (v?: number) => (v ? `${Math.round(v / 1024)}GB` : '-')
|
||||
},
|
||||
...valueCols,
|
||||
lastActiveCol
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.user' }),
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name'
|
||||
},
|
||||
...valueCols,
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeStorage' }),
|
||||
dataIndex: 'active_volumes',
|
||||
key: 'active_volumes'
|
||||
},
|
||||
lastActiveCol
|
||||
];
|
||||
}, [activeTableTab, tableSort, intl]);
|
||||
|
||||
const tableRows: ResourceBreakdownItem[] = tableData?.items ?? [];
|
||||
// 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);
|
||||
|
||||
// Export opens a preview modal (matches the Tokens tab): re-filter + preview
|
||||
// the rows, then download. "Chart" = the by-date trend, "Table" = the active
|
||||
@@ -397,7 +296,7 @@ const StorageTab: React.FC = () => {
|
||||
}
|
||||
: {
|
||||
groupBy: [activeTableTab],
|
||||
columns: tableColumns,
|
||||
columns: exportTableColumns,
|
||||
fileName: `storage_${activeTableTab}_${dateSuffix}.xlsx`,
|
||||
sheetName: tabLabel || 'storage'
|
||||
};
|
||||
@@ -408,21 +307,21 @@ const StorageTab: React.FC = () => {
|
||||
value={dateRange}
|
||||
onChange={(dates) => {
|
||||
setDateRange(dates);
|
||||
setTablePage(1);
|
||||
setPageResetKey((k) => k + 1);
|
||||
}}
|
||||
canManageUsers={canManageUsers}
|
||||
userOptions={userOptions}
|
||||
selectedUsers={selectedUsers}
|
||||
onUsersChange={(ids) => {
|
||||
setSelectedUsers(ids);
|
||||
setTablePage(1);
|
||||
setPageResetKey((k) => k + 1);
|
||||
}}
|
||||
resourceFilter={{
|
||||
options: volumeOptions,
|
||||
value: selectedVolumes,
|
||||
onChange: (ids) => {
|
||||
setSelectedVolumes(ids);
|
||||
setTablePage(1);
|
||||
setPageResetKey((k) => k + 1);
|
||||
},
|
||||
placeholder: intl.formatMessage({ id: 'usage.filter.storage' })
|
||||
}}
|
||||
@@ -462,50 +361,23 @@ const StorageTab: React.FC = () => {
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTableTab}
|
||||
onChange={(k) => {
|
||||
setActiveTableTab(k as GroupKey);
|
||||
setTablePage(1);
|
||||
}}
|
||||
items={TABLE_TABS.filter(
|
||||
(t) => t.key !== 'user' || scope === 'all'
|
||||
).map((t) => ({
|
||||
onChange={(k) => setActiveTableTab(k as GroupKey)}
|
||||
items={TABLE_TABS.map((t) => ({
|
||||
key: t.key,
|
||||
label: t.label,
|
||||
// Keep every pane mounted so each table holds its own page/sort and
|
||||
// switching tabs neither refetches nor resets the other.
|
||||
forceRender: true,
|
||||
children: (
|
||||
<Table
|
||||
rowKey={(row) =>
|
||||
`${row.volume_id ?? ''}|${row.user_id ?? ''}|${row.volume_name ?? ''}`
|
||||
}
|
||||
dataSource={tableRows}
|
||||
columns={tableColumns as any}
|
||||
onChange={(_pagination, _filters, sorter: any) => {
|
||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||
// Sort changed → page 1; cleared (3rd click) → default GB-Days
|
||||
// descending.
|
||||
const next = s?.order
|
||||
? {
|
||||
field: (s.columnKey as Metric) ?? 'storage_gb_days',
|
||||
order: s.order as 'ascend' | 'descend'
|
||||
}
|
||||
: {
|
||||
field: 'storage_gb_days' as Metric,
|
||||
order: 'descend' as const
|
||||
};
|
||||
if (
|
||||
next.field !== tableSort.field ||
|
||||
next.order !== tableSort.order
|
||||
) {
|
||||
setTableSort(next);
|
||||
setTablePage(1);
|
||||
}
|
||||
}}
|
||||
pagination={{
|
||||
size: 'middle',
|
||||
current: tablePage,
|
||||
pageSize: tableData?.pagination.perPage ?? 50,
|
||||
total: tableData?.pagination.total ?? 0,
|
||||
onChange: (p) => setTablePage(p)
|
||||
}}
|
||||
<StorageBreakdownTable
|
||||
key={t.key}
|
||||
groupKey={t.key}
|
||||
dateRange={dateRange}
|
||||
scope={scope}
|
||||
selectedUsers={selectedUsers}
|
||||
selectedVolumes={selectedVolumes}
|
||||
pageResetKey={pageResetKey}
|
||||
refreshKey={refreshKey}
|
||||
/>
|
||||
)
|
||||
}))}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQueryData } from '@/hooks/use-query-data-list';
|
||||
import {
|
||||
queryStorageBreakdown,
|
||||
ResourceBreakdownRequest,
|
||||
ResourceBreakdownResponse
|
||||
} from '../../apis/resource';
|
||||
|
||||
/**
|
||||
* Wraps the `queryStorageBreakdown` request with shared loading state and
|
||||
* in-flight cancellation (previous request is cancelled on each new fetch and
|
||||
* on unmount), so rapid filter/page/sort changes can't race a stale response
|
||||
* onto the table.
|
||||
*/
|
||||
export default function useQueryStorageBreakdown(option?: { key?: string }) {
|
||||
const { detailData, loading, cancelRequest, fetchData } = useQueryData<
|
||||
ResourceBreakdownResponse,
|
||||
ResourceBreakdownRequest
|
||||
>({
|
||||
fetchDetail: queryStorageBreakdown,
|
||||
key: option?.key || 'storageBreakdownTable'
|
||||
});
|
||||
|
||||
return {
|
||||
detailData,
|
||||
loading,
|
||||
cancelRequest,
|
||||
fetchData
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
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
|
||||
]);
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,83 @@
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { useMemo } from 'react';
|
||||
import { ResourceBreakdownItem } from '../../apis/resource';
|
||||
import { parseRollup } from '../../utils/time-buckets';
|
||||
|
||||
type GroupKey = 'volume' | 'user';
|
||||
|
||||
/**
|
||||
* Column factory for the storage breakdown tables, shared by the in-tab table
|
||||
* and the export preview. Sort indicators are uncontrolled (antd manages the
|
||||
* header arrows); the table reports changes through its `onChange`.
|
||||
*/
|
||||
const useStorageColumns = (groupKey: GroupKey) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return useMemo(() => {
|
||||
const valueCols = [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbDays' }),
|
||||
dataIndex: 'storage_gb_days',
|
||||
key: 'storage_gb_days',
|
||||
sorter: true,
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbHours' }),
|
||||
dataIndex: 'storage_gb_hours',
|
||||
key: 'storage_gb_hours',
|
||||
sorter: true,
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
}
|
||||
];
|
||||
// Last Active = the last active day. The backend sends a rollup-tz instant
|
||||
// with its offset; parseRollup keeps that wall clock (no browser-tz convert),
|
||||
// consistent with the trend chart buckets. Shown date-only.
|
||||
const lastActiveCol = {
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active',
|
||||
render: (v?: string) => (v ? parseRollup(v).format('YYYY-MM-DD') : '-')
|
||||
};
|
||||
if (groupKey === 'volume') {
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.tabs.storage' }),
|
||||
dataIndex: 'volume_name',
|
||||
key: 'volume_name'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.type' }),
|
||||
dataIndex: 'storage_type',
|
||||
key: 'storage_type',
|
||||
render: (_v: string, row: ResourceBreakdownItem) =>
|
||||
row.storage_type || row.gpu_type || '-'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.capacity' }),
|
||||
dataIndex: 'capacity_mib',
|
||||
key: 'capacity_mib',
|
||||
render: (v?: number) => (v ? `${Math.round(v / 1024)}GB` : '-')
|
||||
},
|
||||
...valueCols,
|
||||
lastActiveCol
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.user' }),
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name'
|
||||
},
|
||||
...valueCols,
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeStorage' }),
|
||||
dataIndex: 'active_volumes',
|
||||
key: 'active_volumes'
|
||||
},
|
||||
lastActiveCol
|
||||
];
|
||||
}, [groupKey, intl]);
|
||||
};
|
||||
|
||||
export default useStorageColumns;
|
||||
@@ -1,9 +1,7 @@
|
||||
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import PageBox from '@/pages/_components/page-box';
|
||||
import { IconFont, NoResult } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FilterOptionType } from '../../config/types';
|
||||
import useAPIKeys from '../../hooks/use-apikeys-columns';
|
||||
@@ -61,24 +59,6 @@ const APIKeys: React.FC<{
|
||||
}
|
||||
}, [pageResetKey]);
|
||||
|
||||
const renderEmpty = (type?: string) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
<NoResult
|
||||
loading={loading}
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-key" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.keys.nofound'
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.keys.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.keys.subTitle' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingPageResetRef.current && queryParams.page !== 1) {
|
||||
return;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import PluginExtraFields from '@/components/plugin-extra-fields';
|
||||
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import PageBox from '@/pages/_components/page-box';
|
||||
import { IconFont, NoResult } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { BreakdownItem, FilterOptionType } from '../../config/types';
|
||||
import useModelsColumns from '../../hooks/use-models-columns';
|
||||
@@ -64,24 +62,6 @@ const Models: React.FC<{
|
||||
}
|
||||
}, [pageResetKey]);
|
||||
|
||||
const renderEmpty = (type?: string) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
<NoResult
|
||||
loading={loading}
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-resources" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.mymodels.nofound'
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.deployments.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.deployments.subTitle' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingPageResetRef.current && queryParams.page !== 1) {
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { TABLE_SORT_DIRECTIONS } from '@/config/settings';
|
||||
import PageBox from '@/pages/_components/page-box';
|
||||
import { IconFont, NoResult } from '@gpustack/core-ui';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { Table } from 'antd';
|
||||
import _ from 'lodash';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { FilterOptionType } from '../../config/types';
|
||||
import useUsersColumns from '../../hooks/use-users-columns';
|
||||
@@ -52,24 +50,6 @@ const Users: React.FC<{
|
||||
|
||||
const columns = useUsersColumns();
|
||||
|
||||
const renderEmpty = (type?: string) => {
|
||||
if (type !== 'Table') return;
|
||||
return (
|
||||
<NoResult
|
||||
loading={loading}
|
||||
loadend={dataSource.loadend}
|
||||
dataSource={dataSource.dataList}
|
||||
image={<IconFont type="icon-users" />}
|
||||
filters={_.omit(queryParams, ['sort_by'])}
|
||||
noFoundText={intl.formatMessage({
|
||||
id: 'noresult.users.nofound'
|
||||
})}
|
||||
title={intl.formatMessage({ id: 'noresult.users.title' })}
|
||||
subTitle={intl.formatMessage({ id: 'noresult.users.subTitle' })}
|
||||
></NoResult>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (queryParams.page !== 1) {
|
||||
pendingPageResetRef.current = true;
|
||||
|
||||
Reference in New Issue
Block a user