@skyhook-io/k8s-ui 1.18.0 → 1.18.1

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 (63) hide show
  1. package/README.md +13 -0
  2. package/package.json +1 -1
  3. package/src/components/compare/CompareResourcePicker.tsx +18 -4
  4. package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
  5. package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
  6. package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
  7. package/src/components/gitops/GitOpsTableView.tsx +33 -4
  8. package/src/components/gitops/detail-helpers.test.ts +58 -13
  9. package/src/components/gitops/detail-helpers.ts +37 -6
  10. package/src/components/gitops/index.ts +1 -0
  11. package/src/components/issues/IssuesView.tsx +14 -9
  12. package/src/components/logs/LogsViewer.test.tsx +104 -59
  13. package/src/components/logs/LogsViewer.tsx +28 -8
  14. package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
  15. package/src/components/namespace-switcher/NamespacePicker.tsx +42 -8
  16. package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
  17. package/src/components/resources/ResourcesSidebar.tsx +36 -21
  18. package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
  19. package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
  20. package/src/components/resources/ResourcesView.tsx +106 -19
  21. package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
  22. package/src/components/resources/renderers/PodRenderer.tsx +32 -9
  23. package/src/components/resources/resource-utils-trivy.test.ts +87 -0
  24. package/src/components/resources/resource-utils-trivy.ts +122 -0
  25. package/src/components/shared/DetailShell.tsx +9 -7
  26. package/src/components/shared/ManagedByChip.test.tsx +22 -0
  27. package/src/components/shared/ManagedByChip.tsx +24 -4
  28. package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
  29. package/src/components/shared/ResourceActionsBar.tsx +9 -3
  30. package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
  31. package/src/components/shared/ResourceRendererDispatch.tsx +46 -31
  32. package/src/components/timeline/TimelineList.tsx +5 -1
  33. package/src/components/ui/FetchResult.test.tsx +19 -2
  34. package/src/components/ui/FetchResult.tsx +13 -1
  35. package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
  36. package/src/components/ui/MultiSelectPicker.tsx +20 -2
  37. package/src/components/ui/PageHeader.tsx +6 -4
  38. package/src/components/ui/Toast.tsx +33 -26
  39. package/src/components/ui/drawer-components.test.tsx +253 -0
  40. package/src/components/ui/drawer-components.tsx +315 -11
  41. package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
  42. package/src/components/workload/WorkloadView.relationships.test.tsx +33 -0
  43. package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
  44. package/src/components/workload/WorkloadView.tsx +54 -6
  45. package/src/hooks/index.ts +2 -0
  46. package/src/hooks/useMediaQuery.ts +21 -0
  47. package/src/hooks/usePhoneLayout.ts +12 -0
  48. package/src/theme/tailwind-theme.css +5 -0
  49. package/src/types/core.ts +66 -2
  50. package/src/types/gitops-flux-inventory.test.ts +24 -0
  51. package/src/types/gitops.ts +5 -5
  52. package/src/types/index.ts +3 -0
  53. package/src/types/read-coverage.ts +65 -0
  54. package/src/types/resource-coverage.ts +40 -0
  55. package/src/utils/format.test.ts +7 -0
  56. package/src/utils/format.ts +6 -1
  57. package/src/utils/gitops-owner.test.ts +42 -1
  58. package/src/utils/gitops-owner.ts +22 -0
  59. package/src/utils/index.ts +2 -0
  60. package/src/utils/read-actions.test.ts +22 -0
  61. package/src/utils/read-actions.ts +56 -0
  62. package/src/utils/resource-coverage.test.ts +121 -0
  63. package/src/utils/resource-coverage.ts +117 -0
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { renderToString } from 'react-dom/server'
3
3
  import { AlertBanner, EventsSection, ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection, defaultConditionTone } from './drawer-components'
4
+ import type { Relationships } from '../../types'
4
5
 
5
6
  const problems = [
6
7
  { color: 'red' as const, message: 'Application is Degraded' },
@@ -42,6 +43,244 @@ describe('ProblemAlerts', () => {
42
43
  })
43
44
  })
44
45
 
