@skyhook-io/radar-app 0.2.2 → 0.3.1
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/README.md +7 -1
- package/package.json +33 -25
- package/src/App.tsx +1449 -382
- package/src/RadarApp.tsx +132 -19
- package/src/api/apiResources.ts +1 -1
- package/src/api/client.argoResourceSync.test.ts +69 -0
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.metrics.test.ts +106 -0
- package/src/api/client.rightsizing.test.ts +32 -0
- package/src/api/client.ts +2730 -271
- package/src/api/client.yaml.test.ts +45 -0
- package/src/api/diagnose.ts +289 -0
- package/src/api/quotas.ts +16 -0
- package/src/api/rbac.ts +57 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +582 -0
- package/src/components/ConnectionErrorView.tsx +186 -70
- package/src/components/ContextSwitcher.tsx +63 -18
- package/src/components/DebugOverlay.tsx +5 -3
- package/src/components/NamespaceSwitcher.tsx +41 -0
- package/src/components/UserMenu.tsx +69 -21
- package/src/components/applications/ApplicationsView.tsx +936 -0
- package/src/components/audit/AuditSettingsDialog.tsx +79 -17
- package/src/components/audit/AuditView.tsx +65 -62
- package/src/components/compare/CompareViewRoute.tsx +124 -0
- package/src/components/compare/useCompareCandidates.ts +27 -0
- package/src/components/compare/useCompareLauncher.tsx +79 -0
- package/src/components/cost/ApplicationCostTab.test.ts +204 -0
- package/src/components/cost/ApplicationCostTab.tsx +571 -0
- package/src/components/cost/CostTrendChart.tsx +106 -75
- package/src/components/cost/CostView.test.ts +12 -0
- package/src/components/cost/CostView.tsx +507 -223
- package/src/components/cost/CostViewTabs.test.tsx +21 -0
- package/src/components/cost/CostViewTabs.tsx +40 -0
- package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
- package/src/components/cost/CurrentAllocationUse.tsx +126 -0
- package/src/components/cost/WorkloadCostTab.test.ts +153 -0
- package/src/components/cost/WorkloadCostTab.tsx +372 -0
- package/src/components/cost/cloud-console.test.ts +39 -0
- package/src/components/cost/cloud-console.ts +81 -0
- package/src/components/cost/errors.ts +8 -0
- package/src/components/cost/format.test.ts +27 -0
- package/src/components/cost/format.ts +46 -0
- package/src/components/cost/kinds.ts +5 -0
- package/src/components/curl/ServiceCurlButton.tsx +445 -0
- package/src/components/diagnose/AISettings.tsx +147 -0
- package/src/components/diagnose/DiagnoseContext.tsx +495 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +622 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1756 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/TerminalTab.tsx +1 -1
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/dock/index.ts +1 -1
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
- package/src/components/gitops/GitOpsView.tsx +1042 -0
- package/src/components/gitops/RevisionMetaChip.tsx +63 -0
- package/src/components/helm/ChartBrowser.tsx +87 -31
- package/src/components/helm/HelmCompareRoute.tsx +1341 -0
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
- package/src/components/helm/HelmView.tsx +237 -96
- package/src/components/helm/InstallWizard.tsx +94 -38
- package/src/components/helm/ManifestDiffViewer.tsx +8 -27
- package/src/components/helm/OwnedResources.tsx +34 -59
- package/src/components/helm/RevisionHistory.tsx +52 -3
- package/src/components/helm/RoleGatedPanel.tsx +3 -3
- package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
- package/src/components/helm/ValuesDiffPreview.tsx +17 -7
- package/src/components/helm/ValuesViewer.tsx +49 -53
- package/src/components/helm/helm-utils.ts +4 -0
- package/src/components/home/ActivitySummary.tsx +4 -1
- package/src/components/home/ClusterHealthCard.tsx +56 -42
- package/src/components/home/CostCard.tsx +21 -36
- package/src/components/home/GitOpsControllersCard.tsx +110 -0
- package/src/components/home/HelmSummary.tsx +3 -1
- package/src/components/home/HomeView.tsx +339 -105
- package/src/components/home/MCPSetupDialog.tsx +29 -87
- package/src/components/home/TrafficSummary.tsx +2 -2
- package/src/components/home/mcpToolCatalog.ts +333 -0
- package/src/components/issues/IssuesPane.tsx +151 -0
- package/src/components/logs/LogsViewer.tsx +4 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
- package/src/components/nav/PrimaryNavRail.tsx +285 -0
- package/src/components/portforward/PortForwardButton.tsx +118 -47
- package/src/components/portforward/PortForwardManager.tsx +253 -131
- package/src/components/resource/HPACharts.tsx +237 -0
- package/src/components/resource/PVCUsageBar.tsx +59 -0
- package/src/components/resource/PrometheusCharts.tsx +160 -584
- package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
- package/src/components/resource/RestartChart.tsx +133 -0
- package/src/components/resource/RightsizingStrip.test.ts +109 -0
- package/src/components/resource/RightsizingStrip.tsx +363 -0
- package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
- package/src/components/resources/CompositeRenderer.tsx +101 -0
- package/src/components/resources/ImageFilesystemModal.tsx +19 -12
- package/src/components/resources/PodFilesystemModal.tsx +6 -5
- package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
- package/src/components/resources/ResourcesView.tsx +194 -17
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/HPARenderer.tsx +20 -1
- package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
- package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
- package/src/components/resources/renderers/PodRenderer.tsx +30 -6
- package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
- package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
- package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
- package/src/components/resources/renderers/index.ts +2 -0
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
- package/src/components/rightsizing/copy.test.ts +56 -0
- package/src/components/rightsizing/model.test.ts +227 -0
- package/src/components/rightsizing/model.ts +158 -0
- package/src/components/rightsizing/presentation.test.ts +104 -0
- package/src/components/rightsizing/presentation.ts +94 -0
- package/src/components/settings/MyPermissionsDialog.tsx +241 -0
- package/src/components/settings/SettingsDialog.tsx +1505 -165
- package/src/components/shared/CreateResourceDialog.tsx +9 -2
- package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +86 -13
- package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
- package/src/components/timeline/TimelineView.tsx +873 -24
- package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
- package/src/components/traffic/TrafficFlowList.tsx +29 -15
- package/src/components/traffic/TrafficGraph.tsx +42 -24
- package/src/components/traffic/TrafficView.tsx +32 -19
- package/src/components/ui/CommandPalette.tsx +8 -215
- package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
- package/src/components/ui/Markdown.tsx +3 -3
- package/src/components/ui/Omnibar.tsx +602 -0
- package/src/components/ui/RadarOmnibar.tsx +52 -0
- package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
- package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
- package/src/components/ui/UpdateNotification.tsx +48 -36
- package/src/components/ui/command-items.ts +178 -0
- package/src/components/workload/WorkloadView.tsx +1342 -158
- package/src/context/ConnectionContext.tsx +146 -21
- package/src/context/DiagnoseCustomization.tsx +93 -0
- package/src/context/NavCustomization.tsx +75 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/contexts/CapabilitiesContext.tsx +32 -8
- package/src/filter/FilterLocationBridge.tsx +30 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useDocumentTitle.ts +25 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/hooks/useKeyboardShortcuts.tsx +1 -0
- package/src/hooks/useMediaQuery.ts +21 -0
- package/src/hooks/useNavRailPinned.ts +46 -0
- package/src/hooks/useRecentResources.ts +49 -0
- package/src/index.css +162 -1
- package/src/index.ts +73 -1
- package/src/main.tsx +7 -5
- package/src/types/clusterLoadState.ts +33 -0
- package/src/types.ts +2 -0
- package/src/utils/auditBadges.ts +53 -0
- package/src/utils/navigation.ts +64 -1
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
1
|
+
import { useMemo, type ReactNode } from 'react'
|
|
2
|
+
import { useDashboard, useDashboardCRDs, useDashboardHelm, useIssues, type IssuesResponse } from '../../api/client'
|
|
3
|
+
import { useConnection } from '../../context/ConnectionContext'
|
|
4
|
+
import type { ClusterLoadState } from '../../types/clusterLoadState'
|
|
3
5
|
import type { ExtendedMainView, Topology, SelectedResource } from '../../types'
|
|
4
|
-
import { kindToPlural } from '../../utils/navigation'
|
|
5
6
|
import { TopologyPreview } from './TopologyPreview'
|
|
6
7
|
import { HelmSummary } from './HelmSummary'
|
|
7
8
|
import { ActivitySummary } from './ActivitySummary'
|
|
@@ -9,9 +10,22 @@ import { TrafficSummary } from './TrafficSummary'
|
|
|
9
10
|
import { CertificateHealthCard } from './CertificateHealthCard'
|
|
10
11
|
import { NetworkPolicyCoverageCard } from './NetworkPolicyCoverageCard'
|
|
11
12
|
import { CostCard } from './CostCard'
|
|
12
|
-
import {
|
|
13
|
+
import { GitOpsControllersCard } from './GitOpsControllersCard'
|
|
14
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
15
|
+
import {
|
|
16
|
+
AuditCard,
|
|
17
|
+
FreshnessControl,
|
|
18
|
+
PaneLoader,
|
|
19
|
+
StatusDot,
|
|
20
|
+
categoryLabel,
|
|
21
|
+
groupLabel,
|
|
22
|
+
issueTiming,
|
|
23
|
+
subjectRef,
|
|
24
|
+
type Issue,
|
|
25
|
+
} from '@skyhook-io/k8s-ui'
|
|
26
|
+
import { formatCompactAge } from '@skyhook-io/k8s-ui/utils/format'
|
|
13
27
|
import { ClusterHealthCard } from './ClusterHealthCard'
|
|
14
|
-
import { AlertTriangle, Shield } from 'lucide-react'
|
|
28
|
+
import { AlertTriangle, CheckCircle, Loader2, Shield } from 'lucide-react'
|
|
15
29
|
import { clsx } from 'clsx'
|
|
16
30
|
|
|
17
31
|
interface HomeViewProps {
|
|
@@ -20,16 +34,44 @@ interface HomeViewProps {
|
|
|
20
34
|
onNavigateToView: (view: ExtendedMainView, params?: Record<string, string>) => void
|
|
21
35
|
onNavigateToResourceKind: (kind: string, group?: string, filters?: Record<string, string[]>) => void
|
|
22
36
|
onNavigateToResource: (resource: SelectedResource) => void
|
|
37
|
+
fallbackClusterLoadState?: ClusterLoadState
|
|
38
|
+
/**
|
|
39
|
+
* Optional override for the Certificate Health card's click. When an embedded
|
|
40
|
+
* host (Radar Cloud) takes Certs over with its own fleet page, it passes this
|
|
41
|
+
* to route there instead of Radar's TLS-secrets resource list. Omitted in
|
|
42
|
+
* standalone OSS → the card drills into secrets as before.
|
|
43
|
+
*/
|
|
44
|
+
onNavigateToCerts?: () => void
|
|
23
45
|
}
|
|
24
46
|
|
|
25
|
-
export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToResourceKind, onNavigateToResource }: HomeViewProps) {
|
|
26
|
-
const { data, isLoading, error } = useDashboard(namespaces)
|
|
47
|
+
export function HomeView({ namespaces, topology, fallbackClusterLoadState, onNavigateToView, onNavigateToResourceKind, onNavigateToResource, onNavigateToCerts }: HomeViewProps) {
|
|
48
|
+
const { data, isLoading, error, dataUpdatedAt, refetch } = useDashboard(namespaces)
|
|
49
|
+
const { connection } = useConnection()
|
|
50
|
+
const { data: issuesData, isLoading: issuesLoading, isFetching: issuesFetching, error: issuesError } = useIssues(namespaces)
|
|
51
|
+
const issues = issuesData?.issues ?? []
|
|
52
|
+
const issueCount = issuesData?.total_matched ?? issuesData?.total ?? issues.length
|
|
53
|
+
const hasCriticalIssues = issues.some((issue) => issue.severity === 'critical')
|
|
54
|
+
|
|
55
|
+
// SSE is cluster-wide on small/medium clusters; the picker only narrows the
|
|
56
|
+
// dashboard summary, so re-apply the filter here or the legend disagrees.
|
|
57
|
+
const scopedTopology = useMemo<Topology | null>(() => {
|
|
58
|
+
if (!topology) return null
|
|
59
|
+
if (namespaces.length === 0) return topology
|
|
60
|
+
const nsSet = new Set(namespaces)
|
|
61
|
+
const nodes = topology.nodes.filter(n => {
|
|
62
|
+
const ns = n.data.namespace as string | undefined
|
|
63
|
+
return !ns || nsSet.has(ns)
|
|
64
|
+
})
|
|
65
|
+
const nodeIds = new Set(nodes.map(n => n.id))
|
|
66
|
+
const edges = topology.edges.filter(e => nodeIds.has(e.source) && nodeIds.has(e.target))
|
|
67
|
+
return { nodes, edges }
|
|
68
|
+
}, [topology, namespaces])
|
|
27
69
|
// CRDs and Helm load lazily after main dashboard to keep initial load fast
|
|
28
70
|
const { data: crdsData } = useDashboardCRDs(namespaces)
|
|
29
71
|
const { data: helmData } = useDashboardHelm(namespaces)
|
|
30
72
|
|
|
31
73
|
if (isLoading) {
|
|
32
|
-
return <PaneLoader label="Loading dashboard…" className="flex-1" />
|
|
74
|
+
return <PaneLoader label="Loading dashboard…" className="flex-1 min-h-0" />
|
|
33
75
|
}
|
|
34
76
|
|
|
35
77
|
if (error || !data) {
|
|
@@ -56,159 +98,351 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
56
98
|
)
|
|
57
99
|
}
|
|
58
100
|
|
|
59
|
-
const hasProblems = data.problems && data.problems.length > 0
|
|
60
|
-
|
|
61
101
|
return (
|
|
62
102
|
<div className="flex-1 overflow-y-auto">
|
|
63
103
|
<div className="max-w-[1600px] mx-auto px-6 py-6 space-y-6">
|
|
104
|
+
{fallbackClusterLoadState?.loading && (
|
|
105
|
+
<div className="flex items-center gap-2 text-sm text-theme-text-tertiary">
|
|
106
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
107
|
+
<span>{fallbackClusterLoadState.message}</span>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
64
110
|
{/* Row 1: Cluster Health Card (combined health + resource counts) */}
|
|
65
111
|
<ClusterHealthCard
|
|
112
|
+
freshness={
|
|
113
|
+
<FreshnessControl
|
|
114
|
+
mode="auto"
|
|
115
|
+
dataUpdatedAt={dataUpdatedAt}
|
|
116
|
+
onRefresh={() => refetch()}
|
|
117
|
+
connectionState={connection.state}
|
|
118
|
+
/>
|
|
119
|
+
}
|
|
66
120
|
health={data.health}
|
|
67
121
|
counts={data.resourceCounts}
|
|
68
122
|
cluster={data.cluster}
|
|
69
123
|
metrics={data.metrics}
|
|
70
124
|
metricsServerAvailable={data.metricsServerAvailable}
|
|
71
125
|
topCRDs={crdsData?.topCRDs}
|
|
72
|
-
|
|
126
|
+
issueCount={issueCount}
|
|
127
|
+
hasCriticalIssues={hasCriticalIssues}
|
|
73
128
|
nodeVersionSkew={data.nodeVersionSkew}
|
|
74
129
|
onNavigateToKind={onNavigateToResourceKind}
|
|
75
130
|
onNavigateToView={() => onNavigateToView('resources')}
|
|
76
131
|
onWarningEventsClick={() => onNavigateToView('timeline', { view: 'list', filter: 'warnings', time: 'all' })}
|
|
77
|
-
|
|
132
|
+
onIssuesClick={() => onNavigateToView('issues')}
|
|
78
133
|
/>
|
|
79
134
|
|
|
80
|
-
{/* Row 2: Main content columns
|
|
81
|
-
<div className=
|
|
82
|
-
'grid gap-6',
|
|
83
|
-
hasProblems ? 'grid-cols-1 lg:grid-cols-[1fr_420px]' : 'grid-cols-1'
|
|
84
|
-
)}>
|
|
135
|
+
{/* Row 2: Main content columns - teasers left, issues right */}
|
|
136
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_420px]">
|
|
85
137
|
{/* Left column: teaser cards */}
|
|
86
138
|
<div className="flex flex-col gap-6 auto-rows-min">
|
|
87
|
-
{/*
|
|
139
|
+
{/* Live band — Topology + Timeline always render, so a fixed 2-up never strands.
|
|
140
|
+
These are the richest visuals and the most-used live views, so they get the width. */}
|
|
88
141
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
89
142
|
<TopologyPreview
|
|
90
|
-
topology={
|
|
143
|
+
topology={scopedTopology}
|
|
91
144
|
summary={data.topologySummary}
|
|
92
145
|
onNavigate={() => onNavigateToView('topology')}
|
|
93
146
|
/>
|
|
94
|
-
<HelmSummary
|
|
95
|
-
data={helmData}
|
|
96
|
-
onNavigate={() => onNavigateToView('helm')}
|
|
97
|
-
/>
|
|
98
147
|
<ActivitySummary
|
|
99
148
|
namespaces={namespaces}
|
|
100
|
-
topology={
|
|
149
|
+
topology={scopedTopology}
|
|
101
150
|
onNavigate={() => onNavigateToView('timeline')}
|
|
102
151
|
/>
|
|
103
|
-
<TrafficSummary
|
|
104
|
-
data={data.trafficSummary}
|
|
105
|
-
onNavigate={() => onNavigateToView('traffic')}
|
|
106
|
-
/>
|
|
107
|
-
<CostCard onNavigate={() => onNavigateToView('cost')} />
|
|
108
152
|
</div>
|
|
109
153
|
|
|
110
|
-
{/*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
{/* Explore band — flex-grow wrap so the row always fills. The conditional
|
|
155
|
+
Cost card self-hides via BandItem's empty:hidden when OpenCost is absent,
|
|
156
|
+
leaving Traffic + Helm to stretch rather than stranding an empty cell. */}
|
|
157
|
+
<div className="flex flex-wrap gap-6">
|
|
158
|
+
<BandItem>
|
|
159
|
+
<TrafficSummary
|
|
160
|
+
data={data.trafficSummary}
|
|
161
|
+
onNavigate={() => onNavigateToView('traffic')}
|
|
162
|
+
/>
|
|
163
|
+
</BandItem>
|
|
164
|
+
<BandItem>
|
|
165
|
+
<HelmSummary
|
|
166
|
+
data={helmData}
|
|
167
|
+
onNavigate={() => onNavigateToView('helm')}
|
|
168
|
+
/>
|
|
169
|
+
</BandItem>
|
|
170
|
+
<BandItem>
|
|
171
|
+
<CostCard onNavigate={() => onNavigateToView('cost')} />
|
|
172
|
+
</BandItem>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Posture band — same flex-grow wrap so any subset of compliance cards
|
|
176
|
+
fills its row instead of stranding the last one (the old 3-col grid
|
|
177
|
+
left Cluster Audit alone with two empty cells beside it). */}
|
|
178
|
+
{(data.certificateHealth || data.networkPolicyCoverage || data.audit || data.gitopsControllers) && (
|
|
179
|
+
<div className="flex flex-wrap gap-6">
|
|
180
|
+
{data.certificateHealth && (
|
|
181
|
+
<BandItem>
|
|
182
|
+
<CertificateHealthCard
|
|
183
|
+
data={data.certificateHealth}
|
|
184
|
+
onNavigate={onNavigateToCerts ?? (() => onNavigateToResourceKind('secrets', undefined, { type: ['TLS'] }))}
|
|
185
|
+
/>
|
|
186
|
+
</BandItem>
|
|
187
|
+
)}
|
|
188
|
+
{data.networkPolicyCoverage && (
|
|
189
|
+
<BandItem>
|
|
190
|
+
<NetworkPolicyCoverageCard
|
|
191
|
+
data={data.networkPolicyCoverage}
|
|
192
|
+
onNavigate={() => onNavigateToResourceKind('networkpolicies', 'networking.k8s.io')}
|
|
193
|
+
/>
|
|
194
|
+
</BandItem>
|
|
195
|
+
)}
|
|
196
|
+
{data.gitopsControllers && (
|
|
197
|
+
<BandItem>
|
|
198
|
+
<GitOpsControllersCard
|
|
199
|
+
data={data.gitopsControllers}
|
|
200
|
+
onNavigate={() => onNavigateToView('gitops')}
|
|
201
|
+
/>
|
|
202
|
+
</BandItem>
|
|
203
|
+
)}
|
|
204
|
+
{data.audit && (
|
|
205
|
+
<BandItem>
|
|
206
|
+
<AuditCard
|
|
207
|
+
data={data.audit}
|
|
208
|
+
onNavigate={() => onNavigateToView('checks')}
|
|
209
|
+
/>
|
|
210
|
+
</BandItem>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
145
214
|
</div>
|
|
146
215
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
216
|
+
<ProblemsPanel
|
|
217
|
+
issues={issues}
|
|
218
|
+
issueCount={issueCount}
|
|
219
|
+
visibility={issuesData?.visibility}
|
|
220
|
+
hasData={!!issuesData}
|
|
221
|
+
isLoading={issuesLoading && !issuesData}
|
|
222
|
+
isRefreshing={issuesFetching && !!issuesData}
|
|
223
|
+
error={issuesError}
|
|
224
|
+
totalReturned={issues.length}
|
|
225
|
+
onNavigateToIssues={() => onNavigateToView('issues')}
|
|
226
|
+
onResourceClick={onNavigateToResource}
|
|
227
|
+
/>
|
|
154
228
|
</div>
|
|
155
229
|
</div>
|
|
156
230
|
</div>
|
|
157
231
|
)
|
|
158
232
|
}
|
|
159
233
|
|
|
234
|
+
// A self-tiling flex item: grows to share the row, clamps to a sensible min
|
|
235
|
+
// width, and removes itself (empty:hidden) when its card renders null — so a
|
|
236
|
+
// data-gated card (e.g. Cost without OpenCost) can't leave a phantom column.
|
|
237
|
+
function BandItem({ children }: { children: ReactNode }) {
|
|
238
|
+
return <div className="flex-1 min-w-[260px] empty:hidden [&>*]:w-full">{children}</div>
|
|
239
|
+
}
|
|
240
|
+
|
|
160
241
|
// ============================================================================
|
|
161
242
|
// Problems Panel (right sidebar, scrollable)
|
|
162
243
|
// ============================================================================
|
|
163
244
|
|
|
164
245
|
interface ProblemsPanelProps {
|
|
165
|
-
|
|
246
|
+
issues: Issue[]
|
|
247
|
+
issueCount: number
|
|
248
|
+
visibility?: IssuesResponse['visibility']
|
|
249
|
+
hasData: boolean
|
|
250
|
+
isLoading: boolean
|
|
251
|
+
isRefreshing: boolean
|
|
252
|
+
error: unknown
|
|
253
|
+
totalReturned: number
|
|
254
|
+
onNavigateToIssues: () => void
|
|
166
255
|
onResourceClick: (resource: SelectedResource) => void
|
|
167
256
|
}
|
|
168
257
|
|
|
169
258
|
|
|
170
|
-
function ProblemsPanel({
|
|
259
|
+
function ProblemsPanel({
|
|
260
|
+
issues,
|
|
261
|
+
issueCount,
|
|
262
|
+
visibility,
|
|
263
|
+
hasData,
|
|
264
|
+
isLoading,
|
|
265
|
+
isRefreshing,
|
|
266
|
+
error,
|
|
267
|
+
totalReturned,
|
|
268
|
+
onNavigateToIssues,
|
|
269
|
+
onResourceClick,
|
|
270
|
+
}: ProblemsPanelProps) {
|
|
271
|
+
const hasCriticalIssues = issues.some((issue) => issue.severity === 'critical')
|
|
272
|
+
const hasIssues = issueCount > 0
|
|
273
|
+
const hasHardError = !!error && !hasData
|
|
274
|
+
const hasLimitedVisibility = !!visibility?.impact
|
|
275
|
+
const isTruncated = issueCount > totalReturned
|
|
276
|
+
const titleClass = hasCriticalIssues
|
|
277
|
+
? 'text-red-500'
|
|
278
|
+
: hasIssues || hasHardError || hasLimitedVisibility
|
|
279
|
+
? 'text-amber-500'
|
|
280
|
+
: 'text-theme-text-secondary'
|
|
281
|
+
const countClass = hasHardError
|
|
282
|
+
? 'status-unknown'
|
|
283
|
+
: hasCriticalIssues
|
|
284
|
+
? 'status-unhealthy'
|
|
285
|
+
: hasIssues || hasLimitedVisibility
|
|
286
|
+
? 'status-degraded'
|
|
287
|
+
: 'status-healthy'
|
|
288
|
+
const countLabel = isLoading ? '...' : hasHardError ? 'error' : String(issueCount)
|
|
289
|
+
|
|
171
290
|
return (
|
|
172
291
|
<div className="rounded-xl bg-theme-surface shadow-theme-sm flex flex-col lg:max-h-[calc(100vh-280px)] lg:sticky lg:top-0">
|
|
173
292
|
<div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50 shrink-0">
|
|
174
293
|
<div className="flex items-center gap-2">
|
|
175
|
-
<AlertTriangle className=
|
|
176
|
-
<span className=
|
|
294
|
+
<AlertTriangle className={clsx('w-4 h-4', titleClass)} />
|
|
295
|
+
<span className={clsx('text-xs font-semibold uppercase tracking-wider', titleClass)}>Active Issues</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div className="flex items-center gap-2">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
className="rounded-md px-2 py-1 text-xs font-medium text-accent-text transition-colors hover:bg-accent-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-radar-accent)]/40"
|
|
301
|
+
onClick={onNavigateToIssues}
|
|
302
|
+
>
|
|
303
|
+
View all
|
|
304
|
+
</button>
|
|
305
|
+
{isRefreshing && !isLoading && (
|
|
306
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin text-theme-text-tertiary" aria-label="Refreshing issues" />
|
|
307
|
+
)}
|
|
308
|
+
<span className={clsx('badge rounded-full', countClass)}>{countLabel}</span>
|
|
177
309
|
</div>
|
|
178
|
-
<span className="badge status-unhealthy rounded-full">{problems.length}</span>
|
|
179
310
|
</div>
|
|
180
311
|
<div className="overflow-y-auto flex-1 min-h-0">
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
312
|
+
{isLoading ? (
|
|
313
|
+
<ProblemsPanelState
|
|
314
|
+
icon={<Loader2 className="h-5 w-5 animate-spin text-theme-text-tertiary" />}
|
|
315
|
+
title="Loading issues"
|
|
316
|
+
body="Checking live cluster issues for the selected scope."
|
|
317
|
+
/>
|
|
318
|
+
) : hasHardError ? (
|
|
319
|
+
<ProblemsPanelState
|
|
320
|
+
icon={<AlertTriangle className="h-5 w-5 text-amber-500" />}
|
|
321
|
+
title="Issues unavailable"
|
|
322
|
+
body={formatIssueError(error)}
|
|
323
|
+
/>
|
|
324
|
+
) : (
|
|
325
|
+
<>
|
|
326
|
+
{!!error && (
|
|
327
|
+
<ProblemsPanelNotice tone="warning">
|
|
328
|
+
Issue refresh failed. Showing the last successful result.
|
|
329
|
+
</ProblemsPanelNotice>
|
|
330
|
+
)}
|
|
331
|
+
{visibility?.impact && (
|
|
332
|
+
<ProblemsPanelNotice tone="warning">
|
|
333
|
+
Limited visibility - {visibility.impact} Results may be incomplete.
|
|
334
|
+
</ProblemsPanelNotice>
|
|
335
|
+
)}
|
|
336
|
+
{isTruncated && (
|
|
337
|
+
<ProblemsPanelNotice tone="neutral">
|
|
338
|
+
Showing {totalReturned} of {issueCount} issues. Narrow by namespace to see the rest.
|
|
339
|
+
</ProblemsPanelNotice>
|
|
340
|
+
)}
|
|
341
|
+
|
|
342
|
+
{issues.length === 0 ? (
|
|
343
|
+
<ProblemsPanelState
|
|
344
|
+
icon={
|
|
345
|
+
hasLimitedVisibility
|
|
346
|
+
? <AlertTriangle className="h-5 w-5 text-amber-500" />
|
|
347
|
+
: <CheckCircle className="h-5 w-5 text-green-500" />
|
|
348
|
+
}
|
|
349
|
+
title={hasLimitedVisibility ? 'No visible active issues' : 'No active issues'}
|
|
350
|
+
body={
|
|
351
|
+
hasLimitedVisibility
|
|
352
|
+
? 'Radar could not read every workload source in this scope.'
|
|
353
|
+
: 'No critical or warning issues across the selected scope.'
|
|
354
|
+
}
|
|
355
|
+
/>
|
|
356
|
+
) : (
|
|
357
|
+
<div className="divide-y divide-theme-border">
|
|
358
|
+
{issues.map((issue) => {
|
|
359
|
+
const ref = subjectRef(issue)
|
|
360
|
+
const age = issue.first_seen ? formatCompactAge(issue.first_seen) : ''
|
|
361
|
+
const timing = issueTiming(issue)
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<button
|
|
365
|
+
key={issue.id}
|
|
366
|
+
className="w-full flex items-center gap-2 px-3 py-1.5 hover:bg-theme-hover transition-colors text-left"
|
|
367
|
+
onClick={() => onResourceClick({
|
|
368
|
+
kind: ref.kind,
|
|
369
|
+
namespace: ref.namespace ?? '',
|
|
370
|
+
name: ref.name,
|
|
371
|
+
group: ref.group ?? '',
|
|
372
|
+
})}
|
|
373
|
+
>
|
|
374
|
+
<StatusDot tone={issue.severity === 'critical' ? 'unhealthy' : 'degraded'} className="shrink-0" />
|
|
375
|
+
<div className="min-w-0 flex-1">
|
|
376
|
+
<div className="flex items-center gap-1.5">
|
|
377
|
+
<span className="text-[10px] text-theme-text-tertiary bg-theme-elevated px-1 py-0.5 rounded">{issue.kind}</span>
|
|
378
|
+
<span className="text-xs text-theme-text-primary truncate font-medium">{issue.name}</span>
|
|
379
|
+
{(age || timing) && (
|
|
380
|
+
<span className="ml-auto flex shrink-0 items-center gap-1">
|
|
381
|
+
{age && <span className="text-[10px] text-theme-text-tertiary tabular-nums">{age}</span>}
|
|
382
|
+
{timing && (
|
|
383
|
+
<Tooltip content={timing.tooltip} delay={100}>
|
|
384
|
+
<span className="badge-sm text-[10px] text-theme-text-secondary">{timing.chip}</span>
|
|
385
|
+
</Tooltip>
|
|
386
|
+
)}
|
|
387
|
+
</span>
|
|
388
|
+
)}
|
|
389
|
+
</div>
|
|
390
|
+
<div className="flex items-center gap-1.5 mt-0.5">
|
|
391
|
+
<span className="text-[11px] text-theme-text-secondary truncate">{categoryLabel(issue.category)}</span>
|
|
392
|
+
<span className="text-[10px] text-theme-text-tertiary shrink-0">{groupLabel(issue.category_group)}</span>
|
|
393
|
+
{issue.namespace && <span className="text-[10px] text-theme-text-tertiary shrink-0">{issue.namespace}</span>}
|
|
394
|
+
</div>
|
|
395
|
+
{(issue.reason || issue.message) && (
|
|
396
|
+
<div className="text-[10px] text-theme-text-tertiary truncate mt-0.5">
|
|
397
|
+
{issue.reason}
|
|
398
|
+
{issue.reason && issue.message ? ' - ' : ''}
|
|
399
|
+
{issue.message}
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
</button>
|
|
404
|
+
)
|
|
405
|
+
})}
|
|
207
406
|
</div>
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
407
|
+
)}
|
|
408
|
+
</>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function ProblemsPanelState({ icon, title, body }: { icon: ReactNode; title: string; body: string }) {
|
|
416
|
+
return (
|
|
417
|
+
<div className="flex min-h-[220px] flex-col items-center justify-center gap-2 px-6 py-10 text-center">
|
|
418
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-theme-elevated">
|
|
419
|
+
{icon}
|
|
420
|
+
</div>
|
|
421
|
+
<p className="text-sm font-medium text-theme-text-primary">{title}</p>
|
|
422
|
+
<p className="max-w-[280px] text-xs leading-5 text-theme-text-secondary">{body}</p>
|
|
423
|
+
</div>
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function ProblemsPanelNotice({ tone, children }: { tone: 'warning' | 'neutral'; children: ReactNode }) {
|
|
428
|
+
return (
|
|
429
|
+
<div className="px-3 pt-3">
|
|
430
|
+
<div
|
|
431
|
+
className={clsx(
|
|
432
|
+
'rounded-lg border px-3 py-2 text-xs leading-5',
|
|
433
|
+
tone === 'warning'
|
|
434
|
+
? 'border-amber-500/20 bg-amber-500/10 text-theme-text-secondary'
|
|
435
|
+
: 'border-theme-border bg-theme-elevated text-theme-text-secondary',
|
|
436
|
+
)}
|
|
437
|
+
>
|
|
438
|
+
{children}
|
|
211
439
|
</div>
|
|
212
440
|
</div>
|
|
213
441
|
)
|
|
214
442
|
}
|
|
443
|
+
|
|
444
|
+
function formatIssueError(error: unknown): string {
|
|
445
|
+
return error instanceof Error && error.message
|
|
446
|
+
? error.message
|
|
447
|
+
: 'Failed to load active issues.'
|
|
448
|
+
}
|