@skyhook-io/radar-app 1.14.0 → 1.14.2

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 (63) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +6 -4
  3. package/src/api/client.ts +3 -0
  4. package/src/api/diagnose.ts +31 -1
  5. package/src/components/CloudConnectFlow.tsx +173 -13
  6. package/src/components/CloudFunnelButton.tsx +4 -2
  7. package/src/components/ConnectionErrorView.tsx +9 -10
  8. package/src/components/DebugOverlay.tsx +10 -6
  9. package/src/components/cloudConnectHandoff.test.ts +109 -4
  10. package/src/components/cloudConnectHandoff.ts +154 -2
  11. package/src/components/curl/ServiceCurlButton.tsx +19 -26
  12. package/src/components/diagnose/AgentCase.tsx +18 -5
  13. package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
  14. package/src/components/diagnose/AnalysisStory.tsx +564 -0
  15. package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
  16. package/src/components/diagnose/DiagnoseContext.tsx +25 -8
  17. package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
  18. package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
  19. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
  20. package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
  21. package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
  22. package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
  23. package/src/components/diagnose/InvestigationView.tsx +493 -529
  24. package/src/components/diagnose/investigationCase.test.tsx +267 -129
  25. package/src/components/diagnose/investigationCase.ts +122 -77
  26. package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +27 -0
  27. package/src/components/diagnose/investigationEvidence/adapters/resource.ts +28 -0
  28. package/src/components/diagnose/investigationEvidence/builder.ts +11 -2
  29. package/src/components/diagnose/investigationEvidence/identity.test.ts +25 -7
  30. package/src/components/diagnose/investigationEvidence/identity.ts +4 -4
  31. package/src/components/diagnose/investigationEvidence/observations.ts +24 -0
  32. package/src/components/diagnose/investigationEvidence/projection.test.ts +36 -3
  33. package/src/components/diagnose/investigationEvidence/types.ts +2 -0
  34. package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
  35. package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
  36. package/src/components/diagnose/investigationState.test.ts +316 -58
  37. package/src/components/diagnose/investigationState.ts +257 -44
  38. package/src/components/diagnose/investigationStory.test.ts +161 -0
  39. package/src/components/diagnose/investigationStory.ts +207 -0
  40. package/src/components/diagnose/parts.test.tsx +9 -6
  41. package/src/components/diagnose/parts.tsx +977 -193
  42. package/src/components/diagnose/storyParts.test.tsx +426 -0
  43. package/src/components/diagnose/toolCallLabel.test.ts +51 -0
  44. package/src/components/diagnose/toolCallLabel.ts +135 -0
  45. package/src/components/diagnose/useDisclosureReveal.ts +10 -11
  46. package/src/components/helm/HelmCompareRoute.tsx +18 -4
  47. package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
  48. package/src/components/helm/InstallWizard.tsx +8 -3
  49. package/src/components/home/MCPSetupDialog.tsx +15 -9
  50. package/src/components/portforward/PortForwardManager.tsx +6 -13
  51. package/src/components/resource/PrometheusChartsGrid.tsx +3 -3
  52. package/src/components/resource/WorkloadMetricsHelpDialog.tsx +3 -3
  53. package/src/components/resource/WorkloadMetricsSection.tsx +15 -21
  54. package/src/components/resources/PodFilePreview.tsx +3 -3
  55. package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
  56. package/src/components/settings/SettingsDialog.tsx +4 -2
  57. package/src/components/ui/CommandPalette.tsx +10 -6
  58. package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
  59. package/src/components/ui/Disclosure.tsx +47 -0
  60. package/src/components/ui/Markdown.tsx +23 -11
  61. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
  62. package/src/components/ui/UpdateNotification.tsx +18 -2
  63. package/src/index.css +19 -6
@@ -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
@@ -1,6 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import { useSearchParams } from "react-router-dom";
3
3
  import { Loader2, Wifi, WifiOff } from "lucide-react";
