@skyhook-io/k8s-ui 1.8.11 → 1.8.13
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.test.tsx +136 -8
- package/src/components/applications/ApplicationDetail.tsx +1215 -487
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsTableView.tsx +10 -3
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +189 -45
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +201 -21
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +16 -0
- package/src/components/gitops/insights/insights-helpers.ts +5 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -0
- package/src/components/issues/IssuesView.tsx +55 -29
- package/src/components/issues/ResourceIssuesSection.test.tsx +33 -1
- package/src/components/issues/ResourceIssuesSection.tsx +21 -1
- package/src/components/issues/severity.ts +1 -0
- package/src/components/resources/ResourcesView.tsx +109 -19
- package/src/components/resources/column-filter-serialization.test.ts +79 -1
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils.ts +94 -25
- package/src/components/shared/ResourceActionsBar.tsx +54 -22
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +7 -3
- package/src/components/timeline/TimelineToolbarState.test.tsx +24 -0
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyGraph.tsx +76 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +23 -15
- package/src/components/topology/index.ts +2 -0
- package/src/components/ui/Badge.tsx +1 -1
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/Tooltip.test.tsx +21 -0
- package/src/components/ui/Tooltip.tsx +9 -12
- package/src/components/ui/drawer-components.tsx +9 -6
- package/src/components/ui/index.ts +2 -0
- package/src/components/workload/WorkloadView.tsx +24 -3
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +1 -0
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/api-resources.test.ts +36 -1
- package/src/utils/api-resources.ts +16 -0
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/applications.test.ts +34 -0
- package/src/utils/applications.ts +25 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/resource-hierarchy.test.ts +35 -1
- package/src/utils/resource-hierarchy.ts +20 -0
|
@@ -77,6 +77,7 @@ export interface ArgoActionHandlers {
|
|
|
77
77
|
autoSyncEnabled: boolean
|
|
78
78
|
// isRunning: caller derives from status.operationState.phase === 'Running'.
|
|
79
79
|
isRunning: boolean
|
|
80
|
+
operationInProgress?: boolean
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export interface FluxActionHandlers {
|
|
@@ -158,6 +159,9 @@ export interface GitOpsDetailLayoutProps {
|
|
|
158
159
|
// hub-web and OSS can wire their own data sources without the layout
|
|
159
160
|
// knowing.
|
|
160
161
|
renderTabBody: (ctx: { tab: GitOpsDetailTab; fullscreen: boolean }) => ReactNode
|
|
162
|
+
// Optional host slot: inline Git commit metadata next to the status strip's
|
|
163
|
+
// latest revision (author, signature). Wired by the host to a data loader.
|
|
164
|
+
renderRevisionMeta?: (revision: string) => ReactNode
|
|
161
165
|
// Optional: top-right tab-bar accessory (Clear filters + Fullscreen
|
|
162
166
|
// buttons in OSS, can be anything else in hub-web).
|
|
163
167
|
renderTabBarAccessory?: (ctx: { tab: GitOpsDetailTab; fullscreen: boolean }) => ReactNode
|
|
@@ -228,6 +232,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
228
232
|
fullscreen,
|
|
229
233
|
onToggleFullscreen,
|
|
230
234
|
renderTabBody,
|
|
235
|
+
renderRevisionMeta,
|
|
231
236
|
renderTabBarAccessory,
|
|
232
237
|
renderTabBarCounts,
|
|
233
238
|
resourceLoading,
|
|
@@ -252,6 +257,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
252
257
|
}, [identity.name, manageDocumentTitle, documentTitleSuffix])
|
|
253
258
|
|
|
254
259
|
const effectiveSuspended = status?.suspended ?? false
|
|
260
|
+
const argoOperationInProgress = argo?.operationInProgress ?? argo?.isRunning ?? false
|
|
255
261
|
// graphFullscreen hides everything chrome-side; the body region expands.
|
|
256
262
|
// Mirrors the OSS GitOpsDetailView shell behavior so the visual chrome
|
|
257
263
|
// class set carries through.
|
|
@@ -344,8 +350,12 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
344
350
|
icon={ArrowDownUp}
|
|
345
351
|
loading={argo.syncing}
|
|
346
352
|
onClick={argo.onSyncRequested}
|
|
347
|
-
disabled={effectiveSuspended || terminating}
|
|
348
|
-
disabledReason={terminating
|
|
353
|
+
disabled={effectiveSuspended || terminating || argoOperationInProgress}
|
|
354
|
+
disabledReason={terminating
|
|
355
|
+
? terminatingActionTooltip
|
|
356
|
+
: argoOperationInProgress
|
|
357
|
+
? 'Wait for the current Argo operation to finish.'
|
|
358
|
+
: undefined}
|
|
349
359
|
primary
|
|
350
360
|
/>
|
|
351
361
|
<ActionButton
|
|
@@ -447,7 +457,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
447
457
|
)}
|
|
448
458
|
{!fullscreen && (
|
|
449
459
|
<>
|
|
450
|
-
<GitOpsStatusStrip insight={insight ?? undefined} loading={insightLoading} />
|
|
460
|
+
<GitOpsStatusStrip insight={insight ?? undefined} loading={insightLoading} renderRevisionMeta={renderRevisionMeta} />
|
|
451
461
|
<GitOpsIssuesBand
|
|
452
462
|
issues={insight?.issues}
|
|
453
463
|
terminating={terminating}
|
|
@@ -35,7 +35,7 @@ import { DistributionBar } from '../ui/DistributionBar'
|
|
|
35
35
|
import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
|
|
36
36
|
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
37
37
|
import { getGitOpsResourceStatus } from './detail-helpers'
|
|
38
|
-
import { isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
38
|
+
import { isArgoOperationInProgress, isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
39
39
|
import { toggleSet } from './GitOpsGraphFilterRail'
|
|
40
40
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
41
41
|
import { parseContextName } from '../../utils/context-name'
|
|
@@ -1358,13 +1358,20 @@ function buildRowActionItems(
|
|
|
1358
1358
|
const items: RowActionItem[] = []
|
|
1359
1359
|
|
|
1360
1360
|
if (row.tool === 'argo') {
|
|
1361
|
+
const operationInProgress = isArgoOperationInProgress(row.raw)
|
|
1361
1362
|
items.push({
|
|
1362
1363
|
key: 'sync',
|
|
1363
1364
|
label: 'Sync...',
|
|
1364
1365
|
icon: ArrowDownUp,
|
|
1365
1366
|
onClick: () => onAction(row, 'sync'),
|
|
1366
|
-
disabled: suspended || terminating,
|
|
1367
|
-
disabledReason: terminating
|
|
1367
|
+
disabled: suspended || terminating || operationInProgress,
|
|
1368
|
+
disabledReason: terminating
|
|
1369
|
+
? terminatingReason
|
|
1370
|
+
: suspended
|
|
1371
|
+
? suspendedReason
|
|
1372
|
+
: operationInProgress
|
|
1373
|
+
? 'Wait for the current Argo operation to finish.'
|
|
1374
|
+
: undefined,
|
|
1368
1375
|
pending: isPending('sync'),
|
|
1369
1376
|
})
|
|
1370
1377
|
// Refresh / Hard refresh are read-style verbs — they re-read Git and
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildArgoSyncOpts } from './SyncOptionsDialog'
|
|
3
|
+
|
|
4
|
+
describe('buildArgoSyncOpts', () => {
|
|
5
|
+
it('keeps full application sync controls', () => {
|
|
6
|
+
expect(
|
|
7
|
+
buildArgoSyncOpts({
|
|
8
|
+
resourceMode: false,
|
|
9
|
+
revision: ' main ',
|
|
10
|
+
prune: true,
|
|
11
|
+
dryRun: true,
|
|
12
|
+
force: false,
|
|
13
|
+
applyOnly: true,
|
|
14
|
+
replace: true,
|
|
15
|
+
serverSideApply: false,
|
|
16
|
+
}),
|
|
17
|
+
).toEqual({
|
|
18
|
+
revision: 'main',
|
|
19
|
+
prune: true,
|
|
20
|
+
dryRun: true,
|
|
21
|
+
force: false,
|
|
22
|
+
applyOnly: true,
|
|
23
|
+
syncOptions: ['Replace=true'],
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('removes revision, prune, and apply-only semantics from resource sync', () => {
|
|
28
|
+
expect(
|
|
29
|
+
buildArgoSyncOpts({
|
|
30
|
+
resourceMode: true,
|
|
31
|
+
revision: 'unsafe-revision',
|
|
32
|
+
prune: true,
|
|
33
|
+
dryRun: false,
|
|
34
|
+
force: true,
|
|
35
|
+
applyOnly: true,
|
|
36
|
+
replace: false,
|
|
37
|
+
serverSideApply: true,
|
|
38
|
+
}),
|
|
39
|
+
).toEqual({
|
|
40
|
+
revision: undefined,
|
|
41
|
+
prune: false,
|
|
42
|
+
dryRun: false,
|
|
43
|
+
force: true,
|
|
44
|
+
applyOnly: false,
|
|
45
|
+
syncOptions: ['ServerSideApply=true'],
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useState, useEffect, type ComponentType } from 'react'
|
|
2
|
-
import { Loader2, RefreshCw } from 'lucide-react'
|
|
2
|
+
import { AlertTriangle, CheckCircle2, CircleAlert, Loader2, RefreshCw, ShieldCheck, XCircle } from 'lucide-react'
|
|
3
3
|
|
|
4
4
|
import { DialogPortal } from '../ui/DialogPortal'
|
|
5
5
|
import { Input } from '../ui/Input'
|
|
6
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
|
+
import type { GitOpsInsightRef } from '../../types/gitops-insights'
|
|
6
8
|
|
|
7
9
|
// =============================================================================
|
|
8
10
|
// SyncOptionsDialog — Argo CD Sync drawer, shared between per-cluster
|
|
@@ -11,9 +13,9 @@ import { Input } from '../ui/Input'
|
|
|
11
13
|
// the onConfirm callback (POST to /api/argo/applications/{ns}/{name}/sync
|
|
12
14
|
// or the hub-proxied equivalent /c/{ctrl}/api/argo/...).
|
|
13
15
|
//
|
|
14
|
-
//
|
|
15
|
-
// force)
|
|
16
|
-
//
|
|
16
|
+
// Full sync defaults match Argo's most-common path (prune true, no dry-run,
|
|
17
|
+
// no force). Resource sync fixes prune off and omits revision/apply-only so
|
|
18
|
+
// selecting one object cannot opt into deletion behavior.
|
|
17
19
|
// =============================================================================
|
|
18
20
|
|
|
19
21
|
// ArgoSyncOpts is the payload shape the dialog passes to its onConfirm
|
|
@@ -33,12 +35,29 @@ export interface ArgoSyncOpts {
|
|
|
33
35
|
export interface SyncOptionsDialogProps {
|
|
34
36
|
open: boolean
|
|
35
37
|
appLabel: string
|
|
38
|
+
resource?: GitOpsInsightRef
|
|
36
39
|
pending?: boolean
|
|
40
|
+
autoSyncEnabled?: boolean
|
|
41
|
+
validationPending?: boolean
|
|
42
|
+
operationInProgress?: boolean
|
|
43
|
+
validationResult?: ResourceValidationResult | null
|
|
44
|
+
validationError?: string | null
|
|
37
45
|
onCancel: () => void
|
|
38
46
|
onConfirm: (opts: ArgoSyncOpts) => void
|
|
47
|
+
onValidate?: (opts: ArgoSyncOpts) => void
|
|
48
|
+
onValidationReset?: () => void
|
|
39
49
|
}
|
|
40
50
|
|
|
41
|
-
export
|
|
51
|
+
export interface ResourceValidationResult {
|
|
52
|
+
outcome: 'succeeded' | 'failed' | 'inconclusive'
|
|
53
|
+
message: string
|
|
54
|
+
resource?: {
|
|
55
|
+
status?: string
|
|
56
|
+
message?: string
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function SyncOptionsDialog({ open, appLabel, resource, pending, autoSyncEnabled, validationPending, operationInProgress, validationResult, validationError, onCancel, onConfirm, onValidate, onValidationReset }: SyncOptionsDialogProps) {
|
|
42
61
|
const [revision, setRevision] = useState('')
|
|
43
62
|
const [prune, setPrune] = useState(true)
|
|
44
63
|
const [dryRun, setDryRun] = useState(false)
|
|
@@ -46,87 +65,212 @@ export function SyncOptionsDialog({ open, appLabel, pending, onCancel, onConfirm
|
|
|
46
65
|
const [applyOnly, setApplyOnly] = useState(false)
|
|
47
66
|
const [replace, setReplace] = useState(false)
|
|
48
67
|
const [serverSideApply, setServerSideApply] = useState(false)
|
|
68
|
+
const richResourceValidation = !!resource && !!onValidate
|
|
69
|
+
const busy = !!pending || !!validationPending
|
|
70
|
+
const optionsDisabled = busy || !!operationInProgress
|
|
49
71
|
|
|
50
72
|
// Reset on each open so a previous attempt's flags don't leak into the
|
|
51
73
|
// next sync — easy footgun in modal-heavy flows.
|
|
52
74
|
useEffect(() => {
|
|
53
75
|
if (open) {
|
|
54
76
|
setRevision('')
|
|
55
|
-
setPrune(
|
|
77
|
+
setPrune(!resource)
|
|
56
78
|
setDryRun(false)
|
|
57
79
|
setForce(false)
|
|
58
80
|
setApplyOnly(false)
|
|
59
81
|
setReplace(false)
|
|
60
82
|
setServerSideApply(false)
|
|
61
83
|
}
|
|
62
|
-
}, [open])
|
|
84
|
+
}, [open, resource])
|
|
63
85
|
|
|
64
86
|
function submit() {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
})
|
|
87
|
+
onConfirm(buildArgoSyncOpts({ resourceMode: !!resource, revision, prune, dryRun: richResourceValidation ? false : dryRun, force, applyOnly, replace, serverSideApply }))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validate() {
|
|
91
|
+
onValidate?.(buildArgoSyncOpts({ resourceMode: true, revision, prune, dryRun: true, force, applyOnly, replace, serverSideApply }))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateValidationOption(setter: (value: boolean) => void, value: boolean) {
|
|
95
|
+
setter(value)
|
|
96
|
+
onValidationReset?.()
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
return (
|
|
79
|
-
<DialogPortal open={open} onClose={
|
|
100
|
+
<DialogPortal open={open} onClose={busy ? () => {} : onCancel} className="w-[480px]" closable={!busy}>
|
|
80
101
|
<div className="border-b border-theme-border px-4 py-3">
|
|
81
|
-
<h2 className="text-sm font-semibold text-theme-text-primary">Sync application</h2>
|
|
82
|
-
<p className="mt-0.5 text-xs text-theme-text-tertiary">{appLabel}</p>
|
|
102
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">{resource ? 'Sync resource' : 'Sync application'}</h2>
|
|
103
|
+
<p className="mt-0.5 break-all text-xs text-theme-text-tertiary">{appLabel}</p>
|
|
83
104
|
</div>
|
|
84
105
|
<div className="space-y-4 px-4 py-4 text-sm">
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
{resource && (
|
|
107
|
+
<div className="rounded-md border border-amber-500/40 bg-amber-500/5 px-3 py-2.5">
|
|
108
|
+
<div className="flex items-start gap-2">
|
|
109
|
+
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
|
110
|
+
<div className="min-w-0">
|
|
111
|
+
<div className="break-all text-xs font-medium text-theme-text-primary">
|
|
112
|
+
{resource.kind} / {resource.namespace ? `${resource.namespace}/` : ''}{resource.name}
|
|
113
|
+
</div>
|
|
114
|
+
<p className="mt-1 text-[11px] leading-relaxed text-theme-text-secondary">
|
|
115
|
+
Only this resource will be applied. Selective sync bypasses hooks and normal sync-wave ordering; use a full application sync when this resource depends on them.
|
|
116
|
+
</p>
|
|
117
|
+
{autoSyncEnabled && (
|
|
118
|
+
<p className="mt-2 border-t border-amber-500/20 pt-2 text-[11px] leading-relaxed text-theme-text-secondary">
|
|
119
|
+
Auto-sync is enabled. Argo can reconcile this Application independently while the dry-run runs; a dry-run does not pause automation.
|
|
120
|
+
</p>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
{!resource && (
|
|
127
|
+
<label className="block">
|
|
128
|
+
<span className="text-xs font-medium text-theme-text-secondary">Revision (optional)</span>
|
|
129
|
+
<Input
|
|
130
|
+
value={revision}
|
|
131
|
+
onChange={(e) => setRevision(e.target.value)}
|
|
132
|
+
placeholder="HEAD"
|
|
133
|
+
disabled={optionsDisabled}
|
|
134
|
+
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"
|
|
135
|
+
/>
|
|
136
|
+
<span className="mt-0.5 block text-[11px] text-theme-text-tertiary">
|
|
137
|
+
Branch, tag, or commit SHA. Leave empty to use the Application's targetRevision.
|
|
138
|
+
</span>
|
|
139
|
+
</label>
|
|
140
|
+
)}
|
|
98
141
|
|
|
99
142
|
{/* Common (Prune / Dry run) sit above a divider; Advanced toggles
|
|
100
143
|
stay accessible but visually subordinate so the common-case user
|
|
101
144
|
can scan past them without parsing every helper line. */}
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
{!richResourceValidation && (
|
|
146
|
+
<fieldset className="space-y-2">
|
|
147
|
+
<legend className="mb-1 text-xs font-medium text-theme-text-secondary">Sync options</legend>
|
|
148
|
+
{!resource && <Toggle label="Prune" checked={prune} onChange={setPrune} disabled={optionsDisabled} hint="Delete resources that are no longer in Git." />}
|
|
149
|
+
<Toggle label="Dry run" checked={dryRun} onChange={setDryRun} disabled={optionsDisabled} hint="Preview only — Argo computes the diff but applies nothing." />
|
|
150
|
+
</fieldset>
|
|
151
|
+
)}
|
|
107
152
|
<fieldset className="space-y-2 border-t border-theme-border pt-3">
|
|
108
153
|
<legend className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-theme-text-tertiary">Advanced</legend>
|
|
109
|
-
<Toggle label="Apply only" checked={applyOnly} onChange={setApplyOnly} disabled={
|
|
110
|
-
<Toggle label="Force" checked={force} onChange={setForce} disabled={
|
|
111
|
-
<Toggle label="Replace" checked={replace} onChange={setReplace} disabled={
|
|
112
|
-
<Toggle label="Server-side apply" checked={serverSideApply} onChange={setServerSideApply} disabled={
|
|
154
|
+
{!resource && <Toggle label="Apply only" checked={applyOnly} onChange={setApplyOnly} disabled={optionsDisabled} hint="Skip PreSync / PostSync / SyncFail hooks." />}
|
|
155
|
+
<Toggle label="Force" checked={force} onChange={(value) => updateValidationOption(setForce, value)} disabled={optionsDisabled} hint="Use kubectl --force; required for some immutable-field changes." />
|
|
156
|
+
<Toggle label="Replace" checked={replace} onChange={(value) => updateValidationOption(setReplace, value)} disabled={optionsDisabled} hint="kubectl replace instead of apply (drops fields not in source)." />
|
|
157
|
+
<Toggle label="Server-side apply" checked={serverSideApply} onChange={(value) => updateValidationOption(setServerSideApply, value)} disabled={optionsDisabled} hint="Use the K8s server-side apply mechanism for ownership tracking." />
|
|
113
158
|
</fieldset>
|
|
159
|
+
{richResourceValidation && (validationPending || validationResult || validationError) && (
|
|
160
|
+
<ValidationResult pending={!!validationPending} result={validationResult} error={validationError} />
|
|
161
|
+
)}
|
|
162
|
+
{richResourceValidation && !validationPending && operationInProgress && (
|
|
163
|
+
<p className="text-[11px] leading-relaxed text-theme-text-secondary">
|
|
164
|
+
Argo is finishing the current operation. Sync becomes available when it releases the operation slot.
|
|
165
|
+
</p>
|
|
166
|
+
)}
|
|
114
167
|
</div>
|
|
115
168
|
<div className="flex items-center justify-end gap-2 border-t border-theme-border bg-theme-base px-4 py-3">
|
|
116
169
|
<button
|
|
117
170
|
type="button"
|
|
118
171
|
onClick={onCancel}
|
|
119
|
-
disabled={
|
|
172
|
+
disabled={busy}
|
|
120
173
|
className="rounded-md border border-theme-border bg-theme-surface px-3 py-1.5 text-xs text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:cursor-not-allowed disabled:opacity-50"
|
|
121
174
|
>
|
|
122
175
|
Cancel
|
|
123
176
|
</button>
|
|
124
|
-
|
|
177
|
+
{richResourceValidation && (
|
|
178
|
+
<Tooltip content="Runs an Argo selective dry-run. Applies nothing; API admission can still reject the real sync." wrapperClassName="inline-flex">
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
onClick={validate}
|
|
182
|
+
disabled={optionsDisabled}
|
|
183
|
+
className="inline-flex items-center gap-1.5 rounded-md border border-theme-border bg-theme-surface px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary disabled:cursor-not-allowed disabled:opacity-50"
|
|
184
|
+
>
|
|
185
|
+
{validationPending ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <ShieldCheck className="h-3.5 w-3.5" />}
|
|
186
|
+
{validationPending ? 'Running dry-run…' : 'Dry-run'}
|
|
187
|
+
</button>
|
|
188
|
+
</Tooltip>
|
|
189
|
+
)}
|
|
190
|
+
<PrimaryButton onClick={submit} disabled={optionsDisabled} icon={pending ? Loader2 : RefreshCw} loading={pending} label={dryRun && !richResourceValidation ? 'Run dry-run' : resource ? 'Sync resource' : 'Sync now'} />
|
|
125
191
|
</div>
|
|
126
192
|
</DialogPortal>
|
|
127
193
|
)
|
|
128
194
|
}
|
|
129
195
|
|
|
196
|
+
function ValidationResult({ pending, result, error }: { pending: boolean; result?: ResourceValidationResult | null; error?: string | null }) {
|
|
197
|
+
if (pending) {
|
|
198
|
+
return (
|
|
199
|
+
<div className="card-inner flex items-start gap-2 px-3 py-2.5">
|
|
200
|
+
<Loader2 className="mt-0.5 h-4 w-4 shrink-0 animate-spin text-theme-text-secondary" />
|
|
201
|
+
<div>
|
|
202
|
+
<div className="text-xs font-medium text-theme-text-primary">Running selective dry-run</div>
|
|
203
|
+
<p className="mt-0.5 text-[11px] text-theme-text-secondary">Waiting for the selective dry-run result. No resource changes will be applied.</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
if (error) {
|
|
209
|
+
return (
|
|
210
|
+
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2.5">
|
|
211
|
+
<div className="flex items-start gap-2">
|
|
212
|
+
<XCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
|
|
213
|
+
<div>
|
|
214
|
+
<div className="text-xs font-medium text-theme-text-primary">Dry-run could not start or complete</div>
|
|
215
|
+
<p className="mt-0.5 max-h-20 overflow-auto break-words text-[11px] text-theme-text-secondary">{error}</p>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
if (!result) return null
|
|
222
|
+
|
|
223
|
+
const succeeded = result.outcome === 'succeeded'
|
|
224
|
+
const inconclusive = result.outcome === 'inconclusive'
|
|
225
|
+
const Icon = succeeded ? CheckCircle2 : inconclusive ? CircleAlert : XCircle
|
|
226
|
+
const title = succeeded ? 'Dry-run passed' : inconclusive ? 'Dry-run was inconclusive' : 'Dry-run failed'
|
|
227
|
+
const surface = succeeded
|
|
228
|
+
? 'border-emerald-500/30 bg-emerald-500/5'
|
|
229
|
+
: inconclusive
|
|
230
|
+
? 'border-amber-500/30 bg-amber-500/5'
|
|
231
|
+
: 'border-red-500/30 bg-red-500/5'
|
|
232
|
+
const iconTone = succeeded ? 'text-emerald-500' : inconclusive ? 'text-amber-500' : 'text-red-500'
|
|
233
|
+
return (
|
|
234
|
+
<div className={`rounded-md border px-3 py-2.5 ${surface}`}>
|
|
235
|
+
<div className="flex items-start gap-2">
|
|
236
|
+
<Icon className={`mt-0.5 h-4 w-4 shrink-0 ${iconTone}`} />
|
|
237
|
+
<div className="min-w-0">
|
|
238
|
+
<div className="text-xs font-medium text-theme-text-primary">{title}</div>
|
|
239
|
+
<p className="mt-0.5 max-h-20 overflow-auto break-words text-[11px] text-theme-text-secondary">{result.message}</p>
|
|
240
|
+
{(result.resource?.status || result.resource?.message) && (
|
|
241
|
+
<div className="mt-2 max-h-28 overflow-auto break-all font-mono text-[10px] text-theme-text-secondary">
|
|
242
|
+
{result.resource.status}{result.resource.status && result.resource.message ? ' — ' : ''}{result.resource.message}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function buildArgoSyncOpts({ resourceMode, revision, prune, dryRun, force, applyOnly, replace, serverSideApply }: {
|
|
252
|
+
resourceMode: boolean
|
|
253
|
+
revision: string
|
|
254
|
+
prune: boolean
|
|
255
|
+
dryRun: boolean
|
|
256
|
+
force: boolean
|
|
257
|
+
applyOnly: boolean
|
|
258
|
+
replace: boolean
|
|
259
|
+
serverSideApply: boolean
|
|
260
|
+
}): ArgoSyncOpts {
|
|
261
|
+
const syncOptions: string[] = []
|
|
262
|
+
if (replace) syncOptions.push('Replace=true')
|
|
263
|
+
if (serverSideApply) syncOptions.push('ServerSideApply=true')
|
|
264
|
+
return {
|
|
265
|
+
revision: resourceMode ? undefined : revision.trim() || undefined,
|
|
266
|
+
prune: resourceMode ? false : prune,
|
|
267
|
+
dryRun,
|
|
268
|
+
force,
|
|
269
|
+
applyOnly: resourceMode ? false : applyOnly,
|
|
270
|
+
syncOptions,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
130
274
|
function Toggle({ label, checked, onChange, disabled, hint }: { label: string; checked: boolean; onChange: (v: boolean) => void; disabled?: boolean; hint?: string }) {
|
|
131
275
|
return (
|
|
132
276
|
<label className="flex items-start gap-2">
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { createTwoFilesPatch } from 'diff'
|
|
4
|
+
import { GitCompare, Maximize2, ShieldOff, X } from 'lucide-react'
|
|
5
|
+
import type { GitOpsResourceDiff } from '../../../types'
|
|
6
|
+
import { DiffLine, hasDiffBodyChange } from '../../shared/UnifiedDiff'
|
|
7
|
+
|
|
8
|
+
interface ArgoResourceDiffProps {
|
|
9
|
+
diff?: GitOpsResourceDiff | null
|
|
10
|
+
loading: boolean
|
|
11
|
+
// Server error string ({"error"} body), surfaced inline — not a toast.
|
|
12
|
+
error?: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ArgoResourceDiff renders the full Git-rendered desired-vs-live diff for a
|
|
16
|
+
// single Argo CD managed resource. Pure presentation: the host (web/) wires the
|
|
17
|
+
// fetch and passes data / loading / error. The inline view is a unified line
|
|
18
|
+
// diff; the maximize control opens the same content full-screen with side-by-
|
|
19
|
+
// side panes on wide viewports.
|
|
20
|
+
export function ArgoResourceDiff({ diff, loading, error }: ArgoResourceDiffProps) {
|
|
21
|
+
const [maximized, setMaximized] = useState(false)
|
|
22
|
+
|
|
23
|
+
if (loading) {
|
|
24
|
+
// An inline row expansion, not a full pane — a quiet skeleton of the diff's
|
|
25
|
+
// own shape reads as "content arriving here" without the page-level radar
|
|
26
|
+
// loader shouting for attention.
|
|
27
|
+
return (
|
|
28
|
+
<div className="rounded-md border border-theme-border bg-theme-base/50 p-3" aria-busy="true" aria-label="Loading diff">
|
|
29
|
+
<div className="mb-2 flex items-center gap-1.5 text-[11px] text-theme-text-tertiary">
|
|
30
|
+
<GitCompare className="h-3.5 w-3.5 shrink-0" />
|
|
31
|
+
<span>Loading diff…</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="space-y-1.5">
|
|
34
|
+
{[92, 68, 81, 55, 74].map((w, i) => (
|
|
35
|
+
<div key={i} className="h-2.5 animate-pulse rounded bg-theme-hover" style={{ width: `${w}%` }} />
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
if (error) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="rounded-md border border-red-500/40 bg-red-500/5 px-3 py-2 text-xs text-red-700 dark:text-red-400">
|
|
44
|
+
{error}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
if (!diff) return null
|
|
49
|
+
|
|
50
|
+
const unchanged = !docsDiffer(diff.desired, diff.live)
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<div className="mb-2 flex items-center justify-between gap-2">
|
|
55
|
+
<div className="flex items-center gap-2">
|
|
56
|
+
<GitCompare className="h-3.5 w-3.5 shrink-0 text-theme-text-tertiary" />
|
|
57
|
+
<span className="text-[11px] text-theme-text-tertiary">
|
|
58
|
+
desired (Git-rendered) <span className="mx-0.5">→</span> live (normalized)
|
|
59
|
+
</span>
|
|
60
|
+
{diff.redacted && <RedactedChip />}
|
|
61
|
+
</div>
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={() => setMaximized(true)}
|
|
65
|
+
className="flex shrink-0 items-center gap-1 rounded border border-theme-border bg-theme-base px-1.5 py-0.5 text-[10px] text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary"
|
|
66
|
+
>
|
|
67
|
+
<Maximize2 className="h-3 w-3" />
|
|
68
|
+
Expand
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
{unchanged ? (
|
|
72
|
+
<p className="text-[11px] text-theme-text-tertiary">No differences between the Git-rendered desired state and live cluster state.</p>
|
|
73
|
+
) : (
|
|
74
|
+
<div className="max-h-80 overflow-auto rounded-md border border-theme-border bg-theme-base/50">
|
|
75
|
+
<UnifiedDiffBody desired={diff.desired} live={diff.live} />
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{maximized && <ArgoResourceDiffOverlay diff={diff} onClose={() => setMaximized(false)} />}
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function RedactedChip() {
|
|
85
|
+
return (
|
|
86
|
+
<span className="inline-flex items-center gap-1 rounded border border-theme-border bg-theme-elevated px-1.5 py-0.5 text-[10px] text-theme-text-tertiary">
|
|
87
|
+
<ShieldOff className="h-3 w-3" />
|
|
88
|
+
Secret values masked
|
|
89
|
+
</span>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Full-screen compare overlay. Side-by-side desired / live panes on wide
|
|
94
|
+
// viewports; unified diff when narrow. Portaled to body so it escapes the
|
|
95
|
+
// expanded row's overflow clipping.
|
|
96
|
+
function ArgoResourceDiffOverlay({ diff, onClose }: { diff: GitOpsResourceDiff; onClose: () => void }) {
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const onKey = (e: KeyboardEvent) => {
|
|
99
|
+
if (e.key === 'Escape') {
|
|
100
|
+
e.stopPropagation()
|
|
101
|
+
onClose()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
document.addEventListener('keydown', onKey, true)
|
|
105
|
+
return () => document.removeEventListener('keydown', onKey, true)
|
|
106
|
+
}, [onClose])
|
|
107
|
+
|
|
108
|
+
const unchanged = !docsDiffer(diff.desired, diff.live)
|
|
109
|
+
|
|
110
|
+
return createPortal(
|
|
111
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
112
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
113
|
+
<div className="dialog relative flex max-h-[90vh] w-full max-w-6xl flex-col">
|
|
114
|
+
<div className="flex items-center justify-between gap-2 border-b border-theme-border px-4 py-3">
|
|
115
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
116
|
+
<GitCompare className="h-4 w-4 shrink-0 text-theme-text-secondary" />
|
|
117
|
+
<span className="truncate text-sm font-medium text-theme-text-primary">
|
|
118
|
+
desired (Git-rendered) <span className="mx-0.5 text-theme-text-tertiary">→</span> live (normalized)
|
|
119
|
+
</span>
|
|
120
|
+
{diff.redacted && <RedactedChip />}
|
|
121
|
+
</div>
|
|
122
|
+
<button
|
|
123
|
+
onClick={onClose}
|
|
124
|
+
className="flex shrink-0 items-center gap-1 rounded px-2 py-1 text-xs text-theme-text-secondary hover:bg-theme-elevated hover:text-theme-text-primary"
|
|
125
|
+
>
|
|
126
|
+
<X className="h-3.5 w-3.5" />
|
|
127
|
+
Close
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
{unchanged ? (
|
|
131
|
+
<div className="p-6 text-sm text-theme-text-secondary">
|
|
132
|
+
No differences between the Git-rendered desired state and live cluster state.
|
|
133
|
+
</div>
|
|
134
|
+
) : (
|
|
135
|
+
// Unified (not side-by-side) even full-screen: two unhighlighted
|
|
136
|
+
// panes make the reader hunt for the drifted line by eye across a
|
|
137
|
+
// whole manifest. The unified view marks every changed line — the
|
|
138
|
+
// same default Argo's own UI makes. An aligned split-diff with
|
|
139
|
+
// per-line highlighting can supersede this later.
|
|
140
|
+
<div className="min-h-0 flex-1 overflow-auto bg-theme-base/50">
|
|
141
|
+
<UnifiedDiffBody desired={diff.desired} live={diff.live} />
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>,
|
|
146
|
+
document.body,
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
// Renders a unified diff of two YAML documents using the `diff` package, via the
|
|
152
|
+
// shared DiffLine so this reads identically to the Helm manifest diff.
|
|
153
|
+
function UnifiedDiffBody({ desired, live }: { desired: string; live: string }) {
|
|
154
|
+
const patch = createTwoFilesPatch('desired', 'live', desired, live, '', '', { context: 3 })
|
|
155
|
+
const lines = patch.split('\n').filter((line) => !line.startsWith('===') && !line.startsWith('Index:'))
|
|
156
|
+
return (
|
|
157
|
+
<div className="p-3 font-mono text-[11px]">
|
|
158
|
+
{lines.map((line, index) => (
|
|
159
|
+
<DiffLine key={index} line={line} />
|
|
160
|
+
))}
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// True when the desired and live documents differ in a body line (ignoring the
|
|
166
|
+
// patch's own file headers). Cheaper than rendering for the "no differences"
|
|
167
|
+
// short-circuit.
|
|
168
|
+
function docsDiffer(desired: string, live: string): boolean {
|
|
169
|
+
if (desired === live) return false
|
|
170
|
+
const patch = createTwoFilesPatch('desired', 'live', desired, live, '', '', { context: 0 })
|
|
171
|
+
return hasDiffBodyChange(patch)
|
|
172
|
+
}
|