@skyhook-io/radar-app 0.2.2 → 0.3.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/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- 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/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -1,9 +1,11 @@
|
|
|
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'
|
|
6
6
|
import { useOpenLocalTerminal, ClusterName } from '@skyhook-io/k8s-ui'
|
|
7
|
+
import { useAuthMe } from '../api/client'
|
|
8
|
+
import { Tooltip } from './ui/Tooltip'
|
|
7
9
|
|
|
8
10
|
interface ConnectionErrorViewProps {
|
|
9
11
|
connection: ConnectionState
|
|
@@ -27,8 +29,8 @@ function getAuthHints(context: string): AuthHints {
|
|
|
27
29
|
case 'GKE': {
|
|
28
30
|
const result: AuthHints = {
|
|
29
31
|
title: 'GKE Authentication Failed',
|
|
30
|
-
hints: ['
|
|
31
|
-
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' },
|
|
32
34
|
}
|
|
33
35
|
if (parsed.region && parsed.account) {
|
|
34
36
|
const isZone = /^[a-z]+-[a-z]+\d+-[a-z]$/.test(parsed.region)
|
|
@@ -43,8 +45,11 @@ function getAuthHints(context: string): AuthHints {
|
|
|
43
45
|
case 'EKS': {
|
|
44
46
|
const result: AuthHints = {
|
|
45
47
|
title: 'EKS Authentication Failed',
|
|
46
|
-
hints: [
|
|
47
|
-
|
|
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' },
|
|
48
53
|
}
|
|
49
54
|
if (parsed.region) {
|
|
50
55
|
result.fallbackCommand = {
|
|
@@ -57,21 +62,71 @@ function getAuthHints(context: string): AuthHints {
|
|
|
57
62
|
case 'AKS':
|
|
58
63
|
return {
|
|
59
64
|
title: 'AKS Authentication Failed',
|
|
60
|
-
hints: ['
|
|
61
|
-
authCommand: { label: '
|
|
65
|
+
hints: ['Radar could not get Azure credentials for this context.'],
|
|
66
|
+
authCommand: { label: 'Refresh Azure credentials:', command: 'az login' },
|
|
62
67
|
fallbackCommand: { label: 'If that doesn\'t work, refresh cluster credentials:', command: 'az aks get-credentials --name <cluster> --resource-group <rg>' },
|
|
63
68
|
}
|
|
64
69
|
default:
|
|
65
70
|
return {
|
|
66
71
|
title: 'Authentication Failed',
|
|
67
72
|
hints: [
|
|
68
|
-
'
|
|
73
|
+
'Radar could not get Kubernetes credentials for this context',
|
|
69
74
|
'Re-authenticate with your cloud provider and try again',
|
|
70
75
|
],
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
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
|
+
|
|
75
130
|
const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
76
131
|
config: {
|
|
77
132
|
title: 'No Kubeconfig Found',
|
|
@@ -99,6 +154,14 @@ const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
|
99
154
|
'Confirm the cluster is running',
|
|
100
155
|
],
|
|
101
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
|
+
},
|
|
102
165
|
timeout: {
|
|
103
166
|
title: 'Connection Timed Out',
|
|
104
167
|
hints: [
|
|
@@ -120,6 +183,7 @@ const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
|
120
183
|
|
|
121
184
|
function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunInTerminal?: (command: string) => void }) {
|
|
122
185
|
const [copied, setCopied] = useState(false)
|
|
186
|
+
const commandParts = command.split(/(\s+)/)
|
|
123
187
|
|
|
124
188
|
const handleCopy = () => {
|
|
125
189
|
navigator.clipboard.writeText(command).then(() => {
|
|
@@ -132,29 +196,37 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
|
|
|
132
196
|
|
|
133
197
|
return (
|
|
134
198
|
<div className="mt-2 flex items-center gap-2 bg-theme-elevated border border-theme-border rounded-md px-3 py-2 group">
|
|
135
|
-
<code className="text-xs font-mono text-theme-text-primary flex-1 select-all break-
|
|
136
|
-
{
|
|
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
|
+
))}
|
|
137
203
|
</code>
|
|
138
204
|
{onRunInTerminal && (
|
|
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">
|
|
139
217
|
<button
|
|
140
|
-
|
|
218
|
+
type="button"
|
|
219
|
+
onClick={handleCopy}
|
|
220
|
+
aria-label="Copy command to clipboard"
|
|
141
221
|
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
142
|
-
title="Run in terminal"
|
|
143
222
|
>
|
|
144
|
-
|
|
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
|
+
)}
|
|
145
228
|
</button>
|
|
146
|
-
|
|
147
|
-
<button
|
|
148
|
-
onClick={handleCopy}
|
|
149
|
-
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
150
|
-
title="Copy to clipboard"
|
|
151
|
-
>
|
|
152
|
-
{copied ? (
|
|
153
|
-
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
154
|
-
) : (
|
|
155
|
-
<Copy className="w-3.5 h-3.5" />
|
|
156
|
-
)}
|
|
157
|
-
</button>
|
|
229
|
+
</Tooltip>
|
|
158
230
|
</div>
|
|
159
231
|
)
|
|
160
232
|
}
|
|
@@ -162,17 +234,36 @@ function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunI
|
|
|
162
234
|
export function ConnectionErrorView({ connection, onRetry, isRetrying }: ConnectionErrorViewProps) {
|
|
163
235
|
// For auth errors, generate context-aware hints with a specific re-auth command
|
|
164
236
|
const isAuth = connection.errorType === 'auth'
|
|
165
|
-
const
|
|
166
|
-
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
|
|
167
244
|
const openLocalTerminal = useOpenLocalTerminal()
|
|
245
|
+
const { data: authMe } = useAuthMe()
|
|
246
|
+
const rawErrorDefaultOpen = !connection.errorType || connection.errorType === 'unknown'
|
|
247
|
+
const [showRawError, setShowRawError] = useState(rawErrorDefaultOpen)
|
|
168
248
|
|
|
169
|
-
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
setShowRawError(rawErrorDefaultOpen)
|
|
251
|
+
}, [connection.error, rawErrorDefaultOpen])
|
|
252
|
+
|
|
253
|
+
// Auto-retry after successful auth. The terminal shell runs on the server
|
|
254
|
+
// host, so the auth command itself fixes the server's credentials in every
|
|
255
|
+
// mode — but the chained retry curl carries no session cookie, so it 401s
|
|
256
|
+
// once /api/connection is auth-gated. Only chain it when auth is *known*
|
|
257
|
+
// disabled (authMe still loading → don't chain a doomed call).
|
|
170
258
|
const retryCmd = `curl -s -X POST http://${window.location.host}/api/connection/retry > /dev/null`
|
|
171
259
|
|
|
172
260
|
const handleAuthInTerminal = () => {
|
|
173
|
-
if (!
|
|
261
|
+
if (!commandInfo?.authCommand) return
|
|
262
|
+
const cmd = authMe?.authEnabled === false
|
|
263
|
+
? `${commandInfo.authCommand.command} && ${retryCmd}`
|
|
264
|
+
: commandInfo.authCommand.command
|
|
174
265
|
openLocalTerminal({
|
|
175
|
-
initialCommand:
|
|
266
|
+
initialCommand: cmd,
|
|
176
267
|
title: 'Auth',
|
|
177
268
|
})
|
|
178
269
|
}
|
|
@@ -182,32 +273,34 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
182
273
|
}
|
|
183
274
|
|
|
184
275
|
return (
|
|
185
|
-
<div className="flex-1 flex items-start justify-center pt-
|
|
186
|
-
<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">
|
|
187
278
|
<div className="flex flex-col items-center text-center">
|
|
188
|
-
<div className="w-
|
|
189
|
-
<
|
|
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" />
|
|
190
281
|
</div>
|
|
191
282
|
|
|
192
283
|
<h2 className="text-xl font-semibold text-theme-text-primary mb-2">
|
|
193
284
|
{connection.errorType === 'config' ? 'No Cluster Configuration' : 'Cannot Connect to Cluster'}
|
|
194
285
|
</h2>
|
|
195
286
|
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{connection.clusterName && (
|
|
205
|
-
<p className="text-sm text-theme-text-secondary mb-4">
|
|
206
|
-
Cluster: <span className="font-mono text-theme-text-primary">{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
|
+
)}
|
|
207
294
|
</p>
|
|
208
|
-
)}
|
|
209
295
|
|
|
210
|
-
|
|
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">
|
|
211
304
|
<h3 className="text-sm font-medium text-theme-text-primary mb-2">
|
|
212
305
|
{errorInfo.title}
|
|
213
306
|
</h3>
|
|
@@ -219,35 +312,52 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
219
312
|
</li>
|
|
220
313
|
))}
|
|
221
314
|
</ul>
|
|
222
|
-
{
|
|
315
|
+
{commandInfo?.authCommand && (
|
|
223
316
|
<div className="mt-3">
|
|
224
|
-
<p className="text-xs text-theme-text-tertiary">{
|
|
225
|
-
<CopyableCommand command={
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
)}
|
|
233
328
|
</div>
|
|
234
329
|
)}
|
|
235
|
-
{
|
|
330
|
+
{commandInfo?.fallbackCommand && (
|
|
236
331
|
<div className="mt-4 pt-3 border-t border-theme-border/50">
|
|
237
|
-
<p className="text-xs text-theme-text-tertiary">{
|
|
238
|
-
<CopyableCommand command={
|
|
332
|
+
<p className="text-xs text-theme-text-tertiary">{commandInfo.fallbackCommand.label}</p>
|
|
333
|
+
<CopyableCommand command={commandInfo.fallbackCommand.command} onRunInTerminal={handleRunInTerminal} />
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{connection.error && (
|
|
337
|
+
<div className="mt-4 pt-3 border-t border-theme-border/50">
|
|
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>
|
|
239
357
|
</div>
|
|
240
358
|
)}
|
|
241
359
|
</div>
|
|
242
360
|
|
|
243
|
-
{connection.error && (
|
|
244
|
-
<div className="w-full bg-theme-elevated border border-theme-border rounded-lg p-3 mb-6 overflow-auto max-h-32">
|
|
245
|
-
<code className="text-xs text-red-400 font-mono whitespace-pre-wrap break-all">
|
|
246
|
-
{connection.error}
|
|
247
|
-
</code>
|
|
248
|
-
</div>
|
|
249
|
-
)}
|
|
250
|
-
|
|
251
361
|
<div className="flex items-center gap-5">
|
|
252
362
|
<button
|
|
253
363
|
onClick={onRetry}
|
|
@@ -267,8 +377,14 @@ export function ConnectionErrorView({ connection, onRetry, isRetrying }: Connect
|
|
|
267
377
|
)}
|
|
268
378
|
</button>
|
|
269
379
|
|
|
270
|
-
{connection.errorType !== 'config' && <ContextSwitcher />}
|
|
380
|
+
{connection.errorType !== 'config' && <ContextSwitcher triggerName="Switch context" />}
|
|
271
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
|
+
)}
|
|
272
388
|
</div>
|
|
273
389
|
</div>
|
|
274
390
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react'
|
|
1
|
+
import { useMemo, useState, forwardRef } from 'react'
|
|
2
2
|
import { AlertTriangle } from 'lucide-react'
|
|
3
3
|
import {
|
|
4
4
|
ClusterSwitcher,
|
|
@@ -14,13 +14,25 @@ import { parseContextName, type ParsedContextName } from '../utils/context-name'
|
|
|
14
14
|
|
|
15
15
|
interface ContextSwitcherProps {
|
|
16
16
|
className?: string
|
|
17
|
+
variant?: 'chip' | 'segment'
|
|
18
|
+
label?: string
|
|
19
|
+
triggerName?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContextSwitcherHandle {
|
|
23
|
+
open: () => void
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
interface ParsedContext extends ParsedContextName {
|
|
20
27
|
context: ContextInfo
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
|
|
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) => {
|
|
24
36
|
const [showConfirm, setShowConfirm] = useState(false)
|
|
25
37
|
const [pendingSwitch, setPendingSwitch] = useState<ParsedContext | null>(null)
|
|
26
38
|
const [sessionCounts, setSessionCounts] = useState<SessionCounts | null>(null)
|
|
@@ -33,13 +45,37 @@ export function ContextSwitcher({ className = '' }: ContextSwitcherProps) {
|
|
|
33
45
|
const { tabs } = useDock()
|
|
34
46
|
|
|
35
47
|
// Parse contexts and decide whether to render group headers (multi-account only).
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
// hasMultipleSources gates the kubeconfig-source chip — only useful when 2+
|
|
49
|
+
// distinct kubeconfig files are in play. Single-source setups (the common
|
|
50
|
+
// case) skip the chip entirely so the dropdown stays clean.
|
|
51
|
+
const { parsedById, hasMultipleAccounts, hasMultipleSources } = useMemo(() => {
|
|
52
|
+
if (!contexts) return {
|
|
53
|
+
parsedById: new Map<string, ParsedContext>(),
|
|
54
|
+
hasMultipleAccounts: false,
|
|
55
|
+
hasMultipleSources: false,
|
|
56
|
+
}
|
|
57
|
+
// Strip the disambiguation suffix (" (<source>)" or " (<source> #N)")
|
|
58
|
+
// before parsing — qualified names won't match the GKE/EKS/AKS regexes
|
|
59
|
+
// otherwise, and the suffix is redundant with the source chip we
|
|
60
|
+
// render separately.
|
|
61
|
+
const stripSourceSuffix = (name: string, source?: string): string => {
|
|
62
|
+
if (!source) return name
|
|
63
|
+
const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
64
|
+
return name.replace(new RegExp(`\\s+\\(${escaped}(?:\\s+#\\d+)?\\)$`), '')
|
|
65
|
+
}
|
|
66
|
+
const parsed: ParsedContext[] = contexts.map(ctx => ({
|
|
67
|
+
context: ctx,
|
|
68
|
+
...parseContextName(stripSourceSuffix(ctx.name, ctx.source)),
|
|
69
|
+
}))
|
|
39
70
|
const accounts = new Set(parsed.map(p => `${p.provider}:${p.account}`))
|
|
71
|
+
const sources = new Set(contexts.map(c => c.source).filter(Boolean))
|
|
40
72
|
const byId = new Map<string, ParsedContext>()
|
|
41
73
|
for (const p of parsed) byId.set(p.context.name, p)
|
|
42
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
parsedById: byId,
|
|
76
|
+
hasMultipleAccounts: accounts.size > 1,
|
|
77
|
+
hasMultipleSources: sources.size > 1,
|
|
78
|
+
}
|
|
43
79
|
}, [contexts])
|
|
44
80
|
|
|
45
81
|
// Map parsed contexts → generic ClusterSwitcher items, sorted GKE/EKS/AKS/Other → account → name.
|
|
@@ -61,20 +97,16 @@ export function ContextSwitcher({ className = '' }: ContextSwitcherProps) {
|
|
|
61
97
|
: hasMultipleAccounts
|
|
62
98
|
? 'Other'
|
|
63
99
|
: undefined
|
|
64
|
-
// `name` is the raw context — ClusterSwitcher renders it through
|
|
65
|
-
// ClusterName, which collapses GKE/EKS/AKS shapes to the meaningful
|
|
66
|
-
// tail. `secondary` shows the original raw when we collapsed it,
|
|
67
|
-
// so users always see the full context at a glance (rather than
|
|
68
|
-
// having to hover to reveal it).
|
|
69
100
|
return {
|
|
70
101
|
id: p.context.name,
|
|
71
|
-
name: p.
|
|
102
|
+
name: p.raw,
|
|
72
103
|
secondary: p.provider ? p.raw : undefined,
|
|
73
104
|
badge: p.region || undefined,
|
|
105
|
+
sourceLabel: hasMultipleSources ? p.context.source : undefined,
|
|
74
106
|
group: { key: groupKey, label: groupLabel },
|
|
75
107
|
}
|
|
76
108
|
})
|
|
77
|
-
}, [parsedById, hasMultipleAccounts])
|
|
109
|
+
}, [parsedById, hasMultipleAccounts, hasMultipleSources])
|
|
78
110
|
|
|
79
111
|
const performSwitch = async (parsed: ParsedContext) => {
|
|
80
112
|
startSwitch({
|
|
@@ -91,8 +123,10 @@ export function ContextSwitcher({ className = '' }: ContextSwitcherProps) {
|
|
|
91
123
|
endSwitch()
|
|
92
124
|
// Backend may not transition to StateDisconnected on client-side errors
|
|
93
125
|
// (network, timeout) — without this toast the user gets no feedback.
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
if (!shouldSuppressSwitchErrorToast(error)) {
|
|
127
|
+
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
128
|
+
showError('Failed to switch context', message)
|
|
129
|
+
}
|
|
96
130
|
}
|
|
97
131
|
}
|
|
98
132
|
|
|
@@ -152,15 +186,26 @@ export function ContextSwitcher({ className = '' }: ContextSwitcherProps) {
|
|
|
152
186
|
)
|
|
153
187
|
}
|
|
154
188
|
|
|
155
|
-
const
|
|
156
|
-
const currentId =
|
|
189
|
+
const currentCtx = contexts?.find(c => c.isCurrent)
|
|
190
|
+
const currentId = currentCtx?.name
|
|
191
|
+
// Use parsed.raw (the source-stripped form) for the trigger so the
|
|
192
|
+
// disambiguation suffix doesn't double up with the source chip.
|
|
193
|
+
// Fall back to clusterInfo.context for the very-early window before
|
|
194
|
+
// /api/contexts has resolved.
|
|
195
|
+
const currentParsed = currentId ? parsedById.get(currentId) : undefined
|
|
196
|
+
const currentRaw = triggerName || currentParsed?.raw || clusterInfo?.context || currentCtx?.name || 'Unknown'
|
|
197
|
+
const currentSourceLabel = triggerName ? undefined : hasMultipleSources ? currentCtx?.source || undefined : undefined
|
|
157
198
|
|
|
158
199
|
return (
|
|
159
200
|
<>
|
|
160
201
|
<ClusterSwitcher
|
|
202
|
+
ref={ref}
|
|
161
203
|
className={className}
|
|
204
|
+
variant={variant}
|
|
205
|
+
label={label}
|
|
162
206
|
currentId={currentId}
|
|
163
207
|
currentName={currentRaw}
|
|
208
|
+
currentSourceLabel={currentSourceLabel}
|
|
164
209
|
items={items}
|
|
165
210
|
onSelect={handleSelect}
|
|
166
211
|
loading={switchContext.isPending}
|
|
@@ -222,4 +267,4 @@ export function ContextSwitcher({ className = '' }: ContextSwitcherProps) {
|
|
|
222
267
|
)}
|
|
223
268
|
</>
|
|
224
269
|
)
|
|
225
|
-
}
|
|
270
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Bug, X, ChevronDown, ChevronUp } from 'lucide-react'
|
|
3
3
|
import { useRuntimeStats } from '../api/client'
|
|
4
|
+
import { Tooltip } from './ui/Tooltip'
|
|
4
5
|
|
|
5
6
|
function formatUptime(seconds: number): string {
|
|
6
7
|
if (seconds < 60) return `${seconds}s`
|
|
@@ -17,13 +18,14 @@ export function DebugOverlay() {
|
|
|
17
18
|
|
|
18
19
|
if (!visible) {
|
|
19
20
|
return (
|
|
21
|
+
<Tooltip content="Show debug stats" position="left" wrapperClassName="fixed bottom-3 right-3 z-50">
|
|
20
22
|
<button
|
|
21
23
|
onClick={() => setVisible(true)}
|
|
22
|
-
className="
|
|
23
|
-
title="Show debug stats"
|
|
24
|
+
className="p-2 bg-theme-surface/90 border border-theme-border rounded-lg text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
24
25
|
>
|
|
25
26
|
<Bug className="w-4 h-4" />
|
|
26
27
|
</button>
|
|
28
|
+
</Tooltip>
|
|
27
29
|
)
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -86,7 +88,7 @@ export function DebugOverlay() {
|
|
|
86
88
|
)}
|
|
87
89
|
</>
|
|
88
90
|
) : (
|
|
89
|
-
<span className="text-theme-text-tertiary">Loading
|
|
91
|
+
<span className="text-theme-text-tertiary">Loading…</span>
|
|
90
92
|
)}
|
|
91
93
|
</div>
|
|
92
94
|
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import { NamespacePicker, type NamespacePickerHandle } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useNamespaceScope, useSetActiveNamespace } from '../api/client'
|
|
4
|
+
|
|
5
|
+
export type NamespaceSwitcherHandle = NamespacePickerHandle
|
|
6
|
+
|
|
7
|
+
interface NamespaceSwitcherProps {
|
|
8
|
+
className?: string
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
disabledTooltip?: string
|
|
11
|
+
variant?: 'chip' | 'segment'
|
|
12
|
+
label?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OSS Radar's namespace scope control — a thin data container over the shared
|
|
17
|
+
* presentational NamespacePicker (@skyhook-io/k8s-ui). Wires Radar's own API
|
|
18
|
+
* hooks; Radar Hub supplies its own container over the per-cluster apiBase.
|
|
19
|
+
*/
|
|
20
|
+
export const NamespaceSwitcher = forwardRef<NamespaceSwitcherHandle, NamespaceSwitcherProps>(function NamespaceSwitcher(
|
|
21
|
+
{ className, disabled, disabledTooltip, variant, label },
|
|
22
|
+
ref,
|
|
23
|
+
) {
|
|
24
|
+
const { data: scope, isLoading } = useNamespaceScope()
|
|
25
|
+
const setActive = useSetActiveNamespace()
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<NamespacePicker
|
|
29
|
+
ref={ref}
|
|
30
|
+
scope={scope}
|
|
31
|
+
loading={isLoading}
|
|
32
|
+
pending={setActive.isPending}
|
|
33
|
+
onApply={namespaces => setActive.mutate({ namespaces })}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
disabledTooltip={disabledTooltip}
|
|
36
|
+
className={className}
|
|
37
|
+
variant={variant}
|
|
38
|
+
label={label}
|
|
39
|
+
/>
|
|
40
|
+
)
|
|
41
|
+
})
|