@skyhook-io/radar-app 1.9.0 → 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.
@@ -60,6 +60,7 @@ interface ConfigResponse {
60
60
  interface SettingsDialogProps {
61
61
  open: boolean
62
62
  onClose: () => void
63
+ initialSection?: SettingsSectionId
63
64
  }
64
65
 
65
66
  // The settings surface splits into three honest apply buckets:
@@ -68,7 +69,7 @@ interface SettingsDialogProps {
68
69
  // • Live integrations (Prometheus, Argo CD) — their own Apply/Connect endpoints
69
70
  // re-point the running server; effect immediately, NOT part of footer dirty.
70
71
  // • AI diagnose — client-side prefs, self-saving, editable by everyone.
71
- type SectionId =
72
+ export type SettingsSectionId =
72
73
  | 'overview' | 'perms' | 'connection' | 'prometheus' | 'argocd' | 'ai' | 'advanced'
73
74
 
74
75
  // Only STARTUP fields count toward footer dirty. Integration fields (prometheusUrl,
@@ -89,7 +90,11 @@ function normalizeStartup(c: Config) {
89
90
  }
90
91
  }
91
92
 
92
- export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
93
+ export function SettingsDialog({
94
+ open,
95
+ onClose,
96
+ initialSection = 'overview',
97
+ }: SettingsDialogProps) {
93
98
  const dialogRef = useRef<HTMLDivElement>(null)
94
99
  const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
95
100
  const { data: versionInfo } = useVersionCheck()
@@ -107,7 +112,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
107
112
  const [saving, setSaving] = useState(false)
108
113
  const [saveMessage, setSaveMessage] = useState<string | null>(null)
109
114
  const [loadError, setLoadError] = useState<string | null>(null)
110
- const [section, setSection] = useState<SectionId>('overview')
115
+ const [section, setSection] = useState<SettingsSectionId>('overview')
111
116
  const [confirmingClose, setConfirmingClose] = useState(false)
112
117
 
113
118
  // AI Diagnosis prefs are client-side (localStorage) and now SELF-SAVING: the
@@ -117,14 +122,14 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
117
122
  const aiAvailable = diag.available && diag.agents.length > 0
118
123
  const [aiDraft, setAiDraft] = useState<AIDraft>({
119
124
  agent: diag.selectedAgent,
120
- isolated: diag.isolated,
125
+ profile: diag.profile,
121
126
  model: diag.model,
122
127
  effort: diag.effort,
123
128
  })
124
129
  const [aiSaved, setAiSaved] = useState(false)
125
130
  const aiDirty =
126
131
  aiDraft.agent !== diag.selectedAgent ||
127
- aiDraft.isolated !== diag.isolated ||
132
+ aiDraft.profile !== diag.profile ||
128
133
  aiDraft.model !== diag.model ||
129
134
  aiDraft.effort !== diag.effort
130
135
 
@@ -158,16 +163,10 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
158
163
  setAiSaved(false)
159
164
  setAiDraft({
160
165
  agent: diag.selectedAgent,
161
- isolated: diag.isolated,
166
+ profile: diag.profile,
162
167
  model: diag.model,
163
168
  effort: diag.effort,
164
169
  })
165
- // Overview is the landing section — a status-at-a-glance of what Radar is
166
- // connected to (cluster, integrations, MCP, AI), useful to owners and
167
- // viewers alike, rather than dropping owners on a config form or everyone
168
- // on a permissions dump.
169
- setSection('overview')
170
-
171
170
  fetch(apiUrl('/config'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
172
171
  .then((res) => {
173
172
  if (!res.ok) throw new Error(`HTTP ${res.status}`)
@@ -185,6 +184,19 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
185
184
  // eslint-disable-next-line react-hooks/exhaustive-deps
186
185
  }, [open])
187
186
 
187
+ useEffect(() => {
188
+ if (open) setSection(initialSection)
189
+ }, [open, initialSection])
190
+
191
+ useEffect(() => {
192
+ if (!open || diag.agents.length === 0) return
193
+ setAiDraft((current) => {
194
+ const profiles = diag.agents.find((agent) => agent.name === current.agent)?.profiles ?? []
195
+ if (profiles.length === 0 || profiles.includes(current.profile)) return current
196
+ return { ...current, profile: profiles[0] }
197
+ })
198
+ }, [open, diag.agents])
199
+
188
200
  const updateConfigField = useCallback(<K extends keyof Config>(field: K, value: Config[K]) => {
189
201
  setEditedConfig((prev) => ({ ...prev, [field]: value }))
190
202
  setSaveMessage(null)
@@ -234,7 +246,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
234
246
  // agent first, then restore the draft's model/effort.
235
247
  const saveAi = useCallback(() => {
236
248
  diag.setSelectedAgent(aiDraft.agent)
237
- diag.setIsolated(aiDraft.isolated)
249
+ diag.setProfile(aiDraft.profile)
238
250
  diag.setModel(aiDraft.model)
239
251
  diag.setEffort(aiDraft.effort)
240
252
  setAiSaved(true)
@@ -335,7 +347,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
335
347
  // Fixed height so the dialog doesn't jump when switching tabs — short
336
348
  // tabs leave breathing room, tall ones scroll inside the content pane.
337
349
  // max-h keeps it on-screen on short viewports.
338
- 'sm:rounded-xl sm:max-w-4xl sm:mx-4 sm:h-[620px] sm:max-h-[85vh]',
350
+ 'sm:rounded-xl sm:max-w-4xl sm:mx-4 sm:h-[660px] sm:max-h-[85vh]',
339
351
  TRANSITION_PANEL,
340
352
  isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
341
353
  )}
@@ -667,7 +679,7 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
667
679
  // -- Sidebar primitives -------------------------------------------------------
668
680
 
669
681
  interface NavItemDef {
670
- id: SectionId
682
+ id: SettingsSectionId
671
683
  label: string
672
684
  icon: LucideIcon
673
685
  ownerOnly: boolean
@@ -748,8 +760,8 @@ function SectionPane({
748
760
  locked,
749
761
  children,
750
762
  }: {
751
- id: SectionId
752
- active: SectionId
763
+ id: SettingsSectionId
764
+ active: SettingsSectionId
753
765
  title: string
754
766
  caption?: string
755
767
  live?: boolean
@@ -802,7 +814,7 @@ function LockWall() {
802
814
  type OverviewTone = 'ok' | 'warn' | 'off' | 'unknown'
803
815
 
804
816
  interface OverviewRow {
805
- id: SectionId
817
+ id: SettingsSectionId
806
818
  icon: LucideIcon
807
819
  label: string
808
820
  tone: OverviewTone
@@ -817,7 +829,7 @@ interface OverviewRow {
817
829
  // probe, so we don't want it firing when Settings opens on another section);
818
830
  // cluster and Prometheus status are shared app-wide caches, so they're read
819
831
  // unconditionally.
820
- function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s: SectionId) => void }) {
832
+ function OverviewPanel({ active, onNavigate }: { active: boolean; onNavigate: (s: SettingsSectionId) => void }) {
821
833
  const { data: cluster } = useClusterInfo()
822
834
  const { data: prom } = usePrometheusStatus()
823
835
  const { data: argo } = useArgoStatus(active)
@@ -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'
@@ -308,6 +309,17 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
308
309
  // Search / activity-type / kind lifted here too, so they survive the view
309
310
  // switch and drive both views through one source of truth.
310
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
311
323
  // Seed the multi-select from the URL `activity` csv, else the home-page
312
324
  // deep-link preset: 'all'/undefined means no chips selected (everything).
313
325
  const [activityFilter, setActivityFilter] = useState<ActivityFilterKey[]>(
@@ -621,7 +633,9 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
621
633
  const nextPinnedOnly = sp.get('pinnedOnly') === '1' && pinnedLanes.length > 0
622
634
  setPinnedOnly((prev) => (prev === nextPinnedOnly ? prev : nextPinnedOnly))
623
635
  const nextSearch = sp.get('q') ?? ''
624
- setSearch((prev) => (prev === nextSearch ? prev : nextSearch))
636
+ if (nextSearch !== writtenSearchRef.current) {
637
+ setSearch((prev) => (prev === nextSearch ? prev : nextSearch))
638
+ }
625
639
  const nextActivity = parseActivity(sp) ?? []
626
640
  setActivityFilter((prev) => (arraysEqual(prev, nextActivity) ? prev : nextActivity))
627
641
  const nextKinds = parseKinds(sp)
@@ -638,7 +652,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
638
652
  const current = searchParamsRef.current
639
653
  const target = writeTimelineParams(
640
654
  current,
641
- { viewMode, mode, showDeleted, pinnedOnly, search, activityFilter, kindFilter, grouping, sort, selectedEventId },
655
+ { viewMode, mode, showDeleted, pinnedOnly, search: debouncedSearch, activityFilter, kindFilter, grouping, sort, selectedEventId },
642
656
  { isRetained: isRetained || isLocal, requiresNamespaceFilter: scopeRequiresNamespaceFilter },
643
657
  )
644
658
  const targetStr = target.toString()
@@ -652,7 +666,7 @@ export function TimelineView({ namespaces, onResourceClick, initialViewMode, ini
652
666
  const replace = !didMountUrlSyncRef.current || onlyHighFreqDiffer(currentStr, targetStr)
653
667
  didMountUrlSyncRef.current = true
654
668
  setSearchParamsRef.current(target, { replace })
655
- }, [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])
656
670
 
657
671
  // Fetch all activity - zoom controls what's visible in the UI. This ring feeds
658
672
  // the swimlanes and the local strip's histogram, so it also runs in list mode
@@ -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
  };