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 = () => { - + + - } />