kubeops 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +219 -0
  2. package/electron/main.js +429 -0
  3. package/electron/preload.js +13 -0
  4. package/electron-builder.yml +60 -0
  5. package/next.config.ts +8 -0
  6. package/package.json +98 -0
  7. package/postcss.config.mjs +7 -0
  8. package/resources/icon.icns +0 -0
  9. package/resources/icon.ico +0 -0
  10. package/resources/icon.png +0 -0
  11. package/resources/icon.svg +86 -0
  12. package/scripts/build-server.mjs +20 -0
  13. package/scripts/generate-icons.mjs +61 -0
  14. package/server.ts +58 -0
  15. package/src/app/api/clusters/[clusterId]/health/route.ts +27 -0
  16. package/src/app/api/clusters/[clusterId]/metrics/route.ts +196 -0
  17. package/src/app/api/clusters/[clusterId]/namespaces/route.ts +48 -0
  18. package/src/app/api/clusters/[clusterId]/nodes/[nodeName]/route.ts +21 -0
  19. package/src/app/api/clusters/[clusterId]/nodes/route.ts +31 -0
  20. package/src/app/api/clusters/[clusterId]/resources/[namespace]/[...resourcePath]/route.ts +204 -0
  21. package/src/app/api/clusters/route.ts +48 -0
  22. package/src/app/api/kubeconfig/route.ts +25 -0
  23. package/src/app/api/port-forward/route.ts +143 -0
  24. package/src/app/api/tsh/login/route.ts +50 -0
  25. package/src/app/clusters/[clusterId]/app-map/page.tsx +42 -0
  26. package/src/app/clusters/[clusterId]/clusterrolebindings/[name]/page.tsx +5 -0
  27. package/src/app/clusters/[clusterId]/clusterrolebindings/page.tsx +5 -0
  28. package/src/app/clusters/[clusterId]/clusterroles/[name]/page.tsx +5 -0
  29. package/src/app/clusters/[clusterId]/clusterroles/page.tsx +5 -0
  30. package/src/app/clusters/[clusterId]/layout.tsx +9 -0
  31. package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/[name]/page.tsx +5 -0
  32. package/src/app/clusters/[clusterId]/namespaces/[namespace]/configmaps/page.tsx +5 -0
  33. package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/[name]/page.tsx +5 -0
  34. package/src/app/clusters/[clusterId]/namespaces/[namespace]/cronjobs/page.tsx +5 -0
  35. package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/[name]/page.tsx +5 -0
  36. package/src/app/clusters/[clusterId]/namespaces/[namespace]/daemonsets/page.tsx +5 -0
  37. package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/[name]/page.tsx +457 -0
  38. package/src/app/clusters/[clusterId]/namespaces/[namespace]/deployments/page.tsx +5 -0
  39. package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/[name]/page.tsx +5 -0
  40. package/src/app/clusters/[clusterId]/namespaces/[namespace]/endpoints/page.tsx +5 -0
  41. package/src/app/clusters/[clusterId]/namespaces/[namespace]/events/page.tsx +5 -0
  42. package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/[name]/page.tsx +5 -0
  43. package/src/app/clusters/[clusterId]/namespaces/[namespace]/ingresses/page.tsx +5 -0
  44. package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/[name]/page.tsx +5 -0
  45. package/src/app/clusters/[clusterId]/namespaces/[namespace]/jobs/page.tsx +5 -0
  46. package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/[name]/page.tsx +5 -0
  47. package/src/app/clusters/[clusterId]/namespaces/[namespace]/networkpolicies/page.tsx +5 -0
  48. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/exec/page.tsx +173 -0
  49. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/logs/page.tsx +137 -0
  50. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/[podName]/page.tsx +448 -0
  51. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pods/page.tsx +5 -0
  52. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/[name]/page.tsx +5 -0
  53. package/src/app/clusters/[clusterId]/namespaces/[namespace]/pvcs/page.tsx +5 -0
  54. package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/[name]/page.tsx +5 -0
  55. package/src/app/clusters/[clusterId]/namespaces/[namespace]/replicasets/page.tsx +5 -0
  56. package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/[name]/page.tsx +5 -0
  57. package/src/app/clusters/[clusterId]/namespaces/[namespace]/rolebindings/page.tsx +5 -0
  58. package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/[name]/page.tsx +5 -0
  59. package/src/app/clusters/[clusterId]/namespaces/[namespace]/roles/page.tsx +5 -0
  60. package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/[name]/page.tsx +168 -0
  61. package/src/app/clusters/[clusterId]/namespaces/[namespace]/secrets/page.tsx +5 -0
  62. package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/[name]/page.tsx +5 -0
  63. package/src/app/clusters/[clusterId]/namespaces/[namespace]/serviceaccounts/page.tsx +5 -0
  64. package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/[name]/page.tsx +5 -0
  65. package/src/app/clusters/[clusterId]/namespaces/[namespace]/services/page.tsx +5 -0
  66. package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/[name]/page.tsx +302 -0
  67. package/src/app/clusters/[clusterId]/namespaces/[namespace]/statefulsets/page.tsx +5 -0
  68. package/src/app/clusters/[clusterId]/nodes/[nodeName]/page.tsx +5 -0
  69. package/src/app/clusters/[clusterId]/nodes/page.tsx +5 -0
  70. package/src/app/clusters/[clusterId]/page.tsx +635 -0
  71. package/src/app/clusters/[clusterId]/port-forwarding/page.tsx +145 -0
  72. package/src/app/clusters/[clusterId]/pvs/[name]/page.tsx +5 -0
  73. package/src/app/clusters/[clusterId]/pvs/page.tsx +5 -0
  74. package/src/app/clusters/page.tsx +166 -0
  75. package/src/app/favicon.ico +0 -0
  76. package/src/app/globals.css +167 -0
  77. package/src/app/layout.tsx +48 -0
  78. package/src/app/page.tsx +5 -0
  79. package/src/components/clusters/cluster-selector.tsx +64 -0
  80. package/src/components/layout/app-shell.tsx +26 -0
  81. package/src/components/layout/breadcrumbs.tsx +97 -0
  82. package/src/components/layout/command-palette.tsx +112 -0
  83. package/src/components/layout/header.tsx +184 -0
  84. package/src/components/layout/sidebar.tsx +84 -0
  85. package/src/components/layout/theme-toggle.tsx +21 -0
  86. package/src/components/namespaces/namespace-selector.tsx +165 -0
  87. package/src/components/panel/bottom-panel.tsx +127 -0
  88. package/src/components/panel/logs-tab.tsx +109 -0
  89. package/src/components/panel/terminal-tab.tsx +180 -0
  90. package/src/components/pods/pod-watch-button.tsx +44 -0
  91. package/src/components/resources/resource-columns.tsx +320 -0
  92. package/src/components/resources/resource-detail-page.tsx +191 -0
  93. package/src/components/resources/resource-list-page.tsx +78 -0
  94. package/src/components/resources/scale-dialog.tsx +107 -0
  95. package/src/components/settings/settings-dialog.tsx +103 -0
  96. package/src/components/shared/age-display.tsx +27 -0
  97. package/src/components/shared/confirm-dialog.tsx +52 -0
  98. package/src/components/shared/data-table.tsx +149 -0
  99. package/src/components/shared/env-value-resolver.tsx +570 -0
  100. package/src/components/shared/error-display.tsx +109 -0
  101. package/src/components/shared/loading-skeleton.tsx +25 -0
  102. package/src/components/shared/metrics-charts-impl.tsx +434 -0
  103. package/src/components/shared/metrics-charts.tsx +24 -0
  104. package/src/components/shared/port-forward-btn.tsx +60 -0
  105. package/src/components/shared/resource-info-drawer.tsx +542 -0
  106. package/src/components/shared/resource-node.tsx +157 -0
  107. package/src/components/shared/resource-tree-impl.tsx +228 -0
  108. package/src/components/shared/resource-tree.tsx +20 -0
  109. package/src/components/shared/status-badge.tsx +35 -0
  110. package/src/components/shared/yaml-editor.tsx +438 -0
  111. package/src/components/ui/badge.tsx +48 -0
  112. package/src/components/ui/button.tsx +64 -0
  113. package/src/components/ui/card.tsx +92 -0
  114. package/src/components/ui/command.tsx +184 -0
  115. package/src/components/ui/dialog.tsx +158 -0
  116. package/src/components/ui/dropdown-menu.tsx +257 -0
  117. package/src/components/ui/input.tsx +21 -0
  118. package/src/components/ui/popover.tsx +89 -0
  119. package/src/components/ui/scroll-area.tsx +58 -0
  120. package/src/components/ui/select.tsx +190 -0
  121. package/src/components/ui/separator.tsx +28 -0
  122. package/src/components/ui/sheet.tsx +143 -0
  123. package/src/components/ui/skeleton.tsx +13 -0
  124. package/src/components/ui/sonner.tsx +40 -0
  125. package/src/components/ui/table.tsx +116 -0
  126. package/src/components/ui/tabs.tsx +91 -0
  127. package/src/components/ui/tooltip.tsx +57 -0
  128. package/src/hooks/use-age-tick.ts +40 -0
  129. package/src/hooks/use-auto-update.ts +100 -0
  130. package/src/hooks/use-clusters.ts +32 -0
  131. package/src/hooks/use-namespaces.ts +17 -0
  132. package/src/hooks/use-pod-watcher.ts +79 -0
  133. package/src/hooks/use-port-forwards.ts +18 -0
  134. package/src/hooks/use-resource-detail.ts +28 -0
  135. package/src/hooks/use-resource-list.ts +26 -0
  136. package/src/hooks/use-resource-tree.ts +440 -0
  137. package/src/lib/api-client.ts +31 -0
  138. package/src/lib/constants.ts +126 -0
  139. package/src/lib/k8s/client-factory.ts +57 -0
  140. package/src/lib/k8s/kubeconfig-manager.ts +43 -0
  141. package/src/lib/k8s/resource-api.ts +223 -0
  142. package/src/lib/k8s/types.ts +29 -0
  143. package/src/lib/utils.ts +6 -0
  144. package/src/providers/swr-provider.tsx +20 -0
  145. package/src/providers/theme-provider.tsx +17 -0
  146. package/src/stores/cluster-store.ts +32 -0
  147. package/src/stores/namespace-store.ts +27 -0
  148. package/src/stores/panel-store.ts +61 -0
  149. package/src/stores/pod-watcher-store.ts +69 -0
  150. package/src/stores/settings-store.ts +24 -0
  151. package/src/stores/sidebar-store.ts +22 -0
  152. package/src/types/cluster.ts +19 -0
  153. package/src/types/css.d.ts +6 -0
  154. package/src/types/electron.d.ts +25 -0
  155. package/src/types/navigation.ts +4 -0
  156. package/src/types/resource.ts +27 -0
  157. package/tsconfig.json +34 -0
  158. package/ws/exec-handler.ts +112 -0
  159. package/ws/index.ts +2 -0
  160. package/ws/logs-handler.ts +70 -0
