@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,130 @@
1
+ // GCP CAPI Infrastructure Provider utility functions
2
+
3
+ import type { StatusBadge } from './resource-utils'
4
+ import { getCAPIReadyStatus } from './resource-utils-capi'
5
+
6
+ // ============================================================================
7
+ // GCPManagedControlPlane
8
+ // ============================================================================
9
+
10
+ export function getGCPMCPStatus(resource: any): StatusBadge {
11
+ return getCAPIReadyStatus(resource)
12
+ }
13
+
14
+ export function getGCPMCPClusterName(resource: any): string {
15
+ return resource.spec?.clusterName || resource.metadata?.name || '-'
16
+ }
17
+
18
+ export function getGCPMCPProject(resource: any): string {
19
+ return resource.spec?.project || '-'
20
+ }
21
+
22
+ export function getGCPMCPLocation(resource: any): string {
23
+ return resource.spec?.location || '-'
24
+ }
25
+
26
+ export function getGCPMCPVersion(resource: any): string {
27
+ return resource.status?.version || resource.spec?.version || '-'
28
+ }
29
+
30
+ export function getGCPMCPReleaseChannel(resource: any): string {
31
+ return resource.spec?.releaseChannel || '-'
32
+ }
33
+
34
+ export function getGCPMCPAutopilot(resource: any): boolean {
35
+ return !!resource.spec?.enableAutopilot
36
+ }
37
+
38
+ export function getGCPMCPEndpoint(resource: any): string {
39
+ const host = resource.spec?.endpoint?.host || resource.spec?.controlPlaneEndpoint?.host
40
+ const port = resource.spec?.endpoint?.port || resource.spec?.controlPlaneEndpoint?.port
41
+ if (host) return port && port !== 443 ? `${host}:${port}` : host
42
+ return '-'
43
+ }
44
+
45
+ // ============================================================================
46
+ // GCPManagedMachinePool
47
+ // ============================================================================
48
+
49
+ export function getGCPMMPStatus(resource: any): StatusBadge {
50
+ return getCAPIReadyStatus(resource)
51
+ }
52
+
53
+ export function getGCPMMPNodePoolName(resource: any): string {
54
+ return resource.spec?.nodePoolName || resource.metadata?.name || '-'
55
+ }
56
+
57
+ export function getGCPMMPMachineType(resource: any): string {
58
+ return resource.spec?.machineType || resource.spec?.instanceType || 'e2-medium'
59
+ }
60
+
61
+ export function getGCPMMPDiskInfo(resource: any): string {
62
+ const diskType = resource.spec?.diskType || 'pd-standard'
63
+ const diskSize = resource.spec?.diskSizeGb || resource.spec?.diskSizeGB || 100
64
+ return `${diskType} ${diskSize}GB`
65
+ }
66
+
67
+ export function getGCPMMPScaling(resource: any): { min: number; max: number; autoscaling: boolean } {
68
+ const scaling = resource.spec?.scaling || {}
69
+ return {
70
+ min: scaling.minCount ?? 0,
71
+ max: scaling.maxCount ?? 0,
72
+ autoscaling: scaling.enableAutoscaling !== false,
73
+ }
74
+ }
75
+
76
+ export function getGCPMMPReplicas(resource: any): string {
77
+ const ready = resource.status?.replicas ?? 0
78
+ return String(ready)
79
+ }
80
+
81
+ export function getGCPMMPImageType(resource: any): string {
82
+ return resource.spec?.imageType || '-'
83
+ }
84
+
85
+ // ============================================================================
86
+ // GCPMachine
87
+ // ============================================================================
88
+
89
+ export function getGCPMachineStatus(resource: any): StatusBadge {
90
+ return getCAPIReadyStatus(resource)
91
+ }
92
+
93
+ export function getGCPMachineInstanceType(resource: any): string {
94
+ return resource.spec?.instanceType || '-'
95
+ }
96
+
97
+ export function getGCPMachineZone(resource: any): string {
98
+ return resource.spec?.zone || resource.spec?.failureDomain || '-'
99
+ }
100
+
101
+ export function getGCPMachineInstanceID(resource: any): string {
102
+ return resource.status?.instanceID || resource.spec?.providerID || '-'
103
+ }
104
+
105
+ // ============================================================================
106
+ // GCPMachineTemplate
107
+ // ============================================================================
108
+
109
+ export function getGCPMTInstanceType(resource: any): string {
110
+ return resource.spec?.template?.spec?.instanceType || '-'
111
+ }
112
+
113
+ // ============================================================================
114
+ // GCPManagedCluster
115
+ // ============================================================================
116
+
117
+ export function getGCPManagedClusterStatus(resource: any): StatusBadge {
118
+ return getCAPIReadyStatus(resource)
119
+ }
120
+
121
+ export function getGCPManagedClusterEndpoint(resource: any): string {
122
+ const host = resource.spec?.controlPlaneEndpoint?.host
123
+ const port = resource.spec?.controlPlaneEndpoint?.port
124
+ if (host) return port && port !== 443 ? `${host}:${port}` : host
125
+ return '-'
126
+ }
127
+
128
+ export function getGCPManagedClusterProject(resource: any): string {
129
+ return resource.spec?.project || '-'
130
+ }
@@ -66,6 +66,10 @@ import {
66
66
  getRevisionStatus,
67
67
  } from '../resources/resource-utils-knative'
