@skyhook-io/radar-app 0.2.1 → 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/assets/platform-icons/aws_eks.png +0 -0
- package/src/assets/platform-icons/azure-aks.svg +2 -0
- package/src/assets/platform-icons/google_kubernetes_engine.png +0 -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 +116 -52
- 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 +38 -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,5 +1,5 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import type { DashboardResponse, DashboardMetrics, DashboardCRDCount
|
|
1
|
+
import { useState, type ReactNode } from 'react'
|
|
2
|
+
import type { DashboardResponse, DashboardMetrics, DashboardCRDCount } from '../../api/client'
|
|
3
3
|
import { HealthRing } from './HealthRing'
|
|
4
4
|
import {
|
|
5
5
|
AlertTriangle, CheckCircle, XCircle,
|
|
@@ -12,6 +12,9 @@ import { useCapabilitiesContext } from '../../contexts/CapabilitiesContext'
|
|
|
12
12
|
import { MCPSetupDialog } from './MCPSetupDialog'
|
|
13
13
|
import { pluralize, parseContextName } from '@skyhook-io/k8s-ui'
|
|
14
14
|
import { Tooltip } from '../ui/Tooltip'
|
|
15
|
+
import gkeIcon from '../../assets/platform-icons/google_kubernetes_engine.png'
|
|
16
|
+
import eksIcon from '../../assets/platform-icons/aws_eks.png'
|
|
17
|
+
import aksIcon from '../../assets/platform-icons/azure-aks.svg'
|
|
15
18
|
|
|
16
19
|
interface ClusterHealthCardProps {
|
|
17
20
|
health: DashboardResponse['health']
|
|
@@ -20,12 +23,16 @@ interface ClusterHealthCardProps {
|
|
|
20
23
|
metrics: DashboardMetrics | null
|
|
21
24
|
metricsServerAvailable: boolean
|
|
22
25
|
topCRDs?: DashboardCRDCount[] // Loaded lazily, may be undefined
|
|
23
|
-
|
|
26
|
+
issueCount: number
|
|
27
|
+
hasCriticalIssues: boolean
|
|
24
28
|
nodeVersionSkew: DashboardResponse['nodeVersionSkew']
|
|
25
29
|
onNavigateToKind: (kind: string, group?: string) => void
|
|
26
30
|
onNavigateToView: () => void
|
|
27
31
|
onWarningEventsClick?: () => void
|
|
28
|
-
|
|
32
|
+
onIssuesClick?: () => void
|
|
33
|
+
// Freshness/refresh control for the dashboard poll — rendered under the
|
|
34
|
+
// cluster metadata so the overview carries a freshness signal without a band.
|
|
35
|
+
freshness?: ReactNode
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
function getMetricsInstallHint(platform: string): string {
|
|
@@ -49,7 +56,7 @@ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform
|
|
|
49
56
|
content={
|
|
50
57
|
<div className="space-y-1">
|
|
51
58
|
<div className="font-medium">How to fix</div>
|
|
52
|
-
<div>{isPreInstalled ? hint : <>Install by running:<br /><code className="text-[10px] opacity-80">{hint}</code></>}</div>
|
|
59
|
+
<div>{isPreInstalled ? hint : <>Install by running:<br /><code className="inline-code text-[10px] opacity-80">{hint}</code></>}</div>
|
|
53
60
|
</div>
|
|
54
61
|
}
|
|
55
62
|
position="bottom"
|
|
@@ -67,13 +74,13 @@ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform
|
|
|
67
74
|
function getPlatformInfo(platform: string): { name: string; icon: string | null } {
|
|
68
75
|
const platformLower = platform.toLowerCase()
|
|
69
76
|
if (platformLower.includes('gke') || platformLower.includes('google')) {
|
|
70
|
-
return { name: 'Google Kubernetes Engine', icon:
|
|
77
|
+
return { name: 'Google Kubernetes Engine', icon: gkeIcon }
|
|
71
78
|
}
|
|
72
79
|
if (platformLower.includes('eks') || platformLower.includes('amazon') || platformLower.includes('aws')) {
|
|
73
|
-
return { name: 'Amazon EKS', icon:
|
|
80
|
+
return { name: 'Amazon EKS', icon: eksIcon }
|
|
74
81
|
}
|
|
75
82
|
if (platformLower.includes('aks') || platformLower.includes('azure')) {
|
|
76
|
-
return { name: 'Azure Kubernetes Service', icon:
|
|
83
|
+
return { name: 'Azure Kubernetes Service', icon: aksIcon }
|
|
77
84
|
}
|
|
78
85
|
if (platformLower.includes('openshift')) {
|
|
79
86
|
return { name: 'OpenShift', icon: null }
|
|
@@ -93,7 +100,14 @@ function getPlatformInfo(platform: string): { name: string; icon: string | null
|
|
|
93
100
|
if (platformLower.includes('docker')) {
|
|
94
101
|
return { name: 'Docker Desktop', icon: null }
|
|
95
102
|
}
|
|
96
|
-
|
|
103
|
+
// The Go backend returns "generic" for unrecognized platforms — that
|
|
104
|
+
// literal string is no better than the empty case, so fall back to
|
|
105
|
+
// the friendlier "Kubernetes" label for both. Only pass the platform
|
|
106
|
+
// string through when it's actually a recognizable name.
|
|
107
|
+
if (!platform || platformLower === 'generic') {
|
|
108
|
+
return { name: 'Kubernetes', icon: null }
|
|
109
|
+
}
|
|
110
|
+
return { name: platform, icon: null }
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
export function ClusterHealthCard({
|
|
@@ -103,18 +117,32 @@ export function ClusterHealthCard({
|
|
|
103
117
|
metrics,
|
|
104
118
|
metricsServerAvailable,
|
|
105
119
|
topCRDs: _topCRDs,
|
|
106
|
-
|
|
120
|
+
issueCount,
|
|
121
|
+
hasCriticalIssues,
|
|
107
122
|
nodeVersionSkew,
|
|
108
123
|
onNavigateToKind,
|
|
109
124
|
onNavigateToView,
|
|
110
125
|
onWarningEventsClick,
|
|
111
|
-
|
|
126
|
+
onIssuesClick,
|
|
127
|
+
freshness,
|
|
112
128
|
}: ClusterHealthCardProps) {
|
|
113
129
|
void _topCRDs // Reserved for future CRD display
|
|
114
130
|
|
|
115
131
|
const [mcpDialogOpen, setMcpDialogOpen] = useState(false)
|
|
116
|
-
const
|
|
132
|
+
const caps = useCapabilitiesContext()
|
|
133
|
+
// Default to local mode when the backend doesn't ship a `deployment`
|
|
134
|
+
// field (older Radar binaries pre-0.2.2). Local rendering is the safe
|
|
135
|
+
// OSS-shape default — wrong-direction defaults would briefly suppress
|
|
136
|
+
// chrome OSS users expect to see.
|
|
137
|
+
const deployment = caps.deployment ?? { mode: 'local' as const }
|
|
138
|
+
const mcpEnabled = caps.mcpEnabled
|
|
139
|
+
const isCloud = deployment.mode === 'cloud'
|
|
140
|
+
const isInCluster = deployment.mode === 'in-cluster' || deployment.mode === 'cloud'
|
|
117
141
|
const mcpUrl = `${window.location.origin}/mcp`
|
|
142
|
+
// In Cloud, MCP is org-wide and PAT-authed (api.radarhq.io/mcp). The OSS
|
|
143
|
+
// "this binary is your local MCP server" framing is wrong there — Cloud
|
|
144
|
+
// surfaces MCP from the hub Home dashboard instead.
|
|
145
|
+
const showLocalMcpCard = mcpEnabled && !isCloud
|
|
118
146
|
|
|
119
147
|
const restricted = counts.restricted ?? []
|
|
120
148
|
const isRestricted = (kind: string) => restricted.includes(kind)
|
|
@@ -158,13 +186,23 @@ export function ClusterHealthCard({
|
|
|
158
186
|
{ kind: 'cronjobs', label: 'CronJobs', icon: Clock, total: counts.cronJobs.total, subtitle: `${counts.cronJobs.active} active` },
|
|
159
187
|
]
|
|
160
188
|
const platformInfo = getPlatformInfo(cluster.platform)
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
189
|
+
// Headline-name derivation has three branches, in priority order:
|
|
190
|
+
// 1. Local-kubeconfig users get the parsed short clusterName from a
|
|
191
|
+
// string like `gke_koalabackend_us-east1-b_nonprod-cluster-us-east1`
|
|
192
|
+
// (the meaningful tail). Account/region are surfaced separately
|
|
193
|
+
// below as muted metadata, and the raw path is exposed via tooltip
|
|
194
|
+
// on the headline element.
|
|
195
|
+
// 2. In-cluster mode (deployment.mode === 'in-cluster' OR 'cloud')
|
|
196
|
+
// has no meaningful kubeconfig context — bootstrap sets it to
|
|
197
|
+
// the literal "in-cluster" sentinel. Fall back to the platform
|
|
198
|
+
// label ("Google Kubernetes Engine") which IS recognizable.
|
|
199
|
+
// 3. Last resort: the literal cluster.name, or "Cluster".
|
|
200
|
+
// When the card is rendered embedded (cloud mode), the H2 itself is
|
|
201
|
+
// suppressed below — the hub shell already shows the cluster name in
|
|
202
|
+
// its top bar.
|
|
166
203
|
const parsedContext = parseContextName(cluster.name || '')
|
|
167
|
-
const
|
|
204
|
+
const rawHeadline = parsedContext.clusterName || cluster.name || 'Cluster'
|
|
205
|
+
const headlineName = isInCluster ? platformInfo.name : rawHeadline
|
|
168
206
|
|
|
169
207
|
return (
|
|
170
208
|
<div className="rounded-xl bg-theme-surface shadow-theme-sm overflow-hidden">
|
|
@@ -181,31 +219,46 @@ export function ClusterHealthCard({
|
|
|
181
219
|
)}
|
|
182
220
|
<span className="text-xs text-theme-text-secondary truncate">{platformInfo.name}</span>
|
|
183
221
|
</div>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
222
|
+
{/* In Cloud, the hub shell already shows the cluster name in
|
|
223
|
+
its top bar; rendering it again here is redundant and
|
|
224
|
+
makes the card feel like a label rather than content. */}
|
|
225
|
+
{!isCloud && (
|
|
226
|
+
// In-cluster mode's cluster.name is the literal "in-cluster"
|
|
227
|
+
// sentinel, which would leak via the hover tooltip even though
|
|
228
|
+
// the visible text falls back to the platform label. Render no
|
|
229
|
+
// tooltip in that case; local mode keeps it so users can hover
|
|
230
|
+
// to see the full kubeconfig context path.
|
|
231
|
+
<Tooltip content={isInCluster ? '' : cluster.name} wrapperClassName="!block min-w-0">
|
|
232
|
+
<h2 className="text-xl font-semibold text-theme-text-primary truncate leading-tight mb-1.5">
|
|
233
|
+
{headlineName}
|
|
234
|
+
</h2>
|
|
235
|
+
</Tooltip>
|
|
236
|
+
)}
|
|
190
237
|
<div className="flex flex-col gap-0.5 text-xs text-theme-text-tertiary">
|
|
191
238
|
{(parsedContext.account || parsedContext.region) && (
|
|
192
|
-
<
|
|
239
|
+
<Tooltip content={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')} wrapperClassName="min-w-0">
|
|
240
|
+
<span className="truncate font-mono">
|
|
193
241
|
{[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}
|
|
194
242
|
</span>
|
|
243
|
+
</Tooltip>
|
|
195
244
|
)}
|
|
196
245
|
{cluster.version && (
|
|
197
246
|
<span>Kubernetes {cluster.version}</span>
|
|
198
247
|
)}
|
|
199
248
|
<span><span className="font-mono">{counts.namespaces}</span> namespaces</span>
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
249
|
+
{/* Show raw kubeconfig context as muted metadata only when
|
|
250
|
+
it differs from the headline AND we're in local mode
|
|
251
|
+
(in-cluster has no meaningful context name, cloud
|
|
252
|
+
shell already renders the canonical name). */}
|
|
253
|
+
{cluster.name && cluster.name !== headlineName && deployment.mode === 'local' && (
|
|
254
|
+
<Tooltip content={cluster.name}>
|
|
255
|
+
<span className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5">
|
|
205
256
|
{cluster.name}
|
|
206
257
|
</span>
|
|
258
|
+
</Tooltip>
|
|
207
259
|
)}
|
|
208
260
|
</div>
|
|
261
|
+
{freshness && <div className="mt-2">{freshness}</div>}
|
|
209
262
|
{nodeVersionSkew && (
|
|
210
263
|
<Tooltip
|
|
211
264
|
content={
|
|
@@ -229,8 +282,11 @@ export function ClusterHealthCard({
|
|
|
229
282
|
</span>
|
|
230
283
|
</Tooltip>
|
|
231
284
|
)}
|
|
232
|
-
{/* MCP Server indicator
|
|
233
|
-
|
|
285
|
+
{/* MCP Server indicator. OSS-only: in Cloud, MCP discovery
|
|
286
|
+
lives at the hub level (org-wide endpoint, PAT-authed)
|
|
287
|
+
rather than per-cluster, so this localhost/no-auth card
|
|
288
|
+
would mislead a Cloud user. */}
|
|
289
|
+
{showLocalMcpCard && (
|
|
234
290
|
<button
|
|
235
291
|
onClick={() => setMcpDialogOpen(true)}
|
|
236
292
|
className="flex items-center gap-2 mt-3 px-2.5 py-2 bg-purple-500/5 hover:bg-purple-500/10 border border-purple-500/20 rounded-md transition-colors w-full"
|
|
@@ -238,9 +294,11 @@ export function ClusterHealthCard({
|
|
|
238
294
|
<Radio className="w-3.5 h-3.5 text-purple-400 animate-pulse shrink-0" />
|
|
239
295
|
<div className="flex flex-col gap-0.5 min-w-0 flex-1 text-left">
|
|
240
296
|
<span className="text-xs font-medium text-purple-400">MCP Server Live</span>
|
|
241
|
-
<
|
|
297
|
+
<Tooltip content={mcpUrl} wrapperClassName="min-w-0">
|
|
298
|
+
<span className="text-[10px] text-theme-text-tertiary truncate font-mono">
|
|
242
299
|
HTTP · {mcpUrl}
|
|
243
300
|
</span>
|
|
301
|
+
</Tooltip>
|
|
244
302
|
</div>
|
|
245
303
|
<Info className="w-3.5 h-3.5 text-purple-400/60 shrink-0" />
|
|
246
304
|
</button>
|
|
@@ -339,12 +397,14 @@ export function ClusterHealthCard({
|
|
|
339
397
|
<Cpu className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
340
398
|
CPU
|
|
341
399
|
</div>
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
400
|
+
{metricsServerAvailable && (
|
|
401
|
+
<ResourceBar
|
|
402
|
+
label="Used"
|
|
403
|
+
used={formatCPUMillicores(metrics.cpu.usageMillis)}
|
|
404
|
+
total={formatCPUMillicores(metrics.cpu.capacityMillis)}
|
|
405
|
+
percent={metrics.cpu.usagePercent}
|
|
406
|
+
/>
|
|
407
|
+
)}
|
|
348
408
|
<ResourceBar
|
|
349
409
|
label="Requested"
|
|
350
410
|
used={formatCPUMillicores(metrics.cpu.requestsMillis)}
|
|
@@ -359,12 +419,14 @@ export function ClusterHealthCard({
|
|
|
359
419
|
<MemoryStick className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
360
420
|
Memory
|
|
361
421
|
</div>
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
422
|
+
{metricsServerAvailable && (
|
|
423
|
+
<ResourceBar
|
|
424
|
+
label="Used"
|
|
425
|
+
used={formatMemoryMiB(metrics.memory.usageMillis)}
|
|
426
|
+
total={formatMemoryMiB(metrics.memory.capacityMillis)}
|
|
427
|
+
percent={metrics.memory.usagePercent}
|
|
428
|
+
/>
|
|
429
|
+
)}
|
|
368
430
|
<ResourceBar
|
|
369
431
|
label="Requested"
|
|
370
432
|
used={formatMemoryMiB(metrics.memory.requestsMillis)}
|
|
@@ -373,7 +435,7 @@ export function ClusterHealthCard({
|
|
|
373
435
|
/>
|
|
374
436
|
</div>
|
|
375
437
|
)}
|
|
376
|
-
{!
|
|
438
|
+
{!metricsServerAvailable && (
|
|
377
439
|
<MetricsUnavailableHint platform={cluster.platform} metricsServerAvailable={metricsServerAvailable} />
|
|
378
440
|
)}
|
|
379
441
|
</div>
|
|
@@ -387,24 +449,26 @@ export function ClusterHealthCard({
|
|
|
387
449
|
{/* Left column: Warning indicators (aligned with cluster info) */}
|
|
388
450
|
<div className="flex flex-col justify-center gap-1 w-1/4 shrink-0 pr-4 border-r border-theme-border/50">
|
|
389
451
|
{health.warningEvents > 0 && (
|
|
452
|
+
<Tooltip content="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)" wrapperClassName="w-fit">
|
|
390
453
|
<button
|
|
391
454
|
onClick={onWarningEventsClick}
|
|
392
|
-
title="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)"
|
|
393
455
|
className="badge status-degraded w-fit gap-1.5 hover:opacity-80 transition-opacity"
|
|
394
456
|
>
|
|
395
457
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
396
458
|
<span><span className="font-mono">{health.warningEvents}</span> Warning Events</span>
|
|
397
459
|
</button>
|
|
460
|
+
</Tooltip>
|
|
398
461
|
)}
|
|
399
|
-
{
|
|
462
|
+
{issueCount > 0 && (
|
|
463
|
+
<Tooltip content="View grouped live operational issues" wrapperClassName="w-fit">
|
|
400
464
|
<button
|
|
401
|
-
onClick={
|
|
402
|
-
|
|
403
|
-
className="badge status-unhealthy w-fit gap-1.5 hover:opacity-80 transition-opacity"
|
|
465
|
+
onClick={onIssuesClick}
|
|
466
|
+
className={clsx('badge w-fit gap-1.5 hover:opacity-80 transition-opacity', hasCriticalIssues ? 'status-unhealthy' : 'status-degraded')}
|
|
404
467
|
>
|
|
405
468
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
406
|
-
<span>
|
|
469
|
+
<span>{pluralize(issueCount, 'Active Issue')}</span>
|
|
407
470
|
</button>
|
|
471
|
+
</Tooltip>
|
|
408
472
|
)}
|
|
409
473
|
</div>
|
|
410
474
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { OpenCostSummary } from '../../api/client'
|
|
2
2
|
import { useOpenCostSummary } from '../../api/client'
|
|
3
3
|
import { DollarSign } from 'lucide-react'
|
|
4
|
+
import {
|
|
5
|
+
formatCostPerHour,
|
|
6
|
+
formatProjectedDailyRate,
|
|
7
|
+
formatProjectedMonthlyCost,
|
|
8
|
+
formatProjectedMonthlyRate,
|
|
9
|
+
} from '../cost/format'
|
|
4
10
|
|
|
5
11
|
export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
|
|
6
12
|
const { data } = useOpenCostSummary()
|
|
@@ -15,7 +21,6 @@ export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
|
|
|
15
21
|
|
|
16
22
|
function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNavigate?: () => void }) {
|
|
17
23
|
const hourlyCost = data.totalHourlyCost ?? 0
|
|
18
|
-
const monthlyCost = hourlyCost * 730
|
|
19
24
|
const namespaces = data.namespaces ?? []
|
|
20
25
|
const topNamespaces = namespaces.slice(0, 5)
|
|
21
26
|
|
|
@@ -30,10 +35,10 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
30
35
|
<div className="flex flex-col h-full w-full">
|
|
31
36
|
<div className="flex items-center justify-between px-5 py-3 border-b border-theme-border/50">
|
|
32
37
|
<div className="flex items-center gap-2">
|
|
33
|
-
<DollarSign className="w-4 h-4 text-
|
|
34
|
-
<span className="text-xs font-semibold uppercase tracking-wider text-
|
|
38
|
+
<DollarSign className="w-4 h-4 text-accent-text" />
|
|
39
|
+
<span className="text-xs font-semibold uppercase tracking-wider text-accent-text">Cost Insights</span>
|
|
35
40
|
{namespaces.length > 0 && (
|
|
36
|
-
<span className="badge-sm
|
|
41
|
+
<span className="badge-sm border border-theme-border bg-accent-muted text-accent-text">
|
|
37
42
|
{namespaces.length} ns
|
|
38
43
|
</span>
|
|
39
44
|
)}
|
|
@@ -45,13 +50,14 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
45
50
|
<div className="flex items-baseline gap-3 mb-3">
|
|
46
51
|
<div className="flex items-baseline gap-1">
|
|
47
52
|
<span className="text-2xl font-bold text-theme-text-primary tabular-nums">
|
|
48
|
-
{
|
|
53
|
+
{formatProjectedMonthlyCost(hourlyCost)}
|
|
49
54
|
</span>
|
|
50
|
-
<span className="text-xs text-theme-text-tertiary">/
|
|
55
|
+
<span className="text-xs text-theme-text-tertiary">/mo</span>
|
|
51
56
|
</div>
|
|
52
|
-
<div className="flex items-baseline gap-1 text-theme-text-secondary">
|
|
53
|
-
<span className="text-
|
|
54
|
-
<span className="text-[10px] text-theme-text-
|
|
57
|
+
<div className="flex items-baseline gap-1.5 text-theme-text-secondary">
|
|
58
|
+
<span className="text-xs font-medium tabular-nums">{formatProjectedDailyRate(hourlyCost)}</span>
|
|
59
|
+
<span className="text-[10px] text-theme-text-quaternary">·</span>
|
|
60
|
+
<span className="text-xs font-medium tabular-nums">{formatCostPerHour(hourlyCost)}</span>
|
|
55
61
|
</div>
|
|
56
62
|
</div>
|
|
57
63
|
|
|
@@ -63,30 +69,25 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
63
69
|
<div key={ns.name} className="flex items-center gap-2">
|
|
64
70
|
<span className="text-[11px] text-theme-text-secondary truncate w-24 shrink-0">{ns.name}</span>
|
|
65
71
|
<div className="flex-1 h-2 rounded-full overflow-hidden bg-theme-hover">
|
|
66
|
-
<div
|
|
67
|
-
className="h-full rounded-full bg-indigo-500/60"
|
|
68
|
-
style={{ width: `${Math.max(pct, 2)}%` }}
|
|
69
|
-
/>
|
|
72
|
+
<div className="h-full rounded-full bg-indigo-500/60" style={{ width: `${Math.max(pct, 2)}%` }} />
|
|
70
73
|
</div>
|
|
71
|
-
<span className="text-[10px] text-theme-text-tertiary tabular-nums w-
|
|
72
|
-
{
|
|
74
|
+
<span className="text-[10px] text-theme-text-tertiary tabular-nums w-20 text-right shrink-0">
|
|
75
|
+
{formatProjectedMonthlyRate(ns.hourlyCost)}
|
|
73
76
|
</span>
|
|
74
77
|
</div>
|
|
75
78
|
)
|
|
76
79
|
})}
|
|
77
80
|
{namespaces.length > 5 && (
|
|
78
|
-
<span className="text-[10px] text-theme-text-tertiary">
|
|
79
|
-
+{namespaces.length - 5} more namespaces
|
|
80
|
-
</span>
|
|
81
|
+
<span className="text-[10px] text-theme-text-tertiary">+{namespaces.length - 5} more namespaces</span>
|
|
81
82
|
)}
|
|
82
83
|
</div>
|
|
83
84
|
</div>
|
|
84
85
|
|
|
85
86
|
<div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-between">
|
|
86
87
|
<span className="text-[10px] text-theme-text-tertiary">
|
|
87
|
-
{data.currency ?? 'USD'} · {data.window ?? '1h'} window
|
|
88
|
+
{data.currency ?? 'USD'} · projected monthly from {data.window ?? '1h'} window
|
|
88
89
|
</span>
|
|
89
|
-
<span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-
|
|
90
|
+
<span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-accent-text">
|
|
90
91
|
OpenCost
|
|
91
92
|
</span>
|
|
92
93
|
</div>
|
|
@@ -94,19 +95,3 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
94
95
|
</div>
|
|
95
96
|
)
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
function formatCost(value: number): string {
|
|
99
|
-
if (value >= 1000) {
|
|
100
|
-
return `$${(value / 1000).toFixed(1)}k`
|
|
101
|
-
}
|
|
102
|
-
if (value >= 1) {
|
|
103
|
-
return `$${value.toFixed(2)}`
|
|
104
|
-
}
|
|
105
|
-
if (value >= 0.01) {
|
|
106
|
-
return `$${value.toFixed(3)}`
|
|
107
|
-
}
|
|
108
|
-
if (value > 0) {
|
|
109
|
-
return `$${value.toFixed(4)}`
|
|
110
|
-
}
|
|
111
|
-
return '$0.00'
|
|
112
|
-
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { GitBranch, AlertCircle, CheckCircle2 } from 'lucide-react'
|
|
2
|
+
import type { DashboardGitOpsControllers, DashboardGitOpsController } from '../../api/client'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
|
|
5
|
+
interface GitOpsControllersCardProps {
|
|
6
|
+
data: DashboardGitOpsControllers
|
|
7
|
+
onNavigate: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// GitOpsControllersCard surfaces Argo CD / Flux controller pod health
|
|
11
|
+
// on the Home dashboard so an operator can spot "source-controller is
|
|
12
|
+
// CrashLoopBackOff" before drilling into individual GitOps applications
|
|
13
|
+
// and seeing the *symptoms* (apps stuck OutOfSync, sources unfetched).
|
|
14
|
+
//
|
|
15
|
+
// Capability-gated: the parent only renders this card when the backend
|
|
16
|
+
// actually detected controllers in the cluster (DashboardResponse.gitopsControllers
|
|
17
|
+
// is null on non-GitOps clusters). The card itself doesn't have an
|
|
18
|
+
// "empty state" branch — by the time we get here, we have something to show.
|
|
19
|
+
export function GitOpsControllersCard({ data, onNavigate }: GitOpsControllersCardProps) {
|
|
20
|
+
const headerTone =
|
|
21
|
+
data.status === 'crashing'
|
|
22
|
+
? 'text-red-500'
|
|
23
|
+
: data.status === 'degraded'
|
|
24
|
+
? 'text-amber-400'
|
|
25
|
+
: 'text-emerald-500'
|
|
26
|
+
|
|
27
|
+
const headerLabel =
|
|
28
|
+
data.status === 'crashing'
|
|
29
|
+
? 'Controllers crashing'
|
|
30
|
+
: data.status === 'degraded'
|
|
31
|
+
? 'Controllers degraded'
|
|
32
|
+
: 'Controllers healthy'
|
|
33
|
+
|
|
34
|
+
// Group controllers by tool so the card reads as two sections (Argo +
|
|
35
|
+
// Flux) when both are installed. Operators with only one tool see a
|
|
36
|
+
// single-section card without empty placeholders.
|
|
37
|
+
const argo = data.controllers.filter((c) => c.tool === 'argocd')
|
|
38
|
+
const flux = data.controllers.filter((c) => c.tool === 'fluxcd')
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onNavigate}
|
|
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
|
+
>
|
|
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>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
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>
|
|
61
|
+
</div>
|
|
62
|
+
</button>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ControllerSection({ label, controllers }: { label: string; controllers: DashboardGitOpsController[] }) {
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<div className="mb-1 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">{label}</div>
|
|
70
|
+
<div className="flex flex-col gap-1">
|
|
71
|
+
{controllers.map((c) => (
|
|
72
|
+
<ControllerRow key={`${c.tool}-${c.name}`} c={c} />
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function ControllerRow({ c }: { c: DashboardGitOpsController }) {
|
|
80
|
+
// Per-row tone matches the per-controller status. We don't reuse
|
|
81
|
+
// mapHealthToTone because the controller status vocabulary
|
|
82
|
+
// (healthy/degraded/crashing/pending) is different from resource
|
|
83
|
+
// health (Healthy/Degraded/Missing/etc). Mapping inline keeps the
|
|
84
|
+
// intent obvious.
|
|
85
|
+
const tone =
|
|
86
|
+
c.status === 'crashing'
|
|
87
|
+
? 'text-red-500'
|
|
88
|
+
: c.status === 'degraded' || c.status === 'pending'
|
|
89
|
+
? 'text-amber-400'
|
|
90
|
+
: 'text-emerald-500'
|
|
91
|
+
const Icon = c.status === 'crashing' ? AlertCircle : c.status === 'degraded' || c.status === 'pending' ? AlertCircle : CheckCircle2
|
|
92
|
+
return (
|
|
93
|
+
<div className="flex items-center justify-between gap-2 text-[12px]">
|
|
94
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
95
|
+
<Icon className={clsx('h-3 w-3 shrink-0', tone)} />
|
|
96
|
+
<span className="truncate text-theme-text-primary">{c.name}</span>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex shrink-0 items-center gap-1.5 text-[11px] text-theme-text-secondary">
|
|
99
|
+
<span>
|
|
100
|
+
{c.ready}/{c.total} ready
|
|
101
|
+
</span>
|
|
102
|
+
{c.crashReason && (
|
|
103
|
+
<span className={clsx('rounded border px-1 py-px text-[10px] font-medium', 'border-red-500/40 bg-red-500/10 text-red-400')}>
|
|
104
|
+
{c.crashReason}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -88,11 +88,13 @@ export function HelmSummary({ data, onNavigate }: HelmSummaryProps) {
|
|
|
88
88
|
<span className="text-xs font-medium text-theme-text-secondary">
|
|
89
89
|
{data.errorCode === 'unconfigured' ? 'Helm not configured' : 'Helm unavailable'}
|
|
90
90
|
</span>
|
|
91
|
-
<
|
|
91
|
+
<Tooltip content={data.error} wrapperClassName="!block mt-1 min-w-0 text-center">
|
|
92
|
+
<span className="text-[11px] text-center px-2 truncate max-w-full">
|
|
92
93
|
{data.errorCode === 'unconfigured'
|
|
93
94
|
? 'Set rbac.helm=true in the Radar Helm chart values.'
|
|
94
95
|
: data.error}
|
|
95
96
|
</span>
|
|
97
|
+
</Tooltip>
|
|
96
98
|
</div>
|
|
97
99
|
) : !data.releases || data.releases.length === 0 ? (
|
|
98
100
|
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|