@skyhook-io/k8s-ui 1.12.0 → 1.13.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/package.json +4 -4
  2. package/src/components/applications/ApplicationDetail.tsx +2 -2
  3. package/src/components/cluster-switcher/ClusterSwitcher.tsx +32 -7
  4. package/src/components/issues/severity.ts +1 -0
  5. package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
  6. package/src/components/resources/ResourcesView.tsx +419 -9
  7. package/src/components/resources/index.ts +27 -0
  8. package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
  9. package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
  10. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
  11. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
  12. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
  13. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +47 -4
  14. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
  15. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
  16. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
  17. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
  18. package/src/components/resources/renderers/CNPGShared.tsx +27 -0
  19. package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
  20. package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
  21. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
  22. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
  23. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
  24. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
  25. package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
  26. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
  27. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
  28. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
  29. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
  30. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
  31. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
  32. package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
  33. package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
  34. package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
  35. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
  36. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
  37. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
  38. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
  39. package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
  40. package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
  41. package/src/components/resources/renderers/PolicySection.tsx +32 -5
  42. package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
  43. package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
  44. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
  45. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
  46. package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
  47. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
  48. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
  49. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
  50. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
  51. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
  52. package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
  53. package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
  54. package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
  55. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
  56. package/src/components/resources/renderers/calico-cells.tsx +177 -0
  57. package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
  58. package/src/components/resources/renderers/index.ts +16 -1
  59. package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
  60. package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
  61. package/src/components/resources/renderers/velero-cells.tsx +29 -7
  62. package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
  63. package/src/components/resources/resource-utils-calico.test.ts +215 -0
  64. package/src/components/resources/resource-utils-calico.ts +261 -0
  65. package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
  66. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
  67. package/src/components/resources/resource-utils-cnpg.test.ts +54 -0
  68. package/src/components/resources/resource-utils-cnpg.ts +264 -3
  69. package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
  70. package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
  71. package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
  72. package/src/components/resources/resource-utils-kyverno.ts +43 -7
  73. package/src/components/resources/resource-utils-velero.test.ts +26 -0
  74. package/src/components/resources/resource-utils-velero.ts +68 -0
  75. package/src/components/resources/resource-utils.ts +98 -0
  76. package/src/components/shared/ResourceActionsBar.tsx +101 -16
  77. package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
  78. package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
  79. package/src/components/shared/index.ts +1 -1
  80. package/src/components/shared/revision-role-badges.test.ts +120 -0
  81. package/src/components/topology/K8sResourceNode.test.ts +10 -0
  82. package/src/components/topology/K8sResourceNode.tsx +22 -0
  83. package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
  84. package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
  85. package/src/components/topology/TopologyGraph.tsx +6 -6
  86. package/src/components/topology/TopologySearch.tsx +7 -0
  87. package/src/components/topology/layout.ts +3 -0
  88. package/src/components/trace/ReachabilityView.tsx +1 -0
  89. package/src/components/trace/TracePanel.tsx +13 -2
  90. package/src/components/trace/reachFixtures.ts +26 -1
  91. package/src/components/trace/reachGraphModel.test.ts +20 -0
  92. package/src/components/trace/reachGraphModel.ts +8 -1
  93. package/src/components/trace/reachOrigins.test.ts +31 -0
  94. package/src/components/trace/reachOrigins.ts +11 -1
  95. package/src/components/ui/Badge.tsx +10 -0
  96. package/src/components/ui/InClusterConsentDialog.tsx +5 -4
  97. package/src/components/ui/ScheduleValue.test.tsx +80 -0
  98. package/src/components/ui/ScheduleValue.tsx +85 -0
  99. package/src/components/ui/SelectMenu.tsx +202 -28
  100. package/src/components/ui/drawer-components.test.tsx +17 -1
  101. package/src/components/ui/drawer-components.tsx +9 -2
  102. package/src/components/ui/index.ts +2 -0
  103. package/src/components/workload/WorkloadView.tsx +5 -2
  104. package/src/types/core.ts +36 -4
  105. package/src/types/policy.ts +153 -2
  106. package/src/utils/clipboard.test.ts +86 -0
  107. package/src/utils/clipboard.ts +50 -0
  108. package/src/utils/extended-resources.ts +4 -7
  109. package/src/utils/format.test.ts +83 -1
  110. package/src/utils/format.ts +80 -1
  111. package/src/utils/inClusterConsent.test.ts +9 -0
  112. package/src/utils/inClusterConsent.ts +9 -9
  113. package/src/utils/index.ts +1 -0
  114. package/src/utils/navigation.test.ts +5 -0
  115. package/src/utils/navigation.ts +5 -0
  116. package/src/utils/resource-icons.test.ts +49 -2
  117. package/src/utils/resource-icons.ts +29 -2
