@skyhook-io/radar-app 1.12.3 → 1.13.0
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/package.json +5 -5
- package/src/App.tsx +1 -0
- package/src/api/client.ts +38 -6
- package/src/components/cost/ApplicationCostTab.test.ts +45 -0
- package/src/components/cost/ApplicationCostTab.tsx +115 -64
- package/src/components/cost/CostTrendChart.test.ts +65 -0
- package/src/components/cost/CostTrendChart.tsx +107 -17
- package/src/components/cost/CostView.test.ts +36 -1
- package/src/components/cost/CostView.tsx +133 -51
- package/src/components/cost/CurrentAllocationUse.tsx +8 -4
- package/src/components/cost/WorkloadCostTab.test.ts +50 -0
- package/src/components/cost/WorkloadCostTab.tsx +109 -61
- package/src/components/cost/source.test.ts +33 -0
- package/src/components/cost/source.ts +100 -0
- package/src/components/diagnose/parts.test.tsx +12 -4
- package/src/components/diagnose/parts.tsx +19 -15
- package/src/components/home/CostCard.tsx +3 -2
- package/src/components/rightsizing/RightsizingScanView.tsx +36 -12
- package/src/components/rightsizing/copy.test.ts +19 -0
- package/src/components/settings/SettingsDialog.tsx +666 -103
- package/src/components/settings/settings-state.test.ts +42 -0
- package/src/components/settings/settings-state.ts +39 -0
- package/src/index.css +11 -1
|
@@ -33,6 +33,9 @@ import {
|
|
|
33
33
|
import { StatusDot } from "@skyhook-io/k8s-ui";
|
|
34
34
|
import { Markdown } from "../ui/Markdown";
|
|
35
35
|
|
|
36
|
+
const CURSOR_FULL_LOCAL_WARNING =
|
|
37
|
+
"Radar passes Cursor --force, which auto-approves its built-in tools and every MCP server it loads, including your global servers. Cursor’s sandbox does not reliably confine those tools to Radar’s temporary workspace.";
|
|
38
|
+
|
|
36
39
|
// Segmented two-or-more-way selector — shared shape for the agent and execution
|
|
37
40
|
// profile pickers.
|
|
38
41
|
function Segmented<T extends string | boolean>({
|
|
@@ -309,9 +312,11 @@ export function AgentControls({
|
|
|
309
312
|
configured tools and MCP servers. Radar cannot constrain that
|
|
310
313
|
external tooling; it may access local files or the network
|
|
311
314
|
and may be able to change your cluster.{" "}
|
|
312
|
-
{
|
|
313
|
-
?
|
|
314
|
-
:
|
|
315
|
+
{isCursor
|
|
316
|
+
? CURSOR_FULL_LOCAL_WARNING
|
|
317
|
+
: isClaude
|
|
318
|
+
? "Claude uses the permissions from your setup; Radar does not override them."
|
|
319
|
+
: "Radar still enables the agent CLI’s own sandbox, but that sandbox does not constrain external MCP servers."}{" "}
|
|
315
320
|
Choose this only when you need that setup.
|
|
316
321
|
</span>
|
|
317
322
|
</div>
|
|
@@ -336,10 +341,14 @@ export function AgentControls({
|
|
|
336
341
|
Radar must use this agent's normal setup. Radar cannot
|
|
337
342
|
constrain its external tools or MCP servers; they may access
|
|
338
343
|
local files or the network and may be able to change your
|
|
339
|
-
cluster.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
cluster. {isCursor ? (
|
|
345
|
+
CURSOR_FULL_LOCAL_WARNING
|
|
346
|
+
) : (
|
|
347
|
+
<>
|
|
348
|
+
Radar still enables the agent CLI's own sandbox, but that
|
|
349
|
+
sandbox does not constrain external MCP servers.
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
343
352
|
</span>
|
|
344
353
|
</div>
|
|
345
354
|
)}
|
|
@@ -832,7 +841,9 @@ export function ConsentCard({
|
|
|
832
841
|
MCP servers. They may access local files or the network and may
|
|
833
842
|
be able to change your cluster.
|
|
834
843
|
</>,
|
|
835
|
-
agent === "
|
|
844
|
+
agent === "cursor-agent" ? (
|
|
845
|
+
CURSOR_FULL_LOCAL_WARNING
|
|
846
|
+
) : agent === "claude" ? (
|
|
836
847
|
<>
|
|
837
848
|
Claude uses the permissions from your setup; Radar does not
|
|
838
849
|
override them.
|
|
@@ -841,13 +852,6 @@ export function ConsentCard({
|
|
|
841
852
|
<>
|
|
842
853
|
Radar still enables the agent CLI's own sandbox, but that
|
|
843
854
|
sandbox does not constrain external MCP servers.
|
|
844
|
-
{agent === "cursor-agent" && (
|
|
845
|
-
<>
|
|
846
|
-
{" "}
|
|
847
|
-
Cursor always loads your global MCP servers; Radar cannot
|
|
848
|
-
exclude them.
|
|
849
|
-
</>
|
|
850
|
-
)}
|
|
851
855
|
</>
|
|
852
856
|
),
|
|
853
857
|
]
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
formatProjectedMonthlyCost,
|
|
9
9
|
formatProjectedMonthlyRate,
|
|
10
10
|
} from '../cost/format'
|
|
11
|
+
import { costFreshnessLabel } from '../cost/source'
|
|
11
12
|
|
|
12
13
|
export function CostCard({ onNavigate }: { onNavigate?: () => void }) {
|
|
13
14
|
const { data } = useOpenCostSummary()
|
|
@@ -87,13 +88,13 @@ function CostCardContent({ data, onNavigate }: { data: OpenCostSummary; onNaviga
|
|
|
87
88
|
|
|
88
89
|
<div className="px-4 py-1.5 border-t border-theme-border/50 flex items-center justify-between">
|
|
89
90
|
<span className="text-[10px] text-theme-text-tertiary">
|
|
90
|
-
{currency} · projected
|
|
91
|
+
{currency} · projected from {costFreshnessLabel(data.source, data.window, data.dataThrough)}
|
|
91
92
|
{currency !== DEFAULT_COST_CURRENCY && (
|
|
92
93
|
<> · no conversion</>
|
|
93
94
|
)}
|
|
94
95
|
</span>
|
|
95
96
|
<span className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider text-accent-text">
|
|
96
|
-
OpenCost
|
|
97
|
+
{data.source === 'kubecost' ? 'Kubecost' : 'OpenCost'}
|
|
97
98
|
</span>
|
|
98
99
|
</div>
|
|
99
100
|
</div>
|
|
@@ -33,11 +33,17 @@ import {
|
|
|
33
33
|
type ResourceSignal,
|
|
34
34
|
type RightsizingActionTone,
|
|
35
35
|
} from './presentation'
|
|
36
|
+
import { useNavCustomization } from '../../context/NavCustomization'
|
|
36
37
|
|
|
37
38
|
export const RIGHTSIZING_SCAN_DESCRIPTION =
|
|
38
39
|
'Find CPU and memory requests to increase, reduce, or review. Radar never changes them.'
|
|
39
40
|
export const RIGHTSIZING_SCAN_METHODOLOGY =
|
|
40
41
|
'Based on 7 days of history: CPU P95 and memory maximum, plus 15% headroom. Memory reductions require verifiable restart history.'
|
|
42
|
+
export const RIGHTSIZING_METRICS_REQUIRED_TITLE = 'Metrics history is required'
|
|
43
|
+
export const RIGHTSIZING_METRICS_REQUIRED_BODY =
|
|
44
|
+
'Rightsizing needs 7 days of Kubernetes workload history from a PromQL-compatible metrics backend (Prometheus, VictoriaMetrics, Thanos, or Mimir).\nCost Overview remains available without it.'
|
|
45
|
+
export const RIGHTSIZING_EMBEDDED_METRICS_REQUIRED_BODY =
|
|
46
|
+
`${RIGHTSIZING_METRICS_REQUIRED_BODY}\nConfigure metrics for this cluster in the host application or Radar deployment.`
|
|
41
47
|
|
|
42
48
|
export type RightsizingScanSurfaceState =
|
|
43
49
|
| 'discovering'
|
|
@@ -97,6 +103,7 @@ const ACTION_META: Record<
|
|
|
97
103
|
|
|
98
104
|
export function RightsizingScanView({ namespaces }: RightsizingScanViewProps) {
|
|
99
105
|
useAutoPromConnect()
|
|
106
|
+
const settingsAvailable = !useNavCustomization().embedded
|
|
100
107
|
const navigate = useNavigate()
|
|
101
108
|
const [params, setParams] = useSearchParams()
|
|
102
109
|
const { data: clusterInfo } = useClusterInfo()
|
|
@@ -242,16 +249,33 @@ export function RightsizingScanView({ namespaces }: RightsizingScanViewProps) {
|
|
|
242
249
|
/>
|
|
243
250
|
) : surfaceState === 'prometheus_required' ? (
|
|
244
251
|
<CenteredState
|
|
245
|
-
title=
|
|
246
|
-
body=
|
|
252
|
+
title={RIGHTSIZING_METRICS_REQUIRED_TITLE}
|
|
253
|
+
body={settingsAvailable
|
|
254
|
+
? RIGHTSIZING_METRICS_REQUIRED_BODY
|
|
255
|
+
: RIGHTSIZING_EMBEDDED_METRICS_REQUIRED_BODY}
|
|
247
256
|
action={
|
|
248
|
-
<
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
258
|
+
{settingsAvailable && (
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
onClick={() =>
|
|
262
|
+
window.dispatchEvent(
|
|
263
|
+
new CustomEvent('radar:open-settings', { detail: { section: 'prometheus' } }),
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
className="btn-brand px-4 py-2 text-sm font-medium"
|
|
267
|
+
>
|
|
268
|
+
Configure metrics
|
|
269
|
+
</button>
|
|
270
|
+
)}
|
|
271
|
+
<button
|
|
272
|
+
type="button"
|
|
273
|
+
onClick={() => retryPrometheus()}
|
|
274
|
+
className="rounded-lg border border-theme-border px-4 py-2 text-sm font-medium text-theme-text-secondary transition-colors hover:bg-theme-hover hover:text-theme-text-primary"
|
|
275
|
+
>
|
|
276
|
+
Check again
|
|
277
|
+
</button>
|
|
278
|
+
</div>
|
|
255
279
|
}
|
|
256
280
|
/>
|
|
257
281
|
) : surfaceState === 'first_run' ? (
|
|
@@ -860,15 +884,15 @@ function CenteredState({
|
|
|
860
884
|
action?: React.ReactNode
|
|
861
885
|
}) {
|
|
862
886
|
return (
|
|
863
|
-
<div className="flex min-h-
|
|
864
|
-
<div className="flex max-w-
|
|
887
|
+
<div className="flex min-h-56 items-center justify-center rounded-xl border border-theme-border bg-theme-surface px-6 py-8">
|
|
888
|
+
<div className="flex w-full max-w-xl flex-col items-center text-center">
|
|
865
889
|
{loading ? (
|
|
866
890
|
<Loader2 className="h-8 w-8 animate-spin text-theme-text-tertiary" />
|
|
867
891
|
) : (
|
|
868
892
|
<Gauge className="h-8 w-8 text-theme-text-tertiary" />
|
|
869
893
|
)}
|
|
870
894
|
<h2 className="mt-3 text-base font-semibold text-theme-text-primary">{title}</h2>
|
|
871
|
-
<p className="mt-1 text-sm text-theme-text-secondary">{body}</p>
|
|
895
|
+
<p className="mt-1 whitespace-pre-line text-sm text-theme-text-secondary">{body}</p>
|
|
872
896
|
{action && <div className="mt-4">{action}</div>}
|
|
873
897
|
</div>
|
|
874
898
|
</div>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
3
|
getRightsizingScanSurfaceState,
|
|
4
|
+
RIGHTSIZING_EMBEDDED_METRICS_REQUIRED_BODY,
|
|
5
|
+
RIGHTSIZING_METRICS_REQUIRED_BODY,
|
|
6
|
+
RIGHTSIZING_METRICS_REQUIRED_TITLE,
|
|
4
7
|
RIGHTSIZING_SCAN_DESCRIPTION,
|
|
5
8
|
RIGHTSIZING_SCAN_METHODOLOGY,
|
|
6
9
|
} from './RightsizingScanView'
|
|
@@ -17,6 +20,22 @@ describe('rightsizing scan copy', () => {
|
|
|
17
20
|
expect(copy.toLowerCase()).not.toContain('savings')
|
|
18
21
|
})
|
|
19
22
|
|
|
23
|
+
it('describes the metrics contract without assuming one provider or cost source', () => {
|
|
24
|
+
const copy = `${RIGHTSIZING_METRICS_REQUIRED_TITLE} ${RIGHTSIZING_METRICS_REQUIRED_BODY}`
|
|
25
|
+
expect(copy).toContain('Metrics history')
|
|
26
|
+
expect(copy).toContain('PromQL-compatible metrics backend')
|
|
27
|
+
expect(copy).toContain('7 days')
|
|
28
|
+
expect(copy).toContain('Prometheus, VictoriaMetrics, Thanos, or Mimir')
|
|
29
|
+
expect(RIGHTSIZING_METRICS_REQUIRED_BODY).toContain('\nCost Overview')
|
|
30
|
+
expect(copy).not.toContain('OpenCost')
|
|
31
|
+
expect(copy).not.toContain('Kubecost')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('routes embedded configuration through the host instead of standalone Settings', () => {
|
|
35
|
+
expect(RIGHTSIZING_EMBEDDED_METRICS_REQUIRED_BODY).toContain('host application')
|
|
36
|
+
expect(RIGHTSIZING_EMBEDDED_METRICS_REQUIRED_BODY).not.toContain('Settings')
|
|
37
|
+
})
|
|
38
|
+
|
|
20
39
|
it('retains a prior snapshot after a failed rerun but treats a first-run failure as fatal', () => {
|
|
21
40
|
expect(
|
|
22
41
|
getRightsizingScanSurfaceState({
|