refactor(i18n): U15 ApiAccessKeySection i18n 迁移 — 56 个 hash 乱码键清理为语义化 user.apiKey.* 键 + 组件全量接入 useTranslation
- 清理 56 个 apiaccesskey.s{hash} 乱码键为语义化 user.apiKey.* 键 (title/issue/rotate/revoke/descLabel 等)
- ApiAccessKeySection.tsx 全量接入 useTranslation() + t() 调用, UI 中文 57→0
- 复用 common.loading/common.cancel/common.save 通用键
- zh-CN/en-US 完全对齐 (7556 = 7556), 零 hash 键残留
- tsc -b=0, npm build✓ (18.23s), ESLint errors=0/any=0, go build=0, 17/17 测试通过
This commit is contained in:
1 parent
a2edf6aa9f
commit
178c98dd37
3 files changed
+177
-171
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
// ApiAccessKeySection — 单条 CI/CD 访问凭证(凭证卡片布局)// Logan.Li
|
// ApiAccessKeySection — 单条 CI/CD 访问凭证(凭证卡片布局)
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -52,7 +53,7 @@ function toDateInputValue(value?: string) {
|
|||||||
return Number.isNaN(d.getTime()) ? defaultExpiresDate() : d.toISOString().slice(0, 10);
|
return Number.isNaN(d.getTime()) ? defaultExpiresDate() : d.toISOString().slice(0, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据创建/到期时间计算有效期进度(0-100)// Logan.Li */
|
/** Compute expiry progress (0-100) based on created/expires timestamps */
|
||||||
function expiryProgress(createdAt?: string, expiresAt?: string) {
|
function expiryProgress(createdAt?: string, expiresAt?: string) {
|
||||||
if (!createdAt || !expiresAt) return { pct: 0, daysLeft: null as number | null };
|
if (!createdAt || !expiresAt) return { pct: 0, daysLeft: null as number | null };
|
||||||
const start = new Date(createdAt).getTime();
|
const start = new Date(createdAt).getTime();
|
||||||
@@ -71,6 +72,7 @@ function shortKeyId(id?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ApiAccessKeySection() {
|
export function ApiAccessKeySection() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const apiBase = getPublicApiBase();
|
const apiBase = getPublicApiBase();
|
||||||
@@ -137,9 +139,9 @@ export function ApiAccessKeySection() {
|
|||||||
});
|
});
|
||||||
setKeyMeta(res);
|
setKeyMeta(res);
|
||||||
setFormOpen(false);
|
setFormOpen(false);
|
||||||
toast({ title: '凭证信息已更新' });
|
toast({ title: t('user.apiKey.updated') });
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
toast({ title: '更新失败', description: getErrorMessage(e), variant: 'destructive' });
|
toast({ title: t('user.apiKey.updateFailed'), description: getErrorMessage(e), variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(false);
|
setActionLoading(false);
|
||||||
}
|
}
|
||||||
@@ -148,9 +150,9 @@ export function ApiAccessKeySection() {
|
|||||||
|
|
||||||
const hasKey = !!keyMeta?.has_token;
|
const hasKey = !!keyMeta?.has_token;
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: hasKey ? '轮换凭证' : '签发凭证',
|
title: hasKey ? t('user.apiKey.rotateTitle') : t('user.apiKey.issue'),
|
||||||
description: hasKey ? '旧凭证将立即作废,是否继续?' : '凭证明文仅显示一次,请妥善保存。',
|
description: hasKey ? t('user.apiKey.rotateDesc') : t('user.apiKey.issueDesc'),
|
||||||
confirmText: hasKey ? '轮换' : '签发',
|
confirmText: hasKey ? t('user.apiKey.rotateBtn') : t('user.apiKey.issue'),
|
||||||
variant: hasKey ? 'destructive' : 'default',
|
variant: hasKey ? 'destructive' : 'default',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -178,9 +180,9 @@ export function ApiAccessKeySection() {
|
|||||||
expired: false,
|
expired: false,
|
||||||
days_left: Math.ceil((new Date(res.expires_at).getTime() - Date.now()) / 86400000),
|
days_left: Math.ceil((new Date(res.expires_at).getTime() - Date.now()) / 86400000),
|
||||||
});
|
});
|
||||||
toast({ title: '凭证已签发', description: '已保存至本浏览器,请立即复制' });
|
toast({ title: t('user.apiKey.issued'), description: t('user.apiKey.issuedDesc') });
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
toast({ title: '签发失败', description: getErrorMessage(e), variant: 'destructive' });
|
toast({ title: t('user.apiKey.issueFailed'), description: getErrorMessage(e), variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(false);
|
setActionLoading(false);
|
||||||
}
|
}
|
||||||
@@ -188,9 +190,9 @@ export function ApiAccessKeySection() {
|
|||||||
|
|
||||||
const handleRevoke = async () => {
|
const handleRevoke = async () => {
|
||||||
const ok = await confirm({
|
const ok = await confirm({
|
||||||
title: '吊销凭证',
|
title: t('user.apiKey.revokeTitle'),
|
||||||
description: '吊销后 Pipeline / curl 调用将全部失败,是否继续?',
|
description: t('user.apiKey.revokeDesc'),
|
||||||
confirmText: '吊销',
|
confirmText: t('user.apiKey.revokeBtn'),
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
@@ -200,15 +202,15 @@ export function ApiAccessKeySection() {
|
|||||||
setPlainToken('');
|
setPlainToken('');
|
||||||
setSessionApiAccessKey('');
|
setSessionApiAccessKey('');
|
||||||
setKeyMeta({ has_token: false });
|
setKeyMeta({ has_token: false });
|
||||||
toast({ title: '凭证已吊销' });
|
toast({ title: t('user.apiKey.revoked') });
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
toast({ title: '吊销失败', description: getErrorMessage(e), variant: 'destructive' });
|
toast({ title: t('user.apiKey.revokeFailed'), description: getErrorMessage(e), variant: 'destructive' });
|
||||||
} finally {
|
} finally {
|
||||||
setActionLoading(false);
|
setActionLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const authHeader = copyableToken ? `X-AIOps-Access-Key: ${copyableToken}` : 'X-AIOps-Access-Key: <你的访问凭证>';
|
const authHeader = copyableToken ? `X-AIOps-Access-Key: ${copyableToken}` : `X-AIOps-Access-Key: <${t('user.apiKey.placeholderToken')}>`;
|
||||||
const sampleCluster = 'test-k8s';
|
const sampleCluster = 'test-k8s';
|
||||||
const sampleNs = 'default';
|
const sampleNs = 'default';
|
||||||
const sampleDep = 'demo-jar';
|
const sampleDep = 'demo-jar';
|
||||||
@@ -232,27 +234,27 @@ export function ApiAccessKeySection() {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base flex items-center gap-2 text-[var(--dg-text-primary)]">
|
<CardTitle className="text-base flex items-center gap-2 text-[var(--dg-text-primary)]">
|
||||||
<KeyRound className="h-4 w-4 text-[var(--dg-blue)]" />
|
<KeyRound className="h-4 w-4 text-[var(--dg-blue)]" />
|
||||||
CI/CD 访问凭证
|
{t('user.apiKey.title')}
|
||||||
<Badge className="text-[10px] bg-[var(--dg-blue)]/15 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">长期有效</Badge>
|
<Badge className="text-[10px] bg-[var(--dg-blue)]/15 text-[var(--dg-blue)] border-[var(--dg-blue)]/30">{t('user.apiKey.longTerm')}</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription className="text-xs mt-1">
|
<CardDescription className="text-xs mt-1">
|
||||||
每人唯一凭证,Deployment 发布脚本自动引用,不随登录会话过期。
|
{t('user.apiKey.desc')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!loading && !keyMeta?.has_token && (
|
{!loading && !keyMeta?.has_token && (
|
||||||
<Button size="sm" onClick={openCreate} disabled={actionLoading}>
|
<Button size="sm" onClick={openCreate} disabled={actionLoading}>
|
||||||
<Plus className="h-4 w-4 mr-1" />签发凭证
|
<Plus className="h-4 w-4 mr-1" />{t('user.apiKey.issue')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2 py-8 text-sm text-[var(--dg-text-muted)] justify-center">
|
<div className="flex items-center gap-2 py-8 text-sm text-[var(--dg-text-muted)] justify-center">
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />加载中…
|
<Loader2 className="h-4 w-4 animate-spin" />{t('common.loading')}
|
||||||
</div>
|
</div>
|
||||||
) : keyMeta?.has_token ? (
|
) : keyMeta?.has_token ? (
|
||||||
<div className="rounded-xl border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40 overflow-hidden">
|
<div className="rounded-xl border border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)]/40 overflow-hidden">
|
||||||
{/* 凭证主体:左侧色条 + 内容区 // Logan.Li */}
|
{/* credential body: left color bar + content area */}
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'w-1 shrink-0',
|
'w-1 shrink-0',
|
||||||
@@ -261,7 +263,7 @@ export function ApiAccessKeySection() {
|
|||||||
<div className="flex-1 p-4 space-y-4">
|
<div className="flex-1 p-4 space-y-4">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div className="space-y-1 min-w-0 flex-1">
|
<div className="space-y-1 min-w-0 flex-1">
|
||||||
<p className="text-[10px] uppercase tracking-wider text-[var(--dg-text-muted)]">凭证字符串</p>
|
<p className="text-[10px] uppercase tracking-wider text-[var(--dg-text-muted)]">{t('user.apiKey.tokenLabel')}</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<code className="flex-1 min-w-0 text-xs font-mono text-[var(--dg-text-primary)] bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md px-3 py-2 truncate block">
|
<code className="flex-1 min-w-0 text-xs font-mono text-[var(--dg-text-primary)] bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)] rounded-md px-3 py-2 truncate block">
|
||||||
{showToken && copyableToken ? copyableToken : mask}
|
{showToken && copyableToken ? copyableToken : mask}
|
||||||
@@ -272,7 +274,7 @@ export function ApiAccessKeySection() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-9 w-9 shrink-0 border-[var(--dg-border)]"
|
className="h-9 w-9 shrink-0 border-[var(--dg-border)]"
|
||||||
onClick={() => setShowToken(!showToken)}
|
onClick={() => setShowToken(!showToken)}
|
||||||
title={showToken ? '隐藏' : '显示'}
|
title={showToken ? t('user.apiKey.hide') : t('user.apiKey.show')}
|
||||||
>
|
>
|
||||||
{showToken ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
{showToken ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -281,41 +283,41 @@ export function ApiAccessKeySection() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-1.5 shrink-0">
|
<div className="flex flex-wrap gap-1.5 shrink-0">
|
||||||
<Button variant="outline" size="sm" className="h-8 text-xs border-[var(--dg-border)]" onClick={openEdit} disabled={actionLoading}>
|
<Button variant="outline" size="sm" className="h-8 text-xs border-[var(--dg-border)]" onClick={openEdit} disabled={actionLoading}>
|
||||||
<Settings2 className="h-3 w-3 mr-1" />调整
|
<Settings2 className="h-3 w-3 mr-1" />{t('user.apiKey.adjust')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" className="h-8 text-xs border-[var(--dg-border)]" onClick={openCreate} disabled={actionLoading}>
|
<Button variant="outline" size="sm" className="h-8 text-xs border-[var(--dg-border)]" onClick={openCreate} disabled={actionLoading}>
|
||||||
<RotateCw className="h-3 w-3 mr-1" />轮换
|
<RotateCw className="h-3 w-3 mr-1" />{t('user.apiKey.rotate')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" className="h-8 text-xs border-red-500/30 dg-text-red" onClick={handleRevoke} disabled={actionLoading}>
|
<Button variant="outline" size="sm" className="h-8 text-xs border-red-500/30 dg-text-red" onClick={handleRevoke} disabled={actionLoading}>
|
||||||
<Ban className="h-3 w-3 mr-1" />吊销
|
<Ban className="h-3 w-3 mr-1" />{t('user.apiKey.revoke')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-6 gap-y-3 text-xs">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-6 gap-y-3 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[var(--dg-text-muted)] mb-0.5">用途说明</p>
|
<p className="text-[var(--dg-text-muted)] mb-0.5">{t('user.apiKey.descLabel')}</p>
|
||||||
<p className="text-[var(--dg-text-secondary)]">{keyMeta.description || '未填写'}</p>
|
<p className="text-[var(--dg-text-secondary)]">{keyMeta.description || t('user.apiKey.descEmpty')}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[var(--dg-text-muted)] mb-0.5">凭证标识</p>
|
<p className="text-[var(--dg-text-muted)] mb-0.5">{t('user.apiKey.keyIdLabel')}</p>
|
||||||
<p className="font-mono text-[var(--dg-text-secondary)]" title={keyMeta.key_id}>{shortKeyId(keyMeta.key_id)}</p>
|
<p className="font-mono text-[var(--dg-text-secondary)]" title={keyMeta.key_id}>{shortKeyId(keyMeta.key_id)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[var(--dg-text-muted)] mb-0.5">签发时间</p>
|
<p className="text-[var(--dg-text-muted)] mb-0.5">{t('user.apiKey.createdLabel')}</p>
|
||||||
<p className="text-[var(--dg-text-secondary)]">{formatDateTime(keyMeta.created_at)}</p>
|
<p className="text-[var(--dg-text-secondary)]">{formatDateTime(keyMeta.created_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 有效期进度条 // Logan.Li */}
|
{/* expiry progress bar */}
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between text-[11px]">
|
<div className="flex items-center justify-between text-[11px]">
|
||||||
<span className="flex items-center gap-1 text-[var(--dg-text-muted)]">
|
<span className="flex items-center gap-1 text-[var(--dg-text-muted)]">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
{isExpired ? '已过期' : daysLeft != null ? `剩余 ${daysLeft} 天` : '有效期'}
|
{isExpired ? t('user.apiKey.expired') : daysLeft != null ? t('user.apiKey.daysLeft', { days: daysLeft }) : t('user.apiKey.validity')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[var(--dg-text-muted)]">
|
<span className="text-[var(--dg-text-muted)]">
|
||||||
有效至 {formatDateTime(keyMeta.expires_at)}
|
{t('user.apiKey.validUntil', { date: formatDateTime(keyMeta.expires_at) })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 rounded-full bg-[var(--dg-bg-secondary)] overflow-hidden">
|
<div className="h-1.5 rounded-full bg-[var(--dg-bg-secondary)] overflow-hidden">
|
||||||
@@ -329,13 +331,13 @@ export function ApiAccessKeySection() {
|
|||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-[var(--dg-text-muted)] pt-1 border-t border-[var(--dg-border)]/60">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-[var(--dg-text-muted)] pt-1 border-t border-[var(--dg-border)]/60">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<Activity className="h-3 w-3" />
|
<Activity className="h-3 w-3" />
|
||||||
最近调用 {formatDateTime(keyMeta.last_used_at)}
|
{t('user.apiKey.lastUsed', { date: formatDateTime(keyMeta.last_used_at) })}
|
||||||
</span>
|
</span>
|
||||||
{keyMeta.prefix && (
|
{keyMeta.prefix && (
|
||||||
<span>前缀 <code className="text-[var(--dg-text-secondary)]">{keyMeta.prefix}</code></span>
|
<span>{t('user.apiKey.prefix')} <code className="text-[var(--dg-text-secondary)]">{keyMeta.prefix}</code></span>
|
||||||
)}
|
)}
|
||||||
{isExpired && (
|
{isExpired && (
|
||||||
<Badge variant="outline" className="text-[10px] border-red-500/40 dg-text-red">需续期或轮换</Badge>
|
<Badge variant="outline" className="text-[10px] border-red-500/40 dg-text-red">{t('user.apiKey.needRenew')}</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -346,35 +348,35 @@ export function ApiAccessKeySection() {
|
|||||||
<div className="mx-auto mb-3 h-12 w-12 rounded-full bg-[var(--dg-blue)]/10 flex items-center justify-center">
|
<div className="mx-auto mb-3 h-12 w-12 rounded-full bg-[var(--dg-blue)]/10 flex items-center justify-center">
|
||||||
<KeyRound className="h-6 w-6 text-[var(--dg-blue)]" />
|
<KeyRound className="h-6 w-6 text-[var(--dg-blue)]" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-[var(--dg-text-secondary)]">尚未签发 CI/CD 访问凭证</p>
|
<p className="text-sm text-[var(--dg-text-secondary)]">{t('user.apiKey.emptyTitle')}</p>
|
||||||
<p className="text-xs text-[var(--dg-text-muted)] mt-1 mb-4">签发后可用于 Pipeline 触发镜像更新、滚动重启等操作</p>
|
<p className="text-xs text-[var(--dg-text-muted)] mt-1 mb-4">{t('user.apiKey.emptyDesc')}</p>
|
||||||
<Button size="sm" onClick={openCreate} disabled={actionLoading}>
|
<Button size="sm" onClick={openCreate} disabled={actionLoading}>
|
||||||
<Plus className="h-4 w-4 mr-1" />立即签发
|
<Plus className="h-4 w-4 mr-1" />{t('user.apiKey.issueNow')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{keyMeta?.has_token && !copyableToken && (
|
{keyMeta?.has_token && !copyableToken && (
|
||||||
<p className="text-[11px] dg-text-yellow/90">本机未缓存凭证明文,请点击「轮换」重新签发一次;签发后将自动填入 Deployment 发布 curl。</p>
|
<p className="text-[11px] dg-text-yellow/90">{t('user.apiKey.cacheWarning')}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="rounded-lg bg-[var(--dg-bg-tertiary)]/60 p-4 space-y-3">
|
<div className="rounded-lg bg-[var(--dg-bg-tertiary)]/60 p-4 space-y-3">
|
||||||
<p className="text-xs font-medium text-[var(--dg-text-secondary)] flex items-center gap-1.5">
|
<p className="text-xs font-medium text-[var(--dg-text-secondary)] flex items-center gap-1.5">
|
||||||
<Rocket className="h-3.5 w-3.5 dg-text-purple" />调用示例
|
<Rocket className="h-3.5 w-3.5 dg-text-purple" />{t('user.apiKey.examples')}
|
||||||
</p>
|
</p>
|
||||||
<CodeSnippetBlock
|
<CodeSnippetBlock
|
||||||
label="更新镜像"
|
label={t('user.apiKey.updateImage')}
|
||||||
code={updateCurl}
|
code={updateCurl}
|
||||||
copyLabel="curl 已复制"
|
copyLabel={t('user.apiKey.curlCopied')}
|
||||||
copyDisabled={!copyableToken}
|
copyDisabled={!copyableToken}
|
||||||
copyDisabledTitle="请先签发访问凭证"
|
copyDisabledTitle={t('user.apiKey.copyDisabledTitle')}
|
||||||
/>
|
/>
|
||||||
<CodeSnippetBlock
|
<CodeSnippetBlock
|
||||||
label="滚动重启"
|
label={t('user.apiKey.restart')}
|
||||||
code={restartCurl}
|
code={restartCurl}
|
||||||
copyLabel="curl 已复制"
|
copyLabel={t('user.apiKey.curlCopied')}
|
||||||
copyDisabled={!copyableToken}
|
copyDisabled={!copyableToken}
|
||||||
copyDisabledTitle="请先签发访问凭证"
|
copyDisabledTitle={t('user.apiKey.copyDisabledTitle')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -384,24 +386,24 @@ export function ApiAccessKeySection() {
|
|||||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] sm:max-w-md">
|
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)]">
|
<DialogTitle className="text-[var(--dg-text-primary)]">
|
||||||
{formMode === 'edit' ? '调整凭证' : keyMeta?.has_token ? '轮换凭证' : '签发凭证'}
|
{formMode === 'edit' ? t('user.apiKey.adjustTitle') : keyMeta?.has_token ? t('user.apiKey.rotateTitle') : t('user.apiKey.issue')}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="text-xs text-[var(--dg-text-muted)]">
|
<DialogDescription className="text-xs text-[var(--dg-text-muted)]">
|
||||||
{formMode === 'edit' ? '修改用途说明与有效期限,凭证字符串不变。' : '填写用途并设定有效期,签发后请立即保存明文。'}
|
{formMode === 'edit' ? t('user.apiKey.editDesc') : t('user.apiKey.createDesc')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3 py-2">
|
<div className="space-y-3 py-2">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs text-[var(--dg-text-muted)]">用途说明</Label>
|
<Label className="text-xs text-[var(--dg-text-muted)]">{t('user.apiKey.descLabel')}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={formDescription}
|
value={formDescription}
|
||||||
onChange={e => setFormDescription(e.target.value)}
|
onChange={e => setFormDescription(e.target.value)}
|
||||||
placeholder="如 Jenkins 流水线、GitLab CI"
|
placeholder={t('user.apiKey.placeholderDesc')}
|
||||||
className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
className="mt-1 h-9 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs text-[var(--dg-text-muted)]">有效至</Label>
|
<Label className="text-xs text-[var(--dg-text-muted)]">{t('user.apiKey.validUntilLabel')}</Label>
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
value={formExpiresAt}
|
value={formExpiresAt}
|
||||||
@@ -411,10 +413,10 @@ export function ApiAccessKeySection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setFormOpen(false)} className="border-[var(--dg-border)]">取消</Button>
|
<Button variant="outline" onClick={() => setFormOpen(false)} className="border-[var(--dg-border)]">{t('common.cancel')}</Button>
|
||||||
<Button onClick={handleFormSubmit} disabled={actionLoading || !formExpiresAt}>
|
<Button onClick={handleFormSubmit} disabled={actionLoading || !formExpiresAt}>
|
||||||
{actionLoading ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
{actionLoading ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : null}
|
||||||
{formMode === 'edit' ? '保存' : keyMeta?.has_token ? '轮换' : '签发'}
|
{formMode === 'edit' ? t('common.save') : keyMeta?.has_token ? t('user.apiKey.rotateBtn') : t('user.apiKey.issue')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -423,19 +425,19 @@ export function ApiAccessKeySection() {
|
|||||||
<Dialog open={tokenDialogOpen} onOpenChange={setTokenDialogOpen}>
|
<Dialog open={tokenDialogOpen} onOpenChange={setTokenDialogOpen}>
|
||||||
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] sm:max-w-lg">
|
<DialogContent className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] sm:max-w-lg">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-[var(--dg-text-primary)]">请保存凭证明文</DialogTitle>
|
<DialogTitle className="text-[var(--dg-text-primary)]">{t('user.apiKey.saveTokenTitle')}</DialogTitle>
|
||||||
<DialogDescription className="text-xs text-[var(--dg-text-muted)]">
|
<DialogDescription className="text-xs text-[var(--dg-text-muted)]">
|
||||||
仅此一次机会查看完整字符串,关闭后无法再次获取。
|
{t('user.apiKey.saveTokenDesc')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<CodeSnippetBlock
|
<CodeSnippetBlock
|
||||||
label="访问凭证"
|
label={t('user.apiKey.accessTokenLabel')}
|
||||||
code={revealedToken}
|
code={revealedToken}
|
||||||
copyLabel="凭证已复制"
|
copyLabel={t('user.apiKey.credentialCopied')}
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button onClick={() => setTokenDialogOpen(false)}>我已保存</Button>
|
<Button onClick={() => setTokenDialogOpen(false)}>{t('user.apiKey.saved')}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -8018,62 +8018,64 @@
|
|||||||
"breadcrumbNav": "Breadcrumb Navigation"
|
"breadcrumbNav": "Breadcrumb Navigation"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"apiaccesskey.s1a2b3c4d": "CI/CD Access Credential",
|
"apiKey": {
|
||||||
"apiaccesskey.s2b3c4d5e": "Long-term",
|
"title": "CI/CD Access Credential",
|
||||||
"apiaccesskey.s3c4d5e6f": "Unique per user, auto-referenced by deployment scripts, does not expire with login session.",
|
"longTerm": "Long-term",
|
||||||
"apiaccesskey.s4d5e6f7g": "Issue Credential",
|
"desc": "Unique per user, auto-referenced by deployment scripts, does not expire with login session.",
|
||||||
"apiaccesskey.s5e6f7g8h": "Credential String",
|
"issue": "Issue Credential",
|
||||||
"apiaccesskey.s6f7g8h9i": "Hide",
|
"tokenLabel": "Credential String",
|
||||||
"apiaccesskey.s7g8h9i0j": "Show",
|
"hide": "Hide",
|
||||||
"apiaccesskey.s8h9i0j1k": "Edit",
|
"show": "Show",
|
||||||
"apiaccesskey.s9i0j1k2l": "Rotate",
|
"adjust": "Edit",
|
||||||
"apiaccesskey.s0j1k2l3m": "Revoke",
|
"rotate": "Rotate",
|
||||||
"apiaccesskey.s1k2l3m4n": "Description",
|
"revoke": "Revoke",
|
||||||
"apiaccesskey.s2l3m4n5o": "Not specified",
|
"descLabel": "Description",
|
||||||
"apiaccesskey.s3m4n5o6p": "Key ID",
|
"descEmpty": "Not specified",
|
||||||
"apiaccesskey.s4n5o6p7q": "Issued At",
|
"keyIdLabel": "Key ID",
|
||||||
"apiaccesskey.s5o6p7q8r": "Expired",
|
"createdLabel": "Issued At",
|
||||||
"apiaccesskey.s6p7q8r9s": "{{days}} days remaining",
|
"expired": "Expired",
|
||||||
"apiaccesskey.s7q8r9s0t": "Valid until",
|
"daysLeft": "{{days}} days remaining",
|
||||||
"apiaccesskey.s8r9s0t1u": "Valid until {{date}}",
|
"validity": "Valid until",
|
||||||
"apiaccesskey.s9s0t1u2v": "Last used {{date}}",
|
"validUntil": "Valid until {{date}}",
|
||||||
"apiaccesskey.s0t1u2v3w": "Prefix",
|
"lastUsed": "Last used {{date}}",
|
||||||
"apiaccesskey.s1u2v3w4x": "Needs renewal or rotation",
|
"prefix": "Prefix",
|
||||||
"apiaccesskey.s2v3w4x5y": "No CI/CD access credential issued yet",
|
"needRenew": "Needs renewal or rotation",
|
||||||
"apiaccesskey.s3w4x5y6z": "After issuing, use for Pipeline image updates, rolling restarts, etc.",
|
"emptyTitle": "No CI/CD access credential issued yet",
|
||||||
"apiaccesskey.s4x5y6z7a": "Issue Now",
|
"emptyDesc": "After issuing, use for Pipeline image updates, rolling restarts, etc.",
|
||||||
"apiaccesskey.s5y6z7a8b": "Credential plaintext not cached locally; click 'Rotate' to re-issue.",
|
"issueNow": "Issue Now",
|
||||||
"apiaccesskey.s6z7a8b9c": "Usage Examples",
|
"cacheWarning": "Credential plaintext not cached locally; click 'Rotate' to re-issue.",
|
||||||
"apiaccesskey.s7a8b9c0d": "Update Image",
|
"examples": "Usage Examples",
|
||||||
"apiaccesskey.s8b9c0d1e": "curl copied",
|
"updateImage": "Update Image",
|
||||||
"apiaccesskey.s9c0d1e2f": "Please issue a credential first",
|
"curlCopied": "curl copied",
|
||||||
"apiaccesskey.s0d1e2f3g": "Rolling Restart",
|
"copyDisabledTitle": "Please issue a credential first",
|
||||||
"apiaccesskey.s1e2f3g4h": "Credential info updated",
|
"restart": "Rolling Restart",
|
||||||
"apiaccesskey.s2f3g4h5i": "Update failed",
|
"updated": "Credential info updated",
|
||||||
"apiaccesskey.s3g4h5i6j": "Credential issued",
|
"updateFailed": "Update failed",
|
||||||
"apiaccesskey.s4h5i6j7k": "Saved to this browser, please copy now",
|
"issued": "Credential issued",
|
||||||
"apiaccesskey.s5i6j7k8l": "Issue failed",
|
"issuedDesc": "Saved to this browser, please copy now",
|
||||||
"apiaccesskey.s6j7k8l9m": "Revoke Credential",
|
"issueFailed": "Issue failed",
|
||||||
"apiaccesskey.s7k8l9m0n": "After revoking, all Pipeline/curl calls will fail. Continue?",
|
"revokeTitle": "Revoke Credential",
|
||||||
"apiaccesskey.s8l9m0n1o": "Revoke",
|
"revokeDesc": "After revoking, all Pipeline/curl calls will fail. Continue?",
|
||||||
"apiaccesskey.s9m0n1o2p": "Credential revoked",
|
"revokeBtn": "Revoke",
|
||||||
"apiaccesskey.s0n1o2p3q": "Revoke failed",
|
"revoked": "Credential revoked",
|
||||||
"apiaccesskey.s1o2p3q4r": "Rotate Credential",
|
"revokeFailed": "Revoke failed",
|
||||||
"apiaccesskey.s2p3q4r5s": "Old credential will be invalidated immediately. Continue?",
|
"rotateTitle": "Rotate Credential",
|
||||||
"apiaccesskey.s3q4r5s6t": "Rotate",
|
"rotateDesc": "Old credential will be invalidated immediately. Continue?",
|
||||||
"apiaccesskey.s4r5s6t7u": "Plaintext shown only once, please save carefully.",
|
"rotateBtn": "Rotate",
|
||||||
"apiaccesskey.s5s6t7u8v": "Edit Credential",
|
"issueDesc": "Plaintext shown only once, please save carefully.",
|
||||||
"apiaccesskey.s6t7u8v9w": "Update description and expiry; credential string unchanged.",
|
"adjustTitle": "Edit Credential",
|
||||||
"apiaccesskey.s7u8v9w0x": "Set description and expiry; save plaintext immediately after issuing.",
|
"editDesc": "Update description and expiry; credential string unchanged.",
|
||||||
"apiaccesskey.s8v9w0x1y": "e.g. Jenkins pipeline, GitLab CI",
|
"createDesc": "Set description and expiry; save plaintext immediately after issuing.",
|
||||||
"apiaccesskey.s9w0x1y2z": "Valid until",
|
"placeholderDesc": "e.g. Jenkins pipeline, GitLab CI",
|
||||||
"apiaccesskey.s0x1y2z3a": "Save",
|
"validUntilLabel": "Valid until",
|
||||||
"apiaccesskey.s1y2z3a4b": "Save Credential Plaintext",
|
"saveBtn": "Save",
|
||||||
"apiaccesskey.s2z3a4b5c": "This is the only chance to view the full string; cannot be retrieved after closing.",
|
"saveTokenTitle": "Save Credential Plaintext",
|
||||||
"apiaccesskey.s3a4b5c6d": "Access Credential",
|
"saveTokenDesc": "This is the only chance to view the full string; cannot be retrieved after closing.",
|
||||||
"apiaccesskey.s4b5c6d7e": "Credential copied",
|
"accessTokenLabel": "Access Credential",
|
||||||
"apiaccesskey.s5c6d7e8f": "I've saved it",
|
"credentialCopied": "Credential copied",
|
||||||
"apiaccesskey.s6d7e8f9g": "Your access credential"
|
"saved": "I've saved it",
|
||||||
|
"placeholderToken": "Your access credential"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logCenter": {
|
"logCenter": {
|
||||||
"saveQuery": "Save as Query",
|
"saveQuery": "Save as Query",
|
||||||
|
|||||||
@@ -8018,62 +8018,64 @@
|
|||||||
"breadcrumbNav": "面包屑导航"
|
"breadcrumbNav": "面包屑导航"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"apiaccesskey.s1a2b3c4d": "CI/CD 访问凭证",
|
"apiKey": {
|
||||||
"apiaccesskey.s2b3c4d5e": "长期有效",
|
"title": "CI/CD 访问凭证",
|
||||||
"apiaccesskey.s3c4d5e6f": "每人唯一凭证,Deployment 发布脚本自动引用,不随登录会话过期。",
|
"longTerm": "长期有效",
|
||||||
"apiaccesskey.s4d5e6f7g": "签发凭证",
|
"desc": "每人唯一凭证,Deployment 发布脚本自动引用,不随登录会话过期。",
|
||||||
"apiaccesskey.s5e6f7g8h": "凭证字符串",
|
"issue": "签发凭证",
|
||||||
"apiaccesskey.s6f7g8h9i": "隐藏",
|
"tokenLabel": "凭证字符串",
|
||||||
"apiaccesskey.s7g8h9i0j": "显示",
|
"hide": "隐藏",
|
||||||
"apiaccesskey.s8h9i0j1k": "调整",
|
"show": "显示",
|
||||||
"apiaccesskey.s9i0j1k2l": "轮换",
|
"adjust": "调整",
|
||||||
"apiaccesskey.s0j1k2l3m": "吊销",
|
"rotate": "轮换",
|
||||||
"apiaccesskey.s1k2l3m4n": "用途说明",
|
"revoke": "吊销",
|
||||||
"apiaccesskey.s2l3m4n5o": "未填写",
|
"descLabel": "用途说明",
|
||||||
"apiaccesskey.s3m4n5o6p": "凭证标识",
|
"descEmpty": "未填写",
|
||||||
"apiaccesskey.s4n5o6p7q": "签发时间",
|
"keyIdLabel": "凭证标识",
|
||||||
"apiaccesskey.s5o6p7q8r": "已过期",
|
"createdLabel": "签发时间",
|
||||||
"apiaccesskey.s6p7q8r9s": "剩余 {{days}} 天",
|
"expired": "已过期",
|
||||||
"apiaccesskey.s7q8r9s0t": "有效期",
|
"daysLeft": "剩余 {{days}} 天",
|
||||||
"apiaccesskey.s8r9s0t1u": "有效至 {{date}}",
|
"validity": "有效期",
|
||||||
"apiaccesskey.s9s0t1u2v": "最近调用 {{date}}",
|
"validUntil": "有效至 {{date}}",
|
||||||
"apiaccesskey.s0t1u2v3w": "前缀",
|
"lastUsed": "最近调用 {{date}}",
|
||||||
"apiaccesskey.s1u2v3w4x": "需续期或轮换",
|
"prefix": "前缀",
|
||||||
"apiaccesskey.s2v3w4x5y": "尚未签发 CI/CD 访问凭证",
|
"needRenew": "需续期或轮换",
|
||||||
"apiaccesskey.s3w4x5y6z": "签发后可用于 Pipeline 触发镜像更新、滚动重启等操作",
|
"emptyTitle": "尚未签发 CI/CD 访问凭证",
|
||||||
"apiaccesskey.s4x5y6z7a": "立即签发",
|
"emptyDesc": "签发后可用于 Pipeline 触发镜像更新、滚动重启等操作",
|
||||||
"apiaccesskey.s5y6z7a8b": "本机未缓存凭证明文,请点击「轮换」重新签发一次;签发后将自动填入 Deployment 发布 curl。",
|
"issueNow": "立即签发",
|
||||||
"apiaccesskey.s6z7a8b9c": "调用示例",
|
"cacheWarning": "本机未缓存凭证明文,请点击「轮换」重新签发一次;签发后将自动填入 Deployment 发布 curl。",
|
||||||
"apiaccesskey.s7a8b9c0d": "更新镜像",
|
"examples": "调用示例",
|
||||||
"apiaccesskey.s8b9c0d1e": "curl 已复制",
|
"updateImage": "更新镜像",
|
||||||
"apiaccesskey.s9c0d1e2f": "请先签发访问凭证",
|
"curlCopied": "curl 已复制",
|
||||||
"apiaccesskey.s0d1e2f3g": "滚动重启",
|
"copyDisabledTitle": "请先签发访问凭证",
|
||||||
"apiaccesskey.s1e2f3g4h": "凭证信息已更新",
|
"restart": "滚动重启",
|
||||||
"apiaccesskey.s2f3g4h5i": "更新失败",
|
"updated": "凭证信息已更新",
|
||||||
"apiaccesskey.s3g4h5i6j": "凭证已签发",
|
"updateFailed": "更新失败",
|
||||||
"apiaccesskey.s4h5i6j7k": "已保存至本浏览器,请立即复制",
|
"issued": "凭证已签发",
|
||||||
"apiaccesskey.s5i6j7k8l": "签发失败",
|
"issuedDesc": "已保存至本浏览器,请立即复制",
|
||||||
"apiaccesskey.s6j7k8l9m": "吊销凭证",
|
"issueFailed": "签发失败",
|
||||||
"apiaccesskey.s7k8l9m0n": "吊销后 Pipeline / curl 调用将全部失败,是否继续?",
|
"revokeTitle": "吊销凭证",
|
||||||
"apiaccesskey.s8l9m0n1o": "吊销",
|
"revokeDesc": "吊销后 Pipeline / curl 调用将全部失败,是否继续?",
|
||||||
"apiaccesskey.s9m0n1o2p": "凭证已吊销",
|
"revokeBtn": "吊销",
|
||||||
"apiaccesskey.s0n1o2p3q": "吊销失败",
|
"revoked": "凭证已吊销",
|
||||||
"apiaccesskey.s1o2p3q4r": "轮换凭证",
|
"revokeFailed": "吊销失败",
|
||||||
"apiaccesskey.s2p3q4r5s": "旧凭证将立即作废,是否继续?",
|
"rotateTitle": "轮换凭证",
|
||||||
"apiaccesskey.s3q4r5s6t": "轮换",
|
"rotateDesc": "旧凭证将立即作废,是否继续?",
|
||||||
"apiaccesskey.s4r5s6t7u": "凭证明文仅显示一次,请妥善保存。",
|
"rotateBtn": "轮换",
|
||||||
"apiaccesskey.s5s6t7u8v": "调整凭证",
|
"issueDesc": "凭证明文仅显示一次,请妥善保存。",
|
||||||
"apiaccesskey.s6t7u8v9w": "修改用途说明与有效期限,凭证字符串不变。",
|
"adjustTitle": "调整凭证",
|
||||||
"apiaccesskey.s7u8v9w0x": "填写用途并设定有效期,签发后请立即保存明文。",
|
"editDesc": "修改用途说明与有效期限,凭证字符串不变。",
|
||||||
"apiaccesskey.s8v9w0x1y": "如 Jenkins 流水线、GitLab CI",
|
"createDesc": "填写用途并设定有效期,签发后请立即保存明文。",
|
||||||
"apiaccesskey.s9w0x1y2z": "有效至",
|
"placeholderDesc": "如 Jenkins 流水线、GitLab CI",
|
||||||
"apiaccesskey.s0x1y2z3a": "保存",
|
"validUntilLabel": "有效至",
|
||||||
"apiaccesskey.s1y2z3a4b": "请保存凭证明文",
|
"saveBtn": "保存",
|
||||||
"apiaccesskey.s2z3a4b5c": "仅此一次机会查看完整字符串,关闭后无法再次获取。",
|
"saveTokenTitle": "请保存凭证明文",
|
||||||
"apiaccesskey.s3a4b5c6d": "访问凭证",
|
"saveTokenDesc": "仅此一次机会查看完整字符串,关闭后无法再次获取。",
|
||||||
"apiaccesskey.s4b5c6d7e": "凭证已复制",
|
"accessTokenLabel": "访问凭证",
|
||||||
"apiaccesskey.s5c6d7e8f": "我已保存",
|
"credentialCopied": "凭证已复制",
|
||||||
"apiaccesskey.s6d7e8f9g": "你的访问凭证"
|
"saved": "我已保存",
|
||||||
|
"placeholderToken": "你的访问凭证"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"logCenter": {
|
"logCenter": {
|
||||||
"saveQuery": "另存为查询",
|
"saveQuery": "另存为查询",
|
||||||
|
|||||||
Reference in new issue
Block a user