@@ -24,6 +24,14 @@ import {
24
24
  getCNPGPoolerType,
25
25
  getCNPGPoolerMode,
26
26
  getCNPGPoolerInstances,
27
+ getCNPGObjectStoreStatus,
28
+ getCNPGObjectStoreDestination,
29
+ getCNPGObjectStoreRetention,
30
+ getCNPGObjectStoreRecoveryWindows,
31
+ getCNPGDeclarativeStatus,
32
+ getCNPGDeclarativeCluster,
33
+ getCNPGDeclarativeMessage,
34
+ getCNPGImageCatalogEntries,
27
35
  } from '../resource-utils-cnpg'
28
36
 
29
37
  /** Terminal = the operator has stopped reconciling; neighbouring cells are stale. */
@@ -122,6 +130,98 @@ export function CNPGBackupCell({ resource, column }: { resource: any; column: st
122
130
  }
123
131
  }
124
132
 
133
+ export function CNPGDeclarativeCell({ resource, column }: { resource: any; column: string }) {
134
+ switch (column) {
135
+ case 'status': {
136
+ const status = getCNPGDeclarativeStatus(resource)
137
+ return <span className={clsx('badge', status.color)}>{status.text}</span>
138
+ }
139
+ case 'cluster':
140
+ return (
141
+ <span className="text-sm text-theme-text-secondary truncate block">
142
+ {getCNPGDeclarativeCluster(resource) || '-'}
143
+ </span>
144
+ )
145
+ case 'target':
146
+ return (
147
+ <span className="text-sm text-theme-text-secondary truncate block">
148
+ {resource?.spec?.name || '-'}
149
+ </span>
150
+ )
151
+ case 'dbname':
152
+ return (
153
+ <span className="text-sm text-theme-text-secondary truncate block">
154
+ {resource?.spec?.dbname || resource?.spec?.owner || '-'}
155
+ </span>
156
+ )
157
+ case 'message':
158
+ return (
159
+ <span
160
+ className="text-sm text-theme-text-secondary truncate block"
161
+ title={getCNPGDeclarativeMessage(resource) || ''}
162
+ >
163
+ {getCNPGDeclarativeMessage(resource) || '-'}
164
+ </span>
165
+ )
166
+ default:
167
+ return <span className="text-sm text-theme-text-tertiary">-</span>
168
+ }
169
+ }
170
+
171
+ export function CNPGImageCatalogCell({ resource, column }: { resource: any; column: string }) {
172
+ const entries = getCNPGImageCatalogEntries(resource)
173
+ switch (column) {
174
+ case 'majors':
175
+ return (
176
+ <span className="text-sm text-theme-text-secondary">
177
+ {entries.length === 0 ? 'none' : entries.map((e) => e.major).join(', ')}
178
+ </span>
179
+ )
180
+ case 'images':
181
+ return <span className="text-sm text-theme-text-secondary">{String(entries.length)}</span>
182
+ default:
183
+ return <span className="text-sm text-theme-text-tertiary">-</span>
184
+ }
185
+ }
186
+
187
+ export function CNPGObjectStoreCell({ resource, column }: { resource: any; column: string }) {
188
+ switch (column) {
189
+ case 'status': {
190
+ const status = getCNPGObjectStoreStatus(resource)
191
+ return <span className={clsx('badge', status.color)}>{status.text}</span>
192
+ }
193
+ case 'destination':
194
+ // `block` is load-bearing: an inline span with `truncate` establishes no
195
+ // box, so the column collapses to almost nothing regardless of its width.
196
+ // Every sibling cell that renders a long value pairs the two.
197
+ return (
198
+ <span className="text-sm text-theme-text-secondary truncate block">
199
+ {getCNPGObjectStoreDestination(resource)}
200
+ </span>
201
+ )
202
+ case 'retention':
203
+ return (
204
+ <span className="text-sm text-theme-text-secondary">
205
+ {getCNPGObjectStoreRetention(resource) || '-'}
206
+ </span>
207
+ )
208
+ case 'servers': {
209
+ const windows = getCNPGObjectStoreRecoveryWindows(resource)
210
+ if (windows.length === 0) {
211
+ return <span className="text-sm text-theme-text-tertiary">none</span>
212
+ }
213
+ const failing = windows.filter((w) => w.failingSinceLastSuccess).length
214
+ return (
215
+ <span className="text-sm text-theme-text-secondary">
216
+ {failing > 0 ? `${windows.length} (${failing} failing)` : String(windows.length)}
217
+ </span>
218
+ )
219
+ }
220
+ default:
221
+ return <span className="text-sm text-theme-text-tertiary">-</span>
222
+ }
223
+ }
224
+
125
225
  export function CNPGScheduledBackupCell({ resource, column }: { resource: any; column: string }) {
126
226
  switch (column) {
127
227
  case 'status': {
@@ -4,6 +4,12 @@ export * from './argo-cells'
4
4
  export * from './CertificateRenderer'
5
5
  export * from './CertificateRequestRenderer'
6
6
  export * from './certmanager-cells'
7
+ export * from './CalicoHostEndpointRenderer'
8
+ export * from './CalicoIPPoolRenderer'
9
+ export * from './CalicoNetworkPolicyRenderer'
10
+ export * from './CalicoNetworkPolicyDiagram'
11
+ export * from './CalicoTierRenderer'
12
+ export * from './calico-cells'
7
13
  export * from './ChallengeRenderer'
8
14
  export * from './ClusterComplianceReportRenderer'
9
15
  export * from './ClusterExternalSecretRenderer'
@@ -74,6 +80,7 @@ export * from './KyvernoPolicyShared'
74
80
  export * from './KyvernoCELPolicyRenderers'
75
81
  export * from './KyvernoExceptionRenderers'
76
82
  export * from './LeaseRenderer'
83
+ export * from './LookupFailureNote'
77
84
  export * from './NetworkPolicyRenderer'
78
85
  export * from './NetworkPolicyDiagram'
79
86
  export * from './CiliumNetworkPolicyRenderer'
@@ -94,6 +101,7 @@ export * from './ReplicaSetRenderer'
94
101
  export * from './RoleBindingRenderer'
95
102
  export * from './RoleRenderer'
96
103
  export * from './RolloutRenderer'
104
+ export * from './AnalysisRunRenderer'
97
105
  export * from './RuntimeClassRenderer'
98
106
  export * from './SbomReportRenderer'
99
107
  export * from './SealedSecretRenderer'
@@ -109,10 +117,12 @@ export * from './trivy-cells'
109
117
  export * from './trivy-shared'
110
118
  export * from './velero-cells'
111
119
  export * from './VeleroBackupRenderer'
120
+ export * from './VeleroRunMessages'
112
121
  export * from './VeleroBSLRenderer'
113
122
  export * from './VeleroRestoreRenderer'
114
123
  export * from './VeleroScheduleRenderer'
115
124
  export * from './VeleroVSLRenderer'
125
+ export * from './VeleroBackupRepositoryRenderer'
116
126
  export * from './VPARenderer'
117
127
  export * from './VulnerabilityReportRenderer'
118
128
  export * from './WebhookConfigRenderer'
@@ -160,4 +170,9 @@ export * from './ResourceSliceRenderer'
160
170
  export * from './nvidia-cells'
161
171
  export * from './NvidiaClusterPolicyRenderer'
162
172
  export * from './NvidiaDriverRenderer'
163
- export { PolicySection } from './PolicySection'
173
+ export * from './PolicySection'
174
+ export * from './PolicyCoverageSection'
175
+ export * from './KyvernoGlobalContextRenderer'
176
+ export * from './KyvernoQueueRenderers'
177
+ export * from './CNPGObjectStoreRenderer'
178
+ export * from './CNPGDeclarativeRenderer'
@@ -9,6 +9,16 @@ import {
9
9
  getKyvernoPolicyAction,
10
10
  getKyvernoPolicyRuleCount,
11
11
  } from '../resource-utils-kyverno'
12
+ import {
13
+ getKyvernoRequestState,
14
+ getKyvernoRequestType,
15
+ getKyvernoRequestPolicy,
16
+ getKyvernoRequestTriggers,
17
+ getKyvernoReportStatus,
18
+ getKyvernoReportSubject,
19
+ getKyvernoReportSource,
20
+ getKyvernoReportSummary,
21
+ } from '../resource-utils-kyverno-queue'
12
22
 
13
23
  export function PolicyReportCell({ resource, column }: { resource: any; column: string }) {
14
24
  switch (column) {
@@ -92,3 +102,74 @@ export function ClusterPolicyCell({ resource, column }: { resource: any; column:
92
102
  // Same rendering logic as KyvernoPolicy
93
103
  return <KyvernoPolicyCell resource={resource} column={column} />
94
104
  }
105
+
106
+ // ============================================================================
107
+ // UpdateRequest / EphemeralReport
108
+ // ============================================================================
109
+
110
+ export function KyvernoUpdateRequestCell({ resource, column }: { resource: any; column: string }) {
111
+ switch (column) {
112
+ case 'status': {
113
+ const state = getKyvernoRequestState(resource)
114
+ return <span className={clsx('badge', state.color)}>{state.text}</span>
115
+ }
116
+ case 'type': {
117
+ const t = getKyvernoRequestType(resource)
118
+ return <span className="text-sm text-theme-text-secondary">{t === 'unknown' ? '-' : t}</span>
119
+ }
120
+ case 'policy':
121
+ return (
122
+ <span className="text-sm text-theme-text-secondary truncate block" title={getKyvernoRequestPolicy(resource)}>
123
+ {getKyvernoRequestPolicy(resource) || '-'}
124
+ </span>
125
+ )
126
+ case 'triggers': {
127
+ // A generate request carries every trigger it was queued for, so the row
128
+ // has to say "and N more" rather than pretend it is about one resource.
129
+ const triggers = getKyvernoRequestTriggers(resource)
130
+ if (triggers.length === 0) return <span className="text-sm text-theme-text-tertiary">-</span>
131
+ const first = triggers[0]
132
+ const label = `${first.kind ?? ''}/${first.name ?? ''}`
133
+ return (
134
+ <span className="text-sm text-theme-text-secondary truncate block" title={label}>
135
+ {triggers.length === 1 ? label : `${label} +${triggers.length - 1}`}
136
+ </span>
137
+ )
138
+ }
139
+ default:
140
+ return <span className="text-sm text-theme-text-tertiary">-</span>
141
+ }
142
+ }
143
+
144
+ export function KyvernoEphemeralReportCell({ resource, column }: { resource: any; column: string }) {
145
+ switch (column) {
146
+ case 'status': {
147
+ const status = getKyvernoReportStatus(resource)
148
+ return <span className={clsx('badge', status.color)}>{status.text}</span>
149
+ }
150
+ case 'subject': {
151
+ const s = getKyvernoReportSubject(resource)
152
+ if (!s?.kind) return <span className="text-sm text-theme-text-tertiary">-</span>
153
+ const label = s.name ? `${s.kind}/${s.name}` : `${s.kind} (deleted)`
154
+ return (
155
+ <span className="text-sm text-theme-text-secondary truncate block" title={label}>
156
+ {label}
157
+ </span>
158
+ )
159
+ }
160
+ case 'source':
161
+ return (
162
+ <span className="text-sm text-theme-text-secondary truncate block">
163
+ {(getKyvernoReportSource(resource) || '-').replace(/-/g, ' ')}
164
+ </span>
165
+ )
166
+ case 'results':
167
+ return (
168
+ <span className="text-sm text-theme-text-secondary">
169
+ {String(getKyvernoReportSummary(resource).total)}
170
+ </span>
171
+ )
172
+ default:
173
+ return <span className="text-sm text-theme-text-tertiary">-</span>
174
+ }
175
+ }
@@ -0,0 +1,25 @@
1
+ export type NetworkPolicyPeerType =
2
+ | "pod"
3
+ | "namespace"
4
+ | "cidr"
5
+ | "all"
6
+ | "deny"
7
+ | "combined";
8
+
9
+ export const NETWORK_POLICY_PEER_STYLES = {
10
+ pod: "border-emerald-500/30 bg-emerald-500/8",
11
+ namespace: "border-sky-500/30 bg-sky-500/8",
12
+ cidr: "border-amber-500/30 bg-amber-500/8",
13
+ combined: "border-emerald-500/30 bg-emerald-500/8",
14
+ all: "border-theme-border bg-theme-elevated/50",
15
+ deny: "border-red-500/30 bg-red-500/8",
16
+ } as const satisfies Record<NetworkPolicyPeerType, string>;
17
+
18
+ export const NETWORK_POLICY_PEER_DOTS = {
19
+ pod: "bg-emerald-500",
20
+ namespace: "bg-sky-500",
21
+ cidr: "bg-amber-500",
22
+ combined: "bg-emerald-500",
23
+ all: "bg-theme-text-tertiary",
24
+ deny: "bg-red-500",
25
+ } as const satisfies Record<NetworkPolicyPeerType, string>;
@@ -16,6 +16,7 @@ import {
16
16
  getRestoreIncludedNamespaces,
17
17
  getRestoreDuration,
18
18
  getRestoreErrors,
19
+ getRestoreWarnings,
19
20
  getScheduleStatus,
20
21
  getSchedulePaused,
21
22
  getScheduleCronInfo,
@@ -37,6 +38,10 @@ import {
37
38
  // whenever the two differ. Without this the three collapsed partial-failure
38
39
  // phases would be unreachable outside the YAML tab, and the label would stop
39
40
  // being a display choice and start being a loss of information.
41
+ // Foreground colours here carry a light counterpart as well as the dark one.
42
+ // A bare `text-*-400` is tuned for the dark ground and washes out on the light
43
+ // one — the same defect rbac-badges.ts was created to fix, and these cells sit
44
+ // in a table next to badges that already do this correctly.
40
45
  export function VeleroPhaseValue({
41
46
  status,
42
47
  phase,
@@ -84,7 +89,7 @@ export function BackupCell({ resource, column }: { resource: any; column: string
84
89
  case 'expiry': {
85
90
  const exp = getBackupExpiry(resource)
86
91
  const isExpired = exp === 'Expired'
87
- return <span className={clsx('text-sm', isExpired ? 'text-red-400' : 'text-theme-text-secondary')}>{exp}</span>
92
+ return <span className={clsx('text-sm', isExpired ? 'text-red-600 dark:text-red-400' : 'text-theme-text-secondary')}>{exp}</span>
88
93
  }
89
94
  case 'errors': {
90
95
  const errors = getBackupErrors(resource)
@@ -94,9 +99,9 @@ export function BackupCell({ resource, column }: { resource: any; column: string
94
99
  }
95
100
  return (
96
101
  <span className="text-sm">
97
- {errors > 0 && <span className="text-red-400">{errors}E</span>}
102
+ {errors > 0 && <span className="text-red-600 dark:text-red-400">{errors}E</span>}
98
103
  {errors > 0 && warnings > 0 && <span className="text-theme-text-tertiary"> / </span>}
99
- {warnings > 0 && <span className="text-yellow-400">{warnings}W</span>}
104
+ {warnings > 0 && <span className="text-amber-600 dark:text-amber-400">{warnings}W</span>}
100
105
  </span>
101
106
  )
102
107
  }
@@ -128,11 +133,21 @@ export function RestoreCell({ resource, column }: { resource: any; column: strin
128
133
  return <span className="text-sm text-theme-text-secondary">{dur}</span>
129
134
  }
130
135
  case 'errors': {
136
+ // Errors AND warnings, the same shape the Backup column uses. A restore
137
+ // that reported only warnings showed "-" here while carrying a non-zero
138
+ // count on the object, which is the row a reader scans to decide whether
139
+ // the restore is worth opening.
131
140
  const errors = getRestoreErrors(resource)
132
- if (errors === 0) {
141
+ const warnings = getRestoreWarnings(resource)
142
+ if (errors === 0 && warnings === 0) {
133
143
  return <span className="text-sm text-theme-text-tertiary">-</span>
134
144
  }
135
- return <span className="text-sm text-red-400">{errors}</span>
145
+ return (
146
+ <span className="text-sm flex gap-1.5">
147
+ {errors > 0 && <span className="text-red-600 dark:text-red-400">{errors}E</span>}
148
+ {warnings > 0 && <span className="text-amber-600 dark:text-amber-400">{warnings}W</span>}
149
+ </span>
150
+ )
136
151
  }
137
152
  default:
138
153
  return <span className="text-sm text-theme-text-tertiary">-</span>
@@ -157,7 +172,14 @@ export function ScheduleCell({ resource, column }: { resource: any; column: stri
157
172
  </span>
158
173
  {alsoPaused && (
159
174
  <Tooltip content="Also paused — this schedule will not run until it is resumed">
160
- <Pause className="w-3 h-3 shrink-0 text-amber-600 dark:text-amber-400" />
175
+ <>
176
+ <Pause className="w-3 h-3 shrink-0 text-amber-600 dark:text-amber-400" aria-hidden="true" />
177
+ {/* The badge beside this says "Rejected", so without a label the
178
+ row reads as a schedule that is merely broken rather than one
179
+ that is also switched off — and the tooltip carrying that
180
+ second fact needs a pointer to reach. */}
181
+ <span className="sr-only">Also paused — this schedule will not run until it is resumed</span>
182
+ </>
161
183
  </Tooltip>
162
184
  )}
163
185
  </span>
@@ -271,7 +293,7 @@ export function BackupStorageLocationCell({ resource, column }: { resource: any;
271
293
  }
272
294
  case 'default': {
273
295
  const isDefault = getBSLDefault(resource)
274
- return <span className={clsx('text-sm', isDefault ? 'text-blue-400' : 'text-theme-text-tertiary')}>{isDefault ? 'Yes' : '-'}</span>
296
+ return <span className={clsx('text-sm', isDefault ? 'text-blue-600 dark:text-blue-400' : 'text-theme-text-tertiary')}>{isDefault ? 'Yes' : '-'}</span>
275
297
  }
276
298
  case 'lastValidation': {
277
299
  const lastVal = getBSLLastValidation(resource)
@@ -0,0 +1,142 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { VeleroBackupRenderer } from './VeleroBackupRenderer'
4
+ import { VeleroRestoreRenderer } from './VeleroRestoreRenderer'
5
+
6
+ /**
7
+ * Every screen has to end somewhere the reader can act.
8
+ *
9
+ * These two pages each name another object as the reason the reader should care
10
+ * — a storage location that is holding a backup hostage, and the backup a
11
+ * restore came from — and each rendered that name as plain text. The page said
12
+ * "go look at dr-replica" and gave no way to get there.
13
+ *
14
+ * The link has to disappear cleanly when the host does not wire navigation:
15
+ * library consumers render these renderers without it, and a button that goes
16
+ * nowhere is worse than text.
17
+ */
18
+
19
+ const backup = (location: string) => ({
20
+ metadata: { name: 'nightly', namespace: 'velero' },
21
+ spec: { storageLocation: location },
22
+ status: { phase: 'Completed' },
23
+ })
24
+
25
+ const restore = {
26
+ metadata: { name: 'recovery', namespace: 'velero' },
27
+ spec: { backupName: 'nightly' },
28
+ status: { phase: 'Completed' },
29
+ }
30
+
31
+ describe('a backup that blames its storage location', () => {
32
+ it('lets the reader get to the location', () => {
33
+ const html = renderToString(
34
+ <VeleroBackupRenderer
35
+ data={backup('dr-replica')}
36
+ storageLocationPhase="Unavailable"
37
+ onNavigate={vi.fn()}
38
+ />,
39
+ )
40
+ // The name is a control, not a label.
41
+ expect(html).toMatch(/<button[^>]*>dr-replica<\/button>/)
42
+ })
43
+
44
+ it('renders the location as text when navigation is not wired', () => {
45
+ const html = renderToString(
46
+ <VeleroBackupRenderer data={backup('dr-replica')} storageLocationPhase="Unavailable" />,
47
+ )
48
+ expect(html).toContain('dr-replica')
49
+ expect(html).not.toMatch(/<button[^>]*>dr-replica<\/button>/)
50
+ })
51
+
52
+ // The link is the way to the location whether or not it is broken — a healthy
53
+ // one is still the thing you check when deciding where a backup lives.
54
+ it('links the location even when it is healthy', () => {
55
+ const html = renderToString(
56
+ <VeleroBackupRenderer data={backup('default')} storageLocationPhase="Available" onNavigate={vi.fn()} />,
57
+ )
58
+ expect(html).toMatch(/<button[^>]*>default<\/button>/)
59
+ })
60
+ })
61
+
62
+ describe('a restore that names its backup', () => {
63
+ it('lets the reader get to the backup it restored from', () => {
64
+ const html = renderToString(<VeleroRestoreRenderer data={restore} onNavigate={vi.fn()} />)
65
+ expect(html).toMatch(/<button[^>]*>nightly<\/button>/)
66
+ })
67
+
68
+ it('renders the backup as text when navigation is not wired', () => {
69
+ const html = renderToString(<VeleroRestoreRenderer data={restore} />)
70
+ expect(html).toContain('nightly')
71
+ expect(html).not.toMatch(/<button[^>]*>nightly<\/button>/)
72
+ })
73
+ })
74
+
75
+ /**
76
+ * The stalled-run issue says a run is "past the 1m Velero allows for one
77
+ * operation". That budget is spec.itemOperationTimeout, and the Options section
78
+ * listed every sibling spec option except it — so the page quoted a number whose
79
+ * source it did not show, next to the place that source belongs.
80
+ */
81
+ describe('the budget the stall message measures against', () => {
82
+ it('shows the timeout the message quotes', () => {
83
+ const html = renderToString(
84
+ <VeleroBackupRenderer
85
+ data={{
86
+ metadata: { name: 'stuck', namespace: 'velero' },
87
+ spec: { storageLocation: 'default', itemOperationTimeout: '1m0s' },
88
+ status: { phase: 'InProgress' },
89
+ }}
90
+ />,
91
+ )
92
+ expect(html).toContain('Item Operation Timeout')
93
+ expect(html).toContain('1m0s')
94
+ })
95
+
96
+ // Unset is the common case, and it is not "none" — a default applies. But the
97
+ // number is not ours to print: Velero's built-in four hours can be overridden
98
+ // on the controller, so a page that renders "4h" would be stating an install
99
+ // setting it cannot read.
100
+ it('does not invent a number when the field is unset', () => {
101
+ const html = renderToString(
102
+ <VeleroBackupRenderer
103
+ data={{
104
+ metadata: { name: 'plain', namespace: 'velero' },
105
+ spec: { storageLocation: 'default' },
106
+ status: { phase: 'InProgress' },
107
+ }}
108
+ />,
109
+ )
110
+ // 'default' is this file's own word for "unset, and something else decides".
111
+ expect(html).toContain('default')
112
+ expect(html).not.toContain('4h0m0s')
113
+ })
114
+ })
115
+
116
+ /**
117
+ * Claims the phase does not support.
118
+ *
119
+ * The warning banner fired on any run with warnings that was not failed or
120
+ * partially failed — which includes runs still in flight — and said the run
121
+ * "completed". A count taken mid-run is not a final count, and saying otherwise
122
+ * is the same defect as calling a Completed backup restorable.
123
+ */
124
+ describe('a run that has warnings but has not finished', () => {
125
+ const withWarnings = (phase: string) => ({
126
+ metadata: { name: 'w', namespace: 'velero' },
127
+ spec: { storageLocation: 'default' },
128
+ status: { phase, warnings: 2 },
129
+ })
130
+
131
+ it('does not say a run in flight completed', () => {
132
+ const html = renderToString(<VeleroBackupRenderer data={withWarnings('InProgress')} />)
133
+ expect(html).not.toMatch(/completed, with/)
134
+ expect(html).toContain('not a final count')
135
+ })
136
+
137
+ it('still says completed when it did', () => {
138
+ const html = renderToString(<VeleroBackupRenderer data={withWarnings('Completed')} />)
139
+ expect(html).toContain('completed, with 2 warning(s)')
140
+ expect(html).not.toContain('not a final count')
141
+ })
142
+ })