@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 +1 -1
- package/src/App.tsx +10 -2
- package/src/api/client.ts +184 -0
- package/src/api/config.test.ts +19 -0
- package/src/components/CloudConnectFlow.tsx +605 -0
- package/src/components/CloudFunnelButton.tsx +499 -0
- package/src/components/ConnectionErrorView.tsx +2 -1
- package/src/components/UserMenu.tsx +3 -2
- package/src/components/diagnose/DiagnoseSurface.tsx +2 -1
- package/src/components/home/ClusterHealthCard.tsx +10 -5
- package/src/components/nav/PrimaryNavRail.tsx +4 -1
- package/src/components/resources/ImageFilesystemModal.tsx +4 -1
- package/src/components/settings/SettingsDialog.tsx +11 -4
- package/src/components/traffic/TrafficWizard.tsx +4 -2
- package/src/main.tsx +7 -1
- package/src/vite-env.d.ts +8 -0
package/package.json
CHANGED
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=
|
|
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(
|
package/src/api/config.test.ts
CHANGED
|
@@ -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')
|