@skyhook-io/k8s-ui 1.8.10 → 1.8.12

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 (168) hide show
  1. package/package.json +7 -6
  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 +697 -0
  6. package/src/components/applications/ApplicationDetail.tsx +3269 -474
  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/GitOpsDetailLayout.tsx +13 -3
  16. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
  17. package/src/components/gitops/GitOpsTableView.tsx +13 -6
  18. package/src/components/gitops/ManagedResourcesList.tsx +8 -11
  19. package/src/components/gitops/SyncCountdown.tsx +26 -17
  20. package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
  21. package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
  22. package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
  23. package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
  24. package/src/components/gitops/insights/index.ts +1 -0
  25. package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
  26. package/src/components/gitops/insights/insights-helpers.ts +74 -1
  27. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
  28. package/src/components/issues/IssuesView.tsx +91 -52
  29. package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
  30. package/src/components/issues/ResourceIssuesSection.tsx +46 -144
  31. package/src/components/issues/index.ts +3 -0
  32. package/src/components/issues/issue-timing.ts +53 -0
  33. package/src/components/issues/issues.test.ts +102 -1
  34. package/src/components/issues/severity.ts +3 -0
  35. package/src/components/logs/LogCore.tsx +14 -4
  36. package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
  37. package/src/components/logs/useLogStream.ts +8 -1
  38. package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
  39. package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
  40. package/src/components/resources/ResourcesSidebar.tsx +46 -46
  41. package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
  42. package/src/components/resources/ResourcesView.tsx +233 -51
  43. package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
  44. package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
  45. package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
  46. package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
  47. package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
  48. package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
  49. package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
  50. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  51. package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
  52. package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
  53. package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
  54. package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
  55. package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
  56. package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
  57. package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
  58. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
  59. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
  60. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
  61. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
  62. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
  63. package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
  64. package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
  65. package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
  66. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
  67. package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
  68. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
  69. package/src/components/resources/renderers/JobRenderer.tsx +3 -1
  70. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
  71. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
  72. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
  73. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
  74. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
  75. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
  76. package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
  77. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
  78. package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
  79. package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
  80. package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
  81. package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
  82. package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
  83. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
  84. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
  85. package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
  86. package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
  87. package/src/components/resources/renderers/index.ts +1 -0
  88. package/src/components/resources/resource-utils-argo.test.ts +18 -1
  89. package/src/components/resources/resource-utils-argo.ts +8 -0
  90. package/src/components/resources/resource-utils-capi.ts +1 -1
  91. package/src/components/resources/resource-utils.ts +31 -0
  92. package/src/components/shared/DetailShell.tsx +2 -2
  93. package/src/components/shared/ResourceActionsBar.tsx +142 -111
  94. package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
  95. package/src/components/shared/UnifiedDiff.tsx +51 -0
  96. package/src/components/shared/index.ts +1 -0
  97. package/src/components/timeline/TimelineList.tsx +375 -354
  98. package/src/components/timeline/TimelineStrip.test.tsx +194 -0
  99. package/src/components/timeline/TimelineStrip.tsx +836 -0
  100. package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
  101. package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
  102. package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
  103. package/src/components/timeline/TimelineToolbar.tsx +759 -0
  104. package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
  105. package/src/components/timeline/index.ts +23 -0
  106. package/src/components/timeline/scrubber-math.test.ts +235 -0
  107. package/src/components/timeline/scrubber-math.ts +249 -0
  108. package/src/components/timeline/shared.test.tsx +219 -0
  109. package/src/components/timeline/shared.tsx +232 -447
  110. package/src/components/timeline/timeline-filters.test.ts +196 -0
  111. package/src/components/timeline/timeline-filters.ts +180 -0
  112. package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
  113. package/src/components/timeline/timeline-lane-sort.ts +77 -0
  114. package/src/components/timeline/timeline-live.test.ts +131 -0
  115. package/src/components/timeline/timeline-live.ts +77 -0
  116. package/src/components/topology/GroupNode.tsx +9 -2
  117. package/src/components/topology/K8sResourceNode.test.ts +11 -0
  118. package/src/components/topology/K8sResourceNode.tsx +387 -243
  119. package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
  120. package/src/components/topology/TopologyControls.tsx +3 -1
  121. package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
  122. package/src/components/topology/TopologyGraph.tsx +149 -90
  123. package/src/components/topology/TopologyOverlayBar.tsx +30 -0
  124. package/src/components/topology/TopologySearch.tsx +25 -17
  125. package/src/components/topology/index.ts +2 -0
  126. package/src/components/topology/layout.ts +10 -2
  127. package/src/components/topology/topology.css +0 -92
  128. package/src/components/ui/Badge.tsx +6 -1
  129. package/src/components/ui/CodeViewer.tsx +2 -2
  130. package/src/components/ui/Collapse.tsx +59 -0
  131. package/src/components/ui/ConfirmDialog.tsx +7 -3
  132. package/src/components/ui/DialogPortal.tsx +31 -9
  133. package/src/components/ui/Input.tsx +24 -0
  134. package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
  135. package/src/components/ui/MultiSelectPicker.tsx +167 -0
  136. package/src/components/ui/PaneLoader.tsx +10 -2
  137. package/src/components/ui/ResourceBar.tsx +68 -25
  138. package/src/components/ui/SearchBox.tsx +18 -4
  139. package/src/components/ui/SearchPillInput.tsx +2 -2
  140. package/src/components/ui/SelectMenu.tsx +81 -0
  141. package/src/components/ui/drawer-components.tsx +48 -32
  142. package/src/components/ui/index.ts +6 -0
  143. package/src/components/ui/severity-tone.ts +11 -4
  144. package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
  145. package/src/components/workload/WorkloadView.tsx +2706 -475
  146. package/src/components/workload/index.ts +1 -1
  147. package/src/theme/tailwind-theme.css +1 -0
  148. package/src/theme/variables.css +2 -1
  149. package/src/types/core.ts +57 -4
  150. package/src/types/gitops-insights.ts +69 -1
  151. package/src/types/gitops.ts +3 -3
  152. package/src/utils/application-history.test.ts +538 -0
  153. package/src/utils/application-history.ts +257 -0
  154. package/src/utils/application-topology.test.ts +198 -0
  155. package/src/utils/application-topology.ts +261 -0
  156. package/src/utils/applications.test.ts +201 -1
  157. package/src/utils/applications.ts +1182 -393
  158. package/src/utils/index.ts +3 -0
  159. package/src/utils/navigation.test.ts +32 -1
  160. package/src/utils/navigation.ts +70 -0
  161. package/src/utils/resource-hierarchy.test.ts +1024 -1
  162. package/src/utils/resource-hierarchy.ts +925 -267
  163. package/src/utils/resource-icons.ts +2 -0
  164. package/src/utils/topology-neighborhood.test.ts +202 -1
  165. package/src/utils/topology-neighborhood.ts +194 -13
  166. package/src/utils/workflow-execution.test.ts +556 -0
  167. package/src/utils/workflow-execution.ts +814 -0
  168. package/src/utils/workload-colors.ts +2 -3
