@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.
- package/package.json +1 -1
- package/src/App.tsx +111 -58
- package/src/api/client.ts +29 -1
- package/src/components/ConnectionErrorView.tsx +2 -2
- package/src/components/gitops/GitOpsView.tsx +127 -27
- package/src/components/helm/ChartBrowser.tsx +7 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +4 -6
- package/src/components/helm/InstallWizard.tsx +1 -1
- package/src/components/helm/RoleGatedPanel.tsx +2 -2
- package/src/components/home/ClusterHealthCard.tsx +1 -1
- package/src/components/home/GitOpsControllersCard.tsx +14 -12
- package/src/components/home/HomeView.tsx +84 -56
- package/src/components/home/MCPSetupDialog.tsx +20 -86
- package/src/components/home/mcpToolCatalog.ts +276 -0
- package/src/components/issues/IssuesPane.tsx +78 -0
- package/src/components/portforward/PortForwardButton.tsx +1 -1
- package/src/components/portforward/PortForwardManager.tsx +1 -1
- package/src/components/resource/PrometheusCharts.tsx +18 -159
- package/src/components/resources/ImageFilesystemModal.tsx +1 -2
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +5 -3
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -2
- package/src/components/settings/MyPermissionsDialog.tsx +1 -1
- package/src/components/settings/SettingsDialog.tsx +22 -2
- package/src/components/timeline/TimelineSwimlanes.tsx +8 -1311
- package/src/components/ui/Markdown.tsx +1 -1
- package/src/components/ui/UpdateNotification.tsx +1 -1
- 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="
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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="
|
|
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
|
|
27
|
-
// `rules === null` branch fires only on outright fetch failure
|
|
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:
|
|
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="
|
|
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>
|