@skyhook-io/radar-app 1.5.0 → 1.6.0
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 +4 -4
- package/src/App.tsx +168 -42
- package/src/RadarApp.tsx +9 -1
- package/src/api/client.ts +65 -2
- package/src/components/UserMenu.tsx +56 -10
- package/src/components/applications/ApplicationsView.tsx +27 -19
- package/src/components/audit/AuditSettingsDialog.tsx +1 -1
- package/src/components/audit/AuditView.tsx +23 -35
- package/src/components/gitops/GitOpsView.tsx +24 -2
- package/src/components/helm/HelmView.tsx +12 -8
- package/src/components/home/HomeView.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +34 -0
- package/src/components/issues/IssuesPane.tsx +82 -28
- package/src/components/nav/PrimaryNavRail.tsx +282 -0
- package/src/components/resource/HPACharts.tsx +7 -2
- package/src/components/resource/RestartChart.tsx +8 -0
- package/src/components/resources/renderers/HPARenderer.tsx +4 -1
- package/src/components/resources/renderers/WorkloadRenderer.tsx +34 -3
- package/src/components/settings/SettingsDialog.tsx +18 -1
- package/src/components/ui/CommandPalette.tsx +6 -215
- package/src/components/ui/Omnibar.tsx +493 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +3 -1
- package/src/context/NavCustomization.tsx +11 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/utils/navigation.ts +11 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ApplicationsList,
|
|
5
5
|
ApplicationDetail,
|
|
6
6
|
CenteredEmpty,
|
|
7
|
+
PageHeader,
|
|
7
8
|
useToast,
|
|
8
9
|
orderEnvs,
|
|
9
10
|
matchWorkloadAcrossInstances,
|
|
@@ -65,26 +66,33 @@ export function ApplicationsView({ namespaces, onOpenResource }: ApplicationsVie
|
|
|
65
66
|
return <AppDetailRoute app={selected} apps={apps} onBack={() => selectApp(null)} onOpenResource={onOpenResource} />
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
// The header + status + filters + table chassis lives inside ApplicationsList
|
|
70
|
+
// (mirroring GitOpsTableView), which renders only on the data path. To keep
|
|
71
|
+
// the page header from vanishing while loading / on error, the wrapper shows
|
|
72
|
+
// the same header bar above those states. (Keep title + description in sync
|
|
73
|
+
// with ApplicationsList's PageHeader.)
|
|
74
|
+
if (query.isLoading || query.error) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
77
|
+
<div className="shrink-0 border-b border-theme-border px-4 py-4">
|
|
78
|
+
<PageHeader
|
|
79
|
+
icon={Boxes}
|
|
80
|
+
title="Applications"
|
|
81
|
+
description="Deployable software in this cluster — your services, workers, and jobs, grouped by app/release evidence."
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
{query.isLoading ? (
|
|
85
|
+
<CenteredEmpty icon={Boxes} headline="Loading applications…" />
|
|
86
|
+
) : (
|
|
87
|
+
<CenteredEmpty tone="filtered" icon={Boxes} headline="Failed to load applications" body={(query.error as Error).message} />
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<CenteredEmpty tone="filtered" icon={Boxes} headline="Failed to load applications" body={(query.error as Error).message} />
|
|
79
|
-
) : apps.length === 0 ? (
|
|
80
|
-
<CenteredEmpty
|
|
81
|
-
icon={Boxes}
|
|
82
|
-
headline="No applications detected yet"
|
|
83
|
-
body="Deploy services, workers, or jobs to this cluster to see them grouped by app."
|
|
84
|
-
/>
|
|
85
|
-
) : (
|
|
86
|
-
<ApplicationsList apps={apps} onSelect={selectApp} />
|
|
87
|
-
)}
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
95
|
+
<ApplicationsList apps={apps} onSelect={selectApp} />
|
|
88
96
|
</div>
|
|
89
97
|
)
|
|
90
98
|
}
|
|
@@ -73,7 +73,7 @@ export function AuditSettingsDialog({ namespaces, onClose }: AuditSettingsDialog
|
|
|
73
73
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
74
74
|
<div className="bg-theme-surface rounded-xl shadow-xl w-full max-w-lg mx-4 max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
|
75
75
|
<div className="flex items-center justify-between px-5 py-4 border-b border-theme-border shrink-0">
|
|
76
|
-
<h2 className="text-sm font-semibold text-theme-text-primary">
|
|
76
|
+
<h2 className="text-sm font-semibold text-theme-text-primary">Checks Settings</h2>
|
|
77
77
|
<button onClick={onClose} className="p-1 rounded-lg hover:bg-theme-hover transition-colors">
|
|
78
78
|
<X className="w-4 h-4 text-theme-text-tertiary" />
|
|
79
79
|
</button>
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react'
|
|
2
2
|
import { useAudit, useAuditSettings, useUpdateAuditSettings, useCloudRole } from '../../api/client'
|
|
3
3
|
import type { SelectedResource } from '../../types'
|
|
4
|
-
import { ChecksView, PaneLoader, type CheckResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
|
-
import {
|
|
4
|
+
import { ChecksView, PaneLoader, PageHeader, type CheckResourceRef } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { ShieldCheck, Settings } from 'lucide-react'
|
|
6
6
|
import { AuditSettingsDialog } from './AuditSettingsDialog'
|
|
7
7
|
|
|
8
8
|
interface AuditViewProps {
|
|
9
9
|
namespaces: string[]
|
|
10
|
-
onBack: () => void
|
|
11
10
|
onNavigateToResource: (resource: SelectedResource) => void
|
|
12
11
|
}
|
|
13
12
|
|
|
@@ -17,7 +16,7 @@ interface AuditViewProps {
|
|
|
17
16
|
// come pre-computed from radar's /api/audit (pkg/audit.BuildChecks); local
|
|
18
17
|
// ~/.radar settings are this cluster's "policy" and the row hide-menu writes to
|
|
19
18
|
// them.
|
|
20
|
-
export function AuditView({ namespaces,
|
|
19
|
+
export function AuditView({ namespaces, onNavigateToResource }: AuditViewProps) {
|
|
21
20
|
const { data, isLoading, error } = useAudit(namespaces)
|
|
22
21
|
const { data: auditSettings } = useAuditSettings()
|
|
23
22
|
const updateSettings = useUpdateAuditSettings()
|
|
@@ -73,37 +72,26 @@ export function AuditView({ namespaces, onBack, onNavigateToResource }: AuditVie
|
|
|
73
72
|
onNavigateToResource({ kind: ref.kind, namespace: ref.namespace, name: ref.name, group: ref.group })
|
|
74
73
|
|
|
75
74
|
return (
|
|
76
|
-
<div className="flex-1 flex flex-col min-h-0 p-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
|
|
97
|
-
)}
|
|
98
|
-
<button
|
|
99
|
-
onClick={() => setShowSettings(true)}
|
|
100
|
-
className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
101
|
-
title="Checks settings"
|
|
102
|
-
>
|
|
103
|
-
<Settings className="w-4 h-4" />
|
|
104
|
-
</button>
|
|
105
|
-
</div>
|
|
106
|
-
</div>
|
|
75
|
+
<div className="flex-1 flex flex-col min-h-0 p-4 gap-4 overflow-auto">
|
|
76
|
+
<PageHeader
|
|
77
|
+
icon={ShieldCheck}
|
|
78
|
+
title="Checks"
|
|
79
|
+
description="Security, reliability, and efficiency best practices (NSA/CISA, CIS, Polaris, Kubescape), grouped into a remediation queue."
|
|
80
|
+
actions={
|
|
81
|
+
<>
|
|
82
|
+
{ignoredCount > 0 && (
|
|
83
|
+
<button onClick={() => setShowSettings(true)} className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors">{ignoredCount} {ignoredCount === 1 ? 'namespace' : 'namespaces'} hidden</button>
|
|
84
|
+
)}
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setShowSettings(true)}
|
|
87
|
+
className="p-2 rounded-lg hover:bg-theme-hover text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
|
|
88
|
+
title="Checks settings"
|
|
89
|
+
>
|
|
90
|
+
<Settings className="w-4 h-4" />
|
|
91
|
+
</button>
|
|
92
|
+
</>
|
|
93
|
+
}
|
|
94
|
+
/>
|
|
107
95
|
|
|
108
96
|
<ChecksView
|
|
109
97
|
checks={data.groupedChecks ?? []}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from 'react'
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { useLocation, useNavigate } from 'react-router-dom'
|
|
3
3
|
import { useQuery } from '@tanstack/react-query'
|
|
4
4
|
import yaml from 'yaml'
|
|
@@ -204,6 +204,28 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
|
|
|
204
204
|
window.setTimeout(refetchTable, 1200)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// Cold-cache catch-up: right after a cluster/namespace switch (or first open)
|
|
208
|
+
// the GitOps CRD informers can still be warming, so the first list resolves
|
|
209
|
+
// EMPTY even though apps exist — stranding the user on "No applications found"
|
|
210
|
+
// until the 120s poll (which is why a manual Refresh "fixes" it). When the
|
|
211
|
+
// fetch settles empty, briefly retry (bounded) to catch the cache as it syncs.
|
|
212
|
+
// Reset the budget whenever the cluster (apiResources identity) or namespace
|
|
213
|
+
// scope changes so each switch gets a fresh set of retries.
|
|
214
|
+
const coldRetriesRef = useRef(0)
|
|
215
|
+
// While we're still retrying a cold cache, the view shows a spinner (not the
|
|
216
|
+
// false "No applications found") — so the user sees "loading", not "empty".
|
|
217
|
+
const [coldRetrying, setColdRetrying] = useState(false)
|
|
218
|
+
useEffect(() => { coldRetriesRef.current = 0; setColdRetrying(false) }, [apiResources, namespacesParam])
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (apiResourcesLoading || rowsQuery.isFetching) return
|
|
221
|
+
if ((rowsQuery.data?.length ?? 0) > 0) { setColdRetrying(false); return }
|
|
222
|
+
if (coldRetriesRef.current >= 4) { setColdRetrying(false); return }
|
|
223
|
+
setColdRetrying(true)
|
|
224
|
+
const t = window.setTimeout(() => { coldRetriesRef.current += 1; refetchTable() }, 2000)
|
|
225
|
+
return () => window.clearTimeout(t)
|
|
226
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
227
|
+
}, [rowsQuery.data, rowsQuery.isFetching, apiResourcesLoading])
|
|
228
|
+
|
|
207
229
|
const handleRowAction = (row: GitOpsRow, action: GitOpsRowAction) => {
|
|
208
230
|
const { kindName: kind, namespace, name, id } = row
|
|
209
231
|
const settle = { onSuccess: refetchTableAfterMutation, onSettled: () => markAction(id, action, false) }
|
|
@@ -246,7 +268,7 @@ function GitOpsTableView({ namespaces, onClearNamespaces }: { namespaces: string
|
|
|
246
268
|
<>
|
|
247
269
|
<SharedGitOpsTableView
|
|
248
270
|
rows={rowsQuery.data ?? []}
|
|
249
|
-
loading={apiResourcesLoading || countsQuery.isLoading || rowsQuery.isLoading}
|
|
271
|
+
loading={apiResourcesLoading || countsQuery.isLoading || rowsQuery.isLoading || coldRetrying}
|
|
250
272
|
error={(rowsQuery.error as Error | null) ?? null}
|
|
251
273
|
counts={countsQuery.data?.counts ?? {}}
|
|
252
274
|
onRefresh={() => rowsQuery.refetch()}
|
|
@@ -2,7 +2,7 @@ import { useState, useMemo, useRef, useEffect, useCallback, forwardRef } from 'r
|
|
|
2
2
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
3
3
|
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
4
4
|
import { Package, Search, RefreshCw, ArrowUpCircle, LayoutGrid, List, Shield, GitBranch, ChevronRight } from 'lucide-react'
|
|
5
|
-
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import { PaneLoader, PageHeader } from '@skyhook-io/k8s-ui'
|
|
6
6
|
import { clsx } from 'clsx'
|
|
7
7
|
import { useHelmReleases, useHelmBatchUpgradeInfo, isForbiddenError } from '../../api/client'
|
|
8
8
|
import type { HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
@@ -167,6 +167,14 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
167
167
|
<div className="flex h-full w-full">
|
|
168
168
|
{/* Main Content */}
|
|
169
169
|
<div className="flex-1 flex flex-col overflow-hidden min-w-0 w-full">
|
|
170
|
+
{/* Page header — consistent across views; the tabs + search sit below. */}
|
|
171
|
+
<div className="px-4 pt-4 pb-1">
|
|
172
|
+
<PageHeader
|
|
173
|
+
icon={Package}
|
|
174
|
+
title="Helm"
|
|
175
|
+
description="Installed Helm releases and the chart catalog for this cluster."
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
170
178
|
{/* Tab bar */}
|
|
171
179
|
<div className="flex items-center gap-1 px-4 pt-3 border-b border-theme-border bg-theme-surface/50">
|
|
172
180
|
<button
|
|
@@ -204,13 +212,9 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
204
212
|
<>
|
|
205
213
|
{/* Releases Toolbar */}
|
|
206
214
|
<div className="flex items-center gap-4 px-4 py-3 border-b border-theme-border bg-theme-surface/50 shrink-0">
|
|
207
|
-
|
|
208
|
-
<
|
|
209
|
-
|
|
210
|
-
{!isFullyLoaded && (
|
|
211
|
-
<RefreshCw className="w-3.5 h-3.5 animate-spin text-theme-text-tertiary" />
|
|
212
|
-
)}
|
|
213
|
-
</div>
|
|
215
|
+
{!isFullyLoaded && (
|
|
216
|
+
<RefreshCw className="w-3.5 h-3.5 animate-spin text-theme-text-tertiary shrink-0" />
|
|
217
|
+
)}
|
|
214
218
|
<div className="flex-1 relative">
|
|
215
219
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-theme-text-tertiary" />
|
|
216
220
|
<input
|
|
@@ -182,7 +182,7 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
182
182
|
<BandItem>
|
|
183
183
|
<AuditCard
|
|
184
184
|
data={data.audit}
|
|
185
|
-
onNavigate={() => onNavigateToView('
|
|
185
|
+
onNavigate={() => onNavigateToView('checks')}
|
|
186
186
|
/>
|
|
187
187
|
</BandItem>
|
|
188
188
|
)}
|
|
@@ -201,6 +201,40 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
|
|
|
201
201
|
{ arg: 'namespace', desc: 'required for ServiceAccount; omit for User/Group' },
|
|
202
202
|
],
|
|
203
203
|
},
|
|
204
|
+
{
|
|
205
|
+
name: 'query_prometheus',
|
|
206
|
+
desc: "Run PromQL against the cluster's Prometheus (auto-discovered or configured; works with Thanos, VictoriaMetrics, Mimir). Instant queries return current values; range queries return time-series history with automatic step adjustment. Oversized results return a cardinality summary with a suggested topk rewrite instead of raw data.",
|
|
207
|
+
params: [
|
|
208
|
+
{ arg: 'query', required: true, desc: 'PromQL query to execute' },
|
|
209
|
+
{ arg: 'type', desc: 'instant (default) or range' },
|
|
210
|
+
{ arg: 'since', desc: 'range lookback, e.g. 30m, 1h, 24h, 7d (default 1h)' },
|
|
211
|
+
{ arg: 'start', desc: 'range RFC3339 start time; overrides since' },
|
|
212
|
+
{ arg: 'end', desc: 'range RFC3339 end time (default now)' },
|
|
213
|
+
{ arg: 'step', desc: 'range resolution, e.g. 30s, 5m (auto-calculated when omitted)' },
|
|
214
|
+
{ arg: 'max_points', desc: 'max data points per series (default 300, max 600)' },
|
|
215
|
+
{ arg: 'timeout', desc: 'query timeout in seconds (default 30, max 180)' },
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'discover_metrics',
|
|
220
|
+
desc: 'Discover exact Prometheus metric names (with type and help text) or values of one label before writing PromQL. Lists active series from the last hour; flags truncation so the selector can be narrowed.',
|
|
221
|
+
params: [
|
|
222
|
+
{ arg: 'match', desc: 'PromQL series selector filter, e.g. {__name__=~"node_cpu.*"}; required when label is empty' },
|
|
223
|
+
{ arg: 'label', desc: 'discover values of this label instead of metric names, e.g. namespace, pod' },
|
|
224
|
+
{ arg: 'limit', desc: 'max values returned (default 100, max 500)' },
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'get_prometheus_rules',
|
|
229
|
+
desc: 'List Prometheus alerting and recording rules with their PromQL definitions, state (firing/pending/inactive), and active alert instances. The starting point for alert investigation: fetch the rule, then query its expression.',
|
|
230
|
+
params: [
|
|
231
|
+
{ arg: 'type', desc: 'alert or record (omit for both)' },
|
|
232
|
+
{ arg: 'name', desc: 'substring filter on rule name' },
|
|
233
|
+
{ arg: 'group', desc: 'substring filter on rule group name' },
|
|
234
|
+
{ arg: 'state', desc: 'alerting rules only: firing, pending, or inactive' },
|
|
235
|
+
{ arg: 'limit', desc: 'max rules returned (default 50, max 200)' },
|
|
236
|
+
],
|
|
237
|
+
},
|
|
204
238
|
{
|
|
205
239
|
name: 'get_workload_logs',
|
|
206
240
|
desc: 'Aggregated, filtered logs across all pods of a workload (Deployment, StatefulSet, or DaemonSet) — collected concurrently, filtered for errors/warnings, and deduplicated.',
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
1
2
|
import { useIssues } from '../../api/client'
|
|
2
3
|
import type { SelectedResource } from '../../types'
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import {
|
|
5
|
+
IssuesView,
|
|
6
|
+
PaneLoader,
|
|
7
|
+
PageHeader,
|
|
8
|
+
SummaryTile,
|
|
9
|
+
ISSUE_SEVERITIES,
|
|
10
|
+
ISSUE_SEVERITY_LABEL,
|
|
11
|
+
type IssueResourceRef,
|
|
12
|
+
type IssueSeverity,
|
|
13
|
+
type SummaryTone,
|
|
14
|
+
} from '@skyhook-io/k8s-ui'
|
|
15
|
+
import { AlertTriangle } from 'lucide-react'
|
|
16
|
+
|
|
17
|
+
const SEVERITY_TONE: Record<IssueSeverity, SummaryTone> = { critical: 'error', warning: 'warning' }
|
|
5
18
|
|
|
6
19
|
interface IssuesPaneProps {
|
|
7
20
|
namespaces: string[]
|
|
8
|
-
onBack: () => void
|
|
9
21
|
onNavigateToResource: (resource: SelectedResource) => void
|
|
10
22
|
}
|
|
11
23
|
|
|
@@ -13,9 +25,28 @@ interface IssuesPaneProps {
|
|
|
13
25
|
// (IssuesView) the Hub fleet view uses — single cluster here, so no cluster
|
|
14
26
|
// label and in-app (client-side) resource navigation. Classification +
|
|
15
27
|
// owner-grouping come pre-computed from radar's /api/issues
|
|
16
|
-
// (internal/issues.Compose → Classify → Group).
|
|
17
|
-
|
|
28
|
+
// (internal/issues.Compose → Classify → Group). Filtering is the host's job
|
|
29
|
+
// (IssuesView is a pure list); single-cluster gets a light severity filter via
|
|
30
|
+
// the header status tiles (clickable → filter), matching the Applications /
|
|
31
|
+
// GitOps header-tile pattern rather than Hub's fleet facet sidebar.
|
|
32
|
+
export function IssuesPane({ namespaces, onNavigateToResource }: IssuesPaneProps) {
|
|
18
33
|
const { data, isLoading, error } = useIssues(namespaces)
|
|
34
|
+
const [severityFilter, setSeverityFilter] = useState<Set<IssueSeverity>>(new Set())
|
|
35
|
+
|
|
36
|
+
const allIssues = data?.issues ?? []
|
|
37
|
+
const totals = useMemo(() => {
|
|
38
|
+
const t: Record<IssueSeverity, number> = { critical: 0, warning: 0 }
|
|
39
|
+
for (const i of allIssues) t[i.severity] = (t[i.severity] ?? 0) + 1
|
|
40
|
+
return t
|
|
41
|
+
}, [allIssues])
|
|
42
|
+
const shown = severityFilter.size ? allIssues.filter((i) => severityFilter.has(i.severity)) : allIssues
|
|
43
|
+
|
|
44
|
+
const toggleSeverity = (s: IssueSeverity) =>
|
|
45
|
+
setSeverityFilter((prev) => {
|
|
46
|
+
const next = new Set(prev)
|
|
47
|
+
next.has(s) ? next.delete(s) : next.add(s)
|
|
48
|
+
return next
|
|
49
|
+
})
|
|
19
50
|
|
|
20
51
|
const onResourceClick = (ref: IssueResourceRef) =>
|
|
21
52
|
onNavigateToResource({ kind: ref.kind, namespace: ref.namespace ?? '', name: ref.name, group: ref.group ?? '' })
|
|
@@ -33,30 +64,37 @@ export function IssuesPane({ namespaces, onBack, onNavigateToResource }: IssuesP
|
|
|
33
64
|
}
|
|
34
65
|
|
|
35
66
|
return (
|
|
36
|
-
<div className="flex-1 flex flex-col min-h-0 p-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
<div className="flex-1 flex flex-col min-h-0 p-4 gap-4 overflow-auto">
|
|
68
|
+
<PageHeader
|
|
69
|
+
icon={AlertTriangle}
|
|
70
|
+
title="Issues"
|
|
71
|
+
description="Live cluster problems — crashes, scheduling failures, bad references — grouped by the resource they affect."
|
|
72
|
+
actions={
|
|
73
|
+
allIssues.length > 0 ? (
|
|
74
|
+
<>
|
|
75
|
+
<SummaryTile label={allIssues.length === 1 ? 'issue' : 'issues'} value={allIssues.length} />
|
|
76
|
+
{ISSUE_SEVERITIES.map((s) =>
|
|
77
|
+
totals[s] > 0 || severityFilter.has(s) ? (
|
|
78
|
+
<SummaryTile
|
|
79
|
+
key={s}
|
|
80
|
+
label={ISSUE_SEVERITY_LABEL[s]}
|
|
81
|
+
value={totals[s]}
|
|
82
|
+
tone={SEVERITY_TONE[s]}
|
|
83
|
+
active={severityFilter.has(s)}
|
|
84
|
+
onClick={() => toggleSeverity(s)}
|
|
85
|
+
/>
|
|
86
|
+
) : null,
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
) : undefined
|
|
90
|
+
}
|
|
91
|
+
/>
|
|
54
92
|
|
|
55
93
|
{/* Visibility honesty: when RBAC reads are incomplete, an empty queue may
|
|
56
94
|
mean "can't see" rather than "nothing broken" — say so up front so the
|
|
57
95
|
empty state isn't mistaken for a clean bill of health. */}
|
|
58
96
|
{data?.visibility?.impact && (
|
|
59
|
-
<div className="
|
|
97
|
+
<div className="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
98
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-500" />
|
|
61
99
|
<span>Limited visibility — {data.visibility.impact} Results may be incomplete.</span>
|
|
62
100
|
</div>
|
|
@@ -65,14 +103,30 @@ export function IssuesPane({ namespaces, onBack, onNavigateToResource }: IssuesP
|
|
|
65
103
|
{/* Truncation honesty: when more issues matched than were returned, say
|
|
66
104
|
so — don't present a capped list as the complete picture. */}
|
|
67
105
|
{data?.total_matched != null && data.total_matched > (data.issues?.length ?? 0) && (
|
|
68
|
-
<p className="
|
|
106
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
69
107
|
Showing {data.issues?.length ?? 0} of {data.total_matched} issues (capped) — narrow by namespace to see the rest.
|
|
70
108
|
</p>
|
|
71
109
|
)}
|
|
72
110
|
|
|
73
|
-
{/*
|
|
74
|
-
|
|
75
|
-
|
|
111
|
+
{/* Filtered-empty is NOT the healthy empty state: when a severity filter
|
|
112
|
+
hides every row but issues still exist, say "no matches" rather than
|
|
113
|
+
letting IssuesView render its "nothing broken" terminal state. */}
|
|
114
|
+
{severityFilter.size > 0 && allIssues.length > 0 && shown.length === 0 ? (
|
|
115
|
+
<div className="flex flex-col items-center gap-2 py-12 text-center text-sm text-theme-text-secondary">
|
|
116
|
+
<p>No issues match the selected severity.</p>
|
|
117
|
+
<button
|
|
118
|
+
type="button"
|
|
119
|
+
onClick={() => setSeverityFilter(new Set())}
|
|
120
|
+
className="text-xs text-skyhook-600 hover:text-skyhook-500 dark:text-skyhook-400"
|
|
121
|
+
>
|
|
122
|
+
Clear filter
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
/* anyData = the query resolved, i.e. the cluster is reachable; an empty
|
|
127
|
+
list then means "nothing broken" rather than "not connected". */
|
|
128
|
+
<IssuesView issues={shown} anyData={!!data} onResourceClick={onResourceClick} />
|
|
129
|
+
)}
|
|
76
130
|
</div>
|
|
77
131
|
)
|
|
78
132
|
}
|