@skyhook-io/radar-app 1.3.0 → 1.3.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 +1 -1
- package/src/components/gitops/GitOpsView.tsx +127 -27
- package/src/components/helm/HelmReleaseDrawer.tsx +4 -6
- package/src/components/home/GitOpsControllersCard.tsx +14 -12
- package/src/components/home/HomeView.tsx +70 -53
- package/src/components/home/MCPSetupDialog.tsx +19 -85
- package/src/components/home/mcpToolCatalog.ts +276 -0
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +5 -3
- package/src/components/workload/WorkloadView.tsx +2 -1
package/package.json
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
formatGitOpsSourceUrl,
|
|
19
19
|
getGitOpsResourceStatus,
|
|
20
20
|
getGitOpsTool,
|
|
21
|
+
isArgoSuspendedByRadar,
|
|
21
22
|
gitOpsInsightChangeKey,
|
|
22
23
|
initNavigationMap,
|
|
23
24
|
kindToPlural,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
type GitOpsResourceTree,
|
|
35
36
|
type GitOpsInsightRef,
|
|
36
37
|
type GitOpsRow,
|
|
38
|
+
type GitOpsRowAction,
|
|
37
39
|
type GitOpsTreeFilters,
|
|
38
40
|
type GitOpsTreeRef,
|
|
39
41
|
type GitOpsTreePreset,
|
|
@@ -102,6 +104,34 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
|
|
|
102
104
|
const namespacesParam = namespaces.join(',')
|
|
103
105
|
const { data: apiResources, isLoading: apiResourcesLoading } = useAPIResources()
|
|
104
106
|
|
|
107
|
+
const argoSync = useArgoSync()
|
|
108
|
+
const argoRefresh = useArgoRefresh()
|
|
109
|
+
const argoTerminate = useArgoTerminate()
|
|
110
|
+
const argoSuspend = useArgoSuspend()
|
|
111
|
+
const argoResume = useArgoResume()
|
|
112
|
+
const fluxReconcile = useFluxReconcile()
|
|
113
|
+
const fluxSyncWithSource = useFluxSyncWithSource()
|
|
114
|
+
const fluxSuspend = useFluxSuspend()
|
|
115
|
+
const fluxResume = useFluxResume()
|
|
116
|
+
|
|
117
|
+
const [syncDialogRow, setSyncDialogRow] = useState<GitOpsRow | null>(null)
|
|
118
|
+
const [pendingActions, setPendingActions] = useState<Map<string, Set<GitOpsRowAction>>>(new Map())
|
|
119
|
+
|
|
120
|
+
// Mark an action as in-flight (or done) for a given row. Cloning the
|
|
121
|
+
// outer Map + inner Set keeps the state immutable so React rerenders
|
|
122
|
+
// and the per-item spinner flips at the right moment.
|
|
123
|
+
function markAction(rowId: string, action: GitOpsRowAction, on: boolean) {
|
|
124
|
+
setPendingActions((prev) => {
|
|
125
|
+
const next = new Map(prev)
|
|
126
|
+
const current = new Set(next.get(rowId) ?? [])
|
|
127
|
+
if (on) current.add(action)
|
|
128
|
+
else current.delete(action)
|
|
129
|
+
if (current.size === 0) next.delete(rowId)
|
|
130
|
+
else next.set(rowId, current)
|
|
131
|
+
return next
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
useEffect(() => {
|
|
106
136
|
initNavigationMap([...(apiResources ?? []), ...GITOPS_KINDS])
|
|
107
137
|
}, [apiResources])
|
|
@@ -157,23 +187,99 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
|
|
|
157
187
|
refetchInterval: 120_000,
|
|
158
188
|
})
|
|
159
189
|
|
|
190
|
+
// Row mutations invalidate granular keys (['resource', …], ['gitops-tree', …])
|
|
191
|
+
// that don't match the table's aggregate gitops-rows-main / counts queries,
|
|
192
|
+
// so refetch those explicitly — otherwise a row keeps showing the pre-action
|
|
193
|
+
// state (e.g. "Suspend" after a successful suspend) until the 120s poll,
|
|
194
|
+
// inviting a duplicate request. Radar serves reads from an informer cache that
|
|
195
|
+
// lags the write by the watch-propagation delay, so refetch once now (covers
|
|
196
|
+
// an already-current cache) and once shortly after to catch the propagated
|
|
197
|
+
// update; refetch() forces a fetch regardless of staleTime.
|
|
198
|
+
const refetchTable = () => {
|
|
199
|
+
rowsQuery.refetch()
|
|
200
|
+
countsQuery.refetch()
|
|
201
|
+
}
|
|
202
|
+
const refetchTableAfterMutation = () => {
|
|
203
|
+
refetchTable()
|
|
204
|
+
window.setTimeout(refetchTable, 1200)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const handleRowAction = (row: GitOpsRow, action: GitOpsRowAction) => {
|
|
208
|
+
const { kindName: kind, namespace, name, id } = row
|
|
209
|
+
const settle = { onSuccess: refetchTableAfterMutation, onSettled: () => markAction(id, action, false) }
|
|
210
|
+
markAction(id, action, true)
|
|
211
|
+
switch (action) {
|
|
212
|
+
case 'sync':
|
|
213
|
+
// Argo Sync is the one action that confirms — same dialog the
|
|
214
|
+
// detail page uses. The mutation fires from onConfirm; clear the
|
|
215
|
+
// in-flight flag here since the dialog now owns the lifecycle.
|
|
216
|
+
markAction(id, action, false)
|
|
217
|
+
setSyncDialogRow(row)
|
|
218
|
+
return
|
|
219
|
+
case 'refresh':
|
|
220
|
+
argoRefresh.mutate({ namespace, name, hard: false }, settle)
|
|
221
|
+
return
|
|
222
|
+
case 'hard-refresh':
|
|
223
|
+
argoRefresh.mutate({ namespace, name, hard: true }, settle)
|
|
224
|
+
return
|
|
225
|
+
case 'terminate':
|
|
226
|
+
argoTerminate.mutate({ namespace, name }, settle)
|
|
227
|
+
return
|
|
228
|
+
case 'suspend':
|
|
229
|
+
if (row.tool === 'argo') argoSuspend.mutate({ namespace, name }, settle)
|
|
230
|
+
else fluxSuspend.mutate({ kind, namespace, name }, settle)
|
|
231
|
+
return
|
|
232
|
+
case 'resume':
|
|
233
|
+
if (row.tool === 'argo') argoResume.mutate({ namespace, name }, settle)
|
|
234
|
+
else fluxResume.mutate({ kind, namespace, name }, settle)
|
|
235
|
+
return
|
|
236
|
+
case 'reconcile':
|
|
237
|
+
fluxReconcile.mutate({ kind, namespace, name }, settle)
|
|
238
|
+
return
|
|
239
|
+
case 'sync-with-source':
|
|
240
|
+
fluxSyncWithSource.mutate({ kind, namespace, name }, settle)
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
160
245
|
return (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
246
|
+
<>
|
|
247
|
+
<SharedGitOpsTableView
|
|
248
|
+
rows={rowsQuery.data ?? []}
|
|
249
|
+
loading={apiResourcesLoading || countsQuery.isLoading || rowsQuery.isLoading}
|
|
250
|
+
error={(rowsQuery.error as Error | null) ?? null}
|
|
251
|
+
counts={countsQuery.data?.counts ?? {}}
|
|
252
|
+
onRefresh={() => rowsQuery.refetch()}
|
|
253
|
+
onRowClick={(row) => {
|
|
254
|
+
const ns = row.namespace || '_'
|
|
255
|
+
const params = new URLSearchParams()
|
|
256
|
+
params.set('apiGroup', row.group)
|
|
257
|
+
navigate({ pathname: gitOpsDetailPath(row.kindName, ns, row.name), search: params.toString() })
|
|
258
|
+
}}
|
|
259
|
+
onRowAction={handleRowAction}
|
|
260
|
+
pendingRowActions={pendingActions}
|
|
261
|
+
searchHotkey
|
|
262
|
+
globalNamespaces={namespaces}
|
|
263
|
+
onClearNamespaces={onClearNamespaces}
|
|
264
|
+
/>
|
|
265
|
+
<SyncOptionsDialog
|
|
266
|
+
open={!!syncDialogRow}
|
|
267
|
+
appLabel={syncDialogRow ? `${syncDialogRow.namespace}/${syncDialogRow.name}` : ''}
|
|
268
|
+
pending={argoSync.isPending}
|
|
269
|
+
onCancel={() => setSyncDialogRow(null)}
|
|
270
|
+
onConfirm={(opts) => {
|
|
271
|
+
if (!syncDialogRow) return
|
|
272
|
+
const { namespace, name } = syncDialogRow
|
|
273
|
+
argoSync.mutate(
|
|
274
|
+
{ namespace, name, ...opts },
|
|
275
|
+
// onSettled so the dialog closes on both success and error —
|
|
276
|
+
// otherwise the error toast surfaces behind the still-open
|
|
277
|
+
// modal and the user can't read it.
|
|
278
|
+
{ onSuccess: refetchTableAfterMutation, onSettled: () => setSyncDialogRow(null) },
|
|
279
|
+
)
|
|
280
|
+
}}
|
|
281
|
+
/>
|
|
282
|
+
</>
|
|
177
283
|
)
|
|
178
284
|
}
|
|
179
285
|
|
|
@@ -219,15 +325,9 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
219
325
|
// pre-suspend prune/selfHeal state for restoration on resume. When present,
|
|
220
326
|
// the app is in a deliberately-paused state (vs. Manual mode, which is a
|
|
221
327
|
// normal operational choice) and should surface a Suspended chip alongside
|
|
222
|
-
// the other status indicators.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
Boolean(
|
|
226
|
-
resourceQ.data?.metadata?.annotations?.['radarhq.io/suspended-prune'] ||
|
|
227
|
-
resourceQ.data?.metadata?.annotations?.['radarhq.io/suspended-selfheal'] ||
|
|
228
|
-
resourceQ.data?.metadata?.annotations?.['skyhook.io/suspended-prune'] ||
|
|
229
|
-
resourceQ.data?.metadata?.annotations?.['skyhook.io/suspended-selfheal'],
|
|
230
|
-
)
|
|
328
|
+
// the other status indicators. Shared with the fleet table's row normalizer
|
|
329
|
+
// (isArgoSuspendedByRadar) so both surfaces agree on what "suspended" means.
|
|
330
|
+
const argoSuspendedByRadar = kind === 'applications' && isArgoSuspendedByRadar(resourceQ.data)
|
|
231
331
|
const effectiveSuspended = (status?.suspended ?? false) || argoSuspendedByRadar
|
|
232
332
|
// Lifecycle gate: when the resource is pending deletion, mutating
|
|
233
333
|
// actions are futile (the controller is processing finalizers and
|
|
@@ -608,7 +708,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
608
708
|
onCancel={() => setSyncDialogOpen(false)}
|
|
609
709
|
onConfirm={(opts) => {
|
|
610
710
|
argoSync.mutate({ namespace, name, ...opts }, {
|
|
611
|
-
|
|
711
|
+
onSettled: () => setSyncDialogOpen(false),
|
|
612
712
|
})
|
|
613
713
|
}}
|
|
614
714
|
/>
|
|
@@ -627,7 +727,7 @@ function GitOpsDetailView({ namespaces, onOpenResource }: GitOpsViewProps) {
|
|
|
627
727
|
return
|
|
628
728
|
}
|
|
629
729
|
argoRollback.mutate({ namespace, name, id, ...opts }, {
|
|
630
|
-
|
|
730
|
+
onSettled: () => setRollbackTarget(null),
|
|
631
731
|
})
|
|
632
732
|
}}
|
|
633
733
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { flushSync } from 'react-dom'
|
|
3
|
-
import {
|
|
3
|
+
import { FetchResult, useDockReservedHeight } from '@skyhook-io/k8s-ui'
|
|
4
4
|
import { startViewTransitionSafe } from '@skyhook-io/k8s-ui/utils/view-transition'
|
|
5
5
|
import { TRANSITION_DRAWER } from '../../utils/animation'
|
|
6
6
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
@@ -60,7 +60,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
60
60
|
const canViewSensitive = canAtLeast('member')
|
|
61
61
|
const helmNamespace = release.storageNamespace || release.namespace
|
|
62
62
|
|
|
63
|
-
const { data: releaseDetail, isLoading, refetch: refetchRelease } = useHelmRelease(
|
|
63
|
+
const { data: releaseDetail, isLoading, error: releaseError, refetch: refetchRelease } = useHelmRelease(
|
|
64
64
|
helmNamespace,
|
|
65
65
|
release.name
|
|
66
66
|
)
|
|
@@ -446,10 +446,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
446
446
|
|
|
447
447
|
{/* Content */}
|
|
448
448
|
<div className="flex-1 overflow-y-auto" style={{ viewTransitionName: 'helm-drawer-content' }}>
|
|
449
|
-
{
|
|
450
|
-
<
|
|
451
|
-
) : !releaseDetail ? (
|
|
452
|
-
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">Release not found</div>
|
|
449
|
+
{!releaseDetail ? (
|
|
450
|
+
<FetchResult loading={isLoading} error={releaseError} notFoundMessage="Release not found" className="h-32" />
|
|
453
451
|
) : (
|
|
454
452
|
<>
|
|
455
453
|
{activeTab === 'overview' && (
|
|
@@ -41,21 +41,23 @@ export function GitOpsControllersCard({ data, onNavigate }: GitOpsControllersCar
|
|
|
41
41
|
<button
|
|
42
42
|
type="button"
|
|
43
43
|
onClick={onNavigate}
|
|
44
|
-
className="
|
|
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
|
|
47
|
-
<div className="flex items-center
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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,71 +113,81 @@ 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
|
-
{/*
|
|
116
|
+
{/* Live band — Topology + 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
|
-
{/*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
131
|
+
{/* Explore band — flex-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 */}
|
|
@@ -193,6 +203,13 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
193
203
|
)
|
|
194
204
|
}
|
|
195
205
|
|
|
206
|
+
// A self-tiling flex item: grows to share the row, clamps to a sensible min
|
|
207
|
+
// width, and removes itself (empty:hidden) when its card renders null — so a
|
|
208
|
+
// data-gated card (e.g. Cost without OpenCost) can't leave a phantom column.
|
|
209
|
+
function BandItem({ children }: { children: ReactNode }) {
|
|
210
|
+
return <div className="flex-1 min-w-[260px] empty:hidden [&>*]:w-full">{children}</div>
|
|
211
|
+
}
|
|
212
|
+
|
|
196
213
|
// ============================================================================
|
|
197
214
|
// Problems Panel (right sidebar, scrollable)
|
|
198
215
|
// ============================================================================
|
|
@@ -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
|
|
212
|
-
|
|
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
|
|
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
308
|
+
<div className="flex items-center gap-2">
|
|
309
|
+
<code className="text-[11px] font-mono text-purple-400">{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.
|
|
387
|
-
<span className="text-theme-text-secondary">{p.
|
|
320
|
+
<span key={p.arg} className="badge-sm font-mono bg-theme-elevated text-theme-text-secondary" title={p.desc}>
|
|
321
|
+
<span className="text-theme-text-secondary">{p.arg}</span>
|
|
388
322
|
{p.required && <span className="text-red-400">*</span>}
|
|
389
323
|
</span>
|
|
390
324
|
))}
|
|
@@ -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
|
+
]
|
|
@@ -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
|
}
|
|
@@ -255,7 +255,7 @@ export function WorkloadView({
|
|
|
255
255
|
}, [searchParams, setSearchParams])
|
|
256
256
|
|
|
257
257
|
// Fetch resource with relationships
|
|
258
|
-
const { data: resourceResponse, isLoading: resourceLoading, refetch: refetchResource } = useResourceWithRelationships<any>(kindProp, namespace, name, rest.group)
|
|
258
|
+
const { data: resourceResponse, isLoading: resourceLoading, error: resourceError, refetch: refetchResource } = useResourceWithRelationships<any>(kindProp, namespace, name, rest.group)
|
|
259
259
|
const resource = resourceResponse?.resource
|
|
260
260
|
const relationships = resourceResponse?.relationships
|
|
261
261
|
const certificateInfo = resourceResponse?.certificateInfo
|
|
@@ -398,6 +398,7 @@ export function WorkloadView({
|
|
|
398
398
|
relationships={relationships}
|
|
399
399
|
certificateInfo={certificateInfo}
|
|
400
400
|
isLoading={resourceLoading}
|
|
401
|
+
resourceError={resourceError}
|
|
401
402
|
refetch={refetchResource}
|
|
402
403
|
// Timeline
|
|
403
404
|
allEvents={allEvents}
|