@skyhook-io/k8s-ui 1.11.0 → 1.13.0

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 (123) hide show
  1. package/package.json +4 -4
  2. package/src/components/gitops/GitOpsStatusBadge.tsx +40 -0
  3. package/src/components/gitops/GitOpsTableView.tsx +22 -2
  4. package/src/components/gitops/ReconcilingIndicator.test.tsx +31 -0
  5. package/src/components/gitops/detail-helpers.ts +4 -1
  6. package/src/components/issues/severity.ts +1 -0
  7. package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
  8. package/src/components/resources/ResourcesView.tsx +412 -5
  9. package/src/components/resources/index.ts +27 -0
  10. package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
  11. package/src/components/resources/renderers/AlertRenderer.tsx +4 -1
  12. package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
  13. package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
  14. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
  15. package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
  16. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +71 -4
  17. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
  18. package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
  19. package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
  20. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +11 -0
  21. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
  22. package/src/components/resources/renderers/CNPGShared.tsx +27 -0
  23. package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
  24. package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
  25. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
  26. package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
  27. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
  28. package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
  29. package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
  30. package/src/components/resources/renderers/CompositeRenderer.test.tsx +81 -0
  31. package/src/components/resources/renderers/CompositeRenderer.tsx +67 -7
  32. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +4 -1
  33. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +4 -1
  34. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +4 -1
  35. package/src/components/resources/renderers/KustomizationRenderer.tsx +4 -1
  36. package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
  37. package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
  38. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
  39. package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
  40. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
  41. package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
  42. package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
  43. package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
  44. package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
  45. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
  46. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +4 -1
  47. package/src/components/resources/renderers/PodRenderer.tsx +13 -0
  48. package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
  49. package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
  50. package/src/components/resources/renderers/PolicySection.test.tsx +159 -0
  51. package/src/components/resources/renderers/PolicySection.tsx +197 -0
  52. package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
  53. package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
  54. package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
  55. package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
  56. package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
  57. package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
  58. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
  59. package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
  60. package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
  61. package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
  62. package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
  63. package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
  64. package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
  65. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
  66. package/src/components/resources/renderers/WorkloadRenderer.tsx +15 -1
  67. package/src/components/resources/renderers/calico-cells.tsx +177 -0
  68. package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
  69. package/src/components/resources/renderers/index.ts +16 -0
  70. package/src/components/resources/renderers/kyverno-cells.tsx +90 -0
  71. package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
  72. package/src/components/resources/renderers/policyreport-subject.test.tsx +27 -0
  73. package/src/components/resources/renderers/velero-cells.tsx +29 -7
  74. package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
  75. package/src/components/resources/resource-utils-calico.test.ts +215 -0
  76. package/src/components/resources/resource-utils-calico.ts +261 -0
  77. package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
  78. package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
  79. package/src/components/resources/resource-utils-cnpg.test.ts +117 -0
  80. package/src/components/resources/resource-utils-cnpg.ts +324 -3
  81. package/src/components/resources/resource-utils-crossplane.test.ts +68 -0
  82. package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
  83. package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
  84. package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
  85. package/src/components/resources/resource-utils-kyverno.ts +43 -7
  86. package/src/components/resources/resource-utils-velero.test.ts +26 -0
  87. package/src/components/resources/resource-utils-velero.ts +68 -0
  88. package/src/components/resources/resource-utils.ts +98 -0
  89. package/src/components/shared/ResourceActionsBar.tsx +101 -16
  90. package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
  91. package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
  92. package/src/components/shared/index.ts +1 -1
  93. package/src/components/shared/revision-role-badges.test.ts +120 -0
  94. package/src/components/topology/K8sResourceNode.test.ts +10 -0
  95. package/src/components/topology/K8sResourceNode.tsx +22 -0
  96. package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
  97. package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
  98. package/src/components/topology/TopologyGraph.tsx +6 -6
  99. package/src/components/topology/TopologySearch.tsx +7 -0
  100. package/src/components/topology/layout.ts +3 -0
  101. package/src/components/trace/ReachabilityView.tsx +1 -0
  102. package/src/components/trace/TracePanel.tsx +12 -2
  103. package/src/components/trace/reachFixtures.ts +26 -1
  104. package/src/components/trace/reachGraphModel.test.ts +20 -0
  105. package/src/components/trace/reachGraphModel.ts +8 -1
  106. package/src/components/trace/reachOrigins.test.ts +31 -0
  107. package/src/components/trace/reachOrigins.ts +11 -1
  108. package/src/components/ui/Badge.tsx +10 -0
  109. package/src/components/ui/ScheduleValue.test.tsx +80 -0
  110. package/src/components/ui/ScheduleValue.tsx +85 -0
  111. package/src/components/ui/drawer-components.test.tsx +17 -1
  112. package/src/components/ui/drawer-components.tsx +9 -2
  113. package/src/components/ui/index.ts +2 -0
  114. package/src/components/workload/WorkloadView.tsx +3 -0
  115. package/src/types/core.ts +29 -0
  116. package/src/types/gitops-flux-status.test.ts +163 -0
  117. package/src/types/gitops.ts +80 -24
  118. package/src/types/index.ts +1 -0
  119. package/src/types/policy.ts +193 -0
  120. package/src/utils/navigation.test.ts +5 -0
  121. package/src/utils/navigation.ts +5 -0
  122. package/src/utils/resource-icons.test.ts +49 -2
  123. package/src/utils/resource-icons.ts +29 -2
