@skyhook-io/k8s-ui 1.19.0 → 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/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/GitOpsActivityInsightView.test.tsx +55 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +30 -12
- 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 +37 -0
- package/src/components/gitops/insights/insights-helpers.ts +12 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -1
- package/src/components/issues/diagnostic.ts +14 -1
- package/src/components/issues/severity.ts +14 -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 +40 -0
- package/src/types/gitops-insights.ts +11 -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
|
@@ -4,6 +4,7 @@ import { AlertTriangle, CheckCircle2, CircleAlert, Loader2, RefreshCw, ShieldChe
|
|
|
4
4
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
5
5
|
import { Input } from '../ui/Input'
|
|
6
6
|
import { Tooltip } from '../ui/Tooltip'
|
|
7
|
+
import { SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
7
8
|
import type { GitOpsInsightRef } from '../../types/gitops-insights'
|
|
8
9
|
|
|
9
10
|
// =============================================================================
|
|
@@ -13,9 +14,13 @@ import type { GitOpsInsightRef } from '../../types/gitops-insights'
|
|
|
13
14
|
// the onConfirm callback (POST to /api/argo/applications/{ns}/{name}/sync
|
|
14
15
|
// or the hub-proxied equivalent /c/{ctrl}/api/argo/...).
|
|
15
16
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
17
|
+
// Prune starts off, as in Argo CD's UI and CLI, and is always sent. Resource
|
|
18
|
+
// sync does not prune and omits revision/apply-only.
|
|
19
|
+
//
|
|
20
|
+
// Sync options start from the Application's own (applicationSyncOptions). A
|
|
21
|
+
// sync whose options the user leaves alone sends none, so Radar applies the
|
|
22
|
+
// Application's current ones; flipping Replace or Server-side apply sends the
|
|
23
|
+
// Application's options with that edit, keeping all other options.
|
|
19
24
|
// =============================================================================
|
|
20
25
|
|
|
21
26
|
// ArgoSyncOpts is the payload shape the dialog passes to its onConfirm
|
|
@@ -29,19 +34,35 @@ export interface ArgoSyncOpts {
|
|
|
29
34
|
dryRun: boolean
|
|
30
35
|
force: boolean
|
|
31
36
|
applyOnly: boolean
|
|
32
|
-
|
|
37
|
+
// Omitted: sync with the Application's own spec.syncPolicy.syncOptions.
|
|
38
|
+
// Present: the options for this sync, replacing the Application's ([] = none).
|
|
39
|
+
// Only a Radar advertising `argoSyncInheritance` reads an omission that
|
|
40
|
+
// way; see argoSyncOptionsForRequest.
|
|
41
|
+
syncOptions?: string[]
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export interface SyncOptionsDialogProps {
|
|
36
45
|
open: boolean
|
|
37
46
|
appLabel: string
|
|
47
|
+
multiSource?: boolean
|
|
48
|
+
comparedRevision?: string
|
|
49
|
+
// An omitted revision will use the last compared one: the serving Radar
|
|
50
|
+
// pins (argoSyncInheritance) and the comparison matches the current
|
|
51
|
+
// source(s) (argoComparedRevisionCurrent). Otherwise the target is resolved
|
|
52
|
+
// when the sync runs.
|
|
53
|
+
pinsComparedRevision?: boolean
|
|
38
54
|
resource?: GitOpsInsightRef
|
|
55
|
+
// The Application's spec.syncPolicy.syncOptions. Without it the toggles
|
|
56
|
+
// are disabled and the submitted syncOptions are omitted.
|
|
57
|
+
applicationSyncOptions?: string[]
|
|
39
58
|
pending?: boolean
|
|
40
59
|
autoSyncEnabled?: boolean
|
|
41
60
|
validationPending?: boolean
|
|
42
61
|
operationInProgress?: boolean
|
|
43
62
|
validationResult?: ResourceValidationResult | null
|
|
44
63
|
validationError?: string | null
|
|
64
|
+
// Why the last confirm failed, for a host that keeps the dialog open to retry.
|
|
65
|
+
syncError?: string | null
|
|
45
66
|
onCancel: () => void
|
|
46
67
|
onConfirm: (opts: ArgoSyncOpts) => void
|
|
47
68
|
onValidate?: (opts: ArgoSyncOpts) => void
|
|
@@ -57,39 +78,94 @@ export interface ResourceValidationResult {
|
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
80
|
|
|
60
|
-
export function SyncOptionsDialog({ open, appLabel, resource, pending, autoSyncEnabled, validationPending, operationInProgress, validationResult, validationError, onCancel, onConfirm, onValidate, onValidationReset }: SyncOptionsDialogProps) {
|
|
81
|
+
export function SyncOptionsDialog({ open, appLabel, multiSource, comparedRevision, pinsComparedRevision, resource, applicationSyncOptions, pending, autoSyncEnabled, validationPending, operationInProgress, validationResult, validationError, syncError, onCancel, onConfirm, onValidate, onValidationReset }: SyncOptionsDialogProps) {
|
|
61
82
|
const titleId = useId()
|
|
83
|
+
const syncOptionsUnknown = applicationSyncOptions === undefined
|
|
84
|
+
const liveAppOptions = applicationSyncOptions ?? EMPTY_OPTIONS
|
|
85
|
+
// Joined so a caller passing a fresh array each render isn't a change.
|
|
86
|
+
const liveAppOptionsKey = liveAppOptions.join('\n')
|
|
87
|
+
// The options the toggles started from, which the request is built from.
|
|
88
|
+
// They follow the Application until the user edits an option, so a refetch
|
|
89
|
+
// while the dialog is open never replaces what the user sees or chose.
|
|
90
|
+
const [appOptions, setAppOptions] = useState(liveAppOptions)
|
|
91
|
+
const [optionsEdited, setOptionsEdited] = useState(false)
|
|
92
|
+
const optionsChangedWhileEditing = optionsEdited && liveAppOptionsKey !== appOptions.join('\n')
|
|
93
|
+
const appReplace = appOptions.includes(REPLACE_OPTION)
|
|
94
|
+
const appServerSideApply = appOptions.includes(SERVER_SIDE_APPLY_OPTION)
|
|
95
|
+
const otherAppOptions = appOptions.filter((option) => !isToggledOption(option))
|
|
96
|
+
const comparedRevisionLabel = comparedRevision && /^[0-9a-f]{40}$/i.test(comparedRevision) ? comparedRevision.slice(0, 7) : comparedRevision
|
|
62
97
|
const [revision, setRevision] = useState('')
|
|
63
|
-
const [prune, setPrune] = useState(
|
|
98
|
+
const [prune, setPrune] = useState(false)
|
|
64
99
|
const [dryRun, setDryRun] = useState(false)
|
|
65
100
|
const [force, setForce] = useState(false)
|
|
66
101
|
const [applyOnly, setApplyOnly] = useState(false)
|
|
67
|
-
const [replace, setReplace] = useState(
|
|
68
|
-
const [serverSideApply, setServerSideApply] = useState(
|
|
102
|
+
const [replace, setReplace] = useState(appReplace)
|
|
103
|
+
const [serverSideApply, setServerSideApply] = useState(appServerSideApply)
|
|
69
104
|
const richResourceValidation = !!resource && !!onValidate
|
|
70
105
|
const busy = !!pending || !!validationPending
|
|
71
106
|
const optionsDisabled = busy || !!operationInProgress
|
|
107
|
+
const revisionDisabled = !!autoSyncEnabled && !dryRun
|
|
108
|
+
const targetLabel = pinsComparedRevision
|
|
109
|
+
? `the revision Argo last compared${comparedRevisionLabel ? ` (${comparedRevisionLabel})` : ''}`
|
|
110
|
+
: 'the target revision, resolved when the sync runs'
|
|
111
|
+
|
|
112
|
+
function startFromApplicationOptions(options: string[]) {
|
|
113
|
+
setAppOptions(options)
|
|
114
|
+
setReplace(options.includes(REPLACE_OPTION))
|
|
115
|
+
setServerSideApply(options.includes(SERVER_SIDE_APPLY_OPTION))
|
|
116
|
+
}
|
|
72
117
|
|
|
73
118
|
// Reset on each open so a previous attempt's flags don't leak into the
|
|
74
119
|
// next sync — easy footgun in modal-heavy flows.
|
|
75
120
|
useEffect(() => {
|
|
76
121
|
if (open) {
|
|
77
122
|
setRevision('')
|
|
78
|
-
setPrune(
|
|
123
|
+
setPrune(false)
|
|
79
124
|
setDryRun(false)
|
|
80
125
|
setForce(false)
|
|
81
126
|
setApplyOnly(false)
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
setOptionsEdited(false)
|
|
128
|
+
startFromApplicationOptions(liveAppOptions)
|
|
84
129
|
}
|
|
130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
131
|
}, [open, resource])
|
|
86
132
|
|
|
133
|
+
// A revision that no longer applies is cleared rather than left on screen
|
|
134
|
+
// while the sync deploys something else.
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (revisionDisabled || multiSource) setRevision('')
|
|
137
|
+
}, [revisionDisabled, multiSource])
|
|
138
|
+
|
|
139
|
+
// The Application's options changed while the dialog is open and the user
|
|
140
|
+
// hasn't edited one: start from the new ones, and drop a dry-run result
|
|
141
|
+
// that was computed with the old ones.
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (!open || optionsEdited || liveAppOptionsKey === appOptions.join('\n')) return
|
|
144
|
+
startFromApplicationOptions(liveAppOptions)
|
|
145
|
+
onValidationReset?.()
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, [liveAppOptionsKey])
|
|
148
|
+
|
|
149
|
+
// Omitting the options means "the Application's current ones", which is
|
|
150
|
+
// what the toggles show only while the snapshot is still current.
|
|
151
|
+
const optionEdits = syncOptionsUnknown
|
|
152
|
+
? { replace: false, serverSideApply: false }
|
|
153
|
+
: { replace, serverSideApply, applicationSyncOptions: appOptions, sendUnchanged: appOptions.join('\n') !== liveAppOptionsKey }
|
|
154
|
+
|
|
155
|
+
// Replace and Force can take workloads down, so a real sync using either says
|
|
156
|
+
// so beside the button and in its label: Argo CD's own panel asks for a
|
|
157
|
+
// confirmation on each. Replace counts when inherited from the Application.
|
|
158
|
+
const disruptive = !dryRun || richResourceValidation
|
|
159
|
+
? [replace && 'replace', force && 'force'].filter((mode): mode is string => !!mode)
|
|
160
|
+
: []
|
|
161
|
+
const syncLabel = resource ? 'Sync resource' : 'Sync now'
|
|
162
|
+
|
|
87
163
|
function submit() {
|
|
88
|
-
onConfirm(buildArgoSyncOpts({ resourceMode: !!resource, revision, prune, dryRun: richResourceValidation ? false : dryRun, force, applyOnly,
|
|
164
|
+
onConfirm(buildArgoSyncOpts({ resourceMode: !!resource, revision: multiSource || revisionDisabled ? '' : revision, prune, dryRun: richResourceValidation ? false : dryRun, force, applyOnly, ...optionEdits }))
|
|
89
165
|
}
|
|
90
166
|
|
|
91
167
|
function validate() {
|
|
92
|
-
onValidate?.(buildArgoSyncOpts({ resourceMode: true, revision, prune, dryRun: true, force, applyOnly,
|
|
168
|
+
onValidate?.(buildArgoSyncOpts({ resourceMode: true, revision, prune, dryRun: true, force, applyOnly, ...optionEdits }))
|
|
93
169
|
}
|
|
94
170
|
|
|
95
171
|
function updateValidationOption(setter: (value: boolean) => void, value: boolean) {
|
|
@@ -97,13 +173,18 @@ export function SyncOptionsDialog({ open, appLabel, resource, pending, autoSyncE
|
|
|
97
173
|
onValidationReset?.()
|
|
98
174
|
}
|
|
99
175
|
|
|
176
|
+
function updateSyncOption(setter: (value: boolean) => void, value: boolean) {
|
|
177
|
+
setOptionsEdited(true)
|
|
178
|
+
updateValidationOption(setter, value)
|
|
179
|
+
}
|
|
180
|
+
|
|
100
181
|
return (
|
|
101
|
-
<DialogPortal ariaLabelledBy={titleId} open={open} onClose={busy ? () => {} : onCancel} className="w-[480px]" closable={!busy}>
|
|
102
|
-
<div className="border-b border-theme-border px-4 py-3">
|
|
182
|
+
<DialogPortal ariaLabelledBy={titleId} open={open} onClose={busy ? () => {} : onCancel} className="flex max-h-[calc(100vh-2rem)] w-[480px] flex-col" closable={!busy}>
|
|
183
|
+
<div className="shrink-0 border-b border-theme-border px-4 py-3">
|
|
103
184
|
<h2 id={titleId} className="text-sm font-semibold text-theme-text-primary">{resource ? 'Sync resource' : 'Sync application'}</h2>
|
|
104
185
|
<p className="mt-0.5 break-all text-xs text-theme-text-tertiary">{appLabel}</p>
|
|
105
186
|
</div>
|
|
106
|
-
<div className="space-y-4 px-4 py-4 text-sm">
|
|
187
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4 text-sm">
|
|
107
188
|
{resource && (
|
|
108
189
|
<div className="rounded-md border border-amber-500/40 bg-amber-500/5 px-3 py-2.5">
|
|
109
190
|
<div className="flex items-start gap-2">
|
|
@@ -113,32 +194,37 @@ export function SyncOptionsDialog({ open, appLabel, resource, pending, autoSyncE
|
|
|
113
194
|
{resource.kind} / {resource.namespace ? `${resource.namespace}/` : ''}{resource.name}
|
|
114
195
|
</div>
|
|
115
196
|
<p className="mt-1 text-[11px] leading-relaxed text-theme-text-secondary">
|
|
116
|
-
|
|
197
|
+
Syncs only this resource, without hooks. Resources it depends on aren't synced; namespace creation still applies. Uses {multiSource && pinsComparedRevision ? 'the revisions Argo last compared' : targetLabel}.
|
|
117
198
|
</p>
|
|
118
199
|
{autoSyncEnabled && (
|
|
119
|
-
<p className="mt-
|
|
120
|
-
Auto-sync
|
|
200
|
+
<p className="mt-1 text-[11px] leading-relaxed text-theme-text-secondary">
|
|
201
|
+
Auto-sync stays on during the dry run, so Argo can still sync the Application meanwhile.
|
|
121
202
|
</p>
|
|
122
203
|
)}
|
|
123
204
|
</div>
|
|
124
205
|
</div>
|
|
125
206
|
</div>
|
|
126
207
|
)}
|
|
127
|
-
{!resource && (
|
|
208
|
+
{!resource && !multiSource && (
|
|
128
209
|
<label className="block">
|
|
129
210
|
<span className="text-xs font-medium text-theme-text-secondary">Revision (optional)</span>
|
|
130
211
|
<Input
|
|
131
212
|
value={revision}
|
|
132
213
|
onChange={(e) => setRevision(e.target.value)}
|
|
133
|
-
placeholder=
|
|
134
|
-
disabled={optionsDisabled}
|
|
214
|
+
placeholder={pinsComparedRevision ? 'Last compared revision' : 'Target revision'}
|
|
215
|
+
disabled={optionsDisabled || revisionDisabled}
|
|
135
216
|
className="mt-1 w-full rounded-md border border-theme-border bg-theme-base px-2 py-1.5 font-mono text-xs text-theme-text-primary outline-none placeholder:text-theme-text-tertiary focus:border-sky-500"
|
|
136
217
|
/>
|
|
137
218
|
<span className="mt-0.5 block text-[11px] text-theme-text-tertiary">
|
|
138
|
-
|
|
219
|
+
{revisionDisabled
|
|
220
|
+
? `Auto-sync is on, so this sync uses ${targetLabel}. Turn off auto-sync to deploy another revision; a dry run can preview one.`
|
|
221
|
+
: `Branch, tag, or commit SHA. Leave empty for ${targetLabel}.`}
|
|
139
222
|
</span>
|
|
140
223
|
</label>
|
|
141
224
|
)}
|
|
225
|
+
{!resource && multiSource && (
|
|
226
|
+
<p className="text-xs text-theme-text-tertiary">{pinsComparedRevision ? 'Syncs each source at the revision Argo last compared.' : "Syncs each source's target revision, resolved when the sync runs."}</p>
|
|
227
|
+
)}
|
|
142
228
|
|
|
143
229
|
{/* Common (Prune / Dry run) sit above a divider; Advanced toggles
|
|
144
230
|
stay accessible but visually subordinate so the common-case user
|
|
@@ -154,9 +240,38 @@ export function SyncOptionsDialog({ open, appLabel, resource, pending, autoSyncE
|
|
|
154
240
|
<legend className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Advanced</legend>
|
|
155
241
|
{!resource && <Toggle label="Apply only" checked={applyOnly} onChange={setApplyOnly} disabled={optionsDisabled} hint="Skip PreSync / PostSync / SyncFail hooks." />}
|
|
156
242
|
<Toggle label="Force" checked={force} onChange={(value) => updateValidationOption(setForce, value)} disabled={optionsDisabled} hint="Use kubectl --force; required for some immutable-field changes." />
|
|
157
|
-
<Toggle label="Replace" checked={replace} onChange={(value) =>
|
|
158
|
-
<Toggle label="Server-side apply" checked={serverSideApply} onChange={(value) =>
|
|
243
|
+
<Toggle label="Replace" checked={replace} onChange={(value) => updateSyncOption(setReplace, value)} disabled={optionsDisabled || syncOptionsUnknown} hint={`kubectl replace instead of apply (drops fields not in source).${appReplace ? FROM_APPLICATION : ''}`} />
|
|
244
|
+
<Toggle label="Server-side apply" checked={serverSideApply} onChange={(value) => updateSyncOption(setServerSideApply, value)} disabled={optionsDisabled || syncOptionsUnknown} hint={`Use the K8s server-side apply mechanism for ownership tracking.${appServerSideApply ? FROM_APPLICATION : ''}`} />
|
|
245
|
+
{syncOptionsUnknown && (
|
|
246
|
+
<p className="text-xs text-theme-text-tertiary">This view can't show or change the Application's sync options, so Replace and Server-side apply are unavailable here.</p>
|
|
247
|
+
)}
|
|
248
|
+
{optionsChangedWhileEditing && (
|
|
249
|
+
<p className="text-xs text-theme-text-secondary">The Application's sync options changed while this dialog was open. This sync uses the options shown here. Cancel and reopen to load the latest.</p>
|
|
250
|
+
)}
|
|
251
|
+
{otherAppOptions.length > 0 && (
|
|
252
|
+
<div>
|
|
253
|
+
<div className="text-xs font-medium text-theme-text-secondary">Also used for this sync</div>
|
|
254
|
+
<ul className="mt-1 space-y-1 text-[11px] text-theme-text-tertiary">
|
|
255
|
+
{otherAppOptions.map((option, index) => <li key={index} className="break-all font-mono">{option}</li>)}
|
|
256
|
+
</ul>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
159
259
|
</fieldset>
|
|
260
|
+
{syncError && !pending && (
|
|
261
|
+
<p role="alert" className={`flex items-start gap-1.5 text-xs ${SEVERITY_TEXT.error}`}>
|
|
262
|
+
<XCircle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
263
|
+
<span className="max-h-20 min-w-0 overflow-auto break-words">Sync failed: {syncError}</span>
|
|
264
|
+
</p>
|
|
265
|
+
)}
|
|
266
|
+
{disruptive.length > 0 && (
|
|
267
|
+
<p className={`flex items-start gap-1.5 text-xs ${SEVERITY_TEXT.warning}`}>
|
|
268
|
+
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
269
|
+
<span>
|
|
270
|
+
{replace && 'Replace deletes and recreates resources instead of updating them, which can cause downtime. '}
|
|
271
|
+
{force && 'Force removes resources immediately, skipping graceful deletion, which can lose data.'}
|
|
272
|
+
</span>
|
|
273
|
+
</p>
|
|
274
|
+
)}
|
|
160
275
|
{richResourceValidation && (validationPending || validationResult || validationError) && (
|
|
161
276
|
<ValidationResult pending={!!validationPending} result={validationResult} error={validationError} />
|
|
162
277
|
)}
|
|
@@ -188,7 +303,7 @@ export function SyncOptionsDialog({ open, appLabel, resource, pending, autoSyncE
|
|
|
188
303
|
</button>
|
|
189
304
|
</Tooltip>
|
|
190
305
|
)}
|
|
191
|
-
<PrimaryButton onClick={submit} disabled={optionsDisabled} icon={pending ? Loader2 : RefreshCw} loading={pending} label={dryRun && !richResourceValidation ? 'Run dry-run' :
|
|
306
|
+
<PrimaryButton onClick={submit} disabled={optionsDisabled} icon={pending ? Loader2 : RefreshCw} loading={pending} label={dryRun && !richResourceValidation ? 'Run dry-run' : disruptive.length > 0 ? `${syncLabel === 'Sync now' ? 'Sync' : syncLabel} with ${disruptive.join(' and ')}` : syncLabel} warning={disruptive.length > 0} />
|
|
192
307
|
</div>
|
|
193
308
|
</DialogPortal>
|
|
194
309
|
)
|
|
@@ -249,7 +364,19 @@ function ValidationResult({ pending, result, error }: { pending: boolean; result
|
|
|
249
364
|
)
|
|
250
365
|
}
|
|
251
366
|
|
|
252
|
-
|
|
367
|
+
const EMPTY_OPTIONS: string[] = []
|
|
368
|
+
const REPLACE_OPTION = 'Replace=true'
|
|
369
|
+
const SERVER_SIDE_APPLY_OPTION = 'ServerSideApply=true'
|
|
370
|
+
const FROM_APPLICATION = " On in the Application's sync policy."
|
|
371
|
+
|
|
372
|
+
// The dialog's Replace and Server-side apply toggles own these keys, whatever
|
|
373
|
+
// value the Application gives them.
|
|
374
|
+
function isToggledOption(option: string): boolean {
|
|
375
|
+
const key = option.split('=')[0].trim()
|
|
376
|
+
return key === 'Replace' || key === 'ServerSideApply'
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function buildArgoSyncOpts({ resourceMode, revision, prune, dryRun, force, applyOnly, replace, serverSideApply, applicationSyncOptions = EMPTY_OPTIONS, sendUnchanged = false }: {
|
|
253
380
|
resourceMode: boolean
|
|
254
381
|
revision: string
|
|
255
382
|
prune: boolean
|
|
@@ -258,18 +385,36 @@ export function buildArgoSyncOpts({ resourceMode, revision, prune, dryRun, force
|
|
|
258
385
|
applyOnly: boolean
|
|
259
386
|
replace: boolean
|
|
260
387
|
serverSideApply: boolean
|
|
388
|
+
applicationSyncOptions?: string[]
|
|
389
|
+
// Send the options even when the toggles match applicationSyncOptions,
|
|
390
|
+
// because those are no longer the Application's current ones.
|
|
391
|
+
sendUnchanged?: boolean
|
|
261
392
|
}): ArgoSyncOpts {
|
|
262
|
-
const
|
|
263
|
-
if (replace) syncOptions.push('Replace=true')
|
|
264
|
-
if (serverSideApply) syncOptions.push('ServerSideApply=true')
|
|
265
|
-
return {
|
|
393
|
+
const opts: ArgoSyncOpts = {
|
|
266
394
|
revision: resourceMode ? undefined : revision.trim() || undefined,
|
|
267
395
|
prune: resourceMode ? false : prune,
|
|
268
396
|
dryRun,
|
|
269
397
|
force,
|
|
270
398
|
applyOnly: resourceMode ? false : applyOnly,
|
|
271
|
-
syncOptions,
|
|
272
399
|
}
|
|
400
|
+
const unchanged = replace === applicationSyncOptions.includes(REPLACE_OPTION) &&
|
|
401
|
+
serverSideApply === applicationSyncOptions.includes(SERVER_SIDE_APPLY_OPTION)
|
|
402
|
+
if (unchanged && !sendUnchanged) return opts
|
|
403
|
+
|
|
404
|
+
const syncOptions = applicationSyncOptions.filter((option) => !isToggledOption(option))
|
|
405
|
+
if (replace) syncOptions.push(REPLACE_OPTION)
|
|
406
|
+
if (serverSideApply) syncOptions.push(SERVER_SIDE_APPLY_OPTION)
|
|
407
|
+
return { ...opts, syncOptions }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// argoSyncOptionsForRequest is the syncOptions to send for a sync whose
|
|
411
|
+
// options are `syncOptions` (undefined = the Application's own). Only a Radar
|
|
412
|
+
// advertising `argoSyncInheritance` reads an omitted list as the
|
|
413
|
+
// Application's own; an older one syncs with none, so it is sent the
|
|
414
|
+
// Application's options as the caller last read them.
|
|
415
|
+
export function argoSyncOptionsForRequest(syncOptions: string[] | undefined, applicationSyncOptions: string[] | undefined, serverInheritsOptions: boolean): string[] | undefined {
|
|
416
|
+
if (syncOptions !== undefined || serverInheritsOptions) return syncOptions
|
|
417
|
+
return applicationSyncOptions && applicationSyncOptions.length > 0 ? applicationSyncOptions : undefined
|
|
273
418
|
}
|
|
274
419
|
|
|
275
420
|
function Toggle({ label, checked, onChange, disabled, hint }: { label: string; checked: boolean; onChange: (v: boolean) => void; disabled?: boolean; hint?: string }) {
|
|
@@ -290,13 +435,13 @@ function Toggle({ label, checked, onChange, disabled, hint }: { label: string; c
|
|
|
290
435
|
)
|
|
291
436
|
}
|
|
292
437
|
|
|
293
|
-
function PrimaryButton({ onClick, disabled, icon: Icon, loading, label }: { onClick: () => void; disabled?: boolean; icon: ComponentType<{ className?: string }>; loading?: boolean; label: string }) {
|
|
438
|
+
function PrimaryButton({ onClick, disabled, icon: Icon, loading, label, warning }: { onClick: () => void; disabled?: boolean; icon: ComponentType<{ className?: string }>; loading?: boolean; label: string; warning?: boolean }) {
|
|
294
439
|
return (
|
|
295
440
|
<button
|
|
296
441
|
type="button"
|
|
297
442
|
onClick={onClick}
|
|
298
443
|
disabled={disabled}
|
|
299
|
-
className=
|
|
444
|
+
className={`${warning ? 'bg-amber-600 text-theme-text-primary hover:bg-amber-700' : 'btn-brand'} inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-50`}
|
|
300
445
|
>
|
|
301
446
|
<Icon className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} />
|
|
302
447
|
{label}
|
|
@@ -92,6 +92,18 @@ describe('hasSyncPolicy', () => {
|
|
|
92
92
|
})
|
|
93
93
|
expect(hasSyncPolicy(app)).toBe(true)
|
|
94
94
|
})
|
|
95
|
+
|
|
96
|
+
// Argo CD 3.x switches automation off in place with enabled: false; the
|
|
97
|
+
// Automation facet must count that app as Manual, not Auto.
|
|
98
|
+
test('automated.enabled=false is manual', () => {
|
|
99
|
+
const row = (automated: Record<string, unknown>) => normalizeArgoApplication({
|
|
100
|
+
metadata: { name: 'guestbook', namespace: 'argocd' },
|
|
101
|
+
spec: { project: 'default', source: {}, destination: {}, syncPolicy: { automated } },
|
|
102
|
+
status: {},
|
|
103
|
+
})
|
|
104
|
+
expect(row({ prune: true, selfHeal: true }).autoSync).toBe(true)
|
|
105
|
+
expect(row({ enabled: false, prune: true, selfHeal: true }).autoSync).toBe(false)
|
|
106
|
+
})
|
|
95
107
|
})
|
|
96
108
|
|
|
97
109
|
describe('normalizeArgoApplicationSet destination', () => {
|