@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,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,
|
|
@@ -23,12 +23,16 @@ interface ClusterHealthCardProps {
|
|
|
23
23
|
metrics: DashboardMetrics | null
|
|
24
24
|
metricsServerAvailable: boolean
|
|
25
25
|
topCRDs?: DashboardCRDCount[] // Loaded lazily, may be undefined
|
|
26
|
-
|
|
26
|
+
issueCount: number
|
|
27
|
+
hasCriticalIssues: boolean
|
|
27
28
|
nodeVersionSkew: DashboardResponse['nodeVersionSkew']
|
|
28
29
|
onNavigateToKind: (kind: string, group?: string) => void
|
|
29
30
|
onNavigateToView: () => void
|
|
30
31
|
onWarningEventsClick?: () => void
|
|
31
|
-
|
|
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
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
function getMetricsInstallHint(platform: string): string {
|
|
@@ -52,7 +56,7 @@ function MetricsUnavailableHint({ platform, metricsServerAvailable }: { platform
|
|
|
52
56
|
content={
|
|
53
57
|
<div className="space-y-1">
|
|
54
58
|
<div className="font-medium">How to fix</div>
|
|
55
|
-
<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>
|
|
56
60
|
</div>
|
|
57
61
|
}
|
|
58
62
|
position="bottom"
|
|
@@ -113,12 +117,14 @@ export function ClusterHealthCard({
|
|
|
113
117
|
metrics,
|
|
114
118
|
metricsServerAvailable,
|
|
115
119
|
topCRDs: _topCRDs,
|
|
116
|
-
|
|
120
|
+
issueCount,
|
|
121
|
+
hasCriticalIssues,
|
|
117
122
|
nodeVersionSkew,
|
|
118
123
|
onNavigateToKind,
|
|
119
124
|
onNavigateToView,
|
|
120
125
|
onWarningEventsClick,
|
|
121
|
-
|
|
126
|
+
onIssuesClick,
|
|
127
|
+
freshness,
|
|
122
128
|
}: ClusterHealthCardProps) {
|
|
123
129
|
void _topCRDs // Reserved for future CRD display
|
|
124
130
|
|
|
@@ -217,24 +223,24 @@ export function ClusterHealthCard({
|
|
|
217
223
|
its top bar; rendering it again here is redundant and
|
|
218
224
|
makes the card feel like a label rather than content. */}
|
|
219
225
|
{!isCloud && (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// kubeconfig context path.
|
|
228
|
-
title={isInCluster ? undefined : cluster.name}
|
|
229
|
-
>
|
|
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">
|
|
230
233
|
{headlineName}
|
|
231
234
|
</h2>
|
|
235
|
+
</Tooltip>
|
|
232
236
|
)}
|
|
233
237
|
<div className="flex flex-col gap-0.5 text-xs text-theme-text-tertiary">
|
|
234
238
|
{(parsedContext.account || parsedContext.region) && (
|
|
235
|
-
<
|
|
239
|
+
<Tooltip content={[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')} wrapperClassName="min-w-0">
|
|
240
|
+
<span className="truncate font-mono">
|
|
236
241
|
{[parsedContext.account, parsedContext.region].filter(Boolean).join(' · ')}
|
|
237
242
|
</span>
|
|
243
|
+
</Tooltip>
|
|
238
244
|
)}
|
|
239
245
|
{cluster.version && (
|
|
240
246
|
<span>Kubernetes {cluster.version}</span>
|
|
@@ -245,14 +251,14 @@ export function ClusterHealthCard({
|
|
|
245
251
|
(in-cluster has no meaningful context name, cloud
|
|
246
252
|
shell already renders the canonical name). */}
|
|
247
253
|
{cluster.name && cluster.name !== headlineName && deployment.mode === 'local' && (
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
title={cluster.name}
|
|
251
|
-
>
|
|
254
|
+
<Tooltip content={cluster.name}>
|
|
255
|
+
<span className="font-mono text-[10px] text-theme-text-disabled break-all leading-snug pt-0.5">
|
|
252
256
|
{cluster.name}
|
|
253
257
|
</span>
|
|
258
|
+
</Tooltip>
|
|
254
259
|
)}
|
|
255
260
|
</div>
|
|
261
|
+
{freshness && <div className="mt-2">{freshness}</div>}
|
|
256
262
|
{nodeVersionSkew && (
|
|
257
263
|
<Tooltip
|
|
258
264
|
content={
|
|
@@ -288,9 +294,11 @@ export function ClusterHealthCard({
|
|
|
288
294
|
<Radio className="w-3.5 h-3.5 text-purple-400 animate-pulse shrink-0" />
|
|
289
295
|
<div className="flex flex-col gap-0.5 min-w-0 flex-1 text-left">
|
|
290
296
|
<span className="text-xs font-medium text-purple-400">MCP Server Live</span>
|
|
291
|
-
<
|
|
297
|
+
<Tooltip content={mcpUrl} wrapperClassName="min-w-0">
|
|
298
|
+
<span className="text-[10px] text-theme-text-tertiary truncate font-mono">
|
|
292
299
|
HTTP · {mcpUrl}
|
|
293
300
|
</span>
|
|
301
|
+
</Tooltip>
|
|
294
302
|
</div>
|
|
295
303
|
<Info className="w-3.5 h-3.5 text-purple-400/60 shrink-0" />
|
|
296
304
|
</button>
|
|
@@ -389,12 +397,14 @@ export function ClusterHealthCard({
|
|
|
389
397
|
<Cpu className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
390
398
|
CPU
|
|
391
399
|
</div>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
)}
|
|
398
408
|
<ResourceBar
|
|
399
409
|
label="Requested"
|
|
400
410
|
used={formatCPUMillicores(metrics.cpu.requestsMillis)}
|
|
@@ -409,12 +419,14 @@ export function ClusterHealthCard({
|
|
|
409
419
|
<MemoryStick className="w-3.5 h-3.5 text-theme-text-tertiary" />
|
|
410
420
|
Memory
|
|
411
421
|
</div>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
)}
|
|
418
430
|
<ResourceBar
|
|
419
431
|
label="Requested"
|
|
420
432
|
used={formatMemoryMiB(metrics.memory.requestsMillis)}
|
|
@@ -423,7 +435,7 @@ export function ClusterHealthCard({
|
|
|
423
435
|
/>
|
|
424
436
|
</div>
|
|
425
437
|
)}
|
|
426
|
-
{!
|
|
438
|
+
{!metricsServerAvailable && (
|
|
427
439
|
<MetricsUnavailableHint platform={cluster.platform} metricsServerAvailable={metricsServerAvailable} />
|
|
428
440
|
)}
|
|
429
441
|
</div>
|
|
@@ -437,24 +449,26 @@ export function ClusterHealthCard({
|
|
|
437
449
|
{/* Left column: Warning indicators (aligned with cluster info) */}
|
|
438
450
|
<div className="flex flex-col justify-center gap-1 w-1/4 shrink-0 pr-4 border-r border-theme-border/50">
|
|
439
451
|
{health.warningEvents > 0 && (
|
|
452
|
+
<Tooltip content="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)" wrapperClassName="w-fit">
|
|
440
453
|
<button
|
|
441
454
|
onClick={onWarningEventsClick}
|
|
442
|
-
title="Native Kubernetes Warning events (e.g., ImagePullBackOff, FailedScheduling)"
|
|
443
455
|
className="badge status-degraded w-fit gap-1.5 hover:opacity-80 transition-opacity"
|
|
444
456
|
>
|
|
445
457
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
446
458
|
<span><span className="font-mono">{health.warningEvents}</span> Warning Events</span>
|
|
447
459
|
</button>
|
|
460
|
+
</Tooltip>
|
|
448
461
|
)}
|
|
449
|
-
{
|
|
462
|
+
{issueCount > 0 && (
|
|
463
|
+
<Tooltip content="View grouped live operational issues" wrapperClassName="w-fit">
|
|
450
464
|
<button
|
|
451
|
-
onClick={
|
|
452
|
-
|
|
453
|
-
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')}
|
|
454
467
|
>
|
|
455
468
|
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
|
|
456
|
-
<span>
|
|
469
|
+
<span>{pluralize(issueCount, 'Active Issue')}</span>
|
|
457
470
|
</button>
|
|
471
|
+
</Tooltip>
|
|
458
472
|
)}
|
|
459
473
|
</div>
|
|
460
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">
|