@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.
- package/package.json +7 -7
- package/src/App.tsx +69 -16
- package/src/api/apiResources.test.ts +11 -0
- package/src/api/apiResources.ts +51 -12
- package/src/api/client.capacity.test.ts +92 -0
- package/src/api/client.ts +2905 -2081
- package/src/api/config.test.ts +47 -0
- package/src/api/config.ts +15 -0
- package/src/api/diagnose.ts +15 -15
- package/src/components/ConnectionErrorView.test.tsx +88 -0
- package/src/components/ConnectionErrorView.tsx +128 -22
- package/src/components/capacity/CapacityActivity.tsx +787 -0
- package/src/components/capacity/CapacityDemand.tsx +961 -0
- package/src/components/capacity/CapacityOverview.tsx +1529 -0
- package/src/components/capacity/CapacityPoolDetail.tsx +1626 -0
- package/src/components/capacity/CapacityView.test.tsx +2287 -0
- package/src/components/capacity/CapacityView.tsx +85 -0
- package/src/components/capacity/ClusterSchedulingCard.tsx +603 -0
- package/src/components/capacity/DemandNomination.test.tsx +151 -0
- package/src/components/capacity/certaintyGlyph.test.tsx +191 -0
- package/src/components/capacity/coverageCertainty.test.ts +162 -0
- package/src/components/capacity/podDemandGate.test.ts +47 -0
- package/src/components/capacity/podDemandGate.ts +22 -0
- package/src/components/capacity/schedulingBar.test.ts +244 -0
- package/src/components/capacity/shared.tsx +1841 -0
- package/src/components/diagnose/AISettings.tsx +21 -7
- package/src/components/diagnose/AgentSetupNotice.tsx +117 -0
- package/src/components/diagnose/DiagnoseContext.tsx +127 -57
- package/src/components/diagnose/DiagnoseSurface.tsx +33 -15
- package/src/components/diagnose/LocalDiagnoseAction.tsx +50 -27
- package/src/components/diagnose/agentCatalog.ts +30 -0
- package/src/components/diagnose/parts.test.tsx +125 -0
- package/src/components/diagnose/parts.tsx +166 -75
- package/src/components/home/CapacityCard.test.tsx +150 -0
- package/src/components/home/CapacityCard.tsx +125 -0
- package/src/components/home/HomeView.tsx +15 -1
- package/src/components/issues/IssuesPane.test.ts +142 -0
- package/src/components/issues/IssuesPane.tsx +142 -38
- package/src/components/nav/PrimaryNavRail.test.tsx +20 -0
- package/src/components/nav/PrimaryNavRail.tsx +191 -103
- package/src/components/resources/ResourcesView.tsx +9 -8
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +29 -1
- package/src/components/resources/renderers/PodRenderer.tsx +32 -3
- package/src/components/settings/SettingsDialog.tsx +31 -19
- package/src/components/timeline/TimelineView.tsx +17 -3
- package/src/components/ui/command-items.ts +222 -98
- package/src/components/workload/WorkloadView.tsx +16 -83
- package/src/context/ConnectionContext.test.ts +39 -0
- package/src/context/ConnectionContext.tsx +155 -51
- package/src/context/DiagnoseCustomization.tsx +1 -1
- package/src/utils/shell-safe.test.ts +55 -0
- package/src/utils/shell-safe.ts +21 -0
|
@@ -1,14 +1,38 @@
|
|
|
1
|
-
import type { ComponentType } from
|
|
2
|
-
import type { ReactNode } from
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
43
|
-
{ view:
|
|
44
|
-
{ view:
|
|
45
|
-
{ view:
|
|
46
|
-
{ view:
|
|
47
|
-
{ view:
|
|
48
|
-
{ view:
|
|
49
|
-
{ view:
|
|
50
|
-
{ view:
|
|
51
|
-
{ view:
|
|
52
|
-
{ view:
|
|
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({
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
103
|
-
pinned &&
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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({
|
|
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
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
</
|
|
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(
|
|
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 ?
|
|
284
|
+
aria-current={active ? "page" : undefined}
|
|
208
285
|
className={clsx(
|
|
209
|
-
|
|
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 &&
|
|
289
|
+
!pinned && "max-w-10 overflow-hidden",
|
|
213
290
|
active
|
|
214
|
-
?
|
|
215
|
-
:
|
|
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
|
-
|
|
223
|
-
active ?
|
|
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
|
|
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(
|
|
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
|
-
|
|
267
|
-
!pinned &&
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|