feat(alert): A2 告警引擎分布式安全 — Redis 分布式锁(SetNX)

- Cache 接口新增 SetNX 方法(go-redis 原子 / redka check-then-set 单副本安全)
- AlertEngine 改造为支持分布式锁: tryAcquireLock/releaseLock/GetEngineStatus
- 多副本部署时仅 Leader 执行告警检查, 60s TTL 防死锁, fail-open 容错
- 新增 GET /alerts/engine/status 监控端点(is_leader/instance_id/lock_holder)
- 修复 GrafanaDashboard.tsx Panel.options 类型不匹配导致构建中断
- 同步未提交的前端类型安全迁移(140+ 文件)
- ROADMAP/TDD_PLAN/PROJECT_EVALUATION 三文档同步更新

验证: go build 零错误, Docker 部署 healthy, 引擎状态端点正常,
锁竞争测试通过(非 Leader 跳过检查), 5/5 核心 API 回归通过
This commit is contained in:
attacker committed 2026-06-26 21:13:15 +08:00
1 parent 5ca0e6d83a
commit c68aef4444
150 files changed
+1038 -690

No files matched your search

@@ -11,19 +11,20 @@ import {
} from '@/components/ui/table';
import { RefreshCw, ChevronRight, BarChart3, Download, Cpu, HardDrive } from 'lucide-react';
import { kubernetesApi } from '@/api';
import type { JsonData } from '@/types/common';
export default function CapacityPage() {
const { clusterCode } = useParams<{ clusterCode: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [capacity, setCapacity] = useState<any[]>([]);
const [clusters, setClusters] = useState<any[]>([]);
const [capacity, setCapacity] = useState<JsonData[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [selectedNs, setSelectedNs] = useState('all');
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
useEffect(() => {
kubernetesApi.getClusters().then(( res: any) => {
kubernetesApi.getClusters().then((res: JsonData) => {
const list = res?.list || [];
setClusters(list);
if (!activeCluster && list.length > 0) {
@@ -36,7 +37,7 @@ export default function CapacityPage() {
useEffect(() => {
if (!activeCluster) return;
kubernetesApi.getNamespaces(activeCluster).then(( res: any) => {
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
}).catch(() => {});
}, [activeCluster]);
@@ -46,7 +47,7 @@ export default function CapacityPage() {
setLoading(true);
try {
const ns = selectedNs === 'all' ? undefined : selectedNs;
const res: Record<string, any> = await kubernetesApi.getCapacity(activeCluster, ns);
const res: JsonData = await kubernetesApi.getCapacity(activeCluster, ns);
setCapacity(Array.isArray(res) ? res : (res?.list || []));
} catch { setCapacity([]); } finally { setLoading(false); }
}, [activeCluster, selectedNs]);
@@ -54,8 +55,8 @@ export default function CapacityPage() {
useEffect(() => { loadCapacity(); }, [loadCapacity]);
const rows = useMemo(() =>
capacity.flatMap(( d: any) =>
(d.containers || []).map(( c: any) => ({
capacity.flatMap((d: JsonData) =>
(d.containers || []).map((c: JsonData) => ({
namespace: d.namespace,
deployment: d.name,
replicas: d.replicas,
@@ -83,7 +84,7 @@ export default function CapacityPage() {
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/capacity`); }}>
<SelectTrigger className="w-40 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
{clusters.map(( c: any) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
{clusters.map((c: JsonData) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
</SelectContent>
</Select>
<Select value={selectedNs} onValueChange={setSelectedNs}>
@@ -25,20 +25,21 @@ import {
} from 'lucide-react';
import { kubernetesApi } from '@/api';
import type { JsonData } from '@/types/common';
const CHART_COLORS = ['#3B82F6', '#207060', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#14B8A6', '#F97316'];
export default function ClusterComparePage() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<any>(null);
const [data, setData] = useState<JsonData>(null);
const [error, setError] = useState<string>('');
const loadData = useCallback(async () => {
setLoading(true);
setError('');
try {
const res: Record<string, any> = await kubernetesApi.getMultiClusterComparison();
const res: JsonData = await kubernetesApi.getMultiClusterComparison();
setData(res || null);
} catch (e) {
setError(e?.message || '加载失败');
@@ -50,11 +51,11 @@ export default function ClusterComparePage() {
useEffect(() => { loadData(); }, [loadData]);
const clusters: Record<string, any>[] = data?.clusters || [];
const totals: Record<string, any> = data?.totals || {};
const clusters: JsonData[] = data?.clusters || [];
const totals: JsonData = data?.totals || {};
// 图表数据准备
const resourceBarData = clusters.map(( c: any) => ({
const resourceBarData = clusters.map((c: JsonData) => ({
name: c.code || c.name,
节点: c.node_count || 0,
Pod: c.pod_count || 0,
@@ -62,22 +63,22 @@ export default function ClusterComparePage() {
Service: c.service_count || 0,
}));
const usageBarData = clusters.map(( c: any) => ({
const usageBarData = clusters.map((c: JsonData) => ({
name: c.code || c.name,
'CPU %': Number((c.cpu_usage || 0).toFixed(1)),
'内存 %': Number((c.memory_usage || 0).toFixed(1)),
}));
const radarData = clusters.length > 0 ? [
{ metric: '节点数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, c.node_count || 0])) },
{ metric: 'Pod数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, Math.min(c.pod_count || 0, 100)])) },
{ metric: '部署数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, c.deployment_count || 0])) },
{ metric: 'CPU使用', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, Number((c.cpu_usage || 0).toFixed(1))])) },
{ metric: '内存使用', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, Number((c.memory_usage || 0).toFixed(1))])) },
{ metric: 'CPU核数', ...Object.fromEntries(clusters.map(( c: any) => [c.code || c.name, c.total_cpu_cores || 0])) },
{ metric: '节点数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.node_count || 0])) },
{ metric: 'Pod数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Math.min(c.pod_count || 0, 100)])) },
{ metric: '部署数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.deployment_count || 0])) },
{ metric: 'CPU使用', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.cpu_usage || 0).toFixed(1))])) },
{ metric: '内存使用', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, Number((c.memory_usage || 0).toFixed(1))])) },
{ metric: 'CPU核数', ...Object.fromEntries(clusters.map((c: JsonData) => [c.code || c.name, c.total_cpu_cores || 0])) },
] : [];
const nodeRolePie = clusters.flatMap(( c: any) => [
const nodeRolePie = clusters.flatMap((c: JsonData) => [
{ name: `${c.code}-Master`, value: c.master_count || 0, fill: CHART_COLORS[1] },
{ name: `${c.code}-Worker`, value: c.worker_count || 0, fill: CHART_COLORS[0] },
]).filter(d => d.value > 0);
@@ -221,7 +222,7 @@ export default function ClusterComparePage() {
<PolarGrid stroke="#1e293b" />
<PolarAngleAxis dataKey="metric" tick={{ fill: '#94a3b8', fontSize: 11 }} />
<PolarRadiusAxis tick={{ fill: '#64748b', fontSize: 10 }} />
{clusters.map(( c: any, i: number) => (
{clusters.map((c: JsonData, i: number) => (
<Radar key={i} name={c.code || c.name}
dataKey={c.code || c.name}
stroke={CHART_COLORS[i % CHART_COLORS.length]}
@@ -248,7 +249,7 @@ export default function ClusterComparePage() {
<PieChart>
<Pie data={nodeRolePie} cx="50%" cy="45%" innerRadius={50} outerRadius={90}
paddingAngle={3} dataKey="value"
label={( e: any) => `${e.name}: ${e.value}`}
label={(e: JsonData) => `${e.name}: ${e.value}`}
labelLine={{ stroke: '#475569' }}
>
{nodeRolePie.map((_, i) => (
@@ -293,7 +294,7 @@ export default function ClusterComparePage() {
</TableRow>
</TableHeader>
<TableBody>
{clusters.map(( c: any) => (
{clusters.map((c: JsonData) => (
<TableRow key={c.id}
className="border-slate-800/50 hover:bg-slate-800/30 cursor-pointer"
onClick={() => navigate(`/kubernetes/${c.code}`)}
@@ -22,8 +22,9 @@ import {
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import type { JsonData } from '@/types/common';
const NAMESPACE_ICON: Record<string, React.ComponentType<any>> = {
const NAMESPACE_ICON: Record<string, React.ComponentType<Record<string, unknown>>> = {
'kube-system': Server,
'kube-public': Shield,
'kube-node-lease': Activity,
@@ -35,19 +36,19 @@ export default function ClusterOverviewPage() {
const navigate = useNavigate();
const { toast } = useToast();
const [, setClusters] = useState<any[]>([]);
const [nodes, setNodes] = useState<any[]>([]);
const [, setClusters] = useState<JsonData[]>([]);
const [nodes, setNodes] = useState<JsonData[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [clusterInfo, setClusterInfo] = useState<any>(null);
const [clusterInfo, setClusterInfo] = useState<JsonData>(null);
const [loading, setLoading] = useState(false);
// Load cluster list + current cluster info
const loadClusterInfo = useCallback(async () => {
try {
const res: Record<string, any> = await kubernetesApi.getClusters();
const res: JsonData = await kubernetesApi.getClusters();
const list = res?.list || [];
setClusters(list);
const current = list.find(( c: any) => (c.code || String(c.id)) === clusterCode);
const current = list.find((c: JsonData) => (c.code || String(c.id)) === clusterCode);
setClusterInfo(current || null);
if (!current && list.length > 0 && clusterCode) {
toast({ title: '集群未找到', description: `找不到集群 ${clusterCode}`, variant: 'destructive' });
@@ -72,7 +73,7 @@ export default function ClusterOverviewPage() {
useEffect(() => { loadClusterInfo(); }, [loadClusterInfo]);
useEffect(() => { loadData(); }, [loadData]);
const getNodeRole = ( node: any) => {
const getNodeRole = (node: JsonData) => {
if (node?.role === 'master' || node?.role === 'control-plane') return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
return 'bg-slate-500/20 text-slate-400 border-slate-500/30';
};
@@ -157,7 +158,7 @@ export default function ClusterOverviewPage() {
) : nodes.length === 0 ? (
<TableRow><TableCell colSpan={8} className="text-center text-slate-500 py-12"><Cpu className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无节点</TableCell></TableRow>
) : (
nodes.map(( n: any) => {
nodes.map((n: JsonData) => {
const cpuCores = n.cpu || 0;
const cpuUsageM = n.cpu_usage_m ? parseFloat(n.cpu_usage_m.replace('m','')) : 0;
const cpuPct = cpuCores > 0 ? (cpuUsageM / 1000 / cpuCores) * 100 : 0;
@@ -15,13 +15,14 @@ import { DeploymentLogsTab } from './deployment-detail/DeploymentLogsTab';
import { DeploymentHpaTab } from './deployment-detail/DeploymentHpaTab';
import { DeploymentPodModals } from './deployment-detail/DeploymentPodModals';
import { YamlViewer } from './YamlViewer';
import type { JsonData } from '@/types/common';
export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBack }: DeploymentDetailProps) {
const { toast } = useToast();
const confirm = useConfirm();
const [deployment, setDeployment] = useState<any>(null);
const [pods, setPods] = useState<any[]>([]);
const [hpas, setHpas] = useState<any[]>([]);
const [deployment, setDeployment] = useState<JsonData>(null);
const [pods, setPods] = useState<JsonData[]>([]);
const [hpas, setHpas] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [actionLoading, setActionLoading] = useState('');
const [, setScaleValue] = useState(0);
@@ -45,19 +46,19 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []),
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => ({ items: [] })),
]);
const toArray = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || []));
const allDeps = toArray(depRes);
const allPods = toArray(podRes);
const allHpas = toArray(hpaRes);
const dep = allDeps.find((d: any) => d.name === deploymentName && d.namespace === namespace);
const dep = allDeps.find((d: JsonData) => d.name === deploymentName && d.namespace === namespace);
setDeployment(dep || null);
if (dep) {
setScaleValue(dep.replicas || 0);
setNewImage(dep.image || '');
}
setPods(allPods.filter((p: any) => p.namespace === namespace && p.name.startsWith(deploymentName)));
setHpas(allHpas.filter((h: any) => h.target_name === deploymentName && h.namespace === namespace));
setPods(allPods.filter((p: JsonData) => p.namespace === namespace && p.name.startsWith(deploymentName)));
setHpas(allHpas.filter((h: JsonData) => h.target_name === deploymentName && h.namespace === namespace));
} catch (e) {
console.error('加载详情失败:', e);
} finally {
@@ -103,7 +104,7 @@ export function DeploymentDetail({ clusterCode, namespace, deploymentName, onBac
} finally { setActionLoading(''); }
};
const handleHpaToggle = async (hpa: any) => {
const handleHpaToggle = async (hpa: JsonData) => {
const ok = await confirm({
title: '删除 HPA',
description: `确定删除 HPA "${hpa.name}"?`,
@@ -14,13 +14,14 @@ 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: any | null;
onSaved: (updated?: any) => void;
manifest: JsonData | null;
onSaved: (updated?: JsonData) => void;
onCancel: () => void;
}
@@ -36,7 +37,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
const { toast } = useToast();
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(!manifest);
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
// 更新策略
const [strategyType, setStrategyType] = useState<'RollingUpdate' | 'Recreate'>('RollingUpdate');
@@ -119,13 +120,13 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
// 加载节点列表
if (clusterCode) {
setNodesLoading(true);
kubernetesApi.getClusterNodes(clusterCode).then((res: any) => {
const nodes: Record<string, any>[] = Array.isArray(res) ? res : (res?.items || res?.list || []);
const mapped = nodes.map((n: any) => {
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: Record<string, any>[] = Array.isArray(status.addresses) ? status.addresses : [];
const internalIP = addresses.find((a: any) => a.type === 'InternalIP')?.address || '';
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);
@@ -143,7 +144,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
}
// tolerations
const tols: Record<string, any>[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : [];
const tols: JsonData[] = Array.isArray(podSpec.tolerations) ? podSpec.tolerations : [];
setTolerations(tols.map(t => ({
key: t.key || '',
operator: t.operator || 'Equal',
@@ -172,7 +173,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
setShareProcessNamespace(!!podSpec.shareProcessNamespace);
// hostAliases(M8 补全)
const ha: Record<string, any>[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : [];
const ha: JsonData[] = Array.isArray(podSpec.hostAliases) ? podSpec.hostAliases : [];
setHostAliases(ha.map(a => ({
ip: a.ip || '',
hostnames: Array.isArray(a.hostnames) ? a.hostnames.join(', ') : '',
@@ -182,7 +183,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
const dc = podSpec.dnsConfig || {};
setDnsNameservers(Array.isArray(dc.nameservers) ? dc.nameservers.join('\n') : '');
setDnsSearches(Array.isArray(dc.searches) ? dc.searches.join('\n') : '');
const opts: Record<string, any>[] = Array.isArray(dc.options) ? dc.options : [];
const opts: JsonData[] = Array.isArray(dc.options) ? dc.options : [];
setDnsOptions(opts.map(o => ({ name: o.name || '', value: o.value || '' })));
setLoading(false);
@@ -229,7 +230,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
if (podSpec.nodeName) delete podSpec.nodeName;
if (podSpec.affinity) delete podSpec.affinity.nodeAffinity;
} else if (scheduleMode === 'nodeAffinity') {
const matchExpressions: Record<string, any>[] = labelSelectorKv
const matchExpressions: JsonData[] = labelSelectorKv
.filter(s => s.key.trim())
.map(s => ({ key: s.key.trim(), operator: 'In', values: [s.value] }));
if (matchExpressions.length > 0) {
@@ -250,7 +251,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
const tolsK8s = tolerations
.filter(t => t.key || t.operator === 'Exists')
.map(t => {
const out: Record<string, any> = {};
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;
@@ -269,7 +270,7 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
if (serviceAccountName) podSpec.serviceAccountName = serviceAccountName;
// Pod 安全上下文
const sc: Record<string, any> = {};
const sc: JsonData = {};
if (podFsGroup) sc.fsGroup = Number(podFsGroup);
if (podRunAsUser) sc.runAsUser = Number(podRunAsUser);
if (podRunAsGroup) sc.runAsGroup = Number(podRunAsGroup);
@@ -295,13 +296,13 @@ export function DeploymentEditAdvanced({ clusterCode, namespace, deploymentName,
else delete podSpec.hostAliases;
// dnsConfig(M8 补全)— 仅 dnsPolicy=None 时 dnsConfig 才有意义,但仍允许编辑
const dcObj: Record<string, any> = {};
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: Record<string, any> = { name: o.name.trim() };
const out: JsonData = { name: o.name.trim() };
if (o.value.trim()) out.value = o.value.trim();
return out;
});
@@ -10,13 +10,14 @@ import { Badge } from '@/components/ui/badge';
import { Plus, Trash2, Save, Loader2, Tags, FileText, Hash, Lock } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { saveDeploymentManifest } from './deployment-manifest-save';
import type { JsonData } from '@/types/common';
interface DeploymentEditBasicProps {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: any | null;
onSaved: (updated?: any) => void;
manifest: JsonData | null;
onSaved: (updated?: JsonData) => void;
onCancel: () => void;
}
@@ -40,7 +41,7 @@ export function DeploymentEditBasic({ clusterCode, namespace, deploymentName, ma
const { toast } = useToast();
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(!manifest);
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
const [replicas, setReplicas] = useState(1);
const [labels, setLabels] = useState<Array<{ key: string; value: string }>>([]);
const [annotations, setAnnotations] = useState<Array<{ key: string; value: string }>>([]);
@@ -25,12 +25,13 @@ import {
toContainerForm, emptyContainer, containerToK8s,
} from './deployment-edit/container-utils';
import type { ContainerForm, DeploymentEditContainersProps } from './deployment-edit/types';
import type { JsonData } from '@/types/common';
export function DeploymentEditContainers({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: DeploymentEditContainersProps) {
const { toast } = useToast();
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(!manifest);
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
const [containers, setContainers] = useState<ContainerForm[]>([]);
const [initContainers, setInitContainers] = useState<ContainerForm[]>([]);
const [imagePullSecrets, setImagePullSecrets] = useState<string[]>([]);
@@ -41,7 +42,7 @@ export function DeploymentEditContainers({ clusterCode, namespace, deploymentNam
const podSpec = manifest?.spec?.template?.spec || {};
setContainers((Array.isArray(podSpec.containers) ? podSpec.containers : []).map(toContainerForm));
setInitContainers((Array.isArray(podSpec.initContainers) ? podSpec.initContainers : []).map(toContainerForm));
setImagePullSecrets((Array.isArray(podSpec.imagePullSecrets) ? podSpec.imagePullSecrets : []).map((s: any) => s.name || ''));
setImagePullSecrets((Array.isArray(podSpec.imagePullSecrets) ? podSpec.imagePullSecrets : []).map((s: JsonData) => s.name || ''));
setLoading(false);
}, [manifest]);
@@ -27,13 +27,14 @@ import {
emptyVolumeEntry,
emptyMountRow,
} from './deployment-volume-utils';
import type { JsonData } from '@/types/common';
interface Props {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: any | null;
onSaved: (updated?: any) => void;
manifest: JsonData | null;
onSaved: (updated?: JsonData) => void;
onCancel: () => void;
}
@@ -54,10 +55,10 @@ export function DeploymentEditStorage({ clusterCode, namespace, deploymentName,
const { toast } = useToast();
const [saving, setSaving] = useState(false);
const [loading, setLoading] = useState(!manifest);
const [rawManifest, setRawManifest] = useState<any>(manifest || {});
const [rawManifest, setRawManifest] = useState<JsonData>(manifest || {});
const [entries, setEntries] = useState<VolumeEntry[]>([]);
const [containers, setContainers] = useState<string[]>([]);
const [passthroughVolumes, setPassthroughVolumes] = useState<Record<string, any>[]>([]);
const [passthroughVolumes, setPassthroughVolumes] = useState<JsonData[]>([]);
const [resources, setResources] = useState<NamespaceResourceLists>({ configMaps: [], secrets: [], pvcs: [] });
const [resourcesLoading, setResourcesLoading] = useState(false);
const [createResource, setCreateResource] = useState<CreateResourceKind | null>(null);
@@ -24,6 +24,7 @@ import { HistoryTab } from './deployment-view/HistoryTab';
import type { DeploymentViewProps, SvcItem, IngItem, EventItem } from './deployment-view/types';
export type { DeploymentViewProps } from './deployment-view/types';
import type { JsonData } from '@/types/common';
export function DeploymentView(props: DeploymentViewProps) {
const { clusterCode, namespace, deployment, pods, revisions, onRefresh } = props;
@@ -44,8 +45,8 @@ export function DeploymentView(props: DeploymentViewProps) {
const [tabLoading, setTabLoading] = useState('');
// HPA / CronHPA state // Logan.Li
const [hpas, setHpas] = useState<any[]>([]);
const [cronHpas, setCronHpas] = useState<any[]>([]);
const [hpas, setHpas] = useState<JsonData[]>([]);
const [cronHpas, setCronHpas] = useState<JsonData[]>([]);
const ready = deployment?.ready_replicas ?? deployment?.readyReplicas ?? 0;
const total = deployment?.replicas ?? 0;
@@ -59,37 +60,37 @@ export function DeploymentView(props: DeploymentViewProps) {
kubernetesApi.getNamespaceServices(clusterCode, namespace).catch(() => []),
kubernetesApi.getNamespaceIngresses(clusterCode, namespace).catch(() => []),
]).then(([svcRes, ingRes]) => {
const toArr = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
const toArr = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
const allSvcs = toArr(svcRes);
const allIngs = toArr(ingRes);
const selector = deployment?.spec?.selector?.matchLabels || deployment?.spec?.template?.metadata?.labels || {};
const matchedSvcs = allSvcs.filter((s: any) => {
const matchedSvcs = allSvcs.filter((s: JsonData) => {
const svcSel = s.spec?.selector || s.selector || {};
if (Object.keys(svcSel).length === 0) return false;
return Object.entries(svcSel).every(([k, v]) => selector[k] === v);
}).map((s: any) => ({
}).map((s: JsonData) => ({
name: s.name || s.metadata?.name,
type: s.spec?.type || s.type || 'ClusterIP',
clusterIP: s.spec?.clusterIP || s.cluster_ip || '<none>',
ports: (s.spec?.ports || s.ports || []).map((p: any) => `${p.port}${p.targetPort ? ':' + p.targetPort : ''}/${p.protocol || 'TCP'}`),
ports: (s.spec?.ports || s.ports || []).map((p: JsonData) => `${p.port}${p.targetPort ? ':' + p.targetPort : ''}/${p.protocol || 'TCP'}`),
selector: s.spec?.selector || s.selector || {},
age: s.age || '',
}));
const svcNames = new Set(matchedSvcs.map((s: any) => s.name));
const matchedIngs = allIngs.filter((i: any) => {
const svcNames = new Set(matchedSvcs.map((s: JsonData) => s.name));
const matchedIngs = allIngs.filter((i: JsonData) => {
const rules = i.spec?.rules || i.rules || [];
return rules.some((r: any) =>
(r.http?.paths || r.paths || []).some((p: any) => {
return rules.some((r: JsonData) =>
(r.http?.paths || r.paths || []).some((p: JsonData) => {
const backendSvc = p.backend?.service?.name || p.backendService || '';
return svcNames.has(backendSvc);
})
);
}).map((i: any) => ({
}).map((i: JsonData) => ({
name: i.name || i.metadata?.name,
className: i.spec?.ingressClassName || i.ingressClass || '-',
hosts: (i.spec?.rules || i.rules || []).map((r: any) => r.host).filter(Boolean),
hosts: (i.spec?.rules || i.rules || []).map((r: JsonData) => r.host).filter(Boolean),
rules: i.spec?.rules || i.rules || [],
age: i.age || '',
}));
@@ -103,16 +104,16 @@ export function DeploymentView(props: DeploymentViewProps) {
useEffect(() => {
if (tab !== 'events') return;
setTabLoading('events');
kubernetesApi.getEvents(clusterCode, namespace, undefined).then((res: any) => {
kubernetesApi.getEvents(clusterCode, namespace, undefined).then((res: JsonData) => {
const allEvents = Array.isArray(res) ? res : (res?.list || res?.items || res?.data || []);
const depName = deployment?.name || '';
const depEvents = allEvents.filter((e: any) => {
const depEvents = allEvents.filter((e: JsonData) => {
const obj = String(e.object || '');
if (!depName || !obj) return false;
if (obj.endsWith(`/${depName}`)) return true;
const name = obj.split('/').pop() || '';
return name === depName || name.startsWith(`${depName}-`);
}).map((e: any) => normalizeK8sEvent(e));
}).map((e: JsonData) => normalizeK8sEvent(e));
setEvents(depEvents);
}).catch(() => {}).finally(() => setTabLoading(''));
}, [tab, clusterCode, namespace, deployment?.name]);
@@ -125,16 +126,16 @@ export function DeploymentView(props: DeploymentViewProps) {
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => []),
Promise.resolve([]), // CronHPA 待后端支持
]).then(([hpaRes]) => {
const toArr = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
const toArr = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
const allHpas = toArr(hpaRes);
const depName = deployment?.name || '';
setHpas(allHpas.filter((h: any) => (h.scaleTargetRef?.name || h.targetRef?.name || '') === depName));
setHpas(allHpas.filter((h: JsonData) => (h.scaleTargetRef?.name || h.targetRef?.name || '') === depName));
setCronHpas([]);
}).catch(() => {}).finally(() => setTabLoading(''));
}, [tab, clusterCode, namespace, deployment?.name]);
// 回滚 // Logan.Li
const handleRollback = async (rev: any) => {
const handleRollback = async (rev: JsonData) => {
const revision = rev.revision ?? rev.revision_number ?? rev.number;
const ok = await confirm({
title: '回滚版本',
@@ -9,6 +9,7 @@ import { RefreshCw, ChevronRight, Bell, AlertTriangle, X } from 'lucide-react';
import { kubernetesApi } from '@/api';
import { EventTable } from './EventTable';
import { normalizeK8sEvent, formatEventRelative } from './k8s-event-utils';
import type { JsonData } from '@/types/common';
interface RankItem { name: string; count: number }
@@ -76,20 +77,20 @@ export default function EventsPage() {
const { clusterCode } = useParams<{ clusterCode: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [events, setEvents] = useState<any[]>([]);
const [clusters, setClusters] = useState<any[]>([]);
const [events, setEvents] = useState<JsonData[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [selectedNs, setSelectedNs] = useState('all');
const [filterType, setFilterType] = useState('all');
const [timeWindow, setTimeWindow] = useState('0');
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
const [summary, setSummary] = useState<any>(null);
const [summary, setSummary] = useState<JsonData>(null);
const [selectedReason, setSelectedReason] = useState('');
const [selectedKind, setSelectedKind] = useState('');
useEffect(() => {
kubernetesApi.getClusters().then((res: any) => {
kubernetesApi.getClusters().then((res: JsonData) => {
const list = res?.list || [];
setClusters(list);
if (!activeCluster && list.length > 0) {
@@ -102,7 +103,7 @@ export default function EventsPage() {
useEffect(() => {
if (!activeCluster) return;
kubernetesApi.getNamespaces(activeCluster).then((res: any) => {
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
}).catch(() => {});
}, [activeCluster]);
@@ -113,7 +114,7 @@ export default function EventsPage() {
try {
const ns = selectedNs === 'all' ? undefined : selectedNs;
const ft = filterType === 'all' ? undefined : filterType;
const res: Record<string, any> = await kubernetesApi.getEvents(activeCluster, ns, ft);
const res: JsonData = await kubernetesApi.getEvents(activeCluster, ns, ft);
setEvents(Array.isArray(res) ? res : (res?.list || []));
} catch { setEvents([]); } finally { setLoading(false); }
}, [activeCluster, selectedNs, filterType]);
@@ -123,7 +124,7 @@ export default function EventsPage() {
try {
const ns = selectedNs === 'all' ? undefined : selectedNs;
const hours = parseInt(timeWindow) || undefined;
const res: Record<string, any> = await kubernetesApi.getEventsSummary(activeCluster, ns, hours);
const res: JsonData = await kubernetesApi.getEventsSummary(activeCluster, ns, hours);
setSummary(res || null);
} catch { setSummary(null); }
}, [activeCluster, selectedNs, timeWindow]);
@@ -151,15 +152,15 @@ export default function EventsPage() {
const normalCount = useMemo(() => events.filter((e) => e.type === 'Normal').length, [events]);
const reasonData = useMemo<RankItem[]>(() =>
(summary?.by_reason || []).map((r: any) => ({ name: r.reason || 'Unknown', count: r.count })),
(summary?.by_reason || []).map((r: JsonData) => ({ name: r.reason || 'Unknown', count: r.count })),
[summary]);
const nsData = useMemo<RankItem[]>(() =>
(summary?.by_namespace || []).map((n: any) => ({ name: n.namespace || '<cluster>', count: n.count })),
(summary?.by_namespace || []).map((n: JsonData) => ({ name: n.namespace || '<cluster>', count: n.count })),
[summary]);
const kindData = useMemo<RankItem[]>(() =>
(summary?.by_kind || []).map((k: any) => ({ name: k.kind || 'Unknown', count: k.count })),
(summary?.by_kind || []).map((k: JsonData) => ({ name: k.kind || 'Unknown', count: k.count })),
[summary]);
return (
@@ -185,7 +186,7 @@ export default function EventsPage() {
<Select value={activeCluster} onValueChange={(v) => { setActiveCluster(v); navigate(`/kubernetes/${v}/events`); }}>
<SelectTrigger className="w-40 h-8 text-xs"><SelectValue /></SelectTrigger>
<SelectContent>
{clusters.map((c: any) => (
{clusters.map((c: JsonData) => (
<SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>
))}
</SelectContent>
@@ -260,7 +261,7 @@ export default function EventsPage() {
最新告警 ({summary.recent_warnings.length})
</h3>
<div className="space-y-1">
{summary.recent_warnings.map((w: any, i: number) => (
{summary.recent_warnings.map((w: JsonData, i: number) => (
<div key={i} className="text-xs py-1.5 px-2 rounded hover:bg-[var(--dg-bg-tertiary)]/50">
<div className="flex items-center gap-2 flex-wrap">
<span className="shrink-0 font-mono text-[var(--dg-warning)]">{w.reason}</span>
@@ -3,6 +3,7 @@ import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Loader2 } from 'lucide-react';
import { kubernetesApi } from '@/api';
import type { JsonData } from '@/types/common';
interface Props {
/** 集群下路径,如 events、namespace/default/deployments */
@@ -14,7 +15,7 @@ export default function K8sShortcutRedirect({ target }: Props) {
useEffect(() => {
kubernetesApi.getClusters()
.then((res: any) => {
.then((res: JsonData) => {
const list = res?.list || [];
if (list.length > 0) {
const code = list[0].code || String(list[0].id);
@@ -14,16 +14,17 @@ import {
Tooltip,
ResponsiveContainer,
} from 'recharts';
import type { JsonData } from '@/types/common';
// 资源使用趋势数据 — 从节点 metrics 实时计算(不再用硬编码 mock) // Logan.Li
// 由于当前无 time-series 存储,用当前快照展示一个合理的汇总值
export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; onClose: () => void }) {
const [nodes, setNodes] = useState<any[]>([]);
const [pods, setPods] = useState<any[]>([]);
const [deployments, setDeployments] = useState<any[]>([]);
const [services, setServices] = useState<any[]>([]);
const [ingresses, setIngresses] = useState<any[]>([]);
export function KubernetesClusterDetail({ cluster, onClose }: { cluster?: JsonData; onClose: () => void }) {
const [nodes, setNodes] = useState<JsonData[]>([]);
const [pods, setPods] = useState<JsonData[]>([]);
const [deployments, setDeployments] = useState<JsonData[]>([]);
const [services, setServices] = useState<JsonData[]>([]);
const [ingresses, setIngresses] = useState<JsonData[]>([]);
// 从节点数据计算集群级 CPU/内存使用率(实时快照) // Logan.Li
const clusterMetrics = useMemo(() => {
@@ -60,7 +61,7 @@ export function KubernetesClusterDetail({ cluster, onClose }: { cluster: any; on
kubernetesApi.getServices(clusterId).catch(() => []),
kubernetesApi.getIngresses(clusterId).catch(() => []),
]);
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
setNodes(toArray(nodesRes));
setPods(toArray(podsRes));
setDeployments(toArray(depRes));
@@ -14,6 +14,7 @@ import {
} from '@/components/ui/dialog';
import { kubernetesApi } from '@/api';
import { toast } from 'sonner';
import type { JsonData } from '@/types/common';
// 名称即 code,只允许英文/数字/连字符
const NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
@@ -36,7 +37,7 @@ export function KubernetesClusterDialog({
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
editingCluster: any;
editingCluster?: JsonData;
onSaved: () => void;
}) {
const [formData, setFormData] = useState({ ...emptyForm });
@@ -48,7 +49,7 @@ export function KubernetesClusterDialog({
useEffect(() => {
if (!open) return;
if (editingCluster && editingCluster.id) {
kubernetesApi.getClusterDetail(editingCluster.id).then(( res: any) => {
kubernetesApi.getClusterDetail(editingCluster.id).then((res: JsonData) => {
const d = res || {};
setHasToken(!!d.has_token);
setHasKubeConfig(!!d.has_kube_config);
@@ -33,29 +33,30 @@ import type { KubernetesCluster } from '@/types';
import { KubernetesClusterDetail } from './KubernetesClusterDetail';
import { KubernetesClusterDialog } from './KubernetesClusterDialog';
import { KubectlTerminal } from './KubectlTerminal';
import type { JsonData } from '@/types/common';
export function KubernetesClusters() {
const [clusters, setClusters] = useState<any[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [selectedCluster, setSelectedCluster] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
// 创建/编辑集群弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editingCluster, setEditingCluster] = useState<any>(null);
const [editingCluster, setEditingCluster] = useState<JsonData>(null);
// kubectl 终端
const [kubectlCluster, setKubectlCluster] = useState<{id: number; name: string} | null>(null);
const [kubectlTerminalOpen, setKubectlTerminalOpen] = useState(false);
// 导出资源弹窗
const [exportCluster, setExportCluster] = useState<any>(null);
const [exportCluster, setExportCluster] = useState<JsonData>(null);
const [exporting, setExporting] = useState(false);
const loadData = async () => {
try {
setLoading(true);
const res: Record<string, any> = await kubernetesApi.getClusters();
const res: JsonData = await kubernetesApi.getClusters();
setClusters(res?.list || []);
} catch (e) {
console.error('加载集群列表失败:', e);
@@ -77,7 +78,7 @@ export function KubernetesClusters() {
if (!editingCluster) {
// 新建集群后,等列表刷新完再取最新一条同步
setTimeout(async () => {
const res: Record<string, any> = await kubernetesApi.getClusters();
const res: JsonData = await kubernetesApi.getClusters();
const list = res?.list || [];
if (list.length > 0) {
handleSync(list[list.length - 1].id, true);
@@ -99,7 +100,7 @@ export function KubernetesClusters() {
const handleSync = async (id: number | string, silent?: boolean) => {
try {
if (!silent) toast.info('正在同步集群资源...');
const res: Record<string, any> = await kubernetesApi.syncCluster(id);
const res: JsonData = await kubernetesApi.syncCluster(id);
if (!silent) toast.success(res?.message || '同步完成');
loadData();
} catch (e) {
@@ -11,18 +11,19 @@ import {
} from '@/components/ui/table';
import { RefreshCw, Search, Server, Loader2 } from 'lucide-react';
import { kubernetesApi } from '@/api';
import type { JsonData } from '@/types/common';
export function KubernetesNodes() {
const [clusters, setClusters] = useState<any[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [selectedCluster, setSelectedCluster] = useState('');
const [nodes, setNodes] = useState<any[]>([]);
const [nodes, setNodes] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const [roleFilter, setRoleFilter] = useState('all');
const loadClusters = useCallback(async () => {
try {
const res: Record<string, any> = await kubernetesApi.getClusters();
const res: JsonData = await kubernetesApi.getClusters();
const list = res?.list || [];
setClusters(list);
if (list.length > 0 && !selectedCluster) setSelectedCluster(String(list[0].id));
@@ -35,7 +36,7 @@ export function KubernetesNodes() {
if (!selectedCluster) return;
setLoading(true);
try {
const res: Record<string, any> = await kubernetesApi.getNodes(selectedCluster);
const res: JsonData = await kubernetesApi.getNodes(selectedCluster);
setNodes(Array.isArray(res) ? res : (res?.list || []));
} catch { setNodes([]); } finally { setLoading(false); }
}, [selectedCluster]);
@@ -25,15 +25,16 @@ import {
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { DeploymentDetail } from './DeploymentDetail';
import type { JsonData } from '@/types/common';
export function KubernetesWorkloads() {
const { toast } = useToast();
const [clusters, setClusters] = useState<any[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [selectedCluster, setSelectedCluster] = useState('');
const [namespaces, setNamespaces] = useState<string[]>([]);
const [selectedNamespace, setSelectedNamespace] = useState('all');
const [deployments, setDeployments] = useState<any[]>([]);
const [pods, setPods] = useState<any[]>([]);
const [deployments, setDeployments] = useState<JsonData[]>([]);
const [pods, setPods] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const [activeTab, setActiveTab] = useState('deployments');
@@ -44,7 +45,7 @@ export function KubernetesWorkloads() {
const loadClusters = useCallback(async () => {
try {
const res: Record<string, any> = await kubernetesApi.getClusters();
const res: JsonData = await kubernetesApi.getClusters();
const list = res?.list || [];
setClusters(list);
if (list.length > 0 && !selectedCluster) setSelectedCluster(list[0].code || String(list[0].id));
@@ -57,7 +58,7 @@ export function KubernetesWorkloads() {
const loadNamespaces = useCallback(async () => {
if (!selectedCluster) return;
try {
const res: Record<string, any> = await kubernetesApi.getNamespaces(selectedCluster);
const res: JsonData = await kubernetesApi.getNamespaces(selectedCluster);
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
setNamespaces(nsList);
} catch { setNamespaces([]); }
@@ -75,7 +76,7 @@ export function KubernetesWorkloads() {
kubernetesApi.getNamespaceDeployments(clusterCode, selectedNamespace).catch(() => []),
kubernetesApi.getNamespacePods(clusterCode, selectedNamespace).catch(() => []),
]);
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
setDeployments(toArray(depRes));
setPods(toArray(podRes));
} else {
@@ -83,7 +84,7 @@ export function KubernetesWorkloads() {
kubernetesApi.getAllDeployments(clusterCode).catch(() => []),
kubernetesApi.getAllPods(clusterCode).catch(() => []),
]);
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
setDeployments(toArray(depRes));
setPods(toArray(podRes));
}
@@ -111,7 +112,7 @@ export function KubernetesWorkloads() {
return <Badge className={map[status] || 'bg-slate-500/20 text-slate-400 border-slate-500/30'}>{status || 'Unknown'}</Badge>;
};
const handleQuickScale = async ( dep: any, delta: number) => {
const handleQuickScale = async (dep: JsonData, delta: number) => {
const key = `scale-${dep.name}`;
setActionLoading((prev) => ({ ...prev, [key]: 'loading' }));
try {
@@ -14,6 +14,7 @@ import {
} from 'lucide-react';
import { kubernetesApi } from '@/api';
import type { JsonData } from '@/types/common';
const RESOURCE_LINKS = [
{ type: 'deployments', label: 'Deployments', icon: Boxes, desc: '工作负载副本管理' },
@@ -45,7 +46,7 @@ export default function NamespaceOverviewPage() {
kubernetesApi.getHPAs(clusterCode, namespace!),
kubernetesApi.getCronJobs(clusterCode, namespace!),
]);
const toArr = ( v: any) => Array.isArray(v) ? v : (v?.list || v?.data || []);
const toArr = (v: JsonData) => Array.isArray(v) ? v : (v?.list || v?.data || []);
setCounts({
deployments: toArr(results[0].status === 'fulfilled' ? results[0].value : []).length,
pods: toArr(results[1].status === 'fulfilled' ? results[1].value : []).length,
@@ -22,6 +22,7 @@ import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import type { JsonData } from '@/types/common';
type ResourceType =
| 'deployments' | 'pods' | 'services' | 'ingresses'
@@ -46,9 +47,9 @@ const VisualActions = ({ name, opts }: { name: string; opts: { onYaml?: (n: stri
const RESOURCE_CONFIG: Record<ResourceType, {
title: string;
subtitle: string;
icon: React.ComponentType<any>;
icon: React.ComponentType<Record<string, unknown>>;
columns: string[];
renderRow: ( item: any, opts: { clusterCode: string; namespace: string; onDetail?: ( item: any) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void; onTerminal?: ( item: any) => void; onFileBrowser?: ( item: any) => void }) => React.ReactNode;
renderRow: (item: JsonData, opts: { clusterCode: string; namespace: string; onDetail?: (item: JsonData) => void; onYaml?: (name: string) => void; onEdit?: (name: string) => void; onTerminal?: (item: JsonData) => void; onFileBrowser?: (item: JsonData) => void }) => React.ReactNode;
}> = {
deployments: {
title: 'Deployments', subtitle: 'Deployment 工作负载运行状态', icon: Boxes,
@@ -97,9 +98,9 @@ const RESOURCE_CONFIG: Record<ResourceType, {
<TableCell>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10" title="终端"
onClick={() => (opts as Record<string, any>).onTerminal?.(p)}><Terminal className="h-3.5 w-3.5 mr-1" />终端</Button>
onClick={() => (opts as JsonData).onTerminal?.(p)}><Terminal className="h-3.5 w-3.5 mr-1" />终端</Button>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-amber-400 hover:text-amber-300 hover:bg-amber-500/10" title="文件浏览器"
onClick={() => (opts as Record<string, any>).onFileBrowser?.(p)}><HardDrive className="h-3.5 w-3.5 mr-1" />文件浏览器</Button>
onClick={() => (opts as JsonData).onFileBrowser?.(p)}><HardDrive className="h-3.5 w-3.5 mr-1" />文件浏览器</Button>
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-blue-400 hover:text-blue-300 hover:bg-blue-500/10" title="查看YAML"
onClick={() => opts.onYaml?.(p.name)}><FileCode className="h-3.5 w-3.5 mr-1" />YAML</Button>
</div>
@@ -331,20 +332,20 @@ export default function NamespaceResourcePage() {
}, [location.pathname]);
const config = RESOURCE_CONFIG[type];
const [clusters, setClusters] = useState<any[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [data, setData] = useState<any[]>([]);
const [data, setData] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState('');
const [yamlItem, setYamlItem] = useState<{resource: string; name: string; editMode?: boolean} | null>(null);
const [visualItem, setVisualItem] = useState<{ resource: string; name: string; isCreate?: boolean } | null>(null);
const [showYamlImport, setShowYamlImport] = useState(false);
const [terminalPod, setTerminalPod] = useState<any>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<any>(null);
const [terminalPod, setTerminalPod] = useState<JsonData>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<JsonData>(null);
useEffect(() => {
kubernetesApi.getClusters().then(( res: any) => {
kubernetesApi.getClusters().then((res: JsonData) => {
const list = res?.list || [];
setClusters(list);
if (!clusterCode && list.length > 0) {
@@ -356,7 +357,7 @@ export default function NamespaceResourcePage() {
useEffect(() => {
if (!clusterCode) return;
kubernetesApi.getNamespaces(clusterCode).then(( res: any) => {
kubernetesApi.getNamespaces(clusterCode).then((res: JsonData) => {
const nsList: string[] = Array.isArray(res) ? res : (res?.list || res?.data || []);
setNamespaces(nsList);
if (!namespace && nsList.length > 0) {
@@ -369,7 +370,7 @@ export default function NamespaceResourcePage() {
if (!clusterCode || !namespace) return;
setLoading(true);
try {
let res: Record<string, any> = [];
let res: JsonData = [];
switch (type) {
case 'deployments': res = await kubernetesApi.getNamespaceDeployments(clusterCode, namespace).catch(() => []); break;
case 'pods': res = await kubernetesApi.getNamespacePods(clusterCode, namespace).catch(() => []); break;
@@ -392,9 +393,9 @@ export default function NamespaceResourcePage() {
useEffect(() => { loadData(); }, [loadData]);
const filtered = data.filter(( d: any) => (d.name || '').toLowerCase().includes(search.toLowerCase()));
const filtered = data.filter((d: JsonData) => (d.name || '').toLowerCase().includes(search.toLowerCase()));
const handleDetail = ( item: any) => {
const handleDetail = (item: JsonData) => {
if (type === 'deployments') {
// Kuboard 风格双模式路由(默认 view 模式)
navigate(`/kubernetes/${clusterCode}/namespace/${namespace}/workload/view/Deployment/${item.name}`);
@@ -523,7 +524,7 @@ export default function NamespaceResourcePage() {
) : filtered.length === 0 ? (
<TableRow><TableCell colSpan={config.columns.length} className="text-center text-slate-500 py-12"><config.icon className="h-8 w-8 mx-auto mb-2 opacity-50" />暂无 {config.title}</TableCell></TableRow>
) : (
filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit, onTerminal: ( p: any) => setTerminalPod(p), onFileBrowser: ( p: any) => setFileBrowserPod(p) }))
filtered.map((item) => config.renderRow(item, { clusterCode: clusterCode || '', namespace: namespace || '', onDetail: handleDetail, onYaml: handleYaml, onEdit: handleEdit, onTerminal: (p: JsonData) => setTerminalPod(p), onFileBrowser: (p: JsonData) => setFileBrowserPod(p) }))
)}
</TableBody>
</Table>
@@ -537,7 +538,7 @@ export default function NamespaceResourcePage() {
clusterId={clusters[0]?.id || clusters[0]?.code || clusterCode || ''}
namespace={namespace || ''}
podName={terminalPod?.name || ''}
containers={(terminalPod?.containers || []).map(( c: any) => c.name).filter(Boolean)}
containers={(terminalPod?.containers || []).map((c: JsonData) => c.name).filter(Boolean)}
/>
{/* ============ Pod 文件浏览器 ============ */}
<PodFileBrowser
@@ -546,7 +547,7 @@ export default function NamespaceResourcePage() {
clusterCode={clusterCode || ''}
namespace={namespace || ''}
podName={fileBrowserPod?.name || ''}
containers={(fileBrowserPod?.containers || []).map(( c: any) => c.name).filter(Boolean)}
containers={(fileBrowserPod?.containers || []).map((c: JsonData) => c.name).filter(Boolean)}
/>
</div>
);
@@ -17,6 +17,7 @@ import {
ArrowLeft,
HardDrive,
} from 'lucide-react';
import type { JsonData } from '@/types/common';
export interface PodFileBrowserProps {
@@ -54,10 +55,10 @@ export function PodFileBrowser({
setError('');
setPreview(null);
try {
const res: Record<string, any> = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined);
const res: JsonData = await kubernetesApi.listPodFiles(clusterCode, namespace, podName, path, container || undefined);
// 兼容后端 {files: [...]} 和 {entries: [...]} 两种返回格式
const raw: Record<string, any>[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []);
const list: FileEntry[] = raw.map(( it: any) => ({
const raw: JsonData[] = res?.entries || res?.files || res?.data?.entries || res?.data?.files || (Array.isArray(res) ? res : []);
const list: FileEntry[] = raw.map((it: JsonData) => ({
name: it.name ?? it.Name ?? '',
isDir: Boolean(it.isDir ?? it.is_dir ?? it.IsDir ?? it.dir ?? it.directory),
size: typeof it.size === 'number' ? it.size : (it.Size ? Number(it.Size) : undefined),
@@ -66,7 +67,7 @@ export function PodFileBrowser({
setEntries(list);
setCurrentPath(path);
} catch (e) {
setError((e as Record<string, any>).message || '加载文件列表失败');
setError((e as JsonData).message || '加载文件列表失败');
setEntries([]);
} finally {
setLoading(false);
@@ -83,7 +84,7 @@ export function PodFileBrowser({
}
setLoading(true);
try {
const res: Record<string, any> = await kubernetesApi.execPod(clusterCode, namespace, podName, ['pwd'], container || undefined);
const res: JsonData = await kubernetesApi.execPod(clusterCode, namespace, podName, ['pwd'], container || undefined);
const stdout = (res?.stdout || res?.data?.stdout || '').trim();
const workdir = stdout && stdout.startsWith('/') ? stdout : '/';
workdirDetected.current = true;
@@ -116,7 +117,7 @@ export function PodFileBrowser({
const text = await kubernetesApi.readPodFileText(clusterCode, namespace, podName, filePath, container || undefined);
setPreview({ file: fileName, content: text });
} catch (e) {
setPreview({ file: fileName, content: `读取失败: ${(e as Record<string, any>).message}` });
setPreview({ file: fileName, content: `读取失败: ${(e as JsonData).message}` });
}
};
@@ -134,7 +135,7 @@ export function PodFileBrowser({
await kubernetesApi.uploadPodFile(clusterCode, namespace, podName, currentPath, file, container || undefined);
loadFiles(currentPath);
} catch (err) {
setError(`上传失败: ${(err as Record<string, any>).message}`);
setError(`上传失败: ${(err as JsonData).message}`);
} finally {
setUploading(false);
if (fileInputRef.current) fileInputRef.current.value = '';
@@ -21,6 +21,7 @@ import {
import { cn } from '@/lib/utils';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import type { JsonData } from '@/types/common';
/**
* 增强版 Pod 日志查看器 — 增强 DeploymentDetail 的日志体验
@@ -153,7 +154,7 @@ export function PodLogsViewer({
linesRef.current = [];
setLines([]);
try {
const res: Record<string, any> = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, {
const res: JsonData = await kubernetesApi.getPodLogsEnhanced(clusterCode, namespace, podName, {
container: container || undefined,
tailLines: tailLines,
previous,
@@ -11,19 +11,20 @@ import {
} from '@/components/ui/table';
import { Cpu, HardDrive, RefreshCw, ChevronRight, Activity } from 'lucide-react';
import { kubernetesApi } from '@/api';
import type { JsonData } from '@/types/common';
export default function PodMetricsPage() {
const { clusterCode } = useParams<{ clusterCode: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [metrics, setMetrics] = useState<any[]>([]);
const [clusters, setClusters] = useState<any[]>([]);
const [metrics, setMetrics] = useState<JsonData[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [selectedNs, setSelectedNs] = useState('all');
const [activeCluster, setActiveCluster] = useState(clusterCode || '');
useEffect(() => {
kubernetesApi.getClusters().then(( res: any) => {
kubernetesApi.getClusters().then((res: JsonData) => {
const list = res?.list || [];
setClusters(list);
if (!activeCluster && list.length > 0) {
@@ -36,7 +37,7 @@ export default function PodMetricsPage() {
useEffect(() => {
if (!activeCluster) return;
kubernetesApi.getNamespaces(activeCluster).then(( res: any) => {
kubernetesApi.getNamespaces(activeCluster).then((res: JsonData) => {
setNamespaces(Array.isArray(res) ? res : (res?.list || res?.data || []));
}).catch(() => {});
}, [activeCluster]);
@@ -46,7 +47,7 @@ export default function PodMetricsPage() {
setLoading(true);
try {
const ns = selectedNs === 'all' ? undefined : selectedNs;
const res: Record<string, any> = await kubernetesApi.getPodMetrics(activeCluster, ns);
const res: JsonData = await kubernetesApi.getPodMetrics(activeCluster, ns);
setMetrics(Array.isArray(res) ? res : (res?.list || []));
} catch { setMetrics([]); } finally { setLoading(false); }
}, [activeCluster, selectedNs]);
@@ -55,8 +56,8 @@ export default function PodMetricsPage() {
// Flatten for table
const rows = useMemo(() =>
metrics.flatMap(( pm: any) =>
(pm.containers || []).map(( c: any) => ({
metrics.flatMap((pm: JsonData) =>
(pm.containers || []).map((c: JsonData) => ({
pod: pm.name,
namespace: pm.namespace,
container: c.name,
@@ -91,7 +92,7 @@ export default function PodMetricsPage() {
<Select value={activeCluster} onValueChange={v => { setActiveCluster(v); navigate(`/kubernetes/${v}/metrics/pods`); }}>
<SelectTrigger className="w-44 h-8 text-xs bg-slate-800 border-slate-700"><SelectValue /></SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700">
{clusters.map(( c: any) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
{clusters.map((c: JsonData) => <SelectItem key={c.id} value={c.code || String(c.id)} className="text-xs">{c.name || c.code}</SelectItem>)}
</SelectContent>
</Select>
<Select value={selectedNs} onValueChange={setSelectedNs}>
@@ -23,11 +23,12 @@ import {
buildTopology, getNodeIcon, getNodeColor, NODE_TYPE_LABELS, CUSTOM_NODE_TYPES,
isCustomNode, type TopologyLayoutData,
} from './topology-helpers';
import type { JsonData } from '@/types/common';
export function TopologyView() {
const { clusterCode } = useParams<{ clusterCode?: string }>();
const _confirm = useConfirm();
const [clusters, setClusters] = useState<any[]>([]);
const [clusters, setClusters] = useState<JsonData[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<string>('');
const [topology, setTopology] = useState<{ nodes: TopologyNode[]; edges: TopologyEdge[] }>({ nodes: [], edges: [] });
const [loading, setLoading] = useState(false);
@@ -52,13 +53,13 @@ export function TopologyView() {
const loadClusters = useCallback(async () => {
try {
const res: Record<string, any> = await kubernetesApi.getClusters();
const res: JsonData = await kubernetesApi.getClusters();
const list = res?.list || [];
setClusters(list);
// Auto-select from URL param (clusterCode) if provided, otherwise first cluster
if (!selectedClusterId) {
if (clusterCode) {
const matched = list.find(( c: any) => (c.code || String(c.id)) === clusterCode);
const matched = list.find((c: JsonData) => (c.code || String(c.id)) === clusterCode);
if (matched) setSelectedClusterId(String(matched.id));
} else if (list.length > 0) {
setSelectedClusterId(String(list[0].id));
@@ -72,8 +73,8 @@ export function TopologyView() {
// 加载已保存的布局 // Logan.Li
const loadSavedLayout = useCallback(async (clusterId: string) => {
try {
const res: Record<string, any> = await kubernetesApi.getTopology(clusterId);
const layout = res as Record<string, any>;
const res: JsonData = await kubernetesApi.getTopology(clusterId);
const layout = res as JsonData;
if (layout?.layout_data) {
const data = JSON.parse(layout.layout_data) as TopologyLayoutData;
setSavedLayout(data);
@@ -94,7 +95,7 @@ export function TopologyView() {
if (!selectedClusterId) return;
setLoading(true);
try {
const toArray = ( v: any) => (Array.isArray(v) ? v : (v?.list || []));
const toArray = (v: JsonData) => (Array.isArray(v) ? v : (v?.list || []));
const [nodesRes, svcRes, depRes, ingRes] = await Promise.all([
kubernetesApi.getNodes(selectedClusterId).catch(() => []),
kubernetesApi.getServices(selectedClusterId).catch(() => []),
@@ -196,7 +197,7 @@ export function TopologyView() {
const handleEditNode = (node: TopologyNode) => {
setEditingNode(node);
setEditForm({ name: node.name, type: node.type, note: (node.data as Record<string, any>)?.note || '' });
setEditForm({ name: node.name, type: node.type, note: (node.data as JsonData)?.note || '' });
setEditDialogOpen(true);
};
@@ -20,6 +20,7 @@ import { YamlViewer } from './YamlViewer';
import { PodExecTerminal } from './PodExecTerminal';
import { PodFileBrowser } from './PodFileBrowser';
import { formatK8sTime, statusColor, STATUS_COLORS } from './k8s-yaml-utils';
import type { JsonData } from '@/types/common';
type WorkloadKind = 'StatefulSet' | 'DaemonSet';
type ViewMode = 'view' | 'yaml';
@@ -67,14 +68,14 @@ export default function WorkloadViewPage() {
const kind: WorkloadKind = location.pathname.includes('/DaemonSet/') ? 'DaemonSet' : 'StatefulSet';
const mode: ViewMode = params.mode === 'yaml' ? 'yaml' : 'view';
const [manifest, setManifest] = useState<any>(null);
const [pods, setPods] = useState<any[]>([]);
const [manifest, setManifest] = useState<JsonData>(null);
const [pods, setPods] = useState<JsonData[]>([]);
const [loading, setLoading] = useState(true);
const [scaleModal, setScaleModal] = useState(false);
const [scaleValue, setScaleValue] = useState(1);
const [actionLoading, setActionLoading] = useState(false);
const [terminalPod, setTerminalPod] = useState<any>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<any>(null);
const [terminalPod, setTerminalPod] = useState<JsonData>(null);
const [fileBrowserPod, setFileBrowserPod] = useState<JsonData>(null);
const [yamlModal, setYamlModal] = useState(false);
const loadData = useCallback(async () => {
@@ -95,7 +96,7 @@ export default function WorkloadViewPage() {
// Filter pods by name prefix and namespace
const podList = Array.isArray(podsRes) ? podsRes : (podsRes?.list || podsRes?.data || []);
const filtered = podList.filter(( p: any) => {
const filtered = podList.filter((p: JsonData) => {
const podName = p.name || p.metadata?.name || '';
const podNs = p.namespace || p.namespace_name || '';
return podName.startsWith(name) && (!podNs || podNs === namespace);
@@ -384,7 +385,7 @@ export default function WorkloadViewPage() {
</TableRow>
</TableHeader>
<TableBody>
{vcts.map(( vct: any, idx: number) => {
{vcts.map((vct: JsonData, idx: number) => {
const accessModes = (vct.spec?.accessModes || []).join(', ');
const storageClass = vct.spec?.storageClassName || vct.spec?.storageClassName || '-';
const size = vct.spec?.resources?.requests?.storage || '-';
@@ -415,7 +416,7 @@ export default function WorkloadViewPage() {
<div className="mb-3">
<p className="text-[11px] text-slate-500 mb-2 uppercase tracking-wide">Init Containers</p>
<div className="space-y-2">
{initContainers.map(( c: any, idx: number) => (
{initContainers.map((c: JsonData, idx: number) => (
<ContainerInfo key={`init-${idx}`} c={c} />
))}
</div>
@@ -426,7 +427,7 @@ export default function WorkloadViewPage() {
) : (
<div className="space-y-3">
<p className="text-[11px] text-slate-500 mb-1 uppercase tracking-wide">Containers</p>
{containers.map(( c: any, idx: number) => (
{containers.map((c: JsonData, idx: number) => (
<ContainerInfo key={idx} c={c} />
))}
</div>
@@ -456,7 +457,7 @@ export default function WorkloadViewPage() {
<TableBody>
{pods.length === 0 ? (
<TableRow><TableCell colSpan={6} className="text-center text-slate-500 py-8">暂无 Pod</TableCell></TableRow>
) : pods.map(( pod: any) => (
) : pods.map((pod: JsonData) => (
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
<TableCell>
@@ -531,7 +532,7 @@ export default function WorkloadViewPage() {
clusterId={clusterCode}
namespace={namespace}
podName={terminalPod.name}
containers={terminalPod.containers?.map(( c: any) => c.name).filter(Boolean) || []}
containers={terminalPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []}
/>
)}
@@ -543,7 +544,7 @@ export default function WorkloadViewPage() {
clusterCode={clusterCode}
namespace={namespace}
podName={fileBrowserPod.name}
containers={fileBrowserPod.containers?.map(( c: any) => c.name).filter(Boolean) || []}
containers={fileBrowserPod.containers?.map((c: JsonData) => c.name).filter(Boolean) || []}
/>
)}
</div>
@@ -551,7 +552,7 @@ export default function WorkloadViewPage() {
}
// Container info sub-component
function ContainerInfo({ c }: { c: any}) {
function ContainerInfo({ c }: { c: JsonData}) {
return (
<div className="border border-slate-800 rounded-lg p-3 bg-slate-950/30">
<div className="flex items-center justify-between mb-2">
@@ -570,7 +571,7 @@ function ContainerInfo({ c }: { c: any}) {
</div>
<div className="text-slate-400">
端口: <span className="text-slate-200 font-mono">
{(c.ports || []).map(( p: any) => p.containerPort || p.port).join(', ') || '-'}
{(c.ports || []).map((p: JsonData) => p.containerPort || p.port).join(', ') || '-'}
</span>
</div>
</div>
@@ -6,6 +6,7 @@ import { ModalOverlay } from '@/components/ui/modal-overlay';
import { useToast } from '@/hooks/use-toast';
import { kubernetesApi } from '@/api';
import { YamlCodeMirror } from './YamlCodeMirror';
import type { JsonData } from '@/types/common';
interface YamlImportDialogProps {
clusterCode: string;
@@ -30,7 +31,7 @@ export function YamlImportDialog({ clusterCode, onClose, onApplied }: YamlImport
onApplied?.();
onClose();
} catch (e) {
toast({ title: '导入失败', description: (e as Record<string, any>).message, variant: 'destructive' });
toast({ title: '导入失败', description: (e as JsonData).message, variant: 'destructive' });
} finally { setApplying(false); }
};
@@ -10,6 +10,7 @@ import { getYamlResourceConfig, yamlDownloadFilename } from './k8s-yaml-resource
import { YamlEditorToolbar } from './YamlEditorToolbar';
import { YamlCodeMirror } from './YamlCodeMirror';
import { downloadYamlFile } from '@/utils/export';
import type { JsonData } from '@/types/common';
interface YamlViewerProps {
clusterCode: string;
@@ -61,7 +62,7 @@ export function YamlViewer({
setRawYaml(text);
await applyStripped(text);
} catch (e) {
toast({ title: '获取YAML失败', description: (e as Record<string, any>).message, variant: 'destructive' });
toast({ title: '获取YAML失败', description: (e as JsonData).message, variant: 'destructive' });
const err = `# Error: ${(e as Error).message}`;
setRawYaml(err);
setDisplayYaml(err);
@@ -7,14 +7,15 @@ import {
} from '@/components/ui/table';
import { TabsContent } from '@/components/ui/tabs';
import { CreateHPADialog } from './CreateHPADialog';
import type { JsonData } from '@/types/common';
export interface DeploymentHpaTabProps {
hpas: any[];
hpas: JsonData[];
clusterCode: string;
namespace: string;
deploymentName: string;
onCreated: () => void;
onDeleteHpa: (hpa: any) => void;
onDeleteHpa: (hpa: JsonData) => void;
}
export function DeploymentHpaTab({
@@ -58,7 +59,7 @@ export function DeploymentHpaTab({
</TableRow>
</TableHeader>
<TableBody>
{hpas.map((hpa: any) => {
{hpas.map((hpa: JsonData) => {
const cpu = hpa.cpu_percent || 0;
const mem = hpa.mem_percent || 0;
const metricsParts = [];
@@ -12,9 +12,10 @@ import {
} from '@/components/ui/select';
import { TabsContent } from '@/components/ui/tabs';
import { extractContainers } from './helpers';
import type { JsonData } from '@/types/common';
export interface DeploymentLogsTabProps {
pods: any[];
pods: JsonData[];
logTail: number;
onLogTailChange: (value: number) => void;
logsTabPod: string;
@@ -61,7 +62,7 @@ export function DeploymentLogsTab({
<SelectValue placeholder="选择 Pod…" />
</SelectTrigger>
<SelectContent>
{pods.map((p: any) => (
{pods.map((p: JsonData) => (
<SelectItem key={p.name} value={p.name}>{p.name}</SelectItem>
))}
</SelectContent>
@@ -71,7 +72,7 @@ export function DeploymentLogsTab({
className="bg-blue-600 hover:bg-blue-700 h-8"
disabled={!logsTabPod}
onClick={() => {
const pod = pods.find((p: any) => p.name === logsTabPod);
const pod = pods.find((p: JsonData) => p.name === logsTabPod);
onOpenLogsViewer(logsTabPod, pod ? extractContainers(pod) : []);
}}
>
@@ -7,12 +7,13 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { CreateHPADialog } from './CreateHPADialog';
import type { JsonData } from '@/types/common';
export interface DeploymentOverviewProps {
deploymentName: string;
namespace: string;
deployment: any;
hpas: any[];
deployment?: JsonData;
hpas: JsonData[];
clusterCode: string;
ready: number;
total: number;
@@ -9,9 +9,10 @@ import {
import { TabsContent } from '@/components/ui/tabs';
import { extractContainers } from './helpers';
import type { PodWithContainers } from './types';
import type { JsonData } from '@/types/common';
export interface DeploymentPodsTabProps {
pods: any[];
pods: JsonData[];
onOpenLogsViewer: (pod: PodWithContainers) => void;
onOpenExec: (pod: PodWithContainers) => void;
onOpenFileBrowser: (pod: PodWithContainers) => void;
@@ -42,7 +43,7 @@ export function DeploymentPodsTab({ pods, onOpenLogsViewer, onOpenExec, onOpenFi
</TableCell>
</TableRow>
) : (
pods.map((pod: any) => (
pods.map((pod: JsonData) => (
<TableRow key={pod.name} className="border-slate-800 hover:bg-slate-800/30">
<TableCell className="font-medium text-slate-200">{pod.name}</TableCell>
<TableCell>
@@ -1,10 +1,11 @@
import type { JsonData } from "@/types/common";
// helpers.ts — DeploymentDetail 工具函数 // Logan.Li
/** 从 Pod 对象提取容器名列表(兼容多种字段) */
export function extractContainers(pod: any): string[] {
export function extractContainers(pod: JsonData): string[] {
const c = pod?.containers;
if (Array.isArray(c)) return c.map((x: any) => typeof x === 'string' ? x : (x?.name || x?.Name)).filter(Boolean);
if (Array.isArray(pod?.container_statuses)) return pod.container_statuses.map((x: any) => x?.name).filter(Boolean);
if (Array.isArray(c)) return c.map((x: JsonData) => typeof x === 'string' ? x : (x?.name || x?.Name)).filter(Boolean);
if (Array.isArray(pod?.container_statuses)) return pod.container_statuses.map((x: JsonData) => x?.name).filter(Boolean);
if (Array.isArray(pod?.containerNames)) return pod.containerNames.filter(Boolean);
return [];
}
@@ -3,10 +3,11 @@
import type {
ContainerForm, ProbeDef, LifecycleHandler,
} from './types';
import type { JsonData } from '@/types/common';
// K8s container spec → form conversion // Logan.Li
export function toContainerForm(c: any): ContainerForm {
const probe = (p: any): ProbeDef | null => {
export function toContainerForm(c: JsonData): ContainerForm {
const probe = (p: JsonData): ProbeDef | null => {
if (!p) return null;
let type: ProbeDef['type'] = 'none';
if (p.httpGet) type = 'httpGet';
@@ -26,7 +27,7 @@ export function toContainerForm(c: any): ContainerForm {
failureThreshold: p.failureThreshold,
};
};
const lh = (h: any): LifecycleHandler => {
const lh = (h: JsonData): LifecycleHandler => {
if (!h) return { type: 'none' };
let type: LifecycleHandler['type'] = 'none';
if (h.exec) type = 'exec';
@@ -48,8 +49,8 @@ export function toContainerForm(c: any): ContainerForm {
command: Array.isArray(c.command) ? c.command : [],
args: Array.isArray(c.args) ? c.args : [],
workingDir: c.workingDir || '',
env: (Array.isArray(c.env) ? c.env : []).map((e: any) => ({ name: e.name || '', value: e.value ?? '' })),
ports: (Array.isArray(c.ports) ? c.ports : []).map((p: any) => ({ name: p.name || '', containerPort: p.containerPort ?? '', protocol: p.protocol || 'TCP' })),
env: (Array.isArray(c.env) ? c.env : []).map((e: JsonData) => ({ name: e.name || '', value: e.value ?? '' })),
ports: (Array.isArray(c.ports) ? c.ports : []).map((p: JsonData) => ({ name: p.name || '', containerPort: p.containerPort ?? '', protocol: p.protocol || 'TCP' })),
resources: {
requests: c.resources?.requests || {},
limits: c.resources?.limits || {},
@@ -71,7 +72,7 @@ export function toContainerForm(c: any): ContainerForm {
capabilitiesAdd: sc.capabilities?.add || [],
capabilitiesDrop: sc.capabilities?.drop || [],
} : null,
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: any) => ({
volumeMounts: (Array.isArray(c.volumeMounts) ? c.volumeMounts : []).map((m: JsonData) => ({
name: m.name || '', mountPath: m.mountPath || '', subPath: m.subPath || '', readOnly: m.readOnly || false,
})),
};
@@ -88,9 +89,9 @@ export function emptyContainer(name = ''): ContainerForm {
};
}
export function probeToK8s(p: ProbeDef | null | undefined): any {
export function probeToK8s(p: ProbeDef | null | undefined): JsonData {
if (!p || p.type === 'none' || !p.type) return undefined;
const out: Record<string, any> = {};
const out: JsonData = {};
if (p.type === 'httpGet') {
out.httpGet = {};
if (p.httpPath) out.httpGet.path = p.httpPath;
@@ -111,28 +112,28 @@ export function probeToK8s(p: ProbeDef | null | undefined): any {
return Object.keys(out).length > 0 ? out : undefined;
}
export function handlerToK8s(h: LifecycleHandler | undefined): any {
export function handlerToK8s(h: LifecycleHandler | undefined): JsonData {
if (!h || h.type === 'none' || !h.type) return undefined;
if (h.type === 'exec') {
const cmds = (h.execCommand || '').split('\n').map(s => s.trim()).filter(Boolean);
return cmds.length ? { exec: { command: cmds } } : undefined;
}
if (h.type === 'httpGet') {
const o: Record<string, any> = { httpGet: {} };
const o: JsonData = { httpGet: {} };
if (h.httpPath) o.httpGet.path = h.httpPath;
if (h.httpPort !== '' && h.httpPort != null) o.httpGet.port = Number(h.httpPort);
return o;
}
if (h.type === 'tcpSocket') {
const o: Record<string, any> = { tcpSocket: {} };
const o: JsonData = { tcpSocket: {} };
if (h.tcpPort !== '' && h.tcpPort != null) o.tcpSocket.port = Number(h.tcpPort);
return o;
}
return undefined;
}
export function containerToK8s(c: ContainerForm): any {
const out: Record<string, any> = { name: c.name };
export function containerToK8s(c: ContainerForm): JsonData {
const out: JsonData = { name: c.name };
if (c.image) out.image = c.image;
if (c.imagePullPolicy) out.imagePullPolicy = c.imagePullPolicy;
if (c.workingDir) out.workingDir = c.workingDir;
@@ -140,14 +141,14 @@ export function containerToK8s(c: ContainerForm): any {
if (c.args.length > 0) out.args = c.args;
if (c.env.length > 0) {
out.env = c.env.filter(e => e.name.trim()).map(e => {
const o: Record<string, any> = { name: e.name };
const o: JsonData = { name: e.name };
if (e.value != null) o.value = e.value;
return o;
});
}
if (c.ports.length > 0) {
out.ports = c.ports.filter(p => p.containerPort !== '').map(p => {
const o: Record<string, any> = { containerPort: typeof p.containerPort === 'number' ? p.containerPort : Number(p.containerPort) };
const o: JsonData = { containerPort: typeof p.containerPort === 'number' ? p.containerPort : Number(p.containerPort) };
if (p.name) o.name = p.name;
if (p.protocol) o.protocol = p.protocol;
return o;
@@ -174,14 +175,14 @@ export function containerToK8s(c: ContainerForm): any {
const rp = probeToK8s(c.readinessProbe); if (rp) out.readinessProbe = rp;
const sp = probeToK8s(c.startupProbe); if (sp) out.startupProbe = sp;
if (c.lifecycle) {
const lc: Record<string, any> = {};
const lc: JsonData = {};
const ps = handlerToK8s(c.lifecycle.postStart); if (ps) lc.postStart = ps;
const pst = handlerToK8s(c.lifecycle.preStop); if (pst) lc.preStop = pst;
if (Object.keys(lc).length > 0) out.lifecycle = lc;
}
if (c.securityContext) {
const s = c.securityContext;
const sc: Record<string, any> = {};
const sc: JsonData = {};
if (s.runAsUser !== '' && s.runAsUser != null) sc.runAsUser = Number(s.runAsUser);
if (s.runAsGroup !== '' && s.runAsGroup != null) sc.runAsGroup = Number(s.runAsGroup);
if (s.runAsNonRoot != null) sc.runAsNonRoot = s.runAsNonRoot;
@@ -199,7 +200,7 @@ export function containerToK8s(c: ContainerForm): any {
}
if (c.volumeMounts.length > 0) {
out.volumeMounts = c.volumeMounts.filter(m => m.name.trim() && m.mountPath.trim()).map(m => {
const o: Record<string, any> = { name: m.name, mountPath: m.mountPath };
const o: JsonData = { name: m.name, mountPath: m.mountPath };
if (m.subPath) o.subPath = m.subPath;
if (m.readOnly) o.readOnly = true;
return o;
@@ -1,3 +1,4 @@
import type { JsonData } from "@/types/common";
// deployment-edit/types.ts — 容器编辑器类型定义
// 作者: Logan <admin@attacker.club>
@@ -64,8 +65,8 @@ export interface DeploymentEditContainersProps {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: any | null;
onSaved: (updated?: any) => void;
manifest: JsonData | null;
onSaved: (updated?: JsonData) => void;
onCancel: () => void;
}
@@ -1,12 +1,13 @@
// Deployment manifest 统一保存 // Logan.Li
import { kubernetesApi } from '@/api';
import { prepareManifestForSave } from './k8s-manifest-utils';
import type { JsonData } from '@/types/common';
export async function saveDeploymentManifest(
clusterCode: string,
namespace: string,
deploymentName: string,
manifest: Record<string, any>,
manifest: JsonData,
): Promise<void> {
const yaml = await prepareManifestForSave(manifest);
await kubernetesApi.updateResourceYaml(clusterCode, namespace, 'deployments', deploymentName, yaml);
@@ -6,8 +6,9 @@ import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table';
import { Activity, Clock, Loader2 } from 'lucide-react';
import type { JsonData } from '@/types/common';
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any[]; cronHpas: any[]; loading: string; replicas: number }) {
export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: JsonData[]; cronHpas: JsonData[]; loading: string; replicas: number }) {
if (loading === 'scale') {
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
}
@@ -42,20 +43,20 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any
</TableRow>
</TableHeader>
<TableBody>
{hpas.map((hpa: any, i: number) => {
{hpas.map((hpa: JsonData, i: number) => {
const minR = hpa.spec?.minReplicas ?? hpa.minReplicas ?? 1;
const maxR = hpa.spec?.maxReplicas ?? hpa.maxReplicas ?? 10;
const metrics = hpa.spec?.metrics || hpa.metrics || [];
const curR = hpa.status?.currentReplicas ?? hpa.currentReplicas ?? 0;
const conditions = hpa.status?.conditions || hpa.conditions || [];
const ableToScale = conditions.find((c: any) => c.type === 'AbleToScale');
const ableToScale = conditions.find((c: JsonData) => c.type === 'AbleToScale');
const ok = ableToScale?.status === 'True';
return (
<TableRow key={hpa.name || i} className="border-[var(--dg-border)]">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{hpa.name || hpa.metadata?.name}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{minR} → {maxR}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">
{metrics.map((m: any, j: number) => {
{metrics.map((m: JsonData, j: number) => {
const resource = m.resource || m.object?.metric?.name || '';
const target = m.resource?.target?.averageUtilization || m.resource?.target?.averageValue || m.resource?.target?.type || '-';
return <div key={j}>{resource?.name || resource}: {target}{m.resource?.target?.averageUtilization ? '%' : ''}</div>;
@@ -98,16 +99,16 @@ export function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any
</TableRow>
</TableHeader>
<TableBody>
{cronHpas.map((cron: any, i: number) => {
{cronHpas.map((cron: JsonData, i: number) => {
const jobs = cron.spec?.jobs || cron.jobs || [];
const tz = cron.spec?.timezone || 'UTC';
return (
<TableRow key={cron.name || i} className="border-[var(--dg-border)]">
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cron.name || cron.metadata?.name}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{
jobs.map((j: any) => j.targetSize || j.targetReplicas || '?').join(', ')
jobs.map((j: JsonData) => j.targetSize || j.targetReplicas || '?').join(', ')
}</TableCell>
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: any) => j.schedule || '?').join(', ')}</TableCell>
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: JsonData) => j.schedule || '?').join(', ')}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
<TableCell>
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
@@ -6,8 +6,9 @@ import {
} from '@/components/ui/table';
import { RotateCcw, Loader2 } from 'lucide-react';
import { formatK8sTime } from '../k8s-yaml-utils';
import type { JsonData } from '@/types/common';
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: any[]; rollbackLoading: number | null; onRollback: (rev: any) => void }) {
export function HistoryTab({ revisions, rollbackLoading, onRollback }: { revisions: JsonData[]; rollbackLoading: number | null; onRollback: (rev: JsonData) => void }) {
if (revisions.length === 0) {
return <div className="text-center text-[var(--dg-text-muted)] py-12">暂无历史版本</div>;
}
@@ -8,6 +8,7 @@ import {
import { formatK8sTime, statusColor } from '../k8s-yaml-utils';
import { getPodTimeline, PodConditionsRow } from './helpers';
import type { DeploymentViewProps } from './types';
import type { JsonData } from '@/types/common';
export function PodsTab({
clusterCode, pods, onShowPodLogs, onShowPodTerminal, onShowPodYaml, onShowPodFileBrowser,
@@ -61,9 +62,9 @@ export function PodsTab({
<div className="mt-3 pt-3 border-t border-[var(--dg-border)]">
<PodConditionsRow pod={pod} />
{/* Container details */}
{(pod as Record<string, any>).containers && ((pod as Record<string, any>).containers as any[]).length > 0 && (
{(pod as JsonData).containers && ((pod as JsonData).containers as JsonData[]).length > 0 && (
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2">
{((pod as Record<string, any>).containers as any[]).map((c: any, idx: number) => (
{((pod as JsonData).containers as JsonData[]).map((c: JsonData, idx: number) => (
<div key={idx} className="p-2 rounded bg-[var(--dg-bg-tertiary)] text-xs">
<div className="flex items-center justify-between">
<span className="font-medium text-[var(--dg-text-primary)]">{c.name}</span>
@@ -5,6 +5,7 @@ import {
} from '@/components/ui/table';
import { Network, ExternalLink, Loader2 } from 'lucide-react';
import type { SvcItem, IngItem } from './types';
import type { JsonData } from '@/types/common';
export function ServicesTab({ services, ingresses, loading }: { services: SvcItem[]; ingresses: IngItem[]; loading: string; namespace: string }) {
if (loading === 'services') {
@@ -76,7 +77,7 @@ export function ServicesTab({ services, ingresses, loading }: { services: SvcIte
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.className}</TableCell>
<TableCell className="text-sm text-[var(--dg-text-muted)]">{ing.hosts.join(', ')}</TableCell>
<TableCell className="text-xs text-[var(--dg-text-muted)]">
{ing.rules.flatMap((r: any) => (r.http?.paths || r.paths || []).map((p: any) => p.path || '/')).join(', ')}
{ing.rules.flatMap((r: JsonData) => (r.http?.paths || r.paths || []).map((p: JsonData) => p.path || '/')).join(', ')}
</TableCell>
</TableRow>
))}
@@ -4,18 +4,19 @@ import {
formatK8sTime, formatDuration, elapsedSeconds, STATUS_COLORS,
} from '../k8s-yaml-utils';
import type { PodItem } from './types';
import type { JsonData } from '@/types/common';
// Pod 条件时间线提取 // Logan.Li
export function getPodTimeline(pod: PodItem) {
const cond: Record<string, any>[] = (pod as Record<string, any>).conditions || (pod as Record<string, any>).pod_conditions || [];
const find = (t: string) => cond.find((c: any) => (c.type || c.Type) === t);
const ts = (c: any) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
const cond: JsonData[] = (pod as JsonData).conditions || (pod as JsonData).pod_conditions || [];
const find = (t: string) => cond.find((c: JsonData) => (c.type || c.Type) === t);
const ts = (c: JsonData) => c?.lastTransitionTime || c?.LastTransitionTime || c?.last_transition_time;
return {
scheduled: ts(find('PodScheduled') || {}),
initialized: ts(find('Initialized') || {}),
containersReady: ts(find('ContainersReady') || {}),
ready: ts(find('Ready') || {}),
startTime: pod.start_time || (pod as Record<string, any>).startTime,
startTime: pod.start_time || (pod as JsonData).startTime,
};
}
@@ -1,9 +1,10 @@
import type { JsonData } from "@/types/common";
// deployment-view/types.ts — Deployment 详情看板类型定义 // Logan.Li
export interface PodItem {
name: string; namespace?: string; status?: string; node_name?: string; node?: string;
ip?: string; pod_ip?: string; restart_count?: number; age?: string; created_at?: string;
start_time?: string; containers?: any[]; image?: string; images?: string[];
start_time?: string; containers?: JsonData[]; image?: string; images?: string[];
}
export interface ReplicaSetItem {
@@ -12,18 +13,18 @@ export interface ReplicaSetItem {
}
export interface SvcItem {
name: string; type: string; clusterIP: string; ports: any[]; selector: Record<string, string>; age: string;
name: string; type: string; clusterIP: string; ports: JsonData[]; selector: Record<string, string>; age: string;
}
export interface IngItem {
name: string; className: string; hosts: string[]; rules: any[]; age: string;
name: string; className: string; hosts: string[]; rules: JsonData[]; age: string;
}
export type EventItem = import('../k8s-event-utils').NormalizedEvent;
export interface DeploymentViewProps {
clusterCode: string; namespace: string; deployment: any;
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: any[];
clusterCode: string; namespace: string; deployment?: JsonData;
pods: PodItem[]; replicaSets: ReplicaSetItem[]; revisions: JsonData[];
onRefresh: () => void;
onShowPodLogs: (pod: PodItem) => void;
onShowPodTerminal: (pod: PodItem) => void;
@@ -1,3 +1,4 @@
import type { JsonData } from "@/types/common";
// Deployment Pod 数据卷 ↔ K8s YAML 双向映射 // Logan.Li
export type VolumeType =
@@ -26,7 +27,7 @@ export interface VolumeForm {
secretName?: string;
secItems?: string;
/** 未识别卷类型时保留原始对象,避免保存丢失 // Logan.Li */
_raw?: Record<string, any>;
_raw?: JsonData;
}
export interface MountForm {
@@ -65,7 +66,7 @@ export function emptyVolumeEntry(defaultContainer = ''): VolumeEntry {
const SYSTEM_VOLUME_PREFIXES = ['kube-api-access-', 'default-token-', 'kube-'];
/** 是否系统/不可编辑卷(保存时保留)// Logan.Li */
export function isPassthroughVolume(v: Record<string, any>): boolean {
export function isPassthroughVolume(v: JsonData): boolean {
if (!v || typeof v !== 'object') return false;
const name = String(v.name || '');
if (SYSTEM_VOLUME_PREFIXES.some((p) => name.startsWith(p))) return true;
@@ -91,7 +92,7 @@ export function parseKeyToPathItems(text?: string): Array<{ key: string; path: s
}
/** K8s volume → 表单 // Logan.Li */
export function volumeToForm(v: Record<string, any>): VolumeForm {
export function volumeToForm(v: JsonData): VolumeForm {
if (!v || typeof v !== 'object') return { name: '', type: 'emptyDir' };
const name = v.name || '';
if (v.persistentVolumeClaim) {
@@ -150,22 +151,22 @@ export function volumeToForm(v: Record<string, any>): VolumeForm {
}
/** 表单 → K8s volume // Logan.Li */
export function formToVolume(f: VolumeForm): Record<string, any> | null {
export function formToVolume(f: VolumeForm): JsonData | null {
if (f.type === 'custom' && f._raw) return JSON.parse(JSON.stringify(f._raw));
const volName = f.name?.trim();
if (!volName) return null;
const v: Record<string, any> = { name: volName };
const v: JsonData = { name: volName };
switch (f.type) {
case 'emptyDir': {
const ed: Record<string, any> = {};
const ed: JsonData = {};
if (f.medium?.trim()) ed.medium = f.medium.trim();
if (f.sizeLimit?.trim()) ed.sizeLimit = f.sizeLimit.trim();
v.emptyDir = ed;
break;
}
case 'hostPath': {
const hp: Record<string, any> = { path: f.path?.trim() || '/' };
const hp: JsonData = { path: f.path?.trim() || '/' };
if (f.pathType?.trim()) hp.type = f.pathType.trim();
v.hostPath = hp;
break;
@@ -173,7 +174,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
case 'configMap': {
const cmName = f.configMapName?.trim();
if (!cmName) return null;
const cm: Record<string, any> = { name: cmName };
const cm: JsonData = { name: cmName };
const items = parseKeyToPathItems(f.cmItems);
if (items) cm.items = items;
v.configMap = cm;
@@ -182,7 +183,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
case 'secret': {
const secName = f.secretName?.trim();
if (!secName) return null;
const sec: Record<string, any> = { secretName: secName };
const sec: JsonData = { secretName: secName };
const items = parseKeyToPathItems(f.secItems);
if (items) sec.items = items;
v.secret = sec;
@@ -191,7 +192,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
case 'persistentVolumeClaim': {
const claim = f.pvcName?.trim();
if (!claim) return null;
const pvc: Record<string, any> = { claimName: claim };
const pvc: JsonData = { claimName: claim };
if (f.pvcReadOnly) pvc.readOnly = true;
v.persistentVolumeClaim = pvc;
break;
@@ -200,7 +201,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
const server = f.nfsServer?.trim();
const path = f.nfsPath?.trim();
if (!server || !path) return null;
const nfs: Record<string, any> = { server, path };
const nfs: JsonData = { server, path };
if (f.nfsReadOnly) nfs.readOnly = true;
v.nfs = nfs;
break;
@@ -211,7 +212,7 @@ export function formToVolume(f: VolumeForm): Record<string, any> | null {
return v;
}
export function mountToForm(containerName: string, vm: Record<string, any>): MountForm {
export function mountToForm(containerName: string, vm: JsonData): MountForm {
return {
containerName,
volumeName: vm.name || '',
@@ -221,9 +222,9 @@ export function mountToForm(containerName: string, vm: Record<string, any>): Mou
};
}
export function formToMount(m: MountForm): Record<string, any> | null {
export function formToMount(m: MountForm): JsonData | null {
if (!m.volumeName?.trim() || !m.mountPath?.trim()) return null;
const vm: Record<string, any> = { name: m.volumeName.trim(), mountPath: m.mountPath.trim() };
const vm: JsonData = { name: m.volumeName.trim(), mountPath: m.mountPath.trim() };
if (m.subPath?.trim()) vm.subPath = m.subPath.trim();
if (m.readOnly) vm.readOnly = true;
return vm;
@@ -231,7 +232,7 @@ export function formToMount(m: MountForm): Record<string, any> | null {
const SYSTEM_MOUNT_PREFIXES = ['/var/run/secrets/kubernetes.io/serviceaccount'];
export function isPassthroughMount(vm: Record<string, any>, passthroughVolumeNames: Set<string>): boolean {
export function isPassthroughMount(vm: JsonData, passthroughVolumeNames: Set<string>): boolean {
if (!vm) return false;
if (passthroughVolumeNames.has(vm.name)) return true;
const mp = vm.mountPath || '';
@@ -239,14 +240,14 @@ export function isPassthroughMount(vm: Record<string, any>, passthroughVolumeNam
}
/** 从 manifest 解析卷与挂载表单 // Logan.Li */
export function parseStorageFromManifest(manifest: Record<string, any>): {
export function parseStorageFromManifest(manifest: JsonData): {
volumes: VolumeForm[];
mounts: MountForm[];
containers: string[];
passthroughVolumes: Record<string, any>[];
passthroughVolumes: JsonData[];
} {
const podSpec = manifest?.spec?.template?.spec || {};
const rawVols: Record<string, any>[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : [];
const rawVols: JsonData[] = Array.isArray(podSpec.volumes) ? podSpec.volumes : [];
const passthroughVolumes = rawVols.filter(isPassthroughVolume);
const editableVols = rawVols.filter((v) => !isPassthroughVolume(v));
@@ -260,7 +261,7 @@ export function parseStorageFromManifest(manifest: Record<string, any>): {
const mounts: MountForm[] = [];
for (const c of allContainers) {
const cName = c.name || '(unnamed)';
const vms: Record<string, any>[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
const vms: JsonData[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
for (const vm of vms) {
if (isPassthroughMount(vm, passthroughNames)) continue;
mounts.push(mountToForm(cName, vm));
@@ -276,10 +277,10 @@ export function parseStorageFromManifest(manifest: Record<string, any>): {
}
/** 解析为 Kuboard 一卷一卡条目(支持一卷多容器挂载)// Logan.Li */
export function parseVolumeEntries(manifest: Record<string, any>): {
export function parseVolumeEntries(manifest: JsonData): {
entries: VolumeEntry[];
containers: string[];
passthroughVolumes: Record<string, any>[];
passthroughVolumes: JsonData[];
} {
const { volumes, mounts, containers, passthroughVolumes } = parseStorageFromManifest(manifest);
const defaultContainer = containers.find((c) => c !== '(unnamed)') || containers[0] || '';
@@ -326,32 +327,32 @@ export function volumeEntriesToStorage(entries: VolumeEntry[]): {
/** 将表单写回 manifest.spec.template.spec // Logan.Li */
export function applyStorageToManifest(
manifest: Record<string, any>,
manifest: JsonData,
volumes: VolumeForm[],
mounts: MountForm[],
passthroughVolumes: Record<string, any>[],
): Record<string, any> {
passthroughVolumes: JsonData[],
): JsonData {
const base = JSON.parse(JSON.stringify(manifest));
base.spec = base.spec || {};
base.spec.template = base.spec.template || {};
base.spec.template.spec = base.spec.template.spec || {};
const podSpec = base.spec.template.spec;
const userVols = volumes.map(formToVolume).filter(Boolean) as Record<string, any>[];
const userVols = volumes.map(formToVolume).filter(Boolean) as JsonData[];
const mergedVols = [...passthroughVolumes, ...userVols];
if (mergedVols.length > 0) podSpec.volumes = mergedVols;
else delete podSpec.volumes;
const passthroughNames = new Set(passthroughVolumes.map((v) => v.name));
const applyMounts = (c: Record<string, any>) => {
const applyMounts = (c: JsonData) => {
const cName = c.name || '(unnamed)';
const existing: Record<string, any>[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
const existing: JsonData[] = Array.isArray(c.volumeMounts) ? c.volumeMounts : [];
const kept = existing.filter((vm) => isPassthroughMount(vm, passthroughNames));
const user = mounts
.filter((m) => m.containerName === cName)
.map(formToMount)
.filter(Boolean) as Record<string, any>[];
.filter(Boolean) as JsonData[];
const merged = [...kept, ...user];
if (merged.length > 0) c.volumeMounts = merged;
else delete c.volumeMounts;
@@ -6,10 +6,11 @@ import { Label } from '@/components/ui/label';
import { AlertTriangle, Loader2, Minus, Plus, Trash2 } from 'lucide-react';
import { ModalShell } from './ModalShell';
import type { ActionModal } from './types';
import type { JsonData } from '@/types/common';
export interface ActionModalsProps {
modal: ActionModal;
deployment: any;
deployment?: JsonData;
namespace: string;
deploymentName: string;
newImage: string;
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { useToast } from '@/hooks/use-toast';
import { useUserStore } from '@/store';
import type { JsonData } from '@/types/common';
export interface CICDWebhookContentProps {
clusterCode: string;
@@ -13,7 +14,7 @@ export interface CICDWebhookContentProps {
export function CICDWebhookContent({ clusterCode, namespace, deploymentName }: CICDWebhookContentProps) {
const { toast } = useToast();
const token = useUserStore(( s: any) => s.token) || localStorage.getItem('token') || '';
const token = useUserStore((s: JsonData) => s.token) || localStorage.getItem('token') || '';
const webhookUrl = `${window.location.origin}/api/v1/clusters/${clusterCode}/namespaces/${namespace}/deployments/${deploymentName}/update-image`;
const curlCmd = `curl -X POST '${webhookUrl}' \\
-H 'Authorization: Bearer ${token ? token.substring(0, 16) + '...' : 'YOUR_TOKEN'}' \\
@@ -6,12 +6,13 @@ import { DeploymentEditContainers } from '../DeploymentEditContainers';
import { DeploymentEditStorage } from '../DeploymentEditStorage';
import { DeploymentEditAdvanced } from '../DeploymentEditAdvanced';
import { DeploymentEditService } from '../DeploymentEditService';
import type { JsonData } from '@/types/common';
export interface EditModeContainerProps {
clusterCode: string;
namespace: string;
deploymentName: string;
manifest: any | null;
manifest: JsonData | null;
onSaved: () => void;
onCancel: () => void;
}
@@ -19,13 +20,13 @@ export interface EditModeContainerProps {
export function EditModeContainer({ clusterCode, namespace, deploymentName, manifest, onSaved, onCancel }: EditModeContainerProps) {
type EditTab = 'basic' | 'containers' | 'storage' | 'advanced' | 'service';
const [activeTab, setActiveTab] = useState<EditTab>('basic');
const [editManifest, setEditManifest] = useState<any>(manifest);
const [editManifest, setEditManifest] = useState<JsonData>(manifest);
useEffect(() => {
if (manifest) setEditManifest(manifest);
}, [manifest]);
const handleSaved = (updated?: any) => {
const handleSaved = (updated?: JsonData) => {
if (updated) setEditManifest(updated);
onSaved();
};
@@ -8,6 +8,7 @@ import { FileText, RefreshCw, Loader2, X } from 'lucide-react';
import { kubernetesApi } from '@/api';
import { useToast } from '@/hooks/use-toast';
import { useUserStore } from '@/store';
import type { JsonData } from '@/types/common';
export interface PodLogsModalProps {
clusterCode: string;
@@ -28,7 +29,7 @@ export function PodLogsModal({ clusterCode, namespace, podName, containers, onCl
const loadLogs = async () => {
setLoading(true);
try {
const res: Record<string, any> = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, tail, container || undefined);
const res: JsonData = await kubernetesApi.getPodLogs(clusterCode, namespace, podName, tail, container || undefined);
setLogs(res?.data?.logs || res?.logs || res || '无日志');
} catch {
setLogs('获取日志失败');
@@ -1,5 +1,6 @@
// k8s-event-utils — K8s Event 字段归一化与展示 // Logan.Li
import { elapsedSeconds, formatDuration, formatK8sTime } from './k8s-yaml-utils';
import type { JsonData } from '@/types/common';
export interface NormalizedEvent {
name: string;
@@ -18,7 +19,7 @@ export interface NormalizedEvent {
}
/** 后端/API 原始事件 → 统一结构 // Logan.Li */
export function normalizeK8sEvent(raw: Record<string, any>): NormalizedEvent {
export function normalizeK8sEvent(raw: JsonData): NormalizedEvent {
const objectKind = raw.object_kind || raw.objectKind || '';
const objectName = raw.object_name || raw.objectName || '';
const object = raw.object
@@ -1,5 +1,6 @@
// K8s manifest 解析/序列化/精简共用工具 // Logan.Li
import { cleanYAML } from './k8s-yaml-utils';
import type { JsonData } from '@/types/common';
/** 主流方案:编辑/展示时应排除的 metadata 注解 // Logan.Li */
const STRIP_ANNOTATIONS = new Set([
@@ -91,7 +92,7 @@ function matchesDefault(val: unknown, def: unknown): boolean {
return false;
}
function stripMetadataBlock(meta: Record<string, any> | undefined): void {
function stripMetadataBlock(meta: JsonData | undefined): void {
if (!meta || typeof meta !== 'object') return;
for (const k of STRIP_METADATA_KEYS) delete meta[k];
if (meta.annotations) {
@@ -124,7 +125,7 @@ function pruneEmpty(node: unknown): void {
}
}
function stripObjectDefaults(obj: Record<string, any> | undefined, defaults: Record<string, unknown>): void {
function stripObjectDefaults(obj: JsonData | undefined, defaults: Record<string, unknown>): void {
if (!obj || typeof obj !== 'object') return;
for (const [key, def] of Object.entries(defaults)) {
if (key in obj && matchesDefault(obj[key], def)) delete obj[key];
@@ -140,7 +141,7 @@ function isSystemMountPath(path: string): boolean {
}
/** 移除系统卷及对应 volumeMount // Logan.Li */
function stripSystemVolumes(spec: Record<string, any> | undefined): void {
function stripSystemVolumes(spec: JsonData | undefined): void {
if (!spec || typeof spec !== 'object') return;
const volumes: Array<{ name?: string }> = spec.volumes;
if (Array.isArray(volumes)) {
@@ -150,7 +151,7 @@ function stripSystemVolumes(spec: Record<string, any> | undefined): void {
spec.volumes = volumes.filter((v) => !v?.name || !isSystemVolumeName(v.name));
if (spec.volumes.length === 0) delete spec.volumes;
const stripMounts = (containers: Array<Record<string, any>> | undefined) => {
const stripMounts = (containers: Array<JsonData> | undefined) => {
if (!Array.isArray(containers)) return;
for (const c of containers) {
if (!Array.isArray(c.volumeMounts)) continue;
@@ -168,7 +169,7 @@ function stripSystemVolumes(spec: Record<string, any> | undefined): void {
}
}
function stripContainerDefaults(container: Record<string, any>): void {
function stripContainerDefaults(container: JsonData): void {
if (!container || typeof container !== 'object') return;
stripObjectDefaults(container, CONTAINER_DEFAULTS);
if (container.imagePullPolicy) {
@@ -181,7 +182,7 @@ function stripContainerDefaults(container: Record<string, any>): void {
if (container.securityContext && isEmptyCollection(container.securityContext)) delete container.securityContext;
}
function stripPodSpecDefaults(spec: Record<string, any> | undefined): void {
function stripPodSpecDefaults(spec: JsonData | undefined): void {
if (!spec || typeof spec !== 'object') return;
stripObjectDefaults(spec, POD_SPEC_DEFAULTS);
if ('serviceAccount' in spec) delete spec.serviceAccount;
@@ -195,7 +196,7 @@ function stripPodSpecDefaults(spec: Record<string, any> | undefined): void {
}
/** Deployment 默认滚动更新策略 // Logan.Li */
function stripDeploymentStrategy(spec: Record<string, any> | undefined): void {
function stripDeploymentStrategy(spec: JsonData | undefined): void {
if (!spec?.strategy || spec.strategy.type !== 'RollingUpdate') return;
const ru = spec.strategy.rollingUpdate;
if (!ru || typeof ru !== 'object') return;
@@ -208,7 +209,7 @@ function stripDeploymentStrategy(spec: Record<string, any> | undefined): void {
}
}
function stripServiceSpec(spec: Record<string, any> | undefined): void {
function stripServiceSpec(spec: JsonData | undefined): void {
if (!spec || typeof spec !== 'object') return;
for (const k of SERVICE_RUNTIME_KEYS) delete spec[k];
stripObjectDefaults(spec, SERVICE_SPEC_DEFAULTS);
@@ -223,19 +224,19 @@ function stripServiceSpec(spec: Record<string, any> | undefined): void {
}
/** Ingress 精简(对齐集群导出 cleanResourceYAML)// Logan.Li */
function stripIngressSpec(spec: Record<string, any> | undefined): void {
function stripIngressSpec(spec: JsonData | undefined): void {
if (!spec || typeof spec !== 'object') return;
if (!spec.ingressClassName) delete spec.ingressClassName;
}
function stripWorkloadTemplate(spec: Record<string, any> | undefined): void {
function stripWorkloadTemplate(spec: JsonData | undefined): void {
if (!spec?.template || typeof spec.template !== 'object') return;
if (spec.template.metadata) stripMetadataBlock(spec.template.metadata);
const tplSpec = spec.template.spec;
if (tplSpec) stripPodSpecDefaults(tplSpec);
}
function stripKindDefaults(manifest: Record<string, any>): void {
function stripKindDefaults(manifest: JsonData): void {
const kind = String(manifest.kind || '').toLowerCase();
if (!manifest.spec) return;
if (kind === 'deployment' || kind === 'statefulset' || kind === 'daemonset') {
@@ -249,7 +250,7 @@ function stripKindDefaults(manifest: Record<string, any>): void {
}
/** 对象级剥离运行时字段 // Logan.Li */
export function stripRuntimeFields(manifest: Record<string, any>): Record<string, any> {
export function stripRuntimeFields(manifest: JsonData): JsonData {
const cloned = JSON.parse(JSON.stringify(manifest));
delete cloned.status;
stripMetadataBlock(cloned.metadata);
@@ -260,23 +261,23 @@ export function stripRuntimeFields(manifest: Record<string, any>): Record<string
return cloned;
}
export async function parseManifest(text: string): Promise<Record<string, any> | null> {
export async function parseManifest(text: string): Promise<JsonData | null> {
if (!text?.trim()) return null;
try { return JSON.parse(text); } catch { /* fallthrough */ }
try {
const jsyaml: Record<string, any> = await import('js-yaml');
const jsyaml: JsonData = await import('js-yaml');
const parsed = jsyaml.load(text);
return parsed && typeof parsed === 'object' ? parsed : null;
} catch { return null; }
}
export async function dumpManifest(manifest: Record<string, any>): Promise<string> {
const jsyaml: Record<string, any> = await import('js-yaml');
export async function dumpManifest(manifest: JsonData): Promise<string> {
const jsyaml: JsonData = await import('js-yaml');
return jsyaml.dump(manifest, { lineWidth: 120, noRefs: true, sortKeys: false });
}
/** manifest → 精简 YAML(对象剥离 + 文本级默认值清理)// Logan.Li */
export async function manifestToEditableYaml(manifest: Record<string, any>): Promise<string> {
export async function manifestToEditableYaml(manifest: JsonData): Promise<string> {
const stripped = stripRuntimeFields(manifest);
const dumped = await dumpManifest(stripped);
return cleanYAML(dumped);
@@ -284,7 +285,7 @@ export async function manifestToEditableYaml(manifest: Record<string, any>): Pro
/** 原始 YAML → 可编辑 manifest + 精简 YAML // Logan.Li */
export async function parseEditableManifest(text: string): Promise<{
manifest: Record<string, any> | null;
manifest: JsonData | null;
yaml: string;
}> {
const parsed = await parseManifest(text);
@@ -295,7 +296,7 @@ export async function parseEditableManifest(text: string): Promise<{
}
/** 保存前:剥离运行时字段 + 清理默认值 // Logan.Li */
export async function prepareManifestForSave(manifest: Record<string, any>): Promise<string> {
export async function prepareManifestForSave(manifest: JsonData): Promise<string> {
return manifestToEditableYaml(manifest);
}
@@ -1,6 +1,7 @@
import type { JsonData } from "@/types/common";
// 网络/配置/存储资源新建时的空 manifest 模板 // Logan.Li
export function createEmptyManifest(resource: string, namespace: string, name: string): Record<string, any> {
export function createEmptyManifest(resource: string, namespace: string, name: string): JsonData {
switch (resource) {
case 'services':
return {
@@ -2,6 +2,7 @@ import {
Network, Server, Globe, Cloud, Boxes, Database, Zap, Box,
} from 'lucide-react';
import type { TopologyNode, TopologyEdge } from '@/types';
import type { JsonData } from '@/types/common';
// K8s 资源类型 → 拓扑节点类型标签 // Logan.Li
export const NODE_TYPE_LABELS: Record<string, string> = {
@@ -59,7 +60,7 @@ export function getNodeColor(type: string) {
* buildTopology — 从真实 K8s 资源构建分层拓扑图
* 层级: Cluster → Ingress → Service → Deployment → Node
*/
export function buildTopology( cluster: any, ingresses: any[], services: any[], deployments: any[], nodes: any[],
export function buildTopology(cluster: JsonData, ingresses: JsonData[], services: JsonData[], deployments: JsonData[], nodes: JsonData[],
): { nodes: TopologyNode[]; edges: TopologyEdge[] } {
const tNodes: TopologyNode[] = [];
const tEdges: TopologyEdge[] = [];