@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,56 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ParsedContextInfo {
|
|
4
|
+
raw: string
|
|
5
|
+
provider: string | null
|
|
6
|
+
account: string | null
|
|
7
|
+
region: string | null
|
|
8
|
+
clusterName: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ContextSwitchState {
|
|
12
|
+
isSwitching: boolean
|
|
13
|
+
targetContext: ParsedContextInfo | null
|
|
14
|
+
progressMessage: string | null
|
|
15
|
+
startSwitch: (context: ParsedContextInfo) => void
|
|
16
|
+
updateProgress: (message: string) => void
|
|
17
|
+
endSwitch: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ContextSwitchContext = createContext<ContextSwitchState | null>(null)
|
|
21
|
+
|
|
22
|
+
export function ContextSwitchProvider({ children }: { children: ReactNode }) {
|
|
23
|
+
const [isSwitching, setIsSwitching] = useState(false)
|
|
24
|
+
const [targetContext, setTargetContext] = useState<ParsedContextInfo | null>(null)
|
|
25
|
+
const [progressMessage, setProgressMessage] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
const startSwitch = useCallback((context: ParsedContextInfo) => {
|
|
28
|
+
setIsSwitching(true)
|
|
29
|
+
setTargetContext(context)
|
|
30
|
+
setProgressMessage(null)
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const updateProgress = useCallback((message: string) => {
|
|
34
|
+
setProgressMessage(message)
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const endSwitch = useCallback(() => {
|
|
38
|
+
setIsSwitching(false)
|
|
39
|
+
setTargetContext(null)
|
|
40
|
+
setProgressMessage(null)
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ContextSwitchContext.Provider value={{ isSwitching, targetContext, progressMessage, startSwitch, updateProgress, endSwitch }}>
|
|
45
|
+
{children}
|
|
46
|
+
</ContextSwitchContext.Provider>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function useContextSwitch() {
|
|
51
|
+
const context = useContext(ContextSwitchContext)
|
|
52
|
+
if (!context) {
|
|
53
|
+
throw new Error('useContextSwitch must be used within ContextSwitchProvider')
|
|
54
|
+
}
|
|
55
|
+
return context
|
|
56
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Slot-based customization of Radar's top nav.
|
|
2
|
+
//
|
|
3
|
+
// Lets library consumers (e.g. Radar Hub) swap out the brand area, the
|
|
4
|
+
// context picker, and append items on the right of the action bar —
|
|
5
|
+
// without forking App.tsx or building a parallel nav.
|
|
6
|
+
//
|
|
7
|
+
// The `embedded` flag hides chrome that only makes sense for Radar's
|
|
8
|
+
// standalone OSS binary: GitHub star link, update-from-GitHub notifier,
|
|
9
|
+
// Radar's own OIDC/proxy-mode UserMenu. Consumers typically provide
|
|
10
|
+
// their own auth UI via `rightExtras`.
|
|
11
|
+
//
|
|
12
|
+
// Default (no provider): Radar renders its standalone nav unchanged.
|
|
13
|
+
import { createContext, useContext } from 'react';
|
|
14
|
+
import type { ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
interface NavCustomizationBase {
|
|
17
|
+
/** Replaces Radar's Skyhook/radar logo + wordmark. */
|
|
18
|
+
brandSlot?: ReactNode;
|
|
19
|
+
/** Replaces the ContextSwitcher (kubeconfig-context picker). */
|
|
20
|
+
contextSlot?: ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Slot-based customization of Radar's top nav.
|
|
25
|
+
*
|
|
26
|
+
* Standalone-mode consumers pass `embedded: false` (or omit it) and may
|
|
27
|
+
* optionally append items via `rightExtras`. Embedded-mode consumers must
|
|
28
|
+
* supply `rightExtras` — Radar's OSS chrome (GitHub star, update notifier,
|
|
29
|
+
* built-in UserMenu) is hidden, so the host app owns the right side of the
|
|
30
|
+
* nav and must render its own user/auth UI there.
|
|
31
|
+
*/
|
|
32
|
+
export type NavCustomization =
|
|
33
|
+
| (NavCustomizationBase & {
|
|
34
|
+
embedded?: false;
|
|
35
|
+
/** Appended to the right of the action bar (before the UserMenu). */
|
|
36
|
+
rightExtras?: ReactNode;
|
|
37
|
+
})
|
|
38
|
+
| (NavCustomizationBase & {
|
|
39
|
+
embedded: true;
|
|
40
|
+
/** Required in embedded mode: Radar's own UserMenu is hidden. */
|
|
41
|
+
rightExtras: ReactNode;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const NavCustomizationContext = createContext<NavCustomization>({});
|
|
45
|
+
|
|
46
|
+
export function NavCustomizationProvider({
|
|
47
|
+
value,
|
|
48
|
+
children,
|
|
49
|
+
}: {
|
|
50
|
+
value: NavCustomization | undefined;
|
|
51
|
+
children: ReactNode;
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<NavCustomizationContext.Provider value={value ?? {}}>
|
|
55
|
+
{children}
|
|
56
|
+
</NavCustomizationContext.Provider>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useNavCustomization(): NavCustomization {
|
|
61
|
+
return useContext(NavCustomizationContext);
|
|
62
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, ReactNode } from 'react'
|
|
2
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
3
|
+
|
|
4
|
+
type Theme = 'dark' | 'light'
|
|
5
|
+
|
|
6
|
+
interface ThemeContextType {
|
|
7
|
+
theme: Theme
|
|
8
|
+
setTheme: (theme: Theme) => void
|
|
9
|
+
toggleTheme: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
|
13
|
+
|
|
14
|
+
const THEME_STORAGE_KEY = 'radar-theme'
|
|
15
|
+
|
|
16
|
+
function getInitialTheme(): Theme {
|
|
17
|
+
// Check localStorage first
|
|
18
|
+
if (typeof window !== 'undefined') {
|
|
19
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY)
|
|
20
|
+
if (stored === 'light' || stored === 'dark') {
|
|
21
|
+
return stored
|
|
22
|
+
}
|
|
23
|
+
// Check system preference
|
|
24
|
+
if (window.matchMedia('(prefers-color-scheme: light)').matches) {
|
|
25
|
+
return 'light'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return 'dark' // Default to dark
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
32
|
+
const [theme, setThemeState] = useState<Theme>(getInitialTheme)
|
|
33
|
+
|
|
34
|
+
const setTheme = (newTheme: Theme) => {
|
|
35
|
+
setThemeState(newTheme)
|
|
36
|
+
localStorage.setItem(THEME_STORAGE_KEY, newTheme)
|
|
37
|
+
fetch(apiUrl('/settings'), {
|
|
38
|
+
method: 'PUT',
|
|
39
|
+
credentials: getCredentialsMode(),
|
|
40
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
41
|
+
body: JSON.stringify({ theme: newTheme }),
|
|
42
|
+
}).then((res) => {
|
|
43
|
+
if (!res.ok) console.warn('[settings] Failed to persist theme:', res.status)
|
|
44
|
+
}).catch((err) => console.warn('[settings] Failed to persist theme:', err))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const toggleTheme = () => {
|
|
48
|
+
setTheme(theme === 'dark' ? 'light' : 'dark')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Apply theme to document
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
54
|
+
document.documentElement.style.colorScheme = theme
|
|
55
|
+
}, [theme])
|
|
56
|
+
|
|
57
|
+
// Sync theme from server (persisted settings survive port changes in desktop app)
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
fetch(apiUrl('/settings'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
|
|
60
|
+
.then((res) => res.ok ? res.json() : null)
|
|
61
|
+
.then((data) => {
|
|
62
|
+
if (data?.theme && (data.theme === 'dark' || data.theme === 'light') && data.theme !== theme) {
|
|
63
|
+
setThemeState(data.theme)
|
|
64
|
+
localStorage.setItem(THEME_STORAGE_KEY, data.theme)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch((err) => console.warn('[settings] Failed to load theme from server:', err))
|
|
68
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
69
|
+
|
|
70
|
+
// Listen for system theme changes
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: light)')
|
|
73
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
74
|
+
// Only auto-switch if user hasn't explicitly set a preference
|
|
75
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY)
|
|
76
|
+
if (!stored) {
|
|
77
|
+
setThemeState(e.matches ? 'light' : 'dark')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
mediaQuery.addEventListener('change', handleChange)
|
|
81
|
+
return () => mediaQuery.removeEventListener('change', handleChange)
|
|
82
|
+
}, [])
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
|
86
|
+
{children}
|
|
87
|
+
</ThemeContext.Provider>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function useTheme() {
|
|
92
|
+
const context = useContext(ThemeContext)
|
|
93
|
+
if (context === undefined) {
|
|
94
|
+
throw new Error('useTheme must be used within a ThemeProvider')
|
|
95
|
+
}
|
|
96
|
+
return context
|
|
97
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, ReactNode } from 'react'
|
|
2
|
+
import { useCapabilities, useNamespaceCapabilities } from '../api/client'
|
|
3
|
+
import type { Capabilities, ResourcePermissions } from '../types'
|
|
4
|
+
|
|
5
|
+
// Default capabilities for local development (when running locally, all features work)
|
|
6
|
+
const defaultCapabilities: Capabilities = {
|
|
7
|
+
exec: true,
|
|
8
|
+
localTerminal: true,
|
|
9
|
+
logs: true,
|
|
10
|
+
portForward: true,
|
|
11
|
+
secrets: true,
|
|
12
|
+
secretsUpdate: true,
|
|
13
|
+
helmWrite: true,
|
|
14
|
+
nodeWrite: true,
|
|
15
|
+
mcpEnabled: true,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Restricted capabilities for error/failure cases (fail-closed)
|
|
19
|
+
const restrictedCapabilities: Capabilities = {
|
|
20
|
+
exec: false,
|
|
21
|
+
localTerminal: false,
|
|
22
|
+
logs: false,
|
|
23
|
+
portForward: false,
|
|
24
|
+
secrets: false,
|
|
25
|
+
secretsUpdate: false,
|
|
26
|
+
helmWrite: false,
|
|
27
|
+
nodeWrite: false,
|
|
28
|
+
mcpEnabled: false,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CapabilitiesContext = createContext<Capabilities>(defaultCapabilities)
|
|
32
|
+
|
|
33
|
+
export function CapabilitiesProvider({ children }: { children: ReactNode }) {
|
|
34
|
+
const { data: capabilities, error } = useCapabilities()
|
|
35
|
+
|
|
36
|
+
// Determine which capabilities to use:
|
|
37
|
+
// 1. If we have fetched capabilities, use them
|
|
38
|
+
// 2. If there's an error, use restricted (fail-closed)
|
|
39
|
+
// 3. If still loading, use defaults (assumes local dev where everything works)
|
|
40
|
+
let value: Capabilities
|
|
41
|
+
if (capabilities) {
|
|
42
|
+
value = capabilities
|
|
43
|
+
} else if (error) {
|
|
44
|
+
// Log error for debugging and use restricted capabilities
|
|
45
|
+
console.error('Failed to fetch capabilities, using restricted mode:', error)
|
|
46
|
+
value = restrictedCapabilities
|
|
47
|
+
} else {
|
|
48
|
+
// Still loading - use defaults for smooth UX
|
|
49
|
+
value = defaultCapabilities
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<CapabilitiesContext.Provider value={value}>
|
|
54
|
+
{children}
|
|
55
|
+
</CapabilitiesContext.Provider>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useCapabilitiesContext(): Capabilities {
|
|
60
|
+
return useContext(CapabilitiesContext)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Convenience hooks for specific capabilities
|
|
64
|
+
export function useCanExec(): boolean {
|
|
65
|
+
return useContext(CapabilitiesContext).exec
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function useCanViewLogs(): boolean {
|
|
69
|
+
return useContext(CapabilitiesContext).logs
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useCanPortForward(): boolean {
|
|
73
|
+
return useContext(CapabilitiesContext).portForward
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function useCanViewSecrets(): boolean {
|
|
77
|
+
return useContext(CapabilitiesContext).secrets
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function useCanUpdateSecrets(): boolean {
|
|
81
|
+
return useContext(CapabilitiesContext).secretsUpdate
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function useCanHelmWrite(): boolean {
|
|
85
|
+
return useContext(CapabilitiesContext).helmWrite
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function useCanNodeWrite(): boolean {
|
|
89
|
+
return useContext(CapabilitiesContext).nodeWrite
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// RBAC resource permission hooks
|
|
93
|
+
export function useResourcePermissions(): ResourcePermissions | undefined {
|
|
94
|
+
return useContext(CapabilitiesContext).resources
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function useRestrictedResources(): string[] {
|
|
98
|
+
const resources = useContext(CapabilitiesContext).resources
|
|
99
|
+
return useMemo(() => {
|
|
100
|
+
if (!resources) return []
|
|
101
|
+
return Object.entries(resources)
|
|
102
|
+
.filter(([, allowed]) => !allowed)
|
|
103
|
+
.map(([kind]) => kind)
|
|
104
|
+
}, [resources])
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function useHasLimitedAccess(): boolean {
|
|
108
|
+
const resources = useContext(CapabilitiesContext).resources
|
|
109
|
+
if (!resources) return false
|
|
110
|
+
return Object.values(resources).some(allowed => !allowed)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Namespace-scoped capability hooks: lazily re-check exec/logs/portForward
|
|
114
|
+
// scoped to a specific namespace when global RBAC checks denied them.
|
|
115
|
+
// Falls back to global capability values while the namespace check is loading
|
|
116
|
+
// or when all capabilities are already granted.
|
|
117
|
+
export function useNamespacedCapabilities(namespace: string | undefined) {
|
|
118
|
+
const globalCaps = useContext(CapabilitiesContext)
|
|
119
|
+
const { data: nsCaps, error } = useNamespaceCapabilities(namespace, globalCaps)
|
|
120
|
+
|
|
121
|
+
if (error) {
|
|
122
|
+
console.warn(`Failed to fetch namespace capabilities for ${namespace}, using global:`, error)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return useMemo(() => ({
|
|
126
|
+
canExec: nsCaps?.exec ?? globalCaps.exec,
|
|
127
|
+
canViewLogs: nsCaps?.logs ?? globalCaps.logs,
|
|
128
|
+
canPortForward: nsCaps?.portForward ?? globalCaps.portForward,
|
|
129
|
+
}), [globalCaps.exec, globalCaps.logs, globalCaps.portForward, nsCaps])
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useAnimatedUnmount } from '@skyhook-io/k8s-ui/hooks/useAnimatedUnmount'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createElement, useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import { FolderOpen } from 'lucide-react'
|
|
3
|
+
import { useToast } from '../components/ui/Toast'
|
|
4
|
+
import { isDesktopApp, desktopSaveFile } from '../utils/desktop-download'
|
|
5
|
+
import { openFile, openFolder } from '../utils/desktop-open-folder'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns a download override function when running in the desktop app,
|
|
9
|
+
* or undefined when running in a browser (so the default blob URL approach is used).
|
|
10
|
+
* The returned function shows toast notifications for success/failure and silently handles user cancellation.
|
|
11
|
+
*/
|
|
12
|
+
export function useDesktopDownload(): ((content: string, mime: string, filename: string) => void) | undefined {
|
|
13
|
+
const [isDesktop, setIsDesktop] = useState(false)
|
|
14
|
+
const { showSuccess, showError } = useToast()
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
isDesktopApp().then(setIsDesktop)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
const download = useCallback((content: string, _mime: string, filename: string) => {
|
|
21
|
+
desktopSaveFile(content, filename)
|
|
22
|
+
.then((path) => showSuccess(
|
|
23
|
+
'File saved',
|
|
24
|
+
path,
|
|
25
|
+
{
|
|
26
|
+
label: 'Show in Finder',
|
|
27
|
+
icon: createElement(FolderOpen, { className: 'w-3.5 h-3.5' }),
|
|
28
|
+
onClick: () => openFolder(path),
|
|
29
|
+
},
|
|
30
|
+
() => openFile(path),
|
|
31
|
+
))
|
|
32
|
+
.catch((err: Error) => {
|
|
33
|
+
if (err.message !== 'cancelled') {
|
|
34
|
+
showError('Save failed', err.message)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}, [showSuccess, showError])
|
|
38
|
+
|
|
39
|
+
if (!isDesktop) return undefined
|
|
40
|
+
return download
|
|
41
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
+
import type { Topology, K8sEvent, ViewMode } from '../types'
|
|
3
|
+
import type { ConnectionState } from '../context/ConnectionContext'
|
|
4
|
+
import { getApiBase, getCredentialsMode } from '../api/config'
|
|
5
|
+
|
|
6
|
+
interface UseEventSourceReturn {
|
|
7
|
+
topology: Topology | null
|
|
8
|
+
events: K8sEvent[]
|
|
9
|
+
connected: boolean
|
|
10
|
+
reconnect: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface UseEventSourceOptions {
|
|
14
|
+
onContextSwitchComplete?: () => void
|
|
15
|
+
onContextSwitchProgress?: (message: string) => void
|
|
16
|
+
onContextChanged?: (context: string) => void
|
|
17
|
+
onConnectionStateChange?: (status: ConnectionState) => void
|
|
18
|
+
onDeferredReady?: () => void
|
|
19
|
+
onK8sEvent?: (event: K8sEvent) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MAX_EVENTS = 100 // Keep last 100 events
|
|
23
|
+
|
|
24
|
+
// Dynamic throttle based on cluster size - fast for small, protective for large
|
|
25
|
+
function getTopologyThrottleMs(nodeCount: number): number {
|
|
26
|
+
if (nodeCount < 100) return 500 // Small clusters: 0.5s
|
|
27
|
+
if (nodeCount < 300) return 1000 // Medium clusters: 1s
|
|
28
|
+
if (nodeCount < 500) return 2000 // Large clusters: 2s
|
|
29
|
+
return 3000 // Very large clusters: 3s
|
|
30
|
+
}
|
|
31
|
+
const INITIAL_RECONNECT_DELAY_MS = 3000
|
|
32
|
+
const MAX_RECONNECT_DELAY_MS = 30000 // Cap at 30 seconds
|
|
33
|
+
|
|
34
|
+
export function useEventSource(
|
|
35
|
+
namespaces: string[],
|
|
36
|
+
viewMode: ViewMode = 'resources',
|
|
37
|
+
options?: UseEventSourceOptions,
|
|
38
|
+
/** When set, SSE reconnects with this namespace filter (for large clusters that require server-side filtering) */
|
|
39
|
+
forceNamespaceFilter?: string[],
|
|
40
|
+
/** When true, evaluates NetworkPolicies and annotates edges with allow/block/unprotected */
|
|
41
|
+
showPolicyEffect?: boolean,
|
|
42
|
+
): UseEventSourceReturn {
|
|
43
|
+
const [topology, setTopology] = useState<Topology | null>(null)
|
|
44
|
+
const [events, setEvents] = useState<K8sEvent[]>([])
|
|
45
|
+
const [connected, setConnected] = useState(false)
|
|
46
|
+
const eventSourceRef = useRef<EventSource | null>(null)
|
|
47
|
+
const reconnectTimeoutRef = useRef<number | null>(null)
|
|
48
|
+
const waitingForTopologyAfterSwitch = useRef(false)
|
|
49
|
+
const reconnectDelayRef = useRef(INITIAL_RECONNECT_DELAY_MS) // Exponential backoff
|
|
50
|
+
|
|
51
|
+
// Throttling state for topology updates
|
|
52
|
+
const lastTopologyUpdateRef = useRef<number>(0)
|
|
53
|
+
const pendingTopologyRef = useRef<Topology | null>(null)
|
|
54
|
+
const throttleTimeoutRef = useRef<number | null>(null)
|
|
55
|
+
const currentNodeCountRef = useRef<number>(0) // Track node count for dynamic throttle
|
|
56
|
+
|
|
57
|
+
// Serialize namespaces for stable dependency (used for events clearing)
|
|
58
|
+
const namespacesKey = namespaces.join(',')
|
|
59
|
+
|
|
60
|
+
// SSE namespace filter: only used for large clusters that require server-side filtering.
|
|
61
|
+
// Small/medium clusters get all-namespace data and filter on the frontend.
|
|
62
|
+
const sseNamespaceFilter = forceNamespaceFilter?.join(',') || ''
|
|
63
|
+
|
|
64
|
+
// Use ref to avoid stale closures while not triggering reconnection on callback changes
|
|
65
|
+
const optionsRef = useRef(options)
|
|
66
|
+
optionsRef.current = options
|
|
67
|
+
|
|
68
|
+
const connect = useCallback(() => {
|
|
69
|
+
// Clean up existing connection
|
|
70
|
+
if (eventSourceRef.current) {
|
|
71
|
+
eventSourceRef.current.close()
|
|
72
|
+
// Clear stale topology so consumers show loading state instead of old data
|
|
73
|
+
setTopology(null)
|
|
74
|
+
}
|
|
75
|
+
if (reconnectTimeoutRef.current) {
|
|
76
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build URL — only pass namespace filter for large clusters (forceNamespaceFilter)
|
|
80
|
+
const params = new URLSearchParams()
|
|
81
|
+
if (sseNamespaceFilter) {
|
|
82
|
+
params.set('namespaces', sseNamespaceFilter)
|
|
83
|
+
}
|
|
84
|
+
if (viewMode && viewMode !== 'resources') {
|
|
85
|
+
params.set('view', viewMode)
|
|
86
|
+
}
|
|
87
|
+
if (showPolicyEffect) {
|
|
88
|
+
params.set('policyEffect', 'true')
|
|
89
|
+
}
|
|
90
|
+
const url = `${getApiBase()}/events/stream${params.toString() ? `?${params}` : ''}`
|
|
91
|
+
|
|
92
|
+
// Mirror the fetch credentials mode: 'include' sets withCredentials so
|
|
93
|
+
// cookies flow cross-origin (embedded in Radar Hub); 'omit' turns them
|
|
94
|
+
// off (pure-bearer auth). Default same-origin standalone behaves as
|
|
95
|
+
// before.
|
|
96
|
+
const es = new EventSource(url, { withCredentials: getCredentialsMode() === 'include' })
|
|
97
|
+
eventSourceRef.current = es
|
|
98
|
+
|
|
99
|
+
es.onopen = () => {
|
|
100
|
+
console.log('SSE connected')
|
|
101
|
+
setConnected(true)
|
|
102
|
+
// Reset backoff on successful connection
|
|
103
|
+
reconnectDelayRef.current = INITIAL_RECONNECT_DELAY_MS
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
es.onerror = (error) => {
|
|
107
|
+
console.error('SSE error:', error)
|
|
108
|
+
setConnected(false)
|
|
109
|
+
es.close()
|
|
110
|
+
|
|
111
|
+
// Reconnect with exponential backoff
|
|
112
|
+
const delay = reconnectDelayRef.current
|
|
113
|
+
reconnectTimeoutRef.current = window.setTimeout(() => {
|
|
114
|
+
console.log(`SSE reconnecting after ${delay}ms...`)
|
|
115
|
+
connect()
|
|
116
|
+
}, delay)
|
|
117
|
+
// Increase delay for next attempt (exponential backoff with cap)
|
|
118
|
+
reconnectDelayRef.current = Math.min(delay * 1.5, MAX_RECONNECT_DELAY_MS)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle topology updates with dynamic throttling based on cluster size
|
|
122
|
+
es.addEventListener('topology', (event) => {
|
|
123
|
+
try {
|
|
124
|
+
const data = JSON.parse(event.data) as Topology
|
|
125
|
+
const now = Date.now()
|
|
126
|
+
const timeSinceLastUpdate = now - lastTopologyUpdateRef.current
|
|
127
|
+
|
|
128
|
+
// Update node count for dynamic throttle calculation
|
|
129
|
+
currentNodeCountRef.current = data.nodes?.length || 0
|
|
130
|
+
const throttleMs = getTopologyThrottleMs(currentNodeCountRef.current)
|
|
131
|
+
|
|
132
|
+
// If waiting for topology after context switch, update immediately
|
|
133
|
+
if (waitingForTopologyAfterSwitch.current) {
|
|
134
|
+
waitingForTopologyAfterSwitch.current = false
|
|
135
|
+
lastTopologyUpdateRef.current = now
|
|
136
|
+
setTopology(data)
|
|
137
|
+
optionsRef.current?.onContextSwitchComplete?.()
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Throttle updates: if we updated recently, queue this update
|
|
142
|
+
if (timeSinceLastUpdate < throttleMs) {
|
|
143
|
+
pendingTopologyRef.current = data
|
|
144
|
+
|
|
145
|
+
// Schedule update for when throttle period ends (if not already scheduled)
|
|
146
|
+
if (!throttleTimeoutRef.current) {
|
|
147
|
+
const delay = throttleMs - timeSinceLastUpdate
|
|
148
|
+
throttleTimeoutRef.current = window.setTimeout(() => {
|
|
149
|
+
throttleTimeoutRef.current = null
|
|
150
|
+
if (pendingTopologyRef.current) {
|
|
151
|
+
lastTopologyUpdateRef.current = Date.now()
|
|
152
|
+
currentNodeCountRef.current = pendingTopologyRef.current.nodes?.length || 0
|
|
153
|
+
setTopology(pendingTopologyRef.current)
|
|
154
|
+
pendingTopologyRef.current = null
|
|
155
|
+
}
|
|
156
|
+
}, delay)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Enough time has passed, update immediately
|
|
160
|
+
lastTopologyUpdateRef.current = now
|
|
161
|
+
pendingTopologyRef.current = null
|
|
162
|
+
setTopology(data)
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error('Failed to parse topology:', e)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// Handle K8s events
|
|
170
|
+
es.addEventListener('k8s_event', (event) => {
|
|
171
|
+
try {
|
|
172
|
+
const data = JSON.parse(event.data) as K8sEvent
|
|
173
|
+
data.timestamp = Date.now()
|
|
174
|
+
setEvents((prev) => [data, ...prev].slice(0, MAX_EVENTS))
|
|
175
|
+
optionsRef.current?.onK8sEvent?.(data)
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.error('Failed to parse event:', e)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Handle heartbeat (just log, keeps connection alive)
|
|
182
|
+
es.addEventListener('heartbeat', () => {
|
|
183
|
+
// Connection is alive
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Handle context switch progress events
|
|
187
|
+
es.addEventListener('context_switch_progress', (event) => {
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(event.data) as { message: string }
|
|
190
|
+
optionsRef.current?.onContextSwitchProgress?.(data.message)
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.error('Failed to parse context_switch_progress event:', e)
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Handle context changed event - clear state while new data loads
|
|
197
|
+
es.addEventListener('context_changed', (event) => {
|
|
198
|
+
try {
|
|
199
|
+
const data = JSON.parse(event.data) as { context: string }
|
|
200
|
+
console.log('Context changed to:', data.context)
|
|
201
|
+
// Clear topology and events - new data will come via topology event
|
|
202
|
+
setTopology(null)
|
|
203
|
+
setEvents([])
|
|
204
|
+
// Mark that we're waiting for new topology data
|
|
205
|
+
waitingForTopologyAfterSwitch.current = true
|
|
206
|
+
// Notify caller to invalidate caches (e.g., helm releases, resources)
|
|
207
|
+
optionsRef.current?.onContextChanged?.(data.context)
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.error('Failed to parse context_changed event:', e)
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Handle deferred informer sync completion — refetch dashboard data
|
|
214
|
+
es.addEventListener('deferred_ready', () => {
|
|
215
|
+
optionsRef.current?.onDeferredReady?.()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// Handle connection state events (for graceful startup)
|
|
219
|
+
es.addEventListener('connection_state', (event) => {
|
|
220
|
+
try {
|
|
221
|
+
const data = JSON.parse(event.data) as ConnectionState
|
|
222
|
+
optionsRef.current?.onConnectionStateChange?.(data)
|
|
223
|
+
} catch (e) {
|
|
224
|
+
console.error('Failed to parse connection_state event:', e)
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
}, [sseNamespaceFilter, viewMode, showPolicyEffect])
|
|
228
|
+
|
|
229
|
+
// Reconnect function for manual reconnection
|
|
230
|
+
const reconnect = useCallback(() => {
|
|
231
|
+
connect()
|
|
232
|
+
}, [connect])
|
|
233
|
+
|
|
234
|
+
// Connect on mount and when namespaces/viewMode changes
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
connect()
|
|
237
|
+
|
|
238
|
+
return () => {
|
|
239
|
+
if (eventSourceRef.current) {
|
|
240
|
+
eventSourceRef.current.close()
|
|
241
|
+
}
|
|
242
|
+
if (reconnectTimeoutRef.current) {
|
|
243
|
+
clearTimeout(reconnectTimeoutRef.current)
|
|
244
|
+
}
|
|
245
|
+
if (throttleTimeoutRef.current) {
|
|
246
|
+
clearTimeout(throttleTimeoutRef.current)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}, [connect])
|
|
250
|
+
|
|
251
|
+
// Clear events when namespaces change
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
setEvents([])
|
|
254
|
+
}, [namespacesKey])
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
topology,
|
|
258
|
+
events,
|
|
259
|
+
connected,
|
|
260
|
+
reconnect,
|
|
261
|
+
}
|
|
262
|
+
}
|