@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,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')
|
|
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
|
-
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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,
|