@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
|
@@ -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,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
signupUrlFor as buildSignupUrl,
|
|
16
16
|
} from './cloudConnectHandoff'
|
|
17
17
|
import { showApiError } from './ui/Toast'
|
|
18
|
+
import { useConnection } from '../context/ConnectionContext'
|
|
18
19
|
import {
|
|
19
20
|
ApiError,
|
|
20
21
|
cloudInstallActive,
|
|
@@ -32,7 +33,9 @@ import {
|
|
|
32
33
|
} from '../api/client'
|
|
33
34
|
|
|
34
35
|
// OSS → Cloud funnel: a quiet globe button in the top bar that opens a modal
|
|
35
|
-
// 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
|
|
36
39
|
// the in-product connect flow against this server; "wizard" links to the Hub's
|
|
37
40
|
// connect wizard.
|
|
38
41
|
//
|
|
@@ -57,7 +60,7 @@ const FALLBACK_APP_URL = 'https://app.radarhq.io'
|
|
|
57
60
|
const DEFAULT_ASSURANCES = [
|
|
58
61
|
'Secure outbound-only tunnel',
|
|
59
62
|
'Disconnect and delete your data anytime',
|
|
60
|
-
'SOC 2 Type II',
|
|
63
|
+
'SOC 2 Type II · SSO & SCIM on Enterprise',
|
|
61
64
|
'3 clusters free, no card required',
|
|
62
65
|
]
|
|
63
66
|
// Other OSS surfaces (a GitOps app that deploys to another cluster, say)
|
|
@@ -70,6 +73,7 @@ export function openCloudFunnel() {
|
|
|
70
73
|
window.dispatchEvent(new Event(OPEN_EVENT))
|
|
71
74
|
}
|
|
72
75
|
const ABOUT_URL = 'https://radarhq.io/about'
|
|
76
|
+
const BENCHMARK_URL = 'https://radarhq.io/benchmark'
|
|
73
77
|
const PRICING_URL = 'https://radarhq.io/pricing'
|
|
74
78
|
const SEEN_KEY = 'radar.cloudFunnel.seen'
|
|
75
79
|
|
|
@@ -98,6 +102,7 @@ export function CloudFunnelButton() {
|
|
|
98
102
|
const [open, setOpen] = useState(false)
|
|
99
103
|
const [seen, setSeen] = useState(readSeen)
|
|
100
104
|
const [inFlowView, setInFlowView] = useState(false)
|
|
105
|
+
const [selfManaged, setSelfManaged] = useState(false)
|
|
101
106
|
const [blocked, setBlocked] = useState<CloudInstallBlocked | null>(null)
|
|
102
107
|
// Set once an in-app attempt has ended without connecting, naming what
|
|
103
108
|
// happened (a flow failure kind, a blocked plan, a canceled plan, or the
|
|
@@ -113,6 +118,8 @@ export function CloudFunnelButton() {
|
|
|
113
118
|
const capabilities = useCapabilities()
|
|
114
119
|
const clusterInfo = useClusterInfo()
|
|
115
120
|
const lane = capabilities.data?.cloudConnect?.lane ?? 'wizard'
|
|
121
|
+
const clusterConnected = useConnection().connection.state === 'connected'
|
|
122
|
+
const pitchLane = lane === 'driver' && !clusterConnected ? 'wizard' : lane
|
|
116
123
|
const appUrl = capabilities.data?.cloudConnect?.appUrl || FALLBACK_APP_URL
|
|
117
124
|
// utm_content names the link that was clicked. It travels only in the link
|
|
118
125
|
// the user opens; Radar sends nothing on its own. Only the blocked card may
|
|
@@ -124,7 +131,7 @@ export function CloudFunnelButton() {
|
|
|
124
131
|
// learns that someone opened it, which is congruent with what the dialog is
|
|
125
132
|
// for; it must not learn that Radar is merely running.
|
|
126
133
|
const connectInfo = useCloudConnectInfo(capabilities.data?.cloudConnect?.apiUrl, open, {
|
|
127
|
-
lane,
|
|
134
|
+
lane: pitchLane,
|
|
128
135
|
mode: capabilities.data?.deployment?.mode,
|
|
129
136
|
})
|
|
130
137
|
|
|
@@ -138,14 +145,14 @@ export function CloudFunnelButton() {
|
|
|
138
145
|
// carries Cloud settings — installed by a colleague after a handoff, or from
|
|
139
146
|
// another machine. The plan would refuse to install over it; better to say
|
|
140
147
|
// "already connected" and point at it than to offer a click that ends blocked.
|
|
141
|
-
const discovered = useCloudInstallDiscover(open && lane === 'driver', clusterInfo.data?.context)
|
|
148
|
+
const discovered = useCloudInstallDiscover(open && lane === 'driver' && clusterConnected, clusterInfo.data?.context)
|
|
142
149
|
// The server ranks these: one it can link to first. A failed lookup is
|
|
143
150
|
// not a verdict — the CTA returns and the plan does its own inspection —
|
|
144
151
|
// and neither is the previous opening's answer: this component outlives
|
|
145
152
|
// the dialog, so a reopen refetches over cached data, and the gate must
|
|
146
153
|
// hold for that refetch too (isFetching, not isPending) while a refetch
|
|
147
154
|
// that fails must not keep showing what it found last time.
|
|
148
|
-
const alreadyConnected = discovered.isError ? undefined : discovered.data?.connected[0]
|
|
155
|
+
const alreadyConnected = !clusterConnected || discovered.isError ? undefined : discovered.data?.connected[0]
|
|
149
156
|
const discoverPending = lane === 'driver' && discovered.isFetching
|
|
150
157
|
|
|
151
158
|
// The flow is server-owned: polling here both drives the live progress view
|
|
@@ -188,6 +195,7 @@ export function CloudFunnelButton() {
|
|
|
188
195
|
|
|
189
196
|
const openModal = () => {
|
|
190
197
|
setOpen(true)
|
|
198
|
+
setSelfManaged(false)
|
|
191
199
|
setSeen(true)
|
|
192
200
|
markSeen()
|
|
193
201
|
// Re-open lands on a live flow if one is running.
|
|
@@ -279,14 +287,24 @@ export function CloudFunnelButton() {
|
|
|
279
287
|
<>
|
|
280
288
|
{/* Tooltip is suppressed while the modal is open — it portals above the
|
|
281
289
|
modal backdrop and would otherwise paint on top of the dialog. */}
|
|
282
|
-
<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 in Slack.
|
|
295
|
+
</>
|
|
296
|
+
}
|
|
297
|
+
delay={100}
|
|
298
|
+
position="bottom"
|
|
299
|
+
disabled={open}
|
|
300
|
+
>
|
|
283
301
|
<button
|
|
284
302
|
onClick={openModal}
|
|
285
303
|
aria-label="Radar Cloud"
|
|
286
304
|
aria-haspopup="dialog"
|
|
287
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"
|
|
288
306
|
>
|
|
289
|
-
<
|
|
307
|
+
<Cloud className="w-4 h-4" />
|
|
290
308
|
{cloudInstallActive(flow?.state) ? (
|
|
291
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" />
|
|
292
310
|
) : (
|
|
@@ -302,6 +320,7 @@ export function CloudFunnelButton() {
|
|
|
302
320
|
<DialogPortal
|
|
303
321
|
open={open}
|
|
304
322
|
onClose={() => setOpen(false)}
|
|
323
|
+
ariaLabel="Radar Cloud"
|
|
305
324
|
className="w-[580px] max-w-full max-h-[calc(100vh-2rem)] overflow-hidden flex flex-col"
|
|
306
325
|
>
|
|
307
326
|
<button
|
|
@@ -329,13 +348,21 @@ export function CloudFunnelButton() {
|
|
|
329
348
|
onExit={() => exitFlow(outcomeOf(flowForView))}
|
|
330
349
|
/>
|
|
331
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
|
+
</>
|
|
332
358
|
) : (
|
|
333
359
|
<>
|
|
334
360
|
<div className="min-h-0 overflow-y-auto">
|
|
335
|
-
<PitchBody lane={
|
|
361
|
+
<PitchBody lane={pitchLane} freeTier={connectInfo.data?.freeTier} />
|
|
336
362
|
</div>
|
|
337
363
|
<ModalFooter
|
|
338
364
|
lane={lane}
|
|
365
|
+
clusterConnected={clusterConnected}
|
|
339
366
|
signupUrl={signupUrl}
|
|
340
367
|
// One link name whether or not an attempt preceded the click; the
|
|
341
368
|
// outcome, when present, is what says an attempt happened.
|
|
@@ -356,6 +383,7 @@ export function CloudFunnelButton() {
|
|
|
356
383
|
// in-cluster, so the CTA would escape before classification.
|
|
357
384
|
selfLoading={inCluster && self.isPending}
|
|
358
385
|
onConnect={startConnect}
|
|
386
|
+
onSelfManaged={() => setSelfManaged(true)}
|
|
359
387
|
onLater={() => setOpen(false)}
|
|
360
388
|
/>
|
|
361
389
|
</>
|
|
@@ -391,13 +419,14 @@ function Eyebrow() {
|
|
|
391
419
|
return (
|
|
392
420
|
<div className="flex items-center gap-3 mb-5">
|
|
393
421
|
<RadarSweep />
|
|
394
|
-
<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>
|
|
395
423
|
</div>
|
|
396
424
|
)
|
|
397
425
|
}
|
|
398
426
|
|
|
399
427
|
function ModalFooter({
|
|
400
428
|
lane,
|
|
429
|
+
clusterConnected,
|
|
401
430
|
signupUrl,
|
|
402
431
|
driverBrowserUrl,
|
|
403
432
|
prepareFailed,
|
|
@@ -412,9 +441,11 @@ function ModalFooter({
|
|
|
412
441
|
discoverPending = false,
|
|
413
442
|
clustersUrl,
|
|
414
443
|
onConnect,
|
|
444
|
+
onSelfManaged,
|
|
415
445
|
onLater,
|
|
416
446
|
}: {
|
|
417
447
|
lane: 'driver' | 'wizard'
|
|
448
|
+
clusterConnected: boolean
|
|
418
449
|
signupUrl: string
|
|
419
450
|
// Same destination as signupUrl, distinct utm_content, and the outcome of
|
|
420
451
|
// an in-app attempt when one preceded this render.
|
|
@@ -443,6 +474,7 @@ function ModalFooter({
|
|
|
443
474
|
// settings say it is connected but not where.
|
|
444
475
|
clustersUrl?: string
|
|
445
476
|
onConnect: () => void
|
|
477
|
+
onSelfManaged: () => void
|
|
446
478
|
onLater: () => void
|
|
447
479
|
}) {
|
|
448
480
|
const gitops = self?.ownership === 'gitops'
|
|
@@ -457,7 +489,7 @@ function ModalFooter({
|
|
|
457
489
|
// fast click would escape to signup before we could route this install.
|
|
458
490
|
const selfPending = selfLoading === true
|
|
459
491
|
return (
|
|
460
|
-
<div className="shrink-0 px-8 py-
|
|
492
|
+
<div className="shrink-0 px-8 py-6 bg-theme-base border-t border-theme-border">
|
|
461
493
|
{self && self.ownership !== 'unknown' && (
|
|
462
494
|
<div className="mb-3.5 card-inner p-3 text-[12px] leading-relaxed text-theme-text-secondary">
|
|
463
495
|
{ambiguous ? (
|
|
@@ -534,7 +566,7 @@ function ModalFooter({
|
|
|
534
566
|
{alreadyConnected.clusterUrl ? 'Open in Radar Cloud' : 'Open Radar Cloud'}
|
|
535
567
|
</a>
|
|
536
568
|
)
|
|
537
|
-
) : lane === 'driver' ? (
|
|
569
|
+
) : lane === 'driver' && clusterConnected ? (
|
|
538
570
|
<>
|
|
539
571
|
<button
|
|
540
572
|
onClick={onConnect}
|
|
@@ -545,30 +577,32 @@ function ModalFooter({
|
|
|
545
577
|
inspect step and a plan the user approves in the browser. */}
|
|
546
578
|
{prepareFailed ? 'Try again' : 'Connect this cluster…'}
|
|
547
579
|
</button>
|
|
548
|
-
{/* Always visible: the browser wizard is a different workflow, not
|
|
549
|
-
a recovery path — install-averse operators need the door before
|
|
550
|
-
anything fails, or they close the modal instead. */}
|
|
551
|
-
<a
|
|
552
|
-
href={driverBrowserUrl}
|
|
553
|
-
target="_blank"
|
|
554
|
-
rel="noopener noreferrer"
|
|
555
|
-
className="whitespace-nowrap text-[12.5px] text-theme-text-secondary hover:text-theme-text-primary hover:underline underline-offset-2 transition-colors"
|
|
556
|
-
>
|
|
557
|
-
or set up in the browser
|
|
558
|
-
</a>
|
|
559
580
|
</>
|
|
560
581
|
) : cliOnly ? null : (
|
|
561
582
|
<a
|
|
562
|
-
href={self?.wizardUrl || signupUrl}
|
|
583
|
+
href={lane === 'driver' ? driverBrowserUrl : self?.wizardUrl || signupUrl}
|
|
563
584
|
aria-disabled={selfPending}
|
|
564
585
|
target="_blank"
|
|
565
586
|
rel="noopener noreferrer"
|
|
566
587
|
onClick={(e) => { if (selfPending) e.preventDefault() }}
|
|
567
588
|
className={`px-5 py-2 rounded-[10px] bg-emerald-500 hover:bg-emerald-400 text-emerald-950 text-[13.5px] font-bold shadow-[0_0_22px_rgba(16,185,129,0.35)] hover:shadow-[0_0_30px_rgba(16,185,129,0.5)] hover:-translate-y-px transition-all ${selfPending ? 'opacity-60 pointer-events-none' : ''}`}
|
|
568
589
|
>
|
|
569
|
-
{self?.ownership === 'helm' || gitops ? 'Connect this cluster' : 'Try Cloud free'}
|
|
590
|
+
{lane === 'driver' ? 'Continue in Radar Cloud' : self?.ownership === 'helm' || gitops ? 'Connect this cluster' : 'Try Cloud free'}
|
|
570
591
|
</a>
|
|
571
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
|
+
)}
|
|
572
606
|
<button onClick={onLater} className="ml-auto whitespace-nowrap text-[12px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors">
|
|
573
607
|
{lane === 'driver' && alreadyConnected ? 'Close' : 'Maybe later'}
|
|
574
608
|
</button>
|
|
@@ -584,18 +618,29 @@ function ModalFooter({
|
|
|
584
618
|
)}
|
|
585
619
|
{/* Mechanics, not marketing: a falsifiable claim the plan card then
|
|
586
620
|
fulfills. Sits next to the button whose click it de-risks. */}
|
|
587
|
-
{lane === 'driver' && !alreadyConnected && (
|
|
621
|
+
{lane === 'driver' && clusterConnected && !alreadyConnected && (
|
|
588
622
|
<p className="mt-2.5 text-[11px] leading-relaxed text-theme-text-tertiary">
|
|
589
|
-
Nothing installs on click.
|
|
590
|
-
{clusterName ? <span className="text-theme-text-secondary">{clusterName}</span> : '
|
|
591
|
-
|
|
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>
|
|
592
637
|
</p>
|
|
593
638
|
)}
|
|
594
639
|
{/* A 2-column grid, not flex-wrap: the long data-locality chip cannot
|
|
595
640
|
share a single row with the other three at this width, and flex
|
|
596
641
|
wrapping strands it as a 3+1 orphan. Two balanced columns read as a
|
|
597
642
|
designed layout at any chip length the Hub sends. */}
|
|
598
|
-
<div className="mt-
|
|
643
|
+
<div className="mt-5 grid grid-cols-2 gap-x-3 gap-y-2 text-[11px] text-theme-text-tertiary">
|
|
599
644
|
{assuranceItems(assurances).map((item) => (
|
|
600
645
|
<span key={item} className="flex items-center gap-1">
|
|
601
646
|
<Check className="w-3 h-3 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
@@ -616,26 +661,31 @@ function PitchBody({ lane, freeTier }: { lane: 'driver' | 'wizard'; freeTier?: s
|
|
|
616
661
|
// unreachable or predates the field).
|
|
617
662
|
const freeLine = freeTier || 'free for 3 clusters'
|
|
618
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.
|
|
619
667
|
const highlights = [
|
|
620
|
-
{ icon:
|
|
621
|
-
{ icon:
|
|
622
|
-
{ icon:
|
|
623
|
-
{ icon:
|
|
624
|
-
{ icon:
|
|
668
|
+
{ icon: Bell, lead: 'Alerts', rest: ' in Slack when something breaks. Once, not for every 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' },
|
|
625
673
|
]
|
|
626
674
|
return (
|
|
627
|
-
<div className="px-8 pt-7 pb-
|
|
675
|
+
<div className="px-8 pt-7 pb-5">
|
|
628
676
|
<Eyebrow />
|
|
629
|
-
<h3 className="text-[22px] font-semibold leading-tight tracking-tight text-theme-text-primary mb-3">
|
|
630
|
-
|
|
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.
|
|
631
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: Slack alerts, AI root cause and every cluster in one place, for
|
|
682
|
+
you and your team.
|
|
683
|
+
</p>
|
|
632
684
|
<p className="text-[14px] leading-relaxed text-theme-text-secondary mb-6">
|
|
633
|
-
The hosted side of Radar: your clusters in one place, run by us.
|
|
634
|
-
<br />
|
|
635
685
|
The Radar you're running{' '}
|
|
636
686
|
<b className="text-theme-text-primary font-semibold">stays free and open source, always.</b>
|
|
637
687
|
</p>
|
|
638
|
-
<ul className="space-y-2.5 mb-
|
|
688
|
+
<ul className="space-y-2.5 mb-3">
|
|
639
689
|
{highlights.map(({ icon: Icon, lead, rest }) => (
|
|
640
690
|
<li key={lead} className="flex items-start gap-2.5 text-[13px] leading-relaxed text-theme-text-secondary">
|
|
641
691
|
<Icon className="w-4 h-4 shrink-0 mt-[3px] text-emerald-600 dark:text-emerald-400" />
|
|
@@ -664,46 +714,46 @@ function PitchBody({ lane, freeTier }: { lane: 'driver' | 'wizard'; freeTier?: s
|
|
|
664
714
|
{/* No heading: the disclosure's own label already names this one. */}
|
|
665
715
|
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
666
716
|
{lane === 'driver'
|
|
667
|
-
?
|
|
668
|
-
:
|
|
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, Radar's agent found the root cause 3× faster than the same model on plain
|
|
728
|
+
kubectl, and 2× faster than the other AI SRE tools we tested. Run it automatically on every alert, or
|
|
729
|
+
on demand from any issue, with no CLI or API key.{' '}
|
|
730
|
+
<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">
|
|
731
|
+
See the benchmarks →
|
|
732
|
+
</a>
|
|
669
733
|
</p>
|
|
670
734
|
</section>
|
|
671
735
|
<section>
|
|
672
736
|
<h4 className="text-[12.5px] font-semibold text-theme-text-primary mb-0.5">What it costs</h4>
|
|
673
737
|
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
674
|
-
Radar Cloud is {freeLine}
|
|
675
|
-
|
|
676
|
-
|
|
738
|
+
Radar Cloud is {freeLine}, with 100 investigations a month included, then $1 per investigation.
|
|
739
|
+
Paid plans add more clusters, more included investigations, and enterprise features like SSO, SCIM
|
|
740
|
+
and audit logs.{' '}
|
|
677
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">
|
|
678
742
|
See pricing →
|
|
679
743
|
</a>
|
|
680
744
|
</p>
|
|
681
745
|
</section>
|
|
682
746
|
<section>
|
|
683
|
-
|
|
747
|
+
{/* No heading: "built by Skyhook" already answers who is behind it. */}
|
|
684
748
|
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
685
|
-
|
|
686
|
-
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.{' '}
|
|
687
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">
|
|
688
751
|
Meet us →
|
|
689
752
|
</a>
|
|
690
753
|
</p>
|
|
691
754
|
</section>
|
|
692
|
-
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
693
|
-
Prefer your own VPC? You can run the Radar Cloud control plane yourself.{' '}
|
|
694
|
-
<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">
|
|
695
|
-
Read the docs
|
|
696
|
-
</a>
|
|
697
|
-
.
|
|
698
|
-
</p>
|
|
699
755
|
</div>
|
|
700
756
|
</Collapse>
|
|
701
|
-
<div className="mb-5 border-l-2 border-emerald-500/40 pl-3.5">
|
|
702
|
-
<p className="text-[12px] leading-relaxed text-theme-text-secondary">
|
|
703
|
-
Don't need Radar Cloud right now? That's fine. What you're running is already a full product,
|
|
704
|
-
not a demo. We're here if you ever do.
|
|
705
|
-
</p>
|
|
706
|
-
</div>
|
|
707
757
|
</div>
|
|
708
758
|
)
|
|
709
759
|
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { useMemo,
|
|
2
|
-
import { AlertTriangle } from 'lucide-react'
|
|
1
|
+
import { useMemo, forwardRef } from 'react'
|
|
3
2
|
import {
|
|
4
3
|
ClusterSwitcher,
|
|
5
4
|
type ClusterSwitcherItem,
|
|
6
|
-
pluralize,
|
|
7
5
|
} from '@skyhook-io/k8s-ui'
|
|
8
|
-
import { useContexts,
|
|
9
|
-
import { useContextSwitch } from '../context/ContextSwitchContext'
|
|
10
|
-
import { useToast } from '../components/ui/Toast'
|
|
11
|
-
import { useDock } from '../components/dock'
|
|
6
|
+
import { useContexts, useClusterInfo, useCapabilities } from '../api/client'
|
|
12
7
|
import type { ContextInfo } from '../types'
|
|
8
|
+
import { useContextSwitchFlow } from './useContextSwitchFlow'
|
|
13
9
|
import { parseContextForSwitcher, visibleContextQualifier, type ParsedContextName } from '../utils/context-name'
|
|
14
10
|
|
|
15
11
|
interface ContextSwitcherProps {
|
|
@@ -28,22 +24,11 @@ interface ParsedContext extends ParsedContextName {
|
|
|
28
24
|
nameQualifier?: string
|
|
29
25
|
}
|
|
30
26
|
|
|
31
|
-
function shouldSuppressSwitchErrorToast(error: unknown): boolean {
|
|
32
|
-
const message = error instanceof Error ? error.message : ''
|
|
33
|
-
return message.includes('cluster connection failed:')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
27
|
export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcherProps>(({ className = '', variant, label, triggerName }, ref) => {
|
|
37
|
-
const [showConfirm, setShowConfirm] = useState(false)
|
|
38
|
-
const [pendingSwitch, setPendingSwitch] = useState<ParsedContext | null>(null)
|
|
39
|
-
const [sessionCounts, setSessionCounts] = useState<SessionCounts | null>(null)
|
|
40
|
-
|
|
41
28
|
const { data: contexts, isLoading: contextsLoading } = useContexts()
|
|
42
29
|
const { data: clusterInfo } = useClusterInfo()
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const { showError } = useToast()
|
|
46
|
-
const { tabs } = useDock()
|
|
30
|
+
const { data: capabilities } = useCapabilities()
|
|
31
|
+
const switchFlow = useContextSwitchFlow()
|
|
47
32
|
|
|
48
33
|
// Parse contexts and decide whether to render group headers (multi-account only).
|
|
49
34
|
// hasMultipleSources gates the kubeconfig-source chip — only useful when 2+
|
|
@@ -101,69 +86,10 @@ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcher
|
|
|
101
86
|
})
|
|
102
87
|
}, [parsedById, hasMultipleAccounts, hasMultipleSources])
|
|
103
88
|
|
|
104
|
-
const
|
|
105
|
-
startSwitch({
|
|
106
|
-
raw: parsed.raw,
|
|
107
|
-
provider: parsed.provider,
|
|
108
|
-
account: parsed.account,
|
|
109
|
-
region: parsed.region,
|
|
110
|
-
clusterName: parsed.clusterName,
|
|
111
|
-
})
|
|
112
|
-
try {
|
|
113
|
-
await switchContext.mutateAsync({ name: parsed.context.name })
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('Failed to switch context:', error)
|
|
116
|
-
endSwitch()
|
|
117
|
-
// Backend may not transition to StateDisconnected on client-side errors
|
|
118
|
-
// (network, timeout) — without this toast the user gets no feedback.
|
|
119
|
-
if (!shouldSuppressSwitchErrorToast(error)) {
|
|
120
|
-
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
121
|
-
showError('Failed to switch context', message)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const handleSelect = async (item: ClusterSwitcherItem) => {
|
|
89
|
+
const handleSelect = (item: ClusterSwitcherItem) => {
|
|
127
90
|
const parsed = parsedById.get(item.id)
|
|
128
|
-
if (!parsed
|
|
129
|
-
|
|
130
|
-
// Active sessions (port forwards from API + terminal tabs from dock) get
|
|
131
|
-
// a confirmation prompt — switching contexts kills both.
|
|
132
|
-
try {
|
|
133
|
-
const counts = await fetchSessionCounts()
|
|
134
|
-
const terminalTabs = tabs.filter(t => t.type === 'terminal').length
|
|
135
|
-
const total = counts.portForwards + terminalTabs
|
|
136
|
-
if (total > 0) {
|
|
137
|
-
setSessionCounts({ ...counts, execSessions: terminalTabs, total })
|
|
138
|
-
setPendingSwitch(parsed)
|
|
139
|
-
setShowConfirm(true)
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
} catch (error) {
|
|
143
|
-
// Session-counts is best-effort; failing it shouldn't block the user.
|
|
144
|
-
// But warn — if there ARE active sessions we couldn't see, the switch
|
|
145
|
-
// will silently kill them.
|
|
146
|
-
console.error('Failed to check sessions:', error)
|
|
147
|
-
showError(
|
|
148
|
-
'Could not check active sessions',
|
|
149
|
-
'Switching anyway. Any open port-forwards or terminals will be terminated.',
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
performSwitch(parsed)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const handleConfirmSwitch = () => {
|
|
156
|
-
setShowConfirm(false)
|
|
157
|
-
if (pendingSwitch) {
|
|
158
|
-
performSwitch(pendingSwitch)
|
|
159
|
-
setPendingSwitch(null)
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const handleCancelSwitch = () => {
|
|
164
|
-
setShowConfirm(false)
|
|
165
|
-
setPendingSwitch(null)
|
|
166
|
-
setSessionCounts(null)
|
|
91
|
+
if (!parsed) return
|
|
92
|
+
switchFlow.requestSwitch(parsed.context)
|
|
167
93
|
}
|
|
168
94
|
|
|
169
95
|
// In-cluster mode renders a static badge instead of a switcher (only one
|
|
@@ -205,63 +131,18 @@ export const ContextSwitcher = forwardRef<ContextSwitcherHandle, ContextSwitcher
|
|
|
205
131
|
currentSourceLabel={currentSourceLabel}
|
|
206
132
|
items={items}
|
|
207
133
|
onSelect={handleSelect}
|
|
208
|
-
loading={
|
|
209
|
-
disabled={contextsLoading}
|
|
134
|
+
loading={switchFlow.isPending}
|
|
135
|
+
disabled={contextsLoading || !capabilities || capabilities.configManagement === 'operator'}
|
|
210
136
|
searchable={items.length > 1}
|
|
211
137
|
showGroupHeaders={hasMultipleAccounts}
|
|
212
138
|
errorSlot={
|
|
213
|
-
|
|
214
|
-
<span className="text-xs text-red-400">{
|
|
139
|
+
switchFlow.error ? (
|
|
140
|
+
<span className="text-xs text-red-400">{switchFlow.error.message}</span>
|
|
215
141
|
) : undefined
|
|
216
142
|
}
|
|
217
143
|
/>
|
|
218
144
|
|
|
219
|
-
{
|
|
220
|
-
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50">
|
|
221
|
-
<div className="bg-theme-surface border border-theme-border rounded-lg shadow-xl max-w-md mx-4 overflow-hidden">
|
|
222
|
-
<div className="px-4 py-3 border-b border-theme-border flex items-center gap-2">
|
|
223
|
-
<AlertTriangle className="w-5 h-5 text-amber-400" />
|
|
224
|
-
<span className="font-medium text-theme-text-primary">Active Sessions</span>
|
|
225
|
-
</div>
|
|
226
|
-
<div className="px-4 py-4">
|
|
227
|
-
<p className="text-sm text-theme-text-secondary mb-3">
|
|
228
|
-
Switching contexts will terminate active sessions:
|
|
229
|
-
</p>
|
|
230
|
-
<ul className="text-sm text-theme-text-primary space-y-1 mb-4">
|
|
231
|
-
{sessionCounts.portForwards > 0 && (
|
|
232
|
-
<li className="flex items-center gap-2">
|
|
233
|
-
<span className="w-1.5 h-1.5 rounded-full bg-blue-400" />
|
|
234
|
-
{pluralize(sessionCounts.portForwards, 'port forward')}
|
|
235
|
-
</li>
|
|
236
|
-
)}
|
|
237
|
-
{sessionCounts.execSessions > 0 && (
|
|
238
|
-
<li className="flex items-center gap-2">
|
|
239
|
-
<span className="w-1.5 h-1.5 rounded-full bg-green-400" />
|
|
240
|
-
{pluralize(sessionCounts.execSessions, 'terminal session')}
|
|
241
|
-
</li>
|
|
242
|
-
)}
|
|
243
|
-
</ul>
|
|
244
|
-
<p className="text-xs text-theme-text-tertiary">
|
|
245
|
-
Switch to: <span className="text-theme-text-secondary">{pendingSwitch.clusterName}</span>
|
|
246
|
-
</p>
|
|
247
|
-
</div>
|
|
248
|
-
<div className="px-4 py-3 border-t border-theme-border flex justify-end gap-2">
|
|
249
|
-
<button
|
|
250
|
-
onClick={handleCancelSwitch}
|
|
251
|
-
className="px-3 py-1.5 text-sm rounded-md bg-theme-elevated hover:bg-theme-hover text-theme-text-secondary transition-colors"
|
|
252
|
-
>
|
|
253
|
-
Cancel
|
|
254
|
-
</button>
|
|
255
|
-
<button
|
|
256
|
-
onClick={handleConfirmSwitch}
|
|
257
|
-
className="px-3 py-1.5 text-sm rounded-md bg-amber-500 hover:bg-amber-600 text-white transition-colors"
|
|
258
|
-
>
|
|
259
|
-
Switch Anyway
|
|
260
|
-
</button>
|
|
261
|
-
</div>
|
|
262
|
-
</div>
|
|
263
|
-
</div>
|
|
264
|
-
)}
|
|
145
|
+
{switchFlow.confirmDialog}
|
|
265
146
|
</>
|
|
266
147
|
)
|
|
267
148
|
})
|