@skyhook-io/k8s-ui 1.18.1 → 1.18.2

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 (85) hide show
  1. package/README.md +8 -0
  2. package/package.json +1 -1
  3. package/src/components/applications/ApplicationsView.tsx +8 -7
  4. package/src/components/cluster-switcher/ClusterSwitcher.tsx +4 -0
  5. package/src/components/compare/ResourceCompareView.tsx +6 -2
  6. package/src/components/gitops/GitOpsTableView.tsx +13 -8
  7. package/src/components/issues/IssuesView.tsx +2 -2
  8. package/src/components/issues/diagnostic.ts +2 -0
  9. package/src/components/issues/severity.ts +1 -0
  10. package/src/components/logs/LogCore.tsx +13 -11
  11. package/src/components/logs/WorkloadLogsViewer.tsx +1 -1
  12. package/src/components/namespace-switcher/NamespacePicker.tsx +2 -0
  13. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +28 -0
  14. package/src/components/resources/ResourcesSidebar.tsx +26 -5
  15. package/src/components/resources/ResourcesView.crossplane-pause.test.tsx +133 -0
  16. package/src/components/resources/ResourcesView.flagger.test.tsx +31 -0
  17. package/src/components/resources/ResourcesView.kind-view.test.tsx +96 -1
  18. package/src/components/resources/ResourcesView.opentelemetry.test.tsx +29 -0
  19. package/src/components/resources/ResourcesView.tsx +238 -66
  20. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  21. package/src/components/resources/generic-status.ts +15 -0
  22. package/src/components/resources/printer-columns.test.ts +36 -0
  23. package/src/components/resources/printer-columns.ts +12 -1
  24. package/src/components/resources/readiness-gates.test.ts +95 -0
  25. package/src/components/resources/renderers/CompositeRenderer.tsx +8 -12
  26. package/src/components/resources/renderers/CrossplanePauseContext.test.tsx +153 -0
  27. package/src/components/resources/renderers/CrossplanePauseContext.tsx +39 -0
  28. package/src/components/resources/renderers/FlaggerRenderers.tsx +96 -0
  29. package/src/components/resources/renderers/GenericRenderer.tsx +19 -11
  30. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +5 -12
  31. package/src/components/resources/renderers/OpenTelemetryRenderers.tsx +73 -0
  32. package/src/components/resources/renderers/crossplane-cells.tsx +5 -5
  33. package/src/components/resources/renderers/index.ts +3 -0
  34. package/src/components/resources/resource-utils-crossplane.test.ts +45 -0
  35. package/src/components/resources/resource-utils-crossplane.ts +44 -8
  36. package/src/components/resources/resource-utils-flagger.test.tsx +82 -0
  37. package/src/components/resources/resource-utils-flagger.ts +76 -0
  38. package/src/components/resources/resource-utils-opentelemetry.test.tsx +74 -0
  39. package/src/components/resources/resource-utils-opentelemetry.ts +97 -0
  40. package/src/components/resources/resource-utils.ts +80 -2
  41. package/src/components/shared/CreateResourceDialog.test.tsx +14 -0
  42. package/src/components/shared/CreateResourceDialog.tsx +5 -0
  43. package/src/components/shared/DetailShell.phone.test.tsx +54 -0
  44. package/src/components/shared/DetailShell.tsx +14 -3
  45. package/src/components/shared/EditableYamlView.phone.test.tsx +44 -0
  46. package/src/components/shared/EditableYamlView.test.tsx +58 -0
  47. package/src/components/shared/EditableYamlView.tsx +18 -3
  48. package/src/components/shared/FilterRailPanel.test.tsx +39 -0
  49. package/src/components/shared/FilterRailPanel.tsx +62 -0
  50. package/src/components/shared/GitOpsWriteWarning.managers.test.tsx +85 -0
  51. package/src/components/shared/GitOpsWriteWarning.tsx +87 -5
  52. package/src/components/shared/ManagedByChip.manager.test.tsx +115 -0
  53. package/src/components/shared/ManagedByChip.menu.test.tsx +103 -0
  54. package/src/components/shared/ManagedByChip.tsx +135 -2
  55. package/src/components/shared/ResourceActionsBar.tsx +22 -6
  56. package/src/components/shared/ResourceRendererDispatch.tsx +23 -0
  57. package/src/components/shared/SetImageDialog.tsx +3 -0
  58. package/src/components/topology/TopologyControls.tsx +1 -1
  59. package/src/components/ui/CrossplaneOperationNotice.test.tsx +153 -0
  60. package/src/components/ui/CrossplaneOperationNotice.tsx +139 -0
  61. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +55 -6
  62. package/src/components/ui/ForceDeleteConfirmDialog.tsx +127 -22
  63. package/src/components/ui/PageHeader.tsx +3 -2
  64. package/src/components/ui/SummaryTile.tsx +13 -3
  65. package/src/components/ui/Tooltip.touch.test.tsx +55 -0
  66. package/src/components/ui/Tooltip.tsx +11 -2
  67. package/src/components/ui/YamlReview.test.tsx +52 -0
  68. package/src/components/ui/YamlReview.tsx +7 -0
  69. package/src/components/ui/drawer-components.test.tsx +27 -0
  70. package/src/components/ui/drawer-components.tsx +22 -42
  71. package/src/components/workload/WorkloadView.relationships.test.tsx +40 -0
  72. package/src/components/workload/WorkloadView.tsx +57 -34
  73. package/src/hooks/useKeepInViewport.mounted.test.tsx +72 -0
  74. package/src/hooks/useKeepInViewport.test.ts +27 -0
  75. package/src/hooks/useKeepInViewport.ts +87 -0
  76. package/src/types/core.ts +53 -1
  77. package/src/utils/api-resources.ts +1 -0
  78. package/src/utils/gitops-owner.ts +136 -8
  79. package/src/utils/gitops-write-guard.test.ts +332 -0
  80. package/src/utils/gitops-write-guard.ts +175 -11
  81. package/src/utils/index.ts +1 -0
  82. package/src/utils/read-actions.ts +34 -0
  83. package/src/utils/touch-intent.test.ts +39 -0
  84. package/src/utils/touch-intent.ts +35 -0
  85. package/vitest.config.ts +26 -0
