@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.
@@ -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 swimlane fetch can exclude deletes server-side (before LIMIT)
297
- // rather than only hiding them client-side after the 10k cap.
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
- resetLensToFull(deriveLiveSelection(capped, now))
526
- }, [isLocal, scrubberDomain, resetLensToFull])
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. The heavy 10k
644
- // ring feeds the swimlanes and the local strip's histogram, so it also runs in
645
- // list mode when that strip is shown; the list itself fetches its own 2000.
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
- limit: 10000,
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
- const focusedAppTimelineLimited = Boolean(focusedAppKey) && unscopedEvents.length >= 10_000
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 "No events yet" empty state —
891
- // that reads as a quiet cluster rather than a load failure.
892
- if (isError) {
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.
@@ -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, danger-first, for inline tooltips.
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
  }