使用 i18n.t() 全局方式(避免 hook 定位问题): - 288 目标文件中 135 个成功转换 - 2228 处中文替换为 i18n.t() 调用 - 1006 个新翻译 key 添加到 zh-CN.json + en-US.json - 词典总量: 3554 keys (22 namespaces) - tsc 零错误 + Vite 构建成功 - 6 个含代码示例/数据字符串的文件安全回退
181 lines
11 KiB
TypeScript
181 lines
11 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Key, Lock, Eye, EyeOff, Copy,
|
|
CheckCircle2, XCircle, AlertTriangle,
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
|
import { toast } from 'sonner';
|
|
import { useCredentialDetail } from '@/hooks/api';
|
|
import type { SecretType } from './types';
|
|
import { TYPE_CONFIG, TYPE_BADGE_CLASS } from './constants';
|
|
import i18n from '@/i18n';
|
|
|
|
export interface CredDetailDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
credId: number | null;
|
|
}
|
|
|
|
// 凭据详情弹窗 // Logan.Li
|
|
export function CredDetailDialog({ open, onClose, credId }: CredDetailDialogProps) {
|
|
const [showMap, setShowMap] = useState<Record<string, boolean>>({});
|
|
const { data: detail, isLoading: loading } = useCredentialDetail(credId, open);
|
|
|
|
if (!open || !credId) return null;
|
|
|
|
const toggleShow = (key: string) => setShowMap(prev => ({ ...prev, [key]: !prev[key] }));
|
|
const copyValue = (val: string) => { navigator.clipboard.writeText(val); toast.success(i18n.t('aiops.agentCliCopied')); };
|
|
|
|
const st = detail?.secret_type as SecretType;
|
|
const cfg = TYPE_CONFIG[st];
|
|
const content = (detail as Record<string, unknown>)?.content as Record<string, unknown> || {};
|
|
|
|
const Field = ({ label, value, sensitive, fieldKey }: { label: string; value: unknown; sensitive?: boolean; fieldKey?: string }) => {
|
|
if (value === undefined || value === null || value === '') return null;
|
|
const isLong = typeof value === 'string' && value.length > 80;
|
|
const show = fieldKey ? showMap[fieldKey] : false;
|
|
return (
|
|
<div className="mb-3">
|
|
<div className="flex items-center gap-2 text-xs text-[var(--dg-text-muted)] mb-1">
|
|
<Lock className="h-3 w-3" />{label}
|
|
</div>
|
|
<div className="flex gap-1">
|
|
{isLong ? (
|
|
<textarea readOnly value={sensitive && !show ? '••••••••' : String(value)}
|
|
className="flex-1 rounded-md bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] text-[var(--dg-text-primary)] p-2 text-xs font-mono resize-none"
|
|
rows={4} />
|
|
) : (
|
|
<Input readOnly value={sensitive && !show ? '••••••••' : String(value)}
|
|
className="flex-1 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] font-mono text-xs" />
|
|
)}
|
|
<div className="flex flex-col gap-0.5 shrink-0">
|
|
{sensitive && (
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => toggleShow(fieldKey!)}>
|
|
{show ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
</Button>
|
|
)}
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => copyValue(String(value))}>
|
|
<Copy className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const detailRecord = detail as Record<string, unknown> | undefined;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onClose}>
|
|
<DialogContent className="sm:max-w-[680px] max-h-[85vh] overflow-y-auto bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
{cfg ? <cfg.icon className={`h-5 w-5 ${cfg.color}`} /> : <Key className="h-5 w-5 text-[var(--dg-text-muted)]" />}
|
|
凭据详情
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
{loading ? (
|
|
<div className="text-center py-8 text-[var(--dg-text-muted)]"> {i18n.t('monitor.logPanel.logResults.loading')} </div>
|
|
) : detail ? (
|
|
<div className="space-y-4">
|
|
{/* 基本信息 // Logan.Li */}
|
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s5b478615')} </span><span className="text-[var(--dg-text-primary)] font-medium">{detailRecord?.name as string}</span></div>
|
|
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.se91f5a13')} </span>
|
|
<Badge variant="outline" className={`text-xs ml-1 ${TYPE_BADGE_CLASS[st] || ''}`}>{cfg?.label || st}</Badge>
|
|
</div>
|
|
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('approval.detail.statusLabel')} </span>
|
|
{detailRecord?.is_active
|
|
? <Badge className="bg-green-500/20 dg-text-green text-xs ml-1"><CheckCircle2 className="h-3 w-3 mr-0.5" /> {i18n.t('system.userActive')} </Badge>
|
|
: <Badge className="bg-red-500/20 dg-text-red text-xs ml-1"><XCircle className="h-3 w-3 mr-0.5" /> {i18n.t('system.userInactive')} </Badge>}
|
|
{detailRecord?.expires_at && new Date(detailRecord.expires_at as string) < new Date() && (
|
|
<Badge className="bg-yellow-500/20 dg-text-yellow text-xs ml-1"><AlertTriangle className="h-3 w-3 mr-0.5" /> {i18n.t('system.expired2')} </Badge>
|
|
)}
|
|
</div>
|
|
<div className="col-span-2 cursor-pointer" onClick={() => copyValue((detailRecord?.env_prefix as string) || '')}>
|
|
<span className="text-[var(--dg-text-muted)]"> {i18n.t('deploy.cred.refIdLabel')} </span>
|
|
<code className="text-[var(--dg-blue)] font-mono text-xs">{detailRecord?.env_prefix as string || '无'}</code>
|
|
<Copy className="h-3 w-3 inline ml-1 text-[var(--dg-text-muted)]" />
|
|
</div>
|
|
{(detailRecord?.tags as string[])?.length > 0 && (
|
|
<div className="col-span-2">
|
|
<span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s1655dd5f')} </span>
|
|
{(detailRecord?.tags as string[]).map((t: string) => (
|
|
<Badge key={t} variant="outline" className="text-xs ml-1 border-[var(--dg-border)]">{t}</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
<div className="col-span-2"><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s4a4e3b5a')} </span><span className="text-[var(--dg-text-secondary)]">{(detailRecord?.description as string) || '无'}</span></div>
|
|
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s76d768fb')} </span><span className="text-[var(--dg-text-secondary)] text-xs">{new Date(detailRecord?.created_at as string).toLocaleString()}</span></div>
|
|
<div><span className="text-[var(--dg-text-muted)]"> {i18n.t('pipeline.creddetaildialo.s3820b7e1')} </span><span className="text-[var(--dg-text-secondary)] text-xs">{new Date(detailRecord?.updated_at as string).toLocaleString()}</span></div>
|
|
</div>
|
|
|
|
{/* 密钥数据 // Logan.Li */}
|
|
<div className="border-t border-[var(--dg-border)] pt-3">
|
|
<div className="flex items-center gap-2 text-sm text-[var(--dg-text-primary)] font-medium mb-3">
|
|
<Key className="h-4 w-4 text-yellow-500" />密钥数据
|
|
</div>
|
|
<div className="p-3 rounded-md bg-yellow-500/5 border border-yellow-500/20 mb-3">
|
|
<p className="text-xs dg-text-yellow/80"> {i18n.t('pipeline.creddetaildialo.s03b218d4')} </p>
|
|
</div>
|
|
|
|
{st === 'ssh' && <>
|
|
<Field label={i18n.t('pipeline.creddetaildialo.s2272f5e9')} value={content.ssh_user} />
|
|
<Field label={i18n.t('pipeline.creddetaildialo.s3f284a59')} value={content.ssh_port} />
|
|
<Field label={i18n.t('system.sshPassword')} value={content.password} sensitive fieldKey="ssh_password" />
|
|
<Field label={i18n.t('system.sshPrivateKey')} value={content.private_key} sensitive fieldKey="ssh_private_key" />
|
|
<Field label={i18n.t('system.sshPublicKey')} value={content.public_key} fieldKey="ssh_public_key" />
|
|
</>}
|
|
{st === 'docker' && <>
|
|
<Field label={i18n.t('deploy.form.registryAddr')} value={content.server} />
|
|
<Field label={i18n.t('system.userName')} value={content.username} />
|
|
<Field label={i18n.t('system.passwordLabel')} value={content.password} sensitive fieldKey="docker_password" />
|
|
</>}
|
|
{st === 'tls' && <>
|
|
<Field label={i18n.t('pipeline.creddetaildialo.sd40b5115')} value={content.cert} sensitive fieldKey="tls_cert" />
|
|
<Field label={i18n.t('system.sshPrivateKey')} value={content.key} sensitive fieldKey="tls_key" />
|
|
<Field label={i18n.t('pipeline.credformdialogt.sa24356be')} value={content.ca} sensitive fieldKey="tls_ca" />
|
|
</>}
|
|
{st === 'aksk' && <>
|
|
<Field label="Access Key ID" value={content.access_key} />
|
|
<Field label="Secret Access Key" value={content.secret_key} sensitive fieldKey="aksk_secret_key" />
|
|
</>}
|
|
{st === 'database' && <>
|
|
<Field label={i18n.t('pipeline.credformdialogt.s84b916da')} value={String(content.db_type || '').toUpperCase()} />
|
|
<Field label={i18n.t('agent.agentHost')} value={content.host} />
|
|
<Field label={i18n.t('system.ldapPort')} value={content.port} />
|
|
<Field label={i18n.t('pipeline.credformdialogt.s5ccbbd01')} value={content.db_name} />
|
|
<Field label={i18n.t('system.userName')} value={content.username} />
|
|
<Field label={i18n.t('system.passwordLabel')} value={content.password} sensitive fieldKey="db_password" />
|
|
</>}
|
|
{st === 'api' && <>
|
|
<Field label="API Key" value={content.api_key} sensitive fieldKey="api_key" />
|
|
<Field label={i18n.t('pipeline.creddetaildialo.s48ec3494')} value={content.endpoint} />
|
|
</>}
|
|
{st === 'generic' && <>
|
|
<Field label="Key" value={content.generic_key} />
|
|
<Field label="Value" value={content.generic_value} sensitive fieldKey="generic_value" />
|
|
</>}
|
|
</div>
|
|
|
|
{/* 引用提示 // Logan.Li */}
|
|
<div className="border-t border-[var(--dg-border)] pt-3">
|
|
<div className="text-xs text-[var(--dg-text-muted)] mb-2"> {i18n.t('deploy.cred.refHint')} </div>
|
|
<code className="text-xs bg-[var(--dg-bg-tertiary)] px-3 py-1.5 rounded block text-[var(--dg-blue)]">
|
|
{'${'}{String(detailRecord?.variable_name || String(detailRecord?.name || '').toUpperCase().replace(/[^A-Z0-9]/g, '_'))}{'}'}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-[var(--dg-text-muted)]"> {i18n.t('deploy.cred.noCredData')} </div>
|
|
)}
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose}> {i18n.t('agent.portClosed')} </Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
} |