- 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
338 lines
8.5 KiB
Markdown
338 lines
8.5 KiB
Markdown
# 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)
|
|
```typescript
|
|
// 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
|
|
```typescript
|
|
// 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`/`mouseleave` per window
|
|
- Set `hoveredWindowId` on shell windows
|
|
- Global `keydown` handler 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:
|
|
```typescript
|
|
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
|
|
|
|
### 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`/`N` for next/prev
|
|
- **Font scaling**: Ctrl+wheel for terminal font size
|
|
|
|
### Backend Implementation (Go)
|
|
```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
|
|
```typescript
|
|
// 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**:
|
|
```typescript
|
|
function atBottom(): boolean {
|
|
const threshold = 50; // px
|
|
return (scrollTop + clientHeight + threshold) >= scrollHeight;
|
|
}
|
|
```
|
|
|
|
### Color Coding
|
|
Parse lines and color-code by pattern:
|
|
|
|
```typescript
|
|
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
|
|
```typescript
|
|
<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.45` for 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
|
|
```typescript
|
|
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
|
|
```typescript
|
|
interface ShellState {
|
|
connected: boolean;
|
|
rows: number;
|
|
cols: number;
|
|
buffer: string[]; // Last N lines
|
|
cursor: { x, y };
|
|
}
|
|
```
|
|
|
|
### Logs State
|
|
```typescript
|
|
interface LogsState {
|
|
lines: LogEntry[];
|
|
scrollTop: number;
|
|
autoScroll: boolean;
|
|
timestamp: boolean;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Gotchas
|
|
|
|
### Critical Issues
|
|
1. **Space key routing**: Must distinguish between shell space (input) and canvas space-pan
|
|
2. **Ctrl/⌘+scroll**: Intercept on canvas with `{ capture: true }`, check modifier
|
|
3. **Terminal resize**: Send `TermSize` on window resize events
|
|
4. **Paste security**: Sanitize pasted content, avoid escape sequences
|
|
5. **Cursor position**: xterm.js tracks cursor; keep in sync with server
|
|
6. **Buffer overflow**: Implement LRU buffer, don't memory leak
|
|
|
|
### Performance
|
|
1. **Throttle updates**: Don't render on every log line, batch
|
|
2. **Virtual scrolling**: For logs with many lines
|
|
3. **Web Worker**: Offload parsing to worker for large logs
|
|
|
|
### Animation
|
|
1. **Blinking cursor**: CSS animation or xterm.js built-in
|
|
2. **Auto-scroll indicator**: Small arrow when scrolled up
|
|
3. **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)
|