@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,116 @@
|
|
|
1
|
+
// Runtime configuration for Radar's frontend.
|
|
2
|
+
//
|
|
3
|
+
// When Radar runs as its own binary (standalone or in-cluster), the
|
|
4
|
+
// frontend is same-origin with the Go API server and `apiBase` is `/api`.
|
|
5
|
+
// When Radar's web is embedded as `@skyhook-io/radar-app` in another
|
|
6
|
+
// frontend (e.g. Radar Hub), the host app calls `setApiBase()` to point
|
|
7
|
+
// at a cluster-scoped proxy URL like `/c/{cluster_id}/api` before mounting.
|
|
8
|
+
//
|
|
9
|
+
// This module is deliberately a small mutable singleton rather than a
|
|
10
|
+
// React context so it works in non-React code paths (EventSource,
|
|
11
|
+
// WebSocket URL construction) without threading props through every
|
|
12
|
+
// call site. Multi-instance hosting (mounting two Radar apps for two
|
|
13
|
+
// clusters on the same page) is a V2 concern — it would require
|
|
14
|
+
// replacing this with a context.
|
|
15
|
+
|
|
16
|
+
let apiBase = '/api';
|
|
17
|
+
let basename = '';
|
|
18
|
+
let authHeadersProvider: () => Record<string, string> = () => ({});
|
|
19
|
+
let credentialsMode: RequestCredentials = 'same-origin';
|
|
20
|
+
|
|
21
|
+
export function getApiBase(): string {
|
|
22
|
+
return apiBase;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sets the base URL used for REST API, SSE, and WebSocket requests.
|
|
27
|
+
* Call before mounting the Radar app. Trailing slashes are stripped.
|
|
28
|
+
*
|
|
29
|
+
* Accepts either:
|
|
30
|
+
* - A relative path: `/api`, `/c/abc/api` (URLs derive scheme + host from window.location)
|
|
31
|
+
* - An absolute URL: `https://api.radar.skyhook.io/c/abc/api` (URLs use that origin)
|
|
32
|
+
*/
|
|
33
|
+
export function setApiBase(url: string): void {
|
|
34
|
+
apiBase = url.replace(/\/+$/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns the router basename (e.g. `/c/abc` when mounted in Radar Hub),
|
|
39
|
+
* or `''` for standalone use.
|
|
40
|
+
*/
|
|
41
|
+
export function getBasename(): string {
|
|
42
|
+
return basename;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sets the router basename. React Router strips this from location.pathname
|
|
47
|
+
* inside the app, so all internal paths remain relative to `/`. Used by
|
|
48
|
+
* full-page navigation helpers (auth redirects, etc.) where we still need
|
|
49
|
+
* to construct a URL relative to the host app's origin.
|
|
50
|
+
*/
|
|
51
|
+
export function setBasename(value: string): void {
|
|
52
|
+
basename = value.replace(/\/+$/, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds an absolute path under the configured basename — used for full
|
|
57
|
+
* window.location navigation (OIDC login redirect, return-path tracking)
|
|
58
|
+
* where React Router doesn't apply.
|
|
59
|
+
*/
|
|
60
|
+
export function routePath(path: string): string {
|
|
61
|
+
const prefix = basename;
|
|
62
|
+
if (!prefix) return path;
|
|
63
|
+
if (path.startsWith(prefix + '/') || path === prefix) return path;
|
|
64
|
+
return prefix + path;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds a WebSocket URL for a given API path.
|
|
69
|
+
*
|
|
70
|
+
* When apiBase is relative, uses window.location.host + apiBase + path.
|
|
71
|
+
* When apiBase is absolute, swaps http(s) → ws(s) and appends path.
|
|
72
|
+
*/
|
|
73
|
+
export function getWsUrl(path: string): string {
|
|
74
|
+
const base = apiBase;
|
|
75
|
+
if (base.startsWith('http://') || base.startsWith('https://')) {
|
|
76
|
+
return base.replace(/^http/, 'ws') + path;
|
|
77
|
+
}
|
|
78
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
79
|
+
return `${protocol}//${window.location.host}${base}${path}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Convenience: `${apiBase}${path}` for fetch call sites. */
|
|
83
|
+
export function apiUrl(path: string): string {
|
|
84
|
+
return getApiBase() + path;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Register a function that returns HTTP headers to attach to every Radar
|
|
89
|
+
* API request. Used by library consumers (e.g. Radar Hub) to inject
|
|
90
|
+
* Authorization bearer tokens or session tokens. The provider is called
|
|
91
|
+
* on each request so it can read fresh state.
|
|
92
|
+
*
|
|
93
|
+
* Standalone Radar leaves this empty — auth is cookie-based in OIDC or
|
|
94
|
+
* proxy mode, no explicit headers needed.
|
|
95
|
+
*/
|
|
96
|
+
export function setAuthHeadersProvider(fn: () => Record<string, string>): void {
|
|
97
|
+
authHeadersProvider = fn;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Internal: returns the registered auth headers for fetch() calls. */
|
|
101
|
+
export function getAuthHeaders(): Record<string, string> {
|
|
102
|
+
return authHeadersProvider();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Sets the `credentials` mode for all Radar fetches. Default 'same-origin'
|
|
107
|
+
* (standalone Radar binary). Library consumers at a different origin should
|
|
108
|
+
* set 'include' so browser cookies + CORS preflight cooperate.
|
|
109
|
+
*/
|
|
110
|
+
export function setCredentialsMode(mode: RequestCredentials): void {
|
|
111
|
+
credentialsMode = mode;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getCredentialsMode(): RequestCredentials {
|
|
115
|
+
return credentialsMode;
|
|
116
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type { TrafficSourcesResponse, TrafficFlowsResponse } from '../types'
|
|
3
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from './config'
|
|
4
|
+
|
|
5
|
+
async function fetchJSON<T>(path: string): Promise<T> {
|
|
6
|
+
const response = await fetch(apiUrl(path), {
|
|
7
|
+
credentials: getCredentialsMode(),
|
|
8
|
+
headers: getAuthHeaders(),
|
|
9
|
+
})
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
12
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
13
|
+
}
|
|
14
|
+
return response.json()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Connection info returned by connect endpoint
|
|
18
|
+
export interface TrafficConnectionInfo {
|
|
19
|
+
connected: boolean
|
|
20
|
+
localPort?: number
|
|
21
|
+
address?: string
|
|
22
|
+
namespace?: string
|
|
23
|
+
serviceName?: string
|
|
24
|
+
contextName?: string
|
|
25
|
+
error?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Get available traffic sources and recommendations
|
|
29
|
+
export function useTrafficSources() {
|
|
30
|
+
return useQuery<TrafficSourcesResponse>({
|
|
31
|
+
queryKey: ['traffic-sources'],
|
|
32
|
+
queryFn: () => fetchJSON('/traffic/sources'),
|
|
33
|
+
staleTime: 30000, // 30 seconds
|
|
34
|
+
retry: 1,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get traffic flows
|
|
39
|
+
export interface UseTrafficFlowsOptions {
|
|
40
|
+
namespaces?: string[]
|
|
41
|
+
since?: string // Duration like "5m", "1h"
|
|
42
|
+
enabled?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function useTrafficFlows(options: UseTrafficFlowsOptions = {}) {
|
|
46
|
+
const { namespaces = [], since, enabled = true } = options
|
|
47
|
+
|
|
48
|
+
const params = new URLSearchParams()
|
|
49
|
+
// Traffic backend only supports single namespace, use first if provided
|
|
50
|
+
if (namespaces.length === 1) params.set('namespace', namespaces[0])
|
|
51
|
+
if (since) params.set('since', since)
|
|
52
|
+
const queryString = params.toString()
|
|
53
|
+
|
|
54
|
+
return useQuery<TrafficFlowsResponse>({
|
|
55
|
+
queryKey: ['traffic-flows', namespaces, since],
|
|
56
|
+
queryFn: () => fetchJSON(`/traffic/flows${queryString ? `?${queryString}` : ''}`),
|
|
57
|
+
staleTime: 5000, // 5 seconds
|
|
58
|
+
enabled,
|
|
59
|
+
retry: 1,
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get active traffic source
|
|
64
|
+
export function useActiveTrafficSource() {
|
|
65
|
+
return useQuery<{ active: string }>({
|
|
66
|
+
queryKey: ['traffic-source-active'],
|
|
67
|
+
queryFn: () => fetchJSON('/traffic/source'),
|
|
68
|
+
staleTime: 60000, // 1 minute
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Set active traffic source
|
|
73
|
+
export function useSetTrafficSource() {
|
|
74
|
+
const queryClient = useQueryClient()
|
|
75
|
+
|
|
76
|
+
return useMutation({
|
|
77
|
+
mutationFn: async (source: string) => {
|
|
78
|
+
const response = await fetch(apiUrl('/traffic/source'), {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
credentials: getCredentialsMode(),
|
|
81
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
82
|
+
body: JSON.stringify({ source }),
|
|
83
|
+
})
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
86
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
87
|
+
}
|
|
88
|
+
return response.json()
|
|
89
|
+
},
|
|
90
|
+
meta: {
|
|
91
|
+
errorMessage: 'Failed to change traffic source',
|
|
92
|
+
successMessage: 'Traffic source changed',
|
|
93
|
+
},
|
|
94
|
+
onSuccess: () => {
|
|
95
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-source-active'] })
|
|
96
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-flows'] })
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Refetch traffic sources (for polling during wizard)
|
|
102
|
+
export function useRefetchTrafficSources() {
|
|
103
|
+
const queryClient = useQueryClient()
|
|
104
|
+
return () => queryClient.invalidateQueries({ queryKey: ['traffic-sources'] })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get traffic connection status
|
|
108
|
+
export function useTrafficConnectionStatus() {
|
|
109
|
+
return useQuery<TrafficConnectionInfo>({
|
|
110
|
+
queryKey: ['traffic-connection'],
|
|
111
|
+
queryFn: () => fetchJSON('/traffic/connection'),
|
|
112
|
+
staleTime: 5000, // 5 seconds
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Connect to traffic source (starts port-forward if needed)
|
|
117
|
+
export function useTrafficConnect() {
|
|
118
|
+
const queryClient = useQueryClient()
|
|
119
|
+
|
|
120
|
+
return useMutation<TrafficConnectionInfo, Error>({
|
|
121
|
+
mutationFn: async () => {
|
|
122
|
+
const response = await fetch(apiUrl('/traffic/connect'), {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
credentials: getCredentialsMode(),
|
|
125
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
126
|
+
})
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
129
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
130
|
+
}
|
|
131
|
+
return response.json()
|
|
132
|
+
},
|
|
133
|
+
onSuccess: () => {
|
|
134
|
+
// Invalidate flows to refetch with new connection
|
|
135
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-flows'] })
|
|
136
|
+
queryClient.invalidateQueries({ queryKey: ['traffic-connection'] })
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { XCircle, RefreshCw, Loader2, Copy, Check, TerminalSquare } from 'lucide-react'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
import type { ConnectionState } from '../context/ConnectionContext'
|
|
4
|
+
import { ContextSwitcher } from './ContextSwitcher'
|
|
5
|
+
import { parseContextName } from '../utils/context-name'
|
|
6
|
+
import { useOpenLocalTerminal } from '@skyhook-io/k8s-ui'
|
|
7
|
+
|
|
8
|
+
interface ConnectionErrorViewProps {
|
|
9
|
+
connection: ConnectionState
|
|
10
|
+
onRetry: () => void
|
|
11
|
+
isRetrying: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AuthHints {
|
|
15
|
+
title: string
|
|
16
|
+
hints: string[]
|
|
17
|
+
/** Primary auth command — usually sufficient on its own */
|
|
18
|
+
authCommand?: { label: string; command: string }
|
|
19
|
+
/** Secondary command shown as fallback if primary doesn't resolve the issue */
|
|
20
|
+
fallbackCommand?: { label: string; command: string }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getAuthHints(context: string): AuthHints {
|
|
24
|
+
const parsed = parseContextName(context)
|
|
25
|
+
|
|
26
|
+
switch (parsed.provider) {
|
|
27
|
+
case 'GKE': {
|
|
28
|
+
const result: AuthHints = {
|
|
29
|
+
title: 'GKE Authentication Failed',
|
|
30
|
+
hints: ['Your Google Cloud credentials have expired.'],
|
|
31
|
+
authCommand: { label: 'Re-authenticate with Google Cloud:', command: 'gcloud auth login' },
|
|
32
|
+
}
|
|
33
|
+
if (parsed.region && parsed.account) {
|
|
34
|
+
const isZone = /^[a-z]+-[a-z]+\d+-[a-z]$/.test(parsed.region)
|
|
35
|
+
const flag = isZone ? '--zone' : '--region'
|
|
36
|
+
result.fallbackCommand = {
|
|
37
|
+
label: 'If that doesn\'t work, refresh cluster credentials:',
|
|
38
|
+
command: `gcloud container clusters get-credentials ${parsed.clusterName} ${flag} ${parsed.region} --project ${parsed.account}`,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
}
|
|
43
|
+
case 'EKS': {
|
|
44
|
+
const result: AuthHints = {
|
|
45
|
+
title: 'EKS Authentication Failed',
|
|
46
|
+
hints: ['Your AWS credentials have expired.'],
|
|
47
|
+
authCommand: { label: 'Re-authenticate with AWS:', command: 'aws sso login' },
|
|
48
|
+
}
|
|
49
|
+
if (parsed.region) {
|
|
50
|
+
result.fallbackCommand = {
|
|
51
|
+
label: 'If that doesn\'t work, refresh cluster credentials:',
|
|
52
|
+
command: `aws eks update-kubeconfig --name ${parsed.clusterName} --region ${parsed.region}`,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result
|
|
56
|
+
}
|
|
57
|
+
case 'AKS':
|
|
58
|
+
return {
|
|
59
|
+
title: 'AKS Authentication Failed',
|
|
60
|
+
hints: ['Your Azure credentials have expired.'],
|
|
61
|
+
authCommand: { label: 'Re-authenticate with Azure:', command: 'az login' },
|
|
62
|
+
fallbackCommand: { label: 'If that doesn\'t work, refresh cluster credentials:', command: 'az aks get-credentials --name <cluster> --resource-group <rg>' },
|
|
63
|
+
}
|
|
64
|
+
default:
|
|
65
|
+
return {
|
|
66
|
+
title: 'Authentication Failed',
|
|
67
|
+
hints: [
|
|
68
|
+
'Your credentials may have expired',
|
|
69
|
+
'Re-authenticate with your cloud provider and try again',
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const errorHints: Record<string, { title: string; hints: string[] }> = {
|
|
76
|
+
config: {
|
|
77
|
+
title: 'No Kubeconfig Found',
|
|
78
|
+
hints: [
|
|
79
|
+
'Radar could not find a kubeconfig file at ~/.kube/config',
|
|
80
|
+
'If your kubeconfig is at a custom path, set the KUBECONFIG environment variable in your shell profile (~/.zshrc or ~/.bashrc)',
|
|
81
|
+
'You can also pass --kubeconfig <path> when launching from the terminal',
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
rbac: {
|
|
85
|
+
title: 'Insufficient Permissions',
|
|
86
|
+
hints: [
|
|
87
|
+
'Your user account can connect but lacks required RBAC permissions',
|
|
88
|
+
'Ask your cluster admin for a ClusterRole with list/watch access',
|
|
89
|
+
'For read-only access, the built-in "view" ClusterRole is usually sufficient',
|
|
90
|
+
'You can also try: kubectl auth can-i --list',
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
network: {
|
|
94
|
+
title: 'Network Unreachable',
|
|
95
|
+
hints: [
|
|
96
|
+
'The cluster may be unreachable from your network',
|
|
97
|
+
'Check if VPN connection is required',
|
|
98
|
+
'Verify firewall rules allow access',
|
|
99
|
+
'Confirm the cluster is running',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
timeout: {
|
|
103
|
+
title: 'Connection Timed Out',
|
|
104
|
+
hints: [
|
|
105
|
+
'The cluster is taking too long to respond',
|
|
106
|
+
'The cluster may be under heavy load',
|
|
107
|
+
'Network latency may be too high',
|
|
108
|
+
'Try again or check cluster health',
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
unknown: {
|
|
112
|
+
title: 'Connection Failed',
|
|
113
|
+
hints: [
|
|
114
|
+
'Check your kubeconfig is valid',
|
|
115
|
+
'Verify the cluster endpoint is correct',
|
|
116
|
+
'Try switching to a different context',
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function CopyableCommand({ command, onRunInTerminal }: { command: string; onRunInTerminal?: (command: string) => void }) {
|
|
122
|
+
const [copied, setCopied] = useState(false)
|
|
123
|
+
|
|
124
|
+
const handleCopy = () => {
|
|
125
|
+
navigator.clipboard.writeText(command).then(() => {
|
|
126
|
+
setCopied(true)
|
|
127
|
+
setTimeout(() => setCopied(false), 2000)
|
|
128
|
+
}).catch(() => {
|
|
129
|
+
// Clipboard API may be unavailable (e.g., non-HTTPS context)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<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-all">
|
|
136
|
+
{command}
|
|
137
|
+
</code>
|
|
138
|
+
{onRunInTerminal && (
|
|
139
|
+
<button
|
|
140
|
+
onClick={() => onRunInTerminal(command)}
|
|
141
|
+
className="shrink-0 text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
142
|
+
title="Run in terminal"
|
|
143
|
+
>
|
|
144
|
+
<TerminalSquare className="w-3.5 h-3.5" />
|
|
145
|
+
</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>
|
|
158
|
+
</div>
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function ConnectionErrorView({ connection, onRetry, isRetrying }: ConnectionErrorViewProps) {
|
|
163
|
+
// For auth errors, generate context-aware hints with a specific re-auth command
|
|
164
|
+
const isAuth = connection.errorType === 'auth'
|
|
165
|
+
const authInfo = isAuth ? getAuthHints(connection.context || '') : null
|
|
166
|
+
const errorInfo = authInfo || errorHints[connection.errorType || 'unknown'] || errorHints.unknown
|
|
167
|
+
const openLocalTerminal = useOpenLocalTerminal()
|
|
168
|
+
|
|
169
|
+
// Build a command that auto-retries connection after successful auth
|
|
170
|
+
const retryCmd = `curl -s -X POST http://${window.location.host}/api/connection/retry > /dev/null`
|
|
171
|
+
|
|
172
|
+
const handleAuthInTerminal = () => {
|
|
173
|
+
if (!authInfo?.authCommand) return
|
|
174
|
+
openLocalTerminal({
|
|
175
|
+
initialCommand: `${authInfo.authCommand.command} && ${retryCmd}`,
|
|
176
|
+
title: 'Auth',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const handleRunInTerminal = (command: string) => {
|
|
181
|
+
openLocalTerminal({ initialCommand: command, title: 'Auth' })
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="flex-1 flex items-start justify-center pt-16 px-8">
|
|
186
|
+
<div className="max-w-lg w-full">
|
|
187
|
+
<div className="flex flex-col items-center text-center">
|
|
188
|
+
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center mb-6">
|
|
189
|
+
<XCircle className="w-10 h-10 text-red-400" />
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<h2 className="text-xl font-semibold text-theme-text-primary mb-2">
|
|
193
|
+
{connection.errorType === 'config' ? 'No Cluster Configuration' : 'Cannot Connect to Cluster'}
|
|
194
|
+
</h2>
|
|
195
|
+
|
|
196
|
+
<p className="text-sm text-theme-text-secondary mb-1">
|
|
197
|
+
Context: <span className="font-mono text-theme-text-primary">{connection.context || '(none)'}</span>
|
|
198
|
+
</p>
|
|
199
|
+
|
|
200
|
+
{connection.clusterName && (
|
|
201
|
+
<p className="text-sm text-theme-text-secondary mb-4">
|
|
202
|
+
Cluster: <span className="font-mono text-theme-text-primary">{connection.clusterName}</span>
|
|
203
|
+
</p>
|
|
204
|
+
)}
|
|
205
|
+
|
|
206
|
+
<div className="w-full bg-theme-surface border border-theme-border rounded-lg p-4 mb-6 text-left">
|
|
207
|
+
<h3 className="text-sm font-medium text-theme-text-primary mb-2">
|
|
208
|
+
{errorInfo.title}
|
|
209
|
+
</h3>
|
|
210
|
+
<ul className="text-sm text-theme-text-secondary space-y-1">
|
|
211
|
+
{errorInfo.hints.map((hint, i) => (
|
|
212
|
+
<li key={i} className="flex items-start gap-2">
|
|
213
|
+
<span className="text-theme-text-tertiary mt-0.5">-</span>
|
|
214
|
+
<span>{hint}</span>
|
|
215
|
+
</li>
|
|
216
|
+
))}
|
|
217
|
+
</ul>
|
|
218
|
+
{authInfo?.authCommand && (
|
|
219
|
+
<div className="mt-3">
|
|
220
|
+
<p className="text-xs text-theme-text-tertiary">{authInfo.authCommand.label}</p>
|
|
221
|
+
<CopyableCommand command={authInfo.authCommand.command} onRunInTerminal={handleRunInTerminal} />
|
|
222
|
+
<button
|
|
223
|
+
onClick={handleAuthInTerminal}
|
|
224
|
+
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"
|
|
225
|
+
>
|
|
226
|
+
<TerminalSquare className="w-3.5 h-3.5" />
|
|
227
|
+
Authenticate in terminal
|
|
228
|
+
</button>
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
{authInfo?.fallbackCommand && (
|
|
232
|
+
<div className="mt-4 pt-3 border-t border-theme-border/50">
|
|
233
|
+
<p className="text-xs text-theme-text-tertiary">{authInfo.fallbackCommand.label}</p>
|
|
234
|
+
<CopyableCommand command={authInfo.fallbackCommand.command} onRunInTerminal={handleRunInTerminal} />
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{connection.error && (
|
|
240
|
+
<div className="w-full bg-theme-elevated border border-theme-border rounded-lg p-3 mb-6 overflow-auto max-h-32">
|
|
241
|
+
<code className="text-xs text-red-400 font-mono whitespace-pre-wrap break-all">
|
|
242
|
+
{connection.error}
|
|
243
|
+
</code>
|
|
244
|
+
</div>
|
|
245
|
+
)}
|
|
246
|
+
|
|
247
|
+
<div className="flex items-center gap-5">
|
|
248
|
+
<button
|
|
249
|
+
onClick={onRetry}
|
|
250
|
+
disabled={isRetrying}
|
|
251
|
+
className="inline-flex items-center gap-2 px-4 py-2 btn-brand rounded-lg"
|
|
252
|
+
>
|
|
253
|
+
{isRetrying ? (
|
|
254
|
+
<>
|
|
255
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
256
|
+
Connecting...
|
|
257
|
+
</>
|
|
258
|
+
) : (
|
|
259
|
+
<>
|
|
260
|
+
<RefreshCw className="w-4 h-4" />
|
|
261
|
+
Retry Connection
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
</button>
|
|
265
|
+
|
|
266
|
+
{connection.errorType !== 'config' && <ContextSwitcher />}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
)
|
|
272
|
+
}
|