@@ -1,5 +1,5 @@
1
1
  import { ArchiveRestore, Filter } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getRestoreStatus,
5
5
  getRestoreBackupName,
@@ -17,13 +17,20 @@ import {
17
17
  isBackupPartialFailurePhase,
18
18
  } from '../resource-utils-velero'
19
19
  import { VeleroPhaseValue } from './velero-cells'
20
+ import { veleroPhaseLabel } from '../resource-utils-velero'
20
21
  import { formatAge } from '../resource-utils'
22
+ import { VeleroRunMessages, type VeleroRunMessagesFetch } from './VeleroRunMessages'
21
23
 
22
24
  interface VeleroRestoreRendererProps {
23
25
  data: any
26
+ /** Wired by the host, which owns the fetch. Optional: a consumer that does
27
+ * not wire it gets the counts on their own, with no button. */
28
+ messages?: VeleroRunMessagesFetch
29
+ /** The backup a restore came from is the one thing every reader wants next. */
30
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
24
31
  }
25
32
 
26
- export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
33
+ export function VeleroRestoreRenderer({ data, messages, onNavigate }: VeleroRestoreRendererProps) {
27
34
  const status = data.status || {}
28
35
  const conditions = status.conditions || []
29
36
 
@@ -79,7 +86,9 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
79
86
  <AlertBanner
80
87
  variant="warning"
81
88
  title={`${warnings} Warning(s)`}
82
- message={`Restore completed with ${warnings} warning(s).`}
89
+ message={phase === 'Completed'
90
+ ? `This restore completed, with ${warnings} warning(s).`
91
+ : `This restore has reported ${warnings} warning(s) so far. It is ${veleroPhaseLabel(phase).toLowerCase()}, so this is not a final count.`}
83
92
  />
84
93
  )}
85
94
 
@@ -89,7 +98,15 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
89
98
  <Property label="Phase" value={
90
99
  <VeleroPhaseValue status={restoreStatus} phase={phase} />
91
100
  } />
92
- <Property label="Backup" value={getRestoreBackupName(data)} />
101
+ <Property label="Backup" value={
102
+ <ResourceLink
103
+ name={getRestoreBackupName(data)}
104
+ kind="Backup"
105
+ namespace={data?.metadata?.namespace ?? ''}
106
+ group="velero.io"
107
+ onNavigate={onNavigate}
108
+ />
109
+ } />
93
110
  {status.startTimestamp && (
94
111
  <Property label="Started" value={formatAge(status.startTimestamp) + ' ago'} />
95
112
  )}
@@ -108,6 +125,7 @@ export function VeleroRestoreRenderer({ data }: VeleroRestoreRendererProps) {
108
125
  : '0'
109
126
  } />
110
127
  </PropertyList>
128
+ {messages && <VeleroRunMessages {...messages} errors={errors} warnings={warnings} />}
111
129
  </Section>
112
130
 
