diff --git a/src/locales/en-US/usage.ts b/src/locales/en-US/usage.ts
index ece2734d..588f2e76 100644
--- a/src/locales/en-US/usage.ts
+++ b/src/locales/en-US/usage.ts
@@ -38,5 +38,69 @@ export default {
'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
- 'usage.table.inputTokensCached': 'Input Tokens Cached'
+ 'usage.table.inputTokensCached': 'Input Tokens Cached',
+
+ // --- Resource usage: shared metrics & units ---
+ 'usage.metric.tokens': 'Tokens',
+ 'usage.metric.input': 'Input',
+ 'usage.metric.output': 'Output',
+ 'usage.metric.gpuHours': 'GPU Hours',
+ 'usage.metric.instanceHours': 'Instance Hours',
+ 'usage.metric.gbDays': 'GB-Days',
+ 'usage.metric.gbHours': 'GB-Hours',
+ 'usage.metric.activeUsers': 'Active Users',
+ 'usage.metric.activeInstances': 'Active Instances',
+ 'usage.metric.activeStorage': 'Active Storage',
+ 'usage.metric.activeVolumes': 'Active Volumes',
+ 'usage.metric.storageTypes': 'Storage Types',
+ 'usage.metric.gpuHours.tip':
+ '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.',
+ 'usage.metric.instanceHours.tip':
+ 'Total running time summed across all instances, regardless of how many GPUs each uses. One instance running for 2 hours = 2 instance-hours.',
+ 'usage.metric.gbDays.tip':
+ 'Storage capacity integrated over time, in GB × days: 10 GB kept for 5 days = 50 GB-days. (= GB-Hours ÷ 24)',
+ 'usage.metric.gbHours.tip':
+ 'Storage capacity integrated over time, in GB × hours: 10 GB kept for 5 hours = 50 GB-hours.',
+
+ // --- Resource usage: common table / labels ---
+ 'usage.common.noData': 'No data',
+ 'usage.common.unknown': 'unknown',
+ 'usage.table.date': 'Date',
+ 'usage.table.name': 'Name',
+ 'usage.table.user': 'User',
+ 'usage.table.users': 'Users',
+ 'usage.table.type': 'Type',
+ 'usage.table.instance': 'Instance',
+ 'usage.table.instanceType': 'Instance Type',
+ 'usage.table.instanceTypes': 'Instance Types',
+ 'usage.table.instances': 'Instances',
+ 'usage.table.capacity': 'Capacity',
+ 'usage.export.tableNamed': 'Export Table Data — {name}',
+
+ // --- Summary tab ---
+ 'usage.summary.compute': 'Compute',
+ 'usage.summary.tokensOverTime': 'Tokens over time',
+ 'usage.summary.gpuHoursOverTime': 'GPU Hours over time',
+ 'usage.summary.gbDaysOverTime': 'GB-Days over time',
+
+ // --- GPU Instances / Storage filters ---
+ 'usage.filter.instance': 'Filter by instance',
+ 'usage.filter.storage': 'Filter by storage',
+
+ // --- Resource events ---
+ 'usage.events.resourceType': 'Resource type',
+ 'usage.events.eventType': 'Event type',
+ 'usage.events.col.time': 'Time',
+ 'usage.events.col.resource': 'Resource',
+ 'usage.events.col.event': 'Event',
+ 'usage.events.col.message': 'Message',
+ 'usage.events.resource.gpuInstance': 'GPU Instance',
+ 'usage.events.resource.cpuInstance': 'CPU Instance',
+ 'usage.events.type.created': 'Created',
+ 'usage.events.type.deleted': 'Deleted',
+ 'usage.events.type.started': 'Started',
+ 'usage.events.type.stopped': 'Stopped',
+ 'usage.events.type.updated': 'Updated',
+ 'usage.events.type.attached': 'Attached',
+ 'usage.events.type.detached': 'Detached'
};
diff --git a/src/locales/ja-JP/usage.ts b/src/locales/ja-JP/usage.ts
index 522000ae..8740a752 100644
--- a/src/locales/ja-JP/usage.ts
+++ b/src/locales/ja-JP/usage.ts
@@ -38,5 +38,69 @@ export default {
'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
- 'usage.table.inputTokensCached': 'Input Tokens Cached'
+ 'usage.table.inputTokensCached': 'Input Tokens Cached',
+
+ // --- Resource usage: shared metrics & units ---
+ 'usage.metric.tokens': 'トークン数',
+ 'usage.metric.input': '入力',
+ 'usage.metric.output': '出力',
+ 'usage.metric.gpuHours': 'GPU 時間',
+ 'usage.metric.instanceHours': 'インスタンス時間',
+ 'usage.metric.gbDays': 'GB·日',
+ 'usage.metric.gbHours': 'GB·時間',
+ 'usage.metric.activeUsers': 'アクティブユーザー',
+ 'usage.metric.activeInstances': 'アクティブインスタンス',
+ 'usage.metric.activeStorage': 'アクティブストレージ',
+ 'usage.metric.activeVolumes': 'アクティブボリューム',
+ 'usage.metric.storageTypes': 'ストレージタイプ',
+ 'usage.metric.gpuHours.tip':
+ 'インスタンスの稼働時間を GPU 数で重み付けした値:N 個の GPU を使用するインスタンスが H 時間稼働すると N × H GPU 時間としてカウントされます。すべてのインスタンスが単一の GPU を使用する場合はインスタンス時間と等しくなります。',
+ 'usage.metric.instanceHours.tip':
+ 'GPU の数に関係なく、すべてのインスタンスの稼働時間を合計した値。1 つのインスタンスが 2 時間稼働 = 2 インスタンス時間。',
+ 'usage.metric.gbDays.tip':
+ 'ストレージ容量を時間で積分した値(GB × 日):10 GB を 5 日間保持 = 50 GB·日。(= GB·時間 ÷ 24)',
+ 'usage.metric.gbHours.tip':
+ 'ストレージ容量を時間で積分した値(GB × 時間):10 GB を 5 時間保持 = 50 GB·時間。',
+
+ // --- Resource usage: common table / labels ---
+ 'usage.common.noData': 'データがありません',
+ 'usage.common.unknown': '不明',
+ 'usage.table.date': '日付',
+ 'usage.table.name': '名前',
+ 'usage.table.user': 'ユーザー',
+ 'usage.table.users': 'ユーザー',
+ 'usage.table.type': 'タイプ',
+ 'usage.table.instance': 'インスタンス',
+ 'usage.table.instanceType': 'インスタンスタイプ',
+ 'usage.table.instanceTypes': 'インスタンスタイプ',
+ 'usage.table.instances': 'インスタンス',
+ 'usage.table.capacity': '容量',
+ 'usage.export.tableNamed': 'テーブルデータをエクスポート — {name}',
+
+ // --- Summary tab ---
+ 'usage.summary.compute': 'コンピュート',
+ 'usage.summary.tokensOverTime': 'トークン数の推移',
+ 'usage.summary.gpuHoursOverTime': 'GPU 時間の推移',
+ 'usage.summary.gbDaysOverTime': 'GB·日の推移',
+
+ // --- GPU Instances / Storage filters ---
+ 'usage.filter.instance': 'インスタンスで絞り込み',
+ 'usage.filter.storage': 'ストレージで絞り込み',
+
+ // --- Resource events ---
+ 'usage.events.resourceType': 'リソースタイプ',
+ 'usage.events.eventType': 'イベントタイプ',
+ 'usage.events.col.time': '時刻',
+ 'usage.events.col.resource': 'リソース',
+ 'usage.events.col.event': 'イベント',
+ 'usage.events.col.message': 'メッセージ',
+ 'usage.events.resource.gpuInstance': 'GPU インスタンス',
+ 'usage.events.resource.cpuInstance': 'CPU インスタンス',
+ 'usage.events.type.created': '作成済み',
+ 'usage.events.type.deleted': '削除済み',
+ 'usage.events.type.started': '開始',
+ 'usage.events.type.stopped': '停止',
+ 'usage.events.type.updated': '更新済み',
+ 'usage.events.type.attached': 'アタッチ済み',
+ 'usage.events.type.detached': 'デタッチ済み'
};
diff --git a/src/locales/ru-RU/usage.ts b/src/locales/ru-RU/usage.ts
index 1dd8c058..2d27e472 100644
--- a/src/locales/ru-RU/usage.ts
+++ b/src/locales/ru-RU/usage.ts
@@ -38,5 +38,69 @@ export default {
'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
- 'usage.table.inputTokensCached': 'Input Tokens Cached'
+ 'usage.table.inputTokensCached': 'Input Tokens Cached',
+
+ // --- Resource usage: shared metrics & units ---
+ 'usage.metric.tokens': 'Токены',
+ 'usage.metric.input': 'Вход',
+ 'usage.metric.output': 'Выход',
+ 'usage.metric.gpuHours': 'GPU-часы',
+ 'usage.metric.instanceHours': 'Часы инстансов',
+ 'usage.metric.gbDays': 'ГБ-дни',
+ 'usage.metric.gbHours': 'ГБ-часы',
+ 'usage.metric.activeUsers': 'Активные пользователи',
+ 'usage.metric.activeInstances': 'Активные инстансы',
+ 'usage.metric.activeStorage': 'Активное хранилище',
+ 'usage.metric.activeVolumes': 'Активные тома',
+ 'usage.metric.storageTypes': 'Типы хранилищ',
+ 'usage.metric.gpuHours.tip':
+ 'Время работы инстанса, взвешенное по количеству GPU: инстанс с N GPU, работающий H часов, считается как N × H GPU-часов. Равно часам инстансов, когда каждый инстанс использует один GPU.',
+ 'usage.metric.instanceHours.tip':
+ 'Суммарное время работы всех инстансов независимо от количества используемых GPU. Один инстанс, работающий 2 часа = 2 часа инстанса.',
+ 'usage.metric.gbDays.tip':
+ 'Ёмкость хранилища, проинтегрированная по времени, в ГБ × дни: 10 ГБ в течение 5 дней = 50 ГБ-дней. (= ГБ-часы ÷ 24)',
+ 'usage.metric.gbHours.tip':
+ 'Ёмкость хранилища, проинтегрированная по времени, в ГБ × часы: 10 ГБ в течение 5 часов = 50 ГБ-часов.',
+
+ // --- Resource usage: common table / labels ---
+ 'usage.common.noData': 'Нет данных',
+ 'usage.common.unknown': 'неизвестно',
+ 'usage.table.date': 'Дата',
+ 'usage.table.name': 'Имя',
+ 'usage.table.user': 'Пользователь',
+ 'usage.table.users': 'Пользователи',
+ 'usage.table.type': 'Тип',
+ 'usage.table.instance': 'Инстанс',
+ 'usage.table.instanceType': 'Тип инстанса',
+ 'usage.table.instanceTypes': 'Типы инстансов',
+ 'usage.table.instances': 'Инстансы',
+ 'usage.table.capacity': 'Ёмкость',
+ 'usage.export.tableNamed': 'Экспорт данных таблицы — {name}',
+
+ // --- Summary tab ---
+ 'usage.summary.compute': 'Вычисления',
+ 'usage.summary.tokensOverTime': 'Токены по времени',
+ 'usage.summary.gpuHoursOverTime': 'GPU-часы по времени',
+ 'usage.summary.gbDaysOverTime': 'ГБ-дни по времени',
+
+ // --- GPU Instances / Storage filters ---
+ 'usage.filter.instance': 'Фильтр по инстансу',
+ 'usage.filter.storage': 'Фильтр по хранилищу',
+
+ // --- Resource events ---
+ 'usage.events.resourceType': 'Тип ресурса',
+ 'usage.events.eventType': 'Тип события',
+ 'usage.events.col.time': 'Время',
+ 'usage.events.col.resource': 'Ресурс',
+ 'usage.events.col.event': 'Событие',
+ 'usage.events.col.message': 'Сообщение',
+ 'usage.events.resource.gpuInstance': 'GPU-инстанс',
+ 'usage.events.resource.cpuInstance': 'CPU-инстанс',
+ 'usage.events.type.created': 'Создан',
+ 'usage.events.type.deleted': 'Удалён',
+ 'usage.events.type.started': 'Запущен',
+ 'usage.events.type.stopped': 'Остановлен',
+ 'usage.events.type.updated': 'Обновлён',
+ 'usage.events.type.attached': 'Подключён',
+ 'usage.events.type.detached': 'Отключён'
};
diff --git a/src/locales/tr-TR/usage.ts b/src/locales/tr-TR/usage.ts
index f9f94481..ecbc6e74 100644
--- a/src/locales/tr-TR/usage.ts
+++ b/src/locales/tr-TR/usage.ts
@@ -38,5 +38,69 @@ export default {
'usage.chart.cached': 'Cached',
'usage.chart.uncached': 'Uncached',
'usage.chart.inputTokensCached': 'Input Tokens (Cached/Uncached)',
- 'usage.table.inputTokensCached': 'Input Tokens Cached'
+ 'usage.table.inputTokensCached': 'Input Tokens Cached',
+
+ // --- Resource usage: shared metrics & units ---
+ 'usage.metric.tokens': 'Token',
+ 'usage.metric.input': 'Girdi',
+ 'usage.metric.output': 'Çıktı',
+ 'usage.metric.gpuHours': 'GPU Saati',
+ 'usage.metric.instanceHours': 'Örnek Saati',
+ 'usage.metric.gbDays': 'GB-Gün',
+ 'usage.metric.gbHours': 'GB-Saat',
+ 'usage.metric.activeUsers': 'Aktif Kullanıcılar',
+ 'usage.metric.activeInstances': 'Aktif Örnekler',
+ 'usage.metric.activeStorage': 'Aktif Depolama',
+ 'usage.metric.activeVolumes': 'Aktif Birimler',
+ 'usage.metric.storageTypes': 'Depolama Türleri',
+ 'usage.metric.gpuHours.tip':
+ 'Örnek çalışma süresinin GPU sayısına göre ağırlıklandırılmış hali: N GPU kullanan bir örnek H saat çalıştığında N × H GPU-saat olarak sayılır. Her örnek tek bir GPU kullandığında Örnek Saati ile eşittir.',
+ 'usage.metric.instanceHours.tip':
+ 'Her örneğin kullandığı GPU sayısından bağımsız olarak tüm örneklerin toplam çalışma süresi. 2 saat çalışan bir örnek = 2 örnek-saat.',
+ 'usage.metric.gbDays.tip':
+ 'Depolama kapasitesinin zamana göre integrali, GB × gün cinsinden: 5 gün boyunca tutulan 10 GB = 50 GB-gün. (= GB-Saat ÷ 24)',
+ 'usage.metric.gbHours.tip':
+ 'Depolama kapasitesinin zamana göre integrali, GB × saat cinsinden: 5 saat boyunca tutulan 10 GB = 50 GB-saat.',
+
+ // --- Resource usage: common table / labels ---
+ 'usage.common.noData': 'Veri yok',
+ 'usage.common.unknown': 'bilinmiyor',
+ 'usage.table.date': 'Tarih',
+ 'usage.table.name': 'Ad',
+ 'usage.table.user': 'Kullanıcı',
+ 'usage.table.users': 'Kullanıcılar',
+ 'usage.table.type': 'Tür',
+ 'usage.table.instance': 'Örnek',
+ 'usage.table.instanceType': 'Örnek Türü',
+ 'usage.table.instanceTypes': 'Örnek Türleri',
+ 'usage.table.instances': 'Örnekler',
+ 'usage.table.capacity': 'Kapasite',
+ 'usage.export.tableNamed': 'Tablo Verilerini Dışa Aktar — {name}',
+
+ // --- Summary tab ---
+ 'usage.summary.compute': 'Hesaplama',
+ 'usage.summary.tokensOverTime': 'Zaman içinde Token',
+ 'usage.summary.gpuHoursOverTime': 'Zaman içinde GPU Saati',
+ 'usage.summary.gbDaysOverTime': 'Zaman içinde GB-Gün',
+
+ // --- GPU Instances / Storage filters ---
+ 'usage.filter.instance': 'Örneğe göre filtrele',
+ 'usage.filter.storage': 'Depolamaya göre filtrele',
+
+ // --- Resource events ---
+ 'usage.events.resourceType': 'Kaynak türü',
+ 'usage.events.eventType': 'Olay türü',
+ 'usage.events.col.time': 'Zaman',
+ 'usage.events.col.resource': 'Kaynak',
+ 'usage.events.col.event': 'Olay',
+ 'usage.events.col.message': 'Mesaj',
+ 'usage.events.resource.gpuInstance': 'GPU Örneği',
+ 'usage.events.resource.cpuInstance': 'CPU Örneği',
+ 'usage.events.type.created': 'Oluşturuldu',
+ 'usage.events.type.deleted': 'Silindi',
+ 'usage.events.type.started': 'Başlatıldı',
+ 'usage.events.type.stopped': 'Durduruldu',
+ 'usage.events.type.updated': 'Güncellendi',
+ 'usage.events.type.attached': 'Eklendi',
+ 'usage.events.type.detached': 'Ayrıldı'
};
diff --git a/src/locales/zh-CN/usage.ts b/src/locales/zh-CN/usage.ts
index 8ed8f2f3..7c588eb9 100644
--- a/src/locales/zh-CN/usage.ts
+++ b/src/locales/zh-CN/usage.ts
@@ -38,5 +38,69 @@ export default {
'usage.chart.cached': '缓存',
'usage.chart.uncached': '非缓存',
'usage.chart.inputTokensCached': '输入 Token 数(缓存/非缓存)',
- 'usage.table.inputTokensCached': '输入 Token 缓存数'
+ 'usage.table.inputTokensCached': '输入 Token 缓存数',
+
+ // --- Resource usage: shared metrics & units ---
+ 'usage.metric.tokens': 'Token 数',
+ 'usage.metric.input': '输入',
+ 'usage.metric.output': '输出',
+ 'usage.metric.gpuHours': 'GPU 小时',
+ 'usage.metric.instanceHours': '实例小时',
+ 'usage.metric.gbDays': 'GB·天',
+ 'usage.metric.gbHours': 'GB·小时',
+ 'usage.metric.activeUsers': '活跃用户',
+ 'usage.metric.activeInstances': '活跃实例',
+ 'usage.metric.activeStorage': '活跃存储',
+ 'usage.metric.activeVolumes': '活跃存储卷',
+ 'usage.metric.storageTypes': '存储类型',
+ 'usage.metric.gpuHours.tip':
+ '实例运行时长按 GPU 数量加权:一个使用 N 个 GPU 的实例运行 H 小时记为 N × H GPU 小时。当每个实例仅使用单个 GPU 时,等于实例小时。',
+ 'usage.metric.instanceHours.tip':
+ '所有实例运行时长的总和,与每个实例使用的 GPU 数量无关。一个实例运行 2 小时 = 2 实例小时。',
+ 'usage.metric.gbDays.tip':
+ '存储容量随时间的积分,单位为 GB × 天:10 GB 保留 5 天 = 50 GB·天。(= GB·小时 ÷ 24)',
+ 'usage.metric.gbHours.tip':
+ '存储容量随时间的积分,单位为 GB × 小时:10 GB 保留 5 小时 = 50 GB·小时。',
+
+ // --- Resource usage: common table / labels ---
+ 'usage.common.noData': '暂无数据',
+ 'usage.common.unknown': '未知',
+ 'usage.table.date': '日期',
+ 'usage.table.name': '名称',
+ 'usage.table.user': '用户',
+ 'usage.table.users': '用户',
+ 'usage.table.type': '类型',
+ 'usage.table.instance': '实例',
+ 'usage.table.instanceType': '实例类型',
+ 'usage.table.instanceTypes': '实例类型',
+ 'usage.table.instances': '实例',
+ 'usage.table.capacity': '容量',
+ 'usage.export.tableNamed': '导出表格数据 — {name}',
+
+ // --- Summary tab ---
+ 'usage.summary.compute': '算力',
+ 'usage.summary.tokensOverTime': 'Token 数趋势',
+ 'usage.summary.gpuHoursOverTime': 'GPU 小时趋势',
+ 'usage.summary.gbDaysOverTime': 'GB·天趋势',
+
+ // --- GPU Instances / Storage filters ---
+ 'usage.filter.instance': '按实例查询',
+ 'usage.filter.storage': '按存储查询',
+
+ // --- Resource events ---
+ 'usage.events.resourceType': '资源类型',
+ 'usage.events.eventType': '事件类型',
+ 'usage.events.col.time': '时间',
+ 'usage.events.col.resource': '资源',
+ 'usage.events.col.event': '事件',
+ 'usage.events.col.message': '消息',
+ 'usage.events.resource.gpuInstance': 'GPU 实例',
+ 'usage.events.resource.cpuInstance': 'CPU 实例',
+ 'usage.events.type.created': '已创建',
+ 'usage.events.type.deleted': '已删除',
+ 'usage.events.type.started': '已启动',
+ 'usage.events.type.stopped': '已停止',
+ 'usage.events.type.updated': '已更新',
+ 'usage.events.type.attached': '已挂载',
+ 'usage.events.type.detached': '已卸载'
};
diff --git a/src/pages/usage/components/gpu-instances-tab.tsx b/src/pages/usage/components/gpu-instances-tab.tsx
index 3d071507..daa1a0ff 100644
--- a/src/pages/usage/components/gpu-instances-tab.tsx
+++ b/src/pages/usage/components/gpu-instances-tab.tsx
@@ -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: (
),
color: coolColors[0]
@@ -184,24 +202,26 @@ const GpuInstancesTab: React.FC = () => {
) as string,
value: (
),
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 —
// " x " 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}
diff --git a/src/pages/usage/components/resource-events.tsx b/src/pages/usage/components/resource-events.tsx
index ce6c62fd..ac2d0726 100644
--- a/src/pages/usage/components/resource-events.tsx
+++ b/src/pages/usage/components/resource-events.tsx
@@ -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 = {
- 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 = {
created: 'green',
deleted: 'red',
@@ -52,14 +30,6 @@ const EVENT_COLOR: Record = {
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 = 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 = 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 = 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) => {
diff --git a/src/pages/usage/components/storage-tab.tsx b/src/pages/usage/components/storage-tab.tsx
index fffa0718..365d2147 100644
--- a/src/pages/usage/components/storage-tab.tsx
+++ b/src/pages/usage/components/storage-tab.tsx
@@ -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: (
),
color: coolColors[0]
@@ -172,24 +188,24 @@ const StorageTab: React.FC = () => {
) as string,
value: (
),
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}
diff --git a/src/pages/usage/components/summary-tab.tsx b/src/pages/usage/components/summary-tab.tsx
index ff7b6229..3fe3f6e3 100644
--- a/src/pages/usage/components/summary-tab.tsx
+++ b/src/pages/usage/components/summary-tab.tsx
@@ -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 = (
);
@@ -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 = () => {
-
-
-
+
+
+
>
}
@@ -411,25 +423,28 @@ const SummaryTab: React.FC = () => {
-
+
>
}
@@ -438,23 +453,29 @@ const SummaryTab: React.FC = () => {
-
+
+
-
>
}
/>