@skyhook-io/radar-app 1.14.3 → 1.14.5

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/api/client.ts +2 -0
  3. package/src/api/diagnose.ts +7 -1
  4. package/src/components/diagnose/AIMarkdown.tsx +8 -3
  5. package/src/components/diagnose/ActivityTurn.tsx +1 -1
  6. package/src/components/diagnose/AssessmentCard.tsx +269 -238
  7. package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
  8. package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
  9. package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
  10. package/src/components/diagnose/InvestigationView.tsx +32 -24
  11. package/src/components/diagnose/investigationCase.test.tsx +83 -0
  12. package/src/components/diagnose/investigationCase.ts +3 -2
  13. package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +22 -0
  14. package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
  15. package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
  16. package/src/components/diagnose/investigationEvidence/adapters/listings.ts +2 -0
  17. package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
  18. package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
  19. package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
  20. package/src/components/diagnose/investigationEvidence/observations.ts +10 -5
  21. package/src/components/diagnose/investigationEvidence/types.ts +1 -1
  22. package/src/components/diagnose/investigationState.test.ts +2 -2
  23. package/src/components/diagnose/investigationState.ts +2 -2
  24. package/src/components/diagnose/investigationStory.ts +1 -1
  25. package/src/components/diagnose/parts.test.tsx +153 -2
  26. package/src/components/home/mcpToolCatalog.ts +27 -0
  27. package/src/components/rightsizing/RightsizingScanView.tsx +194 -63
  28. package/src/components/rightsizing/empty-state.test.tsx +67 -0
  29. package/src/components/rightsizing/model.test.ts +118 -1
  30. package/src/components/rightsizing/model.ts +36 -13
  31. package/src/components/rightsizing/notices.test.tsx +175 -0
  32. package/src/components/ui/Markdown.tsx +33 -6
  33. package/src/components/ui/Omnibar.tsx +82 -69
  34. package/src/components/workload/WorkloadView.tsx +7 -1
@@ -1,4 +1,4 @@
1
- import { useState, useMemo, useRef, useEffect, useCallback, forwardRef, useImperativeHandle } from 'react'
1
+ import { useState, useMemo, useRef, useEffect, useCallback, forwardRef, useImperativeHandle, type ReactNode } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { Search, CornerDownLeft, Loader2, AlertTriangle } from 'lucide-react'
4
4
  import { clsx } from 'clsx'
@@ -424,74 +424,12 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
424
424
 
425
425
  const hero = size === 'hero'
426
426
 
