@skyhook-io/radar-app 1.9.0 → 1.9.2

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 (52) hide show
  1. package/package.json +7 -7
  2. package/src/App.tsx +69 -16
  3. package/src/api/apiResources.test.ts +11 -0
  4. package/src/api/apiResources.ts +51 -12
  5. package/src/api/client.capacity.test.ts +92 -0
  6. package/src/api/client.ts +2905 -2081
  7. package/src/api/config.test.ts +47 -0
  8. package/src/api/config.ts +15 -0
  9. package/src/api/diagnose.ts +15 -15
  10. package/src/components/ConnectionErrorView.test.tsx +88 -0
  11. package/src/components/ConnectionErrorView.tsx +128 -22
  12. package/src/components/capacity/CapacityActivity.tsx +787 -0
  13. package/src/components/capacity/CapacityDemand.tsx +961 -0
  14. package/src/components/capacity/CapacityOverview.tsx +1529 -0
  15. package/src/components/capacity/CapacityPoolDetail.tsx +1626 -0
  16. package/src/components/capacity/CapacityView.test.tsx +2287 -0
  17. package/src/components/capacity/CapacityView.tsx +85 -0
  18. package/src/components/capacity/ClusterSchedulingCard.tsx +603 -0
  19. package/src/components/capacity/DemandNomination.test.tsx +151 -0
  20. package/src/components/capacity/certaintyGlyph.test.tsx +191 -0
  21. package/src/components/capacity/coverageCertainty.test.ts +162 -0
  22. package/src/components/capacity/podDemandGate.test.ts +47 -0
  23. package/src/components/capacity/podDemandGate.ts +22 -0
  24. package/src/components/capacity/schedulingBar.test.ts +244 -0
  25. package/src/components/capacity/shared.tsx +1841 -0
  26. package/src/components/diagnose/AISettings.tsx +21 -7
  27. package/src/components/diagnose/AgentSetupNotice.tsx +117 -0
  28. package/src/components/diagnose/DiagnoseContext.tsx +127 -57
  29. package/src/components/diagnose/DiagnoseSurface.tsx +33 -15
  30. package/src/components/diagnose/LocalDiagnoseAction.tsx +50 -27
  31. package/src/components/diagnose/agentCatalog.ts +30 -0
  32. package/src/components/diagnose/parts.test.tsx +125 -0
  33. package/src/components/diagnose/parts.tsx +166 -75
  34. package/src/components/home/CapacityCard.test.tsx +150 -0
  35. package/src/components/home/CapacityCard.tsx +125 -0
  36. package/src/components/home/HomeView.tsx +15 -1
  37. package/src/components/issues/IssuesPane.test.ts +142 -0
  38. package/src/components/issues/IssuesPane.tsx +142 -38
  39. package/src/components/nav/PrimaryNavRail.test.tsx +20 -0
  40. package/src/components/nav/PrimaryNavRail.tsx +191 -103
  41. package/src/components/resources/ResourcesView.tsx +9 -8
  42. package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +29 -1
  43. package/src/components/resources/renderers/PodRenderer.tsx +32 -3
  44. package/src/components/settings/SettingsDialog.tsx +31 -19
  45. package/src/components/timeline/TimelineView.tsx +17 -3
  46. package/src/components/ui/command-items.ts +222 -98
  47. package/src/components/workload/WorkloadView.tsx +16 -83
  48. package/src/context/ConnectionContext.test.ts +39 -0
  49. package/src/context/ConnectionContext.tsx +155 -51
  50. package/src/context/DiagnoseCustomization.tsx +1 -1
  51. package/src/utils/shell-safe.test.ts +55 -0
  52. package/src/utils/shell-safe.ts +21 -0