113
131
  {/* Progress section (if in progress) */}
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { VeleroRunMessages } from './VeleroRunMessages'
4
+
5
+ /**
6
+ * "Warnings: 2" was a dead end. The number is on the Backup; the text is in a
7
+ * results file in object storage that only Velero's controller can hand out a
8
+ * link to — so the page could tell an operator something was wrong and nothing
9
+ * about what.
10
+ *
11
+ * The two failures are the interesting part and both are ordinary: the
12
+ * controller is not running, or the storage the link points at is somewhere this
13
+ * process cannot reach. Neither may be swallowed, because a button that does
14
+ * nothing is worse than the bare number it replaced.
15
+ */
16
+ describe('the messages behind a run’s counts', () => {
17
+ it('offers to fetch them when a run reported any', () => {
18
+ const html = renderToString(<VeleroRunMessages errors={0} warnings={2} onFetch={() => {}} />)
19
+ expect(html).toContain('Show the messages')
20
+ // Never "all N": the server caps the list, so the label must not promise a
21
+ // number the fetch may not deliver.
22
+ expect(html).not.toContain('all 2')
23
+ expect(html).toContain('Velero has to be asked')
24
+ })
25
+
26
+ it('says nothing at all for a clean run', () => {
27
+ // No button, no explanation, no row: a run that reported nothing has nothing
28
+ // to explain, and an empty section would imply otherwise.
29
+ expect(renderToString(<VeleroRunMessages errors={0} warnings={0} onFetch={() => {}} />)).toBe('')
30
+ })
31
+
32
+ it('names each message’s scope alongside it', () => {
33
+ const html = renderToString(
34
+ <VeleroRunMessages
35
+ errors={0}
36
+ warnings={1}
37
+ messages={{
38
+ errors: [],
39
+ warnings: [{ scope: 'demo-app', message: 'could not restore ConfigMap' }],
40
+ }}
41
+ />,
42
+ )
43
+ expect(html).toContain('demo-app')
44
+ expect(html).toContain('could not restore ConfigMap')
45
+ // Scope and message are separate spans with only a margin between them, so
46
+ // without a real separator a screen reader reads "demo-appcould not
47
+ // restore" — correct on screen, wrong to anyone listening to it.
48
+ expect(html).not.toMatch(/demo-app<\/span><span[^>]*>could not restore/)
49
+ // The offer is gone once they are on screen.
50
+ expect(html).not.toContain('Velero has to be asked')
51
+ })
52
+
53
+ // The note itself is the host's to build — a denial and a fault read
54
+ // differently, and only the host knows which it got. This asserts the slot is
55
+ // rendered, not what it says.
56
+ it('shows why the fetch failed instead of an empty list', () => {
57
+ const html = renderToString(
58
+ <VeleroRunMessages
59
+ errors={0}
60
+ warnings={2}
61
+ lookupNote={<span>Velero did not answer — check that it is running.</span>}
62
+ onFetch={() => {}}
63
+ />,
64
+ )
65
+ expect(html).toContain('check that it is running')
66
+ // And the offer stays, so a failed fetch is retryable.
67
+ expect(html).toContain('Show the messages')
68
+ })
69
+
70
+ // The counts come from the CR and the messages from a file written separately.
71
+ // They can disagree, and saying so is better than rendering nothing under a
72
+ // non-zero number and letting it read as a broken button.
73
+ it('says so when the counts and the file disagree', () => {
74
+ const html = renderToString(
75
+ <VeleroRunMessages errors={0} warnings={2} messages={{ errors: [], warnings: [] }} />,
76
+ )
77
+ expect(html).toContain('results file it returned holds none')
78
+ })
79
+
80
+ // The counts sit two lines above this list. They come from the object and the
81
+ // messages come from a file written separately, so a run can report two and
82
+ // return one — and a list of one under "Warnings: 2" is a number the reader
83
+ // cannot reconcile with what is beside it.
84
+ it('says so when it returned fewer than the count promised', () => {
85
+ const html = renderToString(
86
+ <VeleroRunMessages
87
+ errors={0}
88
+ warnings={2}
89
+ messages={{ errors: [], warnings: [{ scope: 'velero', message: 'the only one' }] }}
90
+ />,
91
+ )
92
+ expect(html).toContain('reported 2 on this run and returned 1')
93
+ })
94
+
95
+ // Truncation is a shortfall this already explains on its own line; saying it
96
+ // twice, in two different vocabularies, reads as two different problems.
97
+ it('does not also report a mismatch when the list was capped', () => {
98
+ const html = renderToString(
99
+ <VeleroRunMessages
100
+ errors={0}
101
+ warnings={9000}
102
+ messages={{
103
+ errors: [],
104
+ warnings: [{ scope: 'velero', message: 'one of very many' }],
105
+ truncated: true,
106
+ }}
107
+ />,
108
+ )
109
+ expect(html).not.toContain('and returned')
110
+ expect(html).toContain('Only the first')
111
+ })
112
+
113
+ it('admits when the list was capped', () => {
114
+ const html = renderToString(
115
+ <VeleroRunMessages
116
+ errors={0}
117
+ warnings={9000}
118
+ messages={{
119
+ errors: [],
120
+ warnings: [{ scope: 'velero', message: 'one of very many' }],
121
+ truncated: true,
122
+ }}
123
+ />,
124
+ )
125
+ expect(html).toContain('Only the first')
126
+ })
127
+ })
128
+
129
+ /**
130
+ * An error and a warning were separated by an icon and a colour, and by nothing
131
+ * else — so to anyone not seeing them, the distinction that decides whether to
132
+ * act simply was not there.
133
+ */
134
+ describe('telling an error from a warning without seeing it', () => {
135
+ const both = {
136
+ errors: [{ scope: 'velero', message: 'the failure' }],
137
+ warnings: [{ scope: 'demo-app', message: 'the caution' }],
138
+ }
139
+
140
+ it('names the severity in text', () => {
141
+ const html = renderToString(<VeleroRunMessages errors={1} warnings={1} messages={both} />)
142
+ expect(html).toContain('Error: ')
143
+ expect(html).toContain('Warning: ')
144
+ })
145
+
146
+ it('announces the region so a late arrival is not silent', () => {
147
+ const html = renderToString(
148
+ <VeleroRunMessages errors={1} warnings={0} messages={{ errors: both.errors, warnings: [] }} />,
149
+ )
150
+ expect(html).toContain('aria-live')
151
+ })
152
+ })
@@ -0,0 +1,139 @@
1
+ import { AlertTriangle, XCircle, Loader2 } from 'lucide-react'
2
+ import type { VeleroMessage } from '../../../types/policy'
3
+
4
+ /**
5
+ * The messages behind a run's error and warning counts.
6
+ *
7
+ * A Backup that reports "Warnings: 2" carries the number and nothing else — the
8
+ * text is in a results file in object storage, and only Velero's controller can
9
+ * hand out a link to it. So the counts rendered as two coloured digits with
10
+ * nowhere to go: enough to worry an operator, not enough to act on.
11
+ *
12
+ * Fetched on a click rather than on page load. Reading them asks Velero to
13
+ * create a DownloadRequest and then pulls an object out of storage, which is
14
+ * real work to do on every drawer open, and the counts already answer "is
15
+ * anything wrong". Same bargain as the network trace's probes.
16
+ *
17
+ * Both failure modes are rendered here rather than swallowed, because both are
18
+ * ordinary and each points somewhere different: a controller that is not running
19
+ * (nothing serves the request) and storage this process cannot reach (the link
20
+ * resolves to somewhere only the cluster can see).
21
+ */
22
+
23
+ /**
24
+ * What the host supplies: the fetch and its outcome. Deliberately separate from
25
+ * the counts, which the renderer already has from the object itself — a host
26
+ * that had to pass those too would be restating data it was handed.
27
+ */
28
+ export interface VeleroRunMessagesFetch {
29
+ /** Undefined until fetched. */
30
+ messages?: { warnings: VeleroMessage[]; errors: VeleroMessage[]; truncated?: boolean }
31
+ loading?: boolean
32
+ /** Rendered by the host when the fetch failed, so a denial is not silence and
33
+ * is not dressed up as a fault. Same slot the storage-location panel uses for
34
+ * its lookup, so both failures in this integration read the same way. */
35
+ lookupNote?: React.ReactNode
36
+ onFetch?: () => void
37
+ }
38
+
39
+ export interface VeleroRunMessagesProps extends VeleroRunMessagesFetch {
40
+ errors: number
41
+ warnings: number
42
+ }
43
+
44
+ export function VeleroRunMessages({
45
+ errors,
46
+ warnings,
47
+ messages,
48
+ loading,
49
+ lookupNote,
50
+ onFetch,
51
+ }: VeleroRunMessagesProps) {
52
+ const total = errors + warnings
53
+ // Nothing to explain, and no button for a run that reported nothing.
54
+ if (total === 0) return null
55
+
56
+ const rows = messages ? [...messages.errors, ...messages.warnings] : []
57
+ const severityOf = (i: number) => (messages && i < messages.errors.length ? 'error' : 'warning')
58
+
59
+ return (
60
+ // aria-live so the outcome reaches a screen reader: this region swaps a
61
+ // button for a list (or a failure) with no navigation, and the same
62
+ // treatment PaneLoader and BoardSkeleton already use for content that
63
+ // arrives late.
64
+ <div className="mt-2 space-y-2" aria-live="polite">
65
+ {!messages && (
66
+ <div className="flex items-center gap-2">
67
+ <button
68
+ type="button"
69
+ onClick={onFetch}
70
+ disabled={loading || !onFetch}
71
+ className="btn-brand text-xs px-2 py-1 disabled:opacity-50"
72
+ >
73
+ {loading ? (
74
+ <span className="flex items-center gap-1.5">
75
+ <Loader2 className="w-3 h-3 animate-spin" />
76
+ Asking Velero…
77
+ </span>
78
+ ) : (
79
+ // Not "all N": the server caps what it returns, so a run with
80
+ // thousands of them would have the button promise more than the
81
+ // fetch can deliver. The counts are on the two lines above.
82
+ total === 1 ? 'Show the message' : 'Show the messages'
83
+ )}
84
+ </button>
85
+ <span className="text-xs text-theme-text-tertiary">
86
+ Not stored on this object — Velero has to be asked for them.
87
+ </span>
88
+ </div>
89
+ )}
90
+
91
+ {lookupNote}
92
+
93
+ {/* The count comes from the object; the messages come from a file written
94
+ separately, so they can disagree — and the counts are rendered two
95
+ lines above this list. A list of one under "Warnings: 2" is the same
96
+ defect as any other number that cannot be reconciled with what is
97
+ beside it. Truncation is excluded: it has its own line below. */}
98
+ {messages && !messages.truncated && rows.length !== total && (
99
+ <div className="text-xs text-theme-text-tertiary">
100
+ {rows.length === 0
101
+ ? `Velero reported ${total} ${total === 1 ? 'message' : 'messages'} on this run, but the results file it returned holds none.`
102
+ : `Velero reported ${total} on this run and returned ${rows.length}.`}
103
+ </div>
104
+ )}
105
+
106
+ {messages && rows.length > 0 && (
107
+ <ul className="space-y-1">
108
+ {rows.map((m, i) => (
109
+ <li key={`${m.scope}-${i}`} className="flex items-start gap-2 text-xs">
110
+ {severityOf(i) === 'error' ? (
111
+ <XCircle className="w-3.5 h-3.5 mt-0.5 shrink-0 text-red-500 dark:text-red-400" aria-hidden />
112
+ ) : (
113
+ <AlertTriangle className="w-3.5 h-3.5 mt-0.5 shrink-0 text-amber-500 dark:text-amber-400" aria-hidden />
114
+ )}
115
+ {/* The icon and its colour were the only thing separating an error
116
+ from a warning, which is the distinction that decides whether
117
+ anyone acts. Said in text for anyone not seeing the icon. */}
118
+ <span className="sr-only">{severityOf(i) === 'error' ? 'Error: ' : 'Warning: '}</span>
119
+ <div className="min-w-0">
120
+ {/* The separator is a real text node, not just the margin: to a
121
+ screen reader two adjacent spans run together, and this read
122
+ as "clusterfailed to list stub VolumeGroupSnapshotContents". */}
123
+ <span className="text-theme-text-tertiary font-mono">{m.scope}</span>
124
+ <span className="sr-only">: </span>
125
+ <span className="text-theme-text-secondary break-words ml-1.5">{m.message}</span>
126
+ </div>
127
+ </li>
128
+ ))}
129
+ </ul>
130
+ )}
131
+
132
+ {messages?.truncated && (
133
+ <div className="text-xs text-theme-text-tertiary">
134
+ Only the first {rows.length} are shown.
135
+ </div>
136
+ )}
137
+ </div>
138
+ )
139
+ }
@@ -0,0 +1,88 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { VeleroScheduleRenderer } from './VeleroScheduleRenderer'
4
+ import { ScheduleCell } from './velero-cells'
5
+
6
+ const schedule = (over: Record<string, unknown> = {}) => ({
7
+ metadata: { name: 'nightly', namespace: 'velero' },
8
+ spec: {
9
+ schedule: '0 1 * * *',
10
+ paused: false,
11
+ template: { storageLocation: 'default', ttl: '720h0m0s' },
12
+ ...(over.spec as object),
13
+ },
14
+ status: { phase: 'Enabled', ...(over.status as object) },
15
+ })
16
+
17
+ /**
18
+ * A schedule is the only Velero object that describes work that has not happened
19
+ * yet, so every sentence on this page is a prediction. Velero does not
20
+ * re-validate a paused schedule, which bounds what those predictions may say.
21
+ */
22
+ describe('what a schedule promises', () => {
23
+ it('says a paused schedule creates nothing', () => {
24
+ const html = renderToString(
25
+ <VeleroScheduleRenderer data={schedule({ spec: { paused: true } })} />,
26
+ )
27
+ expect(html).toContain('No new backups will be created')
28
+ })
29
+
30
+ it('says an invalid active schedule is not running', () => {
31
+ const html = renderToString(
32
+ <VeleroScheduleRenderer
33
+ data={schedule({ status: { phase: 'FailedValidation', validationErrors: ['invalid cron'] } })}
34
+ />,
35
+ )
36
+ expect(html).toContain('no backups are being created')
37
+ expect(html).toContain('invalid cron')
38
+ })
39
+
40
+ /**
41
+ * The claim to guard. Velero validates a schedule when it is active and leaves
42
+ * the result in place while it is paused, so a validation error recorded
43
+ * before the pause may already be fixed in the spec. Radar's issue source
44
+ * deliberately declines to raise this for a paused schedule; the page saying
45
+ * "will not create backups when resumed until this is fixed" made the stronger
46
+ * claim the data does not support.
47
+ */
48
+ it('does not predict what a paused schedule will do on resume', () => {
49
+ const html = renderToString(
50
+ <VeleroScheduleRenderer
51
+ data={schedule({
52
+ spec: { paused: true },
53
+ status: { phase: 'FailedValidation', validationErrors: ['invalid cron'] },
54
+ })}
55
+ />,
56
+ )
57
+ expect(html).toContain('does not re-validate a paused schedule')
58
+ expect(html).toContain('may or may not still be true')
59
+ expect(html).not.toContain('when resumed until this is fixed')
60
+ })
61
+
62
+ // A paused schedule that is also rejected carries two independent facts, and
63
+ // the badge only has room for one. The rejection is the one to act on, so
64
+ // paused rides alongside — but it has to say so, or the row reads as a
65
+ // schedule that is merely broken rather than one that is also switched off.
66
+ it('keeps the pause visible on a row whose badge is showing the rejection', () => {
67
+ const html = renderToString(
68
+ <ScheduleCell
69
+ resource={schedule({
70
+ spec: { paused: true },
71
+ status: { phase: 'FailedValidation', validationErrors: ['invalid cron'] },
72
+ })}
73
+ column="status"
74
+ />,
75
+ )
76
+ expect(html).toContain('will not run until it is resumed')
77
+ })
78
+
79
+ // Every other Velero page reaches the thing it names. The template's storage
80
+ // location is the location every backup this schedule creates gets written to.
81
+ it('lets the reader reach the location its backups will be written to', () => {
82
+ const html = renderToString(
83
+ <VeleroScheduleRenderer data={schedule()} onNavigate={() => {}} />,
84
+ )
85
+ expect(html).toContain('default')
86
+ expect(html).toMatch(/role="button"|<button|cursor-pointer/)
87
+ })
88
+ })
@@ -1,5 +1,5 @@
1
1
  import { Clock, Archive } from 'lucide-react'