68
68
  import { getHTTPProxyStatus } from '../resources/resource-utils-contour'
69
+ import { getClusterStatus as getCAPIClusterStatus, getMachineStatus, getMachineDeploymentStatus, getMachineSetStatus, getMachinePoolStatus, getKCPStatus, getClusterClassStatus, getMachineHealthCheckStatus } from '../resources/resource-utils-capi'
70
+ import { getAWSMCPStatus, getAWSMMPStatus, getAWSMachineStatus, getAWSManagedClusterStatus } from '../resources/resource-utils-aws-capi'
71
+ import { getGCPMCPStatus, getGCPMMPStatus, getGCPMachineStatus, getGCPManagedClusterStatus } from '../resources/resource-utils-gcp-capi'
72
+ import { getAzureMCPStatus, getAzureMMPStatus, getAzureMachineStatus, getAzureManagedClusterStatus } from '../resources/resource-utils-azure-capi'
69
73
  import {
70
74
  PodRenderer,
71
75
  WorkloadRenderer,
@@ -174,9 +178,31 @@ import {
174
178
  LeaseRenderer,
175
179
  TraefikIngressRouteRenderer,
176
180
  ContourHTTPProxyRenderer,
181
+ CAPIClusterRenderer,
182
+ CAPIMachineRenderer,
183
+ CAPIMachineDeploymentRenderer,
184
+ CAPIKubeadmControlPlaneRenderer,
185
+ CAPIMachineSetRenderer,
186
+ CAPIMachinePoolRenderer,
187
+ CAPIClusterClassRenderer,
188
+ CAPIMachineHealthCheckRenderer,
189
+ CAPIMachineDrainRuleRenderer,
190
+ CAPIKubeadmConfigRenderer,
191
+ AWSManagedControlPlaneRenderer,
192
+ AWSManagedMachinePoolRenderer,
193
+ AWSMachineRenderer,
194
+ AWSMachineTemplateRenderer,
195
+ AWSManagedClusterRenderer,
196
+ GCPManagedControlPlaneRenderer,
197
+ GCPManagedMachinePoolRenderer,
198
+ GCPMachineRenderer,
199
+ AzureManagedControlPlaneRenderer,
200
+ AzureManagedMachinePoolRenderer,
201
+ AzureMachineRenderer,
177
202
  } from '../resources/renderers'
178
203
  import type { SelectedResource, Relationships, ResourceRef, SecretCertificateInfo, ResolvedEnvFrom } from '../../types'
179
204
  import type { CopyHandler } from '../ui/drawer-components'
205
+ import { AlertBanner } from '../ui/drawer-components'
180
206
 
181
207
  /**
182
208
  * Override map letting each platform consumer swap in its own renderer components.
@@ -243,6 +269,19 @@ const KNOWN_KINDS = new Set([
243
269
  'knativeingresses', 'knativecertificates', 'serverlessservices', 'domainmappings',
244
270
  'ingressroutes', 'ingressroutetcps', 'ingressrouteudps',
245
271
  'httpproxies',
272
+ 'machinedeployments', 'machines', 'machinesets', 'machinepools',
273
+ 'kubeadmcontrolplanes', 'clusterclasses', 'machinehealthchecks',
274
+ 'machinedrainrules', 'kubeadmconfigs', 'kubeadmconfigtemplates',
275
+ 'kubeadmcontrolplanetemplates',
276
+ // AWS CAPI Infrastructure Provider
277
+ 'awsmanagedcontrolplanes', 'awsmanagedmachinepools', 'awsmachines',
278
+ 'awsmachinetemplates', 'awsmanagedclusters',
279
+ // GCP CAPI Infrastructure Provider
280
+ 'gcpmanagedcontrolplanes', 'gcpmanagedmachinepools', 'gcpmachines',
281
+ 'gcpmachinetemplates', 'gcpmanagedclusters',
282
+ // Azure CAPI Infrastructure Provider
283
+ 'azuremanagedcontrolplanes', 'azuremanagedmachinepools', 'azuremachines',
284
+ 'azuremachinetemplates', 'azuremanagedclusters',
246
285
  ])
247
286
 
248
287
  // ============================================================================
@@ -397,9 +436,40 @@ export function ResourceRendererDispatch({
397
436
  {kind === 'externalsecrets' && <ExternalSecretRenderer data={data} onNavigate={onNavigate} />}
398
437
  {kind === 'clusterexternalsecrets' && <ClusterExternalSecretRenderer data={data} onNavigate={onNavigate} />}
399
438
  {(kind === 'secretstores' || kind === 'clustersecretstores') && <SecretStoreRenderer data={data} />}
400
- {kind === 'clusters' && <CNPGClusterRenderer data={data} onNavigate={onNavigate} />}
439
+ {kind === 'clusters' && !data?.apiVersion?.includes('cluster.x-k8s.io') && <CNPGClusterRenderer data={data} onNavigate={onNavigate} />}
440
+ {kind === 'clusters' && data?.apiVersion?.includes('cluster.x-k8s.io') && <CAPIClusterRenderer data={data} onNavigate={onNavigate} />}
401
441
  {kind === 'scheduledbackups' && <CNPGScheduledBackupRenderer data={data} onNavigate={onNavigate} />}
402
442
  {kind === 'poolers' && <CNPGPoolerRenderer data={data} onNavigate={onNavigate} />}
443
+ {/* Cluster API (CAPI) */}
444
+ {'topology.cluster.x-k8s.io/owned' in (data?.metadata?.labels ?? {}) && data?.apiVersion?.includes('cluster.x-k8s.io') && (
445
+ <AlertBanner
446
+ variant="warning"
447
+ title="Topology-controlled — this resource is managed by ClusterClass. Manual changes will be reconciled back."
448
+ />
449
+ )}
450
+ {kind === 'machines' && data?.apiVersion?.includes('cluster.x-k8s.io') && <CAPIMachineRenderer data={data} onNavigate={onNavigate} />}
451
+ {kind === 'machinedeployments' && <CAPIMachineDeploymentRenderer data={data} onNavigate={onNavigate} />}
452
+ {kind === 'machinesets' && data?.apiVersion?.includes('cluster.x-k8s.io') && <CAPIMachineSetRenderer data={data} onNavigate={onNavigate} />}
453
+ {kind === 'machinepools' && <CAPIMachinePoolRenderer data={data} onNavigate={onNavigate} />}
454
+ {kind === 'kubeadmcontrolplanes' && <CAPIKubeadmControlPlaneRenderer data={data} onNavigate={onNavigate} />}
455
+ {kind === 'clusterclasses' && <CAPIClusterClassRenderer data={data} />}
456
+ {kind === 'machinehealthchecks' && <CAPIMachineHealthCheckRenderer data={data} />}
457
+ {kind === 'machinedrainrules' && <CAPIMachineDrainRuleRenderer data={data} />}
458
+ {(kind === 'kubeadmconfigs' || kind === 'kubeadmconfigtemplates') && <CAPIKubeadmConfigRenderer data={data} />}
459
+ {/* AWS CAPI Infrastructure Provider */}
460
+ {kind === 'awsmanagedcontrolplanes' && <AWSManagedControlPlaneRenderer data={data} onNavigate={onNavigate} />}
461
+ {kind === 'awsmanagedmachinepools' && <AWSManagedMachinePoolRenderer data={data} onNavigate={onNavigate} />}
462
+ {kind === 'awsmachines' && <AWSMachineRenderer data={data} onNavigate={onNavigate} />}
463
+ {kind === 'awsmachinetemplates' && <AWSMachineTemplateRenderer data={data} />}
464
+ {kind === 'awsmanagedclusters' && <AWSManagedClusterRenderer data={data} />}
465
+ {/* GCP CAPI Infrastructure Provider */}
466
+ {kind === 'gcpmanagedcontrolplanes' && <GCPManagedControlPlaneRenderer data={data} onNavigate={onNavigate} />}
467
+ {kind === 'gcpmanagedmachinepools' && <GCPManagedMachinePoolRenderer data={data} onNavigate={onNavigate} />}
468
+ {kind === 'gcpmachines' && <GCPMachineRenderer data={data} onNavigate={onNavigate} />}
469
+ {/* Azure CAPI Infrastructure Provider */}
470
+ {kind === 'azuremanagedcontrolplanes' && <AzureManagedControlPlaneRenderer data={data} onNavigate={onNavigate} />}
471
+ {kind === 'azuremanagedmachinepools' && <AzureManagedMachinePoolRenderer data={data} onNavigate={onNavigate} />}
472
+ {kind === 'azuremachines' && <AzureMachineRenderer data={data} onNavigate={onNavigate} />}
403
473
  {kind === 'virtualservices' && <IstioVirtualServiceRenderer data={data} onNavigate={onNavigate} />}
