@skyhook-io/radar-app 1.8.12 → 1.9.0
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/App.tsx +7 -4
- package/src/api/diagnose.ts +1 -0
- package/src/api/timelineSource.test.ts +464 -21
- package/src/api/timelineSource.ts +521 -184
- package/src/components/applications/ApplicationsView.tsx +2 -1
- package/src/components/diagnose/AISettings.tsx +47 -16
- package/src/components/diagnose/DiagnoseContext.tsx +3 -0
- package/src/components/diagnose/InvestigationView.tsx +8 -3
- package/src/components/diagnose/LocalDiagnoseAction.tsx +21 -9
- package/src/components/home/mcpToolCatalog.ts +1 -1
- package/src/components/settings/SettingsDialog.tsx +22 -17
- package/src/components/timeline/RetainedTimelineScrubber.tsx +21 -13
- package/src/components/timeline/TimelineList.tsx +72 -49
- package/src/components/timeline/TimelineView.tsx +66 -16
- package/src/utils/auditBadges.ts +1 -1
|
@@ -92,6 +92,10 @@ export type TimelineMode =
|
|
|
92
92
|
// burying the lanes in a day of history; presets/URL widen it deliberately.
|
|
93
93
|
const DEFAULT_LIVE_WIDTH_MS = 60 * 60 * 1000
|
|
94
94
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
95
|
+
// Presets wider than this land the swimlane on a bounded recent lens instead of
|
|
96
|
+
// the full range, so it never renders every resource lane at once (a 30d domain
|
|
97
|
+
// has thousands). 7d and narrower keep the full-range lens.
|
|
98
|
+
const WIDE_LENS_THRESHOLD_MS = 7 * DAY_MS
|
|
95
99
|
// Fallback cap for a retained hand-entered ?from&to when the source doesn't
|
|
96
100
|
// declare maxRangeDays — mirrors the retained source's own default.
|
|
97
101
|
const DEFAULT_MAX_RANGE_DAYS = 7
|
|
@@ -293,8 +297,8 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
293
297
|
const effectiveInitialMode = scopeRequiresNamespaceFilter ? 'list' : (parseView(searchParams) ?? initialViewMode ?? DEFAULT_VIEW)
|
|
294
298
|
const [viewMode, setViewMode] = useState<TimelineViewMode>(effectiveInitialMode)
|
|
295
299
|
// Shared across list + swimlane so the toggle carries across the view switch,
|
|
296
|
-
// and so the
|
|
297
|
-
//
|
|
300
|
+
// and so the fetch can exclude deletes at the source rather than only hiding
|
|
301
|
+
// them client-side.
|
|
298
302
|
const [showDeleted, setShowDeleted] = useState(() => searchParams.get('deleted') !== '0')
|
|
299
303
|
// ?pinnedOnly=1 is inert without pins: honoring it with no stored pins would
|
|
300
304
|
// arm a filter that hides everything. Gate the read on stored pins so the param
|
|
@@ -519,11 +523,21 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
519
523
|
// plain fixed widths.
|
|
520
524
|
const all = isLocal && scrubberDomain != null && widthMs >= scrubberDomain.maxSelectionMs
|
|
521
525
|
const now = Date.now()
|
|
526
|
+
const sel = deriveLiveSelection(capped, now)
|
|
522
527
|
setMode({ kind: 'live', widthMs: capped, all: all || undefined })
|
|
523
528
|
setFrozenAsOfMs(null)
|
|
524
529
|
setNowTick(now)
|
|
525
|
-
|
|
526
|
-
|
|
530
|
+
// A full-range lens on a very wide domain (e.g. 30d) renders EVERY resource
|
|
531
|
+
// lane at once — thousands of rows — which freezes the browser for seconds
|
|
532
|
+
// before the view settles. Land directly on a bounded recent lens instead;
|
|
533
|
+
// the density strip still spans the full selected range for context, and the
|
|
534
|
+
// user scrubs it. Narrow presets (<= 7d) keep the full-range lens unchanged.
|
|
535
|
+
if (capped > WIDE_LENS_THRESHOLD_MS) {
|
|
536
|
+
resetLensToRecent(sel)
|
|
537
|
+
} else {
|
|
538
|
+
resetLensToFull(sel)
|
|
539
|
+
}
|
|
540
|
+
}, [isLocal, scrubberDomain, resetLensToFull, resetLensToRecent])
|
|
527
541
|
|
|
528
542
|
// "→ Now" → LIVE, width = current selection width. Pins to now and resets the
|
|
529
543
|
// lens to the live edge.
|
|
@@ -640,22 +654,26 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
640
654
|
setSearchParamsRef.current(target, { replace })
|
|
641
655
|
}, [viewMode, mode, showDeleted, pinnedOnly, search, activityFilter, kindFilter, grouping, sort, selectedEventId, isRetained, isLocal, scopeRequiresNamespaceFilter])
|
|
642
656
|
|
|
643
|
-
// Fetch all activity - zoom controls what's visible in the UI.
|
|
644
|
-
//
|
|
645
|
-
//
|
|
646
|
-
const { data: activity, isLoading, isError, refetch } = timelineSource.useEvents({
|
|
657
|
+
// Fetch all activity - zoom controls what's visible in the UI. This ring feeds
|
|
658
|
+
// the swimlanes and the local strip's histogram, so it also runs in list mode
|
|
659
|
+
// when that strip is shown; the list itself fetches its own 2000.
|
|
660
|
+
const { data: activity, isLoading, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
|
|
647
661
|
namespaces: timelineNamespaces,
|
|
648
662
|
timeRange: 'all',
|
|
649
663
|
includeK8sEvents: true,
|
|
650
664
|
includeManaged: true,
|
|
651
665
|
includeDeleted: showDeleted,
|
|
652
|
-
|
|
666
|
+
// Only the local in-memory ring imposes a client-side size cap (it can't
|
|
667
|
+
// hold more than its ring anyway). Retained mode renders every event its
|
|
668
|
+
// ring holds — the hub bounds the ring itself (retention depth + a newest-
|
|
669
|
+
// 50k cap flagged as truncated, never a silent cut), so a busy window
|
|
670
|
+
// never silently drops its oldest events on the client.
|
|
671
|
+
limit: isRetained ? undefined : 10000,
|
|
653
672
|
// The local strip derives its histogram from this ring fetch, so it must
|
|
654
673
|
// run in list mode too whenever the strip is shown.
|
|
655
674
|
enabled: appScopeReady && (showSwimlanes || showLocalScrubber),
|
|
656
675
|
fromMs: isRetained ? selection.fromMs : undefined,
|
|
657
676
|
toMs: isRetained ? selection.toMs : undefined,
|
|
658
|
-
sliding: isRetained && mode.kind === 'live',
|
|
659
677
|
})
|
|
660
678
|
|
|
661
679
|
// Topology powers both swimlane hierarchy and application-scoped attribution.
|
|
@@ -721,7 +739,12 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
721
739
|
)
|
|
722
740
|
const focusedAppLoading = Boolean(focusedAppKey) && appsLoading
|
|
723
741
|
const focusedAppUnavailable = Boolean(focusedAppKey) && (!appScopeReady || (!appsLoading && (appsError || !focusedApp)))
|
|
724
|
-
|
|
742
|
+
// Only local truncates the swimlane fetch at 10k (the in-memory ring cap), so
|
|
743
|
+
// hitting 10k there genuinely means older app activity is hidden. Retained
|
|
744
|
+
// sends no client limit — the full hub ring is on screen (the hub caps it at
|
|
745
|
+
// the newest 50k, surfaced by the truncated flag and its banner, never a
|
|
746
|
+
// silent cut) — so this "showing newest 10k" banner would be false there.
|
|
747
|
+
const focusedAppTimelineLimited = isLocal && Boolean(focusedAppKey) && unscopedEvents.length >= 10_000
|
|
725
748
|
const clearFocusedApp = useCallback(() => {
|
|
726
749
|
const next = new URLSearchParams(searchParamsRef.current)
|
|
727
750
|
next.delete('app')
|
|
@@ -802,6 +825,26 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
802
825
|
return (
|
|
803
826
|
<div className="flex-1 flex flex-col min-h-0">
|
|
804
827
|
{appScopeBar}
|
|
828
|
+
{/* A row-capped ring load means the OLDEST part of the retention
|
|
829
|
+
window is not loaded — say so, or an old selection reads as
|
|
830
|
+
"nothing happened back then". */}
|
|
831
|
+
{ringTruncated && (
|
|
832
|
+
<div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
|
|
833
|
+
<AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
|
|
834
|
+
History is truncated: showing the newest {timelineSource.capabilities.ringLimit.toLocaleString()} events of the retention window — the oldest activity is not loaded.
|
|
835
|
+
</div>
|
|
836
|
+
)}
|
|
837
|
+
{/* Failing background polls with a loaded ring: keep the data, say
|
|
838
|
+
it's going stale. The full-screen error is reserved for no-data. */}
|
|
839
|
+
{isError && activity && (
|
|
840
|
+
<div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
|
|
841
|
+
<AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
|
|
842
|
+
Live updates are failing — the timeline may be stale.
|
|
843
|
+
<button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
|
|
844
|
+
Retry now
|
|
845
|
+
</button>
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
805
848
|
{isRetained ? (
|
|
806
849
|
<RetainedTimelineScrubber
|
|
807
850
|
source={timelineSource}
|
|
@@ -887,18 +930,26 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
887
930
|
)
|
|
888
931
|
}
|
|
889
932
|
|
|
890
|
-
// A failed fetch must not render as the swimlane
|
|
891
|
-
// that reads as a quiet cluster rather than
|
|
892
|
-
|
|
933
|
+
// A failed fetch with NOTHING loaded must not render as the swimlane
|
|
934
|
+
// "No events yet" empty state — that reads as a quiet cluster rather than
|
|
935
|
+
// a load failure. With a loaded ring on screen, a failing background poll
|
|
936
|
+
// must NOT blank it (gate on data before error); the stale-data banner
|
|
937
|
+
// below carries the warning instead.
|
|
938
|
+
if (isError && !activity) {
|
|
939
|
+
// Surface the server's own message — a generic "failed to load" would
|
|
940
|
+
// swallow whatever the hub said. "Try again" is a full resync: the
|
|
941
|
+
// retained source drops its delta cursor and reloads the whole ring.
|
|
942
|
+
const detail = error?.message?.trim()
|
|
893
943
|
return wrap(
|
|
894
944
|
<div className="flex-1 flex flex-col">
|
|
895
945
|
<div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
|
|
896
946
|
<div />
|
|
897
947
|
<ViewModeToggle viewMode={viewMode} onViewModeChange={setViewMode} />
|
|
898
948
|
</div>
|
|
899
|
-
<div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-3">
|
|
949
|
+
<div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-3 px-6">
|
|
900
950
|
<AlertTriangle className="w-10 h-10 text-amber-400/70" />
|
|
901
951
|
<p className="text-base">Failed to load timeline data</p>
|
|
952
|
+
{detail && <p className="max-w-md text-center text-sm text-theme-text-tertiary">{detail}</p>}
|
|
902
953
|
<button
|
|
903
954
|
onClick={() => refetch()}
|
|
904
955
|
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border-light rounded-lg hover:bg-theme-hover transition-colors"
|
|
@@ -971,7 +1022,6 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
|
|
|
971
1022
|
// to own the range — passing it scrubber-less would hide the list's own
|
|
972
1023
|
// range dropdown and leave the user with no time control at all.
|
|
973
1024
|
selectionWindow={showScrubber ? selection : undefined}
|
|
974
|
-
sliding={showScrubber && mode.kind === 'live'}
|
|
975
1025
|
onVisibleWindowChange={setListVisibleWindow}
|
|
976
1026
|
// Seeded with the swimlane's window at the switch (see the viewMode
|
|
977
1027
|
// effect); afterwards, dragging the strip band retargets the scroll.
|
package/src/utils/auditBadges.ts
CHANGED
|
@@ -8,7 +8,7 @@ export interface AuditBadgeMessage {
|
|
|
8
8
|
export interface AuditSeverityCounts {
|
|
9
9
|
danger: number
|
|
10
10
|
warning: number
|
|
11
|
-
/** The finding messages behind the counts,
|
|
11
|
+
/** The finding messages behind the counts, High-first, for inline tooltips.
|
|
12
12
|
* Lets a badge say WHAT is wrong on hover instead of just a count. */
|
|
13
13
|
messages: AuditBadgeMessage[]
|
|
14
14
|
}
|