@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.
Files changed (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. 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
  }
@@ -3,5 +3,6 @@ export {
3
3
  useRegisterShortcut,
4
4
  useRegisterShortcuts,
5
5
  useActiveShortcuts,
6
+ useSuppressBaseShortcuts,
6
7
  } from '@skyhook-io/k8s-ui'
7
8
  export type { KeyboardShortcut, ShortcutCategory } from '@skyhook-io/k8s-ui'
@@ -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
- #root {
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 (_e) { /* ClipboardEvent dispatch failed, fall back to insertText */ }
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. Library consumers
153
- // (e.g. radar-hub-web) render <RadarApp apiBase="..." basename="..." /> instead.
154
- ReactDOM.createRoot(document.getElementById('root')!).render(
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
+ }
@@ -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'), {