@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,537 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin } from 'lucide-react'
|
|
4
|
+
import { clsx } from 'clsx'
|
|
5
|
+
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
|
+
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
7
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
8
|
+
|
|
9
|
+
interface Config {
|
|
10
|
+
kubeconfig?: string
|
|
11
|
+
kubeconfigDirs?: string[]
|
|
12
|
+
namespace?: string
|
|
13
|
+
port?: number
|
|
14
|
+
noBrowser?: boolean
|
|
15
|
+
timelineStorage?: 'memory' | 'sqlite'
|
|
16
|
+
timelineDbPath?: string
|
|
17
|
+
historyLimit?: number
|
|
18
|
+
prometheusUrl?: string
|
|
19
|
+
mcp?: boolean | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ConfigResponse {
|
|
23
|
+
file: Config
|
|
24
|
+
effective: Config
|
|
25
|
+
isDesktop: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SettingsDialogProps {
|
|
29
|
+
open: boolean
|
|
30
|
+
onClose: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
34
|
+
const dialogRef = useRef<HTMLDivElement>(null)
|
|
35
|
+
const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
|
|
36
|
+
const [configData, setConfigData] = useState<ConfigResponse | null>(null)
|
|
37
|
+
const [editedConfig, setEditedConfig] = useState<Config>({})
|
|
38
|
+
const [saving, setSaving] = useState(false)
|
|
39
|
+
const [saveMessage, setSaveMessage] = useState<string | null>(null)
|
|
40
|
+
const [configDirty, setConfigDirty] = useState(false)
|
|
41
|
+
const [loadError, setLoadError] = useState<string | null>(null)
|
|
42
|
+
|
|
43
|
+
// Load config on open
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!open) return
|
|
46
|
+
setSaveMessage(null)
|
|
47
|
+
setConfigDirty(false)
|
|
48
|
+
setLoadError(null)
|
|
49
|
+
|
|
50
|
+
fetch(apiUrl('/config'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
|
|
51
|
+
.then((res) => {
|
|
52
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
53
|
+
return res.json()
|
|
54
|
+
})
|
|
55
|
+
.then((data: ConfigResponse) => {
|
|
56
|
+
setConfigData(data)
|
|
57
|
+
setEditedConfig(data.file)
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
console.warn('[settings] Failed to load config:', err)
|
|
61
|
+
setLoadError('Failed to load configuration.')
|
|
62
|
+
})
|
|
63
|
+
}, [open])
|
|
64
|
+
|
|
65
|
+
// ESC key
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!open) return
|
|
68
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
69
|
+
if (e.key === 'Escape') {
|
|
70
|
+
e.stopPropagation()
|
|
71
|
+
onClose()
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
document.addEventListener('keydown', handleKeyDown, true)
|
|
75
|
+
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
|
76
|
+
}, [open, onClose])
|
|
77
|
+
|
|
78
|
+
// Focus trap
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (open && dialogRef.current) {
|
|
81
|
+
dialogRef.current.focus()
|
|
82
|
+
}
|
|
83
|
+
}, [open])
|
|
84
|
+
|
|
85
|
+
const updateConfigField = useCallback(<K extends keyof Config>(field: K, value: Config[K]) => {
|
|
86
|
+
setEditedConfig((prev) => ({ ...prev, [field]: value }))
|
|
87
|
+
setConfigDirty(true)
|
|
88
|
+
setSaveMessage(null)
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
const saveConfig = useCallback(async () => {
|
|
92
|
+
setSaving(true)
|
|
93
|
+
setSaveMessage(null)
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(apiUrl('/config'), {
|
|
96
|
+
method: 'PUT',
|
|
97
|
+
credentials: getCredentialsMode(),
|
|
98
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
99
|
+
body: JSON.stringify(editedConfig),
|
|
100
|
+
})
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const data = await res.json().catch(() => null)
|
|
103
|
+
setSaveMessage(`Error: ${data?.error || res.statusText}`)
|
|
104
|
+
} else {
|
|
105
|
+
setConfigDirty(false)
|
|
106
|
+
setSaveMessage('Saved. Changes take effect on next launch.')
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
setSaveMessage(`Error: ${err}`)
|
|
110
|
+
} finally {
|
|
111
|
+
setSaving(false)
|
|
112
|
+
}
|
|
113
|
+
}, [editedConfig])
|
|
114
|
+
|
|
115
|
+
const resetConfig = useCallback(() => {
|
|
116
|
+
setEditedConfig({})
|
|
117
|
+
setConfigDirty(true)
|
|
118
|
+
setSaveMessage('All fields cleared. Press Save to apply.')
|
|
119
|
+
}, [])
|
|
120
|
+
|
|
121
|
+
if (!shouldRender) return null
|
|
122
|
+
|
|
123
|
+
const isDesktop = configData?.isDesktop ?? false
|
|
124
|
+
|
|
125
|
+
return createPortal(
|
|
126
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
127
|
+
{/* Backdrop */}
|
|
128
|
+
<div
|
|
129
|
+
className={clsx(
|
|
130
|
+
'absolute inset-0 bg-black/60 backdrop-blur-sm',
|
|
131
|
+
TRANSITION_BACKDROP,
|
|
132
|
+
isOpen ? 'opacity-100' : 'opacity-0'
|
|
133
|
+
)}
|
|
134
|
+
onClick={onClose}
|
|
135
|
+
/>
|
|
136
|
+
|
|
137
|
+
{/* Dialog */}
|
|
138
|
+
<div
|
|
139
|
+
ref={dialogRef}
|
|
140
|
+
tabIndex={-1}
|
|
141
|
+
className={clsx(
|
|
142
|
+
'relative bg-theme-surface border border-theme-border shadow-theme-lg w-full outline-none flex flex-col',
|
|
143
|
+
'max-sm:inset-0 max-sm:absolute max-sm:rounded-none max-sm:max-h-full max-sm:border-0',
|
|
144
|
+
'sm:rounded-xl sm:max-w-xl sm:mx-4 sm:max-h-[85vh]',
|
|
145
|
+
TRANSITION_PANEL,
|
|
146
|
+
isOpen ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
|
|
147
|
+
)}
|
|
148
|
+
>
|
|
149
|
+
{/* Header */}
|
|
150
|
+
<div className="flex items-center justify-between p-4 border-b border-theme-border shrink-0">
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<Settings className="w-5 h-5 text-theme-text-secondary" />
|
|
153
|
+
<h2 className="text-lg font-semibold text-theme-text-primary">Settings</h2>
|
|
154
|
+
</div>
|
|
155
|
+
<button
|
|
156
|
+
onClick={onClose}
|
|
157
|
+
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
158
|
+
>
|
|
159
|
+
<X className="w-5 h-5" />
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Content */}
|
|
164
|
+
<div className="overflow-y-auto p-4 flex-1">
|
|
165
|
+
{loadError && (
|
|
166
|
+
<div className="mb-3 px-3 py-2 text-xs text-amber-700 dark:text-amber-300 bg-amber-500/10 border border-amber-500/20 rounded-md">
|
|
167
|
+
{loadError}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
<StartupConfigTab
|
|
171
|
+
config={editedConfig}
|
|
172
|
+
effectiveConfig={configData?.effective}
|
|
173
|
+
isDesktop={isDesktop}
|
|
174
|
+
onChange={updateConfigField}
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Footer */}
|
|
179
|
+
<div className="flex items-center justify-between gap-3 p-4 border-t border-theme-border shrink-0">
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
<button
|
|
182
|
+
onClick={resetConfig}
|
|
183
|
+
disabled={saving}
|
|
184
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-md transition-colors disabled:opacity-50"
|
|
185
|
+
title="Reset all configuration to defaults"
|
|
186
|
+
>
|
|
187
|
+
<RotateCcw className="w-3.5 h-3.5" />
|
|
188
|
+
Reset
|
|
189
|
+
</button>
|
|
190
|
+
{saveMessage && (
|
|
191
|
+
<span className={clsx(
|
|
192
|
+
'text-xs',
|
|
193
|
+
saveMessage.startsWith('Error') ? 'text-red-400' : 'text-green-400'
|
|
194
|
+
)}>
|
|
195
|
+
{saveMessage}
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
<button
|
|
200
|
+
onClick={saveConfig}
|
|
201
|
+
disabled={saving || !configDirty}
|
|
202
|
+
className="flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium btn-brand rounded-md"
|
|
203
|
+
>
|
|
204
|
+
{saving && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
|
205
|
+
Save
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>,
|
|
210
|
+
document.body
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// -- Startup Configuration Tab ------------------------------------------------
|
|
215
|
+
|
|
216
|
+
function StartupConfigTab({
|
|
217
|
+
config,
|
|
218
|
+
effectiveConfig,
|
|
219
|
+
isDesktop,
|
|
220
|
+
onChange,
|
|
221
|
+
}: {
|
|
222
|
+
config: Config
|
|
223
|
+
effectiveConfig?: Config
|
|
224
|
+
isDesktop: boolean
|
|
225
|
+
onChange: <K extends keyof Config>(field: K, value: Config[K]) => void
|
|
226
|
+
}) {
|
|
227
|
+
return (
|
|
228
|
+
<div className="space-y-4">
|
|
229
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
230
|
+
Changes require a restart to take effect.
|
|
231
|
+
{isDesktop
|
|
232
|
+
? ' Quit and relaunch Radar to apply.'
|
|
233
|
+
: ' Stop and restart the radar command to apply.'}
|
|
234
|
+
</p>
|
|
235
|
+
|
|
236
|
+
<ConfigField
|
|
237
|
+
label="Kubeconfig"
|
|
238
|
+
help="Path to kubeconfig file"
|
|
239
|
+
value={config.kubeconfig ?? ''}
|
|
240
|
+
effectiveValue={effectiveConfig?.kubeconfig}
|
|
241
|
+
placeholder="~/.kube/config"
|
|
242
|
+
onChange={(v) => onChange('kubeconfig', v || undefined)}
|
|
243
|
+
/>
|
|
244
|
+
|
|
245
|
+
<ConfigField
|
|
246
|
+
label="Kubeconfig Directories"
|
|
247
|
+
help="Comma-separated directories containing kubeconfig files"
|
|
248
|
+
value={config.kubeconfigDirs?.join(', ') ?? ''}
|
|
249
|
+
effectiveValue={effectiveConfig?.kubeconfigDirs?.join(', ')}
|
|
250
|
+
placeholder="/path/to/dir1, /path/to/dir2"
|
|
251
|
+
onChange={(v) => onChange('kubeconfigDirs', v ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined)}
|
|
252
|
+
/>
|
|
253
|
+
|
|
254
|
+
<ConfigField
|
|
255
|
+
label="Default Namespace"
|
|
256
|
+
help="Initial namespace filter on startup"
|
|
257
|
+
value={config.namespace ?? ''}
|
|
258
|
+
effectiveValue={effectiveConfig?.namespace}
|
|
259
|
+
placeholder="All namespaces"
|
|
260
|
+
onChange={(v) => onChange('namespace', v || undefined)}
|
|
261
|
+
/>
|
|
262
|
+
|
|
263
|
+
<ConfigNumberField
|
|
264
|
+
label="Port"
|
|
265
|
+
help={isDesktop
|
|
266
|
+
? 'Fixed server port (leave empty for random). Set this to keep a stable MCP endpoint.'
|
|
267
|
+
: 'Server port'}
|
|
268
|
+
value={config.port}
|
|
269
|
+
effectiveValue={effectiveConfig?.port}
|
|
270
|
+
placeholder={isDesktop ? 'Random' : '9280'}
|
|
271
|
+
onChange={(v) => onChange('port', v)}
|
|
272
|
+
/>
|
|
273
|
+
|
|
274
|
+
{!isDesktop && (
|
|
275
|
+
<ConfigToggle
|
|
276
|
+
label="Open browser on start"
|
|
277
|
+
value={!(config.noBrowser ?? false)}
|
|
278
|
+
onChange={(v) => onChange('noBrowser', !v ? true : undefined)}
|
|
279
|
+
/>
|
|
280
|
+
)}
|
|
281
|
+
|
|
282
|
+
<div className="border-t border-theme-border pt-4 mt-4">
|
|
283
|
+
<h4 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-3">Timeline</h4>
|
|
284
|
+
|
|
285
|
+
<div className="space-y-4">
|
|
286
|
+
<div>
|
|
287
|
+
<label className="block text-sm font-medium text-theme-text-primary mb-1">
|
|
288
|
+
Storage Backend
|
|
289
|
+
</label>
|
|
290
|
+
<select
|
|
291
|
+
value={config.timelineStorage ?? 'memory'}
|
|
292
|
+
onChange={(e) => onChange('timelineStorage', e.target.value === 'memory' ? undefined : e.target.value as 'sqlite')}
|
|
293
|
+
className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary focus:outline-none focus:border-blue-500"
|
|
294
|
+
>
|
|
295
|
+
<option value="memory">Memory (default)</option>
|
|
296
|
+
<option value="sqlite">SQLite (persistent)</option>
|
|
297
|
+
</select>
|
|
298
|
+
<EffectiveHint current={config.timelineStorage} effective={effectiveConfig?.timelineStorage} />
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<ConfigNumberField
|
|
302
|
+
label="History Limit"
|
|
303
|
+
help="Maximum events to retain"
|
|
304
|
+
value={config.historyLimit}
|
|
305
|
+
effectiveValue={effectiveConfig?.historyLimit}
|
|
306
|
+
placeholder="10000"
|
|
307
|
+
onChange={(v) => onChange('historyLimit', v)}
|
|
308
|
+
/>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="border-t border-theme-border pt-4 mt-4">
|
|
313
|
+
<h4 className="text-xs font-medium text-theme-text-secondary uppercase tracking-wider mb-3">Integrations</h4>
|
|
314
|
+
|
|
315
|
+
<div className="space-y-4">
|
|
316
|
+
<ConfigField
|
|
317
|
+
label="Prometheus URL"
|
|
318
|
+
help="Manual Prometheus/VictoriaMetrics URL (skips auto-discovery)"
|
|
319
|
+
value={config.prometheusUrl ?? ''}
|
|
320
|
+
effectiveValue={effectiveConfig?.prometheusUrl}
|
|
321
|
+
placeholder="http://prometheus-server.monitoring:9090"
|
|
322
|
+
onChange={(v) => onChange('prometheusUrl', v || undefined)}
|
|
323
|
+
/>
|
|
324
|
+
|
|
325
|
+
<MCPSection
|
|
326
|
+
mcpEnabled={config.mcp ?? true}
|
|
327
|
+
onToggle={(v) => onChange('mcp', v)}
|
|
328
|
+
isDesktop={isDesktop}
|
|
329
|
+
portPinned={config.port != null && config.port > 0}
|
|
330
|
+
onPinPort={(port) => onChange('port', port)}
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// -- MCP Section --------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
function MCPSection({
|
|
341
|
+
mcpEnabled,
|
|
342
|
+
onToggle,
|
|
343
|
+
isDesktop,
|
|
344
|
+
portPinned,
|
|
345
|
+
onPinPort,
|
|
346
|
+
}: {
|
|
347
|
+
mcpEnabled: boolean
|
|
348
|
+
onToggle: (value: boolean) => void
|
|
349
|
+
isDesktop: boolean
|
|
350
|
+
portPinned: boolean
|
|
351
|
+
onPinPort: (port: number) => void
|
|
352
|
+
}) {
|
|
353
|
+
const [copied, setCopied] = useState(false)
|
|
354
|
+
|
|
355
|
+
const currentPort = Number(window.location.port) || 80
|
|
356
|
+
const mcpUrl = `http://localhost:${currentPort}/mcp`
|
|
357
|
+
|
|
358
|
+
const handleCopy = () => {
|
|
359
|
+
navigator.clipboard.writeText(mcpUrl)
|
|
360
|
+
setCopied(true)
|
|
361
|
+
setTimeout(() => setCopied(false), 2000)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const handlePinPort = () => {
|
|
365
|
+
onPinPort(currentPort)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className="space-y-3">
|
|
370
|
+
<ConfigToggle
|
|
371
|
+
label="MCP Server (AI tools)"
|
|
372
|
+
value={mcpEnabled}
|
|
373
|
+
onChange={onToggle}
|
|
374
|
+
/>
|
|
375
|
+
|
|
376
|
+
{mcpEnabled && (
|
|
377
|
+
<div className="space-y-2 pl-0.5">
|
|
378
|
+
<div>
|
|
379
|
+
<label className="block text-xs text-theme-text-secondary mb-1">MCP Endpoint</label>
|
|
380
|
+
<div className="flex items-center gap-2">
|
|
381
|
+
<code className="flex-1 px-2.5 py-1.5 text-xs font-mono bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary truncate">
|
|
382
|
+
{mcpUrl}
|
|
383
|
+
</code>
|
|
384
|
+
<button
|
|
385
|
+
onClick={handleCopy}
|
|
386
|
+
className="shrink-0 p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-elevated rounded-md transition-colors"
|
|
387
|
+
title="Copy MCP URL"
|
|
388
|
+
>
|
|
389
|
+
{copied ? <Check className="w-3.5 h-3.5 text-green-500" /> : <Copy className="w-3.5 h-3.5" />}
|
|
390
|
+
</button>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
{isDesktop && !portPinned && (
|
|
395
|
+
<div className="flex items-start gap-2 px-2.5 py-2 text-xs bg-amber-500/10 border border-amber-500/20 rounded-md">
|
|
396
|
+
<span className="text-amber-700 dark:text-amber-300 flex-1">
|
|
397
|
+
Port changes on every restart. Pin it to keep a stable MCP endpoint.
|
|
398
|
+
</span>
|
|
399
|
+
<button
|
|
400
|
+
onClick={handlePinPort}
|
|
401
|
+
className="shrink-0 flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-amber-800 dark:text-amber-200 hover:text-amber-900 dark:hover:text-white bg-amber-500/20 hover:bg-amber-500/30 rounded transition-colors"
|
|
402
|
+
>
|
|
403
|
+
<Pin className="w-3 h-3" />
|
|
404
|
+
Pin port {currentPort}
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
|
|
409
|
+
{isDesktop && portPinned && (
|
|
410
|
+
<p className="text-xs text-green-600 dark:text-green-400/80 px-0.5">
|
|
411
|
+
Port is pinned. MCP endpoint will remain stable across restarts.
|
|
412
|
+
</p>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// -- Shared Field Components --------------------------------------------------
|
|
421
|
+
|
|
422
|
+
function ConfigField({
|
|
423
|
+
label,
|
|
424
|
+
help,
|
|
425
|
+
value,
|
|
426
|
+
effectiveValue,
|
|
427
|
+
placeholder,
|
|
428
|
+
onChange,
|
|
429
|
+
}: {
|
|
430
|
+
label: string
|
|
431
|
+
help?: string
|
|
432
|
+
value: string
|
|
433
|
+
effectiveValue?: string
|
|
434
|
+
placeholder?: string
|
|
435
|
+
onChange: (value: string) => void
|
|
436
|
+
}) {
|
|
437
|
+
return (
|
|
438
|
+
<div>
|
|
439
|
+
<label className="block text-sm font-medium text-theme-text-primary mb-1">
|
|
440
|
+
{label}
|
|
441
|
+
</label>
|
|
442
|
+
{help && <p className="text-xs text-theme-text-tertiary mb-1">{help}</p>}
|
|
443
|
+
<input
|
|
444
|
+
type="text"
|
|
445
|
+
value={value}
|
|
446
|
+
onChange={(e) => onChange(e.target.value)}
|
|
447
|
+
placeholder={placeholder}
|
|
448
|
+
className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-blue-500"
|
|
449
|
+
/>
|
|
450
|
+
<EffectiveHint current={value || undefined} effective={effectiveValue} />
|
|
451
|
+
</div>
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function ConfigNumberField({
|
|
456
|
+
label,
|
|
457
|
+
help,
|
|
458
|
+
value,
|
|
459
|
+
effectiveValue,
|
|
460
|
+
placeholder,
|
|
461
|
+
onChange,
|
|
462
|
+
}: {
|
|
463
|
+
label: string
|
|
464
|
+
help?: string
|
|
465
|
+
value?: number
|
|
466
|
+
effectiveValue?: number
|
|
467
|
+
placeholder?: string
|
|
468
|
+
onChange: (value: number | undefined) => void
|
|
469
|
+
}) {
|
|
470
|
+
return (
|
|
471
|
+
<div>
|
|
472
|
+
<label className="block text-sm font-medium text-theme-text-primary mb-1">
|
|
473
|
+
{label}
|
|
474
|
+
</label>
|
|
475
|
+
{help && <p className="text-xs text-theme-text-tertiary mb-1">{help}</p>}
|
|
476
|
+
<input
|
|
477
|
+
type="number"
|
|
478
|
+
value={value ?? ''}
|
|
479
|
+
onChange={(e) => onChange(e.target.value ? parseInt(e.target.value, 10) || undefined : undefined)}
|
|
480
|
+
placeholder={placeholder}
|
|
481
|
+
className="w-full px-3 py-1.5 text-sm bg-theme-elevated border border-theme-border rounded-md text-theme-text-primary placeholder:text-theme-text-tertiary focus:outline-none focus:border-blue-500"
|
|
482
|
+
/>
|
|
483
|
+
<EffectiveHint current={value} effective={effectiveValue} />
|
|
484
|
+
</div>
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function ConfigToggle({
|
|
489
|
+
label,
|
|
490
|
+
value,
|
|
491
|
+
onChange,
|
|
492
|
+
}: {
|
|
493
|
+
label: string
|
|
494
|
+
value: boolean
|
|
495
|
+
onChange: (value: boolean) => void
|
|
496
|
+
}) {
|
|
497
|
+
return (
|
|
498
|
+
<label className="flex items-center justify-between py-1 cursor-pointer group">
|
|
499
|
+
<span className="text-sm text-theme-text-primary group-hover:text-theme-text-primary">{label}</span>
|
|
500
|
+
<button
|
|
501
|
+
role="switch"
|
|
502
|
+
aria-checked={value}
|
|
503
|
+
onClick={() => onChange(!value)}
|
|
504
|
+
className={clsx(
|
|
505
|
+
'relative w-9 h-5 rounded-full transition-colors',
|
|
506
|
+
value ? 'bg-skyhook-600' : 'bg-theme-elevated border border-theme-border'
|
|
507
|
+
)}
|
|
508
|
+
>
|
|
509
|
+
<span
|
|
510
|
+
className={clsx(
|
|
511
|
+
'absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white transition-transform shadow-sm',
|
|
512
|
+
value && 'translate-x-4'
|
|
513
|
+
)}
|
|
514
|
+
/>
|
|
515
|
+
</button>
|
|
516
|
+
</label>
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function EffectiveHint({
|
|
521
|
+
current,
|
|
522
|
+
effective,
|
|
523
|
+
}: {
|
|
524
|
+
current?: string | number
|
|
525
|
+
effective?: string | number
|
|
526
|
+
}) {
|
|
527
|
+
if (!effective || effective === current) return null
|
|
528
|
+
const currentStr = current != null ? String(current) : ''
|
|
529
|
+
const effectiveStr = String(effective)
|
|
530
|
+
if (currentStr === effectiveStr) return null
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<p className="text-xs text-amber-600 dark:text-amber-400/80 mt-0.5">
|
|
534
|
+
Currently running: {effectiveStr} (restart to apply)
|
|
535
|
+
</p>
|
|
536
|
+
)
|
|
537
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react'
|
|
2
|
+
import { CreateResourceDialog as BaseCreateResourceDialog } from '@skyhook-io/k8s-ui'
|
|
3
|
+
import { useApplyResource } from '../../api/client'
|
|
4
|
+
|
|
5
|
+
type BaseProps = ComponentProps<typeof BaseCreateResourceDialog>
|
|
6
|
+
|
|
7
|
+
export function CreateResourceDialog(props: Omit<BaseProps, 'onApply' | 'isApplying'>) {
|
|
8
|
+
const applyResource = useApplyResource()
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<BaseCreateResourceDialog
|
|
12
|
+
{...props}
|
|
13
|
+
onApply={(params) => applyResource.mutateAsync(params)}
|
|
14
|
+
isApplying={applyResource.isPending}
|
|
15
|
+
/>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
EditableYamlView as BaseEditableYamlView,
|
|
4
|
+
SaveSuccessAnimation,
|
|
5
|
+
} from '@skyhook-io/k8s-ui'
|
|
6
|
+
import { useUpdateResource } from '../../api/client'
|
|
7
|
+
|
|
8
|
+
// Re-export SaveSuccessAnimation as-is (pure component, no wrapper needed)
|
|
9
|
+
export { SaveSuccessAnimation }
|
|
10
|
+
|
|
11
|
+
type BaseProps = ComponentProps<typeof BaseEditableYamlView>
|
|
12
|
+
|
|
13
|
+
export function EditableYamlView(props: Omit<BaseProps, 'onSave' | 'isSaving' | 'saveError'>) {
|
|
14
|
+
const updateResource = useUpdateResource()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<BaseEditableYamlView
|
|
18
|
+
{...props}
|
|
19
|
+
onSave={(params) => updateResource.mutateAsync(params)}
|
|
20
|
+
isSaving={updateResource.isPending}
|
|
21
|
+
saveError={updateResource.error?.message ?? null}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useMemo } from 'react'
|
|
2
|
+
import { Search } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export function LargeClusterNamespacePicker({ namespaces, onSelect }: {
|
|
5
|
+
namespaces: { name: string }[] | undefined
|
|
6
|
+
onSelect: (ns: string) => void
|
|
7
|
+
}) {
|
|
8
|
+
const [search, setSearch] = useState('')
|
|
9
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
inputRef.current?.focus()
|
|
13
|
+
}, [])
|
|
14
|
+
|
|
15
|
+
const sorted = useMemo(() => {
|
|
16
|
+
if (!namespaces) return []
|
|
17
|
+
return [...namespaces].sort((a, b) => a.name.localeCompare(b.name))
|
|
18
|
+
}, [namespaces])
|
|
19
|
+
|
|
20
|
+
const filtered = useMemo(() => {
|
|
21
|
+
if (!search.trim()) return sorted
|
|
22
|
+
const q = search.toLowerCase()
|
|
23
|
+
return sorted.filter(ns => ns.name.toLowerCase().includes(q))
|
|
24
|
+
}, [sorted, search])
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="text-left">
|
|
28
|
+
<div className="relative mb-2">
|
|
29
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
30
|
+
<input
|
|
31
|
+
ref={inputRef}
|
|
32
|
+
type="text"
|
|
33
|
+
value={search}
|
|
34
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
35
|
+
placeholder="Search namespaces..."
|
|
36
|
+
className="w-full bg-theme-base text-theme-text-primary text-sm rounded-lg px-3 py-2 pl-9 border border-theme-border-light focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500/50 placeholder:text-theme-text-tertiary"
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="max-h-[240px] overflow-y-auto rounded-lg border border-theme-border bg-theme-base">
|
|
40
|
+
{!namespaces ? (
|
|
41
|
+
<div className="px-3 py-6 text-center text-sm text-theme-text-tertiary">
|
|
42
|
+
Loading namespaces...
|
|
43
|
+
</div>
|
|
44
|
+
) : filtered.length === 0 ? (
|
|
45
|
+
<div className="px-3 py-6 text-center text-sm text-theme-text-tertiary">
|
|
46
|
+
No namespaces match “{search}”
|
|
47
|
+
</div>
|
|
48
|
+
) : (
|
|
49
|
+
filtered.map((ns) => (
|
|
50
|
+
<button
|
|
51
|
+
key={ns.name}
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => onSelect(ns.name)}
|
|
54
|
+
className="w-full text-left px-3 py-2 text-sm text-theme-text-primary hover:bg-theme-hover transition-colors border-b border-theme-border last:border-b-0"
|
|
55
|
+
>
|
|
56
|
+
{ns.name}
|
|
57
|
+
</button>
|
|
58
|
+
))
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
{sorted.length > 0 && (
|
|
62
|
+
<p className="mt-2 text-xs text-theme-text-tertiary text-center">
|
|
63
|
+
{filtered.length === sorted.length
|
|
64
|
+
? `${sorted.length} namespaces`
|
|
65
|
+
: `${filtered.length} of ${sorted.length} namespaces`}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
ResourceRendererDispatch as BaseResourceRendererDispatch,
|
|
4
|
+
getResourceStatus,
|
|
5
|
+
} from '@skyhook-io/k8s-ui'
|
|
6
|
+
import { PrometheusCharts } from '../resource/PrometheusCharts'
|
|
7
|
+
import { useResourceEvents } from '../../api/client'
|
|
8
|
+
|
|
9
|
+
// Re-export getResourceStatus as-is (pure function, no wrapper needed)
|
|
10
|
+
export { getResourceStatus }
|
|
11
|
+
|
|
12
|
+
type BaseProps = ComponentProps<typeof BaseResourceRendererDispatch>
|
|
13
|
+
|
|
14
|
+
export function ResourceRendererDispatch(props: Omit<BaseProps, 'events' | 'eventsLoading' | 'renderMetrics'>) {
|
|
15
|
+
const { data: events, isLoading: eventsLoading } = useResourceEvents(
|
|
16
|
+
props.resource.kind,
|
|
17
|
+
props.resource.namespace,
|
|
18
|
+
props.resource.name
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<BaseResourceRendererDispatch
|
|
23
|
+
{...props}
|
|
24
|
+
events={events}
|
|
25
|
+
eventsLoading={eventsLoading}
|
|
26
|
+
renderMetrics={({ kind, namespace, name }) => (
|
|
27
|
+
<PrometheusCharts kind={kind} namespace={namespace} name={name} />
|
|
28
|
+
)}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DiffViewer, DiffBadge } from '@skyhook-io/k8s-ui'
|