@skyhook-io/radar-app 1.9.4 → 1.9.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/radar-app",
3
- "version": "1.9.4",
3
+ "version": "1.9.5",
4
4
  "description": "Radar's full web UI as a reusable React component. Used by Radar's own binary and by external consumers like Radar Cloud.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/App.tsx CHANGED
@@ -9,7 +9,7 @@ import { DebugOverlay } from './components/DebugOverlay'
9
9
  import { GlobalDiagnoseButton } from './components/diagnose/LocalDiagnoseAction'
10
10
  import { useDiagnoseLayout } from './components/diagnose/DiagnoseContext'
11
11
  import { DiagnoseSurface } from './components/diagnose/DiagnoseSurface'
12
- import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader } from '@skyhook-io/k8s-ui'
12
+ import { TopologyGraph, TopologySearch, TopologyBreadcrumb, TopologyFilterSidebar, TopologyControls, FreshnessControl, gitOpsRouteForKind, gitOpsRouteForResource, ScopePill, PaneLoader, assetUrl } from '@skyhook-io/k8s-ui'
13
13
  import { initNavigationMap } from '@skyhook-io/k8s-ui/utils/navigation'
14
14
  import { useAPIResources, findAPIResourceForRoute } from './api/apiResources'
15
15
  import { TimelineView } from './components/timeline/TimelineView'
