@skyhook-io/radar-app 1.3.1 → 1.3.3

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/App.tsx +111 -58
  3. package/src/api/client.ts +29 -1
  4. package/src/components/ConnectionErrorView.tsx +2 -2
  5. package/src/components/gitops/GitOpsView.tsx +127 -27
  6. package/src/components/helm/ChartBrowser.tsx +7 -3
  7. package/src/components/helm/HelmReleaseDrawer.tsx +4 -6
  8. package/src/components/helm/InstallWizard.tsx +1 -1
  9. package/src/components/helm/RoleGatedPanel.tsx +2 -2
  10. package/src/components/home/ClusterHealthCard.tsx +1 -1
  11. package/src/components/home/GitOpsControllersCard.tsx +14 -12
  12. package/src/components/home/HomeView.tsx +84 -56
  13. package/src/components/home/MCPSetupDialog.tsx +20 -86
  14. package/src/components/home/mcpToolCatalog.ts +276 -0
  15. package/src/components/issues/IssuesPane.tsx +78 -0
  16. package/src/components/portforward/PortForwardButton.tsx +1 -1
  17. package/src/components/portforward/PortForwardManager.tsx +1 -1
  18. package/src/components/resource/PrometheusCharts.tsx +18 -159
  19. package/src/components/resources/ImageFilesystemModal.tsx +1 -2
  20. package/src/components/resources/renderers/RoleBindingRenderer.tsx +5 -3
  21. package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -2
  22. package/src/components/settings/MyPermissionsDialog.tsx +1 -1
  23. package/src/components/settings/SettingsDialog.tsx +22 -2
  24. package/src/components/timeline/TimelineSwimlanes.tsx +8 -1311
  25. package/src/components/ui/Markdown.tsx +1 -1
  26. package/src/components/ui/UpdateNotification.tsx +1 -1
  27. package/src/components/workload/WorkloadView.tsx +190 -7
@@ -41,21 +41,23 @@ export function GitOpsControllersCard({ data, onNavigate }: GitOpsControllersCar
41
41
  <button
42
42
  type="button"
43
43
  onClick={onNavigate}
44
- className="flex flex-col gap-3 rounded-xl bg-theme-surface p-4 text-left shadow-theme-sm transition-colors hover:bg-theme-hover"
44
+ className="group h-[260px] rounded-xl bg-theme-surface shadow-theme-sm hover:-translate-y-1 hover:shadow-theme-md transition-all duration-200 text-left animate-fade-in-up"
45
45
  >
46
- <div className="flex items-center justify-between">
47
- <div className="flex items-center gap-2">
48
- <GitBranch className="h-4 w-4 text-theme-text-tertiary" />
49
- <span className="text-xs font-semibold uppercase tracking-wider text-theme-text-secondary">
50
- GitOps Controllers
51
- </span>
46
+ <div className="flex flex-col h-full w-full">
47
+ <div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
48
+ <div className="flex items-center gap-2">
49
+ <GitBranch className={clsx('h-4 w-4', headerTone)} />
50
+ <span className={clsx('text-xs font-semibold uppercase tracking-wider', headerTone)}>
51
+ GitOps Controllers
52
+ </span>
53
+ </div>
54
+ <span className={clsx('text-[11px] font-medium', headerTone)}>{headerLabel}</span>
52
55
  </div>
53
- <span className={clsx('text-[11px] font-medium', headerTone)}>{headerLabel}</span>
54
- </div>
55
56
 
56
- <div className="flex flex-col gap-2">
57
- {argo.length > 0 && <ControllerSection label="Argo CD" controllers={argo} />}
58
- {flux.length > 0 && <ControllerSection label="Flux CD" controllers={flux} />}
57
+ <div className="flex-1 min-h-0 overflow-y-auto px-5 py-3 flex flex-col gap-3">
58
+ {argo.length > 0 && <ControllerSection label="Argo CD" controllers={argo} />}
59
+ {flux.length > 0 && <ControllerSection label="Flux CD" controllers={flux} />}
60
+ </div>
59
61
  </div>
60
62
  </button>
61
63
  )
