@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.
- package/package.json +1 -1
- package/src/components/resources/ResourcesView.tsx +267 -14
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +96 -0
- package/src/components/resources/renderers/AWSMachineTemplateRenderer.tsx +39 -0
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +49 -0
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +174 -0
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +89 -0
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +41 -0
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +71 -0
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +106 -0
- package/src/components/resources/renderers/CAPIClusterClassRenderer.tsx +156 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +240 -0
- package/src/components/resources/renderers/CAPIKubeadmConfigRenderer.tsx +97 -0
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +124 -0
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +128 -0
- package/src/components/resources/renderers/CAPIMachineDrainRuleRenderer.tsx +38 -0
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +146 -0
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +92 -0
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +170 -0
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +101 -0
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +53 -0
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +75 -0
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +102 -0
- package/src/components/resources/renderers/aws-capi-cells.tsx +117 -0
- package/src/components/resources/renderers/azure-capi-cells.tsx +83 -0
- package/src/components/resources/renderers/capi-cells.tsx +158 -0
- package/src/components/resources/renderers/gcp-capi-cells.tsx +68 -0
- package/src/components/resources/renderers/index.ts +25 -0
- package/src/components/resources/resource-utils-aws-capi.ts +211 -0
- package/src/components/resources/resource-utils-azure-capi.ts +123 -0
- package/src/components/resources/resource-utils-capi.ts +305 -0
- package/src/components/resources/resource-utils-gcp-capi.ts +130 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +97 -2
- package/src/components/topology/K8sResourceNode.tsx +8 -0
- package/src/components/topology/TopologyControls.tsx +16 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +15 -0
- package/src/components/topology/TopologyGraph.tsx +2 -1
- package/src/components/topology/layout.ts +11 -0
- package/src/components/topology/topology.css +29 -0
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/drawer-components.tsx +54 -17
- package/src/types/core.ts +18 -2
- package/src/utils/api-resources.ts +4 -0
- 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
|
+
}
|