2
- import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
2
+ import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
3
3
  import {
4
4
  getScheduleStatus,
5
5
  getScheduleCron,
@@ -13,9 +13,11 @@ import { VeleroPhaseValue } from './velero-cells'
13
13
 
14
14
  interface VeleroScheduleRendererProps {
15
15
  data: any
16
+ /** The location every backup this schedule creates will be written to. */
17
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
16
18
  }
17
19
 
18
- export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
20
+ export function VeleroScheduleRenderer({ data, onNavigate }: VeleroScheduleRendererProps) {
19
21
  const status = data.status || {}
20
22
  const conditions = status.conditions || []
21
23
 
@@ -47,7 +49,7 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
47
49
  variant="error"
48
50
  title="Validation Failed"
49
51
  message={isPaused
50
- ? 'The schedule spec failed validation. It will not create backups when resumed until this is fixed.'
52
+ ? 'The schedule spec failed validation. Velero recorded that while the schedule was active; it does not re-validate a paused schedule, so this may or may not still be true when it resumes.'
51
53
  : 'The schedule spec failed validation and is not active — no backups are being created.'}
52
54
  items={validationErrors.length > 0 ? validationErrors : undefined}
53
55
  />
@@ -72,7 +74,15 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
72
74
  <Section title="Backup Template" icon={Archive} defaultExpanded>
73
75
  <PropertyList>
74
76
  {template.storageLocation && (
75
- <Property label="Storage Location" value={template.storageLocation} />
77
+ <Property label="Storage Location" value={
78
+ <ResourceLink
79
+ name={template.storageLocation}
80
+ kind="BackupStorageLocation"
81
+ namespace={data?.metadata?.namespace ?? ''}
82
+ group="velero.io"
83
+ onNavigate={onNavigate}
84
+ />
85
+ } />
76
86
  )}
77
87
  {template.ttl && (
78
88
  <Property label="TTL" value={template.ttl} />
@@ -93,7 +103,7 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
93
103
  <Property label="Excluded Namespaces" value={
94
104
  <div className="flex flex-wrap gap-1">
95
105
  {templateExcludedNs.map((ns: string) => (
96
- <span key={ns} className="badge-sm bg-red-500/10 text-red-400">{ns}</span>
106
+ <span key={ns} className="badge-sm bg-red-500/10 text-red-700 dark:text-red-400">{ns}</span>
97
107
  ))}
98
108
  </div>
99
109
  } />
@@ -111,7 +121,7 @@ export function VeleroScheduleRenderer({ data }: VeleroScheduleRendererProps) {
111
121
  <Property label="Excluded Resources" value={
112
122
  <div className="flex flex-wrap gap-1">
113
123
  {templateExcludedResources.map((r: string) => (
114
- <span key={r} className="badge-sm bg-red-500/10 text-red-400">{r}</span>
124
+ <span key={r} className="badge-sm bg-red-500/10 text-red-700 dark:text-red-400">{r}</span>
115
125
  ))}
116
126
  </div>
117
127
  } />
@@ -1,6 +1,8 @@
1
1
  import { useState, useEffect } from 'react'
2
2
  import { Server, ExternalLink, Scale, Minus, Plus, Loader2, Shield } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
+ import { PolicySection } from './PolicySection'
5
+ import type { PolicyResourceResponse } from '../../../types/policy'
4
6
  import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection, AlertBanner, ResourceLink, ResourceRefBadge, useOperationalIssuesShown } from '../../ui/drawer-components'
5
7
  import { DialogPortal } from '../../ui/DialogPortal'
6
8
  import { Tooltip } from '../../ui/Tooltip'
@@ -40,6 +42,14 @@ interface WorkloadRendererProps {
40
42
  rbacData?: RBACSubjectResponse | null
41
43
  rbacLoading?: boolean
42
44
  rbacError?: Error | null
45
+ /**
46
+ * Policy findings for this workload. Undefined means the host didn't wire the
47
+ * fetch, so the section is omitted entirely — consumers that skip it get the
48
+ * original sections and nothing breaks.
49
+ */
50
+ policyData?: PolicyResourceResponse | null
51
+ policyLoading?: boolean
52
+ policyError?: Error | null
43
53
  }
44
54
 
45
55
  // Check if the workload is actively progressing (scaling, rolling update)
@@ -140,7 +150,7 @@ function compactHPASummary(diagnosis: HPADiagnosis): string {
140
150
  return diagnosis.summary
141
151
  }
142
152
 
143
- export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, onRequestRefresh, rbacData, rbacLoading, rbacError }: WorkloadRendererProps) {
153
+ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale, isScalePending, scaleBlockedBy, scalerDiagnostics, onRequestRefresh, rbacData, rbacLoading, rbacError, policyData, policyLoading, policyError }: WorkloadRendererProps) {
144
154
  const status = data.status || {}
145
155
  const spec = data.spec || {}
146
156
  const metadata = data.metadata || {}
@@ -372,6 +382,10 @@ export function WorkloadRenderer({ kind, data, onNavigate, onViewPods, onScale,
372
382
  </div>
373
383
  </DialogPortal>
374
384
 
385
+ {policyData !== undefined && (
386
+ <PolicySection data={policyData} loading={policyLoading} error={policyError} />
387
+ )}
388
+
375
389
  <Section title="Strategy">
376
390
  <PropertyList>
377
391
  {isDaemonSet || isStatefulSet ? (