@@ -1,14 +1,38 @@
1
- import type { ComponentType } from 'react'
2
- import type { ReactNode } from 'react'
3
- import { Home, Network, List, Clock, AlertTriangle, Package, GitBranch, Boxes, Activity, DollarSign, ShieldCheck, Settings, PanelLeftClose, PanelLeftOpen } from 'lucide-react'
4
- import { clsx } from 'clsx'
5
- import type { MainView } from '../../types'
6
- import { Tooltip } from '../ui/Tooltip'
1
+ import type { ComponentType } from "react";
2
+ import type { ReactNode } from "react";
3
+ import {
4
+ Home,
5
+ Network,
6
+ List,
7
+ Clock,
8
+ AlertTriangle,
9
+ Package,
10
+ GitBranch,
11
+ Boxes,
12
+ Activity,
13
+ DollarSign,
14
+ Gauge,
15
+ ShieldCheck,
16
+ Settings,
17
+ PanelLeftClose,
18
+ PanelLeftOpen,
19
+ } from "lucide-react";
20
+ import { clsx } from "clsx";
21
+ import type { MainView } from "../../types";
22
+ import { Tooltip } from "../ui/Tooltip";
7
23
 
8
24
  // The views the rail can navigate to. Broader than k8s-ui's ExtendedMainView
9
25
  // (which omits 'applications') — it mirrors the navigable subset of App.tsx's
10
26
  // own view union, so onNavigate accepts App's setMainView directly.
11
- type NavRailView = MainView | 'issues' | 'traffic' | 'gitops' | 'applications' | 'cost' | 'checks'
27
+ type NavRailView =
28
+ | MainView
29
+ | "issues"
30
+ | "traffic"
31
+ | "gitops"
32
+ | "applications"
33
+ | "cost"
34
+ | "capacity"
35
+ | "checks";
12
36
 
13
37
  // Primary left nav rail for standalone (non-embedded) Radar.
14
38
  //
@@ -27,51 +51,63 @@ type NavRailView = MainView | 'issues' | 'traffic' | 'gitops' | 'applications' |
27
51
  // non-breaking and avoids triple-stacked left chrome.
28
52
 
29
53
  interface NavItemDef {
30
- view: NavRailView
31
- icon: ComponentType<{ className?: string }>
32
- label: string
54
+ view: NavRailView;
55
+ icon: ComponentType<{ className?: string }>;
56
+ label: string;
33
57
  }
34
58
 
35
59
  // The full standalone view set, flat (no group dividers). Order descends by
36
60
  // day-to-day frequency: Home, then the Resources/Issues/Topology core ("what's
37
61
  // running / what's wrong / how's it wired"), then app + temporal views,
38
- // delivery, and finally the periodic posture/spend pair. The rail's vertical
62
+ // delivery, and finally the posture tail (Checks, Capacity, Cost). Capacity
63
+ // sits there — next to Cost, its spend-lever sibling. It renders on every
64
+ // cluster (like Issues/Topology): the view itself reads the cluster's capacity
65
+ // posture across all node managers, not just Karpenter. The rail's vertical
39
66
  // room lets us surface the views the 8-slot pill bar dropped (Issues,
40
67
  // Applications, Cost).
41
68
  const NAV_ITEMS: NavItemDef[] = [
42
- { view: 'home', icon: Home, label: 'Home' },
43
- { view: 'resources', icon: List, label: 'Resources' },
44
- { view: 'issues', icon: AlertTriangle, label: 'Issues' },
45
- { view: 'topology', icon: Network, label: 'Topology' },
46
- { view: 'applications', icon: Boxes, label: 'Applications' },
47
- { view: 'timeline', icon: Clock, label: 'Timeline' },
48
- { view: 'traffic', icon: Activity, label: 'Live Traffic' },
49
- { view: 'helm', icon: Package, label: 'Helm' },
50
- { view: 'gitops', icon: GitBranch, label: 'GitOps' },
51
- { view: 'checks', icon: ShieldCheck, label: 'Checks' },
52
- { view: 'cost', icon: DollarSign, label: 'Cost' },
53
- ]
69
+ { view: "home", icon: Home, label: "Home" },
70
+ { view: "resources", icon: List, label: "Resources" },
71
+ { view: "issues", icon: AlertTriangle, label: "Issues" },
72
+ { view: "topology", icon: Network, label: "Topology" },
73
+ { view: "applications", icon: Boxes, label: "Applications" },
74
+ { view: "timeline", icon: Clock, label: "Timeline" },
75
+ { view: "traffic", icon: Activity, label: "Live Traffic" },
76
+ { view: "helm", icon: Package, label: "Helm" },
77
+ { view: "gitops", icon: GitBranch, label: "GitOps" },
78
+ { view: "checks", icon: ShieldCheck, label: "Checks" },
79
+ { view: "capacity", icon: Gauge, label: "Capacity" },
80
+ { view: "cost", icon: DollarSign, label: "Cost" },
81
+ ];
54
82
 