@@ -33,6 +33,7 @@ import { DockProvider, BottomDock, useDock, useDockReservedHeight, useOpenLocalT
33
33
  import { DURATION_DOCK } from '@skyhook-io/k8s-ui/utils/animation'
34
34
  import { ContextSwitcher } from './components/ContextSwitcher'
35
35
  import { NamespaceSwitcher, type NamespaceSwitcherHandle } from './components/NamespaceSwitcher'
36
+ import { CloudFunnelButton } from './components/CloudFunnelButton'
36
37
  import { useNavCustomization } from './context/NavCustomization'
37
38
  import type { FleetTakeoverTarget } from './context/NavCustomization'
38
39
  import { PrimaryNavRail } from './components/nav/PrimaryNavRail'
@@ -68,6 +69,8 @@ import { type OmnibarHandle } from './components/ui/Omnibar'
68
69
  import { RadarOmnibar } from './components/ui/RadarOmnibar'
69
70
  import type { ContextSwitcherHandle } from './components/ContextSwitcher'
70
71
 
72
+ const radarLogoUrl = assetUrl('/images/radar/radar-icon.svg')
73
+
71
74
  // All possible node kinds (core + GitOps)
72
75
  const ALL_NODE_KINDS: NodeKind[] = [
73
76
  'Internet', 'Ingress', 'Gateway', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'TLSRoute',
@@ -1878,6 +1881,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
1878
1881
  {/* AI investigations (self-hides when no agent CLI is present) */}
1879
1882
  <GlobalDiagnoseButton />
1880
1883
 
1884
+ {/* Radar Cloud funnel — OSS-only chrome, same gate as the star.
1885
+ Cloud embeds render chromeless anyway; the explicit gate is
1886
+ belt-and-braces for future chrome-bearing embedders. */}
1887
+ {!navCustomization.embedded && <CloudFunnelButton />}
1888
+
1881
1889
  {/* Local terminal */}
1882
1890
  {capabilities.localTerminal && (
1883
1891
  <Tooltip content="Open local terminal">
@@ -2566,7 +2574,7 @@ function Logo() {
2566
2574
  <div className="flex items-center gap-2.5">
2567
2575
  <div className="relative w-7 h-7 rounded-lg overflow-hidden flex-shrink-0 bg-emerald-500/10 border border-emerald-500/20">
2568
2576
  <img
2569
- src="/images/radar/radar-icon.svg"
2577
+ src={radarLogoUrl}
2570
2578
  alt=""
2571
2579
  aria-hidden
2572
2580
  className="w-full h-full p-0.5"
package/src/api/client.ts CHANGED
@@ -1545,6 +1545,190 @@ export function useCapabilities() {
1545
1545
  });
1546
1546
  }
1547
1547
 
1548
+ // ============================================================================
1549
+ // In-product Cloud Connect (driver lane)
1550
+ // ============================================================================
1551
+
1552
+ export interface CloudInstallRecoveryGuidance {
1553
+ summary: string
1554
+ lines?: string[]
1555
+ inspect?: string[]
1556
+ clusterUrl?: string
1557
+ }
1558
+
1559
+ export interface CloudInstallPlanSummary {
1560
+ mode: 'fresh' | 'adopt'
1561
+ contextName: string
1562
+ namespace: string
1563
+ release: string
1564
+ defaultClusterName: string
1565
+ targetChartVersion: string
1566
+ targetAppVersion: string
1567
+ currentChartVersion?: string
1568
+ currentRevision?: number
1569
+ currentImageTag?: string
1570
+ preservedImageRepository?: string
1571
+ uncertainty?: string
1572
+ advisories?: string[]
1573
+ sharedListener?: boolean
1574
+ }
1575
+
1576
+ export interface CloudInstallConnected {
1577
+ clusterId: string
1578
+ clusterUrl: string
1579
+ trackCommand: string
1580
+ rollback?: CloudInstallRecoveryGuidance
1581
+ }
1582
+
1583
+ export interface CloudInstallFailure {
1584
+ kind: string
1585
+ message: string
1586
+ guidance?: CloudInstallRecoveryGuidance
1587
+ retrySafe: boolean
1588
+ }
1589
+
1590
+ export interface CloudInstallBlocked {
1591
+ reason: 'gitops' | 'preflight' | 'unsupported'
1592
+ message: string
1593
+ blocking?: string[]
1594
+ }
1595
+
1596
+ export type CloudInstallState =
1597
+ | 'idle'
1598
+ | 'preparing'
1599
+ | 'ready'
1600
+ | 'starting'
1601
+ | 'awaiting_approval'
1602
+ | 'provisioning'
1603
+ | 'waiting_tunnel'
1604
+ | 'connected'
1605
+ | 'failed'
1606
+ | 'blocked'
1607
+
1608
+ export interface CloudInstallStatus {
1609
+ flowId?: string
1610
+ state: CloudInstallState
1611
+ plan?: CloudInstallPlanSummary
1612
+ clusterName?: string
1613
+ connectUrl?: string
1614
+ connected?: CloudInstallConnected
1615
+ failure?: CloudInstallFailure
1616
+ blocked?: CloudInstallBlocked
1617
+ }
1618
+
1619
+ export const cloudInstallActive = (state: CloudInstallState | undefined): boolean =>
1620
+ state === 'preparing' ||
1621
+ state === 'ready' ||
1622
+ state === 'starting' ||
1623
+ state === 'awaiting_approval' ||
1624
+ state === 'provisioning' ||
1625
+ state === 'waiting_tunnel';
1626
+
1627
+ // Poll while a flow is live so progress renders in near-real-time; the flow
1628
+ // itself is server-owned, so this hook merely observes (and re-attaches after
1629
+ // a page reload). gcTime 0: never cache connect-flow payloads.
1630
+ // What the Hub says about connecting to it. Fetched from the Hub itself, not
1631
+ // from Radar's API, so the dialog states current terms instead of whatever was
1632
+ // compiled into this binary months ago — a stale free-tier claim is a promise
1633
+ // Radar can't keep. Every field is optional and falls back per-field, so a Hub
1634
+ // that doesn't serve this (self-hosted, older, offline) renders exactly what
1635
+ // Radar renders today.
1636
+ export interface CloudConnectInfo {
1637
+ assurances?: string[]
1638
+ notice?: string
1639
+ }
1640
+
1641
+ // Deliberately a bare cross-origin GET: no credentials, no identifiers, no
1642
+ // params. The Hub learns only what any HTTP request reveals.
1643
+ export function useCloudConnectInfo(apiUrl: string | undefined, enabled: boolean) {
1644
+ return useQuery<CloudConnectInfo>({
1645
+ queryKey: ["cloud-connect-info", apiUrl],
1646
+ queryFn: async () => {
1647
+ const res = await fetch(`${apiUrl}/api/connect/info`, {
1648
+ credentials: "omit",
1649
+ signal: AbortSignal.timeout(4000),
1650
+ });
1651
+ if (!res.ok) throw new Error(`connect info: ${res.status}`);
1652
+ return res.json();
1653
+ },
1654
+ // No apiUrl means the server decided this deployment must not fetch.
1655
+ enabled: enabled && !!apiUrl,
1656
+ staleTime: 5 * 60 * 1000,
1657
+ retry: false,
1658
+ });
1659
+ }
1660
+
1661
+ export function useCloudInstallStatus(enabled: boolean) {
1662
+ const queryClient = useQueryClient()
1663
+ const query = useQuery<CloudInstallStatus>({
1664
+ queryKey: ['cloud-install-status'],
1665
+ queryFn: () => fetchJSON('/cloud/install/status'),
1666
+ enabled,
1667
+ gcTime: 0,
1668
+ staleTime: 0,
1669
+ refetchInterval: (q) => (cloudInstallActive(q.state.data?.state) ? 1500 : false),
1670
+ })
1671
+ return {
1672
+ ...query,
1673
+ invalidate: () => queryClient.invalidateQueries({ queryKey: ['cloud-install-status'] }),
1674
+ }
1675
+ }
1676
+
1677
+ // In-cluster Radar describing its own installation, so the modal can route to
1678
+ // the right handoff instead of a generic signup link.
1679
+ export interface CloudConnectSelf {
1680
+ ownership: 'helm' | 'gitops' | 'ambiguous' | 'unknown'
1681
+ namespace?: string
1682
+ release?: string
1683
+ deploymentName?: string
1684
+ chart?: string
1685
+ controller?: string
1686
+ wizardUrl?: string
1687
+ }
1688
+
1689
+ export function useCloudConnectSelf(enabled: boolean) {
1690
+ return useQuery<CloudConnectSelf>({
1691
+ queryKey: ['cloud-connect-self'],
1692
+ queryFn: () => fetchJSON('/cloud/connect/self'),
1693
+ enabled,
1694
+ staleTime: 60000,
1695
+ })
1696
+ }
1697
+
1698
+ export function prepareCloudInstall(): Promise<CloudInstallStatus> {
1699
+ return fetchJSON('/cloud/install/prepare', { method: 'POST' })
1700
+ }
1701
+
1702
+ export function startCloudInstall(body: {
1703
+ flowId: string
1704
+ clusterName: string
1705
+ acceptAdoption?: boolean
1706
+ acknowledgeIncompleteDiscovery?: boolean
1707
+ acknowledgeSharedListener?: boolean
1708
+ }): Promise<CloudInstallStatus> {
1709
+ return fetchJSON('/cloud/install/start', {
1710
+ method: 'POST',
1711
+ headers: { 'Content-Type': 'application/json' },
1712
+ body: JSON.stringify(body),
1713
+ })
1714
+ }
1715
+
1716
+ export function cancelCloudInstall(flowId: string): Promise<CloudInstallStatus> {
1717
+ return fetchJSON('/cloud/install/cancel', {
1718
+ method: 'POST',
1719
+ headers: { 'Content-Type': 'application/json' },
1720
+ body: JSON.stringify({ flowId }),
1721
+ })
1722
+ }
1723
+
1724
+ export function dismissCloudInstall(flowId: string): Promise<CloudInstallStatus> {
1725
+ return fetchJSON('/cloud/install/dismiss', {
1726
+ method: 'POST',
1727
+ headers: { 'Content-Type': 'application/json' },
1728
+ body: JSON.stringify({ flowId }),
1729
+ })
1730
+ }
1731
+
1548
1732
  // Namespace-scoped capabilities. Users with namespace-scoped RoleBindings may
1549
1733
  // have these permissions in specific namespaces.
1550
1734
  export function useNamespaceCapabilities(
@@ -40,6 +40,25 @@ describe('stripBasename', () => {
40
40
  expect(stripBasename('/c/abcdef/resources')).toBe('/c/abcdef/resources')
41
41
  })
42
42
 
43
+ // Standalone Radar sets a basename too once --base-path is configured, so the
44
+ // session-expiry return path round-trips through this in a new shape. Store
45
+ // strips once (client.ts) and restore strips again defensively (App.tsx) before
46
+ // navigate() re-applies the basename — the net effect must be the original
47
+ // route, never a doubled one.
48
+ it('round-trips a return path under a base path without doubling', () => {
49
+ setBasename('/radar')
50
+ const stored = stripBasename('/radar/resources/pods') + '?namespaces=prod'
51
+ expect(stored).toBe('/resources/pods?namespaces=prod')
52
+ // Restore side strips a second time; already-relative values are untouched.
53
+ expect(stripBasename(stored)).toBe('/resources/pods?namespaces=prod')
54
+ })
55
+
56
+ it('leaves a nested base path return path relative', () => {
57
+ setBasename('/tools/radar')
58
+ expect(stripBasename('/tools/radar/topology')).toBe('/topology')
59
+ expect(stripBasename(stripBasename('/tools/radar/topology'))).toBe('/topology')
60
+ })
61
+
43
62
  it('inverts routePath', () => {
44
63
  setBasename('/c/abc')
45
64
  expect(stripBasename(routePath('/auth/login'))).toBe('/auth/login')