@skyhook-io/radar-app 1.8.6 → 1.8.7
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/README.md +7 -1
- package/package.json +11 -9
- package/src/App.tsx +221 -181
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.ts +262 -34
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +580 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +327 -26
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +121 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +15 -4
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.tsx +0 -3
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/settings/SettingsDialog.tsx +80 -23
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +51 -10
- package/src/components/timeline/TimelineView.tsx +701 -15
- package/src/components/timeline/TimelineView.urlparams.test.ts +294 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +319 -35
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +157 -0
- package/src/index.ts +12 -0
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useState } from 'react'
|
|
1
|
+
import { ServerOff, RefreshCw, Loader2, Copy, Check, TerminalSquare, ChevronRight } from 'lucide-react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
3
|
import type { ConnectionState } from '../context/ConnectionContext'
|
|
4
4
|
import { ContextSwitcher } from './ContextSwitcher'
|
|
5
5
|
import { parseContextName } from '../utils/context-name'
|
|
@@ -29,8 +29,8 @@ function getAuthHints(context: string): AuthHints {
|
|
|
29
29
|
case 'GKE': {
|
|
30
30
|
const result: AuthHints = {
|
|
31
31
|
title: 'GKE Authentication Failed',
|
|
32
|
-
hints: ['
|
|
33
|
-
authCommand: { label: '
|
|
32
|
+
hints: ['Radar could not get Google Cloud credentials for this context.'],
|
|
33
|
+
authCommand: { label: 'Refresh Google Cloud credentials:', command: 'gcloud auth login' },
|
|
34
34
|
}
|
|
35
35
|
if (parsed.region && parsed.account) {
|
|
36
36
|
const isZone = /^[a-z]+-[a-z]+\d+-[a-z]$/.test(parsed.region)
|
|
@@ -45,8 +45,11 @@ function getAuthHints(context: string): AuthHints {
|
|
|
45
45
|
case 'EKS': {
|
|
46
46
|
const result: AuthHints = {
|
|
47
47
|
title: 'EKS Authentication Failed',
|
|
48
|
-
hints: [
|
|
49
|
-
|
|
48
|
+
hints: [
|
|
49
|
+
'Radar could not get AWS credentials for this context.',
|
|
50
|
+
'For AWS SSO contexts, the SSO session may need login.',
|
|
51
|
+
],
|
|
52
|
+
authCommand: { label: 'If this context uses AWS SSO, refresh credentials:', command: 'aws sso login' },
|
|
50
53
|
}
|
|
51
54
|
if (parsed.region) {
|
|
52
55
|
result.fallbackCommand = {
|
|
@@ -59,21 +62,71 @@ function getAuthHints(context: string): AuthHints {
|
|
|
59
62
|
case 'AKS':
|
|
60
63
|
return {
|
|
61
64
|
title: 'AKS Authentication Failed',
|
|
62
|
-
hints: ['
|
|
63
|
-
authCommand: { label: '
|
|
65
|
+
hints: ['Radar could not get Azure credentials for this context.'],
|
|
66
|
+
authCommand: { label: 'Refresh Azure credentials:', command: 'az login' },
|
|
64
67
|
fallbackCommand: { label: 'If that doesn\'t work, refresh cluster credentials:', command: 'az aks get-credentials --name <cluster> --resource-group <rg>' },
|
|
65
68
|
}
|
|
66
69
|
default:
|
|
67
70
|
return {
|
|
68
71
|
title: 'Authentication Failed',
|
|
69
72
|
hints: [
|
|
70
|
-
'
|
|
73
|
+
'Radar could not get Kubernetes credentials for this context',
|
|
71
74
|
'Re-authenticate with your cloud provider and try again',
|
|
72
75
|
],
|
|
73
76
|
}
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
function getTimeoutHints(context: string): AuthHints | null {
|
|
81
|
+
const parsed = parseContextName(context)
|
|
82
|
+
const baseHints = [
|
|
83
|
+
'The Kubernetes API did not respond before the deadline.',
|
|
84
|
+
'Check VPN, firewall rules, and whether the cluster endpoint is reachable.',
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
switch (parsed.provider) {
|
|
88
|
+
case 'GKE': {
|
|
89
|
+
const result: AuthHints = {
|
|
90
|
+
title: 'Connection Timed Out',
|
|
91
|
+
hints: [...baseHints, 'If the endpoint is reachable, Google Cloud credentials may need refresh.'],
|
|
92
|
+
authCommand: { label: 'If network access looks healthy, refresh Google Cloud credentials:', command: 'gcloud auth login' },
|
|
93
|
+
}
|
|
94
|
+
if (parsed.region && parsed.account) {
|
|
95
|
+
const isZone = /^[a-z]+-[a-z]+\d+-[a-z]$/.test(parsed.region)
|
|
96
|
+
const flag = isZone ? '--zone' : '--region'
|
|
97
|
+
result.fallbackCommand = {
|
|
98
|
+
label: 'If that does not work, refresh cluster credentials:',
|
|
99
|
+
command: `gcloud container clusters get-credentials ${parsed.clusterName} ${flag} ${parsed.region} --project ${parsed.account}`,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result
|
|
103
|
+
}
|
|
104
|
+
case 'EKS': {
|
|
105
|
+
const result: AuthHints = {
|
|
106
|
+
title: 'Connection Timed Out',
|
|
107
|
+
hints: [...baseHints, 'If the endpoint is reachable, AWS credentials or SSO may need refresh.'],
|
|
108
|
+
authCommand: { label: 'If this context uses AWS SSO and network access looks healthy, refresh credentials:', command: 'aws sso login' },
|
|
109
|
+
}
|
|
110
|
+
if (parsed.region) {
|
|
111
|
+
result.fallbackCommand = {
|
|
112
|
+
label: 'If that does not work, refresh cluster credentials:',
|
|
113
|
+
command: `aws eks update-kubeconfig --name ${parsed.clusterName} --region ${parsed.region}`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
case 'AKS':
|
|
119
|
+
return {
|
|
120
|
+
title: 'Connection Timed Out',
|
|
121
|
+
hints: [...baseHints, 'If the endpoint is reachable, Azure credentials may need refresh.'],
|
|
122
|
+
authCommand: { label: 'If network access looks healthy, refresh Azure credentials:', command: 'az login' },
|
|
123
|
+
fallbackCommand: { label: 'If that does not work, refresh cluster credentials:', command: 'az aks get-credentials --name <cluster> --resource-group <rg>' },
|
|
124
|
+
}
|
|
125
|
+
default:
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
77
130
|
const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
78
131
|
config: {
|
|
79
132
|
title: 'No Kubeconfig Found',
|
|
@@ -101,6 +154,14 @@ const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
|
101
154
|
'Confirm the cluster is running',
|
|
102
155
|
],
|
|
103
156
|
},
|
|
157
|
+
tls: {
|
|
158
|
+
title: 'Certificate Error',
|
|
159
|
+
hints: [
|
|
160
|
+
'Radar reached the Kubernetes API, but could not verify its TLS certificate',
|
|
161
|
+
'Check the kubeconfig cluster server hostname and certificate-authority settings',
|
|
162
|
+
'If this cluster intentionally uses a private CA, refresh the kubeconfig for this context',
|
|
163
|
+
],
|
|
164
|
+
},
|
|
104
165
|
timeout: {
|
|
105
166
|
title: 'Connection Timed Out',
|
|
106
167
|
hints: [
|
|
@@ -122,6 +183,7 @@ const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
|
122
183
|
|
|
123
184
|
function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunInTerminal?: (command: string) => void }) {
|
|
124
185
|
const [copied, setCopied] = useState(false)
|
|
186
|
+
const commandParts = command.split(/(\s+)/)
|
|
125
187
|
|
|
126
188
|
const handleCopy = () => {
|
|
127
189
|
navigator.clipboard.writeText(command).then(() => {
|
|
@@ -134,30 +196,36 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
|
|
|
134
196
|
|
|
135
197
|
return (
|
|
136
198
|
<div className="mt-2 flex items-center gap-2 bg-theme-elevated border border-theme-border rounded-md px-3 py-2 group">
|
|
137
|
-
<code className="text-xs font-mono text-theme-text-primary flex-1 select-all break-
|
|
138
|
-
{
|
|
199
|
+
<code className="text-xs font-mono text-theme-text-primary flex-1 min-w-0 select-all whitespace-pre-wrap break-normal">
|
|
200
|
+
{commandParts.map((part, index) => (
|
|
201
|
+
/\s+/.test(part) ? part : <span key={index} className="inline-block whitespace-nowrap">{part}</span>
|
|
202
|
+
))}
|
|
139
203
|
</code>
|
|
140
204
|
{onRunInTerminal && (
|
|
141
205
|
<Tooltip content="Run in terminal" wrapperClassName="shrink-0">
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
onClick={() => onRunInTerminal(command)}
|
|
209
|
+
aria-label="Run command in terminal"
|
|
210
|
+
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
211
|
+
>
|
|
212
|
+
<TerminalSquare className="w-3.5 h-3.5" />
|
|
213
|
+
</button>
|
|
214
|
+
</Tooltip>
|
|
215
|
+
)}
|
|
216
|
+
<Tooltip content="Copy to clipboard" wrapperClassName="shrink-0">
|
|
142
217
|
<button
|
|
143
|
-
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={handleCopy}
|
|
220
|
+
aria-label="Copy command to clipboard"
|
|
144
221
|
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
145
222
|
>
|
|
146
|
-
|
|
223
|
+
{copied ? (
|
|
224
|
+
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
225
|
+
) : (
|
|
226
|
+
<Copy className="w-3.5 h-3.5" />
|
|
227
|
+
)}
|
|
147
228
|
</button>
|
|
148
|
-
</Tooltip>
|
|
149
|
-
)}
|
|
150
|
-
<Tooltip content="Copy to clipboard" wrapperClassName="shrink-0">
|
|
151
|
-
<button
|
|
152
|
-
onClick={handleCopy}
|
|
153
|
-
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
154
|
-
>
|
|
155
|
-
{copied ? (
|
|
156
|
-
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
157
|
-
) : (
|
|
158
|
-
<Copy className="w-3.5 h-3.5" />
|
|
159
|
-
)}
|
|
160
|
-
</button>
|
|
161
229
|
</Tooltip>
|
|
162
230
|
</div>
|
|
163
231
|
)
|
|
@@ -166,10 +234,21 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
|
|
|
166
234
|
export function ConnectionErrorView({ connection, onRetry, isRetrying }: ConnectionErrorViewProps) {
|
|
167
235
|
// For auth errors, generate context-aware hints with a specific re-auth command
|
|
168
236
|
const isAuth = connection.errorType === 'auth'
|
|
169
|
-
const
|
|
170
|
-
const
|
|
237
|
+
const isTimeout = connection.errorType === 'timeout'
|
|
238
|
+
const commandInfo = isAuth
|
|
239
|
+
? getAuthHints(connection.context || '')
|
|
240
|
+
: isTimeout
|
|
241
|
+
? getTimeoutHints(connection.context || '')
|
|
242
|
+
: null
|
|
243
|
+
const errorInfo = commandInfo || errorHints[connection.errorType || 'unknown'] || errorHints.unknown
|
|
171
244
|
const openLocalTerminal = useOpenLocalTerminal()
|
|
172
245
|
const { data: authMe } = useAuthMe()
|
|
246
|
+
const rawErrorDefaultOpen = !connection.errorType || connection.errorType === 'unknown'
|
|
247
|
+
const [showRawError, setShowRawError] = useState(rawErrorDefaultOpen)
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
setShowRawError(rawErrorDefaultOpen)
|
|
251
|
+
}, [connection.error, rawErrorDefaultOpen])
|
|
173
252
|
|
|
174
253
|
// Auto-retry after successful auth. The terminal shell runs on the server
|
|
175
254
|
// host, so the auth command itself fixes the server's credentials in every
|
|
@@ -179,10 +258,10 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
179
258
|
const retryCmd = `curl -s -X POST http://${window.location.host}/api/connection/retry > /dev/null`
|
|
180
259
|
|
|
181
260
|
const handleAuthInTerminal = () => {
|
|
182
|
-
if (!
|
|
261
|
+
if (!commandInfo?.authCommand) return
|
|
183
262
|
const cmd = authMe?.authEnabled === false
|
|
184
|
-
? `${
|
|
185
|
-
:
|
|
263
|
+
? `${commandInfo.authCommand.command} && ${retryCmd}`
|
|
264
|
+
: commandInfo.authCommand.command
|
|
186
265
|
openLocalTerminal({
|
|
187
266
|
initialCommand: cmd,
|
|
188
267
|
title: 'Auth',
|
|
@@ -194,32 +273,34 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
194
273
|
}
|
|
195
274
|
|
|
196
275
|
return (
|
|
197
|
-
<div className="flex-1 flex items-start justify-center pt-
|
|
198
|
-
<div className="max-w-
|
|
276
|
+
<div className="flex-1 flex items-start justify-center pt-12 px-8">
|
|
277
|
+
<div className="max-w-xl w-full">
|
|
199
278
|
<div className="flex flex-col items-center text-center">
|
|
200
|
-
<div className="w-
|
|
201
|
-
<
|
|
279
|
+
<div className="w-14 h-14 rounded-full bg-red-500/10 flex items-center justify-center mb-5">
|
|
280
|
+
<ServerOff className="w-8 h-8 text-red-400" />
|
|
202
281
|
</div>
|
|
203
282
|
|
|
204
283
|
<h2 className="text-xl font-semibold text-theme-text-primary mb-2">
|
|
205
284
|
{connection.errorType === 'config' ? 'No Cluster Configuration' : 'Cannot Connect to Cluster'}
|
|
206
285
|
</h2>
|
|
207
286
|
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{connection.clusterName && (
|
|
217
|
-
<p className="text-sm text-theme-text-secondary mb-4">
|
|
218
|
-
Cluster: <span className="inline-code">{connection.clusterName}</span>
|
|
287
|
+
<div className="mb-6 space-y-1">
|
|
288
|
+
<p className="text-sm text-theme-text-secondary inline-flex items-center gap-1.5">
|
|
289
|
+
Context: {connection.context ? (
|
|
290
|
+
<ClusterName name={connection.context} />
|
|
291
|
+
) : (
|
|
292
|
+
<span className="inline-code">(none)</span>
|
|
293
|
+
)}
|
|
219
294
|
</p>
|
|
220
|
-
)}
|
|
221
295
|
|
|
222
|
-
|
|
296
|
+
{connection.clusterName && (
|
|
297
|
+
<p className="text-sm text-theme-text-secondary">
|
|
298
|
+
Cluster: <span className="inline-code">{connection.clusterName}</span>
|
|
299
|
+
</p>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div className="w-full bg-theme-surface border border-theme-border rounded-lg p-4 mb-5 text-left">
|
|
223
304
|
<h3 className="text-sm font-medium text-theme-text-primary mb-2">
|
|
224
305
|
{errorInfo.title}
|
|
225
306
|
</h3>
|
|
@@ -231,35 +312,52 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
231
312
|
</li>
|
|
232
313
|
))}
|
|
233
314
|
</ul>
|
|
234
|
-
{
|
|
315
|
+
{commandInfo?.authCommand && (
|
|
235
316
|
<div className="mt-3">
|
|
236
|
-
<p className="text-xs text-theme-text-tertiary">{
|
|
237
|
-
<CopyableCommand command={
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
317
|
+
<p className="text-xs text-theme-text-tertiary">{commandInfo.authCommand.label}</p>
|
|
318
|
+
<CopyableCommand command={commandInfo.authCommand.command} onRunInTerminal={handleRunInTerminal} />
|
|
319
|
+
{isAuth && (
|
|
320
|
+
<button
|
|
321
|
+
onClick={handleAuthInTerminal}
|
|
322
|
+
className="mt-3 w-full inline-flex items-center justify-center gap-2 px-3 py-2 text-xs font-medium btn-brand rounded-md"
|
|
323
|
+
>
|
|
324
|
+
<TerminalSquare className="w-3.5 h-3.5" />
|
|
325
|
+
Authenticate in terminal
|
|
326
|
+
</button>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
{commandInfo?.fallbackCommand && (
|
|
331
|
+
<div className="mt-4 pt-3 border-t border-theme-border/50">
|
|
332
|
+
<p className="text-xs text-theme-text-tertiary">{commandInfo.fallbackCommand.label}</p>
|
|
333
|
+
<CopyableCommand command={commandInfo.fallbackCommand.command} onRunInTerminal={handleRunInTerminal} />
|
|
245
334
|
</div>
|
|
246
335
|
)}
|
|
247
|
-
{
|
|
336
|
+
{connection.error && (
|
|
248
337
|
<div className="mt-4 pt-3 border-t border-theme-border/50">
|
|
249
|
-
<
|
|
250
|
-
|
|
338
|
+
<button
|
|
339
|
+
type="button"
|
|
340
|
+
aria-expanded={showRawError}
|
|
341
|
+
aria-controls="connection-raw-error"
|
|
342
|
+
onClick={() => setShowRawError((open) => !open)}
|
|
343
|
+
className="flex items-center gap-1 text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
344
|
+
>
|
|
345
|
+
<ChevronRight className={`h-3.5 w-3.5 transition-transform duration-200 ${showRawError ? 'rotate-90' : ''}`} />
|
|
346
|
+
Raw error
|
|
347
|
+
</button>
|
|
348
|
+
<div className={`issue-details-motion ${showRawError ? 'issue-details-motion-open' : ''}`}>
|
|
349
|
+
<div className="overflow-hidden">
|
|
350
|
+
<div id="connection-raw-error" className="mt-2 bg-theme-elevated border border-theme-border rounded-md p-3 overflow-auto max-h-32">
|
|
351
|
+
<code className="text-xs text-theme-text-tertiary font-mono whitespace-pre-wrap break-words">
|
|
352
|
+
{connection.error}
|
|
353
|
+
</code>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
251
357
|
</div>
|
|
252
358
|
)}
|
|
253
359
|
</div>
|
|
254
360
|
|
|
255
|
-
{connection.error && (
|
|
256
|
-
<div className="w-full bg-theme-elevated border border-theme-border rounded-lg p-3 mb-6 overflow-auto max-h-32">
|
|
257
|
-
<code className="text-xs text-red-400 font-mono whitespace-pre-wrap break-words">
|
|
258
|
-
{connection.error}
|
|
259
|
-
</code>
|
|
260
|
-
</div>
|
|
261
|
-
)}
|
|
262
|
-
|
|
263
361
|
<div className="flex items-center gap-5">
|
|
264
362
|
<button
|
|
265
363
|
onClick={onRetry}
|
|
@@ -279,8 +377,14 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
279
377
|
)}
|
|
280
378
|
</button>
|
|
281
379
|
|
|
282
|
-
{connection.errorType !== 'config' && <ContextSwitcher />}
|
|
380
|
+
{connection.errorType !== 'config' && <ContextSwitcher triggerName="Switch context" />}
|
|
283
381
|
</div>
|
|
382
|
+
|
|
383
|
+
{isAuth && (
|
|
384
|
+
<p className="mt-4 text-xs text-theme-text-tertiary">
|
|
385
|
+
Radar will keep retrying after credentials are refreshed.
|
|
386
|
+
</p>
|
|
387
|
+
)}
|
|
284
388
|
</div>
|
|
285
389
|
</div>
|
|
286
390
|
</div>
|
|
@@ -16,6 +16,7 @@ interface ContextSwitcherProps {
|
|
|
16
16
|
className?: string
|
|
17
17
|
variant?: 'chip' | 'segment'
|
|
18
18
|
label?: string
|
|
19
|
+
triggerName?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface ContextSwitcherHandle {
|
|
@@ -26,7 +27,12 @@ interface ParsedContext extends ParsedContextName {
|
|
|
26
27
|
context: ContextInfo
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
function shouldSuppressSwitchErrorToast(error: unknown): boolean {
|
|
31
|
+
const message = error instanceof Error ? error.message : ''
|
|
32
|
+
return message.includes('cluster connection failed:')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcherProps>(({ className = '', variant, label, triggerName }, ref) => {
|
|
30
36
|
const [showConfirm, setShowConfirm] = useState(false)
|
|
31
37
|
const [pendingSwitch, setPendingSwitch] = useState<ParsedContext | null>(null)
|
|
32
38
|
const [sessionCounts, setSessionCounts] = useState<SessionCounts | null>(null)
|
|
@@ -117,8 +123,10 @@ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcher
|
|
|
117
123
|
endSwitch()
|
|
118
124
|
// Backend may not transition to StateDisconnected on client-side errors
|
|
119
125
|
// (network, timeout) — without this toast the user gets no feedback.
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
if (!shouldSuppressSwitchErrorToast(error)) {
|
|
127
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
128
|
+
showError('Failed to switch context', message)
|
|
129
|
+
}
|
|
122
130
|
}
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -185,8 +193,8 @@ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcher
|
|
|
185
193
|
// Fall back to clusterInfo.context for the very-early window before
|
|
186
194
|
// /api/contexts has resolved.
|
|
187
195
|
const currentParsed = currentId ? parsedById.get(currentId) : undefined
|
|
188
|
-
const currentRaw = currentParsed?.raw || clusterInfo?.context || currentCtx?.name || 'Unknown'
|
|
189
|
-
const currentSourceLabel = hasMultipleSources ? currentCtx?.source || undefined : undefined
|
|
196
|
+
const currentRaw = triggerName || currentParsed?.raw || clusterInfo?.context || currentCtx?.name || 'Unknown'
|
|
197
|
+
const currentSourceLabel = triggerName ? undefined : hasMultipleSources ? currentCtx?.source || undefined : undefined
|
|
190
198
|
|
|
191
199
|
return (
|
|
192
200
|
<>
|