1.30 k8s管理

This commit is contained in:
dengqichen 2025-12-13 13:58:12 +08:00
parent be6b6d75ac
commit faf983afe0
10 changed files with 1152 additions and 576 deletions

View File

@ -10,7 +10,7 @@ const Forbidden: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleGoHome = () => { const handleGoHome = () => {
navigate('/dashboard', { replace: true }); navigate('/', { replace: true });
}; };
const handleGoBack = () => { const handleGoBack = () => {

View File

@ -10,7 +10,7 @@ const NotFound: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleGoHome = () => { const handleGoHome = () => {
navigate('/dashboard', { replace: true }); navigate('/', { replace: true });
}; };
const handleGoBack = () => { const handleGoBack = () => {

View File

@ -0,0 +1,273 @@
import React, { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Package, Tag, FileCode, Copy, Check, Box } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { YamlViewerDialog } from './YamlViewerDialog';
import { PodListDialog } from './PodListDialog';
import type { K8sDeploymentResponse } from '../types';
import dayjs from 'dayjs';
interface DeploymentCardProps {
deployment: K8sDeploymentResponse;
}
export const DeploymentCard: React.FC<DeploymentCardProps> = ({ deployment }) => {
const { toast } = useToast();
const [labelDialogOpen, setLabelDialogOpen] = useState(false);
const [yamlDialogOpen, setYamlDialogOpen] = useState(false);
const [podListDialogOpen, setPodListDialogOpen] = useState(false);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false);
const [copiedImage, setCopiedImage] = useState(false);
const labelCount = deployment.labels ? Object.keys(deployment.labels).length : 0;
const getReplicaStatus = () => {
const ready = deployment.readyReplicas ?? 0;
const desired = deployment.replicas ?? 0;
if (desired === 0) {
return { variant: 'secondary' as const, text: '0/0' };
}
if (ready === desired) {
return { variant: 'default' as const, text: `${ready}/${desired}` };
} else if (ready > 0) {
return { variant: 'secondary' as const, text: `${ready}/${desired}` };
} else {
return { variant: 'destructive' as const, text: `${ready}/${desired}` };
}
};
const status = getReplicaStatus();
const handleCopyImage = (e: React.MouseEvent) => {
e.stopPropagation();
if (!deployment.image) return;
navigator.clipboard.writeText(deployment.image).then(() => {
setCopiedImage(true);
toast({
title: '已复制',
description: '已复制镜像地址到剪贴板',
});
setTimeout(() => setCopiedImage(false), 2000);
});
};
const handleCopyLabel = (key: string, value: string, index: number) => {
const text = `${key}:${value}`;
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
toast({
title: '已复制',
description: `已复制标签到剪贴板`,
});
setTimeout(() => setCopiedIndex(null), 2000);
});
};
const handleCopyAll = () => {
if (!deployment.labels || Object.keys(deployment.labels).length === 0) return;
const allLabelsText = Object.entries(deployment.labels)
.map(([key, value]) => `${key}:${value}`)
.join('\n');
navigator.clipboard.writeText(allLabelsText).then(() => {
setCopiedAll(true);
toast({
title: '已复制',
description: `已复制全部${labelCount}个标签`,
});
setTimeout(() => setCopiedAll(false), 2000);
});
};
return (
<>
<Card className="hover:shadow-lg transition-all hover:border-primary/50 border-border">
<CardContent className="p-4">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="flex-shrink-0">
<Package className="h-5 w-5 text-primary" />
</div>
<h3 className="font-medium text-base truncate" title={deployment.deploymentName}>
{deployment.deploymentName}
</h3>
</div>
<Badge variant={status.variant} className="flex-shrink-0 ml-2">
{status.text}
</Badge>
</div>
<div className="space-y-2 text-sm text-muted-foreground">
{deployment.image && (
<div className="flex items-center justify-between gap-2 group">
<div className="truncate flex-1" title={deployment.image}>
<span className="font-medium">:</span> {deployment.image}
</div>
<Button
variant="ghost"
size="sm"
onClick={handleCopyImage}
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
>
{copiedImage ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
)}
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">{labelCount}</span>
<span></span>
{labelCount > 0 && <Tag className="h-3 w-3" />}
</div>
</div>
{deployment.k8sUpdateTime && (
<div className="text-xs">
: {dayjs(deployment.k8sUpdateTime).format('YYYY-MM-DD HH:mm')}
</div>
)}
</div>
<div className="mt-4 pt-3 border-t flex gap-2">
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
setLabelDialogOpen(true);
}}
className="h-8 w-8 p-0"
title="查看标签"
>
<Tag className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
setYamlDialogOpen(true);
}}
className="h-8 w-8 p-0"
title="查看YAML"
>
<FileCode className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => {
e.stopPropagation();
setPodListDialogOpen(true);
}}
className="h-8 flex-1"
title="查看Pod"
>
<Box className="h-4 w-4 mr-1" />
Pod
</Button>
</div>
</CardContent>
</Card>
{/* 标签Dialog */}
{labelDialogOpen && (
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => setLabelDialogOpen(false)}>
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-2xl bg-white rounded-lg shadow-lg p-6" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold flex items-center gap-2">
<Tag className="h-5 w-5" />
</h3>
<Button
variant="outline"
size="sm"
onClick={handleCopyAll}
className="flex items-center gap-2"
>
{copiedAll ? (
<>
<Check className="h-4 w-4 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4" />
</>
)}
</Button>
</div>
<div className="max-h-[60vh] overflow-y-auto">
{deployment.labels && Object.keys(deployment.labels).length > 0 ? (
<div className="space-y-2">
{Object.entries(deployment.labels).map(([key, value], index) => (
<div
key={key}
className="flex items-center justify-between gap-4 p-3 rounded-lg border border-border hover:border-primary/50 transition-colors bg-muted/30"
>
<div className="flex-1 min-w-0 font-mono text-sm">
<span className="text-blue-600 dark:text-blue-400 font-semibold">{key}</span>
<span className="text-muted-foreground">:</span>
<span className="text-foreground">{value}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyLabel(key, value, index)}
className="flex-shrink-0"
>
{copiedIndex === index ? (
<>
<Check className="h-4 w-4 mr-1 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
</>
)}
</Button>
</div>
))}
</div>
) : (
<div className="text-center text-muted-foreground py-8">
</div>
)}
</div>
</div>
</div>
)}
{/* YAML查看器 */}
<YamlViewerDialog
open={yamlDialogOpen}
onOpenChange={setYamlDialogOpen}
title={`Deployment: ${deployment.deploymentName} - YAML配置`}
yamlContent={deployment.yamlConfig}
/>
{/* Pod列表 */}
<PodListDialog
open={podListDialogOpen}
onOpenChange={setPodListDialogOpen}
deploymentId={deployment.id}
deploymentName={deployment.deploymentName}
/>
</>
);
};

