Files
aiops/frontend/src/components/deploy/CredentialFormDialog.tsx
T
Logan.Li 7fb2236b4d refactor(frontend): P0 API 层收口与 http 统一
- 统一 http.ts baseURL:get/post/download/upload 均走 resolveApiUrl
- agentUtils 下沉至 lib/agent-utils,API 层移除对 components 的依赖
- 扩展 metricsApi/snmpApi/prometheusProxyApi/zabbixProxyApi
- 扩展 pipelineApi/credentialApi 覆盖流水线与凭证场景
- 14 个组件/页面禁止直连 http,统一走 @/services/api
2026-06-25 23:49:46 +08:00

147 lines
6.7 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 '@/services/api';
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 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: Record<string, any> = 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('请填写名称和地址'); 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('已更新');
} else {
if (isRepo) await deployApi.createRepo(payload);
else await deployApi.createRegistry(payload);
toast.success('已创建');
}
onOpenChange(false);
onSave();
} catch { toast.error('保存失败'); }
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 ? '编辑' : '新增'}{isRepo ? ' Git 仓库' : ' 镜像仓库'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-1"><Label>名称 *</Label>
<Input value={name} onChange={e => setName(e.target.value)} placeholder={isRepo ? '如:GitLab CE' : '如:Harbor'} className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>
<div className="space-y-1"><Label>{isRepo ? '仓库 URL *' : '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>默认命名空间</Label>
<Input value={namespace} onChange={e => setNamespace(e.target.value)} placeholder="如:aiops" className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /></div>}
{/* 关联凭证 */}
<div className="space-y-1">
<Label>关联凭证 <span className="text-xs text-[var(--dg-text-secondary)] opacity-70">(认证由凭据管理统一配置)</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 ? '选择凭据(API/密码等)' : '选择 Docker 密码凭据'} />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">不关联凭证</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 text-amber-400 flex items-center gap-1 mt-1">
<Plus className="h-3 w-3" />
暂无{isRepo ? '' : 'Docker 密码'}类型凭证,请在凭证配置页面新增
</p>
)}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>取消</Button>
<Button onClick={handleSave} disabled={saving}>{saving ? '保存中...' : '保存'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}