@skyhook-io/radar-app 1.0.0 → 1.0.1
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 +5 -5
- package/src/App.tsx +135 -34
- package/src/api/client.ts +94 -3
- package/src/components/ContextSwitcher.tsx +49 -16
- package/src/components/NamespaceSwitcher.tsx +298 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +30 -13
- package/src/components/helm/HelmView.tsx +33 -7
- package/src/components/portforward/PortForwardManager.tsx +152 -111
- package/src/components/resources/ResourcesView.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +2 -2
- package/src/components/ui/NamespaceSelector.tsx +0 -436
|
@@ -42,11 +42,44 @@ interface PortForwardSession {
|
|
|
42
42
|
localPort: number
|
|
43
43
|
listenAddress: string
|
|
44
44
|
serviceName?: string
|
|
45
|
+
servicePort?: number
|
|
46
|
+
scheme?: 'http' | 'https'
|
|
45
47
|
startedAt: string
|
|
46
48
|
status: 'running' | 'stopped' | 'error'
|
|
47
49
|
error?: string
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
function sessionUrl(session: PortForwardSession): string {
|
|
53
|
+
return `${session.scheme || 'http'}://localhost:${session.localPort}`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatPortLabel(session: PortForwardSession): string {
|
|
57
|
+
if (session.servicePort && session.servicePort !== session.podPort) {
|
|
58
|
+
return `${session.servicePort} → ${session.podPort}`
|
|
59
|
+
}
|
|
60
|
+
return String(session.podPort)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Build the recreate request body for toggle-listen / change-port flows.
|
|
64
|
+
// When the original session was service-resolved, the recreate must also go
|
|
65
|
+
// through the service path (servicePort + serviceName). Sending podName+podPort
|
|
66
|
+
// would skip resolution and validate against the pod's declared containerPorts,
|
|
67
|
+
// which can fail even though the original session worked: services can route to
|
|
68
|
+
// any port the container actually listens on, regardless of containerPort
|
|
69
|
+
// declarations. Going through service also re-resolves to a currently-running
|
|
70
|
+
// pod if the original has since been replaced.
|
|
71
|
+
function buildRecreateBody(session: PortForwardSession, overrides: { localPort: number; listenAddress: string }) {
|
|
72
|
+
const base = {
|
|
73
|
+
namespace: session.namespace,
|
|
74
|
+
localPort: overrides.localPort,
|
|
75
|
+
listenAddress: overrides.listenAddress,
|
|
76
|
+
}
|
|
77
|
+
if (session.serviceName && session.servicePort) {
|
|
78
|
+
return { ...base, serviceName: session.serviceName, podPort: session.servicePort }
|
|
79
|
+
}
|
|
80
|
+
return { ...base, podName: session.podName, podPort: session.podPort }
|
|
81
|
+
}
|
|
82
|
+
|
|
50
83
|
// --- Shared query ------------------------------------------------------------
|
|
51
84
|
|
|
52
85
|
function usePortForwardQuery() {
|
|
@@ -139,10 +172,25 @@ export function PortForwardProvider({ children }: { children: ReactNode }) {
|
|
|
139
172
|
const measureAnchor = useCallback(() => {
|
|
140
173
|
if (!indicatorRef.current) return
|
|
141
174
|
const rect = indicatorRef.current.getBoundingClientRect()
|
|
175
|
+
// Align panel right edge with indicator right edge, but clamp so the
|
|
176
|
+
// panel's left edge never runs off the viewport's left edge — happens on
|
|
177
|
+
// narrow windows / split-screens where the indicator is closer to the
|
|
178
|
+
// left edge than the panel is wide. PANEL_WIDTH must match the panel's
|
|
179
|
+
// Tailwind w-80 (20rem = 320px).
|
|
180
|
+
const PANEL_WIDTH = 320
|
|
181
|
+
const MARGIN = 8
|
|
182
|
+
const desiredRight = Math.max(MARGIN, window.innerWidth - rect.right)
|
|
183
|
+
const maxRight = Math.max(MARGIN, window.innerWidth - PANEL_WIDTH - MARGIN)
|
|
184
|
+
const right = Math.min(desiredRight, maxRight)
|
|
185
|
+
// Keep the caret pointing at the indicator's horizontal center even after
|
|
186
|
+
// the panel has been clamped away from the indicator.
|
|
187
|
+
const panelRightX = window.innerWidth - right
|
|
188
|
+
const indicatorCenterX = rect.right - rect.width / 2
|
|
189
|
+
const caretRight = panelRightX - indicatorCenterX - 6
|
|
142
190
|
setAnchor({
|
|
143
191
|
top: rect.bottom + 10,
|
|
144
|
-
right
|
|
145
|
-
caretRight
|
|
192
|
+
right,
|
|
193
|
+
caretRight,
|
|
146
194
|
})
|
|
147
195
|
}, [])
|
|
148
196
|
|
|
@@ -415,14 +463,7 @@ export function PortForwardPanel() {
|
|
|
415
463
|
const res = await fetch(apiUrl('/portforwards'), {
|
|
416
464
|
method: 'POST',
|
|
417
465
|
headers: { 'Content-Type': 'application/json' },
|
|
418
|
-
body: JSON.stringify({
|
|
419
|
-
namespace: session.namespace,
|
|
420
|
-
podName: session.podName || undefined,
|
|
421
|
-
serviceName: session.serviceName || undefined,
|
|
422
|
-
podPort: session.podPort,
|
|
423
|
-
localPort: session.localPort,
|
|
424
|
-
listenAddress: newAddress,
|
|
425
|
-
}),
|
|
466
|
+
body: JSON.stringify(buildRecreateBody(session, { localPort: session.localPort, listenAddress: newAddress })),
|
|
426
467
|
})
|
|
427
468
|
if (!res.ok) {
|
|
428
469
|
const error = await res.json().catch(() => ({}))
|
|
@@ -469,14 +510,7 @@ export function PortForwardPanel() {
|
|
|
469
510
|
const res = await fetch(apiUrl('/portforwards'), {
|
|
470
511
|
method: 'POST',
|
|
471
512
|
headers: { 'Content-Type': 'application/json' },
|
|
472
|
-
body: JSON.stringify({
|
|
473
|
-
namespace: session.namespace,
|
|
474
|
-
podName: session.podName || undefined,
|
|
475
|
-
serviceName: session.serviceName || undefined,
|
|
476
|
-
podPort: session.podPort,
|
|
477
|
-
localPort: newPort,
|
|
478
|
-
listenAddress: session.listenAddress,
|
|
479
|
-
}),
|
|
513
|
+
body: JSON.stringify(buildRecreateBody(session, { localPort: newPort, listenAddress: session.listenAddress })),
|
|
480
514
|
})
|
|
481
515
|
if (!res.ok) {
|
|
482
516
|
const error = await res.json().catch(() => ({}))
|
|
@@ -506,7 +540,7 @@ export function PortForwardPanel() {
|
|
|
506
540
|
async (session: PortForwardSession) => {
|
|
507
541
|
commitInteraction()
|
|
508
542
|
try {
|
|
509
|
-
await navigator.clipboard.writeText(
|
|
543
|
+
await navigator.clipboard.writeText(sessionUrl(session))
|
|
510
544
|
} catch (err) {
|
|
511
545
|
// Clipboard API can reject in non-secure contexts, denied permissions, or
|
|
512
546
|
// when the document isn't focused. Surface the failure — the checkmark
|
|
@@ -529,7 +563,7 @@ export function PortForwardPanel() {
|
|
|
529
563
|
const handleOpenUrl = useCallback(
|
|
530
564
|
(session: PortForwardSession) => {
|
|
531
565
|
commitInteraction()
|
|
532
|
-
openExternal(
|
|
566
|
+
openExternal(sessionUrl(session))
|
|
533
567
|
},
|
|
534
568
|
[commitInteraction]
|
|
535
569
|
)
|
|
@@ -611,7 +645,7 @@ export function PortForwardPanel() {
|
|
|
611
645
|
</div>
|
|
612
646
|
|
|
613
647
|
{/* Sessions list */}
|
|
614
|
-
<div className="max-h-
|
|
648
|
+
<div className="max-h-[28rem] overflow-y-auto">
|
|
615
649
|
{isQueryError ? (
|
|
616
650
|
<div className="p-3 text-xs bg-red-500/10 border-b border-theme-border">
|
|
617
651
|
<div className={clsx('badge-sm mb-1 inline-block', SEVERITY_BADGE.error)}>
|
|
@@ -636,37 +670,84 @@ export function PortForwardPanel() {
|
|
|
636
670
|
<div
|
|
637
671
|
key={session.id}
|
|
638
672
|
className={clsx(
|
|
639
|
-
'p-3',
|
|
673
|
+
'p-3 space-y-1',
|
|
640
674
|
session.status === 'error' ? 'bg-red-500/10' : 'hover:bg-theme-elevated'
|
|
641
675
|
)}
|
|
642
676
|
>
|
|
677
|
+
{/* Row 1: status dot + name | stop button */}
|
|
643
678
|
<div className="flex items-start justify-between gap-2">
|
|
644
|
-
<div className="flex-
|
|
645
|
-
<
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
session.status === 'running' ? 'bg-green-500' : 'bg-red-500'
|
|
650
|
-
)}
|
|
651
|
-
/>
|
|
652
|
-
<span className="text-sm text-theme-text-primary font-medium truncate">
|
|
653
|
-
{session.serviceName || session.podName}
|
|
654
|
-
</span>
|
|
655
|
-
{session.status === 'error' && (
|
|
656
|
-
<span className={clsx('badge-sm', SEVERITY_BADGE.error)}>Failed</span>
|
|
679
|
+
<div className="flex items-start gap-2 min-w-0 flex-1">
|
|
680
|
+
<span
|
|
681
|
+
className={clsx(
|
|
682
|
+
'w-2 h-2 rounded-full shrink-0 mt-[7px]',
|
|
683
|
+
session.status === 'running' ? 'bg-green-500' : 'bg-red-500'
|
|
657
684
|
)}
|
|
658
|
-
|
|
659
|
-
<
|
|
660
|
-
{session.
|
|
661
|
-
</
|
|
662
|
-
{session.status === 'error' &&
|
|
663
|
-
<
|
|
664
|
-
{session.error}
|
|
665
|
-
</div>
|
|
685
|
+
/>
|
|
686
|
+
<span className="text-sm text-theme-text-primary font-medium break-all line-clamp-2">
|
|
687
|
+
{session.serviceName || session.podName}
|
|
688
|
+
</span>
|
|
689
|
+
{session.status === 'error' && (
|
|
690
|
+
<span className={clsx('badge-sm shrink-0', SEVERITY_BADGE.error)}>Failed</span>
|
|
666
691
|
)}
|
|
692
|
+
</div>
|
|
693
|
+
<div className="flex items-center gap-0.5 shrink-0">
|
|
667
694
|
{session.status === 'running' && (
|
|
668
|
-
<
|
|
669
|
-
{
|
|
695
|
+
<Tooltip
|
|
696
|
+
content={session.listenAddress === '0.0.0.0' ? 'Switch to localhost only' : 'Allow access from other machines'}
|
|
697
|
+
delay={300} position="bottom" disabled={!isPanelOpen}
|
|
698
|
+
>
|
|
699
|
+
<button
|
|
700
|
+
onClick={() => toggleListenAddress(session)}
|
|
701
|
+
disabled={togglingId === session.id || changingPortId === session.id}
|
|
702
|
+
className={clsx(
|
|
703
|
+
'flex items-center justify-center p-1.5 rounded transition-colors',
|
|
704
|
+
session.listenAddress === '0.0.0.0'
|
|
705
|
+
? `${SEVERITY_BADGE.warning} hover:bg-amber-500/30`
|
|
706
|
+
: 'text-theme-text-disabled hover:text-theme-text-primary hover:bg-theme-hover'
|
|
707
|
+
)}
|
|
708
|
+
>
|
|
709
|
+
{togglingId === session.id ? (
|
|
710
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
711
|
+
) : session.listenAddress === '0.0.0.0' ? (
|
|
712
|
+
<Globe className="w-3.5 h-3.5" />
|
|
713
|
+
) : (
|
|
714
|
+
<Monitor className="w-3.5 h-3.5" />
|
|
715
|
+
)}
|
|
716
|
+
</button>
|
|
717
|
+
</Tooltip>
|
|
718
|
+
)}
|
|
719
|
+
<Tooltip content={session.status === 'error' ? 'Dismiss' : 'Stop'} delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
720
|
+
<button
|
|
721
|
+
onClick={() => {
|
|
722
|
+
commitInteraction()
|
|
723
|
+
stopPortForward(session.id)
|
|
724
|
+
}}
|
|
725
|
+
disabled={stoppingIds.has(session.id)}
|
|
726
|
+
className="p-1.5 text-theme-text-tertiary hover:text-red-400 hover:bg-theme-hover rounded disabled:opacity-50"
|
|
727
|
+
>
|
|
728
|
+
<Trash2 className="w-3.5 h-3.5" />
|
|
729
|
+
</button>
|
|
730
|
+
</Tooltip>
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
{/* Row 2: namespace · port translation */}
|
|
735
|
+
<div className="text-xs text-theme-text-disabled">
|
|
736
|
+
{session.namespace} · {formatPortLabel(session)}
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
{/* Row 2.5: error message */}
|
|
740
|
+
{session.status === 'error' && session.error && (
|
|
741
|
+
<div className="text-xs text-red-400 bg-red-500/10 px-2 py-1 rounded">
|
|
742
|
+
{session.error}
|
|
743
|
+
</div>
|
|
744
|
+
)}
|
|
745
|
+
|
|
746
|
+
{/* Row 3: URL (+ optional toggle) | copy + open */}
|
|
747
|
+
{session.status === 'running' && (
|
|
748
|
+
<div className="pt-0.5 flex items-center justify-between gap-2">
|
|
749
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
750
|
+
{editingPortId === session.id ? (
|
|
670
751
|
<div className="flex items-center text-xs bg-theme-base rounded text-accent-text font-mono">
|
|
671
752
|
<span className="pl-2 py-1 text-theme-text-disabled select-none">
|
|
672
753
|
{session.listenAddress === '0.0.0.0' ? '0.0.0.0' : 'localhost'}:
|
|
@@ -709,6 +790,7 @@ export function PortForwardPanel() {
|
|
|
709
790
|
/>
|
|
710
791
|
</div>
|
|
711
792
|
) : (
|
|
793
|
+
<>
|
|
712
794
|
<Tooltip content="Click to change local port" delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
713
795
|
<code
|
|
714
796
|
className={clsx(
|
|
@@ -732,74 +814,33 @@ export function PortForwardPanel() {
|
|
|
732
814
|
<PenLine className="w-3 h-3 text-theme-text-disabled opacity-0 group-hover/port:opacity-100 transition-opacity" />
|
|
733
815
|
</code>
|
|
734
816
|
</Tooltip>
|
|
817
|
+
</>
|
|
818
|
+
)}
|
|
819
|
+
</div>
|
|
820
|
+
<div className="flex items-center gap-0.5 shrink-0">
|
|
821
|
+
<Tooltip content={copiedId === session.id ? 'Copied!' : 'Copy URL'} delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
822
|
+
<button
|
|
823
|
+
onClick={() => handleCopyUrl(session)}
|
|
824
|
+
className="p-1 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover rounded"
|
|
825
|
+
>
|
|
826
|
+
{copiedId === session.id ? (
|
|
827
|
+
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
828
|
+
) : (
|
|
829
|
+
<Copy className="w-3.5 h-3.5" />
|
|
735
830
|
)}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
: 'bg-theme-elevated text-theme-text-tertiary hover:bg-theme-hover hover:text-theme-text-primary'
|
|
748
|
-
)}
|
|
749
|
-
>
|
|
750
|
-
{togglingId === session.id ? (
|
|
751
|
-
<Loader2 className="w-3 h-3 animate-spin" />
|
|
752
|
-
) : session.listenAddress === '0.0.0.0' ? (
|
|
753
|
-
<Globe className="w-3 h-3" />
|
|
754
|
-
) : (
|
|
755
|
-
<Monitor className="w-3 h-3" />
|
|
756
|
-
)}
|
|
757
|
-
{session.listenAddress === '0.0.0.0' ? 'network' : 'local'}
|
|
758
|
-
</button>
|
|
759
|
-
</Tooltip>
|
|
760
|
-
</div>
|
|
761
|
-
)}
|
|
762
|
-
</div>
|
|
763
|
-
|
|
764
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
765
|
-
{session.status === 'running' && (
|
|
766
|
-
<>
|
|
767
|
-
<Tooltip content={copiedId === session.id ? 'Copied!' : 'Copy URL'} delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
768
|
-
<button
|
|
769
|
-
onClick={() => handleCopyUrl(session)}
|
|
770
|
-
className="p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover rounded"
|
|
771
|
-
>
|
|
772
|
-
{copiedId === session.id ? (
|
|
773
|
-
<Check className="w-3.5 h-3.5 text-green-400" />
|
|
774
|
-
) : (
|
|
775
|
-
<Copy className="w-3.5 h-3.5" />
|
|
776
|
-
)}
|
|
777
|
-
</button>
|
|
778
|
-
</Tooltip>
|
|
779
|
-
<Tooltip content="Open in browser" delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
780
|
-
<button
|
|
781
|
-
onClick={() => handleOpenUrl(session)}
|
|
782
|
-
className="p-1.5 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover rounded"
|
|
783
|
-
>
|
|
784
|
-
<ExternalLink className="w-3.5 h-3.5" />
|
|
785
|
-
</button>
|
|
786
|
-
</Tooltip>
|
|
787
|
-
</>
|
|
788
|
-
)}
|
|
789
|
-
<Tooltip content={session.status === 'error' ? 'Dismiss' : 'Stop'} delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
790
|
-
<button
|
|
791
|
-
onClick={() => {
|
|
792
|
-
commitInteraction()
|
|
793
|
-
stopPortForward(session.id)
|
|
794
|
-
}}
|
|
795
|
-
disabled={stoppingIds.has(session.id)}
|
|
796
|
-
className="p-1.5 text-theme-text-tertiary hover:text-red-400 hover:bg-theme-hover rounded disabled:opacity-50"
|
|
797
|
-
>
|
|
798
|
-
<Trash2 className="w-3.5 h-3.5" />
|
|
799
|
-
</button>
|
|
800
|
-
</Tooltip>
|
|
831
|
+
</button>
|
|
832
|
+
</Tooltip>
|
|
833
|
+
<Tooltip content="Open in browser" delay={300} position="bottom" disabled={!isPanelOpen}>
|
|
834
|
+
<button
|
|
835
|
+
onClick={() => handleOpenUrl(session)}
|
|
836
|
+
className="p-1 text-theme-text-tertiary hover:text-theme-text-primary hover:bg-theme-hover rounded"
|
|
837
|
+
>
|
|
838
|
+
<ExternalLink className="w-3.5 h-3.5" />
|
|
839
|
+
</button>
|
|
840
|
+
</Tooltip>
|
|
841
|
+
</div>
|
|
801
842
|
</div>
|
|
802
|
-
|
|
843
|
+
)}
|
|
803
844
|
</div>
|
|
804
845
|
))}
|
|
805
846
|
</div>
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '@skyhook-io/k8s-ui'
|
|
14
14
|
import type { ResourceQueryResult } from '@skyhook-io/k8s-ui'
|
|
15
15
|
import type { SelectedResource } from '../../types'
|
|
16
|
-
import type
|
|
16
|
+
import { kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
17
17
|
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
18
18
|
import { getSkeletonYaml } from '../../utils/skeleton-yaml'
|
|
19
19
|
|
|
@@ -204,7 +204,7 @@ export function ResourcesView({ namespaces, selectedResource, onResourceClick, o
|
|
|
204
204
|
initialYaml={createDialogYaml}
|
|
205
205
|
title={createDialogTitle}
|
|
206
206
|
onCreated={(result) => {
|
|
207
|
-
onResourceClick?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
|
|
207
|
+
onResourceClick?.({ kind: kindToPlural(result.kind), namespace: result.namespace, name: result.name, group: '' })
|
|
208
208
|
}}
|
|
209
209
|
/>
|
|
210
210
|
</>
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type RendererOverrides,
|
|
9
9
|
} from '@skyhook-io/k8s-ui'
|
|
10
10
|
import type { SelectedResource, ResourceRef, ResolvedEnvFrom } from '../../types'
|
|
11
|
-
import type
|
|
11
|
+
import { kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
12
12
|
import {
|
|
13
13
|
useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
|
|
14
14
|
useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
|
|
@@ -383,7 +383,7 @@ export function WorkloadView({
|
|
|
383
383
|
initialYaml={duplicateYaml}
|
|
384
384
|
title="Duplicate Resource"
|
|
385
385
|
onCreated={(result) => {
|
|
386
|
-
rest.onNavigateToResource?.({ kind: result.kind, namespace: result.namespace, name: result.name, group: '' })
|
|
386
|
+
rest.onNavigateToResource?.({ kind: kindToPlural(result.kind), namespace: result.namespace, name: result.name, group: '' })
|
|
387
387
|
}}
|
|
388
388
|
/>
|
|
389
389
|
</>
|