@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
@@ -0,0 +1,47 @@
1
+ import { useState, type ReactNode } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import { Collapse, CollapseChevron, useDisclosure } from '@skyhook-io/k8s-ui/components/ui/Collapse'
4
+
5
+ // In-flow disclosure with the app-standard motion — the replacement for native
6
+ // <details>/<summary> on primary paths. Native details snaps open with no
7
+ // transition and its marker ignores the caret language; this pairs a button
8
+ // header (aria-expanded / aria-controls via useDisclosure) with CollapseChevron
9
+ // and an animated Collapse panel. Content stays mounted while closed, exactly
10
+ // like native details, so server-rendered markup and in-page search still see
11
+ // the collapsed copy.
12
+ export function Disclosure({
13
+ summary,
14
+ children,
15
+ className,
16
+ summaryClassName,
17
+ chevronClassName = 'h-3.5 w-3.5',
18
+ defaultOpen = false,
19
+ }: {
20
+ summary: ReactNode
21
+ children: ReactNode
22
+ /** Wrapper (what the old <details> carried). */
23
+ className?: string
24
+ /** Header button (what the old <summary> carried). */
25
+ summaryClassName?: string
26
+ chevronClassName?: string
27
+ defaultOpen?: boolean
28
+ }) {
29
+ const [open, setOpen] = useState(defaultOpen)
30
+ const disclosure = useDisclosure(open)
31
+ return (
32
+ <div className={className}>
33
+ <button
34
+ type="button"
35
+ {...disclosure.buttonProps}
36
+ onClick={() => setOpen((value) => !value)}
37
+ className={clsx('flex w-full cursor-pointer items-center gap-1.5 text-left', summaryClassName)}
38
+ >
39
+ <CollapseChevron open={open} className={chevronClassName} />
40
+ {summary}
41
+ </button>
42
+ <Collapse open={open} id={disclosure.panelId}>
43
+ {children}
44
+ </Collapse>
45
+ </div>
46
+ )
47
+ }
@@ -2,12 +2,20 @@ import ReactMarkdown from 'react-markdown'
2
2
  import remarkGfm from 'remark-gfm'
3
3
  import { clsx } from 'clsx'
4
4
 
5
+ import type { ReactNode } from 'react'
6
+
5
7
  interface MarkdownProps {
6
8
  children: string
7
9
  className?: string
10
+ /**
11
+ * Intercepts links before the default anchor renders. Return null to fall
12
+ * through. Lets a caller turn a reserved href (an in-page evidence
13
+ * reference, say) into a control instead of an external link.
14
+ */
15
+ linkRenderer?: (href: string | undefined, children: ReactNode) => ReactNode | null
8
16
  }
9
17
 
10
- export function Markdown({ children, className }: MarkdownProps) {
18
+ export function Markdown({ children, className, linkRenderer }: MarkdownProps) {
11
19
  return (
12
20
  <div className={clsx('markdown-content', className)}>
13
21
  <ReactMarkdown
@@ -28,16 +36,20 @@ export function Markdown({ children, className }: MarkdownProps) {
28
36
  p: ({ children }) => (
29
37
  <p className="text-theme-text-secondary my-2 leading-relaxed">{children}</p>
30
38
  ),
31
- a: ({ href, children }) => (
32
- <a
33
- href={href}
34
- target="_blank"
35
- rel="noopener noreferrer"
36
- className="text-blue-400 hover:text-blue-300 underline underline-offset-2"
37
- >
38
- {children}
39
- </a>
40
- ),
39
+ a: ({ href, children }) => {
40
+ const custom = linkRenderer?.(href, children)
41
+ if (custom !== null && custom !== undefined) return <>{custom}</>
42
+ return (
43
+ <a
44
+ href={href}
45
+ target="_blank"
46
+ rel="noopener noreferrer"
47
+ className="text-blue-400 hover:text-blue-300 underline underline-offset-2"
48
+ >
49
+ {children}
50
+ </a>
51
+ )
52
+ },
41
53
  ul: ({ children }) => (
42
54
  <ul className="list-disc list-outside pl-4 my-2 space-y-1 text-theme-text-secondary">{children}</ul>
43
55
  ),
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useRef } from 'react'
2
2
  import { X } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
4
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayTransitionStyle } from '../../utils/animation'
5
5
  import { useActiveShortcuts, type ShortcutCategory } from '../../hooks/useKeyboardShortcuts'
6
6
 
7
7
  interface ShortcutHelpOverlayProps {
@@ -232,6 +232,7 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
232
232
  TRANSITION_BACKDROP,
233
233
  isOpen ? 'opacity-100' : 'opacity-0'
234
234
  )}
235
+ style={overlayTransitionStyle(isOpen, 'dialog')}
235
236
  onClick={onClose}
236
237
  />
237
238
 
@@ -243,6 +244,7 @@ export function ShortcutHelpOverlay({ onClose, currentView, isOpen = true }: Sho
243
244
  TRANSITION_PANEL,
244
245
  isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-[0.97]'
245
246
  )}
