@skyhook-io/k8s-ui 1.8.10 → 1.8.12
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 +7 -6
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +697 -0
- package/src/components/applications/ApplicationDetail.tsx +3269 -474
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +13 -6
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
- package/src/components/gitops/insights/insights-helpers.ts +74 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
- package/src/components/issues/IssuesView.tsx +91 -52
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
- package/src/components/issues/ResourceIssuesSection.tsx +46 -144
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +3 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- 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/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- 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/JobRenderer.tsx +3 -1
- 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/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- 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-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +142 -111
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +375 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +149 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +25 -17
- package/src/components/topology/index.ts +2 -0
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/drawer-components.tsx +48 -32
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2706 -475
- package/src/components/workload/index.ts +1 -1
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +2 -1
- package/src/types/core.ts +57 -4
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +201 -1
- package/src/utils/applications.ts +1182 -393
- package/src/utils/index.ts +3 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +1024 -1
- package/src/utils/resource-hierarchy.ts +925 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
|
@@ -93,23 +93,32 @@ function parseInterval(interval: string): number | null {
|
|
|
93
93
|
if (!interval) return null
|
|
94
94
|
|
|
95
95
|
let totalSeconds = 0
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
let index = 0
|
|
97
|
+
|
|
98
|
+
while (index < interval.length) {
|
|
99
|
+
const start = index
|
|
100
|
+
while (index < interval.length) {
|
|
101
|
+
const code = interval.charCodeAt(index)
|
|
102
|
+
if (code < 48 || code > 57) break
|
|
103
|
+
index += 1
|
|
104
|
+
}
|
|
105
|
+
if (start === index) {
|
|
106
|
+
index += 1
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
const value = parseInt(interval.slice(start, index), 10)
|
|
110
|
+
const unit = interval[index]
|
|
111
|
+
if (unit === 'h') {
|
|
112
|
+
totalSeconds += value * 3600
|
|
113
|
+
index += 1
|
|
114
|
+
} else if (unit === 'm') {
|
|
115
|
+
totalSeconds += value * 60
|
|
116
|
+
index += 1
|
|
117
|
+
} else if (unit === 's') {
|
|
118
|
+
totalSeconds += value
|
|
119
|
+
index += 1
|
|
120
|
+
} else if (index < interval.length) {
|
|
121
|
+
index += 1
|
|
113
122
|
}
|
|
114
123
|
}
|
|
115
124
|
|
|
@@ -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,7 +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
|
+
import { Input } from '../ui/Input'
|
|
6
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
7
|
+
import type { GitOpsInsightRef } from '../../types/gitops-insights'
|
|
5
8
|
|
|
6
9
|
// =============================================================================
|
|
7
10
|
// SyncOptionsDialog — Argo CD Sync drawer, shared between per-cluster
|
|
@@ -10,9 +13,9 @@ import { DialogPortal } from '../ui/DialogPortal'
|
|
|
10
13
|
// the onConfirm callback (POST to /api/argo/applications/{ns}/{name}/sync
|
|
11
14
|
// or the hub-proxied equivalent /c/{ctrl}/api/argo/...).
|
|
12
15
|
//
|
|
13
|
-
//
|
|
14
|
-
// force)
|
|
15
|
-
//
|
|
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.
|
|
16
19
|
// =============================================================================
|
|
17
20
|
|
|
18
21
|
// ArgoSyncOpts is the payload shape the dialog passes to its onConfirm
|
|
@@ -32,12 +35,29 @@ export interface ArgoSyncOpts {
|
|
|
32
35
|
export interface SyncOptionsDialogProps {
|
|
33
36
|
open: boolean
|
|
34
37
|
appLabel: string
|
|
38
|
+
resource?: GitOpsInsightRef
|
|
35
39
|
pending?: boolean
|
|
40
|
+
autoSyncEnabled?: boolean
|
|
41
|
+
validationPending?: boolean
|
|
42
|
+
operationInProgress?: boolean
|
|
43
|
+
validationResult?: ResourceValidationResult | null
|
|
44
|
+
validationError?: string | null
|
|
36
45
|
onCancel: () => void
|
|
37
46
|
onConfirm: (opts: ArgoSyncOpts) => void
|
|
47
|
+
onValidate?: (opts: ArgoSyncOpts) => void
|
|
48
|
+
onValidationReset?: () => void
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
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) {
|
|
41
61
|
const [revision, setRevision] = useState('')
|
|
42
62
|
const [prune, setPrune] = useState(true)
|
|
43
63
|
const [dryRun, setDryRun] = useState(false)
|
|
@@ -45,88 +65,212 @@ export function SyncOptionsDialog({ open, appLabel, pending, onCancel, onConfirm
|
|
|
45
65
|
const [applyOnly, setApplyOnly] = useState(false)
|
|
46
66
|
const [replace, setReplace] = useState(false)
|
|
47
67
|
const [serverSideApply, setServerSideApply] = useState(false)
|
|
68
|
+
const richResourceValidation = !!resource && !!onValidate
|
|
69
|
+
const busy = !!pending || !!validationPending
|
|
70
|
+
const optionsDisabled = busy || !!operationInProgress
|
|
48
71
|
|
|
49
72
|
// Reset on each open so a previous attempt's flags don't leak into the
|
|
50
73
|
// next sync — easy footgun in modal-heavy flows.
|
|
51
74
|
useEffect(() => {
|
|
52
75
|
if (open) {
|
|
53
76
|
setRevision('')
|
|
54
|
-
setPrune(
|
|
77
|
+
setPrune(!resource)
|
|
55
78
|
setDryRun(false)
|
|
56
79
|
setForce(false)
|
|
57
80
|
setApplyOnly(false)
|
|
58
81
|
setReplace(false)
|
|
59
82
|
setServerSideApply(false)
|
|
60
83
|
}
|
|
61
|
-
}, [open])
|
|
84
|
+
}, [open, resource])
|
|
62
85
|
|
|
63
86
|
function submit() {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
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?.()
|
|
75
97
|
}
|
|
76
98
|
|
|
77
99
|
return (
|
|
78
|
-
<DialogPortal open={open} onClose={
|
|
100
|
+
<DialogPortal open={open} onClose={busy ? () => {} : onCancel} className="w-[480px]" closable={!busy}>
|
|
79
101
|
<div className="border-b border-theme-border px-4 py-3">
|
|
80
|
-
<h2 className="text-sm font-semibold text-theme-text-primary">Sync application</h2>
|
|
81
|
-
<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>
|
|
82
104
|
</div>
|
|
83
105
|
<div className="space-y-4 px-4 py-4 text-sm">
|
|
84
|
-
|
|
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
|
+
}
|