@skyhook-io/radar-app 1.9.5 → 1.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/api/client.authRedirect.test.ts +194 -0
- package/src/api/client.trace.test.ts +37 -0
- package/src/api/client.ts +131 -1
- package/src/components/CloudConnectFlow.tsx +7 -7
- package/src/components/CloudFunnelButton.tsx +87 -64
- package/src/components/home/mcpToolCatalog.ts +7 -5
- package/src/components/workload/WorkloadView.tsx +558 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
1
|
+
import { useEffect, useId, useState, type ReactNode } from 'react'
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
3
3
|
import { Bell, Check, Globe, History, Sparkles, Users, X } from 'lucide-react'
|
|
4
|
+
import { Collapse, CollapseChevron } from '@skyhook-io/k8s-ui/components/ui/Collapse'
|
|
4
5
|
import { DialogPortal } from '@skyhook-io/k8s-ui/components/ui/DialogPortal'
|
|
5
6
|
import { Tooltip } from './ui/Tooltip'
|
|
6
7
|
import { CloudConnectFlow } from './CloudConnectFlow'
|
|
@@ -34,6 +35,9 @@ const FALLBACK_APP_URL = 'https://app.radarhq.io'
|
|
|
34
35
|
// offline laptop all render exactly what Radar rendered before this fetch
|
|
35
36
|
// existed — the dialog never waits on the network and never shows a gap.
|
|
36
37
|
const DEFAULT_ASSURANCES = ['Free for 3 clusters', 'No credit card', 'Your cluster data stays in your cluster']
|
|
38
|
+
// A product fact, not funnel copy — appended even when the Hub supplies its
|
|
39
|
+
// own assurances (deduped if the Hub starts sending it).
|
|
40
|
+
const SOC2_ASSURANCE = 'SOC 2 compliant'
|
|
37
41
|
const SIGNUP_QUERY = '?utm_source=radar-oss&utm_medium=app&utm_campaign=cloud-modal'
|
|
38
42
|
const ABOUT_URL = 'https://radarhq.io/about'
|
|
39
43
|
const SELF_HOSTED_DOCS_URL = 'https://radarhq.io/docs/cloud/self-hosted/'
|
|
@@ -193,7 +197,7 @@ export function CloudFunnelButton() {
|
|
|
193
197
|
<DialogPortal
|
|
194
198
|
open={open}
|
|
195
199
|
onClose={() => setOpen(false)}
|
|
196
|
-
className="w-[
|
|
200
|
+
className="w-[580px] max-w-full max-h-[calc(100vh-2rem)] overflow-hidden flex flex-col"
|
|
197
201
|
>
|
|
198
202
|
<button
|
|
199
203
|
onClick={() => setOpen(false)}
|
|
@@ -208,7 +212,7 @@ export function CloudFunnelButton() {
|
|
|
208
212
|
more with the connect flow, whose plan card is the tallest state. */}
|
|
209
213
|
{showFlow && flowForView ? (
|
|
210
214
|
<div className="min-h-0 overflow-y-auto">
|
|
211
|
-
<div className="px-
|
|
215
|
+
<div className="px-8 pt-7">
|
|
212
216
|
<Eyebrow />
|
|
213
217
|
</div>
|
|
214
218
|
<CloudConnectFlow
|
|
@@ -245,6 +249,39 @@ export function CloudFunnelButton() {
|
|
|
245
249
|
)
|
|
246
250
|
}
|
|
247
251
|
|
|
252
|
+
// Secondary copy the pitch shouldn't spend vertical space on until asked for.
|
|
253
|
+
function Fold({ summary, className = '', children }: { summary: string; className?: string; children: ReactNode }) {
|
|
254
|
+
const [open, setOpen] = useState(false)
|
|
255
|
+
const bodyId = useId()
|
|
256
|
+
return (
|
|
257
|
+
<div className={className}>
|
|
258
|
+
<button
|
|
259
|
+
type="button"
|
|
260
|
+
onClick={() => setOpen((v) => !v)}
|
|
261
|
+
aria-expanded={open}
|
|
262
|
+
aria-controls={bodyId}
|
|
263
|
+
className="flex items-center gap-1.5 text-[11.5px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors"
|
|
264
|
+
>
|
|
265
|
+
<CollapseChevron open={open} className="w-3 h-3" />
|
|
266
|
+
{summary}
|
|
267
|
+
</button>
|
|
268
|
+
<Collapse open={open}>
|
|
269
|
+
<p id={bodyId} className="mt-2 pl-[18px] text-[11.5px] leading-relaxed text-theme-text-tertiary">
|
|
270
|
+
{children}
|
|
271
|
+
</p>
|
|
272
|
+
</Collapse>
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function assuranceItems(fromHub?: string[]): string[] {
|
|
278
|
+
const items = fromHub?.length ? fromHub : DEFAULT_ASSURANCES
|
|
279
|
+
if (items.some((item) => item.toLowerCase().includes('soc 2'))) return items
|
|
280
|
+
// Before the last item: the closer ("data stays in your cluster") is the
|
|
281
|
+
// longest line and wraps most naturally when it comes last.
|
|
282
|
+
return [...items.slice(0, -1), SOC2_ASSURANCE, items[items.length - 1]]
|
|
283
|
+
}
|
|
284
|
+
|
|
248
285
|
function RadarSweep() {
|
|
249
286
|
return (
|
|
250
287
|
<div
|
|
@@ -263,32 +300,13 @@ function RadarSweep() {
|
|
|
263
300
|
|
|
264
301
|
function Eyebrow() {
|
|
265
302
|
return (
|
|
266
|
-
<div className="flex items-center gap-
|
|
303
|
+
<div className="flex items-center gap-3 mb-5">
|
|
267
304
|
<RadarSweep />
|
|
268
305
|
<span className="font-mono text-[10.5px] tracking-[0.16em] uppercase text-emerald-600 dark:text-emerald-400">Radar Cloud</span>
|
|
269
306
|
</div>
|
|
270
307
|
)
|
|
271
308
|
}
|
|
272
309
|
|
|
273
|
-
function Faces() {
|
|
274
|
-
return (
|
|
275
|
-
<div className="flex shrink-0" aria-hidden>
|
|
276
|
-
{[
|
|
277
|
-
['R', 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-300'],
|
|
278
|
-
['N', 'bg-sky-500/20 text-sky-700 dark:text-sky-300'],
|
|
279
|
-
['E', 'bg-amber-500/20 text-amber-700 dark:text-amber-300'],
|
|
280
|
-
].map(([initial, color], i) => (
|
|
281
|
-
<div
|
|
282
|
-
key={initial}
|
|
283
|
-
className={`w-6 h-6 text-[10px] ${color} rounded-full grid place-items-center font-bold border-2 border-theme-surface ${i > 0 ? '-ml-1.5' : ''}`}
|
|
284
|
-
>
|
|
285
|
-
{initial}
|
|
286
|
-
</div>
|
|
287
|
-
))}
|
|
288
|
-
</div>
|
|
289
|
-
)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
310
|
function ModalFooter({
|
|
293
311
|
lane,
|
|
294
312
|
signupUrl,
|
|
@@ -328,9 +346,9 @@ function ModalFooter({
|
|
|
328
346
|
// fast click would escape to signup before we could route this install.
|
|
329
347
|
const selfPending = selfLoading === true
|
|
330
348
|
return (
|
|
331
|
-
<div className="px-
|
|
349
|
+
<div className="shrink-0 px-8 py-5 bg-theme-base border-t border-theme-border">
|
|
332
350
|
{self && self.ownership !== 'unknown' && (
|
|
333
|
-
<div className="mb-3 card-inner text-[
|
|
351
|
+
<div className="mb-3.5 card-inner p-3 text-[12px] leading-relaxed text-theme-text-secondary">
|
|
334
352
|
{ambiguous ? (
|
|
335
353
|
<>
|
|
336
354
|
Radar found conflicting management metadata on this install, so it can't say whether a Helm
|
|
@@ -367,14 +385,20 @@ function ModalFooter({
|
|
|
367
385
|
</div>
|
|
368
386
|
)}
|
|
369
387
|
{notice && (
|
|
370
|
-
<div className="mb-3 card-inner text-[
|
|
388
|
+
<div className="mb-3.5 card-inner p-3 text-[12px] leading-relaxed text-theme-text-secondary">{notice}</div>
|
|
389
|
+
)}
|
|
390
|
+
{lane === 'driver' && (
|
|
391
|
+
<p className="mb-3.5 text-[12px] leading-relaxed text-theme-text-secondary">
|
|
392
|
+
The guided setup runs right here in the app — Radar installs the Cloud agent on this cluster, and
|
|
393
|
+
you approve the connection in your browser.
|
|
394
|
+
</p>
|
|
371
395
|
)}
|
|
372
|
-
<div className="flex items-center gap-
|
|
396
|
+
<div className="flex flex-wrap items-center gap-x-5 gap-y-2.5">
|
|
373
397
|
{lane === 'driver' ? (
|
|
374
398
|
<>
|
|
375
399
|
<button
|
|
376
400
|
onClick={onConnect}
|
|
377
|
-
className="px-
|
|
401
|
+
className="whitespace-nowrap px-6 py-2.5 rounded-[10px] bg-emerald-500 hover:bg-emerald-400 text-emerald-950 text-[14px] 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"
|
|
378
402
|
>
|
|
379
403
|
Connect this cluster
|
|
380
404
|
</button>
|
|
@@ -382,9 +406,9 @@ function ModalFooter({
|
|
|
382
406
|
href={driverEscapeUrl}
|
|
383
407
|
target="_blank"
|
|
384
408
|
rel="noopener noreferrer"
|
|
385
|
-
className="text-[
|
|
409
|
+
className="whitespace-nowrap text-[13px] text-theme-text-secondary hover:text-theme-text-primary underline underline-offset-2 transition-colors"
|
|
386
410
|
>
|
|
387
|
-
or
|
|
411
|
+
or set up in the browser
|
|
388
412
|
</a>
|
|
389
413
|
</>
|
|
390
414
|
) : cliOnly ? null : (
|
|
@@ -399,33 +423,32 @@ function ModalFooter({
|
|
|
399
423
|
{self?.ownership === 'helm' || gitops ? 'Connect this cluster' : 'Try Cloud free'}
|
|
400
424
|
</a>
|
|
401
425
|
)}
|
|
402
|
-
<button onClick={onLater} className="text-[
|
|
426
|
+
<button onClick={onLater} className="whitespace-nowrap text-[13px] text-theme-text-tertiary hover:text-theme-text-primary transition-colors">
|
|
403
427
|
Maybe later
|
|
404
428
|
</button>
|
|
405
429
|
</div>
|
|
406
|
-
<div className="mt-
|
|
407
|
-
{(assurances
|
|
408
|
-
<span key={item} className="flex items-
|
|
409
|
-
<Check className="w-3 h-3 text-emerald-600 dark:text-emerald-400" />
|
|
430
|
+
<div className="mt-4 grid grid-cols-2 gap-x-5 gap-y-2 text-[11.5px] text-theme-text-tertiary">
|
|
431
|
+
{assuranceItems(assurances).map((item) => (
|
|
432
|
+
<span key={item} className="flex items-start gap-1.5">
|
|
433
|
+
<Check className="w-3 h-3 mt-[3px] shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
410
434
|
{item}
|
|
411
435
|
</span>
|
|
412
436
|
))}
|
|
413
437
|
</div>
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
call.{' '}
|
|
438
|
+
<Fold summary="Prefer to run the control plane in your own VPC?" className="mt-3.5">
|
|
439
|
+
Self-hosting is fully self-serve — set it up yourself, whenever you're ready.{' '}
|
|
417
440
|
<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">
|
|
418
441
|
Read the docs
|
|
419
442
|
</a>
|
|
420
443
|
.
|
|
421
|
-
</
|
|
444
|
+
</Fold>
|
|
422
445
|
</div>
|
|
423
446
|
)
|
|
424
447
|
}
|
|
425
448
|
|
|
426
449
|
// Headline defuses the paywall fear before anything is pitched; the grid
|
|
427
|
-
// carries the concrete capabilities; the
|
|
428
|
-
//
|
|
450
|
+
// carries the concrete capabilities; the anti-sell closes — the credibility
|
|
451
|
+
// beat that makes the sell land.
|
|
429
452
|
function ModalBody() {
|
|
430
453
|
const features = [
|
|
431
454
|
{
|
|
@@ -456,43 +479,43 @@ function ModalBody() {
|
|
|
456
479
|
},
|
|
457
480
|
]
|
|
458
481
|
return (
|
|
459
|
-
<div className="px-
|
|
482
|
+
<div className="px-8 pt-7 pb-2">
|
|
460
483
|
<Eyebrow />
|
|
461
|
-
<h3 className="text-[
|
|
484
|
+
<h3 className="text-[22px] font-semibold leading-tight tracking-tight text-theme-text-primary mb-3 text-balance">
|
|
462
485
|
First things first: Radar stays free.
|
|
463
486
|
</h3>
|
|
464
|
-
<p className="text-[
|
|
487
|
+
<p className="text-[14px] leading-relaxed text-theme-text-secondary mb-5">
|
|
465
488
|
The app you're looking at is Apache 2.0 — every feature, forever, no rug pulls.{' '}
|
|
466
489
|
<b className="text-theme-text-primary font-semibold">Radar Cloud is how we keep the lights on:</b> the
|
|
467
490
|
same Radar, plus the parts that are genuinely hard to run on your own.
|
|
468
491
|
</p>
|
|
469
|
-
<div className="grid grid-cols-2 gap-
|
|
492
|
+
<div className="grid grid-cols-2 gap-3 mb-5">
|
|
470
493
|
{features.map(({ icon: Icon, title, body, wide }) => (
|
|
471
|
-
<div
|
|
494
|
+
<div
|
|
495
|
+
key={title}
|
|
496
|
+
className={`card-inner-lg p-3.5 flex gap-3 ${
|
|
497
|
+
wide ? 'col-span-2 bg-emerald-500/[0.06] border-emerald-500/25 dark:bg-emerald-500/[0.08]' : ''
|
|
498
|
+
}`}
|
|
499
|
+
>
|
|
472
500
|
<Icon className="w-4 h-4 shrink-0 mt-0.5 text-emerald-600 dark:text-emerald-400" />
|
|
473
501
|
<div>
|
|
474
|
-
<div className="text-[
|
|
475
|
-
<p className="mt-
|
|
502
|
+
<div className="text-[13px] font-semibold text-theme-text-primary">{title}</div>
|
|
503
|
+
<p className="mt-1 text-[12px] leading-relaxed text-theme-text-tertiary">{body}</p>
|
|
476
504
|
</div>
|
|
477
505
|
</div>
|
|
478
506
|
))}
|
|
479
507
|
</div>
|
|
480
|
-
<div className="
|
|
481
|
-
<
|
|
482
|
-
|
|
483
|
-
</
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
<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">
|
|
492
|
-
Meet us →
|
|
493
|
-
</a>
|
|
494
|
-
</p>
|
|
495
|
-
</div>
|
|
508
|
+
<div className="mb-5 border-l-2 border-emerald-500/40 pl-3.5">
|
|
509
|
+
<p className="text-[12.5px] leading-relaxed text-theme-text-secondary">
|
|
510
|
+
If it's just you and one cluster — honestly, stay right here. This app is the product, not a demo.
|
|
511
|
+
</p>
|
|
512
|
+
<p className="mt-2 text-[11.5px] leading-relaxed text-theme-text-tertiary">
|
|
513
|
+
Radar is built in the open by many hands, and overseen by a small team of humans — the kind you
|
|
514
|
+
can actually talk to.{' '}
|
|
515
|
+
<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">
|
|
516
|
+
Meet us →
|
|
517
|
+
</a>
|
|
518
|
+
</p>
|
|
496
519
|
</div>
|
|
497
520
|
</div>
|
|
498
521
|
)
|
|
@@ -109,14 +109,16 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
|
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
name: 'diagnose',
|
|
112
|
-
desc: 'One-call root-cause bundle. Workloads get spec + resourceContext + current AND previous logs across pods + warning events + startup blockers; GitOps reconcilers, including Flux HelmRelease, get status summary + parsed related issues.',
|
|
112
|
+
desc: 'One-call root-cause bundle. Workloads get spec + resourceContext + current AND previous logs across pods + warning events + startup blockers; GitOps reconcilers, including Flux HelmRelease, get status summary + parsed related issues; network entry kinds (Service / Ingress / HTTPRoute / GRPCRoute / Gateway) get a path-shaped trace naming the first broken hop, with an optional one-shot reachability test.',
|
|
113
113
|
params: [
|
|
114
|
-
{ arg: 'kind', required: true, desc: 'pod, deployment, statefulset, daemonset, application, kustomization,
|
|
114
|
+
{ arg: 'kind', required: true, desc: 'pod, deployment, statefulset, daemonset, application, kustomization, Flux HelmRelease, service, ingress, httproute, grpcroute, or gateway' },
|
|
115
115
|
{ arg: 'namespace', required: true, desc: 'resource namespace' },
|
|
116
116
|
{ arg: 'name', required: true, desc: 'resource name' },
|
|
117
|
-
{ arg: '
|
|
118
|
-
{ arg: '
|
|
119
|
-
{ arg: '
|
|
117
|
+
{ arg: 'probe', desc: 'network kinds only: add active DNS/TCP/TLS/HTTP probes against the declared path (0-3s wall time)' },
|
|
118
|
+
{ arg: 'in_cluster', desc: 'network kinds: run the probe from inside the cluster via short-lived self-destructing pods (real dataplane) - confirms a route the apiserver proxy could only reach indirectly. Creates up to 5 short-lived probe pods (one per dialed target) under your RBAC' },
|
|
119
|
+
{ arg: 'container', desc: 'workload kinds: specific container (defaults to all)' },
|
|
120
|
+
{ arg: 'tail_lines', desc: 'workload kinds: lines per pod/stream (default 100)' },
|
|
121
|
+
{ arg: 'since', desc: 'workload kinds: only logs newer than this duration' },
|
|
120
122
|
],
|
|
121
123
|
},
|
|
122
124
|
{
|