-
Notifications
You must be signed in to change notification settings - Fork 3
Frontend Architecture
React application structure, components, and state management for D1 Manager.
The D1 Manager frontend is a modern React 19 application built with TypeScript, Vite, and Tailwind CSS. It provides an intuitive interface for managing D1 databases without requiring SQL knowledge.
Technology Stack:
- React - UI library
- TypeScript - Type safety
- Vite - Build tool
- Tailwind CSS - Styling
- shadcn/ui - Component library
src/
βββ components/ # React components
β βββ ui/ # shadcn/ui components
β βββ DatabaseView.tsx # Database list view
β βββ TableView.tsx # Table data browser
β βββ QueryConsole.tsx # SQL query editor
β βββ SchemaDesigner.tsx # Visual table creator
β βββ FilterBar.tsx # Row filtering
β βββ CrossDatabaseSearch.tsx
β βββ DatabaseComparison.tsx
β βββ MigrationWizard.tsx
β βββ CascadeImpactSimulator.tsx
β βββ ...
βββ contexts/ # React contexts
β βββ ThemeContext.tsx # Theme state management
βββ hooks/ # Custom React hooks
β βββ useTheme.ts # Theme hook
βββ services/ # External services
β βββ api.ts # API client
β βββ auth.ts # Authentication
βββ lib/ # Shared utilities
β βββ utils.ts # Helper functions
βββ App.tsx # Main application component
βββ main.tsx # React entry point
βββ index.css # Global styles + Tailwind
function App() {
// State management
const [databases, setDatabases] = useState<D1Database[]>([]);
const [currentView, setCurrentView] = useState<View>({ type: 'list' });
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Load databases on mount
useEffect(() => {
loadDatabases();
}, []);
// Render based on current view
return (
<ThemeProvider>
<div className="min-h-screen bg-background">
<Header />
{currentView.type === 'list' && (
<DatabaseList databases={databases} />
)}
{currentView.type === 'database' && (
<DatabaseView {...currentView} />
)}
{currentView.type === 'table' && (
<TableView {...currentView} />
)}
{currentView.type === 'query' && (
<QueryConsole {...currentView} />
)}
</div>
</ThemeProvider>
);
}View State:
type View =
| { type: "list" }
| { type: "database"; databaseId: string; databaseName: string }
| {
type: "table";
databaseId: string;
databaseName: string;
tableName: string;
}
| { type: "query"; databaseId: string; databaseName: string };Database State:
const [databases, setDatabases] = useState<D1Database[]>([]);
const [selectedDatabases, setSelectedDatabases] = useState<string[]>([]);UI State:
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showUploadDialog, setShowUploadDialog] = useState(false);Displays list of tables in a database:
interface DatabaseViewProps {
databaseId: string;
databaseName: string;
}
function DatabaseView({ databaseId, databaseName }: DatabaseViewProps) {
const [tables, setTables] = useState<TableInfo[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [selectedTables, setSelectedTables] = useState<string[]>([]);
useEffect(() => {
loadTables();
}, [databaseId]);
const filteredTables = tables.filter(table =>
table.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="container mx-auto p-6">
<Header databaseName={databaseName} />
<SearchBar value={searchTerm} onChange={setSearchTerm} />
<TableGrid tables={filteredTables} onSelect={handleTableClick} />
<BulkActions selected={selectedTables} />
</div>
);
}Features:
- Table search
- Multi-select
- Bulk operations
- Quick actions (Browse, Schema, Export)
Displays table data with pagination and filtering:
interface TableViewProps {
databaseId: string;
databaseName: string;
tableName: string;
}
function TableView({ databaseId, databaseName, tableName }: TableViewProps) {
const [data, setData] = useState<any[]>([]);
const [schema, setSchema] = useState<ColumnInfo[]>([]);
const [filters, setFilters] = useState<Record<string, FilterCondition>>({});
const [page, setPage] = useState(0);
const [totalRows, setTotalRows] = useState(0);
const ROWS_PER_PAGE = 50;
useEffect(() => {
loadTableData();
}, [databaseId, tableName, filters, page]);
return (
<div className="container mx-auto p-6">
<Header databaseName={databaseName} tableName={tableName} />
<FilterBar schema={schema} filters={filters} onChange={setFilters} />
<DataGrid data={data} schema={schema} />
<Pagination
page={page}
totalPages={Math.ceil(totalRows / ROWS_PER_PAGE)}
onPageChange={setPage}
/>
<ColumnManagement schema={schema} />
</div>
);
}Features:
- Pagination (50 rows/page)
- Row-level filtering
- Sortable columns
- Column management
- CSV export
SQL query editor with execution and results:
interface QueryConsoleProps {
databaseId: string;
databaseName: string;
}
function QueryConsole({ databaseId, databaseName }: QueryConsoleProps) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<QueryResult | null>(null);
const [executing, setExecuting] = useState(false);
const [skipValidation, setSkipValidation] = useState(false);
const handleExecute = async () => {
setExecuting(true);
try {
const result = await api.executeQuery(databaseId, query, skipValidation);
setResults(result);
} catch (error) {
setResults({ error: error.message });
} finally {
setExecuting(false);
}
};
// Keyboard shortcut: Ctrl+Enter
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
handleExecute();
}
};
return (
<div className="container mx-auto p-6">
<Header databaseName={databaseName} />
<QueryEditor
value={query}
onChange={setQuery}
onKeyDown={handleKeyDown}
/>
<ExecuteButton onClick={handleExecute} loading={executing} />
<SkipValidationCheckbox checked={skipValidation} onChange={setSkipValidation} />
{results && <ResultsDisplay results={results} />}
</div>
);
}Features:
- Syntax highlighting
- Ctrl+Enter execution
- Skip validation option
- Query history
- Saved queries
- CSV export
Visual table creation tool:
interface Column {
name: string;
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'NUMERIC';
notNull: boolean;
primaryKey: boolean;
unique: boolean;
defaultValue?: string;
}
function SchemaDesigner({ databaseId, onClose }: Props) {
const [tableName, setTableName] = useState('');
const [columns, setColumns] = useState<Column[]>([
{ name: '', type: 'TEXT', notNull: false, primaryKey: false, unique: false }
]);
const addColumn = () => {
setColumns([...columns, {
name: '',
type: 'TEXT',
notNull: false,
primaryKey: false,
unique: false
}]);
};
const removeColumn = (index: number) => {
setColumns(columns.filter((_, i) => i !== index));
};
const generateSQL = () => {
const columnDefs = columns.map(col => {
let def = `${col.name} ${col.type}`;
if (col.primaryKey) def += ' PRIMARY KEY';
if (col.notNull) def += ' NOT NULL';
if (col.unique) def += ' UNIQUE';
if (col.defaultValue) def += ` DEFAULT ${col.defaultValue}`;
return def;
});
return `CREATE TABLE ${tableName} (\n ${columnDefs.join(',\n ')}\n);`;
};
return (
<Dialog>
<DialogContent>
<DialogHeader>Create Table</DialogHeader>
<Input label="Table Name" value={tableName} onChange={setTableName} />
{columns.map((col, i) => (
<ColumnEditor
key={i}
column={col}
onChange={updatedCol => {
const newColumns = [...columns];
newColumns[i] = updatedCol;
setColumns(newColumns);
}}
onRemove={() => removeColumn(i)}
/>
))}
<Button onClick={addColumn}>Add Column</Button>
<CodeBlock language="sql">{generateSQL()}</CodeBlock>
<DialogFooter>
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={handleCreate}>Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}Features:
- Visual column builder
- Real-time SQL preview
- Type selection
- Constraint configuration
- Validation
Type-aware row filtering:
interface FilterBarProps {
schema: ColumnInfo[];
filters: Record<string, FilterCondition>;
onChange: (filters: Record<string, FilterCondition>) => void;
}
function FilterBar({ schema, filters, onChange }: FilterBarProps) {
const getOperatorsForType = (type: string) => {
if (type === 'TEXT') {
return ['contains', 'equals', 'notEquals', 'startsWith', 'endsWith', 'isNull', 'isNotNull'];
}
if (type === 'INTEGER' || type === 'REAL') {
return ['equals', 'notEquals', 'gt', 'gte', 'lt', 'lte', 'isNull', 'isNotNull'];
}
return ['equals', 'isNull', 'isNotNull'];
};
return (
<div className="mb-4 p-4 bg-muted rounded-lg">
<div className="flex justify-between items-center mb-2">
<h3>Filters</h3>
<Button variant="ghost" onClick={() => onChange({})}>Clear All</Button>
</div>
{schema.map(column => (
<div key={column.name} className="flex gap-2 mb-2">
<Label>{column.name}:</Label>
<Select
value={filters[column.name]?.operator || ''}
onChange={operator => handleFilterChange(column.name, operator)}
>
{getOperatorsForType(column.type).map(op => (
<option key={op} value={op}>{op}</option>
))}
</Select>
<Input
value={filters[column.name]?.value || ''}
onChange={value => handleValueChange(column.name, value)}
disabled={['isNull', 'isNotNull'].includes(filters[column.name]?.operator)}
/>
</div>
))}
</div>
);
}Features:
- Type-aware operators
- One filter per column
- URL persistence
- Active indicators
Used for component-specific state:
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);Used for global state (theme):
// ThemeContext.tsx
export const ThemeContext = createContext<ThemeContextType>(null!);
export function ThemeProvider({ children }: Props) {
const [theme, setTheme] = useState<Theme>('system');
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
root.classList.add(systemTheme);
} else {
root.classList.add(theme);
}
localStorage.setItem('theme', theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// Usage in components
const { theme, setTheme } = useTheme();D1 Manager doesn't use Redux or MobX because:
- Application state is simple enough for React hooks
- Most state is component-local
- Only theme needs global state (Context API sufficient)
- Reduces bundle size and complexity
Uses state instead of react-router:
type View =
| { type: "list" }
| { type: "database"; databaseId: string; databaseName: string }
| {
type: "table";
databaseId: string;
databaseName: string;
tableName: string;
}
| { type: "query"; databaseId: string; databaseName: string };
const [currentView, setCurrentView] = useState<View>({ type: "list" });
// Navigate to database view
const viewDatabase = (id: string, name: string) => {
setCurrentView({ type: "database", databaseId: id, databaseName: name });
};
// Navigate back to list
const backToList = () => {
setCurrentView({ type: "list" });
};Benefits:
- Simpler than react-router for single-page app
- Type-safe navigation
- No URL management needed
- Lighter bundle size
Used for filters (shareable filtered views):
// Set filters in URL
const updateFilters = (filters: FilterCondition[]) => {
const params = new URLSearchParams();
filters.forEach((f) => {
params.set(`filter_${f.column}`, f.operator);
params.set(`filterValue_${f.column}`, f.value);
});
window.history.pushState({}, "", `?${params.toString()}`);
};
// Read filters from URL
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const filters = {};
params.forEach((value, key) => {
if (key.startsWith("filter_")) {
const column = key.replace("filter_", "");
filters[column] = {
operator: value,
value: params.get(`filterValue_${column}`),
};
}
});
setFilters(filters);
}, []);Centralized API client:
class APIService {
private baseURL: string;
constructor() {
this.baseURL = import.meta.env.VITE_WORKER_API || window.location.origin;
}
async listDatabases(): Promise<D1Database[]> {
const response = await fetch(`${this.baseURL}/api/databases`, {
credentials: "include",
});
if (!response.ok) throw new Error("Failed to load databases");
const data = await response.json();
return data.result;
}
async executeQuery(
dbId: string,
query: string,
skipValidation: boolean,
): Promise<QueryResult> {
const response = await fetch(`${this.baseURL}/api/query/${dbId}/execute`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ query, skipValidation }),
});
if (!response.ok) throw new Error("Query execution failed");
return response.json();
}
// ... more methods
}
export const api = new APIService();Usage in Components:
import { api } from "../services/api";
const loadDatabases = async () => {
try {
const data = await api.listDatabases();
setDatabases(data);
} catch (error) {
setError(error.message);
}
};Utility-first CSS framework:
<div className="container mx-auto p-6">
<div className="bg-card rounded-lg shadow-md p-4">
<h2 className="text-2xl font-bold mb-4">Database List</h2>
<Button className="bg-primary hover:bg-primary/90">
Create Database
</Button>
</div>
</div>/* index.css */
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--primary: 222.2 47.4% 11.2%;
/* ... more variables */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--primary: 210 40% 98%;
/* ... more variables */
}Usage:
.my-component {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}Pre-built, customizable components:
src/components/ui/
βββ button.tsx
βββ card.tsx
βββ dialog.tsx
βββ input.tsx
βββ label.tsx
βββ select.tsx
βββ ...
Usage:
import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog';
<Dialog>
<DialogContent>
<DialogHeader>Create Database</DialogHeader>
<Input placeholder="Database name" />
<Button>Create</Button>
</DialogContent>
</Dialog>Vite automatically splits code:
- Vendor chunks
- Route-based splitting (if using)
- Dynamic imports
import { lazy, Suspense } from 'react';
const CascadeImpactSimulator = lazy(() => import('./components/CascadeImpactSimulator'));
<Suspense fallback={<Loading />}>
<CascadeImpactSimulator />
</Suspense>import { useMemo } from "react";
const filteredTables = useMemo(
() => tables.filter((t) => t.name.includes(searchTerm)),
[tables, searchTerm],
);import { useEffect, useState } from "react";
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
const debouncedSearch = useDebounce(searchTerm, 300);- Architecture - Overall system architecture
- Worker Implementation - Backend details
- Local Development - Development setup
- Technology Stack - Complete tech stack
Need Help? See Troubleshooting or open an issue.
- Database Management
- R2 Backup Restore
- Scheduled Backups
- Table Operations
- Query Console
- Schema Designer
- Column Management
- Bulk Operations
- Job History
- Time Travel
- Read Replication
- Undo Rollback
- Foreign Key Visualizer
- ER Diagram
- Foreign Key Dependencies
- Foreign Key Navigation
- Circular Dependency Detector
- Cascade Impact Simulator
- AI Search
- FTS5 Full Text Search
- Cross Database Search
- Index Analyzer
- Database Comparison
- Database Optimization