@skyhook-io/radar-app 0.1.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 +67 -0
- package/package.json +80 -0
- package/src/App.tsx +1538 -0
- package/src/RadarApp.tsx +145 -0
- package/src/api/apiResources.ts +28 -0
- package/src/api/client.ts +2583 -0
- package/src/api/config.ts +116 -0
- package/src/api/traffic.ts +139 -0
- package/src/components/ConnectionErrorView.tsx +272 -0
- package/src/components/ContextSwitcher.tsx +481 -0
- package/src/components/DebugOverlay.tsx +94 -0
- package/src/components/UserMenu.tsx +87 -0
- package/src/components/audit/AuditSettingsDialog.tsx +162 -0
- package/src/components/audit/AuditView.tsx +123 -0
- package/src/components/cost/CostTrendChart.tsx +388 -0
- package/src/components/cost/CostView.tsx +545 -0
- package/src/components/dock/BottomDock.tsx +96 -0
- package/src/components/dock/DockContext.tsx +11 -0
- package/src/components/dock/LocalTerminalTab.tsx +22 -0
- package/src/components/dock/LogsTab.tsx +26 -0
- package/src/components/dock/NodeTerminalTab.tsx +50 -0
- package/src/components/dock/TerminalTab.tsx +42 -0
- package/src/components/dock/TrafficFlowListTab.tsx +18 -0
- package/src/components/dock/WorkloadLogsTab.tsx +23 -0
- package/src/components/dock/index.ts +2 -0
- package/src/components/gitops/GitOpsActions.tsx +1 -0
- package/src/components/gitops/GitOpsStatusBadge.tsx +1 -0
- package/src/components/gitops/ManagedResourcesList.tsx +1 -0
- package/src/components/gitops/SyncCountdown.tsx +1 -0
- package/src/components/gitops/index.ts +4 -0
- package/src/components/helm/ChartBrowser.tsx +580 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +774 -0
- package/src/components/helm/HelmView.tsx +475 -0
- package/src/components/helm/InstallWizard.tsx +1060 -0
- package/src/components/helm/ManifestDiffViewer.tsx +91 -0
- package/src/components/helm/ManifestViewer.tsx +61 -0
- package/src/components/helm/OwnedResources.tsx +465 -0
- package/src/components/helm/RevisionHistory.tsx +167 -0
- package/src/components/helm/ValuesDiffPreview.tsx +190 -0
- package/src/components/helm/ValuesViewer.tsx +365 -0
- package/src/components/helm/helm-utils.ts +37 -0
- package/src/components/home/ActivitySummary.tsx +262 -0
- package/src/components/home/CertificateHealthCard.tsx +105 -0
- package/src/components/home/ClusterHealthCard.tsx +483 -0
- package/src/components/home/CostCard.tsx +112 -0
- package/src/components/home/HealthRing.tsx +1 -0
- package/src/components/home/HelmSummary.tsx +129 -0
- package/src/components/home/HomeView.tsx +224 -0
- package/src/components/home/MCPSetupDialog.tsx +417 -0
- package/src/components/home/NetworkPolicyCoverageCard.tsx +109 -0
- package/src/components/home/TopologyPreview.tsx +219 -0
- package/src/components/home/TrafficSummary.tsx +154 -0
- package/src/components/logs/JsonLogLine.tsx +1 -0
- package/src/components/logs/LogCore.tsx +2 -0
- package/src/components/logs/LogsViewer.tsx +44 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +40 -0
- package/src/components/logs/useLogBuffer.ts +2 -0
- package/src/components/logs/useLogSearch.ts +1 -0
- package/src/components/portforward/PortForwardButton.tsx +375 -0
- package/src/components/portforward/PortForwardManager.tsx +871 -0
- package/src/components/resource/PrometheusCharts.tsx +687 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +214 -0
- package/src/components/resources/ImageFilesystemModal.tsx +745 -0
- package/src/components/resources/PodFilesystemModal.tsx +407 -0
- package/src/components/resources/ResourceDetailDrawer.tsx +43 -0
- package/src/components/resources/ResourcesView.tsx +190 -0
- package/src/components/resources/drawer-components.tsx +1 -0
- package/src/components/resources/file-browser-utils.ts +35 -0
- package/src/components/resources/renderers/AlertRenderer.tsx +1 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +17 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +1 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRenderer.tsx +1 -0
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +1 -0
- package/src/components/resources/renderers/ChallengeRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +1 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/EventRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/GatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/GenericRenderer.tsx +1 -0
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +1 -0
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +1 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/JobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +1 -0
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +1 -0
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +1 -0
- package/src/components/resources/renderers/KustomizationRenderer.tsx +1 -0
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/LeaseRenderer.tsx +1 -0
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +1 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +44 -0
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +1 -0
- package/src/components/resources/renderers/OrderRenderer.tsx +1 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +1 -0
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/PodRenderer.tsx +94 -0
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +1 -0
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +1 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +1 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +1 -0
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/SbomReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretRenderer.tsx +1 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +1 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +26 -0
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/StorageClassRenderer.tsx +1 -0
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +1 -0
- package/src/components/resources/renderers/VPARenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroBackupRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroRestoreRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +1 -0
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +1 -0
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +1 -0
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +1 -0
- package/src/components/resources/renderers/WorkloadRenderer.tsx +52 -0
- package/src/components/resources/renderers/argo-cells.tsx +1 -0
- package/src/components/resources/renderers/certmanager-cells.tsx +1 -0
- package/src/components/resources/renderers/cnpg-cells.tsx +1 -0
- package/src/components/resources/renderers/eso-cells.tsx +1 -0
- package/src/components/resources/renderers/flux-cells.tsx +1 -0
- package/src/components/resources/renderers/index.ts +91 -0
- package/src/components/resources/renderers/istio-cells.tsx +1 -0
- package/src/components/resources/renderers/karpenter-cells.tsx +1 -0
- package/src/components/resources/renderers/keda-cells.tsx +1 -0
- package/src/components/resources/renderers/knative-cells.tsx +1 -0
- package/src/components/resources/renderers/kyverno-cells.tsx +1 -0
- package/src/components/resources/renderers/prometheus-cells.tsx +1 -0
- package/src/components/resources/renderers/traefik-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-cells.tsx +1 -0
- package/src/components/resources/renderers/trivy-shared.tsx +1 -0
- package/src/components/resources/renderers/velero-cells.tsx +1 -0
- package/src/components/resources/resource-utils-argo.ts +2 -0
- package/src/components/resources/resource-utils-certmanager.ts +2 -0
- package/src/components/resources/resource-utils-cnpg.ts +2 -0
- package/src/components/resources/resource-utils-eso.ts +2 -0
- package/src/components/resources/resource-utils-flux.ts +2 -0
- package/src/components/resources/resource-utils-istio.ts +2 -0
- package/src/components/resources/resource-utils-karpenter.ts +2 -0
- package/src/components/resources/resource-utils-keda.ts +2 -0
- package/src/components/resources/resource-utils-knative.ts +2 -0
- package/src/components/resources/resource-utils-kyverno.ts +2 -0
- package/src/components/resources/resource-utils-prometheus.ts +2 -0
- package/src/components/resources/resource-utils-traefik.ts +1 -0
- package/src/components/resources/resource-utils-trivy.ts +2 -0
- package/src/components/resources/resource-utils-velero.ts +2 -0
- package/src/components/resources/resource-utils.ts +5 -0
- package/src/components/settings/SettingsDialog.tsx +537 -0
- package/src/components/shared/CreateResourceDialog.tsx +17 -0
- package/src/components/shared/EditableYamlView.tsx +24 -0
- package/src/components/shared/LargeClusterNamespacePicker.tsx +70 -0
- package/src/components/shared/ResourceRendererDispatch.tsx +31 -0
- package/src/components/timeline/DiffViewer.tsx +1 -0
- package/src/components/timeline/TimelineList.tsx +69 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1308 -0
- package/src/components/timeline/TimelineView.tsx +157 -0
- package/src/components/timeline/shared.tsx +1 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +571 -0
- package/src/components/traffic/TrafficFlowList.tsx +415 -0
- package/src/components/traffic/TrafficFlowListContext.tsx +68 -0
- package/src/components/traffic/TrafficGraph.tsx +1546 -0
- package/src/components/traffic/TrafficView.tsx +1213 -0
- package/src/components/traffic/TrafficWizard.tsx +386 -0
- package/src/components/traffic/index.ts +3 -0
- package/src/components/ui/CodeViewer.tsx +8 -0
- package/src/components/ui/CommandPalette.tsx +460 -0
- package/src/components/ui/ConfirmDialog.tsx +1 -0
- package/src/components/ui/DiagnosticsOverlay.tsx +619 -0
- package/src/components/ui/ErrorBoundary.tsx +46 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +1 -0
- package/src/components/ui/Markdown.tsx +108 -0
- package/src/components/ui/MetricsChart.tsx +1 -0
- package/src/components/ui/NamespaceSelector.tsx +436 -0
- package/src/components/ui/ResourceBar.tsx +1 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +301 -0
- package/src/components/ui/Toast.tsx +1 -0
- package/src/components/ui/Tooltip.tsx +1 -0
- package/src/components/ui/UpdateNotification.tsx +299 -0
- package/src/components/ui/YamlEditor.tsx +1 -0
- package/src/components/workload/WorkloadView.tsx +532 -0
- package/src/context/ConnectionContext.tsx +173 -0
- package/src/context/ContextSwitchContext.tsx +56 -0
- package/src/context/NavCustomization.tsx +62 -0
- package/src/context/ThemeContext.tsx +97 -0
- package/src/contexts/CapabilitiesContext.tsx +130 -0
- package/src/hooks/useAnimatedUnmount.ts +1 -0
- package/src/hooks/useDesktopDownload.ts +41 -0
- package/src/hooks/useEventSource.ts +262 -0
- package/src/hooks/useFavorites.ts +69 -0
- package/src/hooks/useKeyboardShortcuts.tsx +7 -0
- package/src/hooks/useRefreshAnimation.ts +1 -0
- package/src/index.css +243 -0
- package/src/index.ts +17 -0
- package/src/main.tsx +158 -0
- package/src/types/gitops.ts +2 -0
- package/src/types.ts +3 -0
- package/src/utils/animation.ts +2 -0
- package/src/utils/badge-colors.ts +2 -0
- package/src/utils/context-name.ts +2 -0
- package/src/utils/desktop-download.ts +66 -0
- package/src/utils/desktop-open-folder.ts +21 -0
- package/src/utils/format.ts +2 -0
- package/src/utils/log-format.ts +12 -0
- package/src/utils/navigation.ts +23 -0
- package/src/utils/resource-hierarchy.ts +2 -0
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.ts +2 -0
- package/src/utils/traffic-colors.ts +54 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
2
|
+
import { X, Copy, Check, ExternalLink } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
5
|
+
import { openExternal } from '../../utils/navigation'
|
|
6
|
+
import { useDiagnostics } from '../../api/client'
|
|
7
|
+
import type { DiagnosticsSnapshot, DiagMetricsSourceHealth, DiagDropRecord, DiagErrorEntry } from '../../api/client'
|
|
8
|
+
|
|
9
|
+
interface DiagnosticsOverlayProps {
|
|
10
|
+
onClose: () => void
|
|
11
|
+
isOpen?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DiagnosticsOverlay({ onClose, isOpen = true }: DiagnosticsOverlayProps) {
|
|
15
|
+
const { data, isLoading, error } = useDiagnostics(true)
|
|
16
|
+
const [copied, setCopied] = useState<'json' | 'formatted' | null>(null)
|
|
17
|
+
const [reportOpened, setReportOpened] = useState(false)
|
|
18
|
+
|
|
19
|
+
// Close on Escape (capture phase)
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handler = (e: KeyboardEvent) => {
|
|
22
|
+
if (e.key === 'Escape') {
|
|
23
|
+
e.preventDefault()
|
|
24
|
+
e.stopPropagation()
|
|
25
|
+
onClose()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
document.addEventListener('keydown', handler, true)
|
|
29
|
+
return () => document.removeEventListener('keydown', handler, true)
|
|
30
|
+
}, [onClose])
|
|
31
|
+
|
|
32
|
+
const copyToClipboard = useCallback(async (type: 'json' | 'formatted') => {
|
|
33
|
+
if (!data) return
|
|
34
|
+
const text = type === 'json'
|
|
35
|
+
? JSON.stringify(data, null, 2)
|
|
36
|
+
: formatForGitHub(data)
|
|
37
|
+
try {
|
|
38
|
+
await navigator.clipboard.writeText(text)
|
|
39
|
+
setCopied(type)
|
|
40
|
+
setTimeout(() => setCopied(null), 2000)
|
|
41
|
+
} catch {
|
|
42
|
+
// Clipboard API can fail on non-HTTPS origins or without focus
|
|
43
|
+
console.warn('Failed to copy to clipboard')
|
|
44
|
+
}
|
|
45
|
+
}, [data])
|
|
46
|
+
|
|
47
|
+
const openBugReport = useCallback(() => {
|
|
48
|
+
if (!data) return
|
|
49
|
+
const body = formatForBugReport(data)
|
|
50
|
+
const url = `https://github.com/skyhook-io/radar/issues/new?labels=bug&body=${encodeURIComponent(body)}`
|
|
51
|
+
if (url.length > 8000) {
|
|
52
|
+
// URL too long for GitHub — copy diagnostics to clipboard and open blank issue
|
|
53
|
+
navigator.clipboard.writeText(body).catch(() => {})
|
|
54
|
+
openExternal('https://github.com/skyhook-io/radar/issues/new?labels=bug&template=bug_report.md')
|
|
55
|
+
setCopied('formatted')
|
|
56
|
+
setTimeout(() => setCopied(null), 2000)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
openExternal(url)
|
|
60
|
+
setReportOpened(true)
|
|
61
|
+
setTimeout(() => setReportOpened(false), 2000)
|
|
62
|
+
}, [data])
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div className="fixed inset-0 z-[100] flex items-start justify-center pt-[8vh]">
|
|
66
|
+
{/* Backdrop */}
|
|
67
|
+
<div
|
|
68
|
+
className={clsx(
|
|
69
|
+
'absolute inset-0 bg-theme-base/60 backdrop-blur-sm',
|
|
70
|
+
TRANSITION_BACKDROP,
|
|
71
|
+
isOpen ? 'opacity-100' : 'opacity-0'
|
|
72
|
+
)}
|
|
73
|
+
onClick={onClose}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
{/* Panel */}
|
|
77
|
+
<div className={clsx(
|
|
78
|
+
'relative w-full max-w-2xl mx-4 dialog overflow-hidden flex flex-col max-h-[84vh]',
|
|
79
|
+
TRANSITION_PANEL,
|
|
80
|
+
isOpen ? 'opacity-100 scale-100 translate-y-0' : 'opacity-0 scale-[0.97] translate-y-3'
|
|
81
|
+
)}>
|
|
82
|
+
{/* Header */}
|
|
83
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-theme-border shrink-0">
|
|
84
|
+
<div className="flex items-center gap-3">
|
|
85
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">Diagnostics</h2>
|
|
86
|
+
{data && (
|
|
87
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
88
|
+
v{data.radarVersion} · up {data.uptime}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<button onClick={onClose} className="p-1 rounded-md text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/50">
|
|
93
|
+
<X className="w-4 h-4" />
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Content */}
|
|
98
|
+
<div className="overflow-y-auto flex-1 px-5 py-4 space-y-4">
|
|
99
|
+
{isLoading && (
|
|
100
|
+
<div className="text-sm text-theme-text-tertiary text-center py-8">Loading diagnostics...</div>
|
|
101
|
+
)}
|
|
102
|
+
{error && (
|
|
103
|
+
<div className="text-sm text-red-400 text-center py-8">Failed to load diagnostics: {(error as Error).message}</div>
|
|
104
|
+
)}
|
|
105
|
+
{data && (
|
|
106
|
+
<>
|
|
107
|
+
<ErrorLogSection data={data} />
|
|
108
|
+
<ConnectionSection data={data} />
|
|
109
|
+
<KubeconfigSection data={data} />
|
|
110
|
+
<ClusterSection data={data} />
|
|
111
|
+
<CacheSection data={data} />
|
|
112
|
+
<MetricsSection data={data} />
|
|
113
|
+
<EventPipelineSection data={data} />
|
|
114
|
+
<InformersSection data={data} />
|
|
115
|
+
<PrometheusSection data={data} />
|
|
116
|
+
<TrafficSection data={data} />
|
|
117
|
+
<PermissionsSection data={data} />
|
|
118
|
+
<APIDiscoverySection data={data} />
|
|
119
|
+
<RuntimeSection data={data} />
|
|
120
|
+
<ConfigSection data={data} />
|
|
121
|
+
{data.errors && data.errors.length > 0 && (
|
|
122
|
+
<Section title="Collection Errors" warn>
|
|
123
|
+
{data.errors.map((e, i) => <Row key={i} label={`Error ${i + 1}`} value={e} />)}
|
|
124
|
+
</Section>
|
|
125
|
+
)}
|
|
126
|
+
</>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Footer */}
|
|
131
|
+
<div className="flex items-center gap-2 px-5 py-3 border-t border-theme-border shrink-0">
|
|
132
|
+
<CopyButton label="Copy as Markdown" onClick={() => copyToClipboard('formatted')} copied={copied === 'formatted'} />
|
|
133
|
+
<CopyButton label="Copy Raw JSON" onClick={() => copyToClipboard('json')} copied={copied === 'json'} />
|
|
134
|
+
<div className="flex-1" />
|
|
135
|
+
<button
|
|
136
|
+
onClick={openBugReport}
|
|
137
|
+
disabled={!data}
|
|
138
|
+
className={clsx(
|
|
139
|
+
'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed',
|
|
140
|
+
reportOpened
|
|
141
|
+
? 'bg-green-500/20 text-green-400'
|
|
142
|
+
: 'bg-theme-elevated text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/80'
|
|
143
|
+
)}
|
|
144
|
+
>
|
|
145
|
+
{reportOpened ? <Check className="w-3.5 h-3.5" /> : <ExternalLink className="w-3.5 h-3.5" />}
|
|
146
|
+
{reportOpened ? 'Opened!' : 'Report Bug'}
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Section components ---
|
|
155
|
+
|
|
156
|
+
function Section({ title, children, warn }: { title: string; children: React.ReactNode; warn?: boolean }) {
|
|
157
|
+
return (
|
|
158
|
+
<div className={clsx(
|
|
159
|
+
'rounded-lg border px-3.5 py-2.5',
|
|
160
|
+
warn ? 'border-yellow-500/30 bg-yellow-500/5' : 'border-theme-border-light bg-theme-elevated/20'
|
|
161
|
+
)}>
|
|
162
|
+
<h3 className="text-[11px] font-semibold text-theme-text-tertiary uppercase tracking-wider mb-1.5">{title}</h3>
|
|
163
|
+
<div className="space-y-0.5">{children}</div>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function Row({ label, value, warn }: { label: string; value: React.ReactNode; warn?: boolean }) {
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex items-baseline justify-between gap-4 text-xs">
|
|
171
|
+
<span className="text-theme-text-secondary shrink-0">{label}</span>
|
|
172
|
+
<span className={clsx(
|
|
173
|
+
'text-right truncate',
|
|
174
|
+
warn ? 'text-yellow-400' : 'text-theme-text-primary'
|
|
175
|
+
)}>{value}</span>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function ErrorLogSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
181
|
+
if (!data.recentErrors || data.recentErrors.length === 0) return null
|
|
182
|
+
const entries = data.recentErrors.slice(-10).reverse()
|
|
183
|
+
return (
|
|
184
|
+
<Section title={`Recent Errors (${data.recentErrors.length}${data.totalErrorsRecorded && data.totalErrorsRecorded > data.recentErrors.length ? ` of ${data.totalErrorsRecorded} total` : ''})`} warn>
|
|
185
|
+
{entries.map((e: DiagErrorEntry, i: number) => (
|
|
186
|
+
<Row key={i} label={`[${e.source}] ${new Date(e.time).toLocaleTimeString()}`} value={e.message} warn={e.level === 'error'} />
|
|
187
|
+
))}
|
|
188
|
+
</Section>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function ConnectionSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
193
|
+
if (!data.connection) return null
|
|
194
|
+
const c = data.connection
|
|
195
|
+
const warn = c.state !== 'connected'
|
|
196
|
+
return (
|
|
197
|
+
<Section title="Connection" warn={warn}>
|
|
198
|
+
<Row label="State" value={c.state} warn={warn} />
|
|
199
|
+
<Row label="Context" value={c.context} />
|
|
200
|
+
{c.clusterName && <Row label="Cluster" value={c.clusterName} />}
|
|
201
|
+
{c.error && <Row label="Error" value={c.error} warn />}
|
|
202
|
+
{c.errorType && <Row label="Error Type" value={c.errorType} warn />}
|
|
203
|
+
</Section>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function KubeconfigSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
208
|
+
if (!data.kubeconfig) return null
|
|
209
|
+
const k = data.kubeconfig
|
|
210
|
+
// Missing exec plugins are the single strongest signal for desktop-app
|
|
211
|
+
// multi-cluster failures (radar#411) — GUI apps often don't inherit the
|
|
212
|
+
// user's PATH, so aws/gcloud/doctl/kubelogin can be invisible even though
|
|
213
|
+
// the CLI works fine. Highlight the whole section when that's the case.
|
|
214
|
+
const missing = k.execPluginsMissing ?? []
|
|
215
|
+
const present = k.execPluginsPresent ?? []
|
|
216
|
+
const hasMissing = missing.length > 0
|
|
217
|
+
return (
|
|
218
|
+
<Section title="Kubeconfig" warn={hasMissing}>
|
|
219
|
+
<Row label="Mode" value={k.mode || '(not initialized)'} />
|
|
220
|
+
<Row label="Files Loaded" value={k.fileCount} />
|
|
221
|
+
<Row label="Contexts (post-merge)" value={k.contextCount} />
|
|
222
|
+
<Row label="Enriched From Shell" value={k.enrichedFromShell ? 'Yes' : 'No'} />
|
|
223
|
+
<Row
|
|
224
|
+
label="Current Context Uses Exec"
|
|
225
|
+
value={k.currentContextUsesExec ? 'Yes' : 'No'}
|
|
226
|
+
/>
|
|
227
|
+
{present.length > 0 && (
|
|
228
|
+
<Row label="Exec Plugins on PATH" value={present.join(', ')} />
|
|
229
|
+
)}
|
|
230
|
+
{hasMissing && (
|
|
231
|
+
<Row
|
|
232
|
+
label="Exec Plugins MISSING from PATH"
|
|
233
|
+
value={missing.join(', ')}
|
|
234
|
+
warn
|
|
235
|
+
/>
|
|
236
|
+
)}
|
|
237
|
+
</Section>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function ClusterSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
242
|
+
if (!data.cluster) return null
|
|
243
|
+
const c = data.cluster
|
|
244
|
+
return (
|
|
245
|
+
<Section title="Cluster">
|
|
246
|
+
<Row label="Platform" value={c.platform} />
|
|
247
|
+
<Row label="Kubernetes" value={c.kubernetesVersion} />
|
|
248
|
+
<Row label="Nodes" value={c.nodeCount} />
|
|
249
|
+
<Row label="Namespaces" value={c.namespaceCount} />
|
|
250
|
+
{c.inCluster && <Row label="In-Cluster" value="Yes" />}
|
|
251
|
+
</Section>
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function CacheSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
256
|
+
if (!data.cache) return null
|
|
257
|
+
return (
|
|
258
|
+
<Section title="Cache">
|
|
259
|
+
<Row label="Total Resources" value={data.cache.totalResources.toLocaleString()} />
|
|
260
|
+
<Row label="Watched Kinds" value={data.cache.watchedKinds.length} />
|
|
261
|
+
</Section>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function MetricsSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
266
|
+
if (!data.metrics) return null
|
|
267
|
+
const m = data.metrics
|
|
268
|
+
const pod = m.podMetrics
|
|
269
|
+
const node = m.nodeMetrics
|
|
270
|
+
const warn = pod.consecutiveErrors > 0 || node.consecutiveErrors > 0
|
|
271
|
+
return (
|
|
272
|
+
<Section title="Metrics Collection" warn={warn}>
|
|
273
|
+
<MetricsSourceRow label="Pod Metrics" source={pod} />
|
|
274
|
+
<MetricsSourceRow label="Node Metrics" source={node} />
|
|
275
|
+
<Row label="Poll Loop" value={`${m.totalCollections} collections, every ${m.pollIntervalSec}s, buffer ${m.bufferSize} points`} />
|
|
276
|
+
{m.lastAttempt && <Row label="Last Attempt" value={new Date(m.lastAttempt).toLocaleTimeString()} />}
|
|
277
|
+
</Section>
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function MetricsSourceRow({ label, source }: { label: string; source: DiagMetricsSourceHealth }) {
|
|
282
|
+
const status = source.collecting ? 'collecting' : source.consecutiveErrors > 0 ? `${source.consecutiveErrors} errors` : 'idle'
|
|
283
|
+
const warn = source.consecutiveErrors > 0
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
<Row label={label} value={`${status} (${source.trackedCount} tracked, ${source.totalDataPoints} points)`} warn={warn} />
|
|
287
|
+
{source.lastError && <Row label={` Last Error`} value={source.lastError} warn />}
|
|
288
|
+
</>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function EventPipelineSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
293
|
+
if (!data.eventPipeline) return null
|
|
294
|
+
const ep = data.eventPipeline
|
|
295
|
+
const totalDropped = Object.values(ep.dropped).reduce((a, b) => a + b, 0)
|
|
296
|
+
const totalReceived = Object.values(ep.received).reduce((a, b) => a + b, 0)
|
|
297
|
+
const warn = totalDropped > 0
|
|
298
|
+
return (
|
|
299
|
+
<Section title="Event Pipeline" warn={warn}>
|
|
300
|
+
<Row label="Total Received" value={totalReceived.toLocaleString()} />
|
|
301
|
+
<Row label="Total Dropped" value={totalDropped.toLocaleString()} warn={warn} />
|
|
302
|
+
<Row label="Uptime" value={ep.uptime} />
|
|
303
|
+
{ep.recentDrops && ep.recentDrops.length > 0 && (
|
|
304
|
+
<div className="mt-1.5 pt-1.5 border-t border-theme-border-light">
|
|
305
|
+
<span className="text-[10px] text-theme-text-tertiary uppercase">Recent Drops ({ep.recentDrops.length})</span>
|
|
306
|
+
{ep.recentDrops.slice(0, 5).map((d: DiagDropRecord, i: number) => (
|
|
307
|
+
<Row key={i} label={`${d.kind}/${d.name}`} value={d.reason} warn />
|
|
308
|
+
))}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
</Section>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function InformersSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
316
|
+
if (!data.informers) return null
|
|
317
|
+
const inf = data.informers
|
|
318
|
+
return (
|
|
319
|
+
<Section title="Informers">
|
|
320
|
+
<Row label="Typed" value={inf.typedCount} />
|
|
321
|
+
<Row label="Dynamic (CRDs)" value={inf.dynamicCount} />
|
|
322
|
+
{inf.watchedCRDs && inf.watchedCRDs.length > 0 && (
|
|
323
|
+
<Row label="Watched CRDs" value={inf.watchedCRDs.join(', ')} />
|
|
324
|
+
)}
|
|
325
|
+
</Section>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function PrometheusSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
330
|
+
if (!data.prometheus) return null
|
|
331
|
+
const p = data.prometheus
|
|
332
|
+
const warn = !p.connected
|
|
333
|
+
return (
|
|
334
|
+
<Section title="Prometheus" warn={warn}>
|
|
335
|
+
<Row label="Connected" value={p.connected ? 'Yes' : 'No'} warn={warn} />
|
|
336
|
+
{p.address && <Row label="Address" value={p.address} />}
|
|
337
|
+
{p.serviceName && <Row label="Service" value={`${p.serviceNamespace}/${p.serviceName}`} />}
|
|
338
|
+
</Section>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function TrafficSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
343
|
+
if (!data.traffic) return null
|
|
344
|
+
const t = data.traffic
|
|
345
|
+
return (
|
|
346
|
+
<Section title="Traffic">
|
|
347
|
+
<Row label="Active Source" value={t.activeSource || 'none'} />
|
|
348
|
+
{t.detected && t.detected.length > 0 && <Row label="Detected" value={t.detected.join(', ')} />}
|
|
349
|
+
{t.notDetected && t.notDetected.length > 0 && <Row label="Not Detected" value={t.notDetected.join(', ')} />}
|
|
350
|
+
</Section>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function PermissionsSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
355
|
+
if (!data.permissions) return null
|
|
356
|
+
const p = data.permissions
|
|
357
|
+
const warn = (p.restricted && p.restricted.length > 0) || false
|
|
358
|
+
return (
|
|
359
|
+
<Section title="Permissions" warn={warn}>
|
|
360
|
+
<Row label="Capabilities" value={[
|
|
361
|
+
p.exec && 'exec', p.logs && 'logs', p.portForward && 'port-forward',
|
|
362
|
+
p.secrets && 'secrets', p.helmWrite && 'helm-write',
|
|
363
|
+
].filter(Boolean).join(', ') || 'none'} />
|
|
364
|
+
{p.namespaceScoped && <Row label="Scope" value={`namespace: ${p.namespace}`} warn />}
|
|
365
|
+
{p.restricted && p.restricted.length > 0 && (
|
|
366
|
+
<Row label="Restricted" value={p.restricted.join(', ')} warn />
|
|
367
|
+
)}
|
|
368
|
+
</Section>
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function APIDiscoverySection({ data }: { data: DiagnosticsSnapshot }) {
|
|
373
|
+
if (!data.apiDiscovery) return null
|
|
374
|
+
const d = data.apiDiscovery
|
|
375
|
+
return (
|
|
376
|
+
<Section title="API Discovery">
|
|
377
|
+
<Row label="Total Resources" value={d.totalResources} />
|
|
378
|
+
<Row label="CRDs" value={d.crdCount} />
|
|
379
|
+
{d.lastRefresh && <Row label="Last Refresh" value={new Date(d.lastRefresh).toLocaleTimeString()} />}
|
|
380
|
+
</Section>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function RuntimeSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
385
|
+
if (!data.runtime) return null
|
|
386
|
+
const rt = data.runtime
|
|
387
|
+
return (
|
|
388
|
+
<Section title="Runtime">
|
|
389
|
+
<Row label="Heap" value={`${rt.heapMB.toFixed(1)} MB (${rt.heapObjectsK.toFixed(1)}K objects)`} />
|
|
390
|
+
<Row label="Goroutines" value={rt.goroutines} />
|
|
391
|
+
<Row label="CPUs" value={rt.numCPU} />
|
|
392
|
+
{data.sse && <Row label="SSE Clients" value={data.sse.connectedClients} />}
|
|
393
|
+
</Section>
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function ConfigSection({ data }: { data: DiagnosticsSnapshot }) {
|
|
398
|
+
if (!data.config) return null
|
|
399
|
+
const cfg = data.config
|
|
400
|
+
return (
|
|
401
|
+
<Section title="Config">
|
|
402
|
+
<Row label="Port" value={cfg.port} />
|
|
403
|
+
<Row label="Dev Mode" value={cfg.devMode ? 'Yes' : 'No'} />
|
|
404
|
+
{cfg.namespace && <Row label="Namespace Filter" value={cfg.namespace} />}
|
|
405
|
+
<Row label="Timeline Storage" value={cfg.timelineStorage} />
|
|
406
|
+
<Row label="History Limit" value={cfg.historyLimit.toLocaleString()} />
|
|
407
|
+
<Row label="MCP Enabled" value={cfg.mcpEnabled ? 'Yes' : 'No'} />
|
|
408
|
+
<Row label="Prometheus URL" value={cfg.hasPrometheusURL ? 'Set' : 'Auto-discover'} />
|
|
409
|
+
</Section>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// --- Copy button ---
|
|
414
|
+
|
|
415
|
+
function CopyButton({ label, onClick, copied }: { label: string; onClick: () => void; copied: boolean }) {
|
|
416
|
+
return (
|
|
417
|
+
<button
|
|
418
|
+
onClick={onClick}
|
|
419
|
+
className={clsx(
|
|
420
|
+
'flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors',
|
|
421
|
+
copied
|
|
422
|
+
? 'bg-green-500/20 text-green-400'
|
|
423
|
+
: 'bg-theme-elevated text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated/80'
|
|
424
|
+
)}
|
|
425
|
+
>
|
|
426
|
+
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
|
427
|
+
{copied ? 'Copied!' : label}
|
|
428
|
+
</button>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// --- GitHub-friendly formatting ---
|
|
433
|
+
|
|
434
|
+
function formatForGitHub(data: DiagnosticsSnapshot, includeRawJson = true): string {
|
|
435
|
+
const lines: string[] = []
|
|
436
|
+
lines.push(`## Radar Diagnostics`)
|
|
437
|
+
lines.push(``)
|
|
438
|
+
lines.push(`**Version:** ${data.radarVersion} | **Go:** ${data.goVersion} | **OS:** ${data.goos}/${data.goarch} | **Uptime:** ${data.uptime}`)
|
|
439
|
+
lines.push(``)
|
|
440
|
+
|
|
441
|
+
if (data.connection) {
|
|
442
|
+
const c = data.connection
|
|
443
|
+
lines.push(`### Connection`)
|
|
444
|
+
lines.push(`- State: \`${c.state}\``)
|
|
445
|
+
lines.push(`- Context: \`${c.context}\``)
|
|
446
|
+
if (c.clusterName) lines.push(`- Cluster: \`${c.clusterName}\``)
|
|
447
|
+
if (c.error) lines.push(`- Error: ${c.error}`)
|
|
448
|
+
if (c.errorType) lines.push(`- Error Type: \`${c.errorType}\``)
|
|
449
|
+
lines.push(``)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (data.kubeconfig) {
|
|
453
|
+
const k = data.kubeconfig
|
|
454
|
+
lines.push(`### Kubeconfig`)
|
|
455
|
+
lines.push(`- Mode: \`${k.mode || '(not initialized)'}\` | Files: ${k.fileCount} | Contexts (post-merge): ${k.contextCount} | Enriched From Shell: ${k.enrichedFromShell ? 'Yes' : 'No'}`)
|
|
456
|
+
lines.push(`- Current Context Uses Exec: ${k.currentContextUsesExec ? 'Yes' : 'No'}`)
|
|
457
|
+
if (k.execPluginsPresent && k.execPluginsPresent.length > 0) {
|
|
458
|
+
lines.push(`- Exec Plugins on PATH: \`${k.execPluginsPresent.join('`, `')}\``)
|
|
459
|
+
}
|
|
460
|
+
if (k.execPluginsMissing && k.execPluginsMissing.length > 0) {
|
|
461
|
+
lines.push(`- **Exec Plugins MISSING from PATH:** \`${k.execPluginsMissing.join('`, `')}\``)
|
|
462
|
+
}
|
|
463
|
+
lines.push(``)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (data.cluster) {
|
|
467
|
+
const c = data.cluster
|
|
468
|
+
lines.push(`### Cluster`)
|
|
469
|
+
lines.push(`- Platform: \`${c.platform}\` | K8s: \`${c.kubernetesVersion}\` | Nodes: ${c.nodeCount} | Namespaces: ${c.namespaceCount}${c.inCluster ? ' | In-Cluster' : ''}`)
|
|
470
|
+
lines.push(``)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (data.cache) {
|
|
474
|
+
lines.push(`### Cache`)
|
|
475
|
+
lines.push(`- Total Resources: ${data.cache.totalResources.toLocaleString()} | Watched Kinds: ${data.cache.watchedKinds.length}`)
|
|
476
|
+
lines.push(``)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (data.metrics) {
|
|
480
|
+
const m = data.metrics
|
|
481
|
+
const pod = m.podMetrics
|
|
482
|
+
const node = m.nodeMetrics
|
|
483
|
+
lines.push(`### Metrics Collection`)
|
|
484
|
+
lines.push(`- Pod: ${pod.collecting ? 'collecting' : 'idle'} (${pod.trackedCount} tracked, ${pod.totalDataPoints} points, ${pod.consecutiveErrors} errors)`)
|
|
485
|
+
lines.push(`- Node: ${node.collecting ? 'collecting' : 'idle'} (${node.trackedCount} tracked, ${node.totalDataPoints} points, ${node.consecutiveErrors} errors)`)
|
|
486
|
+
lines.push(`- Poll loop: ${m.totalCollections} collections, every ${m.pollIntervalSec}s, buffer ${m.bufferSize} points`)
|
|
487
|
+
if (pod.lastError) lines.push(`- Pod Error: ${pod.lastError}`)
|
|
488
|
+
if (node.lastError) lines.push(`- Node Error: ${node.lastError}`)
|
|
489
|
+
lines.push(``)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (data.eventPipeline) {
|
|
493
|
+
const ep = data.eventPipeline
|
|
494
|
+
const totalReceived = Object.values(ep.received).reduce((a, b) => a + b, 0)
|
|
495
|
+
const totalDropped = Object.values(ep.dropped).reduce((a, b) => a + b, 0)
|
|
496
|
+
lines.push(`### Event Pipeline`)
|
|
497
|
+
lines.push(`- Received: ${totalReceived.toLocaleString()} | Dropped: ${totalDropped.toLocaleString()} | Uptime: ${ep.uptime}`)
|
|
498
|
+
if (ep.recentDrops && ep.recentDrops.length > 0) {
|
|
499
|
+
lines.push(`- Recent drops: ${ep.recentDrops.slice(0, 5).map(d => `${d.kind}/${d.name} (${d.reason})`).join(', ')}`)
|
|
500
|
+
}
|
|
501
|
+
lines.push(``)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (data.timeline) {
|
|
505
|
+
const t = data.timeline
|
|
506
|
+
lines.push(`### Timeline`)
|
|
507
|
+
lines.push(`- Storage: \`${t.storageType}\` | Events: ${t.totalEvents.toLocaleString()} | Errors: ${t.storeErrors} | Drops: ${t.totalDrops}`)
|
|
508
|
+
lines.push(``)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (data.informers) {
|
|
512
|
+
const inf = data.informers
|
|
513
|
+
lines.push(`### Informers`)
|
|
514
|
+
lines.push(`- Typed: ${inf.typedCount} | Dynamic: ${inf.dynamicCount}`)
|
|
515
|
+
if (inf.watchedCRDs && inf.watchedCRDs.length > 0) {
|
|
516
|
+
lines.push(`- CRDs: ${inf.watchedCRDs.join(', ')}`)
|
|
517
|
+
}
|
|
518
|
+
lines.push(``)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (data.prometheus) {
|
|
522
|
+
const p = data.prometheus
|
|
523
|
+
lines.push(`### Prometheus`)
|
|
524
|
+
lines.push(`- Connected: ${p.connected ? 'Yes' : 'No'}${p.serviceName ? ` | Service: ${p.serviceNamespace}/${p.serviceName}` : ''}`)
|
|
525
|
+
lines.push(``)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (data.traffic) {
|
|
529
|
+
const t = data.traffic
|
|
530
|
+
lines.push(`### Traffic`)
|
|
531
|
+
lines.push(`- Active: \`${t.activeSource || 'none'}\`${t.detected?.length ? ` | Detected: ${t.detected.join(', ')}` : ''}`)
|
|
532
|
+
lines.push(``)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (data.permissions) {
|
|
536
|
+
const p = data.permissions
|
|
537
|
+
const caps = [p.exec && 'exec', p.logs && 'logs', p.portForward && 'port-forward', p.secrets && 'secrets', p.helmWrite && 'helm-write'].filter(Boolean).join(', ')
|
|
538
|
+
lines.push(`### Permissions`)
|
|
539
|
+
lines.push(`- Capabilities: ${caps || 'none'}${p.namespaceScoped ? ` | Scope: namespace \`${p.namespace}\`` : ''}`)
|
|
540
|
+
if (p.restricted && p.restricted.length > 0) lines.push(`- Restricted: ${p.restricted.join(', ')}`)
|
|
541
|
+
lines.push(``)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (data.apiDiscovery) {
|
|
545
|
+
const d = data.apiDiscovery
|
|
546
|
+
lines.push(`### API Discovery`)
|
|
547
|
+
lines.push(`- Total Resources: ${d.totalResources} | CRDs: ${d.crdCount}`)
|
|
548
|
+
lines.push(``)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (data.runtime) {
|
|
552
|
+
const rt = data.runtime
|
|
553
|
+
lines.push(`### Runtime`)
|
|
554
|
+
lines.push(`- Heap: ${rt.heapMB.toFixed(1)} MB | Objects: ${rt.heapObjectsK.toFixed(1)}K | Goroutines: ${rt.goroutines} | CPUs: ${rt.numCPU}`)
|
|
555
|
+
if (data.sse) lines.push(`- SSE Clients: ${data.sse.connectedClients}`)
|
|
556
|
+
lines.push(``)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (data.config) {
|
|
560
|
+
const cfg = data.config
|
|
561
|
+
lines.push(`### Config`)
|
|
562
|
+
lines.push(`- Port: ${cfg.port} | Dev: ${cfg.devMode} | Timeline: \`${cfg.timelineStorage}\` | History: ${cfg.historyLimit} | MCP: ${cfg.mcpEnabled} | Prometheus URL: ${cfg.hasPrometheusURL ? 'manual' : 'auto'}`)
|
|
563
|
+
lines.push(``)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (data.errors && data.errors.length > 0) {
|
|
567
|
+
lines.push(`### Collection Errors`)
|
|
568
|
+
for (const e of data.errors) {
|
|
569
|
+
lines.push(`- ${e}`)
|
|
570
|
+
}
|
|
571
|
+
lines.push(``)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (data.recentErrors && data.recentErrors.length > 0) {
|
|
575
|
+
lines.push(`### Recent Errors (${data.recentErrors.length}${data.totalErrorsRecorded && data.totalErrorsRecorded > data.recentErrors.length ? ` of ${data.totalErrorsRecorded} total` : ''})`)
|
|
576
|
+
for (const e of data.recentErrors.slice(-10).reverse()) {
|
|
577
|
+
lines.push(`- **[${e.source}]** ${e.message} _(${new Date(e.time).toLocaleTimeString()})_`)
|
|
578
|
+
}
|
|
579
|
+
lines.push(``)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (includeRawJson) {
|
|
583
|
+
lines.push(`<details><summary>Raw JSON</summary>`)
|
|
584
|
+
lines.push(``)
|
|
585
|
+
lines.push('```json')
|
|
586
|
+
lines.push(JSON.stringify(data, null, 2))
|
|
587
|
+
lines.push('```')
|
|
588
|
+
lines.push(`</details>`)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return lines.join('\n')
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function formatForBugReport(data: DiagnosticsSnapshot): string {
|
|
595
|
+
const diagnostics = formatForGitHub(data, false)
|
|
596
|
+
|
|
597
|
+
const lines: string[] = []
|
|
598
|
+
lines.push(`## Describe the bug`)
|
|
599
|
+
lines.push(``)
|
|
600
|
+
lines.push(`<!-- A clear and concise description of what the bug is. -->`)
|
|
601
|
+
lines.push(``)
|
|
602
|
+
lines.push(`## To reproduce`)
|
|
603
|
+
lines.push(``)
|
|
604
|
+
lines.push(`<!-- Steps to reproduce the behavior -->`)
|
|
605
|
+
lines.push(``)
|
|
606
|
+
lines.push(`## Expected behavior`)
|
|
607
|
+
lines.push(``)
|
|
608
|
+
lines.push(`<!-- What you expected to happen -->`)
|
|
609
|
+
lines.push(``)
|
|
610
|
+
lines.push(`## Diagnostics`)
|
|
611
|
+
lines.push(``)
|
|
612
|
+
lines.push(`<details><summary>Diagnostics snapshot</summary>`)
|
|
613
|
+
lines.push(``)
|
|
614
|
+
lines.push(diagnostics)
|
|
615
|
+
lines.push(``)
|
|
616
|
+
lines.push(`</details>`)
|
|
617
|
+
|
|
618
|
+
return lines.join('\n')
|
|
619
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Component, ReactNode } from 'react'
|
|
2
|
+
import { AlertTriangle, RefreshCw } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface State {
|
|
9
|
+
hasError: boolean
|
|
10
|
+
error: Error | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
14
|
+
state: State = { hasError: false, error: null }
|
|
15
|
+
|
|
16
|
+
static getDerivedStateFromError(error: Error): State {
|
|
17
|
+
return { hasError: true, error }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
componentDidCatch(error: Error, info: React.ErrorInfo) {
|
|
21
|
+
console.error('[ErrorBoundary]', error, info.componentStack)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
handleReset = () => this.setState({ hasError: false, error: null })
|
|
25
|
+
|
|
26
|
+
render() {
|
|
27
|
+
if (!this.state.hasError) return this.props.children
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex flex-col items-center justify-center h-full p-8 text-center">
|
|
31
|
+
<AlertTriangle className="w-12 h-12 text-red-400 mb-4" />
|
|
32
|
+
<h2 className="text-lg font-semibold text-theme-text-primary mb-2">Something went wrong</h2>
|
|
33
|
+
<p className="text-sm text-theme-text-secondary mb-4 max-w-md">
|
|
34
|
+
{this.state.error?.message || 'An unexpected error occurred'}
|
|
35
|
+
</p>
|
|
36
|
+
<button
|
|
37
|
+
onClick={this.handleReset}
|
|
38
|
+
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg"
|
|
39
|
+
>
|
|
40
|
+
<RefreshCw className="w-4 h-4" />
|
|
41
|
+
Try Again
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ForceDeleteConfirmDialog } from '@skyhook-io/k8s-ui'
|