404
474
  {kind === 'destinationrules' && <IstioDestinationRuleRenderer data={data} onNavigate={onNavigate} />}
405
475
  {kind === 'serviceentries' && <IstioServiceEntryRenderer data={data} />}
@@ -546,7 +616,32 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
546
616
  if (k === 'clusterexternalsecrets') return getClusterExternalSecretStatus(data)
547
617
  if (k === 'secretstores') return getSecretStoreStatus(data)
548
618
  if (k === 'clustersecretstores') return getClusterSecretStoreStatus(data)
549
- if (k === 'clusters') return getCNPGClusterStatus(data)
619
+ if (k === 'clusters') {
620
+ if (data.apiVersion?.includes('cluster.x-k8s.io')) return getCAPIClusterStatus(data)
621
+ return getCNPGClusterStatus(data)
622
+ }
623
+ if (k === 'machines' && data.apiVersion?.includes('cluster.x-k8s.io')) return getMachineStatus(data)
624
+ if (k === 'machinedeployments') return getMachineDeploymentStatus(data)
625
+ if (k === 'machinesets') return getMachineSetStatus(data)
626
+ if (k === 'machinepools') return getMachinePoolStatus(data)
627
+ if (k === 'kubeadmcontrolplanes') return getKCPStatus(data)
628
+ if (k === 'clusterclasses') return getClusterClassStatus(data)
629
+ if (k === 'machinehealthchecks') return getMachineHealthCheckStatus(data)
630
+ // AWS CAPI Infrastructure Provider
631
+ if (k === 'awsmanagedcontrolplanes') return getAWSMCPStatus(data)
632
+ if (k === 'awsmanagedmachinepools') return getAWSMMPStatus(data)
633
+ if (k === 'awsmachines') return getAWSMachineStatus(data)
634
+ if (k === 'awsmanagedclusters') return getAWSManagedClusterStatus(data)
635
+ // GCP CAPI Infrastructure Provider
636
+ if (k === 'gcpmanagedcontrolplanes') return getGCPMCPStatus(data)
637
+ if (k === 'gcpmanagedmachinepools') return getGCPMMPStatus(data)
638
+ if (k === 'gcpmachines') return getGCPMachineStatus(data)
639
+ if (k === 'gcpmanagedclusters') return getGCPManagedClusterStatus(data)
640
+ // Azure CAPI Infrastructure Provider
641
+ if (k === 'azuremanagedcontrolplanes') return getAzureMCPStatus(data)
642
+ if (k === 'azuremanagedmachinepools') return getAzureMMPStatus(data)
643
+ if (k === 'azuremachines') return getAzureMachineStatus(data)
644
+ if (k === 'azuremanagedclusters') return getAzureManagedClusterStatus(data)
550
645
  if (k === 'scheduledbackups') return getCNPGScheduledBackupStatus(data)