View File

@ -1,323 +0,0 @@
import React, { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogBody,
} from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Loader2, Package, Tag, FileCode, Copy, Check } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { YamlViewerDialog } from './YamlViewerDialog';
import type { K8sDeploymentResponse } from '../types';
import { getK8sDeploymentByNamespace } from '../service';
import dayjs from 'dayjs';
interface DeploymentDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
namespaceName: string;
namespaceId: number;
externalSystemId: number;
}
export const DeploymentDialog: React.FC<DeploymentDialogProps> = ({
open,
onOpenChange,
namespaceName,
namespaceId,
externalSystemId,
}) => {
const { toast } = useToast();
const [deployments, setDeployments] = useState<K8sDeploymentResponse[]>([]);
const [loading, setLoading] = useState(false);
const [labelDialogOpen, setLabelDialogOpen] = useState(false);
const [yamlDialogOpen, setYamlDialogOpen] = useState(false);
const [selectedDeployment, setSelectedDeployment] = useState<K8sDeploymentResponse | null>(null);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false);
const [copiedImageId, setCopiedImageId] = useState<number | null>(null);
const loadDeployments = async () => {
setLoading(true);
try {
const data = await getK8sDeploymentByNamespace(externalSystemId, namespaceId);
setDeployments(data || []);
} catch (error) {
console.error('加载Deployment失败:', error);
toast({
title: '加载失败',
description: '加载Deployment列表失败',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open && namespaceId && externalSystemId) {
loadDeployments();
}
}, [open, namespaceId, externalSystemId]);
const handleViewLabels = (deployment: K8sDeploymentResponse, e: React.MouseEvent) => {
e.stopPropagation();
setSelectedDeployment(deployment);
setLabelDialogOpen(true);
};
const handleViewYaml = (deployment: K8sDeploymentResponse, e: React.MouseEvent) => {
e.stopPropagation();
setSelectedDeployment(deployment);
setYamlDialogOpen(true);
};
const handleCopyLabel = (key: string, value: string, index: number) => {
const text = `${key}:${value}`;
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
toast({
title: '已复制',
description: `已复制标签到剪贴板`,
});
setTimeout(() => setCopiedIndex(null), 2000);
});
};
const handleCopyAll = () => {
if (!selectedDeployment?.labels || Object.keys(selectedDeployment.labels).length === 0) return;
const allLabelsText = Object.entries(selectedDeployment.labels)
.map(([key, value]) => `${key}:${value}`)
.join('\n');
navigator.clipboard.writeText(allLabelsText).then(() => {
setCopiedAll(true);
const labelCount = Object.keys(selectedDeployment.labels!).length;
toast({
title: '已复制',
description: `已复制全部${labelCount}个标签`,
});
setTimeout(() => setCopiedAll(false), 2000);
});
};
const handleCopyImage = (image: string, deploymentId: number, e: React.MouseEvent) => {
e.stopPropagation();
navigator.clipboard.writeText(image).then(() => {
setCopiedImageId(deploymentId);
toast({
title: '已复制',
description: '已复制镜像地址到剪贴板',
});
setTimeout(() => setCopiedImageId(null), 2000);
});
};
const getReplicaStatus = (deployment: K8sDeploymentResponse) => {
const ready = deployment.readyReplicas ?? 0;
const desired = deployment.replicas ?? 0;
if (desired === 0) {
return { variant: 'secondary' as const, text: '0/0' };
}
if (ready === desired) {
return { variant: 'default' as const, text: `${ready}/${desired}` };
} else if (ready > 0) {
return { variant: 'secondary' as const, text: `${ready}/${desired}` };
} else {
return { variant: 'destructive' as const, text: `${ready}/${desired}` };
}
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Package className="h-5 w-5 text-primary" />
<span>: {namespaceName} Deployment</span>
</DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : deployments.length === 0 ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
Deployment
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{deployments.map((deployment) => {
const status = getReplicaStatus(deployment);
return (
<div
key={deployment.id}
className="border rounded-lg p-4 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between mb-3">
<h4 className="font-semibold truncate" title={deployment.deploymentName}>
{deployment.deploymentName}
</h4>
<Badge variant={status.variant}>
{status.text}
</Badge>
</div>
<div className="space-y-2 text-sm text-muted-foreground">
{deployment.image && (
<div className="flex items-center justify-between gap-2 group">
<div className="truncate flex-1" title={deployment.image}>
<span className="font-medium">:</span> {deployment.image}
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleCopyImage(deployment.image!, deployment.id, e)}
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
>
{copiedImageId === deployment.id ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
)}
{deployment.labels && Object.keys(deployment.labels).length > 0 ? (
<div>
<span className="font-medium">:</span> {Object.keys(deployment.labels).length}
</div>
) : (
<div className="h-5 w-24 bg-muted rounded animate-pulse" />
)}
{deployment.k8sUpdateTime && (
<div className="text-xs">
: {dayjs(deployment.k8sUpdateTime).format('YYYY-MM-DD HH:mm')}
</div>
)}
</div>
<div className="mt-4 pt-3 border-t">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={(e) => handleViewLabels(deployment, e)}
className="h-8 w-8 p-0"
title="查看标签"
>
<Tag className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => handleViewYaml(deployment, e)}
className="h-8 w-8 p-0"
title="查看YAML"
>
<FileCode className="h-4 w-4" />
</Button>
</div>
</div>
</div>
);
})}
</div>
)}
</DialogBody>
</DialogContent>
</Dialog>
{/* 标签Dialog */}
<Dialog open={labelDialogOpen} onOpenChange={setLabelDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
- {selectedDeployment?.deploymentName}
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={handleCopyAll}
className="flex items-center gap-2"
>
{copiedAll ? (
<>
<Check className="h-4 w-4 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4" />
</>
)}
</Button>
</div>
</DialogHeader>
<DialogBody className="max-h-[60vh] overflow-y-auto">
{selectedDeployment?.labels && Object.keys(selectedDeployment.labels).length > 0 ? (
<div className="space-y-2">
{Object.entries(selectedDeployment.labels).map(([key, value], index) => (
<div
key={key}
className="flex items-center justify-between gap-4 p-3 rounded-lg border border-border hover:border-primary/50 transition-colors bg-muted/30"
>
<div className="flex-1 min-w-0 font-mono text-sm">
<span className="text-blue-600 dark:text-blue-400 font-semibold">{key}</span>
<span className="text-muted-foreground">:</span>
<span className="text-foreground">{value}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyLabel(key, value, index)}
className="flex-shrink-0"
>
{copiedIndex === index ? (
<>
<Check className="h-4 w-4 mr-1 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
</>
)}
</Button>
</div>
))}
</div>
) : (
<div className="text-center text-muted-foreground py-8">
</div>
)}
</DialogBody>
</DialogContent>
</Dialog>
{/* YAML查看器 */}
<YamlViewerDialog
open={yamlDialogOpen}
onOpenChange={setYamlDialogOpen}
title={`Deployment: ${selectedDeployment?.deploymentName} - YAML配置`}
yamlContent={selectedDeployment?.yamlConfig}
/>
</>
);
};

