@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,532 @@
|
|
|
1
|
+
import { useMemo, useEffect, useCallback, useState } from 'react'
|
|
2
|
+
import { useQueries } from '@tanstack/react-query'
|
|
3
|
+
import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
import { Terminal } from 'lucide-react'
|
|
6
|
+
import {
|
|
7
|
+
WorkloadView as BaseWorkloadView,
|
|
8
|
+
type RendererOverrides,
|
|
9
|
+
} from '@skyhook-io/k8s-ui'
|
|
10
|
+
import type { SelectedResource, ResourceRef, ResolvedEnvFrom } from '../../types'
|
|
11
|
+
import type { NavigateToResource } from '../../utils/navigation'
|
|
12
|
+
import {
|
|
13
|
+
useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
|
|
14
|
+
useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
|
|
15
|
+
useRestartWorkload, useWorkloadRevisions, useRollbackWorkload,
|
|
16
|
+
useFluxReconcile, useFluxSyncWithSource, useFluxSuspend, useFluxResume,
|
|
17
|
+
useArgoSync, useArgoRefresh, useArgoSuspend, useArgoResume,
|
|
18
|
+
useCordonNode, useUncordonNode, useDrainNode,
|
|
19
|
+
useCascadeDeletePreview,
|
|
20
|
+
fetchJSON,
|
|
21
|
+
} from '../../api/client'
|
|
22
|
+
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
23
|
+
import { useResourceAudit } from '../../api/client'
|
|
24
|
+
import { AuditAlerts } from '@skyhook-io/k8s-ui'
|
|
25
|
+
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
26
|
+
import { LogsViewer } from '../logs/LogsViewer'
|
|
27
|
+
import { useCanUpdateSecrets, useCanNodeWrite, useNamespacedCapabilities } from '../../contexts/CapabilitiesContext'
|
|
28
|
+
import { useOpenTerminal, useOpenLogs, useOpenWorkloadLogs, useOpenNodeTerminal } from '../dock'
|
|
29
|
+
import { PortForwardButton } from '../portforward/PortForwardButton'
|
|
30
|
+
import { useToast } from '../ui/Toast'
|
|
31
|
+
import { PodRenderer } from '../resources/renderers/PodRenderer'
|
|
32
|
+
import { NodeRenderer } from '../resources/renderers/NodeRenderer'
|
|
33
|
+
import { ServiceRenderer } from '../resources/renderers/ServiceRenderer'
|
|
34
|
+
import { WorkloadRenderer } from '../resources/renderers/WorkloadRenderer'
|
|
35
|
+
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
36
|
+
import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
|
|
37
|
+
|
|
38
|
+
type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
|
|
39
|
+
|
|
40
|
+
// Stable reference — web renderer wrappers inject platform hooks internally
|
|
41
|
+
const rendererOverrides: RendererOverrides = {
|
|
42
|
+
PodRenderer, NodeRenderer, ServiceRenderer, WorkloadRenderer,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// ROUTE WRAPPER — parses kind/ns/name from URL
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
interface WorkloadViewRouteProps {
|
|
50
|
+
onNavigateToResource?: NavigateToResource
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRouteProps) {
|
|
54
|
+
const location = useLocation()
|
|
55
|
+
const navigate = useNavigate()
|
|
56
|
+
|
|
57
|
+
// Parse /workload/:kind/:ns/:name from pathname
|
|
58
|
+
const parts = location.pathname.replace(/^\//, '').split('/')
|
|
59
|
+
// parts[0] = 'workload', parts[1] = kind, parts[2] = ns, parts[3+] = name (may contain slashes)
|
|
60
|
+
const kind = parts[1] || ''
|
|
61
|
+
const namespace = parts[2] || ''
|
|
62
|
+
const name = parts.slice(3).join('/') || ''
|
|
63
|
+
|
|
64
|
+
if (!kind || !namespace || !name) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex items-center justify-center h-full text-theme-text-tertiary">
|
|
67
|
+
Invalid workload URL
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const handleBack = useCallback(() => {
|
|
73
|
+
if (window.history.length > 1) {
|
|
74
|
+
navigate(-1)
|
|
75
|
+
} else {
|
|
76
|
+
navigate('/')
|
|
77
|
+
}
|
|
78
|
+
}, [navigate])
|
|
79
|
+
|
|
80
|
+
const handleNavigate = useCallback((resource: SelectedResource) => {
|
|
81
|
+
// Navigate to another workload view
|
|
82
|
+
navigate(`/workload/${resource.kind}/${resource.namespace}/${resource.name}`)
|
|
83
|
+
}, [navigate])
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<WorkloadView
|
|
87
|
+
kind={kind}
|
|
88
|
+
namespace={namespace}
|
|
89
|
+
name={name}
|
|
90
|
+
onBack={handleBack}
|
|
91
|
+
onNavigateToResource={onNavigateToResource || handleNavigate}
|
|
92
|
+
/>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// WORKLOAD VIEW WRAPPER — injects data fetching hooks
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
interface WorkloadViewProps {
|
|
101
|
+
kind: string
|
|
102
|
+
namespace: string
|
|
103
|
+
name: string
|
|
104
|
+
onBack: () => void
|
|
105
|
+
onNavigateToResource?: NavigateToResource
|
|
106
|
+
onCollapseToDrawer?: () => void
|
|
107
|
+
expanded?: boolean
|
|
108
|
+
onClose?: () => void
|
|
109
|
+
onExpand?: () => void
|
|
110
|
+
initialTab?: 'detail' | 'yaml'
|
|
111
|
+
group?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function useActionsBarProps(kind: string, namespace: string, name: string) {
|
|
115
|
+
const { showCopied } = useToast()
|
|
116
|
+
const openTerminal = useOpenTerminal()
|
|
117
|
+
const openLogs = useOpenLogs()
|
|
118
|
+
const openWorkloadLogs = useOpenWorkloadLogs()
|
|
119
|
+
const openNodeTerminal = useOpenNodeTerminal()
|
|
120
|
+
const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
|
|
121
|
+
|
|
122
|
+
const deleteMutation = useDeleteResource()
|
|
123
|
+
const restartWorkloadMutation = useRestartWorkload()
|
|
124
|
+
const rollbackMutation = useRollbackWorkload()
|
|
125
|
+
const triggerCronJobMutation = useTriggerCronJob()
|
|
126
|
+
const suspendCronJobMutation = useSuspendCronJob()
|
|
127
|
+
const resumeCronJobMutation = useResumeCronJob()
|
|
128
|
+
|
|
129
|
+
const isRollbackKind = ['deployments', 'statefulsets', 'daemonsets'].includes(kind.toLowerCase())
|
|
130
|
+
const { data: revisionsList, isLoading: revisionsLoading, error: revisionsError } = useWorkloadRevisions(kind.toLowerCase(), namespace, name, isRollbackKind)
|
|
131
|
+
|
|
132
|
+
const fluxReconcileMutation = useFluxReconcile()
|
|
133
|
+
const fluxSyncWithSourceMutation = useFluxSyncWithSource()
|
|
134
|
+
const fluxSuspendMutation = useFluxSuspend()
|
|
135
|
+
const fluxResumeMutation = useFluxResume()
|
|
136
|
+
|
|
137
|
+
const argoSyncMutation = useArgoSync()
|
|
138
|
+
const argoRefreshMutation = useArgoRefresh()
|
|
139
|
+
const argoSuspendMutation = useArgoSuspend()
|
|
140
|
+
const argoResumeMutation = useArgoResume()
|
|
141
|
+
|
|
142
|
+
const { data: cascadePreview, isLoading: cascadeLoading } = useCascadeDeletePreview(kind, namespace, name, true)
|
|
143
|
+
|
|
144
|
+
const canNodeWrite = useCanNodeWrite()
|
|
145
|
+
const cordonMutation = useCordonNode()
|
|
146
|
+
const uncordonMutation = useUncordonNode()
|
|
147
|
+
const drainMutation = useDrainNode()
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
canExec,
|
|
151
|
+
canViewLogs,
|
|
152
|
+
canPortForward,
|
|
153
|
+
onOpenTerminal: openTerminal,
|
|
154
|
+
onOpenLogs: openLogs,
|
|
155
|
+
onOpenWorkloadLogs: openWorkloadLogs,
|
|
156
|
+
onOpenNodeTerminal: openNodeTerminal,
|
|
157
|
+
onCopyCommand: (text: string, message: string, event: React.MouseEvent) => showCopied(text, message, event),
|
|
158
|
+
renderPortForward: ({ type, namespace: ns, name: n, className }: { type: 'pod' | 'service'; namespace: string; name: string; className?: string }) => (
|
|
159
|
+
<PortForwardButton type={type} namespace={ns} name={n} className={className} />
|
|
160
|
+
),
|
|
161
|
+
onDelete: (params: any, callbacks?: any) => deleteMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
162
|
+
isDeleting: deleteMutation.isPending,
|
|
163
|
+
cascadeDependents: cascadePreview?.dependents,
|
|
164
|
+
cascadeLoading,
|
|
165
|
+
onRestart: (params: any) => restartWorkloadMutation.mutate(params),
|
|
166
|
+
isRestarting: restartWorkloadMutation.isPending,
|
|
167
|
+
revisions: revisionsList,
|
|
168
|
+
revisionsLoading,
|
|
169
|
+
revisionsError: revisionsError ?? null,
|
|
170
|
+
onRollback: (params: any, callbacks?: any) => rollbackMutation.mutate(params, { onSuccess: callbacks?.onSuccess }),
|
|
171
|
+
isRollingBack: rollbackMutation.isPending,
|
|
172
|
+
onTriggerCronJob: (params: any) => triggerCronJobMutation.mutate(params),
|
|
173
|
+
isTriggeringCronJob: triggerCronJobMutation.isPending,
|
|
174
|
+
onSuspendCronJob: (params: any) => suspendCronJobMutation.mutate(params),
|
|
175
|
+
isSuspendingCronJob: suspendCronJobMutation.isPending,
|
|
176
|
+
onResumeCronJob: (params: any) => resumeCronJobMutation.mutate(params),
|
|
177
|
+
isResumingCronJob: resumeCronJobMutation.isPending,
|
|
178
|
+
onFluxReconcile: (params: any) => fluxReconcileMutation.mutate(params),
|
|
179
|
+
isFluxReconciling: fluxReconcileMutation.isPending,
|
|
180
|
+
onFluxSyncWithSource: (params: any) => fluxSyncWithSourceMutation.mutate(params),
|
|
181
|
+
isFluxSyncing: fluxSyncWithSourceMutation.isPending,
|
|
182
|
+
onFluxSuspend: (params: any) => fluxSuspendMutation.mutate(params),
|
|
183
|
+
isFluxSuspending: fluxSuspendMutation.isPending,
|
|
184
|
+
onFluxResume: (params: any) => fluxResumeMutation.mutate(params),
|
|
185
|
+
isFluxResuming: fluxResumeMutation.isPending,
|
|
186
|
+
onArgoSync: (params: any) => argoSyncMutation.mutate(params),
|
|
187
|
+
isArgoSyncing: argoSyncMutation.isPending,
|
|
188
|
+
onArgoRefresh: (params: any) => argoRefreshMutation.mutate(params),
|
|
189
|
+
isArgoRefreshing: argoRefreshMutation.isPending,
|
|
190
|
+
onArgoSuspend: (params: any) => argoSuspendMutation.mutate(params),
|
|
191
|
+
isArgoSuspending: argoSuspendMutation.isPending,
|
|
192
|
+
onArgoResume: (params: any) => argoResumeMutation.mutate(params),
|
|
193
|
+
isArgoResuming: argoResumeMutation.isPending,
|
|
194
|
+
canNodeWrite,
|
|
195
|
+
onCordonNode: (params: any) => cordonMutation.mutate(params),
|
|
196
|
+
isCordoningNode: cordonMutation.isPending,
|
|
197
|
+
onUncordonNode: (params: any) => uncordonMutation.mutate(params),
|
|
198
|
+
isUncordoningNode: uncordonMutation.isPending,
|
|
199
|
+
onDrainNode: (params: any) => drainMutation.mutate(params),
|
|
200
|
+
isDrainingNode: drainMutation.isPending,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function WorkloadView({
|
|
205
|
+
kind: kindProp,
|
|
206
|
+
namespace,
|
|
207
|
+
name,
|
|
208
|
+
expanded = true,
|
|
209
|
+
...rest
|
|
210
|
+
}: WorkloadViewProps) {
|
|
211
|
+
const [searchParams, setSearchParams] = useSearchParams()
|
|
212
|
+
|
|
213
|
+
// Tab state from URL query param — migrate legacy tab names
|
|
214
|
+
const rawTab = searchParams.get('tab')
|
|
215
|
+
const migratedTab: TabType = rawTab === 'info' ? 'overview'
|
|
216
|
+
: rawTab === 'events' ? 'timeline'
|
|
217
|
+
: (rawTab as TabType) || 'overview'
|
|
218
|
+
|
|
219
|
+
const handleTabChange = useCallback((tab: TabType) => {
|
|
220
|
+
const params = new URLSearchParams(searchParams)
|
|
221
|
+
if (tab === 'overview') {
|
|
222
|
+
params.delete('tab')
|
|
223
|
+
} else {
|
|
224
|
+
params.set('tab', tab)
|
|
225
|
+
}
|
|
226
|
+
setSearchParams(params, { replace: true })
|
|
227
|
+
}, [searchParams, setSearchParams])
|
|
228
|
+
|
|
229
|
+
// Fetch resource with relationships
|
|
230
|
+
const { data: resourceResponse, isLoading: resourceLoading, refetch: refetchResource } = useResourceWithRelationships<any>(kindProp, namespace, name, rest.group)
|
|
231
|
+
const resource = resourceResponse?.resource
|
|
232
|
+
const relationships = resourceResponse?.relationships
|
|
233
|
+
const certificateInfo = resourceResponse?.certificateInfo
|
|
234
|
+
|
|
235
|
+
// For pods: extract envFrom ConfigMap/Secret names and resolve their keys
|
|
236
|
+
const isPod = kindProp.toLowerCase() === 'pods'
|
|
237
|
+
const { envFromConfigMapNames, envFromSecretNames } = useMemo(() => {
|
|
238
|
+
if (!isPod || !resource) return { envFromConfigMapNames: [] as string[], envFromSecretNames: [] as string[] }
|
|
239
|
+
const cmNames = new Set<string>()
|
|
240
|
+
const secretNames = new Set<string>()
|
|
241
|
+
const containers = [...(resource.spec?.containers || []), ...(resource.spec?.initContainers || [])]
|
|
242
|
+
for (const c of containers) {
|
|
243
|
+
for (const ef of (c.envFrom || [])) {
|
|
244
|
+
if (ef.configMapRef?.name) cmNames.add(ef.configMapRef.name)
|
|
245
|
+
if (ef.secretRef?.name) secretNames.add(ef.secretRef.name)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return { envFromConfigMapNames: Array.from(cmNames), envFromSecretNames: Array.from(secretNames) }
|
|
249
|
+
}, [isPod, resource])
|
|
250
|
+
|
|
251
|
+
const configMapQueries = useQueries({
|
|
252
|
+
queries: envFromConfigMapNames.map((cmName) => ({
|
|
253
|
+
queryKey: ['resources', 'configmaps', namespace, cmName],
|
|
254
|
+
queryFn: () => fetchJSON<any>(`/resources/configmaps/${namespace}/${cmName}`),
|
|
255
|
+
enabled: isPod,
|
|
256
|
+
staleTime: 30000,
|
|
257
|
+
})),
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const secretQueries = useQueries({
|
|
261
|
+
queries: envFromSecretNames.map((secretName) => ({
|
|
262
|
+
queryKey: ['resources', 'secrets', namespace, secretName],
|
|
263
|
+
queryFn: () => fetchJSON<any>(`/resources/secrets/${namespace}/${secretName}`),
|
|
264
|
+
enabled: isPod,
|
|
265
|
+
staleTime: 30000,
|
|
266
|
+
})),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
const resolvedEnvFrom = useMemo(() => {
|
|
270
|
+
if (!isPod || (envFromConfigMapNames.length === 0 && envFromSecretNames.length === 0)) return undefined
|
|
271
|
+
const result: ResolvedEnvFrom = {}
|
|
272
|
+
envFromConfigMapNames.forEach((n, i) => {
|
|
273
|
+
// Single-resource endpoint returns { resource, relationships } wrapper
|
|
274
|
+
const cm = configMapQueries[i]?.data?.resource ?? configMapQueries[i]?.data
|
|
275
|
+
if (cm) result[n] = { keys: Object.keys(cm.data || {}), values: cm.data || {}, isSecret: false }
|
|
276
|
+
})
|
|
277
|
+
envFromSecretNames.forEach((n, i) => {
|
|
278
|
+
const secret = secretQueries[i]?.data?.resource ?? secretQueries[i]?.data
|
|
279
|
+
if (secret) {
|
|
280
|
+
const decodedValues: Record<string, string> = {}
|
|
281
|
+
for (const [k, v] of Object.entries(secret.data || {})) {
|
|
282
|
+
try { decodedValues[k] = atob(v as string) } catch { decodedValues[k] = v as string }
|
|
283
|
+
}
|
|
284
|
+
result[n] = { keys: Object.keys(decodedValues), values: decodedValues, isSecret: true }
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
288
|
+
}, [isPod, envFromConfigMapNames, envFromSecretNames, configMapQueries, secretQueries])
|
|
289
|
+
|
|
290
|
+
// Fetch topology for hierarchy building (only when expanded)
|
|
291
|
+
const { data: topology } = useTopology([namespace], 'resources', { enabled: expanded })
|
|
292
|
+
|
|
293
|
+
// Fetch all events for this resource's namespace (only when expanded)
|
|
294
|
+
const { data: allEvents, isLoading: eventsLoading } = useChanges({
|
|
295
|
+
namespaces: [namespace],
|
|
296
|
+
timeRange: 'all',
|
|
297
|
+
includeK8sEvents: true,
|
|
298
|
+
includeManaged: true,
|
|
299
|
+
limit: 10000,
|
|
300
|
+
enabled: expanded,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// RBAC
|
|
304
|
+
const canUpdateSecrets = useCanUpdateSecrets()
|
|
305
|
+
const updateResource = useUpdateResource()
|
|
306
|
+
const actionsBarProps = useActionsBarProps(kindProp, namespace, name)
|
|
307
|
+
|
|
308
|
+
const handleUpdateResource = useCallback(async (params: { kind: string; namespace: string; name: string; yaml: string }) => {
|
|
309
|
+
await updateResource.mutateAsync(params)
|
|
310
|
+
}, [updateResource])
|
|
311
|
+
|
|
312
|
+
// Duplicate dialog
|
|
313
|
+
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
314
|
+
const [duplicateYaml, setDuplicateYaml] = useState('')
|
|
315
|
+
|
|
316
|
+
const handleDuplicate = useCallback((params: { kind: string; namespace: string; name: string; yaml: string }) => {
|
|
317
|
+
setDuplicateYaml(cleanYamlForDuplicate(params.yaml))
|
|
318
|
+
setDuplicateDialogOpen(true)
|
|
319
|
+
}, [])
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<>
|
|
323
|
+
<BaseWorkloadView
|
|
324
|
+
kind={kindProp}
|
|
325
|
+
namespace={namespace}
|
|
326
|
+
name={name}
|
|
327
|
+
expanded={expanded}
|
|
328
|
+
{...rest}
|
|
329
|
+
// Data
|
|
330
|
+
resource={resource}
|
|
331
|
+
relationships={relationships}
|
|
332
|
+
certificateInfo={certificateInfo}
|
|
333
|
+
isLoading={resourceLoading}
|
|
334
|
+
refetch={refetchResource}
|
|
335
|
+
// Timeline
|
|
336
|
+
allEvents={allEvents}
|
|
337
|
+
eventsLoading={eventsLoading}
|
|
338
|
+
topology={topology}
|
|
339
|
+
// Capabilities
|
|
340
|
+
canUpdateSecrets={canUpdateSecrets}
|
|
341
|
+
// Mutations
|
|
342
|
+
onUpdateResource={handleUpdateResource}
|
|
343
|
+
isUpdatingResource={updateResource.isPending}
|
|
344
|
+
updateResourceError={updateResource.error?.message ?? null}
|
|
345
|
+
// Tab state (URL-synced)
|
|
346
|
+
activeTab={migratedTab}
|
|
347
|
+
onTabChange={handleTabChange}
|
|
348
|
+
// Render props
|
|
349
|
+
renderLogsTab={(props) => <LogsTabContent {...props} />}
|
|
350
|
+
renderMetricsTab={({ kind, namespace: ns, name: n }) => (
|
|
351
|
+
<PrometheusCharts kind={kind} namespace={ns} name={n} showEmptyState />
|
|
352
|
+
)}
|
|
353
|
+
isMetricsAvailable={(kind, res) =>
|
|
354
|
+
isPrometheusSupported(kind) && !(kind === 'Pod' && res?.status?.phase === 'Pending')
|
|
355
|
+
}
|
|
356
|
+
onDuplicate={handleDuplicate}
|
|
357
|
+
actionsBarProps={actionsBarProps}
|
|
358
|
+
rendererOverrides={rendererOverrides}
|
|
359
|
+
resolvedEnvFrom={resolvedEnvFrom}
|
|
360
|
+
renderOverviewExtra={({ kind: k, namespace: ns, name: n }) => (
|
|
361
|
+
<AuditSection kind={k} namespace={ns} name={n} />
|
|
362
|
+
)}
|
|
363
|
+
/>
|
|
364
|
+
<CreateResourceDialog
|
|
365
|
+
open={duplicateDialogOpen}
|
|
366
|
+
onClose={() => setDuplicateDialogOpen(false)}
|
|
367
|
+
initialYaml={duplicateYaml}
|
|
368
|
+
title="Duplicate Resource"
|
|
369
|
+
onCreated={(result) => {
|
|
370
|
+
rest.onNavigateToResource?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
|
|
371
|
+
}}
|
|
372
|
+
/>
|
|
373
|
+
</>
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ============================================================================
|
|
378
|
+
// LOGS TAB — platform-specific (uses data-fetching hooks)
|
|
379
|
+
// ============================================================================
|
|
380
|
+
|
|
381
|
+
const WORKLOAD_LOG_KINDS = new Set(['Deployment', 'StatefulSet', 'DaemonSet'])
|
|
382
|
+
|
|
383
|
+
function LogsTabContent({
|
|
384
|
+
kind,
|
|
385
|
+
apiKind,
|
|
386
|
+
namespace,
|
|
387
|
+
name,
|
|
388
|
+
resource,
|
|
389
|
+
pods,
|
|
390
|
+
selectedPod,
|
|
391
|
+
onSelectPod,
|
|
392
|
+
initialContainer,
|
|
393
|
+
onConsumeInitialContainer,
|
|
394
|
+
}: {
|
|
395
|
+
kind: string
|
|
396
|
+
apiKind: string
|
|
397
|
+
namespace: string
|
|
398
|
+
name: string
|
|
399
|
+
resource: any
|
|
400
|
+
pods: ResourceRef[]
|
|
401
|
+
selectedPod: string | null
|
|
402
|
+
onSelectPod: (name: string | null) => void
|
|
403
|
+
initialContainer: string | null
|
|
404
|
+
onConsumeInitialContainer: () => void
|
|
405
|
+
}) {
|
|
406
|
+
// Workload kinds (Deployment, StatefulSet, DaemonSet) use the aggregated workload logs viewer
|
|
407
|
+
if (WORKLOAD_LOG_KINDS.has(kind)) {
|
|
408
|
+
return (
|
|
409
|
+
<div className="h-full">
|
|
410
|
+
<WorkloadLogsViewer kind={apiKind} namespace={namespace} name={name} />
|
|
411
|
+
</div>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Individual Pod — use LogsViewer with container list from resource data
|
|
416
|
+
if (kind === 'Pod') {
|
|
417
|
+
return <PodLogsTab namespace={namespace} name={name} resource={resource} initialContainer={initialContainer} onConsumeInitialContainer={onConsumeInitialContainer} />
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Other kinds with associated pods (Jobs, CronJobs, ReplicaSets, etc.) — pod selector + LogsViewer
|
|
421
|
+
return (
|
|
422
|
+
<MultiPodLogsTab
|
|
423
|
+
pods={pods}
|
|
424
|
+
namespace={namespace}
|
|
425
|
+
selectedPod={selectedPod}
|
|
426
|
+
onSelectPod={onSelectPod}
|
|
427
|
+
initialContainer={initialContainer}
|
|
428
|
+
/>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function PodLogsTab({ namespace, name, resource, initialContainer, onConsumeInitialContainer }: {
|
|
433
|
+
namespace: string
|
|
434
|
+
name: string
|
|
435
|
+
resource: any
|
|
436
|
+
initialContainer?: string | null
|
|
437
|
+
onConsumeInitialContainer?: () => void
|
|
438
|
+
}) {
|
|
439
|
+
const containers = useMemo(() => {
|
|
440
|
+
const names: string[] = []
|
|
441
|
+
for (const c of resource?.spec?.initContainers || []) if (c.name) names.push(c.name)
|
|
442
|
+
for (const c of resource?.spec?.containers || []) if (c.name) names.push(c.name)
|
|
443
|
+
return names
|
|
444
|
+
}, [resource])
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (initialContainer && containers.includes(initialContainer)) {
|
|
448
|
+
onConsumeInitialContainer?.()
|
|
449
|
+
}
|
|
450
|
+
}, [initialContainer, containers, onConsumeInitialContainer])
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<div className="h-full">
|
|
454
|
+
<LogsViewer
|
|
455
|
+
namespace={namespace}
|
|
456
|
+
podName={name}
|
|
457
|
+
containers={containers}
|
|
458
|
+
initialContainer={initialContainer || undefined}
|
|
459
|
+
/>
|
|
460
|
+
</div>
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function MultiPodLogsTab({ pods, namespace, selectedPod, onSelectPod, initialContainer }: {
|
|
465
|
+
pods: ResourceRef[]
|
|
466
|
+
namespace: string
|
|
467
|
+
selectedPod: string | null
|
|
468
|
+
onSelectPod: (name: string | null) => void
|
|
469
|
+
initialContainer?: string | null
|
|
470
|
+
}) {
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
if (pods.length > 0 && !selectedPod) {
|
|
473
|
+
onSelectPod(pods[0].name)
|
|
474
|
+
}
|
|
475
|
+
}, [pods, selectedPod, onSelectPod])
|
|
476
|
+
|
|
477
|
+
const podNamespace = pods.find(p => p.name === selectedPod)?.namespace || namespace
|
|
478
|
+
|
|
479
|
+
// Fetch container list for the selected pod
|
|
480
|
+
const { data: logsData } = usePodLogs(podNamespace, selectedPod || '', { tailLines: 1 })
|
|
481
|
+
const containers = logsData?.containers || []
|
|
482
|
+
|
|
483
|
+
if (pods.length === 0) {
|
|
484
|
+
return (
|
|
485
|
+
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
|
|
486
|
+
<Terminal className="w-12 h-12 mb-4 opacity-50" />
|
|
487
|
+
<p>No pods available</p>
|
|
488
|
+
</div>
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<div className="h-full flex flex-col">
|
|
494
|
+
{pods.length > 1 && (
|
|
495
|
+
<div className="shrink-0 border-b border-theme-border bg-theme-surface/50 px-4 py-2 flex gap-2 overflow-x-auto">
|
|
496
|
+
{pods.map(pod => (
|
|
497
|
+
<button
|
|
498
|
+
key={pod.name}
|
|
499
|
+
onClick={() => onSelectPod(pod.name)}
|
|
500
|
+
className={clsx(
|
|
501
|
+
'px-3 py-1.5 text-sm rounded-lg whitespace-nowrap transition-colors',
|
|
502
|
+
selectedPod === pod.name
|
|
503
|
+
? 'bg-blue-500 text-theme-text-primary'
|
|
504
|
+
: 'bg-theme-elevated text-theme-text-secondary hover:bg-theme-hover'
|
|
505
|
+
)}
|
|
506
|
+
>
|
|
507
|
+
{pod.name.length > 40 ? '...' + pod.name.slice(-37) : pod.name}
|
|
508
|
+
</button>
|
|
509
|
+
))}
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
{selectedPod && containers.length > 0 && (
|
|
513
|
+
<div className="flex-1 min-h-0">
|
|
514
|
+
<LogsViewer
|
|
515
|
+
key={selectedPod}
|
|
516
|
+
namespace={podNamespace}
|
|
517
|
+
podName={selectedPod}
|
|
518
|
+
containers={containers}
|
|
519
|
+
initialContainer={initialContainer || undefined}
|
|
520
|
+
/>
|
|
521
|
+
</div>
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function AuditSection({ kind, namespace, name }: { kind: string; namespace: string; name: string }) {
|
|
528
|
+
const navigate = useNavigate()
|
|
529
|
+
const { data: findings } = useResourceAudit(kind, namespace, name)
|
|
530
|
+
if (!findings || findings.length === 0) return null
|
|
531
|
+
return <AuditAlerts findings={findings} onViewAll={() => navigate('/audit')} />
|
|
532
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useEffect, useRef, ReactNode } from 'react'
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import type { ContextInfo } from '../types'
|
|
4
|
+
import { getApiBase, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
5
|
+
|
|
6
|
+
export type ConnectionStateType = 'connected' | 'disconnected' | 'connecting'
|
|
7
|
+
|
|
8
|
+
export interface ConnectionState {
|
|
9
|
+
state: ConnectionStateType
|
|
10
|
+
context: string
|
|
11
|
+
clusterName?: string
|
|
12
|
+
error?: string
|
|
13
|
+
errorType?: string // auth, network, timeout, unknown
|
|
14
|
+
progressMessage?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ConnectionStatusResponse extends ConnectionState {
|
|
18
|
+
contexts: ContextInfo[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ConnectionContextValue {
|
|
22
|
+
connection: ConnectionState
|
|
23
|
+
contexts: ContextInfo[]
|
|
24
|
+
retry: () => void
|
|
25
|
+
isRetrying: boolean
|
|
26
|
+
updateFromSSE: (status: ConnectionState) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ConnectionContext = createContext<ConnectionContextValue | null>(null)
|
|
30
|
+
|
|
31
|
+
async function fetchConnectionStatus(): Promise<ConnectionStatusResponse> {
|
|
32
|
+
const response = await fetch(`${getApiBase()}/connection`, {
|
|
33
|
+
credentials: getCredentialsMode(),
|
|
34
|
+
headers: getAuthHeaders(),
|
|
35
|
+
})
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error('Failed to fetch connection status')
|
|
38
|
+
}
|
|
39
|
+
return response.json()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function retryConnection(): Promise<ConnectionState> {
|
|
43
|
+
const response = await fetch(`${getApiBase()}/connection/retry`, {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
credentials: getCredentialsMode(),
|
|
46
|
+
headers: getAuthHeaders(),
|
|
47
|
+
})
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const error = await response.json().catch(() => ({ error: 'Unknown error' }))
|
|
50
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
51
|
+
}
|
|
52
|
+
return response.json()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function ConnectionProvider({ children }: { children: ReactNode }) {
|
|
56
|
+
const queryClient = useQueryClient()
|
|
57
|
+
const [connection, setConnection] = useState<ConnectionState>({
|
|
58
|
+
state: 'connecting',
|
|
59
|
+
context: '',
|
|
60
|
+
})
|
|
61
|
+
const [contexts, setContexts] = useState<ContextInfo[]>([])
|
|
62
|
+
// Track if SSE has started delivering connection_state events
|
|
63
|
+
// Once SSE is active, it becomes the authoritative source for connection state
|
|
64
|
+
const sseActiveRef = useRef(false)
|
|
65
|
+
|
|
66
|
+
// Fetch initial connection status
|
|
67
|
+
// Poll while connecting to get progress updates (SSE not established yet)
|
|
68
|
+
const { data } = useQuery<ConnectionStatusResponse>({
|
|
69
|
+
queryKey: ['connection-status'],
|
|
70
|
+
queryFn: fetchConnectionStatus,
|
|
71
|
+
staleTime: 500, // Allow frequent refetches while connecting
|
|
72
|
+
refetchInterval: connection.state === 'connecting' ? 500 : false, // Poll every 500ms while connecting
|
|
73
|
+
refetchOnWindowFocus: false,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Update state from query result
|
|
77
|
+
// Once SSE is active, only update contexts from poll (SSE handles connection state)
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (data) {
|
|
80
|
+
// Always update contexts from poll data
|
|
81
|
+
setContexts(data.contexts || [])
|
|
82
|
+
// Only update connection state from poll if SSE hasn't taken over
|
|
83
|
+
if (!sseActiveRef.current) {
|
|
84
|
+
setConnection({
|
|
85
|
+
state: data.state,
|
|
86
|
+
context: data.context,
|
|
87
|
+
clusterName: data.clusterName,
|
|
88
|
+
error: data.error,
|
|
89
|
+
errorType: data.errorType,
|
|
90
|
+
progressMessage: data.progressMessage,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, [data])
|
|
95
|
+
|
|
96
|
+
// Retry mutation
|
|
97
|
+
const retryMutation = useMutation({
|
|
98
|
+
mutationFn: retryConnection,
|
|
99
|
+
onMutate: () => {
|
|
100
|
+
// Reset SSE active flag - polling can provide state until SSE reconnects
|
|
101
|
+
sseActiveRef.current = false
|
|
102
|
+
// Set connecting state while retrying
|
|
103
|
+
setConnection(prev => ({
|
|
104
|
+
...prev,
|
|
105
|
+
state: 'connecting',
|
|
106
|
+
error: undefined,
|
|
107
|
+
errorType: undefined,
|
|
108
|
+
progressMessage: 'Connecting to cluster...',
|
|
109
|
+
}))
|
|
110
|
+
},
|
|
111
|
+
onSuccess: (result) => {
|
|
112
|
+
setConnection(result)
|
|
113
|
+
// Clear all query cache to get fresh data from new connection
|
|
114
|
+
queryClient.removeQueries()
|
|
115
|
+
queryClient.invalidateQueries()
|
|
116
|
+
},
|
|
117
|
+
onError: (error: Error) => {
|
|
118
|
+
setConnection(prev => ({
|
|
119
|
+
...prev,
|
|
120
|
+
state: 'disconnected',
|
|
121
|
+
error: error.message,
|
|
122
|
+
progressMessage: undefined,
|
|
123
|
+
}))
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const retry = useCallback(() => {
|
|
128
|
+
retryMutation.mutate()
|
|
129
|
+
}, [retryMutation])
|
|
130
|
+
|
|
131
|
+
// Handler for SSE connection_state events
|
|
132
|
+
const updateFromSSE = useCallback((status: ConnectionState) => {
|
|
133
|
+
// Mark SSE as active - it's now the authoritative source for connection state
|
|
134
|
+
sseActiveRef.current = true
|
|
135
|
+
setConnection(prev => {
|
|
136
|
+
// Don't transition back to 'connecting' from 'connected'. This happens when the
|
|
137
|
+
// pod restarts and the SSE reconnects while the new pod's K8s cache is still
|
|
138
|
+
// syncing. Hiding the main content here causes a flash — keep the 'connected'
|
|
139
|
+
// state and wait for either 'connected' (sync done) or 'disconnected' (failure).
|
|
140
|
+
if (prev.state === 'connected' && status.state === 'connecting') {
|
|
141
|
+
return prev
|
|
142
|
+
}
|
|
143
|
+
return status
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// If we just connected, invalidate queries to fetch fresh data
|
|
147
|
+
if (status.state === 'connected') {
|
|
148
|
+
queryClient.invalidateQueries()
|
|
149
|
+
}
|
|
150
|
+
}, [queryClient])
|
|
151
|
+
|
|
152
|
+
const value: ConnectionContextValue = {
|
|
153
|
+
connection,
|
|
154
|
+
contexts,
|
|
155
|
+
retry,
|
|
156
|
+
isRetrying: retryMutation.isPending,
|
|
157
|
+
updateFromSSE,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<ConnectionContext.Provider value={value}>
|
|
162
|
+
{children}
|
|
163
|
+
</ConnectionContext.Provider>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function useConnection() {
|
|
168
|
+
const context = useContext(ConnectionContext)
|
|
169
|
+
if (!context) {
|
|
170
|
+
throw new Error('useConnection must be used within ConnectionProvider')
|
|
171
|
+
}
|
|
172
|
+
return context
|
|
173
|
+
}
|