55
83
  interface PrimaryNavRailProps {
56
84
  // `string`, not ExtendedMainView: App.tsx's mainView is a superset
57
85
  // (adds 'applications'/'workload'/'compare') that isn't in k8s-ui's
58
86
  // ExtendedMainView. Active state only needs equality, so accept any
59
87
  // view id and compare against our NAV_ITEMS views.
60
- activeView: string
61
- onNavigate: (view: NavRailView) => void
62
- pinned: boolean
63
- onTogglePinned: () => void
88
+ activeView: string;
89
+ onNavigate: (view: NavRailView) => void;
90
+ pinned: boolean;
91
+ onTogglePinned: () => void;
64
92
  // Hidden on narrow windows where the rail is responsively forced slim — an
65
93
  // expand control there would just re-breach the content floor.
66
- showPinToggle?: boolean
94
+ showPinToggle?: boolean;
67
95
  // Rail-bottom "me & my tools" cluster. onOpenSettings opens the Settings
68
96
  // dialog; accountSlot is the account control (App passes <UserMenu variant=…>,
69
97
  // which self-nulls without auth so the row vanishes in no-auth OSS).
70
- onOpenSettings?: () => void
71
- accountSlot?: ReactNode
98
+ onOpenSettings?: () => void;
99
+ accountSlot?: ReactNode;
72
100
  }
73
101
 
74
- export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned, showPinToggle = true, onOpenSettings, accountSlot }: PrimaryNavRailProps) {
102
+ export function PrimaryNavRail({
103
+ activeView,
104
+ onNavigate,
105
+ pinned,
106
+ onTogglePinned,
107
+ showPinToggle = true,
108
+ onOpenSettings,
109
+ accountSlot,
110
+ }: PrimaryNavRailProps) {
75
111
  return (
76
112
  <aside
77
113
  aria-label="Primary navigation"
@@ -81,11 +117,11 @@ export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned,
81
117
  // facet pane happens to sit next to it (the content floor is `base`, which
82
118
  // a `base` rail blends into). Neutral by design: the brand accent lives on
83
119
  // the active item, not the nav background.
84
- 'shrink-0 flex flex-col bg-theme-sidebar border-r border-theme-border h-full transition-[width] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]',
120
+ "shrink-0 flex flex-col bg-theme-sidebar border-r border-theme-border h-full transition-[width] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)]",
85
121
  // w-44 (176px): OSS labels are short (longest is "Applications"), so the
86
122
  // rail is trimmer than Radar Cloud's w-60 (which carries long cluster
87
123
  // names). Keep in sync with the minWidth content-floor calc in App.tsx.
88
- pinned ? 'w-44' : 'w-14',
124
+ pinned ? "w-44" : "w-14",
89
125
  )}
90
126
  >
91
127
  <BrandRow pinned={pinned} onNavigate={onNavigate} />
@@ -99,8 +135,8 @@ export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned,
99
135
  width-triggered) and yields to keeping fly-outs intact. */}
100
136
  <nav
101
137
  className={clsx(
102
- 'flex flex-col gap-0.5 pt-3 px-2',
103
- pinned && 'flex-1 min-h-0 overflow-y-auto',
138
+ "flex flex-col gap-0.5 pt-3 px-2",
139
+ pinned && "flex-1 min-h-0 overflow-y-auto",
104
140
  )}
105
141
  >
