@skyhook-io/k8s-ui 1.4.2 → 1.4.3

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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/components/resources/ResourcesView.tsx +267 -14
  3. package/src/components/resources/renderers/AWSMachineRenderer.tsx +96 -0
  4. package/src/components/resources/renderers/AWSMachineTemplateRenderer.tsx +39 -0
  5. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +49 -0
  6. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +174 -0
  7. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +89 -0
  8. package/src/components/resources/renderers/AzureMachineRenderer.tsx +41 -0
  9. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +71 -0
  10. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +106 -0
  11. package/src/components/resources/renderers/CAPIClusterClassRenderer.tsx +156 -0
  12. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +240 -0
  13. package/src/components/resources/renderers/CAPIKubeadmConfigRenderer.tsx +97 -0
  14. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +124 -0
  15. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +128 -0
  16. package/src/components/resources/renderers/CAPIMachineDrainRuleRenderer.tsx +38 -0
  17. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +146 -0
  18. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +92 -0
  19. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +170 -0
  20. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +101 -0
  21. package/src/components/resources/renderers/GCPMachineRenderer.tsx +53 -0
  22. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +75 -0
  23. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +102 -0
  24. package/src/components/resources/renderers/aws-capi-cells.tsx +117 -0
  25. package/src/components/resources/renderers/azure-capi-cells.tsx +83 -0
  26. package/src/components/resources/renderers/capi-cells.tsx +158 -0
  27. package/src/components/resources/renderers/gcp-capi-cells.tsx +68 -0
  28. package/src/components/resources/renderers/index.ts +25 -0
  29. package/src/components/resources/resource-utils-aws-capi.ts +211 -0
  30. package/src/components/resources/resource-utils-azure-capi.ts +123 -0
  31. package/src/components/resources/resource-utils-capi.ts +305 -0
  32. package/src/components/resources/resource-utils-gcp-capi.ts +130 -0
  33. package/src/components/shared/ResourceRendererDispatch.tsx +97 -2
  34. package/src/components/topology/K8sResourceNode.tsx +8 -0
  35. package/src/components/topology/TopologyControls.tsx +16 -0
  36. package/src/components/topology/TopologyFilterSidebar.tsx +15 -0
  37. package/src/components/topology/TopologyGraph.tsx +2 -1
  38. package/src/components/topology/layout.ts +11 -0
  39. package/src/components/topology/topology.css +29 -0
  40. package/src/components/ui/Badge.tsx +10 -0
  41. package/src/components/ui/drawer-components.tsx +54 -17
  42. package/src/types/core.ts +18 -2
  43. package/src/utils/api-resources.ts +4 -0
  44. package/src/utils/resource-icons.ts +35 -0
