@skyhook-io/k8s-ui 1.8.10 → 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 (116) 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/ApplicationsView.tsx +62 -10
  8. package/src/components/applications/applications-view.test.tsx +47 -0
  9. package/src/components/applications/index.ts +1 -1
  10. package/src/components/audit/AuditAlerts.tsx +14 -10
  11. package/src/components/checks/ChecksView.tsx +2 -3
  12. package/src/components/checks/severity.ts +14 -10
  13. package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
  14. package/src/components/compare/CompareResourcePicker.tsx +2 -2
  15. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  16. package/src/components/gitops/GitOpsTableView.tsx +3 -3
  17. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  18. package/src/components/gitops/SyncCountdown.tsx +26 -17
  19. package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
  20. package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
  21. package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
  22. package/src/components/gitops/insights/insights-helpers.ts +69 -1
  23. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
  24. package/src/components/issues/IssuesView.tsx +51 -38
  25. package/src/components/issues/ResourceIssuesSection.test.tsx +6 -5
  26. package/src/components/issues/ResourceIssuesSection.tsx +28 -146
  27. package/src/components/issues/index.ts +3 -0
  28. package/src/components/issues/issue-timing.ts +53 -0
  29. package/src/components/issues/issues.test.ts +102 -1
  30. package/src/components/issues/severity.ts +2 -0
  31. package/src/components/logs/LogCore.tsx +14 -4
  32. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  33. package/src/components/logs/useLogStream.ts +8 -1
  34. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  35. package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
  36. package/src/components/resources/ResourcesSidebar.tsx +46 -46
  37. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  38. package/src/components/resources/ResourcesView.tsx +233 -51
  39. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  40. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  41. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  42. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  43. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  44. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  45. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  46. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  47. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  48. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  49. package/src/components/resources/renderers/index.ts +1 -0
  50. package/src/components/resources/resource-utils-capi.ts +1 -1
  51. package/src/components/resources/resource-utils.ts +31 -0
  52. package/src/components/shared/DetailShell.tsx +2 -2
  53. package/src/components/shared/ResourceActionsBar.tsx +140 -108
  54. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  55. package/src/components/timeline/TimelineList.tsx +371 -354
  56. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  57. package/src/components/timeline/TimelineStrip.tsx +836 -0
  58. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  59. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  60. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  61. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  62. package/src/components/timeline/TimelineToolbarState.test.tsx +64 -0
  63. package/src/components/timeline/index.ts +23 -0
  64. package/src/components/timeline/scrubber-math.test.ts +235 -0
  65. package/src/components/timeline/scrubber-math.ts +249 -0
  66. package/src/components/timeline/shared.test.tsx +219 -0
  67. package/src/components/timeline/shared.tsx +232 -447
  68. package/src/components/timeline/timeline-filters.test.ts +196 -0
  69. package/src/components/timeline/timeline-filters.ts +180 -0
  70. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  71. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  72. package/src/components/timeline/timeline-live.test.ts +131 -0
  73. package/src/components/timeline/timeline-live.ts +77 -0
  74. package/src/components/topology/GroupNode.tsx +9 -2
  75. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  76. package/src/components/topology/K8sResourceNode.tsx +387 -243
  77. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  78. package/src/components/topology/TopologyGraph.tsx +89 -16
  79. package/src/components/topology/TopologySearch.tsx +2 -2
  80. package/src/components/topology/layout.ts +10 -2
  81. package/src/components/topology/topology.css +0 -92
  82. package/src/components/ui/Badge.tsx +5 -0
  83. package/src/components/ui/CodeViewer.tsx +2 -2
  84. package/src/components/ui/Collapse.tsx +59 -0
  85. package/src/components/ui/ConfirmDialog.tsx +7 -3
  86. package/src/components/ui/DialogPortal.tsx +31 -9
  87. package/src/components/ui/Input.tsx +24 -0
  88. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  89. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  90. package/src/components/ui/PaneLoader.tsx +10 -2
  91. package/src/components/ui/ResourceBar.tsx +68 -25
  92. package/src/components/ui/SearchBox.tsx +18 -4
  93. package/src/components/ui/SearchPillInput.tsx +2 -2
  94. package/src/components/ui/drawer-components.tsx +39 -26
  95. package/src/components/ui/index.ts +4 -0
  96. package/src/components/ui/severity-tone.ts +11 -4
  97. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  98. package/src/components/workload/WorkloadView.tsx +2682 -472
  99. package/src/components/workload/index.ts +1 -1
  100. package/src/theme/variables.css +1 -1
  101. package/src/types/core.ts +57 -4
  102. package/src/utils/application-topology.test.ts +198 -0
  103. package/src/utils/application-topology.ts +261 -0
  104. package/src/utils/applications.test.ts +167 -1
  105. package/src/utils/applications.ts +1157 -393
  106. package/src/utils/index.ts +2 -0
  107. package/src/utils/navigation.test.ts +32 -1
  108. package/src/utils/navigation.ts +70 -0
  109. package/src/utils/resource-hierarchy.test.ts +990 -1
  110. package/src/utils/resource-hierarchy.ts +905 -267
  111. package/src/utils/resource-icons.ts +2 -0
  112. package/src/utils/topology-neighborhood.test.ts +202 -1
  113. package/src/utils/topology-neighborhood.ts +194 -13
  114. package/src/utils/workflow-execution.test.ts +556 -0
  115. package/src/utils/workflow-execution.ts +814 -0
  116. package/src/utils/workload-colors.ts +2 -3
