@skyhook-io/radar-app 1.5.0 → 1.6.0
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 +4 -4
- package/src/App.tsx +168 -42
- package/src/RadarApp.tsx +9 -1
- package/src/api/client.ts +65 -2
- package/src/components/UserMenu.tsx +56 -10
- package/src/components/applications/ApplicationsView.tsx +27 -19
- package/src/components/audit/AuditSettingsDialog.tsx +1 -1
- package/src/components/audit/AuditView.tsx +23 -35
- package/src/components/gitops/GitOpsView.tsx +24 -2
- package/src/components/helm/HelmView.tsx +12 -8
- package/src/components/home/HomeView.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +34 -0
- package/src/components/issues/IssuesPane.tsx +82 -28
- package/src/components/nav/PrimaryNavRail.tsx +282 -0
- package/src/components/resource/HPACharts.tsx +7 -2
- package/src/components/resource/RestartChart.tsx +8 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +34 -3
- package/src/components/settings/SettingsDialog.tsx +18 -1
- package/src/components/ui/CommandPalette.tsx +6 -215
- package/src/components/ui/Omnibar.tsx +493 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +3 -1
- package/src/context/NavCustomization.tsx +11 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/utils/navigation.ts +11 -0
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
|
|
7
|
+
// The views the rail can navigate to. Broader than k8s-ui's ExtendedMainView
|
|
8
|
+
// (which omits 'applications') — it mirrors the navigable subset of App.tsx's
|
|
9
|
+
// own view union, so onNavigate accepts App's setMainView directly.
|
|
10
|
+
type NavRailView = MainView | 'issues' | 'traffic' | 'gitops' | 'applications' | 'cost' | 'checks'
|
|
11
|
+
|
|
12
|
+
// Primary left nav rail for standalone (non-embedded) Radar.
|
|
13
|
+
//
|
|
14
|
+
// Ported from radar-hub-web's src/shell/LeftRail.tsx, simplified for OSS:
|
|
15
|
+
// Radar's window is desktop-only (App.tsx outer `min-w-[800px]`), so there's
|
|
16
|
+
// no mobile slide-in sheet — just two committed states driven by a persisted
|
|
17
|
+
// pin (see useNavRailPinned):
|
|
18
|
+
// - pinned → labeled w-48 sidebar.
|
|
19
|
+
// - unpinned → slim w-14 icon rail; labels surface as `left-full` fly-outs
|
|
20
|
+
// on hover (NOT a whole-rail hover-expand, which would reflow
|
|
21
|
+
// content under the cursor).
|
|
22
|
+
//
|
|
23
|
+
// In embedded mode (Radar Hub) this rail is not rendered at all — the host
|
|
24
|
+
// owns the left chrome via its own fleet LeftRail, and Radar falls back to
|
|
25
|
+
// its top-bar pill nav. That keeps the @skyhook-io/radar-app surface
|
|
26
|
+
// non-breaking and avoids triple-stacked left chrome.
|
|
27
|
+
|
|
28
|
+
interface NavItemDef {
|
|
29
|
+
view: NavRailView
|
|
30
|
+
icon: ComponentType<{ className?: string }>
|
|
31
|
+
label: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The full standalone view set, flat (no group dividers). Order descends by
|
|
35
|
+
// day-to-day frequency: Home, then the Resources/Issues/Topology core ("what's
|
|
36
|
+
// running / what's wrong / how's it wired"), then app + temporal views,
|
|
37
|
+
// delivery, and finally the periodic posture/spend pair. The rail's vertical
|
|
38
|
+
// room lets us surface the views the 8-slot pill bar dropped (Issues,
|
|
39
|
+
// Applications, Cost).
|
|
40
|
+
const NAV_ITEMS: NavItemDef[] = [
|
|
41
|
+
{ view: 'home', icon: Home, label: 'Home' },
|
|
42
|
+
{ view: 'resources', icon: List, label: 'Resources' },
|
|
43
|
+
{ view: 'issues', icon: AlertTriangle, label: 'Issues' },
|
|
44
|
+
{ view: 'topology', icon: Network, label: 'Topology' },
|
|
45
|
+
{ view: 'applications', icon: Boxes, label: 'Applications' },
|
|
46
|
+
{ view: 'timeline', icon: Clock, label: 'Timeline' },
|
|
47
|
+
{ view: 'traffic', icon: Activity, label: 'Traffic' },
|
|
48
|
+
{ view: 'helm', icon: Package, label: 'Helm' },
|
|
49
|
+
{ view: 'gitops', icon: GitBranch, label: 'GitOps' },
|
|
50
|
+
{ view: 'checks', icon: ShieldCheck, label: 'Checks' },
|
|
51
|
+
{ view: 'cost', icon: DollarSign, label: 'Cost' },
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
interface PrimaryNavRailProps {
|
|
55
|
+
// `string`, not ExtendedMainView: App.tsx's mainView is a superset
|
|
56
|
+
// (adds 'applications'/'workload'/'compare') that isn't in k8s-ui's
|
|
57
|
+
// ExtendedMainView. Active state only needs equality, so accept any
|
|
58
|
+
// view id and compare against our NAV_ITEMS views.
|
|
59
|
+
activeView: string
|
|
60
|
+
onNavigate: (view: NavRailView) => void
|
|
61
|
+
pinned: boolean
|
|
62
|
+
onTogglePinned: () => void
|
|
63
|
+
// Hidden on narrow windows where the rail is responsively forced slim — an
|
|
64
|
+
// expand control there would just re-breach the content floor.
|
|
65
|
+
showPinToggle?: boolean
|
|
66
|
+
// Rail-bottom "me & my tools" cluster. onOpenSettings opens the Settings
|
|
67
|
+
// dialog; accountSlot is the account control (App passes <UserMenu variant=…>,
|
|
68
|
+
// which self-nulls without auth so the row vanishes in no-auth OSS).
|
|
69
|
+
onOpenSettings?: () => void
|
|
70
|
+
accountSlot?: ReactNode
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function PrimaryNavRail({ activeView, onNavigate, pinned, onTogglePinned, showPinToggle = true, onOpenSettings, accountSlot }: PrimaryNavRailProps) {
|
|
74
|
+
return (
|
|
75
|
+
<aside
|
|
76
|
+
aria-label="Primary navigation"
|
|
77
|
+
className={clsx(
|
|
78
|
+
// Dedicated sidebar token = the DEEPEST layer of the elevation scale, so
|
|
79
|
+
// the rail reads as chrome on EVERY view — not just ones where a `surface`
|
|
80
|
+
// facet pane happens to sit next to it (the content floor is `base`, which
|
|
81
|
+
// a `base` rail blends into). Neutral by design: the brand accent lives on
|
|
82
|
+
// the active item, not the nav background.
|
|
83
|
+
'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)]',
|
|
84
|
+
// w-44 (176px): OSS labels are short (longest is "Applications"), so the
|
|
85
|
+
// rail is trimmer than Radar Cloud's w-60 (which carries long cluster
|
|
86
|
+
// names). Keep in sync with the minWidth content-floor calc in App.tsx.
|
|
87
|
+
pinned ? 'w-44' : 'w-14',
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<BrandRow pinned={pinned} onNavigate={onNavigate} />
|
|
91
|
+
|
|
92
|
+
{/* Pinned: the nav owns the flex space and scrolls internally on short
|
|
93
|
+
windows, so the rail-bottom account/settings/pin row stays reachable.
|
|
94
|
+
Slim: keep the nav at natural height with a spacer below — an
|
|
95
|
+
overflow-y container would clip the `left-full` fly-out labels
|
|
96
|
+
(overflow-y:auto forces overflow-x to clip), and those ARE the labels
|
|
97
|
+
in slim mode. The slim short-window case is doubly-rare (slim is
|
|
98
|
+
width-triggered) and yields to keeping fly-outs intact. */}
|
|
99
|
+
<nav
|
|
100
|
+
className={clsx(
|
|
101
|
+
'flex flex-col gap-0.5 pt-3 px-2',
|
|
102
|
+
pinned && 'flex-1 min-h-0 overflow-y-auto',
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{NAV_ITEMS.map((item) => (
|
|
106
|
+
<NavRailItem
|
|
107
|
+
key={item.view}
|
|
108
|
+
item={item}
|
|
109
|
+
active={activeView === item.view}
|
|
110
|
+
pinned={pinned}
|
|
111
|
+
onNavigate={onNavigate}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</nav>
|
|
115
|
+
|
|
116
|
+
{!pinned && <div className="flex-1" />}
|
|
117
|
+
|
|
118
|
+
{/* "Me & my tools" — account + settings, the conventional rail-bottom
|
|
119
|
+
cluster (VS Code / Discord / ArgoCD / Radar Cloud's own UserOrgMenu).
|
|
120
|
+
accountSlot self-nulls without auth, so no-auth OSS shows only Settings. */}
|
|
121
|
+
<nav className="flex flex-col gap-0.5 px-2 pt-1 border-t border-theme-border/50">
|
|
122
|
+
{accountSlot}
|
|
123
|
+
{onOpenSettings && (
|
|
124
|
+
<RailActionRow icon={Settings} label="Settings" pinned={pinned} onClick={onOpenSettings} />
|
|
125
|
+
)}
|
|
126
|
+
</nav>
|
|
127
|
+
|
|
128
|
+
{/* Pin / unpin toggle — anchored at the bottom (VS Code / Linear pattern).
|
|
129
|
+
The icon points the direction the rail will move: close-panel when
|
|
130
|
+
expanded, open-panel when slim. Hidden when the rail is responsively
|
|
131
|
+
forced slim (showPinToggle=false) — expanding there isn't available. */}
|
|
132
|
+
{showPinToggle && (
|
|
133
|
+
<div className="px-2 pb-2 pt-1 border-t border-theme-border/50">
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onClick={onTogglePinned}
|
|
137
|
+
aria-label={pinned ? 'Collapse navigation' : 'Expand navigation'}
|
|
138
|
+
title={pinned ? 'Collapse navigation' : 'Expand navigation'}
|
|
139
|
+
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"
|
|
140
|
+
>
|
|
141
|
+
<span className="flex w-10 shrink-0 items-center justify-center">
|
|
142
|
+
{pinned ? <PanelLeftClose className="w-[18px] h-[18px]" /> : <PanelLeftOpen className="w-[18px] h-[18px]" />}
|
|
143
|
+
</span>
|
|
144
|
+
{/* `hidden` (not sr-only) when collapsed: the button's aria-label is
|
|
145
|
+
the accessible name; leaving an "Collapse" label in the a11y tree
|
|
146
|
+
would contradict the "Expand navigation" label. */}
|
|
147
|
+
<span className={clsx('text-[13px] font-medium', !pinned && 'hidden')}>Collapse</span>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</aside>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function BrandRow({ pinned, onNavigate }: { pinned: boolean; onNavigate: (view: NavRailView) => void }) {
|
|
156
|
+
// Clickable brand = secondary home affordance (logo→home convention). The
|
|
157
|
+
// Home nav item below still carries the active state; the brand just navigates.
|
|
158
|
+
return (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={() => onNavigate('home')}
|
|
162
|
+
aria-label="Radar — go to home"
|
|
163
|
+
title="Home"
|
|
164
|
+
// Height matches the top bar header (App.tsx — items-center + py-2 = 51px)
|
|
165
|
+
// so the rail's brand divider and the header's bottom border form one line.
|
|
166
|
+
className="flex h-[51px] w-full items-center border-b border-theme-border/50 shrink-0 transition-opacity hover:opacity-80"
|
|
167
|
+
>
|
|
168
|
+
<span className="flex w-14 shrink-0 items-center justify-center">
|
|
169
|
+
<span className="relative w-7 h-7 rounded-lg overflow-hidden bg-emerald-500/10 border border-emerald-500/20">
|
|
170
|
+
<img
|
|
171
|
+
src="/images/radar/radar-icon.svg"
|
|
172
|
+
alt=""
|
|
173
|
+
aria-hidden
|
|
174
|
+
className="w-full h-full p-0.5"
|
|
175
|
+
onError={(e) => console.error('Radar logo asset failed to load:', (e.currentTarget as HTMLImageElement).src)}
|
|
176
|
+
/>
|
|
177
|
+
</span>
|
|
178
|
+
</span>
|
|
179
|
+
<span className={clsx('flex flex-col leading-none text-left', !pinned && 'opacity-0 pointer-events-none')}>
|
|
180
|
+
<span className="font-semibold text-[15px] tracking-tight text-theme-text-primary">Radar</span>
|
|
181
|
+
<span className="text-[9px] mt-0.5 tracking-wide uppercase text-theme-text-tertiary">by Skyhook</span>
|
|
182
|
+
</span>
|
|
183
|
+
</button>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function NavRailItem({
|
|
188
|
+
item,
|
|
189
|
+
active,
|
|
190
|
+
pinned,
|
|
191
|
+
onNavigate,
|
|
192
|
+
}: {
|
|
193
|
+
item: NavItemDef
|
|
194
|
+
active: boolean
|
|
195
|
+
pinned: boolean
|
|
196
|
+
onNavigate: (view: NavRailView) => void
|
|
197
|
+
}) {
|
|
198
|
+
const { icon: Icon, label, view } = item
|
|
199
|
+
return (
|
|
200
|
+
<div className={clsx('group/item relative', !pinned && 'w-10')}>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => onNavigate(view)}
|
|
204
|
+
aria-current={active ? 'page' : undefined}
|
|
205
|
+
className={clsx(
|
|
206
|
+
'relative flex h-9 w-full items-center rounded-md text-sm font-medium transition-colors',
|
|
207
|
+
// Slim mode: clip the hit area to the icon column so the (opacity-0)
|
|
208
|
+
// label can't capture clicks meant for content to the right.
|
|
209
|
+
!pinned && 'max-w-10 overflow-hidden',
|
|
210
|
+
active
|
|
211
|
+
? 'bg-skyhook-600/10 dark:bg-skyhook-500/15 text-skyhook-700 dark:text-skyhook-300'
|
|
212
|
+
: 'text-theme-text-secondary hover:bg-theme-hover hover:text-theme-text-primary',
|
|
213
|
+
)}
|
|
214
|
+
>
|
|
215
|
+
{/* Left-edge accent bar on active — reads even when the row tint is soft. */}
|
|
216
|
+
<span
|
|
217
|
+
aria-hidden
|
|
218
|
+
className={clsx(
|
|
219
|
+
'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',
|
|
220
|
+
active ? 'opacity-100' : 'opacity-0',
|
|
221
|
+
)}
|
|
222
|
+
/>
|
|
223
|
+
<span className="flex w-10 shrink-0 items-center justify-center">
|
|
224
|
+
<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')} />
|
|
225
|
+
</span>
|
|
226
|
+
<span className={clsx('pr-3 truncate', !pinned && 'opacity-0')}>{label}</span>
|
|
227
|
+
</button>
|
|
228
|
+
|
|
229
|
+
{/* Slim-mode fly-out label — sibling of the button so it escapes the
|
|
230
|
+
button's overflow clip; pointer-events-none so it never eats clicks. */}
|
|
231
|
+
{!pinned && (
|
|
232
|
+
<span
|
|
233
|
+
aria-hidden
|
|
234
|
+
className="pointer-events-none absolute left-full top-1/2 z-50 ml-1 hidden -translate-y-1/2 whitespace-nowrap rounded-md border border-theme-border bg-theme-hover px-2.5 py-1 text-[13px] font-medium text-theme-text-primary opacity-0 shadow-lg shadow-black/30 transition-opacity duration-75 group-hover/item:block group-hover/item:opacity-100"
|
|
235
|
+
>
|
|
236
|
+
{label}
|
|
237
|
+
</span>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// A rail-bottom action row — same icon-column + fly-out treatment as NavRailItem
|
|
244
|
+
// but it's an action (onClick), not navigation, so no active-state/accent bar.
|
|
245
|
+
// Exported so the account control (UserMenu rail variant) can match the look.
|
|
246
|
+
export function RailActionRow({
|
|
247
|
+
icon: Icon,
|
|
248
|
+
label,
|
|
249
|
+
pinned,
|
|
250
|
+
onClick,
|
|
251
|
+
}: {
|
|
252
|
+
icon: ComponentType<{ className?: string }>
|
|
253
|
+
label: string
|
|
254
|
+
pinned: boolean
|
|
255
|
+
onClick: () => void
|
|
256
|
+
}) {
|
|
257
|
+
return (
|
|
258
|
+
<div className={clsx('group/item relative', !pinned && 'w-10')}>
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={onClick}
|
|
262
|
+
className={clsx(
|
|
263
|
+
'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',
|
|
264
|
+
!pinned && 'max-w-10 overflow-hidden',
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
<span className="flex w-10 shrink-0 items-center justify-center">
|
|
268
|
+
<Icon className="w-[18px] h-[18px] text-theme-text-tertiary group-hover/item:text-theme-text-secondary" />
|
|
269
|
+
</span>
|
|
270
|
+
<span className={clsx('pr-3 truncate', !pinned && 'opacity-0')}>{label}</span>
|
|
271
|
+
</button>
|
|
272
|
+
{!pinned && (
|
|
273
|
+
<span
|
|
274
|
+
aria-hidden
|
|
275
|
+
className="pointer-events-none absolute left-full top-1/2 z-50 ml-1 hidden -translate-y-1/2 whitespace-nowrap rounded-md border border-theme-border bg-theme-hover px-2.5 py-1 text-[13px] font-medium text-theme-text-primary opacity-0 shadow-lg shadow-black/30 transition-opacity duration-75 group-hover/item:block group-hover/item:opacity-100"
|
|
276
|
+
>
|
|
277
|
+
{label}
|
|
278
|
+
</span>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
@@ -86,8 +86,13 @@ interface FlatPoint { timestamp: number; value: number }
|
|
|
86
86
|
|
|
87
87
|
function extractFirstSeries(series: PrometheusSeries[]): FlatPoint[] | null {
|
|
88
88
|
for (const s of series) {
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// Drop gap points (null/undefined value) so the line never plots NaN or a
|
|
90
|
+
// false 0. Replica-count series realistically never gap, so the survivors
|
|
91
|
+
// are joined into one continuous line (this bridges a gap, if one occurred —
|
|
92
|
+
// unlike AreaChart, which breaks the path)
|
|
93
|
+
const finite = s.dataPoints.filter((dp): dp is FlatPoint => dp.value != null)
|
|
94
|
+
if (finite.length > 0) {
|
|
95
|
+
return finite.map(dp => ({ timestamp: dp.timestamp, value: dp.value }))
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
return null
|
|
@@ -94,6 +94,14 @@ function collectRestartEvents(series: PrometheusSeries[] | undefined): RestartEv
|
|
|
94
94
|
// window — and use the increase as the marker's restart count.
|
|
95
95
|
let prev: number | null = null
|
|
96
96
|
for (const dp of s.dataPoints) {
|
|
97
|
+
// Skip gap samples and drop the previous reference, so the next finite
|
|
98
|
+
// sample isn't diffed across the gap — a delta across a gap is
|
|
99
|
+
// meaningless and could fabricate a restart count. We compare only
|
|
100
|
+
// consecutive finite samples.
|
|
101
|
+
if (dp.value == null) {
|
|
102
|
+
prev = null
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
97
105
|
if (prev !== null) {
|
|
98
106
|
// Only count positive deltas — restarts that entered the rolling 1h
|
|
99
107
|
// window during the chart range. The first sample's value covers
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { HPARenderer as BaseHPARenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/HPARenderer'
|
|
2
2
|
import { HPACharts } from '../../resource/HPACharts'
|
|
3
|
+
import type { HPADiagnosis } from '@skyhook-io/k8s-ui'
|
|
3
4
|
|
|
4
5
|
interface HPARendererProps {
|
|
5
6
|
data: any
|
|
6
7
|
onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
|
|
8
|
+
hpaDiagnosis?: HPADiagnosis
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function HPARenderer({ data, onNavigate }: HPARendererProps) {
|
|
11
|
+
export function HPARenderer({ data, onNavigate, hpaDiagnosis }: HPARendererProps) {
|
|
10
12
|
return (
|
|
11
13
|
<BaseHPARenderer
|
|
12
14
|
data={data}
|
|
13
15
|
onNavigate={onNavigate}
|
|
16
|
+
hpaDiagnosis={hpaDiagnosis}
|
|
14
17
|
extraSections={<HPACharts data={data} />}
|
|
15
18
|
/>
|
|
16
19
|
)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { WorkloadRenderer as BaseWorkloadRenderer } from '@skyhook-io/k8s-ui/components/resources/renderers/WorkloadRenderer'
|
|
2
2
|
import { useNavigate } from 'react-router-dom'
|
|
3
|
-
import { useScaleWorkload } from '../../../api/client'
|
|
3
|
+
import { useScaleWorkload, fetchJSON } from '../../../api/client'
|
|
4
4
|
import { useRBACSubject } from '../../../api/rbac'
|
|
5
|
-
import { useQueryClient } from '@tanstack/react-query'
|
|
6
|
-
import
|
|
5
|
+
import { useQueries, useQueryClient } from '@tanstack/react-query'
|
|
6
|
+
import { kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
7
|
+
import type { Relationships, ResourceRef, ResourceWithRelationships } from '../../../types'
|
|
8
|
+
import type { ScalerDiagnosis } from '@skyhook-io/k8s-ui/components/resources/renderers/WorkloadRenderer'
|
|
7
9
|
|
|
8
10
|
// Map plural lowercase kind to singular PascalCase for ownerReferences matching
|
|
9
11
|
function getOwnerKind(kind: string): string {
|
|
@@ -40,6 +42,34 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: Wor
|
|
|
40
42
|
const { data: rbacData, isLoading: rbacLoading, error: rbacError } = useRBACSubject(
|
|
41
43
|
'ServiceAccount', namespace, saName, !!namespace,
|
|
42
44
|
)
|
|
45
|
+
const hpaRefs = (scaleBlockedBy ?? []).filter(ref => {
|
|
46
|
+
const refKind = ref.kind.toLowerCase()
|
|
47
|
+
return refKind === 'horizontalpodautoscaler' || refKind === 'hpa'
|
|
48
|
+
})
|
|
49
|
+
const hpaQueries = useQueries({
|
|
50
|
+
queries: hpaRefs.map(ref => ({
|
|
51
|
+
queryKey: ['resource', kindToPlural(ref.kind), ref.namespace, ref.name, ref.group],
|
|
52
|
+
queryFn: () => {
|
|
53
|
+
const ns = ref.namespace || '_'
|
|
54
|
+
const params = new URLSearchParams()
|
|
55
|
+
if (ref.group) params.set('group', ref.group)
|
|
56
|
+
const query = params.toString()
|
|
57
|
+
return fetchJSON<ResourceWithRelationships<any>>(`/resources/${kindToPlural(ref.kind)}/${ns}/${ref.name}${query ? `?${query}` : ''}`)
|
|
58
|
+
},
|
|
59
|
+
enabled: Boolean(ref.kind && ref.name),
|
|
60
|
+
staleTime: 10000,
|
|
61
|
+
retry: false,
|
|
62
|
+
})),
|
|
63
|
+
})
|
|
64
|
+
const scalerDiagnostics: ScalerDiagnosis[] = hpaRefs.map((ref, index) => {
|
|
65
|
+
const query = hpaQueries[index]
|
|
66
|
+
return {
|
|
67
|
+
ref,
|
|
68
|
+
diagnosis: query.data?.hpaDiagnosis,
|
|
69
|
+
loading: query.isLoading,
|
|
70
|
+
error: query.isError ? (query.error instanceof Error ? query.error.message : 'Failed to fetch HPA') : undefined,
|
|
71
|
+
}
|
|
72
|
+
})
|
|
43
73
|
|
|
44
74
|
return (
|
|
45
75
|
<BaseWorkloadRenderer
|
|
@@ -51,6 +81,7 @@ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: Wor
|
|
|
51
81
|
rbacLoading={rbacLoading}
|
|
52
82
|
rbacError={rbacError as Error | null}
|
|
53
83
|
scaleBlockedBy={scaleBlockedBy}
|
|
84
|
+
scalerDiagnostics={scalerDiagnostics}
|
|
54
85
|
onScale={async (replicas) => {
|
|
55
86
|
await scaleMutation.mutateAsync({
|
|
56
87
|
kind,
|
|
@@ -5,7 +5,7 @@ import { clsx } from 'clsx'
|
|
|
5
5
|
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
6
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
7
7
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
|
|
8
|
-
import { useCloudRole } from '../../api/client'
|
|
8
|
+
import { useCloudRole, useVersionCheck } from '../../api/client'
|
|
9
9
|
import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
|
|
10
10
|
import type { DeploymentMode } from '../../types'
|
|
11
11
|
|
|
@@ -38,6 +38,7 @@ interface SettingsDialogProps {
|
|
|
38
38
|
export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsDialogProps) {
|
|
39
39
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
40
40
|
const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
|
|
41
|
+
const { data: versionInfo } = useVersionCheck()
|
|
41
42
|
// Radar configuration (kubeconfig, port, integrations…) is host-level and
|
|
42
43
|
// affects every user of this instance, so it's gated to owners. Personal
|
|
43
44
|
// sections (My permissions) stay visible to everyone. Non-Cloud callers
|
|
@@ -262,6 +263,22 @@ export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsD
|
|
|
262
263
|
</button>
|
|
263
264
|
</div>
|
|
264
265
|
)}
|
|
266
|
+
|
|
267
|
+
{/* About — muted version footer (canonical "Settings → About"). */}
|
|
268
|
+
<div className="flex items-center justify-between gap-2 px-4 py-2 border-t border-theme-border/60 text-[11px] text-theme-text-tertiary shrink-0">
|
|
269
|
+
<span>
|
|
270
|
+
Radar{versionInfo?.currentVersion ? ` v${versionInfo.currentVersion}` : ''}
|
|
271
|
+
<span className="text-theme-text-disabled"> · by Skyhook</span>
|
|
272
|
+
</span>
|
|
273
|
+
<a
|
|
274
|
+
href="https://github.com/skyhook-io/radar"
|
|
275
|
+
target="_blank"
|
|
276
|
+
rel="noopener noreferrer"
|
|
277
|
+
className="text-accent-text hover:underline"
|
|
278
|
+
>
|
|
279
|
+
GitHub
|
|
280
|
+
</a>
|
|
281
|
+
</div>
|
|
265
282
|
</div>
|
|
266
283
|
</div>,
|
|
267
284
|
document.body
|