@skyhook-io/radar-app 1.13.4 → 1.14.0
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/package.json +8 -8
- package/src/App.tsx +60 -24
- package/src/RadarApp.tsx +5 -0
- package/src/api/client.authRedirect.test.ts +12 -1
- package/src/api/client.prometheusStatus.test.ts +24 -0
- package/src/api/client.ts +325 -16
- package/src/api/diagnose.ts +62 -15
- package/src/api/drain-plan.test.ts +86 -0
- package/src/api/quotas.test.ts +26 -0
- package/src/api/quotas.ts +20 -0
- package/src/api/workloadMetrics.test.ts +40 -0
- package/src/api/workloadMetrics.ts +68 -0
- package/src/components/CloudConnectFlow.tsx +179 -42
- package/src/components/CloudFunnelButton.tsx +107 -36
- package/src/components/ConnectionErrorView.test.tsx +30 -0
- package/src/components/ConnectionErrorView.tsx +31 -19
- package/src/components/SyncProgressPanel.tsx +73 -0
- package/src/components/audit/AuditView.tsx +2 -0
- package/src/components/cloudConnectHandoff.test.ts +140 -0
- package/src/components/cloudConnectHandoff.ts +124 -0
- package/src/components/diagnose/AgentCase.tsx +131 -0
- package/src/components/diagnose/DiagnoseContext.tsx +2 -1
- package/src/components/diagnose/DiagnoseSurface.test.tsx +0 -20
- package/src/components/diagnose/DiagnoseSurface.tsx +32 -195
- package/src/components/diagnose/InvestigationEvidencePane.test.tsx +783 -7
- package/src/components/diagnose/InvestigationEvidencePane.tsx +774 -133
- package/src/components/diagnose/InvestigationView.tsx +222 -5
- package/src/components/diagnose/diagnoseEvidenceTypes.ts +22 -1
- package/src/components/diagnose/investigationCase.test.tsx +1568 -0
- package/src/components/diagnose/investigationCase.ts +439 -0
- package/src/components/diagnose/investigationEvidence/adapters/changes.ts +60 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.test.ts +573 -0
- package/src/components/diagnose/investigationEvidence/adapters/diagnose.ts +935 -0
- package/src/components/diagnose/investigationEvidence/adapters/events.ts +96 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.test.ts +242 -0
- package/src/components/diagnose/investigationEvidence/adapters/helm.ts +208 -0
- package/src/components/diagnose/investigationEvidence/adapters/issues.ts +172 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.test.ts +283 -0
- package/src/components/diagnose/investigationEvidence/adapters/logs.ts +183 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.test.ts +528 -0
- package/src/components/diagnose/investigationEvidence/adapters/permissions.ts +292 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.test.ts +1641 -0
- package/src/components/diagnose/investigationEvidence/adapters/prometheus.ts +769 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.test.ts +54 -0
- package/src/components/diagnose/investigationEvidence/adapters/resource.ts +257 -0
- package/src/components/diagnose/investigationEvidence/adapters/topology.ts +207 -0
- package/src/components/diagnose/investigationEvidence/builder.ts +418 -0
- package/src/components/diagnose/investigationEvidence/evidenceFixtures.ts +179 -0
- package/src/components/diagnose/investigationEvidence/identity.test.ts +614 -0
- package/src/components/diagnose/investigationEvidence/identity.ts +261 -0
- package/src/components/diagnose/investigationEvidence/index.ts +55 -0
- package/src/components/diagnose/investigationEvidence/observations.ts +954 -0
- package/src/components/diagnose/investigationEvidence/parse.ts +467 -0
- package/src/components/diagnose/investigationEvidence/projection.test.ts +2703 -0
- package/src/components/diagnose/investigationEvidence/types.ts +512 -0
- package/src/components/diagnose/investigationEvidenceKinds.ts +218 -0
- package/src/components/diagnose/investigationEvidencePresentation.test.ts +31 -0
- package/src/components/diagnose/investigationEvidencePresentation.ts +1 -0
- package/src/components/diagnose/investigationMetrics.test.ts +712 -0
- package/src/components/diagnose/investigationMetrics.ts +393 -0
- package/src/components/diagnose/investigationSourceFocus.ts +2 -0
- package/src/components/diagnose/investigationState.test.ts +322 -0
- package/src/components/diagnose/investigationState.ts +147 -25
- package/src/components/diagnose/parts.test.tsx +482 -0
- package/src/components/diagnose/parts.tsx +418 -41
- package/src/components/diagnose/target.test.ts +19 -0
- package/src/components/diagnose/target.ts +3 -2
- package/src/components/gitops/GitOpsView.tsx +7 -0
- package/src/components/gitops/RemoteDestinationCloudHint.tsx +26 -0
- package/src/components/resource/PrometheusCharts.overlays.test.ts +22 -0
- package/src/components/resource/PrometheusCharts.tsx +4 -3
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +99 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +245 -103
- package/src/components/resource/RightsizingStrip.test.ts +38 -0
- package/src/components/resource/RightsizingStrip.tsx +15 -3
- package/src/components/resource/WorkloadMetricsHelpDialog.test.tsx +79 -0
- package/src/components/resource/WorkloadMetricsHelpDialog.tsx +145 -0
- package/src/components/resource/WorkloadMetricsSection.render.test.tsx +351 -0
- package/src/components/resource/WorkloadMetricsSection.tsx +486 -0
- package/src/components/resource/workloadMetricValues.test.ts +65 -0
- package/src/components/resource/workloadMetricValues.ts +30 -0
- package/src/components/resources/PodFilePreview.test.tsx +131 -0
- package/src/components/resources/PodFilePreview.tsx +394 -0
- package/src/components/resources/PodFilesystemModal.tsx +157 -67
- package/src/components/resources/ResourcesView.tsx +44 -6
- package/src/components/resources/renderers/NamespaceRenderer.tsx +7 -1
- package/src/components/resources/renderers/PodRenderer.tsx +4 -0
- package/src/components/resources/renderers/RolloutRenderer.tsx +15 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +3 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +21 -0
- package/src/components/settings/SettingsDialog.tsx +94 -14
- package/src/components/traffic/TrafficFlowList.tsx +229 -70
- package/src/components/traffic/TrafficGraph.tsx +6 -6
- package/src/components/traffic/TrafficView.tsx +10 -9
- package/src/components/traffic/trafficFilters.test.ts +28 -1
- package/src/components/traffic/trafficFilters.ts +31 -0
- package/src/components/workload/WorkloadView.tsx +19 -8
- package/src/context/ConnectionContext.tsx +48 -3
- package/src/context/DiagnoseCustomization.test.tsx +26 -0
- package/src/context/DiagnoseCustomization.tsx +15 -2
- package/src/index.css +17 -0
- package/src/index.ts +1 -0
- package/src/utils/navigation.ts +1 -1
- package/src/utils/shell-safe.test.ts +25 -1
- package/src/utils/shell-safe.ts +16 -0
- package/src/utils/topology-namespace.test.ts +64 -0
- package/src/utils/topology-namespace.ts +18 -0
- package/src/components/diagnose/investigationEvidence.test.ts +0 -5202
- package/src/components/diagnose/investigationEvidence.ts +0 -4784
- package/src/components/gitops/GitOpsActions.tsx +0 -1
- package/src/components/gitops/GitOpsStatusBadge.tsx +0 -1
- package/src/components/gitops/ManagedResourcesList.tsx +0 -1
- package/src/components/gitops/SyncCountdown.tsx +0 -1
- package/src/components/gitops/index.ts +0 -4
- package/src/components/helm/ManifestDiffViewer.tsx +0 -69
- package/src/components/logs/JsonLogLine.tsx +0 -1
- package/src/components/logs/LogCore.tsx +0 -2
- package/src/components/logs/useLogBuffer.ts +0 -2
- package/src/components/logs/useLogSearch.ts +0 -1
- package/src/components/resource-drawer/ResourceDrawer.tsx +0 -216
- package/src/components/resources/drawer-components.tsx +0 -1
- package/src/components/resources/renderers/AlertRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGPoolerRenderer.tsx +0 -1
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRenderer.tsx +0 -1
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +0 -1
- package/src/components/resources/renderers/ChallengeRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/ClusterIssuerRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigAuditReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ConfigMapRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/EventRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/GatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/GenericRenderer.tsx +0 -1
- package/src/components/resources/renderers/GitRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/HelmRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/IngressRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioAuthorizationPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioGatewayRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioPeerAuthenticationRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioServiceEntryRenderer.tsx +0 -1
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/JobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +0 -1
- package/src/components/resources/renderers/KedaTriggerAuthRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeConfigurationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeServiceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +0 -1
- package/src/components/resources/renderers/KustomizationRenderer.tsx +0 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/LeaseRenderer.tsx +0 -1
- package/src/components/resources/renderers/NetworkPolicyRenderer.tsx +0 -1
- package/src/components/resources/renderers/OCIRepositoryRenderer.tsx +0 -1
- package/src/components/resources/renderers/OrderRenderer.tsx +0 -1
- package/src/components/resources/renderers/PersistentVolumeRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodDisruptionBudgetRenderer.tsx +0 -1
- package/src/components/resources/renderers/PodMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/PriorityClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +0 -1
- package/src/components/resources/renderers/ReplicaSetRenderer.tsx +0 -1
- package/src/components/resources/renderers/RuntimeClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/SbomReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretRenderer.tsx +0 -1
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +0 -1
- package/src/components/resources/renderers/ServiceMonitorRenderer.tsx +0 -1
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/StorageClassRenderer.tsx +0 -1
- package/src/components/resources/renderers/TraefikIngressRouteRenderer.tsx +0 -1
- package/src/components/resources/renderers/VPARenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroScheduleRenderer.tsx +0 -1
- package/src/components/resources/renderers/VeleroVSLRenderer.tsx +0 -1
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +0 -1
- package/src/components/resources/renderers/WebhookConfigRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowRenderer.tsx +0 -1
- package/src/components/resources/renderers/WorkflowTemplateRenderer.tsx +0 -1
- package/src/components/resources/renderers/argo-cells.tsx +0 -1
- package/src/components/resources/renderers/certmanager-cells.tsx +0 -1
- package/src/components/resources/renderers/cnpg-cells.tsx +0 -1
- package/src/components/resources/renderers/eso-cells.tsx +0 -1
- package/src/components/resources/renderers/flux-cells.tsx +0 -1
- package/src/components/resources/renderers/index.ts +0 -94
- package/src/components/resources/renderers/istio-cells.tsx +0 -1
- package/src/components/resources/renderers/karpenter-cells.tsx +0 -1
- package/src/components/resources/renderers/keda-cells.tsx +0 -1
- package/src/components/resources/renderers/knative-cells.tsx +0 -1
- package/src/components/resources/renderers/kyverno-cells.tsx +0 -1
- package/src/components/resources/renderers/prometheus-cells.tsx +0 -1
- package/src/components/resources/renderers/traefik-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-cells.tsx +0 -1
- package/src/components/resources/renderers/trivy-shared.tsx +0 -1
- package/src/components/resources/renderers/velero-cells.tsx +0 -1
- package/src/components/resources/resource-utils-argo.ts +0 -2
- package/src/components/resources/resource-utils-certmanager.ts +0 -2
- package/src/components/resources/resource-utils-eso.ts +0 -2
- package/src/components/resources/resource-utils-flux.ts +0 -2
- package/src/components/resources/resource-utils-istio.ts +0 -2
- package/src/components/resources/resource-utils-karpenter.ts +0 -2
- package/src/components/resources/resource-utils-knative.ts +0 -2
- package/src/components/resources/resource-utils-kyverno.ts +0 -2
- package/src/components/resources/resource-utils-prometheus.ts +0 -2
- package/src/components/resources/resource-utils-traefik.ts +0 -1
- package/src/components/resources/resource-utils-trivy.ts +0 -2
- package/src/components/resources/resource-utils-velero.ts +0 -2
- package/src/components/shared/EditableYamlView.tsx +0 -24
- package/src/components/timeline/DiffViewer.tsx +0 -1
- package/src/components/traffic/index.ts +0 -3
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +0 -1
- package/src/components/ui/MetricsChart.tsx +0 -1
- package/src/components/ui/ResourceBar.tsx +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement, useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { X, File, Link2, ChevronRight, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
|
|
3
|
+
import { X, File, Link2, ChevronRight, ArrowLeft, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
|
|
4
4
|
import { PaneLoader, Input } from '@skyhook-io/k8s-ui'
|
|
5
5
|
import { clsx } from 'clsx'
|
|
6
6
|
import type { FileNode } from '../../types'
|
|
@@ -11,6 +11,7 @@ import { isDesktopApp } from '../../utils/desktop-download'
|
|
|
11
11
|
import { openFile, openFolder } from '../../utils/desktop-open-folder'
|
|
12
12
|
import { useToast } from '../ui/Toast'
|
|
13
13
|
import { Tooltip } from '../ui/Tooltip'
|
|
14
|
+
import { PodFilePreview, PREVIEW_BYTE_CAP } from './PodFilePreview'
|
|
14
15
|
|
|
15
16
|
interface PodFilesystem {
|
|
16
17
|
root: FileNode
|
|
@@ -68,6 +69,56 @@ async function savePodFileToDisk(
|
|
|
68
69
|
return body.path
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
type PodFileToast = Pick<ReturnType<typeof useToast>, 'showSuccess' | 'showError'>
|
|
73
|
+
|
|
74
|
+
// One download path for the listing row and the preview header. Desktop saves
|
|
75
|
+
// straight from the backend; the browser gets a blob.
|
|
76
|
+
async function downloadPodFile(
|
|
77
|
+
namespace: string,
|
|
78
|
+
podName: string,
|
|
79
|
+
container: string,
|
|
80
|
+
node: Pick<FileNode, 'path' | 'name'>,
|
|
81
|
+
{ showSuccess, showError }: PodFileToast,
|
|
82
|
+
) {
|
|
83
|
+
try {
|
|
84
|
+
if (await isDesktopApp()) {
|
|
85
|
+
const savedPath = await savePodFileToDisk(namespace, podName, container, node.path)
|
|
86
|
+
showSuccess(
|
|
87
|
+
'File saved',
|
|
88
|
+
savedPath,
|
|
89
|
+
{
|
|
90
|
+
label: 'Show in Finder',
|
|
91
|
+
icon: createElement(FolderOpen, { className: 'w-3.5 h-3.5' }),
|
|
92
|
+
onClick: () => openFolder(savedPath),
|
|
93
|
+
},
|
|
94
|
+
() => openFile(savedPath),
|
|
95
|
+
)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const params = new URLSearchParams()
|
|
100
|
+
params.set('container', container)
|
|
101
|
+
params.set('path', node.path)
|
|
102
|
+
|
|
103
|
+
const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files/download?${params.toString()}`), {
|
|
104
|
+
credentials: getCredentialsMode(),
|
|
105
|
+
headers: getAuthHeaders(),
|
|
106
|
+
})
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const err = await response.json().catch(() => ({ error: 'Download failed' }))
|
|
109
|
+
throw new Error(err.error || `HTTP ${response.status}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const blob = await response.blob()
|
|
113
|
+
await downloadBlob(blob, node.name)
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
116
|
+
if (message !== 'cancelled') {
|
|
117
|
+
showError(`Could not download ${node.name}`, message)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
71
122
|
interface PodFilesystemModalProps {
|
|
72
123
|
open: boolean
|
|
73
124
|
onClose: () => void
|
|
@@ -94,8 +145,12 @@ export function PodFilesystemModal({
|
|
|
94
145
|
const [filesystem, setFilesystem] = useState<PodFilesystem | null>(null)
|
|
95
146
|
const [isLoading, setIsLoading] = useState(false)
|
|
96
147
|
const [error, setError] = useState<string | null>(null)
|
|
148
|
+
// The file being viewed in place of the listing; null shows the directory.
|
|
149
|
+
const [previewFile, setPreviewFile] = useState<FileNode | null>(null)
|
|
150
|
+
const toast = useToast()
|
|
97
151
|
|
|
98
152
|
const loadDirectory = useCallback(async (dirPath: string) => {
|
|
153
|
+
setPreviewFile(null)
|
|
99
154
|
setIsLoading(true)
|
|
100
155
|
setError(null)
|
|
101
156
|
try {
|
|
@@ -124,19 +179,24 @@ export function PodFilesystemModal({
|
|
|
124
179
|
setError(null)
|
|
125
180
|
setIsLoading(false)
|
|
126
181
|
setCurrentPath('/')
|
|
182
|
+
setPreviewFile(null)
|
|
127
183
|
setSelectedContainer(initialContainer || containers[0] || '')
|
|
128
184
|
}
|
|
129
185
|
}, [open, initialContainer, containers])
|
|
130
186
|
|
|
131
|
-
//
|
|
187
|
+
// ESC steps back one level: out of a file to its listing, then out of the
|
|
188
|
+
// dialog. One listener owns both so the two can never race.
|
|
132
189
|
useEffect(() => {
|
|
133
190
|
if (!open) return
|
|
134
191
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
135
|
-
if (e.key
|
|
192
|
+
if (e.key !== 'Escape') return
|
|
193
|
+
e.stopPropagation()
|
|
194
|
+
if (previewFile) setPreviewFile(null)
|
|
195
|
+
else onClose()
|
|
136
196
|
}
|
|
137
197
|
document.addEventListener('keydown', handleKeyDown, true)
|
|
138
198
|
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
|
139
|
-
}, [open, onClose])
|
|
199
|
+
}, [open, onClose, previewFile])
|
|
140
200
|
|
|
141
201
|
// Focus trap
|
|
142
202
|
useEffect(() => {
|
|
@@ -149,10 +209,19 @@ export function PodFilesystemModal({
|
|
|
149
209
|
|
|
150
210
|
const showFilesystem = filesystem && filesystem.root
|
|
151
211
|
|
|
152
|
-
//
|
|
153
|
-
|
|
212
|
+
// Breadcrumb: the directory path, plus the file when one is open. Clicking
|
|
213
|
+
// the current directory while viewing a file just returns to its listing.
|
|
214
|
+
const breadcrumbPath = previewFile ? previewFile.path : currentPath
|
|
215
|
+
const pathSegments = breadcrumbPath === '/'
|
|
154
216
|
? ['/']
|
|
155
|
-
: ['/', ...
|
|
217
|
+
: ['/', ...breadcrumbPath.split('/').filter(Boolean)]
|
|
218
|
+
const goToDirectory = (dirPath: string) => {
|
|
219
|
+
if (dirPath === currentPath) setPreviewFile(null)
|
|
220
|
+
else loadDirectory(dirPath)
|
|
221
|
+
}
|
|
222
|
+
const downloadPreviewFile = () => {
|
|
223
|
+
if (previewFile) downloadPodFile(namespace, podName, selectedContainer, previewFile, toast)
|
|
224
|
+
}
|
|
156
225
|
|
|
157
226
|
return createPortal(
|
|
158
227
|
<div className="fixed inset-0 z-[100] flex items-center justify-center">
|
|
@@ -163,19 +232,47 @@ export function PodFilesystemModal({
|
|
|
163
232
|
<div
|
|
164
233
|
ref={dialogRef}
|
|
165
234
|
tabIndex={-1}
|
|
166
|
-
className="relative dialog w-full max-w-4xl mx-4
|
|
235
|
+
className="relative dialog w-full max-w-4xl mx-4 h-[85vh] flex flex-col outline-none"
|
|
167
236
|
>
|
|
168
237
|
{/* Header */}
|
|
169
238
|
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
239
|
+
{previewFile ? (
|
|
240
|
+
<>
|
|
241
|
+
<Tooltip content="Back to files">
|
|
242
|
+
<button
|
|
243
|
+
onClick={() => setPreviewFile(null)}
|
|
244
|
+
className="p-2 mr-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
245
|
+
>
|
|
246
|
+
<ArrowLeft className="w-5 h-5" />
|
|
247
|
+
</button>
|
|
248
|
+
</Tooltip>
|
|
249
|
+
<div className="flex-1 min-w-0">
|
|
250
|
+
<h3 className="text-lg font-semibold text-theme-text-primary truncate">{previewFile.name}</h3>
|
|
251
|
+
<p className="text-sm text-theme-text-secondary truncate mt-0.5">
|
|
252
|
+
{namespace}/{podName} · {selectedContainer}
|
|
253
|
+
{previewFile.size !== undefined ? ` · ${formatBytes(previewFile.size)}` : null}
|
|
254
|
+
</p>
|
|
255
|
+
</div>
|
|
256
|
+
<Tooltip content="Download file">
|
|
257
|
+
<button
|
|
258
|
+
onClick={downloadPreviewFile}
|
|
259
|
+
className="p-2 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
260
|
+
>
|
|
261
|
+
<Download className="w-5 h-5" />
|
|
262
|
+
</button>
|
|
263
|
+
</Tooltip>
|
|
264
|
+
</>
|
|
265
|
+
) : (
|
|
266
|
+
<div className="flex-1 min-w-0">
|
|
267
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">Pod Files</h3>
|
|
268
|
+
<p className="text-sm text-theme-text-secondary truncate mt-0.5">
|
|
269
|
+
{namespace}/{podName}
|
|
270
|
+
</p>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
176
273
|
|
|
177
274
|
{/* Container selector */}
|
|
178
|
-
{containers.length > 1 && (
|
|
275
|
+
{!previewFile && containers.length > 1 && (
|
|
179
276
|
<select
|
|
180
277
|
value={selectedContainer}
|
|
181
278
|
onChange={(e) => setSelectedContainer(e.target.value)}
|
|
@@ -206,7 +303,7 @@ export function PodFilesystemModal({
|
|
|
206
303
|
<span key={segmentPath} className="flex items-center gap-1">
|
|
207
304
|
{i > 0 && <ChevronRight className="w-3 h-3 text-theme-text-tertiary shrink-0" />}
|
|
208
305
|
<button
|
|
209
|
-
onClick={() => !isLast &&
|
|
306
|
+
onClick={() => !isLast && goToDirectory(segmentPath)}
|
|
210
307
|
className={clsx(
|
|
211
308
|
'truncate',
|
|
212
309
|
isLast
|
|
@@ -222,7 +319,7 @@ export function PodFilesystemModal({
|
|
|
222
319
|
</div>
|
|
223
320
|
|
|
224
321
|
{/* Search */}
|
|
225
|
-
{showFilesystem && (
|
|
322
|
+
{showFilesystem && !previewFile && (
|
|
226
323
|
<div className="relative flex-1 min-w-[200px]">
|
|
227
324
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
228
325
|
<Input
|
|
@@ -236,9 +333,22 @@ export function PodFilesystemModal({
|
|
|
236
333
|
</div>
|
|
237
334
|
|
|
238
335
|
{/* Content */}
|
|
239
|
-
|
|
336
|
+
{previewFile ? (
|
|
337
|
+
<div className="flex-1 min-h-0 flex flex-col">
|
|
338
|
+
<PodFilePreview
|
|
339
|
+
key={previewFile.path}
|
|
340
|
+
namespace={namespace}
|
|
341
|
+
podName={podName}
|
|
342
|
+
container={selectedContainer}
|
|
343
|
+
filePath={previewFile.path}
|
|
344
|
+
fileName={previewFile.name}
|
|
345
|
+
onDownload={downloadPreviewFile}
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
) : (
|
|
349
|
+
<div className="flex-1 min-h-0 overflow-y-auto p-4">
|
|
240
350
|
{/* Loading */}
|
|
241
|
-
{isLoading && <PaneLoader label="Loading files…" className="h-
|
|
351
|
+
{isLoading && <PaneLoader label="Loading files…" className="h-full" />}
|
|
242
352
|
|
|
243
353
|
{/* Error */}
|
|
244
354
|
{error && !isLoading && (
|
|
@@ -262,9 +372,11 @@ export function PodFilesystemModal({
|
|
|
262
372
|
podName={podName}
|
|
263
373
|
container={selectedContainer}
|
|
264
374
|
onNavigate={loadDirectory}
|
|
375
|
+
onOpenFile={setPreviewFile}
|
|
265
376
|
/>
|
|
266
377
|
)}
|
|
267
378
|
</div>
|
|
379
|
+
)}
|
|
268
380
|
|
|
269
381
|
{/* Footer with stats */}
|
|
270
382
|
<div className="p-3 border-t border-theme-border text-xs text-theme-text-tertiary flex items-center gap-4 shrink-0">
|
|
@@ -300,9 +412,10 @@ interface PodFileTreeViewProps {
|
|
|
300
412
|
podName: string
|
|
301
413
|
container: string
|
|
302
414
|
onNavigate: (path: string) => void
|
|
415
|
+
onOpenFile: (node: FileNode) => void
|
|
303
416
|
}
|
|
304
417
|
|
|
305
|
-
function PodFileTreeView({ root, searchQuery, namespace, podName, container, onNavigate }: PodFileTreeViewProps) {
|
|
418
|
+
function PodFileTreeView({ root, searchQuery, namespace, podName, container, onNavigate, onOpenFile }: PodFileTreeViewProps) {
|
|
306
419
|
const filteredRoot = useMemo(() => {
|
|
307
420
|
if (!searchQuery.trim()) return root
|
|
308
421
|
return filterTree(root, searchQuery.toLowerCase())
|
|
@@ -326,6 +439,7 @@ function PodFileTreeView({ root, searchQuery, namespace, podName, container, onN
|
|
|
326
439
|
podName={podName}
|
|
327
440
|
container={container}
|
|
328
441
|
onNavigate={onNavigate}
|
|
442
|
+
onOpenFile={onOpenFile}
|
|
329
443
|
/>
|
|
330
444
|
))}
|
|
331
445
|
</div>
|
|
@@ -338,72 +452,48 @@ interface PodFileTreeNodeProps {
|
|
|
338
452
|
podName: string
|
|
339
453
|
container: string
|
|
340
454
|
onNavigate: (path: string) => void
|
|
455
|
+
onOpenFile: (node: FileNode) => void
|
|
341
456
|
}
|
|
342
457
|
|
|
343
|
-
function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: PodFileTreeNodeProps) {
|
|
458
|
+
function PodFileTreeNode({ node, namespace, podName, container, onNavigate, onOpenFile }: PodFileTreeNodeProps) {
|
|
344
459
|
const [downloading, setDownloading] = useState(false)
|
|
345
|
-
const
|
|
460
|
+
const toast = useToast()
|
|
346
461
|
const isDir = node.type === 'dir'
|
|
347
462
|
const isSymlink = node.type === 'symlink'
|
|
348
|
-
|
|
463
|
+
// Size is the one preview rejection the listing can see coming. Whether a
|
|
464
|
+
// file is binary or readable is only known once its bytes are read.
|
|
465
|
+
const tooLarge = !isDir && (node.size ?? 0) > PREVIEW_BYTE_CAP
|
|
349
466
|
|
|
350
467
|
const handleDownload = async (e: React.MouseEvent) => {
|
|
351
468
|
e.stopPropagation()
|
|
352
469
|
if (downloading) return
|
|
353
|
-
|
|
354
470
|
setDownloading(true)
|
|
355
471
|
try {
|
|
356
|
-
|
|
357
|
-
const savedPath = await savePodFileToDisk(namespace, podName, container, node.path)
|
|
358
|
-
showSuccess(
|
|
359
|
-
'File saved',
|
|
360
|
-
savedPath,
|
|
361
|
-
{
|
|
362
|
-
label: 'Show in Finder',
|
|
363
|
-
icon: createElement(FolderOpen, { className: 'w-3.5 h-3.5' }),
|
|
364
|
-
onClick: () => openFolder(savedPath),
|
|
365
|
-
},
|
|
366
|
-
() => openFile(savedPath),
|
|
367
|
-
)
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const params = new URLSearchParams()
|
|
372
|
-
params.set('container', container)
|
|
373
|
-
params.set('path', node.path)
|
|
374
|
-
|
|
375
|
-
const response = await fetch(apiUrl(`/pods/${namespace}/${podName}/files/download?${params.toString()}`), {
|
|
376
|
-
credentials: getCredentialsMode(),
|
|
377
|
-
headers: getAuthHeaders(),
|
|
378
|
-
})
|
|
379
|
-
if (!response.ok) {
|
|
380
|
-
const err = await response.json().catch(() => ({ error: 'Download failed' }))
|
|
381
|
-
throw new Error(err.error || `HTTP ${response.status}`)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const blob = await response.blob()
|
|
385
|
-
await downloadBlob(blob, node.name)
|
|
386
|
-
} catch (err) {
|
|
387
|
-
const message = err instanceof Error ? err.message : String(err)
|
|
388
|
-
if (message !== 'cancelled') {
|
|
389
|
-
showError(`Could not download ${node.name}`, message)
|
|
390
|
-
}
|
|
472
|
+
await downloadPodFile(namespace, podName, container, node, toast)
|
|
391
473
|
} finally {
|
|
392
474
|
setDownloading(false)
|
|
393
475
|
}
|
|
394
476
|
}
|
|
395
477
|
|
|
478
|
+
// Directories open in place; anything else opens in the viewer, which
|
|
479
|
+
// reports a symlink to a directory as "not a regular file" itself.
|
|
396
480
|
const handleClick = () => {
|
|
397
|
-
if (isDir)
|
|
398
|
-
|
|
399
|
-
}
|
|
481
|
+
if (isDir) onNavigate(node.path)
|
|
482
|
+
else if (!tooLarge) onOpenFile(node)
|
|
400
483
|
}
|
|
401
484
|
|
|
402
485
|
return (
|
|
486
|
+
<Tooltip
|
|
487
|
+
content={`Too large to preview (${formatBytes(node.size ?? 0)}) — download instead`}
|
|
488
|
+
disabled={!tooLarge}
|
|
489
|
+
wrapperClassName="flex w-full"
|
|
490
|
+
position="bottom"
|
|
491
|
+
>
|
|
403
492
|
<div
|
|
404
493
|
className={clsx(
|
|
405
|
-
'flex items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
|
|
406
|
-
isDir && 'font-medium
|
|
494
|
+
'flex flex-1 min-w-0 items-center gap-1 py-0.5 px-1 rounded hover:bg-theme-elevated',
|
|
495
|
+
isDir && 'font-medium',
|
|
496
|
+
tooLarge ? 'cursor-default' : 'cursor-pointer'
|
|
407
497
|
)}
|
|
408
498
|
onClick={handleClick}
|
|
409
499
|
>
|
|
@@ -415,7 +505,7 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
|
|
|
415
505
|
<File className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
416
506
|
)}
|
|
417
507
|
|
|
418
|
-
<span className=
|
|
508
|
+
<span className={clsx('truncate flex-1', tooLarge ? 'text-theme-text-secondary' : 'text-theme-text-primary')}>{node.name}</span>
|
|
419
509
|
|
|
420
510
|
{isSymlink && node.linkTarget && (
|
|
421
511
|
<span className="text-xs text-cyan-400 truncate max-w-48">
|
|
@@ -435,7 +525,7 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
|
|
|
435
525
|
</span>
|
|
436
526
|
)}
|
|
437
527
|
|
|
438
|
-
{
|
|
528
|
+
{!isDir && (
|
|
439
529
|
<Tooltip content="Download file" wrapperClassName="ml-1">
|
|
440
530
|
<button
|
|
441
531
|
onClick={handleDownload}
|
|
@@ -451,6 +541,6 @@ function PodFileTreeNode({ node, namespace, podName, container, onNavigate }: Po
|
|
|
451
541
|
</Tooltip>
|
|
452
542
|
)}
|
|
453
543
|
</div>
|
|
544
|
+
</Tooltip>
|
|
454
545
|
)
|
|
455
546
|
}
|
|
456
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo, useCallback, useEffect } from 'react'
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useQuery } from '@tanstack/react-query'
|
|
4
|
-
import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
|
|
4
|
+
import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, isKindSyncFailed, isStillLoadingError, useCapabilities, useNamespaceCapabilities, useResources, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
|
|
5
5
|
import { isBadgeWorthy } from '../../utils/auditBadges'
|
|
6
6
|
import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
|
|
@@ -143,7 +143,18 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
143
143
|
}
|
|
144
144
|
},
|
|
145
145
|
staleTime: 10000,
|
|
146
|
-
|
|
146
|
+
// SSE invalidation isn't running while connecting, and mid-sync counts
|
|
147
|
+
// are what unlatch guarded kinds as their informers finish — poll fast
|
|
148
|
+
// during the shell, settle to the safety net once connected.
|
|
149
|
+
refetchInterval: connection.state === 'connecting' ? 3000 : 60000,
|
|
150
|
+
// During the first seconds of the progressive shell the endpoint 503s
|
|
151
|
+
// (cluster_connecting) until the mid-sync cache handle exists; keep the
|
|
152
|
+
// query pending rather than parking it in error state, which would
|
|
153
|
+
// unlatch the large-list guard at the connected flip.
|
|
154
|
+
retry: (failureCount: number, error: Error) =>
|
|
155
|
+
isStillLoadingError(error) ? true : failureCount < 3,
|
|
156
|
+
retryDelay: (failureCount: number, error: Error) =>
|
|
157
|
+
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
147
158
|
})
|
|
148
159
|
|
|
149
160
|
// Determine if selected kind is a CRD (only CRDs should send ?group= to backend)
|
|
@@ -200,8 +211,29 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
200
211
|
const isSelectedKindGuarded = selectedCountKey !== '' && LARGE_RESOURCE_LIST_GUARD_KEYS.has(selectedCountKey)
|
|
201
212
|
const selectedKindSummaryServed = SUMMARY_LIST_KINDS.has(selectedCountKey)
|
|
202
213
|
const selectedKindRowLimit = selectedKindSummaryServed ? SUMMARY_LIST_LIMIT : LARGE_RESOURCE_LIST_LIMIT
|
|
203
|
-
|
|
204
|
-
|
|
214
|
+
// While still 'connecting', /resource-counts is unavailable — its error
|
|
215
|
+
// must NOT unlatch the large-list guard, or a huge Pods list could fetch
|
|
216
|
+
// unguarded on exactly the clusters the guard protects. Counts arrive
|
|
217
|
+
// right after 'connected' and settle the guard then.
|
|
218
|
+
const syncShellActive = connection.state === 'connecting'
|
|
219
|
+
// A kind whose sync deadline fired is unavailable-with-a-reason: hand it to
|
|
220
|
+
// the list endpoint, whose 503 kind_sync_failed renders the honest error —
|
|
221
|
+
// the guard would otherwise trap it on "count unavailable" forever. Safe to
|
|
222
|
+
// unblock: the server serves no rows for a failed kind.
|
|
223
|
+
const selectedCountFailedSync =
|
|
224
|
+
selectedCountUnavailable && countsData?.reasons?.[selectedCountKey] === 'kind_sync_failed'
|
|
225
|
+
// A guarded kind whose informer hasn't finished reports unavailable with a
|
|
226
|
+
// kind_sync_pending reason — "count not known YET", so keep the loading
|
|
227
|
+
// state. Keyed on the reason, not on connection.state: deferred kinds sync
|
|
228
|
+
// after connect. Reasonless unavailable stays a real verification failure
|
|
229
|
+
// and blocks the view as before.
|
|
230
|
+
const selectedCountPendingSync =
|
|
231
|
+
(selectedCountUnavailable && countsData?.reasons?.[selectedCountKey] === 'kind_sync_pending') ||
|
|
232
|
+
(syncShellActive && selectedCountUnavailable && !selectedCountFailedSync)
|
|
233
|
+
const waitingForGuardCount = isSelectedKindGuarded &&
|
|
234
|
+
((!countsData && (!countsIsError || syncShellActive)) || selectedCountPendingSync)
|
|
235
|
+
const largeListBlocked = isSelectedKindGuarded && countsData != null && !selectedCountPendingSync && !selectedCountFailedSync &&
|
|
236
|
+
(selectedCountUnavailable || (selectedCountKnown && (selectedCount ?? 0) > selectedKindRowLimit))
|
|
205
237
|
const selectedKindQueryBlocked = waitingForGuardCount || largeListBlocked
|
|
206
238
|
const podCount = countsData?.counts.Pod
|
|
207
239
|
const podCountKnown = hasResourceCount(countsData?.counts, 'Pod')
|
|
@@ -285,9 +317,15 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
285
317
|
staleTime: 30000,
|
|
286
318
|
refetchInterval: 120000, // Safety net — SSE k8s_event drives near-real-time invalidation
|
|
287
319
|
retry: (failureCount: number, error: Error) => {
|
|
288
|
-
if (isForbiddenError(error)) return false
|
|
320
|
+
if (isForbiddenError(error) || isKindSyncFailed(error)) return false
|
|
321
|
+
// Cluster still connecting, or this kind's informer still completing
|
|
322
|
+
// its initial sync: keep the query in its loading state and retry —
|
|
323
|
+
// the header's sync-progress label explains the wait.
|
|
324
|
+
if (isStillLoadingError(error)) return true
|
|
289
325
|
return failureCount < 3
|
|
290
326
|
},
|
|
327
|
+
retryDelay: (failureCount: number, error: Error) =>
|
|
328
|
+
isStillLoadingError(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),
|
|
291
329
|
})
|
|
292
330
|
|
|
293
331
|
// Map to ResourceQueryResult shape
|
|
@@ -398,7 +436,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
398
436
|
resourceUnavailable={countsData?.unavailable}
|
|
399
437
|
selectedKindQuery={selectedKindQueryResult}
|
|
400
438
|
printerTable={selectedKindQueryBlocked ? null : selectedKindQuery.data?.printerTable ?? null}
|
|
401
|
-
connectionState={connection.state}
|
|
439
|
+
connectionState={connection.state === 'connecting' && connection.syncStatus ? 'syncing' : connection.state}
|
|
402
440
|
largeListGuard={largeListGuard}
|
|
403
441
|
onSelectedKindChange={setSelectedKind}
|
|
404
442
|
topPodMetrics={topPodMetrics}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NamespaceRenderer as BaseNamespaceRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/NamespaceRenderer'
|
|
2
2
|
import type { ResourceRef } from '@skyhook-io/k8s-ui'
|
|
3
3
|
import { useRBACNamespace } from '../../../api/rbac'
|
|
4
|
-
import { useNamespaceQuotas } from '../../../api/quotas'
|
|
4
|
+
import { useNamespaceLimitRanges, useNamespaceQuotas } from '../../../api/quotas'
|
|
5
5
|
import { isForbiddenError } from '../../../api/client'
|
|
6
6
|
|
|
7
7
|
interface NamespaceRendererProps {
|
|
@@ -17,6 +17,9 @@ export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps)
|
|
|
17
17
|
// RBAC sections). Surface other errors (500/503) so a quota-constrained
|
|
18
18
|
// namespace doesn't silently render as quota-free.
|
|
19
19
|
const quotaErr = quotaError && !isForbiddenError(quotaError) ? (quotaError as Error) : null
|
|
20
|
+
// LimitRanges keep their 403: the section states the denial rather than
|
|
21
|
+
// hiding, because "no rules apply here" is the wrong thing to imply.
|
|
22
|
+
const { data: limitRangeData, isLoading: limitRangeLoading, error: limitRangeError } = useNamespaceLimitRanges(name, !!name)
|
|
20
23
|
return (
|
|
21
24
|
<BaseNamespaceRenderer
|
|
22
25
|
data={data}
|
|
@@ -25,6 +28,9 @@ export function NamespaceRenderer({ data, onNavigate }: NamespaceRendererProps)
|
|
|
25
28
|
rbacError={error as Error | null}
|
|
26
29
|
quotaData={quotaData}
|
|
27
30
|
quotaError={quotaErr}
|
|
31
|
+
limitRangeData={limitRangeData}
|
|
32
|
+
limitRangeLoading={limitRangeLoading}
|
|
33
|
+
limitRangeError={limitRangeError}
|
|
28
34
|
onNavigate={onNavigate}
|
|
29
35
|
/>
|
|
30
36
|
)
|
|
@@ -7,6 +7,7 @@ import { useCapabilitiesContext, useNamespacedCapabilities, useIsLocalDeployment
|
|
|
7
7
|
import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodEnvironment, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus, useRevealPodEnvironment } from '../../../api/client'
|
|
8
8
|
import { useRBACSubject } from '../../../api/rbac'
|
|
9
9
|
import { usePolicyResource } from '../../../api/policy'
|
|
10
|
+
import { podLimitRangeNames, useNamespaceLimitRanges } from '../../../api/quotas'
|
|
10
11
|
import { podAwaitsScheduling } from '../../capacity/podDemandGate'
|
|
11
12
|
import { PortForwardInlineButton } from '../../portforward/PortForwardButton'
|
|
12
13
|
import { ImageFilesystemModal } from '../ImageFilesystemModal'
|
|
@@ -79,6 +80,8 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
|
|
|
79
80
|
'pods', namespace ?? '', podName, !!namespace && !!podName,
|
|
80
81
|
)
|
|
81
82
|
|
|
83
|
+
const { data: limitRanges, isSuccess: limitRangesReady } = useNamespaceLimitRanges(namespace ?? '', !!namespace)
|
|
84
|
+
|
|
82
85
|
return (
|
|
83
86
|
<BasePodRenderer
|
|
84
87
|
data={data}
|
|
@@ -110,6 +113,7 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
|
|
|
110
113
|
policyData={policyData ?? null}
|
|
111
114
|
policyLoading={policyLoading}
|
|
112
115
|
policyError={policyError as Error | null}
|
|
116
|
+
namespaceLimitRangeNames={limitRangesReady ? podLimitRangeNames(limitRanges) : undefined}
|
|
113
117
|
canExec={canExec}
|
|
114
118
|
canViewLogs={canViewLogs}
|
|
115
119
|
canPortForward={showPortForward}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { RolloutRenderer as BaseRolloutRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/RolloutRenderer'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useRolloutAction,
|
|
4
|
+
useRolloutAnalysisRuns,
|
|
5
|
+
useRolloutCapabilities,
|
|
6
|
+
useWorkloadRevisions,
|
|
7
|
+
useWorkloadPods,
|
|
8
|
+
type RolloutAction,
|
|
9
|
+
} from '../../../api/client'
|
|
3
10
|
|
|
4
11
|
interface RolloutRendererProps {
|
|
5
12
|
data: any
|
|
@@ -11,6 +18,9 @@ export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
|
|
|
11
18
|
const name = data?.metadata?.name ?? ''
|
|
12
19
|
const { data: capabilities } = useRolloutCapabilities(namespace, name)
|
|
13
20
|
const action = useRolloutAction()
|
|
21
|
+
const { data: analysisRunsResponse, error: analysisRunsError } = useRolloutAnalysisRuns(namespace, name)
|
|
22
|
+
const { data: revisions } = useWorkloadRevisions('rollouts', namespace, name)
|
|
23
|
+
const { data: podsResponse } = useWorkloadPods('rollouts', namespace, name)
|
|
14
24
|
|
|
15
25
|
return (
|
|
16
26
|
<BaseRolloutRenderer
|
|
@@ -19,6 +29,10 @@ export function RolloutRenderer({ data, onNavigate }: RolloutRendererProps) {
|
|
|
19
29
|
capabilities={capabilities}
|
|
20
30
|
onAction={(next: RolloutAction) => action.mutate({ action: next, namespace, name })}
|
|
21
31
|
pendingAction={action.isPending ? action.variables?.action ?? null : null}
|
|
32
|
+
analysisRunHistory={analysisRunsResponse?.items}
|
|
33
|
+
analysisRunHistoryError={analysisRunsError}
|
|
34
|
+
revisions={revisions}
|
|
35
|
+
pods={podsResponse?.pods}
|
|
22
36
|
/>
|
|
23
37
|
)
|
|
24
38
|
}
|
|
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
|
|
3
3
|
import { useScaleWorkload, fetchJSON } from '../../../api/client'
|
|
4
4
|
import { useRBACSubject } from '../../../api/rbac'
|
|
5
5
|
import { usePolicyResource } from '../../../api/policy'
|
|
6
|
+
import { podLimitRangeNames, useNamespaceLimitRanges } from '../../../api/quotas'
|
|
6
7
|
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
7
8
|
import { kindToPlural, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
8
9
|
import type { Relationships, ResourceRef, ResourceWithRelationships, WorkloadPodInfo } from '../../../types'
|
|
@@ -49,6 +50,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workl
|
|
|
49
50
|
const { data: policyData, isLoading: policyLoading, error: policyError } = usePolicyResource(
|
|
50
51
|
kindToPlural(kind), namespace, metadata.name || '', !!namespace && !!metadata.name,
|
|
51
52
|
)
|
|
53
|
+
const { data: limitRanges, isSuccess: limitRangesReady } = useNamespaceLimitRanges(namespace, !!namespace)
|
|
52
54
|
const hpaRefs = (scaleBlockedBy ?? []).filter(ref => {
|
|
53
55
|
const refKind = ref.kind.toLowerCase()
|
|
54
56
|
return refKind === 'horizontalpodautoscaler' || refKind === 'hpa'
|
|
@@ -96,6 +98,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy, workl
|
|
|
96
98
|
policyData={policyData ?? null}
|
|
97
99
|
policyLoading={policyLoading}
|
|
98
100
|
policyError={policyError as Error | null}
|
|
101
|
+
namespaceLimitRangeNames={limitRangesReady ? podLimitRangeNames(limitRanges) : undefined}
|
|
99
102
|
scaleBlockedBy={scaleBlockedBy}
|
|
100
103
|
workloadPods={workloadPods}
|
|
101
104
|
scalerDiagnostics={scalerDiagnostics}
|
|
@@ -799,6 +799,27 @@ function FitWhy({ label, row }: { label: string; row?: RightsizingRow }) {
|
|
|
799
799
|
}
|
|
800
800
|
|
|
801
801
|
function EvidenceNote({ row }: { row: RightsizingRow }) {
|
|
802
|
+
const primary = primaryEvidenceNote(row)
|
|
803
|
+
// A denied pod read qualifies whichever reason applies rather than standing in
|
|
804
|
+
// for it: a row can both be unable to check its pods and have a limit conflict,
|
|
805
|
+
// and hiding either one behind the other loses something the reader needs.
|
|
806
|
+
const inventory =
|
|
807
|
+
row.liveInventoryUnavailable && row.resource === 'memory' ? (
|
|
808
|
+
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
809
|
+
Radar could not read this workload's pods, so it could not check them for
|
|
810
|
+
out-of-memory restarts.
|
|
811
|
+
</p>
|
|
812
|
+
) : null
|
|
813
|
+
if (!primary && !inventory) return null
|
|
814
|
+
return (
|
|
815
|
+
<>
|
|
816
|
+
{primary}
|
|
817
|
+
{inventory}
|
|
818
|
+
</>
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function primaryEvidenceNote(row: RightsizingRow) {
|
|
802
823
|
if (row.hpaManaged)
|
|
803
824
|
return (
|
|
804
825
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|