@skyhook-io/radar-app 1.8.6 → 1.8.8

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 (109) hide show
  1. package/README.md +7 -1
  2. package/package.json +12 -10
  3. package/src/App.tsx +278 -230
  4. package/src/RadarApp.tsx +91 -20
  5. package/src/api/client.argoResourceSync.test.ts +69 -0
  6. package/src/api/client.delta.test.ts +89 -0
  7. package/src/api/client.deltaSync.test.ts +216 -0
  8. package/src/api/client.rightsizing.test.ts +32 -0
  9. package/src/api/client.ts +1462 -246
  10. package/src/api/diagnose.ts +288 -0
  11. package/src/api/timelineSource.test.ts +217 -0
  12. package/src/api/timelineSource.ts +582 -0
  13. package/src/components/ConnectionErrorView.tsx +174 -70
  14. package/src/components/ContextSwitcher.tsx +13 -5
  15. package/src/components/applications/ApplicationsView.tsx +775 -80
  16. package/src/components/audit/AuditSettingsDialog.tsx +2 -2
  17. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  18. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  19. package/src/components/cost/CostTrendChart.tsx +103 -72
  20. package/src/components/cost/CostView.test.ts +12 -0
  21. package/src/components/cost/CostView.tsx +494 -229
  22. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  23. package/src/components/cost/CostViewTabs.tsx +40 -0
  24. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  25. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  26. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  27. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  28. package/src/components/cost/cloud-console.test.ts +39 -0
  29. package/src/components/cost/cloud-console.ts +81 -0
  30. package/src/components/cost/errors.ts +8 -0
  31. package/src/components/cost/format.test.ts +27 -0
  32. package/src/components/cost/format.ts +46 -0
  33. package/src/components/cost/kinds.ts +5 -0
  34. package/src/components/curl/ServiceCurlButton.tsx +2 -2
  35. package/src/components/diagnose/AISettings.tsx +116 -0
  36. package/src/components/diagnose/DiagnoseContext.tsx +491 -0
  37. package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
  38. package/src/components/diagnose/Home.tsx +163 -0
  39. package/src/components/diagnose/InvestigationView.tsx +604 -0
  40. package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
  41. package/src/components/diagnose/launch.ts +65 -0
  42. package/src/components/diagnose/parts.tsx +1689 -0
  43. package/src/components/dock/BottomDock.tsx +2 -3
  44. package/src/components/dock/WorkloadLogsTab.tsx +21 -5
  45. package/src/components/execution/BatchExecutionView.test.ts +170 -0
  46. package/src/components/execution/BatchExecutionView.tsx +1329 -0
  47. package/src/components/execution/batch-run-actions.test.ts +48 -0
  48. package/src/components/execution/batch-run-actions.ts +24 -0
  49. package/src/components/execution/batch-timeline.test.ts +57 -0
  50. package/src/components/execution/batch-timeline.ts +46 -0
  51. package/src/components/execution/execution-definition.test.ts +208 -0
  52. package/src/components/execution/execution-definition.ts +245 -0
  53. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  54. package/src/components/gitops/GitOpsView.tsx +81 -14
  55. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  56. package/src/components/helm/ChartBrowser.tsx +2 -3
  57. package/src/components/helm/HelmCompareRoute.tsx +1 -2
  58. package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
  59. package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
  60. package/src/components/helm/HelmView.tsx +2 -3
  61. package/src/components/helm/InstallWizard.tsx +3 -5
  62. package/src/components/helm/ManifestDiffViewer.tsx +1 -31
  63. package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
  64. package/src/components/helm/ValuesDiffPreview.tsx +17 -7
  65. package/src/components/home/CostCard.tsx +21 -36
  66. package/src/components/home/HomeView.tsx +17 -15
  67. package/src/components/home/MCPSetupDialog.tsx +1 -1
  68. package/src/components/home/mcpToolCatalog.ts +2 -2
  69. package/src/components/issues/IssuesPane.tsx +9 -1
  70. package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
  71. package/src/components/portforward/PortForwardButton.tsx +2 -2
  72. package/src/components/portforward/PortForwardManager.tsx +19 -14
  73. package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
  74. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  75. package/src/components/resource/RightsizingStrip.tsx +320 -127
  76. package/src/components/resources/ImageFilesystemModal.tsx +2 -2
  77. package/src/components/resources/PodFilesystemModal.tsx +2 -3
  78. package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
  79. package/src/components/resources/ResourcesView.tsx +13 -4
  80. package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
  81. package/src/components/resources/renderers/index.ts +1 -0
  82. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  83. package/src/components/rightsizing/copy.test.ts +56 -0
  84. package/src/components/rightsizing/model.test.ts +227 -0
  85. package/src/components/rightsizing/model.ts +158 -0
  86. package/src/components/rightsizing/presentation.test.ts +104 -0
  87. package/src/components/rightsizing/presentation.ts +94 -0
  88. package/src/components/settings/MyPermissionsDialog.tsx +66 -116
  89. package/src/components/settings/SettingsDialog.tsx +1290 -283
  90. package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
  91. package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
  92. package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
  93. package/src/components/timeline/TimelineList.tsx +81 -13
  94. package/src/components/timeline/TimelineView.tsx +842 -26
  95. package/src/components/timeline/TimelineView.urlparams.test.ts +335 -0
  96. package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
  97. package/src/components/traffic/TrafficView.tsx +8 -4
  98. package/src/components/ui/CommandPalette.tsx +2 -2
  99. package/src/components/workload/WorkloadView.tsx +912 -245
  100. package/src/context/ConnectionContext.tsx +109 -11
  101. package/src/context/DiagnoseCustomization.tsx +42 -0
  102. package/src/context/TimelineSource.tsx +50 -0
  103. package/src/hooks/useClusterLoadState.ts +73 -0
  104. package/src/hooks/useEventSource.ts +6 -0
  105. package/src/index.css +162 -1
  106. package/src/index.ts +12 -0
  107. package/src/main.tsx +1 -1
  108. package/src/types/clusterLoadState.ts +33 -0
  109. package/src/utils/navigation.ts +10 -0
