chore(i18n): usage locales
This commit is contained in:
@@ -43,20 +43,38 @@ type Scope = 'self' | 'all';
|
||||
type Metric = 'gpu_hours' | 'instance_hours';
|
||||
type GroupKey = 'gpu_type' | 'instance' | 'user';
|
||||
|
||||
const METRIC_OPTIONS: { value: Metric; label: string }[] = [
|
||||
{ value: 'gpu_hours', label: 'GPU Hours' },
|
||||
{ value: 'instance_hours', label: 'Instance Hours' }
|
||||
];
|
||||
|
||||
const TABLE_TABS: { key: GroupKey; label: string }[] = [
|
||||
{ key: 'gpu_type', label: 'Instance Types' },
|
||||
{ key: 'instance', label: 'Instances' },
|
||||
{ key: 'user', label: 'Users' }
|
||||
];
|
||||
|
||||
const GpuInstancesTab: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const intl = useIntl();
|
||||
|
||||
const METRIC_OPTIONS: { value: Metric; label: string }[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: 'gpu_hours',
|
||||
label: intl.formatMessage({ id: 'usage.metric.gpuHours' })
|
||||
},
|
||||
{
|
||||
value: 'instance_hours',
|
||||
label: intl.formatMessage({ id: 'usage.metric.instanceHours' })
|
||||
}
|
||||
],
|
||||
[intl]
|
||||
);
|
||||
|
||||
const TABLE_TABS: { key: GroupKey; label: string }[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'gpu_type',
|
||||
label: intl.formatMessage({ id: 'usage.table.instanceTypes' })
|
||||
},
|
||||
{
|
||||
key: 'instance',
|
||||
label: intl.formatMessage({ id: 'usage.table.instances' })
|
||||
},
|
||||
{ key: 'user', label: intl.formatMessage({ id: 'usage.table.users' }) }
|
||||
],
|
||||
[intl]
|
||||
);
|
||||
// ``useCoolColors`` returns a memoized factory; resolve it once into a
|
||||
// fixed 5-slot palette here so the rest of the component reads as
|
||||
// array access.
|
||||
@@ -172,8 +190,8 @@ const GpuInstancesTab: React.FC = () => {
|
||||
) as string,
|
||||
value: (
|
||||
<MetricLabel
|
||||
text="GPU Hours"
|
||||
tooltip="Instance running time weighted by GPU count: an instance with N GPUs running for H hours counts as N × H GPU-hours. Equal to Instance Hours when every instance uses a single GPU."
|
||||
text={intl.formatMessage({ id: 'usage.metric.gpuHours' })}
|
||||
tooltip={intl.formatMessage({ id: 'usage.metric.gpuHours.tip' })}
|
||||
/>
|
||||
),
|
||||
color: coolColors[0]
|
||||
@@ -184,24 +202,26 @@ const GpuInstancesTab: React.FC = () => {
|
||||
) as string,
|
||||
value: (
|
||||
<MetricLabel
|
||||
text="Instance Hours"
|
||||
tooltip="Total running time summed across all instances, regardless of how many GPUs each uses. One instance running for 2 hours = 2 instance-hours."
|
||||
text={intl.formatMessage({ id: 'usage.metric.instanceHours' })}
|
||||
tooltip={intl.formatMessage({
|
||||
id: 'usage.metric.instanceHours.tip'
|
||||
})}
|
||||
/>
|
||||
),
|
||||
color: coolColors[1]
|
||||
},
|
||||
{
|
||||
label: (summary?.active_instances ?? 0).toString(),
|
||||
value: 'Active Instances',
|
||||
value: intl.formatMessage({ id: 'usage.metric.activeInstances' }),
|
||||
color: coolColors[2]
|
||||
},
|
||||
{
|
||||
label: (summary?.active_users ?? 0).toString(),
|
||||
value: 'Active Users',
|
||||
value: intl.formatMessage({ id: 'usage.metric.activeUsers' }),
|
||||
color: coolColors[3]
|
||||
}
|
||||
],
|
||||
[summary, coolColors]
|
||||
[summary, coolColors, intl]
|
||||
);
|
||||
|
||||
// Build chart series — single series of the selected metric, plotted
|
||||
@@ -238,7 +258,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
const tableColumns = useMemo(() => {
|
||||
const baseValueCols = [
|
||||
{
|
||||
title: 'GPU Hours',
|
||||
title: intl.formatMessage({ id: 'usage.metric.gpuHours' }),
|
||||
dataIndex: 'gpu_hours',
|
||||
key: 'gpu_hours',
|
||||
sorter: true,
|
||||
@@ -246,7 +266,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: 'Instance Hours',
|
||||
title: intl.formatMessage({ id: 'usage.metric.instanceHours' }),
|
||||
dataIndex: 'instance_hours',
|
||||
key: 'instance_hours',
|
||||
sorter: true,
|
||||
@@ -258,7 +278,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
// Instance Types breakdown: just the pretty product name (or flavor slug
|
||||
// for older rows) — no spec sub-line.
|
||||
const instanceTypeColType = {
|
||||
title: 'Instance Type',
|
||||
title: intl.formatMessage({ id: 'usage.table.instanceType' }),
|
||||
dataIndex: 'gpu_type',
|
||||
key: 'gpu_type',
|
||||
render: (_v: string, row: ResourceBreakdownItem) => instanceTypeLabel(row)
|
||||
@@ -266,7 +286,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
// Instances breakdown: render exactly like the GPU Instances list —
|
||||
// "<product> x <count>" plus the categorized spec popover behind the icon.
|
||||
const instanceTypeColInstance = {
|
||||
title: 'Instance Type',
|
||||
title: intl.formatMessage({ id: 'usage.table.instanceType' }),
|
||||
dataIndex: 'gpu_type',
|
||||
key: 'gpu_type',
|
||||
render: (_v: string, row: ResourceBreakdownItem) => (
|
||||
@@ -285,26 +305,46 @@ const GpuInstancesTab: React.FC = () => {
|
||||
instanceTypeColType,
|
||||
...baseValueCols,
|
||||
{
|
||||
title: 'Active Instances',
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeInstances' }),
|
||||
dataIndex: 'active_instances',
|
||||
key: 'active_instances'
|
||||
},
|
||||
{ title: 'Last Active', dataIndex: 'last_active', key: 'last_active' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active'
|
||||
}
|
||||
];
|
||||
}
|
||||
if (activeTableTab === 'instance') {
|
||||
return [
|
||||
{ title: 'Instance', dataIndex: 'instance_name', key: 'instance_name' },
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.instance' }),
|
||||
dataIndex: 'instance_name',
|
||||
key: 'instance_name'
|
||||
},
|
||||
instanceTypeColInstance,
|
||||
...baseValueCols,
|
||||
{ title: 'Last Active', dataIndex: 'last_active', key: 'last_active' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active'
|
||||
}
|
||||
];
|
||||
}
|
||||
// user tab
|
||||
return [
|
||||
{ title: 'User', dataIndex: 'user_name', key: 'user_name' },
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.user' }),
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name'
|
||||
},
|
||||
...baseValueCols,
|
||||
{ title: 'Last Active', dataIndex: 'last_active', key: 'last_active' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active'
|
||||
}
|
||||
];
|
||||
}, [activeTableTab, tableSort, intl]);
|
||||
|
||||
@@ -319,25 +359,33 @@ const GpuInstancesTab: React.FC = () => {
|
||||
)}`;
|
||||
|
||||
const chartExportColumns = [
|
||||
{ title: 'Date', dataIndex: 'date', key: 'date' },
|
||||
{
|
||||
title: 'GPU Hours',
|
||||
title: intl.formatMessage({ id: 'usage.table.date' }),
|
||||
dataIndex: 'date',
|
||||
key: 'date'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.gpuHours' }),
|
||||
dataIndex: 'gpu_hours',
|
||||
key: 'gpu_hours',
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: 'Instance Hours',
|
||||
title: intl.formatMessage({ id: 'usage.metric.instanceHours' }),
|
||||
dataIndex: 'instance_hours',
|
||||
key: 'instance_hours',
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: 'Active Instances',
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeInstances' }),
|
||||
dataIndex: 'active_instances',
|
||||
key: 'active_instances'
|
||||
},
|
||||
{ title: 'Active Users', dataIndex: 'active_users', key: 'active_users' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeUsers' }),
|
||||
dataIndex: 'active_users',
|
||||
key: 'active_users'
|
||||
}
|
||||
];
|
||||
|
||||
const tabLabel = TABLE_TABS.find((t) => t.key === activeTableTab)?.label;
|
||||
@@ -347,7 +395,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
groupBy: 'date' as const,
|
||||
columns: chartExportColumns,
|
||||
fileName: `gpu-instances_chart_${dateSuffix}.xlsx`,
|
||||
sheetName: 'GPU Instances'
|
||||
sheetName: intl.formatMessage({ id: 'usage.tabs.gpuInstances' })
|
||||
}
|
||||
: {
|
||||
groupBy: activeTableTab,
|
||||
@@ -379,7 +427,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
setSelectedInstances(ids);
|
||||
setTablePage(1);
|
||||
},
|
||||
placeholder: 'Filter by instance'
|
||||
placeholder: intl.formatMessage({ id: 'usage.filter.instance' })
|
||||
}}
|
||||
onRefresh={() => setRefreshKey((k) => k + 1)}
|
||||
onExportChart={() => setExportMode('chart')}
|
||||
@@ -467,8 +515,11 @@ const GpuInstancesTab: React.FC = () => {
|
||||
onCancel={() => setExportMode(null)}
|
||||
title={
|
||||
exportMode === 'chart'
|
||||
? 'Export Chart Data'
|
||||
: `Export Table Data — ${tabLabel}`
|
||||
? intl.formatMessage({ id: 'usage.export.chart' })
|
||||
: intl.formatMessage(
|
||||
{ id: 'usage.export.tableNamed' },
|
||||
{ name: tabLabel }
|
||||
)
|
||||
}
|
||||
queryFn={queryGpuInstancesBreakdown}
|
||||
groupBy={exportConfig.groupBy}
|
||||
@@ -480,7 +531,7 @@ const GpuInstancesTab: React.FC = () => {
|
||||
userOptions={userOptions}
|
||||
resourceFilter={{
|
||||
options: instanceOptions,
|
||||
placeholder: 'Filter by instance',
|
||||
placeholder: intl.formatMessage({ id: 'usage.filter.instance' }),
|
||||
key: 'instance_ids'
|
||||
}}
|
||||
initialDateRange={dateRange}
|
||||
|
||||
@@ -20,28 +20,6 @@ import {
|
||||
import useResourceMeta from '../hooks/use-resource-meta';
|
||||
import ResourceFilterBar from './resource-filter-bar';
|
||||
|
||||
// Users only ever see "Storage" in the product — never "Persistent Volume".
|
||||
const RESOURCE_TYPE_LABELS: Record<string, string> = {
|
||||
gpu_instance: 'GPU Instance',
|
||||
cpu_instance: 'CPU Instance',
|
||||
persistent_volume: 'Storage'
|
||||
};
|
||||
|
||||
const RESOURCE_TYPE_OPTIONS = [
|
||||
{ value: 'gpu_instance', label: RESOURCE_TYPE_LABELS.gpu_instance },
|
||||
{ value: 'persistent_volume', label: RESOURCE_TYPE_LABELS.persistent_volume }
|
||||
];
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{ value: 'created', label: 'Created' },
|
||||
{ value: 'deleted', label: 'Deleted' },
|
||||
{ value: 'phase_to_metered', label: 'Started' },
|
||||
{ value: 'phase_left_metered', label: 'Stopped' },
|
||||
{ value: 'updated', label: 'Updated' },
|
||||
{ value: 'attached', label: 'Attached' },
|
||||
{ value: 'detached', label: 'Detached' }
|
||||
];
|
||||
|
||||
const EVENT_COLOR: Record<string, string> = {
|
||||
created: 'green',
|
||||
deleted: 'red',
|
||||
@@ -52,14 +30,6 @@ const EVENT_COLOR: Record<string, string> = {
|
||||
detached: 'gold'
|
||||
};
|
||||
|
||||
// Raw event_type → human-readable label (the same wording as the filter).
|
||||
// ``phase_to_metered`` / ``phase_left_metered`` bracket the metering window —
|
||||
// when the resource starts / stops accruing metered uptime (the OSS build only
|
||||
// meters, it doesn't charge). Users shouldn't see the internal enum names.
|
||||
const EVENT_LABEL: Record<string, string> = Object.fromEntries(
|
||||
EVENT_TYPE_OPTIONS.map((o) => [o.value, o.label])
|
||||
);
|
||||
|
||||
// Humanize a failure phase enum for display, e.g. "SSHPublicKeyCreateFailed" →
|
||||
// "SSH Public Key Create Failed" (fallback when the backend has no detail).
|
||||
const humanizePhase = (phase: string): string =>
|
||||
@@ -73,6 +43,75 @@ const ResourceEvents: React.FC = () => {
|
||||
const canManageUsers = !!access.canSeeOrgAdmin;
|
||||
const scope = canManageUsers ? 'all' : 'self';
|
||||
|
||||
// Users only ever see "Storage" in the product — never "Persistent Volume".
|
||||
const RESOURCE_TYPE_LABELS: Record<string, string> = useMemo(
|
||||
() => ({
|
||||
gpu_instance: intl.formatMessage({
|
||||
id: 'usage.events.resource.gpuInstance'
|
||||
}),
|
||||
cpu_instance: intl.formatMessage({
|
||||
id: 'usage.events.resource.cpuInstance'
|
||||
}),
|
||||
persistent_volume: intl.formatMessage({ id: 'usage.tabs.storage' })
|
||||
}),
|
||||
[intl]
|
||||
);
|
||||
|
||||
const RESOURCE_TYPE_OPTIONS = useMemo(
|
||||
() => [
|
||||
{ value: 'gpu_instance', label: RESOURCE_TYPE_LABELS.gpu_instance },
|
||||
{
|
||||
value: 'persistent_volume',
|
||||
label: RESOURCE_TYPE_LABELS.persistent_volume
|
||||
}
|
||||
],
|
||||
[RESOURCE_TYPE_LABELS]
|
||||
);
|
||||
|
||||
const EVENT_TYPE_OPTIONS = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: 'created',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.created' })
|
||||
},
|
||||
{
|
||||
value: 'deleted',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.deleted' })
|
||||
},
|
||||
{
|
||||
value: 'phase_to_metered',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.started' })
|
||||
},
|
||||
{
|
||||
value: 'phase_left_metered',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.stopped' })
|
||||
},
|
||||
{
|
||||
value: 'updated',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.updated' })
|
||||
},
|
||||
{
|
||||
value: 'attached',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.attached' })
|
||||
},
|
||||
{
|
||||
value: 'detached',
|
||||
label: intl.formatMessage({ id: 'usage.events.type.detached' })
|
||||
}
|
||||
],
|
||||
[intl]
|
||||
);
|
||||
|
||||
// Raw event_type → human-readable label (the same wording as the filter).
|
||||
// ``phase_to_metered`` / ``phase_left_metered`` bracket the metering window —
|
||||
// when the resource starts / stops accruing metered uptime (the OSS build
|
||||
// only meters, it doesn't charge). Users shouldn't see the internal enum
|
||||
// names.
|
||||
const EVENT_LABEL: Record<string, string> = useMemo(
|
||||
() => Object.fromEntries(EVENT_TYPE_OPTIONS.map((o) => [o.value, o.label])),
|
||||
[EVENT_TYPE_OPTIONS]
|
||||
);
|
||||
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>([
|
||||
dayjs().subtract(29, 'day'),
|
||||
dayjs()
|
||||
@@ -113,7 +152,7 @@ const ResourceEvents: React.FC = () => {
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: 'Time',
|
||||
title: intl.formatMessage({ id: 'usage.events.col.time' }),
|
||||
dataIndex: 'occurred_at',
|
||||
key: 'occurred_at',
|
||||
render: (v: string) =>
|
||||
@@ -121,19 +160,19 @@ const ResourceEvents: React.FC = () => {
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: 'Resource',
|
||||
title: intl.formatMessage({ id: 'usage.events.col.resource' }),
|
||||
dataIndex: 'resource_type',
|
||||
key: 'resource_type',
|
||||
render: (v: string) => RESOURCE_TYPE_LABELS[v] || v,
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
title: intl.formatMessage({ id: 'usage.table.name' }),
|
||||
dataIndex: 'resource_name',
|
||||
key: 'resource_name'
|
||||
},
|
||||
{
|
||||
title: 'Event',
|
||||
title: intl.formatMessage({ id: 'usage.events.col.event' }),
|
||||
dataIndex: 'event_type',
|
||||
key: 'event_type',
|
||||
render: (v: string) => (
|
||||
@@ -142,7 +181,7 @@ const ResourceEvents: React.FC = () => {
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: 'Message',
|
||||
title: intl.formatMessage({ id: 'usage.events.col.message' }),
|
||||
dataIndex: 'event_message',
|
||||
key: 'event_message',
|
||||
render: (v?: string, row?: ResourceEventItem) => {
|
||||
@@ -159,7 +198,7 @@ const ResourceEvents: React.FC = () => {
|
||||
}
|
||||
}
|
||||
],
|
||||
[]
|
||||
[intl, RESOURCE_TYPE_LABELS, EVENT_LABEL]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -184,8 +223,7 @@ const ResourceEvents: React.FC = () => {
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'usage.events.resourceType',
|
||||
defaultMessage: 'Resource type'
|
||||
id: 'usage.events.resourceType'
|
||||
})}
|
||||
value={resourceTypes}
|
||||
onChange={(v) => {
|
||||
@@ -199,8 +237,7 @@ const ResourceEvents: React.FC = () => {
|
||||
mode="multiple"
|
||||
allowClear
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'usage.events.eventType',
|
||||
defaultMessage: 'Event type'
|
||||
id: 'usage.events.eventType'
|
||||
})}
|
||||
value={eventTypes}
|
||||
onChange={(v) => {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import useCoolColors from '@/hooks/use-cool-colors';
|
||||
import { formatLargeNumber } from '@/utils';
|
||||
import { SimpleCard } from '@gpustack/core-ui';
|
||||
import { useAccess } from '@umijs/max';
|
||||
import { useAccess, useIntl } from '@umijs/max';
|
||||
import { Table, Tabs } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
@@ -39,20 +39,36 @@ type Scope = 'self' | 'all';
|
||||
type Metric = 'storage_gb_days' | 'storage_gb_hours';
|
||||
type GroupKey = 'volume' | 'user';
|
||||
|
||||
const METRIC_OPTIONS: { value: Metric; label: string }[] = [
|
||||
{ value: 'storage_gb_days', label: 'GB-Days' },
|
||||
{ value: 'storage_gb_hours', label: 'GB-Hours' }
|
||||
];
|
||||
|
||||
const TABLE_TABS: { key: GroupKey; label: string }[] = [
|
||||
{ key: 'volume', label: 'Storage' },
|
||||
{ key: 'user', label: 'Users' }
|
||||
];
|
||||
|
||||
const StorageTab: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const intl = useIntl();
|
||||
const coolColors = useCoolColors()(4);
|
||||
|
||||
const METRIC_OPTIONS: { value: Metric; label: string }[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: 'storage_gb_days',
|
||||
label: intl.formatMessage({ id: 'usage.metric.gbDays' })
|
||||
},
|
||||
{
|
||||
value: 'storage_gb_hours',
|
||||
label: intl.formatMessage({ id: 'usage.metric.gbHours' })
|
||||
}
|
||||
],
|
||||
[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]
|
||||
);
|
||||
|
||||
// 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.
|
||||
const canManageUsers = !!access.canSeeOrgAdmin;
|
||||
@@ -160,8 +176,8 @@ const StorageTab: React.FC = () => {
|
||||
) as string,
|
||||
value: (
|
||||
<MetricLabel
|
||||
text="GB-Days"
|
||||
tooltip="Storage capacity integrated over time, in GB × days: 10 GB kept for 5 days = 50 GB-days. (= GB-Hours ÷ 24)"
|
||||
text={intl.formatMessage({ id: 'usage.metric.gbDays' })}
|
||||
tooltip={intl.formatMessage({ id: 'usage.metric.gbDays.tip' })}
|
||||
/>
|
||||
),
|
||||
color: coolColors[0]
|
||||
@@ -172,24 +188,24 @@ const StorageTab: React.FC = () => {
|
||||
) as string,
|
||||
value: (
|
||||
<MetricLabel
|
||||
text="GB-Hours"
|
||||
tooltip="Storage capacity integrated over time, in GB × hours: 10 GB kept for 5 hours = 50 GB-hours."
|
||||
text={intl.formatMessage({ id: 'usage.metric.gbHours' })}
|
||||
tooltip={intl.formatMessage({ id: 'usage.metric.gbHours.tip' })}
|
||||
/>
|
||||
),
|
||||
color: coolColors[1]
|
||||
},
|
||||
{
|
||||
label: (summary?.active_volumes ?? 0).toString(),
|
||||
value: 'Storage',
|
||||
value: intl.formatMessage({ id: 'usage.tabs.storage' }),
|
||||
color: coolColors[2]
|
||||
},
|
||||
{
|
||||
label: (summary?.active_users ?? 0).toString(),
|
||||
value: 'Active Users',
|
||||
value: intl.formatMessage({ id: 'usage.metric.activeUsers' }),
|
||||
color: coolColors[3]
|
||||
}
|
||||
],
|
||||
[summary, coolColors]
|
||||
[summary, coolColors, intl]
|
||||
);
|
||||
|
||||
const dataByDate = useMemo(() => {
|
||||
@@ -223,7 +239,7 @@ const StorageTab: React.FC = () => {
|
||||
const tableColumns = useMemo(() => {
|
||||
const valueCols = [
|
||||
{
|
||||
title: 'GB-Days',
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbDays' }),
|
||||
dataIndex: 'storage_gb_days',
|
||||
key: 'storage_gb_days',
|
||||
sorter: true,
|
||||
@@ -232,7 +248,7 @@ const StorageTab: React.FC = () => {
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: 'GB-Hours',
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbHours' }),
|
||||
dataIndex: 'storage_gb_hours',
|
||||
key: 'storage_gb_hours',
|
||||
sorter: true,
|
||||
@@ -244,38 +260,50 @@ const StorageTab: React.FC = () => {
|
||||
if (activeTableTab === 'volume') {
|
||||
return [
|
||||
{
|
||||
title: 'Storage',
|
||||
title: intl.formatMessage({ id: 'usage.tabs.storage' }),
|
||||
dataIndex: 'volume_name',
|
||||
key: 'volume_name'
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
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: 'Capacity',
|
||||
title: intl.formatMessage({ id: 'usage.table.capacity' }),
|
||||
dataIndex: 'capacity_mib',
|
||||
key: 'capacity_mib',
|
||||
render: (v?: number) => (v ? `${Math.round(v / 1024)}GB` : '-')
|
||||
},
|
||||
...valueCols,
|
||||
{ title: 'Last Active', dataIndex: 'last_active', key: 'last_active' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active'
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ title: 'User', dataIndex: 'user_name', key: 'user_name' },
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.user' }),
|
||||
dataIndex: 'user_name',
|
||||
key: 'user_name'
|
||||
},
|
||||
...valueCols,
|
||||
{
|
||||
title: 'Active Storage',
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeStorage' }),
|
||||
dataIndex: 'active_volumes',
|
||||
key: 'active_volumes'
|
||||
},
|
||||
{ title: 'Last Active', dataIndex: 'last_active', key: 'last_active' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.table.lastActive' }),
|
||||
dataIndex: 'last_active',
|
||||
key: 'last_active'
|
||||
}
|
||||
];
|
||||
}, [activeTableTab, tableSort]);
|
||||
}, [activeTableTab, tableSort, intl]);
|
||||
|
||||
const tableRows: ResourceBreakdownItem[] = tableData?.items ?? [];
|
||||
|
||||
@@ -288,25 +316,33 @@ const StorageTab: React.FC = () => {
|
||||
)}`;
|
||||
|
||||
const chartExportColumns = [
|
||||
{ title: 'Date', dataIndex: 'date', key: 'date' },
|
||||
{
|
||||
title: 'GB-Days',
|
||||
title: intl.formatMessage({ id: 'usage.table.date' }),
|
||||
dataIndex: 'date',
|
||||
key: 'date'
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbDays' }),
|
||||
dataIndex: 'storage_gb_days',
|
||||
key: 'storage_gb_days',
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: 'GB-Hours',
|
||||
title: intl.formatMessage({ id: 'usage.metric.gbHours' }),
|
||||
dataIndex: 'storage_gb_hours',
|
||||
key: 'storage_gb_hours',
|
||||
render: (v: number) => (v ?? 0).toFixed(2)
|
||||
},
|
||||
{
|
||||
title: 'Active Volumes',
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeVolumes' }),
|
||||
dataIndex: 'active_volumes',
|
||||
key: 'active_volumes'
|
||||
},
|
||||
{ title: 'Active Users', dataIndex: 'active_users', key: 'active_users' }
|
||||
{
|
||||
title: intl.formatMessage({ id: 'usage.metric.activeUsers' }),
|
||||
dataIndex: 'active_users',
|
||||
key: 'active_users'
|
||||
}
|
||||
];
|
||||
|
||||
const tabLabel = TABLE_TABS.find((t) => t.key === activeTableTab)?.label;
|
||||
@@ -316,7 +352,7 @@ const StorageTab: React.FC = () => {
|
||||
groupBy: 'date' as const,
|
||||
columns: chartExportColumns,
|
||||
fileName: `storage_chart_${dateSuffix}.xlsx`,
|
||||
sheetName: 'Storage'
|
||||
sheetName: intl.formatMessage({ id: 'usage.tabs.storage' })
|
||||
}
|
||||
: {
|
||||
groupBy: activeTableTab,
|
||||
@@ -347,7 +383,7 @@ const StorageTab: React.FC = () => {
|
||||
setSelectedVolumes(ids);
|
||||
setTablePage(1);
|
||||
},
|
||||
placeholder: 'Filter by storage'
|
||||
placeholder: intl.formatMessage({ id: 'usage.filter.storage' })
|
||||
}}
|
||||
onRefresh={() => setRefreshKey((k) => k + 1)}
|
||||
onExportChart={() => setExportMode('chart')}
|
||||
@@ -435,8 +471,11 @@ const StorageTab: React.FC = () => {
|
||||
onCancel={() => setExportMode(null)}
|
||||
title={
|
||||
exportMode === 'chart'
|
||||
? 'Export Chart Data'
|
||||
: `Export Table Data — ${tabLabel}`
|
||||
? intl.formatMessage({ id: 'usage.export.chart' })
|
||||
: intl.formatMessage(
|
||||
{ id: 'usage.export.tableNamed' },
|
||||
{ name: tabLabel }
|
||||
)
|
||||
}
|
||||
queryFn={queryStorageBreakdown}
|
||||
groupBy={exportConfig.groupBy}
|
||||
@@ -448,7 +487,7 @@ const StorageTab: React.FC = () => {
|
||||
userOptions={userOptions}
|
||||
resourceFilter={{
|
||||
options: volumeOptions,
|
||||
placeholder: 'Filter by storage',
|
||||
placeholder: intl.formatMessage({ id: 'usage.filter.storage' }),
|
||||
key: 'volume_ids'
|
||||
}}
|
||||
initialDateRange={dateRange}
|
||||
|
||||
@@ -25,7 +25,7 @@ import useCoolColors from '@/hooks/use-cool-colors';
|
||||
import BarChart from '@/pages/_components/bar-chart';
|
||||
import PieChart from '@/pages/_components/pie-chart';
|
||||
import { formatLargeNumber } from '@/utils';
|
||||
import { useAccess } from '@umijs/max';
|
||||
import { useAccess, useIntl } from '@umijs/max';
|
||||
import { Card, Col, Empty, Row } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
@@ -117,12 +117,13 @@ const DomainSection: React.FC<{
|
||||
trendColor,
|
||||
trendGran
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const donutTotal = round2(donutData.reduce((s, d) => s + (d.value || 0), 0));
|
||||
const trendEmpty = trendData.every((v) => !v);
|
||||
const empty = (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="No data"
|
||||
description={intl.formatMessage({ id: 'usage.common.noData' })}
|
||||
style={{ margin: '32px 0' }}
|
||||
/>
|
||||
);
|
||||
@@ -200,6 +201,8 @@ const DomainSection: React.FC<{
|
||||
|
||||
const SummaryTab: React.FC = () => {
|
||||
const access = useAccess();
|
||||
const intl = useIntl();
|
||||
const t = (id: string) => intl.formatMessage({ id });
|
||||
const coolColors = useCoolColors()(8);
|
||||
|
||||
// No All/My dropdown (matches the Tokens tab): managers see the org-wide
|
||||
@@ -302,8 +305,8 @@ const SummaryTab: React.FC = () => {
|
||||
// --- derived: donuts ---
|
||||
const tokenDonut = useMemo(
|
||||
() => [
|
||||
{ name: 'Input', value: summary?.input_tokens ?? 0 },
|
||||
{ name: 'Output', value: summary?.output_tokens ?? 0 }
|
||||
{ name: t('usage.metric.input'), value: summary?.input_tokens ?? 0 },
|
||||
{ name: t('usage.metric.output'), value: summary?.output_tokens ?? 0 }
|
||||
],
|
||||
[summary]
|
||||
);
|
||||
@@ -320,7 +323,7 @@ const SummaryTab: React.FC = () => {
|
||||
(storageByType?.items ?? [])
|
||||
.filter((i) => (i.storage_gb_days || 0) > 0)
|
||||
.map((i) => ({
|
||||
name: i.gpu_type || 'unknown',
|
||||
name: i.gpu_type || t('usage.common.unknown'),
|
||||
value: i.storage_gb_days
|
||||
})),
|
||||
[storageByType]
|
||||
@@ -386,23 +389,32 @@ const SummaryTab: React.FC = () => {
|
||||
<Row gutter={[0, 12]}>
|
||||
<Col span={24}>
|
||||
<DomainSection
|
||||
title="Tokens"
|
||||
title={t('usage.metric.tokens')}
|
||||
accent={coolColors[0]}
|
||||
donutData={tokenDonut}
|
||||
donutTotalLabel="Tokens"
|
||||
trendTitle="Tokens over time"
|
||||
donutTotalLabel={t('usage.metric.tokens')}
|
||||
trendTitle={t('usage.summary.tokensOverTime')}
|
||||
trendXAxis={tokenTrend.xAxis}
|
||||
trendData={tokenTrend.data}
|
||||
trendColor={coolColors[0]}
|
||||
trendGran={tokenGran}
|
||||
headline={
|
||||
<>
|
||||
<Stat value={fmt(summary?.total_tokens)} label="Tokens" />
|
||||
<Stat value={fmt(summary?.input_tokens)} label="Input" />
|
||||
<Stat value={fmt(summary?.output_tokens)} label="Output" />
|
||||
<Stat
|
||||
value={fmt(summary?.total_tokens)}
|
||||
label={t('usage.metric.tokens')}
|
||||
/>
|
||||
<Stat
|
||||
value={fmt(summary?.input_tokens)}
|
||||
label={t('usage.metric.input')}
|
||||
/>
|
||||
<Stat
|
||||
value={fmt(summary?.output_tokens)}
|
||||
label={t('usage.metric.output')}
|
||||
/>
|
||||
<Stat
|
||||
value={summary?.token_active_users ?? 0}
|
||||
label="Active Users"
|
||||
label={t('usage.metric.activeUsers')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@@ -411,25 +423,28 @@ const SummaryTab: React.FC = () => {
|
||||
|
||||
<Col span={24}>
|
||||
<DomainSection
|
||||
title="Compute"
|
||||
title={t('usage.summary.compute')}
|
||||
accent={coolColors[1]}
|
||||
donutData={computeDonut}
|
||||
donutTotalLabel="GPU Hours"
|
||||
trendTitle="GPU Hours over time"
|
||||
donutTotalLabel={t('usage.metric.gpuHours')}
|
||||
trendTitle={t('usage.summary.gpuHoursOverTime')}
|
||||
trendXAxis={computeTrend.xAxis}
|
||||
trendData={computeTrend.data}
|
||||
trendColor={coolColors[1]}
|
||||
trendGran={granularity}
|
||||
headline={
|
||||
<>
|
||||
<Stat value={fmt(summary?.gpu_hours)} label="GPU Hours" />
|
||||
<Stat
|
||||
value={fmt(summary?.gpu_hours)}
|
||||
label={t('usage.metric.gpuHours')}
|
||||
/>
|
||||
<Stat
|
||||
value={fmt(summary?.instance_hours)}
|
||||
label="Instance Hours"
|
||||
label={t('usage.metric.instanceHours')}
|
||||
/>
|
||||
<Stat
|
||||
value={computeSum?.active_instances ?? 0}
|
||||
label="Active Instances"
|
||||
label={t('usage.metric.activeInstances')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@@ -438,23 +453,29 @@ const SummaryTab: React.FC = () => {
|
||||
|
||||
<Col span={24}>
|
||||
<DomainSection
|
||||
title="Storage"
|
||||
title={t('usage.tabs.storage')}
|
||||
accent={coolColors[3]}
|
||||
donutData={storageDonut}
|
||||
donutTotalLabel="GB-Days"
|
||||
trendTitle="GB-Days over time"
|
||||
donutTotalLabel={t('usage.metric.gbDays')}
|
||||
trendTitle={t('usage.summary.gbDaysOverTime')}
|
||||
trendXAxis={storageTrend.xAxis}
|
||||
trendData={storageTrend.data}
|
||||
trendColor={coolColors[3]}
|
||||
trendGran={granularity}
|
||||
headline={
|
||||
<>
|
||||
<Stat value={fmt(summary?.storage_gb_days)} label="GB-Days" />
|
||||
<Stat
|
||||
value={fmt(summary?.storage_gb_days)}
|
||||
label={t('usage.metric.gbDays')}
|
||||
/>
|
||||
<Stat
|
||||
value={storageSum?.active_volumes ?? 0}
|
||||
label="Active Storage"
|
||||
label={t('usage.metric.activeStorage')}
|
||||
/>
|
||||
<Stat
|
||||
value={storageDonut.length}
|
||||
label={t('usage.metric.storageTypes')}
|
||||
/>
|
||||
<Stat value={storageDonut.length} label="Storage Types" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user