@skyhook-io/k8s-ui 1.14.2 → 1.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/package.json +2 -2
  2. package/src/components/logs/LogCore.tsx +182 -16
  3. package/src/components/logs/LogsViewer.tsx +4 -21
  4. package/src/components/logs/WorkloadLogsViewer.tsx +5 -23
  5. package/src/components/resources/ResourcesView.tsx +150 -125
  6. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +2 -2
  7. package/src/components/resources/renderers/AlertRenderer.tsx +3 -3
  8. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +5 -5
  9. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -1
  10. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -3
  11. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +5 -5
  12. package/src/components/resources/renderers/ChallengeRenderer.tsx +8 -8
  13. package/src/components/resources/renderers/CiliumNetworkPolicyRenderer.tsx +3 -3
  14. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +2 -2
  15. package/src/components/resources/renderers/ClusterNetworkPolicyRenderer.tsx +3 -3
  16. package/src/components/resources/renderers/GatewayRenderer.tsx +2 -2
  17. package/src/components/resources/renderers/GenericRenderer.tsx +1 -1
  18. package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -1
  19. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +3 -3
  20. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +2 -2
  21. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -3
  22. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +2 -2
  23. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -1
  24. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +23 -20
  25. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +2 -2
  26. package/src/components/resources/renderers/NodeRenderer.tsx +3 -3
  27. package/src/components/resources/renderers/OrderRenderer.tsx +6 -6
  28. package/src/components/resources/renderers/RoleRenderer.tsx +3 -3
  29. package/src/components/resources/renderers/RolloutRenderer.tsx +2 -2
  30. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -3
  31. package/src/components/resources/renderers/SecretStoreRenderer.tsx +8 -8
  32. package/src/components/resources/renderers/VPARenderer.tsx +1 -1
  33. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -1
  34. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -9
  35. package/src/components/resources/renderers/badge-no-handrolled.test.tsx +6 -12
  36. package/src/components/resources/renderers/capi-cells.tsx +3 -0
  37. package/src/components/resources/renderers/certmanager-cells.tsx +2 -2
  38. package/src/components/resources/renderers/cnpg-cells.tsx +1 -1
  39. package/src/components/resources/renderers/eso-cells.tsx +17 -1
  40. package/src/components/resources/renderers/knative-cells.tsx +14 -1
  41. package/src/components/resources/renderers/kyverno-cells.tsx +14 -3
  42. package/src/components/resources/renderers/kyverno-modern-cells.tsx +6 -0
  43. package/src/components/resources/renderers/trivy-cells.tsx +7 -0
  44. package/src/components/resources/renderers/velero-cells.tsx +3 -0
  45. package/src/components/resources/resource-utils-calico.test.ts +6 -0
  46. package/src/components/resources/resource-utils-calico.ts +6 -2
  47. package/src/components/resources/resource-utils-certmanager.ts +2 -2
  48. package/src/components/resources/resource-utils-eso.test.ts +26 -1
  49. package/src/components/resources/resource-utils-eso.ts +13 -0
  50. package/src/components/resources/resource-utils-knative.ts +3 -3
  51. package/src/components/resources/resource-utils-trivy.ts +3 -0
  52. package/src/components/resources/resource-utils.ts +17 -1
  53. package/src/components/resources/resources-column-filter.test.ts +20 -0
  54. package/src/components/topology/TopologyGraph.tsx +18 -1
  55. package/src/components/topology/pod-phase-fallback.test.ts +24 -0
  56. package/src/theme/components.css +29 -0
  57. package/src/utils/badge-colors.ts +1 -1
  58. package/src/utils/index.ts +1 -0
  59. package/src/utils/log-export.test.ts +225 -0
  60. package/src/utils/log-export.ts +149 -0
  61. package/src/utils/navigation.test.ts +10 -0
  62. package/src/utils/navigation.ts +4 -0