4
+ import { Disclosure } from "../ui/Disclosure";
4
5
  import {
5
6
  AreaChart,
6
7
  SeriesLegend,
@@ -213,10 +214,9 @@ export function PrometheusChartsGrid({
213
214
 
214
215
  <div className="metrics-layout min-w-0 px-4 pt-4">
215
216
  {isWorkload && <h3 className="mb-2 text-sm font-semibold text-theme-text-primary">Network and storage</h3>}
216
- {isWorkload && <details className="mb-2 text-xs text-theme-text-secondary">
217
- <summary className="cursor-pointer">Pod-name matched · identity unverified</summary>
217
+ {isWorkload && <Disclosure className="mb-2 text-xs text-theme-text-secondary" summary="Pod-name matched · identity unverified">
218
218
  <p className="mt-2 max-w-2xl text-sm leading-relaxed">These charts match current Pod names, independently of the identity-checked charts above. Matching names in a shared backend may include another cluster.</p>
219
- </details>}
219
+ </Disclosure>}
220
220
  <div className="metrics-chart-grid">
221
221
  {chartPanels.filter(({ def }) => !isWorkload || (def.key !== "cpu" && def.key !== "memory")).map(renderPanel)}
222
222
  </div>
@@ -1,6 +1,7 @@
1
1
  import { useLayoutEffect, useId, useRef } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { ExternalLink, X } from 'lucide-react'
4
+ import { Disclosure } from '../ui/Disclosure'
4
5
  import type { WorkloadMetrics } from '../../api/workloadMetrics'
5
6
  import type { PrometheusTimeRange } from '../../api/client'
6
7
 
@@ -124,8 +125,7 @@ export function WorkloadMetricsHelpContent({ data, pending, error }: Pick<Props,
124
125
  <div><dt className="font-medium text-theme-text-primary">Resource usage</dt><dd className="mt-1">CPU and memory include reporting containers and sidecars. Their historical charts show the workload total and maximum Pod; current-Pod charts show individual Pods. Throttling measures CFS periods, not CPU time lost.</dd></div>
125
126
  </dl>
126
127
  </section>
127
- <details className="border-t border-theme-border pt-4">
128
- <summary className="cursor-pointer font-medium text-theme-text-primary">Troubleshooting identity matching</summary>
128
+ <Disclosure className="border-t border-theme-border pt-4" summaryClassName="font-medium text-theme-text-primary" chevronClassName="h-4 w-4" summary="Troubleshooting identity matching">
129
129
  <div className="mt-3 space-y-3">
130
130
  <p>Check the affected chart’s warning first. Scope overrides address cluster identity; they do not add missing metrics, instrumentation or ownership history.</p>
131
131
  <p>If automatic matching cannot establish identity, an operator can explicitly assert the backend’s scope:</p>
@@ -140,6 +140,6 @@ export function WorkloadMetricsHelpContent({ data, pending, error }: Pick<Props,
140
140
  <p><strong className="font-medium">Desktop:</strong> automatic matching works without flags. Desktop does not expose these overrides yet; if one is required, use the standalone CLI with a verified scope.</p>
141
141
  <p><strong className="font-medium">Multiple Beyla observation jobs:</strong> workload charts currently honor <code className="font-mono text-xs">--beyla-job-selector</code> only alongside a verified scope override. Use one matcher, such as <code className="font-mono text-xs">{'\'job="primary-beyla"\''}</code>; Helm uses <code className="font-mono text-xs">traffic.beylaJobSelector</code>. A custom job name alone normally needs no override.</p>
142
142
  </div>
143
- </details>
143
+ </Disclosure>
144
144
  </div>
145
145
  }
@@ -18,6 +18,7 @@ import {
18
18
  } from "../../api/workloadMetrics";
19
19
  import { latestWorkloadValue, workloadPodValues } from "./workloadMetricValues";
20
20
  import { WorkloadMetricsHelpDialog } from './WorkloadMetricsHelpDialog';
21
+ import { Disclosure } from '../ui/Disclosure';
21
22
 
22
23
  interface Props {
23
24
  kind: string;
@@ -87,14 +88,13 @@ export function WorkloadMetricsSection(props: Props) {
87
88
  />}
88
89
  </div>
89
90
  {(data?.panels.cpu || data?.panels.memory || data?.panels.throttling) && <p className="mt-2 text-xs text-theme-text-tertiary">Resource totals include reporting containers and sidecars.</p>}
90
- {data && hasResourceDetails && <details className="mt-2 text-xs text-theme-text-tertiary">
91
- <summary className="cursor-pointer">About resource metrics</summary>
91
+ {data && hasResourceDetails && <Disclosure className="mt-2 text-xs text-theme-text-tertiary" summary="About resource metrics">
92
92
  <div className="mt-2 max-w-2xl space-y-2 text-sm leading-relaxed text-theme-text-secondary">
93
93
  {hasHistoricalTotals && <p>In workload-history CPU and memory charts, Workload is the total across reporting Pods; Maximum Pod exposes skew.</p>}
94
94
  {data.history.throttling?.mode === 'workload-history' && <p>Throttling is weighted by total periods, not an average of Pod percentages.</p>}
95
95
  {resourceNotice?.state === 'stale' && <ul className="space-y-1">{(['cpu', 'memory', 'throttling'] as const).map((key) => data.panels[key]?.reason && <li key={key}>{key}: {data.panels[key]!.reason}</li>)}</ul>}
96
96
  </div>
97
- </details>}
97
+ </Disclosure>}
98
98
  </section>
99
99
  {nameMatchedFallbacks.length > 0 && <details aria-label="Name-matched resource metrics">
100
100
  <summary className="mb-2 cursor-pointer text-xs text-theme-text-secondary">Basic {nameMatchedFallbacks.map((key) => key === 'cpu' ? 'CPU' : 'memory').join(' / ')} · identity unverified</summary>
@@ -116,13 +116,12 @@ export function WorkloadMetricsSection(props: Props) {
116
116
 
117
117
  function HistoryNotice({ scope, omitReason }: { scope?: WorkloadMetrics['history']['cpu']; omitReason?: string }) {
118
118
  if (!scope) return null;
119
- return <details className={`text-xs ${scope.mode === 'workload-history' ? 'text-theme-text-tertiary' : SEVERITY_TEXT.warning}`}>
120
- <summary className="cursor-pointer">{scope.mode === 'workload-history' ? 'Workload history' : scope.mode === 'current-pods' ? 'Current Pods only' : 'Workload history unavailable'}</summary>
119
+ return <Disclosure className={`text-xs ${scope.mode === 'workload-history' ? 'text-theme-text-tertiary' : SEVERITY_TEXT.warning}`} summary={scope.mode === 'workload-history' ? 'Workload history' : scope.mode === 'current-pods' ? 'Current Pods only' : 'Workload history unavailable'}>
121
120
  <div className="my-2 max-w-2xl space-y-2 text-sm leading-relaxed text-theme-text-secondary">
122
121
  <p>{scope.mode === 'workload-history' ? 'Includes previous replicas where ownership and metrics were retained. Missing history produces gaps, not current-Pod substitutes.' : scope.mode === 'current-pods' ? 'Previous replicas are not reconstructed.' : 'Workload history could not be checked.'}</p>
123
122
  {scope.reason && scope.reason !== omitReason && <p>{scope.reason}</p>}
124
123
  </div>
125
- </details>;
124
+ </Disclosure>;
126
125
  }
127
126
 
128
127
  function WorkloadRequests({ data, isLoading, error, setSource }: {
@@ -172,12 +171,11 @@ function WorkloadRequests({ data, isLoading, error, setSource }: {
172
171
  <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"><h3 className="text-sm font-semibold text-theme-text-primary">
173
172
  Requests
174
173
  </h3>
175
- {hasRequests ? <HistoryNotice scope={data.history.requests} /> : <details className={`text-xs ${requestPanel?.state === 'error' ? SEVERITY_TEXT.error : requestPanel?.state === 'partial' ? SEVERITY_TEXT.warning : 'text-theme-text-secondary'}`}>
176
- <summary className="cursor-pointer">{requestPanel?.state === 'error' ? 'Request metrics query failed' : requestPanel?.state === 'partial' ? 'Request metrics withheld' : requestPanel?.state === 'detecting' ? 'Checking request metrics…' : 'No usable HTTP metrics in this window'}</summary>
174
+ {hasRequests ? <HistoryNotice scope={data.history.requests} /> : <Disclosure className={`text-xs ${requestPanel?.state === 'error' ? SEVERITY_TEXT.error : requestPanel?.state === 'partial' ? SEVERITY_TEXT.warning : 'text-theme-text-secondary'}`} summary={requestPanel?.state === 'error' ? 'Request metrics query failed' : requestPanel?.state === 'partial' ? 'Request metrics withheld' : requestPanel?.state === 'detecting' ? 'Checking request metrics…' : 'No usable HTTP metrics in this window'}>
177
175
  {unmatchedSources.length > 0 ? <ul className="mt-2 max-w-2xl space-y-1 text-sm leading-relaxed text-theme-text-secondary">
178
176
  {unmatchedSources.map((candidate) => <li key={candidate.id}><span className="font-medium">{candidate.label}:</span> {data.attribution?.[candidate.id]}</li>)}
179
177
  </ul> : <p className="mt-2 max-w-2xl text-sm leading-relaxed text-theme-text-secondary">{requestPanel?.reason || 'No usable HTTP request samples were returned for this workload.'}</p>}
180
- </details>}
178
+ </Disclosure>}
181
179
  </div>
182
180
  {sources.length > 1 ? (
183
181
  <select
@@ -233,8 +231,7 @@ function WorkloadRequests({ data, isLoading, error, setSource }: {
233
231
  </div>
234
232
  <div className="mt-2 text-xs text-theme-text-tertiary space-y-2">
235
233
  <p>{Math.round(data.rateWindowSeconds / 60)}-minute rates{reportingPods != null && <> · {data.history.requests?.mode === 'workload-history' ? `${reportingPods} Pods reporting` : `${reportingPods} of ${data.podsTotal} current Pods reporting`}</>}</p>
236
- <details>
237
- <summary className="cursor-pointer">Coverage details</summary>
234
+ <Disclosure summary="Coverage details">
238
235
  <div className="mt-2 max-w-2xl space-y-2 text-sm leading-relaxed text-theme-text-secondary">
239
236
  {reportingPods == null && <p>Reporting Pod count unavailable.</p>}
240
237
  <p>
@@ -247,7 +244,7 @@ function WorkloadRequests({ data, isLoading, error, setSource }: {
247
244
  </p>
248
245
  {requestNotice?.state === 'stale' && <ul className="space-y-1">{(['requests', 'errors', 'p50', 'p95'] as const).map((key) => data.panels[key]?.reason && <li key={key}>{key}: {data.panels[key]!.reason}</li>)}</ul>}
249
246
  </div>
250
- </details>
247
+ </Disclosure>
251
248
  </div>
252
249
  </>
253
250
  ) : null}
@@ -352,10 +349,9 @@ function sharedPanelNotice(panels: (WorkloadMetricPanel | undefined)[]) {
352
349
  function MetricNotice({ panel, empty, label }: { panel?: WorkloadMetricPanel; empty?: boolean; label?: string }) {
353
350
  if (!empty && !panel?.reason && panel?.state !== 'stale') return null;
354
351
  const tone = panel?.state === 'error' ? SEVERITY_TEXT.error : panel?.state === 'partial' || panel?.state === 'stale' ? SEVERITY_TEXT.warning : 'text-theme-text-secondary';
355
- if (panel?.state === 'stale' && !empty) return <details className={`mt-2 text-xs ${tone}`}>
356
- <summary className="cursor-pointer">{label && `${label}: `}Historical samples only · no recent samples</summary>
352
+ if (panel?.state === 'stale' && !empty) return <Disclosure className={`mt-2 text-xs ${tone}`} summary={<>{label && `${label}: `}Historical samples only · no recent samples</>}>
357
353
  {panel.reason && <p className="mt-2 max-w-2xl text-sm leading-relaxed text-theme-text-secondary">{panel.reason}</p>}
358
- </details>;
354
+ </Disclosure>;
359
355
  return <p role="status" className={`mt-2 text-xs ${tone}`}>{label && `${label}: `}{panel?.reason || 'No usable samples in this window.'}</p>;
360
356
  }
361
357
 
@@ -421,14 +417,12 @@ function PodComparison({
421
417
  <h4 className="mb-2 text-xs font-medium text-theme-text-secondary">
422
418
  Compare current Pods · latest samples
423
419
  </h4>
424
- {hasTemplate && <details className="mb-2 text-xs text-theme-text-tertiary">
425
- <summary className="cursor-pointer">Template per Pod · actual Pods may differ</summary>
420
+ {hasTemplate && <Disclosure className="mb-2 text-xs text-theme-text-tertiary" summary="Template per Pod · actual Pods may differ">
426
421
  <p className="mt-2 max-w-2xl text-sm leading-relaxed text-theme-text-secondary">Actual Pods can differ after injection or rollout. Template values below are not measured allocations for each Pod.</p>
427
- </details>}
428
- {window.pods < window.podsTotal && <details className={`mb-2 text-xs ${SEVERITY_TEXT.warning}`}>
429
- <summary className="cursor-pointer">Comparing {window.pods} of {window.podsTotal} current Pods</summary>
422
+ </Disclosure>}
423
+ {window.pods < window.podsTotal && <Disclosure className={`mb-2 text-xs ${SEVERITY_TEXT.warning}`} summary={<>Comparing {window.pods} of {window.podsTotal} current Pods</>}>
430
424
  <p className="mt-2 text-sm text-theme-text-secondary">This comparison cap does not limit workload-history totals.</p>
431
- </details>}
425
+ </Disclosure>}
432
426
  {pending.length > 0 && <p role="status" className="mb-2 text-xs text-theme-text-secondary">Matching {pending.map(({ label }) => label).join(' / ')} metrics to current Pods…</p>}
433
427
  {Array.from(notices, ([key, { labels, panel }]) => <MetricNotice key={key} panel={panel} label={labels.join(' / ')} />)}
434
428
  <div className="max-h-72 overflow-auto">
@@ -3,6 +3,7 @@ import Editor from '@monaco-editor/react'
3
3
  import { AlertTriangle, Download, FileText, RotateCw } from 'lucide-react'
4
4
  import { PaneLoader, ensureMonacoRuntime } from '@skyhook-io/k8s-ui'
5
5
  import { formatBytes } from '../../utils/format'
6
+ import { Disclosure } from '../ui/Disclosure'
6
7
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
7
8
 
8
9
  // A curated inline viewer for text files inside a pod container, rendered in
@@ -296,12 +297,11 @@ function PreviewErrorState({
296
297
  </div>
297
298
 
298
299
  {shape.details && (
299
- <details className="mt-4 text-xs text-theme-text-tertiary">
300
- <summary className="cursor-pointer hover:text-theme-text-secondary">Technical details</summary>
300
+ <Disclosure className="mt-4 text-xs text-theme-text-tertiary" summaryClassName="hover:text-theme-text-secondary" summary="Technical details">
301
301
  <div className="mt-2 font-mono whitespace-pre-wrap text-left bg-theme-elevated/40 p-3 rounded max-w-xl">
302
302
  {shape.details}
303
303
  </div>
304
- </details>
304
+ </Disclosure>
305
305
  )}
306
306
  </div>
307
307
  )
@@ -5,6 +5,7 @@ import { CurlButton, CurlPanel, isHttpishPort, defaultScheme, defaultPathForPort
5
5
  import { useResources } from '../../../api/client'
6
6
  import { useNamespacedCapabilities, useIsLocalDeployment } from '../../../contexts/CapabilitiesContext'
7
7
  import type { ResourceRef } from '../../../types'
8
+ import { DURATION_DISCLOSURE } from '@skyhook-io/k8s-ui/utils/animation'
8
9
 
9
10
  interface ServiceRendererProps {
10
11
  data: any
@@ -32,7 +33,7 @@ export function ServiceRenderer({ data, onCopy, copied, onNavigate }: ServiceRen
32
33
  setCurl((p) => (p ? { ...p, closing: true } : null))
33
34
  // Only drop the panel if it's still the one closing. Opening another port
34
35
  // (which sets closing:false) before this fires must not clear the new panel.
35
- window.setTimeout(() => setCurl((p) => (p?.closing ? null : p)), 220)
36
+ window.setTimeout(() => setCurl((p) => (p?.closing ? null : p)), DURATION_DISCLOSURE + 20)
36
37
  }, [])
37
38
  const spec = data.spec || {}
38
39
  const shouldLoadEndpointSlices = Boolean(
@@ -9,7 +9,7 @@ import {
9
9
  import { clsx } from 'clsx'
10
10
  import { useQueryClient } from '@tanstack/react-query'
11
11
  import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
12
- import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
12
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayExitMs, overlayTransitionStyle } from '../../utils/animation'
13
13
  import { apiUrl, getAuthHeaders, getCredentialsMode, routePath } from '../../api/config'
14
14
  import {
15
15
  useCloudRole, useVersionCheck, useClusterInfo, usePrometheusStatus, useArgoStatus, useCapabilities,
@@ -135,7 +135,7 @@ export function SettingsDialog({
135
135
  }: SettingsDialogProps) {
136
136
  const queryClient = useQueryClient()
137
137
  const dialogRef = useRef<HTMLDivElement>(null)
138
- const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
138
+ const { shouldRender, isOpen } = useAnimatedUnmount(open, overlayExitMs('dialog'))
139
139
  const { data: versionInfo } = useVersionCheck()
140
140
  // Radar configuration (kubeconfig, port, integrations…) is host-level and
141
141
  // affects every user of this instance, so it's gated to owners. Personal
@@ -473,6 +473,7 @@ export function SettingsDialog({
473
473
  TRANSITION_BACKDROP,
474
474
  isOpen ? 'opacity-100' : 'opacity-0'
475
475
  )}
476
+ style={overlayTransitionStyle(isOpen, 'dialog')}
476
477
  onClick={() => requestCloseRef.current()}
477
478
  />
478
479
 
@@ -499,6 +500,7 @@ export function SettingsDialog({
499
500
  TRANSITION_PANEL,
500
501
  isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
501
502
  )}
503
+ style={overlayTransitionStyle(isOpen, 'dialog')}
502
504
  >
503
505
  {/* Header — spans both panes */}
504
506
  <div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
@@ -1,5 +1,5 @@
1
1
  import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
2
- import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
2
+ import { TRANSITION_BACKDROP, TRANSITION_PANEL, overlayTransitionStyle } from '../../utils/animation'
3
3
  import { Search, X, ChevronRight } from 'lucide-react'
4
4
  import { clsx } from 'clsx'
5
5
  import { useCommandItems, bestScore, type CommandItem, type CommandItemCallbacks } from './command-items'
@@ -130,15 +130,19 @@ export function CommandPalette({ onClose, isOpen = true, ...callbacks }: Command
130
130
  TRANSITION_BACKDROP,
131
131
  isOpen ? 'opacity-100' : 'opacity-0'
132
132
  )}
133
+ style={overlayTransitionStyle(isOpen, 'dialog')}
133
134
  onClick={onClose}
134
135
  />
135
136
 
136
137
  {/* Panel */}
137
- <div className={clsx(
138
- 'relative w-full max-w-lg mx-4 dialog overflow-hidden',
139
- TRANSITION_PANEL,
140
- isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
141
- )}>
138
+ <div
139
+ className={clsx(
140
+ 'relative w-full max-w-lg mx-4 dialog overflow-hidden',
141
+ TRANSITION_PANEL,
142
+ isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
143
+ )}
144
+ style={overlayTransitionStyle(isOpen, 'dialog')}
145
+ >
142
146
  {/* Search input */}
143
147
  <div className="flex items-center gap-3 px-4 py-3 border-b border-theme-border">
144
148
  <Search className="w-5 h-5 text-theme-text-secondary shrink-0" />
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState, useCallback } from 'react'
2
2
  import { X, Copy, Check, ExternalLink } 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 { openExternal } from '../../utils/navigation'
6
6
  import { useDiagnostics } from '../../api/client'
7
7
  import type { DiagnosticsSnapshot, DiagEnvVar, DiagMetricsSourceHealth, DiagDropRecord, DiagErrorEntry, DiagCacheSyncStatus, DiagInformerSyncStatus, DiagSyncPhase, DiagSampleWindow } from '../../api/client'
@@ -72,15 +72,19 @@ export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverla
72
72
  TRANSITION_BACKDROP,
73
73
  isOpen ? 'opacity-100' : 'opacity-0'
74
74
  )}
75
+ style={overlayTransitionStyle(isOpen, 'dialog')}
75
76
  onClick={onClose}
76
77
  />
77
78
 
78
79
  {/* Panel */}
79
- <div className={clsx(
80
- 'relative w-full max-w-2xl mx-4 dialog overflow-hidden flex flex-col max-h-[84vh]',
81
- TRANSITION_PANEL,
82
- isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
83
- )}>
80
+ <div
81
+ className={clsx(
82
+ 'relative w-full max-w-2xl mx-4 dialog overflow-hidden flex flex-col max-h-[84vh]',
83
+ TRANSITION_PANEL,
84
+ isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
85
+ )}
86
+ style={overlayTransitionStyle(isOpen, 'dialog')}
87
+ >
84
88
  {/* Header */}
85
89
  <div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border shrink-0">
86
90
  <div className="flex items-center gap-3">