@@ -1,4 +1,4 @@
1
- import { useMemo } from 'react'
1
+ import { useMemo, type ReactNode } from 'react'
2
2
  import { useDashboard, useDashboardCRDs, useDashboardHelm } from '../../api/client'
3
3
  import type { DashboardResponse } from '../../api/client'
4
4
  import type { ExtendedMainView, Topology, SelectedResource } from '../../types'
@@ -113,77 +113,88 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
113
113
  )}>
114
114
  {/* Left column: teaser cards */}
115
115
  <div className="flex flex-col gap-6 auto-rows-min">
116
- {/* Primary cards — 2-col grid */}
116
+ {/* Live bandTopology + Timeline always render, so a fixed 2-up never strands.
117
+ These are the richest visuals and the most-used live views, so they get the width. */}
117
118
  <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
118
119
  <TopologyPreview
119
120
  topology={scopedTopology}
120
121
  summary={data.topologySummary}
121
122
  onNavigate={() => onNavigateToView('topology')}
122
123
  />
123
- <HelmSummary
124
- data={helmData}
125
- onNavigate={() => onNavigateToView('helm')}
126
- />
127
124
  <ActivitySummary
128
125
  namespaces={namespaces}
129
126
  topology={scopedTopology}
130
127
  onNavigate={() => onNavigateToView('timeline')}
131
128
  />
132
- <TrafficSummary
133
- data={data.trafficSummary}
134
- onNavigate={() => onNavigateToView('traffic')}
135
- />
136
- <CostCard onNavigate={() => onNavigateToView('cost')} />
137
129
  </div>
138
130
 
139
- {/* Health & compliance cards 3-col when enough cards, 2-col fallback */}
140
- {(data.certificateHealth || data.networkPolicyCoverage || data.audit || data.gitopsControllers) && (() => {
141
- const healthCards = [
142
- data.certificateHealth && (
143
- <CertificateHealthCard
144
- key="certs"
145
- data={data.certificateHealth}
146
- onNavigate={() => onNavigateToResourceKind('secrets', undefined, { type: ['TLS'] })}
147
- />
148
- ),
149
- data.networkPolicyCoverage && (
150
- <NetworkPolicyCoverageCard
151
- key="netpol"
152
- data={data.networkPolicyCoverage}
153
- onNavigate={() => onNavigateToResourceKind('networkpolicies', 'networking.k8s.io')}
154
- />
155
- ),
156
- data.gitopsControllers && (
157
- <GitOpsControllersCard
158
- key="gitops-controllers"
159
- data={data.gitopsControllers}
160
- onNavigate={() => onNavigateToView('gitops')}
161
- />
162
- ),
163
- data.audit && (
164
- <AuditCard
165
- key="audit"
166
- data={data.audit}
167
- onNavigate={() => onNavigateToView('audit')}
168
- />
169
- ),
170
- ].filter(Boolean)
171
-
172
- return (
173
- <div className={clsx(
174
- 'grid gap-6',
175
- healthCards.length >= 3 ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1 sm:grid-cols-2'
176
- )}>
177
- {healthCards}
178
- </div>
179
- )
180
- })()}
131
+ {/* Explore bandflex-grow wrap so the row always fills. The conditional
132
+ Cost card self-hides via BandItem's empty:hidden when OpenCost is absent,
133
+ leaving Traffic + Helm to stretch rather than stranding an empty cell. */}
134
+ <div className="flex flex-wrap gap-6">
135
+ <BandItem>
136
+ <TrafficSummary
137
+ data={data.trafficSummary}
138
+ onNavigate={() => onNavigateToView('traffic')}
139
+ />
140
+ </BandItem>
141
+ <BandItem>
142
+ <HelmSummary
143
+ data={helmData}
144
+ onNavigate={() => onNavigateToView('helm')}
145
+ />
146
+ </BandItem>
147
+ <BandItem>
148
+ <CostCard onNavigate={() => onNavigateToView('cost')} />
149
+ </BandItem>
150
+ </div>
151
+
152
+ {/* Posture band — same flex-grow wrap so any subset of compliance cards
153
+ fills its row instead of stranding the last one (the old 3-col grid
154
+ left Cluster Audit alone with two empty cells beside it). */}
155
+ {(data.certificateHealth || data.networkPolicyCoverage || data.audit || data.gitopsControllers) && (
156
+ <div className="flex flex-wrap gap-6">
157
+ {data.certificateHealth && (
158
+ <BandItem>
159
+ <CertificateHealthCard
160
+ data={data.certificateHealth}
161
+ onNavigate={() => onNavigateToResourceKind('secrets', undefined, { type: ['TLS'] })}
162
+ />
163
+ </BandItem>
164
+ )}
165
+ {data.networkPolicyCoverage && (
166
+ <BandItem>
167
+ <NetworkPolicyCoverageCard
168
+ data={data.networkPolicyCoverage}
169
+ onNavigate={() => onNavigateToResourceKind('networkpolicies', 'networking.k8s.io')}
170
+ />
171
+ </BandItem>
172
+ )}
173
+ {data.gitopsControllers && (
174
+ <BandItem>
175
+ <GitOpsControllersCard
176
+ data={data.gitopsControllers}
177
+ onNavigate={() => onNavigateToView('gitops')}
178
+ />
179
+ </BandItem>
180
+ )}
181
+ {data.audit && (
182
+ <BandItem>
183
+ <AuditCard
184
+ data={data.audit}
185
+ onNavigate={() => onNavigateToView('audit')}
186
+ />
187
+ </BandItem>
188
+ )}
189
+ </div>
190
+ )}
181
191
  </div>
