@skyhook-io/radar-app 1.14.6 → 1.16.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 -0
- package/package.json +6 -5
- package/src/App.tsx +65 -17
- package/src/RadarApp.tsx +51 -21
- package/src/api/apiResources.test.ts +19 -1
- package/src/api/apiResources.ts +18 -3
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.capacity.test.ts +15 -0
- package/src/api/client.trace.test.ts +9 -1
- package/src/api/client.ts +269 -204
- package/src/api/diagnose.test.ts +102 -1
- package/src/api/diagnose.ts +42 -11
- package/src/api/drain-plan.test.ts +0 -10
- package/src/api/httpErrors.test.ts +95 -0
- package/src/api/httpErrors.ts +101 -0
- package/src/api/policy.ts +33 -5
- package/src/api/radarFeatures.render.test.tsx +190 -0
- package/src/api/radarFeatures.test.ts +133 -0
- package/src/api/radarFeatures.ts +147 -0
- package/src/api/timelineSource.test.ts +2 -10
- package/src/api/timelineSource.ts +3 -2
- package/src/api/traffic.ts +4 -3
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +2 -2
- package/src/components/CloudFunnelButton.tsx +99 -55
- package/src/components/NamespaceSwitcher.test.tsx +47 -0
- package/src/components/NamespaceSwitcher.tsx +17 -1
- package/src/components/SelfManagedStart.tsx +95 -0
- package/src/components/applications/ApplicationsView.tsx +20 -6
- package/src/components/audit/UpgradeReadinessView.test.ts +14 -6
- package/src/components/audit/UpgradeReadinessView.tsx +9 -13
- package/src/components/capacity/CapacityView.test.tsx +13 -4
- package/src/components/capacity/shared.tsx +14 -13
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/cost/ApplicationCostTab.tsx +9 -1
- package/src/components/cost/CostConnectionAction.tsx +30 -0
- package/src/components/cost/CostTrendChart.tsx +2 -0
- package/src/components/cost/CostView.tsx +4 -24
- package/src/components/cost/WorkloadCostTab.tsx +7 -1
- package/src/components/cost/source.test.ts +30 -3
- package/src/components/cost/source.ts +37 -2
- package/src/components/curl/ServiceCurlButton.tsx +7 -3
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/dock/NodeTerminalTab.tsx +2 -1
- package/src/components/dock/TerminalTab.tsx +2 -1
- package/src/components/dock/TrafficFlowListTab.tsx +2 -2
- package/src/components/execution/BatchExecutionView.tsx +1 -1
- package/src/components/execution/JobSetAdmission.test.tsx +160 -37
- package/src/components/execution/JobSetAdmission.tsx +16 -7
- package/src/components/gitops/GitOpsView.tsx +12 -0
- package/src/components/helm/TrackChartSourceDialog.tsx +5 -3
- package/src/components/home/HomeView.tsx +7 -3
- package/src/components/home/MCPSetupDialog.tsx +37 -37
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +25 -3
- package/src/components/resource/PVCUsageBar.render.test.tsx +16 -0
- package/src/components/resource/PVCUsageBar.tsx +4 -1
- package/src/components/resource/PrometheusChartsGrid.render.test.tsx +1 -0
- package/src/components/resource/PrometheusChartsGrid.tsx +4 -0
- package/src/components/resources/ImageFilesystemModal.tsx +2 -1
- package/src/components/resources/PodFilePreview.tsx +4 -1
- package/src/components/resources/PodFilesystemModal.tsx +3 -2
- package/src/components/resources/ResourcesView.tsx +2 -1
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/rightsizing/RightsizingScanView.tsx +4 -1
- package/src/components/settings/ConnectionHeadersEditor.tsx +109 -0
- package/src/components/settings/FormSaveActions.tsx +76 -0
- package/src/components/settings/IntegrationConnectionForms.tsx +391 -0
- package/src/components/settings/LocalConfigurationDetails.tsx +389 -0
- package/src/components/settings/LocalConnectionSettings.test.tsx +189 -0
- package/src/components/settings/LocalConnectionSettings.tsx +1015 -0
- package/src/components/settings/LocalIntegrationStatus.tsx +50 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/PrometheusConfigField.tsx +30 -0
- package/src/components/settings/PrometheusConnectionForm.tsx +295 -0
- package/src/components/settings/SettingsDialog.tsx +393 -571
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/currency-options.test.ts +1 -1
- package/src/components/settings/currency-options.ts +1 -1
- package/src/components/settings/settings-state.test.ts +17 -10
- package/src/components/settings/settings-state.ts +21 -12
- package/src/components/timeline/TimelineList.test.ts +56 -0
- package/src/components/timeline/TimelineList.tsx +7 -1
- package/src/components/traffic/TrafficFlowList.tsx +5 -21
- package/src/components/traffic/TrafficFlowListContext.tsx +6 -4
- package/src/components/traffic/TrafficGraph.tsx +79 -48
- package/src/components/traffic/TrafficView.tsx +61 -37
- package/src/components/traffic/TrafficWizard.tsx +4 -2
- package/src/components/traffic/trafficFilters.test.ts +154 -1
- package/src/components/traffic/trafficFilters.ts +141 -0
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +17 -1
- package/src/components/ui/UpdateNotification.tsx +70 -21
- package/src/components/ui/command-items.ts +14 -0
- 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/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.tsx +47 -14
- package/src/components/workload/historyPaging.test.ts +72 -0
- package/src/components/workload/historyPaging.ts +147 -0
- package/src/components/workload/useHistoryPaging.test.tsx +81 -0
- package/src/context/ConnectionContext.tsx +2 -1
- package/src/context/RadarUpgrade.tsx +30 -0
- package/src/context/RadarUpgradeHost.tsx +23 -0
- package/src/hooks/usePreviousIntegrationSettings.render.test.tsx +62 -0
- package/src/hooks/usePreviousIntegrationSettings.test.ts +62 -0
- package/src/hooks/usePreviousIntegrationSettings.ts +89 -0
- package/src/index.ts +1 -0
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/navigation.test.ts +17 -1
- package/src/utils/navigation.ts +16 -0
- package/src/utils/version.ts +11 -0
|
@@ -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
|
+
}
|
|
@@ -91,9 +91,9 @@ describe('Cloud dialog connection availability', () => {
|
|
|
91
91
|
expect(requests).not.toContain('/api/cloud/install/discover')
|
|
92
92
|
expect(requests).not.toContain('/api/cloud/install/prepare')
|
|
93
93
|
await act(async () => { button('How it works and what it costs')!.click() })
|
|
94
|
-
expect(document.body.textContent).toContain('
|
|
94
|
+
expect(document.body.textContent).toContain('Radar runs in your cluster and tunnels outward to Radar Cloud')
|
|
95
95
|
expect(document.body.textContent).not.toContain('Setup runs here in the app')
|
|
96
|
-
expect(document.body.textContent).not.toContain('
|
|
96
|
+
expect(document.body.textContent).not.toContain('see what gets installed')
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
it('restores cluster discovery and Connect in the open dialog when the connection recovers', async () => {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useEffect, useId, useState } from 'react'
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
|
-
import { Bell, Check, Globe, History, Sparkles, Users, X } from 'lucide-react'
|
|
3
|
+
import { Bell, Check, Cloud, Globe, History, Sparkles, Users, X } from 'lucide-react'
|
|
4
4
|
import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
5
5
|
import { DialogPortal } from '@skyhook-io/k8s-ui/components/ui/DialogPortal'
|
|
6
6
|
import { Tooltip } from './ui/Tooltip'
|
|
7
7
|
import { CloudConnectFlow } from './CloudConnectFlow'
|
|
8
|
+
import { SelfManagedStart } from './SelfManagedStart'
|
|
8
9
|
import {
|
|
9
10
|
type Handoff,
|
|
10
|
-
SELF_HOSTED_DOCS_URL,
|
|
11
11
|
exitFor,
|
|
12
12
|
handoffForBlocked,
|
|
13
13
|
handoffForPrepareError,
|
|
@@ -33,7 +33,9 @@ import {
|
|
|
33
33
|
} from '../api/client'
|
|
34
34
|
|
|
35
35
|
// OSS → Cloud funnel: a quiet globe button in the top bar that opens a modal
|
|
36
|
-
// pitching Radar Cloud
|
|
36
|
+
// pitching Radar Cloud: Radar for you and your team, every cluster, around the
|
|
37
|
+
// clock. Alerts and hosted investigations lead because they are the two things
|
|
38
|
+
// the OSS binary cannot do at all. Two lanes (capabilities.cloudConnect): "driver" runs
|
|
37
39
|
// the in-product connect flow against this server; "wizard" links to the Hub's
|
|
38
40
|
// connect wizard.
|
|
39
41
|
//
|
|
@@ -58,7 +60,7 @@ const FALLBACK_APP_URL = 'https://app.radarhq.io'
|
|
|
58
60
|
const DEFAULT_ASSURANCES = [
|
|
59
61
|
'Secure outbound-only tunnel',
|
|
60
62
|
'Disconnect and delete your data anytime',
|
|
61
|
-
'SOC 2 Type II',
|
|
63
|
+
'SOC 2 Type II · SSO & SCIM on Enterprise',
|
|
62
64
|
'3 clusters free, no card required',
|
|
63
65
|
]
|
|
64
66
|
// Other OSS surfaces (a GitOps app that deploys to another cluster, say)
|
|
@@ -71,6 +73,7 @@ export function openCloudFunnel() {
|
|
|
71
73
|
window.dispatchEvent(new Event(OPEN_EVENT))
|
|
72
74
|
}
|
|
73
75
|
const ABOUT_URL = 'https://radarhq.io/about'
|
|
76
|
+
const BENCHMARK_URL = 'https://radarhq.io/benchmark'
|
|
74
77
|
const PRICING_URL = 'https://radarhq.io/pricing'
|
|
75
78
|
const SEEN_KEY = 'radar.cloudFunnel.seen'
|
|
76
79
|
|
|
@@ -99,6 +102,7 @@ export function CloudFunnelButton() {
|
|
|
99
102
|
const [open, setOpen] = useState(false)
|
|
100
103
|
const [seen, setSeen] = useState(readSeen)
|
|
101
104
|
const [inFlowView, setInFlowView] = useState(false)
|
|
105
|
+
const [selfManaged, setSelfManaged] = useState(false)
|
|
102
106
|
const [blocked, setBlocked] = useState<CloudInstallBlocked | null>(null)
|
|
103
107
|
// Set once an in-app attempt has ended without connecting, naming what
|
|
104
108
|
// happened (a flow failure kind, a blocked plan, a canceled plan, or the
|
|
@@ -191,6 +195,7 @@ export function CloudFunnelButton() {
|
|
|
191
195
|
|
|
192
196
|
const openModal = () => {
|
|
193
197
|
setOpen(true)
|
|
198
|
+
setSelfManaged(false)
|
|
194
199
|
setSeen(true)
|
|
195
200
|
markSeen()
|
|
196
201
|
// Re-open lands on a live flow if one is running.
|
|
@@ -282,14 +287,24 @@ export function CloudFunnelButton() {
|
|
|
282
287
|
<>
|
|
283
288
|
{/* Tooltip is suppressed while the modal is open — it portals above the
|
|
284
289
|
modal backdrop and would otherwise paint on top of the dialog. */}
|
|
285
|
-
<Tooltip
|
|
290
|
+
<Tooltip
|
|
291
|
+
content={
|
|
292
|
+
<>
|
|
293
|
+
<span className="block font-semibold">Meet Radar Cloud</span>
|
|
294
|
+
See all your clusters in one place, share with your team, and get alerts and automatic AI investigations.
|
|
295
|
+
</>
|
|
296
|
+
}
|
|
297
|
+
delay={100}
|
|
298
|
+
position="bottom"
|
|
299
|
+
disabled={open}
|
|
300
|
+
>
|
|
286
301
|
<button
|
|
287
302
|
onClick={openModal}
|
|
288
303
|
aria-label="Radar Cloud"
|
|
289
304
|
aria-haspopup="dialog"
|
|
290
305
|
className="relative p-1.5 rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
291
306
|
>
|
|
292
|
-
<
|
|
307
|
+
<Cloud className="w-4 h-4" />
|
|
293
308
|
{cloudInstallActive(flow?.state) ? (
|
|
294
309
|
<span className="absolute top-0.5 right-0.5 w-[7px] h-[7px] rounded-full bg-emerald-500 animate-pulse motion-reduce:animate-none" />
|
|
295
310
|
) : (
|
|
@@ -305,6 +320,7 @@ export function CloudFunnelButton() {
|
|
|
305
320
|
<DialogPortal
|
|
306
321
|
open={open}
|
|
307
322
|
onClose={() => setOpen(false)}
|
|
323
|
+
ariaLabel="Radar Cloud"
|
|
308
324
|
className="w-[580px] max-w-full max-h-[calc(100vh-2rem)] overflow-hidden flex flex-col"
|
|
309
325
|
>
|
|
310
326
|
<button
|
|
@@ -332,6 +348,13 @@ export function CloudFunnelButton() {
|
|
|
332
348
|
onExit={() => exitFlow(outcomeOf(flowForView))}
|
|
333
349
|
/>
|
|
334
350
|
</div>
|
|
351
|
+
) : selfManaged ? (
|
|
352
|
+
<>
|
|
353
|
+
<div className="shrink-0 px-8 pt-7">
|
|
354
|
+
<Eyebrow />
|
|
355
|
+
</div>
|
|
356
|
+
<SelfManagedStart appUrl={appUrl} onBack={() => setSelfManaged(false)} />
|
|
357
|
+
</>
|
|
335
358
|
) : (
|
|
336
359
|
<>
|
|
337
360
|
<div className="min-h-0 overflow-y-auto">
|
|
@@ -360,6 +383,7 @@ export function CloudFunnelButton() {
|
|
|
360
383
|
// in-cluster, so the CTA would escape before classification.
|
|
361
384
|
selfLoading={inCluster && self.isPending}
|
|
362
385
|
onConnect={startConnect}
|
|
386
|
+
onSelfManaged={() => setSelfManaged(true)}
|
|
363
387
|
onLater={() => setOpen(false)}
|
|
364
388
|
/>
|
|
365
389
|
</>
|
|
@@ -395,7 +419,7 @@ function Eyebrow() {
|
|
|
395
419
|
return (
|
|
396
420
|
<div className="flex items-center gap-3 mb-5">
|
|
397
421
|
<RadarSweep />
|
|
398
|
-
<span className="font-mono text-[
|
|
422
|
+
<span className="font-mono text-[16px] tracking-[0.12em] uppercase text-emerald-600 dark:text-emerald-400">Radar Cloud</span>
|
|
399
423
|
</div>
|
|
400
424
|
)
|
|
401
425
|
}
|
|
@@ -417,6 +441,7 @@ function ModalFooter({
|
|
|
417
441
|
discoverPending = false,
|
|
418
442
|
clustersUrl,
|
|
419
443
|
onConnect,
|
|
444
|
+
onSelfManaged,
|
|
420
445
|
onLater,
|
|
421
446
|
}: {
|
|
422
447
|
lane: 'driver' | 'wizard'
|
|
@@ -449,6 +474,7 @@ function ModalFooter({
|
|
|
449
474
|
// settings say it is connected but not where.
|
|
450
475
|
clustersUrl?: string
|
|
451
476
|
onConnect: () => void
|
|
477
|
+
onSelfManaged: () => void
|
|
452
478
|
onLater: () => void
|
|
453
479
|
}) {
|
|
454
480
|
const gitops = self?.ownership === 'gitops'
|
|
@@ -463,7 +489,7 @@ function ModalFooter({
|
|
|
463
489
|
// fast click would escape to signup before we could route this install.
|
|
464
490
|
const selfPending = selfLoading === true
|
|
465
491
|
return (
|
|
466
|
-
<div className="shrink-0 px-8 py-
|
|
492
|
+
<div className="shrink-0 px-8 py-6 bg-theme-base border-t border-theme-border">
|
|
467
493
|
{self && self.ownership !== 'unknown' && (
|
|
468
494
|
<div className="mb-3.5 card-inner p-3 text-[12px] leading-relaxed text-theme-text-secondary">
|
|
469
495
|
{ambiguous ? (
|
|
@@ -551,17 +577,6 @@ function ModalFooter({
|
|
|
551
577
|
inspect step and a plan the user approves in the browser. */}
|
|
552
578
|
{prepareFailed ? 'Try again' : 'Connect this cluster…'}
|
|
553
579
|
</button>
|
|
554
|
-
{/* Always visible: the browser wizard is a different workflow, not
|
|
555
|
-
a recovery path — install-averse operators need the door before
|
|
556
|
-
anything fails, or they close the modal instead. */}
|
|
557
|
-
<a
|
|
558
|
-
href={driverBrowserUrl}
|
|
559
|
-
target="_blank"
|
|
560
|
-
rel="noopener noreferrer"
|
|
561
|
-
className="whitespace-nowrap text-[12.5px] text-theme-text-secondary hover:text-theme-text-primary hover:underline underline-offset-2 transition-colors"
|
|
562
|
-
>
|
|
563
|
-
or set up in the browser
|
|
564
|
-
</a>
|
|
565
580
|
</>
|
|
566
581
|
) : cliOnly ? null : (
|
|
567
582
|
<a
|
|
@@ -575,6 +590,19 @@ function ModalFooter({
|
|
|
575
590
|
{lane === 'driver' ? 'Continue in Radar Cloud' : self?.ownership === 'helm' || gitops ? 'Connect this cluster' : 'Try Cloud free'}
|
|
576
591
|
</a>
|
|
577
592
|
)}
|
|
593
|
+
{/* Self-Managed is a paid product with its own trial, so it gets a real
|
|
594
|
+
button beside the hosted CTA rather than a link near the exit, the
|
|
595
|
+
way GitLab and n8n show their self-managed tiers. Outlined, so the
|
|
596
|
+
hosted CTA stays the default. */}
|
|
597
|
+
{!(lane === 'driver' && alreadyConnected) && (
|
|
598
|
+
<button
|
|
599
|
+
type="button"
|
|
600
|
+
onClick={onSelfManaged}
|
|
601
|
+
className="whitespace-nowrap px-4 py-2 rounded-[10px] border border-theme-border bg-theme-surface hover:bg-theme-hover text-theme-text-primary text-[13px] font-medium transition-colors"
|
|
602
|
+
>
|
|
603
|
+
Run Radar Cloud yourself
|
|
604
|
+
</button>
|
|
605
|
+
)}
|
|
578
606
|
<button onClick={onLater} className="ml-auto whitespace-nowrap text-[12px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors">
|
|
579
607
|
{lane === 'driver' && alreadyConnected ? 'Close' : 'Maybe later'}
|
|
580
608
|
</button>
|
|
@@ -592,16 +620,27 @@ function ModalFooter({
|
|
|
592
620
|
fulfills. Sits next to the button whose click it de-risks. */}
|
|
593
621
|
{lane === 'driver' && clusterConnected && !alreadyConnected && (
|
|
594
622
|
<p className="mt-2.5 text-[11px] leading-relaxed text-theme-text-tertiary">
|
|
595
|
-
Nothing installs on click.
|
|
596
|
-
{clusterName ? <span className="text-theme-text-secondary">{clusterName}</span> : '
|
|
597
|
-
|
|
623
|
+
Nothing installs on click. You'll see what gets installed in{' '}
|
|
624
|
+
{clusterName ? <span className="text-theme-text-secondary">{clusterName}</span> : 'this cluster'} and approve
|
|
625
|
+
it before anything changes.{' '}
|
|
626
|
+
{/* Always offered: the browser wizard is a different workflow, not a
|
|
627
|
+
recovery path. Install-averse operators need this door before
|
|
628
|
+
anything fails, or they close the dialog instead. */}
|
|
629
|
+
<a
|
|
630
|
+
href={driverBrowserUrl}
|
|
631
|
+
target="_blank"
|
|
632
|
+
rel="noopener noreferrer"
|
|
633
|
+
className="whitespace-nowrap text-theme-text-secondary underline underline-offset-2 hover:text-theme-text-primary"
|
|
634
|
+
>
|
|
635
|
+
Or set up in the browser →
|
|
636
|
+
</a>
|
|
598
637
|
</p>
|
|
599
638
|
)}
|
|
600
639
|
{/* A 2-column grid, not flex-wrap: the long data-locality chip cannot
|
|
601
640
|
share a single row with the other three at this width, and flex
|
|
602
641
|
wrapping strands it as a 3+1 orphan. Two balanced columns read as a
|
|
603
642
|
designed layout at any chip length the Hub sends. */}
|
|
604
|
-
<div className="mt-
|
|
643
|
+
<div className="mt-5 grid grid-cols-2 gap-x-3 gap-y-2 text-[11px] text-theme-text-tertiary">
|
|
605
644
|
{assuranceItems(assurances).map((item) => (
|
|
606
645
|
<span key={item} className="flex items-center gap-1">
|
|
607
646
|
<Check className="w-3 h-3 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
@@ -622,26 +661,31 @@ function PitchBody({ lane, freeTier }: { lane: 'driver' | 'wizard'; freeTier?: s
|
|
|
622
661
|
// unreachable or predates the field).
|
|
623
662
|
const freeLine = freeTier || 'free for 3 clusters'
|
|
624
663
|
// lead is the scannable anchor (medium, primary); rest stays secondary.
|
|
664
|
+
// Each bullet is one line at the dialog's width, so the first screen stays
|
|
665
|
+
// skimmable. Alerts and investigations lead: they are the two things the
|
|
666
|
+
// OSS binary cannot do at all. Team, fleet and history follow.
|
|
625
667
|
const highlights = [
|
|
626
|
-
{ icon:
|
|
627
|
-
{ icon:
|
|
628
|
-
{ icon:
|
|
629
|
-
{ icon:
|
|
630
|
-
{ icon:
|
|
668
|
+
{ icon: Bell, lead: 'Alerts', rest: ' to Slack or any webhook. One per failure, not per crash-looping pod.' },
|
|
669
|
+
{ icon: Sparkles, lead: 'AI investigations', rest: ': root cause with evidence, automatic on alerts or on demand' },
|
|
670
|
+
{ icon: Users, lead: 'Your team', rest: ': share any view with just a link, scoped by your existing RBAC' },
|
|
671
|
+
{ icon: Globe, lead: 'Every cluster', rest: ': the whole fleet in one view and one MCP endpoint' },
|
|
672
|
+
{ icon: History, lead: 'History', rest: ': events and changes kept long after Kubernetes deletes them' },
|
|
631
673
|
]
|
|
632
674
|
return (
|
|
633
|
-
<div className="px-8 pt-7 pb-
|
|
675
|
+
<div className="px-8 pt-7 pb-5">
|
|
634
676
|
<Eyebrow />
|
|
635
|
-
<h3 className="text-[22px] font-semibold leading-tight tracking-tight text-theme-text-primary mb-3">
|
|
636
|
-
|
|
677
|
+
<h3 className="text-[22px] font-semibold leading-tight tracking-tight text-theme-text-primary mb-3 text-balance">
|
|
678
|
+
Close the laptop. Radar keeps watching.
|
|
637
679
|
</h3>
|
|
680
|
+
<p className="text-[14px] leading-relaxed text-theme-text-secondary mb-1.5">
|
|
681
|
+
Radar Cloud is the hosted side of Radar: alerts, AI root cause and every cluster in one place, for
|
|
682
|
+
you and your team.
|
|
683
|
+
</p>
|
|
638
684
|
<p className="text-[14px] leading-relaxed text-theme-text-secondary mb-6">
|
|
639
|
-
The hosted side of Radar: your clusters in one place, run by us.
|
|
640
|
-
<br />
|
|
641
685
|
The Radar you're running{' '}
|
|
642
686
|
<b className="text-theme-text-primary font-semibold">stays free and open source, always.</b>
|
|
643
687
|
</p>
|
|
644
|
-
<ul className="space-y-2.5 mb-
|
|
688
|
+
<ul className="space-y-2.5 mb-3">
|
|
645
689
|
{highlights.map(({ icon: Icon, lead, rest }) => (
|
|
646
690
|
<li key={lead} className="flex items-start gap-2.5 text-[13px] leading-relaxed text-theme-text-secondary">
|
|
647
691
|
<Icon className="w-4 h-4 shrink-0 mt-[3px] text-emerald-600 dark:text-emerald-400" />
|
|
@@ -670,46 +714,46 @@ function PitchBody({ lane, freeTier }: { lane: 'driver' | 'wizard'; freeTier?: s
|
|
|
670
714
|
{/* No heading: the disclosure's own label already names this one. */}
|
|
671
715
|
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
672
716
|
{lane === 'driver'
|
|
673
|
-
?
|
|
674
|
-
:
|
|
717
|
+
? "Setup runs here in the app: Radar is installed in your cluster and tunnels outward to Radar Cloud, so there's no ingress to open."
|
|
718
|
+
: "Radar runs in your cluster and tunnels outward to Radar Cloud, so there's no ingress to open."}{' '}
|
|
719
|
+
Live views are fetched from your cluster when someone opens them, within their permissions. Radar
|
|
720
|
+
Cloud stores event history, changes and investigation results, so you can go back in time to
|
|
721
|
+
debug.
|
|
722
|
+
</p>
|
|
723
|
+
</section>
|
|
724
|
+
<section>
|
|
725
|
+
<h4 className="text-[12.5px] font-semibold text-theme-text-primary mb-0.5">Investigations</h4>
|
|
726
|
+
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
727
|
+
In our public benchmark, the same agent diagnosed faults 3× faster on average with Radar than with
|
|
728
|
+
plain kubectl, and Radar answered 1.6–3.6× faster on average than the other AI SRE tools we tested.
|
|
729
|
+
Run it automatically on the alert rules you choose, or on demand from any issue, with no CLI or API
|
|
730
|
+
key.{' '}
|
|
731
|
+
<a href={BENCHMARK_URL} target="_blank" rel="noopener noreferrer" className="whitespace-nowrap text-theme-text-secondary underline underline-offset-2 hover:text-theme-text-primary">
|
|
732
|
+
See the benchmarks →
|
|
733
|
+
</a>
|
|
675
734
|
</p>
|
|
676
735
|
</section>
|
|
677
736
|
<section>
|
|
678
737
|
<h4 className="text-[12.5px] font-semibold text-theme-text-primary mb-0.5">What it costs</h4>
|
|
679
738
|
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
680
|
-
Radar Cloud is {freeLine}
|
|
681
|
-
|
|
682
|
-
Apache 2.0 either way: every feature, forever.{' '}
|
|
739
|
+
Radar Cloud is {freeLine}, including 100 investigations a month. Paid plans add investigations for
|
|
740
|
+
every cluster, with optional $1 overage, plus enterprise features like SSO, SCIM and audit logs.{' '}
|
|
683
741
|
<a href={PRICING_URL} target="_blank" rel="noopener noreferrer" className="whitespace-nowrap text-theme-text-secondary underline underline-offset-2 hover:text-theme-text-primary">
|
|
684
742
|
See pricing →
|
|
685
743
|
</a>
|
|
686
744
|
</p>
|
|
687
745
|
</section>
|
|
688
746
|
<section>
|
|
689
|
-
|
|
747
|
+
{/* No heading: "built by Skyhook" already answers who is behind it. */}
|
|
690
748
|
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
691
|
-
|
|
692
|
-
humans, the kind you can actually talk to.{' '}
|
|
749
|
+
Built in the open by Skyhook, a CNCF Silver member and a small team you can actually talk to.{' '}
|
|
693
750
|
<a href={ABOUT_URL} target="_blank" rel="noopener noreferrer" className="whitespace-nowrap text-theme-text-secondary underline underline-offset-2 hover:text-theme-text-primary">
|
|
694
751
|
Meet us →
|
|
695
752
|
</a>
|
|
696
753
|
</p>
|
|
697
754
|
</section>
|
|
698
|
-
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
699
|
-
Prefer your own VPC? You can run the Radar Cloud control plane yourself.{' '}
|
|
700
|
-
<a href={SELF_HOSTED_DOCS_URL} target="_blank" rel="noopener noreferrer" className="text-theme-text-secondary underline underline-offset-2 hover:text-theme-text-primary">
|
|
701
|
-
Read the docs
|
|
702
|
-
</a>
|
|
703
|
-
.
|
|
704
|
-
</p>
|
|
705
755
|
</div>
|
|
706
756
|
</Collapse>
|
|
707
|
-
<div className="mb-5 border-l-2 border-emerald-500/40 pl-3.5">
|
|
708
|
-
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
709
|
-
Don't need Radar Cloud right now? That's fine. What you're running is already a full product,
|
|
710
|
-
not a demo. We're here if you ever do.
|
|
711
|
-
</p>
|
|
712
|
-
</div>
|
|
713
757
|
</div>
|
|
714
758
|
)
|
|
715
759
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
const state = vi.hoisted(() => ({
|
|
6
|
+
mode: 'local' as string | undefined,
|
|
7
|
+
authEnabled: false as boolean | undefined,
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
vi.mock('@skyhook-io/k8s-ui', () => ({
|
|
11
|
+
NamespacePicker: ({ limitedListHelp }: { limitedListHelp?: ReactNode }) => <div>{limitedListHelp ?? 'no-help'}</div>,
|
|
12
|
+
}))
|
|
13
|
+
vi.mock('../api/client', () => ({
|
|
14
|
+
useNamespaceScope: () => ({ data: undefined, isLoading: false }),
|
|
15
|
+
useSetActiveNamespace: () => ({ isPending: false, mutate: vi.fn() }),
|
|
16
|
+
useCapabilities: () => ({ data: state.mode === undefined ? undefined : { deployment: { mode: state.mode } } }),
|
|
17
|
+
useAuthMe: () => ({ data: state.authEnabled === undefined ? undefined : { authEnabled: state.authEnabled } }),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
import { NamespaceSwitcher } from './NamespaceSwitcher'
|
|
21
|
+
|
|
22
|
+
describe('NamespaceSwitcher namespace help', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
state.mode = 'local'
|
|
25
|
+
state.authEnabled = false
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('links to the namespace docs in local Radar without auth', () => {
|
|
29
|
+
const html = renderToStaticMarkup(<NamespaceSwitcher />)
|
|
30
|
+
expect(html).toContain('How to add namespaces')
|
|
31
|
+
expect(html).toContain('#namespaces-missing-from-the-picker')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// --namespaces and config.json belong to whoever runs the server; on a shared
|
|
35
|
+
// install the viewer can't use them.
|
|
36
|
+
it.each([
|
|
37
|
+
['in-cluster Radar', 'in-cluster', false],
|
|
38
|
+
['Radar Cloud', 'cloud', true],
|
|
39
|
+
['auth-enabled local Radar', 'local', true],
|
|
40
|
+
['capabilities still loading', undefined, false],
|
|
41
|
+
['auth state still loading', 'local', undefined],
|
|
42
|
+
] as const)('offers no help for %s', (_label, mode, authEnabled) => {
|
|
43
|
+
state.mode = mode
|
|
44
|
+
state.authEnabled = authEnabled
|
|
45
|
+
expect(renderToStaticMarkup(<NamespaceSwitcher />)).toContain('no-help')
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { forwardRef } from 'react'
|
|
2
2
|
import { NamespacePicker, type NamespacePickerHandle } from '@skyhook-io/k8s-ui'
|
|
3
|
-
import { useNamespaceScope, useSetActiveNamespace } from '../api/client'
|
|
3
|
+
import { useAuthMe, useCapabilities, useNamespaceScope, useSetActiveNamespace } from '../api/client'
|
|
4
4
|
|
|
5
5
|
export type NamespaceSwitcherHandle = NamespacePickerHandle
|
|
6
6
|
|
|
7
|
+
const NAMESPACES_HELP_URL = 'https://radarhq.io/docs/configuration/files#namespaces-missing-from-the-picker'
|
|
8
|
+
|
|
7
9
|
interface NamespaceSwitcherProps {
|
|
8
10
|
className?: string
|
|
9
11
|
disabled?: boolean
|
|
@@ -24,6 +26,19 @@ export const NamespaceSwitcher = forwardRef<NamespaceSwitcherHandle, NamespaceSw
|
|
|
24
26
|
const { data: scope, isLoading } = useNamespaceScope()
|
|
25
27
|
const setActive = useSetActiveNamespace()
|
|
26
28
|
|
|
29
|
+
const { data: capabilities } = useCapabilities()
|
|
30
|
+
const { data: authMe } = useAuthMe()
|
|
31
|
+
// --namespaces and ~/.radar/config.json only reach a Radar the user launched
|
|
32
|
+
// themselves. With auth enabled, a non-authoritative list is also the
|
|
33
|
+
// per-user RBAC filter on a shared install, where neither applies.
|
|
34
|
+
const canConfigureNamespaces = capabilities?.deployment?.mode === 'local' && authMe?.authEnabled === false
|
|
35
|
+
|
|
36
|
+
const limitedListHelp = canConfigureNamespaces ? (
|
|
37
|
+
<a href={NAMESPACES_HELP_URL} target="_blank" rel="noreferrer" className="text-accent-text hover:underline">
|
|
38
|
+
How to add namespaces
|
|
39
|
+
</a>
|
|
40
|
+
) : undefined
|
|
41
|
+
|
|
27
42
|
return (
|
|
28
43
|
<NamespacePicker
|
|
29
44
|
ref={ref}
|
|
@@ -36,6 +51,7 @@ export const NamespaceSwitcher = forwardRef<NamespaceSwitcherHandle, NamespaceSw
|
|
|
36
51
|
className={className}
|
|
37
52
|
variant={variant}
|
|
38
53
|
label={label}
|
|
54
|
+
limitedListHelp={limitedListHelp}
|
|
39
55
|
/>
|
|
40
56
|
)
|
|
41
57
|
})
|