@skyhook-io/radar-app 1.8.13 → 1.9.1

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.
@@ -1,5 +1,6 @@
1
1
  import { useState, useCallback, useMemo } from 'react'
2
2
  import {
3
+ SEVERITY_TEXT,
3
4
  TimelineList as TimelineListUI,
4
5
  eventsForApplication,
5
6
  type ActivityTypeFilter,
@@ -15,10 +16,11 @@ import { AlertTriangle, RefreshCw } from 'lucide-react'
15
16
 
16
17
  export type { ActivityTypeFilter, ActivityFilterKey }
17
18
 
18
- // Server-side cap on the list fetch. Generous so a busy query window isn't
19
- // silently truncated the list is already bounded to the selection range, so
20
- // this only caps pathological bursts. Surfaced to the list as `truncatedAt` so a
21
- // window that does hit it shows an end-of-list note instead of dropping silently.
19
+ // Cap on the list result applied client-side in both modes over the loaded
20
+ // window (applyClientFilters slices). Generous so a busy query window isn't
21
+ // silently truncated; the list is already bounded to the selection range, so
22
+ // this only caps pathological bursts. Surfaced as `truncatedAt` so a window
23
+ // that hits it shows an end-of-list note instead of dropping silently.
22
24
  const LIST_FETCH_LIMIT = 2000
23
25
  const APP_SCOPED_FETCH_LIMIT = 10000
24
26
 
@@ -39,14 +41,12 @@ interface TimelineListProps {
39
41
  kindFilter: string[]
40
42
  onKindFilterChange: (kinds: string[]) => void
41
43
  // The shared scrubber selection [from,to]. When set (retained mode always;
42
- // local mode once the scrubber owns the range), it drives the fetch window and
44
+ // local mode once the scrubber owns the range), it drives the query window and
43
45
  // hides the built-in range dropdown so the list can't drift from the
44
- // swimlane/URL. Retained scopes server-side; local loads the ring and bounds it
45
- // client-side (see useLocalEvents).
46
+ // swimlane/URL. In both modes a window the loaded ring covers slices it
47
+ // client-side with no fetch; a frozen selection older than a truncated ring's
48
+ // oldest row fetches its own server window (see createRingEventsHook).
46
49
  selectionWindow?: { fromMs: number; toMs: number }
47
- // LIVE mode: quantize the base fetch so the sliding window doesn't churn the
48
- // query key every tick.
49
- sliding?: boolean
50
50
  // Time span of the rows visible in the list's scrollport — the host renders
51
51
  // it as the scrubber lens so scrolling the list moves the lens.
52
52
  onVisibleWindowChange?: (window: { fromMs: number; toMs: number } | null) => void
@@ -58,7 +58,7 @@ interface TimelineListProps {
58
58
  appScopeLoading?: boolean
59
59
  }
60
60
 
61
- export function TimelineList({ namespaces, onViewChange, currentView, onResourceClick, initialFilter, initialTimeRange, showDeleted, onShowDeletedChange, search, onSearchChange, activityFilter, onActivityFilterChange, kindFilter, onKindFilterChange, selectionWindow, sliding, onVisibleWindowChange, scrollToMs, focusedAppIndex, appScoped = false, topology, appScopeLoading = false }: TimelineListProps) {
61
+ export function TimelineList({ namespaces, onViewChange, currentView, onResourceClick, initialFilter, initialTimeRange, showDeleted, onShowDeletedChange, search, onSearchChange, activityFilter, onActivityFilterChange, kindFilter, onKindFilterChange, selectionWindow, onVisibleWindowChange, scrollToMs, focusedAppIndex, appScoped = false, topology, appScopeLoading = false }: TimelineListProps) {
62
62
  const hasLimitedAccess = useHasLimitedAccess()
63
63
  const timelineSource = useTimelineSource()
64
64
  const [queryParams, setQueryParams] = useState<{ timeRange: TimeRange; kinds: string[] }>({
@@ -71,7 +71,7 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
71
71
  }, [])
72
72
 
73
73
  const fetchLimit = appScoped ? APP_SCOPED_FETCH_LIMIT : LIST_FETCH_LIMIT
74
- const { data: unscopedEvents = [], isLoading, isError, refetch } = timelineSource.useEvents({
74
+ const { data: fetchedEvents, isLoading, isError, error, refetch } = timelineSource.useEvents({
75
75
  namespaces,
76
76
  kinds: queryParams.kinds,
77
77
  timeRange: queryParams.timeRange,
@@ -81,23 +81,27 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
81
81
  limit: fetchLimit,
82
82
  fromMs: selectionWindow?.fromMs,
83
83
  toMs: selectionWindow?.toMs,
84
- sliding,
85
84
  })
86
- const events = useMemo(
87
- () => appScoped
85
+ const events = useMemo(() => {
86
+ const unscoped = fetchedEvents ?? []
87
+ return appScoped
88
88
  ? focusedAppIndex
89
- ? eventsForApplication(unscopedEvents, topology, focusedAppIndex)
89
+ ? eventsForApplication(unscoped, topology, focusedAppIndex)
90
90
  : []
91
- : unscopedEvents,
92
- [appScoped, focusedAppIndex, topology, unscopedEvents],
93
- )
94
- const sourceTruncated = unscopedEvents.length >= fetchLimit
91
+ : unscoped
92
+ }, [appScoped, focusedAppIndex, topology, fetchedEvents])
93
+ const sourceTruncated = (fetchedEvents?.length ?? 0) >= fetchLimit
95
94
 
96
- if (isError) {
95
+ // Full-screen error only when nothing is loaded; a failing background poll
96
+ // with data on screen keeps rendering (data before error).
97
+ if (isError && !fetchedEvents) {
97
98
  return (
98
99
  <div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary gap-3">
99
100
  <AlertTriangle className="w-10 h-10 text-amber-400/70" />
100
101
  <p className="text-base">Failed to load timeline data</p>
102
+ {error?.message?.trim() && (
103
+ <p className="max-w-md px-6 text-center text-sm text-theme-text-tertiary">{error.message.trim()}</p>
104
+ )}
101
105
  <button
102
106
  onClick={() => refetch()}
103
107
  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"
@@ -109,34 +113,53 @@ export function TimelineList({ namespaces, onViewChange, currentView, onResource
109
113
  )
110
114
  }
111
115
 
116
+ // Failing background polls with rows on screen: keep the data, say it may
117
+ // be stale. Only when the list owns its own range — under a scrubber the
118
+ // view-level banner already reports the shared failure, and a second note
119
+ // would double up.
120
+ const staleNote = isError && fetchedEvents && !selectionWindow ? (
121
+ <div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
122
+ <AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
123
+ Live updates are failing — the list may be stale.
124
+ <button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
125
+ Retry now
126
+ </button>
127
+ </div>
128
+ ) : null
129
+
112
130
  return (
113
- <TimelineListUI
114
- events={events}
115
- isLoading={isLoading || appScopeLoading}
116
- onQueryChange={handleQueryChange}
117
- hasLimitedAccess={hasLimitedAccess}
118
- namespaces={namespaces}
119
- onViewChange={onViewChange}
120
- currentView={currentView}
121
- onResourceClick={onResourceClick}
122
- initialFilter={initialFilter}
123
- initialTimeRange={initialTimeRange}
124
- hideRangeSelector={!!selectionWindow}
125
- showDeleted={showDeleted}
126
- onShowDeletedChange={onShowDeletedChange}
127
- search={search}
128
- onSearchChange={onSearchChange}
129
- activityFilter={activityFilter}
130
- onActivityFilterChange={onActivityFilterChange}
131
- kindFilter={kindFilter}
132
- onKindFilterChange={onKindFilterChange}
133
- onVisibleWindowChange={onVisibleWindowChange}
134
- scrollToMs={scrollToMs}
135
- truncatedAt={fetchLimit}
136
- isTruncated={sourceTruncated}
137
- truncationMessage={appScoped && sourceTruncated
138
- ? `Showing application activity found in the newest ${fetchLimit.toLocaleString()} events in this range — narrow the query to see older activity`
139
- : undefined}
140
- />
131
+ <div className="flex h-full min-h-0 flex-1 flex-col">
132
+ {staleNote}
133
+ <div className="min-h-0 flex-1">
134
+ <TimelineListUI
135
+ events={events}
136
+ isLoading={isLoading || appScopeLoading}
137
+ onQueryChange={handleQueryChange}
138
+ hasLimitedAccess={hasLimitedAccess}
139
+ namespaces={namespaces}
140
+ onViewChange={onViewChange}
141
+ currentView={currentView}
142
+ onResourceClick={onResourceClick}
143
+ initialFilter={initialFilter}
144
+ initialTimeRange={initialTimeRange}
145
+ hideRangeSelector={!!selectionWindow}
146
+ showDeleted={showDeleted}
147
+ onShowDeletedChange={onShowDeletedChange}
148
+ search={search}
149
+ onSearchChange={onSearchChange}
150
+ activityFilter={activityFilter}
151
+ onActivityFilterChange={onActivityFilterChange}
152
+ kindFilter={kindFilter}
153
+ onKindFilterChange={onKindFilterChange}
154
+ onVisibleWindowChange={onVisibleWindowChange}
155
+ scrollToMs={scrollToMs}
156
+ truncatedAt={fetchLimit}
157
+ isTruncated={sourceTruncated}
158
+ truncationMessage={appScoped && sourceTruncated
159
+ ? `Showing application activity found in the newest ${fetchLimit.toLocaleString()} events in this range — narrow the query to see older activity`
160
+ : undefined}
161
+ />
162
+ </div>
163
+ </div>
141
164
  )
142
165
  }
@@ -17,6 +17,7 @@ import {
17
17
  type TimelineGrouping,
18
18
  type TimelineSort,
19
19
  type PinnedLaneRef,
20
+ useDebouncedValue,
20
21
  } from '@skyhook-io/k8s-ui'
21
22
  import { TimelineList } from './TimelineList'
22
23
  import type { ActivityFilterKey } from './TimelineList'
@@ -92,6 +93,10 @@ export type TimelineMode =
92
93
  // burying the lanes in a day of history; presets/URL widen it deliberately.
93
94
  const DEFAULT_LIVE_WIDTH_MS = 60 * 60 * 1000
94
95
  const DAY_MS = 24 * 60 * 60 * 1000
96
+ // Presets wider than this land the swimlane on a bounded recent lens instead of
97
+ // the full range, so it never renders every resource lane at once (a 30d domain
98
+ // has thousands). 7d and narrower keep the full-range lens.
99
+ const WIDE_LENS_THRESHOLD_MS = 7 * DAY_MS
95
100
  // Fallback cap for a retained hand-entered ?from&to when the source doesn't
96
101
  // declare maxRangeDays — mirrors the retained source's own default.
97
102
  const DEFAULT_MAX_RANGE_DAYS = 7
@@ -293,8 +298,8 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
293
298
  const effectiveInitialMode = scopeRequiresNamespaceFilter ? 'list' : (parseView(searchParams) ?? initialViewMode ?? DEFAULT_VIEW)
294
299
  const [viewMode, setViewMode] = useState<TimelineViewMode>(effectiveInitialMode)
295
300
  // 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.
301
+ // and so the fetch can exclude deletes at the source rather than only hiding
302
+ // them client-side.
298
303
  const [showDeleted, setShowDeleted] = useState(() => searchParams.get('deleted') !== '0')
299
304
  // ?pinnedOnly=1 is inert without pins: honoring it with no stored pins would
300
305
  // arm a filter that hides everything. Gate the read on stored pins so the param
@@ -304,6 +309,17 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
304
309
  // Search / activity-type / kind lifted here too, so they survive the view
305
310
  // switch and drive both views through one source of truth.
306
311
  const [search, setSearch] = useState(() => searchParams.get('q') ?? '')
312
+ // The `q` URL write is debounced so typing doesn't navigate per keystroke
313
+ // (clearing applies immediately — a delayed clear makes the × feel broken).
314
+ // The URL therefore lags the input while typing, so the URL→state read below
315
+ // must not sync a `q` that is merely the echo of our own write — it would
316
+ // revert in-flight keystrokes. Echoes are recognized by value: they carry
317
+ // exactly the debounced search we wrote. Ref instead of a dep: the read
318
+ // effect keys on searchParams alone (see its comment) and only needs the
319
+ // written value at fire time.
320
+ const debouncedSearch = useDebouncedValue(search, 300, (v) => v === '')
321
+ const writtenSearchRef = useRef(debouncedSearch)
322
+ writtenSearchRef.current = debouncedSearch
307
323
  // Seed the multi-select from the URL `activity` csv, else the home-page
308
324
  // deep-link preset: 'all'/undefined means no chips selected (everything).
309
325
  const [activityFilter, setActivityFilter] = useState<ActivityFilterKey[]>(
@@ -519,11 +535,21 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
519
535
  // plain fixed widths.
520
536
  const all = isLocal && scrubberDomain != null && widthMs >= scrubberDomain.maxSelectionMs
521
537
  const now = Date.now()
538
+ const sel = deriveLiveSelection(capped, now)
522
539
  setMode({ kind: 'live', widthMs: capped, all: all || undefined })
523
540
  setFrozenAsOfMs(null)
524
541
  setNowTick(now)
525
- resetLensToFull(deriveLiveSelection(capped, now))
526
- }, [isLocal, scrubberDomain, resetLensToFull])
542
+ // A full-range lens on a very wide domain (e.g. 30d) renders EVERY resource
543
+ // lane at once — thousands of rows — which freezes the browser for seconds
544
+ // before the view settles. Land directly on a bounded recent lens instead;
545
+ // the density strip still spans the full selected range for context, and the
546
+ // user scrubs it. Narrow presets (<= 7d) keep the full-range lens unchanged.
547
+ if (capped > WIDE_LENS_THRESHOLD_MS) {
548
+ resetLensToRecent(sel)
549
+ } else {
550
+ resetLensToFull(sel)
551
+ }
552
+ }, [isLocal, scrubberDomain, resetLensToFull, resetLensToRecent])
527
553
 
528
554
  // "→ Now" → LIVE, width = current selection width. Pins to now and resets the
529
555
  // lens to the live edge.
@@ -607,7 +633,9 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
607
633
  const nextPinnedOnly = sp.get('pinnedOnly') === '1' && pinnedLanes.length > 0
608
634
  setPinnedOnly((prev) => (prev === nextPinnedOnly ? prev : nextPinnedOnly))
609
635
  const nextSearch = sp.get('q') ?? ''
610
- setSearch((prev) => (prev === nextSearch ? prev : nextSearch))
636
+ if (nextSearch !== writtenSearchRef.current) {
637
+ setSearch((prev) => (prev === nextSearch ? prev : nextSearch))
638
+ }
611
639
  const nextActivity = parseActivity(sp) ?? []
612
640
  setActivityFilter((prev) => (arraysEqual(prev, nextActivity) ? prev : nextActivity))
613
641
  const nextKinds = parseKinds(sp)
@@ -624,7 +652,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
624
652
  const current = searchParamsRef.current
625
653
  const target = writeTimelineParams(
626
654
  current,
627
- { viewMode, mode, showDeleted, pinnedOnly, search, activityFilter, kindFilter, grouping, sort, selectedEventId },
655
+ { viewMode, mode, showDeleted, pinnedOnly, search: debouncedSearch, activityFilter, kindFilter, grouping, sort, selectedEventId },
628
656
  { isRetained: isRetained || isLocal, requiresNamespaceFilter: scopeRequiresNamespaceFilter },
629
657
  )
630
658
  const targetStr = target.toString()
@@ -638,24 +666,28 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
638
666
  const replace = !didMountUrlSyncRef.current || onlyHighFreqDiffer(currentStr, targetStr)
639
667
  didMountUrlSyncRef.current = true
640
668
  setSearchParamsRef.current(target, { replace })
641
- }, [viewMode, mode, showDeleted, pinnedOnly, search, activityFilter, kindFilter, grouping, sort, selectedEventId, isRetained, isLocal, scopeRequiresNamespaceFilter])
669
+ }, [viewMode, mode, showDeleted, pinnedOnly, debouncedSearch, activityFilter, kindFilter, grouping, sort, selectedEventId, isRetained, isLocal, scopeRequiresNamespaceFilter])
642
670
 
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({
671
+ // Fetch all activity - zoom controls what's visible in the UI. This ring feeds
672
+ // the swimlanes and the local strip's histogram, so it also runs in list mode
673
+ // when that strip is shown; the list itself fetches its own 2000.
674
+ const { data: activity, isLoading, isError, error, refetch, truncated: ringTruncated } = timelineSource.useEvents({
647
675
  namespaces: timelineNamespaces,
648
676
  timeRange: 'all',
649
677
  includeK8sEvents: true,
650
678
  includeManaged: true,
651
679
  includeDeleted: showDeleted,
652
- limit: 10000,
680
+ // Only the local in-memory ring imposes a client-side size cap (it can't
681
+ // hold more than its ring anyway). Retained mode renders every event its
682
+ // ring holds — the hub bounds the ring itself (retention depth + a newest-
683
+ // 50k cap flagged as truncated, never a silent cut), so a busy window
684
+ // never silently drops its oldest events on the client.
685
+ limit: isRetained ? undefined : 10000,
653
686
  // The local strip derives its histogram from this ring fetch, so it must
654
687
  // run in list mode too whenever the strip is shown.
655
688
  enabled: appScopeReady && (showSwimlanes || showLocalScrubber),
656
689
  fromMs: isRetained ? selection.fromMs : undefined,
657
690
  toMs: isRetained ? selection.toMs : undefined,
658
- sliding: isRetained && mode.kind === 'live',
659
691
  })
660
692
 
661
693
  // Topology powers both swimlane hierarchy and application-scoped attribution.
@@ -721,7 +753,12 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
721
753
  )
722
754
  const focusedAppLoading = Boolean(focusedAppKey) && appsLoading
723
755
  const focusedAppUnavailable = Boolean(focusedAppKey) && (!appScopeReady || (!appsLoading && (appsError || !focusedApp)))
724
- const focusedAppTimelineLimited = Boolean(focusedAppKey) && unscopedEvents.length >= 10_000
756
+ // Only local truncates the swimlane fetch at 10k (the in-memory ring cap), so
757
+ // hitting 10k there genuinely means older app activity is hidden. Retained
758
+ // sends no client limit — the full hub ring is on screen (the hub caps it at
759
+ // the newest 50k, surfaced by the truncated flag and its banner, never a
760
+ // silent cut) — so this "showing newest 10k" banner would be false there.
761
+ const focusedAppTimelineLimited = isLocal && Boolean(focusedAppKey) && unscopedEvents.length >= 10_000
725
762
  const clearFocusedApp = useCallback(() => {
726
763
  const next = new URLSearchParams(searchParamsRef.current)
727
764
  next.delete('app')
@@ -802,6 +839,26 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
802
839
  return (
803
840
  <div className="flex-1 flex flex-col min-h-0">
804
841
  {appScopeBar}
842
+ {/* A row-capped ring load means the OLDEST part of the retention
843
+ window is not loaded — say so, or an old selection reads as
844
+ "nothing happened back then". */}
845
+ {ringTruncated && (
846
+ <div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
847
+ <AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
848
+ History is truncated: showing the newest {timelineSource.capabilities.ringLimit.toLocaleString()} events of the retention window — the oldest activity is not loaded.
849
+ </div>
850
+ )}
851
+ {/* Failing background polls with a loaded ring: keep the data, say
852
+ it's going stale. The full-screen error is reserved for no-data. */}
853
+ {isError && activity && (
854
+ <div className="flex items-center gap-1.5 border-b border-theme-border px-4 py-1.5 text-xs text-theme-text-tertiary">
855
+ <AlertTriangle className={`h-3.5 w-3.5 shrink-0 ${SEVERITY_TEXT.warning}`} />
856
+ Live updates are failing — the timeline may be stale.
857
+ <button type="button" onClick={() => refetch()} className="underline hover:text-theme-text-primary">
858
+ Retry now
859
+ </button>
860
+ </div>
861
+ )}
805
862
  {isRetained ? (
806
863
  <RetainedTimelineScrubber
807
864
  source={timelineSource}
@@ -887,18 +944,26 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
887
944
  )
888
945
  }
889
946
 
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) {
947
+ // A failed fetch with NOTHING loaded must not render as the swimlane
948
+ // "No events yet" empty state — that reads as a quiet cluster rather than
949
+ // a load failure. With a loaded ring on screen, a failing background poll
950
+ // must NOT blank it (gate on data before error); the stale-data banner
951
+ // below carries the warning instead.
952
+ if (isError && !activity) {
953
+ // Surface the server's own message — a generic "failed to load" would
954
+ // swallow whatever the hub said. "Try again" is a full resync: the
955
+ // retained source drops its delta cursor and reloads the whole ring.
956
+ const detail = error?.message?.trim()
893
957
  return wrap(
894
958
  <div className="flex-1 flex flex-col">
895
959
  <div className="flex items-center justify-between px-4 py-2 border-b border-theme-border">
896
960
  <div />
897
961
  <ViewModeToggle viewMode={viewMode} onViewModeChange={setViewMode} />
898
962
  </div>
899
- <div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-3">
963
+ <div className="flex-1 flex flex-col items-center justify-center text-theme-text-tertiary gap-3 px-6">
900
964
  <AlertTriangle className="w-10 h-10 text-amber-400/70" />
901
965
  <p className="text-base">Failed to load timeline data</p>
966
+ {detail && <p className="max-w-md text-center text-sm text-theme-text-tertiary">{detail}</p>}
902
967
  <button
903
968
  onClick={() => refetch()}
904
969
  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 +1036,6 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
971
1036
  // to own the range — passing it scrubber-less would hide the list's own
972
1037
  // range dropdown and leave the user with no time control at all.
973
1038
  selectionWindow={showScrubber ? selection : undefined}
974
- sliding={showScrubber && mode.kind === 'live'}
975
1039
  onVisibleWindowChange={setListVisibleWindow}
976
1040
  // Seeded with the swimlane's window at the switch (see the viewMode
977
1041
  // effect); afterwards, dragging the strip band retargets the scroll.
@@ -41,7 +41,7 @@ export type DiagnoseConsentCopy = {
41
41
  /** Detail list under the body; each entry is rendered as its own "•" row. */
42
42
  bullets?: ReactNode[];
43
43
  /** Label for the settings link. `null` hides it — for hosts with one fixed
44
- * agent and no isolation choice, where it would open an empty dialog. */
44
+ * agent and no execution-profile choice, where it would open an empty dialog. */
45
45
  settingsLabel?: string | null;
46
46
  approveLabel?: string;
47
47
  };
@@ -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
  }