@skyhook-io/radar-app 1.13.5 → 1.14.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 (242) hide show
  1. package/package.json +9 -9
  2. package/src/App.tsx +66 -28
  3. package/src/api/client.authRedirect.test.ts +12 -1
  4. package/src/api/client.prometheusStatus.test.ts +24 -0
  5. package/src/api/client.ts +324 -16
  6. package/src/api/diagnose.ts +32 -1
  7. package/src/api/drain-plan.test.ts +86 -0
  8. package/src/api/quotas.test.ts +26 -0
  9. package/src/api/quotas.ts +20 -0
  10. package/src/api/workloadMetrics.test.ts +40 -0
  11. package/src/api/workloadMetrics.ts +68 -0
  12. package/src/components/CloudConnectFlow.tsx +343 -46
  13. package/src/components/CloudFunnelButton.tsx +110 -37
  14. package/src/components/ConnectionErrorView.tsx +9 -10
  15. package/src/components/DebugOverlay.tsx +10 -6
  16. package/src/components/SyncProgressPanel.tsx +73 -0
  17. package/src/components/audit/AuditView.tsx +2 -0
  18. package/src/components/cloudConnectHandoff.test.ts +245 -0
  19. package/src/components/cloudConnectHandoff.ts +276 -0
  20. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  21. package/src/components/diagnose/AgentCase.tsx +18 -5
  22. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  23. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  24. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  25. package/src/components/diagnose/DiagnoseContext.tsx +26 -8
  26. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  27. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  28. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  29. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  30. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  31. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  32. package/src/components/diagnose/InvestigationView.tsx +493 -529
  33. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  34. package/src/components/diagnose/investigationCase.ts +122 -77
  35. package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
  36. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
  37. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
  38. package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
  39. package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
  40. package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
  41. package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
  42. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
  43. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
  44. package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
  45. package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
  46. package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
  47. package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
  48. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +81 -0
  49. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +285 -0
  50. package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
  51. package/src/components/diagnose/investigationEvidence/builder.ts +427 -0
  52. package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
  53. package/src/components/diagnose/investigationEvidence/identity.test.ts +632 -0
  54. package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
  55. package/src/components/diagnose/investigationEvidence/index.ts +55 -0
  56. package/src/components/diagnose/investigationEvidence/observations.ts +978 -0
  57. package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
  58. package/src/components/diagnose/investigationEvidence/projection.test.ts +2736 -0
  59. package/src/components/diagnose/investigationEvidence/types.ts +514 -0
  60. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  61. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  62. package/src/components/diagnose/investigationState.test.ts +316 -58
  63. package/src/components/diagnose/investigationState.ts +257 -44
  64. package/src/components/diagnose/investigationStory.test.ts +161 -0
  65. package/src/components/diagnose/investigationStory.ts +207 -0
  66. package/src/components/diagnose/parts.test.tsx +9 -6
  67. package/src/components/diagnose/parts.tsx +977 -193
  68. package/src/components/diagnose/storyParts.test.tsx +426 -0
  69. package/src/components/diagnose/target.test.ts +19 -0
  70. package/src/components/diagnose/target.ts +3 -2
  71. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  72. package/src/components/diagnose/toolCallLabel.ts +135 -0
  73. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  74. package/src/components/gitops/GitOpsView.tsx +7 -0
  75. package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
  76. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  77. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  78. package/src/components/helm/InstallWizard.tsx +8 -3
  79. package/src/components/home/MCPSetupDialog.tsx +15 -9
  80. package/src/components/portforward/PortForwardManager.tsx +6 -13
  81. package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
  82. package/src/components/resource/PrometheusCharts.tsx +4 -3
  83. package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
  84. package/src/components/resource/PrometheusChartsGrid.tsx +75 -35
  85. package/src/components/resource/RightsizingStrip.test.ts +38 -0
  86. package/src/components/resource/RightsizingStrip.tsx +15 -3
  87. package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
  88. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
  89. package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
  90. package/src/components/resource/WorkloadMetricsSection.tsx +480 -0
  91. package/src/components/resource/workloadMetricValues.test.ts +65 -0
  92. package/src/components/resource/workloadMetricValues.ts +30 -0
  93. package/src/components/resources/PodFilePreview.tsx +3 -3
  94. package/src/components/resources/ResourcesView.tsx +44 -6
  95. package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
  96. package/src/components/resources/renderers/PodRenderer.tsx +4 -0
  97. package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
  98. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  99. package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
  100. package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
  101. package/src/components/settings/SettingsDialog.tsx +98 -16
  102. package/src/components/traffic/TrafficFlowList.tsx +229 -70
  103. package/src/components/traffic/TrafficGraph.tsx +6 -6
  104. package/src/components/traffic/TrafficView.tsx +10 -9
  105. package/src/components/traffic/trafficFilters.test.ts +28 -1
  106. package/src/components/traffic/trafficFilters.ts +31 -0
  107. package/src/components/ui/CommandPalette.tsx +10 -6
  108. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  109. package/src/components/ui/Disclosure.tsx +47 -0
  110. package/src/components/ui/Markdown.tsx +23 -11
  111. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  112. package/src/components/ui/UpdateNotification.tsx +18 -2
  113. package/src/components/workload/WorkloadView.tsx +19 -8
  114. package/src/context/ConnectionContext.tsx +48 -3
  115. package/src/context/DiagnoseCustomization.tsx +1 -0
  116. package/src/index.css +36 -6
  117. package/src/utils/navigation.ts +1 -1
  118. package/src/utils/topology-namespace.test.ts +64 -0
  119. package/src/utils/topology-namespace.ts +18 -0
  120. package/src/components/diagnose/investigationEvidence.test.ts +0 -6617
  121. package/src/components/diagnose/investigationEvidence.ts +0 -5572
  122. package/src/components/gitops/GitOpsActions.tsx +0 -1
  123. package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
  124. package/src/components/gitops/ManagedResourcesList.tsx +0 -1
  125. package/src/components/gitops/SyncCountdown.tsx +0 -1
  126. package/src/components/gitops/index.ts +0 -4
  127. package/src/components/helm/ManifestDiffViewer.tsx +0 -69
  128. package/src/components/logs/JsonLogLine.tsx +0 -1
  129. package/src/components/logs/LogCore.tsx +0 -2
  130. package/src/components/logs/useLogBuffer.ts +0 -2
  131. package/src/components/logs/useLogSearch.ts +0 -1
  132. package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
  133. package/src/components/resources/drawer-components.tsx +0 -1
  134. package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
  135. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
  136. package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
  137. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
  138. package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
  139. package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
  140. package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
  141. package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
  142. package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
  143. package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
  144. package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
  145. package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
  146. package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
  147. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
  148. package/src/components/resources/renderers/EventRenderer.tsx +0 -1
  149. package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
  150. package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
  151. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
  152. package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
  153. package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
  154. package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
  155. package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
  156. package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
  157. package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
  158. package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
  159. package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
  160. package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
  161. package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
  162. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
  163. package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
  164. package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
  165. package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
  166. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
  167. package/src/components/resources/renderers/JobRenderer.tsx +0 -1
  168. package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
  169. package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
  170. package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
  171. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
  172. package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
  173. package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
  174. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
  175. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
  176. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
  177. package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
  178. package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
  179. package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
  180. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
  181. package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
  182. package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
  183. package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
  184. package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
  185. package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
  186. package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
  187. package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
  188. package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
  189. package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
  190. package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
  191. package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
  192. package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
  193. package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
  194. package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
  195. package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
  196. package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
  197. package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
  198. package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
  199. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
  200. package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
  201. package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
  202. package/src/components/resources/renderers/VPARenderer.tsx +0 -1
  203. package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
  204. package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
  205. package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
  206. package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
  207. package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
  208. package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
  209. package/src/components/resources/renderers/argo-cells.tsx +0 -1
  210. package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
  211. package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
  212. package/src/components/resources/renderers/eso-cells.tsx +0 -1
  213. package/src/components/resources/renderers/flux-cells.tsx +0 -1
  214. package/src/components/resources/renderers/index.ts +0 -94
  215. package/src/components/resources/renderers/istio-cells.tsx +0 -1
  216. package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
  217. package/src/components/resources/renderers/keda-cells.tsx +0 -1
  218. package/src/components/resources/renderers/knative-cells.tsx +0 -1
  219. package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
  220. package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
  221. package/src/components/resources/renderers/traefik-cells.tsx +0 -1
  222. package/src/components/resources/renderers/trivy-cells.tsx +0 -1
  223. package/src/components/resources/renderers/trivy-shared.tsx +0 -1
  224. package/src/components/resources/renderers/velero-cells.tsx +0 -1
  225. package/src/components/resources/resource-utils-argo.ts +0 -2
  226. package/src/components/resources/resource-utils-certmanager.ts +0 -2
  227. package/src/components/resources/resource-utils-eso.ts +0 -2
  228. package/src/components/resources/resource-utils-flux.ts +0 -2
  229. package/src/components/resources/resource-utils-istio.ts +0 -2
  230. package/src/components/resources/resource-utils-karpenter.ts +0 -2
  231. package/src/components/resources/resource-utils-knative.ts +0 -2
  232. package/src/components/resources/resource-utils-kyverno.ts +0 -2
  233. package/src/components/resources/resource-utils-prometheus.ts +0 -2
  234. package/src/components/resources/resource-utils-traefik.ts +0 -1
  235. package/src/components/resources/resource-utils-trivy.ts +0 -2
  236. package/src/components/resources/resource-utils-velero.ts +0 -2
  237. package/src/components/shared/EditableYamlView.tsx +0 -24
  238. package/src/components/timeline/DiffViewer.tsx +0 -1
  239. package/src/components/traffic/index.ts +0 -3
  240. package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
  241. package/src/components/ui/MetricsChart.tsx +0 -1
  242. package/src/components/ui/ResourceBar.tsx +0 -1