@@ -12,6 +12,7 @@ import {
12
12
  getClusterClassStatus,
13
13
  getMachineHealthCheckStatus, getMachineHealthCheckHealthy, getMachineHealthCheckClusterName,
14
14
  getClusterProvider,
15
+ getMachineDeploymentUpToDate,
15
16
  } from '../resource-utils-capi'
16
17
 
17
18
  function StatusBadge({ resource, getStatus }: { resource: any; getStatus: (r: any) => { text: string; color: string } }) {
@@ -78,6 +79,8 @@ export function CAPIMachineDeploymentCell({ resource, column }: { resource: any;
78
79
  return <TextCell value={getMachineClusterName(resource)} />
79
80
  case 'ready':
80
81
  return <TextCell value={getMachineDeploymentReplicas(resource)} />
82
+ case 'upToDate':
83
+ return <TextCell value={getMachineDeploymentUpToDate(resource)} />
81
84
  case 'version':
82
85
  return <TextCell value={getMachineDeploymentVersion(resource)} />
83
86
  default:
@@ -48,8 +48,8 @@ export function CertificateCell({ resource, column }: { resource: any; column: s
48
48
  case 'expires': {
49
49
  const expiry = getCertificateExpiry(resource)
50
50
  return (
51
- <span className={clsx(
52
- 'text-sm font-medium',
51
+ <span title={expiry.text} className={clsx(
52
+ 'text-sm font-medium truncate block',
53
53
  expiry.level === 'unhealthy' ? 'text-red-400' :
54
54
  expiry.level === 'degraded' ? 'text-yellow-400' :
55
55
  expiry.level === 'healthy' ? 'text-green-400' :
@@ -287,7 +287,7 @@ export function CNPGPoolerCell({ resource, column }: { resource: any; column: st
287
287
  return (
288
288
  <span className={clsx(
289
289
  'badge-sm',
290
- type === 'rw' ? 'bg-blue-500/20 text-blue-400' : 'bg-purple-500/20 text-purple-400'
290
+ type === 'rw' ? 'status-blue' : 'status-purple'
291
291
  )}>
292
292
  {type}
293
293
  </span>
@@ -4,6 +4,7 @@ import { clsx } from 'clsx'
4
4
  import {
5
5
  getExternalSecretStatus,
6
6
  getExternalSecretStore,
7
+ getExternalSecretStoreKey,
7
8
  getExternalSecretRefreshInterval,
8
9
  getExternalSecretLastSync,
9
10
  getClusterExternalSecretStatus,
@@ -14,7 +15,11 @@ import {
14
15
  getClusterSecretStoreStatus,
15
16
  } from '../resource-utils-eso'
16
17
 
17
- export function ExternalSecretCell({ resource, column }: { resource: any; column: string }) {
18
+ export function ExternalSecretCell({ resource, column, storeProviders }: {
19
+ resource: any
20
+ column: string
21
+ storeProviders?: Record<string, string>
22
+ }) {
18
23
  switch (column) {
19
24
  case 'status': {
20
25
  const status = getExternalSecretStatus(resource)
@@ -32,6 +37,17 @@ export function ExternalSecretCell({ resource, column }: { resource: any; column
32
37
  </span>
33
38
  )
34
39
  }
40
+ case 'provider': {
41
+ // The provider lives on the referenced store, not on this object. Three
42
+ // states, and the middle one matters: no map yet means the stores are
43
+ // still loading, which is not the same as a store we looked for and could
44
+ // not read.
45
+ if (!storeProviders) return <span className="text-sm text-theme-text-tertiary">…</span>
46
+ const provider = storeProviders[getExternalSecretStoreKey(resource)]
47
+ return provider
48
+ ? <span className="text-sm text-theme-text-secondary truncate block" title={provider}>{provider}</span>
49
+ : <span className="text-sm text-theme-text-tertiary" title="The referenced store could not be read">-</span>
50
+ }
35
51
  case 'refreshInterval': {
36
52
  const interval = getExternalSecretRefreshInterval(resource)
37
53
  return <span className="text-sm text-theme-text-secondary">{interval}</span>
@@ -28,6 +28,9 @@ import {
28
28
  getDomainMappingUrl,
29
29
  getServerlessServiceMode,
30
30
  } from '../resource-utils-knative'
31
+ // Knative's Certificate reports status.notAfter like any other TLS certificate,
32
+ // so it shares the expiry formatter rather than re-deriving the thresholds.
33
+ import { getCertificateExpiry, healthColors } from '../resource-utils'
31
34
 
32
35
  function StatusCell({ resource, getStatus }: { resource: any; getStatus: (r: any) => { text: string; color: string } }) {
33
36
  const status = getStatus(resource)
@@ -366,6 +369,16 @@ export function KnativeCertificateCell({ resource, column }: { resource: any; co
366
369
  }
367
370
  case 'secretName':
368
371
  return <TextCell value={resource?.spec?.secretName || '-'} />
372
+ case 'expires': {
373
+ const expiry = getCertificateExpiry(resource)
374
+ // "Expired 365d ago" is far wider than the column; bound it like the
375
+ // other status badges that carry arbitrary-length text.
376
+ return (
377
+ <span className={clsx('badge truncate max-w-full', healthColors[expiry.level])} title={expiry.text}>
378
+ {expiry.text}
379
+ </span>
380
+ )
381
+ }
369
382
  default:
370
383
  return <DefaultCell />
371
384
  }
@@ -384,7 +397,7 @@ export function ServerlessServiceCell({ resource, column }: { resource: any; col
384
397
  return (
385
398
  <span className={clsx(
386
399
  'badge',
387
- mode === 'Proxy' ? 'bg-yellow-500/20 text-yellow-400' : 'bg-green-500/20 text-green-400'
400
+ mode === 'Proxy' ? 'status-amber' : 'status-green'
388
401
  )}>
389
402
  {mode}
390
403
  </span>
@@ -8,6 +8,7 @@ import {
8
8
  getKyvernoPolicyStatus,
9
9
  getKyvernoEnforcement,
10
10
  getKyvernoPolicyRuleCount,
11
+ getKyvernoPolicyRuleTypes,
11
12
  } from '../resource-utils-kyverno'
12
13
  import {
13
14
  getKyvernoRequestState,
@@ -83,16 +84,26 @@ export function KyvernoPolicyCell({ resource, column }: { resource: any; column:
83
84
  return (
84
85
  <span className={clsx(
85
86
  'badge',
87
+ // Enforce and Audit are configured postures, so they take colour-named
88
+ // accents. A discrepancy is not a posture — the policy declares Enforce
89
+ // while admission evaluation is off, so it blocks nothing it claims to
90
+ // — and that is a fault, which is what the alert tier is for.
86
91
  blocks
87
- ? 'bg-red-500/20 text-red-400'
92
+ ? 'status-red'
88
93
  : discrepancy
89
- ? 'bg-orange-500/20 text-orange-400'
90
- : 'bg-yellow-500/20 text-yellow-400',
94
+ ? 'status-alert'
95
+ : 'status-amber',
91
96
  )}>
92
97
  {label}
93
98
  </span>
94
99
  )
95
100
  }
101
+ case 'ruleTypes': {
102
+ const types = getKyvernoPolicyRuleTypes(resource)
103
+ return (
104
+ <span className="text-sm text-theme-text-secondary truncate block" title={types}>{types}</span>
105
+ )
106
+ }
96
107
  case 'rules': {
97
108
  const count = getKyvernoPolicyRuleCount(resource)
98
109
  return <span className="text-sm text-theme-text-secondary">{count}</span>
@@ -129,6 +129,12 @@ export function KyvernoCleanupPolicyCell({ resource, column }: { resource: any;
129
129
  const status = getKyvernoCleanupPolicyStatus(resource)
130
130
  return <span className={clsx('badge', status.color)}>{status.text}</span>
131
131
  }
132
+ case 'lastRun': {
133
+ const last = getKyvernoLastExecutionTime(resource)
134
+ return last
135
+ ? <span className="text-sm text-theme-text-secondary">{formatAge(last)}</span>
136
+ : <span className="text-sm text-theme-text-tertiary">-</span>
137
+ }
132
138
  case 'schedule': {
133
139
  const schedule = getKyvernoCleanupSchedule(resource)
134
140
  return schedule ? (
@@ -6,6 +6,7 @@ import {
6
6
  getVulnerabilityReportSummary,
7
7
  getVulnerabilityReportContainer,
8
8
  getVulnerabilityReportImage,
9
+ getSbomReportImage,
9
10
  getConfigAuditReportSummary,
10
11
  getConfigAuditReportStatus,
11
12
  getExposedSecretReportSummary,
@@ -133,6 +134,12 @@ export function ClusterComplianceReportCell({ resource, column }: { resource: an
133
134
 
134
135
  export function SbomReportCell({ resource, column }: { resource: any; column: string }) {
135
136
  switch (column) {
137
+ case 'image': {
138
+ const image = getSbomReportImage(resource)
139
+ return (
140
+ <span className="text-sm text-theme-text-secondary truncate block" title={image}>{image}</span>
141
+ )
142
+ }
136
143
  case 'container':
137
144
  return <span className="text-sm text-theme-text-secondary">{getSbomReportContainer(resource)}</span>
138
145
  case 'components': {
@@ -30,6 +30,7 @@ import {
30
30
  getVSLConfig,
31
31
  getBackupRepositoryStatus,
32
32
  getBackupRepositoryType,
33
+ getBackupRepositoryVolumeNamespace,
33
34
  } from '../resource-utils-velero'
34
35
 
35
36
  // The detail-page Phase value. The badge carries the same label the table
@@ -235,6 +236,8 @@ export function BackupRepositoryCell({ resource, column }: { resource: any; colu
235
236
  </span>
236
237
  )
237
238
  }
239
+ case 'volumeNamespace':
240
+ return <span className="text-sm text-theme-text-secondary">{getBackupRepositoryVolumeNamespace(resource)}</span>
238
241
  case 'repositoryType': {
239
242
  const type = getBackupRepositoryType(resource)
240
243
  // restic is on its way out (no new backups since v1.17, restore dropped in
@@ -174,6 +174,12 @@ describe('IPPool derivations', () => {
174
174
  getCalicoIPPoolAllowedUses({ spec: { allowedUses: ['Tunnel'] } }),
175
175
  ).toBe('Tunnel')
176
176
  })
177
+
178
+ it('treats an empty allowedUses list as the default, not as no uses', () => {
179
+ // Calico: "If not specified or empty, defaults to ["Tunnel", "Workload"]".
180
+ // A dash here would read as a pool that allocates for nothing.
181
+ expect(getCalicoIPPoolAllowedUses({ spec: { allowedUses: [] } })).toBe('Workload, Tunnel')
182
+ })
177
183
  })
178
184
 
179
185
  describe('derived policy types', () => {
@@ -256,6 +256,10 @@ export function getCalicoIPPoolEncapsulation(pool: any): string {
256
256
 
257
257
  export function getCalicoIPPoolAllowedUses(pool: any): string {
258
258
  const uses = pool?.spec?.allowedUses
259
- if (!Array.isArray(uses)) return 'Workload, Tunnel'
260
- return uses.length > 0 ? uses.map(String).join(', ') : '-'
259
+ // Calico documents an empty list as equivalent to an absent one: "If not
260
+ // specified or empty, defaults to ["Tunnel", "Workload"] for
261
+ // back-compatibility." Rendering a dash there would read as a pool that
262
+ // allocates for nothing.
263
+ if (!Array.isArray(uses) || uses.length === 0) return 'Workload, Tunnel'
264
+ return uses.map(String).join(', ')
261
265
  }
@@ -126,7 +126,7 @@ export function getOrderState(order: any): StatusBadge {
126
126
  case 'valid':
127
127
  return { text: 'Valid', color: healthColors.healthy, level: 'healthy' }
128
128
  case 'ready':
129
- return { text: 'Ready', color: 'bg-blue-500/20 text-blue-400', level: 'healthy' }
129
+ return { text: 'Ready', color: 'status-blue', level: 'healthy' }
130
130
  case 'pending':
131
131
  return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
132
132
  case 'invalid':
@@ -162,7 +162,7 @@ export function getChallengeState(challenge: any): StatusBadge {
162
162
  case 'valid':
163
163
  return { text: 'Valid', color: healthColors.healthy, level: 'healthy' }
164
164
  case 'ready':
165
- return { text: 'Ready', color: 'bg-blue-500/20 text-blue-400', level: 'healthy' }
165
+ return { text: 'Ready', color: 'status-blue', level: 'healthy' }
166
166
  case 'pending':
167
167
  return { text: 'Pending', color: healthColors.degraded, level: 'degraded' }
168
168
  case 'processing':
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { getSecretStoreProviderType, getSecretStoreProviderKey } from './resource-utils-eso'
2
+ import { getSecretStoreProviderType, getSecretStoreProviderKey, getExternalSecretStoreKey } from './resource-utils-eso'
3
3
 
4
4
  // The provider label answers "which backend holds these secrets?", so it has to
5
5
  // track the upstream `spec.provider` field names rather than plausible ones.
@@ -54,3 +54,28 @@ describe('getSecretStoreProviderKey', () => {
54
54
  expect(key('ParameterStore')).toBe('aws')
55
55
  })
56
56
  })
57
+
58
+ describe('getExternalSecretStoreKey', () => {
59
+ // A ClusterSecretStore is referenceable from any namespace, so keying it by
60
+ // namespace would miss every ExternalSecret outside the store's own.
61
+ it('keys a ClusterSecretStore by name alone', () => {
62
+ expect(getExternalSecretStoreKey({
63
+ metadata: { namespace: 'prod' },
64
+ spec: { secretStoreRef: { name: 'vault-global', kind: 'ClusterSecretStore' } },
65
+ })).toBe('vault-global')
66
+ })
67
+
68
+ it('scopes a SecretStore to the ExternalSecret namespace', () => {
69
+ expect(getExternalSecretStoreKey({
70
+ metadata: { namespace: 'prod' },
71
+ spec: { secretStoreRef: { name: 'aws', kind: 'SecretStore' } },
72
+ })).toBe('prod/aws')
73
+ })
74
+
75
+ it('treats an omitted kind as a SecretStore, as the CRD defaults it', () => {
76
+ expect(getExternalSecretStoreKey({
77
+ metadata: { namespace: 'prod' },
78
+ spec: { secretStoreRef: { name: 'aws' } },
79
+ })).toBe('prod/aws')
80
+ })
81
+ })
@@ -235,6 +235,19 @@ export function getExternalSecretStore(resource: any): { name: string; kind: str
235
235
  }
236
236
  }
237
237
 
238
+ /**
239
+ * Key for looking this ExternalSecret's store up in a provider map.
240
+ *
241
+ * A ClusterSecretStore is cluster-scoped and referenceable from any namespace,
242
+ * so it keys by name alone; a SecretStore only resolves within the
243
+ * ExternalSecret's own namespace.
244
+ */
245
+ export function getExternalSecretStoreKey(resource: any): string {
246
+ const store = getExternalSecretStore(resource)
247
+ if (store.kind === 'ClusterSecretStore') return store.name
248
+ return `${resource?.metadata?.namespace ?? ''}/${store.name}`
249
+ }
250
+
238
251
  export function getExternalSecretRefreshInterval(resource: any): string {
239
252
  return resource.spec?.refreshInterval || '-'
240
253
  }
@@ -62,14 +62,14 @@ export function getRevisionStatus(resource: any): StatusBadge {
62
62
  if (ready?.status === 'True' && active?.status === 'False') {
63
63
  const reason = active.reason || ''
64
64
  if (reason === 'NoTraffic') {
65
- return { text: 'Scaled to Zero', color: 'bg-blue-500/20 text-blue-400', level: 'healthy' }
65
+ return { text: 'Scaled to Zero', color: 'status-blue', level: 'healthy' }
66
66
  }
67
- return { text: reason || 'Inactive', color: 'bg-yellow-500/20 text-yellow-400', level: 'degraded' }
67
+ return { text: reason || 'Inactive', color: 'status-amber', level: 'degraded' }
68
68
  }
69
69
 
70
70
  // Check for activating: Active condition is Unknown (scaling up)
71
71
  if (ready?.status === 'True' && active?.status === 'Unknown') {
72
- return { text: 'Activating', color: 'bg-blue-500/20 text-blue-400', level: 'healthy' }
72
+ return { text: 'Activating', color: 'status-blue', level: 'healthy' }
73
73
  }
74
74
 
75
75
  return getKnativeConditionStatus(resource)
@@ -102,6 +102,9 @@ export const getExposedSecretReportSummary = getVulnerabilityReportSummary
102
102
  export const getExposedSecretReportContainer = getVulnerabilityReportContainer
103
103
  export const getExposedSecretReportImage = getVulnerabilityReportImage
104
104
 
105
+ // SBOM reports carry the same report.artifact/registry shape as the scan reports.
106
+ export const getSbomReportImage = getVulnerabilityReportImage
107
+
105
108
  export const getExposedSecretReportStatus = getVulnerabilityReportStatus
106
109
 
107
110
  // ============================================================================
@@ -11,7 +11,8 @@ import { getNodePoolStatus, getNodeClaimStatus } from './resource-utils-karpente
11
11
  import { getScaledObjectStatus, getScaledJobStatus } from './resource-utils-keda'
12
12
  import { getGitRepositoryStatus, getOCIRepositoryStatus, getHelmRepositoryStatus, getHelmRepositoryType, getKustomizationStatus, getFluxHelmReleaseStatus, getFluxAlertStatus } from './resource-utils-flux'
13
13
  import { getArgoApplicationStatus, getArgoApplicationSetStatus, getArgoApplicationSync, getArgoApplicationHealth, getArgoApplicationProject } from './resource-utils-argo'
14
- import { getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
14
+ import { getVulnerabilityReportImage as _getVulnerabilityReportImage } from './resource-utils-trivy'
15
+ import { getKyvernoPolicyRuleTypes as _getKyvernoPolicyRuleTypes, getPolicyReportStatus as _getPolicyReportStatus, getKyvernoPolicyStatus as _getKyvernoPolicyStatus } from './resource-utils-kyverno'
15
16
  import { getResourceClaimStatus as _getResourceClaimStatus, getResourceClaimDeviceClasses as _getResourceClaimDeviceClasses, getResourceClaimTemplateDeviceClasses as _getResourceClaimTemplateDeviceClasses, getResourceClaimAllocation as _getResourceClaimAllocation, getResourceClaimReservedFor as _getResourceClaimReservedFor } from './resource-utils-dra'
16
17
  import { getNvidiaClusterPolicyStatus as _getNvidiaClusterPolicyStatus, getNvidiaClusterPolicyEnabledComponents as _getNvidiaClusterPolicyEnabledComponents, getNvidiaDriverStatus as _getNvidiaDriverStatus } from './resource-utils-nvidia'
17
18
  import { getClusterQueueStatus as _getClusterQueueStatus, getLocalQueueStatus as _getLocalQueueStatus, getKueueWorkloadStatus as _getKueueWorkloadStatus, getResourceFlavorStatus as _getResourceFlavorStatus, getAdmissionCheckStatus as _getAdmissionCheckStatus, getProvisioningRequestStatus as _getProvisioningRequestStatus } from './resource-utils-kueue'
@@ -2594,6 +2595,20 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
2594
2595
  case 'mig':
2595
2596
  if (kindLower === 'nvidiaclusterpolicies') return resource.spec?.mig?.strategy || ''
2596
2597
  break
2598
+ // Trivy keeps the scanned image under report.artifact, so the generic
2599
+ // spec/status probe finds nothing and the header would offer a filter
2600
+ // button that never populates.
2601
+ case 'image':
2602
+ if (kindLower === 'vulnerabilityreports' || kindLower === 'sbomreports'
2603
+ || kindLower === 'clustersbomreports' || kindLower === 'exposedsecretreports') {
2604
+ return _getVulnerabilityReportImage(resource)
2605
+ }
2606
+ break
2607
+ case 'ruleTypes':
2608
+ if (kindLower === 'kyvernopolicies' || kindLower === 'clusterpolicies') {
2609
+ return _getKyvernoPolicyRuleTypes(resource)
2610
+ }
2611
+ break
2597
2612
  // The fallback would read spec.timeZone and yield '' for an undeclared
2598
2613
  // zone, so those rows would drop out of a filter the cell shows a value for.
2599
2614
  case 'timeZone':
@@ -2622,6 +2637,7 @@ export {
2622
2637
  getVulnerabilityReportSummary,
2623
2638
  getVulnerabilityReportStatus,
2624
2639
  getVulnerabilityReportImage,
2640
+ getSbomReportImage,
2625
2641
  getVulnerabilityReportContainer,
2626
2642
  getConfigAuditReportSummary,
2627
2643
  getConfigAuditReportStatus,
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest'
2
+ import { getCellFilterValue } from './resource-utils'
2
3
  import { getCellFilterKind, hasCuratedColumns, isColumnFilterableByDistinctCount, normalizeKindToPlural, SKIP_FILTER_COLUMNS } from './ResourcesView'
3
4
 
4
5
  describe('isColumnFilterableByDistinctCount', () => {
@@ -98,3 +99,22 @@ describe('GPU ecosystem column routing', () => {
98
99
  expect(hasCuratedColumns('inferenceobjectives', 'example.io')).toBe(false)
99
100
  })
100
101
  })
102
+
103
+ describe('filter values for columns whose data sits outside spec/status', () => {
104
+ // getCellFilterValue's fallback probes status[key]/spec[key]. A column whose
105
+ // value lives elsewhere needs an explicit case, or the header reserves a
106
+ // filter button that can never populate.
107
+ it('reads the scanned image out of the Trivy report', () => {
108
+ const report = {
109
+ report: { registry: { server: 'ghcr.io' }, artifact: { repository: 'acme/api', tag: 'v2' } },
110
+ }
111
+ for (const kind of ['vulnerabilityreports', 'sbomreports', 'clustersbomreports']) {
112
+ expect(getCellFilterValue(report, 'image', kind)).toContain('acme/api')
113
+ }
114
+ })
115
+
116
+ it('reads Kyverno rule types out of the rules array', () => {
117
+ const policy = { spec: { rules: [{ validate: {} }, { mutate: {} }] } }
118
+ expect(getCellFilterValue(policy, 'ruleTypes', 'clusterpolicies')).toBe('validate, mutate')
119
+ })
120
+ })
@@ -245,6 +245,23 @@ interface TopologyGraphProps {
245
245
  children?: ReactNode
246
246
  }
247
247
 
248
+ // A pod group's children normally carry the health the server computed. Without
249
+ // it, the phase is honest for every state except Running: a crash-looping pod
250
+ // sits at Phase=Running with its container restarting, so that one case says
251
+ // unknown rather than repeating the bug this fallback exists behind.
252
+ export function phaseOnlyHealth(phase: string | undefined): HealthStatus {
253
+ switch (phase) {
254
+ case 'Failed':
255
+ return 'unhealthy'
256
+ case 'Pending':
257
+ return 'degraded'
258
+ case 'Succeeded':
259
+ return 'neutral'
260
+ default:
261
+ return 'unknown'
262
+ }
263
+ }
264
+
248
265
  export function TopologyGraph({
249
266
  topology,
250
267
  viewMode,
@@ -430,7 +447,7 @@ export function TopologyGraph({
430
447
  // the kubelet's Waiting->Running oscillation, which the phase alone
431
448
  // hides — a crash-looping pod sits at Phase=Running. Deriving it here
432
449
  // again would rebuild that logic in a second place and get it wrong.
433
- status: pod.status ?? 'unknown',
450
+ status: pod.status ?? phaseOnlyHealth(pod.phase),
434
451
  data: {
435
452
  ...podGroupNode.data,
436
453
  namespace: pod.namespace,
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { phaseOnlyHealth } from './TopologyGraph'
3
+
4
+ // Only reached when the server sent no per-pod status — an older Radar behind a
5
+ // newer embedding host. The point of the fallback is that it must not repeat the
6
+ // bug it stands in for.
7
+ describe('phaseOnlyHealth', () => {
8
+ it('does not call a Running pod healthy', () => {
9
+ // A crash-looping pod sits at Phase=Running with its container restarting,
10
+ // so Running is the one phase that hides a fault.
11
+ expect(phaseOnlyHealth('Running')).toBe('unknown')
12
+ })
13
+
14
+ it('reads the phases that are honest on their own', () => {
15
+ expect(phaseOnlyHealth('Failed')).toBe('unhealthy')
16
+ expect(phaseOnlyHealth('Pending')).toBe('degraded')
17
+ expect(phaseOnlyHealth('Succeeded')).toBe('neutral')
18
+ })
19
+
20
+ it('says nothing for a phase it does not recognise', () => {
21
+ expect(phaseOnlyHealth(undefined)).toBe('unknown')
22
+ expect(phaseOnlyHealth('SomeFuturePhase')).toBe('unknown')
23
+ })
24
+ })
@@ -228,6 +228,35 @@
228
228
  border-color: transparent;
229
229
  }
230
230
 
231
+ /* Colour-named accents carry no health meaning — they exist so an
232
+ informational badge can pick a hue without borrowing a status semantic.
233
+ A categorical badge (an admission verb, a policy action, a configured
234
+ mode) belongs here; only evidence about a resource's condition belongs on
235
+ .status-healthy / -degraded / -unhealthy. */
236
+ .status-green {
237
+ background-color: light-dark(rgba(34, 197, 94, 0.15), rgba(34, 197, 94, 0.2));
238
+ color: light-dark(#15803d, #4ade80);
239
+ border-color: transparent;
240
+ }
241
+
242
+ .status-red {
243
+ background-color: light-dark(rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.2));
244
+ color: light-dark(#b91c1c, #f87171);
245
+ border-color: transparent;
246
+ }
247
+
248
+ .status-amber {
249
+ background-color: light-dark(rgba(234, 179, 8, 0.15), rgba(234, 179, 8, 0.2));
250
+ color: light-dark(#a16207, #facc15);
251
+ border-color: transparent;
252
+ }
253
+
254
+ .status-blue {
255
+ background-color: light-dark(rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.2));
256
+ color: light-dark(#2563eb, #60a5fa);
257
+ border-color: transparent;
258
+ }
259
+
231
260
  .status-cyan {
232
261
  background-color: light-dark(rgba(6, 182, 212, 0.15), rgba(6, 182, 212, 0.2));
233
262
  color: light-dark(#0891b2, #22d3ee);
@@ -74,7 +74,7 @@ export const NODEPOOL_MODE_BADGE: Record<string, string> = {
74
74
 
75
75
  // Translucent gray badge for "inactive / unknown / pending / unset / disabled" states.
76
76
  // The de-facto fallback in renderers when no severity or category applies.
77
- export const BADGE_INACTIVE = 'bg-gray-500/20 text-gray-400'
77
+ export const BADGE_INACTIVE = 'status-unknown'
78
78
 
79
79
  // Best practices category colors
80
80
  export const BP_CATEGORY_BADGE: Record<string, string> = {
@@ -7,6 +7,7 @@ export * from './navigation'
7
7
  export * from './context-name'
8
8
  export * from './animation'
9
9
  export * from './resource-hierarchy'
10
+ export * from './log-export'
10
11
  export * from './log-format'
11
12
  export * from './download'
12
13
  export * from './clipboard'