Localize the managed-service resources panel.

Add components.serviceResources and move the resources/scaling panel
(metrics, storage usage/expansion, CPU/memory adjust, dump restore,
upgrade toasts) onto it; route through the locale-aware router.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-11 21:02:57 +03:30
parent 96d24b50a1
commit deef498f65
4 changed files with 136 additions and 50 deletions
@@ -1,10 +1,11 @@
'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api';
import { toast } from 'react-toastify';
import { useT } from '@/i18n/I18nProvider';
import { useLocalizedRouter } from '@/i18n/navigation';
import type { Application, Invoice, OptionalServiceResourcesMap, ResourceUsage } from '@/types';
import {
ResourceUpgradeConfirmModal,
@@ -88,7 +89,9 @@ export function ManagedServiceResourcesPanel({
isStopped: boolean;
needsRenewal?: boolean;
}) {
const router = useRouter();
const t = useT();
const sr = t.components.serviceResources;
const router = useLocalizedRouter();
const queryClient = useQueryClient();
const dbFileInputRef = useRef<HTMLInputElement>(null);
const [showResources, setShowResources] = useState(false);
@@ -164,11 +167,11 @@ export function ManagedServiceResourcesPanel({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
queryClient.invalidateQueries({ queryKey: ['resources', serviceId] });
toast.success('Resources updated');
toast.success(sr.resourcesUpdated);
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to update resources');
toast.error(msg || sr.updateFailed);
},
});
@@ -185,14 +188,14 @@ export function ManagedServiceResourcesPanel({
setPendingUpgradePayload(null);
const paidAmount = res.data.paidAmount || 0;
if (paidAmount > 0) {
toast.success(`Resources upgraded! Paid ${paidAmount.toLocaleString()} Toman`);
toast.success(sr.upgradedPaid.replace('{amount}', paidAmount.toLocaleString('en-US')));
} else {
toast.success('Resources updated successfully');
toast.success(sr.updatedSuccess);
}
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to upgrade resources');
toast.error(msg || sr.upgradeFailed);
},
});
@@ -205,7 +208,7 @@ export function ManagedServiceResourcesPanel({
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to calculate upgrade cost');
toast.error(msg || sr.calcFailed);
},
});
@@ -213,7 +216,7 @@ export function ManagedServiceResourcesPanel({
mutationFn: (data: UpgradePayload) =>
api.post<Invoice>(`/billing/applications/${serviceId}/upgrade/invoice`, data).then((r) => r.data),
onSuccess: (invoice) => {
toast.success('Invoice created. Choose how you want to pay.');
toast.success(sr.invoiceCreated);
queryClient.invalidateQueries({ queryKey: ['invoices'] });
setShowUpgradeConfirm(false);
setUpgradeCostData(null);
@@ -222,7 +225,7 @@ export function ManagedServiceResourcesPanel({
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to create upgrade invoice');
toast.error(msg || sr.invoiceFailed);
},
});
@@ -237,12 +240,12 @@ export function ManagedServiceResourcesPanel({
onSuccess: (res) => {
const data = res.data;
setDbRestoreLogs(data.logs || null);
if (data.success) toast.success('Database restored successfully');
else toast.error(data.message || 'Database restore failed');
if (data.success) toast.success(sr.restoredSuccess);
else toast.error(data.message || sr.restoreFailed);
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || 'Failed to upload database dump');
toast.error(msg || sr.uploadFailed);
setDbRestoreLogs(null);
},
});
@@ -250,17 +253,17 @@ export function ManagedServiceResourcesPanel({
const handleDbFileUpload = useCallback(
(file: File) => {
if (!file.name.endsWith('.sql') && !file.name.endsWith('.gz') && !file.name.endsWith('.dump')) {
toast.error('Please upload a .sql, .dump, or .gz file');
toast.error(sr.invalidFile);
return;
}
if (file.size > 500 * 1024 * 1024) {
toast.error('File size must be less than 500MB');
toast.error(sr.fileTooLarge);
return;
}
setDbRestoreLogs(null);
dbUploadMutation.mutate(file);
},
[dbUploadMutation],
[dbUploadMutation, sr],
);
const currentDbGi =
@@ -297,7 +300,7 @@ export function ManagedServiceResourcesPanel({
const applyResources = () => {
if (needsRenewal) {
toast.warn('Renew the service before changing resources');
toast.warn(sr.renewFirst);
return;
}
@@ -374,16 +377,16 @@ export function ManagedServiceResourcesPanel({
<div className="card">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<BarChart3 className="w-5 h-5" /> Resources &amp; Scaling
<BarChart3 className="w-5 h-5" /> {sr.title}
</h2>
<button type="button" onClick={() => setShowResources(!showResources)} className="btn-secondary text-sm">
{showResources ? (
<>
<ChevronDown className="w-4 h-4 inline" /> Hide
<ChevronDown className="w-4 h-4 inline" /> {sr.hide}
</>
) : (
<>
<BarChart3 className="w-4 h-4 inline" /> Monitor
<BarChart3 className="w-4 h-4 inline" /> {sr.monitor}
</>
)}
</button>
@@ -392,28 +395,28 @@ export function ManagedServiceResourcesPanel({
{showResources && (
<div className="space-y-6">
{!isDeployed ? (
<p className="text-sm text-gray-500 text-center py-6">Deploy the service to view metrics and adjust resources.</p>
<p className="text-sm text-gray-500 text-center py-6">{sr.deployToView}</p>
) : resourcesLoading ? (
<div className="text-center py-6 text-gray-400 text-sm">Loading metrics</div>
<div className="text-center py-6 text-gray-400 text-sm">{sr.loadingMetrics}</div>
) : metricsWorkload ? (
<div className="border border-gray-200 rounded-xl p-4 space-y-4 bg-slate-50/50">
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-gray-800">{metricsWorkload.title}</h3>
<h3 className="text-sm font-semibold text-gray-800">{(sr.workloads as Record<string, string>)[workloadKey(app)] ?? metricsWorkload.title}</h3>
<span className="text-[11px] text-gray-400 font-mono">{metricsWorkload.deploymentName}</span>
</div>
<div className="grid grid-cols-3 gap-2 text-center">
<div className="bg-blue-50 rounded-lg p-3">
<p className="text-[10px] text-blue-600 font-medium">Replicas</p>
<p className="text-[10px] text-blue-600 font-medium">{sr.replicas}</p>
<p className="text-lg font-bold text-blue-800">
{metricsWorkload.configured.readyReplicas}/{metricsWorkload.configured.replicas}
</p>
</div>
<div className="bg-green-50 rounded-lg p-3">
<p className="text-[10px] text-green-600 font-medium">Pods</p>
<p className="text-[10px] text-green-600 font-medium">{sr.pods}</p>
<p className="text-lg font-bold text-green-800">{metricsWorkload.pods.length}</p>
</div>
<div className="bg-purple-50 rounded-lg p-3">
<p className="text-[10px] text-purple-600 font-medium">Metrics</p>
<p className="text-[10px] text-purple-600 font-medium">{sr.metrics}</p>
<p className="text-lg font-bold text-purple-800 flex justify-center">
{metricsWorkload.metrics.length > 0 ? (
<CheckCircle className="w-5 h-5 text-purple-700" />
@@ -469,21 +472,21 @@ export function ManagedServiceResourcesPanel({
</div>
) : (
<p className="text-sm text-gray-400 text-center py-4">
{isStopped ? 'Service is stopped.' : 'No resource metrics yet.'}
{isStopped ? sr.serviceStopped : sr.noMetrics}
</p>
)}
<div className="border-t pt-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-1">
<Database className="w-4 h-4" /> Storage
<Database className="w-4 h-4" /> {sr.storage}
</h3>
{storageUsageLoading ? (
<p className="text-sm text-gray-400">Loading storage</p>
<p className="text-sm text-gray-400">{sr.loadingStorage}</p>
) : storageSlice ? (
<div className="bg-gray-50 rounded-xl p-4 space-y-3">
<div className="flex justify-between text-xs text-gray-600">
<span>Used {storageSlice.usedGi.toFixed(2)} GiB</span>
<span>Allocated {storageSlice.allocatedGi.toFixed(1)} GiB</span>
<span>{sr.used} {storageSlice.usedGi.toFixed(2)} GiB</span>
<span>{sr.allocated} {storageSlice.allocatedGi.toFixed(1)} GiB</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div
@@ -527,29 +530,27 @@ export function ManagedServiceResourcesPanel({
</div>
<span className="text-sm text-gray-600">GB</span>
<p className="text-xs text-gray-400 w-full">
Only expansion is allowed. Use Apply changes below.
{app.billingCycle
? ' Additional storage is charged for the remaining billing period.'
: ''}
{sr.expansionNote}
{app.billingCycle ? sr.storageChargedNote : ''}
</p>
</div>
)}
</div>
) : (
<p className="text-sm text-gray-400">Storage metrics unavailable.</p>
<p className="text-sm text-gray-400">{sr.storageUnavailable}</p>
)}
</div>
<div className="border-t pt-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-1">
<Settings className="w-4 h-4" /> Adjust CPU / memory
<Settings className="w-4 h-4" /> {sr.adjustCpuMem}
</h3>
{app.productType === 'managed_database' && (
<DatabaseWorkloadResources values={dbResources} onChange={(p) => setDbResources((v) => ({ ...v, ...p }))} />
)}
{app.productType === 'managed_redis' && (
<OptionalServiceResourceFields
title="Redis resources"
title={sr.redisResources}
accentClass="text-red-500"
borderClass="border-red-200"
bgClass="bg-red-50/30"
@@ -559,7 +560,7 @@ export function ManagedServiceResourcesPanel({
)}
{app.productType === 'managed_rabbitmq' && (
<OptionalServiceResourceFields
title="RabbitMQ resources"
title={sr.rabbitmqResources}
accentClass="text-orange-500"
borderClass="border-orange-200"
bgClass="bg-orange-50/30"
@@ -568,10 +569,7 @@ export function ManagedServiceResourcesPanel({
/>
)}
{app.billingCycle && (
<p className="text-xs text-gray-500 mt-3">
Upgrades that increase cost are charged for the remaining billing period (wallet or invoice), same as
applications.
</p>
<p className="text-xs text-gray-500 mt-3">{sr.upgradeChargeNote}</p>
)}
<button
type="button"
@@ -581,11 +579,11 @@ export function ManagedServiceResourcesPanel({
>
{resourcesPending ? (
<>
<Clock className="w-3 h-3 inline animate-spin" /> Applying
<Clock className="w-3 h-3 inline animate-spin" /> {sr.applying}
</>
) : (
<>
<RefreshCw className="w-3 h-3 inline" /> Apply changes
<RefreshCw className="w-3 h-3 inline" /> {sr.applyChanges}
</>
)}
</button>
@@ -594,7 +592,7 @@ export function ManagedServiceResourcesPanel({
{isDatabase && (
<div className="border-t pt-4">
<h3 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-1">
<Scale className="w-4 h-4" /> Restore database dump
<Scale className="w-4 h-4" /> {sr.restoreDump}
</h3>
<div
onDrop={(e) => {
@@ -625,12 +623,12 @@ export function ManagedServiceResourcesPanel({
}}
/>
{dbUploadMutation.isPending ? (
<p className="text-sm text-gray-700">Restoring database</p>
<p className="text-sm text-gray-700">{sr.restoring}</p>
) : (
<>
<Database className="w-8 h-8 mx-auto text-gray-400 mb-2" />
<p className="text-sm font-medium text-gray-700">Upload SQL dump to restore</p>
<p className="text-xs text-gray-500 mt-1">.sql, .gz, or .dump max 500MB</p>
<p className="text-sm font-medium text-gray-700">{sr.uploadToRestore}</p>
<p className="text-xs text-gray-500 mt-1">{sr.uploadFormats}</p>
</>
)}
</div>