@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
@@ -0,0 +1,276 @@
1
+ // Human-facing catalog for the MCP setup dialog (MCPSetupDialog.tsx).
2
+ //
3
+ // Source of truth for the *set* of tools is the backend registration in
4
+ // internal/mcp/tools.go. This list must stay in sync with it — the Go test
5
+ // TestSetupDialogCoversAllTools (internal/mcp/tools_catalog_test.go) parses
6
+ // this file and fails CI if the tool names here don't exactly match the
7
+ // registered tools. When you add or remove an MCP tool there, update this
8
+ // catalog too.
9
+ //
10
+ // Descriptions here are intentionally shorter and more human-facing than the
11
+ // LLM-oriented routing descriptions in tools.go — different audience, so they
12
+ // are not shared verbatim.
13
+
14
+ export interface MCPToolParam {
15
+ arg: string
16
+ required?: boolean
17
+ desc: string
18
+ }
19
+
20
+ export interface MCPToolInfo {
21
+ name: string
22
+ write?: boolean
23
+ desc: string
24
+ params: MCPToolParam[]
25
+ }
26
+
27
+ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
28
+ {
29
+ name: 'get_dashboard',
30
+ desc: 'Cluster or namespace health overview: resource counts, failing pods, unhealthy workloads, recent warning events, and Helm status. Start here before drilling into specific resources.',
31
+ params: [{ arg: 'namespace', desc: 'filter to a specific namespace' }],
32
+ },
33
+ {
34
+ name: 'top_resources',
35
+ desc: 'Live CPU/memory ranking (like `kubectl top`) joined with Kubernetes context — pod status, restarts, owner workload, requests, and limits. Ranks pods, workloads, or nodes.',
36
+ params: [
37
+ { arg: 'kind', desc: 'pods (default), workloads, or nodes' },
38
+ { arg: 'namespace', desc: 'filter pods/workloads to a namespace' },
39
+ { arg: 'sort', desc: 'cpu (default) or memory' },
40
+ { arg: 'limit', desc: 'max rows (default 20, max 100)' },
41
+ ],
42
+ },
43
+ {
44
+ name: 'list_resources',
45
+ desc: 'List Kubernetes resources of a given kind with compact summaries plus per-row health, managedBy, and issue counts. Supports built-in kinds and CRDs.',
46
+ params: [
47
+ { arg: 'kind', required: true, desc: 'resource kind, e.g. pods, deployments, services' },
48
+ { arg: 'group', desc: 'API group when the kind is ambiguous (e.g. serving.knative.dev)' },
49
+ { arg: 'namespace', desc: 'filter to a specific namespace' },
50
+ { arg: 'context', desc: 'per-row context: default (summaryContext) or none' },
51
+ ],
52
+ },
53
+ {
54
+ name: 'get_resource',
55
+ desc: 'A single resource: minified spec/status/metadata plus resourceContext (relationships, refs, issue/audit/policy rollups). Optionally include heavier event/metrics sidecars.',
56
+ params: [
57
+ { arg: 'kind', required: true, desc: 'resource kind, e.g. pod, deployment, service' },
58
+ { arg: 'name', required: true, desc: 'resource name' },
59
+ { arg: 'namespace', desc: 'omit for cluster-scoped kinds (Node, ClusterRole, IngressClass, etc.)' },
60
+ { arg: 'group', desc: 'API group when the kind is ambiguous (e.g. serving.knative.dev for Knative Service vs core Service)' },
61
+ { arg: 'include', desc: 'events, metrics' },
62
+ { arg: 'context', desc: 'resourceContext tier: basic (default) or none' },
63
+ ],
64
+ },
65
+ {
66
+ name: 'get_topology',
67
+ desc: 'Topology graph of relationships between resources — Services, workloads, Pods, Ingresses, owners. Use traffic view for network flow or resources view for ownership hierarchy.',
68
+ params: [
69
+ { arg: 'namespace', desc: 'filter to a specific namespace' },
70
+ { arg: 'view', desc: 'traffic or resources' },
71
+ { arg: 'format', desc: 'graph (default) or summary (text)' },
72
+ ],
73
+ },
74
+ {
75
+ name: 'get_neighborhood',
76
+ desc: 'BFS-expanded topology around one resource — its upstream/downstream Services, workloads, Pods, refs, and owners. Cheaper and more focused than full topology once you have a suspect.',
77
+ params: [
78
+ { arg: 'kind', required: true, desc: 'resource kind, e.g. pod, deployment, service, application' },
79
+ { arg: 'name', required: true, desc: 'resource name' },
80
+ { arg: 'namespace', desc: 'resource namespace; omit for cluster-scoped kinds' },
81
+ { arg: 'group', desc: 'API group to disambiguate colliding kinds' },
82
+ { arg: 'profile', desc: 'auto (default) or all (every edge type, heavier)' },
83
+ { arg: 'hops', desc: 'BFS depth (default 1, max 2)' },
84
+ { arg: 'max_nodes', desc: 'node budget (default 25)' },
85
+ ],
86
+ },
87
+ {
88
+ name: 'get_events',
89
+ desc: 'Recent Kubernetes warning events, deduplicated and sorted by recency. Shows reason, message, and occurrence count. Filter to a specific resource by kind/name.',
90
+ params: [
91
+ { arg: 'namespace', desc: 'filter to a specific namespace' },
92
+ { arg: 'limit', desc: 'max events (default 20, max 100)' },
93
+ { arg: 'kind', desc: 'filter to events for this resource kind' },
94
+ { arg: 'name', desc: 'filter to events for this resource name' },
95
+ ],
96
+ },
97
+ {
98
+ name: 'get_pod_logs',
99
+ desc: 'Filtered log lines from a pod, prioritizing errors and warnings, falling back to recent tail lines. Optional grep, since, and previous-container logs.',
100
+ params: [
101
+ { arg: 'namespace', required: true, desc: 'pod namespace' },
102
+ { arg: 'name', required: true, desc: 'pod name' },
103
+ { arg: 'container', desc: 'container name (defaults to first)' },
104
+ { arg: 'tail_lines', desc: 'lines from end (default 200)' },
105
+ { arg: 'grep', desc: 'regex to filter lines, like kubectl logs | grep' },
106
+ { arg: 'since', desc: 'only logs newer than this duration (e.g. 30s, 10m, 1h)' },
107
+ { arg: 'previous', desc: 'logs from the previous terminated container (CrashLoopBackOff)' },
108
+ ],
109
+ },
110
+ {
111
+ name: 'diagnose',
112
+ desc: 'One-call workload root-cause bundle: spec + resourceContext + current AND previous logs across all pods + warning events + startup-blocker analysis. For Pod/Deployment/StatefulSet/DaemonSet.',
113
+ params: [
114
+ { arg: 'kind', required: true, desc: 'pod, deployment, statefulset, or daemonset' },
115
+ { arg: 'namespace', required: true, desc: 'workload namespace' },
116
+ { arg: 'name', required: true, desc: 'resource name' },
117
+ { arg: 'container', desc: 'specific container (defaults to all)' },
118
+ { arg: 'tail_lines', desc: 'lines per pod/stream (default 100)' },
119
+ { arg: 'since', desc: 'only logs newer than this duration' },
120
+ ],
121
+ },
122
+ {
123
+ name: 'list_namespaces',
124
+ desc: 'List all Kubernetes namespaces with their status. Use to discover available namespaces before filtering other queries.',
125
+ params: [],
126
+ },
127
+ {
128
+ name: 'get_changes',
129
+ desc: 'Recent resource creates, updates, and deletes from the cluster timeline. Use to investigate what changed before an incident.',
130
+ params: [
131
+ { arg: 'namespace', desc: 'filter to a specific namespace' },
132
+ { arg: 'kind', desc: 'filter to a resource kind (e.g. Deployment)' },
133
+ { arg: 'name', desc: 'filter to a specific resource name' },
134
+ { arg: 'since', desc: 'lookback duration, e.g. 1h, 30m (default 1h)' },
135
+ { arg: 'limit', desc: 'max changes (default 20, max 50)' },
136
+ ],
137
+ },
138
+ {
139
+ name: 'get_cluster_audit',
140
+ desc: 'Best-practice and security posture findings — Security, Reliability, Efficiency — each with remediation guidance. Static config posture, independent of live operational health.',
141
+ params: [
142
+ { arg: 'namespace', desc: 'filter to a specific namespace' },
143
+ { arg: 'category', desc: 'Security, Reliability, or Efficiency' },
144
+ { arg: 'severity', desc: 'danger or warning' },
145
+ { arg: 'limit', desc: 'max findings (default 30, max 100)' },
146
+ ],
147
+ },
148
+ {
149
+ name: 'list_helm_releases',
150
+ desc: 'All Helm releases in the cluster with status and resource health — name, namespace, chart, version.',
151
+ params: [{ arg: 'namespace', desc: 'filter to a specific namespace' }],
152
+ },
153
+ {
154
+ name: 'get_helm_release',
155
+ desc: 'Detailed Helm release info with owned resources and their status. Optionally include values, revision history, or a manifest diff between revisions.',
156
+ params: [
157
+ { arg: 'namespace', required: true, desc: 'release namespace' },
158
+ { arg: 'name', required: true, desc: 'release name' },
159
+ { arg: 'include', desc: 'values, history, diff' },
160
+ { arg: 'diff_revision_1', desc: 'first revision for diff' },
161
+ { arg: 'diff_revision_2', desc: 'second revision for diff (defaults to current)' },
162
+ ],
163
+ },
164
+ {
165
+ name: 'list_packages',
166
+ desc: 'Unified "what\'s installed" view across Helm releases, workload labels, CRD registrations, and GitOps declarations (Argo + Flux), with sources, versions, and health.',
167
+ params: [
168
+ { arg: 'namespace', desc: 'filter to a specific namespace' },
169
+ { arg: 'source', desc: 'H (Helm), L (labels), C (CRDs), A (Argo), F (Flux)' },
170
+ { arg: 'chart', desc: 'case-insensitive chart-name substring' },
171
+ ],
172
+ },
173
+ {
174
+ name: 'issues',
175
+ desc: 'Ranked list of what is broken right now — failing workloads, dangling references, scheduling blockers, and false CRD conditions. Live operational state (distinct from get_cluster_audit posture).',
176
+ params: [
177
+ { arg: 'namespace', desc: 'filter to one namespace' },
178
+ { arg: 'severity', desc: 'comma-separated: critical, warning' },
179
+ { arg: 'kind', desc: 'comma-separated kind filter' },
180
+ { arg: 'limit', desc: 'max issues (default 200, max 1000)' },
181
+ { arg: 'filter', desc: 'optional CEL boolean expression' },
182
+ ],
183
+ },
184
+ {
185
+ name: 'search',
186
+ desc: 'Find resources by content/term match — config keys, env refs, images, label values, ConfigMap data, CRD fields, status messages. Secret values are never indexed.',
187
+ params: [
188
+ { arg: 'query', required: true, desc: 'tokens AND\'d; modifiers kind:, ns:, label:, image:' },
189
+ { arg: 'limit', desc: 'max hits (default 50, max 500)' },
190
+ { arg: 'include', desc: 'summary (default), raw, or none' },
191
+ { arg: 'filter', desc: 'optional CEL boolean expression' },
192
+ { arg: 'context', desc: 'per-hit context: default (summaryContext) or none' },
193
+ ],
194
+ },
195
+ {
196
+ name: 'get_subject_permissions',
197
+ desc: 'Effective RBAC for a ServiceAccount, User, or Group: the bindings that grant access, a flattened rule list, and (for SAs) the Pods running as it. Answers "what\'s the blast radius if compromised?".',
198
+ params: [
199
+ { arg: 'kind', required: true, desc: 'ServiceAccount, User, or Group' },
200
+ { arg: 'name', required: true, desc: 'subject name' },
201
+ { arg: 'namespace', desc: 'required for ServiceAccount; omit for User/Group' },
202
+ ],
203
+ },
204
+ {
205
+ name: 'get_workload_logs',
206
+ desc: 'Aggregated, filtered logs across all pods of a workload (Deployment, StatefulSet, or DaemonSet) — collected concurrently, filtered for errors/warnings, and deduplicated.',
207
+ params: [
208
+ { arg: 'kind', desc: 'deployment (default), statefulset, or daemonset' },
209
+ { arg: 'namespace', required: true, desc: 'workload namespace' },
210
+ { arg: 'name', required: true, desc: 'workload name' },
211
+ { arg: 'container', desc: 'specific container (defaults to all)' },
212
+ { arg: 'tail_lines', desc: 'lines per pod (default 100)' },
213
+ { arg: 'grep', desc: 'regex to filter lines, like kubectl logs | grep' },
214
+ { arg: 'since', desc: 'only logs newer than this duration' },
215
+ { arg: 'previous', desc: 'logs from the previous terminated container' },
216
+ ],
217
+ },
218
+ {
219
+ name: 'manage_workload',
220
+ write: true,
221
+ desc: 'Operate on a workload: restart triggers a rolling restart, scale changes the replica count, rollback reverts to a previous revision.',
222
+ params: [
223
+ { arg: 'action', required: true, desc: 'restart, scale, or rollback' },
224
+ { arg: 'kind', required: true, desc: 'deployment, statefulset, or daemonset' },
225
+ { arg: 'namespace', required: true, desc: 'workload namespace' },
226
+ { arg: 'name', required: true, desc: 'workload name' },
227
+ { arg: 'replicas', desc: 'target replica count (for scale)' },
228
+ { arg: 'revision', desc: 'target revision (for rollback)' },
229
+ ],
230
+ },
231
+ {
232
+ name: 'manage_cronjob',
233
+ write: true,
234
+ desc: 'Operate on a CronJob: trigger creates a manual Job run, suspend pauses the schedule, resume re-enables it.',
235
+ params: [
236
+ { arg: 'action', required: true, desc: 'trigger, suspend, or resume' },
237
+ { arg: 'namespace', required: true, desc: 'cronjob namespace' },
238
+ { arg: 'name', required: true, desc: 'cronjob name' },
239
+ ],
240
+ },
241
+ {
242
+ name: 'manage_gitops',
243
+ write: true,
244
+ desc: 'Operate on GitOps resources. ArgoCD: sync, refresh, terminate, rollback, suspend, resume. FluxCD: reconcile, sync-with-source, suspend, resume.',
245
+ params: [
246
+ { arg: 'action', required: true, desc: 'sync/reconcile, refresh, terminate, rollback, suspend, or resume' },
247
+ { arg: 'tool', required: true, desc: 'argocd or fluxcd' },
248
+ { arg: 'namespace', required: true, desc: 'resource namespace' },
249
+ { arg: 'name', required: true, desc: 'resource name' },
250
+ { arg: 'kind', desc: 'FluxCD resource kind (e.g. kustomization, helmrelease)' },
251
+ ],
252
+ },
253
+ {
254
+ name: 'apply_resource',
255
+ write: true,
256
+ desc: 'Create or update a resource from YAML. apply mode is a server-side apply with Force=true (can take field ownership from Helm/Flux); create mode fails if it exists. Multi-document YAML supported.',
257
+ params: [
258
+ { arg: 'yaml', required: true, desc: 'YAML manifest (multi-document with --- supported)' },
259
+ { arg: 'mode', desc: 'apply (default) or create' },
260
+ { arg: 'dry_run', desc: 'validate without persisting' },
261
+ { arg: 'namespace', desc: 'override namespace for the resource' },
262
+ ],
263
+ },
264
+ {
265
+ name: 'manage_node',
266
+ write: true,
267
+ desc: 'Operate on a node: cordon marks it unschedulable, uncordon reverses that, drain cordons then evicts all non-DaemonSet pods.',
268
+ params: [
269
+ { arg: 'action', required: true, desc: 'cordon, uncordon, or drain' },
270
+ { arg: 'name', required: true, desc: 'node name' },
271
+ { arg: 'delete_empty_dir_data', desc: 'evict pods with emptyDir volumes (default true)' },
272
+ { arg: 'force', desc: 'evict pods not managed by a controller (default false)' },
273
+ { arg: 'timeout', desc: 'drain timeout in seconds (default 60)' },
274
+ ],
275
+ },
276
+ ]
@@ -0,0 +1,78 @@
1
+ import { useIssues } from '../../api/client'
2
+ import type { SelectedResource } from '../../types'
3
+ import { IssuesView, PaneLoader, type IssueResourceRef } from '@skyhook-io/k8s-ui'
4
+ import { AlertTriangle, ArrowLeft } from 'lucide-react'
5
+
6
+ interface IssuesPaneProps {
7
+ namespaces: string[]
8
+ onBack: () => void
9
+ onNavigateToResource: (resource: SelectedResource) => void
10
+ }
11
+
12
+ // The per-cluster Issues surface. Renders the same shared triage queue
13
+ // (IssuesView) the Hub fleet view uses — single cluster here, so no cluster
14
+ // label and in-app (client-side) resource navigation. Classification +
15
+ // owner-grouping come pre-computed from radar's /api/issues
16
+ // (internal/issues.Compose → Classify → Group).
17
+ export function IssuesPane({ namespaces, onBack, onNavigateToResource }: IssuesPaneProps) {
18
+ const { data, isLoading, error } = useIssues(namespaces)
19
+
20
+ const onResourceClick = (ref: IssueResourceRef) =>
21
+ onNavigateToResource({ kind: ref.kind, namespace: ref.namespace ?? '', name: ref.name, group: ref.group ?? '' })
22
+
23
+ if (isLoading) {
24
+ return <PaneLoader label="Loading issues…" className="flex-1" />
25
+ }
26
+
27
+ if (error) {
28
+ return (
29
+ <div className="flex-1 flex items-center justify-center text-theme-text-secondary">
30
+ <p>Failed to load issues</p>
31
+ </div>
32
+ )
33
+ }
34
+
35
+ return (
36
+ <div className="flex-1 flex flex-col min-h-0 p-6 gap-6 overflow-auto">
37
+ <div className="flex items-center gap-4">
38
+ <button
39
+ onClick={onBack}
40
+ className="p-1.5 rounded-lg hover:bg-theme-hover transition-colors"
41
+ >
42
+ <ArrowLeft className="w-5 h-5 text-theme-text-secondary" />
43
+ </button>
44
+ <div className="flex-1">
45
+ <div className="flex items-center gap-2">
46
+ <AlertTriangle className="w-5 h-5 text-theme-text-secondary" />
47
+ <h1 className="text-lg font-semibold text-theme-text-primary">Issues</h1>
48
+ </div>
49
+ <p className="text-sm text-theme-text-tertiary mt-1 ml-7">
50
+ Live cluster problems — crashes, scheduling failures, bad references — grouped by the resource they affect.
51
+ </p>
52
+ </div>
53
+ </div>
54
+
55
+ {/* Visibility honesty: when RBAC reads are incomplete, an empty queue may
56
+ mean "can't see" rather than "nothing broken" — say so up front so the
57
+ empty state isn't mistaken for a clean bill of health. */}
58
+ {data?.visibility?.impact && (
59
+ <div className="-mt-3 flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-xs text-theme-text-secondary">
60
+ <AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
61
+ <span>Limited visibility — {data.visibility.impact} Results may be incomplete.</span>
62
+ </div>
63
+ )}
64
+
65
+ {/* Truncation honesty: when more issues matched than were returned, say
66
+ so — don't present a capped list as the complete picture. */}
67
+ {data?.total_matched != null && data.total_matched > (data.issues?.length ?? 0) && (
68
+ <p className="-mt-3 text-xs text-theme-text-tertiary">
69
+ Showing {data.issues?.length ?? 0} of {data.total_matched} issues (capped) — narrow by namespace to see the rest.
70
+ </p>
71
+ )}
72
+
73
+ {/* anyData = the query resolved, i.e. the cluster is reachable; an empty
74
+ list then means "nothing broken" rather than "not connected". */}
75
+ <IssuesView issues={data?.issues ?? []} anyData={!!data} onResourceClick={onResourceClick} />
76
+ </div>
77
+ )
78
+ }
@@ -308,7 +308,7 @@ export function PortForwardButton({
308
308
  className="w-full px-3 py-2 text-left text-sm text-theme-text-primary hover:bg-theme-elevated flex items-center justify-between"
309
309
  >
310
310
  <span className="flex items-center gap-2 shrink-0">
311
- <code className="text-accent-text">{port.port}</code>
311
+ <code className="inline-code">{port.port}</code>
312
312
  <span className="text-theme-text-disabled">/{port.protocol || 'TCP'}</span>
313
313
  </span>
314
314
  {port.name && (
@@ -794,7 +794,7 @@ export function PortForwardPanel() {
794
794
  <Tooltip content="Click to change local port" delay={300} position="bottom" disabled={!isPanelOpen}>
795
795
  <code
796
796
  className={clsx(
797
- 'group/port text-xs bg-theme-base px-2 py-1 rounded text-accent-text transition-all inline-flex items-center gap-1',
797
+ 'inline-code group/port text-xs transition-all inline-flex items-center gap-1',
798
798
  changingPortId === session.id
799
799
  ? 'opacity-50'
800
800
  : 'cursor-pointer hover:ring-1 hover:ring-blue-500/50'
@@ -1,10 +1,7 @@
1
1
  import { useState, useMemo } from 'react'
2
- import { clsx } from 'clsx'
3
- import { BarChart3, Wifi, WifiOff, Loader2 } from 'lucide-react'
4
2
  import {
5
- AreaChart,
3
+ PrometheusChartsView,
6
4
  MetricsSummary as BaseMetricsSummary,
7
- SeriesLegend,
8
5
  type TimeSeries,
9
6
  type ReferenceLine,
10
7
  } from '@skyhook-io/k8s-ui/components/charts'
@@ -95,7 +92,6 @@ export function PrometheusCharts({ kind, namespace, name, showEmptyState = false
95
92
  const { data: status, isLoading: statusLoading } = usePrometheusStatus()
96
93
  const connectMutation = usePrometheusConnect()
97
94
 
98
- const categories = kind === 'Node' ? NODE_CATEGORIES : WORKLOAD_CATEGORIES
99
95
  const [activeCategory, setActiveCategory] = useState<PrometheusMetricCategory>('cpu')
100
96
  const [timeRange, setTimeRange] = useState<PrometheusTimeRange>('1h')
101
97
 
@@ -116,161 +112,24 @@ export function PrometheusCharts({ kind, namespace, name, showEmptyState = false
116
112
  return computeRequestLimitLines(resource, kind, activeCategory)
117
113
  }, [resource, kind, activeCategory])
118
114
 
119
- if (!isSupported) {
120
- return null
121
- }
122
-
123
- // Loading state — checking Prometheus availability (only show when explicitly requested)
124
- if (statusLoading) {
125
- if (!showEmptyState) return null
126
- return (
127
- <div className="flex items-center justify-center py-12 text-theme-text-tertiary">
128
- <Loader2 className="w-5 h-5 animate-spin mr-2" />
129
- Checking Prometheus availability...
130
- </div>
131
- )
132
- }
133
-
134
- // When embedded in Overview (showEmptyState=false), hide when not connected or no data
135
- if (!showEmptyState) {
136
- if (!isConnected) return null
137
- if (!metricsLoading && !metricsError && !metrics?.result?.series?.length) return null
138
- }
139
-
140
- if (!isConnected) {
141
- return (
142
- <div className="flex flex-col items-center justify-center py-12 gap-4">
143
- <WifiOff className="w-10 h-10 text-theme-text-quaternary" />
144
- <div className="text-center">
145
- <p className="text-sm text-theme-text-secondary mb-1">Prometheus not connected</p>
146
- <p className="text-xs text-theme-text-tertiary mb-4">
147
- {status?.error || 'Connect to view historical CPU, memory, and network metrics'}
148
- </p>
149
- <button
150
- onClick={() => connectMutation.mutate()}
151
- disabled={connectMutation.isPending}
152
- className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg btn-brand"
153
- >
154
- {connectMutation.isPending ? (
155
- <Loader2 className="w-4 h-4 animate-spin" />
156
- ) : (
157
- <Wifi className="w-4 h-4" />
158
- )}
159
- Discover Prometheus
160
- </button>
161
- </div>
162
- </div>
163
- )
164
- }
165
-
166
- const activeCategoryDef = categories.find(c => c.key === activeCategory) || categories[0]
167
-
168
115
  return (
169
- <div className="flex flex-col h-full">
170
- {/* Toolbar */}
171
- <div className="shrink-0 flex items-center justify-between px-4 py-2.5 border-b border-theme-border bg-theme-surface/50">
172
- {/* Category tabs */}
173
- <div className="flex items-center gap-1">
174
- <BarChart3 className="w-4 h-4 text-theme-text-tertiary mr-2" />
175
- {categories.map(cat => (
176
- <button
177
- key={cat.key}
178
- onClick={() => setActiveCategory(cat.key)}
179
- className={clsx(
180
- 'px-2.5 py-1 text-xs font-medium rounded-md transition-colors',
181
- activeCategory === cat.key
182
- ? 'bg-theme-elevated text-theme-text-primary shadow-sm'
183
- : 'text-theme-text-tertiary hover:text-theme-text-secondary hover:bg-theme-elevated/50'
184
- )}
185
- >
186
- {cat.label}
187
- </button>
188
- ))}
189
- </div>
190
-
191
- {/* Time range selector */}
192
- <select
193
- value={timeRange}
194
- onChange={e => {
195
- const next = e.target.value as PrometheusTimeRange
196
- setTimeRange(next)
197
- onTimeRangeChange?.(next)
198
- }}
199
- className="px-2 py-1 text-xs rounded-md bg-theme-elevated border border-theme-border text-theme-text-secondary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
200
- >
201
- {TIME_RANGES.map(tr => (
202
- <option key={tr.value} value={tr.value}>{tr.label}</option>
203
- ))}
204
- </select>
205
- </div>
206
-
207
- {/* Chart area — fixed min-height prevents layout shift while loading */}
208
- <div className="min-h-[280px] p-4">
209
- {metricsLoading ? (
210
- <div className="flex items-center justify-center min-h-[240px] text-theme-text-tertiary">
211
- <Loader2 className="w-5 h-5 animate-spin mr-2" />
212
- Loading metrics...
213
- </div>
214
- ) : metricsError ? (
215
- <div className="flex items-center justify-center h-full text-red-400 text-sm">
216
- Failed to load metrics: {(metricsError as Error).message}
217
- </div>
218
- ) : metrics?.result?.series?.length ? (
219
- <div className="h-full flex flex-col gap-4">
220
- {/* Summary stats */}
221
- <MetricsSummary
222
- series={metrics.result.series}
223
- category={activeCategoryDef}
224
- unit={metrics.unit}
225
- />
226
-
227
- {/* Main chart */}
228
- <div className="flex-1 min-h-0">
229
- <AreaChart
230
- series={metrics.result.series}
231
- color={activeCategoryDef.chartColor}
232
- fillColor={activeCategoryDef.fillColor}
233
- unit={metrics.unit}
234
- referenceLines={referenceLines}
235
- />
236
- </div>
237
-
238
- {/* Per-pod legend for workload-level queries */}
239
- {metrics.result.series.length > 1 && (
240
- <SeriesLegend series={metrics.result.series} color={activeCategoryDef.chartColor} />
241
- )}
242
- </div>
243
- ) : (
244
- <div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
245
- <BarChart3 className="w-8 h-8 mb-2 opacity-40" />
246
- <p className="text-sm">No data for this time range</p>
247
- <p className="text-xs text-theme-text-quaternary mt-1">
248
- Try a different time range or check that metrics are being collected
249
- </p>
250
- {metrics?.hint && (
251
- <p className="mt-3 px-3 py-2 w-full max-w-lg text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-500/10 border border-yellow-500/30 rounded">
252
- {metrics.hint}
253
- </p>
254
- )}
255
- {metrics?.query && (
256
- <details className="mt-3 w-full max-w-lg text-left">
257
- <summary className="text-xs text-theme-text-quaternary cursor-pointer hover:text-theme-text-tertiary">
258
- Diagnostics: show PromQL query
259
- </summary>
260
- <div className="mt-2 p-2 bg-theme-base border border-theme-border rounded text-xs font-mono text-theme-text-secondary break-all">
261
- {metrics.query}
262
- </div>
263
- <p className="mt-1.5 text-xs text-theme-text-quaternary">
264
- This query returned no results. Verify in your Prometheus UI that the metric names and labels
265
- ({activeCategoryDef.key === 'cpu' ? 'pod, namespace, container' : 'pod, namespace'}) exist.
266
- Custom label relabeling in your Prometheus configuration may require adjustments.
267
- </p>
268
- </details>
269
- )}
270
- </div>
271
- )}
272
- </div>
273
- </div>
116
+ <PrometheusChartsView
117
+ kind={kind}
118
+ showEmptyState={showEmptyState}
119
+ statusLoading={statusLoading}
120
+ isConnected={isConnected}
121
+ statusError={status?.error}
122
+ onConnect={() => connectMutation.mutate()}
123
+ connecting={connectMutation.isPending}
124
+ category={activeCategory}
125
+ onCategoryChange={setActiveCategory}
126
+ range={timeRange}
127
+ onRangeChange={(r) => { setTimeRange(r); onTimeRangeChange?.(r) }}
128
+ metrics={metrics}
129
+ metricsLoading={metricsLoading}
130
+ metricsError={(metricsError as Error) ?? null}
131
+ referenceLines={referenceLines}
132
+ />
274
133
  )
275
134
  }
276
135
 
@@ -444,7 +444,7 @@ function AuthenticationHelp({ image, registryType, onRetry }: AuthenticationHelp
444
444
  </p>
445
445
 
446
446
  <p className="text-xs text-theme-text-tertiary text-center max-w-md mb-6">
447
- Registry: <span className="font-mono text-theme-text-secondary">{registry}</span>
447
+ Registry: <span className="inline-code">{registry}</span>
448
448
  {registryType && registryType !== 'generic' && (
449
449
  <> ({formatAuthMethod(registryType)})</>
450
450
  )}
@@ -743,4 +743,3 @@ function FileTreeNode({ node, depth, defaultExpanded = true, image, namespace, p
743
743
  </div>
744
744
  )
745
745
  }
746
-
@@ -21,10 +21,11 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
21
21
  const namespace = isClusterRole ? '' : (data?.metadata?.namespace ?? '')
22
22
  const name = roleRef.name ?? ''
23
23
 
24
- const { data: role, isLoading } = useResource<any>(kind, namespace, name)
24
+ const { data: role, isLoading, error } = useResource<any>(kind, namespace, name)
25
25
  // `role` is undefined while loading, then the resource, then potentially
26
- // null on 404. Pass [] for "loaded but no rules" so the base renderer's
27
- // `rules === null` branch fires only on outright fetch failure (orphan).
26
+ // null on 404 / 403. Pass [] for "loaded but no rules" so the base
27
+ // renderer's `rules === null` branch fires only on outright fetch failure
28
+ // (orphan or forbidden); `roleRulesError` then disambiguates the two.
28
29
  const rules =
29
30
  isLoading
30
31
  ? undefined
@@ -38,6 +39,7 @@ export function RoleBindingRenderer({ data, onNavigate }: RoleBindingRendererPro
38
39
  onNavigate={onNavigate}
39
40
  roleRules={rules ?? null}
40
41
  roleRulesLoading={isLoading}
42
+ roleRulesError={error}
41
43
  />
42
44
  )
43
45
  }
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
3
3
  import { useScaleWorkload } from '../../../api/client'
4
4
  import { useRBACSubject } from '../../../api/rbac'
5
5
  import { useQueryClient } from '@tanstack/react-query'
6
+ import type { Relationships, ResourceRef } from '../../../types'
6
7
 
7
8
  // Map plural lowercase kind to singular PascalCase for ownerReferences matching
8
9
  function getOwnerKind(kind: string): string {
@@ -19,10 +20,12 @@ function getOwnerKind(kind: string): string {
19
20
  interface WorkloadRendererProps {
20
21
  kind: string
21
22
  data: any
22
- onNavigate?: (ref: { kind: string; namespace: string; name: string }) => void
23
+ onNavigate?: (ref: ResourceRef) => void
24
+ relationships?: Relationships
25
+ scaleBlockedBy?: ResourceRef[]
23
26
  }
24
27
 
25
- export function WorkloadRenderer({ kind, data, onNavigate }: WorkloadRendererProps) {
28
+ export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: WorkloadRendererProps) {
26
29
  const navigate = useNavigate()
27
30
  const queryClient = useQueryClient()
28
31
  const scaleMutation = useScaleWorkload()
@@ -47,6 +50,7 @@ export function WorkloadRenderer({ kind, data, onNavigate }: WorkloadRendererPro
47
50
  rbacData={rbacData ?? null}
48
51
  rbacLoading={rbacLoading}
49
52
  rbacError={rbacError as Error | null}
53
+ scaleBlockedBy={scaleBlockedBy}
50
54
  onScale={async (replicas) => {
51
55
  await scaleMutation.mutateAsync({
52
56
  kind,
@@ -109,7 +109,7 @@ export function MyPermissionsDialog({ open, onClose }: MyPermissionsDialogProps)
109
109
 
110
110
  <p className="text-xs text-theme-text-tertiary">
111
111
  Computed by the Kubernetes API via{' '}
112
- <code className="bg-theme-elevated px-1 rounded">SelfSubjectRulesReview</code>.
112
+ <code className="inline-code">SelfSubjectRulesReview</code>.
113
113
  Shows what you can do in <span className="text-theme-text-secondary">{namespace}</span>,
114
114
  plus any cluster-scoped rules that apply everywhere.
115
115
  </p>