@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.
- package/package.json +7 -7
- package/src/App.tsx +34 -13
- 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/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 +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/home/mcpToolCatalog.ts +1 -1
- package/src/components/resources/ResourcesView.tsx +9 -8
- package/src/components/settings/SettingsDialog.tsx +31 -19
- package/src/components/timeline/RetainedTimelineScrubber.tsx +21 -13
- package/src/components/timeline/TimelineList.tsx +72 -49
- package/src/components/timeline/TimelineView.tsx +83 -19
- package/src/context/DiagnoseCustomization.tsx +1 -1
- package/src/utils/auditBadges.ts +1 -1
|
@@ -27,13 +27,14 @@ import {
|
|
|
27
27
|
type Diagnosis,
|
|
28
28
|
type DiagnoseStep,
|
|
29
29
|
type AgentInfo,
|
|
30
|
+
type ExecutionProfile,
|
|
30
31
|
type RunSummary,
|
|
31
32
|
} from "../../api/diagnose";
|
|
32
33
|
import { StatusDot } from "@skyhook-io/k8s-ui";
|
|
33
34
|
import { Markdown } from "../ui/Markdown";
|
|
34
35
|
|
|
35
|
-
// Segmented two-or-more-way selector — shared shape for the agent
|
|
36
|
-
//
|
|
36
|
+
// Segmented two-or-more-way selector — shared shape for the agent and execution
|
|
37
|
+
// profile pickers.
|
|
37
38
|
function Segmented<T extends string | boolean>({
|
|
38
39
|
label,
|
|
39
40
|
options,
|
|
@@ -228,15 +229,15 @@ function SelectMenu({
|
|
|
228
229
|
);
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
// AgentControls is the full AI-diagnosis config block (agent,
|
|
232
|
+
// AgentControls is the full AI-diagnosis config block (agent, execution profile, model,
|
|
232
233
|
// effort) — pure + prop-driven. It lives in Settings, not the investigation panel,
|
|
233
234
|
// since these are set-once preferences rather than per-run knobs.
|
|
234
235
|
export function AgentControls({
|
|
235
236
|
agents,
|
|
236
237
|
selectedAgent,
|
|
237
238
|
onSelectAgent,
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
profile,
|
|
240
|
+
onSetProfile,
|
|
240
241
|
model,
|
|
241
242
|
onSetModel,
|
|
242
243
|
effort,
|
|
@@ -245,8 +246,8 @@ export function AgentControls({
|
|
|
245
246
|
agents: AgentInfo[];
|
|
246
247
|
selectedAgent: string;
|
|
247
248
|
onSelectAgent: (name: string) => void;
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
profile: ExecutionProfile;
|
|
250
|
+
onSetProfile: (v: ExecutionProfile) => void;
|
|
250
251
|
model: string;
|
|
251
252
|
onSetModel: (v: string) => void;
|
|
252
253
|
effort: string;
|
|
@@ -255,6 +256,17 @@ export function AgentControls({
|
|
|
255
256
|
const isCodex = selectedAgent === "codex";
|
|
256
257
|
const isClaude = selectedAgent === "claude";
|
|
257
258
|
const isCursor = selectedAgent === "cursor-agent";
|
|
259
|
+
const selectedAgentInfo = agents.find((a) => a.name === selectedAgent);
|
|
260
|
+
const selectedAgentLabel =
|
|
261
|
+
selectedAgentInfo?.label || selectedAgent || "agent";
|
|
262
|
+
const profiles = selectedAgentInfo?.profiles ?? [];
|
|
263
|
+
const shownProfile = profiles.includes(profile)
|
|
264
|
+
? profile
|
|
265
|
+
: (profiles[0] ?? profile);
|
|
266
|
+
const profileLabels: Record<ExecutionProfile, string> = {
|
|
267
|
+
safeguarded: "Radar safeguards",
|
|
268
|
+
"full-local": `Your ${selectedAgentLabel} setup`,
|
|
269
|
+
};
|
|
258
270
|
return (
|
|
259
271
|
<div className="space-y-3">
|
|
260
272
|
{agents.length >= 2 && (
|
|
@@ -268,29 +280,66 @@ export function AgentControls({
|
|
|
268
280
|
}))}
|
|
269
281
|
/>
|
|
270
282
|
)}
|
|
271
|
-
{
|
|
283
|
+
{profiles.length > 0 && (
|
|
272
284
|
<div>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
285
|
+
{profiles.length > 1 ? (
|
|
286
|
+
<>
|
|
287
|
+
<Segmented<ExecutionProfile>
|
|
288
|
+
label="How Radar runs it"
|
|
289
|
+
value={shownProfile}
|
|
290
|
+
onChange={onSetProfile}
|
|
291
|
+
options={profiles.map((value) => ({
|
|
292
|
+
value,
|
|
293
|
+
label: profileLabels[value],
|
|
294
|
+
}))}
|
|
295
|
+
/>
|
|
296
|
+
{shownProfile === "safeguarded" ? (
|
|
297
|
+
<p className="mt-1.5 text-[11px] leading-snug text-theme-text-tertiary">
|
|
298
|
+
{isClaude
|
|
299
|
+
? "Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
|
|
300
|
+
: isCodex
|
|
301
|
+
? "Radar excludes your Codex configuration and other MCP servers. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
|
|
302
|
+
: "Radar uses this agent’s safeguarded execution profile. Review the agent’s documented restrictions before continuing."}
|
|
303
|
+
</p>
|
|
304
|
+
) : (
|
|
305
|
+
<div className="mt-1.5 flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
|
|
306
|
+
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
|
|
307
|
+
<span>
|
|
308
|
+
Uses your agent's normal configuration and other
|
|
309
|
+
configured tools and MCP servers. Radar cannot constrain that
|
|
310
|
+
external tooling; it may access local files or the network
|
|
311
|
+
and may be able to change your cluster.{" "}
|
|
312
|
+
{isClaude
|
|
313
|
+
? "Claude uses the permissions from your setup; Radar does not override them."
|
|
314
|
+
: "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
|
|
315
|
+
Choose this only when you need that setup.
|
|
316
|
+
</span>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</>
|
|
320
|
+
) : shownProfile === "safeguarded" ? (
|
|
321
|
+
<div className="flex items-start gap-1.5 rounded border border-theme-border bg-theme-base p-2 text-[11px] leading-snug text-theme-text-secondary">
|
|
322
|
+
<ShieldCheck className="mt-0.5 h-3 w-3 shrink-0 text-accent" />
|
|
323
|
+
<span>
|
|
324
|
+
Radar always runs this agent with safeguards.
|
|
325
|
+
{isClaude
|
|
326
|
+
? " Claude’s built-in tools are disabled, and MCP access is limited to Radar’s read-only investigation tools. Your Claude settings, hooks, and CLAUDE.md instructions still apply and are outside Radar’s control."
|
|
327
|
+
: isCodex
|
|
328
|
+
? " Your Codex configuration and other MCP servers are excluded. Codex’s sandboxed shell can still read files on this machine; it cannot write or reach the network."
|
|
329
|
+
: " Review the agent’s documented restrictions before continuing."}
|
|
330
|
+
</span>
|
|
331
|
+
</div>
|
|
287
332
|
) : (
|
|
288
|
-
<div className="
|
|
333
|
+
<div className="flex items-start gap-1.5 rounded border border-amber-500/40 bg-amber-500/10 p-2 text-[11px] leading-snug text-theme-text-secondary">
|
|
289
334
|
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
|
|
290
335
|
<span>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
local files
|
|
336
|
+
Radar must use this agent's normal setup. Radar cannot
|
|
337
|
+
constrain its external tools or MCP servers; they may access
|
|
338
|
+
local files or the network and may be able to change your
|
|
339
|
+
cluster. Radar still enables the agent CLI's own sandbox,
|
|
340
|
+
but that sandbox does not constrain external MCP servers.
|
|
341
|
+
{isCursor &&
|
|
342
|
+
" Cursor always loads your global MCP servers, so Radar cannot exclude them."}
|
|
294
343
|
</span>
|
|
295
344
|
</div>
|
|
296
345
|
)}
|
|
@@ -304,7 +353,7 @@ export function AgentControls({
|
|
|
304
353
|
onChange={onSetModel}
|
|
305
354
|
hint="Aliases always resolve to the latest of that tier."
|
|
306
355
|
/>
|
|
307
|
-
) : (
|
|
356
|
+
) : isCodex || isCursor ? (
|
|
308
357
|
<TextField
|
|
309
358
|
label="Model"
|
|
310
359
|
value={model}
|
|
@@ -317,11 +366,19 @@ export function AgentControls({
|
|
|
317
366
|
hint={
|
|
318
367
|
isCursor
|
|
319
368
|
? "Leave empty for your Cursor default, or enter a model slug Cursor supports."
|
|
320
|
-
:
|
|
321
|
-
? "
|
|
369
|
+
: shownProfile === "full-local"
|
|
370
|
+
? "Your Codex setup uses its configured model; set a slug here to override it."
|
|
322
371
|
: "Leave empty for Codex's default, or enter a model your Codex version supports."
|
|
323
372
|
}
|
|
324
373
|
/>
|
|
374
|
+
) : (
|
|
375
|
+
<TextField
|
|
376
|
+
label="Model"
|
|
377
|
+
value={model}
|
|
378
|
+
placeholder="Default"
|
|
379
|
+
onChange={onSetModel}
|
|
380
|
+
hint="Leave empty for the agent's default, or enter a model identifier it supports."
|
|
381
|
+
/>
|
|
325
382
|
)}
|
|
326
383
|
{isCodex && (
|
|
327
384
|
<SelectMenu
|
|
@@ -591,10 +648,12 @@ function ConsentCardShell({
|
|
|
591
648
|
bullets,
|
|
592
649
|
settingsLabel,
|
|
593
650
|
approveLabel = "Approve & investigate",
|
|
651
|
+
warning = false,
|
|
594
652
|
onOpenSettings,
|
|
595
653
|
onApprove,
|
|
596
654
|
onCancel,
|
|
597
655
|
}: DiagnoseConsentCopy & {
|
|
656
|
+
warning?: boolean;
|
|
598
657
|
onOpenSettings?: () => void;
|
|
599
658
|
onApprove: () => void;
|
|
600
659
|
onCancel: () => void;
|
|
@@ -606,9 +665,19 @@ function ConsentCardShell({
|
|
|
606
665
|
? "Change the agent and how it runs in Settings"
|
|
607
666
|
: settingsLabel;
|
|
608
667
|
return (
|
|
609
|
-
<div
|
|
668
|
+
<div
|
|
669
|
+
className={
|
|
670
|
+
warning
|
|
671
|
+
? "rounded-lg border border-amber-500/40 bg-amber-500/10 p-4"
|
|
672
|
+
: "rounded-lg border border-theme-border bg-theme-elevated p-4"
|
|
673
|
+
}
|
|
674
|
+
>
|
|
610
675
|
<div className="mb-2 flex items-center gap-2">
|
|
611
|
-
|
|
676
|
+
{warning ? (
|
|
677
|
+
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
678
|
+
) : (
|
|
679
|
+
<ShieldCheck className="h-4 w-4 text-accent" />
|
|
680
|
+
)}
|
|
612
681
|
<div className="text-sm font-medium text-theme-text-primary">
|
|
613
682
|
{title}
|
|
614
683
|
</div>
|
|
@@ -660,7 +729,7 @@ function ConsentCardShell({
|
|
|
660
729
|
export function ConsentCard({
|
|
661
730
|
agentName,
|
|
662
731
|
agent,
|
|
663
|
-
|
|
732
|
+
profile,
|
|
664
733
|
copy,
|
|
665
734
|
onOpenSettings,
|
|
666
735
|
onApprove,
|
|
@@ -668,7 +737,7 @@ export function ConsentCard({
|
|
|
668
737
|
}: {
|
|
669
738
|
agentName: string;
|
|
670
739
|
agent?: string;
|
|
671
|
-
|
|
740
|
+
profile: ExecutionProfile;
|
|
672
741
|
copy?: DiagnoseConsentCopy;
|
|
673
742
|
onOpenSettings?: () => void;
|
|
674
743
|
onApprove: () => void;
|
|
@@ -679,17 +748,19 @@ export function ConsentCard({
|
|
|
679
748
|
// Tier 1: a host (e.g. radar-hub-web) supplied its own copy — use it verbatim.
|
|
680
749
|
if (copy) return <ConsentCardShell {...copy} {...chrome} />;
|
|
681
750
|
|
|
682
|
-
// Tier 2: OSS BYO-local default. Cursor can't be isolated (no flag suppresses
|
|
683
|
-
// its global MCP servers), so it gets its own honest framing rather than the
|
|
684
|
-
// isolated/my-setup pair.
|
|
685
|
-
const isCursor = agent === "cursor-agent";
|
|
686
751
|
return (
|
|
687
752
|
<ConsentCardShell
|
|
688
753
|
{...chrome}
|
|
754
|
+
warning={profile === "full-local"}
|
|
755
|
+
approveLabel={
|
|
756
|
+
profile === "full-local"
|
|
757
|
+
? "Continue with my agent setup"
|
|
758
|
+
: "Approve & investigate"
|
|
759
|
+
}
|
|
689
760
|
title={
|
|
690
|
-
|
|
691
|
-
? "Run
|
|
692
|
-
:
|
|
761
|
+
profile === "safeguarded"
|
|
762
|
+
? "Run an AI investigation with Radar safeguards?"
|
|
763
|
+
: `Run using your ${agentName} setup?`
|
|
693
764
|
}
|
|
694
765
|
body={
|
|
695
766
|
<>
|
|
@@ -698,48 +769,68 @@ export function ConsentCard({
|
|
|
698
769
|
your own {agentName}
|
|
699
770
|
</span>{" "}
|
|
700
771
|
on your machine — no Radar cloud, no API key, no account. Radar sends
|
|
701
|
-
this resource's spec, recent events, and pod logs to it (and on
|
|
702
|
-
its model provider under your account, not to Radar). Transcripts
|
|
703
|
-
kept in your local Radar history on this machine until cleared.
|
|
704
|
-
{
|
|
772
|
+
this resource's spec, recent events, and pod logs to it (and on
|
|
773
|
+
to its model provider under your account, not to Radar). Transcripts
|
|
774
|
+
are kept in your local Radar history on this machine until cleared.
|
|
775
|
+
{profile === "safeguarded" && (
|
|
705
776
|
<>
|
|
706
777
|
{" "}
|
|
707
|
-
|
|
708
|
-
<span className="font-medium">read</span>
|
|
709
|
-
cluster.
|
|
778
|
+
Radar's investigation tools can only{" "}
|
|
779
|
+
<span className="font-medium">read</span> your cluster.
|
|
710
780
|
</>
|
|
711
781
|
)}
|
|
712
782
|
</>
|
|
713
783
|
}
|
|
714
|
-
bullets={
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
784
|
+
bullets={
|
|
785
|
+
profile === "safeguarded"
|
|
786
|
+
? [
|
|
787
|
+
agent === "claude" ? (
|
|
788
|
+
<>
|
|
789
|
+
Radar safeguards disable Claude's built-in tools and limit
|
|
790
|
+
MCP access to Radar's read-only investigation tools. Your
|
|
791
|
+
Claude settings, hooks, and CLAUDE.md instructions still apply
|
|
792
|
+
and are outside Radar's control.
|
|
793
|
+
</>
|
|
794
|
+
) : agent === "codex" ? (
|
|
795
|
+
<>
|
|
796
|
+
Radar safeguards exclude your Codex configuration and other MCP
|
|
797
|
+
servers. Codex's sandboxed shell can still read files on
|
|
798
|
+
this machine; it cannot write or reach the network.
|
|
799
|
+
</>
|
|
800
|
+
) : (
|
|
801
|
+
<>
|
|
802
|
+
Radar uses this agent's safeguarded execution profile.
|
|
803
|
+
Review the agent's documented restrictions before
|
|
804
|
+
continuing.
|
|
805
|
+
</>
|
|
806
|
+
),
|
|
807
|
+
]
|
|
808
|
+
: [
|
|
728
809
|
<>
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
810
|
+
Radar cannot constrain the agent's other configured tools or
|
|
811
|
+
MCP servers. They may access local files or the network and may
|
|
812
|
+
be able to change your cluster.
|
|
813
|
+
</>,
|
|
814
|
+
agent === "claude" ? (
|
|
815
|
+
<>
|
|
816
|
+
Claude uses the permissions from your setup; Radar does not
|
|
817
|
+
override them.
|
|
818
|
+
</>
|
|
819
|
+
) : (
|
|
820
|
+
<>
|
|
821
|
+
Radar still enables the agent CLI's own sandbox, but that
|
|
822
|
+
sandbox does not constrain external MCP servers.
|
|
823
|
+
{agent === "cursor-agent" && (
|
|
824
|
+
<>
|
|
825
|
+
{" "}
|
|
826
|
+
Cursor always loads your global MCP servers; Radar cannot
|
|
827
|
+
exclude them.
|
|
828
|
+
</>
|
|
829
|
+
)}
|
|
830
|
+
</>
|
|
831
|
+
),
|
|
832
|
+
]
|
|
833
|
+
}
|
|
743
834
|
/>
|
|
744
835
|
);
|
|
745
836
|
}
|
|
@@ -141,7 +141,7 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
|
|
|
141
141
|
params: [
|
|
142
142
|
{ arg: 'namespace', desc: 'filter to a specific namespace' },
|
|
143
143
|
{ arg: 'category', desc: 'Security, Reliability, or Efficiency' },
|
|
144
|
-
{ arg: 'severity', desc: '
|
|
144
|
+
{ arg: 'severity', desc: 'posture priority: critical, high, medium, or low (built-ins use high or medium)' },
|
|
145
145
|
{ arg: 'limit', desc: 'max findings (default 30, max 100)' },
|
|
146
146
|
],
|
|
147
147
|
},
|
|
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'
|
|
|
4
4
|
import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
|
|
5
5
|
import { isBadgeWorthy } from '../../utils/auditBadges'
|
|
6
6
|
import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
|
|
7
|
-
import { apiUrl, getAuthHeaders, getCredentialsMode,
|
|
7
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
|
|
8
8
|
import { useAPIResources } from '../../api/apiResources'
|
|
9
9
|
import { useConnection } from '../../context/ConnectionContext'
|
|
10
10
|
import { initNavigationMap } from '@skyhook-io/k8s-ui'
|
|
@@ -198,7 +198,13 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
198
198
|
const podCountAllowsBulkMetrics = countsData != null && podCountKnown && !podCountUnavailable && (podCount ?? 0) <= LARGE_RESOURCE_LIST_LIMIT
|
|
199
199
|
const selectedKindName = selectedKind?.name.toLowerCase() ?? ''
|
|
200
200
|
const topPodMetricsEnabled = selectedKindName === 'pods' && podCountAllowsBulkMetrics
|
|
201
|
-
|
|
201
|
+
// Node metrics back the Nodes table and, for the Pods table, the pod-vs-node
|
|
202
|
+
// context line in the CPU/Memory tooltip (a pod can be fine against its own
|
|
203
|
+
// limit yet at risk from a saturated node). Nodes are cluster-wide, so the
|
|
204
|
+
// pods case is not gated on the namespace filter.
|
|
205
|
+
const topNodeMetricsEnabled =
|
|
206
|
+
((selectedKindName === 'nodes' && namespaces.length === 0) || selectedKindName === 'pods') &&
|
|
207
|
+
podCountAllowsBulkMetrics
|
|
202
208
|
const largeListGuard = selectedKind && largeListBlocked
|
|
203
209
|
? {
|
|
204
210
|
kind: selectedKind.name,
|
|
@@ -295,13 +301,8 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
295
301
|
// any host that mounts RadarApp under a non-empty basename (Radar Cloud).
|
|
296
302
|
// Strip the basename here so react-router can re-apply it cleanly.
|
|
297
303
|
const handleNavigate = useMemo(() => {
|
|
298
|
-
const base = getBasename()
|
|
299
304
|
return (path: string, options?: { replace?: boolean }) => {
|
|
300
|
-
|
|
301
|
-
if (base && (p === base || p.startsWith(base + '/') || p.startsWith(base + '?'))) {
|
|
302
|
-
p = p.slice(base.length) || '/'
|
|
303
|
-
}
|
|
304
|
-
navigate(p, { replace: options?.replace })
|
|
305
|
+
navigate(stripBasename(path), { replace: options?.replace })
|
|
305
306
|
}
|
|
306
307
|
}, [navigate])
|
|
307
308
|
|
|
@@ -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)
|
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
type ScrubberRange,
|
|
14
14
|
type TimelineLiveState,
|
|
15
15
|
} from '@skyhook-io/k8s-ui'
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
import {
|
|
17
|
+
RETAINED_CLOCK_SKEW_SLACK_MS,
|
|
18
|
+
type TimelineSource,
|
|
19
|
+
type TimelineOverviewBucket,
|
|
20
|
+
type TimelineOverviewResult,
|
|
20
21
|
} from '../../api/timelineSource'
|
|
21
22
|
import { getApiBase } from '../../api/config'
|
|
22
23
|
|
|
@@ -25,8 +26,11 @@ const DAY_MS = 24 * HOUR_MS
|
|
|
25
26
|
const EMPTY_BUCKETS: TimelineOverviewBucket[] = []
|
|
26
27
|
const MAX_STRIP_BARS = 512
|
|
27
28
|
|
|
28
|
-
//
|
|
29
|
-
|
|
29
|
+
// Absolute per-request ceiling on the retained events endpoint, matching the
|
|
30
|
+
// hub's own timelineEventsMaxRange. The effective cap is the smaller of this
|
|
31
|
+
// and the embedder's declared retention depth (maxRangeDays), so a host that
|
|
32
|
+
// advertises 30d of retention can load all 30d in one view.
|
|
33
|
+
const MAX_SELECTION_MS = 31 * DAY_MS
|
|
30
34
|
|
|
31
35
|
// Group the server's hour buckets into fixed display buckets aligned to the
|
|
32
36
|
// display size, summing counts. The host owns this so the pure scrubber only
|
|
@@ -83,8 +87,8 @@ export function buildPresets(maxRangeDays: number): ScrubberPreset[] {
|
|
|
83
87
|
{ label: '24h', ms: DAY_MS },
|
|
84
88
|
{ label: '7d', ms: 7 * DAY_MS },
|
|
85
89
|
]
|
|
86
|
-
// 30d
|
|
87
|
-
//
|
|
90
|
+
// 30d loads the full retained window in one request (bounded by the hub's
|
|
91
|
+
// per-request cap); shown only when the retention depth reaches it.
|
|
88
92
|
if (maxRangeDays >= 30) presets.push({ label: '30d', ms: 30 * DAY_MS })
|
|
89
93
|
return presets
|
|
90
94
|
}
|
|
@@ -176,17 +180,21 @@ export function RetainedTimelineScrubber({ source, selection, onSelectionChange,
|
|
|
176
180
|
const availableFromMs = overview.data?.availableFromMs
|
|
177
181
|
|
|
178
182
|
const domain = useMemo<ScrubberRange>(() => {
|
|
179
|
-
// Clamp the domain floor to the
|
|
183
|
+
// Clamp the domain floor to the LOADED window. availableFromMs can point
|
|
180
184
|
// at ancient synthesized-historical event times (resource creation dates on
|
|
181
|
-
// long-lived clusters),
|
|
182
|
-
//
|
|
183
|
-
|
|
185
|
+
// long-lived clusters), and a host may declare maxRangeDays deeper than the
|
|
186
|
+
// ring the client actually loads (MAX_SELECTION_MS) — either would stretch
|
|
187
|
+
// the strip over regions that render empty despite overview density. The
|
|
188
|
+
// ring's window slides forward by the clock-skew slack, so the floor does
|
|
189
|
+
// too — without it the oldest slack-width sliver is brushable but never
|
|
190
|
+
// loadable.
|
|
191
|
+
const floor = now - Math.min(maxRangeDays * DAY_MS, MAX_SELECTION_MS) + RETAINED_CLOCK_SKEW_SLACK_MS
|
|
184
192
|
const fromMs = availableFromMs != null ? Math.max(availableFromMs, floor) : floor
|
|
185
193
|
return { fromMs: Math.min(fromMs, now - HOUR_MS), toMs: now }
|
|
186
194
|
}, [availableFromMs, now, maxRangeDays])
|
|
187
195
|
|
|
188
196
|
const domainWidth = domain.toMs - domain.fromMs
|
|
189
|
-
const maxSelectionMs = Math.min(MAX_SELECTION_MS, domainWidth)
|
|
197
|
+
const maxSelectionMs = Math.min(maxRangeDays * DAY_MS, MAX_SELECTION_MS, domainWidth)
|
|
190
198
|
|
|
191
199
|
// The histogram spans the QUERY RANGE (selection) directly —
|
|
192
200
|
// no ×8 framing, no minimap. The query is the view, so a narrow window is never
|