@@ -17,6 +17,7 @@ import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
17
17
  import { pluralize } from '../../utils/pluralize'
18
18
  import {
19
19
  type AppEntry,
20
+ type AppRow,
20
21
  type AppHealth,
21
22
  type AppWorkloadClass,
22
23
  type AppSource,
@@ -36,12 +37,34 @@ import {
36
37
  isSystemNamespace,
37
38
  searchTextForEntry,
38
39
  foldAppGroups,
40
+ batchSignalForApp,
41
+ batchRuntimeForApp,
39
42
  type FoldedRow,
40
43
  } from '../../utils/applications'
41
44
  import { ReadyBar } from './ReadyBar'
42
- import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo } from './AppChips'
45
+ import { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo, BatchSignalChip } from './AppChips'
43
46
  import { AppIdentityTooltip, EnvHint } from './AppTooltips'
44
47
 
48
+ function ApplicationRuntimeCell({ apps, workloadClass, ready, desired }: { apps: AppRow[]; workloadClass: AppWorkloadClass; ready: number; desired: number }) {
49
+ if (workloadClass !== 'job') return <ReadyBar ready={ready} desired={desired} />
50
+ const runtimes = apps.map(batchRuntimeForApp)
51
+ const worst = runtimes.reduce((current, runtime) => (HEALTH_RANK[runtime.health] ?? 0) > (HEALTH_RANK[current.health] ?? 0) ? runtime : current, runtimes[0])
52
+ if (!worst) return <span className="text-xs text-theme-text-tertiary">Idle</span>
53
+ const same = runtimes.every((runtime) => runtime.label === worst.label)
54
+ const label = same ? worst.label : `${runtimes.length} states`
55
+ return (
56
+ <Tooltip content={runtimes.map((runtime, index) => `${apps[index]?.name ?? 'app'}: ${runtime.label} — ${runtime.detail}`).join('\n')} delay={150}>
57
+ <span className="inline-flex items-center gap-1.5 text-xs text-theme-text-secondary"><StatusDot tone={mapHealthToTone(worst.health)} />{label}</span>
58
+ </Tooltip>
59
+ )
60
+ }
61
+
62
+ function applicationRuntimeLabel(apps: AppRow[], workloadClass: AppWorkloadClass, health: AppHealth): string {
63
+ if (workloadClass !== 'job') return HEALTH_META[health].label
64
+ const labels = Array.from(new Set(apps.map((app) => batchRuntimeForApp(app).label)))
65
+ return labels.length === 1 ? labels[0] : 'Multiple batch states'
66
+ }
67
+
45
68
  // ApplicationsView — the shared, variant-agnostic core behind the Applications
46
69
  // surface. It owns the entire list chassis: a health hero header (PageHeader +
47
70
  // SummaryTiles + DistributionBar), a left facet rail (Availability / Class /
@@ -52,7 +75,7 @@ import { AppIdentityTooltip, EnvHint } from './AppTooltips'
52
75
  // the per-instance row. Styling mirrors the Resources table so the surfaces
53
76
  // read as one design.
54
77
 
55
- // Availability is the one status facet — map app health onto the shared facet
78
+ // Status is the one health facet — map app health onto the shared facet
56
79
  // tone so its dots read red/amber/green/grey like the GitOps sync+health facets.
