- 新增 PageShell/PageTabs/ContentPanel/PageLoading/PageEmpty 页面骨架 - index.css 设计令牌文档化,布局令牌与纯色主按钮(去除渐变粉彩) - 全站路由页接入 PageShell,云账单三 Tab 为标杆 - 主题体系:跟随系统 + 主流/开发者分组,新增 Dracula/Tokyo Night,One Dark 暖灰差异化 - 登录页/侧栏/顶栏去渐变光球,企业克制风 - 修复 AIAlertAnalyzer JSX 与构建依赖导入问题
589 lines
28 KiB
TypeScript
589 lines
28 KiB
TypeScript
// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8) // Logan.Li
|
||
// 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / 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 {
|
||
Save, Loader2, Settings, RefreshCw, Shield, Clock, Network, Plus, Trash2,
|
||
} from 'lucide-react';
|
||
import { useToast } from '@/hooks/use-toast';
|
||
import { kubernetesApi } from '@/api';
|
||
import { saveDeploymentManifest } from './deployment-manifest-save';
|
||
import { SchedulingSection, type ScheduleMode } from './deployment-edit-advanced/SchedulingSection';
|
||
import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection';
|
||
import type { JsonData } from '@/types/common';
|
||
|
||
interface Props {
|
||
clusterCode: string;
|
||
namespace: string;
|
||
deploymentName: string;
|
||
manifest: JsonData | null;
|
||
onSaved: (updated?: JsonData) => 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<JsonData>(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');
|
||
|
||
// ──── 调度策略 ────
|
||
const [scheduleMode, setScheduleMode] = useState<ScheduleMode>('nodeSelector');
|
||
const [nodeName, setNodeName] = useState('');
|
||
const [availableNodes, setAvailableNodes] = useState<Array<{ name: string; ip: string; labels: Record<string, string> }>>([]);
|
||
const [nodesLoading, setNodesLoading] = useState(false);
|
||
const [labelSelectorKv, setLabelSelectorKv] = useState<Array<{ key: string; value: string }>>([]);
|
||
const [nodeLabels, setNodeLabels] = useState<Record<string, 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 → labelSelectorKv
|
||
const ns = podSpec.nodeSelector || {};
|
||
const nsEntries = Object.entries(ns).map(([k, v]) => ({ key: k, value: String(v) }));
|
||
setLabelSelectorKv(nsEntries);
|
||
|
||
// nodeName
|
||
setNodeName(podSpec.nodeName || '');
|
||
|
||
// Determine scheduleMode
|
||
if (podSpec.nodeName) {
|
||
setScheduleMode('nodeName');
|
||
} else if (podSpec.affinity?.nodeAffinity) {
|
||
setScheduleMode('nodeAffinity');
|
||
} else if (nsEntries.length > 0) {
|
||
setScheduleMode('nodeSelector');
|
||
}
|
||
|
||
// 加载节点列表
|
||
if (clusterCode) {
|
||
setNodesLoading(true);
|
||
kubernetesApi.getClusterNodes(clusterCode).then((res: JsonData) => {
|
||
const nodes: JsonData[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
|
||
const mapped = nodes.map((n: JsonData) => {
|
||
const meta = n.metadata || {};
|
||
const status = n.status || {};
|
||
const addresses: JsonData[] = Array.isArray(status.addresses) ? status.addresses : [];
|
||
const internalIP = addresses.find((a: JsonData) => a.type === 'InternalIP')?.address || '';
|
||
return { name: meta.name || '', ip: internalIP, labels: meta.labels || {} };
|
||
});
|
||
setAvailableNodes(mapped);
|
||
// 聚合标签
|
||
const lblMap: Record<string, string[]> = {};
|
||
for (const n of mapped) {
|
||
for (const [k, v] of Object.entries(n.labels)) {
|
||
const sv = String(v);
|
||
if (!lblMap[k]) lblMap[k] = [];
|
||
if (!lblMap[k].includes(sv)) lblMap[k].push(sv);
|
||
}
|
||
}
|
||
setNodeLabels(lblMap);
|
||
}).catch(() => {}).finally(() => setNodesLoading(false));
|
||
}
|
||
|
||
// tolerations
|
||
const tols: JsonData[] = 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: JsonData[] = 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: JsonData[] = Array.isArray(dc.options) ? dc.options : [];
|
||
setDnsOptions(opts.map(o => ({ name: o.name || '', value: o.value || '' })));
|
||
|
||
setLoading(false);
|
||
}, [manifest]);
|
||
|
||
// labelSelector 操作
|
||
const addNs = () => setLabelSelectorKv(prev => [...prev, { key: '', value: '' }]);
|
||
const removeNs = (i: number) => setLabelSelectorKv(prev => prev.filter((_, idx) => idx !== i));
|
||
const updateNs = (i: number, patch: Partial<{ key: string; value: string }>) =>
|
||
setLabelSelectorKv(prev => prev.map((n, idx) => (idx === i ? { ...n, ...patch } : n)));
|
||
|
||
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 / nodeName / nodeAffinity
|
||
if (scheduleMode === 'nodeName' && nodeName.trim()) {
|
||
podSpec.nodeName = nodeName.trim();
|
||
delete podSpec.nodeSelector;
|
||
if (podSpec.affinity) delete podSpec.affinity.nodeAffinity;
|
||
} else if (scheduleMode === 'nodeSelector') {
|
||
const nsObj: Record<string, string> = {};
|
||
for (const { key, value } of labelSelectorKv) {
|
||
const k = key.trim();
|
||
if (k) nsObj[k] = value;
|
||
}
|
||
if (Object.keys(nsObj).length > 0) podSpec.nodeSelector = nsObj;
|
||
else delete podSpec.nodeSelector;
|
||
if (podSpec.nodeName) delete podSpec.nodeName;
|
||
if (podSpec.affinity) delete podSpec.affinity.nodeAffinity;
|
||
} else if (scheduleMode === 'nodeAffinity') {
|
||
const matchExpressions: JsonData[] = labelSelectorKv
|
||
.filter(s => s.key.trim())
|
||
.map(s => ({ key: s.key.trim(), operator: 'In', values: [s.value] }));
|
||
if (matchExpressions.length > 0) {
|
||
podSpec.affinity = podSpec.affinity || {};
|
||
podSpec.affinity.nodeAffinity = {
|
||
requiredDuringSchedulingIgnoredDuringExecution: {
|
||
nodeSelectorTerms: [{ matchExpressions }],
|
||
},
|
||
};
|
||
} else if (podSpec.affinity) {
|
||
delete podSpec.affinity.nodeAffinity;
|
||
}
|
||
delete podSpec.nodeSelector;
|
||
if (podSpec.nodeName) delete podSpec.nodeName;
|
||
}
|
||
|
||
// tolerations
|
||
const tolsK8s = tolerations
|
||
.filter(t => t.key || t.operator === 'Exists')
|
||
.map(t => {
|
||
const out: JsonData = {};
|
||
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: JsonData = {};
|
||
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: JsonData = {};
|
||
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: JsonData = { 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;
|
||
|
||
await saveDeploymentManifest(clusterCode, namespace, deploymentName, base);
|
||
toast({ title: '高级设置已保存', description: `${deploymentName} 更新已提交` });
|
||
onSaved(base);
|
||
} catch (e) {
|
||
toast({ title: '保存失败', description: e?.message || String(e), variant: 'destructive' });
|
||
} finally { setSaving(false); }
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<Card className="dg-card">
|
||
<CardContent className="p-12 text-center">
|
||
<Loader2 className="h-6 w-6 animate-spin mx-auto dg-text-muted" />
|
||
<p className="dg-text-muted mt-3 text-sm">加载中...</p>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (!manifest) {
|
||
return (
|
||
<Card className="dg-card">
|
||
<CardContent className="p-8 text-center dg-text-muted">
|
||
未获取到 manifest 数据,请使用 YAML 编辑模式直接修改。
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* 更新策略 */}
|
||
<Card className="dg-card">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||
<RefreshCw className="h-4 w-4 dg-text-blue" />更新策略 (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-[var(--dg-text-secondary)] 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-[var(--dg-text-secondary)] 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="dg-text-muted text-xs">maxSurge(整数或百分比,超出期望副本数)</Label>
|
||
<Input value={maxSurge} onChange={e => setMaxSurge(e.target.value)}
|
||
placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">maxUnavailable(整数或百分比,不可用上限)</Label>
|
||
<Input value={maxUnavailable} onChange={e => setMaxUnavailable(e.target.value)}
|
||
placeholder="25%" className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 历史版本 / 进度 */}
|
||
<Card className="dg-card">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||
<Clock className="h-4 w-4 dg-text-yellow" />历史版本 & 进度
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">revisionHistoryLimit(保留 RS 历史)</Label>
|
||
<Input type="number" value={revisionHistoryLimit} onChange={e => setRevisionHistoryLimit(e.target.value)}
|
||
className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">progressDeadlineSeconds(升级超时秒数)</Label>
|
||
<Input type="number" value={progressDeadlineSeconds} onChange={e => setProgressDeadlineSeconds(e.target.value)}
|
||
className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 容忍 (Tolerations) — 此前仅有状态无 UI // Logan.Li */}
|
||
<Card className="dg-card">
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm text-[var(--dg-text-primary)]">容忍 (Tolerations)</CardTitle>
|
||
<Button size="sm" variant="outline" className="h-7 border-[var(--dg-border)] text-[var(--dg-text-secondary)] text-xs"
|
||
onClick={() => setTolerations(prev => [...prev, { key: '', operator: 'Equal', value: '', effect: 'NoSchedule', tolerationSeconds: '' }])}>
|
||
<Plus className="h-3 w-3 mr-1" />添加
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-0 space-y-2">
|
||
{tolerations.length === 0 && <p className="text-xs dg-text-muted">未配置容忍</p>}
|
||
{tolerations.map((t, i) => (
|
||
<div key={i} className="grid grid-cols-[1fr_80px_1fr_100px_80px_auto] gap-2 items-center">
|
||
<Input value={t.key} placeholder="key" onChange={e => setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, key: e.target.value } : x))}
|
||
className="h-7 dg-input text-xs font-mono" />
|
||
<select value={t.operator} onChange={e => setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, operator: e.target.value } : x))}
|
||
className="h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1">
|
||
<option value="Equal">Equal</option>
|
||
<option value="Exists">Exists</option>
|
||
</select>
|
||
<Input value={t.value} placeholder="value" disabled={t.operator === 'Exists'}
|
||
onChange={e => setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, value: e.target.value } : x))}
|
||
className="h-7 dg-input text-xs font-mono" />
|
||
<select value={t.effect} onChange={e => setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, effect: e.target.value } : x))}
|
||
className="h-7 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-1">
|
||
<option value="">任意</option>
|
||
<option value="NoSchedule">NoSchedule</option>
|
||
<option value="PreferNoSchedule">PreferNoSchedule</option>
|
||
<option value="NoExecute">NoExecute</option>
|
||
</select>
|
||
<Input value={t.tolerationSeconds} placeholder="秒" disabled={t.effect !== 'NoExecute'}
|
||
onChange={e => setTolerations(prev => prev.map((x, idx) => idx === i ? { ...x, tolerationSeconds: e.target.value } : x))}
|
||
className="h-7 dg-input text-xs font-mono" />
|
||
<Button variant="ghost" size="icon" className="h-7 w-7 dg-text-red" onClick={() => setTolerations(prev => prev.filter((_, idx) => idx !== i))}>
|
||
<Trash2 className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<SchedulingSection
|
||
scheduleMode={scheduleMode}
|
||
onScheduleModeChange={setScheduleMode}
|
||
nodeName={nodeName}
|
||
onNodeNameChange={setNodeName}
|
||
availableNodes={availableNodes}
|
||
nodesLoading={nodesLoading}
|
||
labelSelectorKv={labelSelectorKv}
|
||
onAddLabelSelector={addNs}
|
||
onRemoveLabelSelector={removeNs}
|
||
onUpdateLabelSelector={updateNs}
|
||
nodeLabels={nodeLabels}
|
||
/>
|
||
|
||
{/* Pod 级别杂项 */}
|
||
<Card className="dg-card">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||
<Settings className="h-4 w-4 dg-text-secondary" />Pod 级别
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">dnsPolicy</Label>
|
||
<select value={dnsPolicy} onChange={e => setDnsPolicy(e.target.value)}
|
||
className="h-8 mt-1 w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] 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="dg-text-muted text-xs">restartPolicy</Label>
|
||
<select value={restartPolicy} onChange={e => setRestartPolicy(e.target.value)}
|
||
className="h-8 mt-1 w-full bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded text-xs text-[var(--dg-text-secondary)] px-2">
|
||
<option value="Always">Always</option>
|
||
<option value="OnFailure">OnFailure</option>
|
||
<option value="Never">Never</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">schedulerName</Label>
|
||
<Input value={schedulerName} onChange={e => setSchedulerName(e.target.value)}
|
||
className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">terminationGracePeriodSeconds</Label>
|
||
<Input type="number" value={terminationGracePeriodSeconds} onChange={e => setTerminationGracePeriodSeconds(e.target.value)}
|
||
className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div className="col-span-2">
|
||
<Label className="dg-text-muted text-xs">serviceAccountName</Label>
|
||
<Input value={serviceAccountName} onChange={e => setServiceAccountName(e.target.value)}
|
||
placeholder="default" className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Pod 安全上下文 */}
|
||
<Card className="dg-card">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm text-[var(--dg-text-primary)] 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="dg-text-muted text-xs">fsGroup</Label>
|
||
<Input type="number" value={podFsGroup} onChange={e => setPodFsGroup(e.target.value)}
|
||
placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">runAsUser</Label>
|
||
<Input type="number" value={podRunAsUser} onChange={e => setPodRunAsUser(e.target.value)}
|
||
placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div>
|
||
<Label className="dg-text-muted text-xs">runAsGroup</Label>
|
||
<Input type="number" value={podRunAsGroup} onChange={e => setPodRunAsGroup(e.target.value)}
|
||
placeholder="1000" className="h-8 mt-1 dg-input text-sm font-mono" />
|
||
</div>
|
||
<div className="flex items-end pb-1">
|
||
<label className="flex items-center gap-2 text-xs dg-text-secondary 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="dg-card">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm text-[var(--dg-text-primary)] flex items-center gap-2">
|
||
<Network className="h-4 w-4 dg-text-cyan" />主机设置 (Host Namespace)
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="pt-0 grid grid-cols-2 gap-3">
|
||
<label className="flex items-center gap-2 text-xs text-[var(--dg-text-secondary)] 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-[var(--dg-text-secondary)] 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-[var(--dg-text-secondary)] 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-[var(--dg-text-secondary)] cursor-pointer">
|
||
<input type="checkbox" checked={shareProcessNamespace} onChange={e => setShareProcessNamespace(e.target.checked)}
|
||
className="accent-blue-500" />
|
||
shareProcessNamespace(共享进程命名空间)
|
||
</label>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<DnsHostSection
|
||
dnsNameservers={dnsNameservers}
|
||
onDnsNameserversChange={setDnsNameservers}
|
||
dnsSearches={dnsSearches}
|
||
onDnsSearchesChange={setDnsSearches}
|
||
dnsOptions={dnsOptions}
|
||
onDnsOptionsChange={setDnsOptions}
|
||
hostAliases={hostAliases}
|
||
onHostAliasesChange={setHostAliases}
|
||
/>
|
||
|
||
{/* 操作栏 */}
|
||
<div className="flex justify-end gap-2 pt-2">
|
||
<Button variant="outline" className="border-[var(--dg-border)] dg-text-secondary" 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>
|
||
);
|
||
} |