106
142
  {NAV_ITEMS.map((item) => (
@@ -122,7 +158,12 @@ export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned,
122
158
  <nav className="flex flex-col gap-0.5 px-2 pt-1 border-t border-theme-border/50">
123
159
  {accountSlot}
124
160
  {onOpenSettings && (
125
- <RailActionRow icon={Settings} label="Settings" pinned={pinned} onClick={onOpenSettings} />
161
+ <RailActionRow
162
+ icon={Settings}
163
+ label="Settings"
164
+ pinned={pinned}
165
+ onClick={onOpenSettings}
166
+ />
126
167
  )}
127
168
  </nav>
128
169
 
@@ -131,60 +172,96 @@ export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned,
131
172
  expanded, open-panel when slim. Hidden when the rail is responsively
132
173
  forced slim (showPinToggle=false) — expanding there isn't available. */}
133
174
  {showPinToggle && (
134
- <div className="px-2 pb-2 pt-1 border-t border-theme-border/50">
135
- <Tooltip content={pinned ? 'Collapse navigation' : 'Expand navigation'} position="right" wrapperClassName="!block w-full shrink-0">
136
- <button
137
- type="button"
138
- onClick={onTogglePinned}
139
- aria-label={pinned ? 'Collapse navigation' : 'Expand navigation'}
140
- className="group/pin relative flex h-9 w-full items-center rounded-md text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary transition-colors"
141
- >
142
- <span className="flex w-10 shrink-0 items-center justify-center">
143
- {pinned ? <PanelLeftClose className="w-[18px] h-[18px]" /> : <PanelLeftOpen className="w-[18px] h-[18px]" />}
144
- </span>
145
- {/* `hidden` (not sr-only) when collapsed: the button's aria-label is
175
+ <div className="px-2 pb-2 pt-1 border-t border-theme-border/50">
176
+ <Tooltip
177
+ content={pinned ? "Collapse navigation" : "Expand navigation"}
178
+ position="right"
179
+ wrapperClassName="!block w-full shrink-0"
180
+ >
181
+ <button
182
+ type="button"
183
+ onClick={onTogglePinned}
184
+ aria-label={pinned ? "Collapse navigation" : "Expand navigation"}
185
+ className="group/pin relative flex h-9 w-full items-center rounded-md text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-secondary transition-colors"
186
+ >
187
+ <span className="flex w-10 shrink-0 items-center justify-center">
188
+ {pinned ? (
189
+ <PanelLeftClose className="w-[18px] h-[18px]" />
190
+ ) : (
191
+ <PanelLeftOpen className="w-[18px] h-[18px]" />
192
+ )}
193
+ </span>
194
+ {/* `hidden` (not sr-only) when collapsed: the button's aria-label is
146
195
  the accessible name; leaving an "Collapse" label in the a11y tree
147
196
  would contradict the "Expand navigation" label. */}
148
- <span className={clsx('text-[13px] font-medium', !pinned && 'hidden')}>Collapse</span>
149
- </button>
150
- </Tooltip>
151
- </div>
197
+ <span
198
+ className={clsx("text-[13px] font-medium", !pinned && "hidden")}
199
+ >
200
+ Collapse
201
+ </span>
202
+ </button>
203
+ </Tooltip>
204
+ </div>
152
205
  )}
153
206
  </aside>
154
- )
207
+ );
155
208
  }
156
209
 
