Skip to content

Add terminal exec and port forwarding features#2

Merged
nadaverell merged 2 commits into
mainfrom
feature/terminal-exec-portforward
Jan 22, 2026
Merged

Add terminal exec and port forwarding features#2
nadaverell merged 2 commits into
mainfrom
feature/terminal-exec-portforward

Conversation

@nadaverell
Copy link
Copy Markdown
Contributor

@nadaverell nadaverell commented Jan 22, 2026

Summary

Adds two major interactive features to Explorer:

  • Terminal Exec: Open a shell session into any running container directly from the UI. Click the terminal icon next to any running container in the pod details drawer to open a fully interactive shell with xterm.js terminal emulation.

  • Port Forwarding: Forward container ports to localhost with one click. Click port numbers in pod/service views to start forwarding. Active forwards are shown in a manager dropdown in the header with status and easy cleanup.

Key Changes

Backend (Go)

  • exec.go - WebSocket handler proxying kubectl exec with PTY support
  • portforward.go - HTTP handlers for managing port forwards (start/stop/list)
  • server.go - Route registration and WebSocket upgrade handling

Frontend (React)

  • New dock system with tabs for terminals and logs that preserve state when switching
  • Port forward manager showing all active forwards with status indicators
  • Updated pod/service renderers with action buttons for terminal and port forward
  • xterm.js integration with automatic resize handling

nadaverell and others added 2 commits January 22, 2026 03:59
- Add internal/k8s/update.go with UpdateResource and DeleteResource
  functions using dynamic K8s client
- Add YamlEditor component with Monaco editor, YAML validation,
  and inline highlighting for Pod editable fields
Adds two major interactive features to Explorer:

1. **Terminal Exec**: Open a shell session into any running container directly
   from the UI. Uses WebSocket to proxy kubectl exec with full terminal
   emulation via xterm.js. Supports multiple concurrent sessions with a
   tabbed dock interface that preserves state when switching tabs.

2. **Port Forwarding**: Forward container ports to localhost with one click.
   Includes a port forward manager in the UI header showing all active
   forwards with status indicators and easy cleanup.

Backend changes:
- exec.go: WebSocket handler for kubectl exec with PTY support
- portforward.go: HTTP handlers for starting/stopping/listing port forwards
- server.go: Register new routes and CORS for WebSocket

Frontend changes:
- New dock system (BottomDock, DockContext, TerminalTab, LogsTab)
- Port forward components (PortForwardButton, PortForwardManager)
- Updated PodRenderer with terminal/logs buttons per container
- Updated ServiceRenderer with port forward buttons
- xterm.js integration with FitAddon for responsive terminals

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@nadaverell nadaverell merged commit 5844fd6 into main Jan 22, 2026
1 of 2 checks passed
@nadaverell nadaverell deleted the feature/terminal-exec-portforward branch February 6, 2026 13:07
nadaverell added a commit that referenced this pull request May 2, 2026
…#570)

* fix(ui): keep tooltips anchored to their trigger and dismiss on click

Three reported tooltip bugs on app.radarhq.io that share a single root cause
in the shared Tooltip primitive (SKY-818):

1. Hovering a nav button (Home, Topology, Resources, Timeline, Helm,
   Traffic, Audit) flashed the tooltip text at the viewport's top-left
   (0, 0) before snapping into place, overlapping the "Radar / By Skyhook"
   logo. On Timeline the logo literally rendered as "adar" because
   "Timeline" painted over the "R".
2. Clicking a pod row left the row's full-name tooltip pinned above the
   table header, obscuring the column headers after the cursor moved away.
3. Clicking a tab pill left the tab's tooltip stuck under the active pill
   instead of dismissing on click.

Root causes:

- The portal's initial render used `top: 0; left: 0` because position
  state was seeded with zeros and only updated one rAF later. On slower
  clients (or whenever the requestAnimationFrame callback was delayed by
  layout work) that single frame painted at the viewport origin was
  visible to the user.
- Dismissal relied on `mouseleave` and on the trigger unmounting, but for
  nav buttons / tab pills the click triggers a route change without
  unmounting the trigger, and the cursor stays over it — so neither path
  fires and the tooltip lingers.

Fix:

- Extract the position math into a pure helper `computeTooltipPosition`
  with full unit coverage (`tooltip-position.ts` + `.test.ts`). It returns
  `null` when the trigger has no layout, so we never show coords we can't
  trust.
- Track coords as `{ top, left } | null`. While null, render the portal
  with `visibility: hidden` and `aria-hidden`, so no (0, 0) frame is
  ever visible.
- Two-pass measurement: rAF #1 captures the trigger rect with the
  unmeasured tooltip (size 0); rAF #2 re-runs with the now-known tooltip
  size for exact centering. Cleaned up via the existing rafRef so HMR /
  rapid re-renders can't leak frames.
- Add `onPointerDown` on the wrapper that fires before any child onClick,
  guaranteeing the tooltip is hidden before nav/tab/route-change actions
  run. Fixes bugs 2 and 3.
- Add window-level `popstate` and Escape listeners while a tooltip is
  visible, as belt-and-braces for in-app navigation that doesn't go
  through pointerdown on the trigger (e.g. keyboard shortcuts).

No behavior change for the singleton coordinator, the disabled flow, or
the public `Tooltip` / `WithTooltip` API.

Linear: SKY-818
Made-with: Cursor

* review: trim bug-narrative comments from tooltip primitive

No behavior change — keeps the load-bearing WHYs (popstate dismissal
mechanism, second-pass rAF, visibility:hidden guard) but drops the
incident-history framing that would rot.

---------

Co-authored-by: eliran-mic <eliran.mic@gmail.com>
Co-authored-by: Nadav Erell <nadaverell@gmail.com>
nadaverell added a commit that referenced this pull request May 18, 2026
Pod/Workload drawer was placing the new "Permissions via ServiceAccount"
section at position #2 — right under Status, before Containers and
Resource Usage. That's incident/audit framing on a daily-browsing
surface. Operators opening a Pod want to see what's running and how it's
behaving; "what could an attacker do with this SA" comes after.

Move it below the diagnostic block (Status → Containers → Resource
Usage → Conditions). Combined with the earlier collapse-by-default
change (auto-expands only when blast-radius detected), the section
now stays out of the way unless it's actually load-bearing for the
operator's task.

Same move in PodRenderer and WorkloadRenderer.
nadaverell added a commit that referenced this pull request May 19, 2026
Two follow-ups from #655 on per-user RBAC visual test:

1. Topology preview card on Home: the SSE topology stream is cluster-
   wide for small/medium clusters; only the dashboard summary is
   namespace-filtered server-side. That left the kind legend showing
   cluster-wide totals while the "<X> resources · <Y> conn" header had
   already narrowed to the active namespace. Filter the topology
   client-side in HomeView before passing it to TopologyPreview (and
   ActivitySummary) so the legend matches the title. No-op on large
   clusters where forceNamespaceFilter already filters SSE server-side.

2. Cluster info card "0 namespaces" for restricted users: dashboard.go
   only populated resourceCounts.Namespaces when the user could list
   namespaces cluster-wide, leaving a misleading "0" for users with a
   restricted namespace view. Fall back to len(allowedNamespaces),
   mirroring the same pattern in resource_counts.go.

Item #2 from the issue (sidebar count "1" vs page "No Namespace found"
asymmetry) is left as-is — genuine product call, neither option is
obviously better than the other.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant