@skyhook-io/radar-app 1.14.5 → 1.15.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/README.md +2 -9
- package/package.json +6 -6
- package/src/App.tsx +142 -334
- package/src/RadarApp.tsx +3 -8
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.jobset.test.ts +18 -0
- package/src/api/client.resource-identity.test.ts +44 -0
- package/src/api/client.rightsizing.test.ts +7 -3
- package/src/api/client.ts +267 -78
- package/src/api/preferences.test.ts +54 -0
- package/src/api/preferences.ts +38 -0
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +155 -0
- package/src/components/CloudFunnelButton.tsx +113 -63
- package/src/components/ContextSwitcher.tsx +13 -132
- package/src/components/SelfManagedStart.tsx +96 -0
- package/src/components/applications/ApplicationsView.identity.test.ts +39 -0
- package/src/components/applications/ApplicationsView.tsx +5 -3
- package/src/components/audit/AuditSettingsDialog.tsx +15 -11
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/AgentControls.tsx +26 -3
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/diagnose/DiagnoseContext.tsx +1 -0
- package/src/components/diagnose/agentCatalog.ts +6 -0
- package/src/components/diagnose/launch.test.ts +23 -0
- package/src/components/diagnose/launch.ts +4 -0
- package/src/components/diagnose/parts.test.tsx +18 -0
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/dock/NodeTerminalTab.test.tsx +34 -0
- package/src/components/dock/NodeTerminalTab.tsx +5 -3
- package/src/components/dock/WorkloadLogsTab.test.tsx +66 -0
- package/src/components/dock/WorkloadLogsTab.tsx +2 -1
- package/src/components/execution/BatchExecutionView.render.test.tsx +2 -1
- package/src/components/execution/BatchExecutionView.test.ts +52 -1
- package/src/components/execution/BatchExecutionView.tsx +324 -65
- package/src/components/execution/JobSetAdmission.test.tsx +165 -0
- package/src/components/execution/JobSetAdmission.tsx +26 -0
- package/src/components/execution/JobSetMemberComparison.test.tsx +30 -0
- package/src/components/execution/JobSetMemberComparison.tsx +88 -0
- package/src/components/execution/batch-run-actions.test.ts +1 -0
- package/src/components/execution/batch-timeline.test.ts +3 -0
- package/src/components/execution/execution-definition.test.ts +15 -1
- package/src/components/execution/execution-definition.ts +4 -0
- package/src/components/execution/member-collection.render.test.tsx +142 -0
- package/src/components/gitops/GitOpsView.tsx +33 -2
- package/src/components/gitops/destination-toast.test.ts +33 -0
- package/src/components/gitops/destination-toast.ts +46 -0
- package/src/components/gitops/useDestinationCluster.ts +30 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +25 -14
- package/src/components/home/HomeView.tsx +6 -2
- package/src/components/home/MCPSetupDialog.tsx +10 -0
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +75 -22
- package/src/components/logs/WorkloadLogsViewer.tsx +7 -4
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +27 -8
- package/src/components/resource/PVCUsageBar.render.test.tsx +168 -0
- package/src/components/resource/PVCUsageBar.tsx +62 -16
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -1
- package/src/components/resources/ResourcesView.tsx +3 -3
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +8 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.test.tsx +39 -0
- package/src/components/resources/renderers/KueueWorkloadRenderer.tsx +37 -0
- package/src/components/resources/renderers/RayClusterRenderer.test.tsx +18 -0
- package/src/components/resources/renderers/RayClusterRenderer.tsx +17 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/resources/renderers/RayServiceRenderer.test.tsx +69 -0
- package/src/components/resources/renderers/RayServiceRenderer.tsx +38 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +34 -22
- package/src/components/rightsizing/copy.test.ts +7 -0
- package/src/components/rightsizing/model.test.ts +2 -1
- package/src/components/rightsizing/notices.test.tsx +4 -3
- package/src/components/settings/OperatorManagedNotice.tsx +17 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/SettingsDialog.tsx +168 -92
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/settings-state.test.ts +18 -0
- package/src/components/settings/settings-state.ts +19 -0
- package/src/components/timeline/TimelineView.tsx +9 -3
- package/src/components/timeline/TimelineView.urlparams.test.ts +18 -1
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +16 -0
- package/src/components/ui/UpdateNotification.tsx +13 -9
- package/src/components/ui/command-items.ts +14 -3
- package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
- package/src/components/usage-data/UsageDataAsk.tsx +106 -0
- package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
- package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
- package/src/components/useContextSwitchFlow.tsx +147 -0
- package/src/components/whats-new/WhatsNew.test.ts +110 -0
- package/src/components/whats-new/WhatsNew.tsx +423 -0
- package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
- package/src/components/whats-new/check-whats-new.test.ts +65 -0
- package/src/components/whats-new/releaseNotes.ts +121 -0
- package/src/components/workload/WorkloadView.test.ts +17 -1
- package/src/components/workload/WorkloadView.tsx +47 -19
- package/src/context/ContextSwitchContext.tsx +18 -2
- package/src/context/NavCustomization.tsx +4 -58
- package/src/context/ThemeContext.tsx +3 -11
- package/src/hooks/useClusterLoadState.ts +2 -2
- package/src/hooks/useFavorites.ts +3 -5
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/auditBadges.ts +1 -1
- package/src/utils/navigation.test.ts +34 -1
- package/src/utils/navigation.ts +28 -5
- package/src/utils/topology-selection.test.ts +56 -0
- package/src/utils/topology-selection.ts +9 -9
- package/src/utils/version.ts +11 -0
- package/src/components/ui/CommandPalette.tsx +0 -261
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { useCallback, useEffect } from 'react'
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { fetchJSON } from './client'
|
|
4
|
+
import { apiUrl, getApiBase, getAuthHeaders, getCredentialsMode } from './config'
|
|
5
|
+
|
|
6
|
+
export type UsageDataState = 'undecided' | 'on' | 'off'
|
|
7
|
+
export type UsageDataSource = 'default' | 'user' | 'env' | 'deployment'
|
|
8
|
+
|
|
9
|
+
export interface ClusterShape {
|
|
10
|
+
kubernetesVersion: string
|
|
11
|
+
platform: string
|
|
12
|
+
nodes: string
|
|
13
|
+
integrations: string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UsageReport {
|
|
17
|
+
schema: number
|
|
18
|
+
version: string
|
|
19
|
+
os: string
|
|
20
|
+
arch: string
|
|
21
|
+
installMethod: string
|
|
22
|
+
mode: string
|
|
23
|
+
periodStart: string
|
|
24
|
+
periodEnd: string
|
|
25
|
+
setup: {
|
|
26
|
+
authMode: string
|
|
27
|
+
timelineStorage: string
|
|
28
|
+
mcpEnabled: boolean
|
|
29
|
+
prometheus: string
|
|
30
|
+
costSource: string
|
|
31
|
+
browsers: string[]
|
|
32
|
+
}
|
|
33
|
+
engagement: { sessions: number; activeMinutes: string }
|
|
34
|
+
views: Record<string, number>
|
|
35
|
+
actions: Record<string, number>
|
|
36
|
+
mcpTools: Record<string, number>
|
|
37
|
+
uiEvents: Record<string, number>
|
|
38
|
+
errors: Record<string, number>
|
|
39
|
+
clusters: { contexts: string; used: number; shapes: ClusterShape[] }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UsageDataStatus {
|
|
43
|
+
state: UsageDataState
|
|
44
|
+
source: UsageDataSource
|
|
45
|
+
canChange: boolean
|
|
46
|
+
developmentBuild: boolean
|
|
47
|
+
nextReportAt?: string
|
|
48
|
+
preview: UsageReport
|
|
49
|
+
firstRunPrompt: boolean
|
|
50
|
+
// What's New may ask: never asked, or asked and left unanswered long enough ago.
|
|
51
|
+
ask: boolean
|
|
52
|
+
// Several people use this Radar, so its configuration decides and nobody is asked.
|
|
53
|
+
shared: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const usageDataKey = (apiBase: string) => ['usage-data', apiBase]
|
|
57
|
+
|
|
58
|
+
// `fresh` is for the report preview: it must match what would be sent now, so
|
|
59
|
+
// it refetches every time it is shown instead of reusing the app shell's copy.
|
|
60
|
+
export function useUsageData(enabled = true, { fresh = false }: { fresh?: boolean } = {}) {
|
|
61
|
+
const apiBase = getApiBase()
|
|
62
|
+
return useQuery<UsageDataStatus>({
|
|
63
|
+
queryKey: usageDataKey(apiBase),
|
|
64
|
+
queryFn: () => fetchJSON<UsageDataStatus>('/usage-data'),
|
|
65
|
+
enabled,
|
|
66
|
+
staleTime: fresh ? 0 : 60_000,
|
|
67
|
+
refetchOnMount: fresh ? 'always' : true,
|
|
68
|
+
retry: false,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useSetUsageData() {
|
|
73
|
+
const queryClient = useQueryClient()
|
|
74
|
+
const apiBase = getApiBase()
|
|
75
|
+
return useMutation({
|
|
76
|
+
mutationFn: (enabled: boolean) =>
|
|
77
|
+
fetchJSON<UsageDataStatus>('/usage-data', {
|
|
78
|
+
method: 'PUT',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({ enabled }),
|
|
81
|
+
}),
|
|
82
|
+
meta: { errorMessage: 'Failed to save usage data choice' },
|
|
83
|
+
// A status read still in flight predates the choice; it must not
|
|
84
|
+
// replace the answer the server just confirmed.
|
|
85
|
+
onSuccess: async (status) => {
|
|
86
|
+
const queryKey = usageDataKey(apiBase)
|
|
87
|
+
await queryClient.cancelQueries({ queryKey })
|
|
88
|
+
queryClient.setQueryData(queryKey, status)
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Records that the question was shown, answered or not, and stops this tab
|
|
94
|
+
// offering it again from its cached status. Fire and forget: a failure only
|
|
95
|
+
// means it might show once more.
|
|
96
|
+
export function useMarkUsagePromptShown() {
|
|
97
|
+
const queryClient = useQueryClient()
|
|
98
|
+
const apiBase = getApiBase()
|
|
99
|
+
return useCallback(() => {
|
|
100
|
+
const queryKey = usageDataKey(apiBase)
|
|
101
|
+
void queryClient.cancelQueries({ queryKey })
|
|
102
|
+
queryClient.setQueryData<UsageDataStatus>(queryKey, (prev) => prev && { ...prev, firstRunPrompt: false, ask: false })
|
|
103
|
+
void fetch(apiUrl('/usage-data/prompt-shown'), {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: getAuthHeaders(),
|
|
106
|
+
credentials: getCredentialsMode(),
|
|
107
|
+
}).catch(() => {})
|
|
108
|
+
}, [queryClient, apiBase])
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isRecording(status: UsageDataStatus | undefined): boolean {
|
|
112
|
+
return status?.state === 'on'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Set from the status query so code without hook access (error boundaries,
|
|
116
|
+
// event handlers) can skip the request entirely when usage data is off.
|
|
117
|
+
let recording = false
|
|
118
|
+
|
|
119
|
+
function setUsageRecording(on: boolean) {
|
|
120
|
+
recording = on
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type UsageEvent =
|
|
124
|
+
| { type: 'view'; name: string }
|
|
125
|
+
| { type: 'ui'; name: string }
|
|
126
|
+
| { type: 'session' }
|
|
127
|
+
| { type: 'active'; minutes: number }
|
|
128
|
+
|
|
129
|
+
// Fire and forget; the server drops anything outside its allow-list.
|
|
130
|
+
export function recordUsageEvent(event: UsageEvent): void {
|
|
131
|
+
if (!recording) return
|
|
132
|
+
void fetch(apiUrl('/usage-data/event'), {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: { ...getAuthHeaders(), 'Content-Type': 'application/json' },
|
|
135
|
+
credentials: getCredentialsMode(),
|
|
136
|
+
body: JSON.stringify(event),
|
|
137
|
+
keepalive: true,
|
|
138
|
+
}).catch(() => {})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const ACTIVE_TICK_MINUTES = 5
|
|
142
|
+
|
|
143
|
+
// Counts views, one session per page load, and visible time in five-minute
|
|
144
|
+
// ticks. Nothing is sent unless usage data is on.
|
|
145
|
+
export function useUsageRecording(view: string, status: UsageDataStatus | undefined) {
|
|
146
|
+
const on = isRecording(status)
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
setUsageRecording(on)
|
|
149
|
+
}, [on])
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (on) recordUsageEvent({ type: 'view', name: view })
|
|
153
|
+
}, [view, on])
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (!on) return
|
|
157
|
+
recordUsageEvent({ type: 'session' })
|
|
158
|
+
const tick = window.setInterval(() => {
|
|
159
|
+
if (document.visibilityState === 'visible') {
|
|
160
|
+
recordUsageEvent({ type: 'active', minutes: ACTIVE_TICK_MINUTES })
|
|
161
|
+
}
|
|
162
|
+
}, ACTIVE_TICK_MINUTES * 60_000)
|
|
163
|
+
return () => window.clearInterval(tick)
|
|
164
|
+
}, [on])
|
|
165
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act, type ReactNode } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import type { ConnectionStateType } from '../context/ConnectionContext'
|
|
7
|
+
import { CloudFunnelButton } from './CloudFunnelButton'
|
|
8
|
+
|
|
9
|
+
const connection = vi.hoisted(() => ({ state: 'connecting' as ConnectionStateType }))
|
|
10
|
+
vi.mock('../context/ConnectionContext', () => ({ useConnection: () => ({ connection }) }))
|
|
11
|
+
vi.mock('./ui/Tooltip', () => ({ Tooltip: ({ children }: { children: ReactNode }) => children }))
|
|
12
|
+
vi.mock('./ui/Toast', () => ({ showApiError: vi.fn() }))
|
|
13
|
+
vi.mock('./CloudConnectFlow', () => ({ CloudConnectFlow: () => <div>Install flow</div> }))
|
|
14
|
+
|
|
15
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
16
|
+
let root: Root
|
|
17
|
+
let client: QueryClient
|
|
18
|
+
let lane: 'driver' | 'wizard'
|
|
19
|
+
let discovery: { connected: { namespace: string; deployment: string; clusterUrl: string }[] }
|
|
20
|
+
let discoveryStatus: number
|
|
21
|
+
let flowState: 'idle' | 'awaiting_approval'
|
|
22
|
+
const requests: string[] = []
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
connection.state = 'connecting'
|
|
26
|
+
lane = 'driver'
|
|
27
|
+
discovery = { connected: [] }
|
|
28
|
+
discoveryStatus = 200
|
|
29
|
+
flowState = 'idle'
|
|
30
|
+
requests.length = 0
|
|
31
|
+
client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
32
|
+
const element = document.createElement('div')
|
|
33
|
+
document.body.appendChild(element)
|
|
34
|
+
root = createRoot(element)
|
|
35
|
+
vi.stubGlobal('fetch', vi.fn(async (input: string) => {
|
|
36
|
+
const url = new URL(input, 'http://localhost')
|
|
37
|
+
requests.push(url.pathname + url.search)
|
|
38
|
+
const bodies: Record<string, unknown> = {
|
|
39
|
+
'/api/capabilities': { cloudConnect: { lane, appUrl: 'https://cloud.example', apiUrl: 'https://cloud.example' }, deployment: { mode: 'local' } },
|
|
40
|
+
'/api/cluster-info': { context: 'test-cluster' },
|
|
41
|
+
'/api/cloud/install/status': { state: flowState },
|
|
42
|
+
'/api/cloud/install/discover': discoveryStatus === 200 ? discovery : { error: 'Discovery failed' },
|
|
43
|
+
'/api/cloud/install/prepare': { error: 'Not connected to cluster' },
|
|
44
|
+
'/api/connect/info': {},
|
|
45
|
+
}
|
|
46
|
+
if (!(url.pathname in bodies)) throw new Error(`Unexpected request: ${input}`)
|
|
47
|
+
const status = url.pathname.endsWith('/prepare') ? 503 : url.pathname.endsWith('/discover') ? discoveryStatus : 200
|
|
48
|
+
return new Response(JSON.stringify(bodies[url.pathname]), { status })
|
|
49
|
+
}))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
afterEach(async () => {
|
|
53
|
+
await act(async () => root.unmount())
|
|
54
|
+
client.clear()
|
|
55
|
+
document.body.replaceChildren()
|
|
56
|
+
vi.unstubAllGlobals()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
async function render() {
|
|
60
|
+
await act(async () => {
|
|
61
|
+
root.render(<QueryClientProvider client={client}><CloudFunnelButton /></QueryClientProvider>)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function until(assertion: () => void) {
|
|
66
|
+
await vi.waitFor(async () => {
|
|
67
|
+
await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)) })
|
|
68
|
+
assertion()
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function button(text: string) {
|
|
73
|
+
return Array.from(document.querySelectorAll('button')).find(element => element.textContent === text)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function openDialog() {
|
|
77
|
+
await render()
|
|
78
|
+
await until(() => expect(document.querySelector('[aria-label="Radar Cloud"]')).not.toBeNull())
|
|
79
|
+
await act(async () => { (document.querySelector('[aria-label="Radar Cloud"]') as HTMLButtonElement).click() })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('Cloud dialog connection availability', () => {
|
|
83
|
+
it.each(['connecting', 'disconnected'] as const)('offers Cloud directly while %s without inspecting a cluster', async state => {
|
|
84
|
+
connection.state = state
|
|
85
|
+
await openDialog()
|
|
86
|
+
await until(() => expect(requests).toContain('/api/connect/info?lane=wizard&mode=local'))
|
|
87
|
+
const link = Array.from(document.querySelectorAll('a')).find(element => element.textContent === 'Continue in Radar Cloud')!
|
|
88
|
+
expect(link.href).toBe('https://cloud.example/signup?utm_source=radar-oss&utm_medium=app&utm_campaign=cloud-modal&utm_content=driver-footer-browser-link')
|
|
89
|
+
expect(button('Connect this cluster…')).toBeUndefined()
|
|
90
|
+
expect(button('Try again')).toBeUndefined()
|
|
91
|
+
expect(requests).not.toContain('/api/cloud/install/discover')
|
|
92
|
+
expect(requests).not.toContain('/api/cloud/install/prepare')
|
|
93
|
+
await act(async () => { button('How it works and what it costs')!.click() })
|
|
94
|
+
expect(document.body.textContent).toContain('Radar runs in your cluster and tunnels outward to Radar Cloud')
|
|
95
|
+
expect(document.body.textContent).not.toContain('Setup runs here in the app')
|
|
96
|
+
expect(document.body.textContent).not.toContain('see what gets installed')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('restores cluster discovery and Connect in the open dialog when the connection recovers', async () => {
|
|
100
|
+
await openDialog()
|
|
101
|
+
connection.state = 'connected'
|
|
102
|
+
await render()
|
|
103
|
+
await until(() => expect(button('Connect this cluster…')?.disabled).toBe(false))
|
|
104
|
+
expect(requests).toContain('/api/cloud/install/discover')
|
|
105
|
+
expect(requests).toContain('/api/connect/info?lane=driver&mode=local')
|
|
106
|
+
connection.state = 'disconnected'
|
|
107
|
+
await render()
|
|
108
|
+
expect(button('Connect this cluster…')).toBeUndefined()
|
|
109
|
+
expect(document.body.textContent).toContain('Continue in Radar Cloud')
|
|
110
|
+
connection.state = 'connected'
|
|
111
|
+
await render()
|
|
112
|
+
await until(() => expect(button('Connect this cluster…')?.disabled).toBe(false))
|
|
113
|
+
expect(requests.filter(path => path === '/api/cloud/install/discover')).toHaveLength(2)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('keeps the existing already-connected destination after discovery', async () => {
|
|
117
|
+
connection.state = 'connected'
|
|
118
|
+
discovery.connected = [{ namespace: 'radar', deployment: 'radar', clusterUrl: 'https://cloud.example/clusters/one' }]
|
|
119
|
+
await openDialog()
|
|
120
|
+
await until(() => expect(document.body.textContent).toContain('This cluster is already connected to Radar Cloud.'))
|
|
121
|
+
expect(document.querySelector('a[href="https://cloud.example/clusters/one"]')?.textContent).toBe('Open in Radar Cloud')
|
|
122
|
+
expect(button('Connect this cluster…')).toBeUndefined()
|
|
123
|
+
connection.state = 'disconnected'
|
|
124
|
+
await render()
|
|
125
|
+
expect(document.body.textContent).not.toContain('This cluster is already connected to Radar Cloud.')
|
|
126
|
+
expect(document.body.textContent).toContain('Continue in Radar Cloud')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('still allows inspection after discovery fails and preserves 503 handoff attribution', async () => {
|
|
130
|
+
connection.state = 'connected'
|
|
131
|
+
discoveryStatus = 500
|
|
132
|
+
await openDialog()
|
|
133
|
+
await until(() => expect(button('Connect this cluster…')?.disabled).toBe(false))
|
|
134
|
+
await act(async () => { button('Connect this cluster…')!.click() })
|
|
135
|
+
await until(() => expect(button('Try again')).toBeDefined())
|
|
136
|
+
expect(document.querySelector('a[href*="driver-footer-browser-link"]')?.getAttribute('href')).toContain('radar_outcome=radar_not_connected_to_cluster')
|
|
137
|
+
expect(document.body.textContent).toContain('Not connected to cluster')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('keeps wizard-only deployments on their existing signup path', async () => {
|
|
141
|
+
lane = 'wizard'
|
|
142
|
+
await openDialog()
|
|
143
|
+
expect(document.querySelector('a[href*="wizard-signup-button"]')?.textContent).toBe('Try Cloud free')
|
|
144
|
+
expect(document.body.textContent).not.toContain('Continue in Radar Cloud')
|
|
145
|
+
expect(requests).not.toContain('/api/cloud/install/discover')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('keeps an active server-owned install visible even while disconnected', async () => {
|
|
149
|
+
connection.state = 'disconnected'
|
|
150
|
+
flowState = 'awaiting_approval'
|
|
151
|
+
await openDialog()
|
|
152
|
+
await until(() => expect(document.body.textContent).toContain('Install flow'))
|
|
153
|
+
expect(document.body.textContent).not.toContain('Continue in Radar Cloud')
|
|
154
|
+
})
|
|
155
|
+
})
|