Skip to content

Frontend Architecture

Temp edited this page Feb 11, 2026 · 3 revisions

Frontend Architecture

React application structure, components, and state management for D1 Manager.

Overview

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

Application Structure

File Organization

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

Main Application (App.tsx)

Component Structure

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>
  );
}

State Management

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);

Key Components

DatabaseView

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)

TableView

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

QueryConsole

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

SchemaDesigner

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

FilterBar

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

State Management

Local State (useState)

Used for component-specific state:

const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

Context State (useContext)

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();

No Redux/MobX

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

Routing

State-Based Routing

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

URL Query Parameters

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);
}, []);

API Integration

API Service (api.ts)

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);
  }
};

Styling

Tailwind CSS

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>

CSS Variables (Theme System)

/* 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));
}

shadcn/ui Components

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>

Performance Optimization

Code Splitting

Vite automatically splits code:

  • Vendor chunks
  • Route-based splitting (if using)
  • Dynamic imports

Lazy Loading

import { lazy, Suspense } from 'react';

const CascadeImpactSimulator = lazy(() => import('./components/CascadeImpactSimulator'));

<Suspense fallback={<Loading />}>
  <CascadeImpactSimulator />
</Suspense>

Memoization

import { useMemo } from "react";

const filteredTables = useMemo(
  () => tables.filter((t) => t.name.includes(searchTerm)),
  [tables, searchTerm],
);

Debouncing

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);

Next Steps


Need Help? See Troubleshooting or open an issue.

Clone this wiki locally