Files
aiops/frontend/src/components/kubernetes/DeploymentEditAdvanced.tsx
T

290 lines
12 KiB
TypeScript

// DeploymentEditAdvanced.tsx — 编辑器「高级设置」Tab (M8) // Logan.Li
// 编辑内容:更新策略 / 历史版本 / 调度(节点选择/容忍) / DNS / 安全上下文 / 终止宽限期 / 调度器 / SA
// Q21 拆分: 提取 types(parser+builder) + UpdateStrategyCard + TolerationsCard + PodMiscCard + PodSecurityCard + HostSettingsCard
// 作者: Logan <admin@attacker.club>
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2, Save } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/api';
import { saveDeploymentManifest } from './deployment-manifest-save';
import { SchedulingSection } from './deployment-edit-advanced/SchedulingSection';
import { DnsHostSection } from './deployment-edit-advanced/DnsHostSection';
import {
parseAdvancedManifest, buildAdvancedManifest,
type AdvancedState, type Toleration, type NodeInfo,
} from './deployment-edit-advanced/types';
import { UpdateStrategyCard } from './deployment-edit-advanced/UpdateStrategyCard';
import { TolerationsCard } from './deployment-edit-advanced/TolerationsCard';
import { PodMiscCard } from './deployment-edit-advanced/PodMiscCard';
import { PodSecurityCard } from './deployment-edit-advanced/PodSecurityCard';
import { HostSettingsCard } from './deployment-edit-advanced/HostSettingsCard';
import type { JsonData } from '@/types/common';
import i18n from '@/i18n';
interface Props {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: JsonData | null;
onSaved: (updated?: JsonData) => void;
onCancel: () => void;
}
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<AdvancedState['scheduleMode']>('nodeSelector');
const [nodeName, setNodeName] = useState('');
const [availableNodes, setAvailableNodes] = useState<NodeInfo[]>([]);
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[]>([]);
// Pod 级别杂项
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);
// 主机设置
const [hostNetwork, setHostNetwork] = useState(false);
const [hostIPC, setHostIPC] = useState(false);
const [hostPID, setHostPID] = useState(false);
const [shareProcessNamespace, setShareProcessNamespace] = useState(false);
// hostAliases
const [hostAliases, setHostAliases] = useState<Array<{ ip: string; hostnames: string }>>([]);
// dnsConfig
const [dnsNameservers, setDnsNameservers] = useState('');
const [dnsSearches, setDnsSearches] = useState('');
const [dnsOptions, setDnsOptions] = useState<Array<{ name: string; value: string }>>([]);
useEffect(() => {
if (!manifest) { setLoading(false); return; }
setRawManifest(manifest);
const s = parseAdvancedManifest(manifest);
setStrategyType(s.strategyType);
setMaxSurge(s.maxSurge);
setMaxUnavailable(s.maxUnavailable);
setRevisionHistoryLimit(s.revisionHistoryLimit);
setProgressDeadlineSeconds(s.progressDeadlineSeconds);
setScheduleMode(s.scheduleMode);
setNodeName(s.nodeName);
setLabelSelectorKv(s.labelSelectorKv);
setTolerations(s.tolerations);
setDnsPolicy(s.dnsPolicy);
setRestartPolicy(s.restartPolicy);
setSchedulerName(s.schedulerName);
setTerminationGracePeriodSeconds(s.terminationGracePeriodSeconds);
setServiceAccountName(s.serviceAccountName);
setPodFsGroup(s.podFsGroup);
setPodRunAsUser(s.podRunAsUser);
setPodRunAsGroup(s.podRunAsGroup);
setPodRunAsNonRoot(s.podRunAsNonRoot);
setHostNetwork(s.hostNetwork);
setHostIPC(s.hostIPC);
setHostPID(s.hostPID);
setShareProcessNamespace(s.shareProcessNamespace);
setHostAliases(s.hostAliases);
setDnsNameservers(s.dnsNameservers);
setDnsSearches(s.dnsSearches);
setDnsOptions(s.dnsOptions);
// 加载节点列表(副作用,留在 useEffect 内)
if (clusterCode) {
setNodesLoading(true);
kubernetesApi.getClusterNodes(clusterCode).then((res: JsonData) => {
const nodes: JsonData[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
const mapped: NodeInfo[] = 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));
}
setLoading(false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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)));
// tolerations 操作
const addToleration = () =>
setTolerations(prev => [...prev, { key: '', operator: 'Equal', value: '', effect: 'NoSchedule', tolerationSeconds: '' }]);
const removeToleration = (i: number) => setTolerations(prev => prev.filter((_, idx) => idx !== i));
const updateToleration = (i: number, patch: Partial<Toleration>) =>
setTolerations(prev => prev.map((t, idx) => (idx === i ? { ...t, ...patch } : t)));
const save = async () => {
setSaving(true);
try {
const state: AdvancedState = {
strategyType, maxSurge, maxUnavailable,
revisionHistoryLimit, progressDeadlineSeconds,
scheduleMode, nodeName, labelSelectorKv, tolerations,
dnsPolicy, restartPolicy, schedulerName,
terminationGracePeriodSeconds, serviceAccountName,
podFsGroup, podRunAsUser, podRunAsGroup, podRunAsNonRoot,
hostNetwork, hostIPC, hostPID, shareProcessNamespace,
hostAliases, dnsNameservers, dnsSearches, dnsOptions,
};
const base = buildAdvancedManifest(rawManifest, state);
await saveDeploymentManifest(clusterCode, namespace, deploymentName, base);
toast({ title: i18n.t('kubernetes.deploymentedita.s9762d0a4'), description: `${deploymentName} 更新已提交` });
onSaved(base);
} catch (e) {
toast({ title: i18n.t('kubernetes.msg.saveFailed'), description: e instanceof Error ? e.message : String(e), variant: 'destructive' });
} finally { setSaving(false); }
};
if (loading) {
return (
<div 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"> {i18n.t('monitor.logPanel.logResults.loading')} </p>
</div>
);
}
if (!manifest) {
return (
<div className="p-8 text-center dg-text-muted">
{i18n.t('k8s.editAdvanced.noManifest')}
</div>
);
}
return (
<div className="space-y-4">
<UpdateStrategyCard
strategyType={strategyType}
onStrategyTypeChange={setStrategyType}
maxSurge={maxSurge}
onMaxSurgeChange={setMaxSurge}
maxUnavailable={maxUnavailable}
onMaxUnavailableChange={setMaxUnavailable}
revisionHistoryLimit={revisionHistoryLimit}
onRevisionHistoryLimitChange={setRevisionHistoryLimit}
progressDeadlineSeconds={progressDeadlineSeconds}
onProgressDeadlineSecondsChange={setProgressDeadlineSeconds}
/>
<TolerationsCard
tolerations={tolerations}
onAdd={addToleration}
onRemove={removeToleration}
onUpdate={updateToleration}
/>
<SchedulingSection
scheduleMode={scheduleMode}
onScheduleModeChange={setScheduleMode}
nodeName={nodeName}
onNodeNameChange={setNodeName}
availableNodes={availableNodes}
nodesLoading={nodesLoading}
labelSelectorKv={labelSelectorKv}
onAddLabelSelector={addNs}
onRemoveLabelSelector={removeNs}
onUpdateLabelSelector={updateNs}
nodeLabels={nodeLabels}
/>
<PodMiscCard
dnsPolicy={dnsPolicy}
onDnsPolicyChange={setDnsPolicy}
restartPolicy={restartPolicy}
onRestartPolicyChange={setRestartPolicy}
schedulerName={schedulerName}
onSchedulerNameChange={setSchedulerName}
terminationGracePeriodSeconds={terminationGracePeriodSeconds}
onTerminationGracePeriodSecondsChange={setTerminationGracePeriodSeconds}
serviceAccountName={serviceAccountName}
onServiceAccountNameChange={setServiceAccountName}
/>
<PodSecurityCard
podFsGroup={podFsGroup}
onPodFsGroupChange={setPodFsGroup}
podRunAsUser={podRunAsUser}
onPodRunAsUserChange={setPodRunAsUser}
podRunAsGroup={podRunAsGroup}
onPodRunAsGroupChange={setPodRunAsGroup}
podRunAsNonRoot={podRunAsNonRoot}
onPodRunAsNonRootChange={setPodRunAsNonRoot}
/>
<HostSettingsCard
hostNetwork={hostNetwork}
onHostNetworkChange={setHostNetwork}
hostIPC={hostIPC}
onHostIPCChange={setHostIPC}
hostPID={hostPID}
onHostPIDChange={setHostPID}
shareProcessNamespace={shareProcessNamespace}
onShareProcessNamespaceChange={setShareProcessNamespace}
/>
<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}> {i18n.t('rbac.jobRole.cancel')} </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>
);
}