@skyhook-io/radar-app 0.2.0 → 0.2.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 +6 -6
- package/src/App.tsx +1 -1
- package/src/assets/platform-icons/aws_eks.png +0 -0
- package/src/assets/platform-icons/azure-aks.svg +2 -0
- package/src/assets/platform-icons/google_kubernetes_engine.png +0 -0
- package/src/components/home/ClusterHealthCard.tsx +70 -20
- package/src/components/resources/ResourcesView.tsx +8 -9
- package/src/contexts/CapabilitiesContext.tsx +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyhook-io/radar-app",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@monaco-editor/react": "^4.7.0",
|
|
33
33
|
"diff": "^9.0.0",
|
|
34
34
|
"react-markdown": "^10.1.0",
|
|
35
|
-
"react-virtuoso": "^4.18.
|
|
35
|
+
"react-virtuoso": "^4.18.6",
|
|
36
36
|
"remark-gfm": "^4.0.1",
|
|
37
37
|
"shiki": "^4.0.1",
|
|
38
38
|
"yaml": "^2.8.3"
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@skyhook-io/k8s-ui": "*",
|
|
55
55
|
"@tailwindcss/typography": "^0.5.19",
|
|
56
56
|
"@tailwindcss/vite": "^4.2.4",
|
|
57
|
-
"@tanstack/react-query": "^5.
|
|
57
|
+
"@tanstack/react-query": "^5.100.6",
|
|
58
58
|
"@types/diff": "^8.0.0",
|
|
59
59
|
"@types/node": "^25.5.0",
|
|
60
60
|
"@types/react": "^19.2.14",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"@xyflow/react": "^12.10.2",
|
|
64
64
|
"clsx": "^2.1.1",
|
|
65
65
|
"elkjs": "^0.11.1",
|
|
66
|
-
"lucide-react": "^1.
|
|
67
|
-
"postcss": "^8.5.
|
|
66
|
+
"lucide-react": "^1.12.0",
|
|
67
|
+
"postcss": "^8.5.12",
|
|
68
68
|
"prettier": "^3.8.1",
|
|
69
69
|
"react": "^19.2.5",
|
|
70
70
|
"react-dom": "^19.2.5",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"tailwind-merge": "^3.5.0",
|
|
73
73
|
"tailwindcss": "^4.2.4",
|
|
74
74
|
"typescript": "^6.0.2",
|
|
75
|
-
"vite": "^8.0.
|
|
75
|
+
"vite": "^8.0.10"
|
|
76
76
|
},
|
|
77
77
|
"sideEffects": [
|
|
78
78
|
"*.css"
|
package/src/App.tsx
CHANGED
|
@@ -366,7 +366,7 @@ function AppInner() {
|
|
|
366
366
|
const switchContext = useSwitchContext()
|
|
367
367
|
|
|
368
368
|
// View switching keyboard shortcuts
|
|
369
|
-
const views: ExtendedMainView[] = ['home', 'topology', 'resources', 'timeline', 'helm', 'traffic']
|
|
369
|
+
const views: ExtendedMainView[] = ['home', 'topology', 'resources', 'timeline', 'helm', 'traffic', 'cost', 'audit']
|
|
370
370
|
useRegisterShortcuts([
|
|
371
371
|
...views.map((view, i) => ({
|
|
372
372
|
id: `view-${view}`,
|
|
Binary file
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none"><path fill="url(#azure-aks-color-16__paint0_linear_2372_185)" d="M5.511 2l-2.224.412v3.034l2.224.474 2.232-.894V2.762L5.511 2z"/><path fill="#341A6E" d="M3.287 2.412v3.034l2.247.474V2.031l-2.247.381zm.949 2.8l-.63-.124V2.754l.63-.1v2.558zm.98.18l-.724-.118V2.607l.724-.125v2.91z"/><path fill="url(#azure-aks-color-16__paint1_linear_2372_185)" d="M10.325 2.039l-2.224.412v3.033l2.224.475 2.225-.902V2.8l-2.225-.762z"/><path fill="#341A6E" d="M8.101 2.451v3.033l2.232.475V2.07l-2.232.381zm.941 2.8l-.63-.124V2.793l.63-.1V5.25zm.98.179L9.3 5.313V2.646l.723-.133V5.43z"/><path fill="url(#azure-aks-color-16__paint2_linear_2372_185)" d="M3.232 6.184l-2.224.413V9.63l2.224.474 2.232-.894V6.947l-2.232-.763z"/><path fill="#341A6E" d="M1 6.597v3.01l2.248.474V6.192L1 6.597zm.941 2.807l-.63-.132V6.94l.63-.109v2.574zm.988.203l-.723-.117V6.791l.723-.124v2.94z"/><path fill="url(#azure-aks-color-16__paint3_linear_2372_185)" d="M8.031 6.153l-2.224.413v3.033l2.224.482 2.224-.902V6.916l-2.224-.763z"/><path fill="#341A6E" d="M5.807 6.566v3.04l2.24.475V6.192l-2.24.374zm.94 2.807l-.63-.132V6.908l.63-.11v2.575zm.98.171l-.723-.116V6.76l.724-.124v2.908z"/><path fill="url(#azure-aks-color-16__paint4_linear_2372_185)" d="M12.83 6.192l-2.225.412v3.034l2.225.474 2.232-.894V6.954l-2.232-.762z"/><path fill="#341A6E" d="M10.605 6.604v3.003l2.248.474V6.192l-2.248.412zm.95 2.808l-.63-.132V6.947l.63-.11v2.575zm.98.171l-.724-.116V6.799l.723-.125v2.91z"/><path fill="url(#azure-aks-color-16__paint5_linear_2372_185)" d="M5.457 10.415l-2.225.405v3.033l2.225.482 2.232-.902v-2.255l-2.232-.763z"/><path fill="#341A6E" d="M3.232 10.82v3.033l2.248.483v-3.952l-2.248.436zm.95 2.808l-.63-.132v-2.334l.63-.109v2.575zm.98.179l-.724-.117v-2.675l.723-.125v2.917z"/><path fill="url(#azure-aks-color-16__paint6_linear_2372_185)" d="M10.263 10.447l-2.224.412v3.033l2.224.475 2.232-.895V11.21l-2.232-.762z"/><path fill="#341A6E" d="M8.039 10.859v3.033l2.248.475v-3.89l-2.248.382zm.949 2.808l-.63-.133v-2.333l.63-.109v2.575zm.98.17l-.724-.116v-2.668l.724-.124v2.909z"/><defs><linearGradient id="azure-aks-color-16__paint0_linear_2372_185" x1="3.287" x2="7.743" y1="3.96" y2="3.96" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient><linearGradient id="azure-aks-color-16__paint1_linear_2372_185" x1="8.101" x2="12.55" y1="3.999" y2="3.999" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient><linearGradient id="azure-aks-color-16__paint2_linear_2372_185" x1="1.008" x2="5.457" y1="8.144" y2="8.144" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient><linearGradient id="azure-aks-color-16__paint3_linear_2372_185" x1="5.807" x2="10.255" y1="8.113" y2="8.113" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient><linearGradient id="azure-aks-color-16__paint4_linear_2372_185" x1="10.605" x2="15.062" y1="8.152" y2="8.152" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient><linearGradient id="azure-aks-color-16__paint5_linear_2372_185" x1="3.232" x2="7.689" y1="12.376" y2="12.376" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient><linearGradient id="azure-aks-color-16__paint6_linear_2372_185" x1="8.039" x2="12.495" y1="12.407" y2="12.407" gradientUnits="userSpaceOnUse"><stop stop-color="#B77AF4"/><stop offset="1" stop-color="#773ADC"/></linearGradient></defs></svg>
|
|
Binary file
|
|
@@ -12,6 +12,9 @@ import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
|
|
|
12
12
|
import { MCPSetupDialog } from './MCPSetupDialog'
|
|
13
13
|
import { pluralize, parseContextName } from '@skyhook-io/k8s-ui'
|
|
14
14
|
import { Tooltip } from '../ui/Tooltip'
|
|
15
|
+
import gkeIcon from '../../assets/platform-icons/google_kubernetes_engine.png'
|
|
16
|
+
import eksIcon from '../../assets/platform-icons/aws_eks.png'
|
|
17
|
+
import aksIcon from '../../assets/platform-icons/azure-aks.svg'
|
|
15
18
|
|
|
16
19
|
interface ClusterHealthCardProps {
|
|
17
20
|
health: DashboardResponse['health']
|
|
@@ -67,13 +70,13 @@ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform
|
|
|
67
70
|
function getPlatformInfo(platform: string): { name: string; icon: string | null } {
|
|
68
71
|
const platformLower = platform.toLowerCase()
|
|
69
72
|
if (platformLower.includes('gke') || platformLower.includes('google')) {
|
|
70
|
-
return { name: 'Google Kubernetes Engine', icon:
|
|
73
|
+
return { name: 'Google Kubernetes Engine', icon: gkeIcon }
|
|
71
74
|
}
|
|
72
75
|
if (platformLower.includes('eks') || platformLower.includes('amazon') || platformLower.includes('aws')) {
|
|
73
|
-
return { name: 'Amazon EKS', icon:
|
|
76
|
+
return { name: 'Amazon EKS', icon: eksIcon }
|
|
74
77
|
}
|
|
75
78
|
if (platformLower.includes('aks') || platformLower.includes('azure')) {
|
|
76
|
-
return { name: 'Azure Kubernetes Service', icon:
|
|
79
|
+
return { name: 'Azure Kubernetes Service', icon: aksIcon }
|
|
77
80
|
}
|
|
78
81
|
if (platformLower.includes('openshift')) {
|
|
79
82
|
return { name: 'OpenShift', icon: null }
|
|
@@ -93,7 +96,14 @@ function getPlatformInfo(platform: string): { name: string; icon: string | null
|
|
|
93
96
|
if (platformLower.includes('docker')) {
|
|
94
97
|
return { name: 'Docker Desktop', icon: null }
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
// The Go backend returns "generic" for unrecognized platforms — that
|
|
100
|
+
// literal string is no better than the empty case, so fall back to
|
|
101
|
+
// the friendlier "Kubernetes" label for both. Only pass the platform
|
|
102
|
+
// string through when it's actually a recognizable name.
|
|
103
|
+
if (!platform || platformLower === 'generic') {
|
|
104
|
+
return { name: 'Kubernetes', icon: null }
|
|
105
|
+
}
|
|
106
|
+
return { name: platform, icon: null }
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
export function ClusterHealthCard({
|
|
@@ -113,8 +123,20 @@ export function ClusterHealthCard({
|
|
|
113
123
|
void _topCRDs // Reserved for future CRD display
|
|
114
124
|
|
|
115
125
|
const [mcpDialogOpen, setMcpDialogOpen] = useState(false)
|
|
116
|
-
const
|
|
126
|
+
const caps = useCapabilitiesContext()
|
|
127
|
+
// Default to local mode when the backend doesn't ship a `deployment`
|
|
128
|
+
// field (older Radar binaries pre-0.2.2). Local rendering is the safe
|
|
129
|
+
// OSS-shape default — wrong-direction defaults would briefly suppress
|
|
130
|
+
// chrome OSS users expect to see.
|
|
131
|
+
const deployment = caps.deployment ?? { mode: 'local' as const }
|
|
132
|
+
const mcpEnabled = caps.mcpEnabled
|
|
133
|
+
const isCloud = deployment.mode === 'cloud'
|
|
134
|
+
const isInCluster = deployment.mode === 'in-cluster' || deployment.mode === 'cloud'
|
|
117
135
|
const mcpUrl = `${window.location.origin}/mcp`
|
|
136
|
+
// In Cloud, MCP is org-wide and PAT-authed (api.radarhq.io/mcp). The OSS
|
|
137
|
+
// "this binary is your local MCP server" framing is wrong there — Cloud
|
|
138
|
+
// surfaces MCP from the hub Home dashboard instead.
|
|
139
|
+
const showLocalMcpCard = mcpEnabled && !isCloud
|
|
118
140
|
|
|
119
141
|
const restricted = counts.restricted ?? []
|
|
120
142
|
const isRestricted = (kind: string) => restricted.includes(kind)
|
|
@@ -158,13 +180,23 @@ export function ClusterHealthCard({
|
|
|
158
180
|
{ kind: 'cronjobs', label: 'CronJobs', icon: Clock, total: counts.cronJobs.total, subtitle: `${counts.cronJobs.active} active` },
|
|
159
181
|
]
|
|
160
182
|
const platformInfo = getPlatformInfo(cluster.platform)
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
183
|
+
// Headline-name derivation has three branches, in priority order:
|
|
184
|
+
// 1. Local-kubeconfig users get the parsed short clusterName from a
|
|
185
|
+
// string like `gke_koalabackend_us-east1-b_nonprod-cluster-us-east1`
|
|
186
|
+
// (the meaningful tail). Account/region are surfaced separately
|
|
187
|
+
// below as muted metadata, and the raw path is exposed via tooltip
|
|
188
|
+
// on the headline element.
|
|
189
|
+
// 2. In-cluster mode (deployment.mode === 'in-cluster' OR 'cloud')
|
|
190
|
+
// has no meaningful kubeconfig context — bootstrap sets it to
|
|
191
|
+
// the literal "in-cluster" sentinel. Fall back to the platform
|
|
192
|
+
// label ("Google Kubernetes Engine") which IS recognizable.
|
|
193
|
+
// 3. Last resort: the literal cluster.name, or "Cluster".
|
|
194
|
+
// When the card is rendered embedded (cloud mode), the H2 itself is
|
|
195
|
+
// suppressed below — the hub shell already shows the cluster name in
|
|
196
|
+
// its top bar.
|
|
166
197
|
const parsedContext = parseContextName(cluster.name || '')
|
|
167
|
-
const
|
|
198
|
+
const rawHeadline = parsedContext.clusterName || cluster.name || 'Cluster'
|
|
199
|
+
const headlineName = isInCluster ? platformInfo.name : rawHeadline
|
|
168
200
|
|
|
169
201
|
return (
|
|
170
202
|
<div className="rounded-xl bg-theme-surface shadow-theme-sm overflow-hidden">
|
|
@@ -181,12 +213,23 @@ export function ClusterHealthCard({
|
|
|
181
213
|
)}
|
|
182
214
|
<span className="text-xs text-theme-text-secondary truncate">{platformInfo.name}</span>
|
|
183
215
|
</div>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
{/* In Cloud, the hub shell already shows the cluster name in
|
|
217
|
+
its top bar; rendering it again here is redundant and
|
|
218
|
+
makes the card feel like a label rather than content. */}
|
|
219
|
+
{!isCloud && (
|
|
220
|
+
<h2
|
|
221
|
+
className="text-xl font-semibold text-theme-text-primary truncate mb-1.5 leading-tight"
|
|
222
|
+
// In-cluster mode's cluster.name is the literal "in-cluster"
|
|
223
|
+
// sentinel, which would leak via the browser hover tooltip
|
|
224
|
+
// even though the visible text falls back to the platform
|
|
225
|
+
// label. Drop the title attribute entirely in that case;
|
|
226
|
+
// local mode keeps it so users can hover to see the full
|
|
227
|
+
// kubeconfig context path.
|
|
228
|
+
title={isInCluster ? undefined : cluster.name}
|
|
229
|
+
>
|
|
230
|
+
{headlineName}
|
|
231
|
+
</h2>
|
|
232
|
+
)}
|
|
190
233
|
<div className="flex flex-col gap-0.5 text-xs text-theme-text-tertiary">
|
|
191
234
|
{(parsedContext.account || parsedContext.region) && (
|
|
192
235
|
<span className="truncate font-mono" title={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}>
|
|
@@ -197,7 +240,11 @@ export function ClusterHealthCard({
|
|
|
197
240
|
<span>Kubernetes {cluster.version}</span>
|
|
198
241
|
)}
|
|
199
242
|
<span><span className="font-mono">{counts.namespaces}</span> namespaces</span>
|
|
200
|
-
{
|
|
243
|
+
{/* Show raw kubeconfig context as muted metadata only when
|
|
244
|
+
it differs from the headline AND we're in local mode
|
|
245
|
+
(in-cluster has no meaningful context name, cloud
|
|
246
|
+
shell already renders the canonical name). */}
|
|
247
|
+
{cluster.name && cluster.name !== headlineName && deployment.mode === 'local' && (
|
|
201
248
|
<span
|
|
202
249
|
className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5"
|
|
203
250
|
title={cluster.name}
|
|
@@ -229,8 +276,11 @@ export function ClusterHealthCard({
|
|
|
229
276
|
</span>
|
|
230
277
|
</Tooltip>
|
|
231
278
|
)}
|
|
232
|
-
{/* MCP Server indicator
|
|
233
|
-
|
|
279
|
+
{/* MCP Server indicator. OSS-only: in Cloud, MCP discovery
|
|
280
|
+
lives at the hub level (org-wide endpoint, PAT-authed)
|
|
281
|
+
rather than per-cluster, so this localhost/no-auth card
|
|
282
|
+
would mislead a Cloud user. */}
|
|
283
|
+
{showLocalMcpCard && (
|
|
234
284
|
<button
|
|
235
285
|
onClick={() => setMcpDialogOpen(true)}
|
|
236
286
|
className="flex items-center gap-2 mt-3 px-2.5 py-2 bg-purple-500/5 hover:bg-purple-500/10 border border-purple-500/20 rounded-md transition-colors w-full"
|
|
@@ -181,15 +181,14 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
181
181
|
pinned={pinned}
|
|
182
182
|
togglePin={togglePin}
|
|
183
183
|
isPinned={(kind: string, group?: string) => isPinned(kind, group ?? '')}
|
|
184
|
-
// Navigation. basePath is
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
basePath={getBasename() + '/resources'}
|
|
184
|
+
// Navigation. basePath is basename-relative. React Router's useLocation
|
|
185
|
+
// strips the basename from `location.pathname`, so reading the current
|
|
186
|
+
// kind compares basename-relative paths on both sides. URL writes go
|
|
187
|
+
// through `handleNavigate`, which strips any leading basename before
|
|
188
|
+
// handing off to react-router (which re-applies it). Embedding hosts
|
|
189
|
+
// (e.g. Radar Cloud at /c/{cluster}/resources) work without ResourcesView
|
|
190
|
+
// needing to know the basename.
|
|
191
|
+
basePath="/resources"
|
|
193
192
|
locationSearch={location.search}
|
|
194
193
|
locationPathname={location.pathname}
|
|
195
194
|
onNavigate={handleNavigate}
|
|
@@ -13,6 +13,11 @@ const defaultCapabilities: Capabilities = {
|
|
|
13
13
|
helmWrite: true,
|
|
14
14
|
nodeWrite: true,
|
|
15
15
|
mcpEnabled: true,
|
|
16
|
+
// Default to 'local' for the loading window so the UI renders the
|
|
17
|
+
// OSS standalone shape until /api/capabilities resolves. Both
|
|
18
|
+
// alternatives ('in-cluster', 'cloud') would cause OSS users to
|
|
19
|
+
// briefly see suppressed chrome — wrong default direction.
|
|
20
|
+
deployment: { mode: 'local' },
|
|
16
21
|
}
|
|
17
22
|
|
|
18
23
|
// Restricted capabilities for error/failure cases (fail-closed)
|
|
@@ -26,6 +31,7 @@ const restrictedCapabilities: Capabilities = {
|
|
|
26
31
|
helmWrite: false,
|
|
27
32
|
nodeWrite: false,
|
|
28
33
|
mcpEnabled: false,
|
|
34
|
+
deployment: { mode: 'local' },
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
const CapabilitiesContext = createContext<Capabilities>(defaultCapabilities)
|