- Canvas: Infinite zoomable workspace with LOD and navigation - Spotlight: Fuzzy search with type filters and view shortcuts - Krate: Window group container with non-overlapping placement - Detail Window: YAML/Describe/Logs/Shell views with maximize - Top Bar: Cluster info, user presence, admin toggle - Admin Drawer: Multi-user presence and spectate functionality - Minimap: Browse and navigate canvas overview - Collection Window: List/tree views with filtering and sorting - Shell/Logs: Real-time terminal and log streaming - Backend: Go service with K8s API, WebSocket handlers, CRDT sync - Architecture: Full project structure and tech stack
8.5 KiB
8.5 KiB
Shell & Logs Windows Feature Specification
Overview
Shell and Logs windows are specialized detail windows that stream real-time data from Kubernetes pods. Shell provides an interactive terminal, while Logs shows container output.
Shell Window
Purpose
Interactive pseudo-terminal session inside the application, connected to a pod via kubectl exec.
Implementation
Client-Side (xterm.js)
// Initialization
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
const term = new Terminal({
rows: 24,
cols: 80,
fontFamily: 'IBM Plex Mono',
fontSize: 12,
theme: {
background: '#0b0e13',
foreground: '#e6edf6',
cursor: '#4dd6e8'
}
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
WebSocket Connection
// On krate creation with shell tab
const ws = new WebSocket(`/ws/shell?pod=${pod}&ns=${ns}`);
ws.onopen = () => {
term.focus();
};
ws.onmessage = (event) => {
term.write(event.data); // stdout/stderr
};
term.onData((data) => {
ws.send(data); // stdin (keypresses, etc.)
});
term.onResize(({ rows, cols }) => {
ws.send(JSON.stringify({ type: 'resize', rows, cols }));
});
Keyboard Routing
Mouse-Over Focus
- Track
mouseenter/mouseleaveper window - Set
hoveredWindowIdon shell windows - Global
keydownhandler routes to shell when focused
Critical: Space Key
- Must NOT open spotlight when shell has focus
- Shell window handles space as input (terminal command)
- Implementation:
document.addEventListener('keydown', (e) => { if (hoveredWindowId && isShellWindow(hoveredWindowId)) { // Space goes to shell return; // Don't trigger canvas shortcuts } // Handle canvas shortcuts (space pan, etc.) }, { capture: true });
Scroll Wheel Behavior
- On shell content: Scroll terminal (not canvas)
- On empty canvas: Pan
- Ctrl/⌘ + scroll over shell: Zoom canvas (not terminal zoom)
- Use
{ capture: true }on canvas wheel handler - Check
e.ctrlKey || e.metaKey - If true: zoom canvas; otherwise: let event propagate
- Use
Terminal Features
- Full PTY emulation: Support all ANSI escapes
- Resize: Send new size on container resize
- Paste: Support Ctrl+Shift+V or Shift+Insert
- Copy: Ctrl+Shift+C or Ctrl+C (custom)
- Search:
/to search,n/Nfor next/prev - Font scaling: Ctrl+wheel for terminal font size
Backend Implementation (Go)
// WebSocket connection
func (s *Server) handleShell(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer ws.Close()
pod := r.URL.Query().Get("pod")
ns := r.URL.Query().Get("ns")
// Create k8s exec request
req := s.client.CoreV1().RESTClient().Post().
Name(pod).Namespace(ns).Resource("pods").SubResource("exec")
req.VersionedParams(&v1.ExecOptions{
Stdin: true,
Stdout: true,
Stderr: true,
Terminal: true,
}, scheme.ParameterCodec)
exec, err := remotecommand.NewSPDYExecutor(s.config, "POST", req.URL())
if err != nil {
ws.Close()
return
}
// Stream between WebSocket and exec
exec.Stream(remotecommand.StreamOptions{
Stdin: ws,
Stdout: ws,
Stderr: ws,
TermSize: &remotecommand.TerminalSize{
Width: 80,
Height: 24,
},
})
}
Logs Window
Purpose
Live-tailing container logs, showing output in real-time.
Streaming
// WebSocket connection
const ws = new WebSocket(`/ws/logs?pod=${pod}&ns=${ns}`);
ws.onmessage = (event) => {
const line = event.data;
addLogLine(line);
};
function addLogLine(line: string) {
const entry = parseLogLine(line);
logBuffer.push(entry);
// Auto-scroll if at bottom
if (atBottom()) {
scrollToBottom();
}
}
Auto-Scroll Behavior
- New lines arrive: Auto-scroll to bottom
- User scrolls up: Stop auto-scrolling
- Detection:
function atBottom(): boolean { const threshold = 50; // px return (scrollTop + clientHeight + threshold) >= scrollHeight; }
Color Coding
Parse lines and color-code by pattern:
function parseLogLine(line: string): LogEntry {
// Pattern matching for common log levels
if (line.includes('ERROR') || line.includes('ERR')) {
return { text: line, color: '#ef6f6f' }; // Red
} else if (line.includes('WARN') || line.includes('WARNING')) {
return { text: line, color: '#e8b54a' }; // Amber
} else if (line.includes('INFO') || line.includes('I ')) {
return { text: line, color: '#b9c6d8' }; // Default
} else {
return { text: line, color: '#c7d2e0' }; // Muted
}
}
Timestamps
- Default: Include timestamps
- Toggle: Optional button to show/hide
- Format: ISO 8601 or container format
Scrollbar Styling
- Width: 8px
- Track: rgba(140,165,200,.1)
- Thumb: rgba(140,165,200,.3)
- Hover thumb: rgba(140,165,200,.5)
Content Rendering
<pre style={{ overflow: 'auto', height: '286px', fontFamily: 'IBM Plex Mono' }}>
{logs.map((line, i) => (
<div key={i} style={{ color: line.color }}>
{line.timestamp && <span className="timestamp">{line.timestamp} </span>}
{line.text}
</div>
))}
</pre>
View-Specific Behavior
Shell Tab
- Enabled for: Pods, and workloads that support exec
- Disabled for: Deployments, Services, etc.
- Visual:
opacity: 0.45for disabled - Label: Show "shell" but don't route to it
Logs Tab
- Enabled for: Pods and workloads ( Deployments, StatefulSets, DaemonSets)
- Disabled for: ConfigMaps, Secrets, PVCs, etc.
- Visual: Same as shell
Keyboard Shortcuts
| Key | Context | Action |
|---|---|---|
| ↑ / ↓ | Logs shell focused | Navigate scroll (logs) / line history (shell) |
| PageUp / PageDown | Logs focused | Page scroll |
| Ctrl+L | Logs focused | Clear screen (only in shell) |
| Ctrl+C | Shell focused | Send interrupt to shell |
| Ctrl+Z | Shell focused | Send suspend (if supported) |
| Ctrl+Shift+F | Logs focused | Find (future enhancement) |
| Ctrl+Shift+V | Shell focused | Paste |
| Ctrl+Shift+C | Shell focused | Copy selection |
Error Handling
Connection Errors
- Shell: Show error border, keep terminal buffer, retry button
- Logs: Show error indicator, auto-reconnect with backoff
Reconnection Strategy
let retries = 0;
const maxRetries = 5;
function reconnect() {
if (retries >= maxRetries) {
showFatalError('Connection failed');
return;
}
setTimeout(() => {
retries++;
connect();
}, Math.min(1000 * 2^retries, 10000)); // Exponential backoff
}
Buffer Management
- Shell: Keep last 1000 lines in buffer
- Logs: Keep last 2000 lines
- Overflow: Drop oldest lines, mark with "..." indicator
State Management
Shell State
interface ShellState {
connected: boolean;
rows: number;
cols: number;
buffer: string[]; // Last N lines
cursor: { x, y };
}
Logs State
interface LogsState {
lines: LogEntry[];
scrollTop: number;
autoScroll: boolean;
timestamp: boolean;
}
Gotchas
Critical Issues
- Space key routing: Must distinguish between shell space (input) and canvas space-pan
- Ctrl/⌘+scroll: Intercept on canvas with
{ capture: true }, check modifier - Terminal resize: Send
TermSizeon window resize events - Paste security: Sanitize pasted content, avoid escape sequences
- Cursor position: xterm.js tracks cursor; keep in sync with server
- Buffer overflow: Implement LRU buffer, don't memory leak
Performance
- Throttle updates: Don't render on every log line, batch
- Virtual scrolling: For logs with many lines
- Web Worker: Offload parsing to worker for large logs
Animation
- Blinking cursor: CSS animation or xterm.js built-in
- Auto-scroll indicator: Small arrow when scrolled up
- Connection status: Green/amber/red dot
Testing Checklist
- Shell key press → terminal receives it
- Terminal output → WebSocket → UI
- Resize → terminal resizes correctly
- Space in shell ≠ spotlight open
- Ctrl+scroll over shell ≠ zoom canvas
- Logs auto-scroll works (and stops on manual scroll)
- Connection loss → retry with backoff
- Buffer overflow → old lines dropped
- Shell interrupt (Ctrl+C) handled
- Paste works (Ctrl+Shift+V)