247
+ style={overlayTransitionStyle(isOpen, 'dialog')}
246
248
  >
247
249
  {/* Header */}
248
250
  <div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border">
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect } from 'react'
2
+ import { clsx } from 'clsx'
2
3
  import { Download, X, Copy, Check, RotateCw, ArrowDownToLine, Loader2 } from 'lucide-react'
3
4
  import { useQueryClient } from '@tanstack/react-query'
4
5
  import {
@@ -10,6 +11,8 @@ import {
10
11
  } from '../../api/client'
11
12
  import type { DesktopUpdateState } from '../../api/client'
12
13
  import { WithTooltip } from './Tooltip'
14
+ import { TRANSITION_MENU, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
15
+ import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
13
16
 
14
17
  const DISMISSED_KEY = 'radar-update-dismissed'
15
18
 
@@ -103,7 +106,11 @@ export function UpdateNotification() {
103
106
 
104
107
  // Shared in-cluster viewers get a persistent Home notice instead of a
105
108
  // floating action prompt they may not be able to act on.
106
- if (!versionInfo?.updateAvailable || dismissed || deploymentMode === undefined || deploymentMode === 'in-cluster' || deploymentMode === 'cloud') {
109
+ const show = !!versionInfo?.updateAvailable && !dismissed && deploymentMode !== undefined && deploymentMode !== 'in-cluster' && deploymentMode !== 'cloud'
110
+ // Presence outlives `show` by the menu exit so a dismiss fades the chip out
111
+ // instead of snapping it away; the enter runs the same transition in reverse.
112
+ const { shouldRender, isOpen } = useAnimatedUnmount(show, overlayExitMs('menu'))
113
+ if (!shouldRender || !versionInfo) {
107
114
  return null
108
115
  }
109
116
 
@@ -111,7 +118,16 @@ export function UpdateNotification() {
111
118
  const effectiveState: DesktopUpdateState = updateStatus?.state ?? 'idle'
112
119
 
113
120
  return (
114
- <div className="fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-accent/50 rounded-lg shadow-xl p-4 animate-in slide-in-from-right">
121
+ <div
122
+ inert={!show || undefined}
123
+ className={clsx(
124
+ 'fixed bottom-4 right-4 z-50 max-w-sm bg-theme-surface border border-accent/50 rounded-lg shadow-xl p-4 origin-bottom-right',
125
+ TRANSITION_MENU,
126
+ isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-1 scale-[0.97]',
127
+ !show && 'pointer-events-none',
128
+ )}
129
+ style={overlayTransitionStyle(isOpen, 'menu')}
130
+ >
115
131
  <div className="flex items-start gap-3">
116
132
  <div className="flex items-center justify-center w-8 h-8 bg-accent-muted rounded-full shrink-0">
117
133
  <UpdateIcon state={effectiveState} />
@@ -70,6 +70,8 @@ import {
70
70
  useCordonNode,
71
71
  useUncordonNode,
72
72
  useDrainNode,
73
+ useDrainPlan,
74
+ DrainPlanUnsupportedError,
73
75
  useCascadeDeletePreview,
74
76
  useResourceEvents,
75
77
  useResource,
@@ -81,7 +83,7 @@ import {
81
83
  import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
82
84
  import { PrometheusChartsGrid } from '../resource/PrometheusChartsGrid'
83
85
  import { RestartEventLane } from '../resource/RestartChart'
84
- import { RightsizingPanel, RightsizingStrip } from '../resource/RightsizingStrip'
86
+ import { RightsizingPanel } from '../resource/RightsizingStrip'
85
87
  import { WorkloadCostTab } from '../cost/WorkloadCostTab'
86
88
  import { isOpenCostWorkloadKind } from '../cost/kinds'
87
89
  import { useResourceAudit, useResourceIssues, useResources, useTrace, fetchTraceWithProbes, fetchInClusterCapability, runInClusterMerged } from '../../api/client'
@@ -381,6 +383,7 @@ function useActionsBarProps(
381
383
  const cordonMutation = useCordonNode()
382
384
  const uncordonMutation = useUncordonNode()
383
385
  const drainMutation = useDrainNode()
386
+ const drainPlanMutation = useDrainPlan()
384
387
 
385
388
  const { renderAction: renderDiagnose } = useDiagnoseCustomization()
386
389
 
@@ -485,6 +488,13 @@ function useActionsBarProps(
485
488
  onDrainNode: (params: Parameters<typeof drainMutation.mutate>[0]) =>
486
489
  drainMutation.mutate(params),
487
490
  isDrainingNode: drainMutation.isPending,
491
+ onPlanDrain: (params: Parameters<typeof drainPlanMutation.mutate>[0]) =>
492
+ drainPlanMutation.mutate(params),
493
+ onPlanDrainReset: () => drainPlanMutation.reset(),
494
+ drainPlan: drainPlanMutation.data ?? null,
495
+ isPlanningDrain: drainPlanMutation.isPending,
496
+ drainPlanError: drainPlanMutation.error?.message ?? null,
497
+ drainPlanUnsupported: drainPlanMutation.error instanceof DrainPlanUnsupportedError,
488
498
  }
489
499
  }
490
500
 
@@ -2411,16 +2421,9 @@ function MetricsTabContent({
2411
2421
  resource: any
2412
2422
  expanded: boolean
2413
2423
  }) {
2414
- const showRightsizing = expanded && ['Deployment', 'StatefulSet', 'DaemonSet'].includes(kind)
2415
-
2416
2424
  if (expanded) {
2417
2425
  return (
2418
2426
  <div className="flex flex-col h-full">
2419
- {showRightsizing && (
2420
- <div className="px-4 pt-4">
2421
- <RightsizingStrip kind={kind} namespace={namespace} name={name} />
2422
- </div>
2423
- )}
2424
2427
  <div className="flex-1 min-h-0">
2425
2428
  <PrometheusChartsGrid kind={kind} namespace={namespace} name={name} resource={resource} />
2426
2429
  </div>
@@ -2446,9 +2449,17 @@ function DrawerMetricsContent({
2446
2449
  }) {
2447
2450
  const [chartRange, setChartRange] = useState<import('../../api/client').PrometheusTimeRange>('1h')
2448
2451
  const showRestartLane = isPrometheusSupported(kind) && kind !== 'Node'
2452
+ const navigate = useNavigate()
2449
2453
 
2450
2454
  return (
2451
2455
  <div className="flex flex-col h-full">
2456
+ {['Deployment', 'StatefulSet', 'DaemonSet'].includes(kind) && (
2457
+ <div className="px-4 pt-3">
2458
+ <button className="text-xs text-accent hover:underline" onClick={() => navigate(`${buildWorkloadPath({ kind, namespace, name })}?tab=metrics&metricsRange=${chartRange}`)}>
2459
+ Open request and resource dashboard →
2460
+ </button>
2461
+ </div>
2462
+ )}
2452
2463
  <div className="flex-1 min-h-0">
2453
2464
  <PrometheusCharts
2454
2465
  kind={kind}
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react'
2
2
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
3
  import { getApiBase } from '../api/config'
4
- import { apiFetch } from '../api/client'
4
+ import { apiFetch, type SyncStatusSnapshot } from '../api/client'
5
5
 
6
6
  export type ConnectionStateType = 'connected' | 'disconnected' | 'connecting'
7
7
 
@@ -12,6 +12,10 @@ export interface ConnectionState {
12
12
  error?: string
13
13
  errorType?: string // config, auth, auth-rejected, auth-plugin-stuck, rbac, network, timeout, tls, unknown
14
14
  progressMessage?: string
15
+ // Per-kind readiness while the initial informer sync runs ('connecting'
16
+ // with informers started). Drives the progressive app shell — absent once
17
+ // connected or before informers exist.
18
+ syncStatus?: SyncStatusSnapshot
15
19
  }
16
20
 
17
21
  interface ConnectionStatusResponse extends ConnectionState {
@@ -157,7 +161,15 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
157
161
  if (!firstConnect || cacheWarmAtMountRef.current) {
158
162
  queryClient.invalidateQueries()
159
163
  } else {
160
- queryClient.invalidateQueries({ predicate: (q) => q.state.status === 'error' })
164
+ // Detail responses served during the progressive shell are partial —
165
+ // the server skips relationship computation while the topology cache
166
+ // is unavailable — and they resolve as successes, so the error-only
167
+ // sweep would leave an open drawer without its Related Resources
168
+ // forever (focus refetch is globally off). Refetching the resource
169
+ // detail keys is at most the open drawer, so it stays cheap.
170
+ queryClient.invalidateQueries({
171
+ predicate: (q) => q.state.status === 'error' || q.queryKey[0] === 'resource',
172
+ })
161
173
  }
162
174
  }, [queryClient])
163
175
 
@@ -195,12 +207,35 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
195
207
  // while the retry is still running; the retry's own result supersedes it.
196
208
  if (manualRetryPendingRef.current || autoRetryInFlightRef.current) return
197
209
  const current = connectionRef.current
210
+ // The progressive shell renders real resource lists while still
211
+ // 'connecting'. A query client shared across cluster mounts may hold
212
+ // another cluster's data under identical keys — drop it before the
213
+ // shell can show it (the 'connected' path repeats this for the
214
+ // full-refresh case). Deliberately ahead of the SSE-generation guard:
215
+ // live connection_state frames bump the generation and drop the poll,
216
+ // and the shell must not spend the whole sync on another cluster's rows.
217
+ if (current.state === 'connecting' && data.state === 'connecting' &&
218
+ data.syncStatus && cacheWarmAtMountRef.current) {
219
+ cacheWarmAtMountRef.current = false
220
+ // Drop only cluster-scoped data. App chrome (auth identity, kubeconfig
221
+ // contexts) is not another cluster's data, and purging it flips
222
+ // authMePending back on — which holds the whole shell on the splash
223
+ // until /auth/me refetches, delaying the first progressive paint.
224
+ const keep = new Set(['connection-status', 'auth-me', 'contexts'])
225
+ queryClient.removeQueries({ predicate: (q) => !keep.has(String(q.queryKey[0])) })
226
+ }
198
227
  if (!shouldApplyPolledConnection(
199
228
  current.state,
200
229
  data.state,
201
230
  data.sseGenerationAtStart,
202
231
  sseGenerationRef.current,
203
232
  )) {
233
+ // SSE owns the state fields, but connection_state frames don't carry
234
+ // per-kind sync progress — merge it from the poll so the progressive
235
+ // shell keeps ticking while still connecting.
236
+ if (data.state === 'connecting' && data.syncStatus) {
237
+ setConnection(prev => prev.state === 'connecting' ? { ...prev, syncStatus: data.syncStatus } : prev)
238
+ }
204
239
  return
205
240
  }
206
241
  const becameConnected = current.state !== 'connected' && data.state === 'connected'
@@ -211,11 +246,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
211
246
  error: data.error,
212
247
  errorType: data.errorType,
213
248
  progressMessage: data.progressMessage,
249
+ syncStatus: data.syncStatus,
214
250
  })
215
251
  if (becameConnected) {
216
252
  refreshCachesOnConnect()
217
253
  }
218
- }, [data, dataUpdatedAt, refreshCachesOnConnect])
254
+ }, [data, dataUpdatedAt, refreshCachesOnConnect, queryClient])
219
255
 
220
256
  // Retry mutation
221
257
  const retryMutation = useMutation({
@@ -231,6 +267,9 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
231
267
  state: 'connecting',
232
268
  error: undefined,
233
269
  progressMessage: 'Connecting to cluster...',
270
+ // A retry is a fresh attempt — per-kind progress from the previous
271
+ // attempt would mislabel readiness until the next poll.
272
+ syncStatus: undefined,
234
273
  }))
235
274
  },
236
275
  onSuccess: (result) => {
@@ -371,6 +410,12 @@ export function ConnectionProvider({ children }: { children: ReactNode }) {
371
410
  if (prev.state === 'connected' && status.state === 'connecting') {
372
411
  return prev
373
412
  }
413
+ // connection_state frames don't carry per-kind sync progress — keep the
414
+ // last polled snapshot while still connecting, or each SSE progress
415
+ // frame would flicker the progressive shell back to the splash.
416
+ if (status.state === 'connecting' && !status.syncStatus && prev.state === 'connecting') {
417
+ return { ...status, syncStatus: prev.syncStatus }
418
+ }
374
419
  return status
375
420
  })
376
421
 
@@ -23,6 +23,7 @@ export type RenderInvestigationRunActions = (props: {
23
23
 
24
24
  /** Render prop for the resource-level Investigate action. */
25
25
  export type RenderDiagnoseAction = (ctx: {
26
+ /** Kubernetes Kind when loaded or discovered; otherwise its API resource name. */
26
27
  kind: string;
27
28
  /** Kubernetes API group; empty means core. */
28
29
  group?: string;
package/src/index.css CHANGED
@@ -21,6 +21,23 @@
21
21
  }
22
22
 
23
23
  @layer components {
24
+ .metrics-layout {
25
+ container-type: inline-size;
26
+ container-name: metrics;
27
+ }
28
+ .metrics-chart-grid {
29
+ display: grid;
30
+ grid-template-columns: minmax(0, 1fr);
31
+ gap: 0.75rem;
32
+ }
33
+ @container metrics (min-width: 760px) {
34
+ .metrics-chart-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
35
+ .metrics-chart-wide { grid-column: 1 / -1; }
36
+ }
37
+ @container metrics (min-width: 1200px) {
38
+ .metrics-chart-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
39
+ .metrics-chart-wide { grid-column: auto; }
40
+ }
24
41
  .investigation-assessment {
25
42
  background: var(--color-investigation-assessment);
26
43
  border-color: var(--color-investigation-assessment-border);
@@ -327,10 +344,26 @@ body {
327
344
  animation: status-enter 140ms ease-out both;
328
345
  }
329
346
 
347
+ /* Reduced motion is an app-level policy, not a per-class list. Durations and
348
+ delays go to ~0 (a delay left in place would hold a fill-mode:both element
349
+ invisible), every loop runs once (an infinite animation at 0.01ms is still an
350
+ active animation — pulse, ping, Monaco's progress bit), and status spinners
351
+ keep turning because they convey state, not motion. Components that coordinate motion in
352
+ JS (presence hooks, drawer morph) check prefersReducedMotion() themselves and
353
+ dispose at logical close. */
330
354
  @media (prefers-reduced-motion: reduce) {
331
- .animate-transcript-enter,
332
- .animate-status-enter {
333
- animation: none;
355
+ *, *::before, *::after {
356
+ animation-duration: 0.01ms !important;
357
+ animation-delay: 0ms !important;
358
+ animation-iteration-count: 1 !important;
359
+ transition-duration: 0.01ms !important;
360
+ transition-delay: 0ms !important;
361
+ scroll-behavior: auto !important;
362
+ }
363
+ /* The one exception: a spinner that stops reads as "done", not "calm". */
364
+ .animate-spin {
365
+ animation-duration: 1s !important;
366
+ animation-iteration-count: infinite !important;
334
367
  }
335
368
  }
336
369
 
@@ -386,9 +419,6 @@ body {
386
419
  -webkit-text-fill-color: currentColor;
387
420
  color: var(--text-secondary);
388
421
  }
389
- .animate-result-in {
390
- animation: none;
391
- }
392
422
  }
393
423
 
394
424
  /* Gentle colorize when syntax-highlighted code swaps in over plain text — opacity
@@ -14,7 +14,7 @@ export function searchHitToSelectedResource(hit: SearchHit): SelectedResource {
14
14
  }
15
15
 
16
16
  // Re-export shared navigation utilities from @skyhook-io/k8s-ui.
17
- export { kindToPlural, kindToPluralWithGroup, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
17
+ export { kindToPlural, kindToPluralWithGroup, pluralToKind, knownKindForPluralWithGroup, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
18
18
  export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
19
19
 
20
20
  const NETWORK_POLICY_TOPOLOGY_KINDS = new Set([
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { TopologyNode } from '@skyhook-io/k8s-ui/types/core'
3
+ import { isInNamespaceScope, scopeNodesToNamespaces } from './topology-namespace'
4
+
5
+ function node(id: string, kind: string, namespace?: string): TopologyNode {
6
+ return {
7
+ id,
8
+ kind,
9
+ name: id,
10
+ status: 'healthy',
11
+ data: namespace === undefined ? {} : { namespace },
12
+ } as TopologyNode
13
+ }
14
+
15
+ const nodes = [
16
+ node('web', 'Deployment', 'prod'),
17
+ node('ing', 'Ingress', 'staging'),
18
+ node('db', 'StatefulSet', 'prod'),
19
+ node('node-1', 'Node'), // cluster-scoped
20
+ node('pv-1', 'PersistentVolume'), // cluster-scoped
21
+ ]
22
+
23
+ describe('scopeNodesToNamespaces', () => {
24
+ it('returns every node when no namespace is selected', () => {
25
+ expect(scopeNodesToNamespaces(nodes, [])).toHaveLength(5)
26
+ })
27
+
28
+ it('keeps only the selected namespace, plus cluster-scoped nodes', () => {
29
+ const scoped = scopeNodesToNamespaces(nodes, ['prod'])
30
+ expect(scoped.map(n => n.id)).toEqual(['web', 'db', 'node-1', 'pv-1'])
31
+ })
32
+
33
+ it('drops a kind that exists only outside the scope', () => {
34
+ const scoped = scopeNodesToNamespaces(nodes, ['prod'])
35
+ expect(scoped.some(n => n.kind === 'Ingress')).toBe(false)
36
+ })
37
+
38
+ it('accepts several namespaces at once', () => {
39
+ const scoped = scopeNodesToNamespaces(nodes, ['prod', 'staging'])
40
+ expect(scoped).toHaveLength(5)
41
+ })
42
+
43
+ it('returns the same array identity when the scope is empty', () => {
44
+ expect(scopeNodesToNamespaces(nodes, [])).toBe(nodes)
45
+ })
46
+ })
47
+
48
+ describe('isInNamespaceScope', () => {
49
+ it('passes everything for a null scope', () => {
50
+ expect(isInNamespaceScope(node('x', 'Pod', 'any'), null)).toBe(true)
51
+ })
52
+
53
+ it('always passes a node with no namespace', () => {
54
+ expect(isInNamespaceScope(node('node-1', 'Node'), new Set(['prod']))).toBe(true)
55
+ })
56
+
57
+ it('treats an empty-string namespace as cluster-scoped', () => {
58
+ expect(isInNamespaceScope(node('n', 'Node', ''), new Set(['prod']))).toBe(true)
59
+ })
60
+
61
+ it('rejects a namespaced node outside the scope', () => {
62
+ expect(isInNamespaceScope(node('ing', 'Ingress', 'staging'), new Set(['prod']))).toBe(false)
63
+ })
64
+ })
@@ -0,0 +1,18 @@
1
+ import type { TopologyNode } from '@skyhook-io/k8s-ui/types/core'
2
+
3
+ // A node with no namespace is cluster-scoped (Node, PersistentVolume,
4
+ // Namespace, ...) and stays visible in every scope.
5
+ export function isInNamespaceScope(node: TopologyNode, nsSet: Set<string> | null): boolean {
6
+ if (!nsSet) return true
7
+ const ns = node.data?.namespace as string | undefined
8
+ return !ns || nsSet.has(ns)
9
+ }
10
+
11
+ // Shared by the topology graph and the filter sidebar so the two cannot
12
+ // disagree about which nodes are in scope. Namespace carve-out only — kind
13
+ // filtering stays on the graph.
14
+ export function scopeNodesToNamespaces(nodes: TopologyNode[], namespaces: string[]): TopologyNode[] {
15
+ if (namespaces.length === 0) return nodes
16
+ const nsSet = new Set(namespaces)
17
+ return nodes.filter(node => isInNamespaceScope(node, nsSet))
18
+ }