@skyhook-io/k8s-ui 1.17.0 → 1.18.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/README.md +41 -0
- package/package.json +6 -6
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +7 -3
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/Toast.tsx +15 -4
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +4 -2
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +193 -71
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- package/src/utils/view-transition.ts +16 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { applyColumnOrder, moveColumn, pinnedColumnCount, sanitizeColumnOrder } from './column-order'
|
|
3
|
+
|
|
4
|
+
const cols = (...keys: string[]) => keys.map(key => ({ key }))
|
|
5
|
+
const keysOf = (list: { key: string }[]) => list.map(c => c.key)
|
|
6
|
+
|
|
7
|
+
const PODS = cols('name', 'namespace', 'ready', 'status', 'restarts', 'age', 'node')
|
|
8
|
+
|
|
9
|
+
describe('applyColumnOrder', () => {
|
|
10
|
+
it('keeps definition order when no saved order is provided', () => {
|
|
11
|
+
expect(keysOf(applyColumnOrder(PODS, undefined))).toEqual(keysOf(PODS))
|
|
12
|
+
expect(keysOf(applyColumnOrder(PODS, null))).toEqual(keysOf(PODS))
|
|
13
|
+
expect(keysOf(applyColumnOrder(PODS, []))).toEqual(keysOf(PODS))
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('applies the saved order and keeps Name first', () => {
|
|
17
|
+
const order = ['age', 'restarts', 'namespace', 'ready', 'status', 'node']
|
|
18
|
+
expect(keysOf(applyColumnOrder(PODS, order)))
|
|
19
|
+
.toEqual(['name', 'age', 'restarts', 'namespace', 'ready', 'status', 'node'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('ignores a saved Name position', () => {
|
|
23
|
+
const order = ['age', 'name', 'namespace', 'ready', 'status', 'restarts', 'node']
|
|
24
|
+
expect(keysOf(applyColumnOrder(PODS, order))[0]).toBe('name')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('keeps host columns that lead Name in front of it', () => {
|
|
28
|
+
const fleet = cols('cluster', 'name', 'namespace', 'age')
|
|
29
|
+
expect(keysOf(applyColumnOrder(fleet, ['age', 'cluster', 'namespace'])))
|
|
30
|
+
.toEqual(['cluster', 'name', 'age', 'namespace'])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('puts a column added after the save in its default position', () => {
|
|
34
|
+
// 'ip' arrives between status and restarts; the user had moved age up.
|
|
35
|
+
const next = cols('name', 'namespace', 'ready', 'status', 'ip', 'restarts', 'age', 'node')
|
|
36
|
+
const order = ['age', 'namespace', 'ready', 'status', 'restarts', 'node']
|
|
37
|
+
expect(keysOf(applyColumnOrder(next, order)))
|
|
38
|
+
.toEqual(['name', 'age', 'namespace', 'ready', 'status', 'ip', 'restarts', 'node'])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('appends a new trailing column, such as a label column, at the end', () => {
|
|
42
|
+
const next = [...PODS, { key: 'label:app' }]
|
|
43
|
+
const order = ['node', 'age', 'namespace', 'ready', 'status', 'restarts']
|
|
44
|
+
expect(keysOf(applyColumnOrder(next, order)).at(-1)).toBe('label:app')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('keeps several new adjacent columns in their definition order', () => {
|
|
48
|
+
const next = cols('name', 'a', 'b', 'namespace', 'age')
|
|
49
|
+
expect(keysOf(applyColumnOrder(next, ['age', 'namespace'])))
|
|
50
|
+
.toEqual(['name', 'age', 'a', 'b', 'namespace'])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('keeps a hidden column in place: order is independent of visibility', () => {
|
|
54
|
+
const order = ['age', 'restarts', 'namespace', 'ready', 'status', 'node']
|
|
55
|
+
const ordered = applyColumnOrder(PODS, order)
|
|
56
|
+
const visible = new Set(['name', 'age', 'namespace', 'ready', 'status', 'node'])
|
|
57
|
+
expect(keysOf(ordered.filter(c => visible.has(c.key))))
|
|
58
|
+
.toEqual(['name', 'age', 'namespace', 'ready', 'status', 'node'])
|
|
59
|
+
visible.add('restarts')
|
|
60
|
+
expect(keysOf(ordered.filter(c => visible.has(c.key))))
|
|
61
|
+
.toEqual(['name', 'age', 'restarts', 'namespace', 'ready', 'status', 'node'])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('drops saved keys for columns that no longer exist', () => {
|
|
65
|
+
const order = ['gone', 'age', 'namespace', 'ready', 'status', 'restarts', 'node']
|
|
66
|
+
expect(keysOf(applyColumnOrder(PODS, order)))
|
|
67
|
+
.toEqual(['name', 'age', 'namespace', 'ready', 'status', 'restarts', 'node'])
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('sanitizeColumnOrder', () => {
|
|
72
|
+
it('reads anything that is not a key list as no order', () => {
|
|
73
|
+
expect(sanitizeColumnOrder(undefined)).toBeNull()
|
|
74
|
+
expect(sanitizeColumnOrder('age')).toBeNull()
|
|
75
|
+
expect(sanitizeColumnOrder({ 0: 'age' })).toBeNull()
|
|
76
|
+
expect(sanitizeColumnOrder([1, null])).toBeNull()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('keeps string keys, first occurrence wins', () => {
|
|
80
|
+
expect(sanitizeColumnOrder(['age', 3, 'node', 'age'])).toEqual(['age', 'node'])
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('moveColumn', () => {
|
|
85
|
+
it('returns the movable keys with the column moved', () => {
|
|
86
|
+
expect(moveColumn(PODS, 'age', 0)).toEqual(['age', 'namespace', 'ready', 'status', 'restarts', 'node'])
|
|
87
|
+
expect(moveColumn(PODS, 'namespace', 5)).toEqual(['ready', 'status', 'restarts', 'age', 'node', 'namespace'])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('returns null for a no-op, an out-of-range edge move, or a pinned column', () => {
|
|
91
|
+
expect(moveColumn(PODS, 'namespace', -1)).toBeNull()
|
|
92
|
+
expect(moveColumn(PODS, 'node', 99)).toBeNull()
|
|
93
|
+
expect(moveColumn(PODS, 'name', 2)).toBeNull()
|
|
94
|
+
expect(moveColumn(PODS, 'missing', 0)).toBeNull()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('pinnedColumnCount', () => {
|
|
99
|
+
it('pins through Name, and nothing when there is no Name column', () => {
|
|
100
|
+
expect(pinnedColumnCount(PODS)).toBe(1)
|
|
101
|
+
expect(pinnedColumnCount(cols('cluster', 'name', 'age'))).toBe(2)
|
|
102
|
+
expect(pinnedColumnCount(cols('age'))).toBe(0)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The user's column order for a resource table, kept apart from visibility: it
|
|
2
|
+
// lists every movable column, hidden ones included, so a column hidden and later
|
|
3
|
+
// re-shown comes back where it was.
|
|
4
|
+
|
|
5
|
+
interface Keyed {
|
|
6
|
+
key: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Columns up to and including Name stay where the kind defines them. Name is
|
|
10
|
+
* the flexible column and the row link; host-injected leading columns sit in
|
|
11
|
+
* front of it by contract. */
|
|
12
|
+
export function pinnedColumnCount(columns: readonly Keyed[]): number {
|
|
13
|
+
return columns.findIndex(c => c.key === 'name') + 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A saved order read from localStorage: only string keys, first occurrence
|
|
17
|
+
* wins. Anything else reads as "no saved order". */
|
|
18
|
+
export function sanitizeColumnOrder(raw: unknown): string[] | null {
|
|
19
|
+
if (!Array.isArray(raw)) return null
|
|
20
|
+
const seen = new Set<string>()
|
|
21
|
+
for (const k of raw) {
|
|
22
|
+
if (typeof k === 'string' && !seen.has(k)) seen.add(k)
|
|
23
|
+
}
|
|
24
|
+
return seen.size ? Array.from(seen) : null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Columns in the user's saved order.
|
|
29
|
+
*
|
|
30
|
+
* Saved keys that no longer exist are ignored. A column the saved order does not
|
|
31
|
+
* name (a built-in, printer, or custom column) goes
|
|
32
|
+
* where it sits by default: right before the column that follows it in the
|
|
33
|
+
* definition, or at the end when nothing that follows it is in the saved order.
|
|
34
|
+
*/
|
|
35
|
+
export function applyColumnOrder<T extends Keyed>(columns: readonly T[], order: readonly string[] | null | undefined): T[] {
|
|
36
|
+
if (!order?.length) return [...columns]
|
|
37
|
+
const pinned = pinnedColumnCount(columns)
|
|
38
|
+
const movable = columns.slice(pinned)
|
|
39
|
+
const rank = new Map(order.map((k, i) => [k, i]))
|
|
40
|
+
const result = movable
|
|
41
|
+
.filter(c => rank.has(c.key))
|
|
42
|
+
.sort((a, b) => rank.get(a.key)! - rank.get(b.key)!)
|
|
43
|
+
movable.forEach((c, i) => {
|
|
44
|
+
if (rank.has(c.key)) return
|
|
45
|
+
const anchor = movable.slice(i + 1).find(n => rank.has(n.key))
|
|
46
|
+
const at = anchor ? result.indexOf(anchor) : -1
|
|
47
|
+
if (at === -1) result.push(c)
|
|
48
|
+
else result.splice(at, 0, c)
|
|
49
|
+
})
|
|
50
|
+
return [...columns.slice(0, pinned), ...result]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The movable keys of `columns` (already in display order) after moving `key`
|
|
54
|
+
* to `toIndex` within them. Returns null when nothing would change. */
|
|
55
|
+
export function moveColumn(columns: readonly Keyed[], key: string, toIndex: number): string[] | null {
|
|
56
|
+
const keys = columns.slice(pinnedColumnCount(columns)).map(c => c.key)
|
|
57
|
+
const from = keys.indexOf(key)
|
|
58
|
+
if (from === -1) return null
|
|
59
|
+
const to = Math.max(0, Math.min(keys.length - 1, toIndex))
|
|
60
|
+
if (to === from) return null
|
|
61
|
+
keys.splice(from, 1)
|
|
62
|
+
keys.splice(to, 0, key)
|
|
63
|
+
return keys
|
|
64
|
+
}
|
|
@@ -108,8 +108,8 @@ describe('curated column ownership', () => {
|
|
|
108
108
|
// Brace-counting, not parsing: assert the extraction actually saw the whole
|
|
109
109
|
// table, or a silently-missed key would let the guard above pass vacuously.
|
|
110
110
|
it('extracted every curated key', () => {
|
|
111
|
-
expect(CURATED.length).toBe(
|
|
112
|
-
expect(OWNED.size).toBe(
|
|
111
|
+
expect(CURATED.length).toBe(240)
|
|
112
|
+
expect(OWNED.size).toBe(202)
|
|
113
113
|
expect(CURATED).toContain('pods')
|
|
114
114
|
expect(CURATED).toContain('crossplanemanagedresources')
|
|
115
115
|
expect([...OWNED]).toContain('awsmanagedcontrolplanes')
|
|
@@ -166,7 +166,7 @@ describe('every declaration resolves', () => {
|
|
|
166
166
|
.map(m => [m[1], m[2].split(',').map(g => g.trim().replace(/'/g, ''))] as const)
|
|
167
167
|
|
|
168
168
|
it('covers the whole ownership table', () => {
|
|
169
|
-
expect(DECLARED.length).toBe(
|
|
169
|
+
expect(DECLARED.length).toBe(202)
|
|
170
170
|
})
|
|
171
171
|
|
|
172
172
|
it.each(DECLARED)('%s is curated under each group it claims', (key, groups) => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { ArgoApplicationRenderer } from './ArgoApplicationRenderer'
|
|
4
|
+
|
|
5
|
+
describe('Argo Helm value files', () => {
|
|
6
|
+
it.each([true, false, undefined])('shows missing-file behavior for single and multiple sources: %s', optional => {
|
|
7
|
+
const source = { chart: 'cluster', repoURL: 'https://cloudnative-pg.github.io/charts', helm: { valueFiles: ['values.yaml', 'overrides.yaml'], ignoreMissingValueFiles: optional } }
|
|
8
|
+
for (const spec of [{ source }, { sources: [source] }]) {
|
|
9
|
+
const html = renderToStaticMarkup(<ArgoApplicationRenderer data={{ metadata: { name: 'app' }, spec }} />)
|
|
10
|
+
expect(html).toContain('values.yaml, overrides.yaml')
|
|
11
|
+
expect(html).toContain('If a Value File Is Missing')
|
|
12
|
+
expect(html).toContain(optional ? 'Skipped' : 'Render fails')
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
})
|
|
@@ -81,7 +81,10 @@ function SourceProperties({ source }: { source: any }) {
|
|
|
81
81
|
)}
|
|
82
82
|
{source.chart && <Property label="Helm Chart" value={source.chart} />}
|
|
83
83
|
{source.helm?.valueFiles && source.helm.valueFiles.length > 0 && (
|
|
84
|
-
|
|
84
|
+
<>
|
|
85
|
+
<Property label="Value Files" value={source.helm.valueFiles.join(', ')} />
|
|
86
|
+
<Property label="If a Value File Is Missing" value={source.helm.ignoreMissingValueFiles === true ? 'Skipped' : 'Render fails'} />
|
|
87
|
+
</>
|
|
85
88
|
)}
|
|
86
89
|
{source.helm?.parameters && source.helm.parameters.length > 0 && (
|
|
87
90
|
<Property
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
5
|
+
import { expect, it, vi } from "vitest";
|
|
6
|
+
import { HPARenderer } from "./HPARenderer";
|
|
7
|
+
import { VPARenderer } from "./VPARenderer";
|
|
8
|
+
import { KedaScaledObjectRenderer } from "./KedaScaledObjectRenderer";
|
|
9
|
+
import {
|
|
10
|
+
initNavigationMap,
|
|
11
|
+
resetNavigationMap,
|
|
12
|
+
} from "../../../utils/navigation";
|
|
13
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
14
|
+
|
|
15
|
+
it.each(["HPA", "VPA"])(
|
|
16
|
+
"retains a %s custom scale target API group",
|
|
17
|
+
async (kind) => {
|
|
18
|
+
initNavigationMap([
|
|
19
|
+
{
|
|
20
|
+
group: "custom.example.io",
|
|
21
|
+
version: "v1",
|
|
22
|
+
kind: "Deployment",
|
|
23
|
+
name: "deployments",
|
|
24
|
+
namespaced: true,
|
|
25
|
+
isCrd: true,
|
|
26
|
+
verbs: ["get"],
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
const element = document.createElement("div");
|
|
30
|
+
const root = createRoot(element);
|
|
31
|
+
const onNavigate = vi.fn();
|
|
32
|
+
const target = {
|
|
33
|
+
apiVersion: "custom.example.io/v1",
|
|
34
|
+
kind: "Deployment",
|
|
35
|
+
name: "same-name",
|
|
36
|
+
};
|
|
37
|
+
const data = {
|
|
38
|
+
metadata: { namespace: "team" },
|
|
39
|
+
spec: kind === "HPA" ? { scaleTargetRef: target } : { targetRef: target },
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
await act(async () =>
|
|
43
|
+
root.render(
|
|
44
|
+
kind === "HPA" ? (
|
|
45
|
+
<HPARenderer data={data} onNavigate={onNavigate} />
|
|
46
|
+
) : (
|
|
47
|
+
<VPARenderer data={data} onNavigate={onNavigate} />
|
|
48
|
+
),
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
const button = [...element.querySelectorAll("button")].find(
|
|
52
|
+
(b) => b.textContent === "Deployment/same-name",
|
|
53
|
+
);
|
|
54
|
+
expect(button).toBeDefined();
|
|
55
|
+
expect([...element.querySelectorAll("button")].filter(b => b.textContent?.startsWith("Deployment/same-name"))).toHaveLength(1);
|
|
56
|
+
await act(async () => button!.click());
|
|
57
|
+
expect(onNavigate).toHaveBeenCalledWith({
|
|
58
|
+
kind: "Deployment",
|
|
59
|
+
namespace: "team",
|
|
60
|
+
name: "same-name",
|
|
61
|
+
group: "custom.example.io",
|
|
62
|
+
});
|
|
63
|
+
} finally {
|
|
64
|
+
await act(async () => root.unmount());
|
|
65
|
+
resetNavigationMap();
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
it.each(["v1", "", undefined])("keeps Object metric resources separate from the scale target and measures its own Namespace (apiVersion %s)", async (namespaceAPIVersion) => {
|
|
71
|
+
const element = document.createElement("div");
|
|
72
|
+
const root = createRoot(element);
|
|
73
|
+
const onNavigate = vi.fn();
|
|
74
|
+
const data = {
|
|
75
|
+
metadata: { namespace: "team" },
|
|
76
|
+
spec: {
|
|
77
|
+
scaleTargetRef: {
|
|
78
|
+
apiVersion: "apps/v1",
|
|
79
|
+
kind: "Deployment",
|
|
80
|
+
name: "workload",
|
|
81
|
+
},
|
|
82
|
+
metrics: [
|
|
83
|
+
{
|
|
84
|
+
type: "Object",
|
|
85
|
+
object: {
|
|
86
|
+
metric: { name: "requests" },
|
|
87
|
+
describedObject: {
|
|
88
|
+
apiVersion: "networking.k8s.io/v1",
|
|
89
|
+
kind: "Ingress",
|
|
90
|
+
name: "frontend",
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: "Object",
|
|
96
|
+
object: {
|
|
97
|
+
metric: { name: "queue" },
|
|
98
|
+
describedObject: {
|
|
99
|
+
apiVersion: "custom.example.io/v1",
|
|
100
|
+
kind: "Widget",
|
|
101
|
+
name: "input",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: "Object",
|
|
107
|
+
object: {
|
|
108
|
+
metric: { name: "namespace-metric" },
|
|
109
|
+
describedObject: {
|
|
110
|
+
apiVersion: namespaceAPIVersion,
|
|
111
|
+
kind: "Namespace",
|
|
112
|
+
name: "cannot-escape",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
try {
|
|
120
|
+
await act(async () =>
|
|
121
|
+
root.render(<HPARenderer data={data} onNavigate={onNavigate} />),
|
|
122
|
+
);
|
|
123
|
+
for (const [label, expected] of [
|
|
124
|
+
[
|
|
125
|
+
"Ingress/frontend",
|
|
126
|
+
{ kind: "Ingress", group: "networking.k8s.io", namespace: "team", name: "frontend" },
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
"Widget/input",
|
|
130
|
+
{
|
|
131
|
+
kind: "Widget",
|
|
132
|
+
group: "custom.example.io",
|
|
133
|
+
namespace: "team",
|
|
134
|
+
name: "input",
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
[
|
|
138
|
+
"Namespace/team",
|
|
139
|
+
{ kind: "Namespace", group: "", namespace: "", name: "team" },
|
|
140
|
+
],
|
|
141
|
+
] as const) {
|
|
142
|
+
const button = [...element.querySelectorAll("button")].find(
|
|
143
|
+
(b) => b.textContent === label,
|
|
144
|
+
);
|
|
145
|
+
expect(button).toBeDefined();
|
|
146
|
+
await act(async () => button!.click());
|
|
147
|
+
expect(onNavigate).toHaveBeenLastCalledWith(expected);
|
|
148
|
+
}
|
|
149
|
+
expect(element.textContent).toContain("Object metric sources");
|
|
150
|
+
expect(element.textContent).toContain("This HPA's namespace");
|
|
151
|
+
expect(element.textContent).not.toContain("Namespace/cannot-escape");
|
|
152
|
+
} finally {
|
|
153
|
+
await act(async () => root.unmount());
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it.each(["HPA", "VPA"])("does not infer missing %s target identity", (kind) => {
|
|
158
|
+
const target = { name: "ambiguous" };
|
|
159
|
+
const data = {
|
|
160
|
+
metadata: { namespace: "team" },
|
|
161
|
+
spec: kind === "HPA" ? { scaleTargetRef: target } : { targetRef: target },
|
|
162
|
+
};
|
|
163
|
+
const html = renderToStaticMarkup(
|
|
164
|
+
kind === "HPA" ? (
|
|
165
|
+
<HPARenderer data={data} onNavigate={() => {}} />
|
|
166
|
+
) : (
|
|
167
|
+
<VPARenderer data={data} onNavigate={() => {}} />
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
171
|
+
expect(
|
|
172
|
+
[...doc.querySelectorAll("button")].some((b) =>
|
|
173
|
+
b.textContent?.includes("ambiguous"),
|
|
174
|
+
),
|
|
175
|
+
).toBe(false);
|
|
176
|
+
expect(html).not.toContain("Deployment/ambiguous");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.each([
|
|
180
|
+
[
|
|
181
|
+
{ name: "worker" },
|
|
182
|
+
{ kind: "Deployment", group: "apps", namespace: "team", name: "worker" },
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
{ name: "worker", apiVersion: "", kind: "" },
|
|
186
|
+
{ kind: "Deployment", group: "apps", namespace: "team", name: "worker" },
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
{ apiVersion: "custom.example.io/v1", kind: "Deployment", name: "worker" },
|
|
190
|
+
{
|
|
191
|
+
kind: "Deployment",
|
|
192
|
+
group: "custom.example.io",
|
|
193
|
+
namespace: "team",
|
|
194
|
+
name: "worker",
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
])(
|
|
198
|
+
"preserves KEDA documented defaults and explicit custom API identity",
|
|
199
|
+
async (target, expected) => {
|
|
200
|
+
const element = document.createElement("div");
|
|
201
|
+
const root = createRoot(element);
|
|
202
|
+
const onNavigate = vi.fn();
|
|
203
|
+
try {
|
|
204
|
+
await act(async () =>
|
|
205
|
+
root.render(
|
|
206
|
+
<KedaScaledObjectRenderer
|
|
207
|
+
data={{
|
|
208
|
+
metadata: { namespace: "team" },
|
|
209
|
+
spec: { scaleTargetRef: target },
|
|
210
|
+
}}
|
|
211
|
+
onNavigate={onNavigate}
|
|
212
|
+
/>,
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
const button = [...element.querySelectorAll("button")].find(
|
|
216
|
+
(b) => b.textContent === "Deployment/worker",
|
|
217
|
+
);
|
|
218
|
+
expect(button).toBeDefined();
|
|
219
|
+
await act(async () => button!.click());
|
|
220
|
+
expect(onNavigate).toHaveBeenCalledWith(expected);
|
|
221
|
+
} finally {
|
|
222
|
+
await act(async () => root.unmount());
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
it.each([
|
|
228
|
+
["Deployment", "apps"], ["StatefulSet", "apps"], ["DaemonSet", "apps"], ["ReplicaSet", "apps"],
|
|
229
|
+
["Job", "batch"], ["CronJob", "batch"], ["ReplicationController", ""],
|
|
230
|
+
])("keeps versionless VPA %s target navigation", async (kind, group) => {
|
|
231
|
+
const element = document.createElement("div");
|
|
232
|
+
const root = createRoot(element);
|
|
233
|
+
const onNavigate = vi.fn();
|
|
234
|
+
try {
|
|
235
|
+
await act(async () => root.render(<VPARenderer data={{ metadata: { namespace: "team" }, spec: { targetRef: { kind, name: "workload" } } }} onNavigate={onNavigate} />));
|
|
236
|
+
const link = [...element.querySelectorAll("button")].find(button => button.textContent === `${kind}/workload`);
|
|
237
|
+
expect(link).toBeDefined();
|
|
238
|
+
await act(async () => link!.click());
|
|
239
|
+
expect(onNavigate).toHaveBeenLastCalledWith({ kind, group, namespace: "team", name: "workload" });
|
|
240
|
+
} finally {
|
|
241
|
+
await act(async () => root.unmount());
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("pairs each diagnosis metric card with its declared Object source once", async () => {
|
|
246
|
+
const element = document.createElement("div");
|
|
247
|
+
const root = createRoot(element);
|
|
248
|
+
const onNavigate = vi.fn();
|
|
249
|
+
const objects = [
|
|
250
|
+
{ metric: { name: "requests" }, describedObject: { apiVersion: "v1", kind: "Endpoints", name: "input" } },
|
|
251
|
+
{ metric: { name: "requests" }, describedObject: { apiVersion: "custom.example.io/v1", kind: "Endpoints", name: "input" } },
|
|
252
|
+
{ metric: { name: "queue_depth" }, describedObject: { kind: "Namespace", name: "ignored" } },
|
|
253
|
+
];
|
|
254
|
+
try {
|
|
255
|
+
await act(async () => root.render(<HPARenderer
|
|
256
|
+
data={{ metadata: { namespace: "team" }, spec: { metrics: objects.map(object => ({ type: "Object", object })) } }}
|
|
257
|
+
hpaDiagnosis={{ state: "stabilized", summary: "Stabilized", target: {}, bounds: { min: 1, max: 3, current: 2, desired: 2 }, metrics: [
|
|
258
|
+
{ type: "Object", name: "Endpoints/input/requests", current: "10", status: "ok" },
|
|
259
|
+
{ type: "Object", name: "Endpoints (custom.example.io)/input/requests", current: "20", status: "ok" },
|
|
260
|
+
{ type: "Object", name: "Namespace/team/queue_depth", current: "7", status: "ok" },
|
|
261
|
+
{ type: "Object", name: "Service/stale/requests", current: "3", status: "status_only" },
|
|
262
|
+
] }} onNavigate={onNavigate} />));
|
|
263
|
+
expect(element.textContent).not.toContain("Object metric sources");
|
|
264
|
+
expect(element.textContent).not.toContain("ignored");
|
|
265
|
+
const endpoints = [...element.querySelectorAll("button")].filter(button => button.textContent === "Endpoints/input");
|
|
266
|
+
expect(endpoints).toHaveLength(2);
|
|
267
|
+
await act(async () => endpoints[0].click());
|
|
268
|
+
expect(onNavigate).toHaveBeenLastCalledWith({ kind: "Endpoints", group: "", namespace: "team", name: "input" });
|
|
269
|
+
await act(async () => endpoints[1].click());
|
|
270
|
+
expect(onNavigate).toHaveBeenLastCalledWith({ kind: "Endpoints", group: "custom.example.io", namespace: "team", name: "input" });
|
|
271
|
+
const namespace = [...element.querySelectorAll("button")].find(button => button.textContent === "Namespace/team");
|
|
272
|
+
expect(namespace).toBeDefined();
|
|
273
|
+
await act(async () => namespace!.click());
|
|
274
|
+
expect(onNavigate).toHaveBeenLastCalledWith({ kind: "Namespace", group: "", namespace: "", name: "team" });
|
|
275
|
+
expect([...element.querySelectorAll("button")].some(button => button.textContent === "Service/stale")).toBe(false);
|
|
276
|
+
for (const value of ["Current 10", "Current 20", "Current 7"]) expect(element.textContent).toContain(value);
|
|
277
|
+
} finally {
|
|
278
|
+
await act(async () => root.unmount());
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("shows the documented default Target API for KEDA", () => {
|
|
283
|
+
const html = renderToStaticMarkup(<KedaScaledObjectRenderer data={{ metadata: { namespace: "team" }, spec: { scaleTargetRef: { name: "worker" } } }} />);
|
|
284
|
+
expect(html).toContain("Target API");
|
|
285
|
+
expect(html).toContain("apps/v1");
|
|
286
|
+
});
|
|
@@ -27,6 +27,7 @@ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '', canConnect
|
|
|
27
27
|
const className = getClusterClass(data)
|
|
28
28
|
const version = getClusterVersion(data)
|
|
29
29
|
const topology = spec.topology || {}
|
|
30
|
+
const classNamespace = (topology.classRef?.name ? topology.classRef.namespace : topology.classNamespace) || data.metadata?.namespace || ''
|
|
30
31
|
|
|
31
32
|
// v1beta2 replica fields
|
|
32
33
|
const cpReady = status.controlPlane?.readyReplicas ?? status.controlPlane?.replicas
|
|
@@ -201,10 +202,10 @@ export function CAPIClusterRenderer({ data, onNavigate, apiBase = '', canConnect
|
|
|
201
202
|
</Section>
|
|
202
203
|
|
|
203
204
|
{/* Topology (ClusterClass-based) */}
|
|
204
|
-
{
|
|
205
|
+
{className !== '-' && (
|
|
205
206
|
<Section title="Topology" icon={Layers}>
|
|
206
207
|
<PropertyList>
|
|
207
|
-
<Property label="Class" value={
|
|
208
|
+
<Property label="Class" value={<ResourceLink name={className} kind="clusterclasses" group="cluster.x-k8s.io" namespace={classNamespace} onNavigate={onNavigate} />} />
|
|
208
209
|
{topology.version && <Property label="Version" value={topology.version} />}
|
|
209
210
|
{topology.controlPlane?.replicas != null && (
|
|
210
211
|
<Property label="CP Replicas" value={String(topology.controlPlane.replicas)} />
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, type ReactNode } from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { CAPIClusterRenderer } from './CAPIClusterRenderer'
|
|
6
|
+
|
|
7
|
+
async function rendered(element: ReactNode, check: (container: HTMLDivElement) => Promise<void> | void) {
|
|
8
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
9
|
+
const container = document.createElement('div')
|
|
10
|
+
const root = createRoot(container)
|
|
11
|
+
try { await act(async () => root.render(element)); await check(container) }
|
|
12
|
+
finally { await act(async () => root.unmount()); Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false }) }
|
|
13
|
+
}
|
|
14
|
+
async function click(container: HTMLDivElement, text: string) {
|
|
15
|
+
const button = [...container.querySelectorAll('button')].find(button => button.textContent === text)
|
|
16
|
+
expect(button, `missing link ${text}`).toBeDefined()
|
|
17
|
+
await act(async () => button!.click())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('exact reference navigation', () => {
|
|
21
|
+
it('uses the v1beta2 ClusterClass reference namespace',async()=>{
|
|
22
|
+
const navigate=vi.fn()
|
|
23
|
+
await rendered(<CAPIClusterRenderer data={{metadata:{namespace:'app'},spec:{topology:{classRef:{name:'standard',namespace:'classes'}}}}} onNavigate={navigate}/>,async container=>{
|
|
24
|
+
await click(container,'standard');expect(navigate).toHaveBeenCalledWith({kind:'clusterclasses',group:'cluster.x-k8s.io',namespace:'classes',name:'standard'})
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -27,3 +27,26 @@ describe('CNPGBackupRenderer — spec.target is instance selection, not a restor
|
|
|
27
27
|
expect(html(backup())).not.toContain('Backup Target')
|
|
28
28
|
})
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
describe('CNPGBackupRenderer plugin destination', () => {
|
|
33
|
+
it.each([undefined, { barmanObjectName: 'ignored-store', serverName: 'ignored-server' }, { serverName: 'ignored-server' }])('explains the Cluster destination and ignored parameters: %j', (parameters) => {
|
|
34
|
+
const out = renderToString(<CNPGBackupRenderer data={backup({ spec: { method: 'plugin', pluginConfiguration: { name: 'barman-cloud.cloudnative-pg.io', parameters } } })} onNavigate={() => {}} />)
|
|
35
|
+
expect(out).toContain('From the Cluster')
|
|
36
|
+
expect(out).toContain('barman-cloud plugin')
|
|
37
|
+
expect(out).not.toContain('Object Store')
|
|
38
|
+
expect(out).not.toContain('ignored-store')
|
|
39
|
+
expect(out).not.toContain('ignored-server')
|
|
40
|
+
expect(out.match(/<button[^>]*>(?:pg|ignored-store|ignored-server)<\/button>/g)).toHaveLength(1)
|
|
41
|
+
expect(out.includes('Ignored by the barman-cloud plugin')).toBe(!!parameters)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('does not assert a third-party parameter is an ObjectStore destination', () => {
|
|
45
|
+
const out = html(backup({ spec: { method: 'plugin', pluginConfiguration: { name: 'other.example.com', parameters: { barmanObjectName: 'custom-store' } } } }))
|
|
46
|
+
expect(out).toContain('other.example.com')
|
|
47
|
+
expect(out).toContain('Unknown: Radar does not model this plugin')
|
|
48
|
+
expect(out).not.toContain('custom-store')
|
|
49
|
+
expect(out).not.toContain('Object Store')
|
|
50
|
+
expect(out).not.toContain('Ignored by')
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getCNPGBackupServerName,
|
|
13
13
|
getCNPGBackupError,
|
|
14
14
|
getCNPGBackupTarget,
|
|
15
|
-
|
|
15
|
+
CNPG_BARMAN_PLUGIN_NAME,
|
|
16
16
|
} from '../resource-utils-cnpg'
|
|
17
17
|
|
|
18
18
|
interface CNPGBackupRendererProps {
|
|
@@ -51,24 +51,16 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
|
|
|
51
51
|
<PropertyList>
|
|
52
52
|
<Property label="Phase" value={phase} />
|
|
53
53
|
<Property label="Method" value={getCNPGBackupMethod(data)} />
|
|
54
|
-
{/* A plugin-taken backup lands in an ObjectStore rather than in the
|
|
55
|
-
Cluster's own barmanObjectStore, so naming the plugin is what
|
|
56
|
-
tells an operator where to go looking. */}
|
|
57
54
|
{backupPlugin && <Property label="Plugin" value={backupPlugin.name} />}
|
|
58
|
-
{backupPlugin
|
|
55
|
+
{backupPlugin && (
|
|
59
56
|
<Property
|
|
60
|
-
label="
|
|
61
|
-
value={
|
|
62
|
-
<ResourceLink
|
|
63
|
-
name={backupPlugin.parameters.barmanObjectName}
|
|
64
|
-
kind="objectstores"
|
|
65
|
-
group={CNPG_BARMAN_OBJECTSTORE_GROUP}
|
|
66
|
-
namespace={data.metadata?.namespace || ''}
|
|
67
|
-
onNavigate={onNavigate}
|
|
68
|
-
/>
|
|
69
|
-
}
|
|
57
|
+
label="Destination"
|
|
58
|
+
value={backupPlugin.name === CNPG_BARMAN_PLUGIN_NAME ? "From the Cluster's barman-cloud plugin" : 'Unknown: Radar does not model this plugin’s destination'}
|
|
70
59
|
/>
|
|
71
60
|
)}
|
|
61
|
+
{backupPlugin?.name === CNPG_BARMAN_PLUGIN_NAME && backupPlugin.parameters && Object.keys(backupPlugin.parameters).length > 0 && (
|
|
62
|
+
<Property label="Plugin parameters" value="Ignored by the barman-cloud plugin; the destination comes from the Cluster" />
|
|
63
|
+
)}
|
|
72
64
|
<Property label="Duration" value={getCNPGBackupDuration(data)} />
|
|
73
65
|
<Property label="Backup Name" value={getCNPGBackupName(data)} />
|
|
74
66
|
{data.status?.instanceID?.podName && (
|
|
@@ -102,10 +94,7 @@ export function CNPGBackupRenderer({ data, onNavigate }: CNPGBackupRendererProps
|
|
|
102
94
|
}
|
|
103
95
|
return clusterName
|
|
104
96
|
})()} />
|
|
105
|
-
{/*
|
|
106
|
-
Under the plugin method they are never populated because both live
|
|
107
|
-
on the ObjectStore, and rendering them as "-" reads as "not
|
|
108
|
-
configured" rather than "recorded elsewhere". */}
|
|
97
|
+
{/* Plugin backups do not report the in-tree destination fields. */}
|
|
109
98
|
{!backupPlugin && (
|
|
110
99
|
<>
|
|
111
100
|
<Property label="Destination" value={getCNPGBackupDestinationPath(data)} />
|