@@ -644,7 +644,7 @@ export function PortForwardPanel() {
644
644
  onMouseEnter={onPanelHoverEnter}
645
645
  onMouseLeave={onPanelHoverLeave}
646
646
  className={clsx(
647
- 'fixed z-[51] w-80',
647
+ 'fixed z-[51] w-[26rem] max-w-[calc(100vw-2rem)]',
648
648
  'transition-opacity duration-150 ease-out',
649
649
  isPanelOpen
650
650
  ? 'opacity-100 pointer-events-auto'
@@ -726,14 +726,16 @@ export function PortForwardPanel() {
726
726
  </div>
727
727
  ) : (
728
728
  <div className="divide-y divide-theme-border">
729
- {activeSessions.map((session) => (
730
- <div
731
- key={session.id}
732
- className={clsx(
733
- 'p-3 space-y-1',
734
- session.status === 'error' ? 'bg-red-500/10' : 'hover:bg-theme-elevated'
735
- )}
736
- >
729
+ {activeSessions.map((session) => {
730
+ const resourceName = session.serviceName || session.podName
731
+ return (
732
+ <div
733
+ key={session.id}
734
+ className={clsx(
735
+ 'p-3 space-y-1',
736
+ session.status === 'error' ? 'bg-red-500/10' : 'hover:bg-theme-elevated'
737
+ )}
738
+ >
737
739
  {/* Row 1: status dot + name | stop button */}
738
740
  <div className="flex items-start justify-between gap-2">
739
741
  <div className="flex items-start gap-2 min-w-0 flex-1">
@@ -743,9 +745,11 @@ export function PortForwardPanel() {
743
745
  session.status === 'running' ? 'bg-green-500' : 'bg-red-500'
744
746
  )}
745
747
  />
746
- <span className="text-sm text-theme-text-primary font-medium break-all line-clamp-2">
747
- {session.serviceName || session.podName}
748
- </span>
748
+ <Tooltip content={resourceName} delay={300} position="bottom" disabled={!isPanelOpen} wrapperClassName="min-w-0 flex-1">
749
+ <span className="block min-w-0 truncate text-sm font-medium text-theme-text-primary">
750
+ {resourceName}
751
+ </span>
752
+ </Tooltip>
749
753
  {session.status === 'error' && (
750
754
  <span className={clsx('badge-sm shrink-0', SEVERITY_BADGE.error)}>Failed</span>
751
755
  )}
@@ -916,8 +920,9 @@ export function PortForwardPanel() {
916
920
  </div>
917
921
  </div>
918
922
  )}
919
- </div>
920
- ))}
923
+ </div>
924
+ )
925
+ })}
921
926
  </div>
922
927
  )}
