Files
kube-now/src/components/pages/daemonset-details.tsx
Junior 15e6692647
Some checks failed
Test / test (macos-latest) (push) Waiting to run
Test / test (windows-latest) (push) Waiting to run
Test / test (ubuntu-latest) (push) Failing after 13m2s
commit inicial do projeto
2026-04-21 18:18:56 -03:00

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>
);
}