157
- function BrandRow({ pinned, onNavigate }: { pinned: boolean; onNavigate: (view: NavRailView) => void }) {
210
+ function BrandRow({
211
+ pinned,
212
+ onNavigate,
213
+ }: {
214
+ pinned: boolean;
215
+ onNavigate: (view: NavRailView) => void;
216
+ }) {
158
217
  // Clickable brand = secondary home affordance (logo→home convention). The
159
218
  // Home nav item below still carries the active state; the brand just navigates.
160
219
  return (
161
- <Tooltip content="Home" position="right" wrapperClassName="!block w-full shrink-0">
162
- <button
163
- type="button"
164
- onClick={() => onNavigate('home')}
165
- aria-label="Radar — go to home"
166
- // Height matches the top bar header (App.tsx — items-center + py-2 = 51px)
167
- // so the rail's brand divider and the header's bottom border form one line.
168
- className="flex h-[51px] w-full items-center border-b border-theme-border/50 shrink-0 transition-opacity hover:opacity-80"
220
+ <Tooltip
221
+ content="Home"
222
+ position="right"
223
+ wrapperClassName="!block w-full shrink-0"
169
224
  >
170
- <span className="flex w-14 shrink-0 items-center justify-center">
171
- <span className="relative w-7 h-7 rounded-lg overflow-hidden bg-emerald-500/10 border border-emerald-500/20">
172
- <img
173
- src="/images/radar/radar-icon.svg"
174
- alt=""
175
- aria-hidden
176
- className="w-full h-full p-0.5"
177
- onError={(e) => console.error('Radar logo asset failed to load:', (e.currentTarget as HTMLImageElement).src)}
178
- />
225
+ <button
226
+ type="button"
227
+ onClick={() => onNavigate("home")}
228
+ aria-label="Radar — go to home"
229
+ // Height matches the top bar header (App.tsx — items-center + py-2 = 51px)
230
+ // so the rail's brand divider and the header's bottom border form one line.
231
+ className="flex h-[51px] w-full items-center border-b border-theme-border/50 shrink-0 transition-opacity hover:opacity-80"
232
+ >
233
+ <span className="flex w-14 shrink-0 items-center justify-center">
234
+ <span className="relative w-7 h-7 rounded-lg overflow-hidden bg-emerald-500/10 border border-emerald-500/20">
235
+ <img
236
+ src="/images/radar/radar-icon.svg"
237
+ alt=""
238
+ aria-hidden
239
+ className="w-full h-full p-0.5"
240
+ onError={(e) =>
241
+ console.error(
242
+ "Radar logo asset failed to load:",
243
+ (e.currentTarget as HTMLImageElement).src,
244
+ )
245
+ }
246
+ />
247
+ </span>
248
+ </span>
249
+ <span
250
+ className={clsx(
251
+ "flex flex-col leading-none text-left",
252
+ !pinned && "opacity-0 pointer-events-none",
253
+ )}
254
+ >
255
+ <span className="font-semibold text-[15px] tracking-tight text-theme-text-primary">
256
+ Radar
257
+ </span>
258
+ <span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">
259
+ by Skyhook
260
+ </span>
179
261
  </span>
180
- </span>
181
- <span className={clsx('flex flex-col leading-none text-left', !pinned && 'opacity-0 pointer-events-none')}>
182
- <span className="font-semibold text-[15px] tracking-tight text-theme-text-primary">Radar</span>
183
- <span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">by Skyhook</span>
184
- </span>
185
- </button>
262
+ </button>
186
263
  </Tooltip>
187
- )
264
+ );
188
265
  }
189
266
 
