@skyhook-io/radar-app 1.12.2 → 1.12.3
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 +2 -2
- package/src/App.tsx +34 -15
- package/src/api/client.images.test.ts +63 -0
- package/src/api/client.ts +208 -33
- package/src/api/client.yaml.test.ts +3 -3
- package/src/api/version-check.test.ts +78 -0
- package/src/components/CloudConnectFlow.tsx +46 -26
- package/src/components/CloudFunnelButton.tsx +166 -129
- package/src/components/ConnectionErrorView.test.tsx +21 -1
- package/src/components/ConnectionErrorView.tsx +7 -8
- package/src/components/applications/ApplicationsView.tsx +10 -9
- package/src/components/audit/AuditView.tsx +6 -3
- package/src/components/audit/UpgradeReadinessView.test.ts +26 -2
- package/src/components/audit/UpgradeReadinessView.tsx +19 -9
- package/src/components/diagnose/DiagnoseSurface.tsx +14 -10
- package/src/components/gitops/GitOpsView.tsx +8 -3
- package/src/components/helm/HelmReleaseDrawer.tsx +4 -3
- package/src/components/helm/OwnedResources.tsx +10 -2
- package/src/components/home/ClusterHealthCard.test.ts +31 -0
- package/src/components/home/ClusterHealthCard.tsx +60 -1
- package/src/components/home/HomeView.tsx +36 -11
- package/src/components/home/MCPSetupDialog.tsx +5 -4
- package/src/components/home/RadarVersionLine.test.tsx +145 -0
- package/src/components/home/RadarVersionLine.tsx +137 -0
- package/src/components/resources/PodFilesystemModal.tsx +54 -2
- package/src/components/resources/ResourcesView.tsx +31 -8
- package/src/components/resources/renderers/WorkloadRenderer.tsx +13 -5
- package/src/components/settings/SettingsDialog.tsx +21 -4
- package/src/components/ui/ErrorBoundary.test.tsx +55 -0
- package/src/components/ui/ErrorBoundary.tsx +17 -2
- package/src/components/ui/UpdateNotification.test.tsx +49 -0
- package/src/components/ui/UpdateNotification.tsx +6 -2
- package/src/components/workload/WorkloadView.test.ts +60 -0
- package/src/components/workload/WorkloadView.tsx +270 -29
- package/src/contexts/CapabilitiesContext.test.tsx +29 -0
- package/src/contexts/CapabilitiesContext.tsx +7 -3
- package/src/utils/navigation.test.ts +45 -0
- package/src/utils/navigation.ts +5 -5
- package/src/utils/topology-selection.ts +3 -2
- package/src/utils/version.test.ts +37 -0
- package/src/utils/version.ts +56 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { markDailyUpdateCheckAttempt, triggerDailyUpdateCheck, utcDay } from './client'
|
|
3
|
+
|
|
4
|
+
function memoryStorage() {
|
|
5
|
+
const values = new Map<string, string>()
|
|
6
|
+
return {
|
|
7
|
+
getItem: (key: string) => values.get(key) ?? null,
|
|
8
|
+
setItem: (key: string, value: string) => values.set(key, value),
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('daily update checks', () => {
|
|
13
|
+
it('uses UTC days', () => {
|
|
14
|
+
expect(utcDay(new Date('2026-08-29T23:59:59-07:00'))).toBe('2026-08-30')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('attempts once per API base and UTC day', () => {
|
|
18
|
+
const storage = memoryStorage()
|
|
19
|
+
expect(markDailyUpdateCheckAttempt(storage, '/api', new Date('2026-08-29T10:00:00Z'))).toBe(true)
|
|
20
|
+
expect(markDailyUpdateCheckAttempt(storage, '/api', new Date('2026-08-29T20:00:00Z'))).toBe(false)
|
|
21
|
+
expect(markDailyUpdateCheckAttempt(storage, '/api', new Date('2026-08-30T10:00:00Z'))).toBe(true)
|
|
22
|
+
expect(markDailyUpdateCheckAttempt(storage, '/c/cluster-a/api', new Date('2026-08-30T10:00:00Z'))).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('skips the attempt when storage is unavailable', () => {
|
|
26
|
+
const storage = {
|
|
27
|
+
getItem: () => null,
|
|
28
|
+
setItem: () => { throw new Error('blocked') },
|
|
29
|
+
}
|
|
30
|
+
expect(markDailyUpdateCheckAttempt(storage, '/api', new Date('2026-08-29T10:00:00Z'))).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('triggerDailyUpdateCheck', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
const values = new Map<string, string>()
|
|
37
|
+
vi.stubGlobal('localStorage', {
|
|
38
|
+
getItem: (key: string) => values.get(key) ?? null,
|
|
39
|
+
setItem: (key: string, value: string) => values.set(key, value),
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.unstubAllGlobals()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it.each(['local', 'cloud', undefined] as const)('does not report in %s mode', async (mode) => {
|
|
48
|
+
const fetch = vi.fn<typeof globalThis.fetch>(async () => new Response(null, { status: 204 }))
|
|
49
|
+
vi.stubGlobal('fetch', fetch)
|
|
50
|
+
await triggerDailyUpdateCheck(mode)
|
|
51
|
+
expect(fetch).not.toHaveBeenCalled()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('sends one bodyless in-cluster attempt per day', async () => {
|
|
55
|
+
const fetch = vi.fn<typeof globalThis.fetch>(async () => new Response(null, { status: 204 }))
|
|
56
|
+
vi.stubGlobal('fetch', fetch)
|
|
57
|
+
|
|
58
|
+
await triggerDailyUpdateCheck('in-cluster')
|
|
59
|
+
await triggerDailyUpdateCheck('in-cluster')
|
|
60
|
+
|
|
61
|
+
expect(fetch).toHaveBeenCalledOnce()
|
|
62
|
+
expect(fetch).toHaveBeenCalledWith('/api/version-check/browser', expect.objectContaining({
|
|
63
|
+
method: 'POST',
|
|
64
|
+
credentials: 'same-origin',
|
|
65
|
+
keepalive: true,
|
|
66
|
+
}))
|
|
67
|
+
expect(fetch.mock.calls[0][1]).not.toHaveProperty('body')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('does not retry a failed daily attempt', async () => {
|
|
71
|
+
const fetch = vi.fn<typeof globalThis.fetch>(async () => { throw new Error('offline') })
|
|
72
|
+
vi.stubGlobal('fetch', fetch)
|
|
73
|
+
|
|
74
|
+
await expect(triggerDailyUpdateCheck('in-cluster')).rejects.toThrow('offline')
|
|
75
|
+
await triggerDailyUpdateCheck('in-cluster')
|
|
76
|
+
expect(fetch).toHaveBeenCalledOnce()
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useState } from 'react'
|
|
2
2
|
import { useMutation } from '@tanstack/react-query'
|
|
3
3
|
import { AlertTriangle, ArrowUpRight, Check, ExternalLink, GitBranch, Info, Loader2, ShieldAlert, X } from 'lucide-react'
|
|
4
|
+
import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
4
5
|
import {
|
|
5
6
|
ApiError,
|
|
6
7
|
cancelCloudInstall,
|
|
@@ -40,7 +41,7 @@ export function CloudConnectFlow({
|
|
|
40
41
|
<div className="px-8 py-10 flex flex-col items-center gap-3 text-center">
|
|
41
42
|
<Loader2 className="w-5 h-5 animate-spin text-emerald-600 dark:text-emerald-400" />
|
|
42
43
|
<p className="text-[13px] text-theme-text-secondary">
|
|
43
|
-
Checking this cluster and preparing the install
|
|
44
|
+
Checking this cluster and preparing the install. This can take a moment on a slow link.
|
|
44
45
|
</p>
|
|
45
46
|
</div>
|
|
46
47
|
)
|
|
@@ -143,6 +144,7 @@ function PlanCard({
|
|
|
143
144
|
const [acceptAdoption, setAcceptAdoption] = useState(false)
|
|
144
145
|
const [ackUncertainty, setAckUncertainty] = useState(false)
|
|
145
146
|
const [ackShared, setAckShared] = useState(false)
|
|
147
|
+
const [notesOpen, setNotesOpen] = useState(false)
|
|
146
148
|
|
|
147
149
|
// The approval tab is opened synchronously by the click below (popup
|
|
148
150
|
// blockers reject window.open from an async callback) and navigated once the
|
|
@@ -236,19 +238,27 @@ function PlanCard({
|
|
|
236
238
|
their presence visible so nobody approves blind, but don't let the
|
|
237
239
|
wall of text bury the decision. */}
|
|
238
240
|
{plan.advisories && plan.advisories.length > 0 && (
|
|
239
|
-
<
|
|
240
|
-
<
|
|
241
|
+
<div className="mt-2.5">
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
onClick={() => setNotesOpen((v) => !v)}
|
|
245
|
+
aria-expanded={notesOpen}
|
|
246
|
+
className="flex items-center gap-1.5 text-[11.5px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
|
|
247
|
+
>
|
|
248
|
+
<CollapseChevron open={notesOpen} className="w-3.5 h-3.5" />
|
|
241
249
|
<Info className="w-3.5 h-3.5 shrink-0" />
|
|
242
250
|
{plan.advisories.length === 1 ? '1 preflight note' : `${plan.advisories.length} preflight notes`}
|
|
243
|
-
</
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{note}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
251
|
+
</button>
|
|
252
|
+
<Collapse open={notesOpen}>
|
|
253
|
+
<ul className="mt-1.5 space-y-1.5 pl-5">
|
|
254
|
+
{plan.advisories.map((note) => (
|
|
255
|
+
<li key={note} className="text-[11.5px] leading-snug text-theme-text-tertiary">
|
|
256
|
+
{note}
|
|
257
|
+
</li>
|
|
258
|
+
))}
|
|
259
|
+
</ul>
|
|
260
|
+
</Collapse>
|
|
261
|
+
</div>
|
|
252
262
|
)}
|
|
253
263
|
|
|
254
264
|
<label className="mt-3.5 block">
|
|
@@ -312,8 +322,9 @@ function PlanCard({
|
|
|
312
322
|
* so this cannot assert whether an account already exists. Phrased to
|
|
313
323
|
* read correctly for a returning operator and a first-time one alike. */}
|
|
314
324
|
<p className="mt-2.5 text-[11px] text-theme-text-tertiary">
|
|
315
|
-
Nothing is installed yet
|
|
316
|
-
organization first if you don't have one.
|
|
325
|
+
Nothing is installed yet. You'll approve this cluster in the browser, creating your account and
|
|
326
|
+
organization first if you don't have one.{' '}
|
|
327
|
+
{adopt ? 'Then your existing Radar is upgraded and connected.' : 'Then Radar is installed in your cluster.'}
|
|
317
328
|
</p>
|
|
318
329
|
</div>
|
|
319
330
|
)
|
|
@@ -365,7 +376,7 @@ function ApprovalCard({ status, onStatus }: { status: CloudInstallStatus; onStat
|
|
|
365
376
|
</div>
|
|
366
377
|
<p className="text-[12.5px] leading-relaxed text-theme-text-secondary mb-3.5">
|
|
367
378
|
Approve connecting <b className="text-theme-text-primary">{status.clusterName}</b> in the browser tab.
|
|
368
|
-
Sign-in and org setup happen there too
|
|
379
|
+
Sign-in and org setup happen there too, and this screen advances automatically.
|
|
369
380
|
</p>
|
|
370
381
|
{status.connectUrl && (
|
|
371
382
|
<div className="card-inner flex items-center gap-2">
|
|
@@ -391,7 +402,7 @@ function ProgressCard({ status, onStatus }: { status: CloudInstallStatus; onStat
|
|
|
391
402
|
const steps: Array<{ label: string; state: 'done' | 'active' | 'todo' }> = [
|
|
392
403
|
{ label: 'Approved in browser', state: 'done' },
|
|
393
404
|
{ label: `Installing Radar (namespace ${status.plan?.namespace ?? 'radar'})`, state: provisioning ? 'active' : 'done' },
|
|
394
|
-
{ label: 'Waiting for
|
|
405
|
+
{ label: 'Waiting for Radar to connect to Cloud', state: provisioning ? 'todo' : 'active' },
|
|
395
406
|
]
|
|
396
407
|
return (
|
|
397
408
|
<div className="px-8 pt-6 pb-5">
|
|
@@ -419,7 +430,7 @@ function ProgressCard({ status, onStatus }: { status: CloudInstallStatus; onStat
|
|
|
419
430
|
<div className="mt-4">
|
|
420
431
|
{provisioning ? (
|
|
421
432
|
<p className="text-[11px] text-theme-text-tertiary">
|
|
422
|
-
Installing
|
|
433
|
+
Installing. This step completes atomically and can’t be canceled midway.
|
|
423
434
|
</p>
|
|
424
435
|
) : (
|
|
425
436
|
cancel
|
|
@@ -469,8 +480,8 @@ function ConnectedCard({
|
|
|
469
480
|
</h4>
|
|
470
481
|
</div>
|
|
471
482
|
<p className="text-[12.5px] leading-relaxed text-theme-text-secondary mb-4">
|
|
472
|
-
|
|
473
|
-
cluster is now also reachable for your team at one URL.
|
|
483
|
+
Radar is live in the cluster and connected to Radar Cloud. The Radar you're running keeps working,
|
|
484
|
+
and the cluster is now also reachable for your team at one URL.
|
|
474
485
|
</p>
|
|
475
486
|
<div className="flex items-center gap-4 mb-4">
|
|
476
487
|
<a
|
|
@@ -592,14 +603,23 @@ function GuidanceBlock({
|
|
|
592
603
|
}
|
|
593
604
|
|
|
594
605
|
function GuidanceDetails({ title, guidance }: { title: string; guidance: CloudInstallRecoveryGuidance }) {
|
|
606
|
+
const [open, setOpen] = useState(false)
|
|
595
607
|
return (
|
|
596
|
-
<
|
|
597
|
-
<
|
|
608
|
+
<div>
|
|
609
|
+
<button
|
|
610
|
+
type="button"
|
|
611
|
+
onClick={() => setOpen((v) => !v)}
|
|
612
|
+
aria-expanded={open}
|
|
613
|
+
className="flex items-center gap-1.5 text-[11.5px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
|
|
614
|
+
>
|
|
615
|
+
<CollapseChevron open={open} className="w-3.5 h-3.5" />
|
|
598
616
|
{title}
|
|
599
|
-
</
|
|
600
|
-
<
|
|
601
|
-
<
|
|
602
|
-
|
|
603
|
-
|
|
617
|
+
</button>
|
|
618
|
+
<Collapse open={open}>
|
|
619
|
+
<div className="mt-2">
|
|
620
|
+
<GuidanceBlock guidance={guidance} />
|
|
621
|
+
</div>
|
|
622
|
+
</Collapse>
|
|
623
|
+
</div>
|
|
604
624
|
)
|
|
605
625
|
}
|