@@ -3,6 +3,7 @@ import { act, type ComponentProps } from 'react'
3
3
  import { createRoot } from 'react-dom/client'
4
4
  import { afterEach, describe, expect, it, vi } from 'vitest'
5
5
  import { ResourcesView } from './ResourcesView'
6
+ import { ResourceActionsBar } from '../shared/ResourceActionsBar'
6
7
  import { KeyboardShortcutProvider } from '../../hooks/useKeyboardShortcuts'
7
8
  import type { APIResource } from '../../types'
8
9
 
@@ -252,7 +253,7 @@ it('can show selection from compare without turning off the saved preference', (
252
253
  expect(localStorage.getItem(checkboxKey)).toBe('true')
253
254
  })
254
255
 
255
- it('keeps delete confirmation and clears successful selection without hiding checkboxes', () => {
256
+ it('confirms bulk deletion and clears successful selection without hiding checkboxes', () => {
256
257
  const { remove } = renderBulkTable()
257
258
  key('b')
258
259
  act(() => required<HTMLInputElement>('input[title="Select all"]').click())
@@ -268,6 +269,32 @@ it('keeps delete confirmation and clears successful selection without hiding che
268
269
  expect(localStorage.getItem(checkboxKey)).toBe('true')
269
270
  })
270
271
 
272
+ it('requires a new bulk confirmation after submission without clearing uncertain selection', async () => {
273
+ const { remove, render } = renderBulkTable()
274
+ const other = { ...pod, metadata: { ...pod.metadata, name: 'remaining', uid: 'remaining-id' } }
275
+ render(pods, [pod, other])
276
+ key('b')
277
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
278
+ clickButton('Delete')
279
+ const dialog = required<HTMLElement>('[role="dialog"]', document)
280
+ clickButton('Delete 2 resources', dialog)
281
+ expect(remove).toHaveBeenCalledTimes(1)
282
+ expect(remove.mock.calls[0]?.[0]).toHaveLength(2)
283
+ expect(dialog.closest('[inert]')).not.toBeNull()
284
+ expect(host.textContent).toContain('2 selected')
285
+
286
+ render(pods, [other])
287
+ await act(async () => { await new Promise(resolve => setTimeout(resolve, 300)) })
288
+ expect(document.querySelector('[role="dialog"]')).toBeNull()
289
+ expect(host.textContent).toContain('1 selected')
290
+ expect(remove).toHaveBeenCalledTimes(1)
291
+ clickButton('Delete')
292
+ const nextDialog = required<HTMLElement>('[role="dialog"]', document)
293
+ expect(nextDialog.textContent).toContain('Delete 1 Pod?')
294
+ clickButton('Delete 1 resource', nextDialog)
295
+ expect(remove.mock.calls[1]?.[0]).toEqual([{ kind: 'pods', group: '', namespace: 'test', name: 'remaining' }])
296
+ })
297
+
271
298
  it.each(['false', 'garbage', 'null', '1'])('ignores invalid or disabled visibility value %s', value => {
272
299
  localStorage.setItem(checkboxKey, value)
273
300
  renderBulkTable()
@@ -294,3 +321,71 @@ it('does not offer selection without a bulk action callback even with saved visi
294
321
  expect(host.querySelector('input[title="Select all"]')).toBeNull()
295
322
  expect(localStorage.getItem(checkboxKey)).toBe('true')
296
323
  })
324
+
325
+ it('closes single-delete confirmation on submission but keeps the inspected resource until known success', () => {
326
+ const remove = vi.fn<NonNullable<ComponentProps<typeof ResourceActionsBar>['onDelete']>>()
327
+ const close = vi.fn()
328
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
329
+ act(() => root.render(<ResourceActionsBar
330
+ resource={{ kind: 'pods', namespace: 'test', name: 'example' }}
331
+ data={pod}
332
+ onDelete={remove}
333
+ onClose={close}
334
+ />))
335
+ act(() => required<SVGElement>('svg[class*="trash"]').closest<HTMLButtonElement>('button')!.click())
336
+ const dialog = required<HTMLElement>('[role="dialog"]', document)
337
+ clickButton('Delete', dialog)
338
+ expect(remove).toHaveBeenCalledTimes(1)
339
+ expect(dialog.closest('[inert]')).not.toBeNull()
340
+ expect(close).not.toHaveBeenCalled()
341
+ act(() => remove.mock.calls[0]?.[1]?.onSuccess?.())
342
+ expect(close).toHaveBeenCalledTimes(1)
343
+ })
344
+
345
+ it('preserves rows selected after a bulk delete was submitted', () => {
346
+ const { remove, render } = renderBulkTable()
347
+ key('b')
348
+ act(() => required<HTMLInputElement>('input[title="Select all"]').click())
349
+ clickButton('Delete')
350
+ clickButton('Delete 1 resource', required<HTMLElement>('[role="dialog"]', document))
351
+ const other = { ...pod, metadata: { ...pod.metadata, name: 'remaining', uid: 'remaining-id' } }
352
+ render(pods, [pod, other])
353
+ act(() => required<HTMLInputElement>('input[title="Deselect all"]').click())
354
+ expect(host.textContent).toContain('2 selected')
355
+ act(() => remove.mock.calls[0]?.[1]?.onSuccess?.())
356
+ expect(host.textContent).toContain('1 selected')
357
+ render(pods, [other])
358
+ expect(host.textContent).toContain('1 selected')
359
+ })
360
+
361
+ it.each(['name', 'uid'])('does not close a newly inspected resource after an earlier delete succeeds (%s)', change => {
362
+ const remove = vi.fn<NonNullable<ComponentProps<typeof ResourceActionsBar>['onDelete']>>()
363
+ const close = vi.fn()
364
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
365
+ const render = (name: string, uid: string, deleting = false) => act(() => root.render(<ResourceActionsBar
366
+ resource={{ kind: 'pods', namespace: 'test', name }}
367
+ data={{ ...pod, metadata: { ...pod.metadata, name, uid } }}
368
+ onDelete={remove}
369
+ onClose={close}
370
+ isDeleting={deleting}
371
+ />))
372
+ render('example', 'original')
373
+ act(() => required<HTMLButtonElement>('button[aria-label="Delete resource"]').click())
374
+ clickButton('Delete', required<HTMLElement>('[role="dialog"]', document))
375
+ render(change === 'name' ? 'other' : 'example', 'replacement', true)
376
+ expect(required<HTMLButtonElement>('button[aria-label="Deleting resource"]').disabled).toBe(true)
377
+ act(() => remove.mock.calls[0]?.[1]?.onSuccess?.())
378
+ expect(close).not.toHaveBeenCalled()
379
+ })
380
+
381
+ it('ignores a delete success callback after its action bar is unmounted', () => {
382
+ const remove = vi.fn<NonNullable<ComponentProps<typeof ResourceActionsBar>['onDelete']>>()
383
+ const close = vi.fn()
384
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
385
+ act(() => root.render(<ResourceActionsBar resource={{ kind: 'pods', namespace: 'test', name: 'example' }} data={pod} onDelete={remove} onClose={close} />))
386
+ act(() => required<HTMLButtonElement>('button[aria-label="Delete resource"]').click())
387
+ clickButton('Delete', required<HTMLElement>('[role="dialog"]', document))
388
+ act(() => root.render(<div>Another view</div>))
389
+ act(() => remove.mock.calls[0]?.[1]?.onSuccess?.())
390
+ expect(close).not.toHaveBeenCalled()
391
+ })
@@ -0,0 +1,29 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { VirtuosoMockContext } from 'react-virtuoso'
5
+ import { afterEach, expect, it, vi } from 'vitest'
6
+ import { ResourcesView } from './ResourcesView'
7
+ import { KeyboardShortcutProvider } from '../../hooks/useKeyboardShortcuts'
8
+ import type { APIResource } from '../../types'
9
+
10
+ ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
11
+ window.matchMedia = vi.fn().mockReturnValue({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })
12
+ Element.prototype.scrollIntoView = vi.fn()
13
+ let root: ReturnType<typeof createRoot>
14
+ let host: HTMLDivElement
15
+ afterEach(() => { act(() => root?.unmount()); host?.remove(); localStorage.clear() })
16
+
17
+ it('shows reconciliation and labeled Pod snapshots in the actual collector table', async () => {
18
+ host = document.createElement('div'); document.body.append(host); root = createRoot(host)
19
+ const kind: APIResource = { group: 'opentelemetry.io', version: 'v1beta1', kind: 'OpenTelemetryCollector', name: 'opentelemetrycollectors', namespaced: true, isCrd: true, verbs: ['get', 'list'] }
20
+ const data = { apiVersion: 'opentelemetry.io/v1beta1', kind: 'OpenTelemetryCollector', metadata: { name: 'collector', namespace: 'test' }, spec: { mode: 'deployment', managementState: 'managed' }, status: { conditions: [{ type: 'Ready', status: 'False', reason: 'ReconcileError' }], scale: { statusReplicas: '0/1' }, version: '0.162.0' } }
21
+ await act(async () => root.render(<VirtuosoMockContext.Provider value={{ viewportHeight: 600, itemHeight: 48 }}><KeyboardShortcutProvider><ResourcesView namespaces={['test']} apiResources={[kind]} resourceCounts={{ OpenTelemetryCollector: 1 }} selectedKindQuery={{ isLoading: false, data: [data], resourceName: kind.name, group: kind.group }} locationPathname="/resources/opentelemetrycollectors" locationSearch="?apiGroup=opentelemetry.io" onNavigate={() => {}} /></KeyboardShortcutProvider></VirtuosoMockContext.Provider>))
22
+ const table = host.querySelector('table')!
23
+ expect(table).not.toBeNull()
24
+ expect(table.textContent).toContain('Reconciliation')
25
+ expect(table.textContent).toContain('Reconcile failed')
26
+ expect(table.textContent).toContain('Reported Pods')
27
+ expect(table.textContent).toContain('0/1')
28
+ expect(table.textContent).toContain('0.162.0')
29
+ })