@skyhook-io/k8s-ui 1.8.9 → 1.8.11

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 (130) hide show
  1. package/package.json +5 -4
  2. package/src/assets/gitops/argocd.png +0 -0
  3. package/src/assets/gitops/flux.svg +7 -0
  4. package/src/components/applications/AppChips.tsx +10 -0
  5. package/src/components/applications/ApplicationDetail.test.tsx +569 -0
  6. package/src/components/applications/ApplicationDetail.tsx +2517 -450
  7. package/src/components/applications/ApplicationsList.tsx +4 -1
  8. package/src/components/applications/ApplicationsView.tsx +101 -18
  9. package/src/components/applications/applications-view.test.tsx +47 -0
  10. package/src/components/applications/index.ts +1 -1
  11. package/src/components/audit/AuditAlerts.tsx +14 -10
  12. package/src/components/checks/ChecksView.tsx +81 -51
  13. package/src/components/checks/severity.ts +39 -45
  14. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  15. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +14 -59
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
  21. package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
  22. package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
  23. package/src/components/gitops/insights/insights-helpers.ts +69 -1
  24. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
  25. package/src/components/issues/IssuesView.tsx +219 -143
  26. package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
  27. package/src/components/issues/ResourceIssuesSection.tsx +28 -128
  28. package/src/components/issues/index.ts +3 -2
  29. package/src/components/issues/issue-timing.ts +53 -0
  30. package/src/components/issues/issues.test.ts +146 -4
  31. package/src/components/issues/severity.ts +29 -32
  32. package/src/components/logs/LogCore.tsx +14 -4
  33. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  34. package/src/components/logs/useLogStream.ts +8 -1
  35. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  36. package/src/components/resources/ResourcesSidebar.test.tsx +107 -0
  37. package/src/components/resources/ResourcesSidebar.tsx +70 -52
  38. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  39. package/src/components/resources/ResourcesView.tsx +243 -53
  40. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  41. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  42. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  43. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
  44. package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
  45. package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
  46. package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
  47. package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
  48. package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
  49. package/src/components/resources/renderers/PodRenderer.tsx +81 -70
  50. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  51. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  52. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  53. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  54. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  55. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  56. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  57. package/src/components/resources/renderers/index.ts +1 -0
  58. package/src/components/resources/resource-utils-capi.ts +1 -1
  59. package/src/components/resources/resource-utils.ts +31 -0
  60. package/src/components/shared/DetailShell.tsx +2 -2
  61. package/src/components/shared/ResourceActionsBar.tsx +140 -108
  62. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  63. package/src/components/timeline/TimelineList.tsx +371 -354
  64. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  65. package/src/components/timeline/TimelineStrip.tsx +836 -0
  66. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  67. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  68. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  69. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  70. package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
  71. package/src/components/timeline/index.ts +23 -0
  72. package/src/components/timeline/scrubber-math.test.ts +235 -0
  73. package/src/components/timeline/scrubber-math.ts +249 -0
  74. package/src/components/timeline/shared.test.tsx +219 -0
  75. package/src/components/timeline/shared.tsx +232 -447
  76. package/src/components/timeline/timeline-filters.test.ts +196 -0
  77. package/src/components/timeline/timeline-filters.ts +180 -0
  78. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  79. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  80. package/src/components/timeline/timeline-live.test.ts +131 -0
  81. package/src/components/timeline/timeline-live.ts +77 -0
  82. package/src/components/topology/GroupNode.tsx +9 -2
  83. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  84. package/src/components/topology/K8sResourceNode.tsx +387 -243
  85. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  86. package/src/components/topology/TopologyGraph.tsx +89 -16
  87. package/src/components/topology/TopologySearch.tsx +2 -2
  88. package/src/components/topology/layout.ts +10 -2
  89. package/src/components/topology/topology.css +0 -92
  90. package/src/components/ui/Badge.tsx +5 -0
  91. package/src/components/ui/BoardSkeleton.tsx +47 -0
  92. package/src/components/ui/CardSection.tsx +117 -0
  93. package/src/components/ui/CodeViewer.tsx +2 -2
  94. package/src/components/ui/Collapse.tsx +59 -0
  95. package/src/components/ui/ConfirmDialog.tsx +7 -3
  96. package/src/components/ui/DialogPortal.tsx +31 -9
  97. package/src/components/ui/Input.tsx +24 -0
  98. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  99. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  100. package/src/components/ui/PaneLoader.tsx +10 -2
  101. package/src/components/ui/ResourceBar.tsx +68 -25
  102. package/src/components/ui/SearchBox.tsx +18 -4
  103. package/src/components/ui/SearchPillInput.tsx +2 -2
  104. package/src/components/ui/Toast.tsx +1 -1
  105. package/src/components/ui/drawer-components.tsx +39 -26
  106. package/src/components/ui/index.ts +6 -0
  107. package/src/components/ui/severity-tone.ts +63 -0
  108. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  109. package/src/components/workload/WorkloadView.tsx +2682 -472
  110. package/src/components/workload/index.ts +1 -1
  111. package/src/theme/variables.css +15 -1
  112. package/src/types/core.ts +57 -4
  113. package/src/utils/api-resources.test.ts +54 -0
  114. package/src/utils/api-resources.ts +68 -1
  115. package/src/utils/application-topology.test.ts +198 -0
  116. package/src/utils/application-topology.ts +261 -0
  117. package/src/utils/applications.test.ts +167 -1
  118. package/src/utils/applications.ts +1157 -393
  119. package/src/utils/index.ts +2 -0
  120. package/src/utils/navigation.test.ts +32 -1
  121. package/src/utils/navigation.ts +70 -0
  122. package/src/utils/resource-hierarchy.test.ts +990 -1
  123. package/src/utils/resource-hierarchy.ts +905 -267
  124. package/src/utils/resource-icons.ts +2 -0
  125. package/src/utils/topology-neighborhood.test.ts +202 -1
  126. package/src/utils/topology-neighborhood.ts +194 -13
  127. package/src/utils/workflow-execution.test.ts +556 -0
  128. package/src/utils/workflow-execution.ts +814 -0
  129. package/src/utils/workload-colors.ts +2 -3
  130. package/tsconfig.json +2 -2
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ buildHealthSpans,
4
+ sweepAggregateHealth,
5
+ formatAggregateHealthTooltip,
6
+ buildLaneMemberSpans,
7
+ type MemberHealthSpans,
8
+ type AggregateHealthSegment,
9
+ } from './shared'
10
+ import type { ResourceLane } from '../../utils/resource-hierarchy'
11
+
12
+ describe('buildHealthSpans own-resource existence (child cleanup must not kill the lane)', () => {
13
+ const t = (min: number) => new Date(Date.UTC(2026, 0, 1, 10, min)).toISOString()
14
+ const start = Date.UTC(2026, 0, 1, 10, 0)
15
+ const now = Date.UTC(2026, 0, 1, 11, 0)
16
+ const cron = { kind: 'CronJob', namespace: 'team-a', name: 'aliaser' }
17
+ const ev = (over: Record<string, unknown>) => ({
18
+ id: String(Math.random()), source: 'informer', eventType: 'update',
19
+ kind: 'CronJob', namespace: 'team-a', name: 'aliaser',
20
+ timestamp: t(5), healthState: 'idle', ...over,
21
+ }) as any
22
+
23
+ it('a child Job delete does not truncate the parent lane existence', () => {
24
+ const events = [
25
+ ev({ timestamp: t(1) }),
26
+ ev({ kind: 'Job', name: 'aliaser-123', eventType: 'delete', timestamp: t(10), healthState: undefined }),
27
+ ev({ timestamp: t(20) }),
28
+ ]
29
+ const { spans } = buildHealthSpans(events, start, now, events, cron)
30
+ // strip must reach `now`, not stop at the child delete
31
+ expect(spans[spans.length - 1].end).toBe(now)
32
+ })
33
+
34
+ it('the lane resource own delete still ends its existence', () => {
35
+ const events = [
36
+ ev({ timestamp: t(1) }),
37
+ ev({ eventType: 'delete', timestamp: t(30), healthState: undefined }),
38
+ ]
39
+ const { spans } = buildHealthSpans(events, start, now, events, cron)
40
+ expect(spans[spans.length - 1].end).toBe(Date.UTC(2026, 0, 1, 10, 30))
41
+ })
42
+ })
43
+
44
+ describe('buildHealthSpans birth clamp (no pre-birth back-fill)', () => {
45
+ const start = Date.UTC(2026, 0, 1, 10, 0)
46
+ const now = Date.UTC(2026, 0, 1, 12, 40)
47
+ const pod = { kind: 'Pod', namespace: 'team-a', name: 'runner-abc12' }
48
+ const ev = (min: number, over: Record<string, unknown>) => ({
49
+ id: `${min}-${JSON.stringify(over).length}`, source: 'informer', kind: 'Pod', namespace: 'team-a', name: 'runner-abc12',
50
+ timestamp: new Date(Date.UTC(2026, 0, 1, 12, min)).toISOString(), eventType: 'update', ...over,
51
+ }) as any
52
+
53
+ it('a pod whose first event is its creation gets no span before birth', () => {
54
+ const events = [
55
+ ev(25, { reason: 'created', healthState: 'unknown' }),
56
+ ev(25, { eventType: 'add', healthState: 'healthy' }),
57
+ ev(30, { healthState: 'neutral' }),
58
+ ]
59
+ const { spans } = buildHealthSpans(events, start, now, events, pod)
60
+ expect(spans[0].start).toBe(Date.UTC(2026, 0, 1, 12, 25))
61
+ })
62
+
63
+ it('a resource first seen via a later mutation keeps the existed-before assumption', () => {
64
+ const events = [ev(25, { healthState: 'healthy' })]
65
+ const { spans } = buildHealthSpans(events, start, now, events, pod)
66
+ expect(spans[0].start).toBe(start)
67
+ })
68
+ })
69
+
70
+ // --- state-family aggregate health sweep (item 3) ---------------------------
71
+
72
+ const member = (name: string, ...spans: [number, number, string][]): MemberHealthSpans => ({
73
+ name,
74
+ spans: spans.map(([start, end, health]) => ({ start, end, health })),
75
+ })
76
+ // A member healthy across the whole [0,100] window.
77
+ const full = (name: string, health: string): MemberHealthSpans => member(name, [0, 100, health])
78
+
79
+ describe('sweepAggregateHealth: state families', () => {
80
+ it('uniform OK (all healthy) → one healthy segment, not mixed', () => {
81
+ const segs = sweepAggregateHealth([full('a', 'healthy'), full('b', 'healthy')], 0, 100)
82
+ expect(segs).toHaveLength(1)
83
+ expect(segs[0]).toMatchObject({ health: 'healthy', mixed: false, total: 2 })
84
+ expect(segs[0].byState).toEqual({ healthy: ['a', 'b'] })
85
+ })
86
+
87
+ it('uniform BAD (degraded + unhealthy) → one BAD family segment painting the most severe (unhealthy)', () => {
88
+ const segs = sweepAggregateHealth([full('a', 'degraded'), full('b', 'unhealthy')], 0, 100)
89
+ expect(segs).toHaveLength(1)
90
+ expect(segs[0]).toMatchObject({ health: 'unhealthy', mixed: false, total: 2 })
91
+ })
92
+
93
+ it('benign mix (healthy + idle) stays ONE family (OK) → NOT mixed [founder key case]', () => {
94
+ const segs = sweepAggregateHealth([full('a', 'healthy'), full('b', 'idle')], 0, 100)
95
+ expect(segs).toHaveLength(1)
96
+ expect(segs[0].mixed).toBe(false)
97
+ // Tie between healthy and idle → healthy.
98
+ expect(segs[0].health).toBe('healthy')
99
+ })
100
+
101
+ it('OK majority wins (2 idle vs 1 healthy → idle), no tie', () => {
102
+ const segs = sweepAggregateHealth([full('a', 'idle'), full('b', 'idle'), full('c', 'healthy')], 0, 100)
103
+ expect(segs[0]).toMatchObject({ health: 'idle', mixed: false })
104
+ })
105
+
106
+ it('true mix (healthy + unhealthy) → MIXED', () => {
107
+ const segs = sweepAggregateHealth([full('a', 'healthy'), full('b', 'unhealthy')], 0, 100)
108
+ expect(segs).toHaveLength(1)
109
+ expect(segs[0]).toMatchObject({ health: 'mixed', mixed: true })
110
+ })
111
+
112
+ it('rolling + healthy → MIXED (rolling is its own family)', () => {
113
+ const segs = sweepAggregateHealth([full('a', 'rolling'), full('b', 'healthy')], 0, 100)
114
+ expect(segs[0]).toMatchObject({ health: 'mixed', mixed: true })
115
+ })
116
+
117
+ it('uniform rolling → one rolling segment, not mixed', () => {
118
+ const segs = sweepAggregateHealth([full('a', 'rolling'), full('b', 'rolling')], 0, 100)
119
+ expect(segs[0]).toMatchObject({ health: 'rolling', mixed: false })
120
+ })
121
+ })
122
+
123
+ describe('sweepAggregateHealth: slice boundaries + attribution', () => {
124
+ it('splits at a member transition, painting each slice from that slice state set', () => {
125
+ // a healthy throughout; b healthy [0,60] then unhealthy [60,100].
126
+ const segs = sweepAggregateHealth(
127
+ [full('a', 'healthy'), member('b', [0, 60, 'healthy'], [60, 100, 'unhealthy'])],
128
+ 0, 100,
129
+ )
130
+ expect(segs).toHaveLength(2)
131
+ expect(segs[0]).toMatchObject({ start: 0, end: 60, health: 'healthy', mixed: false })
132
+ expect(segs[1]).toMatchObject({ start: 60, end: 100, health: 'mixed', mixed: true })
133
+ })
134
+
135
+ it('merges adjacent slices with identical state + attribution', () => {
136
+ // Boundary at 50 exists in the data but both slices resolve identically → merge.
137
+ const segs = sweepAggregateHealth(
138
+ [member('a', [0, 50, 'healthy'], [50, 100, 'healthy'])],
139
+ 0, 100,
140
+ )
141
+ expect(segs).toHaveLength(1)
142
+ expect(segs[0]).toMatchObject({ start: 0, end: 100, health: 'healthy' })
143
+ })
144
+
145
+ it('paints nothing where no member is alive (gap slice omitted)', () => {
146
+ const segs = sweepAggregateHealth([member('a', [0, 40, 'healthy'], [70, 100, 'healthy'])], 0, 100)
147
+ // Two live slices, the [40,70] gap is dropped.
148
+ expect(segs.map((s) => [s.start, s.end])).toEqual([[0, 40], [70, 100]])
149
+ })
150
+
151
+ it('records per-state member names for attribution', () => {
152
+ const segs = sweepAggregateHealth(
153
+ [full('u1', 'unhealthy'), full('h1', 'healthy'), full('h2', 'healthy')],
154
+ 0, 100,
155
+ )
156
+ expect(segs[0].byState).toEqual({ unhealthy: ['u1'], healthy: ['h1', 'h2'] })
157
+ expect(segs[0].total).toBe(3)
158
+ })
159
+ })
160
+
161
+ describe('formatAggregateHealthTooltip', () => {
162
+ const seg = (over: Partial<AggregateHealthSegment>): AggregateHealthSegment => ({
163
+ start: 0, end: 100, health: 'healthy', mixed: false, total: 1, byState: { healthy: ['a'] }, ...over,
164
+ })
165
+
166
+ it('uniform → "healthy · 23/23"', () => {
167
+ const names = Array.from({ length: 23 }, (_, i) => `p${i}`)
168
+ expect(formatAggregateHealthTooltip(seg({ total: 23, byState: { healthy: names } })))
169
+ .toBe('healthy · 23/23')
170
+ })
171
+
172
+ it('mixed → "mixed · 2/23 unhealthy: <names> · 21 healthy/idle"', () => {
173
+ const healthy = Array.from({ length: 21 }, (_, i) => `h${i}`)
174
+ const s = seg({ health: 'mixed', mixed: true, total: 23, byState: { unhealthy: ['name-a', 'name-b'], healthy } })
175
+ expect(formatAggregateHealthTooltip(s)).toBe('mixed · 2/23 unhealthy: name-a, name-b · 21 healthy/idle')
176
+ })
177
+
178
+ it('caps callout names at 3 + "+N more"', () => {
179
+ const s = seg({ health: 'mixed', mixed: true, total: 6, byState: { unhealthy: ['u1', 'u2', 'u3', 'u4', 'u5'], healthy: ['h1'] } })
180
+ expect(formatAggregateHealthTooltip(s)).toBe('mixed · 5/6 unhealthy: u1, u2, u3, +2 more · 1 healthy/idle')
181
+ })
182
+
183
+ it('rolling callout in a mixed slice is labelled rolling', () => {
184
+ const s = seg({ health: 'mixed', mixed: true, total: 3, byState: { rolling: ['r1'], healthy: ['h1', 'h2'] } })
185
+ expect(formatAggregateHealthTooltip(s)).toBe('mixed · 1/3 rolling: r1 · 2 healthy/idle')
186
+ })
187
+ })
188
+
189
+ describe('buildLaneMemberSpans (leaf gathering)', () => {
190
+ const win = { start: Date.UTC(2026, 0, 1, 10, 0), now: Date.UTC(2026, 0, 1, 11, 0) }
191
+ const ev = (kind: string, name: string, health: string): any => ({
192
+ id: `${name}-add`, source: 'informer', kind, namespace: 'ops', name,
193
+ timestamp: new Date(Date.UTC(2026, 0, 1, 10, 10)).toISOString(), eventType: 'add', healthState: health,
194
+ })
195
+ const leaf = (kind: string, name: string, health: string): ResourceLane => ({
196
+ id: `${kind}/ops/${name}`, kind, namespace: 'ops', name, isWorkload: true, events: [ev(kind, name, health)], children: [],
197
+ })
198
+
199
+ it('returns one member per LEAF resource (grandchildren, not intermediate nodes)', () => {
200
+ const job: ResourceLane = {
201
+ id: 'Job/ops/j', kind: 'Job', namespace: 'ops', name: 'j', isWorkload: true, events: [],
202
+ children: [leaf('Pod', 'j-abc12', 'healthy'), leaf('Pod', 'j-def34', 'unhealthy')],
203
+ }
204
+ const cron: ResourceLane = {
205
+ id: 'CronJob/ops/backup', kind: 'CronJob', namespace: 'ops', name: 'backup', isWorkload: true, events: [], children: [job],
206
+ }
207
+ const members = buildLaneMemberSpans(cron, win.start, win.now)
208
+ expect(members.map((m) => m.name).sort()).toEqual(['j-abc12', 'j-def34'])
209
+ // Feeding these into the sweep yields a MIXED strip (healthy pod vs unhealthy pod).
210
+ const segs = sweepAggregateHealth(members, win.start, win.now)
211
+ expect(segs.some((s) => s.mixed)).toBe(true)
212
+ })
213
+
214
+ it('a childless lane is its own single leaf member', () => {
215
+ const dep = leaf('Deployment', 'web', 'healthy')
216
+ const members = buildLaneMemberSpans(dep, win.start, win.now)
217
+ expect(members.map((m) => m.name)).toEqual(['web'])
218
+ })
219
+ })