@@ -1,5 +1,47 @@
1
1
  import { describe, it, expect } from 'vitest'
2
- import { compareVersions, appGroupingExplainer, APP_IDENTITY_ANNOTATION, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, worstHealth, type AppGroupFoldEntry } from './applications'
2
+ import { compareVersions, appGroupingExplainer, APP_IDENTITY_ANNOTATION, appGroupLagMessage, matchWorkloadAcrossInstances, foldAppGroups, identityEnvInferred, worstHealth, buildAppMembershipIndex, batchActivityForApp, batchRuntimeForApp, servingReadiness, type AppGroupFoldEntry, type AppRow, type AppWorkload } from './applications'
3
+
4
+ describe('batch application runtime', () => {
5
+ it('uses the latest retained outcome instead of historical failure count', () => {
6
+ const app: AppRow = {
7
+ key: 'batch', name: 'batch', health: 'unhealthy', workload_class: 'job',
8
+ workloads: [{
9
+ kind: 'CronJob', namespace: 'demo', name: 'nightly', workload_class: 'job', health: 'unhealthy', ready: 0, desired: 0, restarts: 0,
10
+ batch: { retainedRuns: 4, failedRuns: 1, succeededRuns: 3, latestRunName: 'nightly-new', latestRunPhase: 'Succeeded', latestFinishedAt: '2026-07-10T00:00:00Z' },
11
+ }],
12
+ }
13
+ expect(batchRuntimeForApp(app)).toMatchObject({ label: 'Succeeded', health: 'healthy' })
14
+ })
15
+
16
+ it('does not let one workload success mask another workload latest failure', () => {
17
+ const app: AppRow = {
18
+ key: 'batch', name: 'batch', health: 'healthy', workload_class: 'job',
19
+ workloads: [
20
+ {
21
+ kind: 'CronJob', namespace: 'demo', name: 'nightly', workload_class: 'job', health: 'healthy', ready: 0, desired: 0, restarts: 0,
22
+ batch: { retainedRuns: 1, succeededRuns: 1, latestRunName: 'nightly-new', latestRunPhase: 'Succeeded', latestFinishedAt: '2026-07-10T01:00:00Z' },
23
+ },
24
+ {
25
+ kind: 'CronWorkflow', namespace: 'demo', name: 'sync', workload_class: 'job', health: 'unhealthy', ready: 0, desired: 0, restarts: 0,
26
+ batch: { retainedRuns: 1, failedRuns: 1, latestRunName: 'sync-old', latestRunPhase: 'Failed', latestFinishedAt: '2026-07-10T00:00:00Z' },
27
+ },
28
+ ],
29
+ }
30
+
31
+ expect(batchRuntimeForApp(app)).toEqual({
32
+ label: 'Failed',
33
+ health: 'unhealthy',
34
+ detail: 'CronWorkflow/sync latest retained run sync-old failed.',
35
+ })
36
+ })
37
+
38
+ it('excludes batch workloads from serving readiness', () => {
39
+ expect(servingReadiness([
40
+ { kind: 'Deployment', namespace: 'demo', name: 'api', workload_class: 'service', health: 'healthy', ready: 2, desired: 3, restarts: 0 },
41
+ { kind: 'Job', namespace: 'demo', name: 'run', workload_class: 'job', health: 'healthy', ready: 1, desired: 1, restarts: 0, batch: { retainedRuns: 1 } },
42
+ ])).toEqual({ ready: 2, desired: 3 })
43
+ })
44
+ })
3
45
 