46
+ function RelationshipsWithTruncatedChildren() {
47
+ return <RelatedResourcesSection relationships={{ children: [{ kind: 'Pod', namespace: 'shop', name: 'web-0' }], ownership: { children: { complete: false, truncated: true } } }} />
48
+ }
49
+
50
+ describe('RelatedResourcesSection ownership', () => {
51
+ const rs = { kind: 'ReplicaSet', group: 'apps', namespace: 'shop', name: 'web-7d9f8' }
52
+ const cm = { kind: 'ConfigMap', namespace: 'shop', name: 'web-config' }
53
+ const app = { kind: 'Application', group: 'argoproj.io', namespace: 'argocd', name: 'shop' }
54
+
55
+ it('names further owners apart from the owner', () => {
56
+ const html = renderToString(<RelatedResourcesSection relationships={{ owner: rs, owners: [rs, cm] }} onNavigate={() => {}} />)
57
+ expect(html).toContain('Also Owned By')
58
+ expect(html).toContain('web-config')
59
+ expect(html.match(/web-7d9f8/g)).toHaveLength(1)
60
+ })
61
+
62
+ it('says how Managed By differs from Owner', () => {
63
+ const html = renderToString(<RelatedResourcesSection relationships={{ managers: [app] }} onNavigate={() => {}} />)
64
+ expect(html).toContain("Kubernetes&#x27; garbage collector follows owner references, not these links.")
65
+ })
66
+
67
+ it('puts a coverage hint on a button a keyboard can reach', () => {
68
+ const html = renderToString(
69
+ <RelatedResourcesSection relationships={{ children: [rs], ownership: { children: { complete: false, notChecked: [{ reason: 'read_error' }] } } }} />,
70
+ )
71
+ expect(html).toMatch(/<button type="button"[^>]*aria-label="Radar&#x27;s API discovery missed some API groups/)
72
+ })
73
+
74
+ it('shows management apart from ownership', () => {
75
+ const html = renderToString(<RelatedResourcesSection relationships={{ managers: [app], manages: [rs] }} onNavigate={() => {}} />)
76
+ expect(html).toContain('Managed By')
77
+ expect(html).toContain('Manages')
78
+ expect(html).not.toContain('Owner')
79
+ expect(html).not.toContain('Children')
80
+ })
81
+
82
+ it('says which kinds a children list did not check', () => {
83
+ const html = renderToString(
84
+ <RelatedResourcesSection
85
+ relationships={{
86
+ children: [rs],
87
+ ownership: { children: { complete: false, notChecked: [{ group: 'apps', kind: 'ControllerRevision', namespace: 'shop', reason: 'radar_kind_not_collected' }, { group: 'discovery.k8s.io', kind: 'EndpointSlice', namespace: 'shop', reason: 'radar_kind_not_collected' }] } },
88
+ }}
89
+ />,
90
+ )
91
+ expect(html).toContain('Radar doesn&#x27;t collect ControllerRevision or EndpointSlice objects, so none of those are listed.')
92
+ expect(html).not.toContain('More children than shown')
93
+ })
94
+
95
+ it('says when a children list was cut short', () => {
96
+ const html = renderToString(<RelationshipsWithTruncatedChildren />)
97
+ expect(html).toContain('More children than shown.')
98
+ })
99
+
100
+ it('adds nothing to a complete children list, or one from an older Radar', () => {
101
+ for (const relationships of [{ children: [rs], ownership: { children: { complete: true } } }, { children: [rs] }]) {
102
+ const html = renderToString(<RelatedResourcesSection relationships={relationships} />)
103
+ expect(html).toContain('Children')
104
+ expect(html).not.toContain('Not checked')
105
+ }
106
+ })
107
+
108
+ it('says why no owner is named', () => {
109
+ const cases: Array<[Relationships['ownership'], string]> = [
110
+ [{ primaryOwnerState: 'gone', ownerRef: rs, children: { complete: true } }, 'ReplicaSet web-7d9f8 no longer exists.'],
111
+ [{ primaryOwnerState: 'replaced', ownerRef: rs, children: { complete: true } }, 'ReplicaSet web-7d9f8 was deleted and recreated; this object names the earlier one.'],
112
+ [{ primaryOwnerState: 'unknown', ownerRef: rs, children: { complete: true } }, 'Couldn&#x27;t check ReplicaSet web-7d9f8.'],
113
+ [
114
+ { primaryOwnerState: 'unknown', ownerRef: { kind: 'Endpoints', namespace: 'shop', name: 'web' }, ownerReason: 'radar_kind_not_collected', children: { complete: true } },
115
+ 'Couldn&#x27;t check Endpoints web (Radar doesn&#x27;t collect it).',
116
+ ],
117
+ [{ primaryOwnerState: 'withheld', children: { complete: true } }, 'Not visible to you.'],
118
+ [{ primaryOwnerState: 'unknown', children: { complete: true } }, 'Couldn&#x27;t check the owner it names.'],
119
+ [{ primaryOwnerState: 'gone', ownerRef: { kind: '', namespace: '', name: '' }, children: { complete: true } }, 'The owner it names no longer exists.'],
120
+ [{ primaryOwnerState: 'unresolvable', children: { complete: true } }, 'Its owner reference can&#x27;t be resolved in this cluster.'],
121
+ [{ primaryOwnerState: 'excluded', children: { complete: true } }, 'Its owner reference names an Event'],
122
+ ]
123
+ for (const [ownership, text] of cases) {
124
+ const html = renderToString(<RelatedResourcesSection relationships={{ ownership }} />)
125
+ expect(html).toContain('Owner')
126
+ expect(html).toContain(text)
127
+ }
128
+ })
129
+
130
+ it('lists several owners without a controller as owners', () => {
131
+ const html = renderToString(<RelatedResourcesSection relationships={{ owners: [rs, cm], ownership: { primaryOwnerState: 'several', children: { complete: true } } }} />)
132
+ expect(html).toContain('<span>Owners<!-- --> (2)</span>')
133
+ expect(html).toContain('None of them is its controller.')
134
+ expect(html).not.toContain('<span>Owner</span>')
135
+ expect(html).not.toContain('Also Owned By')
136
+ })
137
+
138
+ it('lists other owners beside a missing controller as also owned by', () => {
139
+ const html = renderToString(<RelatedResourcesSection relationships={{ owners: [cm], ownership: { primaryOwnerState: 'gone', ownerRef: rs, children: { complete: true } } }} />)
140
+ expect(html).toContain('<span>Owner</span>')
141
+ expect(html).toContain('ReplicaSet web-7d9f8 no longer exists.')
142
+ expect(html).toContain('Also Owned By')
143
+ })
144
+
145
+ it('says why owner and children could not be read at all', () => {
146
+ expect(renderToString(<RelatedResourcesSection relationships={{ ownership: { unavailableReason: 'connection_changed', children: { complete: false } } }} />)).toContain(
147
+ 'Owner and children couldn&#x27;t be read: Radar&#x27;s connection changed meanwhile.',
148
+ )
149
+ expect(renderToString(<RelatedResourcesSection relationships={{ ownership: { unavailableReason: 'user_forbidden', children: { complete: false } } }} />)).toContain(
150
+ 'your access lets you open this object, but not list its kind here.',
151
+ )
152
+ for (const reason of ['object_not_found', 'read_error', 'cluster_disconnected']) {
153
+ const html = renderToString(<RelatedResourcesSection relationships={{ ownership: { unavailableReason: reason, children: { complete: false } } }} />)
154
+ expect(html).toContain('Owner and children couldn&#x27;t be read: ')
155
+ expect(html).not.toContain('read: couldn')
156
+ }
157
+ })
158
+
159
+ it('says outright when children could not be checked for an unusual reason, even with none found', () => {
160
+ const loading = renderToString(
161
+ <RelatedResourcesSection relationships={{ ownership: { children: { complete: false, notChecked: [{ kind: 'Pod', namespace: 'shop', reason: 'kind_sync_pending' }] } } }} />,
162
+ )
163
+ expect(loading).toContain('Children')
164
+ expect(loading).toContain('Couldn&#x27;t check Pod (still loading).')
165
+ const routine = renderToString(
166
+ <RelatedResourcesSection relationships={{ ownership: { children: { complete: false, notChecked: [{ kind: 'Lease', namespace: 'shop', reason: 'radar_kind_not_collected' }] } } }} />,
167
+ )
168
+ expect(routine).toBe('')
169
+ const leaf = renderToString(
170
+ <RelatedResourcesSection kind="configmaps" relationships={{ ownership: { children: { complete: false, notChecked: [{ kind: 'Lease', namespace: 'shop', reason: 'radar_kind_not_collected' }] } } }} />,
171
+ )
172
+ expect(leaf).toBe('')
173
+ const owner = renderToString(
174
+ <RelatedResourcesSection kind="services" relationships={{ ownership: { children: { complete: false, notChecked: [{ kind: 'EndpointSlice', namespace: 'shop', reason: 'radar_kind_not_collected' }] } } }} />,
175
+ )
176
+ expect(owner).toContain('Children')
177
+ expect(owner).toContain('No children among the kinds Radar checked.')
178
+ const ownerComplete = renderToString(<RelatedResourcesSection kind="services" relationships={{ ownership: { children: { complete: true } } }} />)
179
+ expect(ownerComplete).toBe('')
180
+ const elsewhere = renderToString(
181
+ <RelatedResourcesSection
182
+ relationships={{ children: [rs], ownership: { children: { complete: false, notChecked: [{ kind: 'Pod', namespace: 'other', reason: 'radar_namespace_not_collected' }] } } }}
183
+ />,
184
+ )
185
+ expect(elsewhere).toContain('Radar doesn&#x27;t collect every namespace, so children in the others aren&#x27;t listed.')
186
+ expect(elsewhere).not.toContain('Couldn&#x27;t check')
187
+ })
188
+
189
+ it('says why a GitOps manager\'s member list could not be read', () => {
190
+ const unread = renderToString(
191
+ <RelatedResourcesSection relationships={{ ownership: { children: { complete: true }, manages: { membership: 'unreadable', unavailableReason: 'kind_sync_pending' } } }} />,
192
+ )
193
+ expect(unread).toContain('Manages')
194
+ expect(unread).toContain('Couldn&#x27;t read the members its status records (still loading).')
195
+ const noList = renderToString(<RelatedResourcesSection relationships={{ manages: [rs], ownership: { children: { complete: true }, manages: { membership: 'unreadable' } } }} />)
196
+ expect(noList).not.toContain('Couldn&#x27;t read')
197
+ expect(noList).toContain('Its status has no member list Radar can read now, so these are the resources the topology links to it.')
198
+ const cut = renderToString(
199
+ <RelatedResourcesSection relationships={{ manages: [rs], ownership: { children: { complete: true }, manages: { membership: 'unreadable', truncated: true } } }} />,
200
+ )
201
+ expect(cut).toContain('More members than shown.')
202
+ const none = renderToString(<RelatedResourcesSection relationships={{ ownership: { children: { complete: true }, manages: { membership: 'unreadable' } } }} />)
203
+ expect(none).toContain('Its status has no member list Radar can read now.')
204
+ expect(none).not.toContain('these are the resources')
205
+ })
206
+
207
+ it('hints, without warning, at kinds API discovery missed', () => {
208
+ const html = renderToString(
209
+ <RelatedResourcesSection relationships={{ children: [rs], ownership: { children: { complete: false, notChecked: [{ reason: 'read_error' }] } } }} />,
210
+ )
211
+ expect(html).toContain('Radar&#x27;s API discovery missed some API groups, so children of their kinds aren&#x27;t listed.')
212
+ expect(html).not.toContain('Couldn&#x27;t check')
213
+ })
214
+
215
+ it("says a remote manager's members aren't listed", () => {
216
+ const html = renderToString(<RelatedResourcesSection relationships={{ ownership: { children: { complete: true }, manages: { membership: 'remote' } } }} />)
217
+ expect(html).toContain('Manages')
218
+ expect(html).toContain("Its destination isn&#x27;t this cluster as far as Radar can tell, so its members aren&#x27;t listed.")
219
+ })
220
+
221
+ it('says which direction Managed By comes from', () => {
222
+ const html = renderToString(<RelatedResourcesSection relationships={{ managers: [app] }} onNavigate={() => {}} />)
223
+ expect(html).toContain('Argo CD Applications or Flux objects whose status lists this resource, and other controllers that manage it without an owner reference.')
224
+ })
225
+
226
+ it("says outright what it couldn't check about this resource when it lists none", () => {
227
+ const html = renderToString(
228
+ <RelatedResourcesSection relationships={{ ownership: { children: { complete: true }, managers: { remote: 1, notChecked: [{ group: 'argoproj.io', kind: 'Application', reason: 'kind_sync_pending' }] } } }} />,
229
+ )
230
+ expect(html).toContain('Managed By')
231
+ expect(html).toContain('1 manager lists it for a cluster Radar can&#x27;t confirm is this one; couldn&#x27;t check Application (still loading).')
232
+ })
233
+
234
+ it("keeps it to the hint beside a listed manager", () => {
235
+ const html = renderToString(
236
+ <RelatedResourcesSection relationships={{ managers: [app], ownership: { children: { complete: true }, managers: { remote: 1 } } }} onNavigate={() => {}} />,
237
+ )
238
+ expect(html).toMatch(/aria-label="[^"]*1 manager lists it for a cluster Radar can&#x27;t confirm is this one/)
239
+ expect(html).not.toMatch(/>1 Manager lists it/)
240
+ })
241
+
242
+ it("keeps a manager kind discovery couldn't read to the hint, as it would show on every resource", () => {
243
+ const gaps = [{ group: 'argoproj.io', kind: 'Application', reason: 'read_error' }, { group: 'kustomize.toolkit.fluxcd.io', kind: 'Kustomization', reason: 'read_error' }]
244
+ const html = renderToString(<RelatedResourcesSection relationships={{ children: [rs], ownership: { children: { complete: true }, managers: { notChecked: gaps } } }} onNavigate={() => {}} />)
245
+ expect(html).not.toContain('Managed By')
246
+ const listed = renderToString(<RelatedResourcesSection relationships={{ managers: [app], ownership: { children: { complete: true }, managers: { notChecked: gaps } } }} onNavigate={() => {}} />)
247
+ expect(listed).toContain('Radar couldn&#x27;t search Application objects, so those managers aren&#x27;t checked.')
248
+ expect(listed).toContain('Radar couldn&#x27;t search Kustomization objects, so those managers aren&#x27;t checked.')
249
+ expect(listed).not.toContain('Radar couldn&#x27;t search Application objects, so those managers aren&#x27;t checked.</div><div>Radar couldn&#x27;t search Application')
250
+ })
251
+
252
+ it('keeps an unreadable list to the hint, since it may name any resource', () => {
253
+ const none = renderToString(<RelatedResourcesSection relationships={{ children: [rs], ownership: { children: { complete: true }, managers: { unreadable: 1 } } }} onNavigate={() => {}} />)
254
+ expect(none).not.toContain('Managed By')
255
+ const listed = renderToString(<RelatedResourcesSection relationships={{ managers: [app], ownership: { children: { complete: true }, managers: { unreadable: 2 } } }} onNavigate={() => {}} />)
256
+ expect(listed).toMatch(/aria-label="[^"]*2 managers&#x27; member lists can&#x27;t be read/)
257
+ })
258
+
259
+ it('says when more than one manager lists a resource', () => {
260
+ const other = { ...app, name: 'other' }
261
+ const html = renderToString(<RelatedResourcesSection relationships={{ managers: [app, other] }} onNavigate={() => {}} />)
262
+ expect(html).toContain('More than one manager lists or links to it.')
263
+ })
264
+
265
+ it('shows no Managed By when nothing lists the resource and the search was settled', () => {
266
+ const html = renderToString(<RelatedResourcesSection relationships={{ children: [rs], ownership: { children: { complete: true } } }} onNavigate={() => {}} />)
267
+ expect(html).not.toContain('Managed By')
268
+ })
269
+
270
+ it('says a recorded member list with nothing to show records none', () => {
271
+ const html = renderToString(<RelatedResourcesSection relationships={{ ownership: { children: { complete: true }, manages: { membership: 'recorded' } } }} />)
272
+ expect(html).toContain('Manages')
273
+ expect(html).toContain('Its status records no members to show.')
274
+ })
275
+
276
+ it('counts recorded members it does not list', () => {
277
+ const html = renderToString(
278
+ <RelatedResourcesSection relationships={{ manages: [rs], ownership: { children: { complete: true }, manages: { membership: 'recorded', missing: 1, withheld: 2 } } }} />,
279
+ )
280
+ expect(html).toContain('Its status also lists 1 not in the cluster, 2 you can&#x27;t view.')
281
+ })
282
+ })
283
+
45
284
  describe('RelatedResourcesSection', () => {
46
285
  it('renders an associated Node', () => {
47
286
  const html = renderToString(
@@ -236,3 +475,17 @@ describe('AlertBanner optional actions', () => {
236
475
  expect(html).toContain('flex-wrap'); if (message) expect(html).toContain(message)
237
476
  })
238
477
  })
478
+
479
+
480
+ describe('RelatedResourcesSection capped lists', () => {
481
+ it('marks a list the server cut short as a lower bound', () => {
482
+ const pods = Array.from({ length: 12 }, (_, i) => ({ kind: 'Pod', namespace: 'shop', name: `web-${i}` }))
483
+ const text = (relationships: Relationships) => renderToString(<RelatedResourcesSection relationships={relationships} onNavigate={() => {}} />).replace(/<!-- -->/g, '')
484
+ const capped = text({ children: pods, ownership: { children: { complete: false, truncated: true } } })
485
+ expect(capped).toContain('Children (12+)')
486
+ expect(capped).toContain('Show the 12 listed')
487
+ const whole = text({ children: pods, ownership: { children: { complete: true } } })
488
+ expect(whole).toContain('Children (12)')
489
+ expect(whole).toContain('Show all 12')
490
+ })
491
+ })