@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
|
@@ -48,7 +48,9 @@ export function JobRenderer({ data }: JobRendererProps) {
|
|
|
48
48
|
const conditions = status.conditions || []
|
|
49
49
|
|
|
50
50
|
const startTime = status.startTime ? new Date(status.startTime) : null
|
|
51
|
-
const
|
|
51
|
+
const terminalCondition = conditions.find((c: any) => (c.type === 'Complete' || c.type === 'Failed') && c.status === 'True')
|
|
52
|
+
const finishedAt = status.completionTime || terminalCondition?.lastTransitionTime
|
|
53
|
+
const completionTime = finishedAt ? new Date(finishedAt) : null
|
|
52
54
|
const duration = startTime && completionTime
|
|
53
55
|
? formatDuration(completionTime.getTime() - startTime.getTime(), true)
|
|
54
56
|
: startTime
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Info } from 'lucide-react'
|
|
2
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
3
|
+
|
|
4
|
+
interface MetricsUnavailableNoticeProps {
|
|
5
|
+
rawError?: string
|
|
6
|
+
diagnosis?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function MetricsUnavailableNotice({ rawError, diagnosis }: MetricsUnavailableNoticeProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="card-inner-lg text-xs text-theme-text-tertiary">
|
|
12
|
+
<div className="flex items-center gap-1.5">
|
|
13
|
+
<span className="min-w-0 leading-5">
|
|
14
|
+
Metrics unavailable. Radar cannot read metrics.k8s.io.
|
|
15
|
+
</span>
|
|
16
|
+
{rawError && (
|
|
17
|
+
<Tooltip
|
|
18
|
+
content={(
|
|
19
|
+
<span className="space-y-1">
|
|
20
|
+
<span className="block">
|
|
21
|
+
This panel uses Kubernetes metrics.k8s.io for live CPU and memory; Prometheus data does not fill it.
|
|
22
|
+
</span>
|
|
23
|
+
<span className="block">
|
|
24
|
+
{diagnosis || 'Check that metrics-server is installed and healthy, and that the v1beta1.metrics.k8s.io APIService is Available.'}
|
|
25
|
+
</span>
|
|
26
|
+
<span className="block">
|
|
27
|
+
Raw error: <span className="font-mono break-words">{rawError}</span>
|
|
28
|
+
</span>
|
|
29
|
+
</span>
|
|
30
|
+
)}
|
|
31
|
+
delay={150}
|
|
32
|
+
position="left"
|
|
33
|
+
>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
className="inline-flex shrink-0 cursor-help items-center gap-1 text-xs font-medium leading-5 text-theme-text-tertiary hover:text-theme-text-secondary"
|
|
37
|
+
aria-label="Metrics error details"
|
|
38
|
+
>
|
|
39
|
+
<Info className="h-3.5 w-3.5" />
|
|
40
|
+
<span>Details</span>
|
|
41
|
+
</button>
|
|
42
|
+
</Tooltip>
|
|
43
|
+
)}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { NodeRenderer } from './NodeRenderer'
|
|
4
|
+
|
|
5
|
+
const node = {
|
|
6
|
+
metadata: { name: 'kind-worker', labels: {} },
|
|
7
|
+
spec: {},
|
|
8
|
+
status: {
|
|
9
|
+
nodeInfo: {
|
|
10
|
+
osImage: 'Container-Optimized OS',
|
|
11
|
+
architecture: 'amd64',
|
|
12
|
+
kernelVersion: '6.1.0',
|
|
13
|
+
containerRuntimeVersion: 'containerd://1.7',
|
|
14
|
+
kubeletVersion: 'v1.33.0',
|
|
15
|
+
kubeProxyVersion: 'v1.33.0',
|
|
16
|
+
},
|
|
17
|
+
capacity: { cpu: '4', memory: '8Gi', pods: '110' },
|
|
18
|
+
allocatable: { cpu: '4', memory: '7Gi', pods: '110' },
|
|
19
|
+
conditions: [],
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('NodeRenderer metrics', () => {
|
|
24
|
+
it('renders a calm unavailable state when metrics-server is absent', () => {
|
|
25
|
+
const html = renderToString(
|
|
26
|
+
<NodeRenderer
|
|
27
|
+
data={node}
|
|
28
|
+
metricsUnavailable
|
|
29
|
+
metricsHistory={{
|
|
30
|
+
dataPoints: [],
|
|
31
|
+
metricsUnavailableReason: 'the server could not find the requested resource',
|
|
32
|
+
}}
|
|
33
|
+
/>,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(html).toContain('Resource Usage')
|
|
37
|
+
expect(html).toContain('Metrics unavailable')
|
|
38
|
+
expect(html).toContain('Radar cannot read metrics.k8s.io')
|
|
39
|
+
expect(html).toContain('Metrics error details')
|
|
40
|
+
expect(html).not.toContain('Install or repair metrics-server and its APIService')
|
|
41
|
+
expect(html).not.toContain('Metrics collection error')
|
|
42
|
+
expect(html).not.toContain('Collecting metrics data')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not let stale live metrics hide an unavailable state', () => {
|
|
46
|
+
const html = renderToString(
|
|
47
|
+
<NodeRenderer
|
|
48
|
+
data={node}
|
|
49
|
+
metricsUnavailable
|
|
50
|
+
metrics={{ usage: { cpu: '100m', memory: '256Mi' }, timestamp: '2026-06-30T00:00:00Z' }}
|
|
51
|
+
/>,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
expect(html).toContain('Metrics unavailable')
|
|
55
|
+
expect(html).not.toContain('100m')
|
|
56
|
+
expect(html).not.toContain('Last updated')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('keeps buffered historical charts visible under an unavailable notice', () => {
|
|
60
|
+
const html = renderToString(
|
|
61
|
+
<NodeRenderer
|
|
62
|
+
data={node}
|
|
63
|
+
metricsUnavailable
|
|
64
|
+
metricsHistory={{
|
|
65
|
+
dataPoints: [{ timestamp: '2026-06-30T00:00:00Z', cpu: 100000000, memory: 268435456 }],
|
|
66
|
+
}}
|
|
67
|
+
/>,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
expect(html).toContain('Metrics unavailable')
|
|
71
|
+
expect(html).toContain('CPU')
|
|
72
|
+
expect(html).toContain('Memory')
|
|
73
|
+
expect(html).not.toContain('Last updated')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('keeps non-absence collection errors visible', () => {
|
|
77
|
+
const html = renderToString(
|
|
78
|
+
<NodeRenderer
|
|
79
|
+
data={node}
|
|
80
|
+
metricsHistory={{ dataPoints: [], collectionError: 'forbidden: no access to nodes' }}
|
|
81
|
+
/>,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect(html).toContain('Metrics collection error')
|
|
85
|
+
expect(html).toContain('forbidden: no access to nodes')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('warns about non-absence collection errors even when historical samples exist', () => {
|
|
89
|
+
const html = renderToString(
|
|
90
|
+
<NodeRenderer
|
|
91
|
+
data={node}
|
|
92
|
+
metricsHistory={{
|
|
93
|
+
collectionError: 'forbidden: no access to nodes',
|
|
94
|
+
dataPoints: [{ timestamp: '2026-06-30T00:00:00Z', cpu: 100000000, memory: 268435456 }],
|
|
95
|
+
}}
|
|
96
|
+
/>,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
expect(html).toContain('Metrics collection error')
|
|
100
|
+
expect(html).toContain('forbidden: no access to nodes')
|
|
101
|
+
expect(html).toContain('CPU')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -5,13 +5,15 @@ import { MetricsChart } from '../../ui/MetricsChart'
|
|
|
5
5
|
import { formatMemoryString } from '../../../utils/format'
|
|
6
6
|
import { getExtendedCapacityRows } from '../../../utils/extended-resources'
|
|
7
7
|
import type { MetricsDataPoint } from '../../../types/core'
|
|
8
|
+
import { MetricsUnavailableNotice } from './MetricsUnavailableNotice'
|
|
8
9
|
|
|
9
10
|
interface NodeRendererProps {
|
|
10
11
|
data: any
|
|
11
12
|
relationships?: { pods?: any[] }
|
|
12
13
|
onViewPods?: () => void
|
|
13
14
|
metrics?: { usage?: { cpu: string; memory: string }; timestamp?: string }
|
|
14
|
-
metricsHistory?: { dataPoints?: MetricsDataPoint[]; collectionError?: string }
|
|
15
|
+
metricsHistory?: { dataPoints?: MetricsDataPoint[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
|
|
16
|
+
metricsUnavailable?: boolean
|
|
15
17
|
hideMetricsServer?: boolean
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -59,7 +61,7 @@ function getNodeProblems(data: any): string[] {
|
|
|
59
61
|
return problems
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
export function NodeRenderer({ data, relationships, onViewPods, metrics, metricsHistory, hideMetricsServer }: NodeRendererProps) {
|
|
64
|
+
export function NodeRenderer({ data, relationships, onViewPods, metrics, metricsHistory, metricsUnavailable, hideMetricsServer }: NodeRendererProps) {
|
|
63
65
|
const status = data.status || {}
|
|
64
66
|
const spec = data.spec || {}
|
|
65
67
|
const metadata = data.metadata || {}
|
|
@@ -74,6 +76,9 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
74
76
|
const problems = getNodeProblems(data)
|
|
75
77
|
const hasProblems = problems.length > 0
|
|
76
78
|
const isCordoned = !!spec.unschedulable
|
|
79
|
+
const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError
|
|
80
|
+
const hasMetricsHistory = !!metricsHistory?.dataPoints?.length
|
|
81
|
+
const currentMetrics = metricsUnavailable ? undefined : metrics
|
|
77
82
|
|
|
78
83
|
// Extract platform info from labels
|
|
79
84
|
const instanceType = labels['node.kubernetes.io/instance-type']
|
|
@@ -122,13 +127,13 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
122
127
|
label: 'CPU',
|
|
123
128
|
capacity: capacity.cpu,
|
|
124
129
|
allocatable: allocatable.cpu,
|
|
125
|
-
inUse:
|
|
130
|
+
inUse: currentMetrics?.usage?.cpu,
|
|
126
131
|
},
|
|
127
132
|
{
|
|
128
133
|
label: 'Memory',
|
|
129
134
|
capacity: formatMemory(capacity.memory),
|
|
130
135
|
allocatable: formatMemory(allocatable.memory),
|
|
131
|
-
inUse:
|
|
136
|
+
inUse: currentMetrics?.usage?.memory ? formatMemory(currentMetrics.usage.memory) : undefined,
|
|
132
137
|
},
|
|
133
138
|
{
|
|
134
139
|
label: 'Pods',
|
|
@@ -182,15 +187,18 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
182
187
|
</Section>
|
|
183
188
|
|
|
184
189
|
{/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
|
|
185
|
-
{!hideMetricsServer && (
|
|
190
|
+
{!hideMetricsServer && (currentMetrics?.usage || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable) && (
|
|
186
191
|
<Section title="Resource Usage" icon={Activity} defaultExpanded>
|
|
187
|
-
{
|
|
192
|
+
{showMetricsUnavailable && (
|
|
193
|
+
<MetricsUnavailableNotice rawError={metricsHistory?.metricsUnavailableReason} diagnosis={metricsHistory?.metricsUnavailableDiagnosis} />
|
|
194
|
+
)}
|
|
195
|
+
{metricsHistory?.collectionError && (
|
|
188
196
|
<div className="mb-3 rounded-lg border border-yellow-500/30 bg-yellow-500/5 px-3 py-2 text-xs text-yellow-400">
|
|
189
197
|
<span className="font-medium">Metrics collection error:</span>{' '}
|
|
190
198
|
<span className="break-all">{metricsHistory.collectionError}</span>
|
|
191
199
|
</div>
|
|
192
200
|
)}
|
|
193
|
-
{
|
|
201
|
+
{hasMetricsHistory && metricsHistory?.dataPoints ? (
|
|
194
202
|
<div className="space-y-4">
|
|
195
203
|
{/* CPU Usage with Chart */}
|
|
196
204
|
<div className="card-inner-lg">
|
|
@@ -224,7 +232,7 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
224
232
|
/>
|
|
225
233
|
</div>
|
|
226
234
|
</div>
|
|
227
|
-
) :
|
|
235
|
+
) : showMetricsUnavailable ? null : currentMetrics?.usage ? (
|
|
228
236
|
/* Fallback to simple display if no history yet */
|
|
229
237
|
<div className="space-y-3">
|
|
230
238
|
<div className="card-inner-lg">
|
|
@@ -232,7 +240,7 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
232
240
|
<span className="text-sm font-medium text-theme-text-primary">CPU</span>
|
|
233
241
|
</div>
|
|
234
242
|
<div className="flex items-baseline gap-2">
|
|
235
|
-
<span className="text-lg font-medium text-blue-400">{
|
|
243
|
+
<span className="text-lg font-medium text-blue-400">{currentMetrics.usage.cpu}</span>
|
|
236
244
|
<span className="text-sm text-theme-text-tertiary">
|
|
237
245
|
/ {allocatable.cpu || capacity.cpu || '?'} allocatable
|
|
238
246
|
</span>
|
|
@@ -243,7 +251,7 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
243
251
|
<span className="text-sm font-medium text-theme-text-primary">Memory</span>
|
|
244
252
|
</div>
|
|
245
253
|
<div className="flex items-baseline gap-2">
|
|
246
|
-
<span className="text-lg font-medium text-purple-400">{formatMemory(
|
|
254
|
+
<span className="text-lg font-medium text-purple-400">{formatMemory(currentMetrics.usage.memory)}</span>
|
|
247
255
|
<span className="text-sm text-theme-text-tertiary">
|
|
248
256
|
/ {formatMemory(allocatable.memory) || formatMemory(capacity.memory) || '?'} allocatable
|
|
249
257
|
</span>
|
|
@@ -253,9 +261,9 @@ export function NodeRenderer({ data, relationships, onViewPods, metrics, metrics
|
|
|
253
261
|
) : (
|
|
254
262
|
<div className="text-xs text-theme-text-tertiary">Collecting metrics data...</div>
|
|
255
263
|
)}
|
|
256
|
-
{
|
|
264
|
+
{!showMetricsUnavailable && currentMetrics?.timestamp && (
|
|
257
265
|
<div className="mt-2 text-xs text-theme-text-tertiary">
|
|
258
|
-
Last updated: {new Date(
|
|
266
|
+
Last updated: {new Date(currentMetrics.timestamp).toLocaleTimeString()}
|
|
259
267
|
</div>
|
|
260
268
|
)}
|
|
261
269
|
</Section>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { PVCRenderer } from './PVCRenderer'
|
|
4
|
+
|
|
5
|
+
describe('PVCRenderer', () => {
|
|
6
|
+
it('does not reassure indefinitely pending PVCs as normal', () => {
|
|
7
|
+
const html = renderToString(
|
|
8
|
+
<PVCRenderer
|
|
9
|
+
data={{
|
|
10
|
+
metadata: { name: 'data', namespace: 'demo' },
|
|
11
|
+
spec: { storageClassName: 'standard' },
|
|
12
|
+
status: { phase: 'Pending' },
|
|
13
|
+
}}
|
|
14
|
+
/>,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
expect(html).toContain('Pending')
|
|
18
|
+
expect(html).toContain('not yet bound')
|
|
19
|
+
expect(html).toContain('check the StorageClass')
|
|
20
|
+
expect(html).not.toContain('This is normal')
|
|
21
|
+
expect(html).not.toContain('expected indefinitely')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -53,8 +53,8 @@ export function PVCRenderer({ data, onNavigate, extraSections }: PVCRendererProp
|
|
|
53
53
|
{isPending && (
|
|
54
54
|
<AlertBanner
|
|
55
55
|
variant="info"
|
|
56
|
-
title="Pending —
|
|
57
|
-
message="
|
|
56
|
+
title="Pending — not yet bound"
|
|
57
|
+
message="A PVC stays Pending while its volume is provisioned, or for a WaitForFirstConsumer StorageClass until a Pod that mounts it is scheduled. If it stays Pending, check the StorageClass, its provisioner, and storage quota."
|
|
58
58
|
/>
|
|
59
59
|
)}
|
|
60
60
|
|
|
@@ -107,3 +107,104 @@ describe('PodRenderer issues banner', () => {
|
|
|
107
107
|
expect(html).toContain('min-w-0 break-words')
|
|
108
108
|
})
|
|
109
109
|
})
|
|
110
|
+
|
|
111
|
+
describe('PodRenderer metrics', () => {
|
|
112
|
+
it('renders a calm unavailable state when metrics-server is absent', () => {
|
|
113
|
+
const html = renderToString(
|
|
114
|
+
<PodRenderer
|
|
115
|
+
data={pod}
|
|
116
|
+
onCopy={() => undefined}
|
|
117
|
+
copied={null}
|
|
118
|
+
metricsUnavailable
|
|
119
|
+
metricsHistory={{
|
|
120
|
+
containers: [],
|
|
121
|
+
metricsUnavailableReason: 'the server could not find the requested resource',
|
|
122
|
+
}}
|
|
123
|
+
/>,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
expect(html).toContain('Resource Usage')
|
|
127
|
+
expect(html).toContain('Metrics unavailable')
|
|
128
|
+
expect(html).toContain('Radar cannot read metrics.k8s.io')
|
|
129
|
+
expect(html).toContain('Metrics error details')
|
|
130
|
+
expect(html).not.toContain('Install or repair metrics-server and its APIService')
|
|
131
|
+
expect(html).not.toContain('Metrics collection error')
|
|
132
|
+
expect(html).not.toContain('Collecting metrics data')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('does not let stale live metrics hide an unavailable state', () => {
|
|
136
|
+
const html = renderToString(
|
|
137
|
+
<PodRenderer
|
|
138
|
+
data={pod}
|
|
139
|
+
onCopy={() => undefined}
|
|
140
|
+
copied={null}
|
|
141
|
+
metricsUnavailable
|
|
142
|
+
metrics={{
|
|
143
|
+
containers: [{ name: 'api', usage: { cpu: '100m', memory: '256Mi' } }],
|
|
144
|
+
timestamp: '2026-06-30T00:00:00Z',
|
|
145
|
+
}}
|
|
146
|
+
/>,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
expect(html).toContain('Metrics unavailable')
|
|
150
|
+
expect(html).not.toContain('100m')
|
|
151
|
+
expect(html).not.toContain('Last updated')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('keeps buffered historical charts visible under an unavailable notice', () => {
|
|
155
|
+
const html = renderToString(
|
|
156
|
+
<PodRenderer
|
|
157
|
+
data={pod}
|
|
158
|
+
onCopy={() => undefined}
|
|
159
|
+
copied={null}
|
|
160
|
+
metricsUnavailable
|
|
161
|
+
metricsHistory={{
|
|
162
|
+
containers: [{
|
|
163
|
+
name: 'api',
|
|
164
|
+
dataPoints: [{ timestamp: '2026-06-30T00:00:00Z', cpu: 100000000, memory: 268435456 }],
|
|
165
|
+
}],
|
|
166
|
+
}}
|
|
167
|
+
/>,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
expect(html).toContain('Metrics unavailable')
|
|
171
|
+
expect(html).toContain('CPU')
|
|
172
|
+
expect(html).toContain('Memory')
|
|
173
|
+
expect(html).not.toContain('Last updated')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('keeps non-absence collection errors visible', () => {
|
|
177
|
+
const html = renderToString(
|
|
178
|
+
<PodRenderer
|
|
179
|
+
data={pod}
|
|
180
|
+
onCopy={() => undefined}
|
|
181
|
+
copied={null}
|
|
182
|
+
metricsHistory={{ containers: [], collectionError: 'forbidden: no access to pods.metrics.k8s.io' }}
|
|
183
|
+
/>,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(html).toContain('Metrics collection error')
|
|
187
|
+
expect(html).toContain('forbidden: no access to pods.metrics.k8s.io')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('warns about non-absence collection errors even when historical samples exist', () => {
|
|
191
|
+
const html = renderToString(
|
|
192
|
+
<PodRenderer
|
|
193
|
+
data={pod}
|
|
194
|
+
onCopy={() => undefined}
|
|
195
|
+
copied={null}
|
|
196
|
+
metricsHistory={{
|
|
197
|
+
collectionError: 'forbidden: no access to pods.metrics.k8s.io',
|
|
198
|
+
containers: [{
|
|
199
|
+
name: 'api',
|
|
200
|
+
dataPoints: [{ timestamp: '2026-06-30T00:00:00Z', cpu: 100000000, memory: 268435456 }],
|
|
201
|
+
}],
|
|
202
|
+
}}
|
|
203
|
+
/>,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
expect(html).toContain('Metrics collection error')
|
|
207
|
+
expect(html).toContain('forbidden: no access to pods.metrics.k8s.io')
|
|
208
|
+
expect(html).toContain('CPU')
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -15,6 +15,7 @@ import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
|
|
|
15
15
|
import type { ResolvedEnvFrom, RBACSubjectResponse, RBACPolicyRule } from '../../../types'
|
|
16
16
|
import { Tooltip } from '../../ui/Tooltip'
|
|
17
17
|
import { MetricsChart } from '../../ui/MetricsChart'
|
|
18
|
+
import { MetricsUnavailableNotice } from './MetricsUnavailableNotice'
|
|
18
19
|
|
|
19
20
|
function parseValidDate(dateStr: string): Date | null {
|
|
20
21
|
const d = new Date(dateStr)
|
|
@@ -73,7 +74,8 @@ interface PodRendererProps {
|
|
|
73
74
|
renderPortAction?: (props: { namespace: string; podName: string; port: number; protocol: string; disabled?: boolean }) => ReactNode
|
|
74
75
|
// Metrics data injection
|
|
75
76
|
metrics?: { containers?: any[]; timestamp?: string }
|
|
76
|
-
metricsHistory?: { containers?: any[]; collectionError?: string }
|
|
77
|
+
metricsHistory?: { containers?: any[]; collectionError?: string; metricsUnavailableReason?: string; metricsUnavailableDiagnosis?: string }
|
|
78
|
+
metricsUnavailable?: boolean
|
|
77
79
|
hideMetricsServer?: boolean
|
|
78
80
|
// Filesystem browser render props
|
|
79
81
|
renderImageBrowser?: (props: { image: string; namespace: string; podName: string; pullSecrets: string[]; onClose: () => void; onSwitchToPodFiles?: () => void }) => ReactNode
|
|
@@ -257,6 +259,7 @@ export function PodRenderer({
|
|
|
257
259
|
renderPortAction,
|
|
258
260
|
metrics,
|
|
259
261
|
metricsHistory,
|
|
262
|
+
metricsUnavailable,
|
|
260
263
|
hideMetricsServer,
|
|
261
264
|
renderImageBrowser,
|
|
262
265
|
renderPodBrowser,
|
|
@@ -280,6 +283,9 @@ export function PodRenderer({
|
|
|
280
283
|
const operationalIssuesShown = useOperationalIssuesShown()
|
|
281
284
|
const podProblems = getPodProblems(data)
|
|
282
285
|
const hasProblems = podProblems.length > 0 && !operationalIssuesShown
|
|
286
|
+
const showMetricsUnavailable = !!metricsUnavailable && !metricsHistory?.collectionError
|
|
287
|
+
const hasMetricsHistory = !!metricsHistory?.containers?.length
|
|
288
|
+
const currentMetrics = metricsUnavailable ? undefined : metrics
|
|
283
289
|
|
|
284
290
|
// Image filesystem modal state
|
|
285
291
|
const [selectedImage, setSelectedImage] = useState<string | null>(null)
|
|
@@ -718,89 +724,94 @@ export function PodRenderer({
|
|
|
718
724
|
)}
|
|
719
725
|
|
|
720
726
|
{/* Resource Usage (from metrics-server) — hidden when Prometheus has CPU/memory data */}
|
|
721
|
-
{!hideMetricsServer && !!(
|
|
727
|
+
{!hideMetricsServer && !!(currentMetrics?.containers?.length || hasMetricsHistory || metricsHistory?.collectionError || showMetricsUnavailable) && (
|
|
722
728
|
<Section title="Resource Usage" icon={Activity} defaultExpanded>
|
|
723
|
-
{
|
|
729
|
+
{showMetricsUnavailable && (
|
|
730
|
+
<MetricsUnavailableNotice rawError={metricsHistory?.metricsUnavailableReason} diagnosis={metricsHistory?.metricsUnavailableDiagnosis} />
|
|
731
|
+
)}
|
|
732
|
+
{metricsHistory?.collectionError && (
|
|
724
733
|
<div className="mb-3 rounded-lg border border-yellow-500/30 bg-yellow-500/5 px-3 py-2 text-xs text-yellow-400">
|
|
725
734
|
<span className="font-medium">Metrics collection error:</span>{' '}
|
|
726
735
|
<span className="break-all">{metricsHistory.collectionError}</span>
|
|
727
736
|
</div>
|
|
728
737
|
)}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
// Get historical data points (from history or empty)
|
|
739
|
-
const dataPoints = 'dataPoints' in historyContainer ? historyContainer.dataPoints : []
|
|
738
|
+
{(hasMetricsHistory || !showMetricsUnavailable) && (
|
|
739
|
+
<div className="space-y-4">
|
|
740
|
+
{(metricsHistory?.containers || currentMetrics?.containers || []).map((historyContainer) => {
|
|
741
|
+
// Find current metrics for this container
|
|
742
|
+
const currentContainerMetrics = currentMetrics?.containers?.find(c => c.name === historyContainer.name)
|
|
743
|
+
// Find the container spec to compare against limits
|
|
744
|
+
const containerSpec = containers.find((c: any) => c.name === historyContainer.name)
|
|
745
|
+
const limits = containerSpec?.resources?.limits
|
|
746
|
+
const requests = containerSpec?.resources?.requests
|
|
740
747
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
<div className="flex items-center justify-between mb-3">
|
|
744
|
-
<span className="text-sm font-medium text-theme-text-primary">{historyContainer.name}</span>
|
|
745
|
-
</div>
|
|
748
|
+
// Get historical data points (from history or empty)
|
|
749
|
+
const dataPoints = 'dataPoints' in historyContainer ? historyContainer.dataPoints : []
|
|
746
750
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
<MetricsChart
|
|
752
|
-
dataPoints={dataPoints}
|
|
753
|
-
type="cpu"
|
|
754
|
-
height={80}
|
|
755
|
-
showAxis={true}
|
|
756
|
-
limit={limits?.cpu}
|
|
757
|
-
request={requests?.cpu}
|
|
758
|
-
/>
|
|
759
|
-
</div>
|
|
760
|
-
<div>
|
|
761
|
-
<div className="text-xs text-theme-text-tertiary mb-2">Memory</div>
|
|
762
|
-
<MetricsChart
|
|
763
|
-
dataPoints={dataPoints}
|
|
764
|
-
type="memory"
|
|
765
|
-
height={80}
|
|
766
|
-
showAxis={true}
|
|
767
|
-
limit={limits?.memory}
|
|
768
|
-
request={requests?.memory}
|
|
769
|
-
/>
|
|
770
|
-
</div>
|
|
751
|
+
return (
|
|
752
|
+
<div key={historyContainer.name} className="card-inner-lg">
|
|
753
|
+
<div className="flex items-center justify-between mb-3">
|
|
754
|
+
<span className="text-sm font-medium text-theme-text-primary">{historyContainer.name}</span>
|
|
771
755
|
</div>
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
756
|
+
|
|
757
|
+
{dataPoints && dataPoints.length > 0 ? (
|
|
758
|
+
<div className="grid grid-cols-2 gap-6">
|
|
759
|
+
<div>
|
|
760
|
+
<div className="text-xs text-theme-text-tertiary mb-2">CPU</div>
|
|
761
|
+
<MetricsChart
|
|
762
|
+
dataPoints={dataPoints}
|
|
763
|
+
type="cpu"
|
|
764
|
+
height={80}
|
|
765
|
+
showAxis={true}
|
|
766
|
+
limit={limits?.cpu}
|
|
767
|
+
request={requests?.cpu}
|
|
768
|
+
/>
|
|
769
|
+
</div>
|
|
770
|
+
<div>
|
|
771
|
+
<div className="text-xs text-theme-text-tertiary mb-2">Memory</div>
|
|
772
|
+
<MetricsChart
|
|
773
|
+
dataPoints={dataPoints}
|
|
774
|
+
type="memory"
|
|
775
|
+
height={80}
|
|
776
|
+
showAxis={true}
|
|
777
|
+
limit={limits?.memory}
|
|
778
|
+
request={requests?.memory}
|
|
779
|
+
/>
|
|
782
780
|
</div>
|
|
783
781
|
</div>
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
782
|
+
) : currentContainerMetrics ? (
|
|
783
|
+
/* Fallback to simple display if no history yet */
|
|
784
|
+
<div className="grid grid-cols-2 gap-4 text-xs">
|
|
785
|
+
<div>
|
|
786
|
+
<div className="text-theme-text-tertiary mb-1">CPU</div>
|
|
787
|
+
<div className="flex items-baseline gap-1">
|
|
788
|
+
<span className="text-sm font-medium text-blue-400">{currentContainerMetrics.usage.cpu}</span>
|
|
789
|
+
{limits?.cpu && (
|
|
790
|
+
<span className="text-theme-text-tertiary">/ {limits.cpu} limit</span>
|
|
791
|
+
)}
|
|
792
|
+
</div>
|
|
793
|
+
</div>
|
|
794
|
+
<div>
|
|
795
|
+
<div className="text-theme-text-tertiary mb-1">Memory</div>
|
|
796
|
+
<div className="flex items-baseline gap-1">
|
|
797
|
+
<span className="text-sm font-medium text-purple-400">{currentContainerMetrics.usage.memory}</span>
|
|
798
|
+
{limits?.memory && (
|
|
799
|
+
<span className="text-theme-text-tertiary">/ {limits.memory} limit</span>
|
|
800
|
+
)}
|
|
801
|
+
</div>
|
|
791
802
|
</div>
|
|
792
803
|
</div>
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
)
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
{
|
|
804
|
+
) : (
|
|
805
|
+
<div className="text-xs text-theme-text-tertiary">Collecting metrics data...</div>
|
|
806
|
+
)}
|
|
807
|
+
</div>
|
|
808
|
+
)
|
|
809
|
+
})}
|
|
810
|
+
</div>
|
|
811
|
+
)}
|
|
812
|
+
{!showMetricsUnavailable && currentMetrics?.timestamp && (
|
|
802
813
|
<div className="mt-2 text-xs text-theme-text-tertiary">
|
|
803
|
-
Last updated: {new Date(
|
|
814
|
+
Last updated: {new Date(currentMetrics.timestamp).toLocaleTimeString()}
|
|
804
815
|
</div>
|
|
805
816
|
)}
|
|
806
817
|
</Section>
|
|
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react'
|
|
|
2
2
|
import { Bell, Search, ChevronRight } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
5
|
+
import { Input } from '../../ui/Input'
|
|
5
6
|
import { BADGE_SEVERITY_COLORS } from '../../ui/Badge'
|
|
6
7
|
import {
|
|
7
8
|
getPrometheusRuleGroups,
|
|
@@ -190,8 +191,7 @@ export function PrometheusRuleRenderer({ data }: PrometheusRuleRendererProps) {
|
|
|
190
191
|
{totalRules > 5 && (
|
|
191
192
|
<div className="relative mb-3">
|
|
192
193
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
193
|
-
<
|
|
194
|
-
type="text"
|
|
194
|
+
<Input
|
|
195
195
|
placeholder="Filter rules by name or expression..."
|
|
196
196
|
value={searchTerm}
|
|
197
197
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
2
|
import { Package, ChevronDown, ChevronRight, Search } from 'lucide-react'
|
|
3
3
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
|
+
import { Input } from '../../ui/Input'
|
|
4
5
|
import { formatAge } from '../resource-utils'
|
|
5
6
|
import { formatTrivyImage } from './trivy-shared'
|
|
6
7
|
|
|
@@ -74,8 +75,7 @@ export function SbomReportRenderer({ data }: SbomReportRendererProps) {
|
|
|
74
75
|
<div className="flex items-center gap-2 mb-2 px-1">
|
|
75
76
|
<div className="relative flex-1">
|
|
76
77
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
77
|
-
<
|
|
78
|
-
type="text"
|
|
78
|
+
<Input
|
|
79
79
|
placeholder="Filter by name, type, or purl..."
|
|
80
80
|
value={searchTerm}
|
|
81
81
|
onChange={(e) => { setSearchTerm(e.target.value); setShowAll(false) }}
|