@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.
- package/package.json +7 -7
- package/src/App.tsx +27 -9
- package/src/api/client.ts +21 -6
- package/src/api/config.test.ts +47 -0
- package/src/api/config.ts +15 -0
- package/src/api/diagnose.ts +10 -15
- package/src/components/diagnose/AISettings.tsx +21 -7
- package/src/components/diagnose/DiagnoseContext.tsx +82 -53
- package/src/components/diagnose/DiagnoseSurface.tsx +13 -8
- package/src/components/diagnose/parts.test.tsx +125 -0
- package/src/components/diagnose/parts.tsx +166 -75
- package/src/components/resources/ResourcesView.tsx +9 -8
- package/src/components/settings/SettingsDialog.tsx +31 -19
- package/src/components/timeline/TimelineView.tsx +17 -3
- package/src/context/DiagnoseCustomization.tsx +1 -1
|
@@ -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
|
|
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({
|
|
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<
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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-[
|
|
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:
|
|
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:
|
|
752
|
-
active:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
};
|