182
192
 
183
193
  {/* Right column: problems panel */}
184
194
  {hasProblems && (
185
195
  <ProblemsPanel
186
196
  problems={data.problems}
197
+ onNavigateToIssues={() => onNavigateToView('issues')}
187
198
  onResourceClick={onNavigateToResource}
188
199
  />
189
200
  )}
@@ -193,25 +204,42 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
193
204
  )
194
205
  }
195
206
 
207
+ // A self-tiling flex item: grows to share the row, clamps to a sensible min
208
+ // width, and removes itself (empty:hidden) when its card renders null — so a
209
+ // data-gated card (e.g. Cost without OpenCost) can't leave a phantom column.
210
+ function BandItem({ children }: { children: ReactNode }) {
211
+ return <div className="flex-1 min-w-[260px] empty:hidden [&>*]:w-full">{children}</div>
212
+ }
213
+
196
214
  // ============================================================================
197
215
  // Problems Panel (right sidebar, scrollable)
198
216
  // ============================================================================
199
217
 
200
218
  interface ProblemsPanelProps {
201
219
  problems: DashboardResponse['problems']
220
+ onNavigateToIssues: () => void
202
221
  onResourceClick: (resource: SelectedResource) => void
203
222
  }
204
223
 
205
224
 
206
- function ProblemsPanel({ problems, onResourceClick }: ProblemsPanelProps) {
225
+ function ProblemsPanel({ problems, onNavigateToIssues, onResourceClick }: ProblemsPanelProps) {
207
226
  return (
208
227
  <div className="rounded-xl bg-theme-surface shadow-theme-sm flex flex-col lg:max-h-[calc(100vh-280px)] lg:sticky lg:top-0">
209
228
  <div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50 shrink-0">
210
229
  <div className="flex items-center gap-2">
211
230
  <AlertTriangle className="w-4 h-4 text-red-500" />
212
- <span className="text-xs font-semibold uppercase tracking-wider text-red-500">Unhealthy Workloads</span>
231
+ <span className="text-xs font-semibold uppercase tracking-wider text-red-500">Active Issues</span>
232
+ </div>
233
+ <div className="flex items-center gap-2">
234
+ <button
235
+ type="button"
236
+ className="rounded-md px-2 py-1 text-xs font-medium text-accent-text transition-colors hover:bg-accent-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent)]/40"
237
+ onClick={onNavigateToIssues}
238
+ >
239
+ View all
240
+ </button>
241
+ <span className="badge status-unhealthy rounded-full">{problems.length}</span>
213
242
  </div>
214
- <span className="badge status-unhealthy rounded-full">{problems.length}</span>
215
243
  </div>
216
244
  <div className="overflow-y-auto flex-1 min-h-0">
217
245
  <div className="divide-y divide-theme-border">
@@ -1,6 +1,7 @@
1
1
  import { useRef, useEffect, useState, useCallback } from 'react'
2
2
  import { X, Copy, Check, Radio, Terminal, MessageSquare, Code2, ChevronRight, Pin } from 'lucide-react'
3
3
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../../api/config'
4
+ import { MCP_TOOL_CATALOG } from './mcpToolCatalog'
4
5
 
5
6
  interface MCPSetupDialogProps {
6
7
  open: boolean
@@ -208,8 +209,9 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
208
209
  verbose YAML output.
209
210
  </p>
210
211
  <p className="text-sm text-theme-text-secondary leading-relaxed">
211
- Read tools are strictly read-only. Write tools (restart, scale, sync) are
212
- non-destructive and annotated so your AI client can distinguish them.
212
+ Read tools are strictly read-only. Write tools (restart, scale, sync, apply,
213
+ node drain) are annotated as destructive so your AI client can flag them and
214
+ prompt before running.
213
215
  </p>
214
216
  </div>
215
217
 
@@ -219,7 +221,7 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
219
221
  <div className="relative">
220
222
  <div className="flex items-center gap-3 bg-theme-base rounded-md px-3 py-2.5">
221
223
  <span className="badge text-purple-400 bg-purple-500/10">HTTP</span>
222
- <code className="text-sm font-mono text-theme-text-primary">{mcpUrl}</code>
224
+ <code className="inline-code text-sm">{mcpUrl}</code>
223
225
  </div>
224
226
  <CopyButton text={mcpUrl} />
225
227
  </div>
@@ -296,95 +298,27 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
296
298
 
297
299
  {/* Available tools */}
298
300
  <div className="space-y-2">
299
- <h4 className="text-sm font-semibold text-theme-text-primary">Tools</h4>
301
+ <div className="flex items-baseline justify-between">
302
+ <h4 className="text-sm font-semibold text-theme-text-primary">Tools</h4>
303
+ <span className="text-[11px] text-theme-text-tertiary">{MCP_TOOL_CATALOG.length} tools</span>
304
+ </div>
300
305
  <div className="grid grid-cols-1 gap-1.5">
301
- {[
302
- { name: 'get_dashboard', desc: 'Get cluster health overview including resource counts, problems (failing pods, unhealthy deployments), recent warning events, and Helm release status. Start here to understand cluster state before drilling into specific resources.', params: [
303
- { name: 'namespace', required: false, desc: 'filter to a specific namespace' },
304
- ]},
305
- { name: 'list_resources', desc: 'List Kubernetes resources of a given kind with minified summaries. Supports all built-in kinds (pods, deployments, services, etc.) and CRDs. Use to discover what\'s running before inspecting individual resources.', params: [
306
- { name: 'kind', required: true, desc: 'resource kind, e.g. pods, deployments, services' },
307
- { name: 'namespace', required: false, desc: 'filter to a specific namespace' },
308
- ]},
309
- { name: 'get_resource', desc: 'Get a single Kubernetes resource: minified spec/status/metadata plus default-on resourceContext (managedBy, exposes, selectedBy, uses, runsOn, issue/audit rollups). Optionally include heavier sidecars (events, metrics, logs).', params: [
310
- { name: 'kind', required: true, desc: 'resource kind, e.g. pod, deployment, service' },
311
- { name: 'namespace', required: false, desc: 'omit for cluster-scoped kinds (Node, ClusterRole, IngressClass, etc.)' },
312
- { name: 'name', required: true, desc: 'resource name' },
313
- { name: 'group', required: false, desc: 'API group when the kind is ambiguous (e.g. serving.knative.dev for Knative Service vs core Service)' },
314
- { name: 'include', required: false, desc: 'events, metrics, logs' },
315
- { name: 'context', required: false, desc: 'resourceContext tier: basic (default) or none (bare minified)' },
316
- ]},
317
- { name: 'get_topology', desc: 'Get the topology graph showing relationships between Kubernetes resources. Returns nodes and edges representing Deployments, Services, Ingresses, Pods, etc. Use \'traffic\' view for network flow or \'resources\' view for ownership hierarchy. Use \'summary\' format for LLM-friendly text descriptions.', params: [
318
- { name: 'namespace', required: false, desc: 'filter to a specific namespace' },
319
- { name: 'view', required: false, desc: 'traffic or resources' },
320
- { name: 'format', required: false, desc: 'graph (default) or summary (text)' },
321
- ]},
322
- { name: 'get_events', desc: 'Get recent Kubernetes warning events, deduplicated and sorted by recency. Useful for diagnosing issues — shows event reason, message, and occurrence count. Filter by resource kind/name to scope to a specific resource.', params: [
323
- { name: 'namespace', required: false, desc: 'filter to a specific namespace' },
324
- { name: 'limit', required: false, desc: 'max events to return (default 20)' },
325
- { name: 'kind', required: false, desc: 'filter to events for this resource kind' },
326
- { name: 'name', required: false, desc: 'filter to events for this resource name' },
327
- ]},
328
- { name: 'get_pod_logs', desc: 'Get filtered log lines from a pod, prioritizing errors and warnings. Returns diagnostically relevant lines (errors, panics, stack traces) or falls back to the last 20 lines if no error patterns match.', params: [
329
- { name: 'namespace', required: true, desc: 'pod namespace' },
330
- { name: 'name', required: true, desc: 'pod name' },
331
- { name: 'container', required: false, desc: 'container name (defaults to first)' },
332
- { name: 'tail_lines', required: false, desc: 'lines from end (default 200)' },
333
- ]},
334
- { name: 'list_namespaces', desc: 'List all Kubernetes namespaces with their status. Use to discover available namespaces before filtering other queries.', params: [] },
335
- { name: 'get_changes', desc: 'Get recent resource changes (creates, updates, deletes) from the cluster timeline. Use to investigate what changed before an incident. Filter by namespace, resource kind, or specific resource name.', params: [
336
- { name: 'namespace', required: false, desc: 'filter to a specific namespace' },
337
- { name: 'kind', required: false, desc: 'filter to a resource kind (e.g. Deployment)' },
338
- { name: 'name', required: false, desc: 'filter to a specific resource name' },
339
- { name: 'since', required: false, desc: 'lookback duration, e.g. 1h, 30m (default 1h)' },
340
- { name: 'limit', required: false, desc: 'max changes to return (default 20, max 50)' },
341
- ]},
342
- { name: 'list_helm_releases', desc: 'List all Helm releases in the cluster with their status and health. Returns release name, namespace, chart, version, status, and resource health.', params: [
343
- { name: 'namespace', required: false, desc: 'filter to a specific namespace' },
344
- ]},
345
- { name: 'get_helm_release', desc: 'Get detailed information about a specific Helm release including owned resources and their status. Optionally include values, revision history, or manifest diff between revisions.', params: [
346
- { name: 'namespace', required: true, desc: 'release namespace' },
347
- { name: 'name', required: true, desc: 'release name' },
348
- { name: 'include', required: false, desc: 'values, history, diff' },
349
- { name: 'diff_revision_1', required: false, desc: 'first revision for diff' },
350
- { name: 'diff_revision_2', required: false, desc: 'second revision for diff (defaults to current)' },
351
- ]},
352
- { name: 'get_workload_logs', desc: 'Get aggregated, AI-filtered logs from all pods of a workload (Deployment, StatefulSet, or DaemonSet). Logs are collected from all matching pods, filtered for errors/warnings, and deduplicated.', params: [
353
- { name: 'kind', required: true, desc: 'deployment, statefulset, or daemonset' },
354
- { name: 'namespace', required: true, desc: 'workload namespace' },
355
- { name: 'name', required: true, desc: 'workload name' },
356
- { name: 'container', required: false, desc: 'specific container name' },
357
- { name: 'tail_lines', required: false, desc: 'lines per pod (default 100)' },
358
- ]},
359
- { name: 'manage_workload', desc: 'Perform operations on a workload. \'restart\' triggers a rolling restart, \'scale\' changes the replica count, \'rollback\' reverts to a previous revision.', params: [
360
- { name: 'action', required: true, desc: 'restart, scale, or rollback' },
361
- { name: 'kind', required: true, desc: 'deployment, statefulset, or daemonset' },
362
- { name: 'namespace', required: true, desc: 'workload namespace' },
363
- { name: 'name', required: true, desc: 'workload name' },
364
- { name: 'replicas', required: false, desc: 'target replica count (for scale)' },
365
- { name: 'revision', required: false, desc: 'target revision (for rollback)' },
366
- ]},
367
- { name: 'manage_cronjob', desc: 'Perform operations on a CronJob. \'trigger\' creates a manual Job run, \'suspend\' pauses the schedule, \'resume\' re-enables it.', params: [
368
- { name: 'action', required: true, desc: 'trigger, suspend, or resume' },
369
- { name: 'namespace', required: true, desc: 'cronjob namespace' },
370
- { name: 'name', required: true, desc: 'cronjob name' },
371
- ]},
372
- { name: 'manage_gitops', desc: 'Perform operations on GitOps resources. ArgoCD: sync, suspend, resume. FluxCD: reconcile, suspend, resume.', params: [
373
- { name: 'action', required: true, desc: 'sync/reconcile, suspend, or resume' },
374
- { name: 'tool', required: true, desc: 'argocd or fluxcd' },
375
- { name: 'namespace', required: true, desc: 'resource namespace' },
376
- { name: 'name', required: true, desc: 'resource name' },
377
- { name: 'kind', required: false, desc: 'FluxCD resource kind (e.g. kustomization, helmrelease)' },
378
- ]},
379
- ].map((tool) => (
306
+ {MCP_TOOL_CATALOG.map((tool) => (
380
307
  <div key={tool.name} className="card-inner space-y-1.5">
381
- <code className="text-[11px] font-mono text-purple-400">{tool.name}</code>
308
+ <div className="flex items-center gap-2">
309
+ <code className="inline-code text-[11px]">{tool.name}</code>
310
+ {tool.write && (
311
+ <span className="badge-sm bg-amber-500/10 text-amber-600 dark:text-amber-400" title="Write tool — annotated as destructive">
312
+ write
313
+ </span>
314
+ )}
315
+ </div>
382
316
  <p className="text-[11px] text-theme-text-tertiary leading-relaxed">{tool.desc}</p>
383
317
  {tool.params.length > 0 && (
384
318
  <div className="flex flex-wrap gap-1.5 pt-0.5">
385
319
  {tool.params.map((p) => (
386
- <span key={p.name} className="badge-sm font-mono bg-theme-elevated text-theme-text-secondary" title={p.desc}>
387
- <span className="text-theme-text-secondary">{p.name}</span>
320
+ <span key={p.arg} className="inline-code text-[11px]" title={p.desc}>
321
+ <span>{p.arg}</span>
388
322
  {p.required && <span className="text-red-400">*</span>}
389
323
  </span>
390
324
  ))}