View File

@ -1,219 +0,0 @@
import React, { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Database, Tag, Copy, Check, FileCode } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { YamlViewerDialog } from './YamlViewerDialog';
import type { K8sNamespaceResponse } from '../types';
interface NamespaceCardProps {
namespace: K8sNamespaceResponse;
onClick: () => void;
}
export const NamespaceCard: React.FC<NamespaceCardProps> = ({ namespace, onClick }) => {
const { toast } = useToast();
const [labelDialogOpen, setLabelDialogOpen] = useState(false);
const [yamlDialogOpen, setYamlDialogOpen] = useState(false);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false);
const labelCount = namespace.labels ? Object.keys(namespace.labels).length : 0;
const deploymentCount = namespace.deploymentCount ?? 0;
const handleCopyLabel = (key: string, value: string, index: number) => {
const text = `${key}:${value}`;
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
toast({
title: '已复制',
description: `已复制标签到剪贴板`,
});
setTimeout(() => setCopiedIndex(null), 2000);
});
};
const handleCopyAll = () => {
if (!namespace.labels || Object.keys(namespace.labels).length === 0) return;
const allLabelsText = Object.entries(namespace.labels)
.map(([key, value]) => `${key}:${value}`)
.join('\n');
navigator.clipboard.writeText(allLabelsText).then(() => {
setCopiedAll(true);
toast({
title: '已复制',
description: `已复制全部${labelCount}个标签`,
});
setTimeout(() => setCopiedAll(false), 2000);
});
};
const handleLabelClick = (e: React.MouseEvent) => {
e.stopPropagation();
if (labelCount > 0) {
setLabelDialogOpen(true);
}
};
const handleYamlClick = (e: React.MouseEvent) => {
e.stopPropagation();
setYamlDialogOpen(true);
};
return (
<>
<Card
className="cursor-pointer hover:shadow-lg transition-all hover:border-primary/50 border-border"
onClick={onClick}
>
<CardContent className="p-4">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3 min-w-0 flex-1">
<div className="flex-shrink-0">
<Database className="h-5 w-5 text-primary" />
</div>
<h3 className="font-medium text-base truncate" title={namespace.namespaceName}>
{namespace.namespaceName}
</h3>
</div>
{namespace.status && (
<Badge
variant={namespace.status === 'Active' ? 'default' : 'secondary'}
className={`text-xs flex-shrink-0 ml-2 ${namespace.status === 'Active' ? 'bg-green-600 hover:bg-green-700' : ''}`}
>
{namespace.status}
</Badge>
)}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">{deploymentCount}</span>
<span>Deployment</span>
</div>
<div className="flex items-center gap-1.5">
<span className="font-medium text-foreground">{labelCount}</span>
<span></span>
{labelCount > 0 && <Tag className="h-3 w-3" />}
</div>
</div>
{namespace.createTime && (
<div className="mt-3 pt-3 border-t text-xs text-muted-foreground">
{new Date(namespace.createTime).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})}
</div>
)}
<div className="mt-3 pt-3 border-t flex gap-2">
<Button
variant="outline"
size="sm"
onClick={handleLabelClick}
className="h-8 w-8 p-0"
title="查看标签"
>
<Tag className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={handleYamlClick}
className="h-8 w-8 p-0"
title="查看YAML"
>
<FileCode className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
{/* 标签Dialog */}
<Dialog open={labelDialogOpen} onOpenChange={setLabelDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={handleCopyAll}
className="flex items-center gap-2"
>
{copiedAll ? (
<>
<Check className="h-4 w-4 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4" />
</>
)}
</Button>
</div>
</DialogHeader>
<DialogBody className="max-h-[60vh] overflow-y-auto">
{namespace.labels && Object.keys(namespace.labels).length > 0 ? (
<div className="space-y-2">
{Object.entries(namespace.labels).map(([key, value], index) => (
<div
key={key}
className="flex items-center justify-between gap-4 p-3 rounded-lg border border-border hover:border-primary/50 transition-colors bg-muted/30"
>
<div className="flex-1 min-w-0 font-mono text-sm">
<span className="text-blue-600 dark:text-blue-400 font-semibold">{key}</span>
<span className="text-muted-foreground">:</span>
<span className="text-foreground">{value}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyLabel(key, value, index)}
className="flex-shrink-0"
>
{copiedIndex === index ? (
<>
<Check className="h-4 w-4 mr-1 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
</>
)}
</Button>
</div>
))}
</div>
) : (
<div className="text-center text-muted-foreground py-8">
</div>
)}
</DialogBody>
</DialogContent>
</Dialog>
{/* YAML查看器 */}
<YamlViewerDialog
open={yamlDialogOpen}
onOpenChange={setYamlDialogOpen}
title={`命名空间: ${namespace.namespaceName} - YAML配置`}
yamlContent={namespace.yamlConfig}
/>
</>
);
};

