批量转换 163 个文件,3204 处中文替换为 t() 调用: - Monitor: 日志中心/告警中心/数据源/大屏/站点拨测/SNMP/中间件 全部转换 - Kubernetes: ResourceVisualEditor/NamespaceResourcePage/拓扑/事件 全部转换 - Deploy: 流水线/发布/凭证/RBAC 全部转换 - Server/Cloud/Agent/Task/Layout/UI: 全部转换 - 翻译词典扩展至 6474 行 (22 namespaces, 5000+ keys) - tsc 编译零错误
150 lines
7.2 KiB
TypeScript
150 lines
7.2 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Plus, Key, Globe, Container } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { credentialApi, deployApi } from '@/api';
|
|
import type { JsonData } from '@/types/common';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
interface Repo {
|
|
id: number; name: string; url: string; credential_id: number | null;
|
|
}
|
|
interface Registry {
|
|
id: number; name: string; registry_url: string; namespace: string; credential_id: number | null;
|
|
}
|
|
interface Credential {
|
|
id: number; name: string; secret_type: string; summary: string;
|
|
}
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onOpenChange: (v: boolean) => void;
|
|
type: 'repo' | 'registry';
|
|
editId: number | null;
|
|
repos: Repo[];
|
|
registries: Registry[];
|
|
onSave: () => void;
|
|
}
|
|
|
|
export default function CredentialFormDialog({ open, onOpenChange, type, editId, repos, registries, onSave }: Props) {
|
|
const { t } = useTranslation();
|
|
const isRepo = type === 'repo';
|
|
const [name, setName] = useState('');
|
|
const [desc, setDesc] = useState('');
|
|
const [url, setUrl] = useState('');
|
|
const [namespace, setNamespace] = useState('');
|
|
const [credentialId, setCredentialId] = useState<number | string>('');
|
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
// Docker 仓库 → docker 类型;Git 仓库 → api 或 docker 类型
|
|
const filteredCredentials = credentials.filter(c =>
|
|
isRepo ? true : c.secret_type === 'docker'
|
|
);
|
|
|
|
const loadCredentials = async () => {
|
|
try {
|
|
const res: JsonData = await credentialApi.list({ page_size: 100 });
|
|
setCredentials(res?.list || []);
|
|
} catch { /* silent */ }
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadCredentials();
|
|
if (editId) {
|
|
if (isRepo) {
|
|
const r = repos.find(x => x.id === editId);
|
|
if (r) { setName(r.name); setDesc(''); setUrl(r.url); setCredentialId(r.credential_id || ''); }
|
|
} else {
|
|
const r = registries.find(x => x.id === editId);
|
|
if (r) { setName(r.name); setDesc(''); setUrl(r.registry_url); setNamespace(r.namespace || ''); setCredentialId(r.credential_id || ''); }
|
|
}
|
|
} else {
|
|
setName(''); setDesc(''); setUrl(''); setNamespace(''); setCredentialId('');
|
|
}
|
|
}, [editId, open, isRepo]);
|
|
|
|
const handleSave = async () => {
|
|
if (!name.trim() || !url.trim()) { toast.error(t('deploy.form.nameAddrRequired')); return; }
|
|
setSaving(true);
|
|
try {
|
|
const cid = credentialId ? Number(credentialId) : null;
|
|
const payload = isRepo
|
|
? { name: name.trim(), description: desc.trim(), url: url.trim(), credential_id: cid }
|
|
: { name: name.trim(), description: desc.trim(), registry_url: url.trim(), namespace: namespace.trim(), credential_id: cid };
|
|
if (editId) {
|
|
if (isRepo) await deployApi.updateRepo(editId, payload);
|
|
else await deployApi.updateRegistry(editId, payload);
|
|
toast.success(t('deploy.toast.updated'));
|
|
} else {
|
|
if (isRepo) await deployApi.createRepo(payload);
|
|
else await deployApi.createRegistry(payload);
|
|
toast.success(t('deploy.toast.created'));
|
|
}
|
|
onOpenChange(false);
|
|
onSave();
|
|
} catch { toast.error(t('common.saveFailed')); }
|
|
finally { setSaving(false); }
|
|
};
|
|
|
|
const CredIcon = isRepo ? Globe : Container;
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-[480px] bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-[var(--dg-text-primary)] flex items-center gap-2">
|
|
<CredIcon className="h-5 w-5 text-[var(--dg-blue)]" />
|
|
{editId ? t('common.edit') : t('deploy.credentialformdialog.s66ab5e')}{isRepo ? t('deploy.credentialformdialog.s0b7cb4') : t('deploy.credentialformdialog.s7f34a8')}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-2">
|
|
<div className="space-y-1"><Label>{t('deploy.form.nameStar')}</Label>
|
|
<Input value={name} onChange={e => setName(e.target.value)} placeholder={isRepo ? t('deploy.credentialformdialog.se9548a') : t('deploy.credentialformdialog.sa4d9ef')} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
|
|
<div className="space-y-1"><Label>{isRepo ? t('deploy.form.repoUrlStar') : 'Registry URL *'}</Label>
|
|
<Input value={url} onChange={e => setUrl(e.target.value)} placeholder={isRepo ? 'http://10.10.10.72:9000/group/repo.git' : 'http://10.10.10.72'} className="font-mono text-sm bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
|
|
{!isRepo && <div className="space-y-1"><Label>{t('deploy.form.defaultNamespace')}</Label>
|
|
<Input value={namespace} onChange={e => setNamespace(e.target.value)} placeholder=t('deploy.credentialformdialog.s2a8bb1') className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>}
|
|
|
|
{/* 关联凭证 */}
|
|
<div className="space-y-1">
|
|
<Label>{t('deploy.cred.linkedCred')} <span className="text-xs text-[var(--dg-text-secondary)] opacity-70">{t('deploy.credentialformdialog.s30220a')}</span></Label>
|
|
<Select value={String(credentialId)} onValueChange={v => setCredentialId(v === '__none' ? '' : v)}>
|
|
<SelectTrigger className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
|
|
<SelectValue placeholder={isRepo ? t('deploy.cred.selectApiCred') : t('deploy.cred.selectDockerPwd')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none">{t('deploy.cred.notLinkedCred')}</SelectItem>
|
|
{filteredCredentials.map(c => (
|
|
<SelectItem key={c.id} value={String(c.id)}>
|
|
<span className="flex items-center gap-1.5">
|
|
<Key className="h-3 w-3 opacity-60" />
|
|
{c.name} {c.summary && <span className="text-[10px] opacity-50">({c.summary})</span>}
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
{filteredCredentials.length === 0 && (
|
|
<p className="text-xs dg-text-yellow flex items-center gap-1 mt-1">
|
|
<Plus className="h-3 w-3" />
|
|
暂无{isRepo ? '' : t('pipeline.dockerPassword')}类型凭证,请在凭证配置页面新增
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => onOpenChange(false)}>{t('common.cancel')}</Button>
|
|
<Button onClick={handleSave} disabled={saving}>{saving ? i18n.t('k8s.topology.saving') : i18n.t('common.save')}</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|