@skyhook-io/radar-app 0.1.5 → 0.1.9
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 +6 -6
- package/src/api/client.ts +102 -10
- package/src/components/ContextSwitcher.tsx +98 -357
- package/src/components/audit/AuditView.tsx +3 -10
- package/src/components/cost/CostView.tsx +2 -8
- package/src/components/helm/ChartBrowser.tsx +16 -6
- package/src/components/helm/HelmReleaseDrawer.tsx +53 -36
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +5 -7
- package/src/components/helm/ManifestDiffViewer.tsx +2 -5
- package/src/components/helm/ManifestViewer.tsx +2 -5
- package/src/components/helm/RoleGatedPanel.tsx +47 -0
- package/src/components/helm/ValuesViewer.tsx +5 -8
- package/src/components/home/HelmSummary.tsx +12 -0
- package/src/components/home/HomeView.tsx +3 -10
- package/src/components/resources/ImageFilesystemModal.tsx +6 -5
- package/src/components/resources/PodFilesystemModal.tsx +2 -6
- package/src/components/resources/ResourcesView.tsx +16 -2
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -7
- package/src/components/traffic/TrafficView.tsx +5 -7
- package/src/components/traffic/TrafficWizard.tsx +7 -12
- package/src/index.ts +6 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
import { flushSync } from 'react-dom'
|
|
3
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
4
|
import { TRANSITION_DRAWER } from '../../utils/animation'
|
|
4
5
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
5
6
|
import { X, Copy, Check, RefreshCw, Package, Code, History, FileText, Settings, Link2, Anchor, GitFork, BookOpen, ArrowUpCircle, Trash2 } from 'lucide-react'
|
|
@@ -12,7 +13,8 @@ import type { SelectedHelmRelease, HelmHook, ChartDependency } from '../../types
|
|
|
12
13
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
13
14
|
import { formatDate } from './helm-utils'
|
|
14
15
|
import { getHelmStatusColor, SEVERITY_BADGE, SEVERITY_TEXT } from '../../utils/badge-colors'
|
|
15
|
-
import {
|
|
16
|
+
import { useCanHelmAct, useCloudRole } from '../../api/client'
|
|
17
|
+
import { RoleGatedPanel } from './RoleGatedPanel'
|
|
16
18
|
import { RevisionHistory } from './RevisionHistory'
|
|
17
19
|
import { ManifestViewer } from './ManifestViewer'
|
|
18
20
|
import { ValuesViewer } from './ValuesViewer'
|
|
@@ -46,7 +48,13 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
46
48
|
const [showUpgradeConfirm, setShowUpgradeConfirm] = useState(false)
|
|
47
49
|
const resizeStartX = useRef(0)
|
|
48
50
|
const resizeStartWidth = useRef(DEFAULT_WIDTH)
|
|
49
|
-
const canHelmWrite =
|
|
51
|
+
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
52
|
+
// Cloud viewers can't view release manifests / values / diffs
|
|
53
|
+
// (backend gate at requireCloudRole('member')). Skip the queries
|
|
54
|
+
// when the role would 403 — saves a round-trip and avoids a
|
|
55
|
+
// transient error state under the role-gated panel.
|
|
56
|
+
const { canAtLeast } = useCloudRole()
|
|
57
|
+
const canViewSensitive = canAtLeast('member')
|
|
50
58
|
|
|
51
59
|
const { data: releaseDetail, isLoading, refetch: refetchRelease } = useHelmRelease(
|
|
52
60
|
release.namespace,
|
|
@@ -58,14 +66,16 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
58
66
|
const { data: manifest, isLoading: manifestLoading } = useHelmManifest(
|
|
59
67
|
release.namespace,
|
|
60
68
|
release.name,
|
|
61
|
-
selectedRevision
|
|
69
|
+
selectedRevision,
|
|
70
|
+
canViewSensitive,
|
|
62
71
|
)
|
|
63
72
|
|
|
64
73
|
// Fetch values
|
|
65
74
|
const { data: values, isLoading: valuesLoading } = useHelmValues(
|
|
66
75
|
release.namespace,
|
|
67
76
|
release.name,
|
|
68
|
-
showAllValues
|
|
77
|
+
showAllValues,
|
|
78
|
+
canViewSensitive,
|
|
69
79
|
)
|
|
70
80
|
|
|
71
81
|
// Fetch diff if comparing revisions
|
|
@@ -73,7 +83,8 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
73
83
|
release.namespace,
|
|
74
84
|
release.name,
|
|
75
85
|
diffRevisions?.rev1 || 0,
|
|
76
|
-
diffRevisions?.rev2 || 0
|
|
86
|
+
diffRevisions?.rev2 || 0,
|
|
87
|
+
canViewSensitive,
|
|
77
88
|
)
|
|
78
89
|
|
|
79
90
|
// Lazy check for upgrade availability
|
|
@@ -325,7 +336,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
325
336
|
'badge transition-colors', SEVERITY_BADGE.warning,
|
|
326
337
|
canHelmWrite ? 'hover:bg-amber-500/30 cursor-pointer' : 'opacity-50 cursor-not-allowed'
|
|
327
338
|
)}
|
|
328
|
-
title={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` :
|
|
339
|
+
title={canHelmWrite ? `Click to upgrade: ${upgradeInfo.currentVersion} → ${upgradeInfo.latestVersion}${upgradeInfo.repositoryName ? ` (${upgradeInfo.repositoryName})` : ''}` : helmActReason}
|
|
329
340
|
>
|
|
330
341
|
<ArrowUpCircle className="w-3 h-3" />
|
|
331
342
|
{upgradeInfo.latestVersion}
|
|
@@ -354,7 +365,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
354
365
|
? 'text-theme-text-secondary hover:text-red-400 hover:bg-red-500/10'
|
|
355
366
|
: 'text-theme-text-disabled cursor-not-allowed'
|
|
356
367
|
)}
|
|
357
|
-
title={canHelmWrite ? 'Uninstall release' :
|
|
368
|
+
title={canHelmWrite ? 'Uninstall release' : helmActReason}
|
|
358
369
|
>
|
|
359
370
|
<Trash2 className="w-4 h-4" />
|
|
360
371
|
</button>
|
|
@@ -403,7 +414,7 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
403
414
|
{/* Content */}
|
|
404
415
|
<div className="flex-1 overflow-y-auto" style={{ viewTransitionName: 'helm-drawer-content' }}>
|
|
405
416
|
{isLoading ? (
|
|
406
|
-
<
|
|
417
|
+
<PaneLoader className="h-32" />
|
|
407
418
|
) : !releaseDetail ? (
|
|
408
419
|
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">Release not found</div>
|
|
409
420
|
) : (
|
|
@@ -421,26 +432,30 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
421
432
|
/>
|
|
422
433
|
)}
|
|
423
434
|
{activeTab === 'manifest' && (
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
435
|
+
<RoleGatedPanel min="member" feature="release manifests">
|
|
436
|
+
<ManifestViewer
|
|
437
|
+
manifest={manifest || ''}
|
|
438
|
+
isLoading={manifestLoading}
|
|
439
|
+
revision={selectedRevision}
|
|
440
|
+
onCopy={(text) => copyToClipboard(text, 'manifest')}
|
|
441
|
+
copied={copied === 'manifest'}
|
|
442
|
+
/>
|
|
443
|
+
</RoleGatedPanel>
|
|
431
444
|
)}
|
|
432
445
|
{activeTab === 'values' && (
|
|
433
|
-
<
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
446
|
+
<RoleGatedPanel min="member" feature="release values">
|
|
447
|
+
<ValuesViewer
|
|
448
|
+
values={values}
|
|
449
|
+
isLoading={valuesLoading}
|
|
450
|
+
showAllValues={showAllValues}
|
|
451
|
+
onToggleAllValues={setShowAllValues}
|
|
452
|
+
onCopy={(text) => copyToClipboard(text, 'values')}
|
|
453
|
+
copied={copied === 'values'}
|
|
454
|
+
namespace={release.namespace}
|
|
455
|
+
name={release.name}
|
|
456
|
+
onApplySuccess={() => refetch()}
|
|
457
|
+
/>
|
|
458
|
+
</RoleGatedPanel>
|
|
444
459
|
)}
|
|
445
460
|
{activeTab === 'resources' && (
|
|
446
461
|
<OwnedResources
|
|
@@ -452,16 +467,18 @@ export function HelmReleaseDrawer({ release, onClose, onNavigateToResource, isOp
|
|
|
452
467
|
<HooksTab hooks={releaseDetail.hooks || []} />
|
|
453
468
|
)}
|
|
454
469
|
{activeTab === 'diff' && diffRevisions && (
|
|
455
|
-
<
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
470
|
+
<RoleGatedPanel min="member" feature="release manifest diffs">
|
|
471
|
+
<ManifestDiffViewer
|
|
472
|
+
diff={diffData?.diff || ''}
|
|
473
|
+
isLoading={diffLoading}
|
|
474
|
+
revision1={diffRevisions.rev1}
|
|
475
|
+
revision2={diffRevisions.rev2}
|
|
476
|
+
onClose={() => {
|
|
477
|
+
setDiffRevisions(null)
|
|
478
|
+
setActiveTab('history')
|
|
479
|
+
}}
|
|
480
|
+
/>
|
|
481
|
+
</RoleGatedPanel>
|
|
465
482
|
)}
|
|
466
483
|
</>
|
|
467
484
|
)}
|
|
@@ -2,6 +2,7 @@ import { useState, useMemo, useRef, useEffect, useCallback, forwardRef } from 'r
|
|
|
2
2
|
import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
|
|
3
3
|
import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
|
|
4
4
|
import { Package, Search, RefreshCw, ArrowUpCircle, LayoutGrid, List, Shield } from 'lucide-react'
|
|
5
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
5
6
|
import { clsx } from 'clsx'
|
|
6
7
|
import { useHelmReleases, useHelmBatchUpgradeInfo, isForbiddenError } from '../../api/client'
|
|
7
8
|
import type { HelmRelease, SelectedHelmRelease, UpgradeInfo, ChartSource } from '../../types'
|
|
@@ -232,9 +233,7 @@ export function HelmView({ namespace, selectedRelease, onReleaseClick }: HelmVie
|
|
|
232
233
|
{/* Releases Table */}
|
|
233
234
|
<div className="flex-1 overflow-auto">
|
|
234
235
|
{isLoading ? (
|
|
235
|
-
<
|
|
236
|
-
Loading...
|
|
237
|
-
</div>
|
|
236
|
+
<PaneLoader className="h-full" />
|
|
238
237
|
) : isForbidden ? (
|
|
239
238
|
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
|
|
240
239
|
<Shield className="w-8 h-8 text-amber-400 mb-2" />
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
2
|
import { X, Package, ChevronRight, ChevronLeft, Play, Loader2, AlertTriangle, CheckCircle, User, BookOpen, Link as LinkIcon, Star, BadgeCheck, Shield, Globe, Building2, Plus, Minus, Terminal } from 'lucide-react'
|
|
3
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
4
5
|
import yaml from 'yaml'
|
|
5
6
|
import { createPatch } from 'diff'
|
|
6
7
|
import { useQueryClient } from '@tanstack/react-query'
|
|
7
8
|
import { useChartDetail, useNamespaces, useArtifactHubChart, installChartWithProgress, type InstallProgressEvent } from '../../api/client'
|
|
8
|
-
import {
|
|
9
|
+
import { useCanHelmAct } from '../../api/client'
|
|
9
10
|
import type { ChartSource, ChartDetail, ArtifactHubChartDetail } from '../../types'
|
|
10
11
|
import { YamlEditor } from '../ui/YamlEditor'
|
|
11
12
|
import { Tooltip } from '../ui/Tooltip'
|
|
@@ -64,7 +65,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
64
65
|
const progressEndRef = useRef<HTMLDivElement>(null)
|
|
65
66
|
|
|
66
67
|
const queryClient = useQueryClient()
|
|
67
|
-
const canHelmWrite =
|
|
68
|
+
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
68
69
|
|
|
69
70
|
// Choose the right data based on source
|
|
70
71
|
const isLocal = source === 'local'
|
|
@@ -277,10 +278,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
277
278
|
{/* Content */}
|
|
278
279
|
<div className="flex-1 overflow-auto p-4">
|
|
279
280
|
{chartLoading ? (
|
|
280
|
-
<
|
|
281
|
-
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
282
|
-
Loading chart details...
|
|
283
|
-
</div>
|
|
281
|
+
<PaneLoader label="Loading chart details…" className="h-32" />
|
|
284
282
|
) : (
|
|
285
283
|
<>
|
|
286
284
|
{step === 'info' && (
|
|
@@ -389,7 +387,7 @@ export function InstallWizard({ repo, chartName, version, source, repoUrl, defau
|
|
|
389
387
|
onClick={handleInstall}
|
|
390
388
|
disabled={!canInstall || isInstalling || !canHelmWrite}
|
|
391
389
|
className="flex items-center gap-2 px-4 py-2 text-sm font-medium btn-brand rounded-lg disabled:cursor-not-allowed"
|
|
392
|
-
title={!canHelmWrite ?
|
|
390
|
+
title={!canHelmWrite ? helmActReason : undefined}
|
|
393
391
|
>
|
|
394
392
|
{isInstalling ? (
|
|
395
393
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { X, GitCompare } from 'lucide-react'
|
|
2
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
2
3
|
import { clsx } from 'clsx'
|
|
3
4
|
|
|
4
5
|
interface ManifestDiffViewerProps {
|
|
@@ -11,11 +12,7 @@ interface ManifestDiffViewerProps {
|
|
|
11
12
|
|
|
12
13
|
export function ManifestDiffViewer({ diff, isLoading, revision1, revision2, onClose }: ManifestDiffViewerProps) {
|
|
13
14
|
if (isLoading) {
|
|
14
|
-
return
|
|
15
|
-
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
16
|
-
Computing diff...
|
|
17
|
-
</div>
|
|
18
|
-
)
|
|
15
|
+
return <PaneLoader label="Computing diff…" className="h-32" />
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
if (!diff) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Copy, Check, Code } from 'lucide-react'
|
|
2
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
2
3
|
import { CodeViewer } from '../ui/CodeViewer'
|
|
3
4
|
|
|
4
5
|
interface ManifestViewerProps {
|
|
@@ -11,11 +12,7 @@ interface ManifestViewerProps {
|
|
|
11
12
|
|
|
12
13
|
export function ManifestViewer({ manifest, isLoading, revision, onCopy, copied }: ManifestViewerProps) {
|
|
13
14
|
if (isLoading) {
|
|
14
|
-
return
|
|
15
|
-
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
16
|
-
Loading manifest...
|
|
17
|
-
</div>
|
|
18
|
-
)
|
|
15
|
+
return <PaneLoader label="Loading manifest…" className="h-32" />
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
if (!manifest) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Lock } from 'lucide-react'
|
|
2
|
+
import { useCloudRole, type CloudRole } from '../../api/client'
|
|
3
|
+
|
|
4
|
+
interface RoleGatedPanelProps {
|
|
5
|
+
/** Minimum Cloud tier required to view the children. */
|
|
6
|
+
min: CloudRole
|
|
7
|
+
/** Human-readable name of the gated content, e.g. "release manifests". */
|
|
8
|
+
feature: string
|
|
9
|
+
children: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RoleGatedPanel wraps tab-content surfaces that require a Cloud role
|
|
14
|
+
* (member+) to view. Renders the children when the gate passes; renders
|
|
15
|
+
* an explanatory empty state when the caller's role is insufficient.
|
|
16
|
+
*
|
|
17
|
+
* Why a per-tab gate rather than hiding tabs from the list: viewers
|
|
18
|
+
* should learn what the platform offers and what their tier can't do,
|
|
19
|
+
* not have features silently vanish. This mirrors how the action
|
|
20
|
+
* buttons (Uninstall, Upgrade, etc.) are rendered visible-but-disabled
|
|
21
|
+
* with a role-aware tooltip.
|
|
22
|
+
*
|
|
23
|
+
* Bypasses for non-Cloud users (OSS, OIDC, etc.) — `canAtLeast` returns
|
|
24
|
+
* true when no Cloud role is present. The backend gate has the same
|
|
25
|
+
* shape, so the SPA stays in lockstep.
|
|
26
|
+
*/
|
|
27
|
+
export function RoleGatedPanel({ min, feature, children }: RoleGatedPanelProps) {
|
|
28
|
+
const { role, canAtLeast } = useCloudRole()
|
|
29
|
+
if (canAtLeast(min)) {
|
|
30
|
+
return <>{children}</>
|
|
31
|
+
}
|
|
32
|
+
return (
|
|
33
|
+
<div className="flex flex-col items-center justify-center h-full py-12 px-6 text-center">
|
|
34
|
+
<div className="rounded-full bg-theme-surface p-3 mb-3">
|
|
35
|
+
<Lock className="w-5 h-5 text-theme-text-tertiary" aria-hidden />
|
|
36
|
+
</div>
|
|
37
|
+
<div className="text-sm font-medium text-theme-text-primary">
|
|
38
|
+
Your role can't view {feature}
|
|
39
|
+
</div>
|
|
40
|
+
<div className="mt-1 max-w-md text-xs text-theme-text-secondary">
|
|
41
|
+
You're signed in as <span className="font-mono text-theme-text-primary">{role ?? 'viewer'}</span>.
|
|
42
|
+
This view requires <span className="font-mono text-theme-text-primary">{min}</span> or higher.
|
|
43
|
+
Ask a {min} or owner if you need access.
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react'
|
|
2
2
|
import { Copy, Check, Settings, Pencil, X, Eye, Play, Loader2 } from 'lucide-react'
|
|
3
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
4
5
|
import yaml from 'yaml'
|
|
5
6
|
import type { HelmValues, ValuesPreviewResponse } from '../../types'
|
|
6
7
|
import { CodeViewer } from '../ui/CodeViewer'
|
|
7
8
|
import { YamlEditor } from '../ui/YamlEditor'
|
|
8
9
|
import { useHelmPreviewValues, useHelmApplyValues } from '../../api/client'
|
|
9
|
-
import {
|
|
10
|
+
import { useCanHelmAct } from '../../api/client'
|
|
10
11
|
import { ValuesDiffPreview } from './ValuesDiffPreview'
|
|
11
12
|
|
|
12
13
|
interface ValuesViewerProps {
|
|
@@ -41,7 +42,7 @@ export function ValuesViewer({
|
|
|
41
42
|
|
|
42
43
|
const previewMutation = useHelmPreviewValues()
|
|
43
44
|
const applyMutation = useHelmApplyValues()
|
|
44
|
-
const canHelmWrite =
|
|
45
|
+
const { allowed: canHelmWrite, reason: helmActReason } = useCanHelmAct()
|
|
45
46
|
|
|
46
47
|
const canEdit = Boolean(namespace && name) && canHelmWrite
|
|
47
48
|
|
|
@@ -138,11 +139,7 @@ export function ValuesViewer({
|
|
|
138
139
|
}, [previewData, namespace, name, applyMutation, handleCancelEdit, onApplySuccess])
|
|
139
140
|
|
|
140
141
|
if (isLoading) {
|
|
141
|
-
return
|
|
142
|
-
<div className="flex items-center justify-center h-32 text-theme-text-tertiary">
|
|
143
|
-
Loading values...
|
|
144
|
-
</div>
|
|
145
|
-
)
|
|
142
|
+
return <PaneLoader label="Loading values…" className="h-32" />
|
|
146
143
|
}
|
|
147
144
|
|
|
148
145
|
if (isEmpty && !isEditing) {
|
|
@@ -234,7 +231,7 @@ export function ValuesViewer({
|
|
|
234
231
|
onClick={handleApply}
|
|
235
232
|
disabled={!!yamlError || applyMutation.isPending || !canHelmWrite}
|
|
236
233
|
className="flex items-center gap-1 px-2.5 py-1 text-xs btn-brand rounded disabled:cursor-not-allowed"
|
|
237
|
-
title={!canHelmWrite ?
|
|
234
|
+
title={!canHelmWrite ? helmActReason : undefined}
|
|
238
235
|
>
|
|
239
236
|
{applyMutation.isPending ? (
|
|
240
237
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
@@ -82,6 +82,18 @@ export function HelmSummary({ data, onNavigate }: HelmSummaryProps) {
|
|
|
82
82
|
<span className="text-xs font-medium text-theme-text-secondary">Access Restricted</span>
|
|
83
83
|
<span className="text-[11px] mt-1">Insufficient permissions to list Helm releases</span>
|
|
84
84
|
</div>
|
|
85
|
+
) : data.error ? (
|
|
86
|
+
<div className="flex flex-col items-center justify-center h-full py-4 text-theme-text-tertiary">
|
|
87
|
+
<Shield className="w-8 h-8 text-amber-400 mb-2" />
|
|
88
|
+
<span className="text-xs font-medium text-theme-text-secondary">
|
|
89
|
+
{data.errorCode === 'unconfigured' ? 'Helm not configured' : 'Helm unavailable'}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="text-[11px] mt-1 text-center px-2 truncate max-w-full" title={data.error}>
|
|
92
|
+
{data.errorCode === 'unconfigured'
|
|
93
|
+
? 'Set rbac.helm=true in the Radar Helm chart values.'
|
|
94
|
+
: data.error}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
85
97
|
) : !data.releases || data.releases.length === 0 ? (
|
|
86
98
|
<div className="flex items-center justify-center h-full py-4 text-xs text-theme-text-tertiary">
|
|
87
99
|
No Helm releases found
|
|
@@ -9,9 +9,9 @@ import { TrafficSummary } from './TrafficSummary'
|
|
|
9
9
|
import { CertificateHealthCard } from './CertificateHealthCard'
|
|
10
10
|
import { NetworkPolicyCoverageCard } from './NetworkPolicyCoverageCard'
|
|
11
11
|
import { CostCard } from './CostCard'
|
|
12
|
-
import { AuditCard, StatusDot, mapHealthToTone } from '@skyhook-io/k8s-ui'
|
|
12
|
+
import { AuditCard, PaneLoader, StatusDot, mapHealthToTone } from '@skyhook-io/k8s-ui'
|
|
13
13
|
import { ClusterHealthCard } from './ClusterHealthCard'
|
|
14
|
-
import { AlertTriangle,
|
|
14
|
+
import { AlertTriangle, Shield } from 'lucide-react'
|
|
15
15
|
import { clsx } from 'clsx'
|
|
16
16
|
|
|
17
17
|
interface HomeViewProps {
|
|
@@ -29,14 +29,7 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
29
29
|
const { data: helmData } = useDashboardHelm(namespaces)
|
|
30
30
|
|
|
31
31
|
if (isLoading) {
|
|
32
|
-
return
|
|
33
|
-
<div className="flex-1 flex items-center justify-center">
|
|
34
|
-
<div className="flex flex-col items-center gap-3">
|
|
35
|
-
<Loader2 className="w-6 h-6 animate-spin text-theme-text-tertiary" />
|
|
36
|
-
<span className="text-sm text-theme-text-tertiary">Loading dashboard...</span>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
39
|
-
)
|
|
32
|
+
return <PaneLoader label="Loading dashboard…" className="flex-1" />
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
if (error || !data) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { X, Folder, File, Link2, ChevronRight, ChevronDown, AlertTriangle, Loader2, Search, Download, HardDrive, Shield, ShieldCheck, Terminal, Copy, Check, RefreshCw } from 'lucide-react'
|
|
4
|
+
import radarLoadingIcon from '@skyhook-io/k8s-ui/assets/radar/radar-icon-loading.svg'
|
|
4
5
|
import { clsx } from 'clsx'
|
|
5
6
|
import { useImageMetadata, ApiError } from '../../api/client'
|
|
6
7
|
import type { FileNode, ImageFilesystem } from '../../types'
|
|
@@ -177,13 +178,13 @@ export function ImageFilesystemModal({
|
|
|
177
178
|
<div className="flex-1 overflow-y-auto p-4">
|
|
178
179
|
{/* Loading state */}
|
|
179
180
|
{isLoading && (
|
|
180
|
-
<div className="flex flex-col items-center justify-center h-64">
|
|
181
|
-
<
|
|
182
|
-
<span className="
|
|
183
|
-
{isLoadingMetadata ? 'Checking image
|
|
181
|
+
<div className="flex flex-col items-center justify-center gap-3 h-64">
|
|
182
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
183
|
+
<span className="text-sm text-theme-text-secondary">
|
|
184
|
+
{isLoadingMetadata ? 'Checking image…' : 'Downloading image layers…'}
|
|
184
185
|
</span>
|
|
185
186
|
{isLoadingFilesystem && metadata && (
|
|
186
|
-
<span className="
|
|
187
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
187
188
|
This may take a moment for large images
|
|
188
189
|
</span>
|
|
189
190
|
)}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import { X, File, Link2, ChevronRight, AlertTriangle, Loader2, Search, Download, FolderOpen } from 'lucide-react'
|
|
4
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
4
5
|
import { clsx } from 'clsx'
|
|
5
6
|
import type { FileNode } from '../../types'
|
|
6
7
|
import { formatBytes } from '../../utils/format'
|
|
@@ -204,12 +205,7 @@ export function PodFilesystemModal({
|
|
|
204
205
|
{/* Content */}
|
|
205
206
|
<div className="flex-1 overflow-y-auto p-4">
|
|
206
207
|
{/* Loading */}
|
|
207
|
-
{isLoading &&
|
|
208
|
-
<div className="flex flex-col items-center justify-center h-64">
|
|
209
|
-
<Loader2 className="w-8 h-8 text-blue-400 animate-spin" />
|
|
210
|
-
<span className="mt-3 text-theme-text-secondary">Loading files...</span>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
208
|
+
{isLoading && <PaneLoader label="Loading files…" className="h-64" />}
|
|
213
209
|
|
|
214
210
|
{/* Error */}
|
|
215
211
|
{error && !isLoading && (
|
|
@@ -120,10 +120,24 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
120
120
|
const openLogs = useOpenLogs()
|
|
121
121
|
const openWorkloadLogs = useOpenWorkloadLogs()
|
|
122
122
|
|
|
123
|
-
// Navigation adapter
|
|
123
|
+
// Navigation adapter. k8s-ui constructs paths from `basePath` (which
|
|
124
|
+
// includes the router basename so they line up with window.location.pathname
|
|
125
|
+
// for path-equality checks) and from `window.location.pathname` directly.
|
|
126
|
+
// React Router's navigate() applies the basename itself, so handing it a
|
|
127
|
+
// path that already contains the basename double-prefixes it
|
|
128
|
+
// (e.g. /c/abc/c/abc/resources/pods). Under that URL, getViewFromPath()
|
|
129
|
+
// sees 'c' as the first segment and falls through to 'home' — which
|
|
130
|
+
// manifests as "click a resource → bounced to the home dashboard" in
|
|
131
|
+
// any host that mounts RadarApp under a non-empty basename (Radar Cloud).
|
|
132
|
+
// Strip the basename here so react-router can re-apply it cleanly.
|
|
124
133
|
const handleNavigate = useMemo(() => {
|
|
134
|
+
const base = getBasename()
|
|
125
135
|
return (path: string, options?: { replace?: boolean }) => {
|
|
126
|
-
|
|
136
|
+
let p = path
|
|
137
|
+
if (base && (p === base || p.startsWith(base + '/') || p.startsWith(base + '?'))) {
|
|
138
|
+
p = p.slice(base.length) || '/'
|
|
139
|
+
}
|
|
140
|
+
navigate(p, { replace: options?.replace })
|
|
127
141
|
}
|
|
128
142
|
}, [navigate])
|
|
129
143
|
|
|
@@ -27,7 +27,7 @@ import { useHasLimitedAccess } from '../../contexts/CapabilitiesContext'
|
|
|
27
27
|
import type { TimelineEvent, Topology } from '../../types'
|
|
28
28
|
import type { NavigateToResource } from '../../utils/navigation'
|
|
29
29
|
import { kindToPlural } from '../../utils/navigation'
|
|
30
|
-
import { pluralize } from '@skyhook-io/k8s-ui'
|
|
30
|
+
import { PaneLoader, pluralize } from '@skyhook-io/k8s-ui'
|
|
31
31
|
import { isChangeEvent, isHistoricalEvent, isOperation, displayKind } from '../../types'
|
|
32
32
|
import { DiffViewer } from './DiffViewer'
|
|
33
33
|
import { getOperationColor, getHealthBadgeColor, getEventTypeColor } from '../../utils/badge-colors'
|
|
@@ -436,12 +436,7 @@ export function TimelineSwimlanes({ events, isLoading, onResourceClick, viewMode
|
|
|
436
436
|
}, [])
|
|
437
437
|
|
|
438
438
|
if (isLoading) {
|
|
439
|
-
return
|
|
440
|
-
<div className="flex items-center justify-center h-full w-full text-theme-text-tertiary">
|
|
441
|
-
<RefreshCw className="w-5 h-5 animate-spin mr-2" />
|
|
442
|
-
Loading timeline...
|
|
443
|
-
</div>
|
|
444
|
-
)
|
|
439
|
+
return <PaneLoader label="Loading timeline…" className="h-full w-full" />
|
|
445
440
|
}
|
|
446
441
|
|
|
447
442
|
// Compute empty state info (but don't early return - we need the toolbar visible)
|
|
@@ -11,7 +11,7 @@ import { Loader2, RefreshCw, Filter, Plug, ChevronDown, List, Activity, AlertTri
|
|
|
11
11
|
import { clsx } from 'clsx'
|
|
12
12
|
import { useQueryClient } from '@tanstack/react-query'
|
|
13
13
|
import { useDock } from '../dock'
|
|
14
|
-
import { EmptyState } from '@skyhook-io/k8s-ui'
|
|
14
|
+
import { EmptyState, PaneLoader } from '@skyhook-io/k8s-ui'
|
|
15
15
|
|
|
16
16
|
// Addon types for filtering
|
|
17
17
|
export type AddonMode = 'show' | 'group' | 'hide'
|
|
@@ -1136,12 +1136,10 @@ export function TrafficView({ namespaces }: TrafficViewProps) {
|
|
|
1136
1136
|
})()}
|
|
1137
1137
|
|
|
1138
1138
|
{isConnecting || (flowsFetching && finalFlows.length === 0) ? (
|
|
1139
|
-
<
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
</div>
|
|
1144
|
-
</div>
|
|
1139
|
+
<PaneLoader
|
|
1140
|
+
label={isConnecting ? 'Connecting to traffic source…' : 'Loading traffic data…'}
|
|
1141
|
+
className="absolute inset-0"
|
|
1142
|
+
/>
|
|
1145
1143
|
) : finalFlows.length > 0 ? (
|
|
1146
1144
|
<TrafficGraph
|
|
1147
1145
|
flows={finalFlows}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react'
|
|
2
2
|
import type { TrafficSourcesResponse, TrafficWizardState } from '../../types'
|
|
3
|
-
import {
|
|
3
|
+
import { CheckCircle2, XCircle, AlertTriangle, Copy, ExternalLink, ArrowRight, ArrowLeft, Package } from 'lucide-react'
|
|
4
|
+
import { PaneLoader } from '@skyhook-io/k8s-ui'
|
|
5
|
+
import radarLoadingIcon from '@skyhook-io/k8s-ui/assets/radar/radar-icon-loading.svg'
|
|
4
6
|
import { InstallWizard } from '../helm/InstallWizard'
|
|
5
7
|
|
|
6
8
|
interface TrafficWizardProps {
|
|
@@ -88,14 +90,7 @@ export function TrafficWizard({
|
|
|
88
90
|
|
|
89
91
|
// Detecting state
|
|
90
92
|
if (state === 'detecting' || sourcesLoading) {
|
|
91
|
-
return
|
|
92
|
-
<div className="flex items-center justify-center h-full w-full">
|
|
93
|
-
<div className="text-center space-y-4">
|
|
94
|
-
<Loader2 className="h-8 w-8 animate-spin text-blue-400 mx-auto" />
|
|
95
|
-
<p className="text-theme-text-secondary">Detecting traffic sources...</p>
|
|
96
|
-
</div>
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
93
|
+
return <PaneLoader label="Detecting traffic sources…" className="h-full w-full" />
|
|
99
94
|
}
|
|
100
95
|
|
|
101
96
|
// Checking state (polling for newly enabled source)
|
|
@@ -104,9 +99,9 @@ export function TrafficWizard({
|
|
|
104
99
|
<>
|
|
105
100
|
<div className="flex items-center justify-center h-full w-full">
|
|
106
101
|
<div className="max-w-md w-full p-6 space-y-6">
|
|
107
|
-
<div className="
|
|
108
|
-
<
|
|
109
|
-
<h2 className="text-lg font-medium text-theme-text-primary">Waiting for traffic source
|
|
102
|
+
<div className="flex flex-col items-center gap-3">
|
|
103
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="h-11 w-11" />
|
|
104
|
+
<h2 className="text-lg font-medium text-theme-text-primary">Waiting for traffic source…</h2>
|
|
110
105
|
<p className="text-sm text-theme-text-secondary">
|
|
111
106
|
Checking for availability
|
|
112
107
|
</p>
|
package/src/index.ts
CHANGED
|
@@ -16,3 +16,9 @@ export {
|
|
|
16
16
|
} from './api/config';
|
|
17
17
|
export type { NavCustomization } from './context/NavCustomization';
|
|
18
18
|
export { ShortcutHelpOverlay } from './components/ui/ShortcutHelpOverlay';
|
|
19
|
+
|
|
20
|
+
// Shared cluster-switcher primitive — re-exported from @skyhook-io/k8s-ui so
|
|
21
|
+
// embedders (Radar Hub) can render a switcher visually identical to OSS Radar's
|
|
22
|
+
// kubeconfig ContextSwitcher without taking a direct dep on k8s-ui internals.
|
|
23
|
+
export { ClusterSwitcher } from '@skyhook-io/k8s-ui';
|
|
24
|
+
export type { ClusterSwitcherProps, ClusterSwitcherItem } from '@skyhook-io/k8s-ui';
|