Localize workload-logs and external-access panels.
Add components.workloadLogs, components.deployStatus and components.externalAccess dictionaries; move the workload logs panel and the service external-access panel onto them (tabs, live-refresh hints, access modes, countdown units, toasts) with RTL-aware spacing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import type { Application, ServiceAccessGrant, ServiceAccessTarget } from '@/types';
|
||||
import { ExternalLink, ShieldAlert, Copy, Check, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
@@ -14,6 +15,7 @@ export function ServiceExternalAccessPanel({
|
||||
appId: string;
|
||||
app: Application;
|
||||
}) {
|
||||
const ea = useT().components.externalAccess;
|
||||
const queryClient = useQueryClient();
|
||||
const [accessTarget, setAccessTarget] = useState<ServiceAccessTarget>('database');
|
||||
const [accessDuration, setAccessDuration] = useState(60);
|
||||
@@ -25,13 +27,13 @@ export function ServiceExternalAccessPanel({
|
||||
const accessTargetOptions: { value: ServiceAccessTarget; label: string }[] = [];
|
||||
const hasDb =
|
||||
(app.databaseType && app.databaseType !== 'none') || app.productType === 'managed_database';
|
||||
if (hasDb) accessTargetOptions.push({ value: 'database', label: 'Database' });
|
||||
if (hasDb) accessTargetOptions.push({ value: 'database', label: ea.targets.database });
|
||||
if (app.enableRedis || app.productType === 'managed_redis') {
|
||||
accessTargetOptions.push({ value: 'redis', label: 'Redis' });
|
||||
accessTargetOptions.push({ value: 'redis', label: ea.targets.redis });
|
||||
}
|
||||
if (app.enableRabbitmq || app.productType === 'managed_rabbitmq') {
|
||||
accessTargetOptions.push({ value: 'rabbitmq_amqp', label: 'RabbitMQ (AMQP)' });
|
||||
accessTargetOptions.push({ value: 'rabbitmq_management', label: 'RabbitMQ Management UI' });
|
||||
accessTargetOptions.push({ value: 'rabbitmq_amqp', label: ea.targets.rabbitmq_amqp });
|
||||
accessTargetOptions.push({ value: 'rabbitmq_management', label: ea.targets.rabbitmq_management });
|
||||
}
|
||||
const hasAccessTargets = accessTargetOptions.length > 0;
|
||||
|
||||
@@ -67,11 +69,11 @@ export function ServiceExternalAccessPanel({
|
||||
onSuccess: () => {
|
||||
refetchAccessGrants();
|
||||
queryClient.invalidateQueries({ queryKey: ['access-grants', appId] });
|
||||
toast.success(accessPersistent ? 'Permanent external access enabled' : 'Temporary external access enabled');
|
||||
toast.success(accessPersistent ? ea.permanentEnabled : ea.temporaryEnabled);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || 'Failed to enable access');
|
||||
toast.error(msg || ea.enableFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,9 +82,9 @@ export function ServiceExternalAccessPanel({
|
||||
onSuccess: () => {
|
||||
refetchAccessGrants();
|
||||
queryClient.invalidateQueries({ queryKey: ['access-grants', appId] });
|
||||
toast.success('Access revoked');
|
||||
toast.success(ea.revoked);
|
||||
},
|
||||
onError: () => toast.error('Failed to revoke access'),
|
||||
onError: () => toast.error(ea.revokeFailed),
|
||||
});
|
||||
|
||||
const copyToClipboard = (text: string, field: string) => {
|
||||
@@ -95,16 +97,16 @@ export function ServiceExternalAccessPanel({
|
||||
accessTargetOptions.find((o) => o.value === target)?.label || target;
|
||||
|
||||
const formatAccessCountdown = (grant: ServiceAccessGrant) => {
|
||||
if (grant.persistent) return 'Permanent (until revoked)';
|
||||
if (grant.persistent) return ea.permanentUntilRevoked;
|
||||
const ms = new Date(grant.expiresAt).getTime() - accessNow;
|
||||
if (ms <= 0) return 'Expired';
|
||||
if (ms <= 0) return ea.expired;
|
||||
const totalSec = Math.floor(ms / 1000);
|
||||
const h = Math.floor(totalSec / 3600);
|
||||
const m = Math.floor((totalSec % 3600) / 60);
|
||||
const s = totalSec % 60;
|
||||
if (h > 0) return `${h}h ${m}m ${s}s`;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
if (h > 0) return `${h}${ea.hShort} ${m}${ea.mShort} ${s}${ea.sShort}`;
|
||||
if (m > 0) return `${m}${ea.mShort} ${s}${ea.sShort}`;
|
||||
return `${s}${ea.sShort}`;
|
||||
};
|
||||
|
||||
if (!hasAccessTargets) return null;
|
||||
@@ -112,22 +114,21 @@ export function ServiceExternalAccessPanel({
|
||||
return (
|
||||
<div className="card">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-2 flex items-center gap-2">
|
||||
<ExternalLink className="w-5 h-5" /> External access
|
||||
<ExternalLink className="w-5 h-5" /> {ea.title}
|
||||
</h2>
|
||||
<p className="text-xs text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-3 py-2 mb-4 flex items-start gap-2">
|
||||
<ShieldAlert className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
Opens a NodePort on the cluster node IP. Anyone who can reach that IP may connect until the session ends or you
|
||||
revoke it. Use short durations for temporary access; permanent keeps the port open until revoked.
|
||||
{ea.warning}
|
||||
</p>
|
||||
|
||||
{!app.latestImageTag ? (
|
||||
<p className="text-sm text-gray-500">Deploy the service first to enable external access.</p>
|
||||
<p className="text-sm text-gray-500">{ea.deployFirst}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-gray-50 rounded-xl p-4 mb-4 space-y-4">
|
||||
<div className="flex flex-wrap gap-4 items-end">
|
||||
<div className="flex-1 min-w-[160px]">
|
||||
<label className="text-xs font-medium text-gray-600 block mb-1">Service</label>
|
||||
<label className="text-xs font-medium text-gray-600 block mb-1">{ea.service}</label>
|
||||
<select
|
||||
value={accessTarget}
|
||||
onChange={(e) => setAccessTarget(e.target.value as ServiceAccessTarget)}
|
||||
@@ -141,7 +142,7 @@ export function ServiceExternalAccessPanel({
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="text-xs font-medium text-gray-600 block mb-1">Access mode</label>
|
||||
<label className="text-xs font-medium text-gray-600 block mb-1">{ea.accessMode}</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -152,7 +153,7 @@ export function ServiceExternalAccessPanel({
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
Temporary
|
||||
{ea.temporary}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -163,7 +164,7 @@ export function ServiceExternalAccessPanel({
|
||||
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
Always open
|
||||
{ea.alwaysOpen}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -171,7 +172,7 @@ export function ServiceExternalAccessPanel({
|
||||
|
||||
{!accessPersistent && (
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-600 block mb-1">Duration</label>
|
||||
<label className="text-xs font-medium text-gray-600 block mb-1">{ea.duration}</label>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{[30, 60, 240].map((mins) => (
|
||||
<button
|
||||
@@ -193,7 +194,7 @@ export function ServiceExternalAccessPanel({
|
||||
|
||||
{accessPersistent && (
|
||||
<p className="text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded-lg px-3 py-2">
|
||||
The port stays exposed until you click Revoke. Only use this when you need a stable external endpoint.
|
||||
{ea.persistentNote}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -203,12 +204,12 @@ export function ServiceExternalAccessPanel({
|
||||
disabled={createAccessMutation.isPending}
|
||||
className="btn-primary text-sm"
|
||||
>
|
||||
{createAccessMutation.isPending ? 'Opening…' : accessPersistent ? 'Open port permanently' : 'Enable access'}
|
||||
{createAccessMutation.isPending ? ea.opening : accessPersistent ? ea.openPermanently : ea.enableAccess}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{accessGrants.filter((g) => g.status === 'active').length === 0 ? (
|
||||
<p className="text-sm text-gray-500">No active external access sessions.</p>
|
||||
<p className="text-sm text-gray-500">{ea.noSessions}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{accessGrants
|
||||
@@ -219,8 +220,8 @@ export function ServiceExternalAccessPanel({
|
||||
<div>
|
||||
<span className="text-sm font-semibold text-gray-800">{accessTargetLabel(grant.target)}</span>
|
||||
{grant.persistent && (
|
||||
<span className="ml-2 text-xs font-medium text-amber-700 bg-amber-50 px-2 py-0.5 rounded-full">
|
||||
Always open
|
||||
<span className="ml-2 rtl:ml-0 rtl:mr-2 text-xs font-medium text-amber-700 bg-amber-50 px-2 py-0.5 rounded-full">
|
||||
{ea.alwaysOpen}
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-2 text-xs text-gray-500">{formatAccessCountdown(grant)}</span>
|
||||
@@ -231,12 +232,12 @@ export function ServiceExternalAccessPanel({
|
||||
disabled={revokeAccessMutation.isPending}
|
||||
className="btn-secondary text-xs text-red-600 border-red-200 hover:bg-red-50"
|
||||
>
|
||||
Revoke
|
||||
{ea.revoke}
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1.5 text-sm font-mono">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-gray-500 text-xs font-sans">Endpoint</span>
|
||||
<span className="text-gray-500 text-xs font-sans">{ea.endpoint}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-800">
|
||||
{grant.host}:{grant.port}
|
||||
@@ -256,7 +257,7 @@ export function ServiceExternalAccessPanel({
|
||||
</div>
|
||||
{grant.connection.url && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-gray-500 text-xs font-sans">URL</span>
|
||||
<span className="text-gray-500 text-xs font-sans">{ea.url}</span>
|
||||
<div className="flex items-center gap-2 max-w-[70%]">
|
||||
<span className="text-gray-800 truncate text-xs" title={grant.connection.url}>
|
||||
{showAccessSecret ? grant.connection.url : '••••••••••••'}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { FileText, ChevronDown, Monitor, Hammer, Pin } from 'lucide-react';
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
@@ -29,6 +30,9 @@ export function WorkloadLogsPanel({
|
||||
isStopped?: boolean;
|
||||
emptyPodMessage?: string;
|
||||
}) {
|
||||
const t = useT();
|
||||
const wl = t.components.workloadLogs;
|
||||
const statusLabel = (s: string) => (t.components.deployStatus as Record<string, string>)[s] ?? s;
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
const [logTab, setLogTab] = useState<'pod' | 'build'>('pod');
|
||||
const logsEndRef = useRef<HTMLPreElement>(null);
|
||||
@@ -60,38 +64,38 @@ export function WorkloadLogsPanel({
|
||||
const podPlaceholder =
|
||||
emptyPodMessage ||
|
||||
(isRunning
|
||||
? 'Loading logs...'
|
||||
? wl.loadingLogs
|
||||
: isStopped
|
||||
? 'Service is stopped. Start it to see logs.'
|
||||
: 'Waiting for workload pods to be ready...');
|
||||
? wl.serviceStopped
|
||||
: wl.waitingPods);
|
||||
|
||||
return (
|
||||
<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">
|
||||
<FileText className="w-5 h-5" /> Logs
|
||||
<FileText className="w-5 h-5" /> {wl.logs}
|
||||
</h2>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
{showLogs && logTab === 'pod' && (
|
||||
<span className="text-xs text-gray-400 flex items-center space-x-1">
|
||||
<span className="text-xs text-gray-400 flex items-center space-x-1 rtl:space-x-reverse">
|
||||
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
<span>Live (every 3s)</span>
|
||||
<span>{wl.liveEvery3s}</span>
|
||||
</span>
|
||||
)}
|
||||
{showBuildLogs && showLogs && logTab === 'build' && (
|
||||
<span className="text-xs text-gray-400 flex items-center space-x-1">
|
||||
<span className="text-xs text-gray-400 flex items-center space-x-1 rtl:space-x-reverse">
|
||||
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||
<span>Auto-refresh (every 5s)</span>
|
||||
<span>{wl.autoEvery5s}</span>
|
||||
</span>
|
||||
)}
|
||||
<button type="button" onClick={() => setShowLogs(!showLogs)} className="btn-secondary text-sm">
|
||||
{showLogs ? (
|
||||
<>
|
||||
<ChevronDown className="w-4 h-4 inline" /> Hide logs
|
||||
<ChevronDown className="w-4 h-4 inline" /> {wl.hideLogs}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-4 h-4 inline" /> Show logs
|
||||
<FileText className="w-4 h-4 inline" /> {wl.showLogs}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -109,7 +113,7 @@ export function WorkloadLogsPanel({
|
||||
logTab === 'pod' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Monitor className="w-4 h-4 inline" /> Pod logs
|
||||
<Monitor className="w-4 h-4 inline" /> {wl.podLogs}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -118,11 +122,11 @@ export function WorkloadLogsPanel({
|
||||
logTab === 'build' ? 'bg-white text-gray-900 shadow-sm' : 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
<Hammer className="w-4 h-4 inline" /> Build logs
|
||||
<Hammer className="w-4 h-4 inline" /> {wl.buildLogs}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">Workload pod output (no image build for this service).</p>
|
||||
<p className="text-xs text-gray-500">{wl.noBuildNote}</p>
|
||||
)}
|
||||
|
||||
{logTab === 'pod' && (
|
||||
@@ -142,19 +146,19 @@ export function WorkloadLogsPanel({
|
||||
<Pin className="w-3 h-3 inline" /> {buildLogsData.version}
|
||||
</span>
|
||||
<span className={`badge ${statusColors[buildLogsData.status] || 'badge-gray'}`}>
|
||||
{buildLogsData.status}
|
||||
{statusLabel(buildLogsData.status)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<pre className="bg-gray-900 text-blue-300 p-4 rounded-xl text-xs font-mono overflow-x-auto max-h-[500px] overflow-y-auto whitespace-pre-wrap break-words">
|
||||
{buildLogsData?.buildLog ||
|
||||
(buildLogsData?.status === 'building'
|
||||
? 'Build in progress... Logs will appear when complete.'
|
||||
? wl.buildInProgress
|
||||
: buildLogsData?.status === 'pending'
|
||||
? 'Build is pending...'
|
||||
? wl.buildPending
|
||||
: buildLogsData?.status === 'no_deployment'
|
||||
? 'No deployments yet. Deploy your app to see build logs.'
|
||||
: 'No build logs available for this deployment.')}
|
||||
? wl.noDeployments
|
||||
: wl.noBuildLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -188,6 +188,70 @@ const en: Dictionary = {
|
||||
twoCores: '2 cores',
|
||||
storageGb: 'Storage (GB)',
|
||||
},
|
||||
deployStatus: {
|
||||
running: 'running',
|
||||
pending: 'pending',
|
||||
building: 'building',
|
||||
deploying: 'deploying',
|
||||
failed: 'failed',
|
||||
build_failed: 'build_failed',
|
||||
cancelled: 'cancelled',
|
||||
stopped: 'stopped',
|
||||
active: 'active',
|
||||
suspended: 'suspended',
|
||||
pending_deletion: 'pending_deletion',
|
||||
},
|
||||
workloadLogs: {
|
||||
logs: 'Logs',
|
||||
liveEvery3s: 'Live (every 3s)',
|
||||
autoEvery5s: 'Auto-refresh (every 5s)',
|
||||
hideLogs: 'Hide logs',
|
||||
showLogs: 'Show logs',
|
||||
podLogs: 'Pod logs',
|
||||
buildLogs: 'Build logs',
|
||||
noBuildNote: 'Workload pod output (no image build for this service).',
|
||||
loadingLogs: 'Loading logs...',
|
||||
serviceStopped: 'Service is stopped. Start it to see logs.',
|
||||
waitingPods: 'Waiting for workload pods to be ready...',
|
||||
buildInProgress: 'Build in progress... Logs will appear when complete.',
|
||||
buildPending: 'Build is pending...',
|
||||
noDeployments: 'No deployments yet. Deploy your app to see build logs.',
|
||||
noBuildLogs: 'No build logs available for this deployment.',
|
||||
},
|
||||
externalAccess: {
|
||||
title: 'External access',
|
||||
warning: 'Opens a NodePort on the cluster node IP. Anyone who can reach that IP may connect until the session ends or you revoke it. Use short durations for temporary access; permanent keeps the port open until revoked.',
|
||||
deployFirst: 'Deploy the service first to enable external access.',
|
||||
service: 'Service',
|
||||
accessMode: 'Access mode',
|
||||
temporary: 'Temporary',
|
||||
alwaysOpen: 'Always open',
|
||||
duration: 'Duration',
|
||||
persistentNote: 'The port stays exposed until you click Revoke. Only use this when you need a stable external endpoint.',
|
||||
opening: 'Opening…',
|
||||
openPermanently: 'Open port permanently',
|
||||
enableAccess: 'Enable access',
|
||||
noSessions: 'No active external access sessions.',
|
||||
revoke: 'Revoke',
|
||||
endpoint: 'Endpoint',
|
||||
url: 'URL',
|
||||
permanentUntilRevoked: 'Permanent (until revoked)',
|
||||
expired: 'Expired',
|
||||
targets: {
|
||||
database: 'Database',
|
||||
redis: 'Redis',
|
||||
rabbitmq_amqp: 'RabbitMQ (AMQP)',
|
||||
rabbitmq_management: 'RabbitMQ Management UI',
|
||||
},
|
||||
permanentEnabled: 'Permanent external access enabled',
|
||||
temporaryEnabled: 'Temporary external access enabled',
|
||||
enableFailed: 'Failed to enable access',
|
||||
revoked: 'Access revoked',
|
||||
revokeFailed: 'Failed to revoke access',
|
||||
hShort: 'h',
|
||||
mShort: 'm',
|
||||
sShort: 's',
|
||||
},
|
||||
},
|
||||
|
||||
nav: {
|
||||
|
||||
@@ -187,6 +187,70 @@ const fa = {
|
||||
twoCores: '۲ هسته',
|
||||
storageGb: 'فضای ذخیره (GB)',
|
||||
},
|
||||
deployStatus: {
|
||||
running: 'در حال اجرا',
|
||||
pending: 'در انتظار',
|
||||
building: 'در حال ساخت',
|
||||
deploying: 'در حال انتشار',
|
||||
failed: 'ناموفق',
|
||||
build_failed: 'ساخت ناموفق',
|
||||
cancelled: 'لغوشده',
|
||||
stopped: 'متوقف',
|
||||
active: 'فعال',
|
||||
suspended: 'معلق',
|
||||
pending_deletion: 'در انتظار حذف',
|
||||
},
|
||||
workloadLogs: {
|
||||
logs: 'لاگها',
|
||||
liveEvery3s: 'زنده (هر ۳ث)',
|
||||
autoEvery5s: 'تازهسازی خودکار (هر ۵ث)',
|
||||
hideLogs: 'پنهانکردن لاگها',
|
||||
showLogs: 'نمایش لاگها',
|
||||
podLogs: 'لاگ پاد',
|
||||
buildLogs: 'لاگ بیلد',
|
||||
noBuildNote: 'خروجی پاد workload (این سرویس بیلد ایمیج ندارد).',
|
||||
loadingLogs: 'در حال بارگذاری لاگها…',
|
||||
serviceStopped: 'سرویس متوقف است. برای دیدن لاگ آن را اجرا کن.',
|
||||
waitingPods: 'در انتظار آمادهشدن پادهای workload…',
|
||||
buildInProgress: 'بیلد در حال انجام… لاگ پس از تکمیل نمایش داده میشود.',
|
||||
buildPending: 'بیلد در صف است…',
|
||||
noDeployments: 'هنوز دیپلویی نیست. برای دیدن لاگ بیلد اپت را منتشر کن.',
|
||||
noBuildLogs: 'لاگ بیلدی برای این دیپلوی موجود نیست.',
|
||||
},
|
||||
externalAccess: {
|
||||
title: 'دسترسی خارجی',
|
||||
warning: 'یک NodePort روی IP نود کلاستر باز میکند. هر کسی که به آن IP دسترسی داشته باشد تا پایان نشست یا لغو آن میتواند متصل شود. برای دسترسی موقت از مدتهای کوتاه استفاده کن؛ حالت دائمی پورت را تا لغو باز نگه میدارد.',
|
||||
deployFirst: 'برای فعالسازی دسترسی خارجی، اول سرویس را منتشر کن.',
|
||||
service: 'سرویس',
|
||||
accessMode: 'حالت دسترسی',
|
||||
temporary: 'موقت',
|
||||
alwaysOpen: 'همیشه باز',
|
||||
duration: 'مدت',
|
||||
persistentNote: 'پورت تا وقتی روی «لغو» کلیک کنی باز میماند. فقط وقتی به یک نقطهٔ پایانیِ خارجیِ پایدار نیاز داری استفاده کن.',
|
||||
opening: 'در حال باز کردن…',
|
||||
openPermanently: 'باز کردن دائمی پورت',
|
||||
enableAccess: 'فعالسازی دسترسی',
|
||||
noSessions: 'نشست دسترسی خارجی فعالی وجود ندارد.',
|
||||
revoke: 'لغو',
|
||||
endpoint: 'نقطهٔ پایانی',
|
||||
url: 'آدرس',
|
||||
permanentUntilRevoked: 'دائمی (تا لغو)',
|
||||
expired: 'منقضیشده',
|
||||
targets: {
|
||||
database: 'دیتابیس',
|
||||
redis: 'Redis',
|
||||
rabbitmq_amqp: 'RabbitMQ (AMQP)',
|
||||
rabbitmq_management: 'رابط مدیریت RabbitMQ',
|
||||
},
|
||||
permanentEnabled: 'دسترسی خارجی دائمی فعال شد',
|
||||
temporaryEnabled: 'دسترسی خارجی موقت فعال شد',
|
||||
enableFailed: 'فعالسازی دسترسی ناموفق بود',
|
||||
revoked: 'دسترسی لغو شد',
|
||||
revokeFailed: 'لغو دسترسی ناموفق بود',
|
||||
hShort: 'س',
|
||||
mShort: 'د',
|
||||
sShort: 'ث',
|
||||
},
|
||||
},
|
||||
|
||||
nav: {
|
||||
|
||||
Reference in New Issue
Block a user