190
267
  function NavRailItem({
@@ -193,40 +270,49 @@ function NavRailItem({
193
270
  pinned,
194
271
  onNavigate,
195
272
  }: {
196
- item: NavItemDef
197
- active: boolean
198
- pinned: boolean
199
- onNavigate: (view: NavRailView) => void
273
+ item: NavItemDef;
274
+ active: boolean;
275
+ pinned: boolean;
276
+ onNavigate: (view: NavRailView) => void;
200
277
  }) {
201
- const { icon: Icon, label, view } = item
278
+ const { icon: Icon, label, view } = item;
202
279
  return (
203
- <div className={clsx('group/item relative', !pinned && 'w-10')}>
280
+ <div className={clsx("group/item relative", !pinned && "w-10")}>
204
281
  <button
205
282
  type="button"
206
283
  onClick={() => onNavigate(view)}
207
- aria-current={active ? 'page' : undefined}
284
+ aria-current={active ? "page" : undefined}
208
285
  className={clsx(
209
- 'relative flex h-9 w-full items-center rounded-md text-sm font-medium transition-colors',
286
+ "relative flex h-9 w-full items-center rounded-md text-sm font-medium transition-colors",
210
287
  // Slim mode: clip the hit area to the icon column so the (opacity-0)
211
288
  // label can't capture clicks meant for content to the right.
212
- !pinned && 'max-w-10 overflow-hidden',
289
+ !pinned && "max-w-10 overflow-hidden",
213
290
  active
214
- ? 'bg-skyhook-600/10 dark:bg-skyhook-500/15 text-skyhook-700 dark:text-skyhook-300'
215
- : 'text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary',
291
+ ? "bg-skyhook-600/10 dark:bg-skyhook-500/15 text-skyhook-700 dark:text-skyhook-300"
292
+ : "text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary",
216
293
  )}
217
294
  >
218
295
  {/* Left-edge accent bar on active — reads even when the row tint is soft. */}
219
296
  <span
220
297
  aria-hidden
221
298
  className={clsx(
222
- 'absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-r-full bg-skyhook-600 dark:bg-skyhook-400 transition-opacity',
223
- active ? 'opacity-100' : 'opacity-0',
299
+ "absolute left-0 top-1/2 h-5 w-[3px] -translate-y-1/2 rounded-r-full bg-skyhook-600 dark:bg-skyhook-400 transition-opacity",
300
+ active ? "opacity-100" : "opacity-0",
224
301
  )}
225
302
  />
226
303
  <span className="flex w-10 shrink-0 items-center justify-center">
227
- <Icon className={clsx('w-[18px] h-[18px]', active ? 'text-skyhook-700 dark:text-skyhook-300' : 'text-theme-text-tertiary group-hover/item:text-theme-text-secondary')} />
304
+ <Icon
305
+ className={clsx(
306
+ "w-[18px] h-[18px]",
307
+ active
308
+ ? "text-skyhook-700 dark:text-skyhook-300"
309
+ : "text-theme-text-tertiary group-hover/item:text-theme-text-secondary",
310
+ )}
311
+ />
312
+ </span>
313
+ <span className={clsx("pr-3 truncate", !pinned && "opacity-0")}>
314
+ {label}
228
315
  </span>
229
- <span className={clsx('pr-3 truncate', !pinned && 'opacity-0')}>{label}</span>
230
316
  </button>
231
317
 
232
318
  {/* Slim-mode fly-out label — sibling of the button so it escapes the
@@ -240,7 +326,7 @@ function NavRailItem({
240
326
  </span>
241
327
  )}
242
328
  </div>
243
- )
329
+ );
244
330
  }
245
331
 
246
332
  // A rail-bottom action row — same icon-column + fly-out treatment as NavRailItem
@@ -252,25 +338,27 @@ export function RailActionRow({
252
338
  pinned,
253
339
  onClick,
254
340
  }: {
255
- icon: ComponentType<{ className?: string }>
256
- label: string
257
- pinned: boolean
258
- onClick: () => void
341
+ icon: ComponentType<{ className?: string }>;
342
+ label: string;
343
+ pinned: boolean;
344
+ onClick: () => void;
259
345
  }) {
260
346
  return (
261
- <div className={clsx('group/item relative', !pinned && 'w-10')}>
347
+ <div className={clsx("group/item relative", !pinned && "w-10")}>
262
348
  <button
263
349
  type="button"
264
350
  onClick={onClick}
265
351
  className={clsx(
266
- 'relative flex h-9 w-full items-center rounded-md text-sm font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary transition-colors',
267
- !pinned && 'max-w-10 overflow-hidden',
352
+ "relative flex h-9 w-full items-center rounded-md text-sm font-medium text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary transition-colors",
353
+ !pinned && "max-w-10 overflow-hidden",
268
354
  )}
269
355
  >
270
356
  <span className="flex w-10 shrink-0 items-center justify-center">
271
357
  <Icon className="w-[18px] h-[18px] text-theme-text-tertiary group-hover/item:text-theme-text-secondary" />
272
358
  </span>
273
- <span className={clsx('pr-3 truncate', !pinned && 'opacity-0')}>{label}</span>
359
+ <span className={clsx("pr-3 truncate", !pinned && "opacity-0")}>
360
+ {label}
361
+ </span>
274
362
  </button>
275
363
  {!pinned && (
276
364
  <span
@@ -281,5 +369,5 @@ export function RailActionRow({
281
369
  </span>
282
370
  )}
283
371
  </div>
284
- )
372
+ );
285
373
  }
@@ -4,7 +4,7 @@ import { useQuery } from '@tanstack/react-query'
4
4
  import { ApiError, debugNamespaceLog, fetchJSON, isForbiddenError, useCapabilities, useNamespaceCapabilities, useSecretCertExpiry, useTopPodMetrics, useTopNodeMetrics, useBulkDeleteResources, useBulkRestartWorkloads, useBulkScaleWorkloads, useAudit } from '../../api/client'
5
5
  import { isBadgeWorthy } from '../../utils/auditBadges'
6
6
  import type { AuditBadgeMessage } from '@skyhook-io/k8s-ui'
7
- import { apiUrl, getAuthHeaders, getCredentialsMode, getBasename } from '../../api/config'
7
+ import { apiUrl, getAuthHeaders, getCredentialsMode, stripBasename } from '../../api/config'
8
8
  import { useAPIResources } from '../../api/apiResources'
9
9
  import { useConnection } from '../../context/ConnectionContext'
10
10
  import { initNavigationMap } from '@skyhook-io/k8s-ui'
@@ -198,7 +198,13 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
198
198
  const podCountAllowsBulkMetrics = countsData != null && podCountKnown && !podCountUnavailable && (podCount ?? 0) <= LARGE_RESOURCE_LIST_LIMIT
199
199
  const selectedKindName = selectedKind?.name.toLowerCase() ?? ''
200
200
  const topPodMetricsEnabled = selectedKindName === 'pods' && podCountAllowsBulkMetrics
201
- const topNodeMetricsEnabled = selectedKindName === 'nodes' && namespaces.length === 0 && podCountAllowsBulkMetrics
201
+ // Node metrics back the Nodes table and, for the Pods table, the pod-vs-node
202
+ // context line in the CPU/Memory tooltip (a pod can be fine against its own
203
+ // limit yet at risk from a saturated node). Nodes are cluster-wide, so the
204
+ // pods case is not gated on the namespace filter.
205
+ const topNodeMetricsEnabled =
206
+ ((selectedKindName === 'nodes' && namespaces.length === 0) || selectedKindName === 'pods') &&
207
+ podCountAllowsBulkMetrics
202
208
  const largeListGuard = selectedKind && largeListBlocked
203
209
  ? {
204
210
  kind: selectedKind.name,
@@ -295,13 +301,8 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
295
301
  // any host that mounts RadarApp under a non-empty basename (Radar Cloud).
296
302
  // Strip the basename here so react-router can re-apply it cleanly.
297
303
  const handleNavigate = useMemo(() => {
298
- const base = getBasename()
299
304
  return (path: string, options?: { replace?: boolean }) => {
300
- let p = path
301
- if (base && (p === base || p.startsWith(base + '/') || p.startsWith(base + '?'))) {
302
- p = p.slice(base.length) || '/'
303
- }
304
- navigate(p, { replace: options?.replace })
305
+ navigate(stripBasename(path), { replace: options?.replace })
305
306
  }
306
307
  }, [navigate])
307
308
 
@@ -1 +1,29 @@
1
- export * from '@skyhook-io/k8s-ui/components/resources/renderers/KarpenterNodePoolRenderer'
1
+ import { KarpenterNodePoolRenderer as BaseKarpenterNodePoolRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/KarpenterNodePoolRenderer'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useCapabilitiesContext } from '../../../contexts/CapabilitiesContext'
4
+
5
+ interface KarpenterNodePoolRendererProps {
6
+ data: any
7
+ onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
8
+ }
9
+
10
+ // The Capacity pool-detail page is a strictly richer view of a NodePool than
11
+ // the raw drawer (ledger, workloads, demand, activity) — bridge to it whenever
12
+ // the Capacity view is actually reachable for this user.
13
+ export function KarpenterNodePoolRenderer({ data, onNavigate }: KarpenterNodePoolRendererProps) {
14
+ const navigate = useNavigate()
15
+ const karpenterAvailable = useCapabilitiesContext().karpenter?.state === 'available'
16
+ const name = data?.metadata?.name
17
+
18
+ return (
19
+ <BaseKarpenterNodePoolRenderer
20
+ data={data}
21
+ onNavigate={onNavigate}
22
+ onOpenCapacity={
23
+ karpenterAvailable && name
24
+ ? () => navigate(`/capacity/pools/${encodeURIComponent(name)}`)
25
+ : undefined
26
+ }
27
+ />
28
+ )
29
+ }
@@ -1,10 +1,12 @@
1
1
  import { PodRenderer as BasePodRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/PodRenderer'
2
2
  import type { CopyHandler } from '@skyhook-io/k8s-ui/components/ui/drawer-components'
3
- import type { ResolvedEnvFrom } from '@skyhook-io/k8s-ui'
3
+ import type { PodEnvironmentRevealResponse, ResolvedEnvFrom } from '@skyhook-io/k8s-ui'
4
+ import { useNavigate } from 'react-router-dom'
4
5
  import { useOpenTerminal, useOpenLogs } from '../../dock'
5
- import { useNamespacedCapabilities, useIsLocalDeployment } from '../../../contexts/CapabilitiesContext'
6
- import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus } from '../../../api/client'
6
+ import { useCapabilitiesContext, useNamespacedCapabilities, useIsLocalDeployment } from '../../../contexts/CapabilitiesContext'
7
+ import { getVisibleLiveMetrics, isLiveMetricsUnavailable, shouldFetchLiveMetrics, usePodEnvironment, usePodMetrics, usePodMetricsHistory, usePrometheusResourceMetrics, usePrometheusStatus, useRevealPodEnvironment } from '../../../api/client'
7
8
  import { useRBACSubject } from '../../../api/rbac'
9
+ import { podAwaitsScheduling } from '../../capacity/podDemandGate'
8
10
  import { PortForwardInlineButton } from '../../portforward/PortForwardButton'
9
11
  import { ImageFilesystemModal } from '../ImageFilesystemModal'
10
12
  import { PodFilesystemModal } from '../PodFilesystemModal'
@@ -21,9 +23,19 @@ interface PodRendererProps {
21
23
  export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, resolvedEnvFrom }: PodRendererProps) {
22
24
  const namespace = data.metadata?.namespace
23
25
  const podName = data.metadata?.name
26
+ const environmentEnabled = [...(data.spec?.initContainers ?? []), ...(data.spec?.containers ?? [])]
27
+ .some((container: any) => container.env?.length > 0 || container.envFrom?.length > 0)
28
+ const environmentQuery = usePodEnvironment(namespace ?? '', podName ?? '', environmentEnabled)
29
+ const revealEnvironment = useRevealPodEnvironment()
24
30
 
25
31
  const openTerminal = useOpenTerminal()
26
32
  const openLogsPanel = useOpenLogs()
33
+ const navigate = useNavigate()
34
+
35
+ // Unscheduled pod on a Karpenter cluster -> bridge into the Capacity Demand
36
+ // view (the purpose-built surface for "why is this pod pending").
37
+ const karpenterAvailable = useCapabilitiesContext().karpenter?.state === 'available'
38
+ const awaitsScheduling = podAwaitsScheduling(data)
27
39
 
28
40
  // Capabilities (namespace-scoped: re-checks RBAC if globally denied)
29
41
  const { canExec, canViewLogs, canPortForward } = useNamespacedCapabilities(namespace)
@@ -69,7 +81,24 @@ export function PodRenderer({ data, onCopy, copied, onNavigate, onOpenLogs, reso
69
81
  copied={copied}
70
82
  onNavigate={onNavigate}
71
83
  onOpenLogs={onOpenLogs}
84
+ onEvaluateCapacity={
85
+ karpenterAvailable && awaitsScheduling
86
+ ? () =>
87
+ navigate(
88
+ `/capacity/demand?pod=${encodeURIComponent(`${data.metadata?.namespace ?? ''}/${data.metadata?.name ?? ''}`)}`,
89
+ )
90
+ : undefined
91
+ }
72
92
  resolvedEnvFrom={resolvedEnvFrom}
93
+ environment={environmentQuery.data}
94
+ environmentLoading={environmentQuery.isLoading}
95
+ environmentError={environmentQuery.error as Error | null}
96
+ onRevealEnvironment={(container, variable): Promise<PodEnvironmentRevealResponse> => revealEnvironment.mutateAsync({
97
+ namespace: namespace ?? '',
98
+ podName: podName ?? '',
99
+ container,
100
+ variable,
101
+ })}
73
102
  rbacData={rbacData ?? null}
74
103
  rbacLoading={rbacLoading}
75
104
  rbacError={rbacError as Error | null}