Web UI Architecture¶
The Databricks DevBox web UI is a modern React application built with TypeScript, Vite, and Tailwind CSS.
Tech Stack¶
- Framework: React 18 with TypeScript
- Build Tool: Vite 6
- Styling: Tailwind CSS 4 + shadcn/ui components
- State Management: React Context + hooks
- HTTP Client: Fetch API
- WebSocket: Native WebSocket API
- Icons: Lucide React
Project Structure¶
web_ui/
├── src/
│ ├── components/ # Reusable UI components
│ │ ├── ui/ # shadcn/ui base components
│ │ ├── ServerCard.tsx
│ │ ├── CreateServerDialog.tsx
│ │ └── LogViewer.tsx
│ ├── lib/
│ │ ├── api.ts # API client functions
│ │ └── utils.ts # Utility functions
│ ├── types/
│ │ └── index.ts # TypeScript type definitions
│ ├── App.tsx # Main application component
│ ├── main.tsx # Application entry point
│ └── index.css # Global styles
├── public/
│ └── logo.png
├── vite.config.ts
├── tailwind.config.js
├── tsconfig.json
└── package.json
Core Components¶
1. Server Card¶
Displays server information and actions.
// ServerCard.tsx
interface ServerCardProps {
server: ServerInstance;
onStart: (id: string) => void;
onStop: (id: string) => void;
onRestart: (id: string) => void;
onDelete: (id: string) => void;
onOpen: (port: number) => void;
}
export function ServerCard({ server, ...actions }: ServerCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>{server.name}</CardTitle>
<Badge variant={statusVariant(server.status)}>
{server.status}
</Badge>
</CardHeader>
<CardContent>
<p>Port: {server.port}</p>
<p>CPU: {server.cpu_percent?.toFixed(1)}%</p>
<p>Memory: {server.memory_mb?.toFixed(0)} MB</p>
<p>Uptime: {formatUptime(server.uptime)}</p>
</CardContent>
<CardFooter>
<Button onClick={() => actions.onStart(server.id)}>Start</Button>
<Button onClick={() => actions.onStop(server.id)}>Stop</Button>
<Button onClick={() => actions.onOpen(server.port)}>Open</Button>
</CardFooter>
</Card>
);
}
2. Create Server Dialog¶
Modal for creating new servers.
// CreateServerDialog.tsx
export function CreateServerDialog({ open, onClose, onSubmit }) {
const [name, setName] = useState("");
const [selectedExtensions, setSelectedExtensions] = useState<string[]>([]);
const [workspace, setWorkspace] = useState<WorkspaceConfig>({});
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Server</DialogTitle>
</DialogHeader>
<Form onSubmit={handleSubmit}>
<Input
label="Server Name"
value={name}
onChange={setName}
/>
<ExtensionGroupSelector
groups={extensionGroups}
selected={selectedExtensions}
onSelect={setSelectedExtensions}
/>
<WorkspaceSelector
config={workspace}
onChange={setWorkspace}
/>
<Button type="submit">Create</Button>
</Form>
</DialogContent>
</Dialog>
);
}
3. Log Viewer¶
Real-time log streaming component.
// LogViewer.tsx
export function LogViewer({ serverId }: { serverId?: string }) {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [connected, setConnected] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
// Connect to WebSocket
const wsUrl = serverId
? `ws://localhost:8000/ws/logs/${serverId}`
: `ws://localhost:8000/ws/logs`;
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => setConnected(true);
ws.onclose = () => setConnected(false);
ws.onmessage = (event) => {
const log = JSON.parse(event.data) as LogEntry;
setLogs((prev) => [...prev, log].slice(-500)); // Keep last 500
};
return () => ws.close();
}, [serverId]);
return (
<div className="h-96 overflow-y-auto bg-black text-white font-mono p-4">
{logs.map((log, i) => (
<LogLine key={i} log={log} />
))}
{connected && <div className="text-green-500">● Connected</div>}
</div>
);
}
API Client¶
HTTP API¶
// lib/api.ts
const API_BASE = 'http://localhost:8000';
export const api = {
// Server management
async listServers(): Promise<ServerInstance[]> {
const res = await fetch(`${API_BASE}/servers`);
return res.json();
},
async createServer(data: CreateServerRequest): Promise<ServerInstance> {
const res = await fetch(`${API_BASE}/servers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return res.json();
},
async startServer(id: string): Promise<void> {
await fetch(`${API_BASE}/servers/${id}/start`, { method: 'POST' });
},
async stopServer(id: string): Promise<void> {
await fetch(`${API_BASE}/servers/${id}/stop`, { method: 'POST' });
},
async deleteServer(id: string): Promise<void> {
await fetch(`${API_BASE}/servers/${id}`, { method: 'DELETE' });
},
// Configuration
async getConfig(): Promise<DevboxConfig> {
const res = await fetch(`${API_BASE}/config`);
return res.json();
},
async getTemplates(): Promise<PackagedAssets> {
const res = await fetch(`${API_BASE}/templates`);
const data = await res.json();
return data.data;
},
};
WebSocket Client¶
// lib/websocket.ts
export class LogStreamClient {
private ws: WebSocket | null = null;
connect(serverId?: string) {
const url = serverId
? `ws://localhost:8000/ws/logs/${serverId}`
: `ws://localhost:8000/ws/logs`;
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => {
const log = JSON.parse(event.data);
this.onLog(log);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
disconnect() {
this.ws?.close();
this.ws = null;
}
onLog(log: LogEntry) {
// Override in consumer
}
}
Type Definitions¶
// types/index.ts
export interface ServerInstance {
id: string;
name: string;
port: number;
workspace_path: string;
extensions: string[];
status: 'running' | 'stopped' | 'failed';
pid?: number;
start_time?: string;
command?: string[];
uptime?: number;
cpu_percent?: number;
memory_mb?: number;
last_update?: string;
}
export interface ExtensionGroup {
name: string;
description: string;
extensions: string[];
user_settings?: Record<string, any>;
}
export interface DevboxConfig {
extension_groups: Record<string, ExtensionGroup>;
server: ServerConfig;
ui: UIConfig;
packaged_assets?: PackagedAssets;
}
export interface CreateServerRequest {
name: string;
extensions?: string[];
}
export interface LogEntry {
type: 'log';
server_id: string;
server_name: string;
level: 'INFO' | 'WARN' | 'ERROR';
source: string;
message: string;
timestamp: string;
}
State Management¶
Application State¶
Using React Context and hooks:
// App.tsx
export function App() {
const [servers, setServers] = useState<ServerInstance[]>([]);
const [config, setConfig] = useState<DevboxConfig | null>(null);
const [loading, setLoading] = useState(true);
// Load initial data
useEffect(() => {
Promise.all([
api.listServers(),
api.getConfig(),
]).then(([servers, config]) => {
setServers(servers);
setConfig(config);
setLoading(false);
});
}, []);
// Auto-refresh servers every 5 seconds
useEffect(() => {
const interval = setInterval(async () => {
const updated = await api.listServers();
setServers(updated);
}, 5000);
return () => clearInterval(interval);
}, []);
// ... render UI
}
Styling¶
Tailwind Configuration¶
// tailwind.config.js
export default {
darkMode: 'class',
content: ['./index.html', './src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: {
primary: '#FF3621', // Databricks orange
secondary: '#00A972',
},
},
},
plugins: [],
};
shadcn/ui Components¶
Pre-built, accessible components:
Button,Card,DialogInput,Select,CheckboxBadge,Alert,TabsTooltip,Dropdown
Build & Deployment¶
Development¶
# Install dependencies
pnpm install
# Start dev server (with HMR)
pnpm dev
# Runs on http://localhost:5173
Production¶
Embedded in Go Binary¶
The built web UI is embedded in the Go binary:
// assets.go
//go:embed web_ui_dist
var webUIFS embed.FS
// In routes.go
assetsSubFS, _ := fs.Sub(webUIFS, "web_ui_dist/assets")
r.StaticFS("/assets", http.FS(assetsSubFS))
// Serve index.html for client-side routing
r.NoRoute(func(c *gin.Context) {
data, _ := webUIFS.ReadFile("web_ui_dist/index.html")
c.Data(http.StatusOK, "text/html; charset=utf-8", data)
})
User Interactions¶
Creating a Server¶
1. User clicks "New Server"
2. Dialog opens with form
3. User enters name, selects extensions
4. User selects workspace source (empty/GitHub/upload)
5. Submit form
6. POST /servers
7. Server created (may take time for extensions)
8. UI auto-refreshes and shows new server
Starting a Server¶
1. User clicks "Start" on server card
2. POST /servers/:id/start
3. Server status changes to "running"
4. Metrics start updating
5. "Open" button becomes available
Opening code-server¶
1. User clicks "Open"
2. Window opens: /vscode/{port}/
3. Proxy forwards to code-server instance
4. code-server loads in new tab
Viewing Logs¶
1. User navigates to Logs tab
2. WebSocket connects to /ws/logs
3. Logs stream in real-time
4. Auto-scrolls to bottom
5. Color-coded by level (INFO/WARN/ERROR)
Performance Optimizations¶
- Auto-refresh: 5-second polling for server list
- WebSocket: Real-time log streaming (no polling)
- Lazy Loading: Components loaded on-demand
- Memoization: React.memo for expensive components
- Virtual Scrolling: For large log lists (future)
Next Steps¶
-
Configuration reference
-
Full API documentation