@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/k8s-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/skyhook-io/radar",
|
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
"./src/components/resources/*.tsx"
|
|
21
21
|
],
|
|
22
22
|
"./components/resources/KueueAdmissionSection": "./src/components/resources/KueueAdmissionSection.tsx",
|
|
23
|
+
"./components/kueue/AdmissionPath": "./src/components/kueue/AdmissionPath.tsx",
|
|
24
|
+
"./components/kueue/QueueEvidence": "./src/components/kueue/QueueEvidence.tsx",
|
|
25
|
+
"./components/kueue/*": [
|
|
26
|
+
"./src/components/kueue/*.ts",
|
|
27
|
+
"./src/components/kueue/*.tsx"
|
|
28
|
+
],
|
|
29
|
+
"./components/resources/KueuePendingPosition": "./src/components/resources/KueuePendingPosition.tsx",
|
|
23
30
|
"./components/resources/renderers/*": [
|
|
24
31
|
"./src/components/resources/renderers/*.tsx",
|
|
25
32
|
"./src/components/resources/renderers/*.ts"
|
|
@@ -89,7 +96,7 @@
|
|
|
89
96
|
"yaml": ">=2.0.0"
|
|
90
97
|
},
|
|
91
98
|
"devDependencies": {
|
|
92
|
-
"@types/node": "^26.6.
|
|
99
|
+
"@types/node": "^26.6.6",
|
|
93
100
|
"@types/react": "^19.3.0",
|
|
94
101
|
"@types/react-dom": "^19.3.0",
|
|
95
102
|
"@xterm/addon-fit": "^0.11.0",
|
|
@@ -1833,6 +1833,8 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
|
|
|
1833
1833
|
);
|
|
1834
1834
|
const failed = activity.reduce((n, item) => n + item.failedRuns, 0);
|
|
1835
1835
|
const active = activity.reduce((n, item) => n + item.activeRuns, 0);
|
|
1836
|
+
const rootActivity = activity.filter(item => item.workload.kind === 'JobSet' && item.workload.group === 'jobset.x-k8s.io')
|
|
1837
|
+
const rootStates = [...new Set(rootActivity.map(item => item.latestRunPhase || 'Unknown'))].join(', ')
|
|
1836
1838
|
|
|
1837
1839
|
return {
|
|
1838
1840
|
latestValue: latest?.latestRunPhase || latest?.label || "None",
|
|
@@ -1841,9 +1843,9 @@ function batchOverviewStats(activity: AppBatchActivity[]) {
|
|
|
1841
1843
|
: "No retained runs",
|
|
1842
1844
|
retainedValue:
|
|
1843
1845
|
retained > 0 ? `${succeeded} succeeded / ${failed} failed` : "0 retained",
|
|
1844
|
-
activeValue: active > 0 ? `${active} running` : "None",
|
|
1846
|
+
activeValue: active > 0 ? `${active} running` : rootStates || "None",
|
|
1845
1847
|
activeDetail:
|
|
1846
|
-
active > 0 ? activeWorkloadNames(activity) : "No active batch work",
|
|
1848
|
+
active > 0 ? activeWorkloadNames(activity) : rootStates ? "JobSet root lifecycle; running Pods are not inferred." : "No active batch work",
|
|
1847
1849
|
scheduleValue:
|
|
1848
1850
|
schedules.length === 0
|
|
1849
1851
|
? "None"
|
|
@@ -2068,6 +2070,9 @@ function workloadRuntimeStatus(workload: AppWorkload): {
|
|
|
2068
2070
|
health: rolloutDisplayHealth(workload.rollout, workload.health),
|
|
2069
2071
|
}
|
|
2070
2072
|
}
|
|
2073
|
+
if (workload.kind === 'JobSet' && workload.group === 'jobset.x-k8s.io') {
|
|
2074
|
+
return { label: workload.batch?.latestRunPhase || 'Unknown', health: healthOf(workload.health) }
|
|
2075
|
+
}
|
|
2071
2076
|
const batch = workload.batch;
|
|
2072
2077
|
if (!batch) {
|
|
2073
2078
|
const health = healthOf(workload.health);
|
|
@@ -2085,6 +2090,7 @@ function workloadRuntimeStatus(workload: AppWorkload): {
|
|
|
2085
2090
|
|
|
2086
2091
|
function workloadRuntimeDetail(workload: AppWorkload): string {
|
|
2087
2092
|
if (workload.rollout && (workload.rollout.active || workload.rollout.phase === 'stalled')) return workload.rollout.detail || `${workload.ready}/${workload.desired} ready`
|
|
2093
|
+
if (workload.kind === 'JobSet' && workload.group === 'jobset.x-k8s.io') return workload.batch?.message || `Root lifecycle: ${workload.batch?.latestRunPhase || 'Unknown'}`
|
|
2088
2094
|
const batch = workload.batch;
|
|
2089
2095
|
if (!batch) return `${workload.ready}/${workload.desired} ready`;
|
|
2090
2096
|
if ((batch.activeRuns ?? 0) > 0)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ClusterSwitcher, type ClusterSwitcherHandle, type ClusterSwitcherItem } from './ClusterSwitcher'
|
|
6
|
+
|
|
7
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
8
|
+
let root: Root
|
|
9
|
+
let host: HTMLDivElement
|
|
10
|
+
|
|
11
|
+
const items: ClusterSwitcherItem[] = [
|
|
12
|
+
{ id: 'prod', name: 'prod' },
|
|
13
|
+
{ id: 'staging', name: 'staging' },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
host = document.createElement('div')
|
|
18
|
+
document.body.appendChild(host)
|
|
19
|
+
root = createRoot(host)
|
|
20
|
+
})
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
act(() => root.unmount())
|
|
23
|
+
host.remove()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
function open(props: { currentSelectable?: boolean }, onSelect: (item: ClusterSwitcherItem) => void) {
|
|
27
|
+
const ref = { current: null as ClusterSwitcherHandle | null }
|
|
28
|
+
act(() => root.render(
|
|
29
|
+
<ClusterSwitcher ref={ref} currentId="prod" currentName="prod" items={items} onSelect={onSelect} searchable={false} {...props} />,
|
|
30
|
+
))
|
|
31
|
+
act(() => ref.current?.open())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function row(name: string) {
|
|
35
|
+
return [...host.querySelectorAll<HTMLButtonElement>('button[data-highlighted]')].find(b => b.textContent?.includes(name))!
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('ClusterSwitcher current row', () => {
|
|
39
|
+
it('is inert by default', () => {
|
|
40
|
+
const onSelect = vi.fn()
|
|
41
|
+
open({}, onSelect)
|
|
42
|
+
expect(row('prod').disabled).toBe(true)
|
|
43
|
+
act(() => row('prod').click())
|
|
44
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('can be picked when the host asks for it', () => {
|
|
48
|
+
const onSelect = vi.fn()
|
|
49
|
+
open({ currentSelectable: true }, onSelect)
|
|
50
|
+
expect(row('prod').disabled).toBe(false)
|
|
51
|
+
act(() => row('prod').click())
|
|
52
|
+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ id: 'prod' }))
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -74,6 +74,9 @@ export interface ClusterSwitcherProps {
|
|
|
74
74
|
variant?: 'chip' | 'segment'
|
|
75
75
|
/** Muted label shown before the value in the 'segment' variant (e.g. "Cluster"). */
|
|
76
76
|
label?: string
|
|
77
|
+
/** Lets the current row be picked, for hosts that show a screen about
|
|
78
|
+
* another cluster on top of the current one (picking it dismisses that). */
|
|
79
|
+
currentSelectable?: boolean
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
// Trigger width cap. With middle-truncation kicking in, this is a
|
|
@@ -102,6 +105,7 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
102
105
|
align = 'left',
|
|
103
106
|
variant = 'chip',
|
|
104
107
|
label,
|
|
108
|
+
currentSelectable = false,
|
|
105
109
|
}, ref) => {
|
|
106
110
|
const [isOpen, setIsOpen] = useState(false)
|
|
107
111
|
const [search, setSearch] = useState('')
|
|
@@ -183,7 +187,7 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
183
187
|
}, [isOpen])
|
|
184
188
|
|
|
185
189
|
const select = (item: ClusterSwitcherItem) => {
|
|
186
|
-
if (item.disabled || item.id === currentId) return
|
|
190
|
+
if (item.disabled || (item.id === currentId && !currentSelectable)) return
|
|
187
191
|
setIsOpen(false)
|
|
188
192
|
if (item.href) {
|
|
189
193
|
window.location.href = item.href
|
|
@@ -341,12 +345,12 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
|
|
|
341
345
|
data-highlighted={idx === highlightedIndex}
|
|
342
346
|
onClick={() => select(item)}
|
|
343
347
|
onMouseEnter={() => setHighlightedIndex(idx)}
|
|
344
|
-
disabled={isCurrent || item.disabled}
|
|
348
|
+
disabled={(isCurrent && !currentSelectable) || item.disabled}
|
|
345
349
|
title={item.title}
|
|
346
350
|
className={`
|
|
347
351
|
w-full flex items-center gap-2 px-3 py-2 text-left transition-colors
|
|
348
352
|
${isCurrent
|
|
349
|
-
?
|
|
353
|
+
? `selection${currentSelectable ? ' cursor-pointer' : ''}`
|
|
350
354
|
: idx === highlightedIndex
|
|
351
355
|
? 'bg-theme-hover cursor-pointer'
|
|
352
356
|
: 'hover:bg-theme-hover cursor-pointer'}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { ResourceCompareView, type ResourceCompareViewProps } from './ResourceCompareView'
|
|
5
|
+
|
|
6
|
+
vi.mock('../ui/YamlEditor', () => ({ YamlDiffEditor: () => null }))
|
|
7
|
+
|
|
8
|
+
const checkout = { kind: 'Deployment', namespace: 'shop', name: 'checkout' }
|
|
9
|
+
|
|
10
|
+
function header(props: Partial<ResourceCompareViewProps>) {
|
|
11
|
+
const html = renderToStaticMarkup(
|
|
12
|
+
<ResourceCompareView a={checkout} b={checkout} aData={{}} bData={{}} onSwap={() => {}} onClose={() => {}} {...props} />,
|
|
13
|
+
)
|
|
14
|
+
// Tags split the name from its namespace, so compare the text a reader sees.
|
|
15
|
+
const host = document.createElement('div')
|
|
16
|
+
host.innerHTML = html
|
|
17
|
+
return host.textContent ?? ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('ResourceCompareView sides', () => {
|
|
21
|
+
it('names the resource once and tells the sides apart by where they run', () => {
|
|
22
|
+
const text = header({ aSubtitle: 'payments-eu', bSubtitle: 'payments-us' })
|
|
23
|
+
expect(text.split('shop/checkout')).toHaveLength(2)
|
|
24
|
+
expect(text).toContain('payments-eu')
|
|
25
|
+
expect(text).toContain('payments-us')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps a cut-off location readable on hover', () => {
|
|
29
|
+
const html = renderToStaticMarkup(
|
|
30
|
+
<ResourceCompareView a={checkout} b={checkout} aData={{}} bData={{}} onSwap={() => {}} onClose={() => {}} aSubtitle="payments-eu" bSubtitle="payments-us" />,
|
|
31
|
+
)
|
|
32
|
+
expect(html).toContain('title="payments-eu"')
|
|
33
|
+
expect(html).toContain('title="payments-us"')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('names both sides when they are different resources', () => {
|
|
37
|
+
const text = header({ b: { ...checkout, name: 'billing' }, aSubtitle: 'payments-eu', bSubtitle: 'payments-eu' })
|
|
38
|
+
expect(text).toContain('shop/checkout')
|
|
39
|
+
expect(text).toContain('shop/billing')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('keeps names when the same kind and name come from different API groups', () => {
|
|
43
|
+
const text = header({ a: { ...checkout, group: 'apps' }, b: { ...checkout, group: 'example.com' }, aSubtitle: 'eu', bSubtitle: 'us' })
|
|
44
|
+
expect(text.split('shop/checkout')).toHaveLength(3)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('keeps names when both sides come from the same place', () => {
|
|
48
|
+
expect(header({ aSubtitle: 'payments-eu', bSubtitle: 'payments-eu' }).split('shop/checkout')).toHaveLength(3)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('keeps names with no location to show', () => {
|
|
52
|
+
expect(header({}).split('shop/checkout')).toHaveLength(3)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -59,12 +59,15 @@ function ResourcePill({
|
|
|
59
59
|
error,
|
|
60
60
|
onChange,
|
|
61
61
|
subtitle,
|
|
62
|
+
subtitleOnly,
|
|
62
63
|
}: {
|
|
63
64
|
resource: CompareResourceRef
|
|
64
65
|
side: CompareSide
|
|
65
66
|
error?: string
|
|
66
67
|
onChange?: () => void
|
|
67
68
|
subtitle?: string
|
|
69
|
+
/** Both sides are the same resource, so where it runs is all that tells them apart. */
|
|
70
|
+
subtitleOnly?: boolean
|
|
68
71
|
}) {
|
|
69
72
|
const tones = SIDE_TONES[side]
|
|
70
73
|
const errTone = 'border-red-400/50 bg-red-500/10'
|
|
@@ -82,13 +85,19 @@ function ResourcePill({
|
|
|
82
85
|
{label}
|
|
83
86
|
</span>
|
|
84
87
|
{error && <AlertTriangle className="w-3 h-3 text-red-400 shrink-0" aria-hidden />}
|
|
85
|
-
<span className={clsx('truncate min-w-0', error ? 'text-red-300' : 'text-theme-text-primary')}>
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
<span className={clsx('truncate min-w-0', error ? 'text-red-300' : 'text-theme-text-primary')} title={subtitleOnly ? subtitle : undefined}>
|
|
89
|
+
{subtitleOnly ? (
|
|
90
|
+
subtitle
|
|
91
|
+
) : (
|
|
92
|
+
<>
|
|
93
|
+
{resource.namespace && <span className="opacity-60">{resource.namespace}/</span>}
|
|
94
|
+
{resource.name}
|
|
95
|
+
{subtitle && (
|
|
96
|
+
<span className="opacity-60 ml-1" aria-label={`Source: ${subtitle}`}>
|
|
97
|
+
· {subtitle}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
100
|
+
</>
|
|
92
101
|
)}
|
|
93
102
|
</span>
|
|
94
103
|
{onChange && (
|
|
@@ -144,6 +153,10 @@ export function ResourceCompareView({
|
|
|
144
153
|
// blank the editor. When the A/B identity changes (navigating to a
|
|
145
154
|
// new pair on the same route), the latch resets so the loader shows
|
|
146
155
|
// again until the new fetches settle.
|
|
156
|
+
// The same resource from two places: say what it is once, up top, so the pills
|
|
157
|
+
// can show the one thing that differs. Long names no longer push that out of view.
|
|
158
|
+
const sameResource = a.kind === b.kind && a.group === b.group && a.namespace === b.namespace && a.name === b.name &&
|
|
159
|
+
Boolean(aSubtitle && bSubtitle) && aSubtitle !== bSubtitle
|
|
147
160
|
const settleKey = `${a.kind}|${a.namespace}|${a.name}|${a.group ?? ''}|${aSubtitle ?? ''}` +
|
|
148
161
|
`|${b.kind}|${b.namespace}|${b.name}|${b.group ?? ''}|${bSubtitle ?? ''}`
|
|
149
162
|
const settleKeyRef = useRef(settleKey)
|
|
@@ -160,15 +173,25 @@ export function ResourceCompareView({
|
|
|
160
173
|
<div className="flex-1 min-w-0 flex flex-col h-full bg-theme-base">
|
|
161
174
|
<div className="h-0.5 w-full bg-gradient-to-r from-blue-400/70 via-skyhook-400/40 to-emerald-400/70" />
|
|
162
175
|
|
|
163
|
-
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-theme-border bg-theme-surface">
|
|
176
|
+
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 px-4 py-2.5 border-b border-theme-border bg-theme-surface">
|
|
164
177
|
<GitCompare className="w-5 h-5 text-skyhook-400 shrink-0" />
|
|
165
|
-
<h2 className=
|
|
178
|
+
<h2 className={clsx('text-sm font-semibold text-theme-text-primary whitespace-nowrap', sameResource ? 'min-w-0 max-w-[30%] truncate' : 'shrink-0')}>
|
|
166
179
|
Compare <span className="text-theme-text-tertiary mx-0.5">·</span>{' '}
|
|
167
180
|
<span className="text-theme-text-secondary font-medium">{kindLabel}</span>
|
|
181
|
+
{sameResource && (
|
|
182
|
+
<>
|
|
183
|
+
<span className="text-theme-text-tertiary mx-0.5">·</span>{' '}
|
|
184
|
+
<span className="text-theme-text-secondary font-medium" title={`${a.namespace}/${a.name}`}>
|
|
185
|
+
{a.namespace && <span className="opacity-60">{a.namespace}/</span>}
|
|
186
|
+
{a.name}
|
|
187
|
+
</span>
|
|
188
|
+
</>
|
|
189
|
+
)}
|
|
168
190
|
</h2>
|
|
169
191
|
|
|
170
|
-
|
|
171
|
-
|
|
192
|
+
{/* The row wraps before the sides get too narrow to tell apart. */}
|
|
193
|
+
<div className="flex items-center gap-2 min-w-72 flex-1">
|
|
194
|
+
<ResourcePill resource={a} side="a" error={aError} onChange={onChangeA} subtitle={aSubtitle} subtitleOnly={sameResource} />
|
|
172
195
|
<Tooltip content="Swap A and B">
|
|
173
196
|
<button
|
|
174
197
|
onClick={onSwap}
|
|
@@ -177,7 +200,7 @@ export function ResourceCompareView({
|
|
|
177
200
|
<ArrowLeftRight className="w-3.5 h-3.5" />
|
|
178
201
|
</button>
|
|
179
202
|
</Tooltip>
|
|
180
|
-
<ResourcePill resource={b} side="b" error={bError} onChange={onChangeB} subtitle={bSubtitle} />
|
|
203
|
+
<ResourcePill resource={b} side="b" error={bError} onChange={onChangeB} subtitle={bSubtitle} subtitleOnly={sameResource} />
|
|
181
204
|
</div>
|
|
182
205
|
|
|
183
206
|
<div className="flex items-center gap-1 shrink-0">
|
|
@@ -22,6 +22,21 @@ const shell: Omit<GitOpsDetailLayoutProps, 'identity' | 'isArgoApp'> = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
describe('GitOpsDetailLayout', () => {
|
|
25
|
+
it.each([false, true])('shows tree limits without insights, including fullscreen=%s', (fullscreen) => {
|
|
26
|
+
const warning = 'Some generated resources could not be checked; the resource tree may be incomplete.'
|
|
27
|
+
const html = renderToString(
|
|
28
|
+
<GitOpsDetailLayout
|
|
29
|
+
{...shell}
|
|
30
|
+
identity={{ kind: 'applications', group: 'argoproj.io', namespace: 'shop', name: 'wordpress', kindLabel: 'Application' }}
|
|
31
|
+
isArgoApp
|
|
32
|
+
fullscreen={fullscreen}
|
|
33
|
+
warnings={[warning, warning]}
|
|
34
|
+
/>,
|
|
35
|
+
)
|
|
36
|
+
expect(html).toContain('Limited resource view')
|
|
37
|
+
expect(html.split(warning)).toHaveLength(2)
|
|
38
|
+
})
|
|
39
|
+
|
|
25
40
|
it('shows the tool chip, facts and Argo operations for an Argo CD Application', () => {
|
|
26
41
|
const html = renderToString(
|
|
27
42
|
<GitOpsDetailLayout
|
|
@@ -39,6 +54,7 @@ describe('GitOpsDetailLayout', () => {
|
|
|
39
54
|
expect(html).toContain('ArgoCD')
|
|
40
55
|
expect(html).toContain('Project')
|
|
41
56
|
expect(html).toContain('Sync…')
|
|
57
|
+
expect(html).not.toContain('Limited resource view')
|
|
42
58
|
})
|
|
43
59
|
|
|
44
60
|
// A root that is not a GitOps resource gets one state: its name and why
|
|
@@ -2,12 +2,28 @@ import { useEffect, type ComponentType, type ReactNode } from 'react'
|
|
|
2
2
|
import { ArrowDownUp, Clock3, GitBranch, GitCommit, Loader2, Pause, Play, RefreshCw, Settings, Trash2, XCircle, Zap } from 'lucide-react'
|
|
3
3
|
import { Collapse, CollapseChevron, useDisclosure } from '../ui/Collapse'
|
|
4
4
|
import { PaneLoader } from '../ui/PaneLoader'
|
|
5
|
+
import { AlertBanner } from '../ui/drawer-components'
|
|
5
6
|
|
|
6
7
|
import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
|
|
7
8
|
import { GitOpsIssuesBand, GitOpsStatusStrip } from './insights'
|
|
8
9
|
import { GitOpsHealthSourceNotice } from './GitOpsHealthSourceNotice'
|
|
9
10
|
import { Tooltip } from '../ui/Tooltip'
|
|
11
|
+
import { RadarUpgradeNote, radarUpgradeNoteText } from '../ui/RadarUpgradeNote'
|
|
10
12
|
import type { GitOpsHealthStatus, GitOpsInsight, GitOpsIssue, GitOpsRemediation, SyncStatus } from '../../types'
|
|
13
|
+
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
14
|
+
|
|
15
|
+
function argoAutoSyncControllerNote(controller: ArgoActionHandlers['autoSyncController']): string {
|
|
16
|
+
if (!controller) return ''
|
|
17
|
+
return controller.kind === 'ApplicationSet'
|
|
18
|
+
? ` ApplicationSet ${controller.name} can turn it back on; to keep it off, change the ApplicationSet's template.`
|
|
19
|
+
: ` Application ${controller.name} manages this one from Git and can turn it back on when it syncs; to keep it off, change it in Git.`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function argoResumeDescription(suspended: boolean, settings: ArgoActionHandlers['resumeSettings']): string {
|
|
23
|
+
if (suspended) return 'Turn auto-sync back on with its current or saved settings.'
|
|
24
|
+
if (!settings) return 'Turn on automated sync, so Argo applies new commits from Git.'
|
|
25
|
+
return `Argo applies new commits${settings.selfHeal ? ' and reverts live edits (self-heal)' : ''}. Resources removed from Git are ${settings.prune ? 'deleted (prune on)' : 'kept (prune off)'}.`
|
|
26
|
+
}
|
|
11
27
|
|
|
12
28
|
// ---------------------------------------------------------------------------
|
|
13
29
|
// Props
|
|
@@ -76,11 +92,22 @@ export interface ArgoActionHandlers {
|
|
|
76
92
|
terminating: boolean
|
|
77
93
|
suspending: boolean
|
|
78
94
|
resuming: boolean
|
|
79
|
-
// autoSyncEnabled: caller derives
|
|
95
|
+
// autoSyncEnabled: caller derives via isArgoAutoSyncEnabled.
|
|
80
96
|
autoSyncEnabled: boolean
|
|
81
97
|
// isRunning: caller derives from status.operationState.phase === 'Running'.
|
|
82
98
|
isRunning: boolean
|
|
83
99
|
operationInProgress?: boolean
|
|
100
|
+
// Set when the connected Radar is too old to turn this app's auto-sync back
|
|
101
|
+
// on (isArgoAutoSyncSwitchedOff): Enable auto-sync renders disabled, with
|
|
102
|
+
// the upgrade note under the header.
|
|
103
|
+
resumeUpgrade?: RadarUpgradeRequirement | null
|
|
104
|
+
// What Enable auto-sync turns on when Radar didn't suspend the app
|
|
105
|
+
// (argoResumeSettings). Without it the tooltip makes no prune claim.
|
|
106
|
+
resumeSettings?: { prune: boolean; selfHeal: boolean } | null
|
|
107
|
+
resumeBlockedReason?: string
|
|
108
|
+
// The Argo CD object that can turn auto-sync back on after Disable
|
|
109
|
+
// (argoAutoSyncController); its tooltip names it.
|
|
110
|
+
autoSyncController?: { kind: 'ApplicationSet' | 'Application'; name: string } | null
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
export interface FluxActionHandlers {
|
|
@@ -125,6 +152,8 @@ export interface GitOpsDetailLayoutProps {
|
|
|
125
152
|
// Insights — drives StatusStrip + IssuesBand
|
|
126
153
|
insight: GitOpsInsight | null
|
|
127
154
|
insightLoading: boolean
|
|
155
|
+
// Tree warnings are independent of the insights request's success.
|
|
156
|
+
warnings?: string[]
|
|
128
157
|
// Issue interactions — caller wires onSelectIssue to jump to Changes tab
|
|
129
158
|
// and highlight the affected resource.
|
|
130
159
|
onSelectIssue?: (issue: GitOpsIssue) => void
|
|
@@ -229,6 +258,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
229
258
|
detail,
|
|
230
259
|
insight,
|
|
231
260
|
insightLoading,
|
|
261
|
+
warnings,
|
|
232
262
|
onSelectIssue,
|
|
233
263
|
onRemediate,
|
|
234
264
|
remediationPending,
|
|
@@ -280,6 +310,9 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
280
310
|
|
|
281
311
|
const effectiveSuspended = status?.suspended ?? false
|
|
282
312
|
const argoOperationInProgress = argo?.operationInProgress ?? argo?.isRunning ?? false
|
|
313
|
+
const argoSyncDisabled = effectiveSuspended || terminating || argoOperationInProgress
|
|
314
|
+
const resumeUpgrade = isArgoApp && argo && !argo.autoSyncEnabled ? argo.resumeUpgrade ?? null : null
|
|
315
|
+
const resumeBlockedReason = isArgoApp && argo && !argo.autoSyncEnabled ? argo.resumeBlockedReason : undefined
|
|
283
316
|
// graphFullscreen hides everything chrome-side; the body region expands.
|
|
284
317
|
// Mirrors the OSS GitOpsDetailView shell behavior so the visual chrome
|
|
285
318
|
// class set carries through.
|
|
@@ -365,6 +398,8 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
365
398
|
{detail.autoSyncMode && <AppFact label="Sync mode" value={detail.autoSyncMode} />}
|
|
366
399
|
</div>
|
|
367
400
|
)}
|
|
401
|
+
{resumeBlockedReason && <p className="mt-2 text-xs text-theme-text-secondary">{resumeBlockedReason}</p>}
|
|
402
|
+
{resumeUpgrade && <RadarUpgradeNote requirement={resumeUpgrade} className="mt-2" />}
|
|
368
403
|
</div>
|
|
369
404
|
{/* Action buttons */}
|
|
370
405
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -376,7 +411,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
376
411
|
icon={ArrowDownUp}
|
|
377
412
|
loading={argo.syncing}
|
|
378
413
|
onClick={argo.onSyncRequested}
|
|
379
|
-
disabled={
|
|
414
|
+
disabled={argoSyncDisabled}
|
|
380
415
|
disabledReason={terminating
|
|
381
416
|
? terminatingActionTooltip
|
|
382
417
|
: argoOperationInProgress
|
|
@@ -411,7 +446,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
411
446
|
{argo.autoSyncEnabled ? (
|
|
412
447
|
<ActionButton
|
|
413
448
|
label="Disable auto-sync"
|
|
414
|
-
description=
|
|
449
|
+
description={`Stop Argo from automatically syncing Git changes.${argoSyncDisabled ? '' : ' Manual Sync still works.'}${argoAutoSyncControllerNote(argo.autoSyncController)}`}
|
|
415
450
|
icon={Pause}
|
|
416
451
|
loading={argo.suspending}
|
|
417
452
|
onClick={argo.onSuspend}
|
|
@@ -421,12 +456,16 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
421
456
|
) : (
|
|
422
457
|
<ActionButton
|
|
423
458
|
label="Enable auto-sync"
|
|
424
|
-
description=
|
|
459
|
+
description={argoResumeDescription(effectiveSuspended, argo.resumeSettings)}
|
|
425
460
|
icon={Play}
|
|
426
461
|
loading={argo.resuming}
|
|
427
|
-
onClick={argo.onResume}
|
|
428
|
-
disabled={terminating}
|
|
429
|
-
disabledReason={terminating
|
|
462
|
+
onClick={() => !resumeBlockedReason && !resumeUpgrade && argo.onResume()}
|
|
463
|
+
disabled={terminating || !!resumeUpgrade || !!resumeBlockedReason}
|
|
464
|
+
disabledReason={terminating
|
|
465
|
+
? terminatingActionTooltip
|
|
466
|
+
: resumeBlockedReason || (resumeUpgrade
|
|
467
|
+
? radarUpgradeNoteText(resumeUpgrade)
|
|
468
|
+
: undefined)}
|
|
430
469
|
/>
|
|
431
470
|
)}
|
|
432
471
|
</>
|
|
@@ -528,6 +567,14 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
|
|
|
528
567
|
<div className="p-4 text-sm text-red-500">Failed to load GitOps view: {resourceError.message}</div>
|
|
529
568
|
) : (
|
|
530
569
|
<div className={bodyShellClass}>
|
|
570
|
+
{((warnings?.length ?? 0) > 0 || (insight?.warnings?.length ?? 0) > 0) && (
|
|
571
|
+
<AlertBanner
|
|
572
|
+
variant="warning"
|
|
573
|
+
title="Limited resource view"
|
|
574
|
+
items={[...new Set([...(warnings ?? []), ...(insight?.warnings ?? [])])]}
|
|
575
|
+
className="shrink-0 rounded-none border-x-0 border-t-0"
|
|
576
|
+
/>
|
|
577
|
+
)}
|
|
531
578
|
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-theme-border bg-theme-base px-4 py-2">
|
|
532
579
|
<div className="flex items-center gap-1 rounded-md border border-theme-border bg-theme-surface p-1">
|
|
533
580
|
<ViewButton active={activeTab === 'topology'} icon={GitBranch} label="Topology" onClick={() => onTabChange('topology')} />
|
|
@@ -38,7 +38,7 @@ export const APP_TREE_API_ERROR_FINDINGS = 'Meanwhile, rows marked "Found by Rad
|
|
|
38
38
|
export const APP_TREE_API_ERROR_NO_FINDINGS = "Radar didn't find a problem on its own."
|
|
39
39
|
|
|
40
40
|
export const REMOTE_DESTINATION_NOTICE =
|
|
41
|
-
"
|
|
41
|
+
"The recorded resources aren't confirmed to belong to this cluster, so local health and generated resources aren't shown."
|
|
42
42
|
export const REMOTE_FLUX_TARGET_NOTICE =
|
|
43
43
|
"This applies its resources to a different cluster, so they aren't visible from here."
|
|
44
44
|
|
|
@@ -34,9 +34,11 @@ import { FacetSection, FacetButton } from '../ui/Facet'
|
|
|
34
34
|
import { SortableTh, TH_CLASS, type SortDir } from '../ui/SortableTh'
|
|
35
35
|
import { DistributionBar } from '../ui/DistributionBar'
|
|
36
36
|
import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
|
|
37
|
+
import { radarUpgradeNoteText } from '../ui/RadarUpgradeNote'
|
|
38
|
+
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
37
39
|
import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
|
|
38
40
|
import { getGitOpsResourceStatus } from './detail-helpers'
|
|
39
|
-
import { isArgoOperationInProgress, isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
41
|
+
import { isArgoAutoSyncEnabled, isArgoAutoSyncSwitchedOff, isArgoOperationInProgress, isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
|
|
40
42
|
import { toggleSet } from './GitOpsGraphFilterRail'
|
|
41
43
|
import { useFilterState, defineFilterSchema } from '../../filter-state'
|
|
42
44
|
import { parseContextName } from '../../utils/context-name'
|
|
@@ -288,6 +290,10 @@ export interface GitOpsTableViewProps {
|
|
|
288
290
|
// In-flight action state, keyed by `row.id`. Drives the per-item
|
|
289
291
|
// spinner so the user can tell which Sync/Refresh is still running.
|
|
290
292
|
pendingRowActions?: Map<string, Set<GitOpsRowAction>>
|
|
293
|
+
// Set when the connected Radar is too old to turn auto-sync back on for an
|
|
294
|
+
// Application with automated.enabled: false; its Resume renders disabled.
|
|
295
|
+
argoResumeUpgrade?: RadarUpgradeRequirement | null
|
|
296
|
+
argoResumeBlockedReason?: string
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
// ----- Main component --------------------------------------------------------
|
|
@@ -330,6 +336,8 @@ export function GitOpsTableView({
|
|
|
330
336
|
onClearNamespaces,
|
|
331
337
|
onRowAction,
|
|
332
338
|
pendingRowActions,
|
|
339
|
+
argoResumeUpgrade,
|
|
340
|
+
argoResumeBlockedReason,
|
|
333
341
|
filtersSide = 'left',
|
|
334
342
|
}: GitOpsTableViewProps) {
|
|
335
343
|
const searchInputRef = useRef<HTMLInputElement>(null)
|
|
@@ -858,6 +866,8 @@ export function GitOpsTableView({
|
|
|
858
866
|
destinationHrefFor={destinationHrefFor}
|
|
859
867
|
onRowAction={onRowAction}
|
|
860
868
|
pendingRowActions={pendingRowActions}
|
|
869
|
+
argoResumeUpgrade={argoResumeUpgrade}
|
|
870
|
+
argoResumeBlockedReason={argoResumeBlockedReason}
|
|
861
871
|
/>
|
|
862
872
|
)}
|
|
863
873
|
</div>
|
|
@@ -1292,6 +1302,8 @@ function GitOpsTable({
|
|
|
1292
1302
|
destinationHrefFor,
|
|
1293
1303
|
onRowAction,
|
|
1294
1304
|
pendingRowActions,
|
|
1305
|
+
argoResumeUpgrade,
|
|
1306
|
+
argoResumeBlockedReason,
|
|
1295
1307
|
}: {
|
|
1296
1308
|
rows: GitOpsRow[]
|
|
1297
1309
|
sort: { key: SortKey; dir: SortDir } | null
|
|
@@ -1303,6 +1315,8 @@ function GitOpsTable({
|
|
|
1303
1315
|
destinationHrefFor?: (row: GitOpsRow, destination: FleetDestinationStamp) => string
|
|
1304
1316
|
onRowAction?: (row: GitOpsRow, action: GitOpsRowAction) => void
|
|
1305
1317
|
pendingRowActions?: Map<string, Set<GitOpsRowAction>>
|
|
1318
|
+
argoResumeUpgrade?: RadarUpgradeRequirement | null
|
|
1319
|
+
argoResumeBlockedReason?: string
|
|
1306
1320
|
}) {
|
|
1307
1321
|
const showActions = !!onRowAction
|
|
1308
1322
|
return (
|
|
@@ -1409,7 +1423,7 @@ function GitOpsTable({
|
|
|
1409
1423
|
className="overflow-visible border-b border-theme-border px-2 py-2 text-right align-middle"
|
|
1410
1424
|
onClick={(e) => e.stopPropagation()}
|
|
1411
1425
|
>
|
|
1412
|
-
<RowActionMenu items={buildRowActionItems(row, onRowAction, pendingRowActions)} />
|
|
1426
|
+
<RowActionMenu items={buildRowActionItems(row, onRowAction, pendingRowActions, argoResumeUpgrade, argoResumeBlockedReason)} />
|
|
1413
1427
|
</td>
|
|
1414
1428
|
)}
|
|
1415
1429
|
</tr>
|
|
@@ -1424,10 +1438,12 @@ function GitOpsTable({
|
|
|
1424
1438
|
// the row's tool (Argo vs Flux), current suspend state, terminating state,
|
|
1425
1439
|
// and Argo's operationState.phase (used to gate the Terminate entry — only
|
|
1426
1440
|
// shown while a sync is mid-flight, mirroring the detail-page condition).
|
|
1427
|
-
function buildRowActionItems(
|
|
1441
|
+
export function buildRowActionItems(
|
|
1428
1442
|
row: GitOpsRow,
|
|
1429
1443
|
onAction: (row: GitOpsRow, action: GitOpsRowAction) => void,
|
|
1430
1444
|
pending?: Map<string, Set<GitOpsRowAction>>,
|
|
1445
|
+
argoResumeUpgrade?: RadarUpgradeRequirement | null,
|
|
1446
|
+
argoResumeBlockedReason?: string,
|
|
1431
1447
|
): RowActionItem[] {
|
|
1432
1448
|
const inFlight = pending?.get(row.id)
|
|
1433
1449
|
const isPending = (action: GitOpsRowAction) => inFlight?.has(action) ?? false
|
|
@@ -1480,13 +1496,16 @@ function buildRowActionItems(
|
|
|
1480
1496
|
pending: isPending('hard-refresh'),
|
|
1481
1497
|
})
|
|
1482
1498
|
if (suspended) {
|
|
1499
|
+
const resumeBlockedReason = isArgoAutoSyncSwitchedOff(row.raw)
|
|
1500
|
+
? argoResumeBlockedReason || (argoResumeUpgrade ? radarUpgradeNoteText(argoResumeUpgrade) : undefined)
|
|
1501
|
+
: undefined
|
|
1483
1502
|
items.push({
|
|
1484
1503
|
key: 'resume',
|
|
1485
1504
|
label: 'Resume',
|
|
1486
1505
|
icon: Play,
|
|
1487
|
-
onClick: () => onAction(row, 'resume'),
|
|
1488
|
-
disabled: terminating,
|
|
1489
|
-
disabledReason: terminating ? terminatingReason :
|
|
1506
|
+
onClick: () => !resumeBlockedReason && onAction(row, 'resume'),
|
|
1507
|
+
disabled: terminating || !!resumeBlockedReason,
|
|
1508
|
+
disabledReason: terminating ? terminatingReason : resumeBlockedReason,
|
|
1490
1509
|
pending: isPending('resume'),
|
|
1491
1510
|
divider: true,
|
|
1492
1511
|
})
|
|
@@ -2007,7 +2026,7 @@ export function normalizeArgoApplication(resource: any): GitOpsRow {
|
|
|
2007
2026
|
destinationNamespace: resource.spec?.destination?.namespace ?? '',
|
|
2008
2027
|
createdAt: resource.metadata?.creationTimestamp ?? '',
|
|
2009
2028
|
lastSync: argoLastSync ?? '',
|
|
2010
|
-
autoSync:
|
|
2029
|
+
autoSync: isArgoAutoSyncEnabled(resource),
|
|
2011
2030
|
terminating: isTerminating(resource),
|
|
2012
2031
|
terminationStartedAt: terminationStartedAt(resource),
|
|
2013
2032
|
raw: resource,
|