@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.
Files changed (177) hide show
  1. package/README.md +7 -1
  2. package/package.json +33 -25
  3. package/src/App.tsx +1449 -382
  4. package/src/RadarApp.tsx +132 -19
  5. package/src/api/apiResources.ts +1 -1
  6. package/src/api/client.argoResourceSync.test.ts +69 -0
  7. package/src/api/client.delta.test.ts +89 -0
  8. package/src/api/client.deltaSync.test.ts +216 -0
  9. package/src/api/client.metrics.test.ts +106 -0
  10. package/src/api/client.rightsizing.test.ts +32 -0
  11. package/src/api/client.ts +2730 -271
  12. package/src/api/client.yaml.test.ts +45 -0
  13. package/src/api/diagnose.ts +289 -0
  14. package/src/api/quotas.ts +16 -0
  15. package/src/api/rbac.ts +57 -0
  16. package/src/api/timelineSource.test.ts +217 -0
  17. package/src/api/timelineSource.ts +582 -0
  18. package/src/components/ConnectionErrorView.tsx +186 -70
  19. package/src/components/ContextSwitcher.tsx +63 -18
  20. package/src/components/DebugOverlay.tsx +5 -3
  21. package/src/components/NamespaceSwitcher.tsx +41 -0
  22. package/src/components/UserMenu.tsx +69 -21
  23. package/src/components/applications/ApplicationsView.tsx +936 -0
  24. package/src/components/audit/AuditSettingsDialog.tsx +79 -17
  25. package/src/components/audit/AuditView.tsx +65 -62
  26. package/src/components/compare/CompareViewRoute.tsx +124 -0
  27. package/src/components/compare/useCompareCandidates.ts +27 -0
  28. package/src/components/compare/useCompareLauncher.tsx +79 -0
  29. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  30. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  31. package/src/components/cost/CostTrendChart.tsx +106 -75
  32. package/src/components/cost/CostView.test.ts +12 -0
  33. package/src/components/cost/CostView.tsx +507 -223
  34. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  35. package/src/components/cost/CostViewTabs.tsx +40 -0
  36. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  37. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  38. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  39. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  40. package/src/components/cost/cloud-console.test.ts +39 -0
  41. package/src/components/cost/cloud-console.ts +81 -0
  42. package/src/components/cost/errors.ts +8 -0
  43. package/src/components/cost/format.test.ts +27 -0
  44. package/src/components/cost/format.ts +46 -0
  45. package/src/components/cost/kinds.ts +5 -0
  46. package/src/components/curl/ServiceCurlButton.tsx +445 -0
  47. package/src/components/diagnose/AISettings.tsx +147 -0
  48. package/src/components/diagnose/DiagnoseContext.tsx +495 -0
  49. package/src/components/diagnose/DiagnoseSurface.tsx +394 -0
  50. package/src/components/diagnose/Home.tsx +163 -0
  51. package/src/components/diagnose/InvestigationView.tsx +622 -0
  52. package/src/components/diagnose/LocalDiagnoseAction.tsx +162 -0
  53. package/src/components/diagnose/launch.ts +65 -0
  54. package/src/components/diagnose/parts.tsx +1756 -0
  55. package/src/components/dock/BottomDock.tsx +2 -3
  56. package/src/components/dock/DockContext.tsx +1 -0
  57. package/src/components/dock/TerminalTab.tsx +1 -1
  58. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  59. package/src/components/dock/index.ts +1 -1
  60. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  61. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  62. package/src/components/execution/batch-run-actions.test.ts +48 -0
  63. package/src/components/execution/batch-run-actions.ts +24 -0
  64. package/src/components/execution/batch-timeline.test.ts +57 -0
  65. package/src/components/execution/batch-timeline.ts +46 -0
  66. package/src/components/execution/execution-definition.test.ts +208 -0
  67. package/src/components/execution/execution-definition.ts +245 -0
  68. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  69. package/src/components/gitops/GitOpsView.tsx +1042 -0
  70. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  71. package/src/components/helm/ChartBrowser.tsx +87 -31
  72. package/src/components/helm/HelmCompareRoute.tsx +1341 -0
  73. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  74. package/src/components/helm/HelmReleaseDrawer.tsx +1073 -102
  75. package/src/components/helm/HelmView.tsx +237 -96
  76. package/src/components/helm/InstallWizard.tsx +94 -38
  77. package/src/components/helm/ManifestDiffViewer.tsx +8 -27
  78. package/src/components/helm/OwnedResources.tsx +34 -59
  79. package/src/components/helm/RevisionHistory.tsx +52 -3
  80. package/src/components/helm/RoleGatedPanel.tsx +3 -3
  81. package/src/components/helm/TrackChartSourceDialog.tsx +185 -0
  82. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  83. package/src/components/helm/ValuesViewer.tsx +49 -53
  84. package/src/components/helm/helm-utils.ts +4 -0
  85. package/src/components/home/ActivitySummary.tsx +4 -1
  86. package/src/components/home/ClusterHealthCard.tsx +56 -42
  87. package/src/components/home/CostCard.tsx +21 -36
  88. package/src/components/home/GitOpsControllersCard.tsx +110 -0
  89. package/src/components/home/HelmSummary.tsx +3 -1
  90. package/src/components/home/HomeView.tsx +339 -105
  91. package/src/components/home/MCPSetupDialog.tsx +29 -87
  92. package/src/components/home/TrafficSummary.tsx +2 -2
  93. package/src/components/home/mcpToolCatalog.ts +333 -0
  94. package/src/components/issues/IssuesPane.tsx +151 -0
  95. package/src/components/logs/LogsViewer.tsx +4 -1
  96. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  97. package/src/components/logs/WorkloadLogsViewer.tsx +4 -1
  98. package/src/components/nav/PrimaryNavRail.tsx +285 -0
  99. package/src/components/portforward/PortForwardButton.tsx +118 -47
  100. package/src/components/portforward/PortForwardManager.tsx +253 -131
  101. package/src/components/resource/HPACharts.tsx +237 -0
  102. package/src/components/resource/PVCUsageBar.tsx +59 -0
  103. package/src/components/resource/PrometheusCharts.tsx +160 -584
  104. package/src/components/resource/PrometheusChartsGrid.tsx +270 -0
  105. package/src/components/resource/RestartChart.tsx +133 -0
  106. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  107. package/src/components/resource/RightsizingStrip.tsx +363 -0
  108. package/src/components/resource-drawer/ResourceDrawer.tsx +3 -1
  109. package/src/components/resources/CompositeRenderer.tsx +101 -0
  110. package/src/components/resources/ImageFilesystemModal.tsx +19 -12
  111. package/src/components/resources/PodFilesystemModal.tsx +6 -5
  112. package/src/components/resources/ResourceDetailDrawer.tsx +13 -3
  113. package/src/components/resources/ResourcesView.tsx +194 -17
  114. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  115. package/src/components/resources/renderers/HPARenderer.tsx +20 -1
  116. package/src/components/resources/renderers/NamespaceRenderer.tsx +31 -0
  117. package/src/components/resources/renderers/NodeRenderer.tsx +10 -4
  118. package/src/components/resources/renderers/PVCRenderer.tsx +19 -1
  119. package/src/components/resources/renderers/PodRenderer.tsx +30 -6
  120. package/src/components/resources/renderers/RoleBindingRenderer.tsx +45 -1
  121. package/src/components/resources/renderers/RoleRenderer.tsx +27 -1
  122. package/src/components/resources/renderers/ServiceAccountRenderer.tsx +28 -1
  123. package/src/components/resources/renderers/ServiceRenderer.tsx +81 -8
  124. package/src/components/resources/renderers/WorkloadRenderer.tsx +51 -4
  125. package/src/components/resources/renderers/index.ts +2 -0
  126. package/src/components/resources/resource-utils.ts +2 -1
  127. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  128. package/src/components/rightsizing/copy.test.ts +56 -0
  129. package/src/components/rightsizing/model.test.ts +227 -0
  130. package/src/components/rightsizing/model.ts +158 -0
  131. package/src/components/rightsizing/presentation.test.ts +104 -0
  132. package/src/components/rightsizing/presentation.ts +94 -0
  133. package/src/components/settings/MyPermissionsDialog.tsx +241 -0
  134. package/src/components/settings/SettingsDialog.tsx +1505 -165
  135. package/src/components/shared/CreateResourceDialog.tsx +9 -2
  136. package/src/components/shared/LargeClusterNamespacePicker.tsx +3 -3
  137. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  138. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  139. package/src/components/timeline/TimelineList.tsx +86 -13
  140. package/src/components/timeline/TimelineSwimlanes.tsx +9 -1299
  141. package/src/components/timeline/TimelineView.tsx +873 -24
  142. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  143. package/src/components/traffic/TrafficFilterSidebar.tsx +10 -45
  144. package/src/components/traffic/TrafficFlowList.tsx +29 -15
  145. package/src/components/traffic/TrafficGraph.tsx +42 -24
  146. package/src/components/traffic/TrafficView.tsx +32 -19
  147. package/src/components/ui/CommandPalette.tsx +8 -215
  148. package/src/components/ui/DiagnosticsOverlay.tsx +219 -9
  149. package/src/components/ui/Markdown.tsx +3 -3
  150. package/src/components/ui/Omnibar.tsx +602 -0
  151. package/src/components/ui/RadarOmnibar.tsx +52 -0
  152. package/src/components/ui/SearchSyntaxHelp.tsx +89 -0
  153. package/src/components/ui/ShortcutHelpOverlay.tsx +3 -2
  154. package/src/components/ui/UpdateNotification.tsx +48 -36
  155. package/src/components/ui/command-items.ts +178 -0
  156. package/src/components/workload/WorkloadView.tsx +1342 -158
  157. package/src/context/ConnectionContext.tsx +146 -21
  158. package/src/context/DiagnoseCustomization.tsx +93 -0
  159. package/src/context/NavCustomization.tsx +75 -0
  160. package/src/context/TimelineSource.tsx +50 -0
  161. package/src/contexts/CapabilitiesContext.tsx +32 -8
  162. package/src/filter/FilterLocationBridge.tsx +30 -0
  163. package/src/hooks/useClusterLoadState.ts +73 -0
  164. package/src/hooks/useDocumentTitle.ts +25 -0
  165. package/src/hooks/useEventSource.ts +6 -0
  166. package/src/hooks/useKeyboardShortcuts.tsx +1 -0
  167. package/src/hooks/useMediaQuery.ts +21 -0
  168. package/src/hooks/useNavRailPinned.ts +46 -0
  169. package/src/hooks/useRecentResources.ts +49 -0
  170. package/src/index.css +162 -1
  171. package/src/index.ts +73 -1
  172. package/src/main.tsx +7 -5
  173. package/src/types/clusterLoadState.ts +33 -0
  174. package/src/types.ts +2 -0
  175. package/src/utils/auditBadges.ts +53 -0
  176. package/src/utils/navigation.ts +64 -1
  177. package/src/components/ui/NamespaceSelector.tsx +0 -436
