@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,407 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { X, File, Link2, ChevronRight, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
import type { FileNode } from '../../types'
|
|
6
|
+
import { formatBytes } from '../../utils/format'
|
|
7
|
+
import { downloadBlob, filterTree } from './file-browser-utils'
|
|
8
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
9
|
+
|
|
10
|
+
interface PodFilesystem {
|
|
11
|
+
root: FileNode
|
|
12
|
+
totalFiles: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fetchPodFiles(
|
|
16
|
+
namespace: string,
|
|
17
|
+
podName: string,
|
|
18
|
+
container: string,
|
|
19
|
+
dirPath: string,
|
|
20
|
+
): Promise<PodFilesystem> {
|
|
21
|
+
const params = new URLSearchParams()
|
|
22
|
+
params.set('container', container)
|
|
23
|
+
params.set('path', dirPath)
|
|
24
|
+
|
|
25
|
+
const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files?${params.toString()}`), {
|
|
26
|
+
credentials: getCredentialsMode(),
|
|
27
|
+
headers: getAuthHeaders(),
|
|
28
|
+
})
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
const error = await response.json().catch(() => ({ error: 'Request failed' }))
|
|
31
|
+
throw new Error(error.error || `HTTP ${response.status}`)
|
|
32
|
+
}
|
|
33
|
+
return response.json()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PodFilesystemModalProps {
|
|
37
|
+
open: boolean
|
|
38
|
+
onClose: () => void
|
|
39
|
+
namespace: string
|
|
40
|
+
podName: string
|
|
41
|
+
containers: string[]
|
|
42
|
+
initialContainer?: string
|
|
43
|
+
onSwitchToImageFiles?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function PodFilesystemModal({
|
|
47
|
+
open,
|
|
48
|
+
onClose,
|
|
49
|
+
namespace,
|
|
50
|
+
podName,
|
|
51
|
+
containers,
|
|
52
|
+
initialContainer,
|
|
53
|
+
onSwitchToImageFiles,
|
|
54
|
+
}: PodFilesystemModalProps) {
|
|
55
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
56
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
57
|
+
const [selectedContainer, setSelectedContainer] = useState(initialContainer || containers[0] || '')
|
|
58
|
+
const [currentPath, setCurrentPath] = useState('/')
|
|
59
|
+
const [filesystem, setFilesystem] = useState<PodFilesystem | null>(null)
|
|
60
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
61
|
+
const [error, setError] = useState<string | null>(null)
|
|
62
|
+
|
|
63
|
+
const loadDirectory = useCallback(async (dirPath: string) => {
|
|
64
|
+
setIsLoading(true)
|
|
65
|
+
setError(null)
|
|
66
|
+
try {
|
|
67
|
+
const result = await fetchPodFiles(namespace, podName, selectedContainer, dirPath)
|
|
68
|
+
setFilesystem(result)
|
|
69
|
+
setCurrentPath(dirPath)
|
|
70
|
+
} catch (err) {
|
|
71
|
+
setError(err instanceof Error ? err.message : 'Failed to list files')
|
|
72
|
+
} finally {
|
|
73
|
+
setIsLoading(false)
|
|
74
|
+
}
|
|
75
|
+
}, [namespace, podName, selectedContainer])
|
|
76
|
+
|
|
77
|
+
// Load root on open or container change
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (open && selectedContainer) {
|
|
80
|
+
loadDirectory('/')
|
|
81
|
+
}
|
|
82
|
+
}, [open, selectedContainer, loadDirectory])
|
|
83
|
+
|
|
84
|
+
// Reset state when modal closes
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!open) {
|
|
87
|
+
setSearchQuery('')
|
|
88
|
+
setFilesystem(null)
|
|
89
|
+
setError(null)
|
|
90
|
+
setIsLoading(false)
|
|
91
|
+
setCurrentPath('/')
|
|
92
|
+
setSelectedContainer(initialContainer || containers[0] || '')
|
|
93
|
+
}
|
|
94
|
+
}, [open, initialContainer, containers])
|
|
95
|
+
|
|
96
|
+
// Handle ESC key
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
if (!open) return
|
|
99
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
100
|
+
if (e.key === 'Escape') { e.stopPropagation(); onClose() }
|
|
101
|
+
}
|
|
102
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
103
|
+
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
|
104
|
+
}, [open, onClose])
|
|
105
|
+
|
|
106
|
+
// Focus trap
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (open && dialogRef.current) {
|
|
109
|
+
dialogRef.current.focus()
|
|
110
|
+
}
|
|
111
|
+
}, [open])
|
|
112
|
+
|
|
113
|
+
if (!open) return null
|
|
114
|
+
|
|
115
|
+
const showFilesystem = filesystem && filesystem.root
|
|
116
|
+
|
|
117
|
+
// Build breadcrumb segments
|
|
118
|
+
const pathSegments = currentPath === '/'
|
|
119
|
+
? ['/']
|
|
120
|
+
: ['/', ...currentPath.split('/').filter(Boolean)]
|
|
121
|
+
|
|
122
|
+
return createPortal(
|
|
123
|
+
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
|
124
|
+
{/* Backdrop */}
|
|
125
|
+
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
|
|
126
|
+
|
|
127
|
+
{/* Modal */}
|
|
128
|
+
<div
|
|
129
|
+
ref={dialogRef}
|
|
130
|
+
tabIndex={-1}
|
|
131
|
+
className="relative dialog w-full max-w-4xl mx-4 max-h-[85vh] flex flex-col outline-none"
|
|
132
|
+
>
|
|
133
|
+
{/* Header */}
|
|
134
|
+
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
135
|
+
<div className="flex-1 min-w-0">
|
|
136
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">Pod Files</h3>
|
|
137
|
+
<p className="text-sm text-theme-text-secondary truncate mt-0.5">
|
|
138
|
+
{namespace}/{podName}
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Container selector */}
|
|
143
|
+
{containers.length > 1 && (
|
|
144
|
+
<select
|
|
145
|
+
value={selectedContainer}
|
|
146
|
+
onChange={(e) => setSelectedContainer(e.target.value)}
|
|
147
|
+
className="mr-4 px-3 py-1.5 text-sm bg-theme-base border border-theme-border rounded-lg text-theme-text-primary focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
148
|
+
>
|
|
149
|
+
{containers.map((c) => (
|
|
150
|
+
<option key={c} value={c}>{c}</option>
|
|
151
|
+
))}
|
|
152
|
+
</select>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<button
|
|
156
|
+
onClick={onClose}
|
|
157
|
+
className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded ml-2"
|
|
158
|
+
>
|
|
159
|
+
<X className="w-5 h-5" />
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Breadcrumb + Search */}
|
|
164
|
+
<div className="p-3 border-b border-theme-border shrink-0 flex items-center gap-3">
|
|
165
|
+
{/* Breadcrumb */}
|
|
166
|
+
<div className="flex items-center gap-1 text-sm min-w-0 flex-shrink overflow-hidden">
|
|
167
|
+
{pathSegments.map((segment, i) => {
|
|
168
|
+
const segmentPath = i === 0 ? '/' : '/' + pathSegments.slice(1, i + 1).join('/')
|
|
169
|
+
const isLast = i === pathSegments.length - 1
|
|
170
|
+
return (
|
|
171
|
+
<span key={segmentPath} className="flex items-center gap-1">
|
|
172
|
+
{i > 0 && <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />}
|
|
173
|
+
<button
|
|
174
|
+
onClick={() => !isLast && loadDirectory(segmentPath)}
|
|
175
|
+
className={clsx(
|
|
176
|
+
'truncate',
|
|
177
|
+
isLast
|
|
178
|
+
? 'text-theme-text-primary font-medium'
|
|
179
|
+
: 'text-blue-400 hover:text-blue-300 hover:underline'
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{segment === '/' ? '/' : segment}
|
|
183
|
+
</button>
|
|
184
|
+
</span>
|
|
185
|
+
)
|
|
186
|
+
})}
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Search */}
|
|
190
|
+
{showFilesystem && (
|
|
191
|
+
<div className="relative flex-1 min-w-[200px]">
|
|
192
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
193
|
+
<input
|
|
194
|
+
type="text"
|
|
195
|
+
placeholder="Filter files..."
|
|
196
|
+
value={searchQuery}
|
|
197
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
198
|
+
className="w-full pl-10 pr-4 py-1.5 bg-theme-base border border-theme-border rounded-lg text-sm text-theme-text-primary placeholder-theme-text-tertiary focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Content */}
|
|
205
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
206
|
+
{/* Loading */}
|
|
207
|
+
{isLoading && (
|
|
208
|
+
<div className="flex flex-col items-center justify-center h-64">
|
|
209
|
+
<Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
|
|
210
|
+
<span className="mt-3 text-theme-text-secondary">Loading files...</span>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* Error */}
|
|
215
|
+
{error && !isLoading && (
|
|
216
|
+
<div className="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
|
|
217
|
+
<div className="flex items-start gap-3">
|
|
218
|
+
<AlertTriangle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
|
|
219
|
+
<div>
|
|
220
|
+
<div className="font-medium text-red-400">Failed to list files</div>
|
|
221
|
+
<div className="text-sm text-theme-text-secondary mt-1">{error}</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* File tree */}
|
|
228
|
+
{showFilesystem && !isLoading && (
|
|
229
|
+
<PodFileTreeView
|
|
230
|
+
root={filesystem.root}
|
|
231
|
+
searchQuery={searchQuery}
|
|
232
|
+
namespace={namespace}
|
|
233
|
+
podName={podName}
|
|
234
|
+
container={selectedContainer}
|
|
235
|
+
onNavigate={loadDirectory}
|
|
236
|
+
/>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{/* Footer with stats */}
|
|
241
|
+
<div className="p-3 border-t border-theme-border text-xs text-theme-text-tertiary flex items-center gap-4 shrink-0">
|
|
242
|
+
{filesystem && !isLoading && (
|
|
243
|
+
<>
|
|
244
|
+
<span>{filesystem.totalFiles} items</span>
|
|
245
|
+
<span>Container: {selectedContainer}</span>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
{onSwitchToImageFiles && (
|
|
249
|
+
<button
|
|
250
|
+
onClick={() => { onClose(); onSwitchToImageFiles() }}
|
|
251
|
+
className="ml-auto text-blue-400 hover:text-blue-300 hover:underline"
|
|
252
|
+
>
|
|
253
|
+
Browse static image from registry →
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>,
|
|
259
|
+
document.body,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// Pod File Tree View
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
interface PodFileTreeViewProps {
|
|
268
|
+
root: FileNode
|
|
269
|
+
searchQuery: string
|
|
270
|
+
namespace: string
|
|
271
|
+
podName: string
|
|
272
|
+
container: string
|
|
273
|
+
onNavigate: (path: string) => void
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function PodFileTreeView({ root, searchQuery, namespace, podName, container, onNavigate }: PodFileTreeViewProps) {
|
|
277
|
+
const filteredRoot = useMemo(() => {
|
|
278
|
+
if (!searchQuery.trim()) return root
|
|
279
|
+
return filterTree(root, searchQuery.toLowerCase())
|
|
280
|
+
}, [root, searchQuery])
|
|
281
|
+
|
|
282
|
+
if (!filteredRoot || !filteredRoot.children || filteredRoot.children.length === 0) {
|
|
283
|
+
return (
|
|
284
|
+
<div className="text-center text-theme-text-tertiary py-8">
|
|
285
|
+
{searchQuery ? 'No files match your filter' : 'Empty directory'}
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div className="font-mono text-sm">
|
|
292
|
+
{filteredRoot.children.map((node) => (
|
|
293
|
+
<PodFileTreeNode
|
|
294
|
+
key={node.path}
|
|
295
|
+
node={node}
|
|
296
|
+
namespace={namespace}
|
|
297
|
+
podName={podName}
|
|
298
|
+
container={container}
|
|
299
|
+
onNavigate={onNavigate}
|
|
300
|
+
/>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
interface PodFileTreeNodeProps {
|
|
307
|
+
node: FileNode
|
|
308
|
+
namespace: string
|
|
309
|
+
podName: string
|
|
310
|
+
container: string
|
|
311
|
+
onNavigate: (path: string) => void
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: PodFileTreeNodeProps) {
|
|
315
|
+
const [downloading, setDownloading] = useState(false)
|
|
316
|
+
const isDir = node.type === 'dir'
|
|
317
|
+
const isSymlink = node.type === 'symlink'
|
|
318
|
+
const isDownloadable = !isDir // files and symlinks can be downloaded
|
|
319
|
+
|
|
320
|
+
const handleDownload = async (e: React.MouseEvent) => {
|
|
321
|
+
e.stopPropagation()
|
|
322
|
+
if (downloading) return
|
|
323
|
+
|
|
324
|
+
setDownloading(true)
|
|
325
|
+
try {
|
|
326
|
+
const params = new URLSearchParams()
|
|
327
|
+
params.set('container', container)
|
|
328
|
+
params.set('path', node.path)
|
|
329
|
+
|
|
330
|
+
const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files/download?${params.toString()}`), {
|
|
331
|
+
credentials: getCredentialsMode(),
|
|
332
|
+
headers: getAuthHeaders(),
|
|
333
|
+
})
|
|
334
|
+
if (!response.ok) {
|
|
335
|
+
const err = await response.json().catch(() => ({ error: 'Download failed' }))
|
|
336
|
+
throw new Error(err.error || `HTTP ${response.status}`)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const blob = await response.blob()
|
|
340
|
+
await downloadBlob(blob, node.name)
|
|
341
|
+
} catch (err) {
|
|
342
|
+
console.error('Download failed:', err)
|
|
343
|
+
} finally {
|
|
344
|
+
setDownloading(false)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const handleClick = () => {
|
|
349
|
+
if (isDir) {
|
|
350
|
+
onNavigate(node.path)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return (
|
|
355
|
+
<div
|
|
356
|
+
className={clsx(
|
|
357
|
+
'flex items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
|
|
358
|
+
isDir && 'font-medium cursor-pointer'
|
|
359
|
+
)}
|
|
360
|
+
onClick={handleClick}
|
|
361
|
+
>
|
|
362
|
+
{isDir ? (
|
|
363
|
+
<FolderOpen className="w-4 h-4 text-amber-400 shrink-0" />
|
|
364
|
+
) : isSymlink ? (
|
|
365
|
+
<Link2 className="w-4 h-4 text-cyan-400 shrink-0" />
|
|
366
|
+
) : (
|
|
367
|
+
<File className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
<span className="text-theme-text-primary truncate flex-1">{node.name}</span>
|
|
371
|
+
|
|
372
|
+
{isSymlink && node.linkTarget && (
|
|
373
|
+
<span className="text-xs text-cyan-400 truncate max-w-48">
|
|
374
|
+
-> {node.linkTarget}
|
|
375
|
+
</span>
|
|
376
|
+
)}
|
|
377
|
+
|
|
378
|
+
{!isDir && node.size !== undefined && (
|
|
379
|
+
<span className="text-xs text-theme-text-tertiary ml-2">
|
|
380
|
+
{formatBytes(node.size)}
|
|
381
|
+
</span>
|
|
382
|
+
)}
|
|
383
|
+
|
|
384
|
+
{node.permissions && (
|
|
385
|
+
<span className="text-xs text-theme-text-tertiary ml-2 font-normal">
|
|
386
|
+
{node.permissions}
|
|
387
|
+
</span>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{isDownloadable && (
|
|
391
|
+
<button
|
|
392
|
+
onClick={handleDownload}
|
|
393
|
+
disabled={downloading}
|
|
394
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-theme-elevated rounded ml-1 disabled:opacity-50"
|
|
395
|
+
title="Download file"
|
|
396
|
+
>
|
|
397
|
+
{downloading ? (
|
|
398
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
399
|
+
) : (
|
|
400
|
+
<Download className="w-3.5 h-3.5" />
|
|
401
|
+
)}
|
|
402
|
+
</button>
|
|
403
|
+
)}
|
|
404
|
+
</div>
|
|
405
|
+
)
|
|
406
|
+
}
|
|
407
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ResourceDetailDrawer as BaseResourceDetailDrawer } from '@skyhook-io/k8s-ui'
|
|
2
|
+
import type { SelectedResource } from '../../types'
|
|
3
|
+
import { WorkloadView } from '../workload/WorkloadView'
|
|
4
|
+
|
|
5
|
+
interface ResourceDetailDrawerProps {
|
|
6
|
+
resource: SelectedResource
|
|
7
|
+
onClose: () => void
|
|
8
|
+
onNavigate?: (resource: SelectedResource) => void
|
|
9
|
+
/** Open directly to YAML view */
|
|
10
|
+
initialTab?: 'detail' | 'yaml'
|
|
11
|
+
/** Controls slide-in/out animation (driven by useAnimatedUnmount) */
|
|
12
|
+
isOpen?: boolean
|
|
13
|
+
/** Whether the drawer is expanded to full-screen WorkloadView */
|
|
14
|
+
expanded?: boolean
|
|
15
|
+
/** Called when user clicks collapse in expanded mode */
|
|
16
|
+
onCollapse?: () => void
|
|
17
|
+
/** Called when user clicks expand button */
|
|
18
|
+
onExpand?: (resource: SelectedResource) => void
|
|
19
|
+
/** Navigate to another resource within expanded WorkloadView */
|
|
20
|
+
onNavigateToResource?: (resource: SelectedResource) => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ResourceDetailDrawer(props: ResourceDetailDrawerProps) {
|
|
24
|
+
return (
|
|
25
|
+
<BaseResourceDetailDrawer {...props}>
|
|
26
|
+
{({ resource, expanded, initialTab, onClose, onExpand, onBack, onNavigateToResource, onCollapseToDrawer }) => (
|
|
27
|
+
<WorkloadView
|
|
28
|
+
kind={resource.kind}
|
|
29
|
+
namespace={resource.namespace}
|
|
30
|
+
name={resource.name}
|
|
31
|
+
group={resource.group}
|
|
32
|
+
expanded={expanded}
|
|
33
|
+
initialTab={initialTab}
|
|
34
|
+
onClose={onClose}
|
|
35
|
+
onExpand={onExpand}
|
|
36
|
+
onBack={onBack ?? (() => {})}
|
|
37
|
+
onNavigateToResource={onNavigateToResource}
|
|
38
|
+
onCollapseToDrawer={onCollapseToDrawer}
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
</BaseResourceDetailDrawer>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
2
|
+
import { useLocation, useNavigate } from 'react-router-dom'
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
4
|
+
import { ApiError, fetchJSON, isForbiddenError, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics } from '../../api/client'
|
|
5
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
6
|
+
import { useAPIResources } from '../../api/apiResources'
|
|
7
|
+
import { initNavigationMap } from '@skyhook-io/k8s-ui'
|
|
8
|
+
import { usePinnedKinds } from '../../hooks/useFavorites'
|
|
9
|
+
import { useOpenLogs, useOpenWorkloadLogs } from '../dock'
|
|
10
|
+
import {
|
|
11
|
+
ResourcesView as BaseResourcesView,
|
|
12
|
+
CORE_RESOURCES,
|
|
13
|
+
} from '@skyhook-io/k8s-ui'
|
|
14
|
+
import type { ResourceQueryResult } from '@skyhook-io/k8s-ui'
|
|
15
|
+
import type { SelectedResource } from '../../types'
|
|
16
|
+
import type { NavigateToResource } from '../../utils/navigation'
|
|
17
|
+
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
18
|
+
import { getSkeletonYaml } from '../../utils/skeleton-yaml'
|
|
19
|
+
|
|
20
|
+
interface ResourceCountsResponse {
|
|
21
|
+
counts: Record<string, number>
|
|
22
|
+
forbidden?: string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ResourcesViewProps {
|
|
26
|
+
namespaces: string[]
|
|
27
|
+
selectedResource?: SelectedResource | null
|
|
28
|
+
onResourceClick?: (resource: SelectedResource | null) => void
|
|
29
|
+
onResourceClickYaml?: NavigateToResource
|
|
30
|
+
onKindChange?: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ResourcesView({ namespaces, selectedResource, onResourceClick, onResourceClickYaml, onKindChange }: ResourcesViewProps) {
|
|
34
|
+
const location = useLocation()
|
|
35
|
+
const navigate = useNavigate()
|
|
36
|
+
|
|
37
|
+
// API resources discovery
|
|
38
|
+
const { data: apiResources } = useAPIResources()
|
|
39
|
+
|
|
40
|
+
// Initialize navigation kind↔plural maps from discovered API resources
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (apiResources) initNavigationMap(apiResources)
|
|
43
|
+
}, [apiResources])
|
|
44
|
+
|
|
45
|
+
// Track the selected kind from the k8s-ui component
|
|
46
|
+
const [selectedKind, setSelectedKind] = useState<{ name: string; kind: string; group: string } | null>(null)
|
|
47
|
+
|
|
48
|
+
// Lightweight resource counts for sidebar badges (~2KB instead of ~608MB)
|
|
49
|
+
const namespacesParam = namespaces.join(',')
|
|
50
|
+
const { data: countsData } = useQuery({
|
|
51
|
+
queryKey: ['resource-counts', namespacesParam],
|
|
52
|
+
queryFn: async () => {
|
|
53
|
+
const params = new URLSearchParams()
|
|
54
|
+
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
55
|
+
return fetchJSON<ResourceCountsResponse>(`/resource-counts?${params}`)
|
|
56
|
+
},
|
|
57
|
+
staleTime: 10000,
|
|
58
|
+
refetchInterval: 60000, // Safety net — SSE k8s_event drives near-real-time invalidation
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Determine if selected kind is a CRD (only CRDs should send ?group= to backend)
|
|
62
|
+
const isSelectedCrd = useMemo(() => {
|
|
63
|
+
if (!selectedKind) return false
|
|
64
|
+
// Check API resources first, fall back to CORE_RESOURCES
|
|
65
|
+
const match = apiResources?.find(r => r.name === selectedKind.name && r.group === selectedKind.group)
|
|
66
|
+
?? CORE_RESOURCES.find(r => r.name === selectedKind.name && r.group === selectedKind.group)
|
|
67
|
+
return match?.isCrd ?? (!!selectedKind.group) // default: has group = likely CRD
|
|
68
|
+
}, [selectedKind, apiResources])
|
|
69
|
+
|
|
70
|
+
// Fetch full data only for the selected kind
|
|
71
|
+
const selectedKindQuery = useQuery({
|
|
72
|
+
queryKey: ['resources', selectedKind?.name, isSelectedCrd ? selectedKind?.group : '', namespaces],
|
|
73
|
+
queryFn: async () => {
|
|
74
|
+
if (!selectedKind) return []
|
|
75
|
+
const params = new URLSearchParams()
|
|
76
|
+
if (namespaces.length > 0) params.set('namespaces', namespacesParam)
|
|
77
|
+
if (isSelectedCrd && selectedKind.group) params.set('group', selectedKind.group)
|
|
78
|
+
const res = await fetch(apiUrl(`/resources/${selectedKind.name}?${params}`), {
|
|
79
|
+
credentials: getCredentialsMode(),
|
|
80
|
+
headers: getAuthHeaders(),
|
|
81
|
+
})
|
|
82
|
+
if (!res.ok) {
|
|
83
|
+
const errorData = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
84
|
+
throw new ApiError(errorData.error || `Failed to fetch ${selectedKind.name}`, res.status, errorData)
|
|
85
|
+
}
|
|
86
|
+
return res.json()
|
|
87
|
+
},
|
|
88
|
+
enabled: !!selectedKind,
|
|
89
|
+
staleTime: 30000,
|
|
90
|
+
refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
|
|
91
|
+
retry: (failureCount: number, error: Error) => {
|
|
92
|
+
if (isForbiddenError(error)) return false
|
|
93
|
+
return failureCount < 3
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Map to ResourceQueryResult shape
|
|
98
|
+
const selectedKindQueryResult: ResourceQueryResult | undefined = useMemo(() => {
|
|
99
|
+
if (!selectedKind) return undefined
|
|
100
|
+
return {
|
|
101
|
+
data: selectedKindQuery.data as any[] | undefined,
|
|
102
|
+
isLoading: selectedKindQuery.isLoading,
|
|
103
|
+
error: selectedKindQuery.error,
|
|
104
|
+
refetch: selectedKindQuery.refetch,
|
|
105
|
+
dataUpdatedAt: selectedKindQuery.dataUpdatedAt,
|
|
106
|
+
}
|
|
107
|
+
}, [selectedKind, selectedKindQuery.data, selectedKindQuery.isLoading, selectedKindQuery.error, selectedKindQuery.refetch, selectedKindQuery.dataUpdatedAt])
|
|
108
|
+
|
|
109
|
+
// Metrics
|
|
110
|
+
const { data: topPodMetrics } = useTopPodMetrics()
|
|
111
|
+
const { data: topNodeMetrics } = useTopNodeMetrics()
|
|
112
|
+
|
|
113
|
+
// Certificate expiry
|
|
114
|
+
const { data: certExpiry, isError: certExpiryError } = useSecretCertExpiry()
|
|
115
|
+
|
|
116
|
+
// Pinned kinds
|
|
117
|
+
const { pinned, togglePin, isPinned } = usePinnedKinds()
|
|
118
|
+
|
|
119
|
+
// Dock actions
|
|
120
|
+
const openLogs = useOpenLogs()
|
|
121
|
+
const openWorkloadLogs = useOpenWorkloadLogs()
|
|
122
|
+
|
|
123
|
+
// Navigation adapter
|
|
124
|
+
const handleNavigate = useMemo(() => {
|
|
125
|
+
return (path: string, options?: { replace?: boolean }) => {
|
|
126
|
+
navigate(path, { replace: options?.replace })
|
|
127
|
+
}
|
|
128
|
+
}, [navigate])
|
|
129
|
+
|
|
130
|
+
// Create resource dialog
|
|
131
|
+
const [createDialogOpen, setCreateDialogOpen] = useState(false)
|
|
132
|
+
const [createDialogYaml, setCreateDialogYaml] = useState('')
|
|
133
|
+
const [createDialogTitle, setCreateDialogTitle] = useState<string | undefined>()
|
|
134
|
+
|
|
135
|
+
const handleCreateResource = useCallback((kind: { name: string; kind: string; group: string } | null) => {
|
|
136
|
+
if (kind?.kind) {
|
|
137
|
+
setCreateDialogYaml(getSkeletonYaml(kind.kind, kind.group))
|
|
138
|
+
setCreateDialogTitle(`Create ${kind.kind}`)
|
|
139
|
+
} else {
|
|
140
|
+
setCreateDialogYaml('')
|
|
141
|
+
setCreateDialogTitle(undefined)
|
|
142
|
+
}
|
|
143
|
+
setCreateDialogOpen(true)
|
|
144
|
+
}, [])
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<BaseResourcesView
|
|
149
|
+
namespaces={namespaces}
|
|
150
|
+
selectedResource={selectedResource}
|
|
151
|
+
onResourceClick={onResourceClick}
|
|
152
|
+
onResourceClickYaml={onResourceClickYaml}
|
|
153
|
+
onKindChange={onKindChange}
|
|
154
|
+
// Injected data
|
|
155
|
+
apiResources={apiResources}
|
|
156
|
+
// Lightweight counts for sidebar (replaces 233 parallel queries)
|
|
157
|
+
resourceCounts={countsData?.counts}
|
|
158
|
+
resourceForbidden={countsData?.forbidden}
|
|
159
|
+
selectedKindQuery={selectedKindQueryResult}
|
|
160
|
+
onSelectedKindChange={setSelectedKind}
|
|
161
|
+
topPodMetrics={topPodMetrics}
|
|
162
|
+
topNodeMetrics={topNodeMetrics}
|
|
163
|
+
certExpiry={certExpiry}
|
|
164
|
+
certExpiryError={certExpiryError}
|
|
165
|
+
// Pinned kinds
|
|
166
|
+
pinned={pinned}
|
|
167
|
+
togglePin={togglePin}
|
|
168
|
+
isPinned={(kind: string, group?: string) => isPinned(kind, group ?? '')}
|
|
169
|
+
// Navigation
|
|
170
|
+
locationSearch={location.search}
|
|
171
|
+
locationPathname={location.pathname}
|
|
172
|
+
onNavigate={handleNavigate}
|
|
173
|
+
// Dock actions
|
|
174
|
+
onOpenLogs={openLogs}
|
|
175
|
+
onOpenWorkloadLogs={openWorkloadLogs}
|
|
176
|
+
// Create resource
|
|
177
|
+
onCreateResource={handleCreateResource}
|
|
178
|
+
/>
|
|
179
|
+
<CreateResourceDialog
|
|
180
|
+
open={createDialogOpen}
|
|
181
|
+
onClose={() => setCreateDialogOpen(false)}
|
|
182
|
+
initialYaml={createDialogYaml}
|
|
183
|
+
title={createDialogTitle}
|
|
184
|
+
onCreated={(result) => {
|
|
185
|
+
onResourceClick?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
</>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@skyhook-io/k8s-ui/components/ui/drawer-components'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FileNode } from '../../types'
|
|
2
|
+
import { isDesktopApp, desktopSaveBlob } from '../../utils/desktop-download'
|
|
3
|
+
|
|
4
|
+
/** Trigger a file download from a Blob. Uses native save dialog on desktop. */
|
|
5
|
+
export async function downloadBlob(blob: Blob, filename: string): Promise<void> {
|
|
6
|
+
if (await isDesktopApp()) {
|
|
7
|
+
await desktopSaveBlob(blob, filename)
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
const url = URL.createObjectURL(blob)
|
|
11
|
+
const a = document.createElement('a')
|
|
12
|
+
a.href = url
|
|
13
|
+
a.download = filename
|
|
14
|
+
a.click()
|
|
15
|
+
URL.revokeObjectURL(url)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Recursively filter a FileNode tree by name substring match. */
|
|
19
|
+
export function filterTree(node: FileNode, query: string): FileNode | null {
|
|
20
|
+
if (node.name.toLowerCase().includes(query)) {
|
|
21
|
+
return node
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (node.type === 'dir' && node.children) {
|
|
25
|
+
const filteredChildren = node.children
|
|
26
|
+
.map((child) => filterTree(child, query))
|
|
27
|
+
.filter((child): child is FileNode => child !== null)
|
|
28
|
+
|
|
29
|
+
if (filteredChildren.length > 0) {
|
|
30
|
+
return { ...node, children: filteredChildren }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@skyhook-io/k8s-ui/components/resources/renderers/AlertRenderer'
|