@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,69 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
3
|
+
|
|
4
|
+
export interface PinnedKind {
|
|
5
|
+
name: string // plural name for API calls, e.g. "pods", "deployments"
|
|
6
|
+
kind: string // singular display name, e.g. "Pod", "Deployment"
|
|
7
|
+
group: string // API group, e.g. "" for core, "source.toolkit.fluxcd.io" for Flux
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'radar-pinned-kinds'
|
|
11
|
+
|
|
12
|
+
function loadPinned(): PinnedKind[] {
|
|
13
|
+
try {
|
|
14
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
15
|
+
if (raw) return JSON.parse(raw)
|
|
16
|
+
} catch {
|
|
17
|
+
// ignore parse errors
|
|
18
|
+
}
|
|
19
|
+
return []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function savePinned(pinned: PinnedKind[]) {
|
|
23
|
+
try {
|
|
24
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(pinned))
|
|
25
|
+
} catch {
|
|
26
|
+
// ignore storage errors
|
|
27
|
+
}
|
|
28
|
+
fetch(apiUrl('/settings'), { method: 'PUT', credentials: getCredentialsMode(), headers: { 'Content-Type': 'application/json', ...getAuthHeaders() }, body: JSON.stringify({ pinnedKinds: pinned }) })
|
|
29
|
+
.then((res) => { if (!res.ok) console.warn('[settings] Failed to persist pinned kinds:', res.status) })
|
|
30
|
+
.catch((err) => console.warn('[settings] Failed to persist pinned kinds:', err))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matches(a: PinnedKind, name: string, group: string): boolean {
|
|
34
|
+
return a.name === name && a.group === group
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function usePinnedKinds() {
|
|
38
|
+
const [pinned, setPinned] = useState<PinnedKind[]>(loadPinned)
|
|
39
|
+
|
|
40
|
+
// Sync from server (persisted settings survive port changes in desktop app)
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
fetch(apiUrl('/settings'), { credentials: getCredentialsMode(), headers: getAuthHeaders() })
|
|
43
|
+
.then((res) => res.ok ? res.json() : null)
|
|
44
|
+
.then((data) => {
|
|
45
|
+
if (data?.pinnedKinds?.length && loadPinned().length === 0) {
|
|
46
|
+
setPinned(data.pinnedKinds)
|
|
47
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(data.pinnedKinds))
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
.catch((err) => console.warn('[settings] Failed to load pinned kinds from server:', err))
|
|
51
|
+
}, [])
|
|
52
|
+
|
|
53
|
+
const togglePin = useCallback((item: PinnedKind) => {
|
|
54
|
+
setPinned((prev) => {
|
|
55
|
+
const exists = prev.some((p) => matches(p, item.name, item.group))
|
|
56
|
+
const next = exists
|
|
57
|
+
? prev.filter((p) => !matches(p, item.name, item.group))
|
|
58
|
+
: [...prev, item]
|
|
59
|
+
savePinned(next)
|
|
60
|
+
return next
|
|
61
|
+
})
|
|
62
|
+
}, [])
|
|
63
|
+
|
|
64
|
+
const isPinned = useCallback((name: string, group: string): boolean => {
|
|
65
|
+
return pinned.some((p) => matches(p, name, group))
|
|
66
|
+
}, [pinned])
|
|
67
|
+
|
|
68
|
+
return { pinned, togglePin, isPinned }
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useRefreshAnimation } from '@skyhook-io/k8s-ui'
|
package/src/index.css
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@config "../tailwind.config.js";
|
|
3
|
+
@variant dark (&:where(.dark, .dark *));
|
|
4
|
+
@import "@skyhook-io/k8s-ui/theme/variables.css";
|
|
5
|
+
@import "@skyhook-io/k8s-ui/theme/tailwind-theme.css";
|
|
6
|
+
@import "@skyhook-io/k8s-ui/theme/components.css";
|
|
7
|
+
@import "@skyhook-io/k8s-ui/components/topology/topology.css";
|
|
8
|
+
@import "@fontsource-variable/dm-sans";
|
|
9
|
+
@import "@fontsource/dm-mono";
|
|
10
|
+
|
|
11
|
+
/* Global: pointer cursor for all interactive elements */
|
|
12
|
+
button, [role="button"], a, label, select, summary, [tabindex]:not([tabindex="-1"]) {
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ============================================
|
|
17
|
+
BASE STYLES
|
|
18
|
+
============================================ */
|
|
19
|
+
|
|
20
|
+
:root {
|
|
21
|
+
/* Font settings */
|
|
22
|
+
font-family: 'DM Sans Variable', 'DM Sans', system-ui, sans-serif;
|
|
23
|
+
line-height: 1.5;
|
|
24
|
+
font-weight: 400;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
body {
|
|
28
|
+
margin: 0;
|
|
29
|
+
min-height: 100vh;
|
|
30
|
+
background-color: var(--bg-base);
|
|
31
|
+
color: var(--text-primary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#root {
|
|
35
|
+
height: 100vh;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/* ============================================
|
|
42
|
+
REACT FLOW CUSTOMIZATIONS
|
|
43
|
+
============================================ */
|
|
44
|
+
|
|
45
|
+
.react-flow__pane {
|
|
46
|
+
cursor: grab;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.react-flow__pane:active {
|
|
50
|
+
cursor: grabbing;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.react-flow__node {
|
|
54
|
+
cursor: pointer !important;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.react-flow__node:active {
|
|
58
|
+
cursor: pointer !important;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* React Flow Controls */
|
|
62
|
+
.react-flow__controls {
|
|
63
|
+
background: var(--bg-surface);
|
|
64
|
+
border: 1px solid var(--border-default);
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
box-shadow: var(--shadow-md);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.react-flow__controls-button {
|
|
70
|
+
background: var(--bg-surface);
|
|
71
|
+
border: none;
|
|
72
|
+
border-bottom: 1px solid var(--border-default);
|
|
73
|
+
color: var(--text-tertiary);
|
|
74
|
+
width: 28px;
|
|
75
|
+
height: 28px;
|
|
76
|
+
padding: 5px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.react-flow__controls-button:hover {
|
|
80
|
+
background: var(--bg-elevated);
|
|
81
|
+
color: var(--text-primary);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.react-flow__controls-button:last-child {
|
|
85
|
+
border-bottom: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.react-flow__controls-button svg {
|
|
89
|
+
fill: currentColor;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* React Flow MiniMap */
|
|
93
|
+
.react-flow__minimap {
|
|
94
|
+
background: var(--bg-surface);
|
|
95
|
+
border: 1px solid var(--border-default);
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.react-flow__minimap-mask {
|
|
100
|
+
fill: light-dark(rgba(232, 237, 245, 0.8), rgba(12, 16, 28, 0.8));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* ============================================
|
|
104
|
+
CUSTOM SCROLLBAR
|
|
105
|
+
============================================ */
|
|
106
|
+
|
|
107
|
+
::-webkit-scrollbar {
|
|
108
|
+
width: 8px;
|
|
109
|
+
height: 8px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
::-webkit-scrollbar-track {
|
|
113
|
+
background: var(--scrollbar-track);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
::-webkit-scrollbar-thumb {
|
|
117
|
+
background: var(--scrollbar-thumb);
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
::-webkit-scrollbar-thumb:hover {
|
|
122
|
+
background: var(--scrollbar-thumb-hover);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ============================================
|
|
126
|
+
ANIMATIONS
|
|
127
|
+
============================================ */
|
|
128
|
+
|
|
129
|
+
@keyframes slide-in-from-right {
|
|
130
|
+
from {
|
|
131
|
+
transform: translateX(100%);
|
|
132
|
+
opacity: 0;
|
|
133
|
+
}
|
|
134
|
+
to {
|
|
135
|
+
transform: translateX(0);
|
|
136
|
+
opacity: 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Expand animation for timeline swimlane children — GPU-composited (transform + opacity only).
|
|
141
|
+
Slides down from the parent row with a fade. Collapse is instant (unmount). */
|
|
142
|
+
@keyframes swimlane-expand {
|
|
143
|
+
from {
|
|
144
|
+
opacity: 0;
|
|
145
|
+
transform: translateY(-8px);
|
|
146
|
+
}
|
|
147
|
+
to {
|
|
148
|
+
opacity: 1;
|
|
149
|
+
transform: translateY(0);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@keyframes fade-out {
|
|
154
|
+
from {
|
|
155
|
+
opacity: 1;
|
|
156
|
+
}
|
|
157
|
+
to {
|
|
158
|
+
opacity: 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.animate-in {
|
|
163
|
+
animation: slide-in-from-right 0.2s ease-out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@keyframes slide-out-to-right {
|
|
167
|
+
from { transform: translateX(0); opacity: 1; }
|
|
168
|
+
to { transform: translateX(100%); opacity: 0; }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.animate-out {
|
|
172
|
+
animation: slide-out-to-right 0.2s ease-in forwards;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* View Transitions: drawer content cross-fade */
|
|
176
|
+
::view-transition-old(drawer-content),
|
|
177
|
+
::view-transition-old(helm-drawer-content) {
|
|
178
|
+
animation-duration: 100ms;
|
|
179
|
+
animation-timing-function: ease-out;
|
|
180
|
+
}
|
|
181
|
+
::view-transition-new(drawer-content),
|
|
182
|
+
::view-transition-new(helm-drawer-content) {
|
|
183
|
+
animation-duration: 200ms;
|
|
184
|
+
animation-timing-function: ease-in;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes fade-in-up {
|
|
188
|
+
from {
|
|
189
|
+
opacity: 0;
|
|
190
|
+
transform: translateY(24px) scale(0.97);
|
|
191
|
+
}
|
|
192
|
+
to {
|
|
193
|
+
opacity: 1;
|
|
194
|
+
transform: translateY(0) scale(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.animate-fade-in-up {
|
|
199
|
+
animation: fade-in-up 0.5s ease-out both;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.slide-in-from-right-5 {
|
|
203
|
+
--tw-enter-translate-x: 1.25rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.fade-in {
|
|
207
|
+
animation: slide-in-from-right 0.2s ease-out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
/* ============================================
|
|
212
|
+
XTERM TERMINAL STYLES
|
|
213
|
+
============================================ */
|
|
214
|
+
|
|
215
|
+
/* Override any potential centering and ensure proper layout */
|
|
216
|
+
.xterm,
|
|
217
|
+
.xterm-viewport,
|
|
218
|
+
.xterm-screen {
|
|
219
|
+
text-align: left !important;
|
|
220
|
+
margin: 0 !important;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Prevent xterm from growing beyond its container */
|
|
224
|
+
.xterm {
|
|
225
|
+
position: absolute !important;
|
|
226
|
+
top: 0 !important;
|
|
227
|
+
left: 0 !important;
|
|
228
|
+
right: 0 !important;
|
|
229
|
+
bottom: 0 !important;
|
|
230
|
+
overflow: hidden !important;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.xterm-screen canvas {
|
|
234
|
+
margin: 0 !important;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Log search highlight */
|
|
238
|
+
mark.log-highlight {
|
|
239
|
+
background: light-dark(rgba(180, 120, 0, 0.35), rgba(250, 200, 0, 0.55));
|
|
240
|
+
color: inherit;
|
|
241
|
+
border-radius: 2px;
|
|
242
|
+
padding: 0 1px;
|
|
243
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @skyhook-io/radar-app — Radar's full web UI as a reusable React component.
|
|
2
|
+
//
|
|
3
|
+
// Source-only package (main points at .ts, no dist/). Consumers need a
|
|
4
|
+
// bundler that transpiles TSX and resolves workspace-style peer deps. The
|
|
5
|
+
// same source is consumed by Radar's binary via main.tsx.
|
|
6
|
+
export { RadarApp, type RadarAppProps } from './RadarApp';
|
|
7
|
+
export {
|
|
8
|
+
setApiBase,
|
|
9
|
+
setBasename,
|
|
10
|
+
setAuthHeadersProvider,
|
|
11
|
+
setCredentialsMode,
|
|
12
|
+
getApiBase,
|
|
13
|
+
getBasename,
|
|
14
|
+
getAuthHeaders,
|
|
15
|
+
getCredentialsMode,
|
|
16
|
+
} from './api/config';
|
|
17
|
+
export type { NavCustomization } from './context/NavCustomization';
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { RadarApp } from './RadarApp'
|
|
4
|
+
import { openExternal } from './utils/navigation'
|
|
5
|
+
import './index.css'
|
|
6
|
+
|
|
7
|
+
// Intercept external link clicks in the Wails desktop app.
|
|
8
|
+
// <a target="_blank"> is swallowed by WKWebView/WebView2 — route through openExternal()
|
|
9
|
+
// which calls the backend /api/desktop/open-url endpoint to open in the system browser.
|
|
10
|
+
window.addEventListener('click', (e: MouseEvent) => {
|
|
11
|
+
const anchor = (e.target as HTMLElement).closest?.('a[href]') as HTMLAnchorElement | null
|
|
12
|
+
if (!anchor) return
|
|
13
|
+
const href = anchor.href
|
|
14
|
+
if (!href || href.startsWith(window.location.origin) || href.startsWith('/') || href.startsWith('#') || href.startsWith('blob:')) return
|
|
15
|
+
// External URL — open via system browser
|
|
16
|
+
e.preventDefault()
|
|
17
|
+
openExternal(href)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// === Wails Desktop Clipboard ===
|
|
21
|
+
//
|
|
22
|
+
// Background: The desktop app uses a RedirectHandler that navigates the Wails
|
|
23
|
+
// webview from wails:// to http://localhost:<port>. After the redirect,
|
|
24
|
+
// window.runtime (Wails JS API) is no longer available. Clipboard operations
|
|
25
|
+
// must use navigator.clipboard and DOM events instead.
|
|
26
|
+
//
|
|
27
|
+
// What works and why:
|
|
28
|
+
// Cmd+C / Cmd+X: Handled in keydown listener below. The Edit menu registers
|
|
29
|
+
// these accelerators with nil callbacks (native responder chain), but WKWebView
|
|
30
|
+
// does NOT dispatch a DOM copy/cut event from the native copy: selector.
|
|
31
|
+
// The keydown event DOES reach JS, so we intercept it here.
|
|
32
|
+
// Cmd+V: Handled by menu.go's explicit WindowExecJS callback which reads
|
|
33
|
+
// navigator.clipboard.readText() and dispatches a synthetic paste event.
|
|
34
|
+
// Right-click Copy/Cut (Monaco): Monaco calls document.execCommand('copy'/'cut'),
|
|
35
|
+
// intercepted by the monkey-patch below.
|
|
36
|
+
// Right-click Paste (Monaco): Not supported — Monaco calls navigator.clipboard
|
|
37
|
+
// .readText() directly (not execCommand), and WKWebView blocks readText() from
|
|
38
|
+
// page JS context. Use Cmd+V instead.
|
|
39
|
+
|
|
40
|
+
// Read selected text from Monaco if it has focus. Monaco uses virtual selection
|
|
41
|
+
// (not DOM selection), so window.getSelection() doesn't work — we access the
|
|
42
|
+
// editor instance exposed by YamlEditor.tsx.
|
|
43
|
+
function getMonacoSelection(): { text: string; editor: any } | null {
|
|
44
|
+
const editor = (window as any).__radarMonacoEditor
|
|
45
|
+
if (!editor?.hasTextFocus?.()) return null
|
|
46
|
+
const sel = editor.getSelection()
|
|
47
|
+
const model = editor.getModel()
|
|
48
|
+
if (!sel || !model) return null
|
|
49
|
+
const text = model.getValueInRange(sel)
|
|
50
|
+
if (!text) return null
|
|
51
|
+
return { text, editor }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getSelectedText(): { text: string; monaco: { text: string; editor: any } | null } {
|
|
55
|
+
const monaco = getMonacoSelection()
|
|
56
|
+
if (monaco) return { text: monaco.text, monaco }
|
|
57
|
+
const sel = window.getSelection()
|
|
58
|
+
const text = sel ? sel.toString() : ''
|
|
59
|
+
return { text, monaco: null }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deleteMonacoSelection(editor: any): void {
|
|
63
|
+
editor.pushUndoStop()
|
|
64
|
+
editor.executeEdits('cut', [{ range: editor.getSelection(), text: '' }])
|
|
65
|
+
editor.pushUndoStop()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleCopyOrCut(isCut: boolean): void {
|
|
69
|
+
const { text, monaco } = getSelectedText()
|
|
70
|
+
if (!text) return
|
|
71
|
+
navigator.clipboard.writeText(text).catch((err) => { console.warn('[Radar] Clipboard write failed:', err) })
|
|
72
|
+
if (isCut) {
|
|
73
|
+
if (monaco) {
|
|
74
|
+
deleteMonacoSelection(monaco.editor)
|
|
75
|
+
} else {
|
|
76
|
+
_origExecCommand('delete')
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cmd+C/X: the menu's nil callback does NOT dispatch a DOM copy event.
|
|
82
|
+
document.addEventListener('keydown', (e) => {
|
|
83
|
+
if (!(e.metaKey || e.ctrlKey)) return
|
|
84
|
+
if (e.key !== 'c' && e.key !== 'x') return
|
|
85
|
+
handleCopyOrCut(e.key === 'x')
|
|
86
|
+
}, true)
|
|
87
|
+
|
|
88
|
+
// Intercept copy/cut DOM events to handle Monaco's virtual selection.
|
|
89
|
+
// These fire from right-click -> Copy in some contexts. When a real
|
|
90
|
+
// ClipboardEvent is available, we write directly to e.clipboardData
|
|
91
|
+
// (synchronous, more reliable than the async clipboard API).
|
|
92
|
+
document.addEventListener('copy', (e: ClipboardEvent) => {
|
|
93
|
+
const result = getMonacoSelection()
|
|
94
|
+
if (result && e.clipboardData) {
|
|
95
|
+
e.preventDefault()
|
|
96
|
+
e.clipboardData.setData('text/plain', result.text)
|
|
97
|
+
}
|
|
98
|
+
}, true)
|
|
99
|
+
|
|
100
|
+
document.addEventListener('cut', (e: ClipboardEvent) => {
|
|
101
|
+
const result = getMonacoSelection()
|
|
102
|
+
if (result && e.clipboardData) {
|
|
103
|
+
e.preventDefault()
|
|
104
|
+
e.clipboardData.setData('text/plain', result.text)
|
|
105
|
+
deleteMonacoSelection(result.editor)
|
|
106
|
+
}
|
|
107
|
+
}, true)
|
|
108
|
+
|
|
109
|
+
// Monkey-patch document.execCommand for Wails WebView compatibility.
|
|
110
|
+
// Handles copy/cut from Monaco's right-click context menu, and paste from
|
|
111
|
+
// any context that calls execCommand('paste').
|
|
112
|
+
const _origExecCommand = document.execCommand.bind(document)
|
|
113
|
+
document.execCommand = function (command: string, showUI?: boolean, value?: string) {
|
|
114
|
+
if (command === 'copy' || command === 'cut') {
|
|
115
|
+
handleCopyOrCut(command === 'cut')
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
if (command === 'paste') {
|
|
119
|
+
navigator.clipboard.readText().then((text) => {
|
|
120
|
+
if (!text) return
|
|
121
|
+
const el = document.activeElement || document.body
|
|
122
|
+
try {
|
|
123
|
+
const dt = new DataTransfer()
|
|
124
|
+
dt.setData('text/plain', text)
|
|
125
|
+
const ev = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })
|
|
126
|
+
if (!el.dispatchEvent(ev)) return
|
|
127
|
+
} catch (_e) { /* ClipboardEvent dispatch failed, fall back to insertText */ }
|
|
128
|
+
_origExecCommand('insertText', false, text)
|
|
129
|
+
}).catch((err) => { console.warn('[Radar] Paste failed:', err) })
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
return _origExecCommand(command, showUI, value)
|
|
133
|
+
} as typeof document.execCommand
|
|
134
|
+
|
|
135
|
+
// Mouse back/forward button navigation (button 3 = back, button 4 = forward).
|
|
136
|
+
// Uses 'mouseup' in capture phase to intercept before the browser's native handler.
|
|
137
|
+
// This prevents double-navigation in browsers (where auxclick + native both fire)
|
|
138
|
+
// and handles desktop WebView (Windows/Linux) where native handling varies.
|
|
139
|
+
// On macOS WKWebView, mouse events don't reach JS — native NSEvent monitor in
|
|
140
|
+
// mouse_darwin.go handles them via WKWebView.goBack()/goForward() directly.
|
|
141
|
+
window.addEventListener('mouseup', (e: MouseEvent) => {
|
|
142
|
+
if (e.button === 3) {
|
|
143
|
+
e.preventDefault()
|
|
144
|
+
window.history.back()
|
|
145
|
+
} else if (e.button === 4) {
|
|
146
|
+
e.preventDefault()
|
|
147
|
+
window.history.forward()
|
|
148
|
+
}
|
|
149
|
+
}, true)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
// Standalone Radar binary: same-origin API, router at root. Library consumers
|
|
153
|
+
// (e.g. radar-hub-web) render <RadarApp apiBase="..." basename="..." /> instead.
|
|
154
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
155
|
+
<React.StrictMode>
|
|
156
|
+
<RadarApp />
|
|
157
|
+
</React.StrictMode>
|
|
158
|
+
)
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Desktop file-save utilities.
|
|
2
|
+
// In the desktop app (Wails), blob URL downloads are silently ignored by
|
|
3
|
+
// WKWebView / WebView2. These helpers route downloads through a backend
|
|
4
|
+
// endpoint that shows the native OS save dialog instead.
|
|
5
|
+
|
|
6
|
+
import { fetchJSON } from '../api/client'
|
|
7
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
8
|
+
|
|
9
|
+
let desktopCheck: Promise<boolean> | null = null
|
|
10
|
+
|
|
11
|
+
/** Returns true when running inside the desktop (Wails) app. Cached after first successful call. */
|
|
12
|
+
export function isDesktopApp(): Promise<boolean> {
|
|
13
|
+
if (!desktopCheck) {
|
|
14
|
+
desktopCheck = fetchJSON<{ isDesktop: boolean }>('/config')
|
|
15
|
+
.then((d) => d.isDesktop ?? false)
|
|
16
|
+
.catch(() => {
|
|
17
|
+
desktopCheck = null // allow retry on next call
|
|
18
|
+
return false
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
return desktopCheck
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Save text content via native save dialog. Returns the chosen file path, or throws with message 'cancelled' if the user dismisses the dialog. */
|
|
25
|
+
export async function desktopSaveFile(content: string, filename: string): Promise<string> {
|
|
26
|
+
const res = await fetch(apiUrl('/desktop/save-file'), {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
credentials: getCredentialsMode(),
|
|
29
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
30
|
+
body: JSON.stringify({ content, filename }),
|
|
31
|
+
})
|
|
32
|
+
if (res.status === 204) throw new Error('cancelled')
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const body = await res.json().catch(() => ({ error: 'Save failed' }))
|
|
35
|
+
throw new Error(body.error ?? 'Save failed')
|
|
36
|
+
}
|
|
37
|
+
const body = await res.json()
|
|
38
|
+
return body.path
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Save a Blob via native save dialog. Returns the chosen file path, or throws with message 'cancelled' if the user dismisses the dialog. */
|
|
42
|
+
export async function desktopSaveBlob(blob: Blob, filename: string): Promise<string> {
|
|
43
|
+
const contentBase64 = await new Promise<string>((resolve, reject) => {
|
|
44
|
+
const reader = new FileReader()
|
|
45
|
+
reader.onloadend = () => {
|
|
46
|
+
const dataUrl = reader.result as string
|
|
47
|
+
resolve(dataUrl.split(',')[1]) // strip "data:...;base64,"
|
|
48
|
+
}
|
|
49
|
+
reader.onerror = () => reject(new Error('Failed to read file'))
|
|
50
|
+
reader.readAsDataURL(blob)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const res = await fetch(apiUrl('/desktop/save-file'), {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
credentials: getCredentialsMode(),
|
|
56
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
57
|
+
body: JSON.stringify({ contentBase64, filename }),
|
|
58
|
+
})
|
|
59
|
+
if (res.status === 204) throw new Error('cancelled')
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const body = await res.json().catch(() => ({ error: 'Save failed' }))
|
|
62
|
+
throw new Error(body.error ?? 'Save failed')
|
|
63
|
+
}
|
|
64
|
+
const body = await res.json()
|
|
65
|
+
return body.path
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
|
+
|
|
3
|
+
/** Reveal a file in the system file manager (Finder on macOS, Explorer on Windows). */
|
|
4
|
+
export function openFolder(path: string): void {
|
|
5
|
+
fetch(apiUrl('/desktop/open-folder'), {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
credentials: getCredentialsMode(),
|
|
8
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
9
|
+
body: JSON.stringify({ path }),
|
|
10
|
+
}).catch((err) => console.warn('[desktop] Failed to open folder:', err))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Open a file with the system default application. */
|
|
14
|
+
export function openFile(path: string): void {
|
|
15
|
+
fetch(apiUrl('/desktop/open-file'), {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
credentials: getCredentialsMode(),
|
|
18
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
19
|
+
body: JSON.stringify({ path }),
|
|
20
|
+
}).catch((err) => console.warn('[desktop] Failed to open file:', err))
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
|
+
|
|
3
|
+
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
4
|
+
export { kindToPlural, pluralToKind, refToSelectedResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
5
|
+
export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
6
|
+
|
|
7
|
+
// radar-specific: open URL in system browser (desktop app support)
|
|
8
|
+
export function openExternal(url: string): void {
|
|
9
|
+
fetch(apiUrl('/desktop/open-url'), {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
credentials: getCredentialsMode(),
|
|
12
|
+
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
|
|
13
|
+
body: JSON.stringify({ url }),
|
|
14
|
+
})
|
|
15
|
+
.then((res) => {
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
window.open(url, '_blank')
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
.catch(() => {
|
|
21
|
+
window.open(url, '_blank')
|
|
22
|
+
})
|
|
23
|
+
}
|