@skyhook-io/k8s-ui 1.8.9 → 1.8.11
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 +5 -4
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
- package/tsconfig.json +2 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { memo } from
|
|
2
|
-
import { Handle, Position } from
|
|
1
|
+
import { memo } from "react";
|
|
2
|
+
import { Handle, Position } from "@xyflow/react";
|
|
3
3
|
import {
|
|
4
4
|
ChevronDown,
|
|
5
5
|
ChevronUp,
|
|
6
|
+
Layers,
|
|
7
|
+
Radio,
|
|
6
8
|
TriangleAlert,
|
|
7
9
|
} from 'lucide-react'
|
|
8
10
|
import { clsx } from 'clsx'
|
|
@@ -12,132 +14,166 @@ import { healthToSeverity, SEVERITY_DOT, SEVERITY_TEXT } from '../../utils/badge
|
|
|
12
14
|
import { workloadHue } from '../../utils/workload-colors'
|
|
13
15
|
import { ownershipOf } from '../../utils/topology-neighborhood'
|
|
14
16
|
import { midTruncate } from '../../utils/format'
|
|
17
|
+
import { getTopologyIcon } from '../../utils/resource-icons'
|
|
15
18
|
import { Tooltip } from '../ui/Tooltip'
|
|
16
19
|
import { AuditBadgeTooltip, type AuditBadgeMessage } from '../audit/AuditBadgeTooltip'
|
|
20
|
+
import argoCdLogo from '../../assets/gitops/argocd.png'
|
|
21
|
+
import fluxLogo from '../../assets/gitops/flux.svg'
|
|
17
22
|
|
|
18
23
|
// Get actionable tooltip content for health issues
|
|
19
24
|
function getIssueTooltip(issue: string | undefined): React.ReactNode {
|
|
20
|
-
if (!issue) return null
|
|
25
|
+
if (!issue) return null;
|
|
21
26
|
|
|
22
|
-
const issueDetails: Record<
|
|
27
|
+
const issueDetails: Record<
|
|
28
|
+
string,
|
|
29
|
+
{ title: string; description: string; action: string }
|
|
30
|
+
> = {
|
|
23
31
|
OOMKilled: {
|
|
24
|
-
title:
|
|
25
|
-
description:
|
|
26
|
-
|
|
32
|
+
title: "Out of Memory (OOMKilled)",
|
|
33
|
+
description:
|
|
34
|
+
"Container exceeded its memory limit and was killed by the kernel.",
|
|
35
|
+
action: "Increase memory limits or optimize memory usage.",
|
|
27
36
|
},
|
|
28
37
|
CrashLoopBackOff: {
|
|
29
|
-
title:
|
|
30
|
-
description:
|
|
31
|
-
|
|
38
|
+
title: "CrashLoopBackOff",
|
|
39
|
+
description:
|
|
40
|
+
"Container is repeatedly crashing and Kubernetes is backing off restarts.",
|
|
41
|
+
action: "Check container logs for crash reason.",
|
|
32
42
|
},
|
|
33
43
|
ImagePullBackOff: {
|
|
34
|
-
title:
|
|
35
|
-
description:
|
|
36
|
-
action:
|
|
44
|
+
title: "ImagePullBackOff",
|
|
45
|
+
description: "Kubernetes cannot pull the container image.",
|
|
46
|
+
action: "Verify image name, tag, and registry credentials.",
|
|
37
47
|
},
|
|
38
48
|
ErrImagePull: {
|
|
39
|
-
title:
|
|
40
|
-
description:
|
|
41
|
-
action:
|
|
49
|
+
title: "Image Pull Error",
|
|
50
|
+
description: "Failed to pull the container image.",
|
|
51
|
+
action: "Check image name and registry access.",
|
|
42
52
|
},
|
|
43
53
|
CreateContainerConfigError: {
|
|
44
|
-
title:
|
|
45
|
-
description:
|
|
46
|
-
|
|
54
|
+
title: "Container Config Error",
|
|
55
|
+
description:
|
|
56
|
+
"Invalid container configuration (e.g., missing ConfigMap/Secret).",
|
|
57
|
+
action: "Verify referenced ConfigMaps and Secrets exist.",
|
|
47
58
|
},
|
|
48
59
|
Pending: {
|
|
49
|
-
title:
|
|
50
|
-
description:
|
|
51
|
-
action:
|
|
60
|
+
title: "Pending",
|
|
61
|
+
description: "Pod is waiting to be scheduled to a node.",
|
|
62
|
+
action:
|
|
63
|
+
"Open the pod to see the scheduler verdict (taints, resources, affinity).",
|
|
52
64
|
},
|
|
53
65
|
FailedScheduling: {
|
|
54
|
-
title:
|
|
55
|
-
description:
|
|
56
|
-
action:
|
|
66
|
+
title: "Scheduling Failed",
|
|
67
|
+
description: "No suitable node found for this pod.",
|
|
68
|
+
action: "Check node resources, taints, tolerations, and affinity rules.",
|
|
57
69
|
},
|
|
58
70
|
Unschedulable: {
|
|
59
|
-
title:
|
|
60
|
-
description:
|
|
61
|
-
action:
|
|
71
|
+
title: "Unschedulable",
|
|
72
|
+
description: "The scheduler tried every node and none fit.",
|
|
73
|
+
action:
|
|
74
|
+
"Open the pod for the decomposed reason — arch/OS mismatch, untolerated taint, insufficient resources, or affinity.",
|
|
62
75
|
},
|
|
63
76
|
QuotaExceeded: {
|
|
64
|
-
title:
|
|
65
|
-
description:
|
|
66
|
-
|
|
77
|
+
title: "ResourceQuota Exceeded",
|
|
78
|
+
description:
|
|
79
|
+
"A namespace ResourceQuota is at its hard limit, so new pods are rejected at admission.",
|
|
80
|
+
action:
|
|
81
|
+
"Open the namespace to see quota usage; raise the quota or free usage.",
|
|
67
82
|
},
|
|
68
83
|
QuotaNearLimit: {
|
|
69
|
-
title:
|
|
70
|
-
description:
|
|
71
|
-
|
|
84
|
+
title: "ResourceQuota Near Limit",
|
|
85
|
+
description:
|
|
86
|
+
"A namespace ResourceQuota is close to its hard limit and will soon block new pods.",
|
|
87
|
+
action: "Open the namespace to see quota usage.",
|
|
72
88
|
},
|
|
73
89
|
IPExhaustion: {
|
|
74
|
-
title:
|
|
75
|
-
description:
|
|
76
|
-
|
|
90
|
+
title: "IP Exhaustion (CNI)",
|
|
91
|
+
description:
|
|
92
|
+
"The pod was scheduled but the CNI could not assign an IP — the node/subnet pool is exhausted.",
|
|
93
|
+
action:
|
|
94
|
+
"Free IPs, scale the subnet/ENI pool, or move the pod to a node with capacity.",
|
|
77
95
|
},
|
|
78
96
|
SandboxCreationFailed: {
|
|
79
|
-
title:
|
|
80
|
-
description:
|
|
81
|
-
action:
|
|
97
|
+
title: "Sandbox Creation Failed",
|
|
98
|
+
description: "The kubelet could not create the pod sandbox.",
|
|
99
|
+
action: "Check kubelet/CNI events on the node.",
|
|
82
100
|
},
|
|
83
101
|
VolumeMount: {
|
|
84
|
-
title:
|
|
85
|
-
description:
|
|
86
|
-
action:
|
|
102
|
+
title: "Volume Mount Failed",
|
|
103
|
+
description: "The pod was scheduled but a volume could not be mounted.",
|
|
104
|
+
action: "Check the PVC/PV binding and the CSI driver on the node.",
|
|
87
105
|
},
|
|
88
106
|
VolumeAttach: {
|
|
89
|
-
title:
|
|
90
|
-
description:
|
|
91
|
-
action:
|
|
107
|
+
title: "Volume Attach Failed",
|
|
108
|
+
description: "A volume could not be attached to the node.",
|
|
109
|
+
action: "Check the CSI driver and cloud-provider attach limits.",
|
|
92
110
|
},
|
|
93
111
|
VolumeMultiAttach: {
|
|
94
|
-
title:
|
|
95
|
-
description:
|
|
96
|
-
|
|
112
|
+
title: "Volume Multi-Attach",
|
|
113
|
+
description:
|
|
114
|
+
"The volume is still attached to another node — a RWO volume cannot attach in two places.",
|
|
115
|
+
action:
|
|
116
|
+
"Wait for the old pod to terminate, or cordon/drain the stale node.",
|
|
97
117
|
},
|
|
98
118
|
PodSecurityViolation: {
|
|
99
|
-
title:
|
|
100
|
-
description:
|
|
101
|
-
|
|
119
|
+
title: "Pod Security Violation",
|
|
120
|
+
description:
|
|
121
|
+
"Pod Security Admission rejected the pod template at admission.",
|
|
122
|
+
action: "Align the pod securityContext with the namespace PSA level.",
|
|
102
123
|
},
|
|
103
124
|
WebhookDenied: {
|
|
104
|
-
title:
|
|
105
|
-
description:
|
|
106
|
-
|
|
125
|
+
title: "Admission Webhook Denied",
|
|
126
|
+
description:
|
|
127
|
+
"A validating/mutating admission webhook rejected pod creation.",
|
|
128
|
+
action: "Check the webhook policy that denied the request.",
|
|
107
129
|
},
|
|
108
130
|
Evicted: {
|
|
109
|
-
title:
|
|
110
|
-
description:
|
|
111
|
-
|
|
131
|
+
title: "Pod Evicted",
|
|
132
|
+
description:
|
|
133
|
+
"Pod was evicted from the node (usually due to resource pressure).",
|
|
134
|
+
action:
|
|
135
|
+
"Check node resource usage and set appropriate resource requests.",
|
|
112
136
|
},
|
|
113
|
-
}
|
|
137
|
+
};
|
|
114
138
|
|
|
115
|
-
const details = issueDetails[issue]
|
|
139
|
+
const details = issueDetails[issue];
|
|
116
140
|
if (!details) {
|
|
117
141
|
return (
|
|
118
142
|
<div className="max-w-xs">
|
|
119
143
|
<div className="font-medium text-red-400">{issue}</div>
|
|
120
|
-
<div className="text-theme-text-secondary text-[10px] mt-1">
|
|
144
|
+
<div className="text-theme-text-secondary text-[10px] mt-1">
|
|
145
|
+
Click to view details
|
|
146
|
+
</div>
|
|
121
147
|
</div>
|
|
122
|
-
)
|
|
148
|
+
);
|
|
123
149
|
}
|
|
124
150
|
|
|
125
151
|
return (
|
|
126
152
|
<div className="max-w-xs">
|
|
127
153
|
<div className="font-medium text-red-400">{details.title}</div>
|
|
128
|
-
<div className="text-theme-text-secondary text-[10px] mt-1">
|
|
154
|
+
<div className="text-theme-text-secondary text-[10px] mt-1">
|
|
155
|
+
{details.description}
|
|
156
|
+
</div>
|
|
129
157
|
<div className="text-blue-400 text-[10px] mt-1.5 border-t border-theme-border pt-1.5">
|
|
130
158
|
💡 {details.action}
|
|
131
159
|
</div>
|
|
132
160
|
</div>
|
|
133
|
-
)
|
|
161
|
+
);
|
|
134
162
|
}
|
|
135
163
|
|
|
136
164
|
// Default dimensions for unknown CRD kinds
|
|
137
|
-
export const DEFAULT_NODE_DIMENSIONS = { width: 260, height: 84 }
|
|
165
|
+
export const DEFAULT_NODE_DIMENSIONS = { width: 260, height: 84 };
|
|
166
|
+
|
|
167
|
+
const NODE_NAME_LENGTH_LIMITS: Partial<Record<NodeKind, number>> = {
|
|
168
|
+
Job: 40,
|
|
169
|
+
Workflow: 48,
|
|
170
|
+
};
|
|
138
171
|
|
|
139
172
|
// Node dimensions for ELK layout - sized for typical K8s resource names
|
|
140
|
-
export const NODE_DIMENSIONS: Record<
|
|
173
|
+
export const NODE_DIMENSIONS: Record<
|
|
174
|
+
NodeKind,
|
|
175
|
+
{ width: number; height: number }
|
|
176
|
+
> = {
|
|
141
177
|
Internet: { width: 120, height: 52 },
|
|
142
178
|
Ingress: { width: 300, height: 84 },
|
|
143
179
|
Gateway: { width: 300, height: 84 },
|
|
@@ -159,9 +195,17 @@ export const NODE_DIMENSIONS: Record<NodeKind, { width: number; height: number }
|
|
|
159
195
|
PodGroup: { width: 200, height: 64 },
|
|
160
196
|
ConfigMap: { width: 180, height: 84 },
|
|
161
197
|
Secret: { width: 180, height: 84 },
|
|
198
|
+
ServiceAccount: { width: 220, height: 84 },
|
|
199
|
+
SealedSecret: { width: 220, height: 84 },
|
|
200
|
+
ServiceMonitor: { width: 240, height: 84 },
|
|
201
|
+
PodMonitor: { width: 240, height: 84 },
|
|
162
202
|
HorizontalPodAutoscaler: { width: 280, height: 84 },
|
|
163
|
-
Job: { width:
|
|
203
|
+
Job: { width: 300, height: 84 },
|
|
164
204
|
CronJob: { width: 200, height: 84 },
|
|
205
|
+
Workflow: { width: 400, height: 84 },
|
|
206
|
+
CronWorkflow: { width: 240, height: 84 },
|
|
207
|
+
WorkflowTemplate: { width: 240, height: 84 },
|
|
208
|
+
ClusterWorkflowTemplate: { width: 260, height: 84 },
|
|
165
209
|
PersistentVolumeClaim: { width: 200, height: 84 },
|
|
166
210
|
Namespace: { width: 180, height: 84 },
|
|
167
211
|
Node: { width: 280, height: 84 },
|
|
@@ -198,183 +242,215 @@ export const NODE_DIMENSIONS: Record<NodeKind, { width: number; height: number }
|
|
|
198
242
|
KubeadmControlPlane: { width: 300, height: 84 },
|
|
199
243
|
ClusterClass: { width: 280, height: 84 },
|
|
200
244
|
MachineHealthCheck: { width: 300, height: 84 },
|
|
201
|
-
}
|
|
202
|
-
|
|
245
|
+
};
|
|
203
246
|
|
|
204
247
|
// Status indicator color (for dot and left bar) - uses centralized severity colors
|
|
205
248
|
function getStatusDotColor(status: HealthStatus): string {
|
|
206
|
-
const severity = healthToSeverity(status)
|
|
207
|
-
return SEVERITY_DOT[severity]
|
|
249
|
+
const severity = healthToSeverity(status);
|
|
250
|
+
return SEVERITY_DOT[severity];
|
|
208
251
|
}
|
|
209
252
|
|
|
210
253
|
// Cached style objects for status states (avoid creating new objects each render)
|
|
211
254
|
const STATUS_STYLES: Record<HealthStatus, React.CSSProperties> = {
|
|
212
255
|
degraded: {
|
|
213
|
-
border:
|
|
214
|
-
backgroundColor:
|
|
256
|
+
border: "2px solid rgb(234 179 8 / 0.6)",
|
|
257
|
+
backgroundColor: "rgb(251 146 60 / 0.12)",
|
|
215
258
|
},
|
|
216
259
|
unhealthy: {
|
|
217
|
-
border:
|
|
218
|
-
backgroundColor:
|
|
260
|
+
border: "2px solid rgb(239 68 68 / 0.7)",
|
|
261
|
+
backgroundColor: "rgb(248 113 113 / 0.15)",
|
|
219
262
|
},
|
|
220
263
|
// neutral = intentional/idle (suspended, scaled-to-0) — sky outline, calm.
|
|
221
264
|
// Distinct from `unknown`/`healthy` which carry no outline.
|
|
222
265
|
neutral: {
|
|
223
|
-
border:
|
|
224
|
-
backgroundColor:
|
|
266
|
+
border: "2px solid rgb(56 189 248 / 0.55)",
|
|
267
|
+
backgroundColor: "rgb(56 189 248 / 0.1)",
|
|
225
268
|
},
|
|
226
269
|
healthy: {},
|
|
227
270
|
unknown: {},
|
|
228
|
-
}
|
|
271
|
+
};
|
|
229
272
|
|
|
230
273
|
function getStatusStyle(status: HealthStatus): React.CSSProperties {
|
|
231
|
-
return STATUS_STYLES[status] || STATUS_STYLES.healthy
|
|
274
|
+
return STATUS_STYLES[status] || STATUS_STYLES.healthy;
|
|
232
275
|
}
|
|
233
276
|
|
|
234
|
-
|
|
235
277
|
// Format subtitle based on node kind. In summary mode the pod tier is
|
|
236
278
|
// collapsed, so workload/service nodes carry a podSummary — append it so the
|
|
237
279
|
// count of pods (and any unhealthy/pending) is still visible without children.
|
|
238
|
-
function getSubtitle(
|
|
239
|
-
|
|
240
|
-
|
|
280
|
+
function getSubtitle(
|
|
281
|
+
kind: NodeKind,
|
|
282
|
+
nodeData: Record<string, unknown>,
|
|
283
|
+
): string {
|
|
284
|
+
const base = baseSubtitle(kind, nodeData);
|
|
285
|
+
const ps = nodeData.podSummary as PodSummary | undefined;
|
|
241
286
|
if (ps && SUMMARY_POD_KINDS.has(kind)) {
|
|
242
|
-
let suffix = `${ps.total} pods
|
|
243
|
-
if (ps.unhealthy > 0) suffix += ` (${ps.unhealthy} unhealthy)
|
|
244
|
-
else if (ps.degraded > 0) suffix += ` (${ps.degraded} pending)
|
|
245
|
-
return base ? `${base} • ${suffix}` : suffix
|
|
287
|
+
let suffix = `${ps.total} pods`;
|
|
288
|
+
if (ps.unhealthy > 0) suffix += ` (${ps.unhealthy} unhealthy)`;
|
|
289
|
+
else if (ps.degraded > 0) suffix += ` (${ps.degraded} pending)`;
|
|
290
|
+
return base ? `${base} • ${suffix}` : suffix;
|
|
246
291
|
}
|
|
247
|
-
return base
|
|
292
|
+
return base;
|
|
248
293
|
}
|
|
249
294
|
|
|
250
295
|
// Kinds that own pods and therefore carry a podSummary in summary mode.
|
|
251
296
|
const SUMMARY_POD_KINDS = new Set<NodeKind>([
|
|
252
|
-
|
|
253
|
-
|
|
297
|
+
"Deployment",
|
|
298
|
+
"StatefulSet",
|
|
299
|
+
"DaemonSet",
|
|
300
|
+
"Rollout",
|
|
301
|
+
"Job",
|
|
302
|
+
"CronJob",
|
|
303
|
+
"Workflow",
|
|
304
|
+
"CronWorkflow",
|
|
305
|
+
"Service",
|
|
306
|
+
]);
|
|
254
307
|
|
|
255
|
-
function baseSubtitle(kind: NodeKind, nodeData: Record<string, unknown>): string {
|
|
308
|
+
export function baseSubtitle(kind: NodeKind, nodeData: Record<string, unknown>): string {
|
|
309
|
+
if (nodeData.deploymentMembership === 'source-only') {
|
|
310
|
+
return `Declared by ${nodeData.deploymentSourceLabel ?? 'deployment source'}`
|
|
311
|
+
}
|
|
256
312
|
switch (kind) {
|
|
257
|
-
case
|
|
258
|
-
case
|
|
259
|
-
case
|
|
260
|
-
case
|
|
261
|
-
case
|
|
313
|
+
case "Deployment":
|
|
314
|
+
case "Rollout":
|
|
315
|
+
case "DaemonSet":
|
|
316
|
+
case "StatefulSet":
|
|
317
|
+
case "ReplicaSet": {
|
|
262
318
|
// Use statusSummary if available (includes issue info like "0/3 OOMKilled")
|
|
263
|
-
const statusSummary = nodeData.statusSummary as string
|
|
319
|
+
const statusSummary = nodeData.statusSummary as string;
|
|
264
320
|
if (statusSummary) {
|
|
265
|
-
return statusSummary
|
|
321
|
+
return statusSummary;
|
|
266
322
|
}
|
|
267
|
-
const ready = nodeData.readyReplicas ?? 0
|
|
268
|
-
const total = nodeData.totalReplicas ?? 0
|
|
269
|
-
return `${ready}/${total} ready
|
|
323
|
+
const ready = nodeData.readyReplicas ?? 0;
|
|
324
|
+
const total = nodeData.totalReplicas ?? 0;
|
|
325
|
+
return `${ready}/${total} ready`;
|
|
270
326
|
}
|
|
271
|
-
case
|
|
327
|
+
case "Application": {
|
|
272
328
|
// ArgoCD Application - show sync and health status
|
|
273
|
-
const syncStatus = (nodeData.syncStatus as string) ||
|
|
274
|
-
const healthStatus = (nodeData.healthStatus as string) ||
|
|
275
|
-
return `${syncStatus} • ${healthStatus}
|
|
329
|
+
const syncStatus = (nodeData.syncStatus as string) || "Unknown";
|
|
330
|
+
const healthStatus = (nodeData.healthStatus as string) || "Unknown";
|
|
331
|
+
return `${syncStatus} • ${healthStatus}`;
|
|
276
332
|
}
|
|
277
|
-
case
|
|
333
|
+
case "Kustomization": {
|
|
278
334
|
// FluxCD Kustomization - show ready status and resource count
|
|
279
|
-
const ready = (nodeData.ready as string) ||
|
|
280
|
-
const resources = nodeData.resourceCount as number
|
|
281
|
-
return resources ? `${ready} • ${resources} resources` : ready
|
|
335
|
+
const ready = (nodeData.ready as string) || "Unknown";
|
|
336
|
+
const resources = nodeData.resourceCount as number;
|
|
337
|
+
return resources ? `${ready} • ${resources} resources` : ready;
|
|
282
338
|
}
|
|
283
|
-
case
|
|
339
|
+
case "HelmRelease": {
|
|
284
340
|
// FluxCD HelmRelease - show ready status and revision
|
|
285
|
-
const ready = (nodeData.ready as string) ||
|
|
286
|
-
const revision = nodeData.revision as number
|
|
287
|
-
return revision ? `${ready} • rev ${revision}` : ready
|
|
341
|
+
const ready = (nodeData.ready as string) || "Unknown";
|
|
342
|
+
const revision = nodeData.revision as number;
|
|
343
|
+
return revision ? `${ready} • rev ${revision}` : ready;
|
|
288
344
|
}
|
|
289
|
-
case
|
|
345
|
+
case "GitRepository": {
|
|
290
346
|
// FluxCD GitRepository - show ready status and branch/revision
|
|
291
|
-
const ready = (nodeData.ready as string) ||
|
|
292
|
-
const branch = nodeData.branch as string
|
|
293
|
-
return branch ? `${ready} • ${branch}` : ready
|
|
347
|
+
const ready = (nodeData.ready as string) || "Unknown";
|
|
348
|
+
const branch = nodeData.branch as string;
|
|
349
|
+
return branch ? `${ready} • ${branch}` : ready;
|
|
294
350
|
}
|
|
295
|
-
case
|
|
296
|
-
return (nodeData.phase as string) ||
|
|
297
|
-
case
|
|
298
|
-
const svcType = (nodeData.type as string) ||
|
|
299
|
-
const port = nodeData.port
|
|
300
|
-
return port ? `${svcType} :${port}` : svcType
|
|
351
|
+
case "Pod":
|
|
352
|
+
return (nodeData.phase as string) || "Unknown";
|
|
353
|
+
case "Service": {
|
|
354
|
+
const svcType = (nodeData.type as string) || "ClusterIP";
|
|
355
|
+
const port = nodeData.port;
|
|
356
|
+
return port ? `${svcType} :${port}` : svcType;
|
|
301
357
|
}
|
|
302
|
-
case
|
|
303
|
-
return (nodeData.hostname as string) ||
|
|
304
|
-
case
|
|
305
|
-
const listeners = nodeData.listenerCount as number || 0
|
|
306
|
-
const addresses = nodeData.addresses as string[]
|
|
307
|
-
const addr = addresses?.length ? addresses[0] :
|
|
308
|
-
return addr
|
|
358
|
+
case "Ingress":
|
|
359
|
+
return (nodeData.hostname as string) || "No host";
|
|
360
|
+
case "Gateway": {
|
|
361
|
+
const listeners = (nodeData.listenerCount as number) || 0;
|
|
362
|
+
const addresses = nodeData.addresses as string[];
|
|
363
|
+
const addr = addresses?.length ? addresses[0] : "";
|
|
364
|
+
return addr
|
|
365
|
+
? `${listeners} listeners • ${addr}`
|
|
366
|
+
: `${listeners} listeners`;
|
|
309
367
|
}
|
|
310
|
-
case
|
|
311
|
-
case
|
|
312
|
-
case
|
|
313
|
-
case
|
|
314
|
-
const hostnames = nodeData.hostnames as string[]
|
|
315
|
-
const rulesCount = nodeData.rulesCount as number || 0
|
|
316
|
-
const host = hostnames?.length ? hostnames[0] :
|
|
317
|
-
return host ? `${host} • ${rulesCount} rules` : `${rulesCount} rules
|
|
368
|
+
case "HTTPRoute":
|
|
369
|
+
case "GRPCRoute":
|
|
370
|
+
case "TCPRoute":
|
|
371
|
+
case "TLSRoute": {
|
|
372
|
+
const hostnames = nodeData.hostnames as string[];
|
|
373
|
+
const rulesCount = (nodeData.rulesCount as number) || 0;
|
|
374
|
+
const host = hostnames?.length ? hostnames[0] : "";
|
|
375
|
+
return host ? `${host} • ${rulesCount} rules` : `${rulesCount} rules`;
|
|
318
376
|
}
|
|
319
|
-
case
|
|
320
|
-
const min = nodeData.minReplicas ?? 1
|
|
321
|
-
const max = nodeData.maxReplicas ?? 10
|
|
322
|
-
const current = nodeData.current ?? 0
|
|
323
|
-
return `${current} (${min}-${max})
|
|
377
|
+
case "HorizontalPodAutoscaler": {
|
|
378
|
+
const min = nodeData.minReplicas ?? 1;
|
|
379
|
+
const max = nodeData.maxReplicas ?? 10;
|
|
380
|
+
const current = nodeData.current ?? 0;
|
|
381
|
+
return `${current} (${min}-${max})`;
|
|
324
382
|
}
|
|
325
383
|
case 'ConfigMap':
|
|
326
384
|
return `${nodeData.keys ?? 0} keys`
|
|
327
385
|
case 'Secret':
|
|
328
386
|
return `${nodeData.keys ?? 0} keys`
|
|
387
|
+
case 'WorkflowTemplate':
|
|
388
|
+
case 'ClusterWorkflowTemplate': {
|
|
389
|
+
const entrypoint = nodeData.entrypoint as string
|
|
390
|
+
const templateCount = nodeData.templateCount as number
|
|
391
|
+
if (entrypoint && templateCount) return `${entrypoint} • ${templateCount} templates`
|
|
392
|
+
if (entrypoint) return entrypoint
|
|
393
|
+
return templateCount ? `${templateCount} templates` : ''
|
|
394
|
+
}
|
|
395
|
+
case 'ServiceAccount':
|
|
396
|
+
return 'Workload identity'
|
|
397
|
+
case 'SealedSecret':
|
|
398
|
+
return nodeData.targetSecret ? `Creates ${nodeData.targetSecret}` : 'Encrypted secret'
|
|
399
|
+
case 'ServiceMonitor':
|
|
400
|
+
case 'PodMonitor': {
|
|
401
|
+
const count = (nodeData.endpointCount as number) || 0
|
|
402
|
+
return `${count} scrape endpoint${count === 1 ? '' : 's'}`
|
|
403
|
+
}
|
|
329
404
|
case 'PersistentVolumeClaim': {
|
|
330
405
|
const storage = (nodeData.storage as string) || ''
|
|
331
406
|
const phase = (nodeData.phase as string) || ''
|
|
332
407
|
return storage ? `${storage} (${phase})` : phase
|
|
333
408
|
}
|
|
334
|
-
case
|
|
335
|
-
const count = (nodeData.podCount as number) || 0
|
|
336
|
-
const healthy = (nodeData.healthy as number) || 0
|
|
337
|
-
const unhealthy = (nodeData.unhealthy as number) || 0
|
|
409
|
+
case "PodGroup": {
|
|
410
|
+
const count = (nodeData.podCount as number) || 0;
|
|
411
|
+
const healthy = (nodeData.healthy as number) || 0;
|
|
412
|
+
const unhealthy = (nodeData.unhealthy as number) || 0;
|
|
338
413
|
if (unhealthy > 0) {
|
|
339
|
-
return `${count} pods (${unhealthy} unhealthy)
|
|
414
|
+
return `${count} pods (${unhealthy} unhealthy)`;
|
|
340
415
|
}
|
|
341
|
-
return `${count} pods (${healthy} healthy)
|
|
416
|
+
return `${count} pods (${healthy} healthy)`;
|
|
342
417
|
}
|
|
343
|
-
case
|
|
344
|
-
const url = nodeData.url as string
|
|
418
|
+
case "KnativeService": {
|
|
419
|
+
const url = nodeData.url as string;
|
|
345
420
|
if (url) {
|
|
346
421
|
// Show just the hostname from the URL for compactness
|
|
347
422
|
try {
|
|
348
|
-
return new URL(url).hostname
|
|
423
|
+
return new URL(url).hostname;
|
|
349
424
|
} catch {
|
|
350
|
-
return url
|
|
425
|
+
return url;
|
|
351
426
|
}
|
|
352
427
|
}
|
|
353
|
-
return (nodeData.latestRevision as string) ||
|
|
428
|
+
return (nodeData.latestRevision as string) || "";
|
|
354
429
|
}
|
|
355
|
-
case
|
|
356
|
-
const instanceType = nodeData.instanceType as string
|
|
357
|
-
return instanceType ||
|
|
430
|
+
case "Node": {
|
|
431
|
+
const instanceType = nodeData.instanceType as string;
|
|
432
|
+
return instanceType || "";
|
|
358
433
|
}
|
|
359
|
-
case
|
|
360
|
-
return
|
|
434
|
+
case "Internet":
|
|
435
|
+
return "";
|
|
361
436
|
default:
|
|
362
|
-
return
|
|
437
|
+
return "";
|
|
363
438
|
}
|
|
364
439
|
}
|
|
365
440
|
|
|
366
441
|
interface K8sResourceNodeProps {
|
|
367
442
|
data: {
|
|
368
|
-
kind: NodeKind
|
|
369
|
-
name: string
|
|
370
|
-
status: HealthStatus
|
|
371
|
-
nodeData: Record<string, unknown
|
|
372
|
-
selected?: boolean
|
|
443
|
+
kind: NodeKind;
|
|
444
|
+
name: string;
|
|
445
|
+
status: HealthStatus;
|
|
446
|
+
nodeData: Record<string, unknown>;
|
|
447
|
+
selected?: boolean;
|
|
373
448
|
/** Hover-focus: when a sibling workload is focused, non-members dim. */
|
|
374
449
|
dimmed?: boolean
|
|
375
450
|
onExpand?: (nodeId: string) => void
|
|
376
451
|
onCollapse?: (nodeId: string) => void
|
|
377
452
|
isExpanded?: boolean
|
|
453
|
+
onToggleReplicaSets?: (ownerID: string) => void
|
|
378
454
|
}
|
|
379
455
|
id: string
|
|
380
456
|
}
|
|
@@ -383,35 +459,37 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
383
459
|
data,
|
|
384
460
|
id,
|
|
385
461
|
}: K8sResourceNodeProps) {
|
|
386
|
-
const { kind, name, status, nodeData, selected, dimmed, onExpand, onCollapse, isExpanded } = data
|
|
462
|
+
const { kind, name, status, nodeData, selected, dimmed, onExpand, onCollapse, isExpanded, onToggleReplicaSets } = data
|
|
387
463
|
// Cluster Audit findings joined onto this node by the host (web/ enriches each
|
|
388
464
|
// node's data by auditKey). The host only counts "badge-worthy" findings —
|
|
389
465
|
// reference-integrity / lifecycle, "this resource is actually broken" — not the
|
|
390
466
|
// posture/best-practice nags that fire near-universally, so the indicator stays
|
|
391
467
|
// a signal. Colored by worst severity (danger red, else warning amber).
|
|
392
|
-
const auditDanger =
|
|
393
|
-
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
468
|
+
const auditDanger =
|
|
469
|
+
typeof nodeData.auditDanger === "number" ? nodeData.auditDanger : 0;
|
|
470
|
+
const auditWarning =
|
|
471
|
+
typeof nodeData.auditWarning === "number" ? nodeData.auditWarning : 0;
|
|
472
|
+
const auditTotal = auditDanger + auditWarning;
|
|
473
|
+
const auditMessages = Array.isArray(nodeData.auditMessages)
|
|
474
|
+
? (nodeData.auditMessages as AuditBadgeMessage[])
|
|
475
|
+
: [];
|
|
476
|
+
// Workload ownership (application graph): when present, it owns the node fill.
|
|
477
|
+
// Kind is shown by a real icon, not arbitrary color; health remains dot/border.
|
|
399
478
|
const { ownerColorIndex } = ownershipOf(nodeData)
|
|
400
|
-
const hue = ownerColorIndex !== null
|
|
401
|
-
? workloadHue(ownerColorIndex)
|
|
402
|
-
: undefined
|
|
479
|
+
const hue = ownerColorIndex !== null ? workloadHue(ownerColorIndex) : undefined
|
|
403
480
|
const subtitle = getSubtitle(kind, nodeData)
|
|
404
481
|
const isInternet = kind === 'Internet'
|
|
405
482
|
const isPodGroup = kind === 'PodGroup'
|
|
406
|
-
const isSmallNode = kind === 'ConfigMap' || kind === 'Secret' || kind === 'HorizontalPodAutoscaler'
|
|
483
|
+
const isSmallNode = kind === 'ConfigMap' || kind === 'Secret' || kind === 'ServiceAccount' || kind === 'SealedSecret' || kind === 'ServiceMonitor' || kind === 'PodMonitor' || kind === 'HorizontalPodAutoscaler'
|
|
407
484
|
const canExpand = isPodGroup && onExpand && !isExpanded
|
|
408
485
|
const canCollapse = isPodGroup && onCollapse && isExpanded
|
|
409
486
|
const statusIssue = nodeData.statusIssue as string | undefined
|
|
410
487
|
const issueTooltip = getIssueTooltip(statusIssue)
|
|
411
488
|
const policyStatus = nodeData.policyStatus as string | undefined
|
|
489
|
+
const deploymentMembership = nodeData.deploymentMembership as 'runtime-only' | 'source-only' | undefined
|
|
490
|
+
const deploymentSourceLabel = nodeData.deploymentSourceLabel as string | undefined
|
|
412
491
|
|
|
413
|
-
|
|
414
|
-
const iconClass = `topology-icon topology-icon-${kind.toLowerCase()}`
|
|
492
|
+
const Icon = getTopologyIcon(kind);
|
|
415
493
|
|
|
416
494
|
if (isInternet) {
|
|
417
495
|
return (
|
|
@@ -423,13 +501,13 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
423
501
|
/>
|
|
424
502
|
<div
|
|
425
503
|
className={clsx(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
selected &&
|
|
504
|
+
"flex items-center gap-2 px-4 py-2 rounded-full",
|
|
505
|
+
"selection border border-skyhook-500/30",
|
|
506
|
+
"shadow-lg shadow-skyhook-500/20",
|
|
507
|
+
selected && "ring-2 ring-skyhook-400",
|
|
430
508
|
)}
|
|
431
509
|
>
|
|
432
|
-
<
|
|
510
|
+
<Icon className="h-5 w-5 shrink-0 text-accent-text" aria-hidden />
|
|
433
511
|
<span className="text-sm font-medium text-skyhook-300">Internet</span>
|
|
434
512
|
<span className="w-2 h-2 rounded-full bg-green-500" />
|
|
435
513
|
</div>
|
|
@@ -439,7 +517,7 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
439
517
|
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
440
518
|
/>
|
|
441
519
|
</>
|
|
442
|
-
)
|
|
520
|
+
);
|
|
443
521
|
}
|
|
444
522
|
|
|
445
523
|
return (
|
|
@@ -452,93 +530,159 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
452
530
|
|
|
453
531
|
<div
|
|
454
532
|
className={clsx(
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
selected &&
|
|
458
|
-
!selected && dimmed &&
|
|
459
|
-
isSmallNode && !dimmed &&
|
|
533
|
+
"relative rounded-lg overflow-hidden transition-opacity duration-150",
|
|
534
|
+
"bg-theme-surface topology-node-card",
|
|
535
|
+
selected && "topology-node-selected",
|
|
536
|
+
!selected && dimmed && "opacity-30",
|
|
537
|
+
isSmallNode && !dimmed && "opacity-90",
|
|
460
538
|
// Status bar via CSS pseudo-element (defined in index.css)
|
|
461
|
-
(status ===
|
|
462
|
-
|
|
463
|
-
status ===
|
|
539
|
+
(status === "healthy" || status === "unknown") &&
|
|
540
|
+
"topology-node-status-bar",
|
|
541
|
+
status === "healthy" && "topology-node-status-healthy",
|
|
542
|
+
status === "unknown" && "topology-node-status-unknown",
|
|
464
543
|
)}
|
|
465
544
|
style={{
|
|
466
545
|
width: NODE_DIMENSIONS[kind]?.width ?? DEFAULT_NODE_DIMENSIONS.width,
|
|
467
546
|
...getStatusStyle(status),
|
|
547
|
+
...(hue ? { backgroundColor: hue.wash } : undefined),
|
|
468
548
|
}}
|
|
469
549
|
>
|
|
470
|
-
{/* Workload tint — layered over the surface, inset past the 4px status bar */}
|
|
471
|
-
{hue && (
|
|
472
|
-
<div
|
|
473
|
-
className="pointer-events-none absolute inset-y-0 right-0 rounded-r-lg"
|
|
474
|
-
style={{ left: 4, background: hue.wash }}
|
|
475
|
-
/>
|
|
476
|
-
)}
|
|
477
|
-
|
|
478
550
|
{/* Content */}
|
|
479
|
-
<div className={clsx(
|
|
480
|
-
'pl-3 pr-3',
|
|
481
|
-
isSmallNode ? 'py-2' : 'py-2.5'
|
|
482
|
-
)}>
|
|
551
|
+
<div className={clsx("pl-3 pr-3", isSmallNode ? "py-2" : "py-2.5")}>
|
|
483
552
|
{/* Header row: icon + kind label + (right-aligned) policy badge + expand/collapse + status dot */}
|
|
484
553
|
<div className="flex items-center gap-1.5 mb-0.5">
|
|
485
|
-
<
|
|
554
|
+
<Icon
|
|
555
|
+
className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary"
|
|
556
|
+
aria-hidden
|
|
557
|
+
/>
|
|
486
558
|
<span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary font-medium">
|
|
487
|
-
{isPodGroup ?
|
|
559
|
+
{isPodGroup ? "Pod Group" : displayKind(kind)}
|
|
488
560
|
</span>
|
|
489
561
|
<div className="ml-auto flex items-center gap-1.5">
|
|
562
|
+
{deploymentMembership && (
|
|
563
|
+
<Tooltip
|
|
564
|
+
content={deploymentMembership === 'runtime-only'
|
|
565
|
+
? `Observed in runtime but not present in the visible ${deploymentSourceLabel ?? 'deployment'} inventory.`
|
|
566
|
+
: `Declared by ${deploymentSourceLabel ?? 'the deployment source'} but not connected to the current runtime neighborhood.`}
|
|
567
|
+
position="right"
|
|
568
|
+
>
|
|
569
|
+
<span className="inline-flex cursor-help items-center gap-1 rounded bg-theme-elevated px-1 py-0.5 text-[9px] font-medium normal-case tracking-normal text-theme-text-tertiary">
|
|
570
|
+
{deploymentMembership === 'runtime-only'
|
|
571
|
+
? <Radio className="h-2.5 w-2.5" aria-hidden />
|
|
572
|
+
: deploymentSourceLabel === 'Argo CD'
|
|
573
|
+
? <img src={argoCdLogo} alt="" className="h-2.5 w-2.5 object-contain" />
|
|
574
|
+
: deploymentSourceLabel === 'Flux'
|
|
575
|
+
? <img src={fluxLogo} alt="" className="h-2.5 w-2.5 object-contain" />
|
|
576
|
+
: <Layers className="h-2.5 w-2.5" aria-hidden />}
|
|
577
|
+
{!isSmallNode && (deploymentMembership === 'runtime-only' ? 'Runtime only' : `${deploymentSourceLabel ?? 'Source'} only`)}
|
|
578
|
+
</span>
|
|
579
|
+
</Tooltip>
|
|
580
|
+
)}
|
|
581
|
+
{onToggleReplicaSets && (
|
|
582
|
+
<Tooltip content={nodeData.replicaSetsCollapsed ? 'Show ReplicaSet' : 'Hide stable ReplicaSet'} position="right">
|
|
583
|
+
<button
|
|
584
|
+
type="button"
|
|
585
|
+
onClick={(event) => {
|
|
586
|
+
event.stopPropagation()
|
|
587
|
+
onToggleReplicaSets(id)
|
|
588
|
+
}}
|
|
589
|
+
className="inline-flex items-center gap-1 rounded px-1 py-0.5 text-[9px] font-medium text-theme-text-tertiary hover:bg-theme-elevated hover:text-theme-text-primary"
|
|
590
|
+
>
|
|
591
|
+
{nodeData.replicaSetCount as number} RS
|
|
592
|
+
{nodeData.replicaSetsCollapsed ? <ChevronDown className="h-3 w-3" aria-hidden /> : <ChevronUp className="h-3 w-3" aria-hidden />}
|
|
593
|
+
</button>
|
|
594
|
+
</Tooltip>
|
|
595
|
+
)}
|
|
490
596
|
{policyStatus && (
|
|
491
|
-
<Tooltip
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
597
|
+
<Tooltip
|
|
598
|
+
content={
|
|
599
|
+
policyStatus === "protected"
|
|
600
|
+
? "Protected by NetworkPolicy"
|
|
601
|
+
: "No NetworkPolicy coverage"
|
|
602
|
+
}
|
|
603
|
+
position="right"
|
|
604
|
+
>
|
|
605
|
+
<span
|
|
606
|
+
className={clsx(
|
|
607
|
+
"inline-flex items-center justify-center w-3 h-3 rounded-sm text-[8px] font-bold cursor-help leading-none",
|
|
608
|
+
policyStatus === "protected"
|
|
609
|
+
? "bg-green-500/20 text-green-500 border border-green-500/30"
|
|
610
|
+
: "bg-yellow-500/20 text-yellow-500 border border-yellow-500/30",
|
|
611
|
+
)}
|
|
612
|
+
>
|
|
613
|
+
{policyStatus === "protected" ? "✓" : "!"}
|
|
499
614
|
</span>
|
|
500
615
|
</Tooltip>
|
|
501
616
|
)}
|
|
502
617
|
{canExpand && (
|
|
503
|
-
<
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
onExpand(id)
|
|
507
|
-
}}
|
|
508
|
-
className="p-0.5 hover:bg-theme-elevated rounded transition-colors"
|
|
509
|
-
title="Expand to show individual pods"
|
|
618
|
+
<Tooltip
|
|
619
|
+
content="Expand to show individual pods"
|
|
620
|
+
position="right"
|
|
510
621
|
>
|
|
511
|
-
<
|
|
512
|
-
|
|
622
|
+
<button
|
|
623
|
+
onClick={(e) => {
|
|
624
|
+
e.stopPropagation();
|
|
625
|
+
onExpand(id);
|
|
626
|
+
}}
|
|
627
|
+
className="p-0.5 hover:bg-theme-elevated rounded transition-colors"
|
|
628
|
+
>
|
|
629
|
+
<ChevronDown className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
630
|
+
</button>
|
|
631
|
+
</Tooltip>
|
|
513
632
|
)}
|
|
514
633
|
{canCollapse && (
|
|
515
|
-
<
|
|
516
|
-
|
|
517
|
-
e
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
634
|
+
<Tooltip content="Collapse back to group" position="right">
|
|
635
|
+
<button
|
|
636
|
+
onClick={(e) => {
|
|
637
|
+
e.stopPropagation();
|
|
638
|
+
onCollapse(id);
|
|
639
|
+
}}
|
|
640
|
+
className="p-0.5 hover:bg-theme-elevated rounded transition-colors"
|
|
641
|
+
>
|
|
642
|
+
<ChevronUp className="w-3.5 h-3.5 text-theme-text-secondary" />
|
|
643
|
+
</button>
|
|
644
|
+
</Tooltip>
|
|
525
645
|
)}
|
|
526
646
|
{auditTotal > 0 && (
|
|
527
647
|
<Tooltip
|
|
528
|
-
content={
|
|
529
|
-
|
|
530
|
-
|
|
648
|
+
content={
|
|
649
|
+
auditMessages.length > 0 ? (
|
|
650
|
+
<AuditBadgeTooltip
|
|
651
|
+
messages={auditMessages}
|
|
652
|
+
clickHint={false}
|
|
653
|
+
/>
|
|
654
|
+
) : (
|
|
655
|
+
`${auditTotal} audit ${auditTotal === 1 ? "finding" : "findings"}${auditDanger > 0 ? ` · ${auditDanger} danger` : ""}`
|
|
656
|
+
)
|
|
657
|
+
}
|
|
531
658
|
position="right"
|
|
532
659
|
>
|
|
533
|
-
<TriangleAlert
|
|
660
|
+
<TriangleAlert
|
|
661
|
+
className={clsx(
|
|
662
|
+
"w-3 h-3 cursor-help",
|
|
663
|
+
auditDanger > 0
|
|
664
|
+
? SEVERITY_TEXT.error
|
|
665
|
+
: SEVERITY_TEXT.warning,
|
|
666
|
+
)}
|
|
667
|
+
/>
|
|
534
668
|
</Tooltip>
|
|
535
669
|
)}
|
|
536
670
|
{issueTooltip ? (
|
|
537
671
|
<Tooltip content={issueTooltip} position="right">
|
|
538
|
-
<span
|
|
672
|
+
<span
|
|
673
|
+
className={clsx(
|
|
674
|
+
"w-1.5 h-1.5 rounded-full cursor-help",
|
|
675
|
+
getStatusDotColor(status),
|
|
676
|
+
)}
|
|
677
|
+
/>
|
|
539
678
|
</Tooltip>
|
|
540
679
|
) : (
|
|
541
|
-
<span
|
|
680
|
+
<span
|
|
681
|
+
className={clsx(
|
|
682
|
+
"w-1.5 h-1.5 rounded-full",
|
|
683
|
+
getStatusDotColor(status),
|
|
684
|
+
)}
|
|
685
|
+
/>
|
|
542
686
|
)}
|
|
543
687
|
</div>
|
|
544
688
|
</div>
|
|
@@ -546,7 +690,7 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
546
690
|
{/* Name — middle-ellipsis so the differentiating suffix survives
|
|
547
691
|
(a column of pods sharing a long prefix must stay tellable apart). */}
|
|
548
692
|
<div className="text-sm font-medium text-theme-text-primary truncate pr-1">
|
|
549
|
-
{midTruncate(name, 34)}
|
|
693
|
+
{midTruncate(name, NODE_NAME_LENGTH_LIMITS[kind] ?? 34)}
|
|
550
694
|
</div>
|
|
551
695
|
|
|
552
696
|
{/* Subtitle */}
|
|
@@ -564,5 +708,5 @@ export const K8sResourceNode = memo(function K8sResourceNode({
|
|
|
564
708
|
className="!bg-transparent !border-0 !w-0 !h-0"
|
|
565
709
|
/>
|
|
566
710
|
</>
|
|
567
|
-
)
|
|
568
|
-
})
|
|
711
|
+
);
|
|
712
|
+
});
|