1.30 k8s管理
This commit is contained in:
parent
be6b6d75ac
commit
faf983afe0
@ -10,7 +10,7 @@ const Forbidden: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleGoHome = () => {
|
||||
navigate('/dashboard', { replace: true });
|
||||
navigate('/', { replace: true });
|
||||
};
|
||||
|
||||
const handleGoBack = () => {
|
||||
|
||||
@ -10,7 +10,7 @@ const NotFound: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleGoHome = () => {
|
||||
navigate('/dashboard', { replace: true });
|
||||
navigate('/', { replace: true });
|
||||
};
|
||||
|
||||
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 { Button } from '@/components/ui/button';
|
||||
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 { NamespaceCard } from './components/NamespaceCard';
|
||||
import { DeploymentDialog } from './components/DeploymentDialog';
|
||||
import { DeploymentCard } from './components/DeploymentCard';
|
||||
import { SyncHistoryTab } from './components/SyncHistoryTab';
|
||||
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 { K8sNamespaceResponse } from './types';
|
||||
import type { K8sNamespaceResponse, K8sDeploymentResponse } from './types';
|
||||
import { SystemType } from '@/pages/Resource/External/List/types';
|
||||
|
||||
const K8sManagement: React.FC = () => {
|
||||
@ -19,12 +18,13 @@ const K8sManagement: React.FC = () => {
|
||||
const [k8sClusters, setK8sClusters] = useState<ExternalSystemResponse[]>([]);
|
||||
const [selectedClusterId, setSelectedClusterId] = useState<number | null>(null);
|
||||
const [namespaces, setNamespaces] = useState<K8sNamespaceResponse[]>([]);
|
||||
const [selectedNamespaceId, setSelectedNamespaceId] = useState<number | null>(null);
|
||||
const [deployments, setDeployments] = useState<K8sDeploymentResponse[]>([]);
|
||||
const [syncingNamespace, setSyncingNamespace] = useState(false);
|
||||
const [syncingDeployment, setSyncingDeployment] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<K8sNamespaceResponse | null>(null);
|
||||
const [deploymentDialogOpen, setDeploymentDialogOpen] = useState(false);
|
||||
|
||||
// 加载K8S集群列表
|
||||
const loadK8sClusters = async () => {
|
||||
@ -58,6 +58,10 @@ const K8sManagement: React.FC = () => {
|
||||
try {
|
||||
const data = await getK8sNamespaceList(selectedClusterId);
|
||||
setNamespaces(data || []);
|
||||
// 默认选择第一个命名空间
|
||||
if (data && data.length > 0 && !selectedNamespaceId) {
|
||||
setSelectedNamespaceId(data[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载命名空间失败:', error);
|
||||
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(() => {
|
||||
loadK8sClusters();
|
||||
}, []);
|
||||
@ -78,6 +99,12 @@ const K8sManagement: React.FC = () => {
|
||||
}
|
||||
}, [selectedClusterId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedNamespaceId) {
|
||||
loadDeployments();
|
||||
}
|
||||
}, [selectedNamespaceId, selectedClusterId]);
|
||||
|
||||
// 同步命名空间
|
||||
const handleSyncNamespace = async () => {
|
||||
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) {
|
||||
return (
|
||||
<PageContainer>
|
||||
@ -163,11 +212,6 @@ const K8sManagement: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const handleNamespaceClick = (namespace: K8sNamespaceResponse) => {
|
||||
setSelectedNamespace(namespace);
|
||||
setDeploymentDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<div className="mb-6">
|
||||
@ -185,8 +229,8 @@ const K8sManagement: React.FC = () => {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1 max-w-xs">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div className="w-64">
|
||||
<Select
|
||||
value={selectedClusterId ? String(selectedClusterId) : undefined}
|
||||
onValueChange={(value) => setSelectedClusterId(Number(value))}
|
||||
@ -204,6 +248,53 @@ const K8sManagement: React.FC = () => {
|
||||
</Select>
|
||||
</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
|
||||
onClick={handleSyncNamespace}
|
||||
disabled={!selectedClusterId || syncingNamespace}
|
||||
@ -230,6 +321,19 @@ const K8sManagement: React.FC = () => {
|
||||
同步Deployment
|
||||
</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
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
variant={showHistory ? "default" : "outline"}
|
||||
@ -253,20 +357,23 @@ const K8sManagement: React.FC = () => {
|
||||
) : (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>命名空间</CardTitle>
|
||||
<CardTitle>Deployment</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{namespaces.length === 0 ? (
|
||||
{!selectedNamespaceId ? (
|
||||
<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 className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{namespaces.map((namespace) => (
|
||||
<NamespaceCard
|
||||
key={namespace.id}
|
||||
namespace={namespace}
|
||||
onClick={() => handleNamespaceClick(namespace)}
|
||||
{deployments.map((deployment) => (
|
||||
<DeploymentCard
|
||||
key={deployment.id}
|
||||
deployment={deployment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -274,16 +381,6 @@ const K8sManagement: React.FC = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{selectedNamespace && (
|
||||
<DeploymentDialog
|
||||
open={deploymentDialogOpen}
|
||||
onOpenChange={setDeploymentDialogOpen}
|
||||
namespaceName={selectedNamespace.namespaceName}
|
||||
namespaceId={selectedNamespace.id}
|
||||
externalSystemId={selectedClusterId!}
|
||||
/>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import type {
|
||||
K8sDeploymentQuery,
|
||||
K8sSyncHistoryResponse,
|
||||
K8sSyncHistoryQuery,
|
||||
K8sPodResponse,
|
||||
} from './types';
|
||||
|
||||
// ==================== K8S命名空间接口 ====================
|
||||
@ -142,3 +143,24 @@ export const getK8sSyncHistoryBySystem = async (
|
||||
): Promise<K8sSyncHistoryResponse[]> => {
|
||||
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;
|
||||
}
|
||||
|
||||
// ==================== 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