2242 lines
89 KiB
TypeScript
2242 lines
89 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
import { LogsModal } from '@/components/ui/logs-modal';
|
|
import { YamlEditor } from '@/components/ui/yaml-editor';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { useDaemonSetDetails, useKubectl } from '@/hooks/useKubectl';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import {
|
|
ArrowLeft,
|
|
Shield,
|
|
RefreshCw,
|
|
AlertCircle,
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock,
|
|
Play,
|
|
Pause,
|
|
Loader2,
|
|
MoreVertical,
|
|
Edit,
|
|
Trash2,
|
|
FileText,
|
|
Activity,
|
|
Container,
|
|
Zap,
|
|
Calendar,
|
|
Tag,
|
|
Server,
|
|
AlertTriangle,
|
|
Eye,
|
|
Download,
|
|
Copy,
|
|
ExternalLink,
|
|
Info,
|
|
Database,
|
|
Network,
|
|
HardDrive,
|
|
Cpu,
|
|
MemoryStick,
|
|
List,
|
|
GitBranch,
|
|
Settings,
|
|
Terminal,
|
|
ChevronDown,
|
|
RotateCcw,
|
|
Trash,
|
|
Key,
|
|
} from 'lucide-react';
|
|
|
|
export function DaemonSetDetailsPage() {
|
|
const { namespaceName, daemonSetName } = useParams<{
|
|
namespaceName: string;
|
|
daemonSetName: string;
|
|
}>();
|
|
const navigate = useNavigate();
|
|
const { showToast } = useToast();
|
|
const { executeCommand } = useKubectl();
|
|
|
|
const {
|
|
daemonSetInfo,
|
|
pods,
|
|
events,
|
|
configMaps,
|
|
secrets,
|
|
services,
|
|
loading,
|
|
error,
|
|
isRefreshing,
|
|
refetch,
|
|
backgroundRefetch,
|
|
} = useDaemonSetDetails(daemonSetName || '', namespaceName || '');
|
|
|
|
const [logsModalOpen, setLogsModalOpen] = useState(false);
|
|
const [selectedPod, setSelectedPod] = useState<any>(null);
|
|
const [selectedContainer, setSelectedContainer] = useState<string>('');
|
|
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
const [restarting, setRestarting] = useState(false);
|
|
const [yamlDrawerOpen, setYamlDrawerOpen] = useState(false);
|
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('configuracoes');
|
|
|
|
// Estados para modais de Environment
|
|
const [configMapKeysModalOpen, setConfigMapKeysModalOpen] = useState(false);
|
|
const [selectedConfigMapData, setSelectedConfigMapData] = useState<any>(null);
|
|
const [secretKeysModalOpen, setSecretKeysModalOpen] = useState(false);
|
|
const [selectedSecretData, setSelectedSecretData] = useState<any>(null);
|
|
|
|
// Auto-refresh effect
|
|
useEffect(() => {
|
|
if (!autoRefresh) return;
|
|
|
|
const interval = setInterval(() => {
|
|
backgroundRefetch();
|
|
}, 5000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [autoRefresh, backgroundRefetch]);
|
|
|
|
const handleViewLogs = (pod: any, container: string) => {
|
|
setSelectedPod(pod);
|
|
setSelectedContainer(container);
|
|
setLogsModalOpen(true);
|
|
};
|
|
|
|
const handleRestart = async () => {
|
|
if (!daemonSetInfo) return;
|
|
|
|
setRestarting(true);
|
|
try {
|
|
const command = `kubectl rollout restart daemonset ${daemonSetInfo.metadata.name} -n ${daemonSetInfo.metadata.namespace}`;
|
|
const result = await executeCommand(command);
|
|
|
|
if (result.success) {
|
|
showToast({
|
|
title: 'DaemonSet Reiniciado',
|
|
description: `DaemonSet "${daemonSetInfo.metadata.name}" foi reiniciado com sucesso`,
|
|
variant: 'success',
|
|
});
|
|
|
|
// Refresh after restart
|
|
await refetch();
|
|
} else {
|
|
showToast({
|
|
title: 'Erro ao Reiniciar DaemonSet',
|
|
description: result.error || 'Falha ao reiniciar o DaemonSet',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
const errorMsg = err instanceof Error ? err.message : 'Erro inesperado';
|
|
showToast({
|
|
title: 'Erro',
|
|
description: errorMsg,
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setRestarting(false);
|
|
}
|
|
};
|
|
|
|
const handleYamlView = () => {
|
|
if (!daemonSetName || !namespaceName) return;
|
|
setYamlDrawerOpen(true);
|
|
};
|
|
|
|
const handleTabChange = (value: string) => {
|
|
setActiveTab(value);
|
|
};
|
|
|
|
// Function to handle ConfigMap keys view
|
|
const handleConfigMapKeysView = async (configMapName: string) => {
|
|
try {
|
|
const command = `kubectl get configmap ${configMapName} -n ${namespaceName} -o json`;
|
|
const result = await executeCommand(command);
|
|
if (result && result.success && result.data) {
|
|
const configMapData = JSON.parse(result.data);
|
|
setSelectedConfigMapData(configMapData);
|
|
setConfigMapKeysModalOpen(true);
|
|
} else {
|
|
showToast({
|
|
title: 'Erro ao Carregar ConfigMap',
|
|
description: result.error || 'Falha ao carregar dados do ConfigMap',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
showToast({
|
|
title: 'Erro',
|
|
description: 'Erro inesperado ao carregar ConfigMap',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
// Function to handle Secret keys view
|
|
const handleSecretKeysView = async (secretName: string) => {
|
|
try {
|
|
const command = `kubectl get secret ${secretName} -n ${namespaceName} -o json`;
|
|
const result = await executeCommand(command);
|
|
if (result && result.success && result.data) {
|
|
const secretData = JSON.parse(result.data);
|
|
setSelectedSecretData(secretData);
|
|
setSecretKeysModalOpen(true);
|
|
} else {
|
|
showToast({
|
|
title: 'Erro ao Carregar Secret',
|
|
description: result.error || 'Falha ao carregar dados do Secret',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
showToast({
|
|
title: 'Erro',
|
|
description: 'Erro inesperado ao carregar Secret',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
// Function to get ConfigMap info
|
|
const getConfigMapInfo = (configMap: any) => {
|
|
const dataKeys = Object.keys(configMap.data || {});
|
|
|
|
return {
|
|
keysCount: dataKeys.length,
|
|
keys: dataKeys,
|
|
size: JSON.stringify(configMap.data || {}).length,
|
|
};
|
|
};
|
|
|
|
// Function to get Secret info
|
|
const getSecretInfo = (secret: any) => {
|
|
const secretType = secret.type || 'Opaque';
|
|
const dataKeys = Object.keys(secret.data || {});
|
|
|
|
let variant = 'secondary';
|
|
let icon = Shield;
|
|
|
|
if (secretType === 'kubernetes.io/dockerconfigjson') {
|
|
variant = 'outline';
|
|
icon = Container;
|
|
} else if (secretType === 'kubernetes.io/tls') {
|
|
variant = 'destructive';
|
|
icon = Shield;
|
|
}
|
|
|
|
return {
|
|
type: secretType,
|
|
variant,
|
|
icon,
|
|
keysCount: dataKeys.length,
|
|
keys: dataKeys,
|
|
};
|
|
};
|
|
|
|
// Function to get Secret usage info
|
|
const getSecretUsageInfo = (secret: any) => {
|
|
const secretType = secret.type || 'Opaque';
|
|
const secretInfo = getSecretInfo(secret);
|
|
|
|
// For specific types, use standardized descriptions
|
|
switch (secretType) {
|
|
case 'kubernetes.io/dockerconfigjson':
|
|
return {
|
|
type: 'special',
|
|
badge: 'Registry Config',
|
|
tooltip: 'Credenciais de registry Docker',
|
|
color: 'bg-purple-50 text-purple-700',
|
|
};
|
|
case 'kubernetes.io/tls':
|
|
return {
|
|
type: 'special',
|
|
badge: 'Certificado SSL',
|
|
tooltip: 'Certificado TLS (tls.crt, tls.key)',
|
|
color: 'bg-red-50 text-red-700',
|
|
};
|
|
case 'kubernetes.io/service-account-token':
|
|
return {
|
|
type: 'special',
|
|
badge: 'Service Account Token',
|
|
tooltip: 'Token de autenticação da service account',
|
|
color: 'bg-blue-50 text-blue-700',
|
|
};
|
|
default:
|
|
const { usageEntry } = secret;
|
|
if (usageEntry?.isFullSecret) {
|
|
return {
|
|
type: 'all-keys',
|
|
badge: 'Todas as Chaves',
|
|
tooltip: 'Todas as chaves do secret são usadas',
|
|
color: 'bg-green-50 text-green-700',
|
|
};
|
|
}
|
|
if (usageEntry?.keys.length > 0) {
|
|
return {
|
|
type: 'specific-keys',
|
|
badge: `${usageEntry.keys.length} Chave${usageEntry.keys.length > 1 ? 's' : ''}`,
|
|
tooltip: `Chaves específicas: ${usageEntry.keys.join(', ')}`,
|
|
color: 'bg-blue-50 text-blue-700',
|
|
};
|
|
}
|
|
return {
|
|
type: 'unspecified',
|
|
badge: 'Não especificado',
|
|
tooltip: 'Uso não especificado',
|
|
color: 'bg-gray-50 text-gray-700',
|
|
};
|
|
}
|
|
};
|
|
|
|
// Function to get ConfigMap usage info
|
|
const getConfigMapUsageInfo = (configMap: any) => {
|
|
const { usageEntry } = configMap;
|
|
const configMapInfo = getConfigMapInfo(configMap);
|
|
|
|
if (usageEntry?.isFullConfigMap) {
|
|
return {
|
|
type: 'all-keys',
|
|
keys: configMapInfo.keys,
|
|
color: 'bg-green-50 text-green-700',
|
|
clickable: true,
|
|
};
|
|
}
|
|
if (usageEntry?.keys.length > 0) {
|
|
return {
|
|
type: 'specific-keys',
|
|
keys: usageEntry.keys,
|
|
color: 'bg-blue-50 text-blue-700',
|
|
clickable: true,
|
|
};
|
|
}
|
|
return {
|
|
type: 'unspecified',
|
|
color: '',
|
|
clickable: false,
|
|
};
|
|
};
|
|
|
|
const handleDeleteDaemonSetClick = () => {
|
|
setDeleteModalOpen(true);
|
|
};
|
|
|
|
const handleDeleteDaemonSet = async () => {
|
|
if (!daemonSetInfo) return;
|
|
|
|
setDeleting(true);
|
|
try {
|
|
const command = `kubectl delete daemonset ${daemonSetInfo.metadata.name} -n ${daemonSetInfo.metadata.namespace}`;
|
|
const result = await executeCommand(command);
|
|
|
|
if (result.success) {
|
|
showToast({
|
|
title: 'DaemonSet Excluído',
|
|
description: `DaemonSet "${daemonSetInfo.metadata.name}" foi excluído com sucesso`,
|
|
variant: 'success',
|
|
});
|
|
|
|
// Navigate back to daemonsets list
|
|
navigate('/workloads/daemonsets');
|
|
} else {
|
|
showToast({
|
|
title: 'Erro ao Excluir DaemonSet',
|
|
description: result.error || 'Falha ao excluir o DaemonSet',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
} catch (err) {
|
|
const errorMsg = err instanceof Error ? err.message : 'Erro inesperado';
|
|
showToast({
|
|
title: 'Erro',
|
|
description: errorMsg,
|
|
variant: 'destructive',
|
|
});
|
|
} finally {
|
|
setDeleting(false);
|
|
setDeleteModalOpen(false);
|
|
}
|
|
};
|
|
|
|
const getAge = (creationTimestamp: string) => {
|
|
const now = new Date();
|
|
const created = new Date(creationTimestamp);
|
|
const diffMs = now.getTime() - created.getTime();
|
|
const diffSeconds = Math.floor(diffMs / 1000);
|
|
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffDays > 0) return `${diffDays}d`;
|
|
if (diffHours > 0) return `${diffHours}h`;
|
|
if (diffMinutes > 0) return `${diffMinutes}m`;
|
|
return `${diffSeconds}s`;
|
|
};
|
|
|
|
const getDaemonSetStatus = () => {
|
|
if (!daemonSetInfo)
|
|
return {
|
|
label: 'Unknown',
|
|
color: 'text-gray-600',
|
|
bgColor: 'bg-gray-100',
|
|
icon: Clock,
|
|
};
|
|
|
|
const status = daemonSetInfo.status || {};
|
|
const desired = status.desiredNumberScheduled || 0;
|
|
const current = status.currentNumberScheduled || 0;
|
|
const ready = status.numberReady || 0;
|
|
const misscheduled = status.numberMisscheduled || 0;
|
|
|
|
if (ready === desired && misscheduled === 0) {
|
|
return {
|
|
label: 'Ready',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100',
|
|
icon: CheckCircle,
|
|
};
|
|
}
|
|
|
|
if (current === 0) {
|
|
return {
|
|
label: 'Stopped',
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-100',
|
|
icon: XCircle,
|
|
};
|
|
}
|
|
|
|
return {
|
|
label: 'Not Ready',
|
|
color: 'text-yellow-600',
|
|
bgColor: 'bg-yellow-100',
|
|
icon: Clock,
|
|
};
|
|
};
|
|
|
|
const getPodStatus = (pod: any) => {
|
|
const phase = pod.status?.phase || 'Unknown';
|
|
const conditions = pod.status?.conditions || [];
|
|
|
|
const readyCondition = conditions.find((c: any) => c.type === 'Ready');
|
|
const isReady = readyCondition?.status === 'True';
|
|
|
|
if (phase === 'Running' && isReady) {
|
|
return {
|
|
label: 'Running',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100',
|
|
icon: CheckCircle,
|
|
};
|
|
}
|
|
|
|
if (phase === 'Pending') {
|
|
return {
|
|
label: 'Pending',
|
|
color: 'text-yellow-600',
|
|
bgColor: 'bg-yellow-100',
|
|
icon: Clock,
|
|
};
|
|
}
|
|
|
|
if (phase === 'Failed') {
|
|
return {
|
|
label: 'Failed',
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-100',
|
|
icon: XCircle,
|
|
};
|
|
}
|
|
|
|
if (phase === 'Succeeded') {
|
|
return {
|
|
label: 'Succeeded',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100',
|
|
icon: CheckCircle,
|
|
};
|
|
}
|
|
|
|
return {
|
|
label: phase,
|
|
color: 'text-gray-600',
|
|
bgColor: 'bg-gray-100',
|
|
icon: Clock,
|
|
};
|
|
};
|
|
|
|
const getEventType = (event: any) => {
|
|
if (event.type === 'Warning') {
|
|
return {
|
|
color: 'text-orange-600',
|
|
bgColor: 'bg-orange-100',
|
|
icon: AlertTriangle,
|
|
};
|
|
}
|
|
if (event.type === 'Normal') {
|
|
return { color: 'text-blue-600', bgColor: 'bg-blue-100', icon: Info };
|
|
}
|
|
return { color: 'text-gray-600', bgColor: 'bg-gray-100', icon: Info };
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4" />
|
|
<p className="text-muted-foreground">
|
|
Carregando detalhes do DaemonSet...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<AlertCircle className="h-8 w-8 text-red-500 mx-auto mb-4" />
|
|
<p className="text-red-600 mb-4">{error}</p>
|
|
<Button onClick={() => refetch()}>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Tentar Novamente
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!daemonSetInfo) {
|
|
return (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="text-center">
|
|
<AlertCircle className="h-8 w-8 text-gray-500 mx-auto mb-4" />
|
|
<p className="text-gray-600">DaemonSet não encontrado</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const status = getDaemonSetStatus();
|
|
const StatusIcon = status.icon;
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
</Button>
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-foreground flex items-center">
|
|
<Shield className="h-8 w-8 mr-3 text-blue-600" />
|
|
{daemonSetInfo.metadata.name}
|
|
</h1>
|
|
<div className="text-muted-foreground">
|
|
DaemonSet no namespace{' '}
|
|
<Badge variant="outline">
|
|
{daemonSetInfo.metadata.namespace}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{/* Split button - Live/Manual com dropdown de tempo */}
|
|
<div className="flex">
|
|
{/* Botão principal - Play/Pause */}
|
|
<Button
|
|
onClick={() => setAutoRefresh(!autoRefresh)}
|
|
variant="ghost"
|
|
className="rounded-r-none border-r-0 text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
|
title={
|
|
autoRefresh
|
|
? 'Pausar atualização automática'
|
|
: 'Ativar atualização automática'
|
|
}
|
|
>
|
|
{autoRefresh ? (
|
|
<>
|
|
<Pause className="h-4 w-4 mr-2" />
|
|
Live (5s)
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Atualizar
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Dropdown para configurar tempo */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="rounded-l-none px-2 text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
|
title="Configurar tempo de atualização"
|
|
>
|
|
<ChevronDown className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Atualização automática</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => setAutoRefresh(true)}>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Ativar (5s)
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setAutoRefresh(false)}>
|
|
<Pause className="h-4 w-4 mr-2" />
|
|
Pausar
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => refetch()}>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Atualizar agora
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overview Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Status</CardTitle>
|
|
<Activity className="h-4 w-4 text-blue-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Badge className={`${status.bgColor} ${status.color}`}>
|
|
{status.label}
|
|
</Badge>
|
|
<p className="text-xs text-muted-foreground mt-1">Estado atual</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Desired</CardTitle>
|
|
<Container className="h-4 w-4 text-green-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{daemonSetInfo.status?.desiredNumberScheduled || 0}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Pods desejados</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Ready</CardTitle>
|
|
<CheckCircle className="h-4 w-4 text-purple-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{daemonSetInfo.status?.numberReady || 0}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Pods prontos</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Idade</CardTitle>
|
|
<Calendar className="h-4 w-4 text-orange-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{getAge(daemonSetInfo.metadata.creationTimestamp)}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{new Date(
|
|
daemonSetInfo.metadata.creationTimestamp,
|
|
).toLocaleDateString('pt-BR')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Misscheduled</CardTitle>
|
|
<AlertTriangle className="h-4 w-4 text-red-600" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">
|
|
{daemonSetInfo.status?.numberMisscheduled || 0}
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">Pods mal agendados</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* DaemonSet Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center">
|
|
<Settings className="h-5 w-5 mr-2" />
|
|
Configuração do DaemonSet
|
|
</CardTitle>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
onClick={handleRestart}
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={restarting || !pods || pods.length === 0}
|
|
>
|
|
<RotateCcw
|
|
className={`h-4 w-4 mr-2 ${restarting ? 'animate-spin' : ''}`}
|
|
/>
|
|
{restarting ? 'Reiniciando...' : 'Restart'}
|
|
</Button>
|
|
<Button variant="outline" size="sm" disabled>
|
|
<Terminal className="h-4 w-4 mr-2" />
|
|
Terminal
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
// Se houver pods, pega o primeiro pod running
|
|
const runningPod = pods.find(
|
|
(pod: any) => pod.status?.phase === 'Running',
|
|
);
|
|
const podToUse = runningPod || pods[0];
|
|
if (podToUse) {
|
|
handleViewLogs(
|
|
podToUse,
|
|
podToUse.spec.containers[0]?.name || '',
|
|
);
|
|
} else {
|
|
showToast({
|
|
title: 'Nenhum Pod Encontrado',
|
|
description:
|
|
'Não há pods disponíveis para visualizar logs',
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
}}
|
|
disabled={!pods || pods.length === 0}
|
|
>
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Logs
|
|
</Button>
|
|
<Button onClick={handleYamlView} variant="outline" size="sm">
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
YAML
|
|
</Button>
|
|
<Button
|
|
onClick={handleDeleteDaemonSetClick}
|
|
variant="destructive"
|
|
size="sm"
|
|
>
|
|
<Trash className="h-4 w-4 mr-2" />
|
|
Excluir
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={handleTabChange}
|
|
className="w-full"
|
|
>
|
|
<TabsList className="grid w-full grid-cols-4">
|
|
<TabsTrigger value="configuracoes">Configurações</TabsTrigger>
|
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
<TabsTrigger value="secret">Secret</TabsTrigger>
|
|
<TabsTrigger value="service">Service</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="configuracoes" className="mt-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12">
|
|
{/* Coluna 1: Informações Básicas */}
|
|
<div className="space-y-5 pr-6 border-r border-gray-100 last:border-r-0">
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Nome:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.metadata.name}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Update Strategy:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.updateStrategy?.type ||
|
|
'RollingUpdate'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Min Ready Seconds:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.minReadySeconds || 0}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Coluna 2: Configurações de Serviço */}
|
|
<div className="space-y-5 pr-6 border-r border-gray-100 last:border-r-0">
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Service Account:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.template?.spec?.serviceAccountName ||
|
|
'default'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Pull Secret:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.template?.spec?.imagePullSecrets
|
|
?.length > 0
|
|
? daemonSetInfo.spec.template.spec.imagePullSecrets
|
|
.map((s: any) => s.name)
|
|
.join(', ')
|
|
: 'Nenhum'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Node Selector:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.template?.spec?.nodeSelector
|
|
? Object.entries(
|
|
daemonSetInfo.spec.template.spec.nodeSelector,
|
|
)
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(', ')
|
|
: 'Nenhum'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Coluna 3: Políticas */}
|
|
<div className="space-y-5">
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Restart Policy:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.template?.spec?.restartPolicy ||
|
|
'Always'}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Revision History Limit:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.revisionHistoryLimit || 10}
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between items-start min-h-[20px]">
|
|
<span className="text-sm text-muted-foreground font-medium">
|
|
Tolerations:
|
|
</span>
|
|
<span className="text-sm font-semibold text-right ml-4">
|
|
{daemonSetInfo.spec?.template?.spec?.tolerations
|
|
?.length || 0}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Labels Section */}
|
|
<div className="mt-6 pt-4 border-t">
|
|
<h4 className="font-medium text-sm text-muted-foreground mb-2">
|
|
Labels
|
|
</h4>
|
|
<div className="flex flex-wrap gap-1">
|
|
{Object.entries(daemonSetInfo.metadata.labels || {}).length >
|
|
0 ? (
|
|
Object.entries(daemonSetInfo.metadata.labels || {}).map(
|
|
([key, value]) => (
|
|
<Badge
|
|
key={key}
|
|
variant="secondary"
|
|
className="text-xs"
|
|
>
|
|
{key}: {String(value)}
|
|
</Badge>
|
|
),
|
|
)
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">
|
|
Nenhum label definido
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Annotations Section */}
|
|
<div className="mt-4">
|
|
<h4 className="font-medium text-sm text-muted-foreground mb-2">
|
|
Annotations
|
|
</h4>
|
|
<div className="flex flex-wrap gap-1">
|
|
{Object.entries(daemonSetInfo.metadata.annotations || {})
|
|
.length > 0 ? (
|
|
Object.entries(
|
|
daemonSetInfo.metadata.annotations || {},
|
|
).map(([key, value]) => (
|
|
<Badge key={key} variant="outline" className="text-xs">
|
|
{key}:{' '}
|
|
{typeof value === 'string' && value.length > 50
|
|
? `${value.substring(0, 50)}...`
|
|
: String(value)}
|
|
</Badge>
|
|
))
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">
|
|
Nenhuma annotation definida
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Containers Section */}
|
|
<div className="mt-6 pt-4 border-t">
|
|
<h4 className="font-medium text-sm text-muted-foreground mb-3">
|
|
Containers
|
|
</h4>
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Nome
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Imagem
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Portas
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Tipo
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{/* Init Containers */}
|
|
{daemonSetInfo.spec?.template?.spec?.initContainers?.map(
|
|
(container: any, index: number) => (
|
|
<TableRow
|
|
key={`init-${index}`}
|
|
className="hover:bg-muted/50 h-10"
|
|
>
|
|
<TableCell className="font-medium py-2">
|
|
<div className="flex items-center space-x-1.5">
|
|
<Container className="h-3.5 w-3.5 text-orange-600" />
|
|
<span className="text-xs">
|
|
{container.name}
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span
|
|
className="font-mono text-xs max-w-[180px] truncate block"
|
|
title={container.image}
|
|
>
|
|
{container.image}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<div className="flex flex-wrap gap-0.5">
|
|
{container.ports?.length > 0 ? (
|
|
container.ports.map(
|
|
(port: any, portIndex: number) => (
|
|
<Badge
|
|
key={portIndex}
|
|
variant="outline"
|
|
className="text-xs px-1 py-0 h-5"
|
|
>
|
|
{port.containerPort}
|
|
</Badge>
|
|
),
|
|
)
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">
|
|
Nenhuma
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-orange-50 text-orange-700 border-orange-200"
|
|
>
|
|
Init
|
|
</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
)}
|
|
|
|
{/* Regular Containers */}
|
|
{daemonSetInfo.spec?.template?.spec?.containers?.map(
|
|
(container: any, index: number) => (
|
|
<TableRow
|
|
key={`container-${index}`}
|
|
className="hover:bg-muted/50 h-10"
|
|
>
|
|
<TableCell className="font-medium py-2">
|
|
<div className="flex items-center space-x-1.5">
|
|
<Container className="h-3.5 w-3.5 text-blue-600" />
|
|
<span className="text-xs">
|
|
{container.name}
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span
|
|
className="font-mono text-xs max-w-[180px] truncate block"
|
|
title={container.image}
|
|
>
|
|
{container.image}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<div className="flex flex-wrap gap-0.5">
|
|
{container.ports?.length > 0 ? (
|
|
container.ports.map(
|
|
(port: any, portIndex: number) => (
|
|
<Badge
|
|
key={portIndex}
|
|
variant="outline"
|
|
className="text-xs px-1 py-0 h-5"
|
|
>
|
|
{port.containerPort}
|
|
</Badge>
|
|
),
|
|
)
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">
|
|
Nenhuma
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-blue-50 text-blue-700 border-blue-200"
|
|
>
|
|
Container
|
|
</Badge>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
<TabsContent value="environment" className="mt-6">
|
|
{(() => {
|
|
// Create usage entries exactly like deployment details
|
|
const configMapUsageEntries: Array<{
|
|
configMapName: string;
|
|
usageType: string;
|
|
keys: string[];
|
|
isFullConfigMap: boolean;
|
|
}> = [];
|
|
|
|
const secretUsageEntries: Array<{
|
|
secretName: string;
|
|
usageType: string;
|
|
keys: string[];
|
|
isFullSecret: boolean;
|
|
}> = [];
|
|
|
|
const containers =
|
|
daemonSetInfo?.spec?.template?.spec?.containers || [];
|
|
const initContainers =
|
|
daemonSetInfo?.spec?.template?.spec?.initContainers || [];
|
|
const allContainers = [...containers, ...initContainers];
|
|
|
|
// Track hardcoded environment variables
|
|
const hardcodedVarsEntries: Array<{
|
|
varName: string;
|
|
value: string;
|
|
containerName: string;
|
|
}> = [];
|
|
|
|
// Process all containers following deployment pattern
|
|
allContainers.forEach((container: any) => {
|
|
// ConfigMaps via env (specific keys)
|
|
container.env?.forEach((env: any) => {
|
|
if (
|
|
env.valueFrom?.configMapKeyRef?.name &&
|
|
env.valueFrom?.configMapKeyRef?.key
|
|
) {
|
|
const configMapName = env.valueFrom.configMapKeyRef.name;
|
|
const keyName = env.valueFrom.configMapKeyRef.key;
|
|
|
|
configMapUsageEntries.push({
|
|
configMapName,
|
|
usageType: 'Env',
|
|
keys: [keyName],
|
|
isFullConfigMap: false,
|
|
});
|
|
} else if (
|
|
env.valueFrom?.secretKeyRef?.name &&
|
|
env.valueFrom?.secretKeyRef?.key
|
|
) {
|
|
const secretName = env.valueFrom.secretKeyRef.name;
|
|
const keyName = env.valueFrom.secretKeyRef.key;
|
|
|
|
secretUsageEntries.push({
|
|
secretName,
|
|
usageType: 'Env',
|
|
keys: [keyName],
|
|
isFullSecret: false,
|
|
});
|
|
} else if (env.value !== undefined) {
|
|
// Hardcoded environment variable
|
|
hardcodedVarsEntries.push({
|
|
varName: env.name,
|
|
value: env.value,
|
|
containerName: container.name,
|
|
});
|
|
}
|
|
});
|
|
|
|
// ConfigMaps via envFrom (full configmap)
|
|
container.envFrom?.forEach((envFrom: any) => {
|
|
if (envFrom.configMapRef?.name) {
|
|
const configMapName = envFrom.configMapRef.name;
|
|
|
|
configMapUsageEntries.push({
|
|
configMapName,
|
|
usageType: 'EnvFrom',
|
|
keys: [],
|
|
isFullConfigMap: true,
|
|
});
|
|
}
|
|
|
|
if (envFrom.secretRef?.name) {
|
|
const secretName = envFrom.secretRef.name;
|
|
|
|
secretUsageEntries.push({
|
|
secretName,
|
|
usageType: 'EnvFrom',
|
|
keys: [],
|
|
isFullSecret: true,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Create expanded lists like deployment
|
|
const expandedConfigMaps = configMapUsageEntries
|
|
.map((entry) => {
|
|
const configMap = configMaps.find(
|
|
(cm: any) => cm.metadata?.name === entry.configMapName,
|
|
);
|
|
|
|
return {
|
|
...configMap,
|
|
usageEntry: entry,
|
|
};
|
|
})
|
|
.filter((item) => item.metadata);
|
|
|
|
const expandedSecrets = secretUsageEntries
|
|
.map((entry) => {
|
|
const secret = secrets.find(
|
|
(s: any) => s.metadata?.name === entry.secretName,
|
|
);
|
|
|
|
return {
|
|
...secret,
|
|
usageEntry: entry,
|
|
};
|
|
})
|
|
.filter((item) => item.metadata);
|
|
|
|
// Create hardcoded variable entries (preserve YAML order)
|
|
const expandedHardcodedVars = hardcodedVarsEntries.map(
|
|
(entry) => ({
|
|
metadata: {
|
|
name: entry.varName,
|
|
creationTimestamp:
|
|
daemonSetInfo?.metadata?.creationTimestamp,
|
|
},
|
|
data: {
|
|
[entry.varName]: entry.value,
|
|
},
|
|
usageEntry: {
|
|
varName: entry.varName,
|
|
usageType: 'Hardcoded',
|
|
keys: [entry.varName],
|
|
isHardcoded: true,
|
|
},
|
|
isHardcodedVar: true,
|
|
containerName: entry.containerName,
|
|
value: entry.value,
|
|
}),
|
|
);
|
|
|
|
// Order resources to match YAML order: hardcoded vars first, then references, then envFrom
|
|
const envRefResources = [
|
|
...expandedConfigMaps,
|
|
...expandedSecrets,
|
|
].filter((r) => r.usageEntry?.usageType === 'Env');
|
|
const envFromResources = [
|
|
...expandedConfigMaps,
|
|
...expandedSecrets,
|
|
].filter((r) => r.usageEntry?.usageType === 'EnvFrom');
|
|
|
|
const allEnvResources = [
|
|
...expandedHardcodedVars,
|
|
...envRefResources,
|
|
...envFromResources,
|
|
];
|
|
|
|
const getConfigMapInfo = (configMap: any) => {
|
|
const dataKeys = Object.keys(configMap.data || {});
|
|
|
|
return {
|
|
keysCount: dataKeys.length,
|
|
keys: dataKeys,
|
|
size: JSON.stringify(configMap.data || {}).length,
|
|
};
|
|
};
|
|
|
|
const getSecretInfo = (secret: any) => {
|
|
const dataKeys = Object.keys(secret.data || {});
|
|
|
|
return {
|
|
keysCount: dataKeys.length,
|
|
keys: dataKeys,
|
|
size: JSON.stringify(secret.data || {}).length,
|
|
};
|
|
};
|
|
|
|
return allEnvResources.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<Settings className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
|
<h3 className="text-lg font-medium mb-2">
|
|
Nenhum ConfigMap/Secret Relacionado
|
|
</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Este DaemonSet não possui ConfigMaps ou Secrets associados
|
|
às variáveis de ambiente.
|
|
</p>
|
|
<Badge variant="outline">Nenhum recurso encontrado</Badge>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium">
|
|
Recursos de Ambiente ({allEnvResources.length})
|
|
</h3>
|
|
</div>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Type
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Chaves
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Uso
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Tamanho
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Idade
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8 text-right">
|
|
Ações
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{allEnvResources.map((envResource, index) => {
|
|
const isConfigMap =
|
|
envResource.usageEntry?.configMapName;
|
|
const isSecret = envResource.usageEntry?.secretName;
|
|
const isHardcoded = envResource.isHardcodedVar;
|
|
const { usageEntry } = envResource;
|
|
|
|
const IconComponent = isConfigMap
|
|
? Database
|
|
: isSecret
|
|
? Key
|
|
: Settings;
|
|
const iconColor = isConfigMap
|
|
? 'text-blue-600'
|
|
: isSecret
|
|
? 'text-orange-600'
|
|
: 'text-green-600';
|
|
|
|
const resourceInfo = isHardcoded
|
|
? {
|
|
size:
|
|
envResource.metadata?.name?.length +
|
|
envResource.value?.length || 0,
|
|
}
|
|
: isConfigMap
|
|
? getConfigMapInfo(envResource)
|
|
: getSecretInfo(envResource);
|
|
|
|
const getTypeDisplay = () => {
|
|
if (isHardcoded) return 'Environment Variable';
|
|
if (isConfigMap) return 'ConfigMap';
|
|
if (isSecret) return 'Secret';
|
|
return 'Unknown';
|
|
};
|
|
|
|
return (
|
|
<TableRow key={index} className="hover:bg-muted/50">
|
|
{/* Type Column */}
|
|
<TableCell className="font-medium py-2">
|
|
<div className="flex items-center space-x-2">
|
|
<IconComponent
|
|
className={`h-4 w-4 ${iconColor}`}
|
|
/>
|
|
<span className="cursor-pointer hover:text-blue-600 transition-colors hover:underline">
|
|
{getTypeDisplay()}
|
|
</span>
|
|
<Copy
|
|
className="h-4 w-4 cursor-pointer hover:text-blue-600 flex-shrink-0"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(
|
|
getTypeDisplay(),
|
|
);
|
|
showToast({
|
|
title: 'Type Copiado',
|
|
description: `Type "${getTypeDisplay()}" copiado para a área de transferência`,
|
|
variant: 'success',
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
</TableCell>
|
|
|
|
{/* Chaves Column */}
|
|
<TableCell className="py-2">
|
|
<div className="flex items-center space-x-2">
|
|
{usageEntry.isFullConfigMap ||
|
|
usageEntry.isFullSecret ? (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-green-50 text-green-700 cursor-pointer hover:bg-opacity-80"
|
|
onClick={() =>
|
|
isConfigMap
|
|
? handleConfigMapKeysView(envResource)
|
|
: handleSecretKeysView(envResource)
|
|
}
|
|
>
|
|
(Todas as Chaves)
|
|
</Badge>
|
|
) : isHardcoded ? (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-gray-50 text-gray-700 cursor-pointer hover:bg-opacity-80"
|
|
onClick={() => {
|
|
const envVarData = {
|
|
metadata: {
|
|
name: usageEntry.varName,
|
|
},
|
|
data: {
|
|
[usageEntry.varName || '']:
|
|
envResource.value,
|
|
},
|
|
};
|
|
setSelectedConfigMapData(envVarData);
|
|
setConfigMapKeysModalOpen(true);
|
|
}}
|
|
>
|
|
{usageEntry.varName}
|
|
</Badge>
|
|
) : usageEntry.keys &&
|
|
usageEntry.keys.length > 0 ? (
|
|
<div className="flex flex-wrap gap-1">
|
|
{usageEntry.keys.map((key: string) => (
|
|
<Badge
|
|
key={key}
|
|
variant="outline"
|
|
className="text-xs bg-blue-50 text-blue-700 cursor-pointer hover:bg-opacity-80"
|
|
onClick={() => {
|
|
if (isConfigMap) {
|
|
handleConfigMapKeysView(
|
|
envResource,
|
|
);
|
|
} else {
|
|
handleSecretKeysView(envResource);
|
|
}
|
|
}}
|
|
>
|
|
{key}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs"
|
|
>
|
|
(Não especificado)
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
|
|
{/* Uso Column */}
|
|
<TableCell className="py-2">
|
|
<Badge
|
|
variant="secondary"
|
|
className="text-xs px-1.5 py-0.5 h-5"
|
|
>
|
|
{usageEntry.usageType}
|
|
</Badge>
|
|
</TableCell>
|
|
|
|
{/* Tamanho Column */}
|
|
<TableCell className="py-2">
|
|
<span className="text-xs text-muted-foreground">
|
|
{(resourceInfo.size / 1024).toFixed(1)} KB
|
|
</span>
|
|
</TableCell>
|
|
|
|
{/* Idade Column */}
|
|
<TableCell className="py-2">
|
|
<span className="text-xs text-muted-foreground">
|
|
{getAge(
|
|
envResource.metadata?.creationTimestamp ||
|
|
'',
|
|
)}
|
|
</span>
|
|
</TableCell>
|
|
|
|
{/* Ações Column */}
|
|
<TableCell className="py-2 text-right">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>Ações</DropdownMenuLabel>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
if (isHardcoded) {
|
|
const envVarData = {
|
|
metadata: {
|
|
name: envResource.metadata?.name,
|
|
},
|
|
data: {
|
|
[envResource.metadata?.name ||
|
|
'']: envResource.value,
|
|
},
|
|
};
|
|
setSelectedConfigMapData(envVarData);
|
|
setConfigMapKeysModalOpen(true);
|
|
} else if (isConfigMap) {
|
|
handleConfigMapKeysView(envResource);
|
|
} else {
|
|
handleSecretKeysView(envResource);
|
|
}
|
|
}}
|
|
>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Ver {isHardcoded ? 'Valor' : 'Chaves'}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
showToast({
|
|
title: 'Em Desenvolvimento',
|
|
description:
|
|
'Visualização YAML será implementada em breve',
|
|
variant: 'default',
|
|
});
|
|
}}
|
|
>
|
|
<FileText className="h-4 w-4 mr-2" />
|
|
Ver YAML
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
})()}
|
|
</TabsContent>
|
|
<TabsContent value="secret" className="mt-6">
|
|
{secrets.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<Key className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
|
<h3 className="text-lg font-medium mb-2">
|
|
Nenhum Secret Relacionado
|
|
</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Este DaemonSet não possui Secrets associados.
|
|
</p>
|
|
<Badge variant="outline">Nenhum secret encontrado</Badge>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium">
|
|
Secrets Relacionados ({secrets.length})
|
|
</h3>
|
|
</div>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Nome
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Tipo
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Chaves
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Uso
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Idade
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{secrets.map((secret: any, index: number) => {
|
|
const secretInfo = getSecretInfo(secret);
|
|
const SecretIcon = secretInfo.icon;
|
|
const { usageEntry } = secret;
|
|
return (
|
|
<TableRow key={index} className="hover:bg-muted/50">
|
|
<TableCell className="font-medium py-2">
|
|
<div className="flex items-center space-x-2">
|
|
<SecretIcon className="h-4 w-4 text-orange-600" />
|
|
<span className="cursor-pointer hover:text-blue-600 transition-colors hover:underline">
|
|
{secret.metadata?.name}
|
|
</span>
|
|
<Copy
|
|
className="h-4 w-4 cursor-pointer hover:text-blue-600 flex-shrink-0"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(
|
|
secret.metadata?.name || '',
|
|
);
|
|
showToast({
|
|
title: 'Nome Copiado',
|
|
description: `Nome "${secret.metadata?.name}" copiado para a área de transferência`,
|
|
variant: 'success',
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs px-1.5 py-0.5 h-5"
|
|
>
|
|
{secretInfo.type}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<div className="flex items-center space-x-2">
|
|
{usageEntry.isFullSecret ? (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs bg-green-50 text-green-700 cursor-pointer hover:bg-opacity-80"
|
|
>
|
|
(Todas as Chaves)
|
|
</Badge>
|
|
) : usageEntry.keys.length > 0 ? (
|
|
<div className="flex flex-wrap gap-1">
|
|
{usageEntry.keys.map((key: string) => (
|
|
<Badge
|
|
key={key}
|
|
variant="outline"
|
|
className="text-xs bg-blue-50 text-blue-700 cursor-pointer hover:bg-opacity-80"
|
|
>
|
|
{key}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<Badge variant="outline" className="text-xs">
|
|
(Não especificado)
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<Badge
|
|
variant="secondary"
|
|
className="text-xs px-1.5 py-0.5 h-5"
|
|
>
|
|
{usageEntry.usageType}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span className="text-xs text-muted-foreground">
|
|
{getAge(secret.metadata?.creationTimestamp)}
|
|
</span>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
<TabsContent value="service" className="mt-6">
|
|
{services.length === 0 ? (
|
|
<div className="text-center py-8">
|
|
<Network className="h-12 w-12 mx-auto text-muted-foreground mb-4" />
|
|
<h3 className="text-lg font-medium mb-2">
|
|
Nenhum Service Relacionado
|
|
</h3>
|
|
<p className="text-muted-foreground mb-4">
|
|
Este DaemonSet não possui Services associados.
|
|
</p>
|
|
<Badge variant="outline">Nenhum service encontrado</Badge>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-lg font-medium">
|
|
Services Relacionados ({services.length})
|
|
</h3>
|
|
</div>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Nome
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Tipo
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Cluster IP
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Portas
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Seletores
|
|
</TableHead>
|
|
<TableHead className="text-xs font-medium py-2 h-8">
|
|
Idade
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{services.map((service: any, index: number) => {
|
|
const serviceType = service.spec?.type || 'ClusterIP';
|
|
const ports = service.spec?.ports || [];
|
|
const selectors = service.spec?.selector || {};
|
|
|
|
return (
|
|
<TableRow key={index} className="hover:bg-muted/50">
|
|
<TableCell className="font-medium py-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Network className="h-4 w-4 text-green-600" />
|
|
<span className="cursor-pointer hover:text-blue-600 transition-colors hover:underline">
|
|
{service.metadata?.name}
|
|
</span>
|
|
<Copy
|
|
className="h-4 w-4 cursor-pointer hover:text-blue-600 flex-shrink-0"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(
|
|
service.metadata?.name || '',
|
|
);
|
|
showToast({
|
|
title: 'Nome Copiado',
|
|
description: `Nome "${service.metadata?.name}" copiado para a área de transferência`,
|
|
variant: 'success',
|
|
});
|
|
}}
|
|
/>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<Badge
|
|
variant={
|
|
serviceType === 'ClusterIP'
|
|
? 'secondary'
|
|
: 'outline'
|
|
}
|
|
className="text-xs px-1.5 py-0.5 h-5"
|
|
>
|
|
{serviceType}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span className="text-xs text-muted-foreground font-mono">
|
|
{service.spec?.clusterIP || 'None'}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<div className="flex flex-wrap gap-1">
|
|
{ports.length > 0 ? (
|
|
ports.map((port: any, portIndex: number) => (
|
|
<Badge
|
|
key={portIndex}
|
|
variant="outline"
|
|
className="text-xs px-1.5 py-0.5 h-5"
|
|
>
|
|
{port.port}
|
|
{port.targetPort &&
|
|
port.targetPort !== port.port && (
|
|
<>:{port.targetPort}</>
|
|
)}
|
|
/{port.protocol || 'TCP'}
|
|
</Badge>
|
|
))
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">
|
|
Nenhuma
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<div className="flex flex-wrap gap-1">
|
|
{Object.entries(selectors).length > 0 ? (
|
|
Object.entries(selectors).map(
|
|
([key, value]) => (
|
|
<Badge
|
|
key={key}
|
|
variant="outline"
|
|
className="text-xs px-1.5 py-0.5 h-5"
|
|
>
|
|
{key}={String(value)}
|
|
</Badge>
|
|
),
|
|
)
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">
|
|
Nenhum
|
|
</span>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="py-2">
|
|
<span className="text-xs text-muted-foreground">
|
|
{getAge(service.metadata?.creationTimestamp)}
|
|
</span>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Tabs */}
|
|
<Tabs defaultValue="pods" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-3">
|
|
<TabsTrigger value="pods">Pods ({pods.length})</TabsTrigger>
|
|
<TabsTrigger value="events">Eventos ({events.length})</TabsTrigger>
|
|
<TabsTrigger value="yaml">YAML</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="pods" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Pods do DaemonSet</CardTitle>
|
|
<CardDescription>
|
|
Pods criados e gerenciados por este DaemonSet
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Nome</TableHead>
|
|
<TableHead>Node</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Restarts</TableHead>
|
|
<TableHead>Idade</TableHead>
|
|
<TableHead>Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{pods.map((pod) => {
|
|
const podStatus = getPodStatus(pod);
|
|
const PodStatusIcon = podStatus.icon;
|
|
|
|
return (
|
|
<TableRow key={pod.metadata.name}>
|
|
<TableCell>
|
|
<button
|
|
className="text-blue-600 hover:text-blue-800 hover:underline text-left"
|
|
onClick={() =>
|
|
navigate(
|
|
`/workloads/pods/${pod.metadata.name}/${pod.metadata.namespace}`,
|
|
)
|
|
}
|
|
>
|
|
{pod.metadata.name}
|
|
</button>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline">
|
|
{pod.spec.nodeName || 'N/A'}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center">
|
|
<PodStatusIcon
|
|
className={`h-4 w-4 mr-2 ${podStatus.color}`}
|
|
/>
|
|
<Badge
|
|
className={`${podStatus.bgColor} ${podStatus.color}`}
|
|
>
|
|
{podStatus.label}
|
|
</Badge>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
{pod.status.containerStatuses?.reduce(
|
|
(total: number, container: any) =>
|
|
total + (container.restartCount || 0),
|
|
0,
|
|
) || 0}
|
|
</TableCell>
|
|
<TableCell>
|
|
{getAge(pod.metadata.creationTimestamp)}
|
|
</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
>
|
|
<MoreVertical className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuLabel>
|
|
Ações do Pod
|
|
</DropdownMenuLabel>
|
|
<DropdownMenuItem
|
|
onClick={() =>
|
|
navigate(
|
|
`/workloads/pods/${pod.metadata.name}/${pod.metadata.namespace}`,
|
|
)
|
|
}
|
|
>
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Visualizar
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() =>
|
|
handleViewLogs(
|
|
pod,
|
|
pod.spec.containers[0]?.name || '',
|
|
)
|
|
}
|
|
>
|
|
<Terminal className="h-4 w-4 mr-2" />
|
|
Ver Logs
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
{pods.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center py-8">
|
|
<div className="text-muted-foreground">
|
|
<Shield className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>Nenhum pod encontrado</p>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="events" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Eventos do DaemonSet</CardTitle>
|
|
<CardDescription>
|
|
Eventos relacionados a este DaemonSet
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Tipo</TableHead>
|
|
<TableHead>Motivo</TableHead>
|
|
<TableHead>Mensagem</TableHead>
|
|
<TableHead>Primeira Ocorrência</TableHead>
|
|
<TableHead>Última Ocorrência</TableHead>
|
|
<TableHead>Contagem</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{events.map((event, index) => {
|
|
const eventType = getEventType(event);
|
|
const EventIcon = eventType.icon;
|
|
|
|
return (
|
|
<TableRow key={index}>
|
|
<TableCell>
|
|
<div className="flex items-center">
|
|
<EventIcon
|
|
className={`h-4 w-4 mr-2 ${eventType.color}`}
|
|
/>
|
|
<Badge
|
|
className={`${eventType.bgColor} ${eventType.color}`}
|
|
>
|
|
{event.type}
|
|
</Badge>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{event.reason}</TableCell>
|
|
<TableCell className="max-w-md truncate">
|
|
{event.message}
|
|
</TableCell>
|
|
<TableCell>
|
|
{event.firstTimestamp
|
|
? new Date(event.firstTimestamp).toLocaleString()
|
|
: 'N/A'}
|
|
</TableCell>
|
|
<TableCell>
|
|
{event.lastTimestamp
|
|
? new Date(event.lastTimestamp).toLocaleString()
|
|
: 'N/A'}
|
|
</TableCell>
|
|
<TableCell>{event.count || 1}</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
{events.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={6} className="text-center py-8">
|
|
<div className="text-muted-foreground">
|
|
<Activity className="h-8 w-8 mx-auto mb-2 opacity-50" />
|
|
<p>Nenhum evento encontrado</p>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="yaml" className="space-y-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>YAML do DaemonSet</CardTitle>
|
|
<CardDescription>
|
|
Configuração completa do DaemonSet em formato YAML
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<pre className="bg-gray-100 p-4 rounded-lg overflow-auto text-sm">
|
|
<code>{JSON.stringify(daemonSetInfo, null, 2)}</code>
|
|
</pre>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
{/* YAML Editor */}
|
|
<YamlEditor
|
|
open={yamlDrawerOpen}
|
|
onOpenChange={setYamlDrawerOpen}
|
|
resourceType="daemonset"
|
|
resourceName={daemonSetName || ''}
|
|
namespace={namespaceName || ''}
|
|
title={`YAML do DaemonSet - ${daemonSetName}`}
|
|
onSave={() => {
|
|
setTimeout(() => {
|
|
backgroundRefetch();
|
|
}, 1000);
|
|
}}
|
|
/>
|
|
|
|
{/* Delete Confirmation Modal */}
|
|
<Dialog open={deleteModalOpen} onOpenChange={setDeleteModalOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Excluir DaemonSet</DialogTitle>
|
|
<DialogDescription>
|
|
Tem certeza que deseja excluir o DaemonSet{' '}
|
|
<strong>{daemonSetInfo?.metadata.name}</strong> do namespace{' '}
|
|
<strong>{daemonSetInfo?.metadata.namespace}</strong>?
|
|
<br />
|
|
<br />
|
|
Esta ação não pode ser desfeita e todos os pods associados serão
|
|
removidos de todos os nós.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setDeleteModalOpen(false)}
|
|
disabled={deleting}
|
|
>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleDeleteDaemonSet}
|
|
disabled={deleting}
|
|
>
|
|
{deleting ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Excluindo...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Excluir DaemonSet
|
|
</>
|
|
)}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Logs Modal */}
|
|
{selectedPod && (
|
|
<LogsModal
|
|
open={logsModalOpen}
|
|
onOpenChange={setLogsModalOpen}
|
|
podName={selectedPod.metadata.name}
|
|
namespace={selectedPod.metadata.namespace}
|
|
containerName={selectedContainer}
|
|
/>
|
|
)}
|
|
|
|
{/* Modal para visualizar chaves e valores do ConfigMap */}
|
|
<Dialog
|
|
open={configMapKeysModalOpen}
|
|
onOpenChange={setConfigMapKeysModalOpen}
|
|
>
|
|
<DialogContent className="sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center">
|
|
<Database className="h-5 w-5 mr-2 text-blue-600" />
|
|
Chaves do ConfigMap: {selectedConfigMapData?.metadata?.name}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Visualização das chaves e valores do ConfigMap
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{selectedConfigMapData ? (
|
|
<div className="max-h-[400px] overflow-y-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[200px]">Chave</TableHead>
|
|
<TableHead>Valor</TableHead>
|
|
<TableHead className="w-[100px]">Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{Object.entries(selectedConfigMapData.data || {}).map(
|
|
([key, value]) => (
|
|
<TableRow key={key}>
|
|
<TableCell className="font-medium">{key}</TableCell>
|
|
<TableCell>
|
|
<div className="max-w-[300px] truncate font-mono text-sm bg-gray-50 p-2 rounded">
|
|
{typeof value === 'string' && value.length > 100
|
|
? `${value.substring(0, 100)}...`
|
|
: String(value)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Copy
|
|
className="h-4 w-4 cursor-pointer hover:text-blue-600"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(String(value));
|
|
showToast({
|
|
title: 'Valor Copiado',
|
|
description: `Valor da chave "${key}" copiado para a área de transferência`,
|
|
variant: 'success',
|
|
});
|
|
}}
|
|
/>
|
|
</TableCell>
|
|
</TableRow>
|
|
),
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-4">
|
|
<p className="text-muted-foreground">
|
|
Nenhum dado encontrado para este ConfigMap
|
|
</p>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Modal para visualizar chaves e valores do Secret */}
|
|
<Dialog open={secretKeysModalOpen} onOpenChange={setSecretKeysModalOpen}>
|
|
<DialogContent className="sm:max-w-[600px]">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center">
|
|
<Key className="h-5 w-5 mr-2 text-orange-600" />
|
|
Chaves do Secret: {selectedSecretData?.metadata?.name}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Visualização das chaves e valores do Secret (valores são mostrados
|
|
decodificados)
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{selectedSecretData ? (
|
|
<div className="max-h-[400px] overflow-y-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[200px]">Chave</TableHead>
|
|
<TableHead>Valor</TableHead>
|
|
<TableHead className="w-[100px]">Ações</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{Object.entries(selectedSecretData.data || {}).map(
|
|
([key, encodedValue]) => {
|
|
// Decodificar valor base64
|
|
let decodedValue = '';
|
|
try {
|
|
decodedValue = atob(encodedValue as string);
|
|
} catch (e) {
|
|
decodedValue = '(Erro ao decodificar)';
|
|
}
|
|
return (
|
|
<TableRow key={key}>
|
|
<TableCell className="font-medium">{key}</TableCell>
|
|
<TableCell>
|
|
<div className="max-w-[300px] truncate font-mono text-sm bg-gray-50 p-2 rounded">
|
|
{decodedValue.length > 100
|
|
? `${decodedValue.substring(0, 100)}...`
|
|
: decodedValue}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
navigator.clipboard.writeText(decodedValue);
|
|
showToast({
|
|
title: 'Valor Copiado',
|
|
description: `Valor da chave "${key}" copiado para a área de transferência`,
|
|
variant: 'success',
|
|
});
|
|
}}
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
},
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-4">
|
|
<p className="text-muted-foreground">
|
|
Nenhum dado encontrado para este Secret
|
|
</p>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|