@skyhook-io/k8s-ui 1.5.10 → 1.5.12
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/audit/AuditFindingsTable.tsx +66 -30
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +65 -12
- package/src/components/dock/BottomDock.tsx +23 -12
- package/src/components/gitops/ManagedResourcesList.tsx +4 -2
- package/src/components/logs/LogCore.tsx +109 -14
- package/src/components/resources/ResourcesView.tsx +19 -2
- package/src/components/resources/get-pod-phase-display.test.ts +254 -0
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +2 -4
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -8
- package/src/components/resources/renderers/ChallengeRenderer.tsx +2 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +2 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +2 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +5 -4
- package/src/components/resources/renderers/IngressClassRenderer.tsx +2 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +2 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +3 -2
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +2 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +2 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +5 -5
- package/src/components/resources/renderers/OrderRenderer.tsx +2 -1
- package/src/components/resources/renderers/PodRenderer.tsx +23 -2
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +3 -2
- package/src/components/resources/renderers/RolloutRenderer.tsx +2 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +15 -6
- package/src/components/resources/renderers/aws-capi-cells.tsx +2 -4
- package/src/components/resources/renderers/azure-capi-cells.tsx +3 -8
- package/src/components/resources/renderers/istio-cells.tsx +3 -2
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -1
- package/src/components/resources/resource-utils-karpenter.ts +0 -6
- package/src/components/resources/resource-utils.ts +110 -0
- package/src/components/resources/resources-search-sidebar-hint.test.ts +85 -0
- package/src/components/timeline/shared.tsx +5 -6
- package/src/components/topology/TopologySearch.tsx +49 -16
- package/src/components/topology/topology-search-match.test.ts +112 -0
- package/src/components/ui/Tooltip.tsx +73 -40
- package/src/components/ui/tooltip-position.test.ts +132 -0
- package/src/components/ui/tooltip-position.ts +86 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +3 -3
- package/src/components/workload/WorkloadView.tsx +6 -6
- package/src/types/core.ts +13 -1
- package/src/utils/badge-colors.ts +19 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/validators.test.ts +207 -0
- package/src/utils/validators.ts +178 -0
- package/src/utils/view-transition.test.ts +81 -0
- package/src/utils/view-transition.ts +62 -0
|
@@ -156,7 +156,7 @@ const WORKLOAD_KINDS = new Set(['deployments', 'statefulsets', 'daemonsets'])
|
|
|
156
156
|
|
|
157
157
|
// Columns to skip for auto-detected filters (high cardinality, text-like, or non-filterable)
|
|
158
158
|
const SKIP_FILTER_COLUMNS = new Set([
|
|
159
|
-
'name', '
|
|
159
|
+
'name', 'age', 'keys', 'size', 'images', 'domains', 'hosts', 'rules',
|
|
160
160
|
'ports', 'message', 'url', 'ref', 'revision', 'path', 'selector', 'ready', 'restarts',
|
|
161
161
|
'completions', 'duration', 'schedule', 'lastRun', 'target', 'replicas', 'metrics',
|
|
162
162
|
'capacity', 'accessModes', 'volume', 'step', 'progress', 'template', 'expires',
|
|
@@ -3471,7 +3471,7 @@ export function ResourcesView({
|
|
|
3471
3471
|
{lastUpdated && (
|
|
3472
3472
|
<div className="flex items-center gap-1.5 text-xs text-theme-text-tertiary">
|
|
3473
3473
|
<Clock className="w-3.5 h-3.5" />
|
|
3474
|
-
<span>Updated {formatAge(lastUpdated.toISOString())}</span>
|
|
3474
|
+
<span>Updated <span className="inline-block min-w-[4ch] tabular-nums">{formatAge(lastUpdated.toISOString())}</span></span>
|
|
3475
3475
|
</div>
|
|
3476
3476
|
)}
|
|
3477
3477
|
{/* Column picker */}
|
|
@@ -3578,6 +3578,23 @@ export function ResourcesView({
|
|
|
3578
3578
|
<X className="w-3.5 h-3.5" />
|
|
3579
3579
|
</button>
|
|
3580
3580
|
)}
|
|
3581
|
+
{/*
|
|
3582
|
+
The sidebar's count badge shows the cluster-wide
|
|
3583
|
+
total for the selected kind (from resourceCounts) and
|
|
3584
|
+
deliberately stays unfiltered. When a search yields
|
|
3585
|
+
zero results the user can think the badge is lying.
|
|
3586
|
+
Spell out that the badge is the cluster total, not
|
|
3587
|
+
the filtered count.
|
|
3588
|
+
*/}
|
|
3589
|
+
{searchTerm && (() => {
|
|
3590
|
+
const totalForKind = counts[selectedKind.group ? `${selectedKind.group}/${selectedKind.kind}` : selectedKind.kind] ?? 0
|
|
3591
|
+
if (totalForKind === 0) return null
|
|
3592
|
+
return (
|
|
3593
|
+
<p className="text-xs mt-1 text-theme-text-disabled">
|
|
3594
|
+
The sidebar shows {pluralize(totalForKind, selectedKind.kind)} in the cluster — the count is unfiltered.
|
|
3595
|
+
</p>
|
|
3596
|
+
)
|
|
3597
|
+
})()}
|
|
3581
3598
|
{namespaces.length > 0 && <p className="text-sm mt-1 text-theme-text-disabled">Searching in {namespaces.length === 1 ? `namespace: ${namespaces[0]}` : `${namespaces.length} namespaces`}</p>}
|
|
3582
3599
|
{/* Show active filters as dismissible badges so user can clear them */}
|
|
3583
3600
|
{(() => {
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getPodPhaseDisplay } from './resource-utils'
|
|
3
|
+
|
|
4
|
+
const podRunningHealthy = {
|
|
5
|
+
status: {
|
|
6
|
+
phase: 'Running',
|
|
7
|
+
containerStatuses: [{ name: 'app', ready: true, restartCount: 0 }],
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const podRunningNotReady = {
|
|
12
|
+
status: {
|
|
13
|
+
phase: 'Running',
|
|
14
|
+
containerStatuses: [{ name: 'app', ready: false, restartCount: 0 }],
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const podRunningCycling = {
|
|
19
|
+
status: {
|
|
20
|
+
phase: 'Running',
|
|
21
|
+
containerStatuses: [{ name: 'app', ready: true, restartCount: 6301 }],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const podRunningNotReadyAndCycling = {
|
|
26
|
+
status: {
|
|
27
|
+
phase: 'Running',
|
|
28
|
+
containerStatuses: [{ name: 'app', ready: false, restartCount: 6301 }],
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const podCrashLooping = {
|
|
33
|
+
status: {
|
|
34
|
+
phase: 'Running',
|
|
35
|
+
containerStatuses: [
|
|
36
|
+
{
|
|
37
|
+
name: 'app',
|
|
38
|
+
ready: false,
|
|
39
|
+
restartCount: 12,
|
|
40
|
+
state: { waiting: { reason: 'CrashLoopBackOff' } },
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const podImagePullBackOff = {
|
|
47
|
+
status: {
|
|
48
|
+
phase: 'Pending',
|
|
49
|
+
containerStatuses: [
|
|
50
|
+
{
|
|
51
|
+
name: 'app',
|
|
52
|
+
ready: false,
|
|
53
|
+
restartCount: 0,
|
|
54
|
+
state: { waiting: { reason: 'ImagePullBackOff' } },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const podOOMKilled = {
|
|
61
|
+
status: {
|
|
62
|
+
phase: 'Running',
|
|
63
|
+
containerStatuses: [
|
|
64
|
+
{
|
|
65
|
+
name: 'app',
|
|
66
|
+
ready: false,
|
|
67
|
+
restartCount: 3,
|
|
68
|
+
state: { terminated: { reason: 'OOMKilled', exitCode: 137 } },
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const podTerminating = {
|
|
75
|
+
metadata: { deletionTimestamp: '2026-04-29T10:00:00Z' },
|
|
76
|
+
status: {
|
|
77
|
+
phase: 'Running',
|
|
78
|
+
containerStatuses: [{ name: 'app', ready: true, restartCount: 0 }],
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const podErrImagePull = {
|
|
83
|
+
status: {
|
|
84
|
+
phase: 'Pending',
|
|
85
|
+
containerStatuses: [
|
|
86
|
+
{
|
|
87
|
+
name: 'app',
|
|
88
|
+
ready: false,
|
|
89
|
+
restartCount: 0,
|
|
90
|
+
state: { waiting: { reason: 'ErrImagePull' } },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const podCreateContainerConfigError = {
|
|
97
|
+
status: {
|
|
98
|
+
phase: 'Pending',
|
|
99
|
+
containerStatuses: [
|
|
100
|
+
{
|
|
101
|
+
name: 'app',
|
|
102
|
+
ready: false,
|
|
103
|
+
restartCount: 0,
|
|
104
|
+
state: { waiting: { reason: 'CreateContainerConfigError' } },
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const podMultiContainerPartiallyReady = {
|
|
111
|
+
status: {
|
|
112
|
+
phase: 'Running',
|
|
113
|
+
containerStatuses: [
|
|
114
|
+
{ name: 'app', ready: true, restartCount: 0 },
|
|
115
|
+
{ name: 'sidecar', ready: false, restartCount: 0 },
|
|
116
|
+
{ name: 'proxy', ready: true, restartCount: 0 },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const podMultiContainerCyclingAcrossContainers = {
|
|
122
|
+
status: {
|
|
123
|
+
phase: 'Running',
|
|
124
|
+
containerStatuses: [
|
|
125
|
+
{ name: 'app', ready: true, restartCount: 3 },
|
|
126
|
+
{ name: 'sidecar', ready: true, restartCount: 4 },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const podSucceededWithOOMKilledSidecar = {
|
|
132
|
+
status: {
|
|
133
|
+
phase: 'Succeeded',
|
|
134
|
+
containerStatuses: [
|
|
135
|
+
{ name: 'main', ready: false, restartCount: 0, state: { terminated: { reason: 'Completed', exitCode: 0 } } },
|
|
136
|
+
{ name: 'sidecar', ready: false, restartCount: 1, state: { terminated: { reason: 'OOMKilled', exitCode: 137 } } },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe('getPodPhaseDisplay', () => {
|
|
142
|
+
it('returns Running, healthy for a fully-ready pod with no restarts', () => {
|
|
143
|
+
const r = getPodPhaseDisplay(podRunningHealthy)
|
|
144
|
+
expect(r.phase).toBe('Running')
|
|
145
|
+
expect(r.text).toBe('Running')
|
|
146
|
+
expect(r.level).toBe('healthy')
|
|
147
|
+
expect(r.hint).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('downgrades a Running pod that is not ready (the "0/1" case)', () => {
|
|
151
|
+
const r = getPodPhaseDisplay(podRunningNotReady)
|
|
152
|
+
expect(r.phase).toBe('Running')
|
|
153
|
+
expect(r.text).toBe('Running — Not Ready (0/1)')
|
|
154
|
+
expect(r.level).toBe('degraded')
|
|
155
|
+
expect(r.hint).toMatch(/not ready/i)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('downgrades a Running pod with high restarts even if currently ready', () => {
|
|
159
|
+
const r = getPodPhaseDisplay(podRunningCycling)
|
|
160
|
+
expect(r.text).toBe('Running — Restarting (6301 restarts)')
|
|
161
|
+
expect(r.level).toBe('degraded')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('marks a Running + Not Ready + cycling pod as unhealthy', () => {
|
|
165
|
+
const r = getPodPhaseDisplay(podRunningNotReadyAndCycling)
|
|
166
|
+
expect(r.text).toContain('Not Ready (0/1)')
|
|
167
|
+
expect(r.text).toContain('6301 restarts')
|
|
168
|
+
expect(r.level).toBe('unhealthy')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('always preserves the raw phase field so kubectl users can correlate', () => {
|
|
172
|
+
expect(getPodPhaseDisplay(podRunningNotReady).phase).toBe('Running')
|
|
173
|
+
expect(getPodPhaseDisplay(podCrashLooping).phase).toBe('Running')
|
|
174
|
+
expect(getPodPhaseDisplay(podImagePullBackOff).phase).toBe('Pending')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('flags CrashLoopBackOff as unhealthy with phase preserved', () => {
|
|
178
|
+
const r = getPodPhaseDisplay(podCrashLooping)
|
|
179
|
+
expect(r.text).toBe('Running — CrashLoopBackOff')
|
|
180
|
+
expect(r.level).toBe('unhealthy')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('flags ImagePullBackOff as unhealthy on a Pending pod', () => {
|
|
184
|
+
const r = getPodPhaseDisplay(podImagePullBackOff)
|
|
185
|
+
expect(r.text).toBe('Pending — ImagePullBackOff')
|
|
186
|
+
expect(r.level).toBe('unhealthy')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('flags OOMKilled even when phase is Running', () => {
|
|
190
|
+
const r = getPodPhaseDisplay(podOOMKilled)
|
|
191
|
+
expect(r.text).toBe('Running — OOMKilled')
|
|
192
|
+
expect(r.level).toBe('unhealthy')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('marks pods with deletionTimestamp as Terminating regardless of phase', () => {
|
|
196
|
+
const r = getPodPhaseDisplay(podTerminating)
|
|
197
|
+
expect(r.text).toBe('Running — Terminating')
|
|
198
|
+
expect(r.level).toBe('degraded')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('falls back to Unknown for missing/unknown phase', () => {
|
|
202
|
+
expect(getPodPhaseDisplay({}).level).toBe('unknown')
|
|
203
|
+
expect(getPodPhaseDisplay({}).phase).toBe('Unknown')
|
|
204
|
+
expect(getPodPhaseDisplay({ status: { phase: 'Banana' } }).level).toBe('unknown')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('handles Succeeded/Pending/Failed phases', () => {
|
|
208
|
+
expect(getPodPhaseDisplay({ status: { phase: 'Succeeded' } }).level).toBe('neutral')
|
|
209
|
+
expect(getPodPhaseDisplay({ status: { phase: 'Pending' } }).level).toBe('degraded')
|
|
210
|
+
expect(getPodPhaseDisplay({ status: { phase: 'Failed' } }).level).toBe('unhealthy')
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('flags ErrImagePull as unhealthy', () => {
|
|
214
|
+
const r = getPodPhaseDisplay(podErrImagePull)
|
|
215
|
+
expect(r.text).toBe('Pending — ErrImagePull')
|
|
216
|
+
expect(r.level).toBe('unhealthy')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('flags CreateContainerConfigError as unhealthy', () => {
|
|
220
|
+
const r = getPodPhaseDisplay(podCreateContainerConfigError)
|
|
221
|
+
expect(r.text).toBe('Pending — CreateContainerConfigError')
|
|
222
|
+
expect(r.level).toBe('unhealthy')
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('reports the correct readiness ratio for multi-container pods', () => {
|
|
226
|
+
const r = getPodPhaseDisplay(podMultiContainerPartiallyReady)
|
|
227
|
+
expect(r.text).toBe('Running — Not Ready (2/3)')
|
|
228
|
+
expect(r.level).toBe('degraded')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('sums restartCount across containers when deciding cycling', () => {
|
|
232
|
+
const r = getPodPhaseDisplay(podMultiContainerCyclingAcrossContainers)
|
|
233
|
+
expect(r.text).toBe('Running — Restarting (7 restarts)')
|
|
234
|
+
expect(r.level).toBe('degraded')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('does not flag a Succeeded Job pod as unhealthy when a sidecar OOMed', () => {
|
|
238
|
+
const r = getPodPhaseDisplay(podSucceededWithOOMKilledSidecar)
|
|
239
|
+
expect(r.phase).toBe('Succeeded')
|
|
240
|
+
expect(r.text).toBe('Completed')
|
|
241
|
+
expect(r.level).toBe('neutral')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('does not mistake exactly-RESTART_CYCLING_THRESHOLD restarts for cycling (boundary)', () => {
|
|
245
|
+
const r = getPodPhaseDisplay({
|
|
246
|
+
status: {
|
|
247
|
+
phase: 'Running',
|
|
248
|
+
containerStatuses: [{ name: 'app', ready: true, restartCount: 5 }],
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
expect(r.text).toBe('Running')
|
|
252
|
+
expect(r.level).toBe('healthy')
|
|
253
|
+
})
|
|
254
|
+
})
|
|
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
|
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
4
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
5
5
|
import { getAWSMMPStatus, getAWSMMPInstanceType, getAWSMMPCapacityType, getAWSMMPAMIType, getAWSMMPNodegroupName, getAWSMMPScaling } from '../resource-utils-aws-capi'
|
|
6
|
+
import { CAPACITY_TYPE_BADGE } from '../../../utils/badge-colors'
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
data: any
|
|
@@ -39,10 +40,7 @@ export function AWSManagedMachinePoolRenderer({ data }: Props) {
|
|
|
39
40
|
<Property label="Instance Type" value={getAWSMMPInstanceType(data)} />
|
|
40
41
|
<Property label="AMI Type" value={getAWSMMPAMIType(data)} />
|
|
41
42
|
<Property label="Capacity Type" value={
|
|
42
|
-
<span className={clsx('badge badge-sm', capacityType === 'spot'
|
|
43
|
-
? 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-400 dark:border-amber-700/40'
|
|
44
|
-
: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40'
|
|
45
|
-
)}>{capacityType === 'onDemand' ? 'On-Demand' : capacityType}</span>
|
|
43
|
+
<span className={clsx('badge badge-sm', capacityType === 'spot' ? CAPACITY_TYPE_BADGE.spot : CAPACITY_TYPE_BADGE.onDemand)}>{capacityType === 'onDemand' ? 'On-Demand' : capacityType}</span>
|
|
46
44
|
} />
|
|
47
45
|
{spec.roleName && <Property label="IAM Role" value={spec.roleName} />}
|
|
48
46
|
</PropertyList>
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type ArgoAppStatus,
|
|
10
10
|
type ArgoResource,
|
|
11
11
|
} from '../../../types/gitops'
|
|
12
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
12
13
|
|
|
13
14
|
interface ArgoApplicationRendererProps {
|
|
14
15
|
data: any
|
|
@@ -192,7 +193,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
192
193
|
<span
|
|
193
194
|
className={clsx(
|
|
194
195
|
'badge',
|
|
195
|
-
syncPolicy.automated ? 'bg-green-500/20 text-green-400' :
|
|
196
|
+
syncPolicy.automated ? 'bg-green-500/20 text-green-400' : BADGE_INACTIVE
|
|
196
197
|
)}
|
|
197
198
|
>
|
|
198
199
|
{syncPolicy.automated ? 'Enabled' : 'Disabled'}
|
|
@@ -241,7 +242,7 @@ export function ArgoApplicationRenderer({ data, onTerminate, isTerminating }: Ar
|
|
|
241
242
|
? 'bg-blue-500/20 text-blue-400'
|
|
242
243
|
: operationState.phase === 'Failed' || operationState.phase === 'Error'
|
|
243
244
|
? 'bg-red-500/20 text-red-400'
|
|
244
|
-
:
|
|
245
|
+
: BADGE_INACTIVE
|
|
245
246
|
)}
|
|
246
247
|
>
|
|
247
248
|
{operationState.phase}
|
|
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
|
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
4
|
import { getCAPIConditions } from '../resource-utils-capi'
|
|
5
5
|
import { getAzureMMPStatus, getAzureMMPSKU, getAzureMMPMode, getAzureMMPOSDiskInfo, getAzureMMPScaling, getAzureMMPScaleSetPriority, getAzureMMPOSType } from '../resource-utils-azure-capi'
|
|
6
|
+
import { CAPACITY_TYPE_BADGE, NODEPOOL_MODE_BADGE } from '../../../utils/badge-colors'
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
data: any
|
|
@@ -34,18 +35,12 @@ export function AzureManagedMachinePoolRenderer({ data }: Props) {
|
|
|
34
35
|
{spec.name && <Property label="Pool Name" value={spec.name} />}
|
|
35
36
|
<Property label="VM Size" value={getAzureMMPSKU(data)} />
|
|
36
37
|
<Property label="Mode" value={
|
|
37
|
-
<span className={clsx('badge badge-sm', mode
|
|
38
|
-
? 'bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-950/50 dark:text-purple-400 dark:border-purple-700/40'
|
|
39
|
-
: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40'
|
|
40
|
-
)}>{mode}</span>
|
|
38
|
+
<span className={clsx('badge badge-sm', NODEPOOL_MODE_BADGE[mode] || NODEPOOL_MODE_BADGE.User)}>{mode}</span>
|
|
41
39
|
} />
|
|
42
40
|
<Property label="OS" value={getAzureMMPOSType(data)} />
|
|
43
41
|
<Property label="OS Disk" value={getAzureMMPOSDiskInfo(data)} />
|
|
44
42
|
<Property label="Priority" value={
|
|
45
|
-
<span className={clsx('badge badge-sm', priority === 'Spot'
|
|
46
|
-
? 'bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950/50 dark:text-amber-400 dark:border-amber-700/40'
|
|
47
|
-
: 'bg-sky-100 text-sky-700 border-sky-300 dark:bg-sky-950/50 dark:text-sky-400 dark:border-sky-700/40'
|
|
48
|
-
)}>{priority}</span>
|
|
43
|
+
<span className={clsx('badge badge-sm', priority === 'Spot' ? CAPACITY_TYPE_BADGE.spot : CAPACITY_TYPE_BADGE.regular)}>{priority}</span>
|
|
49
44
|
} />
|
|
50
45
|
{spec.maxPods != null && <Property label="Max Pods" value={String(spec.maxPods)} />}
|
|
51
46
|
</PropertyList>
|
|
@@ -2,6 +2,7 @@ import { Shield, AlertTriangle, Globe, Key, FileText } from 'lucide-react'
|
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection } from '../../ui/drawer-components'
|
|
4
4
|
import { getSolverType } from './ClusterIssuerRenderer'
|
|
5
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
5
6
|
|
|
6
7
|
function getChallengeStateBadge(state: string): { color: string; text: string } {
|
|
7
8
|
switch (state?.toLowerCase()) {
|
|
@@ -20,7 +21,7 @@ function getChallengeStateBadge(state: string): { color: string; text: string }
|
|
|
20
21
|
case 'errored':
|
|
21
22
|
return { text: 'Errored', color: 'bg-red-500/20 text-red-400' }
|
|
22
23
|
default:
|
|
23
|
-
return { text: state || 'Unknown', color:
|
|
24
|
+
return { text: state || 'Unknown', color: BADGE_INACTIVE }
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -5,6 +5,7 @@ import { Section, PropertyList, Property, AlertBanner } from '../../ui/drawer-co
|
|
|
5
5
|
import { formatAge } from '../resource-utils'
|
|
6
6
|
import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER } from './trivy-shared'
|
|
7
7
|
import { pluralize } from '../../../utils/pluralize'
|
|
8
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
8
9
|
|
|
9
10
|
interface ClusterComplianceReportRendererProps {
|
|
10
11
|
data: any
|
|
@@ -185,7 +186,7 @@ export function ClusterComplianceReportRenderer({ data }: ClusterComplianceRepor
|
|
|
185
186
|
) : (
|
|
186
187
|
<CheckCircle2 className="w-3.5 h-3.5 text-green-400 shrink-0" />
|
|
187
188
|
)}
|
|
188
|
-
<span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', SEVERITY_BADGE_COLORS[check.severity] ||
|
|
189
|
+
<span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', SEVERITY_BADGE_COLORS[check.severity] || BADGE_INACTIVE)}>
|
|
189
190
|
{check.severity || '-'}
|
|
190
191
|
</span>
|
|
191
192
|
<span className="text-xs text-theme-text-secondary truncate flex-1">{check.name || check.id}</span>
|
|
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
|
|
|
4
4
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
5
5
|
import { formatAge } from '../resource-utils'
|
|
6
6
|
import { SEVERITY_BADGE_COLORS, SEVERITY_ORDER, TrivyAlertBanner } from './trivy-shared'
|
|
7
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
7
8
|
|
|
8
9
|
interface ConfigAuditReportRendererProps {
|
|
9
10
|
data: any
|
|
@@ -169,7 +170,7 @@ export function ConfigAuditReportRenderer({ data }: ConfigAuditReportRendererPro
|
|
|
169
170
|
) : (
|
|
170
171
|
<XCircle className="w-3.5 h-3.5 text-red-400 shrink-0" />
|
|
171
172
|
)}
|
|
172
|
-
<span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', SEVERITY_BADGE_COLORS[group.severity] ||
|
|
173
|
+
<span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', SEVERITY_BADGE_COLORS[group.severity] || BADGE_INACTIVE)}>
|
|
173
174
|
{group.severity}
|
|
174
175
|
</span>
|
|
175
176
|
<span className="text-xs text-theme-text-secondary truncate flex-1">{group.title}</span>
|
|
@@ -5,6 +5,7 @@ import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
|
5
5
|
import { formatAge } from '../resource-utils'
|
|
6
6
|
import { SEVERITY_BADGE_COLORS, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
|
|
7
7
|
import { pluralize } from '../../../utils/pluralize'
|
|
8
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
8
9
|
|
|
9
10
|
interface ExposedSecretReportRendererProps {
|
|
10
11
|
data: any
|
|
@@ -92,7 +93,7 @@ export function ExposedSecretReportRenderer({ data }: ExposedSecretReportRendere
|
|
|
92
93
|
<tr key={`${secret.ruleID || i}-${i}`} className="border-b border-theme-border/50 hover:bg-theme-hover/50">
|
|
93
94
|
<td className="py-1.5 px-1 text-theme-text-secondary font-mono">{secret.ruleID || '-'}</td>
|
|
94
95
|
<td className="py-1.5 px-1">
|
|
95
|
-
<span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium', SEVERITY_BADGE_COLORS[secret.severity] ||
|
|
96
|
+
<span className={clsx('px-1.5 py-0.5 rounded text-[10px] font-medium', SEVERITY_BADGE_COLORS[secret.severity] || BADGE_INACTIVE)}>
|
|
96
97
|
{secret.severity || '-'}
|
|
97
98
|
</span>
|
|
98
99
|
</td>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Cpu } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
4
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
4
5
|
|
|
5
6
|
interface GatewayClassRendererProps {
|
|
6
7
|
data: any
|
|
@@ -40,7 +41,7 @@ export function GatewayClassRenderer({ data }: GatewayClassRendererProps) {
|
|
|
40
41
|
? 'bg-green-500/20 text-green-400'
|
|
41
42
|
: isNotAccepted
|
|
42
43
|
? 'bg-red-500/20 text-red-400'
|
|
43
|
-
:
|
|
44
|
+
: BADGE_INACTIVE
|
|
44
45
|
)}>
|
|
45
46
|
{isAccepted ? 'True' : isNotAccepted ? 'False' : 'Unknown'}
|
|
46
47
|
</span>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Globe, Radio, Lock } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
4
5
|
|
|
5
6
|
const protocolColors: Record<string, string> = {
|
|
6
7
|
HTTP: 'bg-blue-500/20 text-blue-400',
|
|
@@ -73,7 +74,7 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
|
|
|
73
74
|
? 'bg-green-500/20 text-green-400'
|
|
74
75
|
: isNotAccepted
|
|
75
76
|
? 'bg-red-500/20 text-red-400'
|
|
76
|
-
:
|
|
77
|
+
: BADGE_INACTIVE
|
|
77
78
|
)}>
|
|
78
79
|
{isAccepted ? 'True' : isNotAccepted ? 'False' : 'Unknown'}
|
|
79
80
|
</span>
|
|
@@ -88,7 +89,7 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
|
|
|
88
89
|
? 'bg-green-500/20 text-green-400'
|
|
89
90
|
: isNotProgrammed
|
|
90
91
|
? 'bg-red-500/20 text-red-400'
|
|
91
|
-
:
|
|
92
|
+
: BADGE_INACTIVE
|
|
92
93
|
)}>
|
|
93
94
|
{isProgrammed ? 'True' : isNotProgrammed ? 'False' : 'Unknown'}
|
|
94
95
|
</span>
|
|
@@ -134,7 +135,7 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
|
|
|
134
135
|
<span className="text-sm font-medium text-theme-text-primary">{listener.name}</span>
|
|
135
136
|
<span className={clsx(
|
|
136
137
|
'px-1.5 py-0.5 rounded text-[10px] font-medium',
|
|
137
|
-
protocolColors[listener.protocol] ||
|
|
138
|
+
protocolColors[listener.protocol] || BADGE_INACTIVE
|
|
138
139
|
)}>
|
|
139
140
|
{listener.protocol}:{listener.port}
|
|
140
141
|
</span>
|
|
@@ -157,7 +158,7 @@ export function GatewayRenderer({ data, onNavigate }: GatewayRendererProps) {
|
|
|
157
158
|
? 'bg-green-500/20 text-green-400'
|
|
158
159
|
: isListenerNotAccepted
|
|
159
160
|
? 'bg-red-500/20 text-red-400'
|
|
160
|
-
:
|
|
161
|
+
: BADGE_INACTIVE
|
|
161
162
|
)}>
|
|
162
163
|
{isListenerAccepted ? '\u2713' : isListenerNotAccepted ? '\u2717' : '?'}
|
|
163
164
|
</span>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Globe } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
4
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
4
5
|
|
|
5
6
|
interface IngressClassRendererProps {
|
|
6
7
|
data: any
|
|
@@ -24,7 +25,7 @@ export function IngressClassRenderer({ data }: IngressClassRendererProps) {
|
|
|
24
25
|
'badge',
|
|
25
26
|
isDefault
|
|
26
27
|
? 'bg-green-500/20 text-green-400'
|
|
27
|
-
:
|
|
28
|
+
: BADGE_INACTIVE
|
|
28
29
|
)}>
|
|
29
30
|
{isDefault ? 'Yes' : 'No'}
|
|
30
31
|
</span>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getAuthorizationPolicyRules,
|
|
7
7
|
getAuthorizationPolicySelector,
|
|
8
8
|
} from '../resource-utils-istio'
|
|
9
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
9
10
|
|
|
10
11
|
const actionColors: Record<string, string> = {
|
|
11
12
|
ALLOW: 'bg-green-500/20 text-green-400',
|
|
@@ -52,7 +53,7 @@ export function IstioAuthorizationPolicyRenderer({ data }: IstioAuthorizationPol
|
|
|
52
53
|
<Property label="Action" value={
|
|
53
54
|
<span className={clsx(
|
|
54
55
|
'badge',
|
|
55
|
-
actionColors[action] ||
|
|
56
|
+
actionColors[action] || BADGE_INACTIVE
|
|
56
57
|
)}>
|
|
57
58
|
{action}
|
|
58
59
|
</span>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getIstioGatewayServers,
|
|
7
7
|
getIstioGatewaySelector,
|
|
8
8
|
} from '../resource-utils-istio'
|
|
9
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
9
10
|
|
|
10
11
|
const protocolColors: Record<string, string> = {
|
|
11
12
|
HTTP: 'bg-blue-500/20 text-blue-400',
|
|
@@ -73,7 +74,7 @@ export function IstioGatewayRenderer({ data }: IstioGatewayRendererProps) {
|
|
|
73
74
|
</span>
|
|
74
75
|
<span className={clsx(
|
|
75
76
|
'px-1.5 py-0.5 rounded text-[10px] font-medium',
|
|
76
|
-
protocolColors[protocol] ||
|
|
77
|
+
protocolColors[protocol] || BADGE_INACTIVE
|
|
77
78
|
)}>
|
|
78
79
|
{protocol}:{server.port.number}
|
|
79
80
|
</span>
|
|
@@ -98,7 +99,7 @@ export function IstioGatewayRenderer({ data }: IstioGatewayRendererProps) {
|
|
|
98
99
|
? 'bg-green-500/20 text-green-400'
|
|
99
100
|
: server.tls.mode === 'PASSTHROUGH'
|
|
100
101
|
? 'bg-blue-500/20 text-blue-400'
|
|
101
|
-
:
|
|
102
|
+
: BADGE_INACTIVE
|
|
102
103
|
)}>
|
|
103
104
|
{server.tls.mode || 'SIMPLE'}
|
|
104
105
|
</span>
|
|
@@ -6,12 +6,13 @@ import {
|
|
|
6
6
|
getPeerAuthenticationSelector,
|
|
7
7
|
getPeerAuthenticationPortLevelMtls,
|
|
8
8
|
} from '../resource-utils-istio'
|
|
9
|
+
import { BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
9
10
|
|
|
10
11
|
const modeColors: Record<string, string> = {
|
|
11
12
|
STRICT: 'bg-green-500/20 text-green-400',
|
|
12
13
|
PERMISSIVE: 'bg-yellow-500/20 text-yellow-400',
|
|
13
14
|
DISABLE: 'bg-red-500/20 text-red-400',
|
|
14
|
-
UNSET:
|
|
15
|
+
UNSET: BADGE_INACTIVE,
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
interface IstioPeerAuthenticationRendererProps {
|
|
@@ -2,8 +2,8 @@ import { Server, Cpu, Settings } from 'lucide-react'
|
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
3
|
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
import { kindToPlural } from '../../../utils/navigation'
|
|
5
|
+
import { CAPACITY_TYPE_BADGE, BADGE_INACTIVE } from '../../../utils/badge-colors'
|
|
5
6
|
import {
|
|
6
|
-
CAPACITY_TYPE_BADGE,
|
|
7
7
|
getNodeClaimStatus,
|
|
8
8
|
getNodeClaimInstanceType,
|
|
9
9
|
getNodeClaimNodeName,
|
|
@@ -188,7 +188,7 @@ export function KarpenterNodeClaimRenderer({ data, onNavigate }: KarpenterNodeCl
|
|
|
188
188
|
isComplete && 'bg-green-500/20 text-green-400',
|
|
189
189
|
isCurrent && 'bg-blue-500/20 text-blue-400',
|
|
190
190
|
isFailed && 'bg-red-500/20 text-red-400',
|
|
191
|
-
isPending && !isCurrent &&
|
|
191
|
+
isPending && !isCurrent && BADGE_INACTIVE
|
|
192
192
|
)}
|
|
193
193
|
>
|
|
194
194
|
{isComplete ? '\u2713' : isFailed ? '\u2717' : isCurrent ? '\u25CF' : '\u25CB'}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { KarpenterNodePoolRenderer } from './KarpenterNodePoolRenderer'
|
|
4
|
+
|
|
5
|
+
describe('KarpenterNodePoolRenderer', () => {
|
|
6
|
+
it('renders CPU quantities expressed as millicore strings', () => {
|
|
7
|
+
const html = renderToString(
|
|
8
|
+
<KarpenterNodePoolRenderer
|
|
9
|
+
data={{
|
|
10
|
+
spec: { limits: { cpu: '12000m' } },
|
|
11
|
+
status: { resources: { cpu: '6000m' } },
|
|
12
|
+
}}
|
|
13
|
+
/>,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
expect(html).toContain('6 / 12')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders non-string CPU quantities without throwing', () => {
|
|
20
|
+
expect(() =>
|
|
21
|
+
renderToString(
|
|
22
|
+
<KarpenterNodePoolRenderer
|
|
23
|
+
data={{
|
|
24
|
+
spec: { limits: { cpu: 16 } },
|
|
25
|
+
status: { resources: { cpu: 12 } },
|
|
26
|
+
}}
|
|
27
|
+
/>,
|
|
28
|
+
),
|
|
29
|
+
).not.toThrow()
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
getNodePoolWeight,
|
|
10
10
|
} from '../resource-utils-karpenter'
|
|
11
11
|
|
|
12
|
-
function formatCpuCores(value:
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
const millis = parseInt(
|
|
12
|
+
function formatCpuCores(value: unknown): string {
|
|
13
|
+
const quantity = String(value)
|
|
14
|
+
if (quantity.endsWith('m')) {
|
|
15
|
+
const millis = parseInt(quantity, 10)
|
|
16
16
|
if (!isNaN(millis)) return String(millis / 1000)
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return quantity
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
|