@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.
Files changed (250) hide show
  1. package/README.md +41 -0
  2. package/package.json +6 -6
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +7 -3
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +142 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +40 -81
  67. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  68. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  69. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
  70. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
  71. package/src/components/gitops/tree/layout.test.ts +111 -0
  72. package/src/components/gitops/tree/layout.ts +128 -0
  73. package/src/components/gitops/tree/tree-helpers.test.ts +23 -0
  74. package/src/components/gitops/tree/tree-helpers.ts +12 -2
  75. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  76. package/src/components/issues/IssuesView.tsx +44 -6
  77. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  78. package/src/components/issues/ResourceIssuesSection.tsx +8 -2
  79. package/src/components/issues/diagnostic.ts +2 -0
  80. package/src/components/issues/index.ts +3 -1
  81. package/src/components/issues/issue-timing.ts +12 -1
  82. package/src/components/issues/issues.test.ts +75 -1
  83. package/src/components/issues/severity.ts +30 -2
  84. package/src/components/issues/types.ts +55 -4
  85. package/src/components/logs/LogCore.theme.test.tsx +206 -3
  86. package/src/components/logs/LogCore.tsx +180 -53
  87. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  88. package/src/components/logs/LogsViewer.test.tsx +369 -0
  89. package/src/components/logs/LogsViewer.tsx +91 -13
  90. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  91. package/src/components/logs/StructuredLogLine.tsx +23 -31
  92. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  93. package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
  94. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  95. package/src/components/logs/useLogBuffer.test.ts +8 -0
  96. package/src/components/logs/useLogBuffer.ts +48 -18
  97. package/src/components/logs/useLogSearch.ts +3 -1
  98. package/src/components/logs/useLogStream.ts +17 -1
  99. package/src/components/problems/index.ts +1 -0
  100. package/src/components/problems/problems.test.tsx +63 -0
  101. package/src/components/problems/problems.tsx +165 -0
  102. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  103. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  104. package/src/components/resources/ColumnPickerList.test.tsx +46 -0
  105. package/src/components/resources/ColumnPickerList.tsx +174 -0
  106. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  107. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  108. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  109. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  110. package/src/components/resources/ResourcesView.kind-view.test.tsx +296 -0
  111. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  112. package/src/components/resources/ResourcesView.tsx +392 -181
  113. package/src/components/resources/column-order.test.ts +104 -0
  114. package/src/components/resources/column-order.ts +64 -0
  115. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  116. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  117. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  118. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  119. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  120. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  121. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  122. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  123. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  124. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  125. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  126. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  127. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  128. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  129. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  130. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  131. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  132. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  133. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  134. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  135. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  136. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  137. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  138. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  139. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  140. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  141. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  142. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  143. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  144. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  145. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  146. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  147. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  148. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  149. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  150. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  151. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  152. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  153. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  154. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  155. package/src/components/resources/renderers/PodRenderer.tsx +81 -40
  156. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  157. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  158. package/src/components/resources/resource-utils-capi.ts +1 -1
  159. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  160. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  161. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  162. package/src/components/resources/resource-utils-contour.ts +16 -0
  163. package/src/components/resources/resource-utils-istio.ts +12 -8
  164. package/src/components/resources/resource-utils-knative.ts +18 -0
  165. package/src/components/resources/resource-utils.ts +96 -7
  166. package/src/components/resources/rules-column-width.test.ts +1 -1
  167. package/src/components/resources/summarize-scheduler-message.test.ts +31 -0
  168. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  169. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  170. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  171. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  172. package/src/components/shared/DetailShell.test.tsx +80 -0
  173. package/src/components/shared/DetailShell.tsx +85 -10
  174. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  175. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  176. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  177. package/src/components/shared/EditableYamlView.tsx +1 -1
  178. package/src/components/shared/ResourceActionsBar.tsx +14 -7
  179. package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
  180. package/src/components/shared/index.ts +2 -0
  181. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  182. package/src/components/timeline/DiffViewer.tsx +113 -4
  183. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  184. package/src/components/timeline/shared.test.tsx +48 -0
  185. package/src/components/timeline/shared.tsx +14 -19
  186. package/src/components/timeline/timeline-filters.ts +1 -0
  187. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  188. package/src/components/topology/K8sResourceNode.tsx +44 -2
  189. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  190. package/src/components/topology/TopologyGraph.tsx +3 -71
  191. package/src/components/topology/TopologySearch.tsx +1 -0
  192. package/src/components/topology/internet-groups.test.ts +35 -0
  193. package/src/components/topology/internet-groups.ts +55 -0
  194. package/src/components/topology/layout.ts +1 -1
  195. package/src/components/ui/Badge.tsx +1 -0
  196. package/src/components/ui/FoldSection.tsx +68 -0
  197. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  198. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  199. package/src/components/ui/RefLink.tsx +20 -0
  200. package/src/components/ui/RowActionMenu.tsx +8 -3
  201. package/src/components/ui/Toast.tsx +15 -4
  202. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  203. package/src/components/ui/drawer-components.test.tsx +110 -1
  204. package/src/components/ui/drawer-components.tsx +62 -9
  205. package/src/components/ui/index.ts +4 -2
  206. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  207. package/src/components/ui/monacoRuntime.ts +14 -19
  208. package/src/components/ui/status-tone.test.ts +10 -1
  209. package/src/components/ui/status-tone.tsx +29 -0
  210. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  211. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  212. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  213. package/src/components/workload/WorkloadView.tsx +193 -71
  214. package/src/index.ts +14 -1
  215. package/src/monaco-deep.d.ts +19 -12
  216. package/src/theme/components.css +13 -0
  217. package/src/types/capacity.ts +11 -2
  218. package/src/types/core.ts +53 -2
  219. package/src/types/gitops-flux-status.test.ts +32 -1
  220. package/src/types/gitops-insights.ts +20 -2
  221. package/src/types/gitops-tree.ts +12 -0
  222. package/src/types/gitops.ts +26 -6
  223. package/src/utils/api-resources.ts +7 -0
  224. package/src/utils/application-history.ts +1 -0
  225. package/src/utils/cnpg-backup.test.ts +26 -0
  226. package/src/utils/cnpg-backup.ts +70 -0
  227. package/src/utils/format.test.ts +9 -0
  228. package/src/utils/format.ts +4 -0
  229. package/src/utils/gitops-owner.ts +3 -2
  230. package/src/utils/grant.test.ts +19 -0
  231. package/src/utils/grant.ts +35 -0
  232. package/src/utils/index.ts +4 -0
  233. package/src/utils/k8s-errors.ts +7 -7
  234. package/src/utils/log-format.test.ts +41 -0
  235. package/src/utils/log-format.ts +71 -15
  236. package/src/utils/log-level.ts +12 -8
  237. package/src/utils/navigation.test.ts +57 -0
  238. package/src/utils/navigation.ts +86 -8
  239. package/src/utils/object-reference-navigation.test.ts +43 -0
  240. package/src/utils/resource-hierarchy.test.ts +37 -0
  241. package/src/utils/resource-hierarchy.ts +15 -7
  242. package/src/utils/resource-icons.ts +2 -0
  243. package/src/utils/skeleton-yaml.test.ts +21 -0
  244. package/src/utils/skeleton-yaml.ts +16 -0
  245. package/src/utils/url-path.test.ts +35 -0
  246. package/src/utils/url-path.ts +21 -0
  247. package/src/utils/validators.test.ts +4 -13
  248. package/src/utils/validators.ts +5 -19
  249. package/src/utils/view-transition.test.ts +35 -1
  250. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,296 @@
