@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,91 @@
|
|
|
1
|
+
import { X, GitCompare } from 'lucide-react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
|
|
4
|
+
interface ManifestDiffViewerProps {
|
|
5
|
+
diff: string
|
|
6
|
+
isLoading: boolean
|
|
7
|
+
revision1: number
|
|
8
|
+
revision2: number
|
|
9
|
+
onClose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onClose }: ManifestDiffViewerProps) {
|
|
13
|
+
if (isLoading) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
16
|
+
Computing diff...
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!diff) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="p-4">
|
|
24
|
+
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
25
|
+
<GitCompare className="w-8 h-8 text-theme-text-disabled" />
|
|
26
|
+
<span>No differences found</span>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="p-4">
|
|
34
|
+
<div className="flex items-center justify-between mb-3">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<GitCompare className="w-4 h-4 text-theme-text-secondary" />
|
|
37
|
+
<span className="text-sm font-medium text-theme-text-secondary">
|
|
38
|
+
Comparing Revision {revision1} → {revision2}
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
<button
|
|
42
|
+
onClick={onClose}
|
|
43
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
44
|
+
>
|
|
45
|
+
<X className="w-3.5 h-3.5" />
|
|
46
|
+
Close
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="rounded-lg overflow-hidden max-h-[calc(100vh-300px)] overflow-auto bg-theme-base/50 font-mono text-xs">
|
|
51
|
+
<div className="p-3">
|
|
52
|
+
{diff.split('\n').map((line, index) => (
|
|
53
|
+
<DiffLine key={index} line={line} />
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{/* Legend */}
|
|
59
|
+
<div className="flex items-center gap-4 mt-3 text-xs text-theme-text-tertiary">
|
|
60
|
+
<div className="flex items-center gap-1">
|
|
61
|
+
<span className="w-3 h-3 bg-red-500/20 border border-red-500/50 rounded" />
|
|
62
|
+
<span>Removed</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className="flex items-center gap-1">
|
|
65
|
+
<span className="w-3 h-3 bg-green-500/20 border border-green-500/50 rounded" />
|
|
66
|
+
<span>Added</span>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function DiffLine({ line }: { line: string }) {
|
|
74
|
+
const isAddition = line.startsWith('+') && !line.startsWith('+++')
|
|
75
|
+
const isRemoval = line.startsWith('-') && !line.startsWith('---')
|
|
76
|
+
const isHeader = line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
className={clsx(
|
|
81
|
+
'whitespace-pre',
|
|
82
|
+
isAddition && 'bg-green-500/10 text-green-400',
|
|
83
|
+
isRemoval && 'bg-red-500/10 text-red-400',
|
|
84
|
+
isHeader && 'text-theme-text-tertiary font-bold',
|
|
85
|
+
!isAddition && !isRemoval && !isHeader && 'text-theme-text-secondary'
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{line || ' '}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Copy, Check, Code } from 'lucide-react'
|
|
2
|
+
import { CodeViewer } from '../ui/CodeViewer'
|
|
3
|
+
|
|
4
|
+
interface ManifestViewerProps {
|
|
5
|
+
manifest: string
|
|
6
|
+
isLoading: boolean
|
|
7
|
+
revision?: number
|
|
8
|
+
onCopy: (text: string) => void
|
|
9
|
+
copied: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ManifestViewer({ manifest, isLoading, revision, onCopy, copied }: ManifestViewerProps) {
|
|
13
|
+
if (isLoading) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
16
|
+
Loading manifest...
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!manifest) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
24
|
+
<Code className="w-8 h-8 text-theme-text-disabled" />
|
|
25
|
+
<span>No manifest available</span>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lineCount = manifest.split('\n').length
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="p-4">
|
|
34
|
+
<div className="flex items-center justify-between mb-3">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<span className="text-sm font-medium text-theme-text-secondary">Rendered Manifest</span>
|
|
37
|
+
{revision && (
|
|
38
|
+
<span className="badge bg-theme-elevated text-theme-text-secondary">
|
|
39
|
+
Revision {revision}
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
<span className="text-xs text-theme-text-tertiary">{lineCount} lines</span>
|
|
43
|
+
</div>
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => onCopy(manifest)}
|
|
46
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
47
|
+
>
|
|
48
|
+
{copied ? <Check className="w-3.5 h-3.5 text-green-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
49
|
+
Copy
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<CodeViewer
|
|
54
|
+
code={manifest}
|
|
55
|
+
language="yaml"
|
|
56
|
+
showLineNumbers
|
|
57
|
+
maxHeight="calc(100vh - 300px)"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import { Link2, ExternalLink, AlertCircle, Terminal, FileText, Plug, X, Loader2 } from 'lucide-react'
|
|
3
|
+
import { getResourceIcon } from '../../utils/resource-icons'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
import type { HelmOwnedResource } from '../../types'
|
|
6
|
+
import type { NavigateToResource } from '../../utils/navigation'
|
|
7
|
+
import { kindToPlural } from '../../utils/navigation'
|
|
8
|
+
import { getResourceStatusColor, SEVERITY_BADGE } from '../../utils/badge-colors'
|
|
9
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
10
|
+
import { useOpenTerminal, useOpenLogs } from '../dock'
|
|
11
|
+
import { useStartPortForward } from '../portforward/PortForwardManager'
|
|
12
|
+
import { useAvailablePorts } from '../../api/client'
|
|
13
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
14
|
+
import { useNamespacedCapabilities } from '../../contexts/CapabilitiesContext'
|
|
15
|
+
|
|
16
|
+
interface OwnedResourcesProps {
|
|
17
|
+
resources: HelmOwnedResource[]
|
|
18
|
+
onNavigate?: NavigateToResource
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getIconForKind(kind: string) {
|
|
22
|
+
return getResourceIcon(kind)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Group resources by kind
|
|
26
|
+
function groupByKind(resources: HelmOwnedResource[]): Map<string, HelmOwnedResource[]> {
|
|
27
|
+
const groups = new Map<string, HelmOwnedResource[]>()
|
|
28
|
+
for (const resource of resources) {
|
|
29
|
+
const existing = groups.get(resource.kind) || []
|
|
30
|
+
existing.push(resource)
|
|
31
|
+
groups.set(resource.kind, existing)
|
|
32
|
+
}
|
|
33
|
+
return groups
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Health status types
|
|
37
|
+
type HealthFilter = 'all' | 'healthy' | 'warning' | 'error'
|
|
38
|
+
|
|
39
|
+
// Determine health status of a resource
|
|
40
|
+
function getResourceHealth(resource: HelmOwnedResource): 'healthy' | 'warning' | 'error' | 'unknown' {
|
|
41
|
+
// An issue field always indicates a problem regardless of status
|
|
42
|
+
if (resource.issue) return 'error'
|
|
43
|
+
const status = resource.status
|
|
44
|
+
if (!status) return 'unknown'
|
|
45
|
+
const s = status.toLowerCase()
|
|
46
|
+
if (['running', 'active', 'succeeded', 'bound', 'available'].includes(s)) return 'healthy'
|
|
47
|
+
if (['pending', 'progressing', 'scaled to 0', 'suspended', 'creating'].includes(s)) return 'warning'
|
|
48
|
+
if (['failed', 'error', 'crashloopbackoff', 'imagepullbackoff', 'evicted', 'terminating'].includes(s)) return 'error'
|
|
49
|
+
return 'unknown'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compute health summary
|
|
53
|
+
function computeHealthSummary(resources: HelmOwnedResource[]) {
|
|
54
|
+
let healthy = 0
|
|
55
|
+
let warning = 0
|
|
56
|
+
let error = 0
|
|
57
|
+
let unknown = 0
|
|
58
|
+
|
|
59
|
+
for (const r of resources) {
|
|
60
|
+
const health = getResourceHealth(r)
|
|
61
|
+
if (health === 'healthy') healthy++
|
|
62
|
+
else if (health === 'warning') warning++
|
|
63
|
+
else if (health === 'error') error++
|
|
64
|
+
else unknown++
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { healthy, warning, error, unknown, total: resources.length }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function OwnedResources({ resources, onNavigate }: OwnedResourcesProps) {
|
|
71
|
+
const [healthFilter, setHealthFilter] = useState<HealthFilter>('all')
|
|
72
|
+
|
|
73
|
+
if (!resources || resources.length === 0) {
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
76
|
+
<Link2 className="w-8 h-8 text-theme-text-disabled" />
|
|
77
|
+
<span>No owned resources</span>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const health = computeHealthSummary(resources)
|
|
83
|
+
|
|
84
|
+
// Filter resources by health status
|
|
85
|
+
const filteredResources = healthFilter === 'all'
|
|
86
|
+
? resources
|
|
87
|
+
: resources.filter(r => getResourceHealth(r) === healthFilter)
|
|
88
|
+
|
|
89
|
+
const grouped = groupByKind(filteredResources)
|
|
90
|
+
|
|
91
|
+
const handleFilterClick = (filter: HealthFilter) => {
|
|
92
|
+
setHealthFilter(prev => prev === filter ? 'all' : filter)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="p-4 space-y-4">
|
|
97
|
+
{/* Health summary - clickable badges */}
|
|
98
|
+
<div className="flex items-center justify-between">
|
|
99
|
+
<div className="text-sm text-theme-text-secondary">
|
|
100
|
+
{healthFilter === 'all' ? (
|
|
101
|
+
<>{resources.length} resource{resources.length !== 1 ? 's' : ''} created by this release</>
|
|
102
|
+
) : (
|
|
103
|
+
<span className="flex items-center gap-2">
|
|
104
|
+
Showing {filteredResources.length} of {resources.length} resources
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => setHealthFilter('all')}
|
|
107
|
+
className="p-0.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
108
|
+
title="Clear filter"
|
|
109
|
+
>
|
|
110
|
+
<X className="w-3.5 h-3.5" />
|
|
111
|
+
</button>
|
|
112
|
+
</span>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex items-center gap-2">
|
|
116
|
+
{health.healthy > 0 && (
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => handleFilterClick('healthy')}
|
|
119
|
+
className={clsx(
|
|
120
|
+
'flex items-center gap-1 px-2 py-0.5 text-xs rounded transition-all',
|
|
121
|
+
SEVERITY_BADGE.success,
|
|
122
|
+
healthFilter === 'healthy' && 'ring-2 ring-green-400/50 ring-offset-1 ring-offset-theme-surface'
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
{health.healthy} healthy
|
|
126
|
+
</button>
|
|
127
|
+
)}
|
|
128
|
+
{health.warning > 0 && (
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => handleFilterClick('warning')}
|
|
131
|
+
className={clsx(
|
|
132
|
+
'flex items-center gap-1 px-2 py-0.5 text-xs rounded transition-all',
|
|
133
|
+
SEVERITY_BADGE.warning,
|
|
134
|
+
healthFilter === 'warning' && 'ring-2 ring-amber-400/50 ring-offset-1 ring-offset-theme-surface'
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{health.warning} pending
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
{health.error > 0 && (
|
|
141
|
+
<button
|
|
142
|
+
onClick={() => handleFilterClick('error')}
|
|
143
|
+
className={clsx(
|
|
144
|
+
'flex items-center gap-1 px-2 py-0.5 text-xs rounded transition-all',
|
|
145
|
+
SEVERITY_BADGE.error,
|
|
146
|
+
healthFilter === 'error' && 'ring-2 ring-red-400/50 ring-offset-1 ring-offset-theme-surface'
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
{health.error} failed
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{filteredResources.length === 0 ? (
|
|
156
|
+
<div className="flex flex-col items-center justify-center h-32 text-theme-text-tertiary gap-2">
|
|
157
|
+
<AlertCircle className="w-8 h-8 text-theme-text-disabled" />
|
|
158
|
+
<span>No resources match the selected filter</span>
|
|
159
|
+
<button
|
|
160
|
+
onClick={() => setHealthFilter('all')}
|
|
161
|
+
className="text-xs text-blue-400 hover:text-blue-300"
|
|
162
|
+
>
|
|
163
|
+
Clear filter
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
Array.from(grouped.entries()).map(([kind, items]) => {
|
|
168
|
+
const Icon = getIconForKind(kind)
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div key={kind} className="bg-theme-elevated/30 rounded-lg p-3">
|
|
172
|
+
<div className="flex items-center gap-2 mb-2">
|
|
173
|
+
<Icon className="w-4 h-4 text-theme-text-secondary" />
|
|
174
|
+
<span className="text-sm font-medium text-theme-text-secondary">{kind}</span>
|
|
175
|
+
<span className="text-xs text-theme-text-tertiary">({items.length})</span>
|
|
176
|
+
</div>
|
|
177
|
+
<div className="space-y-1">
|
|
178
|
+
{items.map((resource, idx) => (
|
|
179
|
+
<ResourceItem
|
|
180
|
+
key={`${resource.namespace}-${resource.name}-${idx}`}
|
|
181
|
+
resource={resource}
|
|
182
|
+
onNavigate={onNavigate}
|
|
183
|
+
/>
|
|
184
|
+
))}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface ResourceItemProps {
|
|
195
|
+
resource: HelmOwnedResource
|
|
196
|
+
onNavigate?: NavigateToResource
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ResourceItem({ resource, onNavigate }: ResourceItemProps) {
|
|
200
|
+
const canNavigate = !!onNavigate
|
|
201
|
+
const isPod = resource.kind.toLowerCase() === 'pod'
|
|
202
|
+
const isService = resource.kind.toLowerCase() === 'service'
|
|
203
|
+
const isRunning = resource.status?.toLowerCase() === 'running'
|
|
204
|
+
|
|
205
|
+
const handleClick = () => {
|
|
206
|
+
if (onNavigate) {
|
|
207
|
+
onNavigate({ kind: kindToPlural(resource.kind), namespace: resource.namespace, name: resource.name })
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const isError = resource.status && ['failed', 'error', 'crashloopbackoff', 'imagepullbackoff', 'evicted'].includes(resource.status.toLowerCase())
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
className={clsx(
|
|
216
|
+
'flex items-center justify-between p-2 rounded text-sm group',
|
|
217
|
+
canNavigate
|
|
218
|
+
? 'cursor-pointer hover:bg-theme-elevated/50'
|
|
219
|
+
: 'bg-theme-surface/50'
|
|
220
|
+
)}
|
|
221
|
+
>
|
|
222
|
+
<div
|
|
223
|
+
onClick={canNavigate ? handleClick : undefined}
|
|
224
|
+
className="flex items-center gap-2 min-w-0 flex-1"
|
|
225
|
+
>
|
|
226
|
+
<span className="text-theme-text-primary truncate">{resource.name}</span>
|
|
227
|
+
{resource.namespace && (
|
|
228
|
+
<span className="text-xs text-theme-text-tertiary shrink-0">{resource.namespace}</span>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
233
|
+
{/* Quick actions for pods */}
|
|
234
|
+
{isPod && (
|
|
235
|
+
<PodQuickActions
|
|
236
|
+
namespace={resource.namespace}
|
|
237
|
+
podName={resource.name}
|
|
238
|
+
isRunning={isRunning}
|
|
239
|
+
/>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Quick actions for services */}
|
|
243
|
+
{isService && (
|
|
244
|
+
<ServiceQuickActions
|
|
245
|
+
namespace={resource.namespace}
|
|
246
|
+
serviceName={resource.name}
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{/* Ready count (e.g., 3/3) */}
|
|
251
|
+
{resource.ready && (
|
|
252
|
+
<span className="text-xs text-theme-text-secondary font-mono">{resource.ready}</span>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{/* Status badge */}
|
|
256
|
+
{resource.status && (
|
|
257
|
+
<span
|
|
258
|
+
className={clsx('badge-sm', getResourceStatusColor(resource.status || ''))}
|
|
259
|
+
title={resource.message || resource.status}
|
|
260
|
+
>
|
|
261
|
+
{resource.status}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Issue summary (e.g., "OOMKilled", "CrashLoopBackOff") */}
|
|
266
|
+
{resource.issue && (
|
|
267
|
+
<span
|
|
268
|
+
className="text-xs text-red-400"
|
|
269
|
+
title={resource.summary || resource.issue}
|
|
270
|
+
>
|
|
271
|
+
{resource.issue}
|
|
272
|
+
</span>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Error icon with message tooltip */}
|
|
276
|
+
{isError && resource.message && !resource.issue && (
|
|
277
|
+
<span title={resource.message}>
|
|
278
|
+
<AlertCircle className="w-3.5 h-3.5 text-red-400" />
|
|
279
|
+
</span>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
{canNavigate && (
|
|
283
|
+
<button
|
|
284
|
+
onClick={handleClick}
|
|
285
|
+
className="p-1 text-theme-text-tertiary opacity-0 group-hover:opacity-100 hover:text-theme-text-primary hover:bg-theme-elevated rounded transition-all"
|
|
286
|
+
title="View details"
|
|
287
|
+
>
|
|
288
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
289
|
+
</button>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Quick actions for pods
|
|
297
|
+
interface PodQuickActionsProps {
|
|
298
|
+
namespace: string
|
|
299
|
+
podName: string
|
|
300
|
+
isRunning: boolean
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function PodQuickActions({ namespace, podName, isRunning }: PodQuickActionsProps) {
|
|
304
|
+
const queryClient = useQueryClient()
|
|
305
|
+
const openTerminal = useOpenTerminal()
|
|
306
|
+
const openLogs = useOpenLogs()
|
|
307
|
+
const startPortForward = useStartPortForward()
|
|
308
|
+
const { data: portsData, isLoading: portsLoading } = useAvailablePorts('pod', namespace, podName)
|
|
309
|
+
|
|
310
|
+
// Capabilities (namespace-scoped: re-checks RBAC if globally denied)
|
|
311
|
+
const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
|
|
312
|
+
|
|
313
|
+
const [isLoadingAction, setIsLoadingAction] = useState(false)
|
|
314
|
+
|
|
315
|
+
// Fetch pod data using React Query cache - shared with resource views
|
|
316
|
+
const fetchPodData = useCallback(async () => {
|
|
317
|
+
return queryClient.fetchQuery({
|
|
318
|
+
queryKey: ['resource', 'pods', namespace, podName],
|
|
319
|
+
queryFn: async () => {
|
|
320
|
+
const response = await fetch(apiUrl(`/resources/pods/${namespace}/${podName}`), {
|
|
321
|
+
credentials: getCredentialsMode(),
|
|
322
|
+
headers: getAuthHeaders(),
|
|
323
|
+
})
|
|
324
|
+
if (!response.ok) throw new Error('Failed to fetch pod')
|
|
325
|
+
return response.json()
|
|
326
|
+
},
|
|
327
|
+
staleTime: 30000,
|
|
328
|
+
})
|
|
329
|
+
}, [queryClient, namespace, podName])
|
|
330
|
+
|
|
331
|
+
const handleOpenTerminal = useCallback(async () => {
|
|
332
|
+
if (!isRunning) return
|
|
333
|
+
setIsLoadingAction(true)
|
|
334
|
+
try {
|
|
335
|
+
const data = await fetchPodData()
|
|
336
|
+
const containers = data.resource?.spec?.containers || []
|
|
337
|
+
if (containers.length > 0) {
|
|
338
|
+
openTerminal({
|
|
339
|
+
namespace,
|
|
340
|
+
podName,
|
|
341
|
+
containerName: containers[0].name,
|
|
342
|
+
containers: containers.map((c: { name: string }) => c.name),
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error('Failed to open terminal:', error)
|
|
347
|
+
} finally {
|
|
348
|
+
setIsLoadingAction(false)
|
|
349
|
+
}
|
|
350
|
+
}, [namespace, podName, isRunning, openTerminal, fetchPodData])
|
|
351
|
+
|
|
352
|
+
const handleOpenLogs = useCallback(async () => {
|
|
353
|
+
setIsLoadingAction(true)
|
|
354
|
+
try {
|
|
355
|
+
const data = await fetchPodData()
|
|
356
|
+
const containers = data.resource?.spec?.containers || []
|
|
357
|
+
openLogs({
|
|
358
|
+
namespace,
|
|
359
|
+
podName,
|
|
360
|
+
containers: containers.map((c: { name: string }) => c.name),
|
|
361
|
+
})
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error('Failed to open logs:', error)
|
|
364
|
+
} finally {
|
|
365
|
+
setIsLoadingAction(false)
|
|
366
|
+
}
|
|
367
|
+
}, [namespace, podName, openLogs, fetchPodData])
|
|
368
|
+
|
|
369
|
+
const handlePortForward = useCallback((port: number) => {
|
|
370
|
+
startPortForward.mutate({
|
|
371
|
+
namespace,
|
|
372
|
+
podName,
|
|
373
|
+
podPort: port,
|
|
374
|
+
})
|
|
375
|
+
}, [namespace, podName, startPortForward])
|
|
376
|
+
|
|
377
|
+
const ports = portsData?.ports || []
|
|
378
|
+
|
|
379
|
+
return (
|
|
380
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
381
|
+
{isLoadingAction && <Loader2 className="w-3.5 h-3.5 animate-spin text-theme-text-tertiary" />}
|
|
382
|
+
|
|
383
|
+
{/* Terminal */}
|
|
384
|
+
{isRunning && canExec && (
|
|
385
|
+
<button
|
|
386
|
+
onClick={(e) => { e.stopPropagation(); handleOpenTerminal() }}
|
|
387
|
+
disabled={isLoadingAction}
|
|
388
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
389
|
+
title="Open terminal"
|
|
390
|
+
>
|
|
391
|
+
<Terminal className="w-3.5 h-3.5" />
|
|
392
|
+
</button>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
{/* Logs */}
|
|
396
|
+
{canViewLogs && (
|
|
397
|
+
<button
|
|
398
|
+
onClick={(e) => { e.stopPropagation(); handleOpenLogs() }}
|
|
399
|
+
disabled={isLoadingAction}
|
|
400
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
401
|
+
title="View logs"
|
|
402
|
+
>
|
|
403
|
+
<FileText className="w-3.5 h-3.5" />
|
|
404
|
+
</button>
|
|
405
|
+
)}
|
|
406
|
+
|
|
407
|
+
{/* Port Forward */}
|
|
408
|
+
{canPortForward && !portsLoading && ports.length > 0 && (
|
|
409
|
+
<button
|
|
410
|
+
onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
|
|
411
|
+
disabled={startPortForward.isPending}
|
|
412
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
413
|
+
title={`Port forward :${ports[0].port}`}
|
|
414
|
+
>
|
|
415
|
+
{startPortForward.isPending ? (
|
|
416
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
417
|
+
) : (
|
|
418
|
+
<Plug className="w-3.5 h-3.5" />
|
|
419
|
+
)}
|
|
420
|
+
</button>
|
|
421
|
+
)}
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Quick actions for services
|
|
427
|
+
interface ServiceQuickActionsProps {
|
|
428
|
+
namespace: string
|
|
429
|
+
serviceName: string
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function ServiceQuickActions({ namespace, serviceName }: ServiceQuickActionsProps) {
|
|
433
|
+
const startPortForward = useStartPortForward()
|
|
434
|
+
const { data: portsData, isLoading: portsLoading } = useAvailablePorts('service', namespace, serviceName)
|
|
435
|
+
const { canPortForward } = useNamespacedCapabilities(namespace)
|
|
436
|
+
|
|
437
|
+
const handlePortForward = useCallback((port: number) => {
|
|
438
|
+
startPortForward.mutate({
|
|
439
|
+
namespace,
|
|
440
|
+
serviceName,
|
|
441
|
+
podPort: port,
|
|
442
|
+
})
|
|
443
|
+
}, [namespace, serviceName, startPortForward])
|
|
444
|
+
|
|
445
|
+
const ports = portsData?.ports || []
|
|
446
|
+
|
|
447
|
+
if (!canPortForward || portsLoading || ports.length === 0) return null
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
451
|
+
<button
|
|
452
|
+
onClick={(e) => { e.stopPropagation(); handlePortForward(ports[0].port) }}
|
|
453
|
+
disabled={startPortForward.isPending}
|
|
454
|
+
className="p-1 text-theme-text-tertiary hover:text-blue-400 hover:bg-blue-500/10 rounded transition-colors disabled:opacity-50"
|
|
455
|
+
title={`Port forward :${ports[0].port}`}
|
|
456
|
+
>
|
|
457
|
+
{startPortForward.isPending ? (
|
|
458
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
459
|
+
) : (
|
|
460
|
+
<Plug className="w-3.5 h-3.5" />
|
|
461
|
+
)}
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
)
|
|
465
|
+
}
|