@@ -0,0 +1,448 @@
1
+ 'use client';
2
+
3
+ import { useParams, useRouter } from 'next/navigation';
4
+ import { useResourceDetail } from '@/hooks/use-resource-detail';
5
+ import { LoadingSkeleton } from '@/components/shared/loading-skeleton';
6
+ import { ErrorDisplay } from '@/components/shared/error-display';
7
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
8
+ import { Badge } from '@/components/ui/badge';
9
+ import { Button } from '@/components/ui/button';
10
+ import { StatusBadge } from '@/components/shared/status-badge';
11
+ import { AgeDisplay } from '@/components/shared/age-display';
12
+ import { ArrowLeft, Terminal, ScrollText, Trash2, KeyRound } from 'lucide-react';
13
+ import { useState, useMemo } from 'react';
14
+ import { usePanelStore } from '@/stores/panel-store';
15
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog';
16
+ import { apiClient } from '@/lib/api-client';
17
+ import { toast } from 'sonner';
18
+ import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
19
+ import { YamlEditor } from '@/components/shared/yaml-editor';
20
+ import { PodMetricsCharts } from '@/components/shared/metrics-charts';
21
+ import { EnvValueCell, EnvFromRows, MountedSecretRows, PodLinkedSecrets } from '@/components/shared/env-value-resolver';
22
+ import { PortForwardBtn } from '@/components/shared/port-forward-btn';
23
+ import { PodWatchButton } from '@/components/pods/pod-watch-button';
24
+ import { usePodRestartWatcher } from '@/hooks/use-pod-watcher';
25
+ import { ResourceTreeView } from '@/components/shared/resource-tree';
26
+ import { useResourceTree } from '@/hooks/use-resource-tree';
27
+
28
+ function PodResourceTree({ clusterId, namespace, rootKind, rootName, focusPodName }: {
29
+ clusterId: string;
30
+ namespace: string;
31
+ rootKind: 'Deployment' | 'StatefulSet';
32
+ rootName: string;
33
+ focusPodName: string;
34
+ }) {
35
+ const { nodes, edges, isLoading } = useResourceTree({ clusterId, namespace, rootKind, rootName });
36
+ return (
37
+ <div className="space-y-2">
38
+ <h3 className="text-sm font-semibold">Resource Tree</h3>
39
+ <ResourceTreeView
40
+ treeNodes={nodes}
41
+ treeEdges={edges}
42
+ isLoading={isLoading}
43
+ height={300}
44
+ focusNodeId={`Pod/${focusPodName}`}
45
+ />
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export default function PodDetailPage() {
51
+ const params = useParams();
52
+ const router = useRouter();
53
+ const clusterId = params.clusterId as string;
54
+ const namespace = params.namespace as string;
55
+ const podName = params.podName as string;
56
+
57
+ const [deleteOpen, setDeleteOpen] = useState(false);
58
+ const [deleting, setDeleting] = useState(false);
59
+ const [envDrawerOpen, setEnvDrawerOpen] = useState(false);
60
+ const { addTab } = usePanelStore();
61
+ const decodedClusterId = decodeURIComponent(clusterId);
62
+
63
+ const { data: pod, error, isLoading, mutate } = useResourceDetail({
64
+ clusterId: decodedClusterId,
65
+ namespace,
66
+ resourceType: 'pods',
67
+ name: podName,
68
+ });
69
+
70
+ // Resolve pod's owner to determine tree root
71
+ const ownerRef = pod?.metadata?.ownerReferences?.[0];
72
+
73
+ const { data: ownerRS } = useResourceDetail({
74
+ clusterId: decodedClusterId,
75
+ namespace,
76
+ resourceType: 'replicasets',
77
+ name: ownerRef?.name || '',
78
+ enabled: ownerRef?.kind === 'ReplicaSet',
79
+ });
80
+
81
+ const treeRoot = useMemo(() => {
82
+ if (!ownerRef) return null;
83
+ if (ownerRef.kind === 'StatefulSet') {
84
+ return { rootKind: 'StatefulSet' as const, rootName: ownerRef.name };
85
+ }
86
+ if (ownerRef.kind === 'ReplicaSet') {
87
+ const depRef = ownerRS?.metadata?.ownerReferences?.find((r: any) => r.kind === 'Deployment');
88
+ if (depRef) {
89
+ return { rootKind: 'Deployment' as const, rootName: depRef.name };
90
+ }
91
+ }
92
+ return null;
93
+ }, [ownerRef, ownerRS]);
94
+
95
+ // Watch for restarts on this single pod
96
+ usePodRestartWatcher(decodedClusterId, pod ? [pod] : undefined);
97
+
98
+ if (isLoading) return <LoadingSkeleton />;
99
+ if (error) return <ErrorDisplay error={error} onRetry={() => mutate()} clusterId={clusterId} />;
100
+ if (!pod) return null;
101
+
102
+ const metadata = pod.metadata || {};
103
+ const spec = pod.spec || {};
104
+ const status = pod.status || {};
105
+ const containers = spec.containers || [];
106
+ const containerStatuses = status.containerStatuses || [];
107
+ const labels = metadata.labels || {};
108
+ const phase = metadata.deletionTimestamp ? 'Terminating' : status.phase || 'Unknown';
109
+
110
+ const handleDelete = async () => {
111
+ setDeleting(true);
112
+ try {
113
+ await apiClient.delete(`/api/clusters/${clusterId}/resources/${namespace}/pods/${podName}`);
114
+ toast.success(`${podName} deleted`);
115
+ router.back();
116
+ } catch (err: any) {
117
+ toast.error(`Delete failed: ${err.message}`);
118
+ } finally { setDeleting(false); setDeleteOpen(false); }
119
+ };
120
+
121
+ return (
122
+ <div className="flex flex-col gap-4 p-6">
123
+ <div className="flex items-center gap-3">
124
+ <Button variant="ghost" size="icon" onClick={() => router.back()}>
125
+ <ArrowLeft className="h-4 w-4" />
126
+ </Button>
127
+ <div className="flex-1">
128
+ <div className="flex items-center gap-2">
129
+ <h1 className="text-2xl font-bold">{podName}</h1>
130
+ <StatusBadge status={phase} />
131
+ </div>
132
+ <p className="text-sm text-muted-foreground">Pod in {namespace}</p>
133
+ </div>
134
+ <div className="flex gap-2">
135
+ {containers.map((c: any) => c.name).slice(0, 1).map((containerName: string) => (
136
+ <div key={containerName} className="flex gap-2">
137
+ <Button
138
+ variant="outline"
139
+ size="sm"
140
+ onClick={() => addTab({
141
+ id: `logs-${podName}-${containerName}`,
142
+ type: 'logs',
143
+ title: `Logs: ${podName}`,
144
+ clusterId,
145
+ namespace,
146
+ podName,
147
+ container: containerName,
148
+ })}
149
+ >
150
+ <ScrollText className="h-4 w-4 mr-1" />
151
+ Logs
152
+ </Button>
153
+ <Button
154
+ variant="outline"
155
+ size="sm"
156
+ onClick={() => addTab({
157
+ id: `exec-${podName}-${containerName}`,
158
+ type: 'exec',
159
+ title: `Exec: ${podName}`,
160
+ clusterId,
161
+ namespace,
162
+ podName,
163
+ container: containerName,
164
+ })}
165
+ >
166
+ <Terminal className="h-4 w-4 mr-1" />
167
+ Exec
168
+ </Button>
169
+ </div>
170
+ ))}
171
+ <PodWatchButton clusterId={decodedClusterId} namespace={namespace} podName={podName} />
172
+ <Button variant="destructive" size="sm" onClick={() => setDeleteOpen(true)}>
173
+ <Trash2 className="h-4 w-4 mr-1" />Delete
174
+ </Button>
175
+ </div>
176
+ </div>
177
+
178
+ <Tabs defaultValue="overview">
179
+ <TabsList>
180
+ <TabsTrigger value="overview">Overview</TabsTrigger>
181
+ <TabsTrigger value="yaml">YAML</TabsTrigger>
182
+ </TabsList>
183
+
184
+ <TabsContent value="overview" className="space-y-4 mt-4">
185
+ {/* Resource Tree */}
186
+ {treeRoot && (
187
+ <PodResourceTree
188
+ clusterId={decodedClusterId}
189
+ namespace={namespace}
190
+ rootKind={treeRoot.rootKind}
191
+ rootName={treeRoot.rootName}
192
+ focusPodName={podName}
193
+ />
194
+ )}
195
+
196
+ {/* Metrics */}
197
+ <PodMetricsCharts
198
+ clusterId={decodedClusterId}
199
+ namespace={namespace}
200
+ podName={podName}
201
+ nodeName={spec.nodeName}
202
+ />
203
+
204
+ {/* Pod Info compact */}
205
+ <div className="rounded-md border overflow-hidden">
206
+ <table className="w-full text-xs">
207
+ <tbody>
208
+ <tr className="border-b">
209
+ <td className="px-3 py-1.5 text-muted-foreground font-medium w-[100px]">Node</td>
210
+ <td className="px-3 py-1.5 font-mono">{spec.nodeName || '-'}</td>
211
+ <td className="px-3 py-1.5 text-muted-foreground font-medium w-[100px]">Pod IP</td>
212
+ <td className="px-3 py-1.5 font-mono">{status.podIP || '-'}</td>
213
+ <td className="px-3 py-1.5 text-muted-foreground font-medium w-[100px]">Host IP</td>
214
+ <td className="px-3 py-1.5 font-mono">{status.hostIP || '-'}</td>
215
+ </tr>
216
+ <tr className="border-b">
217
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">QoS</td>
218
+ <td className="px-3 py-1.5">{status.qosClass || '-'}</td>
219
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">Restart</td>
220
+ <td className="px-3 py-1.5">{spec.restartPolicy || '-'}</td>
221
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">Age</td>
222
+ <td className="px-3 py-1.5"><AgeDisplay timestamp={metadata.creationTimestamp} /></td>
223
+ </tr>
224
+ <tr className="border-b">
225
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">SA</td>
226
+ <td className="px-3 py-1.5 font-mono" colSpan={5}>{spec.serviceAccountName || '-'}</td>
227
+ </tr>
228
+ {Object.entries(labels).length > 0 && (
229
+ <tr className="border-b">
230
+ <td className="px-3 py-1.5 text-muted-foreground font-medium align-top">Labels</td>
231
+ <td className="px-3 py-1.5" colSpan={5}>
232
+ <div className="flex flex-wrap gap-1">
233
+ {Object.entries(labels).map(([k, v]) => (
234
+ <Badge key={k} variant="secondary" className="text-[10px] font-mono font-normal py-0 h-5">{k}={v as string}</Badge>
235
+ ))}
236
+ </div>
237
+ </td>
238
+ </tr>
239
+ )}
240
+ {(status.conditions || []).length > 0 && (
241
+ <tr>
242
+ <td className="px-3 py-1.5 text-muted-foreground font-medium align-top">Status</td>
243
+ <td className="px-3 py-1.5" colSpan={5}>
244
+ <div className="flex flex-wrap gap-1.5">
245
+ {(status.conditions || []).map((c: any, i: number) => (
246
+ <Badge key={i} variant={c.status === 'True' ? 'default' : 'outline'} className="text-[10px] font-normal py-0 h-5 gap-1">
247
+ {c.type}
248
+ {c.status !== 'True' && <span className="text-muted-foreground">({c.reason || c.status})</span>}
249
+ </Badge>
250
+ ))}
251
+ </div>
252
+ </td>
253
+ </tr>
254
+ )}
255
+ </tbody>
256
+ </table>
257
+ </div>
258
+
259
+ {/* Containers */}
260
+ <div className="space-y-3">
261
+ <h3 className="text-sm font-semibold">Containers ({containers.length})</h3>
262
+ {containers.map((ctr: any, idx: number) => {
263
+ const cs = containerStatuses.find((s: any) => s.name === ctr.name);
264
+ const stateKey = cs?.state ? Object.keys(cs.state)[0] : 'unknown';
265
+ const envVars = ctr.env || [];
266
+ const envFrom = ctr.envFrom || [];
267
+
268
+ return (
269
+ <div key={idx} className="rounded-md border overflow-hidden">
270
+ {/* Container header */}
271
+ <div className="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b">
272
+ <div className="flex items-center gap-2">
273
+ <h4 className="text-sm font-semibold">{ctr.name}</h4>
274
+ <StatusBadge status={stateKey === 'running' ? 'Running' : stateKey === 'waiting' ? 'Pending' : stateKey === 'terminated' ? 'Failed' : 'Unknown'} />
275
+ </div>
276
+ <div className="flex items-center gap-3">
277
+ {cs && <span className="text-xs text-muted-foreground">Restarts: {cs.restartCount || 0}</span>}
278
+ <Button variant="outline" size="sm" className="h-7 text-xs" onClick={() => setEnvDrawerOpen(true)}>
279
+ <KeyRound className="h-3.5 w-3.5 mr-1" />
280
+ Env ({(ctr.env || []).length + (ctr.envFrom || []).length})
281
+ </Button>
282
+ </div>
283
+ </div>
284
+
285
+ <div className="p-4 space-y-3">
286
+ {/* Container details table */}
287
+ <div className="rounded border overflow-hidden">
288
+ <table className="w-full text-xs">
289
+ <tbody>
290
+ <tr className="border-b">
291
+ <td className="px-3 py-1.5 text-muted-foreground font-medium w-[120px]">Image</td>
292
+ <td className="px-3 py-1.5 font-mono break-all">{ctr.image}</td>
293
+ </tr>
294
+ {ctr.ports && ctr.ports.length > 0 && (
295
+ <tr className="border-b">
296
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">Ports</td>
297
+ <td className="px-3 py-1.5">
298
+ <div className="flex flex-wrap gap-2">
299
+ {ctr.ports.map((p: any, pi: number) => (
300
+ <div key={pi} className="inline-flex items-center gap-1.5">
301
+ <Badge variant="outline" className="font-mono text-[11px] font-normal">
302
+ {p.containerPort}/{p.protocol || 'TCP'}
303
+ {p.name && <span className="text-muted-foreground ml-1">({p.name})</span>}
304
+ </Badge>
305
+ <PortForwardBtn
306
+ clusterId={decodedClusterId}
307
+ namespace={namespace}
308
+ resourceType="pod"
309
+ resourceName={podName}
310
+ port={p.containerPort}
311
+ />
312
+ </div>
313
+ ))}
314
+ </div>
315
+ </td>
316
+ </tr>
317
+ )}
318
+ {ctr.command && (
319
+ <tr className="border-b">
320
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">Command</td>
321
+ <td className="px-3 py-1.5 font-mono">{ctr.command.join(' ')}</td>
322
+ </tr>
323
+ )}
324
+ {ctr.args && (
325
+ <tr className="border-b">
326
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">Args</td>
327
+ <td className="px-3 py-1.5 font-mono break-all">{ctr.args.join(' ')}</td>
328
+ </tr>
329
+ )}
330
+ {(ctr.resources?.requests || ctr.resources?.limits) && (
331
+ <tr className="border-b">
332
+ <td className="px-3 py-1.5 text-muted-foreground font-medium">Resources</td>
333
+ <td className="px-3 py-1.5">
334
+ <div className="flex gap-4">
335
+ {ctr.resources?.requests && (
336
+ <span>
337
+ <span className="text-muted-foreground">Req: </span>
338
+ CPU {ctr.resources.requests.cpu || '-'}, Mem {ctr.resources.requests.memory || '-'}
339
+ </span>
340
+ )}
341
+ {ctr.resources?.limits && (
342
+ <span>
343
+ <span className="text-muted-foreground">Lim: </span>
344
+ CPU {ctr.resources.limits.cpu || '-'}, Mem {ctr.resources.limits.memory || '-'}
345
+ </span>
346
+ )}
347
+ </div>
348
+ </td>
349
+ </tr>
350
+ )}
351
+ {ctr.volumeMounts && ctr.volumeMounts.length > 0 && (
352
+ <tr className="border-b">
353
+ <td className="px-3 py-1.5 text-muted-foreground font-medium align-top">Mounts</td>
354
+ <td className="px-3 py-1.5">
355
+ <div className="space-y-0.5">
356
+ {ctr.volumeMounts.map((vm: any, vi: number) => (
357
+ <div key={vi} className="font-mono">
358
+ <span>{vm.mountPath}</span>
359
+ <span className="text-muted-foreground ml-1">← {vm.name}</span>
360
+ {vm.readOnly && <Badge variant="outline" className="ml-1 text-[9px] py-0 h-3.5">RO</Badge>}
361
+ </div>
362
+ ))}
363
+ </div>
364
+ </td>
365
+ </tr>
366
+ )}
367
+ </tbody>
368
+ </table>
369
+ </div>
370
+
371
+ </div>
372
+ </div>
373
+ );
374
+ })}
375
+ </div>
376
+
377
+ </TabsContent>
378
+
379
+ <TabsContent value="yaml" className="mt-4">
380
+ <YamlEditor
381
+ data={pod}
382
+ apiUrl={`/api/clusters/${clusterId}/resources/${namespace}/pods/${podName}`}
383
+ onSaved={() => mutate()}
384
+ portForwardContext={{
385
+ clusterId: decodedClusterId,
386
+ namespace,
387
+ resourceType: 'pod',
388
+ resourceName: podName,
389
+ }}
390
+ />
391
+ </TabsContent>
392
+ </Tabs>
393
+
394
+ <ConfirmDialog open={deleteOpen} onOpenChange={setDeleteOpen} title={`Delete ${podName}?`} description={`This will delete the pod "${podName}". If managed by a controller, it will be recreated.`} confirmLabel="Delete" variant="destructive" onConfirm={handleDelete} loading={deleting} />
395
+
396
+ {/* Env Variables Drawer */}
397
+ <Sheet open={envDrawerOpen} onOpenChange={setEnvDrawerOpen}>
398
+ <SheetContent side="right" className="w-full sm:max-w-2xl overflow-y-auto p-0">
399
+ <SheetHeader className="px-4 pt-4 pb-2">
400
+ <SheetTitle className="text-base">Environment Variables — {podName}</SheetTitle>
401
+ </SheetHeader>
402
+ <div className="space-y-4 px-4 pb-6 overflow-x-hidden">
403
+ {containers.map((ctr: any, idx: number) => {
404
+ const envVars = ctr.env || [];
405
+ const envFrom = ctr.envFrom || [];
406
+ return (
407
+ <div key={idx} className="space-y-1">
408
+ <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">{ctr.name}</h3>
409
+ <div className="rounded border overflow-hidden">
410
+ <table className="w-full text-xs" style={{ tableLayout: 'fixed' }}>
411
+ <colgroup>
412
+ <col style={{ width: '35%' }} />
413
+ <col style={{ width: '65%' }} />
414
+ </colgroup>
415
+ <thead className="bg-muted/50">
416
+ <tr>
417
+ <th className="px-3 py-1.5 text-left font-medium">Name</th>
418
+ <th className="px-3 py-1.5 text-left font-medium">Value / Source</th>
419
+ </tr>
420
+ </thead>
421
+ <tbody>
422
+ {envVars.map((env: any, ei: number) => (
423
+ <tr key={ei} className="border-t hover:bg-muted/30">
424
+ <td className="px-3 py-1 font-mono font-medium text-blue-600 dark:text-blue-400 truncate" title={env.name}>{env.name}</td>
425
+ <td className="px-3 py-1 font-mono overflow-hidden">
426
+ <div className="break-all">
427
+ <EnvValueCell env={env} clusterId={decodedClusterId} namespace={namespace} />
428
+ </div>
429
+ </td>
430
+ </tr>
431
+ ))}
432
+ <EnvFromRows envFrom={envFrom} clusterId={decodedClusterId} namespace={namespace} />
433
+ <MountedSecretRows volumes={spec.volumes || []} volumeMounts={ctr.volumeMounts || []} clusterId={decodedClusterId} namespace={namespace} />
434
+ </tbody>
435
+ </table>
436
+ </div>
437
+ </div>
438
+ );
439
+ })}
440
+
441
+ {/* Linked Secrets & ConfigMaps */}
442
+ <PodLinkedSecrets podSpec={spec} clusterId={decodedClusterId} namespace={namespace} />
443
+ </div>
444
+ </SheetContent>
445
+ </Sheet>
446
+ </div>
447
+ );
448
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceListPage } from '@/components/resources/resource-list-page';
3
+ export default function PodsPage() {
4
+ return <ResourceListPage resourceType="pods" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceDetailPage } from '@/components/resources/resource-detail-page';
3
+ export default function PVCDetailPage() {
4
+ return <ResourceDetailPage resourceType="pvcs" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceListPage } from '@/components/resources/resource-list-page';
3
+ export default function PVCsPage() {
4
+ return <ResourceListPage resourceType="pvcs" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceDetailPage } from '@/components/resources/resource-detail-page';
3
+ export default function ReplicaSetDetailPage() {
4
+ return <ResourceDetailPage resourceType="replicasets" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceListPage } from '@/components/resources/resource-list-page';
3
+ export default function ReplicaSetsPage() {
4
+ return <ResourceListPage resourceType="replicasets" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceDetailPage } from '@/components/resources/resource-detail-page';
3
+ export default function RoleBindingDetailPage() {
4
+ return <ResourceDetailPage resourceType="rolebindings" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceListPage } from '@/components/resources/resource-list-page';
3
+ export default function RoleBindingsPage() {
4
+ return <ResourceListPage resourceType="rolebindings" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceDetailPage } from '@/components/resources/resource-detail-page';
3
+ export default function RoleDetailPage() {
4
+ return <ResourceDetailPage resourceType="roles" />;
5
+ }
@@ -0,0 +1,5 @@
1
+ 'use client';
2
+ import { ResourceListPage } from '@/components/resources/resource-list-page';
3
+ export default function RolesPage() {
4
+ return <ResourceListPage resourceType="roles" />;
5
+ }