1
+ // @vitest-environment jsdom
2
+ import { act, type ComponentProps } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
+ import { ResourcesView } from './ResourcesView'
6
+ import { KeyboardShortcutProvider } from '../../hooks/useKeyboardShortcuts'
7
+ import type { APIResource } from '../../types'
8
+
9
+ ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
10
+ window.matchMedia = vi.fn().mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })
11
+ Element.prototype.scrollIntoView = vi.fn()
12
+ const pods: APIResource = { group: '', version: 'v1', kind: 'Pod', name: 'pods', namespaced: true, isCrd: false, verbs: ['get', 'list'] }
13
+ const pod = { apiVersion: 'v1', kind: 'Pod', metadata: { name: 'example', namespace: 'test', uid: 'pod-id' }, spec: { containers: [{ name: 'app', image: 'example' }] }, status: { phase: 'Running' } }
14
+ let root: ReturnType<typeof createRoot>
15
+ let host: HTMLDivElement
16
+
17
+ afterEach(() => { act(() => root?.unmount()); host?.remove(); vi.restoreAllMocks(); localStorage.clear() })
18
+
19
+ describe('custom resource kind view', () => {
20
+ it('keeps sidebar navigation while disabling table keys and column settings, then restores the table on null', () => {
21
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
22
+ const selected = vi.fn()
23
+ const storageRead = vi.spyOn(Storage.prototype, 'getItem')
24
+ const storageWrite = vi.spyOn(Storage.prototype, 'setItem')
25
+ const render = (custom: boolean | undefined) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
26
+ namespaces={['test']}
27
+ apiResources={[pods]}
28
+ resourceCounts={{ Pod: 1 }}
29
+ selectedKindQuery={{ isLoading: false, data: [pod], resourceName: 'pods', group: '' }}
30
+ locationPathname="/resources/pods"
31
+ locationSearch=""
32
+ onNavigate={() => {}}
33
+ onResourceClick={selected}
34
+ renderKindView={kind => custom === undefined ? false : custom ? <div data-testid="kind-view">Custom {kind.kind}</div> : null}
35
+ /></KeyboardShortcutProvider>))
36
+ render(true)
37
+ expect(host.querySelector('[data-testid="kind-view"]')?.textContent).toBe('Custom Pod')
38
+ expect(host.querySelectorAll('table')).toHaveLength(0)
39
+ expect(host.textContent).toContain('Workloads')
40
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', bubbles: true })))
41
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
42
+ expect(selected).not.toHaveBeenCalled()
43
+ expect(storageRead.mock.calls.some(([key]) => String(key).startsWith('radar-columns-'))).toBe(false)
44
+ expect(storageWrite.mock.calls.some(([key]) => String(key).startsWith('radar-columns-'))).toBe(false)
45
+ render(false)
46
+ expect(host.querySelector('[data-testid="kind-view"]')).toBeNull()
47
+ expect(host.querySelector('table')).not.toBeNull()
48
+ expect(storageRead.mock.calls.some(([key]) => key === 'radar-columns-pods')).toBe(true)
49
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', bubbles: true })))
50
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
51
+ expect(selected).toHaveBeenCalledWith(expect.objectContaining({ kind: 'pods', namespace: 'test', name: 'example' }))
52
+ render(undefined)
53
+ expect(host.querySelector('table')).not.toBeNull()
54
+ })
55
+ })
56
+
57
+ it('closes bulk dialogs when the same kind switches to a custom view and keeps the inspected resource', async () => {
58
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
59
+ const selected = vi.fn()
60
+ const remove = vi.fn()
61
+ const render = (custom: boolean) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
62
+ namespaces={['test']}
63
+ apiResources={[pods]}
64
+ resourceCounts={{ Pod: 1 }}
65
+ selectedKindQuery={{ isLoading: false, data: [pod], resourceName: 'pods', group: '' }}
66
+ selectedResource={{ kind: 'pods', group: '', namespace: 'test', name: 'example' }}
67
+ locationPathname="/resources/pods"
68
+ locationSearch=""
69
+ onNavigate={() => {}}
70
+ onResourceClick={selected}
71
+ onBulkDelete={remove}
72
+ renderKindView={() => custom ? <div data-testid="custom">Custom</div> : null}
73
+ /></KeyboardShortcutProvider>))
74
+ render(false)
75
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true })))
76
+ act(() => host.querySelector<HTMLInputElement>('input[title="Select all"]')!.click())
77
+ expect(host.textContent).toContain('1 selected')
78
+ act(() => [...host.querySelectorAll('button')].find(b => b.textContent?.trim() === 'Delete')!.click())
79
+ expect(document.querySelector('[role="dialog"]')?.textContent).toContain('Delete 1 Pod?')
80
+ render(true)
81
+ await act(async () => { await new Promise(resolve => setTimeout(resolve, 300)) })
82
+ expect(host.querySelector('[data-testid="custom"]')).not.toBeNull()
83
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
84
+ render(false)
85
+ expect(host.textContent).not.toContain('1 selected')
86
+ expect(host.querySelector<HTMLInputElement>('input[title="Select all"]')!.checked).toBe(false)
87
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
88
+ act(() => host.querySelector<SVGElement>('button .lucide-git-compare')!.closest<HTMLButtonElement>('button')!.click())
89
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'j', bubbles: true })))
90
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })))
91
+ expect(host.querySelector('[aria-label="Compare resources tray"]')?.textContent).toContain('example')
92
+ render(true)
93
+ render(false)
94
+ expect(host.querySelector('[aria-label="Compare resources tray"]')).toBeNull()
95
+ expect(remove).not.toHaveBeenCalled()
96
+ expect(selected).not.toHaveBeenCalled()
97
+ })
98
+
99
+ it('keeps sidebar kind shortcuts active while a custom view is shown', () => {
100
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
101
+ const deployments: APIResource = { group: 'apps', version: 'v1', kind: 'Deployment', name: 'deployments', namespaced: true, isCrd: false, verbs: ['get', 'list'] }
102
+ act(() => root.render(<KeyboardShortcutProvider><ResourcesView
103
+ namespaces={['test']}
104
+ apiResources={[pods, deployments]}
105
+ resourceCounts={{ Pod: 1, Deployment: 1 }}
106
+ locationPathname="/resources/pods"
107
+ locationSearch=""
108
+ onNavigate={() => {}}
109
+ renderKindView={kind => <div data-testid="kind-view">Custom {kind.kind}</div>}
110
+ /></KeyboardShortcutProvider>))
111
+ expect(host.querySelector('[data-testid="kind-view"]')?.textContent).toBe('Custom Pod')
112
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: ']', bubbles: true })))
113
+ expect(host.querySelector('[data-testid="kind-view"]')?.textContent).not.toBe('Custom Pod')
114
+ act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: '[', bubbles: true })))
115
+ expect(host.querySelector('[data-testid="kind-view"]')?.textContent).toBe('Custom Pod')
116
+ })
117
+
118
+ it('skips hidden-table sort and filter aggregation for a loaded list, then resumes them with the table', () => {
119
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
120
+ const getSortValue = vi.fn((resource: typeof pod) => resource.metadata.name)
121
+ const getFilterValue = vi.fn((resource: typeof pod) => resource.metadata.name)
122
+ const extraColumns = [{ key: 'fixture-rank', label: 'Fixture rank', render: () => null, getSortValue, getFilterValue }]
123
+ const list = [pod, { ...pod, metadata: { ...pod.metadata, name: 'another', uid: 'another-id' } }]
124
+ localStorage.setItem('radar-sort-pods', JSON.stringify({ column: 'fixture-rank', direction: 'asc' }))
125
+ const render = (custom: boolean, data = list) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
126
+ namespaces={['test']}
127
+ apiResources={[pods]}
128
+ resourceCounts={{ Pod: data.length }}
129
+ selectedKindQuery={{ isLoading: false, data, resourceName: 'pods', group: '' }}
130
+ locationPathname="/resources/pods"
131
+ locationSearch=""
132
+ onNavigate={() => {}}
133
+ extraLeadingColumns={extraColumns}
134
+ renderKindView={() => custom ? <div data-testid="kind-view">Custom</div> : null}
135
+ /></KeyboardShortcutProvider>))
136
+ render(true)
137
+ expect(getSortValue).not.toHaveBeenCalled()
138
+ expect(getFilterValue).not.toHaveBeenCalled()
139
+ render(false)
140
+ expect(getSortValue).toHaveBeenCalled()
141
+ expect(getFilterValue).toHaveBeenCalled()
142
+ getSortValue.mockClear(); getFilterValue.mockClear()
143
+ render(true, [...list])
144
+ expect(getSortValue).not.toHaveBeenCalled()
145
+ expect(getFilterValue).not.toHaveBeenCalled()
146
+ render(false)
147
+ expect(getSortValue).toHaveBeenCalled()
148
+ expect(getFilterValue).toHaveBeenCalled()
149
+ })
150
+
151
+ const deployments: APIResource = { group: 'apps', version: 'v1', kind: 'Deployment', name: 'deployments', namespaced: true, isCrd: false, verbs: ['get', 'list'] }
152
+ const widgets: APIResource = { group: 'example.com', version: 'v1', kind: 'Widget', name: 'widgets', namespaced: true, isCrd: true, verbs: ['get', 'list'] }
153
+ const checkboxKey = 'radar-resource-checkboxes'
154
+ const key = (value: string) => act(() => document.body.dispatchEvent(new KeyboardEvent('keydown', { key: value, bubbles: true })))
155
+ const required = <T extends Element>(selector: string, parent: ParentNode = host): T => {
156
+ const element = parent.querySelector<T>(selector)
157
+ if (!element) throw new Error(`Missing element: ${selector}`)
158
+ return element
159
+ }
160
+ const clickButton = (label: string, parent: ParentNode = host) => {
161
+ const button = [...parent.querySelectorAll('button')].find(candidate => candidate.textContent?.trim() === label)
162
+ if (!button) throw new Error(`Missing button: ${label}`)
163
+ act(() => button.click())
164
+ }
165
+ const renderBulkTable = (initialKind = pods, bulkEnabled = true) => {
166
+ const selected = vi.fn()
167
+ const remove = vi.fn<NonNullable<ComponentProps<typeof ResourcesView>['onBulkDelete']>>()
168
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
169
+ const render = (kind = initialKind, items = [pod]) => act(() => root.render(<KeyboardShortcutProvider><ResourcesView
170
+ namespaces={['test']}
171
+ apiResources={[pods, deployments, widgets]}
172
+ resourceCounts={{ [kind.kind]: items.length }}
173
+ selectedKindQuery={{ isLoading: false, data: items, resourceName: kind.name, group: kind.group }}
174
+ locationPathname={`/resources/${kind.name}`}
175
+ locationSearch={kind.group ? `?apiGroup=${kind.group}` : ''}
176
+ onNavigate={() => {}}
177
+ onResourceClick={selected}
178
+ onBulkDelete={bulkEnabled ? remove : undefined}
179
+ /></KeyboardShortcutProvider>))
180
+ render()
181
+ return { selected, remove, render }
182
+ }
183
+
184
+ it.each([pods, deployments, widgets])('hides selection by default on $kind and enables it without an empty action bar', kind => {
185
+ renderBulkTable(kind)
186
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
187
+ act(() => required<HTMLButtonElement>('button[aria-label="Show selection checkboxes"]').click())
188
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
189
+ expect(host.textContent).not.toContain('0 selected')
190
+ expect(localStorage.getItem(checkboxKey)).toBe('true')
191
+ })
192
+
193
+ it('remembers both visibility choices across remounts without remembering selected rows', () => {
194
+ renderBulkTable()
195
+ key('b')
196
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
197
+ expect(host.textContent).toContain('1 selected')
198
+ act(() => root.unmount()); host.remove()
199
+ renderBulkTable(deployments)
200
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
201
+ expect(host.textContent).not.toContain('1 selected')
202
+ key('b')
203
+ expect(localStorage.getItem(checkboxKey)).toBe('false')
204
+ act(() => root.unmount()); host.remove()
205
+ renderBulkTable()
206
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
207
+ })
208
+
209
+ it('keeps keyboard activation for inspection while checkboxes select', () => {
210
+ const { selected } = renderBulkTable()
211
+ key('b')
212
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
213
+ expect(selected).not.toHaveBeenCalled()
214
+ expect(host.textContent).toContain('1 selected')
215
+ key('j'); key('Enter')
216
+ expect(selected).toHaveBeenCalledTimes(1)
217
+ expect(host.textContent).toContain('1 selected')
218
+ key('Escape')
219
+ expect(host.textContent).not.toContain('1 selected')
220
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
221
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
222
+ clickButton('Clear selection')
223
+ expect(host.textContent).not.toContain('1 selected')
224
+ expect(localStorage.getItem(checkboxKey)).toBe('true')
225
+ })
226
+
227
+ it('temporarily hides checkboxes for compare and clears selection on kind changes', () => {
228
+ const { render } = renderBulkTable()
229
+ key('b')
230
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
231
+ act(() => required<HTMLButtonElement>('button:has(.lucide-git-compare)').click())
232
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
233
+ key('Escape')
234
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
235
+ expect(host.textContent).not.toContain('1 selected')
236
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
237
+ render(deployments)
238
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
239
+ expect(host.textContent).not.toContain('1 selected')
240
+ render(widgets)
241
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
242
+ expect(localStorage.getItem(checkboxKey)).toBe('true')
243
+ })
244
+
245
+ it('can show selection from compare without turning off the saved preference', () => {
246
+ localStorage.setItem(checkboxKey, 'true')
247
+ renderBulkTable()
248
+ act(() => required<HTMLButtonElement>('button:has(.lucide-git-compare)').click())
249
+ key('b')
250
+ expect(host.querySelector('[aria-label="Compare resources tray"]')).toBeNull()
251
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
252
+ expect(localStorage.getItem(checkboxKey)).toBe('true')
253
+ })
254
+
255
+ it('keeps delete confirmation and clears successful selection without hiding checkboxes', () => {
256
+ const { remove } = renderBulkTable()
257
+ key('b')
258
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
259
+ clickButton('Delete')
260
+ const dialog = required<HTMLElement>('[role="dialog"]', document)
261
+ expect(dialog.textContent).toContain('Delete 1 Pod?')
262
+ expect(remove).not.toHaveBeenCalled()
263
+ clickButton('Delete 1 resource', dialog)
264
+ expect(remove).toHaveBeenCalledWith([{ kind: 'pods', group: '', namespace: 'test', name: 'example' }], expect.objectContaining({ force: false }))
265
+ act(() => remove.mock.calls[0]?.[1]?.onSuccess?.())
266
+ expect(host.textContent).not.toContain('1 selected')
267
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
268
+ expect(localStorage.getItem(checkboxKey)).toBe('true')
269
+ })
270
+
271
+ it.each(['false', 'garbage', 'null', '1'])('ignores invalid or disabled visibility value %s', value => {
272
+ localStorage.setItem(checkboxKey, value)
273
+ renderBulkTable()
274
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
275
+ })
276
+
277
+ it('keeps toggling usable when browser storage is unavailable', () => {
278
+ vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('Storage unavailable') })
279
+ vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('Storage unavailable') })
280
+ renderBulkTable()
281
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
282
+ key('b')
283
+ expect(host.querySelector('input[title="Select all"]')).not.toBeNull()
284
+ key('b')
285
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
286
+ })
287
+
288
+ it('does not offer selection without a bulk action callback even with saved visibility', () => {
289
+ localStorage.setItem(checkboxKey, 'true')
290
+ renderBulkTable(pods, false)
291
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
292
+ expect(host.querySelector('button .lucide-list-checks')).toBeNull()
293
+ key('b')
294
+ expect(host.querySelector('input[title="Select all"]')).toBeNull()
295
+ expect(localStorage.getItem(checkboxKey)).toBe('true')
296
+ })
@@ -1,6 +1,7 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
1
2
  import { describe, expect, it } from 'vitest'