@@ -0,0 +1,21 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { MemoryRouter } from 'react-router-dom'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { CostViewTabs } from './CostViewTabs'
5
+
6
+ describe('CostViewTabs', () => {
7
+ it('preserves namespace and rightsizing filters between cost views', () => {
8
+ const html = renderToStaticMarkup(
9
+ <MemoryRouter
10
+ initialEntries={['/cost/rightsizing?namespaces=shop%2Cprod&rfClass=review&rfQ=api']}
11
+ >
12
+ <CostViewTabs />
13
+ </MemoryRouter>,
14
+ )
15
+
16
+ expect(html).toContain('href="/cost?namespaces=shop%2Cprod&amp;rfClass=review&amp;rfQ=api"')
17
+ expect(html).toContain(
18
+ 'href="/cost/rightsizing?namespaces=shop%2Cprod&amp;rfClass=review&amp;rfQ=api"',
19
+ )
20
+ })
21
+ })
@@ -0,0 +1,40 @@
1
+ import { Link, useLocation, type To } from 'react-router-dom'
2
+ import { clsx } from 'clsx'
3
+
4
+ export function CostViewTabs() {
5
+ const { pathname, search } = useLocation()
6
+ const rightsizing = pathname.startsWith('/cost/rightsizing')
7
+ return (
8
+ <div
9
+ className="flex items-center gap-1 border-b border-theme-border"
10
+ role="tablist"
11
+ aria-label="Cost views"
12
+ >
13
+ <CostTab to={{ pathname: '/cost', search }} active={!rightsizing}>
14
+ Overview
15
+ </CostTab>
16
+ <CostTab to={{ pathname: '/cost/rightsizing', search }} active={rightsizing}>
17
+ Rightsizing
18
+ </CostTab>
19
+ </div>
20
+ )
21
+ }
22
+
23
+ function CostTab({ to, active, children }: { to: To; active: boolean; children: React.ReactNode }) {
24
+ return (
25
+ <Link
26
+ to={to}
27
+ role="tab"
28
+ aria-selected={active}
29
+ className={clsx(
30
+ 'relative px-3 py-2 text-sm font-medium transition-colors',
31
+ active
32
+ ? 'text-theme-text-primary'
33
+ : 'text-theme-text-tertiary hover:text-theme-text-secondary',
34
+ )}
35
+ >
36
+ {children}
37
+ {active && <span className="absolute inset-x-2 bottom-0 h-0.5 rounded-full bg-accent" />}
38
+ </Link>
39
+ )
40
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ALLOCATION_USE_TOOLTIP, formatAllocatedUse } from './CurrentAllocationUse'
3
+
4
+ describe('formatAllocatedUse', () => {
5
+ it('keeps unavailable, zero-allocation, and zero-use states distinct', () => {
6
+ expect(formatAllocatedUse(1, 0, false)).toBe('Usage unavailable')
7
+ expect(formatAllocatedUse(0, 0, false)).toBe('No allocation')
8
+ expect(formatAllocatedUse(1, 0, true)).toBe('0% allocated use')
9
+ expect(formatAllocatedUse(1, 0, true, false)).toBe('—')
10
+ })
11
+
12
+ it('caps bursty or noisy ratios at the allocation ceiling', () => {
13
+ expect(formatAllocatedUse(1, 42.4, true)).toBe('42% allocated use')
14
+ expect(formatAllocatedUse(1, 120, true)).toBe('100% allocated use')
15
+ })
16
+
17
+ it('does not frame the measure as a grade or recoverable savings', () => {
18
+ expect(ALLOCATION_USE_TOOLTIP.toLowerCase()).not.toMatch(/efficient|waste|unused capacity/)
19
+ expect(ALLOCATION_USE_TOOLTIP).toContain('not request headroom')
20
+ })
21
+ })
@@ -0,0 +1,126 @@
1
+ import { clsx } from 'clsx'
2
+ import { HelpCircle } from 'lucide-react'
3
+ import { Tooltip } from '../ui/Tooltip'
4
+ import { formatCostPerHour, formatProjectedMonthlyRate } from './format'
5
+
6
+ interface CurrentAllocationUseProps {
7
+ dataAvailable: boolean
8
+ cpuCost: number
9
+ memoryCost: number
10
+ hourlyCost: number
11
+ cpuAllocationUse: number
12
+ memoryAllocationUse: number
13
+ cpuUsageAvailable: boolean
14
+ memoryUsageAvailable: boolean
15
+ scopeNote?: string
16
+ }
17
+
18
+ export const ALLOCATION_USE_TOOLTIP =
19
+ 'OpenCost allocation uses the greater of requested or observed CPU and memory. The percentages compare observed use with that allocated amount; they are not request headroom or a right-sizing recommendation.'
20
+
21
+ export function formatAllocatedUse(
22
+ allocationCost: number,
23
+ allocationUse: number,
24
+ usageAvailable: boolean,
25
+ dataAvailable = true,
26
+ ): string {
27
+ if (!dataAvailable) return '—'
28
+ if (allocationCost <= 0) return 'No allocation'
29
+ if (!usageAvailable) return 'Usage unavailable'
30
+ const percentage = Math.min(100, Math.max(0, Math.round(allocationUse)))
31
+ return `${percentage}% allocated use`
32
+ }
33
+
34
+ export function CurrentAllocationUse({
35
+ dataAvailable,
36
+ cpuCost,
37
+ memoryCost,
38
+ hourlyCost,
39
+ cpuAllocationUse,
40
+ memoryAllocationUse,
41
+ cpuUsageAvailable,
42
+ memoryUsageAvailable,
43
+ scopeNote,
44
+ }: CurrentAllocationUseProps) {
45
+ const splitTotal = cpuCost + memoryCost
46
+ const cpuPct = splitTotal > 0 ? (cpuCost / splitTotal) * 100 : 0
47
+ const memoryPct = splitTotal > 0 ? (memoryCost / splitTotal) * 100 : 0
48
+
49
+ return (
50
+ <section className="rounded-lg border border-theme-border bg-theme-surface/50 p-4">
51
+ <div className="mb-3 flex items-center justify-between gap-3">
52
+ <div>
53
+ <div className="flex items-center gap-1.5">
54
+ <div className="text-sm font-semibold text-theme-text-primary">Current allocation and use</div>
55
+ <Tooltip content={ALLOCATION_USE_TOOLTIP} className="max-w-[320px] whitespace-normal text-left" delay={150}>
56
+ <HelpCircle className="h-3.5 w-3.5 cursor-help text-theme-text-tertiary transition-colors hover:text-theme-text-secondary" />
57
+ </Tooltip>
58
+ </div>
59
+ <div className="text-xs text-theme-text-tertiary">
60
+ Allocation and CPU use: 1h average · Memory use: current
61
+ {scopeNote ? ` · ${scopeNote}` : ''}
62
+ </div>
63
+ </div>
64
+ <div className="text-right">
65
+ <div className="text-sm font-medium text-theme-text-primary tabular-nums">
66
+ {dataAvailable ? formatProjectedMonthlyRate(hourlyCost) : '—'}
67
+ </div>
68
+ {dataAvailable && (
69
+ <div className="text-[10px] text-theme-text-tertiary tabular-nums">
70
+ {formatCostPerHour(hourlyCost)} current rate
71
+ </div>
72
+ )}
73
+ </div>
74
+ </div>
75
+ <div className="h-3 overflow-hidden rounded-full bg-theme-hover">
76
+ <div className="flex h-full">
77
+ {dataAvailable && (
78
+ <>
79
+ <div className="h-full bg-accent" style={{ width: `${cpuPct}%` }} />
80
+ <div className="h-full bg-amber-500" style={{ width: `${memoryPct}%` }} />
81
+ </>
82
+ )}
83
+ </div>
84
+ </div>
85
+ <div className="mt-3 grid gap-2 text-xs text-theme-text-secondary sm:grid-cols-2">
86
+ <AllocationUseItem
87
+ colorClass="bg-accent"
88
+ label="CPU"
89
+ allocation={dataAvailable ? formatProjectedMonthlyRate(cpuCost) : '—'}
90
+ use={formatAllocatedUse(cpuCost, cpuAllocationUse, cpuUsageAvailable, dataAvailable)}
91
+ />
92
+ <AllocationUseItem
93
+ colorClass="bg-amber-500"
94
+ label="Memory"
95
+ allocation={dataAvailable ? formatProjectedMonthlyRate(memoryCost) : '—'}
96
+ use={formatAllocatedUse(memoryCost, memoryAllocationUse, memoryUsageAvailable, dataAvailable)}
97
+ />
98
+ </div>
99
+ </section>
100
+ )
101
+ }
102
+
103
+ function AllocationUseItem({
104
+ colorClass,
105
+ label,
106
+ allocation,
107
+ use,
108
+ }: {
109
+ colorClass: string
110
+ label: string
111
+ allocation: string
112
+ use: string
113
+ }) {
114
+ return (
115
+ <div className="flex items-center justify-between gap-3 rounded-md bg-theme-base px-2.5 py-2">
116
+ <span className="flex items-center gap-2">
117
+ <span className={clsx('h-2.5 w-2.5 rounded-sm', colorClass)} />
118
+ {label}
119
+ </span>
120
+ <span className="text-right">
121
+ <span className="block font-medium tabular-nums text-theme-text-primary">{allocation}</span>
122
+ <span className="block text-[10px] tabular-nums text-theme-text-tertiary">{use}</span>
123
+ </span>
124
+ </div>
125
+ )
126
+ }
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getWorkloadCostState } from './WorkloadCostTab'
3
+ import { ApiError, type OpenCostWorkloadDetailResponse, type OpenCostWorkloadTrendResponse } from '../../api/client'
4
+
5
+ describe('getWorkloadCostState', () => {
6
+ it('treats scaled-to-zero as a valid zero state', () => {
7
+ const current: OpenCostWorkloadDetailResponse = {
8
+ available: true,
9
+ namespace: 'default',
10
+ kind: 'Deployment',
11
+ name: 'checkout',
12
+ current: {
13
+ name: 'checkout',
14
+ kind: 'Deployment',
15
+ hourlyCost: 0,
16
+ cpuCost: 0,
17
+ memoryCost: 0,
18
+ replicas: 0,
19
+ cpuUsageAvailable: false,
20
+ memoryUsageAvailable: false,
21
+ cpuAllocationUse: 0,
22
+ memoryAllocationUse: 0,
23
+ },
24
+ }
25
+ const trend: OpenCostWorkloadTrendResponse = {
26
+ available: false,
27
+ reason: 'no_metrics',
28
+ namespace: 'default',
29
+ kind: 'Deployment',
30
+ name: 'checkout',
31
+ range: '24h',
32
+ }
33
+
34
+ expect(getWorkloadCostState(current, trend, false)).toBe('zero')
35
+ })
36
+
37
+ it('keeps current cost visible when only historical owner metrics are missing', () => {
38
+ const current: OpenCostWorkloadDetailResponse = {
39
+ available: true,
40
+ namespace: 'default',
41
+ kind: 'Deployment',
42
+ name: 'checkout',
43
+ current: {
44
+ name: 'checkout',
45
+ kind: 'Deployment',
46
+ hourlyCost: 0.2,
47
+ cpuCost: 0.12,
48
+ memoryCost: 0.08,
49
+ replicas: 2,
50
+ cpuUsageAvailable: true,
51
+ memoryUsageAvailable: true,
52
+ cpuAllocationUse: 25,
53
+ memoryAllocationUse: 25,
54
+ },
55
+ }
56
+ const trend: OpenCostWorkloadTrendResponse = {
57
+ available: false,
58
+ reason: 'no_metrics',
59
+ namespace: 'default',
60
+ kind: 'Deployment',
61
+ name: 'checkout',
62
+ range: '24h',
63
+ }
64
+
65
+ expect(getWorkloadCostState(current, trend, false)).toBe('partial_missing_history')
66
+ })
67
+
68
+ it('keeps current cost visible while historical owner metrics are still loading', () => {
69
+ const current: OpenCostWorkloadDetailResponse = {
70
+ available: true,
71
+ namespace: 'default',
72
+ kind: 'Deployment',
73
+ name: 'checkout',
74
+ current: {
75
+ name: 'checkout',
76
+ kind: 'Deployment',
77
+ hourlyCost: 0.2,
78
+ cpuCost: 0.12,
79
+ memoryCost: 0.08,
80
+ replicas: 2,
81
+ cpuUsageAvailable: true,
82
+ memoryUsageAvailable: true,
83
+ cpuAllocationUse: 25,
84
+ memoryAllocationUse: 25,
85
+ },
86
+ }
87
+
88
+ expect(getWorkloadCostState(current, undefined, { trendLoading: true })).toBe('data')
89
+ })
90
+
91
+ it('uses historical data when current metrics are absent but history exists', () => {
92
+ const current: OpenCostWorkloadDetailResponse = {
93
+ available: false,
94
+ reason: 'no_metrics',
95
+ namespace: 'default',
96
+ kind: 'Deployment',
97
+ name: 'checkout',
98
+ }
99
+ const trend: OpenCostWorkloadTrendResponse = {
100
+ available: true,
101
+ namespace: 'default',
102
+ kind: 'Deployment',
103
+ name: 'checkout',
104
+ range: '7d',
105
+ dataPoints: [
106
+ { timestamp: 1700000000, value: 0.1 },
107
+ { timestamp: 1700003600, value: 0.2 },
108
+ ],
109
+ }
110
+
111
+ expect(getWorkloadCostState(current, trend, false)).toBe('partial_missing_current')
112
+ })
113
+
114
+ it('separates load failures from absent workload metrics', () => {
115
+ expect(getWorkloadCostState(undefined, undefined, { currentError: true })).toBe('load_error')
116
+ })
117
+
118
+ it('surfaces workload access and existence failures', () => {
119
+ const current: OpenCostWorkloadDetailResponse = {
120
+ available: false,
121
+ reason: 'access_denied',
122
+ namespace: 'prod',
123
+ kind: 'Deployment',
124
+ name: 'checkout',
125
+ }
126
+
127
+ const missing: OpenCostWorkloadTrendResponse = {
128
+ available: false,
129
+ reason: 'not_found',
130
+ namespace: 'prod',
131
+ kind: 'Deployment',
132
+ name: 'checkout',
133
+ range: '24h',
134
+ }
135
+
136
+ expect(getWorkloadCostState(current, undefined, false)).toBe('access_denied')
137
+ expect(getWorkloadCostState(undefined, missing, false)).toBe('not_found')
138
+ expect(getWorkloadCostState(undefined, undefined, { currentError: new ApiError('denied', 403) })).toBe('access_denied')
139
+ expect(getWorkloadCostState(undefined, undefined, { trendError: new ApiError('missing', 404) })).toBe('not_found')
140
+ })
141
+
142
+ it('shows Prometheus discovery as soon as one query reports it', () => {
143
+ const current: OpenCostWorkloadDetailResponse = {
144
+ available: false,
145
+ reason: 'no_prometheus',
146
+ namespace: 'default',
147
+ kind: 'Deployment',
148
+ name: 'checkout',
149
+ }
150
+
151
+ expect(getWorkloadCostState(current, undefined, { trendLoading: true })).toBe('no_prometheus')
152
+ })
153
+ })