@skyhook-io/radar-app 1.14.0 → 1.14.2
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 +2 -2
- package/src/App.tsx +6 -4
- package/src/api/client.ts +3 -0
- package/src/api/diagnose.ts +31 -1
- package/src/components/CloudConnectFlow.tsx +173 -13
- package/src/components/CloudFunnelButton.tsx +4 -2
- package/src/components/ConnectionErrorView.tsx +9 -10
- package/src/components/DebugOverlay.tsx +10 -6
- package/src/components/cloudConnectHandoff.test.ts +109 -4
- package/src/components/cloudConnectHandoff.ts +154 -2
- package/src/components/curl/ServiceCurlButton.tsx +19 -26
- package/src/components/diagnose/AgentCase.tsx +18 -5
- package/src/components/diagnose/AnalysisStory.test.tsx +308 -0
- package/src/components/diagnose/AnalysisStory.tsx +564 -0
- package/src/components/diagnose/DiagnoseContext.test.ts +10 -0
- package/src/components/diagnose/DiagnoseContext.tsx +25 -8
- package/src/components/diagnose/DiagnoseSurface.tsx +20 -14
- package/src/components/diagnose/InvestigationEvidencePane.story.test.tsx +771 -0
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +8 -33
- package/src/components/diagnose/InvestigationEvidencePane.tsx +809 -168
- package/src/components/diagnose/InvestigationResourceEvidence.test.tsx +30 -0
- package/src/components/diagnose/InvestigationResourceEvidence.tsx +20 -0
- package/src/components/diagnose/InvestigationView.tsx +493 -529
- package/src/components/diagnose/investigationCase.test.tsx +267 -129
- package/src/components/diagnose/investigationCase.ts +122 -77
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +27 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +28 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +11 -2
- package/src/components/diagnose/investigationEvidence/identity.test.ts +25 -7
- package/src/components/diagnose/investigationEvidence/identity.ts +4 -4
- package/src/components/diagnose/investigationEvidence/observations.ts +24 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +36 -3
- package/src/components/diagnose/investigationEvidence/types.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +2 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +3 -0
- package/src/components/diagnose/investigationState.test.ts +316 -58
- package/src/components/diagnose/investigationState.ts +257 -44
- package/src/components/diagnose/investigationStory.test.ts +161 -0
- package/src/components/diagnose/investigationStory.ts +207 -0
- package/src/components/diagnose/parts.test.tsx +9 -6
- package/src/components/diagnose/parts.tsx +977 -193
- package/src/components/diagnose/storyParts.test.tsx +426 -0
- package/src/components/diagnose/toolCallLabel.test.ts +51 -0
- package/src/components/diagnose/toolCallLabel.ts +135 -0
- package/src/components/diagnose/useDisclosureReveal.ts +10 -11
- package/src/components/helm/HelmCompareRoute.tsx +18 -4
- package/src/components/helm/HelmReleaseDrawer.tsx +11 -26
- package/src/components/helm/InstallWizard.tsx +8 -3
- package/src/components/home/MCPSetupDialog.tsx +15 -9
- package/src/components/portforward/PortForwardManager.tsx +6 -13
- package/src/components/resource/PrometheusChartsGrid.tsx +3 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +3 -3
- package/src/components/resource/WorkloadMetricsSection.tsx +15 -21
- package/src/components/resources/PodFilePreview.tsx +3 -3
- package/src/components/resources/renderers/ServiceRenderer.tsx +2 -1
- package/src/components/settings/SettingsDialog.tsx +4 -2
- package/src/components/ui/CommandPalette.tsx +10 -6
- package/src/components/ui/DiagnosticsOverlay.tsx +10 -6
- package/src/components/ui/Disclosure.tsx +47 -0
- package/src/components/ui/Markdown.tsx +23 -11
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -1
- package/src/components/ui/UpdateNotification.tsx +18 -2
- package/src/index.css +19 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"yaml": "^2.9.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@skyhook-io/k8s-ui": ">=1.14.
|
|
44
|
+
"@skyhook-io/k8s-ui": ">=1.14.7",
|
|
45
45
|
"@tanstack/react-query": ">=5",
|
|
46
46
|
"@xyflow/react": ">=12.0.0",
|
|
47
47
|
"clsx": ">=2",
|
package/src/App.tsx
CHANGED
|
@@ -30,7 +30,7 @@ import { ApplicationsView } from './components/applications/ApplicationsView'
|
|
|
30
30
|
import { HelmReleaseDrawer } from './components/helm/HelmReleaseDrawer'
|
|
31
31
|
import { PortForwardProvider, PortForwardIndicator, PortForwardPanel } from './components/portforward/PortForwardManager'
|
|
32
32
|
import { DockProvider, BottomDock, useDock, useDockReservedHeight, useOpenLocalTerminal } from './components/dock'
|
|
33
|
-
import { DURATION_DOCK } from '@skyhook-io/k8s-ui/utils/animation'
|
|
33
|
+
import { DURATION_DOCK, overlayExitMs } from '@skyhook-io/k8s-ui/utils/animation'
|
|
34
34
|
import { ContextSwitcher } from './components/ContextSwitcher'
|
|
35
35
|
import { NamespaceSwitcher, type NamespaceSwitcherHandle } from './components/NamespaceSwitcher'
|
|
36
36
|
import { CloudFunnelButton } from './components/CloudFunnelButton'
|
|
@@ -680,9 +680,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
680
680
|
// Animation hooks for smooth mount/unmount transitions
|
|
681
681
|
const resourceDrawer = useAnimatedUnmount(!!routeSelectedResource, 300)
|
|
682
682
|
const helmDrawer = useAnimatedUnmount(!!(mainView === 'helm' && selectedHelmRelease), 300)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
683
|
+
// Dialog-kind overlays wait their exit duration (shorter than the entrance);
|
|
684
|
+
// drawers keep the symmetric slide.
|
|
685
|
+
const helpOverlay = useAnimatedUnmount(showHelp, overlayExitMs('dialog'))
|
|
686
|
+
const commandPaletteAnim = useAnimatedUnmount(showCommandPalette, overlayExitMs('dialog'))
|
|
687
|
+
const diagnosticsOverlay = useAnimatedUnmount(showDiagnostics, overlayExitMs('dialog'))
|
|
686
688
|
|
|
687
689
|
// Hold last valid values so drawers can animate out before data disappears
|
|
688
690
|
const lastResourceRef = useRef(routeSelectedResource)
|
package/src/api/client.ts
CHANGED
|
@@ -1913,6 +1913,9 @@ export interface CloudInstallAttempted {
|
|
|
1913
1913
|
|
|
1914
1914
|
export interface CloudInstallBlocked {
|
|
1915
1915
|
reason: 'gitops' | 'preflight' | 'unsupported'
|
|
1916
|
+
// The Kubernetes user the attempt acted as; absent when the API server
|
|
1917
|
+
// would not say.
|
|
1918
|
+
identity?: string
|
|
1916
1919
|
// Preflight only: what would unblock it, and the Helm operation that was
|
|
1917
1920
|
// dry-run — the plan card never renders when preflight blocks, so the
|
|
1918
1921
|
// blocked card states what Radar tried itself.
|
package/src/api/diagnose.ts
CHANGED
|
@@ -72,7 +72,8 @@ export interface DiagnosisEvidenceSubject {
|
|
|
72
72
|
group?: string;
|
|
73
73
|
kind: string;
|
|
74
74
|
namespace?: string;
|
|
75
|
-
|
|
75
|
+
/** Absent when the subject is a listing cited for what it does not contain. */
|
|
76
|
+
name?: string;
|
|
76
77
|
container?: string;
|
|
77
78
|
stream?: "current" | "previous";
|
|
78
79
|
/** Evidence kind (resource, logs, events, changes, metrics, …). */
|
|
@@ -85,6 +86,8 @@ export interface DiagnosisEvidenceItem {
|
|
|
85
86
|
ref?: string;
|
|
86
87
|
role?: DiagnosisEvidenceRole;
|
|
87
88
|
claim?: string;
|
|
89
|
+
/** The agent's own statement of what this result does not cover. */
|
|
90
|
+
gap?: string;
|
|
88
91
|
subject?: DiagnosisEvidenceSubject;
|
|
89
92
|
}
|
|
90
93
|
|
|
@@ -94,10 +97,35 @@ export interface DiagnosisRuledOut {
|
|
|
94
97
|
evidenceIndex: number;
|
|
95
98
|
}
|
|
96
99
|
|
|
100
|
+
export type DiagnosisCertainty = "established" | "likely" | "suspected";
|
|
101
|
+
export type DiagnosisStepKind = "mitigate" | "verify" | "investigate";
|
|
102
|
+
|
|
103
|
+
/** One typed next step; `remediation` mirrors `steps[].text` when steps are present. */
|
|
104
|
+
export interface DiagnosisStep {
|
|
105
|
+
text: string;
|
|
106
|
+
kind: DiagnosisStepKind;
|
|
107
|
+
/** The condition under which this step is the right one, when the agent named one. */
|
|
108
|
+
precondition?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
export interface Diagnosis {
|
|
98
112
|
healthy?: boolean;
|
|
99
113
|
inconclusive?: boolean; // investigated but couldn't determine — distinct from healthy
|
|
100
114
|
rootCause: string;
|
|
115
|
+
/**
|
|
116
|
+
* The plain-language headline; `rootCause` stays the technical one-liner.
|
|
117
|
+
* Present only on turns from a backend that emits the story contract; its
|
|
118
|
+
* absence selects the previous rendering.
|
|
119
|
+
*/
|
|
120
|
+
summary?: string;
|
|
121
|
+
/** The agent's own word for how sure it is; rendered in the agent tone, never as a Radar mark. */
|
|
122
|
+
certainty?: DiagnosisCertainty;
|
|
123
|
+
/** What would change the answer or could not be verified. Rendered above the story fold. */
|
|
124
|
+
unresolved?: string[];
|
|
125
|
+
/** A follow-up answer that replaces the assessment on screen (server-validated as a complete verdict). */
|
|
126
|
+
revisesAssessment?: boolean;
|
|
127
|
+
/** Typed next steps; absent on older runs, where `remediation` is the only list. */
|
|
128
|
+
steps?: DiagnosisStep[];
|
|
101
129
|
rootCauseEvidence?: RootCauseEvidence;
|
|
102
130
|
/** The agent's case over Radar's evidence; absent from hosted backends and older runs. */
|
|
103
131
|
evidence?: DiagnosisEvidenceItem[];
|
|
@@ -107,6 +135,8 @@ export interface Diagnosis {
|
|
|
107
135
|
evidenceMalformed?: boolean;
|
|
108
136
|
ruledOut?: DiagnosisRuledOut[];
|
|
109
137
|
report: string;
|
|
138
|
+
/** The agent's evidence ledger, written before its verdict block; shown in Activity, never in Findings. */
|
|
139
|
+
notes?: string;
|
|
110
140
|
remediation: string[];
|
|
111
141
|
recommendedIndex?: number; // 1-based index into remediation of the step Apply performs
|
|
112
142
|
recommendedReason?: string; // why the recommended step is the safe pick
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type ReactNode, useRef, useState } from 'react'
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useMutation } from '@tanstack/react-query'
|
|
3
|
-
import { AlertTriangle, ArrowUpRight, Check, ExternalLink, GitBranch, Info, Loader2, ShieldAlert, X } from 'lucide-react'
|
|
4
|
-
import type
|
|
3
|
+
import { AlertTriangle, ArrowUpRight, Check, Copy, ExternalLink, GitBranch, Info, Loader2, ShieldAlert, X } from 'lucide-react'
|
|
4
|
+
import { type AdminNoteContext, type BlockedExit, composeAdminNote, composeFailureNote, needsAdminHandoff } from './cloudConnectHandoff'
|
|
5
|
+
import { copyText } from '@skyhook-io/k8s-ui/utils/clipboard'
|
|
5
6
|
import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
6
7
|
import {
|
|
7
8
|
ApiError,
|
|
@@ -21,19 +22,22 @@ export function CloudConnectFlow({
|
|
|
21
22
|
status,
|
|
22
23
|
blocked,
|
|
23
24
|
exit,
|
|
25
|
+
where,
|
|
24
26
|
onStatus,
|
|
25
27
|
onExit,
|
|
26
28
|
}: {
|
|
27
29
|
status: CloudInstallStatus
|
|
28
30
|
blocked: CloudInstallBlocked | null
|
|
29
31
|
exit: BlockedExit
|
|
32
|
+
// Which cluster this is, for the note a blocked person hands to an admin.
|
|
33
|
+
where?: AdminNoteContext
|
|
30
34
|
// Push a mutation's status response into the shared query state.
|
|
31
35
|
onStatus: (st: CloudInstallStatus) => void
|
|
32
36
|
// Leave the flow view (back to the pitch, or close after dismiss).
|
|
33
37
|
onExit: () => void
|
|
34
38
|
}) {
|
|
35
39
|
if (blocked) {
|
|
36
|
-
return <BlockedView blocked={blocked} exit={exit} onExit={onExit} />
|
|
40
|
+
return <BlockedView blocked={blocked} exit={exit} where={where} onExit={onExit} />
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
switch (status.state) {
|
|
@@ -59,7 +63,7 @@ export function CloudConnectFlow({
|
|
|
59
63
|
case 'connected':
|
|
60
64
|
return <ConnectedCard status={status} onStatus={onStatus} onExit={onExit} />
|
|
61
65
|
case 'failed':
|
|
62
|
-
return <FailedCard status={status} onStatus={onStatus} onExit={onExit} />
|
|
66
|
+
return <FailedCard status={status} where={where} onStatus={onStatus} onExit={onExit} />
|
|
63
67
|
default:
|
|
64
68
|
return null
|
|
65
69
|
}
|
|
@@ -68,13 +72,20 @@ export function CloudConnectFlow({
|
|
|
68
72
|
function BlockedView({
|
|
69
73
|
blocked,
|
|
70
74
|
exit,
|
|
75
|
+
where,
|
|
71
76
|
onExit,
|
|
72
77
|
}: {
|
|
73
78
|
blocked: CloudInstallBlocked
|
|
74
79
|
exit: BlockedExit
|
|
80
|
+
where?: AdminNoteContext
|
|
75
81
|
onExit: () => void
|
|
76
82
|
}) {
|
|
77
83
|
const copy = blockedCopy(blocked, exit)
|
|
84
|
+
// The person who can act is usually not the one reading this card. What
|
|
85
|
+
// they need is an ask with the link and the evidence — not the card's
|
|
86
|
+
// second-person explanation — so the note is composed on its own; the
|
|
87
|
+
// CopyForAdmin action previews exactly what will be copied.
|
|
88
|
+
const note = composeAdminNote(blocked, exit, where)
|
|
78
89
|
const icon =
|
|
79
90
|
blocked.reason === 'gitops' ? (
|
|
80
91
|
<GitBranch className="w-4 h-4 shrink-0 mt-0.5 text-emerald-600 dark:text-emerald-400" />
|
|
@@ -97,7 +108,7 @@ function BlockedView({
|
|
|
97
108
|
<BlockedSection label="What to do">{copy.next}</BlockedSection>
|
|
98
109
|
</div>
|
|
99
110
|
</div>
|
|
100
|
-
<div className="mt-4 flex items-center gap-4">
|
|
111
|
+
<div className="relative mt-4 flex items-center gap-4">
|
|
101
112
|
<a
|
|
102
113
|
href={exit.href}
|
|
103
114
|
target="_blank"
|
|
@@ -106,6 +117,7 @@ function BlockedView({
|
|
|
106
117
|
>
|
|
107
118
|
{exit.label}
|
|
108
119
|
</a>
|
|
120
|
+
<CopyForAdmin note={note} />
|
|
109
121
|
<button
|
|
110
122
|
onClick={onExit}
|
|
111
123
|
className="ml-auto text-[12.5px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
|
|
@@ -117,6 +129,132 @@ function BlockedView({
|
|
|
117
129
|
)
|
|
118
130
|
}
|
|
119
131
|
|
|
132
|
+
// The person who can act is usually not the one reading a stop card. This
|
|
133
|
+
// hands them the composed note: a preview panel on hover or focus (card-wide,
|
|
134
|
+
// on the elevated surface, sized to the note — a block of text laid over the
|
|
135
|
+
// card, not a hint), and when the clipboard is refused the panel pins open
|
|
136
|
+
// with the note selected so copying is one keystroke.
|
|
137
|
+
function CopyForAdmin({ note }: { note: string }) {
|
|
138
|
+
const [copied, setCopied] = useState<'idle' | 'done' | 'failed'>('idle')
|
|
139
|
+
const [previewing, setPreviewing] = useState(false)
|
|
140
|
+
const [pinned, setPinned] = useState(false)
|
|
141
|
+
const noteRef = useRef<HTMLPreElement | null>(null)
|
|
142
|
+
const panelRef = useRef<HTMLDivElement | null>(null)
|
|
143
|
+
// The panel is anchored above the action row inside the dialog's scroll
|
|
144
|
+
// area. A hover panel taller than the room visible above the row is only
|
|
145
|
+
// partly on screen, and scrolling to see the rest moves it out from under
|
|
146
|
+
// the pointer and closes it. So the ceiling is measured — the row's
|
|
147
|
+
// distance from the visible top of the scroller — rather than guessed. A
|
|
148
|
+
// normal note fits and shows whole; only a longer one scrolls inside.
|
|
149
|
+
const [maxHeight, setMaxHeight] = useState<number>()
|
|
150
|
+
const shown = previewing || pinned
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (!shown) return
|
|
153
|
+
const wrapper = panelRef.current
|
|
154
|
+
// The positioned ancestor the panel is anchored to — the action row.
|
|
155
|
+
const row = wrapper?.offsetParent as HTMLElement | null | undefined
|
|
156
|
+
const scroller = row?.closest<HTMLElement>('.overflow-y-auto')
|
|
157
|
+
if (!wrapper || !row || !scroller) return
|
|
158
|
+
const visibleRoom = row.getBoundingClientRect().top - scroller.getBoundingClientRect().top
|
|
159
|
+
const gap = wrapper.getBoundingClientRect().bottom - (wrapper.firstElementChild?.getBoundingClientRect().bottom ?? 0)
|
|
160
|
+
setMaxHeight(Math.max(96, Math.floor(visibleRoom - gap - 8)))
|
|
161
|
+
}, [shown])
|
|
162
|
+
// Leaving closes on a short delay that entering the panel cancels, so the
|
|
163
|
+
// pointer can cross the row's own space on its way up into the panel.
|
|
164
|
+
const closeTimer = useRef<number | undefined>(undefined)
|
|
165
|
+
const enter = () => {
|
|
166
|
+
window.clearTimeout(closeTimer.current)
|
|
167
|
+
setPreviewing(true)
|
|
168
|
+
}
|
|
169
|
+
const leave = () => {
|
|
170
|
+
window.clearTimeout(closeTimer.current)
|
|
171
|
+
closeTimer.current = window.setTimeout(() => setPreviewing(false), 200)
|
|
172
|
+
}
|
|
173
|
+
useEffect(() => () => window.clearTimeout(closeTimer.current), [])
|
|
174
|
+
const copy = () => {
|
|
175
|
+
// copyText also serves Radar on a plain-HTTP non-loopback address, where
|
|
176
|
+
// the async Clipboard API is missing and the legacy command still works.
|
|
177
|
+
void copyText(note).then((ok) => {
|
|
178
|
+
setCopied(ok ? 'done' : 'failed')
|
|
179
|
+
if (ok) setTimeout(() => setCopied('idle'), 2000)
|
|
180
|
+
else setPinned(true)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
// The panel may not be mounted when the clipboard refuses (the pointer has
|
|
184
|
+
// left the button), so the selection waits for the pinned render.
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (!pinned) return
|
|
187
|
+
const pre = noteRef.current
|
|
188
|
+
const selection = window.getSelection()
|
|
189
|
+
if (!pre || !selection) return
|
|
190
|
+
selection.removeAllRanges()
|
|
191
|
+
const range = document.createRange()
|
|
192
|
+
range.selectNodeContents(pre)
|
|
193
|
+
selection.addRange(range)
|
|
194
|
+
}, [pinned])
|
|
195
|
+
return (
|
|
196
|
+
// Hover is tracked on this wrapper, which contains the panel, and the
|
|
197
|
+
// gap between button and panel is padding rather than margin, so moving
|
|
198
|
+
// the pointer up into the panel (to scroll a long note) never counts as
|
|
199
|
+
// leaving.
|
|
200
|
+
<span className="contents" onMouseEnter={enter} onMouseLeave={leave}>
|
|
201
|
+
{shown && (
|
|
202
|
+
<div ref={panelRef} className="absolute inset-x-0 bottom-full z-20 pb-3">
|
|
203
|
+
<div
|
|
204
|
+
id="admin-note-preview"
|
|
205
|
+
role="tooltip"
|
|
206
|
+
style={maxHeight ? { maxHeight } : undefined}
|
|
207
|
+
className="flex flex-col rounded-xl border border-theme-border bg-theme-elevated p-4 shadow-theme-lg"
|
|
208
|
+
>
|
|
209
|
+
<div className="mb-1.5 flex items-center justify-between text-[10.5px] font-semibold uppercase tracking-wide text-theme-text-tertiary">
|
|
210
|
+
<span>{pinned ? 'Select and copy' : 'What gets copied'}</span>
|
|
211
|
+
{pinned && (
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onClick={() => {
|
|
215
|
+
setPinned(false)
|
|
216
|
+
setCopied('idle')
|
|
217
|
+
}}
|
|
218
|
+
aria-label="Close"
|
|
219
|
+
className="rounded p-0.5 text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
|
|
220
|
+
>
|
|
221
|
+
<X className="w-3 h-3" />
|
|
222
|
+
</button>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
<pre
|
|
226
|
+
ref={noteRef}
|
|
227
|
+
className="min-h-0 select-text overflow-y-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-relaxed text-theme-text-primary"
|
|
228
|
+
>
|
|
229
|
+
{note}
|
|
230
|
+
</pre>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
onClick={copy}
|
|
237
|
+
onFocus={() => setPreviewing(true)}
|
|
238
|
+
onBlur={() => setPreviewing(false)}
|
|
239
|
+
aria-describedby={shown ? 'admin-note-preview' : undefined}
|
|
240
|
+
className="inline-flex items-center gap-1.5 text-[12.5px] text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
241
|
+
>
|
|
242
|
+
{copied === 'done' ? (
|
|
243
|
+
<>
|
|
244
|
+
<Check className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400" /> Copied
|
|
245
|
+
</>
|
|
246
|
+
) : copied === 'failed' ? (
|
|
247
|
+
'Couldn’t copy — the note is selected above, press ⌘C / Ctrl+C'
|
|
248
|
+
) : (
|
|
249
|
+
<>
|
|
250
|
+
<Copy className="w-3.5 h-3.5" /> Copy for a cluster admin
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</button>
|
|
254
|
+
</span>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
120
258
|
function BlockedSection({ label, children }: { label: string; children: ReactNode }) {
|
|
121
259
|
return (
|
|
122
260
|
<div>
|
|
@@ -186,12 +324,12 @@ function blockedCopy(blocked: CloudInstallBlocked, exit: BlockedExit): { title:
|
|
|
186
324
|
</>
|
|
187
325
|
)
|
|
188
326
|
|
|
189
|
-
// Where the exit is an install command, Radar Cloud
|
|
327
|
+
// Where the exit is an install command, Radar Cloud confirms the cluster name (prefilled from the context)
|
|
190
328
|
// first, then shows the instructions — say so, rather than promising a
|
|
191
329
|
// command on the next screen.
|
|
192
330
|
const installNext = (
|
|
193
331
|
<>
|
|
194
|
-
Have an admin with cluster access get the install command from Radar Cloud: it
|
|
332
|
+
Have an admin with cluster access get the install command from Radar Cloud: it confirms the cluster name, then
|
|
195
333
|
shows the Helm, Argo CD or Flux instructions to review before running.
|
|
196
334
|
</>
|
|
197
335
|
)
|
|
@@ -223,7 +361,7 @@ function blockedCopy(blocked: CloudInstallBlocked, exit: BlockedExit): { title:
|
|
|
223
361
|
why: blocked.message,
|
|
224
362
|
next: exit.install ? (
|
|
225
363
|
<>
|
|
226
|
-
Have an admin with repo access get the values patch from Radar Cloud: it
|
|
364
|
+
Have an admin with repo access get the values patch from Radar Cloud: it confirms the cluster name, then shows
|
|
227
365
|
the patch for your controller and the one command that creates the token Secret.
|
|
228
366
|
</>
|
|
229
367
|
) : (
|
|
@@ -237,8 +375,9 @@ function blockedCopy(blocked: CloudInstallBlocked, exit: BlockedExit): { title:
|
|
|
237
375
|
// Fresh offered on "nothing running" alone: say what was not confirmed.
|
|
238
376
|
const unconfirmedNext = (
|
|
239
377
|
<>
|
|
240
|
-
|
|
241
|
-
|
|
378
|
+
An admin with cluster access should confirm nothing is installed — Radar couldn’t read Helm’s release records —
|
|
379
|
+
then get the install command from Radar Cloud: it confirms the cluster name, then shows the Helm, Argo CD or Flux
|
|
380
|
+
instructions to review before running.
|
|
242
381
|
</>
|
|
243
382
|
)
|
|
244
383
|
const next = exit.install ? (a?.releaseUnread ? unconfirmedNext : installNext) : unknownNext
|
|
@@ -262,7 +401,15 @@ function blockedCopy(blocked: CloudInstallBlocked, exit: BlockedExit): { title:
|
|
|
262
401
|
title: 'The cluster blocked part of this install',
|
|
263
402
|
tried,
|
|
264
403
|
why: 'The cluster refused: something already there conflicts with the install, or a policy rejects it. More permission wouldn’t change that.',
|
|
265
|
-
next: exit.install ?
|
|
404
|
+
next: exit.install ? (
|
|
405
|
+
<>
|
|
406
|
+
Once the refusals above are resolved, have an admin with cluster access get the install command from Radar
|
|
407
|
+
Cloud: it confirms the cluster name, then shows the Helm, Argo CD or Flux instructions to review before
|
|
408
|
+
running.
|
|
409
|
+
</>
|
|
410
|
+
) : (
|
|
411
|
+
next
|
|
412
|
+
),
|
|
266
413
|
}
|
|
267
414
|
}
|
|
268
415
|
}
|
|
@@ -644,16 +791,22 @@ function ConnectedCard({
|
|
|
644
791
|
|
|
645
792
|
function FailedCard({
|
|
646
793
|
status,
|
|
794
|
+
where,
|
|
647
795
|
onStatus,
|
|
648
796
|
onExit,
|
|
649
797
|
}: {
|
|
650
798
|
status: CloudInstallStatus
|
|
799
|
+
where?: AdminNoteContext
|
|
651
800
|
onStatus: (st: CloudInstallStatus) => void
|
|
652
801
|
onExit: () => void
|
|
653
802
|
}) {
|
|
654
803
|
const dismiss = useDismiss(status, onStatus, onExit)
|
|
655
804
|
const failure = status.failure
|
|
656
805
|
if (!failure) return null
|
|
806
|
+
// After the Hub approved, the guidance is written for an operator — inspect
|
|
807
|
+
// commands, keep this Hub cluster, don't rerun. Hand it over the same way a
|
|
808
|
+
// blocked card does, as a request to finish the connection.
|
|
809
|
+
const handoff = needsAdminHandoff(failure)
|
|
657
810
|
return (
|
|
658
811
|
<div className="px-8 pt-6 pb-5">
|
|
659
812
|
<div className="flex items-start gap-2.5 mb-3">
|
|
@@ -666,13 +819,20 @@ function FailedCard({
|
|
|
666
819
|
showSummary={failure.guidance.summary !== failure.message}
|
|
667
820
|
/>
|
|
668
821
|
)}
|
|
669
|
-
<div className="mt-4 flex items-center gap-4">
|
|
822
|
+
<div className="relative mt-4 flex items-center gap-4">
|
|
670
823
|
<button
|
|
671
824
|
onClick={dismiss}
|
|
672
825
|
className="px-4 py-1.5 rounded-[10px] bg-theme-elevated hover:bg-theme-hover border border-theme-border text-[12.5px] font-semibold text-theme-text-primary transition-colors"
|
|
673
826
|
>
|
|
674
827
|
{failure.retrySafe ? 'Start over' : 'Close'}
|
|
675
828
|
</button>
|
|
829
|
+
{handoff && (
|
|
830
|
+
<CopyForAdmin
|
|
831
|
+
// The flow's own record of the cluster: a context switch after
|
|
832
|
+
// the install started must not relabel the note for another.
|
|
833
|
+
note={composeFailureNote(failure, { context: status.plan?.contextName ?? where?.context, cluster: where?.cluster })}
|
|
834
|
+
/>
|
|
835
|
+
)}
|
|
676
836
|
</div>
|
|
677
837
|
</div>
|
|
678
838
|
)
|
|
@@ -234,7 +234,8 @@ export function CloudFunnelButton() {
|
|
|
234
234
|
// relabel the CTA (and the link) for cluster B, and a blocked view left
|
|
235
235
|
// armed would reopen on B with A's refusal, feeding it back into the
|
|
236
236
|
// outcome on Back.
|
|
237
|
-
const
|
|
237
|
+
const clusterInfo = useClusterInfo()
|
|
238
|
+
const contextName = clusterInfo.data?.context
|
|
238
239
|
useEffect(() => {
|
|
239
240
|
setHandoff(null)
|
|
240
241
|
setBlocked(null)
|
|
@@ -306,7 +307,8 @@ export function CloudFunnelButton() {
|
|
|
306
307
|
<CloudConnectFlow
|
|
307
308
|
status={flowForView}
|
|
308
309
|
blocked={blocked}
|
|
309
|
-
exit={exitFor(appUrl, 'driver-blocked-card-browser-link', outcomeOf(flowForView))}
|
|
310
|
+
exit={exitFor(appUrl, 'driver-blocked-card-browser-link', outcomeOf(flowForView), clusterInfo.data?.context)}
|
|
311
|
+
where={{ context: clusterInfo.data?.context, cluster: clusterInfo.data?.cluster }}
|
|
310
312
|
onStatus={applyStatus}
|
|
311
313
|
onExit={() => exitFlow(outcomeOf(flowForView))}
|
|
312
314
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerOff, RefreshCw, Loader2, Copy, Check, TerminalSquare
|
|
1
|
+
import { ServerOff, RefreshCw, Loader2, Copy, Check, TerminalSquare } from 'lucide-react'
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
3
|
import type { ConnectionState } from '../context/ConnectionContext'
|
|
4
4
|
import { ContextSwitcher } from './ContextSwitcher'
|
|
@@ -6,6 +6,7 @@ import { parseContextName } from '../utils/context-name'
|
|
|
6
6
|
import { useOpenLocalTerminal, ClusterName } from '@skyhook-io/k8s-ui'
|
|
7
7
|
import { useAuthMe, useContexts } from '../api/client'
|
|
8
8
|
import { Tooltip } from './ui/Tooltip'
|
|
9
|
+
import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
9
10
|
import { allShellSafe, awsProfileFlag } from '../utils/shell-safe'
|
|
10
11
|
import { apiUrl } from '../api/config'
|
|
11
12
|
import { useCapabilitiesContext } from '../contexts/CapabilitiesContext'
|
|
@@ -459,18 +460,16 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
459
460
|
onClick={() => setShowRawError((open) => !open)}
|
|
460
461
|
className="flex items-center gap-1 text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
461
462
|
>
|
|
462
|
-
<
|
|
463
|
+
<CollapseChevron open={showRawError} className="h-3.5 w-3.5" />
|
|
463
464
|
Raw error
|
|
464
465
|
</button>
|
|
465
|
-
<
|
|
466
|
-
<div className="overflow-
|
|
467
|
-
<
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
</code>
|
|
471
|
-
</div>
|
|
466
|
+
<Collapse open={showRawError} id="connection-raw-error">
|
|
467
|
+
<div className="mt-2 bg-theme-elevated border border-theme-border rounded-md p-3 overflow-auto max-h-32">
|
|
468
|
+
<code className="text-xs text-theme-text-tertiary font-mono whitespace-pre-wrap break-words">
|
|
469
|
+
{connection.error}
|
|
470
|
+
</code>
|
|
472
471
|
</div>
|
|
473
|
-
</
|
|
472
|
+
</Collapse>
|
|
474
473
|
</div>
|
|
475
474
|
)}
|
|
476
475
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
|
-
import { Bug, X
|
|
2
|
+
import { Bug, X } from 'lucide-react'
|
|
3
|
+
import { Collapse, CollapseChevron, useDisclosure } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
3
4
|
import { useRuntimeStats } from '../api/client'
|
|
4
5
|
import { Tooltip } from './ui/Tooltip'
|
|
5
6
|
|
|
@@ -14,6 +15,7 @@ function formatUptime(seconds: number): string {
|
|
|
14
15
|
export function DebugOverlay() {
|
|
15
16
|
const [visible, setVisible] = useState(true)
|
|
16
17
|
const [expanded, setExpanded] = useState(false)
|
|
18
|
+
const disclosure = useDisclosure(expanded)
|
|
17
19
|
const { data } = useRuntimeStats(visible)
|
|
18
20
|
|
|
19
21
|
if (!visible) {
|
|
@@ -40,9 +42,11 @@ export function DebugOverlay() {
|
|
|
40
42
|
<div className="flex-1" />
|
|
41
43
|
<button
|
|
42
44
|
onClick={() => setExpanded(!expanded)}
|
|
45
|
+
{...disclosure.buttonProps}
|
|
46
|
+
aria-label={expanded ? 'Collapse debug stats' : 'Expand debug stats'}
|
|
43
47
|
className="p-0.5 text-theme-text-tertiary hover:text-theme-text-secondary"
|
|
44
48
|
>
|
|
45
|
-
|
|
49
|
+
<CollapseChevron open={expanded} className="w-3 h-3" />
|
|
46
50
|
</button>
|
|
47
51
|
<button
|
|
48
52
|
onClick={() => setVisible(false)}
|
|
@@ -60,8 +64,8 @@ export function DebugOverlay() {
|
|
|
60
64
|
<span className="text-theme-text-tertiary">Heap</span>
|
|
61
65
|
<span className="text-theme-text-primary">{runtime.heapMB.toFixed(1)} MB</span>
|
|
62
66
|
</div>
|
|
63
|
-
{expanded
|
|
64
|
-
|
|
67
|
+
<Collapse open={expanded} id={disclosure.panelId}>
|
|
68
|
+
<div className="space-y-0.5">
|
|
65
69
|
<div className="flex justify-between gap-4">
|
|
66
70
|
<span className="text-theme-text-tertiary">Objects</span>
|
|
67
71
|
<span className="text-theme-text-primary">{runtime.heapObjectsK.toFixed(1)}K</span>
|
|
@@ -84,8 +88,8 @@ export function DebugOverlay() {
|
|
|
84
88
|
<span className="text-theme-text-tertiary">Resources</span>
|
|
85
89
|
<span className="text-theme-text-primary">{data?.resourceCount ?? '-'}</span>
|
|
86
90
|
</div>
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
</div>
|
|
92
|
+
</Collapse>
|
|
89
93
|
</>
|
|
90
94
|
) : (
|
|
91
95
|
<span className="text-theme-text-tertiary">Loading…</span>
|