View File

@ -0,0 +1,408 @@
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Box, Tag, FileCode, Copy, Check, Network, Server, Package } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { YamlViewerDialog } from './YamlViewerDialog';
import type { K8sPodResponse } from '../types';
import { PodPhaseLabels, ContainerStateLabels } from '../types';
import dayjs from 'dayjs';
interface PodDetailDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
pod: K8sPodResponse;
deploymentId: number;
}
export const PodDetailDialog: React.FC<PodDetailDialogProps> = ({
open,
onOpenChange,
pod,
}) => {
const { toast } = useToast();
const [yamlDialogOpen, setYamlDialogOpen] = useState(false);
const [labelDialogOpen, setLabelDialogOpen] = useState(false);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false);
const [copiedField, setCopiedField] = useState<string | null>(null);
const phaseLabel = PodPhaseLabels[pod.phase];
const handleCopy = (text: string, field: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedField(field);
toast({
title: '已复制',
description: `已复制${field}到剪贴板`,
});
setTimeout(() => setCopiedField(null), 2000);
});
};
const handleCopyLabel = (key: string, value: string, index: number) => {
const text = `${key}:${value}`;
navigator.clipboard.writeText(text).then(() => {
setCopiedIndex(index);
toast({
title: '已复制',
description: `已复制标签到剪贴板`,
});
setTimeout(() => setCopiedIndex(null), 2000);
});
};
const handleCopyAll = () => {
if (!pod.labels || Object.keys(pod.labels).length === 0) return;
const allLabelsText = Object.entries(pod.labels)
.map(([key, value]) => `${key}:${value}`)
.join('\n');
navigator.clipboard.writeText(allLabelsText).then(() => {
setCopiedAll(true);
const labelCount = Object.keys(pod.labels!).length;
toast({
title: '已复制',
description: `已复制全部${labelCount}个标签`,
});
setTimeout(() => setCopiedAll(false), 2000);
});
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Box className="h-5 w-5 text-primary" />
<span>Pod详情: {pod.name}</span>
</DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-y-auto space-y-6">
{/* 概览信息 */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<Package className="h-4 w-4" />
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-muted-foreground">:</span>
<Badge variant={phaseLabel.variant} className={`ml-2 ${phaseLabel.color}`}>
{phaseLabel.label}
</Badge>
</div>
<div>
<span className="text-muted-foreground">:</span>
<Badge variant={pod.ready ? 'default' : 'destructive'} className="ml-2">
{pod.ready ? '是' : '否'}
</Badge>
</div>
<div>
<span className="text-muted-foreground">:</span>
<span className="ml-2 font-medium">{pod.restartCount}</span>
</div>
{pod.ownerKind && (
<div>
<span className="text-muted-foreground">:</span>
<span className="ml-2 font-medium">{pod.ownerKind}</span>
</div>
)}
{pod.ownerName && (
<div className="col-span-2">
<span className="text-muted-foreground">:</span>
<span className="ml-2 font-medium">{pod.ownerName}</span>
</div>
)}
</div>
</div>
{/* 网络信息 */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<Network className="h-4 w-4" />
</h3>
<div className="space-y-2 text-sm">
{pod.podIP && (
<div className="flex items-center justify-between group">
<span className="text-muted-foreground">Pod IP:</span>
<div className="flex items-center gap-2">
<span className="font-mono">{pod.podIP}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopy(pod.podIP!, 'Pod IP')}
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedField === 'Pod IP' ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</div>
)}
{pod.hostIP && (
<div className="flex items-center justify-between group">
<span className="text-muted-foreground">宿IP:</span>
<div className="flex items-center gap-2">
<span className="font-mono">{pod.hostIP}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopy(pod.hostIP!, '宿主机IP')}
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedField === '宿主机IP' ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</div>
)}
{pod.nodeName && (
<div className="flex items-center justify-between group">
<span className="text-muted-foreground flex items-center gap-1">
<Server className="h-3 w-3" />
:
</span>
<div className="flex items-center gap-2">
<span className="font-mono">{pod.nodeName}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopy(pod.nodeName!, '节点名称')}
className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedField === '节点名称' ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</div>
)}
</div>
</div>
{/* 容器列表 */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3"> ({pod.containers.length})</h3>
<div className="space-y-3">
{pod.containers.map((container, index) => {
const stateLabel = ContainerStateLabels[container.state];
return (
<div key={index} className="border rounded p-3 bg-muted/30">
<div className="flex items-center justify-between mb-2">
<span className="font-medium">{container.name}</span>
<div className="flex items-center gap-2">
<Badge variant={container.ready ? 'default' : 'secondary'}>
{container.ready ? '就绪' : '未就绪'}
</Badge>
<span className={`text-xs ${stateLabel.color}`}>
{stateLabel.label}
</span>
</div>
</div>
<div className="space-y-1 text-xs text-muted-foreground">
<div className="flex items-center justify-between group">
<span>:</span>
<div className="flex items-center gap-1">
<span className="font-mono text-xs truncate max-w-xs" title={container.image}>
{container.image}
</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopy(container.image, `镜像-${container.name}`)}
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedField === `镜像-${container.name}` ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</div>
<div className="flex justify-between">
<span>:</span>
<span>{container.restartCount}</span>
</div>
{(container.cpuRequest || container.cpuLimit) && (
<div className="flex justify-between">
<span>CPU:</span>
<span>{container.cpuRequest || '-'} / {container.cpuLimit || '-'}</span>
</div>
)}
{(container.memoryRequest || container.memoryLimit) && (
<div className="flex justify-between">
<span>:</span>
<span>{container.memoryRequest || '-'} / {container.memoryLimit || '-'}</span>
</div>
)}
{container.startedAt && (
<div className="flex justify-between">
<span>:</span>
<span>{dayjs(container.startedAt).format('YYYY-MM-DD HH:mm:ss')}</span>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
{/* 时间信息 */}
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-3"></h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{dayjs(pod.creationTimestamp).format('YYYY-MM-DD HH:mm:ss')}</span>
</div>
{pod.startTime && (
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span>{dayjs(pod.startTime).format('YYYY-MM-DD HH:mm:ss')}</span>
</div>
)}
</div>
</div>
{/* 标签和注解 */}
{(pod.labels && Object.keys(pod.labels).length > 0) && (
<div className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold flex items-center gap-2">
<Tag className="h-4 w-4" />
({Object.keys(pod.labels).length})
</h3>
<Button
variant="outline"
size="sm"
onClick={() => setLabelDialogOpen(true)}
>
</Button>
</div>
<div className="flex flex-wrap gap-2">
{Object.entries(pod.labels).slice(0, 5).map(([key, value]) => (
<Badge key={key} variant="secondary" className="font-mono text-xs">
{key}: {value}
</Badge>
))}
{Object.keys(pod.labels).length > 5 && (
<Badge variant="outline">+{Object.keys(pod.labels).length - 5} </Badge>
)}
</div>
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => setYamlDialogOpen(true)}
className="flex items-center gap-2"
>
<FileCode className="h-4 w-4" />
YAML
</Button>
</div>
</DialogBody>
</DialogContent>
</Dialog>
{/* 标签Dialog */}
<Dialog open={labelDialogOpen} onOpenChange={setLabelDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<div className="flex items-center justify-between">
<DialogTitle className="flex items-center gap-2">
<Tag className="h-5 w-5" />
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={handleCopyAll}
className="flex items-center gap-2"
>
{copiedAll ? (
<>
<Check className="h-4 w-4 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4" />
</>
)}
</Button>
</div>
</DialogHeader>
<DialogBody className="max-h-[60vh] overflow-y-auto">
{pod.labels && Object.keys(pod.labels).length > 0 ? (
<div className="space-y-2">
{Object.entries(pod.labels).map(([key, value], index) => (
<div
key={key}
className="flex items-center justify-between gap-4 p-3 rounded-lg border border-border hover:border-primary/50 transition-colors bg-muted/30"
>
<div className="flex-1 min-w-0 font-mono text-sm">
<span className="text-blue-600 dark:text-blue-400 font-semibold">{key}</span>
<span className="text-muted-foreground">:</span>
<span className="text-foreground">{value}</span>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopyLabel(key, value, index)}
className="flex-shrink-0"
>
{copiedIndex === index ? (
<>
<Check className="h-4 w-4 mr-1 text-green-600" />
</>
) : (
<>
<Copy className="h-4 w-4 mr-1" />
</>
)}
</Button>
</div>
))}
</div>
) : (
<div className="text-center text-muted-foreground py-8">
</div>
)}
</DialogBody>
</DialogContent>
</Dialog>
{/* YAML查看器 */}
<YamlViewerDialog
open={yamlDialogOpen}
onOpenChange={setYamlDialogOpen}
title={`Pod: ${pod.name} - YAML配置`}
yamlContent={pod.yamlConfig}
/>
</>
);
};

View File

@ -0,0 +1,196 @@
import React, { useEffect, useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Loader2, Box, Copy, Check, Network, Server } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { PodDetailDialog } from './PodDetailDialog';
import { getK8sPodsByDeployment } from '../service';
import type { K8sPodResponse } from '../types';
import { PodPhaseLabels } from '../types';
import dayjs from 'dayjs';
interface PodListDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
deploymentId: number;
deploymentName: string;
}
export const PodListDialog: React.FC<PodListDialogProps> = ({
open,
onOpenChange,
deploymentId,
deploymentName,
}) => {
const { toast } = useToast();
const [pods, setPods] = useState<K8sPodResponse[]>([]);
const [loading, setLoading] = useState(false);
const [selectedPod, setSelectedPod] = useState<K8sPodResponse | null>(null);
const [podDetailOpen, setPodDetailOpen] = useState(false);
const [copiedField, setCopiedField] = useState<string | null>(null);
const loadPods = async () => {
setLoading(true);
try {
const data = await getK8sPodsByDeployment(deploymentId);
setPods(data || []);
} catch (error) {
console.error('加载Pod失败:', error);
toast({
title: '加载失败',
description: '加载Pod列表失败',
variant: 'destructive',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open && deploymentId) {
loadPods();
}
}, [open, deploymentId]);
const handlePodClick = (pod: K8sPodResponse) => {
setSelectedPod(pod);
setPodDetailOpen(true);
};
const handleCopy = (text: string, field: string, e: React.MouseEvent) => {
e.stopPropagation();
navigator.clipboard.writeText(text).then(() => {
setCopiedField(field);
toast({
title: '已复制',
description: `已复制${field}到剪贴板`,
});
setTimeout(() => setCopiedField(null), 2000);
});
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Box className="h-5 w-5 text-primary" />
<span>Deployment: {deploymentName} Pod</span>
</DialogTitle>
</DialogHeader>
<DialogBody className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : pods.length === 0 ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
Deployment下暂无Pod
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{pods.map((pod) => {
const phaseLabel = PodPhaseLabels[pod.phase];
return (
<div
key={pod.name}
className="border rounded-lg p-4 hover:shadow-md transition-all cursor-pointer hover:border-primary/50"
onClick={() => handlePodClick(pod)}
>
<div className="flex items-start justify-between mb-3">
<h4 className="font-semibold text-sm truncate flex-1" title={pod.name}>
{pod.name}
</h4>
<Badge variant={phaseLabel.variant} className={phaseLabel.color}>
{phaseLabel.label}
</Badge>
</div>
<div className="space-y-2 text-xs text-muted-foreground">
<div className="flex items-center justify-between">
<span>:</span>
<Badge variant={pod.ready ? 'default' : 'destructive'} className="text-xs">
{pod.ready ? '就绪' : '未就绪'}
</Badge>
</div>
{pod.restartCount > 0 && (
<div className="flex items-center justify-between">
<span>:</span>
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800 text-xs">
{pod.restartCount}
</Badge>
</div>
)}
{pod.nodeName && (
<div className="flex items-center gap-1 group">
<Server className="h-3 w-3" />
<span className="truncate flex-1" title={pod.nodeName}>{pod.nodeName}</span>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleCopy(pod.nodeName!, `节点-${pod.name}`, e)}
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedField === `节点-${pod.name}` ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
)}
{pod.podIP && (
<div className="flex items-center gap-1 group">
<Network className="h-3 w-3" />
<span className="flex-1">{pod.podIP}</span>
<Button
variant="ghost"
size="sm"
onClick={(e) => handleCopy(pod.podIP!, `IP-${pod.name}`, e)}
className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedField === `IP-${pod.name}` ? (
<Check className="h-3 w-3 text-green-600" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
)}
<div className="pt-2 border-t text-xs">
: {pod.containers.length}
</div>
{pod.creationTimestamp && (
<div className="text-xs">
: {dayjs(pod.creationTimestamp).format('MM-DD HH:mm')}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</DialogBody>
</DialogContent>
</Dialog>
{selectedPod && (
<PodDetailDialog
open={podDetailOpen}
onOpenChange={setPodDetailOpen}
pod={selectedPod}
deploymentId={deploymentId}
/>
)}
</>
);
};

View File

@ -3,15 +3,14 @@ import { PageContainer } from '@/components/ui/page-container';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { RefreshCw, Loader2, Cloud, History as HistoryIcon } from 'lucide-react'; import { RefreshCw, Loader2, Cloud, History as HistoryIcon, Database } from 'lucide-react';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { NamespaceCard } from './components/NamespaceCard'; import { DeploymentCard } from './components/DeploymentCard';
import { DeploymentDialog } from './components/DeploymentDialog';
import { SyncHistoryTab } from './components/SyncHistoryTab'; import { SyncHistoryTab } from './components/SyncHistoryTab';
import { getExternalSystemList } from '@/pages/Resource/External/List/service'; import { getExternalSystemList } from '@/pages/Resource/External/List/service';
import { syncK8sNamespace, syncK8sDeployment, getK8sNamespaceList } from './service'; import { syncK8sNamespace, syncK8sDeployment, getK8sNamespaceList, getK8sDeploymentByNamespace } from './service';
import type { ExternalSystemResponse } from '@/pages/Resource/External/List/types'; import type { ExternalSystemResponse } from '@/pages/Resource/External/List/types';
import type { K8sNamespaceResponse } from './types'; import type { K8sNamespaceResponse, K8sDeploymentResponse } from './types';
import { SystemType } from '@/pages/Resource/External/List/types'; import { SystemType } from '@/pages/Resource/External/List/types';
const K8sManagement: React.FC = () => { const K8sManagement: React.FC = () => {
@ -19,12 +18,13 @@ const K8sManagement: React.FC = () => {
const [k8sClusters, setK8sClusters] = useState<ExternalSystemResponse[]>([]); const [k8sClusters, setK8sClusters] = useState<ExternalSystemResponse[]>([]);
const [selectedClusterId, setSelectedClusterId] = useState<number | null>(null); const [selectedClusterId, setSelectedClusterId] = useState<number | null>(null);
const [namespaces, setNamespaces] = useState<K8sNamespaceResponse[]>([]); const [namespaces, setNamespaces] = useState<K8sNamespaceResponse[]>([]);
const [selectedNamespaceId, setSelectedNamespaceId] = useState<number | null>(null);
const [deployments, setDeployments] = useState<K8sDeploymentResponse[]>([]);
const [syncingNamespace, setSyncingNamespace] = useState(false); const [syncingNamespace, setSyncingNamespace] = useState(false);
const [syncingDeployment, setSyncingDeployment] = useState(false); const [syncingDeployment, setSyncingDeployment] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [showHistory, setShowHistory] = useState(false); const [showHistory, setShowHistory] = useState(false);
const [selectedNamespace, setSelectedNamespace] = useState<K8sNamespaceResponse | null>(null);
const [deploymentDialogOpen, setDeploymentDialogOpen] = useState(false);
// 加载K8S集群列表 // 加载K8S集群列表
const loadK8sClusters = async () => { const loadK8sClusters = async () => {
@ -58,6 +58,10 @@ const K8sManagement: React.FC = () => {
try { try {
const data = await getK8sNamespaceList(selectedClusterId); const data = await getK8sNamespaceList(selectedClusterId);
setNamespaces(data || []); setNamespaces(data || []);
// 默认选择第一个命名空间
if (data && data.length > 0 && !selectedNamespaceId) {
setSelectedNamespaceId(data[0].id);
}
} catch (error) { } catch (error) {
console.error('加载命名空间失败:', error); console.error('加载命名空间失败:', error);
toast({ toast({
@ -68,6 +72,23 @@ const K8sManagement: React.FC = () => {
} }
}; };
// 加载Deployment列表
const loadDeployments = async () => {
if (!selectedClusterId || !selectedNamespaceId) return;
try {
const data = await getK8sDeploymentByNamespace(selectedClusterId, selectedNamespaceId);
setDeployments(data || []);
} catch (error) {
console.error('加载Deployment失败:', error);
toast({
title: '加载失败',
description: '加载Deployment列表失败',
variant: 'destructive',
});
}
};
useEffect(() => { useEffect(() => {
loadK8sClusters(); loadK8sClusters();
}, []); }, []);
@ -78,6 +99,12 @@ const K8sManagement: React.FC = () => {
} }
}, [selectedClusterId]); }, [selectedClusterId]);
useEffect(() => {
if (selectedNamespaceId) {
loadDeployments();
}
}, [selectedNamespaceId, selectedClusterId]);
// 同步命名空间 // 同步命名空间
const handleSyncNamespace = async () => { const handleSyncNamespace = async () => {
if (!selectedClusterId) { if (!selectedClusterId) {
@ -136,6 +163,28 @@ const K8sManagement: React.FC = () => {
} }
}; };
// 刷新页面数据
const handleRefresh = async () => {
if (!selectedClusterId || !selectedNamespaceId) return;
try {
setRefreshing(true);
await loadDeployments();
toast({
title: '刷新成功',
description: 'Deployment列表已更新',
});
} catch (error) {
toast({
title: '刷新失败',
description: '刷新Deployment列表失败',
variant: 'destructive',
});
} finally {
setRefreshing(false);
}
};
if (loading) { if (loading) {
return ( return (
<PageContainer> <PageContainer>
@ -163,11 +212,6 @@ const K8sManagement: React.FC = () => {
); );
} }
const handleNamespaceClick = (namespace: K8sNamespaceResponse) => {
setSelectedNamespace(namespace);
setDeploymentDialogOpen(true);
};
return ( return (
<PageContainer> <PageContainer>
<div className="mb-6"> <div className="mb-6">
@ -185,8 +229,8 @@ const K8sManagement: React.FC = () => {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4 flex-wrap">
<div className="flex-1 max-w-xs"> <div className="w-64">
<Select <Select
value={selectedClusterId ? String(selectedClusterId) : undefined} value={selectedClusterId ? String(selectedClusterId) : undefined}
onValueChange={(value) => setSelectedClusterId(Number(value))} onValueChange={(value) => setSelectedClusterId(Number(value))}
@ -203,6 +247,53 @@ const K8sManagement: React.FC = () => {
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="w-64">
<Select
value={selectedNamespaceId ? String(selectedNamespaceId) : undefined}
onValueChange={(value) => setSelectedNamespaceId(Number(value))}
disabled={!selectedClusterId || namespaces.length === 0}
>
<SelectTrigger>
{selectedNamespaceId ? (
<div className="flex items-center justify-between w-full gap-2">
<div className="flex items-center gap-2 min-w-0">
<Database className="h-4 w-4 flex-shrink-0" />
<span className="truncate">
{namespaces.find(n => n.id === selectedNamespaceId)?.namespaceName}
</span>
</div>
{namespaces.find(n => n.id === selectedNamespaceId)?.status && (
<span className={`text-xs px-2 py-0.5 rounded flex-shrink-0 ${namespaces.find(n => n.id === selectedNamespaceId)?.status === 'Active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'}`}>
{namespaces.find(n => n.id === selectedNamespaceId)?.status}
</span>
)}
</div>
) : (
<span className="text-muted-foreground"></span>
)}
</SelectTrigger>
<SelectContent>
{namespaces.map((namespace) => (
<SelectItem
key={namespace.id}
value={String(namespace.id)}
className="data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary"
>
<div className="flex items-center gap-2 w-full">
<Database className="h-4 w-4 flex-shrink-0" />
<span className="inline-block w-40 truncate">{namespace.namespaceName}</span>
{namespace.status && (
<span className={`text-xs px-2 py-0.5 rounded flex-shrink-0 ml-auto ${namespace.status === 'Active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'}`}>
{namespace.status}
</span>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button <Button
onClick={handleSyncNamespace} onClick={handleSyncNamespace}
@ -230,6 +321,19 @@ const K8sManagement: React.FC = () => {
Deployment Deployment
</Button> </Button>
<Button
onClick={handleRefresh}
disabled={!selectedClusterId || !selectedNamespaceId || refreshing}
variant="outline"
>
{refreshing ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
</Button>
<Button <Button
onClick={() => setShowHistory(!showHistory)} onClick={() => setShowHistory(!showHistory)}
variant={showHistory ? "default" : "outline"} variant={showHistory ? "default" : "outline"}
@ -253,20 +357,23 @@ const K8sManagement: React.FC = () => {
) : ( ) : (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle></CardTitle> <CardTitle>Deployment</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{namespaces.length === 0 ? ( {!selectedNamespaceId ? (
<div className="flex items-center justify-center h-64 text-muted-foreground"> <div className="flex items-center justify-center h-64 text-muted-foreground">
{selectedClusterId ? '暂无命名空间数据' : '请先选择K8S集群'}
</div>
) : deployments.length === 0 ? (
<div className="flex items-center justify-center h-64 text-muted-foreground">
Deployment
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{namespaces.map((namespace) => ( {deployments.map((deployment) => (
<NamespaceCard <DeploymentCard
key={namespace.id} key={deployment.id}
namespace={namespace} deployment={deployment}
onClick={() => handleNamespaceClick(namespace)}
/> />
))} ))}
</div> </div>
@ -274,16 +381,6 @@ const K8sManagement: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
)} )}
{selectedNamespace && (
<DeploymentDialog
open={deploymentDialogOpen}
onOpenChange={setDeploymentDialogOpen}
namespaceName={selectedNamespace.namespaceName}
namespaceId={selectedNamespace.id}
externalSystemId={selectedClusterId!}
/>
)}
</PageContainer> </PageContainer>
); );
}; };

View File

@ -7,6 +7,7 @@ import type {
K8sDeploymentQuery, K8sDeploymentQuery,
K8sSyncHistoryResponse, K8sSyncHistoryResponse,
K8sSyncHistoryQuery, K8sSyncHistoryQuery,
K8sPodResponse,
} from './types'; } from './types';
// ==================== K8S命名空间接口 ==================== // ==================== K8S命名空间接口 ====================
@ -142,3 +143,24 @@ export const getK8sSyncHistoryBySystem = async (
): Promise<K8sSyncHistoryResponse[]> => { ): Promise<K8sSyncHistoryResponse[]> => {
return request.get(`/api/v1/k8s-sync-history/by-system/${externalSystemId}`); return request.get(`/api/v1/k8s-sync-history/by-system/${externalSystemId}`);
}; };
// ==================== K8S Pod接口 ====================
/**
* Deployment下的所有Pod
*/
export const getK8sPodsByDeployment = async (
deploymentId: number
): Promise<K8sPodResponse[]> => {
return request.get(`/api/v1/k8s-deployment/${deploymentId}/pods`);
};
/**
* Pod详情
*/
export const getK8sPodDetail = async (
deploymentId: number,
podName: string
): Promise<K8sPodResponse> => {
return request.get(`/api/v1/k8s-deployment/${deploymentId}/pods/${podName}`);
};

View File

@ -155,3 +155,125 @@ export interface K8sSyncHistoryQuery extends BaseQuery {
/** 同步状态 */ /** 同步状态 */
status?: K8sSyncStatus; status?: K8sSyncStatus;
} }
// ==================== K8S Pod ====================
/**
* Pod状态阶段
*/
export enum PodPhase {
/** 运行中 */
RUNNING = 'Running',
/** 等待中 */
PENDING = 'Pending',
/** 成功 */
SUCCEEDED = 'Succeeded',
/** 失败 */
FAILED = 'Failed',
/** 未知 */
UNKNOWN = 'Unknown',
}
/**
*
*/
export enum ContainerState {
/** 运行中 */
RUNNING = 'running',
/** 等待中 */
WAITING = 'waiting',
/** 已终止 */
TERMINATED = 'terminated',
}
// Pod状态标签映射
export const PodPhaseLabels: Record<PodPhase, { label: string; variant: 'default' | 'destructive' | 'secondary'; color: string }> = {
[PodPhase.RUNNING]: { label: '运行中', variant: 'default', color: 'bg-green-600 hover:bg-green-700' },
[PodPhase.PENDING]: { label: '等待中', variant: 'secondary', color: 'bg-yellow-600 hover:bg-yellow-700' },
[PodPhase.SUCCEEDED]: { label: '成功', variant: 'default', color: 'bg-blue-600 hover:bg-blue-700' },
[PodPhase.FAILED]: { label: '失败', variant: 'destructive', color: 'bg-red-600 hover:bg-red-700' },
[PodPhase.UNKNOWN]: { label: '未知', variant: 'secondary', color: 'bg-gray-600 hover:bg-gray-700' },
};
// 容器状态标签映射
export const ContainerStateLabels: Record<ContainerState, { label: string; color: string }> = {
[ContainerState.RUNNING]: { label: '运行中', color: 'text-green-600' },
[ContainerState.WAITING]: { label: '等待中', color: 'text-yellow-600' },
[ContainerState.TERMINATED]: { label: '已终止', color: 'text-red-600' },
};
/**
*
*/
export interface ContainerInfo {
/** 容器名称 */
name: string;
/** 镜像名称 */
image: string;
/** 镜像ID */
imageID?: string;
/** 容器状态 */
state: ContainerState;
/** 是否就绪 */
ready: boolean;
/** 重启次数 */
restartCount: number;
/** 容器ID */
containerID?: string;
/** 启动时间 */
startedAt?: string;
/** CPU请求量 */
cpuRequest?: string;
/** CPU限制量 */
cpuLimit?: string;
/** 内存请求量 */
memoryRequest?: string;
/** 内存限制量 */
memoryLimit?: string;
/** 状态原因 */
stateReason?: string;
/** 状态消息 */
stateMessage?: string;
}
/**
* K8S Pod响应
*/
export interface K8sPodResponse {
/** Pod名称 */
name: string;
/** 命名空间 */
namespace: string;
/** Pod状态阶段 */
phase: PodPhase;
/** 状态原因 */
reason?: string;
/** 状态消息 */
message?: string;
/** Pod IP地址 */
podIP?: string;
/** 宿主机IP */
hostIP?: string;
/** 节点名称 */
nodeName?: string;
/** 容器列表 */
containers: ContainerInfo[];
/** 重启次数 */
restartCount: number;
/** 创建时间 */
creationTimestamp: string;
/** 启动时间 */
startTime?: string;
/** 标签 */
labels?: Record<string, string>;
/** 注解 */
annotations?: Record<string, string>;
/** 所属控制器类型 */
ownerKind?: string;
/** 所属控制器名称 */
ownerName?: string;
/** 是否就绪 */
ready: boolean;
/** YAML配置 */
yamlConfig?: string;
}