@@ -72,6 +72,9 @@ import { useRegisterShortcut } from '../../hooks/useKeyboardShortcuts'
72
72
  import { CodeViewer } from '../ui/CodeViewer'
73
73
  import { ArgoResourceDiffLoader } from './ArgoResourceDiffLoader'
74
74
  import { RevisionMetaChip } from './RevisionMetaChip'
75
+ import { RemoteDestinationCloudHint } from './RemoteDestinationCloudHint'
76
+
77
+ const GITOPS_HEALTH_DOCS_URL = 'https://radarhq.io/docs/features/gitops#per-resource-health'
75
78
  import type { GitOpsHistoryItem } from '@skyhook-io/k8s-ui'
76
79
 
77
80
  const GITOPS_KINDS: APIResource[] = [
@@ -634,6 +637,9 @@ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOps
634
637
  detail={detail}
635
638
  insight={insightsQ.data ?? null}
636
639
  insightLoading={insightsQ.isLoading}
640
+ healthDocsUrl={GITOPS_HEALTH_DOCS_URL}
641
+ remoteDestinationHint={<RemoteDestinationCloudHint />}
642
+ onOpenSettings={onOpenSettings}
637
643
  renderRevisionMeta={
638
644
  isArgoApp && insightsQ.data?.capabilities?.revisionMetadataAvailable
639
645
  ? (revision) => (
@@ -808,6 +814,7 @@ function GitOpsDetailView({ namespaces, onOpenResource, onOpenSettings }: GitOps
808
814
  onQueryChange={setGraphSearch}
809
815
  filters={graphFilters}
810
816
  showToolbar={false}
817
+ issues={insightsQ.data?.issues}
811
818
  />
812
819
  </div>
813
820
  </div>
@@ -0,0 +1,26 @@
1
+ import { useNavCustomization } from '../../context/NavCustomization'
2
+ import { useCapabilities } from '../../api/client'
3
+ import { openCloudFunnel } from '../CloudFunnelButton'
4
+
5
+ // The one sentence a standalone Radar adds after "this application deploys
6
+ // to another cluster": the thing the user just ran into is what Radar Cloud
7
+ // does. "Radar Cloud" opens the same dialog as the top-right button, so
8
+ // there is one pitch and one connect flow. That dialog connects THIS
9
+ // cluster; the destination is a second connect from Cloud, which is why the
10
+ // copy says "both clusters" and promises the view in Cloud, not here. Rendered only when that button
11
+ // is: the server decides whether there is anything to pitch (cloudConnect
12
+ // capability), and embedded hosts (Radar Cloud itself) mount neither.
13
+ export function RemoteDestinationCloudHint() {
14
+ const { embedded } = useNavCustomization()
15
+ const capabilities = useCapabilities()
16
+ if (embedded || !capabilities.data?.cloudConnect) return null
17
+ return (
18
+ <>
19
+ Connect both clusters to{' '}
20
+ <button type="button" onClick={openCloudFunnel} className="underline decoration-theme-border underline-offset-2 hover:text-theme-text-primary">
21
+ Radar Cloud
22
+ </button>{' '}
23
+ to see this app's resources together.
24
+ </>
25
+ )
26
+ }
@@ -32,6 +32,8 @@ import { getHelmStatusColor, getKindBadgeColor, SEVERITY_BADGE } from '../../uti
32
32
  import { formatDate } from './helm-utils'
33
33
  import { RoleGatedPanel } from './RoleGatedPanel'
34
34
  import { Tooltip } from '../ui/Tooltip'
35
+ import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
36
+ import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
35
37
 
36
38
  type DiffTone = 'success' | 'warning' | 'error' | 'info' | 'neutral'
37
39
 
@@ -342,6 +344,9 @@ function RevisionSelect({
342
344
  onChange: (revision: number) => void
343
345
  }) {
344
346
  const [open, setOpen] = useState(false)
347
+ // Presence outlives `open` by the menu exit so the list fades out in place;
348
+ // the outside-click / Escape listeners below still key off logical `open`.
349
+ const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('menu'))
345
350
  const [activeIndex, setActiveIndex] = useState(0)
346
351
  const rootRef = useRef<HTMLDivElement>(null)
347
352
  const listboxId = useId()
@@ -371,12 +376,14 @@ function RevisionSelect({
371
376
  if (open) setActiveIndex(selectedIndex)
372
377
  }, [open, selectedIndex])
373
378
 
379
+ // The listbox mounts a commit after `open` (presence hook); without
380
+ // `shouldRender` here a reopen at an unchanged activeIndex never scrolls.
374
381
  useEffect(() => {
375
- if (!open) return
382
+ if (!open || !shouldRender) return
376
383
  const revision = revisions[activeIndex]
377
384
  if (!revision) return
378
385
  document.getElementById(`${listboxId}-${revision.revision}`)?.scrollIntoView({ block: 'nearest' })
379
- }, [activeIndex, listboxId, open, revisions])
386
+ }, [activeIndex, listboxId, open, shouldRender, revisions])
380
387
 
381
388
  const selectRevision = (revision: number) => {
382
389
  setOpen(false)
@@ -438,11 +445,18 @@ function RevisionSelect({
438
445
  <span className="min-w-0 truncate">{selected ? formatRevisionOption(selected) : 'No revisions'}</span>
439
446
  <ChevronDown className={clsx('h-3.5 w-3.5 shrink-0 text-theme-text-tertiary transition-transform', open && 'rotate-180')} />
440
447
  </button>
441
- {open && (
448
+ {shouldRender && (
442
449
  <div
443
450
  id={listboxId}
444
451
  role="listbox"
445
- className="absolute right-0 top-full z-50 mt-1 max-h-80 w-[min(36rem,calc(100vw-2rem))] overflow-y-auto rounded-lg border border-theme-border bg-theme-surface p-1 shadow-theme-lg"
452
+ inert={!open || undefined}
453
+ className={clsx(
454
+ 'absolute right-0 top-full z-50 mt-1 max-h-80 w-[min(36rem,calc(100vw-2rem))] origin-top-right overflow-y-auto rounded-lg border border-theme-border bg-theme-surface p-1 shadow-theme-lg',
455
+ TRANSITION_MENU,
456
+ isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
457
+ !open && 'pointer-events-none',
458
+ )}
459
+ style={overlayTransitionStyle(isOpen, 'menu')}
446
460
  >
447
461
  {revisions.map((revision, index) => {
448
462
  const selectedRevision = revision.revision === value
@@ -4,7 +4,7 @@ import { FetchResult, useDockReservedHeight, compareVersions } from '@skyhook-io
4
4
  import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
5
5
  import { TRANSITION_DRAWER } from '../../utils/animation'
6
6
  import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
7
- import { X, Copy, Check, RefreshCw, Package, Code, History, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock, GitCompare, ExternalLink, ChevronRight, SlidersHorizontal, Eye, Loader2 } from 'lucide-react'
7
+ import { X, Copy, Check, RefreshCw, Package, Code, History, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2, GitBranch, AlertTriangle, RotateCcw, Clock, GitCompare, ExternalLink, SlidersHorizontal, Eye, Loader2 } from 'lucide-react'
8
8
  import yaml from 'yaml'
9
9
  import { useNavigate } from 'react-router-dom'
10
10
  import { clsx } from 'clsx'
@@ -25,6 +25,8 @@ import { ManifestViewer } from './ManifestViewer'
25
25
  import { ValuesViewer } from './ValuesViewer'
26
26
  import { OwnedResources } from './OwnedResources'
27
27
  import { TrackChartSourceDialog } from './TrackChartSourceDialog'
28
+ import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
29
+ import { Disclosure } from '../ui/Disclosure'
28
30
 
29
31
  interface HelmReleaseDrawerProps {
30
32
  release: SelectedHelmRelease
@@ -117,7 +119,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
117
119
  const [showTrackSource, setShowTrackSource] = useState(false)
118
120
  const [selectedVersion, setSelectedVersion] = useState<string | null>(null)
119
121
  const [adjustValues, setAdjustValues] = useState(false)
120
- const [renderAdjustValues, setRenderAdjustValues] = useState(false)
121
122
  const [editedUpgradeYaml, setEditedUpgradeYaml] = useState('')
122
123
  const [upgradeYamlError, setUpgradeYamlError] = useState<string | null>(null)
123
124
  const [upgradeValuesSeeded, setUpgradeValuesSeeded] = useState(false)
@@ -357,7 +358,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
357
358
  setUpgradeProgress([])
358
359
  setSelectedVersion(null)
359
360
  setAdjustValues(false)
360
- setRenderAdjustValues(false)
361
361
  setEditedUpgradeYaml('')
362
362
  editedUpgradeYamlRef.current = ''
363
363
  setUpgradeYamlError(null)
@@ -383,7 +383,6 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
383
383
  return
384
384
  }
385
385
 
386
- setRenderAdjustValues(true)
387
386
  setEditedUpgradeYaml('')
388
387
  editedUpgradeYamlRef.current = ''
389
388
  setUpgradeYamlError(null)
@@ -911,24 +910,15 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
911
910
  aria-controls="helm-upgrade-values-panel"
912
911
  className="flex items-center gap-1.5 text-sm font-medium text-theme-text-secondary hover:text-theme-text-primary disabled:opacity-50"
913
912
  >
914
- <ChevronRight className={clsx('w-4 h-4 transition-transform duration-200', adjustValues && 'rotate-90')} />
913
+ <CollapseChevron open={adjustValues} className="w-4 h-4" />
915
914
  <SlidersHorizontal className="w-3.5 h-3.5" />
916
915
  Adjust your values (optional)
917
916
  </button>
918
917
  </Tooltip>
919
- <div
920
- id="helm-upgrade-values-panel"
921
- className={`issue-details-motion ${adjustValues ? 'issue-details-motion-open' : ''}`}
922
- onTransitionEnd={(event) => {
923
- if (event.target !== event.currentTarget) return
924
- if (event.propertyName !== 'grid-template-rows') return
925
- if (!adjustValues) {
926
- setRenderAdjustValues(false)
927
- }
928
- }}
929
- >
930
- <div className="overflow-hidden">
931
- {renderAdjustValues && (
918
+ {/* unmountOnExit: the editor (uncontrolled textarea + seeded values)
919
+ must reset between opens, and the values query only runs while
920
+ the panel is open. */}
921
+ <Collapse open={adjustValues} unmountOnExit id="helm-upgrade-values-panel">
932
922
  <div className="mt-2">
933
923
  <p className="mb-2 text-xs text-theme-text-tertiary">
934
924
  These are your current settings — edit them to carry into {targetVersion}. What you see here is exactly what gets applied.
@@ -988,9 +978,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
988
978
  </Tooltip>
989
979
  </div>
990
980
  </div>
991
- )}
992
- </div>
993
- </div>
981
+ </Collapse>
994
982
  </div>
995
983
  )}
996
984
  {upgradeProgress.length > 0 && <ProgressLog entries={upgradeProgress} />}
@@ -1138,14 +1126,11 @@ function HelmOperationBanner({
1138
1126
  </div>
1139
1127
  <p className="mt-1 text-sm text-theme-text-secondary">{operation.message}</p>
1140
1128
  {hasRawMessage && (
1141
- <details className="mt-2">
1142
- <summary className="cursor-pointer select-none text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary">
1143
- Show raw Helm error
1144
- </summary>
1129
+ <Disclosure className="mt-2" summaryClassName="select-none text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary" summary="Show raw Helm error">
1145
1130
  <pre className="mt-2 max-h-32 overflow-auto whitespace-pre-wrap break-words rounded bg-theme-base/60 p-2 font-mono text-[11px] leading-relaxed text-theme-text-tertiary">
1146
1131
  {rawMessage}
1147
1132
  </pre>
1148
- </details>
1133
+ </Disclosure>
1149
1134
  )}
1150
1135
  {operation.failureDescription && !hasRawMessage && (
1151
1136
  <Tooltip content={operation.failureDescription} wrapperClassName="mt-1 flex">
@@ -9,6 +9,7 @@ import { useChartDetail, useNamespaces, useArtifactHubChart, installChartWithPro
9
9
  import { useCanHelmAct } from '../../api/client'
10
10
  import type { ChartSource, ChartDetail, ArtifactHubChartDetail } from '../../types'
11
11
  import { YamlEditor } from '../ui/YamlEditor'
12
+ import { Collapse, CollapseChevron, useDisclosure } from '@skyhook-io/k8s-ui/components/ui/Collapse'
12
13
  import { Tooltip } from '../ui/Tooltip'
13
14
  import { Markdown } from '../ui/Markdown'
14
15
  import { SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
@@ -656,6 +657,7 @@ interface ValuesStepProps {
656
657
 
657
658
  function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartDetail, source }: ValuesStepProps) {
658
659
  const [showEditor, setShowEditor] = useState(false)
660
+ const editorDisclosure = useDisclosure(showEditor)
659
661
 
660
662
  const isLocal = source === 'local'
661
663
  const localDetail = chartDetail as ChartDetail | undefined
@@ -700,11 +702,12 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
700
702
  {/* Collapsible editor section */}
701
703
  <div className="border border-theme-border rounded-lg overflow-hidden">
702
704
  <button
705
+ {...editorDisclosure.buttonProps}
703
706
  onClick={() => setShowEditor(!showEditor)}
704
707
  className="w-full flex items-center justify-between px-4 py-3 bg-theme-elevated/50 hover:bg-theme-elevated transition-colors"
705
708
  >
706
709
  <div className="flex items-center gap-2">
707
- <ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary transition-transform', showEditor && 'rotate-90')} />
710
+ <CollapseChevron open={showEditor} className="w-4 h-4" />
708
711
  <span className="text-sm font-medium text-theme-text-primary">
709
712
  {hasValues ? (showEditor ? 'Hide' : 'Show') : 'Add'} configuration values
710
713
  </span>
@@ -715,7 +718,9 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
715
718
  </div>
716
719
  </button>
717
720
 
718
- {showEditor && (
721
+ {/* unmountOnExit: the Monaco editor is heavy and its instance should
722
+ not outlive a closed section; values live in `valuesYaml` above. */}
723
+ <Collapse open={showEditor} unmountOnExit id={editorDisclosure.panelId}>
719
724
  <div className="p-4 border-t border-theme-border">
720
725
  {/* Action buttons */}
721
726
  <div className="flex items-center gap-3 mb-4">
@@ -777,7 +782,7 @@ function ValuesStep({ valuesYaml, setValuesYaml, yamlError, setYamlError, chartD
777
782
  </div>
778
783
  )}
779
784
  </div>
780
- )}
785
+ </Collapse>
781
786
  </div>
782
787
  </div>
783
788
  )
@@ -1,8 +1,9 @@
1
1
  import { useRef, useEffect, useState, useCallback } from 'react'
2
- import { X, Copy, Check, Radio, Terminal, MessageSquare, Code2, ChevronRight, Pin } from 'lucide-react'
2
+ import { X, Copy, Check, Radio, Terminal, MessageSquare, Code2, Pin } from 'lucide-react'
3
3
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
4
4
  import { MCP_TOOL_CATALOG } from './mcpToolCatalog'
5
5
  import { Tooltip } from '../ui/Tooltip'
6
+ import { Disclosure } from '../ui/Disclosure'
6
7
 
7
8
  interface MCPSetupDialogProps {
8
9
  open: boolean
@@ -285,17 +286,22 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
285
286
  { icon: Terminal, name: 'OpenAI Codex', path: '~/.codex/config.toml', config: codexConfig },
286
287
  { icon: Terminal, name: 'Gemini CLI', path: '~/.gemini/settings.json', config: geminiConfig },
287
288
  ].map((agent) => (
288
- <details key={agent.name} className="group rounded-md border border-theme-border/50 bg-theme-base/30">
289
- <summary className="flex items-center gap-2 px-3 py-2 select-none list-none hover:bg-theme-hover/50 rounded-md transition-colors [&::-webkit-details-marker]:hidden">
290
- <ChevronRight className="w-3.5 h-3.5 text-theme-text-tertiary transition-transform group-open:rotate-90" />
291
- <agent.icon className="w-4 h-4 text-theme-text-tertiary" />
292
- <span className="text-sm font-medium text-theme-text-primary">{agent.name}</span>
293
- {agent.path && <span className="text-[10px] text-theme-text-tertiary ml-auto">{agent.path}</span>}
294
- </summary>
289
+ <Disclosure
290
+ key={agent.name}
291
+ className="rounded-md border border-theme-border/50 bg-theme-base/30"
292
+ summaryClassName="gap-2 px-3 py-2 select-none hover:bg-theme-hover/50 rounded-md transition-colors"
293
+ summary={
294
+ <>
295
+ <agent.icon className="w-4 h-4 text-theme-text-tertiary" />
296
+ <span className="text-sm font-medium text-theme-text-primary">{agent.name}</span>
297
+ {agent.path && <span className="text-[10px] text-theme-text-tertiary ml-auto">{agent.path}</span>}
298
+ </>
299
+ }
300
+ >
295
301
  <div className="px-3 pb-3 pt-1">
296
302
  <CodeBlock>{agent.config}</CodeBlock>
297
303
  </div>
298
- </details>
304
+ </Disclosure>
299
305
  ))}
300
306
  </div>
301
307
 
@@ -33,6 +33,7 @@ import { openExternal } from '../../utils/navigation'
33
33
  import { apiUrl } from '../../api/config'
34
34
  import { apiFetch, useCapabilities } from '../../api/client'
35
35
  import { pluralize } from '@skyhook-io/k8s-ui'
36
+ import { Collapse } from '@skyhook-io/k8s-ui/components/ui/Collapse'
36
37
 
37
38
  // --- Types -------------------------------------------------------------------
38
39
 
@@ -661,17 +662,10 @@ export function PortForwardPanel() {
661
662
  keeping border and rounded corners correct at every intermediate height. */}
662
663
  <div className="overflow-hidden rounded-xl bg-theme-surface dark:bg-theme-elevated border-2 border-skyhook-500/35 dark:border-skyhook-400/40 shadow-2xl dark:shadow-[0_24px_60px_-12px_rgba(0,0,0,0.75),0_10px_24px_-6px_rgba(0,0,0,0.45)]">
663
664
 
664
- {/* Grid sizer — the height engine. grid-template-rows 0fr→1fr animates
665
- height from 0 to auto. Content clips from the bottom up, creating a
666
- natural top-to-bottom reveal (header appears first, sessions follow). */}
667
- <div
668
- className={clsx(
669
- 'grid transition-[grid-template-rows] duration-300',
670
- isPanelOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'
671
- )}
672
- style={{ transitionTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)' }}
673
- >
674
- <div className="overflow-hidden">
665
+ {/* Height engine — Collapse animates grid-template-rows 0fr→1fr so the
666
+ shell grows from 0 to auto. Content clips from the bottom up, creating
667
+ a natural top-to-bottom reveal (header appears first, sessions follow). */}
668
+ <Collapse open={isPanelOpen}>
675
669
 
676
670
  {/* Header — tinted green when all sessions running, red when any have failed. */}
677
671
  <div
@@ -927,8 +921,7 @@ export function PortForwardPanel() {
927
921
  )}
928
922
  </div>
929
923
 
930
- </div>{/* /overflow-hidden */}
931
- </div>{/* /grid-sizer */}
924
+ </Collapse>
932
925
  </div>{/* /panel-shell */}
933
926
 
934
927
  {/* Caret — rendered after the shell so it paints on top (z-10). Opaque fill
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { computeRequestLimitLines } from './PrometheusCharts'
3
+
4
+ describe('Pod request and limit overlays', () => {
5
+ it('does not draw a Pod limit when one runtime container is unlimited', () => {
6
+ const resource = { spec: { containers: [
7
+ { resources: { requests: { cpu: '100m' }, limits: { cpu: '1' } } },
8
+ { resources: { requests: { cpu: '200m' } } },
9
+ ] } }
10
+ const lines = computeRequestLimitLines(resource, 'Pod', 'cpu')!
11
+ expect(lines).toHaveLength(1)
12
+ expect(lines[0].kind).toBe('request')
13
+ expect(lines[0].value).toBeCloseTo(0.3)
14
+ })
15
+ it('sums fully limited runtime containers at per-Pod scale', () => {
16
+ const resource = { spec: { replicas: 5, template: { spec: {
17
+ containers: [{ resources: { limits: { cpu: '1' } } }],
18
+ initContainers: [{ restartPolicy: 'Always', resources: { limits: { cpu: '200m' } } }],
19
+ } } } }
20
+ expect(computeRequestLimitLines(resource, 'Deployment', 'cpu')![0].value).toBeCloseTo(1.2)
21
+ })
22
+ })
@@ -121,6 +121,7 @@ export function PrometheusCharts({ kind, namespace, name, showEmptyState = false
121
121
  statusError={status?.error}
122
122
  onConnect={() => connectMutation.mutate()}
123
123
  connecting={connectMutation.isPending}
124
+ discovering={status?.discovering === true}
124
125
  category={activeCategory}
125
126
  onCategoryChange={setActiveCategory}
126
127
  range={timeRange}
@@ -161,12 +162,12 @@ export function computeRequestLimitLines(
161
162
  if (runtimeContainers.length === 0) return undefined
162
163
 
163
164
  let reqSum = 0, reqAny = false
164
- let limSum = 0, limAny = false
165
+ let limSum = 0, limAll = true
165
166
  for (const c of runtimeContainers) {
166
167
  const req = readQuantity(c.resources?.requests?.[category], category)
167
168
  const lim = readQuantity(c.resources?.limits?.[category], category)
168
169
  if (req != null) { reqSum += req; reqAny = true }
169
- if (lim != null) { limSum += lim; limAny = true }
170
+ if (lim != null && lim > 0) { limSum += lim } else { limAll = false }
170
171
  }
171
172
 
172
173
  const lines: ReferenceLine[] = []
@@ -177,7 +178,7 @@ export function computeRequestLimitLines(
177
178
  kind: 'request',
178
179
  })
179
180
  }
180
- if (limAny) {
181
+ if (limAll) {
181
182
  lines.push({
182
183
  value: limSum,
183
184
  label: `limit ${formatRequestLimitLabel(limSum, category)}`,
@@ -0,0 +1,99 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { MemoryRouter } from 'react-router-dom'
4
+ import { NavCustomizationProvider } from '../../context/NavCustomization'
5
+ import type { WorkloadMetrics } from '../../api/workloadMetrics'
6
+
7
+ let connected = false
8
+ const ranges: string[] = []
9
+ const charts: { seriesLabels?: string[]; referenceLines?: unknown[] }[] = []
10
+ let workload: WorkloadMetrics
11
+
12
+ vi.mock('../../api/client', async (importActual) => ({
13
+ ...(await importActual<typeof import('../../api/client')>()),
14
+ useAutoPromConnect: () => undefined,
15
+ usePrometheusStatus: () => ({ data: { connected, error: 'Backend query failed' } }),
16
+ usePrometheusConnect: () => ({ mutate: vi.fn(), isPending: false }),
17
+ usePrometheusResourceMetrics: (_kind: string, _ns: string, _name: string, _category: string, range: string) => {
18
+ ranges.push(range)
19
+ return { data: { unit: 'cores', result: { series: [{ labels: { pod: 'web-0' }, dataPoints: [{ timestamp: 100, value: 1 }, { timestamp: 160, value: 2 }] }] } } }
20
+ },
21
+ }))
22
+ vi.mock('../../api/workloadMetrics', () => ({ useWorkloadMetrics: () => ({ data: workload }) }))
23
+ vi.mock('./RestartChart', () => ({ RestartEventLane: () => null }))
24
+ vi.mock('./RightsizingStrip', () => ({ RightsizingStrip: () => null }))
25
+ vi.mock('@skyhook-io/k8s-ui/components/charts', async (importActual) => ({
26
+ ...(await importActual<typeof import('@skyhook-io/k8s-ui/components/charts')>()),
27
+ AreaChart: (props: typeof charts[number]) => { charts.push(props); return <span>Chart</span> },
28
+ }))
29
+
30
+ const { PrometheusChartsGrid } = await import('./PrometheusChartsGrid')
31
+ const resource = { spec: { template: { spec: { containers: [{ resources: { requests: { cpu: '100m' }, limits: { cpu: '200m' } } }] } } } }
32
+ const render = (url = '/workloads/Deployment/demo/web?tab=metrics', embedded = false) => renderToString(
33
+ <MemoryRouter initialEntries={[url]}><NavCustomizationProvider value={embedded ? { embedded: true, rightExtras: null } : {}}>
34
+ <PrometheusChartsGrid kind="Deployment" namespace="demo" name="web" resource={resource} />
35
+ </NavCustomizationProvider></MemoryRouter>,
36
+ )
37
+
38
+ beforeEach(() => {
39
+ connected = false
40
+ ranges.length = 0
41
+ charts.length = 0
42
+ workload = { state: 'available', sources: [], pods: 1, podsTotal: 1, start: 100, end: 160, stepSeconds: 60, rateWindowSeconds: 300, history: {}, comparison: {}, panels: {} }
43
+ })
44
+
45
+ describe('Expanded metrics recovery and scope', () => {
46
+ it('offers standalone setup without losing the backend error or retry', () => {
47
+ const html = render()
48
+ expect(html).toContain('Backend query failed')
49
+ expect(html).toContain('Discover Prometheus')
50
+ expect(html).toContain('Configure metrics')
51
+ expect(html).toContain('workload-metrics.md#what-each-chart-needs')
52
+ })
53
+ it('uses operator guidance instead of standalone settings when embedded', () => {
54
+ const html = render(undefined, true)
55
+ expect(html).toContain('Ask your operator')
56
+ expect(html).not.toContain('Configure metrics')
57
+ expect(html).toContain('Discover Prometheus')
58
+ })
59
+ it.each([
60
+ ['/workloads/Deployment/demo/web?tab=metrics&metricsRange=3h', '3h'],
61
+ ['/applications?app=demo&workload=Deployment%2Fdemo%2Fweb&tab=metrics&metricsRange=6h', '6h'],
62
+ ['/applications?tab=metrics&metricsRange=invalid', '1h'],
63
+ ['/applications?tab=metrics', '1h'],
64
+ ])('reads and validates the URL range: %s', (url, expected) => {
65
+ connected = true
66
+ const html = render(url)
67
+ expect(html).toContain(`value="${expected}" selected=""`)
68
+ expect(ranges.length).toBeGreaterThan(0)
69
+ expect(ranges.every((range) => range === expected)).toBe(true)
70
+ })
71
+ it('leaves unverified charts accessible without template overlays or saturation', () => {
72
+ connected = true
73
+ const html = render()
74
+ expect(html).toContain('<details aria-label="Name-matched resource metrics">')
75
+ expect(html).toContain('Network and storage')
76
+ expect(html).not.toContain('% of limit')
77
+ expect(charts.length).toBeGreaterThan(0)
78
+ expect(charts.every((chart) => !chart.referenceLines?.length)).toBe(true)
79
+ })
80
+ it('labels unlabeled request totals without changing aggregate and quantile labels', () => {
81
+ connected = true
82
+ const series = [{ labels: {}, dataPoints: [{ timestamp: 160, value: 1 }] }]
83
+ workload.panels.requests = { state: 'available', unit: 'requests/s', series }
84
+ workload.panels.p50 = { state: 'available', unit: 'seconds', series }
85
+ workload.panels.p95 = { state: 'available', unit: 'seconds', series }
86
+ workload.panels.cpu = { state: 'available', unit: 'cores', series: [{ ...series[0], labels: { aggregation: 'Workload' } }, { ...series[0], labels: { aggregation: 'Maximum Pod' } }] }
87
+ render()
88
+ expect(charts.map((chart) => chart.seriesLabels)).toContainEqual(['Requests / sec'])
89
+ expect(charts.map((chart) => chart.seriesLabels)).toContainEqual(['p50', 'p95'])
90
+ expect(charts.map((chart) => chart.seriesLabels)).toContainEqual(['Workload', 'Maximum Pod'])
91
+ })
92
+ it('retains template references on verified current-Pod charts', () => {
93
+ connected = true
94
+ workload.history.cpu = { mode: 'current-pods' }
95
+ workload.panels.cpu = { state: 'available', unit: 'cores', series: [{ labels: { pod: 'web-0' }, dataPoints: [{ timestamp: 160, value: 0.1 }] }] }
96
+ render()
97
+ expect(charts.some((chart) => chart.referenceLines?.some((line) => (line as { label: string }).label === 'Template request 100m'))).toBe(true)
98
+ })
99
+ })