551
646
  if (k === 'poolers') return getCNPGPoolerStatus(data)
552
647
  if (k === 'virtualservices') return getVirtualServiceStatus(data)
@@ -135,6 +135,14 @@ export const NODE_DIMENSIONS: Record<NodeKind, { width: number; height: number }
135
135
  TLSOption: { width: 280, height: 56 },
136
136
  TLSStore: { width: 280, height: 56 },
137
137
  HTTPProxy: { width: 280, height: 56 }, // Contour
138
+ CAPICluster: { width: 280, height: 56 }, // Cluster API
139
+ MachineDeployment: { width: 300, height: 56 },
140
+ MachineSet: { width: 280, height: 56 },
141
+ Machine: { width: 260, height: 56 },
142
+ MachinePool: { width: 280, height: 56 },
143
+ KubeadmControlPlane: { width: 300, height: 56 },
144
+ ClusterClass: { width: 280, height: 56 },
145
+ MachineHealthCheck: { width: 300, height: 56 },
138
146
  }
139
147
 
140
148
 
@@ -9,6 +9,8 @@ interface TopologyControlsProps {
9
9
  showNoGrouping?: boolean
10
10
  showPolicyEffect?: boolean
11
11
  onShowPolicyEffectChange?: (show: boolean) => void
12
+ /** Show the "Fleet" button (CAPI cluster management view) */
13
+ showFleetMode?: boolean
12
14
  }
13
15
 