4
46
  describe('compareVersions', () => {
5
47
  it('orders semver', () => {
@@ -87,6 +129,46 @@ describe('identityEnvInferred', () => {
87
129
  })
88
130
  })
89
131
 
132
+ describe('batchActivityForApp', () => {
133
+ const workload = (kind: string, name: string, batch?: AppWorkload['batch']): AppWorkload => ({
134
+ kind,
135
+ namespace: 'prod',
136
+ name,
137
+ health: 'neutral',
138
+ ready: 0,
139
+ desired: 0,
140
+ restarts: 0,
141
+ workload_class: kind === 'Deployment' ? 'service' : 'job',
142
+ batch,
143
+ })
144
+ const app = (workloads: AppWorkload[]): AppRow => ({
145
+ key: 'billing',
146
+ name: 'billing',
147
+ health: 'neutral',
148
+ workloads,
149
+ })
150
+
151
+ it('ranks failed, active, suspended, and quiet batch workloads without inventing retained history', () => {
152
+ const activity = batchActivityForApp(app([
153
+ workload('Deployment', 'api'),
154
+ workload('CronJob', 'nightly-success', { retainedRuns: 2, latestRunPhase: 'Succeeded', latestRunName: 'nightly-success-1' }),
155
+ workload('CronWorkflow', 'hourly-suspended', { suspended: true, retainedRuns: 0, schedule: '0 * * * *' }),
156
+ workload('Job', 'active-reindex', { activeRuns: 1, retainedRuns: 1, latestRunPhase: 'Running' }),
157
+ workload('Workflow', 'failed-migration', { failedRuns: 1, retainedRuns: 1, latestRunPhase: 'Failed', latestRunName: 'failed-migration', message: 'pod crashed' }),
158
+ ]))
159
+
160
+ expect(activity.map((item) => item.workload.name)).toEqual([
161
+ 'failed-migration',
162
+ 'active-reindex',
163
+ 'hourly-suspended',
164
+ 'nightly-success',
165
+ ])
166
+ expect(activity[0]).toMatchObject({ tone: 'rose', label: 'Latest run failed', failedRuns: 1, detail: 'pod crashed' })
167
+ expect(activity[1]).toMatchObject({ tone: 'sky', label: '1 active run', activeRuns: 1 })
168
+ expect(activity[3]).toMatchObject({ tone: 'muted', label: 'Latest run succeeded', retainedRuns: 2 })
169
+ })
170
+ })
171
+
90
172
  // Position-preserving env switch: exact match, stem fallback (suffix, prefix,
91
173
  // discovered tokens), and the explicit no-counterpart null.
92
174
  describe('matchWorkloadAcrossInstances', () => {
@@ -296,3 +378,87 @@ describe('foldAppGroups pathKey disambiguation', () => {
296
378
  expect(rows.filter((r) => r.kind === 'instance').length).toBe(2)
297
379
  })
298
380
  })
381
+
382
+ describe('buildAppMembershipIndex', () => {
383
+ // Minimal AppRow builder — only the fields the index reads.
384
+ const row = (over: Partial<AppRow>): AppRow => ({
385
+ key: 'k', name: 'app', health: 'healthy', workloads: [], ...over,
386
+ })
387
+
388
+ it('indexes workloads by Kind/ns/name and satellites by the app namespace', () => {
389
+ const idx = buildAppMembershipIndex([
390
+ row({
391
+ key: 'team-a/app/billing', name: 'billing', namespace: 'team-a',
392
+ workloads: [{ kind: 'Deployment', namespace: 'team-a', name: 'billing-api', health: 'healthy', ready: 1, desired: 1, restarts: 0 }],
393
+ relationships: { services: ['billing-svc'], ingresses: ['billing-ing'] },
394
+ }),
395
+ ])
396
+ expect(idx.byResource.get('Deployment/team-a/billing-api')?.appName).toBe('billing')
397
+ expect(idx.byResource.get('Service/team-a/billing-svc')?.appName).toBe('billing')
398
+ expect(idx.byResource.get('Ingress/team-a/billing-ing')?.appName).toBe('billing')
399
+ })
400
+
401
+ it('indexes routes under their concrete kind, not a generic "Route"', () => {
402
+ // The server ships "Kind/name" for polymorphic routes; the index must key on
403
+ // the concrete kind so it matches the route lane id (HTTPRoute/…/name).
404
+ const idx = buildAppMembershipIndex([
405
+ row({
406
+ key: 'team-a/app/web', name: 'web', namespace: 'team-a',
407
+ relationships: { routes: ['HTTPRoute/web-http', 'GRPCRoute/web-grpc'] },
408
+ }),
409
+ ])
410
+ expect(idx.byResource.get('HTTPRoute/team-a/web-http')?.appName).toBe('web')
411
+ expect(idx.byResource.get('GRPCRoute/team-a/web-grpc')?.appName).toBe('web')
412
+ expect(idx.byResource.has('Route/team-a/HTTPRoute/web-http')).toBe(false)
413
+ })
414
+
415
+ it('skips satellites for a multi-namespace app (can not place them unambiguously)', () => {
416
+ const idx = buildAppMembershipIndex([
417
+ row({
418
+ key: 'a', name: 'span', namespaces: ['team-a', 'team-b'],
419
+ workloads: [{ kind: 'Deployment', namespace: 'team-a', name: 'api', health: 'healthy', ready: 1, desired: 1, restarts: 0 }],
420
+ relationships: { services: ['svc'] },
421
+ }),
422
+ ])
423
+ expect(idx.byResource.has('Deployment/team-a/api')).toBe(true)
424
+ expect([...idx.byResource.keys()].some((k) => k.startsWith('Service/'))).toBe(false)
425
+ })
426
+
427
+ it('indexes matchKeys by evidence but excludes name-stem (exact kinds only in v1)', () => {
428
+ // matchKeys are namespace-scoped (kind:namespace:value); name-stem stays unscoped.
429
+ const idx = buildAppMembershipIndex([
430
+ row({ key: 'a', name: 'billing', identity: { key: 'billing', env: 'prod', confidence: 'high', evidence: 'x' }, matchKeys: ['instance:team-a:billing-prod', 'helm:team-a:billing-v2', 'name-stem:billing'] }),
431
+ ])
432
+ expect(idx.byEvidence.get('instance:team-a:billing-prod')?.appName).toBe('billing')
433
+ expect(idx.byEvidence.get('helm:team-a:billing-v2')?.appName).toBe('billing')
434
+ expect(idx.byEvidence.has('name-stem:billing')).toBe(false)
435
+ })
436
+
437
+ it('carries env + evidence onto the membership', () => {
438
+ const idx = buildAppMembershipIndex([
439
+ row({ key: 'a', name: 'billing', identity: { key: 'billing', env: 'prod', confidence: 'high', evidence: 'name stem billing' }, matchKeys: ['name:team-a:billing'] }),
440
+ ])
441
+ const m = idx.byEvidence.get('name:team-a:billing')
442
+ expect(m?.env).toBe('prod')
443
+ expect(m?.evidence).toBe('name stem billing')
444
+ })
445
+
446
+ it('collision resolves first-wins (server-sorted row order)', () => {
447
+ const idx = buildAppMembershipIndex([
448
+ row({ key: 'first', name: 'first', matchKeys: ['name:team-a:shared'] }),
449
+ row({ key: 'second', name: 'second', matchKeys: ['name:team-a:shared'] }),
450
+ ])
451
+ expect(idx.byEvidence.get('name:team-a:shared')?.appName).toBe('first')
452
+ })
453
+
454
+ it('same label value in two namespaces indexes as distinct evidence keys', () => {
455
+ // The whole point of namespace scoping: "shared" in team-a and team-b must
456
+ // NOT collapse to one key — each namespace maps to its own app.
457
+ const idx = buildAppMembershipIndex([
458
+ row({ key: 'a', name: 'app-a', matchKeys: ['instance:team-a:shared'] }),
459
+ row({ key: 'b', name: 'app-b', matchKeys: ['instance:team-b:shared'] }),
460
+ ])
461
+ expect(idx.byEvidence.get('instance:team-a:shared')?.appName).toBe('app-a')
462
+ expect(idx.byEvidence.get('instance:team-b:shared')?.appName).toBe('app-b')
463
+ })
464
+ })