@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.
- package/package.json +4 -4
- package/src/components/applications/ApplicationDetail.tsx +2 -2
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +32 -7
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.empty-note.test.ts +41 -0
- package/src/components/resources/ResourcesView.tsx +419 -9
- package/src/components/resources/index.ts +27 -0
- package/src/components/resources/kyverno-legacy-action.test.ts +98 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.test.tsx +241 -0
- package/src/components/resources/renderers/AnalysisRunRenderer.tsx +166 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +31 -2
- package/src/components/resources/renderers/CNPGClusterRenderer.test.tsx +33 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +47 -4
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +243 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.test.tsx +99 -0
- package/src/components/resources/renderers/CNPGObjectStoreRenderer.tsx +235 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +23 -1
- package/src/components/resources/renderers/CNPGShared.tsx +27 -0
- package/src/components/resources/renderers/CalicoHostEndpointRenderer.tsx +71 -0
- package/src/components/resources/renderers/CalicoIPPoolRenderer.tsx +32 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.test.tsx +205 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyDiagram.tsx +537 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.test.tsx +223 -0
- package/src/components/resources/renderers/CalicoNetworkPolicyRenderer.tsx +670 -0
- package/src/components/resources/renderers/CalicoTierRenderer.tsx +19 -0
- package/src/components/resources/renderers/KyvernoCELPolicyRenderers.tsx +13 -6
- package/src/components/resources/renderers/KyvernoExceptionRenderers.tsx +3 -2
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.test.ts +31 -0
- package/src/components/resources/renderers/KyvernoGlobalContextRenderer.tsx +124 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +28 -2
- package/src/components/resources/renderers/KyvernoQueueRenderers.tsx +303 -0
- package/src/components/resources/renderers/LookupFailureNote.test.tsx +60 -0
- package/src/components/resources/renderers/LookupFailureNote.tsx +42 -0
- package/src/components/resources/renderers/NamespaceRenderer.test.ts +30 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -3
- package/src/components/resources/renderers/NetworkPolicyDiagram.tsx +27 -26
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +6 -3
- package/src/components/resources/renderers/PolicyCoverageSection.test.tsx +348 -0
- package/src/components/resources/renderers/PolicyCoverageSection.tsx +939 -0
- package/src/components/resources/renderers/PolicySection.test.tsx +59 -2
- package/src/components/resources/renderers/PolicySection.tsx +32 -5
- package/src/components/resources/renderers/RolloutRenderer.test.tsx +491 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +391 -92
- package/src/components/resources/renderers/VeleroBSLRenderer.test.tsx +374 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +179 -3
- package/src/components/resources/renderers/VeleroBackupRenderer.test.tsx +221 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +119 -6
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/VeleroBackupRepositoryRenderer.tsx +96 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.test.tsx +55 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +22 -4
- package/src/components/resources/renderers/VeleroRunMessages.test.tsx +152 -0
- package/src/components/resources/renderers/VeleroRunMessages.tsx +139 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.test.tsx +88 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +16 -6
- package/src/components/resources/renderers/calico-cells.tsx +177 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +100 -0
- package/src/components/resources/renderers/index.ts +16 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +81 -0
- package/src/components/resources/renderers/network-policy-peer-styles.ts +25 -0
- package/src/components/resources/renderers/velero-cells.tsx +29 -7
- package/src/components/resources/renderers/velero-dead-ends.test.tsx +142 -0
- package/src/components/resources/resource-utils-calico.test.ts +215 -0
- package/src/components/resources/resource-utils-calico.ts +261 -0
- package/src/components/resources/resource-utils-cnpg-declarative.test.ts +104 -0
- package/src/components/resources/resource-utils-cnpg-objectstore.test.ts +122 -0
- package/src/components/resources/resource-utils-cnpg.test.ts +54 -0
- package/src/components/resources/resource-utils-cnpg.ts +264 -3
- package/src/components/resources/resource-utils-kyverno-modern.ts +24 -0
- package/src/components/resources/resource-utils-kyverno-queue.test.ts +164 -0
- package/src/components/resources/resource-utils-kyverno-queue.ts +198 -0
- package/src/components/resources/resource-utils-kyverno.ts +43 -7
- package/src/components/resources/resource-utils-velero.test.ts +26 -0
- package/src/components/resources/resource-utils-velero.ts +68 -0
- package/src/components/resources/resource-utils.ts +98 -0
- package/src/components/shared/ResourceActionsBar.tsx +101 -16
- package/src/components/shared/ResourceRendererDispatch.test.tsx +379 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +199 -22
- package/src/components/shared/index.ts +1 -1
- package/src/components/shared/revision-role-badges.test.ts +120 -0
- package/src/components/topology/K8sResourceNode.test.ts +10 -0
- package/src/components/topology/K8sResourceNode.tsx +22 -0
- package/src/components/topology/TopologyFilterSidebar.test.tsx +31 -0
- package/src/components/topology/TopologyFilterSidebar.tsx +5 -0
- package/src/components/topology/TopologyGraph.tsx +6 -6
- package/src/components/topology/TopologySearch.tsx +7 -0
- package/src/components/topology/layout.ts +3 -0
- package/src/components/trace/ReachabilityView.tsx +1 -0
- package/src/components/trace/TracePanel.tsx +13 -2
- package/src/components/trace/reachFixtures.ts +26 -1
- package/src/components/trace/reachGraphModel.test.ts +20 -0
- package/src/components/trace/reachGraphModel.ts +8 -1
- package/src/components/trace/reachOrigins.test.ts +31 -0
- package/src/components/trace/reachOrigins.ts +11 -1
- package/src/components/ui/Badge.tsx +10 -0
- package/src/components/ui/InClusterConsentDialog.tsx +5 -4
- package/src/components/ui/ScheduleValue.test.tsx +80 -0
- package/src/components/ui/ScheduleValue.tsx +85 -0
- package/src/components/ui/SelectMenu.tsx +202 -28
- package/src/components/ui/drawer-components.test.tsx +17 -1
- package/src/components/ui/drawer-components.tsx +9 -2
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +5 -2
- package/src/types/core.ts +36 -4
- package/src/types/policy.ts +153 -2
- package/src/utils/clipboard.test.ts +86 -0
- package/src/utils/clipboard.ts +50 -0
- package/src/utils/extended-resources.ts +4 -7
- package/src/utils/format.test.ts +83 -1
- package/src/utils/format.ts +80 -1
- package/src/utils/inClusterConsent.test.ts +9 -0
- package/src/utils/inClusterConsent.ts +9 -9
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.test.ts +5 -0
- package/src/utils/navigation.ts +5 -0
- package/src/utils/resource-icons.test.ts +49 -2
- 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={
|
|
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={
|
|
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.
|
|
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={
|
|
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
|
} />
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Badge } from '../../ui/Badge'
|
|
2
|
+
import { Tooltip } from '../../ui/Tooltip'
|
|
3
|
+
import {
|
|
4
|
+
getCalicoPolicyNamespaceSelector,
|
|
5
|
+
getCalicoPolicyRuleCount,
|
|
6
|
+
getCalicoPolicySelector,
|
|
7
|
+
getCalicoPolicyServiceAccountSelector,
|
|
8
|
+
getCalicoPolicyTypes,
|
|
9
|
+
getCalicoIPPoolAllowedUses,
|
|
10
|
+
getCalicoIPPoolBlockSize,
|
|
11
|
+
getCalicoIPPoolEncapsulation,
|
|
12
|
+
isCalicoApiVersion,
|
|
13
|
+
isCalicoPolicyResource,
|
|
14
|
+
} from '../resource-utils-calico'
|
|
15
|
+
|
|
16
|
+
export function CalicoPolicyCell({
|
|
17
|
+
resource,
|
|
18
|
+
column,
|
|
19
|
+
}: {
|
|
20
|
+
resource: any
|
|
21
|
+
column: string
|
|
22
|
+
}) {
|
|
23
|
+
if (!isCalicoPolicyResource(resource))
|
|
24
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
25
|
+
|
|
26
|
+
switch (column) {
|
|
27
|
+
case 'selector':
|
|
28
|
+
return <SelectorCell value={getCalicoPolicySelector(resource)} />
|
|
29
|
+
case 'namespaceSelector':
|
|
30
|
+
return <SelectorCell value={getCalicoPolicyNamespaceSelector(resource)} />
|
|
31
|
+
case 'serviceAccountSelector':
|
|
32
|
+
return (
|
|
33
|
+
<SelectorCell value={getCalicoPolicyServiceAccountSelector(resource)} />
|
|
34
|
+
)
|
|
35
|
+
case 'tier':
|
|
36
|
+
return (
|
|
37
|
+
<span className="text-sm text-theme-text-secondary">
|
|
38
|
+
{resource.spec?.tier || 'default'}
|
|
39
|
+
</span>
|
|
40
|
+
)
|
|
41
|
+
case 'order':
|
|
42
|
+
return (
|
|
43
|
+
<span className="text-sm text-theme-text-secondary">
|
|
44
|
+
{resource.spec?.order ?? '-'}
|
|
45
|
+
</span>
|
|
46
|
+
)
|
|
47
|
+
case 'types':
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-wrap gap-1">
|
|
50
|
+
{getCalicoPolicyTypes(resource).map((type) => (
|
|
51
|
+
<Badge key={type} tone="accent1" size="sm">
|
|
52
|
+
{type}
|
|
53
|
+
</Badge>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
case 'stagedAction':
|
|
58
|
+
return resource.spec?.stagedAction ? (
|
|
59
|
+
<Badge tone="note" size="sm">
|
|
60
|
+
{String(resource.spec.stagedAction)}
|
|
61
|
+
</Badge>
|
|
62
|
+
) : (
|
|
63
|
+
<span className="text-sm text-theme-text-tertiary">-</span>
|
|
64
|
+
)
|
|
65
|
+
case 'rules': {
|
|
66
|
+
const { ingress, egress } = getCalicoPolicyRuleCount(resource)
|
|
67
|
+
return (
|
|
68
|
+
<span className="text-sm text-theme-text-secondary">
|
|
69
|
+
{ingress}i / {egress}e
|
|
70
|
+
</span>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
default:
|
|
74
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function SelectorCell({ value }: { value: string }) {
|
|
79
|
+
return (
|
|
80
|
+
<Tooltip content={value}>
|
|
81
|
+
<span className="text-sm text-theme-text-secondary truncate block font-mono">
|
|
82
|
+
{value}
|
|
83
|
+
</span>
|
|
84
|
+
</Tooltip>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List cells for the Calico resources that describe the network itself rather
|
|
90
|
+
* than policy: IPPool, HostEndpoint and Tier. The values that decide whether a
|
|
91
|
+
* pool is in use — disabled, encapsulation, node selector — belong in the list,
|
|
92
|
+
* not only behind a click on each row.
|
|
93
|
+
*/
|
|
94
|
+
export function CalicoInfraCell({
|
|
95
|
+
resource,
|
|
96
|
+
column,
|
|
97
|
+
}: {
|
|
98
|
+
resource: any
|
|
99
|
+
column: string
|
|
100
|
+
}) {
|
|
101
|
+
if (!isCalicoApiVersion(resource?.apiVersion))
|
|
102
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
103
|
+
|
|
104
|
+
const spec = resource?.spec ?? {}
|
|
105
|
+
switch (column) {
|
|
106
|
+
case 'cidr':
|
|
107
|
+
return <MonoCell value={spec.cidr} />
|
|
108
|
+
case 'blockSize':
|
|
109
|
+
return <TextCell value={getCalicoIPPoolBlockSize(resource)} />
|
|
110
|
+
case 'encapsulation':
|
|
111
|
+
return <TextCell value={getCalicoIPPoolEncapsulation(resource)} />
|
|
112
|
+
case 'natOutgoing':
|
|
113
|
+
return <YesNoCell value={spec.natOutgoing === true} />
|
|
114
|
+
case 'disabled':
|
|
115
|
+
return spec.disabled === true ? (
|
|
116
|
+
<Badge severity="warning" size="sm">
|
|
117
|
+
Disabled
|
|
118
|
+
</Badge>
|
|
119
|
+
) : (
|
|
120
|
+
<span className="text-sm text-theme-text-secondary">No</span>
|
|
121
|
+
)
|
|
122
|
+
case 'allowedUses':
|
|
123
|
+
return <TextCell value={getCalicoIPPoolAllowedUses(resource)} />
|
|
124
|
+
case 'nodeSelector':
|
|
125
|
+
return <MonoCell value={spec.nodeSelector ?? 'all()'} />
|
|
126
|
+
case 'node':
|
|
127
|
+
return <TextCell value={spec.node} />
|
|
128
|
+
case 'interfaceName':
|
|
129
|
+
return <MonoCell value={spec.interfaceName} />
|
|
130
|
+
case 'expectedIPs':
|
|
131
|
+
return (
|
|
132
|
+
<MonoCell
|
|
133
|
+
value={
|
|
134
|
+
Array.isArray(spec.expectedIPs)
|
|
135
|
+
? spec.expectedIPs.join(', ')
|
|
136
|
+
: undefined
|
|
137
|
+
}
|
|
138
|
+
/>
|
|
139
|
+
)
|
|
140
|
+
case 'profiles':
|
|
141
|
+
return (
|
|
142
|
+
<TextCell
|
|
143
|
+
value={
|
|
144
|
+
Array.isArray(spec.profiles) ? spec.profiles.join(', ') : undefined
|
|
145
|
+
}
|
|
146
|
+
/>
|
|
147
|
+
)
|
|
148
|
+
case 'order':
|
|
149
|
+
return <TextCell value={spec.order} />
|
|
150
|
+
case 'defaultAction':
|
|
151
|
+
return <TextCell value={spec.defaultAction ?? 'Deny'} />
|
|
152
|
+
default:
|
|
153
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function TextCell({ value }: { value: unknown }) {
|
|
158
|
+
if (value === undefined || value === null || value === '')
|
|
159
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
160
|
+
return (
|
|
161
|
+
<span className="text-sm text-theme-text-secondary">{String(value)}</span>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function MonoCell({ value }: { value: unknown }) {
|
|
166
|
+
if (value === undefined || value === null || value === '')
|
|
167
|
+
return <span className="text-sm text-theme-text-tertiary">-</span>
|
|
168
|
+
return <SelectorCell value={String(value)} />
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function YesNoCell({ value }: { value: boolean }) {
|
|
172
|
+
return (
|
|
173
|
+
<span className="text-sm text-theme-text-secondary">
|
|
174
|
+
{value ? 'Yes' : 'No'}
|
|
175
|
+
</span>
|
|
176
|
+
)
|
|
177
|
+
}
|