@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.
- package/package.json +1 -1
- package/src/api/client.ts +2 -0
- package/src/api/diagnose.ts +7 -1
- package/src/components/diagnose/AIMarkdown.tsx +8 -3
- package/src/components/diagnose/ActivityTurn.tsx +1 -1
- package/src/components/diagnose/AssessmentCard.tsx +269 -238
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +47 -0
- package/src/components/diagnose/InvestigationEvidencePane.tsx +15 -2
- package/src/components/diagnose/InvestigationEvidenceSections.tsx +1 -1
- package/src/components/diagnose/InvestigationView.tsx +32 -24
- package/src/components/diagnose/investigationCase.test.tsx +83 -0
- package/src/components/diagnose/investigationCase.ts +3 -2
- package/src/components/diagnose/investigationEvidence/adapters/coverage.test.tsx +22 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +8 -2
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +1 -1
- package/src/components/diagnose/investigationEvidence/adapters/listings.ts +2 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +7 -3
- package/src/components/diagnose/investigationEvidence/bodies/streams.tsx +1 -1
- package/src/components/diagnose/investigationEvidence/observations.ts +10 -5
- package/src/components/diagnose/investigationEvidence/types.ts +1 -1
- package/src/components/diagnose/investigationState.test.ts +2 -2
- package/src/components/diagnose/investigationState.ts +2 -2
- package/src/components/diagnose/investigationStory.ts +1 -1
- package/src/components/diagnose/parts.test.tsx +153 -2
- package/src/components/home/mcpToolCatalog.ts +27 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +194 -63
- package/src/components/rightsizing/empty-state.test.tsx +67 -0
- package/src/components/rightsizing/model.test.ts +118 -1
- package/src/components/rightsizing/model.ts +36 -13
- package/src/components/rightsizing/notices.test.tsx +175 -0
- package/src/components/ui/Markdown.tsx +33 -6
- package/src/components/ui/Omnibar.tsx +82 -69
- 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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|