@skyhook-io/radar-app 0.2.2 → 0.3.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 +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// Restore-on-unmount matters for overlay/detail views: closing a resource drawer
|
|
4
|
+
// that opened over a list returns the list's title rather than stranding the
|
|
5
|
+
// resource's. document.title is global to the page, so embedders that don't own
|
|
6
|
+
// the whole tab pass a falsy `label` to opt out and keep their own title
|
|
7
|
+
// (AppInner only feeds a label when the host passed `manageDocumentTitle`).
|
|
8
|
+
//
|
|
9
|
+
// `suffix` is the full trailing string after the label, so a host can rebrand
|
|
10
|
+
// (' — My Cloud') or drop it entirely ('').
|
|
11
|
+
const DEFAULT_SUFFIX = ' · Radar';
|
|
12
|
+
|
|
13
|
+
export function useDocumentTitle(
|
|
14
|
+
label: string | null | undefined,
|
|
15
|
+
suffix: string = DEFAULT_SUFFIX,
|
|
16
|
+
): void {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!label) return;
|
|
19
|
+
const previous = document.title;
|
|
20
|
+
document.title = `${label}${suffix}`;
|
|
21
|
+
return () => {
|
|
22
|
+
document.title = previous;
|
|
23
|
+
};
|
|
24
|
+
}, [label, suffix]);
|
|
25
|
+
}
|
|
@@ -7,6 +7,7 @@ interface UseEventSourceReturn {
|
|
|
7
7
|
topology: Topology | null
|
|
8
8
|
events: K8sEvent[]
|
|
9
9
|
connected: boolean
|
|
10
|
+
connecting: boolean
|
|
10
11
|
reconnect: () => void
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -43,6 +44,7 @@ export function useEventSource(
|
|
|
43
44
|
const [topology, setTopology] = useState<Topology | null>(null)
|
|
44
45
|
const [events, setEvents] = useState<K8sEvent[]>([])
|
|
45
46
|
const [connected, setConnected] = useState(false)
|
|
47
|
+
const [connecting, setConnecting] = useState(true)
|
|
46
48
|
const eventSourceRef = useRef<EventSource | null>(null)
|
|
47
49
|
const reconnectTimeoutRef = useRef<number | null>(null)
|
|
48
50
|
const waitingForTopologyAfterSwitch = useRef(false)
|
|
@@ -66,6 +68,7 @@ export function useEventSource(
|
|
|
66
68
|
optionsRef.current = options
|
|
67
69
|
|
|
68
70
|
const connect = useCallback(() => {
|
|
71
|
+
setConnecting(true)
|
|
69
72
|
// Clean up existing connection
|
|
70
73
|
if (eventSourceRef.current) {
|
|
71
74
|
eventSourceRef.current.close()
|
|
@@ -99,6 +102,7 @@ export function useEventSource(
|
|
|
99
102
|
es.onopen = () => {
|
|
100
103
|
console.log('SSE connected')
|
|
101
104
|
setConnected(true)
|
|
105
|
+
setConnecting(false)
|
|
102
106
|
// Reset backoff on successful connection
|
|
103
107
|
reconnectDelayRef.current = INITIAL_RECONNECT_DELAY_MS
|
|
104
108
|
}
|
|
@@ -106,6 +110,7 @@ export function useEventSource(
|
|
|
106
110
|
es.onerror = (error) => {
|
|
107
111
|
console.error('SSE error:', error)
|
|
108
112
|
setConnected(false)
|
|
113
|
+
setConnecting(false)
|
|
109
114
|
es.close()
|
|
110
115
|
|
|
111
116
|
// Reconnect with exponential backoff
|
|
@@ -257,6 +262,7 @@ export function useEventSource(
|
|
|
257
262
|
topology,
|
|
258
263
|
events,
|
|
259
264
|
connected,
|
|
265
|
+
connecting,
|
|
260
266
|
reconnect,
|
|
261
267
|
}
|
|
262
268
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
// Subscribe to a CSS media query. SSR-safe (returns false before mount) and
|
|
4
|
+
// updates on viewport changes.
|
|
5
|
+
export function useMediaQuery(query: string): boolean {
|
|
6
|
+
const [matches, setMatches] = useState(() => {
|
|
7
|
+
if (typeof window === 'undefined' || !window.matchMedia) return false
|
|
8
|
+
return window.matchMedia(query).matches
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (typeof window === 'undefined' || !window.matchMedia) return
|
|
13
|
+
const mql = window.matchMedia(query)
|
|
14
|
+
const onChange = () => setMatches(mql.matches)
|
|
15
|
+
onChange()
|
|
16
|
+
mql.addEventListener('change', onChange)
|
|
17
|
+
return () => mql.removeEventListener('change', onChange)
|
|
18
|
+
}, [query])
|
|
19
|
+
|
|
20
|
+
return matches
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
// Pin state for the primary left nav rail.
|
|
4
|
+
//
|
|
5
|
+
// "Pinned" = the labeled, full-width sidebar; "unpinned" = the slim 56px
|
|
6
|
+
// icon rail with hover fly-out labels. This is a single committed choice
|
|
7
|
+
// the user makes once (default pinned), persisted to localStorage — NOT a
|
|
8
|
+
// per-route auto-collapse. A rail whose width changes as you navigate is
|
|
9
|
+
// disorienting; the rail is the stable anchor. A user who lives in the
|
|
10
|
+
// wide table views (Resources, GitOps) unpins once and keeps their width;
|
|
11
|
+
// fly-out labels + the ⌘K palette + `g`-mnemonic shortcuts cover the collapsed
|
|
12
|
+
// state's discoverability.
|
|
13
|
+
|
|
14
|
+
const STORAGE_KEY = 'radar.navRail.pinned'
|
|
15
|
+
|
|
16
|
+
function readInitial(): boolean {
|
|
17
|
+
if (typeof window === 'undefined') return true
|
|
18
|
+
// `localStorage` access can throw (SecurityError) when storage is denied —
|
|
19
|
+
// sandboxed embeds, some privacy modes. Default to pinned (expanded), the
|
|
20
|
+
// friendlier first-run state for a non-k8s-expert; power users unpin once.
|
|
21
|
+
try {
|
|
22
|
+
return window.localStorage.getItem(STORAGE_KEY) !== 'false'
|
|
23
|
+
} catch {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useNavRailPinned(): {
|
|
29
|
+
pinned: boolean
|
|
30
|
+
togglePinned: () => void
|
|
31
|
+
} {
|
|
32
|
+
const [pinned, setPinned] = useState(readInitial)
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
try {
|
|
36
|
+
window.localStorage.setItem(STORAGE_KEY, String(pinned))
|
|
37
|
+
} catch {
|
|
38
|
+
// Private-mode / quota failures shouldn't break nav — the in-memory
|
|
39
|
+
// state still drives this session; we just don't persist it.
|
|
40
|
+
}
|
|
41
|
+
}, [pinned])
|
|
42
|
+
|
|
43
|
+
const togglePinned = useCallback(() => setPinned((p) => !p), [])
|
|
44
|
+
|
|
45
|
+
return { pinned, togglePinned }
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Recently-opened resources, persisted to localStorage so the omnibar's empty
|
|
2
|
+
// state can offer "jump back to what I was just looking at" across reloads.
|
|
3
|
+
// Plain functions (not a stateful hook): the omnibar reads fresh each time it
|
|
4
|
+
// opens and writes on open, so two component instances never need to sync.
|
|
5
|
+
|
|
6
|
+
export interface RecentResource {
|
|
7
|
+
kind: string
|
|
8
|
+
group?: string
|
|
9
|
+
namespace?: string
|
|
10
|
+
name: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const KEY = 'radar-recent-resources'
|
|
14
|
+
const MAX = 7
|
|
15
|
+
|
|
16
|
+
// Partition by the current cluster/context: a resource is identified by
|
|
17
|
+
// (kind, ns, name) WITHIN a cluster, so a global store would surface the prior
|
|
18
|
+
// cluster's names after a context switch and open ns/name in the wrong cluster.
|
|
19
|
+
// An UNKNOWN context (key still loading) is a hard no-op — no shared fallback
|
|
20
|
+
// bucket — so a recent is never read or written against an indeterminate cluster.
|
|
21
|
+
function storageKey(contextKey: string): string {
|
|
22
|
+
return `${KEY}::${contextKey}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function keyOf(r: RecentResource): string {
|
|
26
|
+
return `${r.kind}\x00${r.group || ''}\x00${r.namespace || ''}\x00${r.name}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function loadRecentResources(contextKey: string): RecentResource[] {
|
|
30
|
+
if (!contextKey) return []
|
|
31
|
+
try {
|
|
32
|
+
const raw = localStorage.getItem(storageKey(contextKey))
|
|
33
|
+
if (raw) return JSON.parse(raw)
|
|
34
|
+
} catch {
|
|
35
|
+
// ignore parse/storage errors — recents are best-effort
|
|
36
|
+
}
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function recordRecentResource(r: RecentResource, contextKey: string): void {
|
|
41
|
+
if (!r.name || !r.kind || !contextKey) return
|
|
42
|
+
try {
|
|
43
|
+
const k = keyOf(r)
|
|
44
|
+
const next = [r, ...loadRecentResources(contextKey).filter((x) => keyOf(x) !== k)].slice(0, MAX)
|
|
45
|
+
localStorage.setItem(storageKey(contextKey), JSON.stringify(next))
|
|
46
|
+
} catch {
|
|
47
|
+
// ignore storage errors
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.css
CHANGED
|
@@ -81,7 +81,11 @@ body {
|
|
|
81
81
|
color: var(--text-primary);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
#
|
|
84
|
+
/* App-shell layout for standalone Radar (mounts into #radar — see index.html).
|
|
85
|
+
Keyed off #radar, NOT #root: this file ships as the published library
|
|
86
|
+
stylesheet, so a #root rule here would match a host app's own #root and
|
|
87
|
+
reshape its document. A unique id can't collide — host apps have no #radar. */
|
|
88
|
+
#radar {
|
|
85
89
|
height: 100vh;
|
|
86
90
|
display: flex;
|
|
87
91
|
flex-direction: column;
|
|
@@ -249,6 +253,163 @@ body {
|
|
|
249
253
|
animation: fade-in-up 0.5s ease-out both;
|
|
250
254
|
}
|
|
251
255
|
|
|
256
|
+
/* Transcript entries (tool calls, reasoning) easing in as they stream — a small
|
|
257
|
+
rise from below + fade. Transform-only motion (no height change) so it never
|
|
258
|
+
perturbs the investigation's stick-to-bottom scroll geometry. */
|
|
259
|
+
@keyframes transcript-enter {
|
|
260
|
+
from {
|
|
261
|
+
opacity: 0;
|
|
262
|
+
transform: translateY(6px);
|
|
263
|
+
}
|
|
264
|
+
to {
|
|
265
|
+
opacity: 1;
|
|
266
|
+
transform: translateY(0);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
.animate-transcript-enter {
|
|
270
|
+
animation: transcript-enter 180ms ease-out both;
|
|
271
|
+
}
|
|
272
|
+
@media (prefers-reduced-motion: reduce) {
|
|
273
|
+
.animate-transcript-enter {
|
|
274
|
+
animation: none;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* "Thinking" status text — an accent light-sweep across muted text, the now-standard
|
|
279
|
+
live-agent cue (Perplexity/Vercel). Honest: it only marks active work, never fakes
|
|
280
|
+
progress. background-clip paints the gradient INTO the glyphs. */
|
|
281
|
+
@keyframes ai-shimmer {
|
|
282
|
+
from {
|
|
283
|
+
background-position: 200% 0;
|
|
284
|
+
}
|
|
285
|
+
to {
|
|
286
|
+
background-position: -200% 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
.ai-shimmer {
|
|
290
|
+
background: linear-gradient(
|
|
291
|
+
90deg,
|
|
292
|
+
var(--text-tertiary) 0%,
|
|
293
|
+
var(--text-tertiary) 38%,
|
|
294
|
+
var(--accent) 50%,
|
|
295
|
+
var(--text-tertiary) 62%,
|
|
296
|
+
var(--text-tertiary) 100%
|
|
297
|
+
);
|
|
298
|
+
background-size: 200% 100%;
|
|
299
|
+
-webkit-background-clip: text;
|
|
300
|
+
background-clip: text;
|
|
301
|
+
-webkit-text-fill-color: transparent;
|
|
302
|
+
color: transparent;
|
|
303
|
+
animation: ai-shimmer 2.4s linear infinite;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* Synthesis "thinking" dot — a calm breathing pulse (not a spinner) for the staged
|
|
307
|
+
pre-verdict beats. */
|
|
308
|
+
@keyframes synth-pulse {
|
|
309
|
+
0%,
|
|
310
|
+
100% {
|
|
311
|
+
opacity: 0.35;
|
|
312
|
+
transform: scale(0.85);
|
|
313
|
+
}
|
|
314
|
+
50% {
|
|
315
|
+
opacity: 1;
|
|
316
|
+
transform: scale(1.15);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
.animate-synth-pulse {
|
|
320
|
+
animation: synth-pulse 1.1s ease-in-out infinite;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* Result-reveal choreography. The card eases in; the root-cause label gets an accent
|
|
324
|
+
underline that sweeps out; the whole card pulses its accent border once to mark
|
|
325
|
+
"diagnosis ready." Sequenced via animation-delay relative to one mount. */
|
|
326
|
+
@keyframes result-in {
|
|
327
|
+
from {
|
|
328
|
+
opacity: 0;
|
|
329
|
+
transform: translateY(8px);
|
|
330
|
+
}
|
|
331
|
+
to {
|
|
332
|
+
opacity: 1;
|
|
333
|
+
transform: translateY(0);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
.animate-result-in {
|
|
337
|
+
animation: result-in 320ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
|
338
|
+
}
|
|
339
|
+
@keyframes underline-sweep {
|
|
340
|
+
from {
|
|
341
|
+
transform: scaleX(0);
|
|
342
|
+
}
|
|
343
|
+
to {
|
|
344
|
+
transform: scaleX(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
.animate-underline-sweep {
|
|
348
|
+
transform-origin: left;
|
|
349
|
+
animation: underline-sweep 460ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
|
|
350
|
+
}
|
|
351
|
+
/* Richer verdict reveal — a colored ring around the border plus an UNEVEN, breathing
|
|
352
|
+
outer glow (two soft peaks), held longer (~2.4s). Tinted per verdict via --glow
|
|
353
|
+
(amber root-cause, emerald healthy, accent remediation, slate inconclusive). */
|
|
354
|
+
@keyframes verdict-reveal {
|
|
355
|
+
0% {
|
|
356
|
+
box-shadow:
|
|
357
|
+
0 0 0 0 color-mix(in oklab, var(--glow, var(--accent)) 0%, transparent),
|
|
358
|
+
0 0 0 0 transparent;
|
|
359
|
+
}
|
|
360
|
+
18% {
|
|
361
|
+
box-shadow:
|
|
362
|
+
0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 60%, transparent),
|
|
363
|
+
0 0 20px 2px color-mix(in oklab, var(--glow, var(--accent)) 42%, transparent);
|
|
364
|
+
}
|
|
365
|
+
44% {
|
|
366
|
+
box-shadow:
|
|
367
|
+
0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 32%, transparent),
|
|
368
|
+
0 0 10px 1px color-mix(in oklab, var(--glow, var(--accent)) 20%, transparent);
|
|
369
|
+
}
|
|
370
|
+
68% {
|
|
371
|
+
box-shadow:
|
|
372
|
+
0 0 0 1px color-mix(in oklab, var(--glow, var(--accent)) 50%, transparent),
|
|
373
|
+
0 0 24px 3px color-mix(in oklab, var(--glow, var(--accent)) 38%, transparent);
|
|
374
|
+
}
|
|
375
|
+
100% {
|
|
376
|
+
box-shadow:
|
|
377
|
+
0 0 0 0 transparent,
|
|
378
|
+
0 0 0 0 transparent;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
.animate-verdict-reveal {
|
|
382
|
+
animation: verdict-reveal 2400ms cubic-bezier(0.32, 0.72, 0, 1) 120ms both;
|
|
383
|
+
}
|
|
384
|
+
@media (prefers-reduced-motion: reduce) {
|
|
385
|
+
.ai-shimmer {
|
|
386
|
+
animation: none;
|
|
387
|
+
background: none;
|
|
388
|
+
-webkit-text-fill-color: currentColor;
|
|
389
|
+
color: var(--text-secondary);
|
|
390
|
+
}
|
|
391
|
+
.animate-synth-pulse,
|
|
392
|
+
.animate-result-in,
|
|
393
|
+
.animate-underline-sweep,
|
|
394
|
+
.animate-verdict-reveal {
|
|
395
|
+
animation: none;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* Gentle colorize when syntax-highlighted code swaps in over plain text — opacity
|
|
400
|
+
only (no transform → no layout shift) and floored at 0.5 so it never blanks. */
|
|
401
|
+
@keyframes code-colorize {
|
|
402
|
+
from {
|
|
403
|
+
opacity: 0.5;
|
|
404
|
+
}
|
|
405
|
+
to {
|
|
406
|
+
opacity: 1;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
.animate-code-colorize {
|
|
410
|
+
animation: code-colorize 150ms ease-out;
|
|
411
|
+
}
|
|
412
|
+
|
|
252
413
|
.slide-in-from-right-5 {
|
|
253
414
|
--tw-enter-translate-x: 1.25rem;
|
|
254
415
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// bundler that transpiles TSX and resolves workspace-style peer deps. The
|
|
5
5
|
// same source is consumed by Radar's binary via main.tsx.
|
|
6
6
|
export { RadarApp, type RadarAppProps } from './RadarApp';
|
|
7
|
+
export type { ClusterLoadState } from './types/clusterLoadState';
|
|
7
8
|
export {
|
|
8
9
|
setApiBase,
|
|
9
10
|
setBasename,
|
|
@@ -14,7 +15,39 @@ export {
|
|
|
14
15
|
getAuthHeaders,
|
|
15
16
|
getCredentialsMode,
|
|
16
17
|
} from './api/config';
|
|
17
|
-
export type { NavCustomization } from './context/NavCustomization';
|
|
18
|
+
export type { NavCustomization, FleetTakeoverTarget } from './context/NavCustomization';
|
|
19
|
+
// Timeline data-source selection — lets an embedder back the timeline with a
|
|
20
|
+
// retained-history endpoint instead of Radar's local event store. Additive;
|
|
21
|
+
// absent = local (standalone behavior).
|
|
22
|
+
export type {
|
|
23
|
+
TimelineSourceConfig,
|
|
24
|
+
TimelineSourceCapabilities,
|
|
25
|
+
TimelineOverviewBucket,
|
|
26
|
+
TimelineCoverageSpan,
|
|
27
|
+
TimelineOverviewResult,
|
|
28
|
+
} from './api/timelineSource';
|
|
29
|
+
export type {
|
|
30
|
+
RenderDiagnoseAction,
|
|
31
|
+
DiagnoseConsentCopy,
|
|
32
|
+
} from './context/DiagnoseCustomization';
|
|
33
|
+
|
|
34
|
+
// Standalone AI-diagnose surface — mount the investigation panel outside a
|
|
35
|
+
// full <RadarApp>. No router dependency, no client-side cluster state: the
|
|
36
|
+
// backend set via setApiBase() picks the cluster, so hosts remount
|
|
37
|
+
// <DiagnoseProvider key={cluster}> to switch. Mount order: ThemeProvider >
|
|
38
|
+
// DiagnoseCustomizationProvider > DiagnoseProvider > DiagnoseSurface, under a
|
|
39
|
+
// @tanstack/react-query QueryClientProvider.
|
|
40
|
+
export {
|
|
41
|
+
DiagnoseProvider,
|
|
42
|
+
useDiagnose,
|
|
43
|
+
useDiagnoseLayout,
|
|
44
|
+
} from './components/diagnose/DiagnoseContext';
|
|
45
|
+
export type { Target as DiagnoseTarget } from './components/diagnose/DiagnoseContext';
|
|
46
|
+
export { DiagnoseSurface } from './components/diagnose/DiagnoseSurface';
|
|
47
|
+
export { DiagnoseCustomizationProvider } from './context/DiagnoseCustomization';
|
|
48
|
+
// The panel reads Radar's ThemeContext (throws unprovided). It follows
|
|
49
|
+
// localStorage['radar-theme'], so host and panel flip together.
|
|
50
|
+
export { ThemeProvider } from './context/ThemeContext';
|
|
18
51
|
export { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay';
|
|
19
52
|
|
|
20
53
|
// Shared cluster-switcher primitive — re-exported from @skyhook-io/k8s-ui so
|
|
@@ -22,3 +55,42 @@ export { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay';
|
|
|
22
55
|
// kubeconfig ContextSwitcher without taking a direct dep on k8s-ui internals.
|
|
23
56
|
export { ClusterSwitcher } from '@skyhook-io/k8s-ui';
|
|
24
57
|
export type { ClusterSwitcherProps, ClusterSwitcherItem } from '@skyhook-io/k8s-ui';
|
|
58
|
+
|
|
59
|
+
// Shared namespace-scope picker primitive — re-exported so embedders (Radar
|
|
60
|
+
// Hub) can render a namespace filter visually identical to OSS Radar's, driving
|
|
61
|
+
// their own per-cluster scope (Hub via ?namespaces= on the embedded RadarApp).
|
|
62
|
+
export { NamespacePicker } from '@skyhook-io/k8s-ui';
|
|
63
|
+
export type {
|
|
64
|
+
NamespacePickerProps,
|
|
65
|
+
NamespacePickerHandle,
|
|
66
|
+
NamespaceScopeView,
|
|
67
|
+
} from '@skyhook-io/k8s-ui';
|
|
68
|
+
|
|
69
|
+
// Shared bordered shell that groups the cluster + namespace segments into one
|
|
70
|
+
// pill — so Radar Hub's cluster top bar matches OSS Radar's header exactly.
|
|
71
|
+
export { ScopePill } from '@skyhook-io/k8s-ui';
|
|
72
|
+
export type { ScopePillProps } from '@skyhook-io/k8s-ui';
|
|
73
|
+
|
|
74
|
+
// Deep-link builders — so consumers (Radar Hub) construct deep links into a
|
|
75
|
+
// cluster view without hand-rolling Radar's internal URL format, which drifts
|
|
76
|
+
// silently when Radar re-routes. `resourcePath` opens the detail drawer for any
|
|
77
|
+
// kind incl. cluster-scoped; `buildWorkloadPath` is the namespaced-workload
|
|
78
|
+
// full-page view. Both return basename-relative paths; embedders prepend their
|
|
79
|
+
// cluster prefix (e.g. /c/:id).
|
|
80
|
+
export { resourcePath, buildWorkloadPath } from './utils/navigation';
|
|
81
|
+
export type { SelectedResource } from '@skyhook-io/k8s-ui/types/core';
|
|
82
|
+
|
|
83
|
+
// Injectable omnibar — the standalone search/command surface, decoupled from
|
|
84
|
+
// Radar's own data hooks so embedders (Radar Hub) can drive it with fleet
|
|
85
|
+
// search + their own command items while sharing the exact UX (pills, modifier
|
|
86
|
+
// autocomplete, kind-first ranking, match highlighting, keyboard nav, recents).
|
|
87
|
+
export { Omnibar } from './components/ui/Omnibar';
|
|
88
|
+
export type {
|
|
89
|
+
OmnibarProps,
|
|
90
|
+
OmnibarHandle,
|
|
91
|
+
OmnibarRecent,
|
|
92
|
+
OmnibarSearchResult,
|
|
93
|
+
} from './components/ui/Omnibar';
|
|
94
|
+
export { bestScore } from './components/ui/command-items';
|
|
95
|
+
export type { CommandItem } from './components/ui/command-items';
|
|
96
|
+
export type { SearchHit, SearchMatchedField } from './api/client';
|
package/src/main.tsx
CHANGED
|
@@ -124,7 +124,7 @@ document.execCommand = function (command: string, showUI?: boolean, value?: stri
|
|
|
124
124
|
dt.setData('text/plain', text)
|
|
125
125
|
const ev = new ClipboardEvent('paste', { clipboardData: dt, bubbles: true, cancelable: true })
|
|
126
126
|
if (!el.dispatchEvent(ev)) return
|
|
127
|
-
} catch
|
|
127
|
+
} catch { /* ClipboardEvent dispatch failed, fall back to insertText */ }
|
|
128
128
|
_origExecCommand('insertText', false, text)
|
|
129
129
|
}).catch((err) => { console.warn('[Radar] Paste failed:', err) })
|
|
130
130
|
return true
|
|
@@ -149,10 +149,12 @@ window.addEventListener('mouseup', (e: MouseEvent) => {
|
|
|
149
149
|
}, true)
|
|
150
150
|
|
|
151
151
|
|
|
152
|
-
// Standalone Radar binary: same-origin API, router at root.
|
|
153
|
-
//
|
|
154
|
-
|
|
152
|
+
// Standalone Radar binary: same-origin API, router at root. It owns the whole
|
|
153
|
+
// tab, so it opts into per-view document.title. Library consumers (e.g.
|
|
154
|
+
// radar-hub-web) render <RadarApp apiBase="..." basename="..." /> WITHOUT this
|
|
155
|
+
// flag, keeping their own tab title.
|
|
156
|
+
ReactDOM.createRoot(document.getElementById('radar')!).render(
|
|
155
157
|
<React.StrictMode>
|
|
156
|
-
<RadarApp />
|
|
158
|
+
<RadarApp manageDocumentTitle />
|
|
157
159
|
</React.StrictMode>
|
|
158
160
|
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ClusterLoadState {
|
|
2
|
+
loading: boolean
|
|
3
|
+
message: string | null
|
|
4
|
+
pendingKinds: string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const idleClusterLoadState: ClusterLoadState = {
|
|
8
|
+
loading: false,
|
|
9
|
+
message: null,
|
|
10
|
+
pendingKinds: [],
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function dashboardClusterLoadState(data?: {
|
|
14
|
+
deferredLoading?: boolean
|
|
15
|
+
partialData?: string[]
|
|
16
|
+
}): ClusterLoadState {
|
|
17
|
+
const pendingKinds = data?.partialData ?? []
|
|
18
|
+
if (pendingKinds.length > 0) {
|
|
19
|
+
return {
|
|
20
|
+
loading: true,
|
|
21
|
+
message: `Still loading: ${pendingKinds.join(', ')}`,
|
|
22
|
+
pendingKinds,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (data?.deferredLoading) {
|
|
26
|
+
return {
|
|
27
|
+
loading: true,
|
|
28
|
+
message: 'Loading remaining resources…',
|
|
29
|
+
pendingKinds: [],
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return idleClusterLoadState
|
|
33
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
// Re-export all types from the shared @skyhook-io/k8s-ui package.
|
|
2
2
|
// This file exists for backward compatibility — new code should import from '@skyhook-io/k8s-ui' directly.
|
|
3
3
|
export * from '@skyhook-io/k8s-ui/types/core'
|
|
4
|
+
export * from '@skyhook-io/k8s-ui/types/gitops-tree'
|
|
5
|
+
export * from '@skyhook-io/k8s-ui/types/gitops-insights'
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resourceKey, type AuditFinding, type CheckMeta } from '@skyhook-io/k8s-ui'
|
|
2
|
+
|
|
3
|
+
export interface AuditBadgeMessage {
|
|
4
|
+
severity: string
|
|
5
|
+
message: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AuditSeverityCounts {
|
|
9
|
+
danger: number
|
|
10
|
+
warning: number
|
|
11
|
+
/** The finding messages behind the counts, danger-first, for inline tooltips.
|
|
12
|
+
* Lets a badge say WHAT is wrong on hover instead of just a count. */
|
|
13
|
+
messages: AuditBadgeMessage[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* isBadgeWorthy keeps per-resource badges high-signal: only findings whose check
|
|
18
|
+
* is flagged `badgeWorthy` in the registry (reference-integrity / lifecycle —
|
|
19
|
+
* "this resource is actually broken") count. Security-posture and best-practice
|
|
20
|
+
* checks fire on nearly every resource and would turn the badges into noise;
|
|
21
|
+
* they live in the Checks/Audit views. Unknown checks default to NOT badged.
|
|
22
|
+
*/
|
|
23
|
+
export function isBadgeWorthy(
|
|
24
|
+
finding: AuditFinding,
|
|
25
|
+
checks: Record<string, CheckMeta> | undefined,
|
|
26
|
+
): boolean {
|
|
27
|
+
return !!checks?.[finding.checkID]?.badgeWorthy
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* buildAuditSeverityMap keys badge-worthy findings by the same resource key the
|
|
32
|
+
* backend stamps onto topology nodes (`node.data.auditKey`): `group|Kind|ns|name`,
|
|
33
|
+
* group following the audit convention (built-ins → their group, CRDs → "").
|
|
34
|
+
*/
|
|
35
|
+
export function buildAuditSeverityMap(
|
|
36
|
+
findings: AuditFinding[] | undefined,
|
|
37
|
+
checks: Record<string, CheckMeta> | undefined,
|
|
38
|
+
): Map<string, AuditSeverityCounts> {
|
|
39
|
+
const map = new Map<string, AuditSeverityCounts>()
|
|
40
|
+
for (const f of findings ?? []) {
|
|
41
|
+
if (!isBadgeWorthy(f, checks)) continue
|
|
42
|
+
const key = resourceKey(f.group ?? '', f.kind, f.namespace ?? '', f.name)
|
|
43
|
+
const cur = map.get(key) ?? { danger: 0, warning: 0, messages: [] }
|
|
44
|
+
if (f.severity === 'danger') cur.danger++
|
|
45
|
+
else if (f.severity === 'warning') cur.warning++
|
|
46
|
+
cur.messages.push({ severity: f.severity, message: f.message })
|
|
47
|
+
map.set(key, cur)
|
|
48
|
+
}
|
|
49
|
+
for (const cur of map.values()) {
|
|
50
|
+
cur.messages.sort((a, b) => (a.severity === 'danger' ? 0 : 1) - (b.severity === 'danger' ? 0 : 1))
|
|
51
|
+
}
|
|
52
|
+
return map
|
|
53
|
+
}
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
|
+
import { kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
3
|
+
import type { SelectedResource } from '@skyhook-io/k8s-ui/types/core'
|
|
4
|
+
import type { SearchHit } from '../api/client'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Map a resource-search Hit to a SelectedResource. Hit.kind is the singular
|
|
8
|
+
* Kind (e.g. "Deployment"); downstream openers pluralize. `group` is carried so
|
|
9
|
+
* CRD/core collisions disambiguate (Service vs Knative Service), and the
|
|
10
|
+
* namespace defaults to '' for cluster-scoped hits (Node/Namespace/PV).
|
|
11
|
+
*/
|
|
12
|
+
export function searchHitToSelectedResource(hit: SearchHit): SelectedResource {
|
|
13
|
+
return { kind: hit.kind, namespace: hit.namespace ?? '', name: hit.name, group: hit.group || undefined }
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
4
|
-
export { kindToPlural, pluralToKind, refToSelectedResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
17
|
+
export { kindToPlural, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
5
18
|
export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
6
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Build a /workload/:kind/:namespace/:name URL, preserving the API group as a
|
|
22
|
+
* query param so the WorkloadView can resolve CRDs with colliding kind names.
|
|
23
|
+
* Cluster-scoped resources (Node, PersistentVolume, Namespace, …) have no
|
|
24
|
+
* namespace; they're encoded with a '_' sentinel segment so the path stays
|
|
25
|
+
* positional and WorkloadViewRoute can parse it back. '_' is safe — it's not a
|
|
26
|
+
* valid DNS-1123 namespace label, so it can never collide with a real one.
|
|
27
|
+
*/
|
|
28
|
+
export function buildWorkloadPath(resource: SelectedResource): string {
|
|
29
|
+
const kind = encodeURIComponent(resource.kind)
|
|
30
|
+
const namespace = encodeURIComponent(resource.namespace || '_')
|
|
31
|
+
const name = encodeURIComponent(resource.name)
|
|
32
|
+
const base = `/workload/${kind}/${namespace}/${name}`
|
|
33
|
+
return resource.group ? `${base}?apiGroup=${encodeURIComponent(resource.group)}` : base
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Build a /resources/:plural?resource=:namespace/:name URL — the deep link that
|
|
38
|
+
* opens a resource's detail drawer in the resources view. Cluster-scoped
|
|
39
|
+
* resources use ?resource=:name (no slash); the API group rides in ?apiGroup=
|
|
40
|
+
* to disambiguate CRD/core kind collisions. This is the exact form the
|
|
41
|
+
* ResourcesView mount effect parses (the `?resource=` reader in
|
|
42
|
+
* packages/k8s-ui/src/components/resources/ResourcesView.tsx) — keep the two in
|
|
43
|
+
* lockstep.
|
|
44
|
+
*
|
|
45
|
+
* Unlike buildWorkloadPath, this opens the detail drawer for ANY kind,
|
|
46
|
+
* including cluster-scoped resources. Returns a basename-relative path;
|
|
47
|
+
* embedders (Radar Hub) prepend their cluster prefix (e.g. /c/:id).
|
|
48
|
+
*/
|
|
49
|
+
export function resourcePath(resource: SelectedResource): string {
|
|
50
|
+
const params = new URLSearchParams()
|
|
51
|
+
// No name → nothing to open; the kind list is the sane fallback.
|
|
52
|
+
if (resource.name) {
|
|
53
|
+
params.set('resource', resource.namespace ? `${resource.namespace}/${resource.name}` : resource.name)
|
|
54
|
+
}
|
|
55
|
+
if (resource.group) params.set('apiGroup', resource.group)
|
|
56
|
+
const query = params.toString()
|
|
57
|
+
return `/resources/${kindToPlural(resource.kind)}${query ? `?${query}` : ''}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FULLSCREEN_RESOURCE_KINDS = new Set(['pods', 'deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs', 'nodes'])
|
|
61
|
+
|
|
62
|
+
export function relatedResourcePath(resource: SelectedResource): string {
|
|
63
|
+
const apiKind = kindToPlural(resource.kind).toLowerCase()
|
|
64
|
+
if (FULLSCREEN_RESOURCE_KINDS.has(apiKind)) {
|
|
65
|
+
return buildWorkloadPath({ ...resource, kind: apiKind })
|
|
66
|
+
}
|
|
67
|
+
return resourcePath(resource)
|
|
68
|
+
}
|
|
69
|
+
|
|
7
70
|
// radar-specific: open URL in system browser (desktop app support)
|
|
8
71
|
export function openExternal(url: string): void {
|
|
9
72
|
fetch(apiUrl('/desktop/open-url'), {
|