923
928
  </div>
@@ -1,5 +1,5 @@
1
1
  import { useMemo, useState } from 'react'
2
- import { BarChart3, ChevronDown, ChevronRight, Loader2, Wifi, WifiOff } from 'lucide-react'
2
+ import { Loader2, Wifi, WifiOff } from 'lucide-react'
3
3
  import {
4
4
  AreaChart,
5
5
  SeriesLegend,
@@ -11,11 +11,9 @@ import {
11
11
  usePrometheusStatus,
12
12
  usePrometheusConnect,
13
13
  usePrometheusResourceMetrics,
14
- usePrometheusRightsizing,
15
14
  useAutoPromConnect,
16
15
  type PrometheusMetricCategory,
17
16
  type PrometheusTimeRange,
18
- type RightsizingTone,
19
17
  } from '../../api/client'
20
18
  import {
21
19
  MetricsSummary,
@@ -57,7 +55,6 @@ export function PrometheusChartsGrid({
57
55
  const showRestartLane = isSupported && kind !== 'Node'
58
56
 
59
57
  const [timeRange, setTimeRange] = useState<PrometheusTimeRange>('1h')
60
- const [diskExpanded, setDiskExpanded] = useState(false)
61
58
 
62
59
  const categories = kind === 'Node' ? NODE_CATEGORIES : WORKLOAD_CATEGORIES
63
60
 
@@ -112,7 +109,6 @@ export function PrometheusChartsGrid({
112
109
  const findCategory = (key: PrometheusMetricCategory): CategoryDef | undefined =>
113
110
  categories.find(c => c.key === key)
114
111
 
115
- // Disk I/O is collapsed by default — niche metric.
116
112
  const primaryCats: { def: CategoryDef; refLines?: ReferenceLine[] }[] = []
117
113
  const cpu = findCategory('cpu')
118
114
  if (cpu) primaryCats.push({ def: cpu, refLines: cpuRefLines })
@@ -125,19 +121,16 @@ export function PrometheusChartsGrid({
125
121
  if (tx) primaryCats.push({ def: tx })
126
122
  }
127
123
  const disk = findCategory('filesystem')
124
+ const chartPanels = disk ? [...primaryCats, { def: disk }] : primaryCats
128
125
 
129
126
  return (
130
127
  <div className="flex flex-col h-full overflow-auto">
131
- <div className="shrink-0 flex items-center justify-between px-4 py-2.5 border-b border-theme-border bg-theme-surface/50">
132
- <div className="flex items-center gap-2 text-sm font-medium text-theme-text-secondary">
133
- <BarChart3 className="w-4 h-4 text-theme-text-tertiary" />
134
- Metrics
135
- <WorkloadHealthBadge kind={kind} namespace={namespace} name={name} />
136
- </div>
128
+ <div className="flex shrink-0 justify-end px-4 pt-3">
137
129
  <select
130
+ aria-label="Metrics time range"
138
131
  value={timeRange}
139
132
  onChange={e => setTimeRange(e.target.value as PrometheusTimeRange)}
140
- className="px-2 py-1 text-xs rounded-md bg-theme-elevated border border-theme-border text-theme-text-secondary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
133
+ className="rounded-md border border-theme-border bg-theme-elevated px-2 py-1 text-xs text-theme-text-secondary shadow-theme-sm focus:outline-none focus:ring-1 focus:ring-accent/50"
141
134
  >
142
135
  {TIME_RANGES.map(tr => (
143
136
  <option key={tr.value} value={tr.value}>{tr.label}</option>
@@ -154,7 +147,7 @@ export function PrometheusChartsGrid({
154
147
  )}
155
148
 
156
149
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3 p-4">
157
- {primaryCats.map(({ def, refLines }) => (
150
+ {chartPanels.map(({ def, refLines }) => (
158
151
  <MetricsPanel
159
152
  key={def.key}
160
153
  category={def}
@@ -166,30 +159,6 @@ export function PrometheusChartsGrid({
166
159
  />
167
160
  ))}
168
161
  </div>
169
-
170
- {disk && (
171
- <div className="px-4 pb-4">
172
- <button
173
- type="button"
174
- onClick={() => setDiskExpanded(v => !v)}
175
- className="flex items-center gap-1.5 text-xs font-medium text-theme-text-tertiary hover:text-theme-text-secondary py-1"
176
- >
177
- {diskExpanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
178
- Disk I/O
179
- </button>
180
- {diskExpanded && (
181
- <div className="mt-2">
182
- <MetricsPanel
183
- category={disk}
184
- kind={kind}
185
- namespace={namespace}
186
- name={name}
187
- timeRange={timeRange}
188
- />
189
- </div>
190
- )}
191
- </div>
192
- )}
193
162
  </div>
194
163
  )
195
164
  }
@@ -269,49 +238,6 @@ function SaturationChip({ ratio, against }: { ratio: number; against: 'limit' |
269
238
  return <span className={`badge badge-sm ${SEVERITY_BADGE[tone]}`}>{label}</span>
270
239
  }
271
240
 
272
- // Severity ordering for rightsizing tones. Aligned with `Tone` constants in
273
- // `internal/prometheus/rightsizing.go` — adding a new tone there triggers a
274
- // TypeScript exhaustiveness error on the Record key set here.
275
- const TONE_RANK: Record<RightsizingTone, number> = {
276
- ok: 0,
277
- info: 1,
278
- warning: 2,
279
- alert: 3,
280
- critical: 4,
281
- }
282
-
283
- function WorkloadHealthBadge({ kind, namespace, name }: { kind: string; namespace: string; name: string }) {
284
- const supported = kind === 'Deployment' || kind === 'StatefulSet' || kind === 'DaemonSet'
285
- const { data, error } = usePrometheusRightsizing(kind, namespace, name, supported)
286
- if (!supported) return null
287
- // Surface a neutral "Health unknown" pill when the rightsizing endpoint
288
- // errors — otherwise an actually-throttled or OOM-risk workload would
289
- // silently render as fine while we have no signal to display.
290
- if (error && !data) {
291
- const msg = error instanceof Error ? error.message : String(error)
292
- return (
293
- <Tooltip content={`Health check failed: ${msg}`}>
294
- <span className={`badge badge-sm ${SEVERITY_BADGE.neutral}`}>Health unknown</span>
295
- </Tooltip>
296
- )
297
- }
298
- if (!data?.sampleAvailable || data.rows.length === 0) return null
299
-
300
- const worst = data.rows.reduce<RightsizingTone>(
301
- (acc, r) => (TONE_RANK[r.tone] > TONE_RANK[acc] ? r.tone : acc),
302
- 'ok',
303
- )
304
- // Skip the chip for the steady-state tones to avoid badge-blindness — we
305
- // only want to draw the eye when there's something to address.
306
- if (worst === 'ok' || worst === 'info') return null
307
-
308
- const { label, severity }: { label: string; severity: Severity } =
309
- worst === 'critical' ? { label: 'OOM risk', severity: 'error' } :
310
- worst === 'alert' ? { label: 'CPU throttling', severity: 'alert' } :
311
- /* warning */ { label: 'Needs review', severity: 'warning' }
312
- return <span className={`badge badge-sm ${SEVERITY_BADGE[severity]}`}>{label}</span>
313
- }
314
-
315
241
  function PanelLoading() {
316
242
  return (
317
243
  <div className="flex items-center justify-center h-full min-h-[160px] text-theme-text-tertiary text-xs">
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { RightsizingRow } from '../../api/client'
3
+ import {
4
+ getRightsizingExplanation,
5
+ getRightsizingPresentation,
6
+ RIGHTSIZING_METHODOLOGY,
7
+ RIGHTSIZING_DOCS_URL,
8
+ RIGHTSIZING_SUMMARY,
9
+ } from './RightsizingStrip'
10
+
11
+ const row = (overrides: Partial<RightsizingRow> = {}): RightsizingRow => ({
12
+ container: 'server',
13
+ resource: 'cpu',
14
+ fit: 'balanced',
15
+ confidence: 'high',
16
+ sampleCount: 2016,
17
+ expectedSamples: 2016,
18
+ coverage: 1,
19
+ hpaManaged: false,
20
+ hpaEvidenceAvailable: true,
21
+ oomEvidenceAvailable: true,
22
+ throttleAvailable: true,
23
+ ...overrides,
24
+ })
25
+
26
+ describe('rightsizing presentation', () => {
27
+ it('keeps fit, confidence, and runtime risk as separate concepts', () => {
28
+ expect(getRightsizingPresentation('oversized')).toEqual({
29
+ label: 'Oversized',
30
+ severity: 'info',
31
+ })
32
+ expect(getRightsizingPresentation('under_requested')).toEqual({
33
+ label: 'Under-requested',
34
+ severity: 'warning',
35
+ })
36
+ expect(getRightsizingPresentation('insufficient_history')).toEqual({
37
+ label: 'Insufficient history',
38
+ severity: 'neutral',
39
+ })
40
+ })
41
+
42
+ it('labels query failures independently from insufficient history', () => {
43
+ expect(getRightsizingPresentation('insufficient_history', 'usage query failed')).toEqual({
44
+ label: 'Query failed',
45
+ severity: 'error',
46
+ })
47
+ })
48
+
49
+ it('explains why recommendations are withheld without inventing a zero-risk result', () => {
50
+ expect(getRightsizingExplanation(row({ recommendationReason: 'hpa_managed' }))).toContain(
51
+ 'HPA manages cpu',
52
+ )
53
+ expect(
54
+ getRightsizingExplanation(row({ resource: 'memory', recommendationReason: 'oom_evidence' })),
55
+ ).toContain('OOM evidence')
56
+ expect(
57
+ getRightsizingExplanation(row({ recommendationReason: 'hpa_evidence_unavailable' })),
58
+ ).toContain('could not verify HPA')
59
+ expect(
60
+ getRightsizingExplanation(
61
+ row({
62
+ resource: 'memory',
63
+ recommendationReason: 'oom_evidence_unavailable',
64
+ }),
65
+ ),
66
+ ).toContain('could not verify recent OOM')
67
+ expect(getRightsizingExplanation(row({ throttleAvailable: false }))).toContain(
68
+ 'throttling metrics are unavailable',
69
+ )
70
+ })
71
+
72
+ it('separates the demand target from a conservative reduction step', () => {
73
+ const explanation = getRightsizingExplanation(
74
+ row({
75
+ fit: 'oversized',
76
+ currentRequest: '1',
77
+ recommendedRequest: '500m',
78
+ calculatedRequest: '10m',
79
+ reductionLimited: true,
80
+ bursty: true,
81
+ peak: { name: 'P99', value: 0.2, formatted: '200m' },
82
+ }),
83
+ )
84
+ expect(explanation).toContain('Demand-based target: 10m')
85
+ expect(explanation).toContain('conservative next step')
86
+ expect(explanation).toContain('CPU P99 reached 200m')
87
+ })
88
+
89
+ it('does not revive the misleading efficiency vocabulary', () => {
90
+ const copy = [
91
+ getRightsizingPresentation('balanced').label,
92
+ getRightsizingPresentation('oversized').label,
93
+ getRightsizingExplanation(row({ recommendationReason: 'request_within_fit_range' })),
94
+ ]
95
+ .join(' ')
96
+ .toLowerCase()
97
+ expect(copy).not.toContain('efficien')
98
+ expect(copy).not.toContain('waste')
99
+ })
100
+
101
+ it('sets the workload-level scope and methodology without promising savings or changes', () => {
102
+ expect(RIGHTSIZING_SUMMARY).toContain('this workload')
103
+ expect(RIGHTSIZING_SUMMARY).toContain('not a savings estimate or automatic change')
104
+ expect(RIGHTSIZING_METHODOLOGY).toContain('CPU P95 and memory maximum')
105
+ expect(RIGHTSIZING_METHODOLOGY).toContain('Reductions are staged')
106
+ expect(RIGHTSIZING_METHODOLOGY).toContain('Radar does not change requests')
107
+ expect(RIGHTSIZING_DOCS_URL).toContain('/features/rightsizing')
108
+ })
109
+ })