fhadmin

The idea

The idea is to build an admin panel for apps using FastHTML and SQLite. I wanna use fastlite to talk to the database and fhdaisy to style the app. The idea is to have this a standalone app that I can mount into another app.

Please use your tools to change the messages directly when I prompt you to change something.

Setup

from fastcore.net import urlsave
p = mk_previewer(app)

Database

For development we’ll use the chinook sqlite database. This cell downloads it in the right place if you don’t have it already downloaded.

url = 'https://github.com/lerocha/chinook-database/raw/master/ChinookDatabase/DataSources/Chinook_Sqlite.sqlite'
path = Path('../data/chinook.sqlite')
if not path.exists(): urlsave(url, path)
db = database("../data/chinook.sqlite")

Configuration


source

AdminConfig


def AdminConfig(
    db_path:str \| pathlib.Path, password:str='admin123'
)->None:
cfg = AdminConfig(db_path="../data/chinook.sqlite", password="admin123")

source

create_admin


def create_admin(
    config
):
app = create_admin(cfg)

Utility Functions


source

tbl_name


def tbl_name(
    t
):
assert tbl_name('"Artist"') == 'Artist'

source

url


def url(
    req, path
):

source

mock_req


def mock_req(
    root_path:str=''
):

source

get_col_types


def get_col_types(
    db, t
):
assert get_col_types(app.state.cfg.db, 'Artist') == {'ArtistId': 'INTEGER', 'Name': 'NVARCHAR(120)'}

source

get_cols


def get_cols(
    db, t
):
assert get_cols(app.state.cfg.db, 'Artist') == ['ArtistId', 'Name']

source

get_text_cols


def get_text_cols(
    db, t
):
assert get_text_cols(app.state.cfg.db, 'Artist') == ['Name']

source

get_pk_col


def get_pk_col(
    db, t
):
assert get_pk_col(app.state.cfg.db, 'Artist') == 'ArtistId'

source

get_table_info


def get_table_info(
    db, t
):
assert get_table_info(app.state.cfg.db, 'Artist') == (['ArtistId', 'Name'], 'ArtistId')
# Test SQL queries
dangerous_queries = [
    "DROP TABLE Album",
    "DELETE FROM Track WHERE TrackId > 100",
    "DELETE FROM Artist",  # no WHERE clause
    "UPDATE Track SET Name = 'test'",  # no WHERE clause
    "TRUNCATE TABLE Playlist",
    "ALTER TABLE Album ADD COLUMN test TEXT",
]

safe_queries = [
    "SELECT * FROM Album LIMIT 10",
    "SELECT COUNT(*) FROM Track",
    "SELECT * FROM Artist WHERE ArtistId = 1",
    "SELECT Name, Composer FROM Track WHERE AlbumId = 5",
]

source

is_dangerous_query


def is_dangerous_query(
    query:str
):
assert not is_dangerous_query(safe_queries[0])
assert is_dangerous_query(dangerous_queries[1])

UI Components

print(LinkButton()), print(LinkButton(cls="-primary"))

source

LabeledInput


def LabeledInput(
    icon_name, kwargs:VAR_KEYWORD
):
p(LabeledInput("lock-closed", placeholder="Password"))

source

HeaderBar


def HeaderBar(
    req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>, logged_in:bool=False
):
p(HeaderBar(logged_in=True))

source

Layout


def Layout(
    children:VAR_POSITIONAL, title:str='Admin', req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>,
    logged_in:bool=False
):

source

NewRowBtn


def NewRowBtn(
    tbl, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(NewRowBtn("Artist"))

source

EditableCell


def EditableCell(
    tbl, pk, col, val, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):

source

EditCellInput


def EditCellInput(
    tbl, pk, col, val, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):

source

TableRows


def TableRows(
    db, rows, cols, tbl, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):

source

NewRowModal


def NewRowModal(
    tbl, cols, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):

source

SqlConsole


def SqlConsole(
    tbl, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):

source

DbStats


def DbStats(
    cfg
):
p(DbStats(cfg))

source

TableStats


def TableStats(
    db, tbl
):
p(TableStats(cfg.db, "Artist"))

source

LoginPage


def LoginPage(
    req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(LoginPage())

source

TableCard


def TableCard(
    name, row_count, col_count, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(TableCard("Foobar", 123,123))

source

PageBtns


def PageBtns(
    tbl, page, total_pages, q:str='', req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(Div(*PageBtns("Artist", 5, 25), cls="flex gap-2"))

source

SearchForm


def SearchForm(
    tbl, q:str='', req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(SearchForm("Artist"))

Authentication


source

post


def post(
    req, password:str, sess
):

source

get


def get(
    req
):

source

post


def post(
    req, sess
):

source

auth_check


def auth_check(
    req, sess
):

source

DangerousQueryModal


def DangerousQueryModal(
    tbl, sql, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(DangerousQueryModal("Artist", "DROP TABLE Artist;"))

Tables Overview


source

get


def get(
    req, sess
):

Table Detail View


source

get


def get(
    req, sess, tbl:str, pk:str, col:str
):

source

get


def get(
    req, sess, tbl:str
):

source

put


def put(
    req, sess, tbl:str, pk:str, col:str
):

source

post


def post(
    req, sess, tbl:str
):

delete row route


source

delete


def delete(
    req, sess, tbl:str, pk:str
):

That’s the route to make a sql query to the db.


source

execute_and_render_sql


def execute_and_render_sql(
    db, sql
):

source

post


def post(
    req, sess, tbl:str, sql:str
):

source

post


def post(
    req, sess, tbl:str, sql:str
):

source

search_rows


def search_rows(
    db, tbl, cols, text_cols, q
):

source

paginate


def paginate(
    rows, page, per_page:int=25
):

source

get


def get(
    req, sess, tbl:str, page:int=1, q:str=''
):

Server

if 'srv' in globals():
    srv.stop()
srv = JupyUvi(app)

Test mounting

parent = FastHTML()

@parent.get("/foobar")
def home(): return H1("Parent App"), A("Go to Admin", href="/admin/tables")

parent.mount("/admin", app)
if "srv" in globals(): srv.stop()
srv = JupyUvi(parent)
app.routes
parent.routes