Localize the managed-service detail page.
Move the service detail view (header actions, renewal banner, config, deployment history, DB/Redis/RabbitMQ connection panels, renew modal) onto a dashboard.serviceDetail dictionary with localized product-type and deploy-status labels, locale-aware dates and RTL-aware layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Link } from '@/i18n/Link';
|
||||
import { useT, useLocale } from '@/i18n/I18nProvider';
|
||||
import { useLocalizedRouter } from '@/i18n/navigation';
|
||||
import type {
|
||||
Application,
|
||||
Deployment,
|
||||
OptionalServiceCredentials,
|
||||
OptionalServiceResourcesMap,
|
||||
} from '@/types';
|
||||
import { managedServiceTypeLabel } from '@/lib/optional-service-defaults';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Database,
|
||||
@@ -61,8 +62,13 @@ function dbPort(databaseType: string): string {
|
||||
type AppWithOptional = Application & { optionalServiceResources?: OptionalServiceResourcesMap };
|
||||
|
||||
export default function ManagedServiceDetailPage() {
|
||||
const t = useT();
|
||||
const sd = t.dashboard.serviceDetail;
|
||||
const locale = useLocale();
|
||||
const typeLabel = (pt?: string) => (pt && (sd.productTypes as Record<string, string>)[pt]) || pt || '';
|
||||
const statusLabel = (s: string) => (t.components.deployStatus as Record<string, string>)[s] ?? s;
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const router = useLocalizedRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { notifyDeployStarted } = useDeployProgressActions();
|
||||
const confirm = useConfirm();
|
||||
@@ -121,14 +127,14 @@ export default function ManagedServiceDetailPage() {
|
||||
notifyDeployStarted(serviceId, app?.name);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Provisioning started');
|
||||
toast.success(sd.provisioningStarted);
|
||||
queryClient.invalidateQueries({ queryKey: ['deployments', serviceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
},
|
||||
onError: () => {
|
||||
useDeployProgressStore.getState().stopTracking(serviceId);
|
||||
toast.error('Failed to start provisioning');
|
||||
toast.error(sd.provisioningFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -138,7 +144,7 @@ export default function ManagedServiceDetailPage() {
|
||||
notifyDeployStarted(serviceId, app?.name);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Re-provisioning started');
|
||||
toast.success(sd.reprovisioningStarted);
|
||||
queryClient.invalidateQueries({ queryKey: ['deployments', serviceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
@@ -146,29 +152,29 @@ export default function ManagedServiceDetailPage() {
|
||||
onError: (err: unknown) => {
|
||||
useDeployProgressStore.getState().stopTracking(serviceId);
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || 'Failed to re-provision service');
|
||||
toast.error(msg || sd.reprovisionFailed);
|
||||
},
|
||||
});
|
||||
|
||||
const restartMutation = useMutation({
|
||||
mutationFn: () => api.post(`/deployments/applications/${serviceId}/restart`),
|
||||
onSuccess: () => toast.success('Service restarted'),
|
||||
onSuccess: () => toast.success(sd.serviceRestarted),
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || 'Failed to restart service');
|
||||
toast.error(msg || sd.restartFailed);
|
||||
},
|
||||
});
|
||||
|
||||
const renewMutation = useMutation({
|
||||
mutationFn: (cycle: string) => api.post(`/billing/applications/${serviceId}/renew`, { cycle }),
|
||||
onSuccess: () => {
|
||||
toast.success('Service renewed');
|
||||
toast.success(sd.serviceRenewed);
|
||||
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
|
||||
setShowRenewalModal(false);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || 'Renewal failed');
|
||||
toast.error(msg || sd.renewalFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -177,13 +183,13 @@ export default function ManagedServiceDetailPage() {
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
|
||||
if (data?.resourceCredit) {
|
||||
toast.success('Service deleted. Prepaid resources are on your dashboard.');
|
||||
toast.success(sd.deletedWithCredit);
|
||||
} else {
|
||||
toast.success('Service deleted');
|
||||
toast.success(sd.deleted);
|
||||
}
|
||||
router.push('/dashboard/services');
|
||||
},
|
||||
onError: () => toast.error('Failed to delete'),
|
||||
onError: () => toast.error(sd.deleteFailed),
|
||||
});
|
||||
|
||||
const copyToClipboard = useCallback((text: string, field: string) => {
|
||||
@@ -256,12 +262,12 @@ export default function ManagedServiceDetailPage() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/dashboard/services" className="btn-ghost">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<ArrowLeft className="w-4 h-4 rtl:rotate-180" />
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="page-title">{app.name}</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{managedServiceTypeLabel(app.productType)}
|
||||
{typeLabel(app.productType)}
|
||||
{app.dbVersion ? ` · v${app.dbVersion}` : ''}
|
||||
{app.redisVersion && app.productType === 'managed_redis' ? ` · v${app.redisVersion}` : ''}
|
||||
{app.rabbitmqVersion && app.productType === 'managed_rabbitmq'
|
||||
@@ -272,7 +278,7 @@ export default function ManagedServiceDetailPage() {
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className={`badge ${latestStatus === 'running' ? 'badge-green' : 'badge-yellow'}`}>
|
||||
{latestStatus}
|
||||
{statusLabel(latestStatus)}
|
||||
</span>
|
||||
{!isDeployed && (
|
||||
<button
|
||||
@@ -281,7 +287,7 @@ export default function ManagedServiceDetailPage() {
|
||||
onClick={() => deployMutation.mutate()}
|
||||
disabled={deployMutation.isPending || needsRenewal}
|
||||
>
|
||||
<Rocket className="w-4 h-4 inline mr-1" /> Deploy
|
||||
<Rocket className="w-4 h-4 inline mr-1 rtl:mr-0 rtl:ml-1" /> {sd.deploy}
|
||||
</button>
|
||||
)}
|
||||
{isDeployed && !needsRenewal && !isInProgress && (
|
||||
@@ -290,14 +296,14 @@ export default function ManagedServiceDetailPage() {
|
||||
className="btn-primary text-sm"
|
||||
onClick={() => redeployMutation.mutate()}
|
||||
disabled={redeployMutation.isPending}
|
||||
title="Re-run Helm install for this service"
|
||||
title={sd.redeployTitle}
|
||||
>
|
||||
{redeployMutation.isPending ? (
|
||||
<Clock className="w-4 h-4 inline animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4 inline mr-1" />
|
||||
)}
|
||||
Redeploy
|
||||
{sd.redeploy}
|
||||
</button>
|
||||
)}
|
||||
{isDeployed && isRunning && (
|
||||
@@ -312,7 +318,7 @@ export default function ManagedServiceDetailPage() {
|
||||
) : (
|
||||
<RotateCw className="w-4 h-4 inline mr-1" />
|
||||
)}
|
||||
Restart
|
||||
{sd.restart}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -321,15 +327,15 @@ export default function ManagedServiceDetailPage() {
|
||||
disabled={pageLocked}
|
||||
onClick={async () => {
|
||||
const ok = await confirm({
|
||||
title: 'Delete service?',
|
||||
message: `Delete "${app.name}" permanently?`,
|
||||
confirmText: 'Delete',
|
||||
title: sd.deleteServiceTitle,
|
||||
message: sd.deleteServiceMessage.replace('{name}', app.name),
|
||||
confirmText: t.common.delete,
|
||||
variant: 'danger',
|
||||
});
|
||||
if (ok) deleteApplication(serviceId);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
{t.common.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,8 +345,8 @@ export default function ManagedServiceDetailPage() {
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<AlertTriangle className="w-6 h-6 text-amber-600 shrink-0" />
|
||||
<div>
|
||||
<h3 className="font-semibold text-amber-800">Payment required</h3>
|
||||
<p className="text-sm text-amber-600">Renew to restore this service.</p>
|
||||
<h3 className="font-semibold text-amber-800">{sd.paymentRequired}</h3>
|
||||
<p className="text-sm text-amber-600">{sd.renewToRestore}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -348,7 +354,7 @@ export default function ManagedServiceDetailPage() {
|
||||
onClick={() => setShowRenewalModal(true)}
|
||||
className="btn-primary bg-amber-600 hover:bg-amber-700"
|
||||
>
|
||||
<CreditCard className="w-4 h-4 inline mr-1" /> Renew
|
||||
<CreditCard className="w-4 h-4 inline mr-1 rtl:mr-0 rtl:ml-1" /> {sd.renew}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -356,23 +362,23 @@ export default function ManagedServiceDetailPage() {
|
||||
{app.planExpiresAt && (
|
||||
<div className="card flex items-center gap-3 text-sm text-gray-600">
|
||||
<Clock className="w-4 h-4" />
|
||||
Plan expires: {new Date(app.planExpiresAt).toLocaleString()}
|
||||
{app.billingCycle && ` (${app.billingCycle})`}
|
||||
{sd.planExpires.replace('{date}', new Date(app.planExpiresAt).toLocaleString(locale))}
|
||||
{app.billingCycle && ` (${(sd.cycles as Record<string,string>)[app.billingCycle] ?? app.billingCycle})`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="card">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Configuration</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">{sd.configuration}</h2>
|
||||
<dl className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Service type</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{managedServiceTypeLabel(app.productType)}</dd>
|
||||
<dt className="text-sm text-gray-500">{sd.serviceType}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{typeLabel(app.productType)}</dd>
|
||||
</div>
|
||||
{app.productType === 'managed_database' && (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Database engine</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.databaseEngine}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">
|
||||
{app.databaseType}
|
||||
{app.databaseType !== 'none' && app.dbVersion ? ` v${app.dbVersion}` : ''}
|
||||
@@ -380,7 +386,7 @@ export default function ManagedServiceDetailPage() {
|
||||
</div>
|
||||
{app.databaseType !== 'none' && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Storage</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.storage}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{app.dbStorageSize || '1Gi'}</dd>
|
||||
</div>
|
||||
)}
|
||||
@@ -388,33 +394,33 @@ export default function ManagedServiceDetailPage() {
|
||||
)}
|
||||
{app.productType === 'managed_redis' && app.redisVersion && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Redis version</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.redisVersion}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">v{app.redisVersion}</dd>
|
||||
</div>
|
||||
)}
|
||||
{app.productType === 'managed_rabbitmq' && app.rabbitmqVersion && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">RabbitMQ version</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.rabbitmqVersion}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">v{app.rabbitmqVersion}</dd>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">CPU</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.cpu}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{cpuDisplay}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Memory</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.memory}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{memDisplay}</dd>
|
||||
</div>
|
||||
{app.billingCycle && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Billing</dt>
|
||||
<dd className="text-sm font-medium text-gray-900 capitalize">{app.billingCycle}</dd>
|
||||
<dt className="text-sm text-gray-500">{sd.billing}</dt>
|
||||
<dd className="text-sm font-medium text-gray-900">{(sd.cycles as Record<string,string>)[app.billingCycle] ?? app.billingCycle}</dd>
|
||||
</div>
|
||||
)}
|
||||
{app.latestImageTag && (
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-sm text-gray-500">Deploy marker</dt>
|
||||
<dt className="text-sm text-gray-500">{sd.deployMarker}</dt>
|
||||
<dd className="text-xs font-mono text-gray-700 truncate max-w-[200px]" title={app.latestImageTag}>
|
||||
{app.latestImageTag}
|
||||
</dd>
|
||||
@@ -424,19 +430,19 @@ export default function ManagedServiceDetailPage() {
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Deployment History</h2>
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">{sd.deploymentHistory}</h2>
|
||||
{deployments.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<Package className="w-8 h-8 mx-auto text-gray-300 mb-2" />
|
||||
<p className="text-gray-500 text-sm">No deployments yet</p>
|
||||
<p className="text-gray-400 text-xs mt-1">Click Deploy to provision this service</p>
|
||||
<p className="text-gray-500 text-sm">{sd.noDeployments}</p>
|
||||
<p className="text-gray-400 text-xs mt-1">{sd.noDeploymentsHint}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-72 overflow-y-auto">
|
||||
{deployments.slice(0, 10).map((d) => (
|
||||
<div key={d.id} className="flex items-center justify-between p-3 bg-gray-50/80 rounded-xl">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{d.version || d.imageTag || 'Provision'}</p>
|
||||
<p className="text-sm font-medium text-gray-900 truncate">{d.version || d.imageTag || sd.provision}</p>
|
||||
<p className="text-xs text-gray-500">{new Date(d.createdAt).toLocaleString()}</p>
|
||||
{d.errorMessage && (
|
||||
<p className="text-xs text-red-500 mt-1 truncate" title={d.errorMessage}>
|
||||
@@ -444,8 +450,8 @@ export default function ManagedServiceDetailPage() {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<span className={`badge ${statusColors[d.status] || 'badge-gray'} ml-2 shrink-0`}>
|
||||
{d.status}
|
||||
<span className={`badge ${statusColors[d.status] || 'badge-gray'} ml-2 rtl:ml-0 rtl:mr-2 shrink-0`}>
|
||||
{statusLabel(d.status)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -457,16 +463,16 @@ export default function ManagedServiceDetailPage() {
|
||||
{app.productType === 'managed_database' && app.databaseType !== 'none' && (
|
||||
<div className="card">
|
||||
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<Database className="w-5 h-5 text-blue-500" /> Connection
|
||||
<Database className="w-5 h-5 text-blue-500" /> {sd.connection}
|
||||
<span className="badge badge-blue text-xs">{app.databaseType}</span>
|
||||
</h2>
|
||||
<div className="bg-gray-50 rounded-xl p-4 space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3">Internal cluster</h3>
|
||||
<h3 className="text-sm font-semibold text-gray-700 mb-3">{sd.internalCluster}</h3>
|
||||
{[
|
||||
{ label: 'Host', value: `${app.name}-db`, field: 'host' },
|
||||
{ label: 'Port', value: dbPort(app.databaseType), field: 'port' },
|
||||
{ label: 'Database', value: app.name.replace(/-/g, '_'), field: 'database' },
|
||||
{ label: 'Username', value: app.dbUsername || 'appuser', field: 'username' },
|
||||
{ label: sd.host, value: `${app.name}-db`, field: 'host' },
|
||||
{ label: sd.port, value: dbPort(app.databaseType), field: 'port' },
|
||||
{ label: sd.database, value: app.name.replace(/-/g, '_'), field: 'database' },
|
||||
{ label: sd.username, value: app.dbUsername || 'appuser', field: 'username' },
|
||||
].map(({ label, value, field }) => (
|
||||
<div key={field} className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">{label}</span>
|
||||
@@ -487,7 +493,7 @@ export default function ManagedServiceDetailPage() {
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-gray-500">Password</span>
|
||||
<span className="text-sm text-gray-500">{sd.password}</span>
|
||||
<div className="flex items-center gap-1 font-mono text-sm">
|
||||
{showDbPassword ? app.dbPassword || '—' : '••••••••'}
|
||||
<button
|
||||
@@ -511,7 +517,7 @@ export default function ManagedServiceDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 pt-2 border-t border-gray-200">
|
||||
Use external access below for internet-facing connections.
|
||||
{sd.useExternalNote}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -521,7 +527,7 @@ export default function ManagedServiceDetailPage() {
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2">
|
||||
<KeyRound className="w-5 h-5" /> Connection
|
||||
<KeyRound className="w-5 h-5" /> {sd.connection}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
@@ -529,23 +535,23 @@ export default function ManagedServiceDetailPage() {
|
||||
className="btn-secondary text-xs inline-flex items-center gap-1"
|
||||
>
|
||||
{showServiceSecrets ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
|
||||
{showServiceSecrets ? 'Hide secrets' : 'Show secrets'}
|
||||
{showServiceSecrets ? sd.hideSecrets : sd.showSecrets}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{app.productType === 'managed_redis' && (
|
||||
<div className="bg-gray-50 rounded-xl p-4 space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-700">Redis (internal)</h3>
|
||||
<h3 className="text-sm font-semibold text-gray-700">{sd.redisInternal}</h3>
|
||||
{[
|
||||
{ label: 'Host', value: serviceCredentials?.redis?.host || `${app.name}-redis`, field: 'redis-host' },
|
||||
{ label: 'Port', value: String(serviceCredentials?.redis?.port || 6379), field: 'redis-port' },
|
||||
{ label: sd.host, value: serviceCredentials?.redis?.host || `${app.name}-redis`, field: 'redis-host' },
|
||||
{ label: sd.port, value: String(serviceCredentials?.redis?.port || 6379), field: 'redis-port' },
|
||||
{
|
||||
label: 'Password',
|
||||
label: sd.password,
|
||||
value: serviceCredentials?.redis?.password || '',
|
||||
field: 'redis-password',
|
||||
secret: true,
|
||||
},
|
||||
{ label: 'URL', value: serviceCredentials?.redis?.url || '', field: 'redis-url', secret: true },
|
||||
{ label: sd.url, value: serviceCredentials?.redis?.url || '', field: 'redis-url', secret: true },
|
||||
].map(({ label, value, field, secret }) => (
|
||||
<div key={field} className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-gray-500">{label}</span>
|
||||
@@ -573,42 +579,42 @@ export default function ManagedServiceDetailPage() {
|
||||
|
||||
{app.productType === 'managed_rabbitmq' && (
|
||||
<div className="bg-gray-50 rounded-xl p-4 space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-700">RabbitMQ (internal)</h3>
|
||||
<h3 className="text-sm font-semibold text-gray-700">{sd.rabbitmqInternal}</h3>
|
||||
{[
|
||||
{
|
||||
label: 'Host',
|
||||
label: sd.host,
|
||||
value: serviceCredentials?.rabbitmq?.host || `${app.name}-rabbitmq`,
|
||||
field: 'rabbit-host',
|
||||
},
|
||||
{
|
||||
label: 'AMQP Port',
|
||||
label: sd.amqpPort,
|
||||
value: String(serviceCredentials?.rabbitmq?.amqpPort || 5672),
|
||||
field: 'rabbit-amqp-port',
|
||||
},
|
||||
{
|
||||
label: 'Management Port',
|
||||
label: sd.managementPort,
|
||||
value: String(serviceCredentials?.rabbitmq?.managementPort || 15672),
|
||||
field: 'rabbit-mgmt-port',
|
||||
},
|
||||
{
|
||||
label: 'Username',
|
||||
label: sd.username,
|
||||
value: serviceCredentials?.rabbitmq?.username || 'appuser',
|
||||
field: 'rabbit-user',
|
||||
},
|
||||
{
|
||||
label: 'Password',
|
||||
label: sd.password,
|
||||
value: serviceCredentials?.rabbitmq?.password || '',
|
||||
field: 'rabbit-password',
|
||||
secret: true,
|
||||
},
|
||||
{
|
||||
label: 'AMQP URL',
|
||||
label: sd.amqpUrl,
|
||||
value: serviceCredentials?.rabbitmq?.amqpUrl || '',
|
||||
field: 'rabbit-amqp-url',
|
||||
secret: true,
|
||||
},
|
||||
{
|
||||
label: 'Management URL',
|
||||
label: sd.managementUrl,
|
||||
value: serviceCredentials?.rabbitmq?.managementUrl || '',
|
||||
field: 'rabbit-mgmt-url',
|
||||
},
|
||||
@@ -638,7 +644,7 @@ export default function ManagedServiceDetailPage() {
|
||||
)}
|
||||
|
||||
{!isDeployed && (
|
||||
<p className="text-xs text-gray-400 mt-3">Deploy the service to load live credentials.</p>
|
||||
<p className="text-xs text-gray-400 mt-3">{sd.deployToLoadCreds}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -664,23 +670,23 @@ export default function ManagedServiceDetailPage() {
|
||||
isStopped={isStopped}
|
||||
emptyPodMessage={
|
||||
isRunning
|
||||
? 'Loading logs...'
|
||||
? sd.loadingLogs
|
||||
: isStopped
|
||||
? 'Service is stopped.'
|
||||
: 'Deploy or re-provision the service to see workload logs.'
|
||||
? sd.serviceStopped
|
||||
: sd.deployToSeeLogs
|
||||
}
|
||||
/>
|
||||
|
||||
{showRenewalModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl max-w-md w-full p-6">
|
||||
<h2 className="text-xl font-bold mb-2">Renew service</h2>
|
||||
<h2 className="text-xl font-bold mb-2">{sd.renewService}</h2>
|
||||
<p className="text-sm text-gray-500 mb-4">"{app.name}"</p>
|
||||
<div className="bg-gray-50 rounded-xl p-3 mb-4 flex justify-between text-sm">
|
||||
<span className="flex items-center gap-2">
|
||||
<Wallet className="w-4 h-4" /> Wallet
|
||||
<Wallet className="w-4 h-4" /> {sd.wallet}
|
||||
</span>
|
||||
<strong>{walletData?.balance?.toLocaleString() ?? 0} T</strong>
|
||||
<strong>{walletData?.balance?.toLocaleString('en-US') ?? 0} {t.common.currencyShort}</strong>
|
||||
</div>
|
||||
<div className="space-y-2 mb-6">
|
||||
{(['hourly', 'monthly', 'yearly'] as const).map((cycle) => (
|
||||
@@ -691,16 +697,16 @@ export default function ManagedServiceDetailPage() {
|
||||
}`}
|
||||
onClick={() => setSelectedCycle(cycle)}
|
||||
>
|
||||
<span className="capitalize font-medium">{cycle}</span>
|
||||
<span className="font-medium">{(sd.cycles as Record<string,string>)[cycle]}</span>
|
||||
<span className="font-bold">
|
||||
{renewalCostData?.costs[cycle]?.toLocaleString() ?? '—'} T
|
||||
{renewalCostData?.costs[cycle]?.toLocaleString('en-US') ?? '—'} {t.common.currencyShort}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="button" className="btn-secondary flex-1" onClick={() => setShowRenewalModal(false)}>
|
||||
Cancel
|
||||
{t.common.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -708,13 +714,13 @@ export default function ManagedServiceDetailPage() {
|
||||
disabled={renewMutation.isPending}
|
||||
onClick={() => renewMutation.mutate(selectedCycle)}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 inline mr-1" />
|
||||
Pay from wallet
|
||||
<RefreshCw className="w-4 h-4 inline mr-1 rtl:mr-0 rtl:ml-1" />
|
||||
{sd.payFromWallet}
|
||||
</button>
|
||||
</div>
|
||||
{renewalCost != null && walletData && walletData.balance < renewalCost && (
|
||||
<p className="text-xs text-amber-600 mt-3">
|
||||
Insufficient wallet balance. Top up your wallet or pay via invoice.
|
||||
{sd.insufficientRenew}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -949,6 +949,74 @@ const en: Dictionary = {
|
||||
insufficientBalance: 'Insufficient wallet balance',
|
||||
serviceDesc: 'Service: {name} ({cycle})',
|
||||
},
|
||||
serviceDetail: {
|
||||
productTypes: {
|
||||
managed_database: 'Managed Database',
|
||||
managed_redis: 'Managed Redis',
|
||||
managed_rabbitmq: 'Managed RabbitMQ',
|
||||
},
|
||||
deploy: 'Deploy',
|
||||
redeploy: 'Redeploy',
|
||||
redeployTitle: 'Re-run Helm install for this service',
|
||||
restart: 'Restart',
|
||||
deleteServiceTitle: 'Delete service?',
|
||||
deleteServiceMessage: 'Delete “{name}” permanently?',
|
||||
provisioningStarted: 'Provisioning started',
|
||||
provisioningFailed: 'Failed to start provisioning',
|
||||
reprovisioningStarted: 'Re-provisioning started',
|
||||
reprovisionFailed: 'Failed to re-provision service',
|
||||
serviceRestarted: 'Service restarted',
|
||||
restartFailed: 'Failed to restart service',
|
||||
serviceRenewed: 'Service renewed',
|
||||
renewalFailed: 'Renewal failed',
|
||||
deletedWithCredit: 'Service deleted. Prepaid resources are on your dashboard.',
|
||||
deleted: 'Service deleted',
|
||||
deleteFailed: 'Failed to delete',
|
||||
paymentRequired: 'Payment required',
|
||||
renewToRestore: 'Renew to restore this service.',
|
||||
renew: 'Renew',
|
||||
planExpires: 'Plan expires: {date}',
|
||||
configuration: 'Configuration',
|
||||
serviceType: 'Service type',
|
||||
databaseEngine: 'Database engine',
|
||||
storage: 'Storage',
|
||||
redisVersion: 'Redis version',
|
||||
rabbitmqVersion: 'RabbitMQ version',
|
||||
cpu: 'CPU',
|
||||
memory: 'Memory',
|
||||
billing: 'Billing',
|
||||
deployMarker: 'Deploy marker',
|
||||
deploymentHistory: 'Deployment History',
|
||||
noDeployments: 'No deployments yet',
|
||||
noDeploymentsHint: 'Click Deploy to provision this service',
|
||||
provision: 'Provision',
|
||||
connection: 'Connection',
|
||||
internalCluster: 'Internal cluster',
|
||||
host: 'Host',
|
||||
port: 'Port',
|
||||
database: 'Database',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
useExternalNote: 'Use external access below for internet-facing connections.',
|
||||
hideSecrets: 'Hide secrets',
|
||||
showSecrets: 'Show secrets',
|
||||
redisInternal: 'Redis (internal)',
|
||||
rabbitmqInternal: 'RabbitMQ (internal)',
|
||||
url: 'URL',
|
||||
amqpPort: 'AMQP Port',
|
||||
managementPort: 'Management Port',
|
||||
amqpUrl: 'AMQP URL',
|
||||
managementUrl: 'Management URL',
|
||||
deployToLoadCreds: 'Deploy the service to load live credentials.',
|
||||
loadingLogs: 'Loading logs...',
|
||||
serviceStopped: 'Service is stopped.',
|
||||
deployToSeeLogs: 'Deploy or re-provision the service to see workload logs.',
|
||||
renewService: 'Renew service',
|
||||
wallet: 'Wallet',
|
||||
payFromWallet: 'Pay from wallet',
|
||||
insufficientRenew: 'Insufficient wallet balance. Top up your wallet or pay via invoice.',
|
||||
cycles: { hourly: 'Hourly', monthly: 'Monthly', yearly: 'Yearly' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -948,6 +948,74 @@ const fa = {
|
||||
insufficientBalance: 'موجودی کیفپول کافی نیست',
|
||||
serviceDesc: 'سرویس: {name} ({cycle})',
|
||||
},
|
||||
serviceDetail: {
|
||||
productTypes: {
|
||||
managed_database: 'دیتابیس مدیریتشده',
|
||||
managed_redis: 'Redis مدیریتشده',
|
||||
managed_rabbitmq: 'RabbitMQ مدیریتشده',
|
||||
},
|
||||
deploy: 'انتشار',
|
||||
redeploy: 'انتشار مجدد',
|
||||
redeployTitle: 'اجرای مجدد Helm install برای این سرویس',
|
||||
restart: 'ریاستارت',
|
||||
deleteServiceTitle: 'حذف سرویس؟',
|
||||
deleteServiceMessage: 'حذف «{name}» بهصورت دائمی؟',
|
||||
provisioningStarted: 'راهاندازی آغاز شد',
|
||||
provisioningFailed: 'شروع راهاندازی ناموفق بود',
|
||||
reprovisioningStarted: 'راهاندازی مجدد آغاز شد',
|
||||
reprovisionFailed: 'راهاندازی مجدد سرویس ناموفق بود',
|
||||
serviceRestarted: 'سرویس ریاستارت شد',
|
||||
restartFailed: 'ریاستارت سرویس ناموفق بود',
|
||||
serviceRenewed: 'سرویس تمدید شد',
|
||||
renewalFailed: 'تمدید ناموفق بود',
|
||||
deletedWithCredit: 'سرویس حذف شد. منابع پیشپرداختت روی داشبورد است.',
|
||||
deleted: 'سرویس حذف شد',
|
||||
deleteFailed: 'حذف ناموفق بود',
|
||||
paymentRequired: 'پرداخت لازم است',
|
||||
renewToRestore: 'برای بازگرداندن این سرویس تمدید کن.',
|
||||
renew: 'تمدید',
|
||||
planExpires: 'انقضای پلن: {date}',
|
||||
configuration: 'پیکربندی',
|
||||
serviceType: 'نوع سرویس',
|
||||
databaseEngine: 'موتور دیتابیس',
|
||||
storage: 'فضای ذخیره',
|
||||
redisVersion: 'نسخهٔ Redis',
|
||||
rabbitmqVersion: 'نسخهٔ RabbitMQ',
|
||||
cpu: 'CPU',
|
||||
memory: 'حافظه',
|
||||
billing: 'صورتحساب',
|
||||
deployMarker: 'نشان دیپلوی',
|
||||
deploymentHistory: 'تاریخچهٔ دیپلوی',
|
||||
noDeployments: 'هنوز دیپلویی نیست',
|
||||
noDeploymentsHint: 'برای راهاندازی این سرویس روی «انتشار» بزن',
|
||||
provision: 'راهاندازی',
|
||||
connection: 'اتصال',
|
||||
internalCluster: 'کلاستر داخلی',
|
||||
host: 'هاست',
|
||||
port: 'پورت',
|
||||
database: 'دیتابیس',
|
||||
username: 'نام کاربری',
|
||||
password: 'رمز عبور',
|
||||
useExternalNote: 'برای اتصالهای اینترنتی از دسترسی خارجی زیر استفاده کن.',
|
||||
hideSecrets: 'پنهانکردن اسرار',
|
||||
showSecrets: 'نمایش اسرار',
|
||||
redisInternal: 'Redis (داخلی)',
|
||||
rabbitmqInternal: 'RabbitMQ (داخلی)',
|
||||
url: 'آدرس',
|
||||
amqpPort: 'پورت AMQP',
|
||||
managementPort: 'پورت مدیریت',
|
||||
amqpUrl: 'آدرس AMQP',
|
||||
managementUrl: 'آدرس مدیریت',
|
||||
deployToLoadCreds: 'برای بارگذاری اطلاعات ورود زنده، سرویس را منتشر کن.',
|
||||
loadingLogs: 'در حال بارگذاری لاگها…',
|
||||
serviceStopped: 'سرویس متوقف است.',
|
||||
deployToSeeLogs: 'برای دیدن لاگ workload، سرویس را منتشر یا راهاندازی مجدد کن.',
|
||||
renewService: 'تمدید سرویس',
|
||||
wallet: 'کیفپول',
|
||||
payFromWallet: 'پرداخت از کیفپول',
|
||||
insufficientRenew: 'موجودی کیفپول کافی نیست. کیفپول را شارژ کن یا از طریق فاکتور پرداخت کن.',
|
||||
cycles: { hourly: 'ساعتی', monthly: 'ماهانه', yearly: 'سالانه' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user