14
16
  export function TopologyControls({
@@ -19,6 +21,7 @@ export function TopologyControls({
19
21
  showNoGrouping = true,
20
22
  showPolicyEffect = false,
21
23
  onShowPolicyEffectChange,
24
+ showFleetMode = false,
22
25
  }: TopologyControlsProps) {
23
26
  return (
24
27
  <div className="absolute top-4 right-4 z-10 flex items-center gap-2">
@@ -76,6 +79,19 @@ export function TopologyControls({
76
79
  >
77
80
  Traffic
78
81
  </button>
82
+ {showFleetMode && (
83
+ <button
84
+ onClick={() => onViewModeChange('fleet')}
85
+ className={`px-2.5 py-1 text-xs rounded-md transition-colors ${
86
+ viewMode === 'fleet'
87
+ ? 'bg-skyhook-600 text-white'
88
+ : 'text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated'
89
+ }`}
90
+ title="Cluster API fleet view — shows only CAPI resources and nodes"
91
+ >
92
+ Fleet
93
+ </button>
94
+ )}
79
95
  </div>
80
96
  </div>
81
97
  )
@@ -80,6 +80,21 @@ const RESOURCE_KINDS: {
80
80
 
81
81
  // Contour
82
82
  { kind: 'HTTPProxy', label: 'HTTPProxy', icon: getTopologyIcon('HTTPProxy'), color: 'text-violet-400', category: 'networking' },
83
+
84
+ // Cluster API
85
+ { kind: 'CAPICluster', label: 'CAPI Cluster', icon: getTopologyIcon('CAPICluster'), color: 'text-indigo-400', category: 'custom' },
86
+ { kind: 'MachineDeployment', label: 'Machine Deploy', icon: getTopologyIcon('MachineDeployment'), color: 'text-indigo-400', category: 'custom' },
87
+ { kind: 'MachineSet', label: 'MachineSet', icon: getTopologyIcon('MachineSet'), color: 'text-indigo-400', category: 'custom' },
88
+ { kind: 'Machine', label: 'Machine', icon: getTopologyIcon('Machine'), color: 'text-indigo-400', category: 'custom' },
89
+ { kind: 'MachinePool', label: 'MachinePool', icon: getTopologyIcon('MachinePool'), color: 'text-indigo-400', category: 'custom' },
90
+ { kind: 'KubeadmControlPlane', label: 'Control Plane', icon: getTopologyIcon('KubeadmControlPlane'), color: 'text-indigo-400', category: 'custom' },
91
+ { kind: 'ClusterClass', label: 'ClusterClass', icon: getTopologyIcon('ClusterClass'), color: 'text-indigo-400', category: 'custom' },
92
+ { kind: 'MachineHealthCheck', label: 'Health Check', icon: getTopologyIcon('MachineHealthCheck'), color: 'text-indigo-400', category: 'custom' },
93
+ // AWS CAPI infrastructure
94
+ { kind: 'AWSManagedControlPlane', label: 'AWS Control Plane', icon: getTopologyIcon('AWSManagedControlPlane'), color: 'text-amber-400', category: 'custom' },
95
+ { kind: 'AWSManagedMachinePool', label: 'AWS Machine Pool', icon: getTopologyIcon('AWSManagedMachinePool'), color: 'text-amber-400', category: 'custom' },
96
+ { kind: 'AWSMachine', label: 'AWS Machine', icon: getTopologyIcon('AWSMachine'), color: 'text-amber-400', category: 'custom' },
97
+ { kind: 'EKSConfig', label: 'EKS Config', icon: getTopologyIcon('EKSConfig'), color: 'text-amber-400', category: 'custom' },
83
98
  ]
84
99
 
85
100
  const CATEGORIES = [
@@ -484,7 +484,8 @@ export function TopologyGraph({
484
484
  // Smart default: start all namespace groups as chips for large clusters.
485
485
  // Fires once per topology lifecycle (reset on context switch). Returns early
486
486
  // so the re-render with groupLevels set computes the actual layout.
487
- if (!hasAppliedSmartDefaultRef.current && groupLevels.size === 0 && groupingMode === 'namespace' && !hideGroupHeader) {
487
+ // Fleet mode: skip collapse CAPI resources are already filtered, always show expanded
488
+ if (!hasAppliedSmartDefaultRef.current && groupLevels.size === 0 && groupingMode === 'namespace' && !hideGroupHeader && viewMode !== 'fleet') {
488
489
  const uniqueNamespaces = new Set(workingNodes.map(n => n.data.namespace as string).filter(Boolean))
489
490
  if (uniqueNamespaces.size > LARGE_CLUSTER_NS_THRESHOLD) {
490
491
  hasAppliedSmartDefaultRef.current = true
@@ -764,6 +764,17 @@ const KIND_PRIORITY: Record<string, number> = {
764
764
  'Middleware': 3, 'MiddlewareTCP': 3, 'ServersTransport': 4, 'ServersTransportTCP': 4,
765
765
  'TLSOption': 4, 'TLSStore': 4, 'HTTPProxy': 1,
766
766
  'Application': 1, 'Kustomization': 1, 'HelmRelease': 1, 'GitRepository': 2,
767
+ 'CAPICluster': 1, 'MachineDeployment': 2, 'MachineSet': 3, 'Machine': 4,
768
+ 'MachinePool': 2, 'KubeadmControlPlane': 1, 'ClusterClass': 1, 'MachineHealthCheck': 5,
769
+ // AWS CAPI infrastructure provider
770
+ 'AWSManagedControlPlane': 2, 'AWSManagedMachinePool': 3, 'AWSMachine': 5,
771
+ 'AWSMachineTemplate': 5, 'AWSManagedCluster': 2, 'EKSConfig': 5,
772
+ // GCP CAPI infrastructure provider
773
+ 'GCPManagedControlPlane': 2, 'GCPManagedMachinePool': 3, 'GCPMachine': 5,
774
+ 'GCPMachineTemplate': 5, 'GCPManagedCluster': 2,
775
+ // Azure CAPI infrastructure provider
776
+ 'AzureManagedControlPlane': 2, 'AzureManagedMachinePool': 3, 'AzureMachine': 5,
777
+ 'AzureMachineTemplate': 5, 'AzureManagedCluster': 2,
767
778
  }
768
779
 
769
780
  // Compute grid dimensions from workload card count
@@ -97,6 +97,35 @@
97
97
  .topology-icon-nodeclaim { background: #6366f1; }
98
98
  .topology-icon-nodeclass { background: #6366f1; }
99
99
  .topology-icon-httpproxy { background: #7c3aed; }
100
+ .topology-icon-capicluster { background: #4338ca; }
101
+ .topology-icon-machinedeployment { background: #5b4fcf; }
102
+ .topology-icon-machineset { background: #6d5fd6; }
103
+ .topology-icon-machine { background: #7c6fdd; }
104
+ .topology-icon-machinepool { background: #5b4fcf; }
105
+ .topology-icon-kubeadmcontrolplane { background: #4338ca; }
106
+ .topology-icon-clusterclass { background: #3b2fba; }
107
+ .topology-icon-machinehealthcheck { background: #8b7fe4; }
108
+ /* AWS CAPI infrastructure provider */
109
+ .topology-icon-awsmanagedcontrolplane { background: #d97706; }
110
+ .topology-icon-awsmanagedmachinepool { background: #d97706; }
111
+ .topology-icon-awsmachine { background: #f59e0b; }
112
+ .topology-icon-awsmachinetemplate { background: #f59e0b; }
113
+ .topology-icon-awsmanagedcluster { background: #d97706; }
114
+ .topology-icon-eksconfig { background: #f59e0b; }
115
+ .topology-icon-eksconfigtemplate { background: #f59e0b; }
116
+
117
+ /* GCP CAPI infrastructure provider */
118
+ .topology-icon-gcpmanagedcontrolplane { background: #0d9488; }
119
+ .topology-icon-gcpmanagedmachinepool { background: #0d9488; }
120
+ .topology-icon-gcpmachine { background: #14b8a6; }
121
+ .topology-icon-gcpmachinetemplate { background: #14b8a6; }
122
+ .topology-icon-gcpmanagedcluster { background: #0d9488; }
123
+ /* Azure CAPI infrastructure provider */
124
+ .topology-icon-azuremanagedcontrolplane { background: #2563eb; }
125
+ .topology-icon-azuremanagedmachinepool { background: #2563eb; }
126
+ .topology-icon-azuremachine { background: #3b82f6; }
127
+ .topology-icon-azuremachinetemplate { background: #3b82f6; }
128
+ .topology-icon-azuremanagedcluster { background: #2563eb; }
100
129
 
101
130
  /* Background dots - uses CSS variable for theme awareness */
102
131
  .react-flow__background pattern circle {
@@ -116,6 +116,16 @@ const KIND: Record<string, string> = {
116
116
  // Contour
117
117
  HTTPProxy: 'bg-violet-100 text-violet-700 border-violet-300 dark:bg-violet-950/50 dark:text-violet-400 dark:border-violet-700/40',
118
118
 
119
+ // Cluster API
120
+ CAPICluster: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
121
+ MachineDeployment: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
122
+ MachineSet: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
123
+ Machine: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
124
+ MachinePool: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
125
+ KubeadmControlPlane: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
126
+ ClusterClass: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
127
+ MachineHealthCheck: 'bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40',
128
+
119
129
  // Events
120
130
  Event: 'bg-slate-100 text-slate-700 border-slate-300 dark:bg-slate-950/50 dark:text-slate-400 dark:border-slate-700/40',
121
131
  }
@@ -1,7 +1,7 @@
1
1
  import { useState } from 'react'
2
2
  import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { formatAge } from '../resources/resource-utils'
4
+ import { formatAge, formatDuration } from '../resources/resource-utils'
5
5
  import { Tooltip } from './Tooltip'
6
6
  import { getKindColorClass } from '../ui/Badge'
7
7
 
@@ -185,23 +185,60 @@ export function Property({ label, value, copyable, onCopy, copied }: PropertyPro
185
185
  export function ConditionsSection({ conditions }: { conditions?: any[] }) {
186
186
  if (!conditions || conditions.length === 0) return null
187
187
 
188
+ // Sort by lastTransitionTime (most recent first), then alphabetically for ties
189
+ const sorted = [...conditions].sort((a: any, b: any) => {
190
+ const tA = a.lastTransitionTime ? new Date(a.lastTransitionTime).getTime() : 0
191
+ const tB = b.lastTransitionTime ? new Date(b.lastTransitionTime).getTime() : 0
192
+ if (tA !== tB) return tB - tA
193
+ return (a.type || '').localeCompare(b.type || '')
194
+ })
195
+
196
+ const failCount = sorted.filter((c: any) => c.status === 'False').length
197
+
188
198
  return (
189
- <Section title={`Conditions (${conditions.length})`} defaultExpanded={conditions.length <= 4}>
190
- <div className="space-y-2">
191
- {conditions.map((cond: any) => (
192
- <div key={cond.type} className="flex items-start gap-2 text-sm">
193
- <span className={clsx(
194
- 'w-4 h-4 rounded-full flex items-center justify-center text-xs shrink-0 mt-0.5',
195
- cond.status === 'True' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
196
- )}>
197
- {cond.status === 'True' ? '✓' : '✗'}
198
- </span>
199
- <div>
200
- <div className="text-theme-text-primary">{cond.type}</div>
201
- {cond.message && <div className="text-xs text-theme-text-tertiary">{cond.message}</div>}
202
- </div>
203
- </div>
204
- ))}
199
+ <Section
200
+ title={`Conditions (${conditions.length})${failCount > 0 ? ` · ${failCount} failing` : ''}`}
201
+ defaultExpanded={conditions.length <= 6}
202
+ >
203
+ <div className="relative">
204
+ {/* Timeline line sits between timestamp column and dot */}
205
+ <div className="absolute left-[52px] top-2 bottom-2 w-px bg-theme-border" />
206
+
207
+ <div className="space-y-0.5">
208
+ {sorted.map((cond: any) => {
209
+ const isOk = cond.status === 'True'
210
+ const isUnknown = cond.status === 'Unknown'
211
+ const isFail = !isOk && !isUnknown
212
+ return (
213
+ <div key={cond.type} className={clsx(
214
+ 'flex items-start py-1.5 pr-1 text-sm relative',
215
+ isFail && 'border-l-2 border-red-400/60 dark:border-red-500/40'
216
+ )}>
217
+ {/* Timestamp column — fixed width on the left */}
218
+ <div className="w-[48px] shrink-0 text-[10px] text-theme-text-tertiary text-right pr-2 pt-0.5">
219
+ {cond.lastTransitionTime ? formatDuration(Date.now() - new Date(cond.lastTransitionTime).getTime(), true) : ''}
220
+ </div>
221
+ {/* Timeline dot */}
222
+ <span className={clsx(
223
+ 'w-[13px] h-[13px] rounded-full flex items-center justify-center text-[8px] shrink-0 mt-[3px] z-10 ring-2 ring-theme-surface',
224
+ isOk ? 'bg-emerald-500/20 text-emerald-500 dark:bg-emerald-500/30'
225
+ : isUnknown ? 'bg-gray-400/20 text-gray-400 dark:bg-gray-400/30'
226
+ : 'bg-red-500/25 text-red-500 dark:bg-red-500/35'
227
+ )}>
228
+ {isOk ? '✓' : isUnknown ? '?' : '✗'}
229
+ </span>
230
+ {/* Content */}
231
+ <div className="min-w-0 flex-1 pl-2">
232
+ <span className={clsx('font-medium text-[13px]', isOk ? 'text-theme-text-primary' : isUnknown ? 'text-theme-text-secondary' : 'text-red-600 dark:text-red-400')}>{cond.type}</span>
233
+ {cond.reason && cond.reason !== cond.type && (
234
+ <div className="text-[10px] text-theme-text-secondary">{cond.reason}</div>
235
+ )}
236
+ {cond.message && <div className="text-[10.5px] text-theme-text-tertiary break-words leading-relaxed">{cond.message}</div>}
237
+ </div>
238
+ </div>
239
+ )
240
+ })}
241
+ </div>
205
242
  </div>
206
243
  </Section>
207
244
  )
package/src/types/core.ts CHANGED
@@ -100,6 +100,14 @@ export type CoreNodeKind =
100
100
  | 'TLSOption' // Traefik TLSOption
101
101
  | 'TLSStore' // Traefik TLSStore
102
102
  | 'HTTPProxy' // Contour HTTPProxy
103
+ | 'CAPICluster' // Cluster API Cluster
104
+ | 'MachineDeployment' // Cluster API MachineDeployment
105
+ | 'MachineSet' // Cluster API MachineSet
106
+ | 'Machine' // Cluster API Machine
107
+ | 'MachinePool' // Cluster API MachinePool
108
+ | 'KubeadmControlPlane' // Cluster API KubeadmControlPlane
109
+ | 'ClusterClass' // Cluster API ClusterClass
110
+ | 'MachineHealthCheck' // Cluster API MachineHealthCheck
103
111
 
104
112
  // NodeKind can be a core kind or any arbitrary CRD kind string
105
113
  export type NodeKind = CoreNodeKind | (string & {})
@@ -122,6 +130,14 @@ export function displayKind(kind: string): string {
122
130
  TraefikService: 'Traefik Svc',
123
131
  ServersTransport: 'Transport',
124
132
  ServersTransportTCP: 'Transport TCP',
133
+ CAPICluster: 'Cluster',
134
+ MachineDeployment: 'Machine Deploy',
135
+ MachineSet: 'MachineSet',
136
+ Machine: 'Machine',
137
+ MachinePool: 'MachinePool',
138
+ KubeadmControlPlane: 'Control Plane',
139
+ ClusterClass: 'ClusterClass',
140
+ MachineHealthCheck: 'Health Check',
125
141
  }
126
142
  return shortNames[kind] || kind
127
143
  }
@@ -310,8 +326,8 @@ export type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'helm'
310
326
 
311
327
  // Topology view mode (for backwards compatibility, also exported as ViewMode)
312
328
  // NOTE: Must match Go backend constants in internal/topology/types.go
313
- export type TopologyMode = 'resources' | 'traffic'
314
- export type ViewMode = 'resources' | 'traffic'
329
+ export type TopologyMode = 'resources' | 'traffic' | 'fleet'
330
+ export type ViewMode = 'resources' | 'traffic' | 'fleet'
315
331
 
316
332
  // Grouping mode
317
333
  export type GroupingMode = 'none' | 'namespace' | 'app' | 'label'
@@ -156,6 +156,10 @@ export function formatGroupName(group: string): string {
156
156
  'projectcalico.org': 'Calico',
157
157
  'crd.projectcalico.org': 'Calico',
158
158
  'projectcontour.io': 'Contour',
159
+ 'cluster.x-k8s.io': 'Cluster API',
160
+ 'controlplane.cluster.x-k8s.io': 'Cluster API',
161
+ 'bootstrap.cluster.x-k8s.io': 'Cluster API',
162
+ 'infrastructure.cluster.x-k8s.io': 'Cluster API',
159
163
  'ceph.rook.io': 'Rook',
160
164
  'kyverno.io': 'Kyverno',
161
165
  'k8s.nginx.org': 'NGINX',
@@ -61,6 +61,10 @@ import {
61
61
  Lock,
62
62
  ArrowRightLeft,
63
63
 
64
+ // Cluster API
65
+ HeartPulse,
66
+ BookOpen,
67
+
64
68
  // Fallback
65
69
  Puzzle,
66
70
  } from 'lucide-react'
@@ -190,6 +194,37 @@ const KIND_ICON_MAP: Record<string, LucideIcon> = {
190
194
  // Contour
191
195
  httpproxy: Globe,
192
196
 
197
+ // Cluster API
198
+ capicluster: Server,
199
+ machinedeployment: Layers,
200
+ machineset: Layers,
201
+ machine: Cpu,
202
+ machinepool: Layers,
203
+ kubeadmcontrolplane: Shield,
204
+ clusterclass: BookOpen,
205
+ machinehealthcheck: HeartPulse,
206
+
207
+ // AWS CAPI Infrastructure Provider
208
+ awsmanagedcontrolplane: Shield,
209
+ awsmanagedmachinepool: Layers,
210
+ awsmachine: Cpu,
211
+ awsmachinetemplate: Cpu,
212
+ awsmanagedcluster: Server,
213
+
214
+ // GCP CAPI Infrastructure Provider
215
+ gcpmanagedcontrolplane: Shield,
216
+ gcpmanagedmachinepool: Layers,
217
+ gcpmachine: Cpu,
218
+ gcpmachinetemplate: Cpu,
219
+ gcpmanagedcluster: Server,
220
+
221
+ // Azure CAPI Infrastructure Provider
222
+ azuremanagedcontrolplane: Shield,
223
+ azuremanagedmachinepool: Layers,
224
+ azuremachine: Cpu,
225
+ azuremachinetemplate: Cpu,
226
+ azuremanagedcluster: Server,
227
+
193
228
  // Trivy Operator
194
229
  vulnerabilityreport: Shield,
195
230
  configauditreport: ShieldCheck,