Skip to content

Instantly share code, notes, and snippets.

@thomasht86
Created July 23, 2025 04:51
Show Gist options
  • Select an option

  • Save thomasht86/ea8c9529d1fa5ddc2e132f41ae765892 to your computer and use it in GitHub Desktop.

Select an option

Save thomasht86/ea8c9529d1fa5ddc2e132f41ae765892 to your computer and use it in GitHub Desktop.
Generic FastHTML Admin UI (WIP)
# main.py
from fasthtml.common import *
from dataclasses import dataclass, field, fields
from datetime import date
from typing import Text
# 1. App Initialization
# fast_app() provides the app and route decorator objects.
# hdrs can be used to add global CSS or JS. PicoCSS is included by default.
app, rt = fast_app()
# 2. Model Definitions
# These dataclasses define the schema for our application.
# The 'pk' metadata is a custom convention for our app to identify the primary key.
@dataclass
class User:
username: str
email: str
id: int = field(default=None, metadata={"pk": True})
is_active: bool = True
created_date: date = field(default_factory=date.today)
@dataclass
class Vendor:
name: str
contact_person: str
phone_number: str
id: int = field(default=None, metadata={"pk": True})
is_preferred: bool = False
@dataclass
class Product:
name: str
vendor_id: int
sku: str
price: float
stock_quantity: int
id: int = field(default=None, metadata={"pk": True})
description: Text = ""
# 3. Global State and Registry
MODEL_REGISTRY = {}
MODELS_TO_REGISTER = [User, Vendor, Product]
db = database("data/admin_ui.db")
# 4. Database Setup
def setup_database_and_models():
"""Dynamically creates tables for registered models if they don't exist."""
for model_cls in MODELS_TO_REGISTER:
model_name_plural = model_cls.__name__.lower() + "s"
table = getattr(db.t, model_name_plural)
if table not in db.t:
print(f"Creating table for {model_cls.__name__}...")
cols = {f.name: f.type for f in fields(model_cls)}
pk_field = next(
(f.name for f in fields(model_cls) if f.metadata.get("pk")), "id"
)
table.create(**cols, pk=pk_field)
MODEL_REGISTRY[model_name_plural] = (model_cls, table)
print("Database setup complete. Registered models:", list(MODEL_REGISTRY.keys()))
# Add sample data if tables are empty
create_sample_data()
def create_sample_data():
"""Creates sample data for development and testing."""
users_table = MODEL_REGISTRY["users"][1]
vendors_table = MODEL_REGISTRY["vendors"][1]
products_table = MODEL_REGISTRY["products"][1]
# Add sample users if empty
if len(users_table()) == 0:
sample_users = [
User(username="admin", email="admin@example.com", is_active=True),
User(username="john_doe", email="john@example.com", is_active=True),
User(username="jane_smith", email="jane@example.com", is_active=False),
]
for user in sample_users:
users_table.insert(user)
print("Added sample users")
# Add sample vendors if empty
if len(vendors_table()) == 0:
sample_vendors = [
Vendor(
name="Local Farm Co",
contact_person="Ole Hansen",
phone_number="12345678",
is_preferred=True,
),
Vendor(
name="Bergen Bakery",
contact_person="Astrid Larsen",
phone_number="87654321",
is_preferred=False,
),
Vendor(
name="Trondheim Meat",
contact_person="Erik Nordahl",
phone_number="11223344",
is_preferred=True,
),
]
for vendor in sample_vendors:
vendors_table.insert(vendor)
print("Added sample vendors")
# Add sample products if empty
if len(products_table()) == 0:
sample_products = [
Product(
name="Organic Potatoes",
vendor_id=1,
sku="ORG-POT-001",
price=29.99,
stock_quantity=100,
description="Fresh organic potatoes from local farm",
),
Product(
name="Sourdough Bread",
vendor_id=2,
sku="BREAD-SD-001",
price=45.00,
stock_quantity=20,
description="Traditional Norwegian sourdough bread",
),
Product(
name="Premium Beef",
vendor_id=3,
sku="BEEF-PREM-001",
price=199.99,
stock_quantity=15,
description="High quality beef from local cattle",
),
Product(
name="Fresh Carrots",
vendor_id=1,
sku="ORG-CAR-001",
price=19.99,
stock_quantity=80,
description="Organic carrots grown locally",
),
Product(
name="Rye Bread",
vendor_id=2,
sku="BREAD-RYE-001",
price=39.00,
stock_quantity=25,
description="Dark Norwegian rye bread",
),
]
for product in sample_products:
products_table.insert(product)
print("Added sample products")
# 5. UI Helper Functions
def get_input_for_field(field, current_value=None):
"""Returns a fasthtml input component based on a dataclass field's type."""
ft, fn = field.type, field.name
attrs = {"name": fn}
if ft is bool:
attrs["type"] = "checkbox"
attrs["value"] = "true"
if current_value:
attrs["checked"] = True
return Group(
Input(type="hidden", name=fn, value="false"),
Input(**attrs),
)
if ft is Text:
return Textarea(str(current_value or ""), **attrs)
if current_value is not None:
attrs["value"] = str(current_value)
if ft is str:
attrs["type"] = "text"
elif ft is int:
attrs.update({"type": "number", "step": "1"})
elif ft is float:
attrs.update({"type": "number", "step": "any"})
elif ft is date:
attrs["type"] = "date"
else:
attrs["type"] = "text" # Fallback
return Input(**attrs)
def render_cell(model_name, item, field):
"""Renders a single table cell with HTMX attributes for click-to-edit."""
# Convert dict to dataclass if needed
if isinstance(item, dict):
item_id = item.get("id")
field_value = item.get(field.name)
else:
item_id = item.id
field_value = getattr(item, field.name)
field_name = field.name
cell_id = f"cell-{item_id}-{field_name}"
return Td(
field_value,
id=cell_id,
hx_get=f"/{model_name}/edit_cell/{item_id}/{field_name}",
hx_target=f"#{cell_id}",
hx_swap="outerHTML",
)
def render_table(model_name, model_cls, items):
"""Renders a full data table for a given model and its items."""
pk_name = next((f.name for f in fields(model_cls) if f.metadata.get("pk")), "id")
data_fields = [f for f in fields(model_cls) if not f.metadata.get("pk")]
headers = [pk_name] + [f.name for f in data_fields] + ["Actions"]
def render_row(item):
# Convert dict to dataclass if needed
if isinstance(item, dict):
item = model_cls(**item)
item_id = getattr(item, pk_name)
return Tr(
Td(item_id),
*[render_cell(model_name, item, f) for f in data_fields],
Td(
Group(
A(
"Edit",
href=f"/{model_name}/edit/{item_id}",
role="button",
cls="outline",
),
Button(
"Delete",
hx_post=f"/{model_name}/delete/{item_id}",
hx_target=f"#row-{item_id}",
hx_swap="outerHTML",
hx_confirm=f"Delete {model_cls.__name__}?",
cls="outline secondary",
),
)
),
id=f"row-{item_id}",
)
return Table(
Thead(Tr(*[Th(header) for header in headers])), Tbody(*map(render_row, items))
)
def render_form(model_name, model_cls, instance=None):
"""Renders a create/edit form for a given model."""
pk_name = next((f.name for f in fields(model_cls) if f.metadata.get("pk")), "id")
action_url = (
f"/{model_name}/create"
if instance is None
else f"/{model_name}/update/{getattr(instance, pk_name)}"
)
form_fields = [
Label(
f.name.replace("_", " ").title(),
get_input_for_field(f, getattr(instance, f.name, None)),
)
for f in fields(model_cls)
if not f.metadata.get("pk")
]
return Form(
*form_fields, Button("Save", type="submit"), action=action_url, method="post"
)
async def process_and_save(req, model_cls, db_table, item_id=None):
"""Helper to process form data and save to the database."""
form_data = await req.form()
data_dict = {}
for f in fields(model_cls):
if f.name in form_data:
val = form_data.get(f.name)
if f.type is bool:
data_dict[f.name] = val == "true"
elif f.type is int:
data_dict[f.name] = int(val) if val else 0
elif f.type is float:
data_dict[f.name] = float(val) if val else 0.0
elif f.type is date and val:
data_dict[f.name] = date.fromisoformat(val)
elif f.type is not date:
data_dict[f.name] = val
if item_id: # Update
pk_name = next(
(f.name for f in fields(model_cls) if f.metadata.get("pk")), "id"
)
data_dict[pk_name] = item_id
db_table.update(model_cls(**data_dict))
else: # Create
db_table.insert(model_cls(**data_dict))
# 6. Route Handlers
@rt("/")
def get_dashboard():
if not MODEL_REGISTRY:
setup_database_and_models()
cards = []
for model_name in MODEL_REGISTRY.keys():
model_cls, db_table = MODEL_REGISTRY[model_name]
count = len(db_table())
cards.append(
Card(
Header(H3(model_name.title())),
P(f"{count} records"),
Footer(
Group(
A(
f"Manage {model_name.title()}",
href=f"/{model_name}/list",
role="button",
cls="outline",
),
A(
f"New {model_cls.__name__}",
href=f"/{model_name}/new",
role="button",
),
)
),
)
)
return Titled(
"Admin Dashboard", Main(H1("Data Management"), Grid(*cards), cls="container")
)
@rt("/{model_name}/list")
def get_list(model_name: str):
if not MODEL_REGISTRY:
setup_database_and_models()
if model_name not in MODEL_REGISTRY:
return "Model not found", 404
model_cls, db_table = MODEL_REGISTRY[model_name]
items = db_table()
return Titled(
f"Manage {model_name.title()}",
Main(
H1(f"All {model_name.title()}"),
P(
Group(
A("← Dashboard", href="/", cls="outline"),
A(
f"New {model_cls.__name__}",
href=f"/{model_name}/new",
role="button",
),
)
),
render_table(model_name, model_cls, items)
if items
else P(
f"No {model_name} found. ",
A(f"Create the first {model_cls.__name__}", href=f"/{model_name}/new"),
),
cls="container",
),
)
@rt("/{model_name}/new")
def get_new(model_name: str):
if not MODEL_REGISTRY or model_name not in MODEL_REGISTRY:
return "Model not found", 404
model_cls, _ = MODEL_REGISTRY[model_name]
return Titled(
f"New {model_cls.__name__}",
Main(
H1(f"New {model_cls.__name__}"),
P(
A(
f"← Back to {model_name.title()}",
href=f"/{model_name}/list",
cls="outline",
)
),
render_form(model_name, model_cls),
cls="container",
),
)
@rt("/{model_name}/edit/{item_id:int}")
def get_edit(model_name: str, item_id: int):
if not MODEL_REGISTRY or model_name not in MODEL_REGISTRY:
return "Model not found", 404
model_cls, db_table = MODEL_REGISTRY[model_name]
item_data = db_table[item_id]
# Convert dict to dataclass if needed
if isinstance(item_data, dict):
item = model_cls(**item_data)
else:
item = item_data
return Titled(
f"Edit {model_cls.__name__}",
Main(
H1(f"Editing {model_cls.__name__} #{item_id}"),
P(
A(
f"← Back to {model_name.title()}",
href=f"/{model_name}/list",
cls="outline",
)
),
render_form(model_name, model_cls, item),
cls="container",
),
)
@rt("/{model_name}/create")
async def post_create(model_name: str, req: Request):
model_cls, db_table = MODEL_REGISTRY[model_name]
await process_and_save(req, model_cls, db_table)
return RedirectResponse(f"/{model_name}/list", status_code=303)
@rt("/{model_name}/update/{item_id:int}")
async def post_update(model_name: str, item_id: int, req: Request):
model_cls, db_table = MODEL_REGISTRY[model_name]
await process_and_save(req, model_cls, db_table, item_id)
return RedirectResponse(f"/{model_name}/list", status_code=303)
@rt("/{model_name}/delete/{item_id:int}")
def post_delete(model_name: str, item_id: int):
_, db_table = MODEL_REGISTRY[model_name]
db_table.delete(item_id)
return ""
@rt("/{model_name}/edit_cell/{item_id:int}/{field_name}")
def get_edit_cell(model_name: str, item_id: int, field_name: str):
model_cls, db_table = MODEL_REGISTRY[model_name]
item_data = db_table[item_id]
# Convert dict to dataclass if needed
if isinstance(item_data, dict):
item = model_cls(**item_data)
field_value = item_data.get(field_name)
else:
item = item_data
field_value = getattr(item, field_name)
field = next(f for f in fields(model_cls) if f.name == field_name)
input_el = get_input_for_field(field, field_value)
if not isinstance(input_el, (tuple, list)):
input_el.attrs.update(
{
"hx_post": f"/{model_name}/update_cell/{item_id}/{field_name}",
"hx_target": f"#cell-{item_id}-{field_name}",
"hx_swap": "outerHTML",
"hx_trigger": "blur, keyup[key=='Enter']",
"autofocus": "true",
}
)
return input_el
@rt("/{model_name}/update_cell/{item_id:int}/{field_name}")
async def post_update_cell(
model_name: str, item_id: int, field_name: str, req: Request
):
model_cls, db_table = MODEL_REGISTRY[model_name]
item_data = db_table[item_id]
# Convert dict to dataclass if needed
if isinstance(item_data, dict):
item = model_cls(**item_data)
else:
item = item_data
form_data = await req.form()
new_value_str = form_data.get(field_name)
field = next(f for f in fields(model_cls) if f.name == field_name)
# Type coercion
if field.type is bool:
typed_value = new_value_str == "true"
elif field.type is int:
typed_value = int(new_value_str)
elif field.type is float:
typed_value = float(new_value_str)
elif field.type is date:
typed_value = date.fromisoformat(new_value_str)
else:
typed_value = new_value_str
setattr(item, field_name, typed_value)
db_table.update(item)
# Get updated item for rendering
updated_item = db_table[item_id]
if isinstance(updated_item, dict):
updated_item = model_cls(**updated_item)
return render_cell(model_name, updated_item, field)
# 7. Application Startup
def initialize_app():
"""Initialize the application with database setup."""
setup_database_and_models()
# Initialize when module is imported
initialize_app()
if __name__ == "__main__":
serve(port=9000)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment