@skyhook-io/radar-app 1.9.3 → 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 +26 -6
- package/src/api/client.ts +251 -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/audit/AuditView.tsx +13 -0
- package/src/components/audit/ChecksViewTabs.test.tsx +20 -0
- package/src/components/audit/ChecksViewTabs.tsx +43 -0
- package/src/components/audit/UpgradeReadinessView.test.ts +130 -0
- package/src/components/audit/UpgradeReadinessView.tsx +699 -0
- package/src/components/capacity/shared.tsx +9 -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 +15 -6
- 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',
|
|
@@ -145,6 +148,8 @@ function getViewFromPath(pathname: string): ExtendedMainView {
|
|
|
145
148
|
// breakdown; a filter would only hide rows).
|
|
146
149
|
// - A GitOps detail tree spans namespaces — its controller lives in one
|
|
147
150
|
// namespace but manages workloads across many.
|
|
151
|
+
// - Upgrade impact evaluates the full readable cluster scope rather than a
|
|
152
|
+
// browsing filter.
|
|
148
153
|
// - A cluster-scoped resource kind (Nodes, PVs, ClusterRoles…) has no
|
|
149
154
|
// namespace at all.
|
|
150
155
|
// The pick itself is preserved so it re-applies when the user returns to a
|
|
@@ -170,6 +175,12 @@ function namespaceFilterDisabled(
|
|
|
170
175
|
tooltip: 'Capacity is reported across the cluster — the namespace filter doesn’t apply here.',
|
|
171
176
|
}
|
|
172
177
|
}
|
|
178
|
+
if (view === 'checks' && pathname.startsWith('/checks/upgrade')) {
|
|
179
|
+
return {
|
|
180
|
+
disabled: true,
|
|
181
|
+
tooltip: 'Upgrade impact scans every namespace you can access — the namespace filter doesn’t apply.',
|
|
182
|
+
}
|
|
183
|
+
}
|
|
173
184
|
const segments = pathname.replace(/^\//, '').split('/')
|
|
174
185
|
if (view === 'gitops' && segments[1] === 'detail') {
|
|
175
186
|
return {
|
|
@@ -233,6 +244,8 @@ function radarPageTitle(pathname: string, search = '', apiResources?: APIResourc
|
|
|
233
244
|
return slash >= 0 && slash < decoded.length - 1 ? decoded.slice(slash + 1) : decoded
|
|
234
245
|
}
|
|
235
246
|
|
|
247
|
+
if (view === 'checks' && pathSegments[1] === 'upgrade') return 'Upgrade impact'
|
|
248
|
+
|
|
236
249
|
// The landing view reads "Overview" rather than "Home" in the tab.
|
|
237
250
|
if (view === 'capacity') {
|
|
238
251
|
if (pathSegments[1] === 'pools') return decode(pathSegments[2] ?? '') || 'Capacity'
|
|
@@ -401,6 +414,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
401
414
|
|
|
402
415
|
// Get mainView from URL path
|
|
403
416
|
const mainView = getViewFromPath(location.pathname)
|
|
417
|
+
const upgradeReadinessRoute = location.pathname.startsWith('/checks/upgrade')
|
|
404
418
|
|
|
405
419
|
// Initialize the kind→plural discovery map app-wide (not just on ResourcesView
|
|
406
420
|
// mount) so the omnibar can open a CRD hit with an irregular plural from any
|
|
@@ -485,7 +499,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
485
499
|
// The host's URL for the CURRENT view, if taken over. Drives the redirect
|
|
486
500
|
// effect and the "Opening…" splash.
|
|
487
501
|
const viewTakeoverHref =
|
|
488
|
-
mainView === 'issues' || mainView === 'gitops' || mainView === 'checks'
|
|
502
|
+
(mainView === 'issues' || mainView === 'gitops' || mainView === 'checks') && !upgradeReadinessRoute
|
|
489
503
|
? takeover[mainView]
|
|
490
504
|
: undefined
|
|
491
505
|
useEffect(() => {
|
|
@@ -698,6 +712,7 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
698
712
|
newParams.delete('kind')
|
|
699
713
|
newParams.delete('mode')
|
|
700
714
|
newParams.delete('group')
|
|
715
|
+
newParams.delete('target')
|
|
701
716
|
// Open as a normal drawer — never inherit a stale ?full=1/tab from an
|
|
702
717
|
// expanded view we're navigating away from (only expand/drill set those).
|
|
703
718
|
newParams.delete('full')
|
|
@@ -1866,6 +1881,11 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
1866
1881
|
{/* AI investigations (self-hides when no agent CLI is present) */}
|
|
1867
1882
|
<GlobalDiagnoseButton />
|
|
1868
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
|
+
|
|
1869
1889
|
{/* Local terminal */}
|
|
1870
1890
|
{capabilities.localTerminal && (
|
|
1871
1891
|
<Tooltip content="Open local terminal">
|
|
@@ -2285,9 +2305,9 @@ function AppInner({ manageDocumentTitle = false, documentTitleSuffix, onClusterL
|
|
|
2285
2305
|
/>
|
|
2286
2306
|
)}
|
|
2287
2307
|
|
|
2288
|
-
{/*
|
|
2289
|
-
|
|
2290
|
-
{mainView === 'checks' && !isViewTakenOver('checks') && (
|
|
2308
|
+
{/* Checks detail view. Cloud can take over fleet best practices while
|
|
2309
|
+
the target-specific upgrade route continues to render locally. */}
|
|
2310
|
+
{mainView === 'checks' && (!isViewTakenOver('checks') || upgradeReadinessRoute) && (
|
|
2291
2311
|
<AuditView
|
|
2292
2312
|
namespaces={namespaces}
|
|
2293
2313
|
onNavigateToResource={navigateToResourceList}
|
|
@@ -2554,7 +2574,7 @@ function Logo() {
|
|
|
2554
2574
|
<div className="flex items-center gap-2.5">
|
|
2555
2575
|
<div className="relative w-7 h-7 rounded-lg overflow-hidden flex-shrink-0 bg-emerald-500/10 border border-emerald-500/20">
|
|
2556
2576
|
<img
|
|
2557
|
-
src=
|
|
2577
|
+
src={radarLogoUrl}
|
|
2558
2578
|
alt=""
|
|
2559
2579
|
aria-hidden
|
|
2560
2580
|
className="w-full h-full p-0.5"
|
package/src/api/client.ts
CHANGED
|
@@ -391,6 +391,61 @@ export interface AuditResponse {
|
|
|
391
391
|
groupedChecks?: Check[];
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
export type UpgradeReadinessVerdict = 'blocked' | 'warning' | 'review' | 'no_known_blockers' | 'unknown'
|
|
395
|
+
export type UpgradeReadinessLevel = 'blocker' | 'warning' | 'review'
|
|
396
|
+
export type UpgradeReadinessCheckStatus = 'passed' | 'blocked' | 'warning' | 'review' | 'unknown' | 'not_applicable'
|
|
397
|
+
|
|
398
|
+
export interface UpgradeReadinessResourceRef {
|
|
399
|
+
group?: string
|
|
400
|
+
kind: string
|
|
401
|
+
namespace: string
|
|
402
|
+
name: string
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export interface UpgradeReadinessFinding {
|
|
406
|
+
ruleID: string
|
|
407
|
+
title: string
|
|
408
|
+
level: UpgradeReadinessLevel
|
|
409
|
+
resource?: UpgradeReadinessResourceRef
|
|
410
|
+
managedBy?: UpgradeReadinessResourceRef
|
|
411
|
+
evidence: { source: string; path: string; detail?: string }
|
|
412
|
+
appliesFrom?: string
|
|
413
|
+
impact: string
|
|
414
|
+
remediation: string
|
|
415
|
+
references: { title: string; url: string }[]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export interface UpgradeReadinessCheck {
|
|
419
|
+
id: string
|
|
420
|
+
category: string
|
|
421
|
+
title: string
|
|
422
|
+
status: UpgradeReadinessCheckStatus
|
|
423
|
+
summary: string
|
|
424
|
+
evidenceNote?: string
|
|
425
|
+
caveat?: string
|
|
426
|
+
scope: string
|
|
427
|
+
inspected?: number
|
|
428
|
+
appliesFrom?: string
|
|
429
|
+
findings: UpgradeReadinessFinding[]
|
|
430
|
+
references?: { title: string; url: string }[]
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface UpgradeReadinessResponse {
|
|
434
|
+
currentVersion: string
|
|
435
|
+
targetVersion: string
|
|
436
|
+
reviewedThrough: string
|
|
437
|
+
verdict: UpgradeReadinessVerdict
|
|
438
|
+
summary: { blocked: number; warnings: number; reviews: number; passed: number; unknown: number; notApplicable: number; findings: number }
|
|
439
|
+
checks: UpgradeReadinessCheck[]
|
|
440
|
+
coverage: {
|
|
441
|
+
source: 'live'
|
|
442
|
+
state: 'complete' | 'partial' | 'no_access'
|
|
443
|
+
unavailableKinds?: string[]
|
|
444
|
+
scopedNamespaces?: string[]
|
|
445
|
+
scopedKinds?: Record<string, string[]>
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
394
449
|
export interface DashboardCertificateHealth {
|
|
395
450
|
total: number;
|
|
396
451
|
healthy: number;
|
|
@@ -486,6 +541,18 @@ export function useAudit(namespaces: string[] = []) {
|
|
|
486
541
|
});
|
|
487
542
|
}
|
|
488
543
|
|
|
544
|
+
export function useUpgradeReadiness(target?: string) {
|
|
545
|
+
const params = new URLSearchParams()
|
|
546
|
+
if (target) params.set('target', target)
|
|
547
|
+
const query = params.toString()
|
|
548
|
+
return useQuery<UpgradeReadinessResponse>({
|
|
549
|
+
queryKey: ['upgrade-readiness', target ?? 'next'],
|
|
550
|
+
queryFn: ({ signal }) => fetchJSON(`/upgrade-readiness${query ? `?${query}` : ''}`, signal),
|
|
551
|
+
staleTime: 30000,
|
|
552
|
+
placeholderData: (previous) => previous,
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
489
556
|
// Live cluster Issues — the grouped triage queue (radar's /api/issues =
|
|
490
557
|
// internal/issues.Compose+Classify+Group). Single-cluster here; the Hub fleet
|
|
491
558
|
// view fans the same shape across clusters. Do not carry old data across query
|
|
@@ -1478,6 +1545,190 @@ export function useCapabilities() {
|
|
|
1478
1545
|
});
|
|
1479
1546
|
}
|
|
1480
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
|
+
|
|
1481
1732
|
// Namespace-scoped capabilities. Users with namespace-scoped RoleBindings may
|
|
1482
1733
|
// have these permissions in specific namespaces.
|
|
1483
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')
|