@@ -0,0 +1,240 @@
1
+ import { useState } from 'react'
2
+ import { Server, Globe, Network, Layers, Download, CheckCircle, AlertCircle } from 'lucide-react'
3
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
4
+ import { kindToPlural } from '../../../utils/navigation'
5
+ import { formatAge } from '../resource-utils'
6
+ import { getClusterStatus, getClusterClass, getClusterVersion, getClusterEndpoint, getProviderFromInfraKind, parseCAPIConditionMessage } from '../resource-utils-capi'
7
+
8
+ interface Props {
9
+ data: any
10
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
11
+ apiBase?: string
12
+ }
13
+
14
+ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '' }: Props) {
15
+ const status = data.status || {}
16
+ const spec = data.spec || {}
17
+ const conditions = status.v1beta2?.conditions || status.conditions || []
18
+
19
+ const clusterStatus = getClusterStatus(data)
20
+ const isFailed = clusterStatus.level === 'unhealthy'
21
+ const readyCond = conditions.find((c: any) => c.type === 'Ready' || c.type === 'Available')
22
+
23
+ const phase = status.phase || 'Unknown'
24
+ const endpoint = getClusterEndpoint(data)
25
+ const className = getClusterClass(data)
26
+ const version = getClusterVersion(data)
27
+ const topology = spec.topology || {}
28
+
29
+ // v1beta2 replica fields
30
+ const cpReady = status.controlPlane?.readyReplicas ?? status.controlPlane?.replicas
31
+ const cpDesired = status.controlPlane?.desiredReplicas ?? spec.topology?.controlPlane?.replicas
32
+ const wReady = status.workers?.readyReplicas ?? status.workers?.replicas
33
+ const wDesired = status.workers?.desiredReplicas
34
+
35
+ // Refs
36
+ const controlPlaneRef = spec.controlPlaneRef || {}
37
+ const infrastructureRef = spec.infrastructureRef || {}
38
+
39
+ const ns = data.metadata?.namespace || ''
40
+ const name = data.metadata?.name || ''
41
+
42
+ const [downloadState, setDownloadState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
43
+ const [downloadError, setDownloadError] = useState('')
44
+ const [connectState, setConnectState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
45
+ const [connectError, setConnectError] = useState('')
46
+
47
+ const handleConnectToCluster = async () => {
48
+ setConnectState('loading')
49
+ setConnectError('')
50
+ try {
51
+ const res = await fetch(`${apiBase}/api/capi/clusters/${encodeURIComponent(ns)}/${encodeURIComponent(name)}/connect`, { method: 'POST' })
52
+ if (!res.ok) {
53
+ const body = await res.json().catch(() => ({ error: res.statusText }))
54
+ throw new Error(body.error || `HTTP ${res.status}`)
55
+ }
56
+ await res.json()
57
+ setConnectState('success')
58
+ // The page will reload as the context switch triggers a reconnect
59
+ setTimeout(() => window.location.reload(), 1500)
60
+ } catch (err: any) {
61
+ setConnectError(err.message || 'Failed to connect to workload cluster')
62
+ setConnectState('error')
63
+ setTimeout(() => setConnectState('idle'), 5000)
64
+ }
65
+ }
66
+
67
+ const handleDownloadKubeconfig = async () => {
68
+ setDownloadState('loading')
69
+ setDownloadError('')
70
+ try {
71
+ const res = await fetch(`${apiBase}/api/capi/clusters/${encodeURIComponent(ns)}/${encodeURIComponent(name)}/kubeconfig`)
72
+ if (!res.ok) {
73
+ const body = await res.json().catch(() => ({ error: res.statusText }))
74
+ throw new Error(body.error || `HTTP ${res.status}`)
75
+ }
76
+ const blob = await res.blob()
77
+ const url = URL.createObjectURL(blob)
78
+ try {
79
+ const a = document.createElement('a')
80
+ a.href = url
81
+ a.download = `${name}-kubeconfig.yaml`
82
+ a.click()
83
+ } finally {
84
+ URL.revokeObjectURL(url)
85
+ }
86
+ setDownloadState('success')
87
+ setTimeout(() => setDownloadState('idle'), 3000)
88
+ } catch (err: any) {
89
+ setDownloadError(err.message || 'Failed to download kubeconfig')
90
+ setDownloadState('error')
91
+ setTimeout(() => setDownloadState('idle'), 5000)
92
+ }
93
+ }
94
+
95
+ return (
96
+ <>
97
+ {spec.paused && (
98
+ <AlertBanner
99
+ variant="warning"
100
+ title="Cluster Paused"
101
+ message="Reconciliation is paused. Infrastructure and machine changes will not be applied until resumed."
102
+ />
103
+ )}
104
+
105
+ {isFailed && (() => {
106
+ const msg = readyCond?.message || `Cluster is in ${phase} state.`
107
+ const items = parseCAPIConditionMessage(msg)
108
+ return <AlertBanner variant="error" title="Cluster Not Ready" items={items || undefined} message={items ? undefined : msg} />
109
+ })()}
110
+
111
+ {/* Overview */}
112
+ <Section title="Overview" icon={Globe}>
113
+ <PropertyList>
114
+ <Property label="Phase" value={phase} />
115
+ <Property label="Version" value={version} />
116
+ {infrastructureRef.kind && <Property label="Provider" value={getProviderFromInfraKind(infrastructureRef.kind)} />}
117
+ {className !== '-' && <Property label="Cluster Class" value={className} />}
118
+ {endpoint !== '-' && <Property label="Control Plane Endpoint" value={endpoint} />}
119
+ {readyCond?.lastTransitionTime && (
120
+ <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
121
+ )}
122
+ </PropertyList>
123
+ </Section>
124
+
125
+ {/* Kubeconfig Actions */}
126
+ <div className="px-3 py-2 flex items-center gap-2">
127
+ <button
128
+ onClick={handleConnectToCluster}
129
+ disabled={connectState === 'loading' || connectState === 'success'}
130
+ className="btn-brand flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md"
131
+ >
132
+ {connectState === 'loading' && <Globe className="w-3.5 h-3.5 animate-pulse" />}
133
+ {connectState === 'success' && <CheckCircle className="w-3.5 h-3.5" />}
134
+ {connectState === 'error' && <AlertCircle className="w-3.5 h-3.5" />}
135
+ {connectState === 'idle' && <Globe className="w-3.5 h-3.5" />}
136
+ {connectState === 'loading' ? 'Connecting...' : connectState === 'success' ? 'Connected — reloading...' : 'Connect to Cluster'}
137
+ </button>
138
+ <button
139
+ onClick={handleDownloadKubeconfig}
140
+ disabled={downloadState === 'loading'}
141
+ className="btn-brand-muted flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md"
142
+ >
143
+ {downloadState === 'loading' ? <Download className="w-3.5 h-3.5 animate-pulse" /> : <Download className="w-3.5 h-3.5" />}
144
+ {downloadState === 'loading' ? 'Downloading...' : downloadState === 'success' ? 'Downloaded' : 'Download'}
145
+ </button>
146
+ </div>
147
+ {connectState === 'error' && connectError && (
148
+ <p className="text-xs text-red-500 px-3 pb-2">{connectError}</p>
149
+ )}
150
+ {downloadState === 'error' && downloadError && (
151
+ <p className="text-xs text-red-500 px-3 pb-2">{downloadError}</p>
152
+ )}
153
+
154
+ {/* Replicas */}
155
+ {(cpDesired != null || wDesired != null) && (
156
+ <Section title="Replicas" icon={Server}>
157
+ <PropertyList>
158
+ {cpDesired != null && (
159
+ <Property label="Control Plane" value={`${cpReady ?? 0}/${cpDesired} ready`} />
160
+ )}
161
+ {wDesired != null && (
162
+ <Property label="Workers" value={`${wReady ?? 0}/${wDesired} ready`} />
163
+ )}
164
+ </PropertyList>
165
+ </Section>
166
+ )}
167
+
168
+ {/* References */}
169
+ <Section title="References" icon={Network}>
170
+ <PropertyList>
171
+ {controlPlaneRef.kind && (
172
+ <Property
173
+ label="Control Plane"
174
+ value={
175
+ <ResourceLink
176
+ name={controlPlaneRef.name}
177
+ kind={kindToPlural(controlPlaneRef.kind)}
178
+ namespace={controlPlaneRef.namespace || data.metadata?.namespace}
179
+ group={controlPlaneRef.apiVersion?.split('/')?.[0]}
180
+ label={`${controlPlaneRef.kind}/${controlPlaneRef.name}`}
181
+ onNavigate={onNavigate}
182
+ />
183
+ }
184
+ />
185
+ )}
186
+ {infrastructureRef.kind && (
187
+ <Property label="Infrastructure" value={
188
+ <ResourceLink
189
+ name={infrastructureRef.name}
190
+ kind={kindToPlural(infrastructureRef.kind)}
191
+ namespace={infrastructureRef.namespace || data.metadata?.namespace}
192
+ group={infrastructureRef.apiVersion?.split('/')?.[0]}
193
+ label={`${infrastructureRef.kind}/${infrastructureRef.name}`}
194
+ onNavigate={onNavigate}
195
+ />
196
+ } />
197
+ )}
198
+ </PropertyList>
199
+ </Section>
200
+
201
+ {/* Topology (ClusterClass-based) */}
202
+ {topology.class && (
203
+ <Section title="Topology" icon={Layers}>
204
+ <PropertyList>
205
+ <Property label="Class" value={topology.class} />
206
+ {topology.version && <Property label="Version" value={topology.version} />}
207
+ {topology.controlPlane?.replicas != null && (
208
+ <Property label="CP Replicas" value={String(topology.controlPlane.replicas)} />
209
+ )}
210
+ </PropertyList>
211
+ {topology.workers?.machineDeployments?.length > 0 && (
212
+ <div className="mt-2">
213
+ <div className="text-xs font-medium text-theme-text-secondary mb-1">Worker MachineDeployments</div>
214
+ <table className="w-full text-xs">
215
+ <thead>
216
+ <tr className="text-theme-text-tertiary">
217
+ <th className="text-left font-medium py-1">Class</th>
218
+ <th className="text-left font-medium py-1">Name</th>
219
+ <th className="text-left font-medium py-1">Replicas</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody>
223
+ {topology.workers.machineDeployments.map((md: any, i: number) => (
224
+ <tr key={i} className="border-t border-theme-border">
225
+ <td className="py-1 text-theme-text-secondary">{md.class}</td>
226
+ <td className="py-1 text-theme-text-secondary">{md.name || '-'}</td>
227
+ <td className="py-1 text-theme-text-secondary">{md.replicas ?? '-'}</td>
228
+ </tr>
229
+ ))}
230
+ </tbody>
231
+ </table>
232
+ </div>
233
+ )}
234
+ </Section>
235
+ )}
236
+
237
+ <ConditionsSection conditions={conditions} />
238
+ </>
239
+ )
240
+ }
@@ -0,0 +1,97 @@
1
+ import { Settings, FileText } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
3
+
4
+ interface Props {
5
+ data: any
6
+ }
7
+
8
+ export function CAPIKubeadmConfigRenderer({ data }: Props) {
9
+ const status = data.status || {}
10
+ const spec = data.spec || {}
11
+ const conditions = status.v1beta2?.conditions || status.conditions || []
12
+
13
+ const clusterConfig = spec.clusterConfiguration || {}
14
+ const files = spec.files || []
15
+ const preKubeadmCommands = spec.preKubeadmCommands || []
16
+ const postKubeadmCommands = spec.postKubeadmCommands || []
17
+ const certSANs = clusterConfig.certSANs || []
18
+
19
+ return (
20
+ <>
21
+ <Section title="Overview" icon={Settings}>
22
+ <PropertyList>
23
+ {status.ready != null && <Property label="Ready" value={status.ready ? 'Yes' : 'No'} />}
24
+ {status.dataSecretName && <Property label="Data Secret" value={status.dataSecretName} />}
25
+ </PropertyList>
26
+ </Section>
27
+
28
+ {certSANs.length > 0 && (
29
+ <Section title="Cert SANs" icon={Settings}>
30
+ <div className="flex flex-wrap gap-1">
31
+ {certSANs.map((san: string, i: number) => (
32
+ <span key={i} className="badge badge-sm bg-theme-surface text-theme-text-secondary border-theme-border">{san}</span>
33
+ ))}
34
+ </div>
35
+ </Section>
36
+ )}
37
+
38
+ {/* API Server Extra Args */}
39
+ {clusterConfig.apiServer?.extraArgs && Object.keys(clusterConfig.apiServer.extraArgs).length > 0 && (
40
+ <Section title="API Server Extra Args" icon={Settings} defaultExpanded={false}>
41
+ <PropertyList>
42
+ {Object.entries(clusterConfig.apiServer.extraArgs).map(([key, value]) => (
43
+ <Property key={key} label={key} value={String(value)} />
44
+ ))}
45
+ </PropertyList>
46
+ </Section>
47
+ )}
48
+
49
+ {/* Files */}
50
+ {files.length > 0 && (
51
+ <Section title="Files" icon={FileText} defaultExpanded={false}>
52
+ <table className="w-full text-xs">
53
+ <thead>
54
+ <tr className="text-theme-text-tertiary">
55
+ <th className="text-left font-medium py-1">Path</th>
56
+ <th className="text-left font-medium py-1">Owner</th>
57
+ <th className="text-left font-medium py-1">Permissions</th>
58
+ </tr>
59
+ </thead>
60
+ <tbody>
61
+ {files.map((f: any, i: number) => (
62
+ <tr key={i} className="border-t border-theme-border">
63
+ <td className="py-1 text-theme-text-secondary font-mono text-[10px]">{f.path}</td>
64
+ <td className="py-1 text-theme-text-secondary">{f.owner || '-'}</td>
65
+ <td className="py-1 text-theme-text-secondary">{f.permissions || '-'}</td>
66
+ </tr>
67
+ ))}
68
+ </tbody>
69
+ </table>
70
+ </Section>
71
+ )}
72
+
73
+ {/* Commands */}
74
+ {preKubeadmCommands.length > 0 && (
75
+ <Section title="Pre-Kubeadm Commands" icon={Settings} defaultExpanded={false}>
76
+ <div className="text-xs font-mono text-theme-text-secondary space-y-0.5">
77
+ {preKubeadmCommands.map((cmd: string, i: number) => (
78
+ <div key={i} className="truncate">{cmd}</div>
79
+ ))}
80
+ </div>
81
+ </Section>
82
+ )}
83
+
84
+ {postKubeadmCommands.length > 0 && (
85
+ <Section title="Post-Kubeadm Commands" icon={Settings} defaultExpanded={false}>
86
+ <div className="text-xs font-mono text-theme-text-secondary space-y-0.5">
87
+ {postKubeadmCommands.map((cmd: string, i: number) => (
88
+ <div key={i} className="truncate">{cmd}</div>
89
+ ))}
90
+ </div>
91
+ </Section>
92
+ )}
93
+
94
+ <ConditionsSection conditions={conditions} />
95
+ </>
96
+ )
97
+ }
@@ -0,0 +1,124 @@
1
+ import { Server, Shield, Settings } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { kindToPlural } from '../../../utils/navigation'
4
+ import { formatAge } from '../resource-utils'
5
+ import { getKCPStatus, getKCPVersion, getKCPInitialized, getMachineClusterName } from '../resource-utils-capi'
6
+
7
+ interface Props {
8
+ data: any
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
10
+ }
11
+
12
+ export function CAPIKubeadmControlPlaneRenderer({ data, onNavigate }: Props) {
13
+ const status = data.status || {}
14
+ const spec = data.spec || {}
15
+ const conditions = status.v1beta2?.conditions || status.conditions || []
16
+
17
+ const kcpStatus = getKCPStatus(data)
18
+ const isFailed = kcpStatus.level === 'unhealthy'
19
+ const readyCond = conditions.find((c: any) => c.type === 'Ready')
20
+
21
+ const clusterName = getMachineClusterName(data)
22
+ const version = getKCPVersion(data)
23
+ const initialized = getKCPInitialized(data)
24
+ const desired = spec.replicas ?? 0
25
+ const ready = status.readyReplicas ?? 0
26
+ const available = status.availableReplicas ?? status.readyReplicas ?? 0
27
+ const upToDate = status.upToDateReplicas ?? status.updatedReplicas ?? 0
28
+ const updateStrategy = spec.rolloutStrategy || spec.upgradeAfter ? 'RollingUpdate' : undefined
29
+ const machineTemplate = spec.machineTemplate || {}
30
+ const infraRef = machineTemplate.infrastructureRef || {}
31
+ const lastRemediation = status.lastRemediation
32
+ const kubeadmConfigSpec = spec.kubeadmConfigSpec || {}
33
+
34
+ return (
35
+ <>
36
+ {isFailed && (
37
+ <AlertBanner
38
+ variant="error"
39
+ title="Control Plane Not Ready"
40
+ message={readyCond?.message || 'KubeadmControlPlane is not ready.'}
41
+ />
42
+ )}
43
+
44
+ {/* Overview */}
45
+ <Section title="Overview" icon={Shield}>
46
+ <PropertyList>
47
+ <Property label="Cluster" value={clusterName} />
48
+ <Property label="Version" value={version} />
49
+ <Property label="Initialized" value={initialized ? 'Yes' : 'No'} />
50
+ {updateStrategy && <Property label="Update Strategy" value={updateStrategy} />}
51
+ {readyCond?.lastTransitionTime && (
52
+ <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
53
+ )}
54
+ </PropertyList>
55
+ </Section>
56
+
57
+ {/* Replicas */}
58
+ <Section title="Replicas" icon={Server}>
59
+ <PropertyList>
60
+ <Property label="Desired" value={String(desired)} />
61
+ <Property label="Ready" value={String(ready)} />
62
+ <Property label="Available" value={String(available)} />
63
+ <Property label="Up-to-date" value={String(upToDate)} />
64
+ </PropertyList>
65
+ </Section>
66
+
67
+ {/* Machine Template */}
68
+ {infraRef.kind && (
69
+ <Section title="Machine Template" icon={Settings}>
70
+ <PropertyList>
71
+ <Property label="Infrastructure" value={
72
+ <ResourceLink
73
+ name={infraRef.name}
74
+ kind={kindToPlural(infraRef.kind)}
75
+ namespace={infraRef.namespace || data.metadata?.namespace}
76
+ group={infraRef.apiVersion?.split('/')?.[0]}
77
+ label={`${infraRef.kind}/${infraRef.name}`}
78
+ onNavigate={onNavigate}
79
+ />
80
+ } />
81
+ {machineTemplate.nodeDrainTimeout && (
82
+ <Property label="Node Drain Timeout" value={machineTemplate.nodeDrainTimeout} />
83
+ )}
84
+ {machineTemplate.nodeVolumeDetachTimeout && (
85
+ <Property label="Volume Detach Timeout" value={machineTemplate.nodeVolumeDetachTimeout} />
86
+ )}
87
+ {machineTemplate.nodeDeletionTimeout && (
88
+ <Property label="Deletion Timeout" value={machineTemplate.nodeDeletionTimeout} />
89
+ )}
90
+ </PropertyList>
91
+ </Section>
92
+ )}
93
+
94
+ {/* KubeadmConfig Spec highlights */}
95
+ {(kubeadmConfigSpec.clusterConfiguration?.certSANs?.length > 0 || kubeadmConfigSpec.clusterConfiguration?.apiServer) && (
96
+ <Section title="Kubeadm Config" icon={Settings}>
97
+ <PropertyList>
98
+ {kubeadmConfigSpec.clusterConfiguration?.certSANs && (
99
+ <Property label="Cert SANs" value={kubeadmConfigSpec.clusterConfiguration.certSANs.join(', ')} />
100
+ )}
101
+ </PropertyList>
102
+ </Section>
103
+ )}
104
+
105
+ {/* Remediation */}
106
+ {lastRemediation && (
107
+ <Section title="Last Remediation" icon={Shield}>
108
+ <PropertyList>
109
+ <Property label="Machine" value={lastRemediation.machine || '-'} />
110
+ <Property label="Retry Count" value={String(lastRemediation.retryCount ?? 0)} />
111
+ {lastRemediation.timestamp && <Property label="Time" value={lastRemediation.timestamp} />}
112
+ </PropertyList>
113
+ </Section>
114
+ )}
115
+
116
+ {/* Owned Machines hint */}
117
+ <div className="px-3 py-1.5 text-xs text-theme-text-tertiary">
118
+ Machines with label <code className="bg-theme-surface px-1 py-0.5 rounded text-[10px] font-mono select-all">cluster.x-k8s.io/control-plane-name={data.metadata?.name}</code>
119
+ </div>
120
+
121
+ <ConditionsSection conditions={conditions} />
122
+ </>
123
+ )
124
+ }
@@ -0,0 +1,128 @@
1
+ import { Server, Settings } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
+ import { kindToPlural } from '../../../utils/navigation'
4
+ import { formatAge } from '../resource-utils'
5
+ import { getMachineDeploymentStatus, getMachineDeploymentVersion, getMachineClusterName } from '../resource-utils-capi'
6
+
7
+ interface Props {
8
+ data: any
9
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
10
+ }
11
+
12
+ export function CAPIMachineDeploymentRenderer({ data, onNavigate }: Props) {
13
+ const status = data.status || {}
14
+ const spec = data.spec || {}
15
+ const conditions = status.v1beta2?.conditions || status.conditions || []
16
+
17
+ const mdStatus = getMachineDeploymentStatus(data)
18
+ const isFailed = mdStatus.level === 'unhealthy'
19
+ const readyCond = conditions.find((c: any) => c.type === 'Ready')
20
+
21
+ const phase = status.phase || 'Unknown'
22
+ const clusterName = getMachineClusterName(data)
23
+ const version = getMachineDeploymentVersion(data)
24
+ const desired = spec.replicas ?? 0
25
+ const ready = status.readyReplicas ?? 0
26
+ const available = status.availableReplicas ?? 0
27
+ const upToDate = status.upToDateReplicas ?? status.updatedReplicas ?? 0
28
+ const strategy = spec.strategy || {}
29
+ const paused = spec.paused || false
30
+ const infraRef = spec.template?.spec?.infrastructureRef || {}
31
+ const bootstrapRef = spec.template?.spec?.bootstrap?.configRef || {}
32
+
33
+ return (
34
+ <>
35
+ {paused && (
36
+ <AlertBanner
37
+ variant="warning"
38
+ title="MachineDeployment Paused"
39
+ message="Reconciliation is paused. Changes will not be applied until resumed."
40
+ />
41
+ )}
42
+
43
+ {isFailed && (
44
+ <AlertBanner
45
+ variant="error"
46
+ title="MachineDeployment Not Ready"
47
+ message={readyCond?.message || `MachineDeployment is in ${phase} state.`}
48
+ />
49
+ )}
50
+
51
+ {/* Overview */}
52
+ <Section title="Overview" icon={Server}>
53
+ <PropertyList>
54
+ <Property label="Phase" value={phase} />
55
+ <Property label="Cluster" value={clusterName} />
56
+ <Property label="Version" value={version} />
57
+ {readyCond?.lastTransitionTime && (
58
+ <Property label="Since" value={formatAge(readyCond.lastTransitionTime)} />
59
+ )}
60
+ </PropertyList>
61
+ </Section>
62
+
63
+ {/* Replicas */}
64
+ <Section title="Replicas" icon={Server}>
65
+ <PropertyList>
66
+ <Property label="Desired" value={String(desired)} />
67
+ <Property label="Ready" value={String(ready)} />
68
+ <Property label="Available" value={String(available)} />
69
+ <Property label="Up-to-date" value={String(upToDate)} />
70
+ </PropertyList>
71
+ </Section>
72
+
73
+ {/* Strategy */}
74
+ {strategy.type && (
75
+ <Section title="Strategy" icon={Settings}>
76
+ <PropertyList>
77
+ <Property label="Type" value={strategy.type} />
78
+ {strategy.rollingUpdate?.maxSurge != null && (
79
+ <Property label="Max Surge" value={String(strategy.rollingUpdate.maxSurge)} />
80
+ )}
81
+ {strategy.rollingUpdate?.maxUnavailable != null && (
82
+ <Property label="Max Unavailable" value={String(strategy.rollingUpdate.maxUnavailable)} />
83
+ )}
84
+ </PropertyList>
85
+ </Section>
86
+ )}
87
+
88
+ {/* Template References */}
89
+ {(infraRef.kind || bootstrapRef.kind) && (
90
+ <Section title="Machine Template" icon={Settings}>
91
+ <PropertyList>
92
+ {infraRef.kind && (
93
+ <Property label="Infrastructure" value={
94
+ <ResourceLink
95
+ name={infraRef.name}
96
+ kind={kindToPlural(infraRef.kind)}
97
+ namespace={infraRef.namespace || data.metadata?.namespace}
98
+ group={infraRef.apiVersion?.split('/')?.[0]}
99
+ label={`${infraRef.kind}/${infraRef.name}`}
100
+ onNavigate={onNavigate}
101
+ />
102
+ } />
103
+ )}
104
+ {bootstrapRef.kind && (
105
+ <Property label="Bootstrap" value={
106
+ <ResourceLink
107
+ name={bootstrapRef.name}
108
+ kind={kindToPlural(bootstrapRef.kind)}
109
+ namespace={bootstrapRef.namespace || data.metadata?.namespace}
110
+ group={bootstrapRef.apiVersion?.split('/')?.[0]}
111
+ label={`${bootstrapRef.kind}/${bootstrapRef.name}`}
112
+ onNavigate={onNavigate}
113
+ />
114
+ } />
115
+ )}
116
+ </PropertyList>
117
+ </Section>
118
+ )}
119
+
120
+ {/* Owned Machines hint */}
121
+ <div className="px-3 py-1.5 text-xs text-theme-text-tertiary">
122
+ Machines with label <code className="bg-theme-surface px-1 py-0.5 rounded text-[10px] font-mono select-all">cluster.x-k8s.io/deployment-name={data.metadata?.name}</code>
123
+ </div>
124
+
125
+ <ConditionsSection conditions={conditions} />
126
+ </>
127
+ )
128
+ }
@@ -0,0 +1,38 @@
1
+ import { Settings } from 'lucide-react'
2
+ import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
3
+
4
+ interface Props {
5
+ data: any
6
+ }
7
+
8
+ export function CAPIMachineDrainRuleRenderer({ data }: Props) {
9
+ const spec = data.spec || {}
10
+ const conditions = data.status?.v1beta2?.conditions || data.status?.conditions || []
11
+
12
+ const machines = spec.machines || []
13
+ const drain = spec.drain || {}
14
+
15
+ return (
16
+ <>
17
+ <Section title="Drain Configuration" icon={Settings}>
18
+ <PropertyList>
19
+ {drain.behavior && <Property label="Behavior" value={drain.behavior} />}
20
+ {drain.order != null && <Property label="Order" value={String(drain.order)} />}
21
+ </PropertyList>
22
+ </Section>
23
+
24
+ {machines.length > 0 && (
25
+ <Section title="Machine Selectors" icon={Settings}>
26
+ {machines.map((m: any, i: number) => (
27
+ <div key={i} className="text-xs text-theme-text-secondary mb-1">
28
+ {m.clusterName && <span>Cluster: {m.clusterName} </span>}
29
+ {m.namespace && <span>NS: {m.namespace} </span>}
30
+ </div>
31
+ ))}
32
+ </Section>
33
+ )}
34
+
35
+ <ConditionsSection conditions={conditions} />
36
+ </>
37
+ )
38
+ }