@skyhook-io/radar-app 1.8.7 → 1.8.9

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 (56) hide show
  1. package/package.json +4 -4
  2. package/src/App.tsx +69 -61
  3. package/src/RadarApp.tsx +15 -1
  4. package/src/api/apiResources.ts +1 -1
  5. package/src/api/client.argoResourceSync.test.ts +69 -0
  6. package/src/api/client.rightsizing.test.ts +32 -0
  7. package/src/api/client.ts +1222 -244
  8. package/src/api/timelineSource.ts +4 -2
  9. package/src/components/applications/ApplicationsView.tsx +613 -219
  10. package/src/components/cost/ApplicationCostTab.test.ts +204 -0
  11. package/src/components/cost/ApplicationCostTab.tsx +571 -0
  12. package/src/components/cost/CostTrendChart.tsx +103 -72
  13. package/src/components/cost/CostView.test.ts +12 -0
  14. package/src/components/cost/CostView.tsx +494 -229
  15. package/src/components/cost/CostViewTabs.test.tsx +21 -0
  16. package/src/components/cost/CostViewTabs.tsx +40 -0
  17. package/src/components/cost/CurrentAllocationUse.test.ts +21 -0
  18. package/src/components/cost/CurrentAllocationUse.tsx +126 -0
  19. package/src/components/cost/WorkloadCostTab.test.ts +153 -0
  20. package/src/components/cost/WorkloadCostTab.tsx +372 -0
  21. package/src/components/cost/cloud-console.test.ts +39 -0
  22. package/src/components/cost/cloud-console.ts +81 -0
  23. package/src/components/cost/errors.ts +8 -0
  24. package/src/components/cost/format.test.ts +27 -0
  25. package/src/components/cost/format.ts +46 -0
  26. package/src/components/cost/kinds.ts +5 -0
  27. package/src/components/diagnose/AISettings.tsx +7 -12
  28. package/src/components/diagnose/DiagnoseContext.tsx +1 -0
  29. package/src/components/diagnose/DiagnoseSurface.tsx +3 -0
  30. package/src/components/diagnose/InvestigationView.tsx +16 -3
  31. package/src/components/diagnose/parts.tsx +140 -73
  32. package/src/components/gitops/ArgoResourceDiffLoader.tsx +23 -0
  33. package/src/components/gitops/GitOpsView.tsx +81 -14
  34. package/src/components/gitops/RevisionMetaChip.tsx +63 -0
  35. package/src/components/helm/HelmCompareRoute.tsx +1 -2
  36. package/src/components/helm/ManifestDiffViewer.tsx +1 -31
  37. package/src/components/helm/ValuesDiffPreview.tsx +2 -3
  38. package/src/components/home/CostCard.tsx +21 -36
  39. package/src/components/resource/RightsizingStrip.test.ts +109 -0
  40. package/src/components/resource/RightsizingStrip.tsx +319 -123
  41. package/src/components/rightsizing/RightsizingScanView.tsx +938 -0
  42. package/src/components/rightsizing/copy.test.ts +56 -0
  43. package/src/components/rightsizing/model.test.ts +227 -0
  44. package/src/components/rightsizing/model.ts +158 -0
  45. package/src/components/rightsizing/presentation.test.ts +104 -0
  46. package/src/components/rightsizing/presentation.ts +94 -0
  47. package/src/components/settings/MyPermissionsDialog.tsx +66 -116
  48. package/src/components/settings/SettingsDialog.tsx +1268 -318
  49. package/src/components/timeline/TimelineList.tsx +35 -8
  50. package/src/components/timeline/TimelineView.tsx +156 -26
  51. package/src/components/timeline/TimelineView.urlparams.test.ts +43 -2
  52. package/src/components/workload/WorkloadView.tsx +711 -328
  53. package/src/context/DiagnoseCustomization.tsx +36 -2
  54. package/src/index.css +5 -1
  55. package/src/index.ts +4 -1
  56. package/src/main.tsx +1 -1
@@ -1,6 +1,5 @@
1
1
  import { X, GitCompare } from 'lucide-react'
2
- import { PaneLoader } from '@skyhook-io/k8s-ui'
3
- import { clsx } from 'clsx'
2
+ import { PaneLoader, DiffLine, hasDiffBodyChange } from '@skyhook-io/k8s-ui'
4
3
 
5
4
  interface ManifestDiffViewerProps {
6
5
  diff: string
@@ -68,32 +67,3 @@ export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onCl
68
67
  </div>
69
68
  )
70
69
  }
71
-
72
- export function hasDiffBodyChange(diff: string): boolean {
73
- return diff.split('\n').some((line) => {
74
- if (!line || line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) {
75
- return false
76
- }
77
- return line.startsWith('+') || line.startsWith('-')
78
- })
79
- }
80
-
81
- export function DiffLine({ line }: { line: string }) {
82
- const isAddition = line.startsWith('+') && !line.startsWith('+++')
83
- const isRemoval = line.startsWith('-') && !line.startsWith('---')
84
- const isHeader = line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')
85
-
86
- return (
87
- <div
88
- className={clsx(
89
- 'whitespace-pre',
90
- isAddition && 'bg-green-500/10 text-green-700 dark:text-green-400',
91
- isRemoval && 'bg-red-500/10 text-red-700 dark:text-red-400',
92
- isHeader && 'text-theme-text-tertiary font-bold',
93
- !isAddition && !isRemoval && !isHeader && 'text-theme-text-secondary'
94
- )}
95
- >
96
- {line || ' '}
97
- </div>
98
- )
99
- }
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react'
2
2
  import { createPortal } from 'react-dom'
3
3
  import { X, Play, Loader2, FileText, AlertTriangle } from 'lucide-react'
4
4
  import { clsx } from 'clsx'
5
+ import { classifyDiffLine } from '@skyhook-io/k8s-ui'
5
6
  import type { ValuesPreviewResponse } from '../../types'
6
7
 
7
8
  interface ValuesDiffPreviewProps {
@@ -169,9 +170,7 @@ interface DiffLineProps {
169
170
  }
170
171
 
171
172
  function DiffLine({ line, lineNumber }: DiffLineProps) {
172
- const isAddition = line.startsWith('+') && !line.startsWith('+++')
173
- const isDeletion = line.startsWith('-') && !line.startsWith('---')
174
- const isHeader = line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')
173
+ const { isAddition, isRemoval: isDeletion, isHeader } = classifyDiffLine(line)
175
174
 
176
175
  return (
177
176
  <div
@@ -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-indigo-500" />
34
- <span className="text-xs font-semibold uppercase tracking-wider text-indigo-500">Cost Insights</span>
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 bg-indigo-100 text-indigo-700 border-indigo-300 dark:bg-indigo-950/50 dark:text-indigo-400 dark:border-indigo-700/40">
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
- {formatCost(hourlyCost)}
53
+ {formatProjectedMonthlyCost(hourlyCost)}
49
54
  </span>
50
- <span className="text-xs text-theme-text-tertiary">/hr</span>
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-sm font-medium tabular-nums">~{formatCost(monthlyCost)}</span>
54
- <span className="text-[10px] text-theme-text-tertiary">/mo</span>
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-14 text-right shrink-0">
72
- {formatCost(ns.hourlyCost)}/h
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'} &middot; {data.window ?? '1h'} window
88
+ {data.currency ?? 'USD'} &middot; 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-indigo-500">
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,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
+ })