2
3
  import type { ContainerResourceMetrics } from '../../types'
3
- import { podAggregate, readContainer } from './ResourcesView'
4
+ import { buildContainerResourceTooltip, podAggregate, readContainer } from './ResourcesView'
4
5
 
5
6
  // Build a CPU-only container fixture; memory fields default to 0.
6
7
  function cpu(name: string, usage: number, request: number, limit: number): ContainerResourceMetrics {
@@ -109,3 +110,57 @@ describe('podAggregate', () => {
109
110
  expect(result.denom).toBe(384) // summed memory limit
110
111
  })
111
112
  })
113
+
114
+ describe('podAggregate with pod-level resources', () => {
115
+ it("treats a pod-level limit as the ceiling even when containers set none", () => {
116
+ const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 0)]
117
+ const result = podAggregate(containers, 'cpu', { request: 500, limit: 1000 })
118
+ expect(result.mode).toBe('limit')
119
+ expect(result.denom).toBe(1000)
120
+ expect(result.markerPct).toBeCloseTo(50)
121
+ })
122
+
123
+ it("uses the pod-level limit over a partial container sum", () => {
124
+ const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 200)]
125
+ const result = podAggregate(containers, 'cpu', { limit: 1000 })
126
+ expect(result.mode).toBe('limit')
127
+ expect(result.denom).toBe(1000)
128
+ expect(result.markerPct).toBeUndefined()
129
+ })
130
+
131
+ it("uses the container limits when they are tighter than the pod limit", () => {
132
+ const single = podAggregate([cpu('app', 450, 100, 500)], 'cpu', { request: 800, limit: 1000 })
133
+ expect(single.mode).toBe('limit')
134
+ expect(single.denom).toBe(500)
135
+ const both = podAggregate([cpu('app', 200, 0, 300), cpu('sidecar', 50, 0, 300)], 'cpu', { limit: 1000 })
136
+ expect(both.denom).toBe(600)
137
+ })
138
+
139
+ it("uses a pod-level request as the yardstick when nothing is limited", () => {
140
+ const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 0)]
141
+ const result = podAggregate(containers, 'cpu', { request: 800 })
142
+ expect(result.mode).toBe('request')
143
+ expect(result.denom).toBe(800)
144
+ })
145
+ })
146
+
147
+ describe('buildContainerResourceTooltip with a pod-level budget', () => {
148
+ const fmt = (n: number) => `${n}m`
149
+ const containers = [cpu('app', 300, 0, 0), cpu('sidecar', 100, 0, 0)]
150
+
151
+ it('shows the shared pod budget instead of calling covered containers unlimited', () => {
152
+ const html = renderToStaticMarkup(
153
+ buildContainerResourceTooltip('CPU', containers, 'cpu', fmt, undefined, { usage: 400, limit: 1000, request: 500 }),
154
+ )
155
+ expect(html).not.toContain('no limit')
156
+ expect(html).toContain('pod limit')
157
+ expect(html).toContain('Pod (shared)')
158
+ expect(html).toContain('400m · 40% · 1000m limit · req 500m')
159
+ })
160
+
161
+ it('keeps "no limit" when there is no pod-level budget', () => {
162
+ const html = renderToStaticMarkup(buildContainerResourceTooltip('CPU', containers, 'cpu', fmt))
163
+ expect(html).toContain('no limit')
164
+ expect(html).not.toContain('Pod (shared)')
165
+ })
166
+ })