fix: YAML查看器按钮文案 '清理并编辑' → '编辑',统一简洁命名
This commit is contained in:
1 parent
a8b4af4409
commit
4bd9fa629b
14 files changed
+2146
-400
No files matched your search
@@ -0,0 +1,632 @@
|
||||
// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8)
|
||||
// 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / DNS / 安全上下文 / 终止宽限期 / 调度器 / SA
|
||||
// 作者: Logan <admin@attacker.club>
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Save, Loader2, Settings, RefreshCw, Server, Shield, Clock, Network, Plus, Trash2, Globe, MapPin,
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { kubernetesApi } from '@/services/api';
|
||||
|
||||
interface Props {
|
||||
clusterCode: string;
|
||||
namespace: string;
|
||||
deploymentName: string;
|
||||
manifest: any | null;
|
||||
onSaved: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
interface Toleration {
|
||||
key: string;
|
||||
operator: string; // Equal | Exists
|
||||
value: string;
|
||||
effect: string; // NoSchedule | PreferNoSchedule | NoExecute
|
||||
tolerationSeconds: string;
|
||||
}
|
||||
|
||||
export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: Props) {
|
||||
const { toast } = useToast();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [loading, setLoading] = useState(!manifest);
|
||||
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
|
||||
|
||||
// 更新策略
|
||||
const [strategyType, setStrategyType] = useState<'RollingUpdate' | 'Recreate'>('RollingUpdate');
|
||||
const [maxSurge, setMaxSurge] = useState('25%');
|
||||
const [maxUnavailable, setMaxUnavailable] = useState('25%');
|
||||
|
||||
// 历史/进度
|
||||
const [revisionHistoryLimit, setRevisionHistoryLimit] = useState('10');
|
||||
const [progressDeadlineSeconds, setProgressDeadlineSeconds] = useState('600');
|
||||
|
||||
// Pod 级别
|
||||
const [nodeSelectorKv, setNodeSelectorKv] = useState<Array<{ key: string; value: string }>>([]);
|
||||
const [tolerations, setTolerations] = useState<Toleration[]>([]);
|
||||
const [dnsPolicy, setDnsPolicy] = useState('ClusterFirst');
|
||||
const [restartPolicy, setRestartPolicy] = useState('Always');
|
||||
const [schedulerName, setSchedulerName] = useState('default-scheduler');
|
||||
const [terminationGracePeriodSeconds, setTerminationGracePeriodSeconds] = useState('30');
|
||||
const [serviceAccountName, setServiceAccountName] = useState('');
|
||||
|
||||
// Pod 安全上下文
|
||||
const [podFsGroup, setPodFsGroup] = useState('');
|
||||
const [podRunAsUser, setPodRunAsUser] = useState('');
|
||||
const [podRunAsGroup, setPodRunAsGroup] = useState('');
|
||||
const [podRunAsNonRoot, setPodRunAsNonRoot] = useState(false);
|
||||
|
||||
// 主机设置(M8 补全)
|
||||
const [hostNetwork, setHostNetwork] = useState(false);
|
||||
const [hostIPC, setHostIPC] = useState(false);
|
||||
const [hostPID, setHostPID] = useState(false);
|
||||
const [shareProcessNamespace, setShareProcessNamespace] = useState(false);
|
||||
|
||||
// hostAliases(M8 补全)
|
||||
const [hostAliases, setHostAliases] = useState<Array<{ ip: string; hostnames: string }>>([]);
|
||||
|
||||
// dnsConfig(M8 补全)
|
||||
const [dnsNameservers, setDnsNameservers] = useState('');
|
||||
const [dnsSearches, setDnsSearches] = useState('');
|
||||
const [dnsOptions, setDnsOptions] = useState<Array<{ name: string; value: string }>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const m = manifest;
|
||||
if (!m) { setLoading(false); return; }
|
||||
setRawManifest(m);
|
||||
const spec = m?.spec || {};
|
||||
const podSpec = spec?.template?.spec || {};
|
||||
|
||||
// 更新策略
|
||||
const strat = spec.strategy || {};
|
||||
setStrategyType(strat.type || 'RollingUpdate');
|
||||
const ru = strat.rollingUpdate || {};
|
||||
setMaxSurge(String(ru.maxSurge ?? '25%'));
|
||||
setMaxUnavailable(String(ru.maxUnavailable ?? '25%'));
|
||||
|
||||
// 历史/进度
|
||||
setRevisionHistoryLimit(spec.revisionHistoryLimit != null ? String(spec.revisionHistoryLimit) : '10');
|
||||
setProgressDeadlineSeconds(spec.progressDeadlineSeconds != null ? String(spec.progressDeadlineSeconds) : '600');
|
||||
|
||||
// nodeSelector
|
||||
const ns = podSpec.nodeSelector || {};
|
||||
setNodeSelectorKv(Object.entries(ns).map(([k, v]) => ({ key: k, value: String(v) })));
|
||||
|
||||
// tolerations
|
||||
const tols: any[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : [];
|
||||
setTolerations(tols.map(t => ({
|
||||
key: t.key || '',
|
||||
operator: t.operator || 'Equal',
|
||||
value: t.value || '',
|
||||
effect: t.effect || '',
|
||||
tolerationSeconds: t.tolerationSeconds != null ? String(t.tolerationSeconds) : '',
|
||||
})));
|
||||
|
||||
setDnsPolicy(podSpec.dnsPolicy || 'ClusterFirst');
|
||||
setRestartPolicy(podSpec.restartPolicy || 'Always');
|
||||
setSchedulerName(podSpec.schedulerName || 'default-scheduler');
|
||||
setTerminationGracePeriodSeconds(podSpec.terminationGracePeriodSeconds != null ? String(podSpec.terminationGracePeriodSeconds) : '30');
|
||||
setServiceAccountName(podSpec.serviceAccountName || '');
|
||||
|
||||
// Pod securityContext
|
||||
const sc = podSpec.securityContext || {};
|
||||
setPodFsGroup(sc.fsGroup != null ? String(sc.fsGroup) : '');
|
||||
setPodRunAsUser(sc.runAsUser != null ? String(sc.runAsUser) : '');
|
||||
setPodRunAsGroup(sc.runAsGroup != null ? String(sc.runAsGroup) : '');
|
||||
setPodRunAsNonRoot(!!sc.runAsNonRoot);
|
||||
|
||||
// 主机设置(M8 补全)
|
||||
setHostNetwork(!!podSpec.hostNetwork);
|
||||
setHostIPC(!!podSpec.hostIPC);
|
||||
setHostPID(!!podSpec.hostPID);
|
||||
setShareProcessNamespace(!!podSpec.shareProcessNamespace);
|
||||
|
||||
// hostAliases(M8 补全)
|
||||
const ha: any[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : [];
|
||||
setHostAliases(ha.map(a => ({
|
||||
ip: a.ip || '',
|
||||
hostnames: Array.isArray(a.hostnames) ? a.hostnames.join(', ') : '',
|
||||
})));
|
||||
|
||||
// dnsConfig(M8 补全)
|
||||
const dc = podSpec.dnsConfig || {};
|
||||
setDnsNameservers(Array.isArray(dc.nameservers) ? dc.nameservers.join('\n') : '');
|
||||
setDnsSearches(Array.isArray(dc.searches) ? dc.searches.join('\n') : '');
|
||||
const opts: any[] = Array.isArray(dc.options) ? dc.options : [];
|
||||
setDnsOptions(opts.map(o => ({ name: o.name || '', value: o.value || '' })));
|
||||
|
||||
setLoading(false);
|
||||
}, [manifest]);
|
||||
|
||||
// nodeSelector 操作
|
||||
const addNs = () => setNodeSelectorKv(prev => [...prev, { key: '', value: '' }]);
|
||||
const removeNs = (i: number) => setNodeSelectorKv(prev => prev.filter((_, idx) => idx !== i));
|
||||
const updateNs = (i: number, patch: Partial<{ key: string; value: string }>) =>
|
||||
setNodeSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n)));
|
||||
|
||||
// tolerations 操作
|
||||
const addTol = () => setTolerations(prev => [...prev, { key: '', operator: 'Equal', value: '', effect: 'NoSchedule', tolerationSeconds: '' }]);
|
||||
const removeTol = (i: number) => setTolerations(prev => prev.filter((_, idx) => idx !== i));
|
||||
const updateTol = (i: number, patch: Partial<Toleration>) =>
|
||||
setTolerations(prev => prev.map((t, idx) => (idx === i ? { ...t, ...patch } : t)));
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const base = JSON.parse(JSON.stringify(rawManifest));
|
||||
base.spec = base.spec || {};
|
||||
base.spec.template = base.spec.template || {};
|
||||
base.spec.template.spec = base.spec.template.spec || {};
|
||||
const podSpec = base.spec.template.spec;
|
||||
|
||||
// 更新策略
|
||||
base.spec.strategy = { type: strategyType };
|
||||
if (strategyType === 'RollingUpdate') {
|
||||
base.spec.strategy.rollingUpdate = { maxSurge, maxUnavailable };
|
||||
}
|
||||
|
||||
// 历史/进度
|
||||
if (revisionHistoryLimit) base.spec.revisionHistoryLimit = Number(revisionHistoryLimit);
|
||||
if (progressDeadlineSeconds) base.spec.progressDeadlineSeconds = Number(progressDeadlineSeconds);
|
||||
|
||||
// nodeSelector
|
||||
const nsObj: Record<string, string> = {};
|
||||
for (const { key, value } of nodeSelectorKv) {
|
||||
const k = key.trim();
|
||||
if (k) nsObj[k] = value;
|
||||
}
|
||||
if (Object.keys(nsObj).length > 0) podSpec.nodeSelector = nsObj;
|
||||
else delete podSpec.nodeSelector;
|
||||
|
||||
// tolerations
|
||||
const tolsK8s = tolerations
|
||||
.filter(t => t.key || t.operator === 'Exists')
|
||||
.map(t => {
|
||||
const out: any = {};
|
||||
if (t.key) out.key = t.key;
|
||||
if (t.operator) out.operator = t.operator;
|
||||
if (t.value && t.operator !== 'Exists') out.value = t.value;
|
||||
if (t.effect) out.effect = t.effect;
|
||||
if (t.tolerationSeconds && t.effect === 'NoExecute') out.tolerationSeconds = Number(t.tolerationSeconds);
|
||||
return out;
|
||||
});
|
||||
if (tolsK8s.length > 0) podSpec.tolerations = tolsK8s;
|
||||
else delete podSpec.tolerations;
|
||||
|
||||
// 其他 Pod 级别
|
||||
if (dnsPolicy) podSpec.dnsPolicy = dnsPolicy;
|
||||
if (restartPolicy) podSpec.restartPolicy = restartPolicy;
|
||||
if (schedulerName) podSpec.schedulerName = schedulerName;
|
||||
if (terminationGracePeriodSeconds) podSpec.terminationGracePeriodSeconds = Number(terminationGracePeriodSeconds);
|
||||
if (serviceAccountName) podSpec.serviceAccountName = serviceAccountName;
|
||||
|
||||
// Pod 安全上下文
|
||||
const sc: any = {};
|
||||
if (podFsGroup) sc.fsGroup = Number(podFsGroup);
|
||||
if (podRunAsUser) sc.runAsUser = Number(podRunAsUser);
|
||||
if (podRunAsGroup) sc.runAsGroup = Number(podRunAsGroup);
|
||||
if (podRunAsNonRoot) sc.runAsNonRoot = true;
|
||||
if (Object.keys(sc).length > 0) podSpec.securityContext = sc;
|
||||
else delete podSpec.securityContext;
|
||||
|
||||
// 主机设置(M8 补全)— 只在为 true 时写入,false 删除字段
|
||||
if (hostNetwork) podSpec.hostNetwork = true; else delete podSpec.hostNetwork;
|
||||
if (hostIPC) podSpec.hostIPC = true; else delete podSpec.hostIPC;
|
||||
if (hostPID) podSpec.hostPID = true; else delete podSpec.hostPID;
|
||||
if (shareProcessNamespace) podSpec.shareProcessNamespace = true; else delete podSpec.shareProcessNamespace;
|
||||
|
||||
// hostAliases(M8 补全)
|
||||
const haK8s = hostAliases
|
||||
.filter(a => a.ip.trim())
|
||||
.map(a => ({
|
||||
ip: a.ip.trim(),
|
||||
hostnames: a.hostnames.split(/[,\s]+/).map((s: string) => s.trim()).filter(Boolean),
|
||||
}))
|
||||
.filter(a => a.hostnames.length > 0);
|
||||
if (haK8s.length > 0) podSpec.hostAliases = haK8s;
|
||||
else delete podSpec.hostAliases;
|
||||
|
||||
// dnsConfig(M8 补全)— 仅 dnsPolicy=None 时 dnsConfig 才有意义,但仍允许编辑
|
||||
const dcObj: any = {};
|
||||
const ns = dnsNameservers.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
if (ns.length > 0) dcObj.nameservers = ns;
|
||||
const sr = dnsSearches.split('\n').map(s => s.trim()).filter(Boolean);
|
||||
if (sr.length > 0) dcObj.searches = sr;
|
||||
const dkOpts = dnsOptions.filter(o => o.name.trim()).map(o => {
|
||||
const out: any = { name: o.name.trim() };
|
||||
if (o.value.trim()) out.value = o.value.trim();
|
||||
return out;
|
||||
});
|
||||
if (dkOpts.length > 0) dcObj.options = dkOpts;
|
||||
if (Object.keys(dcObj).length > 0) podSpec.dnsConfig = dcObj;
|
||||
else delete podSpec.dnsConfig;
|
||||
|
||||
const json = JSON.stringify(base, null, 2);
|
||||
await kubernetesApi.updateResourceYaml(clusterCode, namespace, 'deployments', deploymentName, json);
|
||||
toast({ title: '高级设置已保存', description: `${deploymentName} 更新已提交` });
|
||||
onSaved();
|
||||
} catch (e: any) {
|
||||
toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' });
|
||||
} finally { setSaving(false); }
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-12 text-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin mx-auto text-slate-500" />
|
||||
<p className="text-slate-500 mt-3 text-sm">加载中...</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!manifest) {
|
||||
return (
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardContent className="p-8 text-center text-slate-500">
|
||||
未获取到 manifest 数据,请使用 YAML 编辑模式直接修改。
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 更新策略 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4 text-blue-400" />更新策略 (Strategy)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-1.5 text-xs text-slate-300 cursor-pointer">
|
||||
<input type="radio" checked={strategyType === 'RollingUpdate'} onChange={() => setStrategyType('RollingUpdate')}
|
||||
className="accent-blue-500" />
|
||||
RollingUpdate(滚动更新)
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 text-xs text-slate-300 cursor-pointer">
|
||||
<input type="radio" checked={strategyType === 'Recreate'} onChange={() => setStrategyType('Recreate')}
|
||||
className="accent-blue-500" />
|
||||
Recreate(重建)
|
||||
</label>
|
||||
</div>
|
||||
{strategyType === 'RollingUpdate' && (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">maxSurge(整数或百分比,超出期望副本数)</Label>
|
||||
<Input value={maxSurge} onChange={e => setMaxSurge(e.target.value)}
|
||||
placeholder="25%" className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">maxUnavailable(整数或百分比,不可用上限)</Label>
|
||||
<Input value={maxUnavailable} onChange={e => setMaxUnavailable(e.target.value)}
|
||||
placeholder="25%" className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 历史版本 / 进度 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-amber-400" />历史版本 & 进度
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">revisionHistoryLimit(保留 RS 历史)</Label>
|
||||
<Input type="number" value={revisionHistoryLimit} onChange={e => setRevisionHistoryLimit(e.target.value)}
|
||||
className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">progressDeadlineSeconds(升级超时秒数)</Label>
|
||||
<Input type="number" value={progressDeadlineSeconds} onChange={e => setProgressDeadlineSeconds(e.target.value)}
|
||||
className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 调度:nodeSelector + tolerations */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Server className="h-4 w-4 text-emerald-400" />调度 (Scheduling)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
{/* nodeSelector */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">nodeSelector(节点标签选择)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addNs}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{nodeSelectorKv.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : nodeSelectorKv.map((n, i) => (
|
||||
<div key={i} className="flex items-center gap-2 mb-1.5">
|
||||
<Input value={n.key} onChange={e => updateNs(i, { key: e.target.value })}
|
||||
placeholder="disktype" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<span className="text-slate-600">:</span>
|
||||
<Input value={n.value} onChange={e => updateNs(i, { value: e.target.value })}
|
||||
placeholder="ssd" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeNs(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* tolerations */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">tolerations(污点容忍)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400" onClick={addTol}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{tolerations.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : tolerations.map((t, i) => (
|
||||
<div key={i} className="bg-slate-950/50 border border-slate-800 rounded p-2 mb-1.5 grid grid-cols-5 gap-2 items-end">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">key</Label>
|
||||
<Input value={t.key} onChange={e => updateTol(i, { key: e.target.value })}
|
||||
placeholder="node-role.kubernetes.io/control-plane"
|
||||
className="h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">operator</Label>
|
||||
<select value={t.operator} onChange={e => updateTol(i, { operator: e.target.value })}
|
||||
className="h-7 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1">
|
||||
<option value="Equal">Equal</option>
|
||||
<option value="Exists">Exists</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">value</Label>
|
||||
<Input value={t.value} onChange={e => updateTol(i, { value: e.target.value })}
|
||||
disabled={t.operator === 'Exists'}
|
||||
className="h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono disabled:opacity-40" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-[10px]">effect</Label>
|
||||
<select value={t.effect} onChange={e => updateTol(i, { effect: e.target.value })}
|
||||
className="h-7 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-1">
|
||||
<option value="">(all)</option>
|
||||
<option value="NoSchedule">NoSchedule</option>
|
||||
<option value="PreferNoSchedule">PreferNoSchedule</option>
|
||||
<option value="NoExecute">NoExecute</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Input value={t.tolerationSeconds} onChange={e => updateTol(i, { tolerationSeconds: e.target.value })}
|
||||
disabled={t.effect !== 'NoExecute'} placeholder="秒"
|
||||
className="h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono disabled:opacity-40" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400" onClick={() => removeTol(i)}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pod 级别杂项 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Settings className="h-4 w-4 text-slate-400" />Pod 级别
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">dnsPolicy</Label>
|
||||
<select value={dnsPolicy} onChange={e => setDnsPolicy(e.target.value)}
|
||||
className="h-8 mt-1 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2">
|
||||
<option value="ClusterFirst">ClusterFirst</option>
|
||||
<option value="ClusterFirstWithHostNet">ClusterFirstWithHostNet</option>
|
||||
<option value="Default">Default</option>
|
||||
<option value="None">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">restartPolicy</Label>
|
||||
<select value={restartPolicy} onChange={e => setRestartPolicy(e.target.value)}
|
||||
className="h-8 mt-1 w-full bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 px-2">
|
||||
<option value="Always">Always</option>
|
||||
<option value="OnFailure">OnFailure</option>
|
||||
<option value="Never">Never</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">schedulerName</Label>
|
||||
<Input value={schedulerName} onChange={e => setSchedulerName(e.target.value)}
|
||||
className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">terminationGracePeriodSeconds</Label>
|
||||
<Input type="number" value={terminationGracePeriodSeconds} onChange={e => setTerminationGracePeriodSeconds(e.target.value)}
|
||||
className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label className="text-slate-500 text-xs">serviceAccountName</Label>
|
||||
<Input value={serviceAccountName} onChange={e => setServiceAccountName(e.target.value)}
|
||||
placeholder="default" className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pod 安全上下文 */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-rose-400" />Pod 安全上下文 (securityContext)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">fsGroup</Label>
|
||||
<Input type="number" value={podFsGroup} onChange={e => setPodFsGroup(e.target.value)}
|
||||
placeholder="1000" className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">runAsUser</Label>
|
||||
<Input type="number" value={podRunAsUser} onChange={e => setPodRunAsUser(e.target.value)}
|
||||
placeholder="1000" className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">runAsGroup</Label>
|
||||
<Input type="number" value={podRunAsGroup} onChange={e => setPodRunAsGroup(e.target.value)}
|
||||
placeholder="1000" className="h-8 mt-1 bg-slate-800 border-slate-700 text-slate-200 text-sm font-mono" />
|
||||
</div>
|
||||
<div className="flex items-end pb-1">
|
||||
<label className="flex items-center gap-2 text-xs text-slate-400 cursor-pointer">
|
||||
<input type="checkbox" checked={podRunAsNonRoot} onChange={e => setPodRunAsNonRoot(e.target.checked)}
|
||||
className="accent-blue-500" />
|
||||
runAsNonRoot
|
||||
</label>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 主机设置(M8 补全) */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Network className="h-4 w-4 text-cyan-400" />主机设置 (Host Namespace)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer">
|
||||
<input type="checkbox" checked={hostNetwork} onChange={e => setHostNetwork(e.target.checked)}
|
||||
className="accent-blue-500" />
|
||||
hostNetwork(共享主机网络命名空间)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer">
|
||||
<input type="checkbox" checked={hostIPC} onChange={e => setHostIPC(e.target.checked)}
|
||||
className="accent-blue-500" />
|
||||
hostIPC(共享主机 IPC 命名空间)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer">
|
||||
<input type="checkbox" checked={hostPID} onChange={e => setHostPID(e.target.checked)}
|
||||
className="accent-blue-500" />
|
||||
hostPID(共享主机 PID 命名空间)
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs text-slate-300 cursor-pointer">
|
||||
<input type="checkbox" checked={shareProcessNamespace} onChange={e => setShareProcessNamespace(e.target.checked)}
|
||||
className="accent-blue-500" />
|
||||
shareProcessNamespace(共享进程命名空间)
|
||||
</label>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* DNS 配置(M8 补全) */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-indigo-400" />DNS 配置 (dnsConfig)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
<p className="text-[11px] text-slate-500">
|
||||
提示:dnsConfig 通常与 <code className="text-slate-400">dnsPolicy: None</code> 搭配使用。每行一个值。
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">nameservers(每行一个 IP)</Label>
|
||||
<textarea value={dnsNameservers} onChange={e => setDnsNameservers(e.target.value)}
|
||||
placeholder={'8.8.8.8\n114.114.114.114'}
|
||||
rows={3}
|
||||
className="w-full mt-1 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 font-mono p-2 resize-y" />
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-slate-500 text-xs">searches(搜索域,每行一个)</Label>
|
||||
<textarea value={dnsSearches} onChange={e => setDnsSearches(e.target.value)}
|
||||
placeholder={'svc.cluster.local\ncluster.local'}
|
||||
rows={3}
|
||||
className="w-full mt-1 bg-slate-800 border border-slate-700 rounded text-xs text-slate-300 font-mono p-2 resize-y" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">options(DNS 选项)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400"
|
||||
onClick={() => setDnsOptions(prev => [...prev, { name: '', value: '' }])}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{dnsOptions.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : dnsOptions.map((o, i) => (
|
||||
<div key={i} className="flex items-center gap-2 mb-1.5">
|
||||
<Input value={o.name} onChange={e => setDnsOptions(prev => prev.map((x, idx) => idx === i ? { ...x, name: e.target.value } : x))}
|
||||
placeholder="ndots" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<span className="text-slate-600">:</span>
|
||||
<Input value={o.value} onChange={e => setDnsOptions(prev => prev.map((x, idx) => idx === i ? { ...x, value: e.target.value } : x))}
|
||||
placeholder="5(可留空)" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400"
|
||||
onClick={() => setDnsOptions(prev => prev.filter((_, idx) => idx !== i))}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 主机别名(M8 补全) */}
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-sm text-slate-200 flex items-center gap-2">
|
||||
<MapPin className="h-4 w-4 text-amber-400" />主机别名 (hostAliases)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-2">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Label className="text-slate-400 text-xs">IP → 主机名映射(注入 Pod 的 /etc/hosts)</Label>
|
||||
<Button size="sm" variant="ghost" className="h-6 text-xs text-slate-400"
|
||||
onClick={() => setHostAliases(prev => [...prev, { ip: '', hostnames: '' }])}>
|
||||
<Plus className="h-3 w-3 mr-1" />添加
|
||||
</Button>
|
||||
</div>
|
||||
{hostAliases.length === 0 ? (
|
||||
<p className="text-xs text-slate-600 italic">未配置</p>
|
||||
) : hostAliases.map((a, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<Input value={a.ip} onChange={e => setHostAliases(prev => prev.map((x, idx) => idx === i ? { ...x, ip: e.target.value } : x))}
|
||||
placeholder="10.10.10.72" className="w-40 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<span className="text-slate-600">→</span>
|
||||
<Input value={a.hostnames} onChange={e => setHostAliases(prev => prev.map((x, idx) => idx === i ? { ...x, hostnames: e.target.value } : x))}
|
||||
placeholder="gitlab.local, registry.local" className="flex-1 h-7 bg-slate-800 border-slate-700 text-slate-300 text-xs font-mono" />
|
||||
<Button size="icon" variant="ghost" className="h-6 w-6 text-red-400"
|
||||
onClick={() => setHostAliases(prev => prev.filter((_, idx) => idx !== i))}>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="outline" className="border-slate-700 text-slate-400" onClick={onCancel}>取消</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" disabled={saving} onClick={save}>
|
||||
{saving ? <Loader2 className="h-4 w-4 animate-spin mr-1" /> : <Save className="h-4 w-4 mr-1" />}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user