427
- return (
428
- <div
429
- ref={containerRef}
430
- className={clsx('relative w-full', hero ? 'max-w-3xl' : 'max-w-lg', open && hero && 'z-[16]')}
431
- // Open on click even when the field is already focused — onFocus alone
432
- // never fires again, so an autofocused hero (Home) wouldn't reveal the
433
- // launcher on a click.
434
- onMouseDown={() => setOpen(true)}
435
- >
436
- <SearchPillInput
437
- className={hero
438
- ? `min-h-14 px-5 rounded-2xl bg-theme-surface border border-theme-border shadow-theme-sm transition-[color,background-color,border-color,box-shadow] duration-[140ms] ${TW_EASE_UI} focus-within:border-[var(--color-brand-500)] focus-within:shadow-[0_0_0_4px_color-mix(in_srgb,var(--color-brand-500)_15%,transparent)]`
439
- : `min-h-8 px-2.5 rounded-md bg-theme-elevated border border-transparent focus-within:border-theme-border focus-within:bg-theme-surface transition-[color,background-color,border-color] duration-[140ms] ${TW_EASE_UI}`}
440
- inputClassName={hero ? 'text-lg py-4' : undefined}
441
- text={text}
442
- pills={pills}
443
- onChange={({ text: t, pills: p }) => { setText(t); setPills(p); setOpen(true) }}
444
- onKeyDown={handleKeyDown}
445
- onFocus={() => { if (skipFocusOpen.current) { skipFocusOpen.current = false; return } setOpen(true) }}
446
- onSuggestingChange={setSuggesting}
447
- modifierOptions={modifierOptions}
448
- placeholder={placeholder}
449
- aria-label="Search resources and commands"
450
- inputRef={inputRef}
451
- leftSlot={<Search className={hero ? 'w-5 h-5 shrink-0 text-theme-text-tertiary' : 'w-3.5 h-3.5 shrink-0 text-theme-text-tertiary'} />}
452
- rightSlot={
453
- <div className="flex items-center gap-1.5 shrink-0">
454
- <SearchSyntaxHelp />
455
- {!hero && !text && pills.length === 0 && (
456
- <kbd className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded border border-theme-border-light">
457
- {mac ? '⌘' : 'Ctrl+'}K
458
- </kbd>
459
- )}
460
- </div>
461
- }
462
- />
463
-
464
- {portal.shouldRender && anchor && createPortal(
465
- <>
466
- {/* Scrim — separates the dropdown from the page, consistently in both
467
- modes. At z-[15] it sits BELOW the rail/top bar (z-20/30), so the
468
- nav chrome stays lit while the content behind the panel dims+blurs:
469
- a "spotlight on search", not a full-screen modal dim (which fits a
470
- centered command palette, not an anchored omnibar). The hero covers
471
- from the top (its box is in the content, lifted to z-[16]); the
472
- top-bar launcher covers from below the field (its box is already in
473
- the z-20 chrome). Click closes. */}
474
- <div
475
- className={clsx(
476
- 'fixed left-0 right-0 bottom-0 z-[15] bg-black/15 dark:bg-black/50 backdrop-blur-[3px]',
477
- TRANSITION_BACKDROP,
478
- portal.isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none',
479
- )}
480
- style={{ top: hero ? 0 : anchor.top, ...overlayTransitionStyle(portal.isOpen, 'menu') }}
481
- onClick={open ? () => { setOpen(false); inputRef.current?.blur() } : undefined}
482
- />
483
- {showPanel && (
484
- <div
485
- ref={panelRef}
486
- inert={!open || undefined}
487
- style={{ position: 'fixed', top: anchor.top + 8, left: anchor.centerX, transform: 'translateX(-50%)', width: hero ? Math.round(anchor.width) : 640, maxWidth: 'calc(100vw - 2rem)', ...overlayTransitionStyle(portal.isOpen, 'menu') }}
488
- className={clsx(
489
- 'z-[121] dialog shadow-theme-lg ring-1 ring-black/5 dark:ring-white/10 overflow-hidden origin-top',
490
- TRANSITION_MENU,
491
- portal.isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
492
- !open && 'pointer-events-none',
493
- )}
494
- >
427
+ // The results panel keeps showing what it showed at logical close while it
428
+ // fades: picking a result clears the query (and recents re-key off `open`)
429
+ // on the same render, and a panel that swaps to a different list mid-exit
430
+ // reads as a glitch. The last open-frame content is frozen for the exit.
431
+ const panelContent = (
432
+ <>
495
433
  <div ref={listRef} className="max-h-[60vh] overflow-y-auto py-1">
496
434
  {recentRows.length > 0 && (
497
435
  <div>
@@ -572,6 +510,81 @@ export const Omnibar = forwardRef<OmnibarHandle, OmnibarProps>(function Omnibar(
572
510
  <span>⇞⇟ page</span>
573
511
  <span>esc close</span>
574
512
  </div>
513
+ </>
514
+ )
515
+ const frozenPanel = useRef<ReactNode>(null)
516
+ if (open) frozenPanel.current = panelContent
517
+ const shownPanel = open ? panelContent : frozenPanel.current
518
+
519
+ return (
520
+ <div
521
+ ref={containerRef}
522
+ className={clsx('relative w-full', hero ? 'max-w-3xl' : 'max-w-lg', portal.shouldRender && hero && 'z-[16]')}
523
+ // Open on click even when the field is already focused — onFocus alone
524
+ // never fires again, so an autofocused hero (Home) wouldn't reveal the
525
+ // launcher on a click.
526
+ onMouseDown={() => setOpen(true)}
527
+ >
528
+ <SearchPillInput
529
+ className={hero
530
+ ? `min-h-14 px-5 rounded-2xl bg-theme-surface border border-theme-border shadow-theme-sm transition-[color,background-color,border-color,box-shadow] duration-[140ms] ${TW_EASE_UI} focus-within:border-[var(--color-brand-500)] focus-within:shadow-[0_0_0_4px_color-mix(in_srgb,var(--color-brand-500)_15%,transparent)]`
531
+ : `min-h-8 px-2.5 rounded-md bg-theme-elevated border border-transparent focus-within:border-theme-border focus-within:bg-theme-surface transition-[color,background-color,border-color] duration-[140ms] ${TW_EASE_UI}`}
532
+ inputClassName={hero ? 'text-lg py-4' : undefined}
533
+ text={text}
534
+ pills={pills}
535
+ onChange={({ text: t, pills: p }) => { setText(t); setPills(p); setOpen(true) }}
536
+ onKeyDown={handleKeyDown}
537
+ onFocus={() => { if (skipFocusOpen.current) { skipFocusOpen.current = false; return } setOpen(true) }}
538
+ onSuggestingChange={setSuggesting}
539
+ modifierOptions={modifierOptions}
540
+ placeholder={placeholder}
541
+ aria-label="Search resources and commands"
542
+ inputRef={inputRef}
543
+ leftSlot={<Search className={hero ? 'w-5 h-5 shrink-0 text-theme-text-tertiary' : 'w-3.5 h-3.5 shrink-0 text-theme-text-tertiary'} />}
544
+ rightSlot={
545
+ <div className="flex items-center gap-1.5 shrink-0">
546
+ <SearchSyntaxHelp />
547
+ {!hero && !text && pills.length === 0 && (
548
+ <kbd className="text-[10px] text-theme-text-tertiary bg-theme-surface px-1 py-0.5 rounded border border-theme-border-light">
549
+ {mac ? '⌘' : 'Ctrl+'}K
550
+ </kbd>
551
+ )}
552
+ </div>
553
+ }
554
+ />
555
+
556
+ {portal.shouldRender && anchor && createPortal(
557
+ <>
558
+ {/* Scrim — separates the dropdown from the page, consistently in both
559
+ modes. At z-[15] it sits BELOW the rail/top bar (z-20/30), so the
560
+ nav chrome stays lit while the content behind the panel dims+blurs:
561
+ a "spotlight on search", not a full-screen modal dim (which fits a
562
+ centered command palette, not an anchored omnibar). The hero covers
563
+ from the top (its box is in the content, lifted to z-[16]); the
564
+ top-bar launcher covers from below the field (its box is already in
565
+ the z-20 chrome). Click closes. */}
566
+ <div
567
+ className={clsx(
568
+ 'fixed left-0 right-0 bottom-0 z-[15] bg-black/15 dark:bg-black/50 backdrop-blur-[3px]',
569
+ TRANSITION_BACKDROP,
570
+ portal.isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none',
571
+ )}
572
+ style={{ top: hero ? 0 : anchor.top, ...overlayTransitionStyle(portal.isOpen, 'menu') }}
573
+ onClick={open ? () => { setOpen(false); inputRef.current?.blur() } : undefined}
574
+ />
575
+ {showPanel && (
576
+ <div
577
+ ref={panelRef}
578
+ inert={!open || undefined}
579
+ style={{ position: 'fixed', top: anchor.top + 8, left: anchor.centerX, transform: 'translateX(-50%)', width: hero ? Math.round(anchor.width) : 640, maxWidth: 'calc(100vw - 2rem)', ...overlayTransitionStyle(portal.isOpen, 'menu') }}
580
+ className={clsx(
581
+ 'z-[121] dialog shadow-theme-lg ring-1 ring-black/5 dark:ring-white/10 overflow-hidden origin-top',
582
+ TRANSITION_MENU,
583
+ portal.isOpen ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 -translate-y-1 scale-[0.97]',
584
+ !open && 'pointer-events-none',
585
+ )}
586
+ >
587
+ {shownPanel}
575
588
  </div>
576
589
  )}
577
590
  </>,
@@ -742,9 +742,15 @@ export function WorkloadView({
742
742
  [helmOwner, helmSourceResource],
743
743
  )
744
744
 
745
- // Fetch topology for hierarchy building (only when expanded)
745
+ // Fetch topology for hierarchy building (only when expanded). Polled like
746
+ // useTrace's "drawer feeling live" pattern — without this, a resource
747
+ // whose status/labels/edges change without its node/edge identity changing
748
+ // (a canary weight step, a Pod's traffic role flipping to stable, an
749
+ // AnalysisRun finishing) never refreshes until something else forces a
750
+ // remount; a genuinely stuck-looking Topology tab, not just a stale one.
746
751
  const { data: topology } = useTopology([namespace], 'resources', {
747
752
  enabled: expanded,
753
+ refetchInterval: expanded ? 5000 : false,
748
754
  })
749
755
 
750
756
  // Always fetched so Recent Events populates on drawer open; allEvents below is