57
80
  const HEALTH_TONE: Record<AppHealth, FacetTone> = {
58
81
  unhealthy: 'error',
@@ -72,7 +95,7 @@ function compareEntries(a: AppEntry, b: AppEntry, key: SortKey): number {
72
95
  case 'name':
73
96
  return a.row.name.localeCompare(b.row.name)
74
97
  case 'ready':
75
- return a.readyRatio - b.readyRatio
98
+ return runtimeSortValue(a) - runtimeSortValue(b)
76
99
  case 'version': {
77
100
  // Sort by distinct-version count first (skewed apps cluster), then the
78
101
  // first tag for a stable, human-meaningful order.
@@ -83,6 +106,10 @@ function compareEntries(a: AppEntry, b: AppEntry, key: SortKey): number {
83
106
  }
84
107
  }
85
108
 
109
+ function runtimeSortValue(entry: AppEntry): number {
110
+ return entry.workloadClass === 'job' ? (HEALTH_RANK[entry.health] ?? 0) : entry.readyRatio
111
+ }
112
+
86
113
  // The env token an entry filters under. Single entries carry one env; fleet
87
114
  // entries carry several — a fleet row matches an env facet if ANY of its slices
88
115
  // do (the same inclusive policy as the Class facet).
@@ -348,7 +375,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
348
375
  {initialLoading ? (
349
376
  <BoardRailSkeleton
350
377
  sections={[
351
- ['Availability', 5],
378
+ ['Status', 5],
352
379
  ['Class', 3],
353
380
  ['Type', 4],
354
381
  ['Environment', 3],
@@ -357,7 +384,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
357
384
  />
358
385
  ) : (
359
386
  <>
360
- <Facet icon={HeartPulse} title="Availability" options={HEALTH_ORDER.map((h) => ({ value: h, label: HEALTH_META[h].label, count: counts.health[h] ?? 0, tone: HEALTH_TONE[h] }))} selected={fHealth} onToggle={(v) => filters.toggle('health', v)} />
387
+ <Facet icon={HeartPulse} title="Status" options={HEALTH_ORDER.map((h) => ({ value: h, label: HEALTH_META[h].label, count: counts.health[h] ?? 0, tone: HEALTH_TONE[h] }))} selected={fHealth} onToggle={(v) => filters.toggle('health', v)} />
361
388
  <Facet icon={Layers} title="Class" options={CLASS_ORDER.map((c) => ({ value: c, label: CLASS_META[c].label, count: counts.workloadClass[c] ?? 0 }))} selected={fClass} onToggle={(v) => filters.toggle('class', v)} />
362
389
  <Facet icon={Shapes} title="Type" options={CATEGORY_ORDER.map((c) => ({ value: c, label: CATEGORY_META[c].label, count: counts.category[c] ?? 0, tooltip: CATEGORY_META[c].tooltip }))} selected={fType} onToggle={(v) => filters.toggle('type', v)} />
363
390
  <Facet icon={Globe} title="Environment" info={<EnvHint />} options={envOptions} selected={fEnv} onToggle={(v) => filters.toggle('env', v)} />
@@ -430,7 +457,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
430
457
  </>
431
458
  )}
432
459
  <th className={TH_CLASS}>Class</th>
433
- <SortableTh label="Ready" sortKey="ready" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
460
+ <SortableTh label="Runtime" sortKey="ready" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
434
461
  <SortableTh label="Version" sortKey="version" activeKey={sort?.key ?? null} direction={sort?.dir ?? 'asc'} onSort={onSort} />
435
462
  <th className={TH_CLASS}>Workloads</th>
436
463
  <th className={clsx(TH_CLASS, 'w-8')} />
@@ -452,11 +479,12 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
452
479
  <span className="flex items-center gap-2">
453
480
  {/* Left status stripe (worst-child health) — the row status
454
481
  gutter shared with the GitOps table. */}
455
- <Tooltip content={HEALTH_META[r.health].label} delay={150}>
482
+ <Tooltip content={applicationRuntimeLabel(r.members.map((member) => member.row), r.workloadClass, r.health)} delay={150}>
456
483
  <span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[r.health].bar)} />
457
484
  </Tooltip>
458
485
  <ChevronRight className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', r.expanded && 'rotate-90')} aria-hidden />
459
486
  <span className="truncate font-semibold text-theme-text-primary">{r.label}</span>
487
+ {r.workloadClass !== 'job' && <BatchSignalChip signal={firstBatchSignal(r.members.map((m) => m.row))} />}
460
488
  <Tooltip
461
489
  content={<AppIdentityTooltip identityKey={r.label} source={r.members[0]?.row.identity?.source} portable={r.members[0]?.row.identity?.portable} fleet={variant === 'fleet'} members={r.members.map((m) => ({ name: m.row.name, env: m.row.identity!.env, confidence: m.row.identity!.confidence, evidence: m.row.identity!.evidence }))} />}
462
490
  delay={150}
@@ -504,7 +532,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
504
532
  </span>
505
533
  </td>
506
534
  <td className="px-2 py-2.5"><ClassBadge workloadClass={r.workloadClass} composition={r.classComposition} /></td>
507
- <td className="px-2 py-2.5"><ReadyBar ready={r.ready} desired={r.desired} /></td>
535
+ <td className="px-2 py-2.5"><ApplicationRuntimeCell apps={r.members.map((member) => member.row)} workloadClass={r.workloadClass} ready={r.ready} desired={r.desired} /></td>
508
536
  <td className="px-2 py-2.5">
509
537
  {r.lag ? (
510
538
  <Tooltip content={`Promotion lag: ${r.lag} (${r.cells.filter((c) => c.version).map((c) => `${c.env}=${c.version}`).join(', ')})`} delay={150}>
@@ -531,10 +559,11 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
531
559
  >
532
560
  <td className={clsx('py-2.5 pr-2', r.child ? 'pl-10' : 'pl-3')}>
533
561
  <span className="flex items-center gap-2">
534
- <Tooltip content={HEALTH_META[e.health].label} delay={150}>
562
+ <Tooltip content={applicationRuntimeLabel([e.row], e.workloadClass, e.health)} delay={150}>
535
563
  <span className={clsx('h-8 w-1 shrink-0 rounded-full', HEALTH_META[e.health].bar)} />
536
564
  </Tooltip>
537
565
  <span className="truncate font-medium text-theme-text-primary">{e.row.name}</span>
566
+ {e.workloadClass !== 'job' && <BatchSignalChip signal={batchSignalForApp(e.row)} />}
538
567
  <ProvenanceBadge tier={e.row.tier} appKey={e.row.key} confidence={e.row.confidence} />
539
568
  <CategoryChip category={e.category} addonReason={e.row.addonReason} />
540
569
  </span>
@@ -601,7 +630,7 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
601
630
  </>
602
631
  )}
603
632
  <td className="px-2 py-2.5"><ClassBadge workloadClass={e.workloadClass} composition={e.classComposition} /></td>
604
- <td className="px-2 py-2.5"><ReadyBar ready={e.ready} desired={e.desired} /></td>
633
+ <td className="px-2 py-2.5"><ApplicationRuntimeCell apps={[e.row]} workloadClass={e.workloadClass} ready={e.ready} desired={e.desired} /></td>
605
634
  <td className="px-2 py-2.5">
606
635
  {e.variant === 'fleet' ? (
607
636
  e.versionSkew ? (
@@ -640,3 +669,26 @@ export function ApplicationsView({ entries: allEntries, variant, onSelect, title
640
669
  </div>
641
670
  )
642
671
  }
672
+
673
+ function firstBatchSignal(rows: AppRow[]) {
674
+ let best = null as ReturnType<typeof batchSignalForApp>
675
+ const rank = (tone: string | undefined) => {
676
+ switch (tone) {
677
+ case 'rose':
678
+ return 5
679
+ case 'amber':
680
+ return 4
681
+ case 'sky':
682
+ return 3
683
+ case 'emerald':
684
+ return 2
685
+ default:
686
+ return 0
687
+ }
688
+ }
689
+ for (const row of rows) {
690
+ const signal = batchSignalForApp(row)
691
+ if (signal && rank(signal.tone) > rank(best?.tone)) best = signal
692
+ }
693
+ return best
694
+ }
@@ -67,6 +67,53 @@ describe('buildSingleAppEntry', () => {
67
67
  expect(e.env).toBe('staging')
68
68
  expect(e.envInferred).toBe(false)
69
69
  })
70
+
71
+ it('uses delivery degradation in the displayed application health', () => {
72
+ const e = buildSingleAppEntry(app({
73
+ health: 'degraded',
74
+ runtimeHealth: 'healthy',
75
+ sourceStatus: { sync: 'Synced', health: 'Degraded' },
76
+ }))
77
+
78
+ expect(e.health).toBe('degraded')
79
+ })
80
+
81
+ it('preserves the latest retained batch outcome when delivery adds no degradation', () => {
82
+ const e = buildSingleAppEntry(app({
83
+ health: 'unhealthy',
84
+ runtimeHealth: 'unhealthy',
85
+ workload_class: 'job',
86
+ workloads: [wl({
87
+ kind: 'CronJob',
88
+ workload_class: 'job',
89
+ health: 'unhealthy',
90
+ ready: 0,
91
+ desired: 0,
92
+ batch: { retainedRuns: 2, failedRuns: 1, succeededRuns: 1, latestRunPhase: 'Succeeded' },
93
+ })],
94
+ }))
95
+
96
+ expect(e.health).toBe('healthy')
97
+ })
98
+
99
+ it('applies delivery degradation to a batch app with a successful latest run', () => {
100
+ const e = buildSingleAppEntry(app({
101
+ health: 'unhealthy',
102
+ runtimeHealth: 'unhealthy',
103
+ workload_class: 'job',
104
+ sourceStatus: { sync: 'Synced', health: 'Degraded' },
105
+ workloads: [wl({
106
+ kind: 'CronJob',
107
+ workload_class: 'job',
108
+ health: 'unhealthy',
109
+ ready: 0,
110
+ desired: 0,
111
+ batch: { retainedRuns: 2, failedRuns: 1, succeededRuns: 1, latestRunPhase: 'Succeeded' },
112
+ })],
113
+ }))
114
+
115
+ expect(e.health).toBe('degraded')
116
+ })
70
117
  })
71
118
 
72
119
  describe('searchTextForEntry (single)', () => {
@@ -7,7 +7,7 @@ export { buildSingleAppEntry } from '../../utils/applications'
7
7
  // Facet moved to the shared ui/ primitives; re-exported here for compatibility.
8
8
  export { Facet } from '../ui/Facet'
9
9
  export { ApplicationDetail } from './ApplicationDetail'
10
- export type { ApplicationDetailProps, SelectedAppWorkload, AppIdentityInstance } from './ApplicationDetail'
10
+ export type { ApplicationDetailProps, SelectedAppWorkload, AppIdentityInstance, ApplicationView } from './ApplicationDetail'
11
11
  export { CenteredEmpty } from '../ui/CenteredEmpty'
12
12
  export { ProvenanceBadge, ClassBadge, CategoryChip, VersionInfo } from './AppChips'
13
13
  export { ProvenanceTooltip, CategoryTooltip, VersionTooltip, AppIdentityTooltip } from './AppTooltips'
@@ -41,14 +41,15 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
41
41
  const warnings = findings.filter(f => f.severity === 'warning').length
42
42
 
43
43
  return (
44
- <div className="border-b-subtle pb-4 last:border-0">
44
+ <section className="rounded-lg border border-theme-border bg-theme-surface p-4 shadow-theme-sm">
45
45
  <button
46
+ type="button"
46
47
  onClick={() => setExpanded(!expanded)}
47
- className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
48
+ className="flex w-full items-center gap-2 rounded-md text-left transition-colors hover:text-theme-text-primary"
48
49
  >
49
50
  <ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform duration-200', expanded && 'rotate-90')} />
50
51
  <ClipboardCheck className="w-4 h-4 text-theme-text-secondary" />
51
- <span className="text-sm font-medium text-theme-text-secondary">Audit Findings</span>
52
+ <span className="text-sm font-semibold text-theme-text-primary">Audit Findings</span>
52
53
  <div className="flex items-center gap-2 ml-1">
53
54
  {dangers > 0 && (
54
55
  <span className={clsx('text-xs font-medium tabular-nums', SEVERITY_TEXT.error)}>{dangers} critical</span>
@@ -64,7 +65,7 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
64
65
  style={{ gridTemplateRows: expanded ? '1fr' : '0fr' }}
65
66
  >
66
67
  <div className="overflow-hidden">
67
- <div className="pl-6 pt-1 flex flex-col gap-0.5">
68
+ <div className="flex flex-col gap-0.5 pt-3">
68
69
  {findings.map((f, i) => {
69
70
  const isDanger = f.severity === 'danger'
70
71
  return (
@@ -74,17 +75,20 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
74
75
  ) : (
75
76
  <AlertTriangle className={clsx('w-3.5 h-3.5 shrink-0 mt-0.5', SEVERITY_TEXT.warning)} />
76
77
  )}
77
- <span className="text-xs text-theme-text-secondary flex-1">{f.message}</span>
78
- <span className={clsx('badge-sm text-[10px] shrink-0', BP_CATEGORY_BADGE[f.category] || DEFAULT_BADGE_COLOR)}>
79
- {f.category}
80
- </span>
78
+ <div className="flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-1">
79
+ <span className="text-xs text-theme-text-secondary">{f.message}</span>
80
+ <span className={clsx('badge-sm text-[10px] shrink-0', BP_CATEGORY_BADGE[f.category] || DEFAULT_BADGE_COLOR)}>
81
+ {f.category}
82
+ </span>
83
+ </div>
81
84
  </div>
82
85
  )
83
86
  })}
84
87
  {onViewAll && (
85
88
  <button
89
+ type="button"
86
90
  onClick={(e) => { e.stopPropagation(); onViewAll() }}
87
- className="flex items-center gap-1 text-xs text-skyhook-500 hover:text-skyhook-400 mt-1 py-1 transition-colors"
91
+ className="mt-1 flex items-center gap-1 py-1 text-xs text-accent-text transition-colors hover:text-accent"
88
92
  >
89
93
  View all findings
90
94
  <ArrowRight className="w-3 h-3" />
@@ -93,6 +97,6 @@ export function AuditAlerts({ findings, onViewAll }: AuditAlertsProps) {
93
97
  </div>
94
98
  </div>
95
99
  </div>
96
- </div>
100
+ </section>
97
101
  )
98
102
  }
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { AlertCircle, AlertOctagon, AlertTriangle, ChevronDown, ChevronRight, ExternalLink, EyeOff, Info, Layers, MoreHorizontal, Search, ShieldCheck, Wrench, X } from 'lucide-react'
4
- import { CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
4
+ import { CardBody, CardSection, ClusterName, EmptyState, FilterPill, DistributionBar, DistributionLegendChip, Input, NEUTRAL_CHIP_CLASS, renderProse } from '../ui'
5
5
  import { useFilterState, defineFilterSchema } from '../../filter-state'
6
6
  import type { CheckMeta, CheckReference } from '../audit'
7
7
  import { CHECK_SEVERITIES, CHECK_SEVERITY_RANK, type Check, type CheckSeverity, type EffectiveCheckFinding, type CheckResourceRef } from './types'
@@ -265,8 +265,7 @@ export function ChecksView({ checks, catalog, anyData, resourceHref, onResourceC
265
265
  </div>
266
266
  <div className="relative">
267
267
  <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-theme-text-tertiary" />
268
- <input
269
- type="text"
268
+ <Input
270
269
  placeholder="Search checks…"
271
270
  value={search}
272
271
  onChange={(e) => filters.setString('q', e.target.value)}
@@ -10,14 +10,15 @@ import {
10
10
  } from '../ui/severity-tone'
11
11
 
12
12
  // The visual language for the 4-tier Checks severity ladder. One hue per tier:
13
- // red=critical, orange=high, amber=medium, neutral=low — read the queue's left
14
- // rail top-to-bottom and severity is obvious without reading a word. The actual
15
- // color strings are shared with the Issue card via the tone module; here we only
16
- // map each tier onto its tone.
13
+ // red=critical, orange=high, yellow=medium, neutral=low — read the queue's left
14
+ // rail top-to-bottom and severity is obvious without reading a word. Medium is
15
+ // yellow (not amber) so it sits a clear hue-step below high's orange rather than
16
+ // blurring into it. The color strings are shared with the Issue card via the
17
+ // tone module; here we only map each tier onto its tone.
17
18
  const CHECK_SEVERITY_TONE: Record<CheckSeverity, SeverityTone> = {
18
19
  critical: 'red',
19
20
  high: 'orange',
20
- medium: 'amber',
21
+ medium: 'yellow',
21
22
  low: 'slate',
22
23
  }
23
24
 
@@ -35,14 +36,17 @@ export const SEVERITY_LABEL: Record<CheckSeverity, string> = {
35
36
  low: 'Low',
36
37
  }
37
38
 
38
- // Pill badge — the loud, explicit severity signal on rows + drawer header.
39
- // Uses the canonical Badge severity tones (BADGE_SEVERITY_COLORS) so the queue's
40
- // severity pills read identically to status badges everywhere else (rendered
41
- // with `badge-sm`).
39
+ // Soft pill badge — the loud, explicit severity signal on rows + drawer header.
40
+ // Critical/high/low reuse the canonical Badge tones; medium takes an explicit
41
+ // yellow (matching its tone above) rather than the shared amber `warning` tone,
42
+ // so it reads a clear step below high's orange without shifting the amber
43
+ // `warning` badge used elsewhere (Issues, GitOps). yellow-800 on yellow-100
44
+ // clears WCAG for the pill text.
42
45
  export const SEVERITY_BADGE_CLASS: Record<CheckSeverity, string> = {
43
46
  critical: sev.error,
44
47
  high: sev.alert,
45
- medium: sev.warning,
48
+ medium:
49
+ 'bg-yellow-100 text-yellow-800 border-yellow-300 dark:bg-yellow-950/50 dark:text-yellow-400 dark:border-yellow-700/40',
46
50
  low: sev.neutral,
47
51
  }
48
52
 
@@ -9,6 +9,7 @@ import {
9
9
  } from 'react'
10
10
  import { ChevronDown, Check, FolderOpen, Loader2, Search, Server, X } from 'lucide-react'
11
11
  import { ClusterName } from '../ui/ClusterName'
12
+ import { Input } from '../ui/Input'
12
13
  import { MiddleEllipsis } from '../ui/MiddleEllipsis'
13
14
  import { StatusDot, type StatusTone } from '../ui/status-tone'
14
15
 
@@ -269,9 +270,8 @@ export const ClusterSwitcher = forwardRef<ClusterSwitcherHandle, ClusterSwitcher
269
270
  <div className="p-2 border-b border-theme-border">
270
271
  <div className="relative">
271
272
  <Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-theme-text-tertiary" />
272
- <input
273
+ <Input
273
274
  ref={searchInputRef}
274
- type="text"
275
275
  value={search}
276
276
  onChange={e => setSearch(e.target.value)}
277
277
  onKeyDown={onSearchKeyDown}
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { clsx } from 'clsx'
3
3
  import { GitCompare, Search, X } from 'lucide-react'
4
4
  import { DialogPortal } from '../ui/DialogPortal'
5
+ import { Input } from '../ui/Input'
5
6
  import { pluralToKind } from '../../utils/navigation'
6
7
  import type { CompareResourceRef } from './ResourceCompareView'
7
8
  import { sortCandidates, filterCandidates } from './sort'
@@ -120,9 +121,8 @@ export function CompareResourcePicker({
120
121
  </div>
121
122
  <div className="relative">
122
123
  <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-theme-text-tertiary pointer-events-none" />
123
- <input
124
+ <Input
124
125
  autoFocus
125
- type="text"
126
126
  value={query}
127
127
  onChange={e => setQuery(e.target.value)}
128
128
  onKeyDown={handleKeyDown}
@@ -77,6 +77,7 @@ export interface ArgoActionHandlers {
77
77
  autoSyncEnabled: boolean
78
78
  // isRunning: caller derives from status.operationState.phase === 'Running'.
79
79
  isRunning: boolean
80
+ operationInProgress?: boolean
80
81
  }
81
82
 
82
83
  export interface FluxActionHandlers {
@@ -158,6 +159,9 @@ export interface GitOpsDetailLayoutProps {
158
159
  // hub-web and OSS can wire their own data sources without the layout
159
160
  // knowing.
160
161
  renderTabBody: (ctx: { tab: GitOpsDetailTab; fullscreen: boolean }) => ReactNode
162
+ // Optional host slot: inline Git commit metadata next to the status strip's
163
+ // latest revision (author, signature). Wired by the host to a data loader.
164
+ renderRevisionMeta?: (revision: string) => ReactNode
161
165
  // Optional: top-right tab-bar accessory (Clear filters + Fullscreen
162
166
  // buttons in OSS, can be anything else in hub-web).
163
167
  renderTabBarAccessory?: (ctx: { tab: GitOpsDetailTab; fullscreen: boolean }) => ReactNode
@@ -228,6 +232,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
228
232
  fullscreen,
229
233
  onToggleFullscreen,
230
234
  renderTabBody,
235
+ renderRevisionMeta,
231
236
  renderTabBarAccessory,
232
237
  renderTabBarCounts,
233
238
  resourceLoading,
@@ -252,6 +257,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
252
257
  }, [identity.name, manageDocumentTitle, documentTitleSuffix])
253
258
 
254
259
  const effectiveSuspended = status?.suspended ?? false
260
+ const argoOperationInProgress = argo?.operationInProgress ?? argo?.isRunning ?? false
255
261
  // graphFullscreen hides everything chrome-side; the body region expands.
256
262
  // Mirrors the OSS GitOpsDetailView shell behavior so the visual chrome
257
263
  // class set carries through.
@@ -344,8 +350,12 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
344
350
  icon={ArrowDownUp}
345
351
  loading={argo.syncing}
346
352
  onClick={argo.onSyncRequested}
347
- disabled={effectiveSuspended || terminating}
348
- disabledReason={terminating ? terminatingActionTooltip : undefined}
353
+ disabled={effectiveSuspended || terminating || argoOperationInProgress}
354
+ disabledReason={terminating
355
+ ? terminatingActionTooltip
356
+ : argoOperationInProgress
357
+ ? 'Wait for the current Argo operation to finish.'
358
+ : undefined}
349
359
  primary
350
360
  />
351
361
  <ActionButton
@@ -447,7 +457,7 @@ export function GitOpsDetailLayout(props: GitOpsDetailLayoutProps) {
447
457
  )}
448
458
  {!fullscreen && (
449
459
  <>
450
- <GitOpsStatusStrip insight={insight ?? undefined} loading={insightLoading} />
460
+ <GitOpsStatusStrip insight={insight ?? undefined} loading={insightLoading} renderRevisionMeta={renderRevisionMeta} />
451
461
  <GitOpsIssuesBand
452
462
  issues={insight?.issues}
453
463
  terminating={terminating}
@@ -1,6 +1,7 @@
1
1
  import { CheckCircle2, CircleDot, GitBranch, HeartPulse, LayoutGrid, List, Search } from 'lucide-react'
2
2
 
3
3
  import { GitOpsFacetButton, GitOpsFilterSection } from './GitOpsTableView'
4
+ import { Input } from '../ui/Input'
4
5
  import type { GitOpsResourceTree } from '../../types/gitops-tree'
5
6
  import type { GitOpsTreePreset } from './tree'
6
7
 
@@ -79,7 +80,7 @@ export function GitOpsGraphFilterRail({
79
80
  <div className="border-b border-theme-border px-3 py-3">
80
81
  <div className="relative">
81
82
  <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-theme-text-tertiary" />
82
- <input
83
+ <Input
83
84
  value={search}
84
85
  onChange={(event) => onSearchChange(event.target.value)}
85
86
  placeholder="Filter resources..."
@@ -26,6 +26,7 @@ import {
26
26
 
27
27
  import { HealthStatusBadge, SyncStatusBadge } from './GitOpsStatusBadge'
28
28
  import { Tooltip } from '../ui/Tooltip'
29
+ import { Input } from '../ui/Input'
29
30
  import { PageHeader } from '../ui/PageHeader'
30
31
  import { SummaryTile, type SummaryTone } from '../ui/SummaryTile'
31
32
  import { FacetSection, FacetButton } from '../ui/Facet'
@@ -34,7 +35,7 @@ import { DistributionBar } from '../ui/DistributionBar'
34
35
  import { RowActionMenu, type RowActionItem } from '../ui/RowActionMenu'
35
36
  import { BoardTableSkeleton, BoardRailSkeleton } from '../ui/BoardSkeleton'
36
37
  import { getGitOpsResourceStatus } from './detail-helpers'
37
- import { isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
38
+ import { isArgoOperationInProgress, isArgoSuspendedByRadar } from '../resources/resource-utils-argo'
38
39
  import { toggleSet } from './GitOpsGraphFilterRail'
39
40
  import { useFilterState, defineFilterSchema } from '../../filter-state'
40
41
  import { parseContextName } from '../../utils/context-name'
@@ -687,7 +688,7 @@ export function GitOpsTableView({
687
688
  <div className="mt-3 flex flex-wrap items-center gap-2">
688
689
  <div className="relative w-full max-w-md">
689
690
  <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-theme-text-tertiary" />
690
- <input
691
+ <Input
691
692
  ref={searchInputRef}
692
693
  value={search}
693
694
  onChange={(e) => filters.setString('q', e.target.value)}
@@ -1129,8 +1130,7 @@ function LabelsDropdown({
1129
1130
  <div className="flex items-center gap-2">
1130
1131
  <div className="relative flex-1">
1131
1132
  <Search className="pointer-events-none absolute left-2 top-1/2 h-3 w-3 -translate-y-1/2 text-theme-text-tertiary" />
1132
- <input
1133
- type="text"
1133
+ <Input
1134
1134
  value={search}
1135
1135
  onChange={(e) => onSearchChange(e.target.value)}
1136
1136
  placeholder="Search labels..."
@@ -1358,13 +1358,20 @@ function buildRowActionItems(
1358
1358
  const items: RowActionItem[] = []
1359
1359
 
1360
1360
  if (row.tool === 'argo') {
1361
+ const operationInProgress = isArgoOperationInProgress(row.raw)
1361
1362
  items.push({
1362
1363
  key: 'sync',
1363
1364
  label: 'Sync...',
1364
1365
  icon: ArrowDownUp,
1365
1366
  onClick: () => onAction(row, 'sync'),
1366
- disabled: suspended || terminating,
1367
- disabledReason: terminating ? terminatingReason : suspended ? suspendedReason : undefined,
1367
+ disabled: suspended || terminating || operationInProgress,
1368
+ disabledReason: terminating
1369
+ ? terminatingReason
1370
+ : suspended
1371
+ ? suspendedReason
1372
+ : operationInProgress
1373
+ ? 'Wait for the current Argo operation to finish.'
1374
+ : undefined,
1368
1375
  pending: isPending('sync'),
1369
1376
  })
1370
1377
  // Refresh / Hard refresh are read-style verbs — they re-read Git and
@@ -1,9 +1,10 @@
1
1
  import { useState } from 'react'
2
- import { ChevronDown, ChevronRight, Package, ExternalLink } from 'lucide-react'
2
+ import { Package, ExternalLink } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
4
  import type { ManagedResource, GitOpsHealthStatus } from '../../types/gitops'
5
5
  import { groupManagedResourcesByKind } from '../../types/gitops'
6
6
  import { pluralize } from '../../utils/pluralize'
7
+ import { Collapse, CollapseChevron } from '../ui/Collapse'
7
8
 
8
9
  interface ManagedResourcesListProps {
9
10
  resources: ManagedResource[]
@@ -45,18 +46,14 @@ export function ManagedResourcesList({
45
46
  onClick={() => setExpanded(!expanded)}
46
47
  className="flex items-center gap-2 w-full text-left mb-2 hover:text-theme-text-primary transition-colors"
47
48
  >
48
- {expanded ? (
49
- <ChevronDown className="w-4 h-4 text-theme-text-tertiary" />
50
- ) : (
51
- <ChevronRight className="w-4 h-4 text-theme-text-tertiary" />
52
- )}
49
+ <CollapseChevron open={expanded} className="w-4 h-4" />
53
50
  <Package className="w-4 h-4 text-theme-text-secondary" />
54
51
  <span className="text-sm font-medium text-theme-text-secondary">
55
52
  {title} ({resources.length})
56
53
  </span>
57
54
  </button>
58
55
 
59
- {expanded && (
56
+ <Collapse open={expanded} mountLazily>
60
57
  <div className="pl-6 space-y-3">
61
58
  {sortedGroups.map(([kind, kindResources]) => (
62
59
  <ResourceKindGroup
@@ -77,7 +74,7 @@ export function ManagedResourcesList({
77
74
  </button>
78
75
  )}
79
76
  </div>
80
- )}
77
+ </Collapse>
81
78
  </div>
82
79
  )
83
80
  }
@@ -98,12 +95,12 @@ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: Res
98
95
  onClick={() => setExpanded(!expanded)}
99
96
  className="flex items-center gap-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors mb-1"
100
97
  >
101
- {expanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
98
+ <CollapseChevron open={expanded} className="w-3 h-3" />
102
99
  <span className="font-medium">{kind}</span>
103
100
  <span className="text-theme-text-tertiary">({resources.length})</span>
104
101
  </button>
105
102
 
106
- {expanded && (
103
+ <Collapse open={expanded} mountLazily>
107
104
  <div className="ml-4 space-y-0.5">
108
105
  {resources.map((resource, idx) => (
109
106
  <ResourceItem
@@ -114,7 +111,7 @@ function ResourceKindGroup({ kind, resources, onResourceClick, showHealth }: Res
114
111
  />
115
112
  ))}
116
113
  </div>
117
- )}
114
+ </Collapse>
118
115
  </div>
119
116
  )
120
117
  }