@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "0.2.0",
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.5",
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.99.0",
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.8.0",
67
- "postcss": "^8.5.8",
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.9"
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}`,
@@ -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>
@@ -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: '/icons/google_kubernetes_engine.png' }
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: '/icons/aws_eks.png' }
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: '/icons/azure-aks.svg' }
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
- return { name: platform || 'Kubernetes', icon: null }
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 { mcpEnabled } = useCapabilitiesContext()
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
- // The raw cluster.name is the kubeconfig context (e.g.
162
- // `gke_koalabackend_us-east1-b_nonprod-cluster-us-east1`). That string
163
- // is the user's primary orientation cue, but the bit they actually
164
- // recognize is the short clusterName. We promote that, push the raw
165
- // path into a tooltip, and surface project/region as muted metadata.
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 headlineName = parsedContext.clusterName || cluster.name || 'Cluster'
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
- <h2
185
- className="text-xl font-semibold text-theme-text-primary truncate mb-1.5 leading-tight"
186
- title={cluster.name}
187
- >
188
- {headlineName}
189
- </h2>
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
- {cluster.name && cluster.name !== headlineName && (
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
- {mcpEnabled && (
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 the full URL prefix where the Resources view
185
- // lives '/resources' for standalone Radar, '/c/{cluster}/resources'
186
- // when embedded in a host app that mounts RadarApp under a basename.
187
- // k8s-ui's ResourcesView uses this to read the current kind out of
188
- // window.location.pathname and to write URL updates (drawer open/close,
189
- // filter changes) back via history.replaceState. Without the basename
190
- // prefix, those writes would drop the host-app route context and the
191
- // URL would no longer be reloadable.
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)