1.30 k8s管理
This commit is contained in:
parent
be6b6d75ac
commit
faf983afe0
@ -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 = () => {
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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))}
|
||||||
@ -204,6 +248,53 @@ const K8sManagement: React.FC = () => {
|
|||||||
</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}
|
||||||
disabled={!selectedClusterId || syncingNamespace}
|
disabled={!selectedClusterId || syncingNamespace}
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}`);
|
||||||
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user