@skyhook-io/k8s-ui 1.18.2 → 1.20.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.
- package/package.json +9 -2
- package/src/components/applications/ApplicationDetail.tsx +8 -2
- package/src/components/charts/AreaChart.test.tsx +39 -1
- package/src/components/charts/AreaChart.tsx +32 -13
- package/src/components/charts/axis.test.ts +17 -1
- package/src/components/charts/axis.ts +13 -0
- package/src/components/charts/format.test.ts +159 -2
- package/src/components/charts/format.ts +118 -0
- package/src/components/cluster-switcher/ClusterSwitcher.test.tsx +54 -0
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +7 -3
- package/src/components/compare/ResourceCompareView.test.tsx +54 -0
- package/src/components/compare/ResourceCompareView.tsx +35 -12
- package/src/components/gitops/GitOpsDetailLayout.test.tsx +16 -0
- package/src/components/gitops/GitOpsDetailLayout.tsx +54 -7
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +1 -1
- package/src/components/gitops/GitOpsTableView.tsx +26 -7
- package/src/components/gitops/RollbackDialog.test.tsx +80 -0
- package/src/components/gitops/RollbackDialog.tsx +25 -6
- package/src/components/gitops/SyncOptionsDialog.render.test.tsx +374 -0
- package/src/components/gitops/SyncOptionsDialog.test.ts +54 -1
- package/src/components/gitops/SyncOptionsDialog.tsx +179 -34
- package/src/components/gitops/applicationset-row.test.ts +12 -0
- package/src/components/gitops/argo-radar-upgrade.test.tsx +369 -0
- package/src/components/gitops/detail-helpers.test.ts +8 -2
- package/src/components/gitops/detail-helpers.ts +9 -11
- package/src/components/gitops/index.ts +1 -1
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +32 -0
- package/src/components/gitops/insights/GitOpsActivityInsightView.test.tsx +55 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +94 -46
- package/src/components/gitops/insights/GitOpsIssuesBand.test.tsx +108 -0
- package/src/components/gitops/insights/GitOpsRollbackInsightView.test.tsx +73 -0
- package/src/components/gitops/insights/GitOpsSyncWindowInsightView.test.tsx +29 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +65 -0
- package/src/components/gitops/insights/insights-helpers.ts +17 -3
- package/src/components/issues/IssueGuidance.test.tsx +190 -0
- package/src/components/issues/IssueGuidance.tsx +48 -0
- package/src/components/issues/IssuesView.tsx +15 -3
- package/src/components/issues/ResourceIssuesSection.tsx +3 -1
- package/src/components/issues/diagnostic.ts +14 -1
- package/src/components/issues/index.ts +1 -0
- package/src/components/issues/severity.ts +15 -0
- package/src/components/issues/types.ts +10 -0
- package/src/components/kueue/AdmissionPath.tsx +223 -0
- package/src/components/kueue/QueueEvidence.tsx +148 -0
- package/src/components/kueue/constants.ts +1 -0
- package/src/components/kueue/eligibility.ts +43 -0
- package/src/components/kueue/flavors-tried.ts +79 -0
- package/src/components/kueue/helpers.test.ts +105 -0
- package/src/components/kueue/policy.ts +100 -0
- package/src/components/kueue/queue-detail.test.tsx +317 -0
- package/src/components/kueue/queue-status.ts +42 -0
- package/src/components/kueue/related-read.ts +48 -0
- package/src/components/kueue/testdata/kueue-v0.18-policy-enums.json +89 -0
- package/src/components/logs/StructuredLogLine.test.tsx +22 -0
- package/src/components/logs/StructuredLogLine.tsx +8 -3
- package/src/components/resources/KueueAdmissionSection.test.tsx +117 -0
- package/src/components/resources/KueueAdmissionSection.tsx +51 -12
- package/src/components/resources/KueuePendingPosition.test.tsx +97 -0
- package/src/components/resources/KueuePendingPosition.tsx +91 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +8 -0
- package/src/components/resources/ResourcesSidebar.tsx +3 -2
- package/src/components/resources/ResourcesView.crd.test.tsx +28 -0
- package/src/components/resources/ResourcesView.node-pods.test.tsx +25 -0
- package/src/components/resources/ResourcesView.strimzi.test.tsx +37 -0
- package/src/components/resources/ResourcesView.tsx +50 -3
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/generic-status.ts +7 -0
- package/src/components/resources/get-pod-phase-display.test.ts +70 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +54 -1
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +7 -8
- package/src/components/resources/renderers/CRDSchemaBrowser.test.tsx +105 -0
- package/src/components/resources/renderers/CRDSchemaBrowser.tsx +134 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.test.tsx +106 -0
- package/src/components/resources/renderers/CustomResourceDefinitionRenderer.tsx +152 -0
- package/src/components/resources/renderers/JobRenderer.test.tsx +35 -0
- package/src/components/resources/renderers/JobRenderer.tsx +7 -2
- package/src/components/resources/renderers/KueueQueueRenderers.test.tsx +5 -5
- package/src/components/resources/renderers/KueueQueueRenderers.tsx +250 -170
- package/src/components/resources/renderers/KueueQueueWorkloads.test.tsx +189 -0
- package/src/components/resources/renderers/KueueQueueWorkloads.tsx +460 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +1 -1
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +23 -52
- package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +11 -6
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +23 -5
- package/src/components/resources/renderers/ServiceAccountRenderer.test.tsx +83 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +15 -10
- package/src/components/resources/renderers/StrimziConnectorRenderer.tsx +61 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +9 -3
- package/src/components/resources/renderers/crd-cells.test.tsx +30 -0
- package/src/components/resources/renderers/crd-cells.tsx +55 -0
- package/src/components/resources/renderers/crd-schema-parts.test.tsx +59 -0
- package/src/components/resources/renderers/crd-schema-parts.tsx +121 -0
- package/src/components/resources/renderers/index.ts +6 -0
- package/src/components/resources/renderers/kueue-cells.tsx +5 -0
- package/src/components/resources/resource-utils-argo.test.ts +94 -1
- package/src/components/resources/resource-utils-argo.ts +84 -5
- package/src/components/resources/resource-utils-crd.test.ts +32 -0
- package/src/components/resources/resource-utils-crd.ts +26 -0
- package/src/components/resources/resource-utils-gpu-ecosystem.test.ts +12 -0
- package/src/components/resources/resource-utils-kueue-admission.test.ts +26 -0
- package/src/components/resources/resource-utils-kueue.test.ts +15 -0
- package/src/components/resources/resource-utils-kueue.ts +137 -4
- package/src/components/resources/resource-utils-opentelemetry.test.tsx +9 -1
- package/src/components/resources/resource-utils-opentelemetry.ts +1 -1
- package/src/components/resources/resource-utils-ray.test.ts +42 -1
- package/src/components/resources/resource-utils-ray.ts +38 -0
- package/src/components/resources/resource-utils-strimzi.test.tsx +118 -0
- package/src/components/resources/resource-utils-strimzi.ts +35 -0
- package/src/components/resources/resource-utils.ts +45 -1
- package/src/components/shared/ResourceActionsBar.tsx +83 -25
- package/src/components/shared/ResourceRendererDispatch.test.tsx +37 -3
- package/src/components/shared/ResourceRendererDispatch.tsx +40 -7
- package/src/components/topology/K8sResourceNode.test.ts +14 -0
- package/src/components/topology/K8sResourceNode.tsx +18 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +96 -94
- package/src/components/topology/layout.ts +3 -1
- package/src/components/topology/pod-group-expansion.test.ts +38 -0
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/RadarUpgradeNote.tsx +8 -0
- package/src/components/ui/drawer-components.tsx +1 -1
- package/src/components/ui/index.ts +1 -0
- package/src/components/workload/PodList.tsx +35 -9
- package/src/components/workload/WorkloadView.spec.test.tsx +41 -1
- package/src/components/workload/WorkloadView.tsx +61 -8
- package/src/components/workload/workload-logs-availability.test.ts +7 -0
- package/src/types/core.ts +41 -0
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +2 -2
- package/src/types/kueue-queues.ts +84 -0
- package/src/types/read-coverage.ts +2 -1
- package/src/types/scheduling.ts +72 -0
- package/src/utils/animation.test.ts +38 -0
- package/src/utils/animation.ts +34 -0
- package/src/utils/api-resources-crd.test.ts +11 -0
- package/src/utils/api-resources.ts +5 -1
- package/src/utils/applications.test.ts +15 -1
- package/src/utils/applications.ts +26 -1
- package/src/utils/crd-schema.test.ts +107 -0
- package/src/utils/crd-schema.ts +201 -0
- package/src/utils/format.test.ts +37 -1
- package/src/utils/format.ts +28 -80
- package/src/utils/gitops-owner.test.ts +28 -2
- package/src/utils/gitops-owner.ts +19 -0
- package/src/utils/kueue-queue-workloads.ts +19 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +90 -1
- package/src/utils/topology-neighborhood.ts +55 -3
- package/src/utils/workload-history-notes.test.ts +76 -0
- package/src/utils/workload-history-notes.ts +93 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { renderToString } from 'react-dom/server'
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
7
|
+
import { GitOpsDetailLayout, type ArgoActionHandlers, type GitOpsDetailStatus } from './GitOpsDetailLayout'
|
|
8
|
+
import { buildRowActionItems, normalizeArgoApplication } from './GitOpsTableView'
|
|
9
|
+
import { ResourceActionsBar } from '../shared/ResourceActionsBar'
|
|
10
|
+
|
|
11
|
+
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
|
12
|
+
|
|
13
|
+
// SSR splits interpolated text with <!-- --> markers; strip so assertions read as prose.
|
|
14
|
+
const html = (node: Parameters<typeof renderToString>[0]) => renderToString(node).replace(/<!-- -->/g, '')
|
|
15
|
+
|
|
16
|
+
// The attributes of the button labelled `label`.
|
|
17
|
+
function buttonAttrs(markup: string, label: string): string {
|
|
18
|
+
const chunk = markup.split('<button').find((part) => part.includes(`${label}</button>`))
|
|
19
|
+
if (chunk === undefined) throw new Error(`no ${label} button rendered`)
|
|
20
|
+
return chunk.slice(0, chunk.indexOf('>'))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const upgrade: RadarUpgradeRequirement = {
|
|
24
|
+
feature: 'Auto-sync controls for Applications with automated.enabled: false',
|
|
25
|
+
currentVersion: 'v1.16.3',
|
|
26
|
+
latestVersion: 'v1.17.0',
|
|
27
|
+
}
|
|
28
|
+
const NOTE = 'Auto-sync controls for Applications with automated.enabled: false need the latest Radar (v1.17) on this cluster. It runs v1.16.3.'
|
|
29
|
+
// RadarUpgradeNote sets the feature in its own <span>.
|
|
30
|
+
const NOTE_MARKUP = 'Auto-sync controls for Applications with automated.enabled: false</span> need the latest Radar (v1.17) on this cluster. It runs v1.16.3.'
|
|
31
|
+
|
|
32
|
+
function app(automated: Record<string, unknown> | undefined, annotations?: Record<string, string>) {
|
|
33
|
+
return {
|
|
34
|
+
apiVersion: 'argoproj.io/v1alpha1',
|
|
35
|
+
metadata: { name: 'guestbook', namespace: 'argocd', annotations },
|
|
36
|
+
spec: { project: 'default', source: {}, destination: {}, syncPolicy: automated ? { automated } : {} },
|
|
37
|
+
status: {},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function argo(overrides: Partial<ArgoActionHandlers> = {}): ArgoActionHandlers {
|
|
42
|
+
const noop = () => {}
|
|
43
|
+
return {
|
|
44
|
+
onSyncRequested: noop, onRefresh: noop, onTerminate: noop, onSuspend: noop, onResume: noop,
|
|
45
|
+
syncing: false, refreshing: false, refreshingKind: 'normal', terminating: false, suspending: false, resuming: false,
|
|
46
|
+
autoSyncEnabled: false, isRunning: false,
|
|
47
|
+
...overrides,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function layout(handlers: ArgoActionHandlers, status: GitOpsDetailStatus | null = null) {
|
|
52
|
+
return (
|
|
53
|
+
<GitOpsDetailLayout
|
|
54
|
+
identity={{ kind: 'applications', group: 'argoproj.io', namespace: 'argocd', name: 'guestbook', toolLabel: 'ArgoCD', kindLabel: 'Application' }}
|
|
55
|
+
status={status}
|
|
56
|
+
terminating={false}
|
|
57
|
+
terminatingChipTooltip=""
|
|
58
|
+
terminatingActionTooltip=""
|
|
59
|
+
detail={{ project: 'default' }}
|
|
60
|
+
insight={null}
|
|
61
|
+
insightLoading={false}
|
|
62
|
+
isArgoApp
|
|
63
|
+
isFlux={false}
|
|
64
|
+
isFluxWorkload={false}
|
|
65
|
+
argo={handlers}
|
|
66
|
+
activeTab="activity"
|
|
67
|
+
onTabChange={() => {}}
|
|
68
|
+
fullscreen={false}
|
|
69
|
+
onToggleFullscreen={() => {}}
|
|
70
|
+
renderTabBody={() => null}
|
|
71
|
+
onNavigateRoot={() => {}}
|
|
72
|
+
/>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const enableAutoSync = (markup: string) => buttonAttrs(markup, 'Enable auto-sync')
|
|
77
|
+
|
|
78
|
+
describe('Enable auto-sync on the GitOps detail page', () => {
|
|
79
|
+
it('is disabled with the upgrade note when the Radar cannot clear enabled: false', () => {
|
|
80
|
+
const markup = html(layout(argo({ resumeUpgrade: upgrade })))
|
|
81
|
+
expect(enableAutoSync(markup)).toMatch(/\sdisabled=""/)
|
|
82
|
+
expect(markup).toContain(NOTE_MARKUP)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('is enabled, with no note, when the Radar advertises the behavior', () => {
|
|
86
|
+
const markup = html(layout(argo({ resumeUpgrade: null })))
|
|
87
|
+
expect(enableAutoSync(markup)).not.toMatch(/\sdisabled=""/)
|
|
88
|
+
expect(markup).not.toContain('need the latest Radar')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Disable auto-sync works on every Radar; only the way back on is gated.
|
|
92
|
+
it('leaves Disable auto-sync alone', () => {
|
|
93
|
+
const markup = html(layout(argo({ autoSyncEnabled: true, resumeUpgrade: upgrade })))
|
|
94
|
+
expect(markup).toContain('Disable auto-sync')
|
|
95
|
+
expect(markup).not.toContain('need the latest Radar')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('tooltip', () => {
|
|
99
|
+
afterEach(() => { vi.useRealTimers() })
|
|
100
|
+
|
|
101
|
+
it.each([
|
|
102
|
+
{ name: 'saved settings', status: { sync: 'OutOfSync', health: 'Healthy', suspended: true } as GitOpsDetailStatus, settings: undefined, copy: 'Turn auto-sync back on with its current or saved settings.' },
|
|
103
|
+
{ name: 'first-time enable', status: { sync: 'OutOfSync', health: 'Healthy', suspended: false } as GitOpsDetailStatus, settings: { prune: false, selfHeal: true }, copy: 'Argo applies new commits and reverts live edits (self-heal). Resources removed from Git are kept (prune off).' },
|
|
104
|
+
{ name: 'first-time enable on a Radar that starts self-heal off', status: { sync: 'OutOfSync', health: 'Healthy', suspended: false } as GitOpsDetailStatus, settings: { prune: false, selfHeal: false }, copy: 'Argo applies new commits. Resources removed from Git are kept (prune off).' },
|
|
105
|
+
{ name: 'a block switched off in place with prune on', status: { sync: 'OutOfSync', health: 'Healthy', suspended: false } as GitOpsDetailStatus, settings: { prune: true, selfHeal: false }, copy: 'Argo applies new commits. Resources removed from Git are deleted (prune on).' },
|
|
106
|
+
{ name: 'unknown settings', status: null, settings: null, copy: 'Turn on automated sync, so Argo applies new commits from Git.' },
|
|
107
|
+
])('explains $name', async ({ status, settings, copy }) => {
|
|
108
|
+
vi.useFakeTimers()
|
|
109
|
+
const element = document.createElement('div')
|
|
110
|
+
document.body.appendChild(element)
|
|
111
|
+
const root = createRoot(element)
|
|
112
|
+
try {
|
|
113
|
+
await act(async () => { root.render(layout(argo({ resumeSettings: settings }), status)) })
|
|
114
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent === 'Enable auto-sync')!
|
|
115
|
+
await act(async () => {
|
|
116
|
+
button.parentElement!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
|
|
117
|
+
vi.advanceTimersByTime(1000)
|
|
118
|
+
})
|
|
119
|
+
expect(document.body.querySelector('[role="tooltip"]')?.textContent).toBe(copy)
|
|
120
|
+
} finally {
|
|
121
|
+
await act(async () => root.unmount())
|
|
122
|
+
element.remove()
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it.each([
|
|
127
|
+
{ name: 'Sync is available', copy: 'Stop Argo from automatically syncing Git changes. Manual Sync still works.' },
|
|
128
|
+
{ name: 'an operation holds Sync', running: true, copy: 'Stop Argo from automatically syncing Git changes.' },
|
|
129
|
+
{ name: 'the Application is suspended', suspended: true, copy: 'Stop Argo from automatically syncing Git changes.' },
|
|
130
|
+
{ name: 'an ApplicationSet owns it', controller: { kind: 'ApplicationSet' as const, name: 'clusters' }, copy: "Stop Argo from automatically syncing Git changes. Manual Sync still works. ApplicationSet clusters can turn it back on; to keep it off, change the ApplicationSet's template." },
|
|
131
|
+
{ name: 'a parent Application manages it', controller: { kind: 'Application' as const, name: 'apps' }, copy: 'Stop Argo from automatically syncing Git changes. Manual Sync still works. Application apps manages this one from Git and can turn it back on when it syncs; to keep it off, change it in Git.' },
|
|
132
|
+
{ name: 'an operation holds Sync on an ApplicationSet-owned app', running: true, controller: { kind: 'ApplicationSet' as const, name: 'clusters' }, copy: "Stop Argo from automatically syncing Git changes. ApplicationSet clusters can turn it back on; to keep it off, change the ApplicationSet's template." },
|
|
133
|
+
] as Array<{ name: string; running?: boolean; suspended?: boolean; controller?: ArgoActionHandlers['autoSyncController']; copy: string }>)('describes Disable auto-sync when $name', async ({ running = false, suspended, controller, copy }) => {
|
|
134
|
+
vi.useFakeTimers()
|
|
135
|
+
const element = document.createElement('div')
|
|
136
|
+
document.body.appendChild(element)
|
|
137
|
+
const root = createRoot(element)
|
|
138
|
+
try {
|
|
139
|
+
await act(async () => { root.render(layout(argo({ autoSyncEnabled: true, isRunning: running, autoSyncController: controller }), suspended ? { sync: 'Synced', health: 'Suspended', suspended: true } as GitOpsDetailStatus : null)) })
|
|
140
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent === 'Disable auto-sync')!
|
|
141
|
+
await act(async () => {
|
|
142
|
+
button.parentElement!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
|
|
143
|
+
vi.advanceTimersByTime(1000)
|
|
144
|
+
})
|
|
145
|
+
expect(document.body.querySelector('[role="tooltip"]')?.textContent).toBe(copy)
|
|
146
|
+
} finally {
|
|
147
|
+
await act(async () => root.unmount())
|
|
148
|
+
element.remove()
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('explains the disabled button with the note copy', async () => {
|
|
153
|
+
vi.useFakeTimers()
|
|
154
|
+
const element = document.createElement('div')
|
|
155
|
+
document.body.appendChild(element)
|
|
156
|
+
const root = createRoot(element)
|
|
157
|
+
await act(async () => { root.render(layout(argo({ resumeUpgrade: upgrade }))) })
|
|
158
|
+
const button = [...element.querySelectorAll('button')].find((b) => b.textContent === 'Enable auto-sync')!
|
|
159
|
+
await act(async () => {
|
|
160
|
+
button.parentElement!.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
|
|
161
|
+
vi.advanceTimersByTime(1000)
|
|
162
|
+
})
|
|
163
|
+
expect(document.body.querySelector('[role="tooltip"]')?.textContent).toBe(NOTE)
|
|
164
|
+
await act(async () => root.unmount())
|
|
165
|
+
element.remove()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('Resume in the GitOps table row menu', () => {
|
|
171
|
+
const resume = (raw: ReturnType<typeof app>, gate: RadarUpgradeRequirement | null) =>
|
|
172
|
+
buildRowActionItems(normalizeArgoApplication(raw), () => {}, undefined, gate).find((item) => item.key === 'resume')
|
|
173
|
+
const suspended = { 'radarhq.io/suspended-prune': 'true' }
|
|
174
|
+
|
|
175
|
+
it('is disabled with the note copy for a suspended app whose automated block says enabled: false', () => {
|
|
176
|
+
const item = resume(app({ enabled: false, prune: true }, suspended), upgrade)
|
|
177
|
+
expect(item).toMatchObject({ disabled: true, disabledReason: NOTE })
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('stays enabled when the Radar advertises the behavior', () => {
|
|
181
|
+
expect(resume(app({ enabled: false, prune: true }, suspended), null)).toMatchObject({ disabled: false, disabledReason: undefined })
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
// A Radar-suspended app has no automated block; every Radar resumes it.
|
|
185
|
+
it('stays enabled for a suspended app without enabled: false', () => {
|
|
186
|
+
expect(resume(app(undefined, suspended), upgrade)).toMatchObject({ disabled: false, disabledReason: undefined })
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('Enable Auto-Sync in the resource actions bar', () => {
|
|
191
|
+
function bar(data: ReturnType<typeof app>, gate: RadarUpgradeRequirement | null) {
|
|
192
|
+
const markup = html(
|
|
193
|
+
<ResourceActionsBar
|
|
194
|
+
resource={{ kind: 'applications', group: 'argoproj.io', namespace: 'argocd', name: 'guestbook' }}
|
|
195
|
+
data={data}
|
|
196
|
+
onArgoResume={() => {}}
|
|
197
|
+
argoResumeUpgrade={gate}
|
|
198
|
+
/>,
|
|
199
|
+
)
|
|
200
|
+
return buttonAttrs(markup, 'Enable Auto-Sync')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
it('is disabled for an app with enabled: false on a Radar without the behavior', () => {
|
|
204
|
+
expect(bar(app({ enabled: false, prune: true }), upgrade)).toMatch(/\sdisabled=""/)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('is enabled when the Radar advertises the behavior', () => {
|
|
208
|
+
expect(bar(app({ enabled: false, prune: true }), null)).not.toMatch(/\sdisabled=""/)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// A Manual app without an automated block resumes correctly on every Radar.
|
|
212
|
+
it('is enabled for a Manual app without an automated block', () => {
|
|
213
|
+
expect(bar(app(undefined), upgrade)).not.toMatch(/\sdisabled=""/)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe("the drawer's one-click Sync", () => {
|
|
218
|
+
it("hands the Application's sync options to the caller", async () => {
|
|
219
|
+
const onArgoSync = vi.fn()
|
|
220
|
+
const host = document.createElement('div')
|
|
221
|
+
document.body.append(host)
|
|
222
|
+
const root = createRoot(host)
|
|
223
|
+
const application = { ...app(undefined), spec: { project: 'default', source: {}, destination: {}, syncPolicy: { syncOptions: ['CreateNamespace=true'] } } }
|
|
224
|
+
await act(async () => root.render(
|
|
225
|
+
<ResourceActionsBar
|
|
226
|
+
resource={{ kind: 'applications', namespace: 'argocd', name: 'guestbook', group: 'argoproj.io' }}
|
|
227
|
+
data={application}
|
|
228
|
+
onArgoSync={onArgoSync}
|
|
229
|
+
/>,
|
|
230
|
+
))
|
|
231
|
+
const sync = [...host.querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Sync')!
|
|
232
|
+
await act(async () => sync.click())
|
|
233
|
+
expect(onArgoSync).toHaveBeenCalledWith({ namespace: 'argocd', name: 'guestbook', applicationSyncOptions: ['CreateNamespace=true'] })
|
|
234
|
+
await act(async () => root.unmount())
|
|
235
|
+
host.remove()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe("the drawer's Sync with an options dialog", () => {
|
|
240
|
+
const source = { repoURL: 'https://example.com/app.git', path: 'app', targetRevision: 'main' }
|
|
241
|
+
const application = { ...app(undefined), status: { sync: { revision: 'abc1234def', comparedTo: { source } } }, spec: { project: 'default', source, destination: {}, syncPolicy: { syncOptions: ['CreateNamespace=true'] } } }
|
|
242
|
+
type OnArgoSync = (params: object) => void | Promise<unknown>
|
|
243
|
+
const bar = (name: string, onArgoSync: OnArgoSync) => (
|
|
244
|
+
<ResourceActionsBar
|
|
245
|
+
resource={{ kind: 'applications', namespace: 'argocd', name, group: 'argoproj.io' }}
|
|
246
|
+
data={{ ...application, metadata: { ...application.metadata, name } }}
|
|
247
|
+
onArgoSync={onArgoSync}
|
|
248
|
+
argoSyncDialog={{ pinsComparedRevision: true }}
|
|
249
|
+
/>
|
|
250
|
+
)
|
|
251
|
+
async function open(onArgoSync: OnArgoSync) {
|
|
252
|
+
const host = document.createElement('div')
|
|
253
|
+
document.body.append(host)
|
|
254
|
+
const root = createRoot(host)
|
|
255
|
+
await act(async () => root.render(bar('guestbook', onArgoSync)))
|
|
256
|
+
const sync = [...host.querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Sync…')!
|
|
257
|
+
await act(async () => sync.click())
|
|
258
|
+
return {
|
|
259
|
+
rerender: async (name: string) => { await act(async () => root.render(bar(name, onArgoSync))) },
|
|
260
|
+
close: async () => { await act(async () => root.unmount()); host.remove() },
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// A closed dialog's portal stays briefly for its exit animation; use the newest.
|
|
264
|
+
const dialog = () => [...document.querySelectorAll('[role="dialog"]')].at(-1)!
|
|
265
|
+
const dialogButton = (label: string) => [...dialog().querySelectorAll<HTMLButtonElement>('button')].find((b) => b.textContent?.trim() === label)!
|
|
266
|
+
|
|
267
|
+
it('opens the dialog instead of syncing, and syncs nothing on Cancel', async () => {
|
|
268
|
+
const onArgoSync = vi.fn<OnArgoSync>()
|
|
269
|
+
const { close } = await open(onArgoSync)
|
|
270
|
+
expect(dialog().textContent).toContain('Leave empty for the revision Argo last compared (abc1234def).')
|
|
271
|
+
expect(onArgoSync).not.toHaveBeenCalled()
|
|
272
|
+
await act(async () => dialogButton('Cancel').click())
|
|
273
|
+
expect(onArgoSync).not.toHaveBeenCalled()
|
|
274
|
+
await close()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('stays open with its choices when the sync fails, and closes once one succeeds', async () => {
|
|
278
|
+
let fail = true
|
|
279
|
+
const onArgoSync = vi.fn<OnArgoSync>(() => (fail ? Promise.reject(new Error('denied')) : Promise.resolve()))
|
|
280
|
+
const { close } = await open(onArgoSync)
|
|
281
|
+
const prune = () => [...dialog().querySelectorAll('label')].find((l) => l.textContent?.startsWith('Prune'))!.querySelector('input')!
|
|
282
|
+
await act(async () => prune().click())
|
|
283
|
+
await act(async () => dialogButton('Sync now').click())
|
|
284
|
+
expect(onArgoSync).toHaveBeenCalledTimes(1)
|
|
285
|
+
expect(dialog().textContent).toContain('Sync now')
|
|
286
|
+
expect(dialog().querySelector('[role="alert"]')?.textContent).toBe('Sync failed: denied')
|
|
287
|
+
expect(prune().checked).toBe(true)
|
|
288
|
+
fail = false
|
|
289
|
+
await act(async () => dialogButton('Sync now').click())
|
|
290
|
+
expect(onArgoSync.mock.calls[1][0]).toMatchObject({ prune: true })
|
|
291
|
+
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 600)) })
|
|
292
|
+
expect(document.body.textContent).not.toContain('Sync now')
|
|
293
|
+
await close()
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('is disabled while an Argo operation runs, instead of opening a dialog that can do nothing', async () => {
|
|
297
|
+
const host = document.createElement('div')
|
|
298
|
+
document.body.append(host)
|
|
299
|
+
const root = createRoot(host)
|
|
300
|
+
const running = { ...application, status: { ...application.status, operationState: { phase: 'Running' } } }
|
|
301
|
+
await act(async () => root.render(
|
|
302
|
+
<ResourceActionsBar
|
|
303
|
+
resource={{ kind: 'applications', namespace: 'argocd', name: 'guestbook', group: 'argoproj.io' }}
|
|
304
|
+
data={running}
|
|
305
|
+
onArgoSync={vi.fn<OnArgoSync>()}
|
|
306
|
+
argoSyncDialog={{ pinsComparedRevision: true }}
|
|
307
|
+
/>,
|
|
308
|
+
))
|
|
309
|
+
const sync = [...host.querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Sync…')!
|
|
310
|
+
expect(sync.disabled).toBe(true)
|
|
311
|
+
await act(async () => root.unmount())
|
|
312
|
+
host.remove()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('closes, dropping its choices, when the drawer switches to another Application', async () => {
|
|
316
|
+
const onArgoSync = vi.fn<OnArgoSync>()
|
|
317
|
+
const { rerender, close } = await open(onArgoSync)
|
|
318
|
+
const prune = [...dialog().querySelectorAll('label')].find((l) => l.textContent?.startsWith('Prune'))!.querySelector('input')!
|
|
319
|
+
await act(async () => prune.click())
|
|
320
|
+
await rerender('other')
|
|
321
|
+
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 600)) })
|
|
322
|
+
expect(document.body.textContent).not.toContain('Sync now')
|
|
323
|
+
await rerender('guestbook')
|
|
324
|
+
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 600)) })
|
|
325
|
+
expect(document.body.textContent).not.toContain('Sync now')
|
|
326
|
+
await rerender('other')
|
|
327
|
+
const reopen = [...document.querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Sync…')!
|
|
328
|
+
await act(async () => reopen.click())
|
|
329
|
+
const prunedAgain = [...dialog().querySelectorAll('label')].find((l) => l.textContent?.startsWith('Prune'))!.querySelector('input')!
|
|
330
|
+
expect(prunedAgain.checked).toBe(false)
|
|
331
|
+
await act(async () => dialogButton('Sync now').click())
|
|
332
|
+
expect(onArgoSync.mock.calls[0][0]).toMatchObject({ name: 'other', prune: false })
|
|
333
|
+
await close()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it("syncs with the dialog's choices and the Application's options", async () => {
|
|
337
|
+
const onArgoSync = vi.fn<OnArgoSync>()
|
|
338
|
+
const { close } = await open(onArgoSync)
|
|
339
|
+
const prune = [...dialog().querySelectorAll('label')].find((l) => l.textContent?.startsWith('Prune'))!.querySelector('input')!
|
|
340
|
+
await act(async () => prune.click())
|
|
341
|
+
const confirm = dialogButton('Sync now')
|
|
342
|
+
expect(confirm.disabled).toBe(false)
|
|
343
|
+
await act(async () => confirm.click())
|
|
344
|
+
expect(onArgoSync).toHaveBeenCalledTimes(1)
|
|
345
|
+
expect(onArgoSync.mock.calls[0][0]).toMatchObject({ namespace: 'argocd', name: 'guestbook', applicationSyncOptions: ['CreateNamespace=true'], prune: true, dryRun: false })
|
|
346
|
+
expect(onArgoSync.mock.calls[0][0]).not.toHaveProperty('syncOptions')
|
|
347
|
+
await act(async () => { await new Promise((resolve) => setTimeout(resolve, 600)) })
|
|
348
|
+
expect(document.body.textContent).not.toContain('Sync now')
|
|
349
|
+
await close()
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe('auto-sync support pending or unavailable', () => {
|
|
354
|
+
it.each(["Checking this cluster's Radar…", "Couldn't check this cluster's Radar support. Refresh to try again."])('disables each affected control with its reason: %s', reason => {
|
|
355
|
+
const markup = html(layout(argo({ resumeBlockedReason: reason })))
|
|
356
|
+
expect(enableAutoSync(markup)).toMatch(/\sdisabled=""/)
|
|
357
|
+
expect(markup).toContain(reason.replace(/'/g, '''))
|
|
358
|
+
const raw = app({ enabled: false }, { 'radarhq.io/suspended-prune': 'true' })
|
|
359
|
+
const onAction = vi.fn()
|
|
360
|
+
const item = buildRowActionItems(normalizeArgoApplication(raw), onAction, undefined, null, reason).find(i => i.key === 'resume')!
|
|
361
|
+
expect(item).toMatchObject({ disabled: true, disabledReason: reason })
|
|
362
|
+
item.onClick()
|
|
363
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
364
|
+
const bar = html(<ResourceActionsBar resource={{ kind: 'applications', group: 'argoproj.io', namespace: 'argocd', name: 'guestbook' }} data={raw} onArgoResume={() => {}} argoResumeBlockedReason={reason} />)
|
|
365
|
+
expect(buttonAttrs(bar, 'Enable Auto-Sync')).toMatch(/\sdisabled=""/)
|
|
366
|
+
const ordinary = buildRowActionItems(normalizeArgoApplication(app(undefined, { 'radarhq.io/suspended-prune': 'true' })), onAction, undefined, null, reason).find(i => i.key === 'resume')!
|
|
367
|
+
expect(ordinary.disabled).toBe(false)
|
|
368
|
+
})
|
|
369
|
+
})
|
|
@@ -20,8 +20,9 @@ describe('parseArgoRollbackID', () => {
|
|
|
20
20
|
test('empty string returns null (would be Number("") === 0)', () => {
|
|
21
21
|
expect(parseArgoRollbackID('')).toBeNull()
|
|
22
22
|
})
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// Argo numbers history from 0, so the first deploy is a rollback target.
|
|
24
|
+
test('"0" is the first history entry', () => {
|
|
25
|
+
expect(parseArgoRollbackID('0')).toBe(0)
|
|
25
26
|
})
|
|
26
27
|
test('negative numbers return null', () => {
|
|
27
28
|
expect(parseArgoRollbackID('-5')).toBeNull()
|
|
@@ -29,6 +30,11 @@ describe('parseArgoRollbackID', () => {
|
|
|
29
30
|
test('Flux condition type string returns null', () => {
|
|
30
31
|
expect(parseArgoRollbackID('Ready')).toBeNull()
|
|
31
32
|
})
|
|
33
|
+
// Number() reads each of these as a number; with 0 now valid, a blank id
|
|
34
|
+
// would otherwise target the first deploy.
|
|
35
|
+
test.each([' ', '0x1f', '1e3', '1.5', '+1', ' 1'])('%j returns null', (id) => {
|
|
36
|
+
expect(parseArgoRollbackID(id)).toBeNull()
|
|
37
|
+
})
|
|
32
38
|
test('positive integer returns the number', () => {
|
|
33
39
|
expect(parseArgoRollbackID('42')).toBe(42)
|
|
34
40
|
})
|
|
@@ -52,19 +52,17 @@ export function gitOpsInsightChangeKey(ref: { kind: string; namespace?: string;
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// parseArgoRollbackID parses an Argo HistoryItem.id into the int64 the
|
|
55
|
-
// rollback API needs.
|
|
56
|
-
//
|
|
57
|
-
// - id is
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// first revision).
|
|
55
|
+
// rollback API needs. Argo's controller numbers history from 0, so "0" is
|
|
56
|
+
// the first deploy and a valid target. Returns null when:
|
|
57
|
+
// - id is missing or empty
|
|
58
|
+
// - id is not a plain non-negative integer: Flux condition rows reuse the
|
|
59
|
+
// same slot for condition.type, and Number() would also read "", " ",
|
|
60
|
+
// "0x1f" or "1e3" as a number and route the rollback to a wrong entry
|
|
61
|
+
// - id is negative
|
|
63
62
|
export function parseArgoRollbackID(id: string | undefined): number | null {
|
|
64
|
-
if (!id) return null
|
|
63
|
+
if (!id || !/^\d+$/.test(id)) return null
|
|
65
64
|
const n = Number(id)
|
|
66
|
-
|
|
67
|
-
return n
|
|
65
|
+
return Number.isSafeInteger(n) ? n : null
|
|
68
66
|
}
|
|
69
67
|
|
|
70
68
|
// describeGitOpsTerminating renders the two tooltip strings the detail
|
|
@@ -44,7 +44,7 @@ export {
|
|
|
44
44
|
gitOpsDetailGroup,
|
|
45
45
|
isDestinationRef,
|
|
46
46
|
} from './detail-helpers'
|
|
47
|
-
export { SyncOptionsDialog } from './SyncOptionsDialog'
|
|
47
|
+
export { SyncOptionsDialog, argoSyncOptionsForRequest } from './SyncOptionsDialog'
|
|
48
48
|
export type { SyncOptionsDialogProps, ArgoSyncOpts } from './SyncOptionsDialog'
|
|
49
49
|
export { RollbackDialog } from './RollbackDialog'
|
|
50
50
|
export type { RollbackDialogProps } from './RollbackDialog'
|
|
@@ -113,6 +113,38 @@ describe('GitOpsChangesView expanded Argo row', () => {
|
|
|
113
113
|
expect(html).not.toContain('Sync the application to create it')
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
+
test.each([
|
|
117
|
+
['ManualDrift', 'The render includes none of the resources this Application manages.', 'Manual'],
|
|
118
|
+
['AutoSyncBlockedEmpty', 'A pruning sync could delete every listed resource.', 'Auto · prune'],
|
|
119
|
+
['SharedResourceWarning', 'Resolve shared ownership before syncing.', 'Manual'],
|
|
120
|
+
['SelfHealDisabled', 'Resolve shared ownership before syncing.', 'Auto'],
|
|
121
|
+
['Failed', 'Fix the comparison error and refresh before syncing.', 'Manual'],
|
|
122
|
+
])('defers resource sync/create advice to the %s hazard', (reason, hazard, autoSyncMode) => {
|
|
123
|
+
const data = insight(false)
|
|
124
|
+
data.summary.autoSyncMode = autoSyncMode
|
|
125
|
+
data.issues = [{ severity: 'warning', scope: 'operation', reason, message: 'Sync warning', hazard }]
|
|
126
|
+
data.changes = [
|
|
127
|
+
{ ...change, drift: undefined },
|
|
128
|
+
{ ...change, ref: { ...change.ref, name: 'missing' }, missing: true, hasLive: false, health: 'Missing', drift: undefined },
|
|
129
|
+
{ ...change, ref: { ...change.ref, name: 'health-missing' }, sync: 'Unknown', health: 'Missing', drift: undefined },
|
|
130
|
+
]
|
|
131
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={data} />)
|
|
132
|
+
expect(count(html, 'See the sync warning above before syncing this Application.')).toBe(3)
|
|
133
|
+
expect(html).not.toContain('Click Sync')
|
|
134
|
+
expect(html).not.toContain('click Sync to force it')
|
|
135
|
+
expect(html).not.toContain('Auto-sync should reconcile')
|
|
136
|
+
expect(html).not.toContain('Sync the application to create it')
|
|
137
|
+
expect(html).not.toContain('Sync to create it')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('keeps ordinary sync advice when there is no hazard', () => {
|
|
141
|
+
const data = insight(false)
|
|
142
|
+
data.summary.autoSyncMode = 'Manual'
|
|
143
|
+
const html = renderToStaticMarkup(<GitOpsChangesView insight={data} />)
|
|
144
|
+
expect(html).toContain('Click Sync to apply the desired state.')
|
|
145
|
+
expect(html).not.toContain('See the sync warning above')
|
|
146
|
+
})
|
|
147
|
+
|
|
116
148
|
test("hands the last-applied list to the Argo diff instead of rendering it alongside", () => {
|
|
117
149
|
let passed: GitOpsDrift | undefined
|
|
118
150
|
const html = renderToStaticMarkup(
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import type { GitOpsInsight } from '../../../types'
|
|
4
|
+
import type { RadarUpgradeRequirement } from '../../../types/fetch-error'
|
|
5
|
+
import { GitOpsActivityInsightView } from './GitOpsInsightViews'
|
|
6
|
+
|
|
7
|
+
function render(insight: GitOpsInsight, rollbackUpgrade?: RadarUpgradeRequirement | null): string {
|
|
8
|
+
return renderToString(<GitOpsActivityInsightView insight={insight} onRollback={() => {}} rollbackUpgrade={rollbackUpgrade} />).replace(/<!-- -->/g, '')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function rollbackButtons(insight: GitOpsInsight, rollbackUpgrade?: RadarUpgradeRequirement | null): { disabled: boolean }[] {
|
|
12
|
+
const html = render(insight, rollbackUpgrade)
|
|
13
|
+
return [...html.matchAll(/<button([^>]*)>Rollback<\/button>/g)].map((m) => ({ disabled: /\sdisabled=""/.test(m[1]) }))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function insight(autoSyncMode: string): GitOpsInsight {
|
|
17
|
+
return {
|
|
18
|
+
summary: { tool: 'argocd', kind: 'Application', namespace: 'argocd', name: 'guestbook', autoSyncMode },
|
|
19
|
+
capabilities: { sync: true, refresh: true, terminate: true, suspend: true, resume: true, syncWithSource: false, selectiveSync: true, rollback: true },
|
|
20
|
+
history: [
|
|
21
|
+
{ phase: 'Succeeded', revision: 'eee555' },
|
|
22
|
+
{ id: '2', revision: 'ccc333', rollbackable: true },
|
|
23
|
+
{ id: '1', revision: 'bbb222', rollbackable: false },
|
|
24
|
+
{ id: '0', revision: 'aaa111' },
|
|
25
|
+
],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('GitOpsActivityInsightView rollback', () => {
|
|
30
|
+
// The rollback API refuses an entry with no source, so its button is
|
|
31
|
+
// disabled; the operation row has no id and gets no button.
|
|
32
|
+
it('disables Rollback only on entries the backend marks not rollbackable', () => {
|
|
33
|
+
expect(rollbackButtons(insight('Manual'))).toEqual([{ disabled: false }, { disabled: true }, { disabled: false }])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('disables every Rollback while auto-sync is on', () => {
|
|
37
|
+
expect(rollbackButtons(insight('Auto · prune'))).toEqual([{ disabled: true }, { disabled: true }, { disabled: true }])
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// An older Radar writes a rollback the API server drops (and refuses id 0),
|
|
41
|
+
// so a newer UI disables every entry and says which Radar it needs.
|
|
42
|
+
describe('on a Radar without argoRollbackSync', () => {
|
|
43
|
+
const upgrade = { feature: 'Argo CD rollbacks', currentVersion: 'v1.16.3', latestVersion: 'v1.17.0' }
|
|
44
|
+
|
|
45
|
+
it('disables every Rollback and shows the upgrade note', () => {
|
|
46
|
+
expect(rollbackButtons(insight('Manual'), upgrade)).toEqual([{ disabled: true }, { disabled: true }, { disabled: true }])
|
|
47
|
+
// RadarUpgradeNote sets the feature in its own <span>.
|
|
48
|
+
expect(render(insight('Manual'), upgrade)).toContain('Argo CD rollbacks</span> need the latest Radar (v1.17) on this cluster. It runs v1.16.3.')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('shows no note when the Radar advertises the behavior', () => {
|
|
52
|
+
expect(render(insight('Manual'), null)).not.toContain('Argo CD rollbacks need')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
})
|