/* eslint-disable @typescript-eslint/no-explicit-any */ // IngressEditor — Ingress Host/Path 路由规则可视化编辑器 (从 ResourceVisualEditor.tsx 拆分) import { useState, useEffect } from 'react'; import { Plus, Trash2, Loader2, Save } from 'lucide-react'; 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 { kubernetesApi } from '@/api'; import i18n from '@/i18n'; export interface IngressEditorProps { manifest: any; clusterCode: string; onSave: (m: any) => void; saving: boolean; isCreate?: boolean; } export function IngressEditor({ manifest, clusterCode, onSave, saving, isCreate }: IngressEditorProps) { const spec = manifest?.spec || {}; const [resName, setResName] = useState(manifest?.metadata?.name || ''); const [ingressClass, setIngressClass] = useState(spec.ingressClassName || 'nginx'); // IngressClass 列表(关联集群中的 IngressController) const [ingressClasses, setIngressClasses] = useState>([]); const [loadingClasses, setLoadingClasses] = useState(false); // 加载集群 IngressClass 列表 useEffect(() => { if (!clusterCode) return; setLoadingClasses(true); kubernetesApi.getIngressClasses(clusterCode).then((res: any) => { const list = Array.isArray(res) ? res : (res?.list || res || []); setIngressClasses(list); // 新建时如果还没指定且有列表,尝试默认选第一个 if (isCreate && (!spec.ingressClassName || spec.ingressClassName === 'nginx') && list.length > 0) { // 优先选 nginx 或第一个 const prefer = list.find((c: any) => c.name?.toLowerCase().includes('nginx')) || list[0]; if (prefer?.name) setIngressClass(prefer.name); } }).catch(() => {}).finally(() => setLoadingClasses(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [clusterCode]); const [rules, setRules] = useState<{ host: string; paths: { path: string; pathType: string; serviceName: string; servicePort: string }[] }[]>( (spec.rules || []).map(( r: any) => ({ host: r.host || '', paths: (r.http?.backend?.servicePort != null ? // Old-style single backend [{ path: '/', pathType: 'Prefix', serviceName: r.http?.backend?.serviceName || '', servicePort: String(r.http?.backend?.servicePort || '80') }] : (r.http?.paths || []).map(( p: any) => ({ path: p.path || '/', pathType: p.pathType || 'Prefix', serviceName: p.backend?.service?.name || p.backend?.serviceName || '', servicePort: String(p.backend?.service?.port?.number ?? p.backend?.servicePort ?? '80'), })) ), })) ); const updateRule = (ri: number, host: string) => { const next = [...rules]; next[ri] = { ...next[ri], host }; setRules(next); }; const addRule = () => setRules([...rules, { host: '', paths: [{ path: '/', pathType: 'Prefix', serviceName: '', servicePort: '80' }] }]); const removeRule = (ri: number) => setRules(rules.filter((_, idx) => idx !== ri)); const updatePath = (ri: number, pi: number, field: string, val: string) => { const next = [...rules]; next[ri].paths[pi] = { ...next[ri].paths[pi], [field]: val }; setRules(next); }; const addPath = (ri: number) => { const next = [...rules]; next[ri].paths.push({ path: '/', pathType: 'Prefix', serviceName: '', servicePort: '80' }); setRules(next); }; const removePath = (ri: number, pi: number) => { const next = [...rules]; next[ri].paths = next[ri].paths.filter((_, idx) => idx !== pi); setRules(next); }; const handleSave = () => { const newRules = rules.map(r => ({ host: r.host, http: { paths: r.paths.map(p => ({ path: p.path, pathType: p.pathType, backend: { service: { name: p.serviceName, port: { number: parseInt(p.servicePort) || 80 } } }, })), }, })); const newSpec: Record = { ...spec, rules: newRules }; if (ingressClass.trim()) newSpec.ingressClassName = ingressClass.trim(); else delete newSpec.ingressClassName; onSave({ ...manifest, metadata: { ...manifest.metadata, name: resName.trim() }, spec: newSpec }); }; return (
setResName(e.target.value)} readOnly={!isCreate} className="mt-1 font-mono text-xs bg-[var(--dg-bg-tertiary)]" />
{/* IngressClass 下拉 + 手工输入支持 // Logan.Li */}
{/* 总是显示输入框支持手工输入或修改 */}
手工输入: setIngressClass(e.target.value)} placeholder="alb / nginx / my-ingress-class" className="flex-1 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)]" />
{/* 帮助提示 */}
您的集群中可能有多个 IngressController,您可在此指定当前 Ingress 对应哪个 IngressController。
从 IngressClass 列表选择,或手工输入名称。
{/* 常见示例 */} {(ingressClasses.length === 0 || ingressClass) && (
常见:alb → ingress.k8s.alibabacloud/alb    nginx → k8s.io/ingress-nginx
)}
{rules.map((rule, ri) => (
updateRule(ri, e.target.value)} placeholder={i18n.t('k8s.rve.hostPlaceholder')} className="flex-1 min-w-0 font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" title={rule.host} />
{rule.paths.map((p, pi) => (
updatePath(ri, pi, 'path', e.target.value)} placeholder={i18n.t('k8s.ingress.pathPlaceholder')} className="font-mono text-xs h-7 min-w-0 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /> updatePath(ri, pi, 'serviceName', e.target.value)} placeholder="Service" className="font-mono text-xs h-7 min-w-0 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" /> updatePath(ri, pi, 'servicePort', e.target.value)} placeholder={i18n.t('system.ldapPort')} type="number" className="font-mono text-xs h-7 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" />
))}
))} {rules.length === 0 &&
{i18n.t('common.name')}
}
); }