from fastcore.net import urlsavefhadmin
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
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
AdminConfig
def AdminConfig(
db_path:str \| pathlib.Path, password:str='admin123'
)->None:
cfg = AdminConfig(db_path="../data/chinook.sqlite", password="admin123")create_admin
def create_admin(
config
):
app = create_admin(cfg)Utility Functions
tbl_name
def tbl_name(
t
):
assert tbl_name('"Artist"') == 'Artist'url
def url(
req, path
):
mock_req
def mock_req(
root_path:str=''
):
get_col_types
def get_col_types(
db, t
):
assert get_col_types(app.state.cfg.db, 'Artist') == {'ArtistId': 'INTEGER', 'Name': 'NVARCHAR(120)'}get_cols
def get_cols(
db, t
):
assert get_cols(app.state.cfg.db, 'Artist') == ['ArtistId', 'Name']get_text_cols
def get_text_cols(
db, t
):
assert get_text_cols(app.state.cfg.db, 'Artist') == ['Name']get_pk_col
def get_pk_col(
db, t
):
assert get_pk_col(app.state.cfg.db, 'Artist') == 'ArtistId'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",
]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"))LabeledInput
def LabeledInput(
icon_name, kwargs:VAR_KEYWORD
):
p(LabeledInput("lock-closed", placeholder="Password"))HeaderBar
def HeaderBar(
req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>, logged_in:bool=False
):
p(HeaderBar(logged_in=True))Layout
def Layout(
children:VAR_POSITIONAL, title:str='Admin', req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>,
logged_in:bool=False
):
NewRowBtn
def NewRowBtn(
tbl, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(NewRowBtn("Artist"))EditableCell
def EditableCell(
tbl, pk, col, val, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
EditCellInput
def EditCellInput(
tbl, pk, col, val, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
TableRows
def TableRows(
db, rows, cols, tbl, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
NewRowModal
def NewRowModal(
tbl, cols, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
SqlConsole
def SqlConsole(
tbl, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
DbStats
def DbStats(
cfg
):
p(DbStats(cfg))TableStats
def TableStats(
db, tbl
):
p(TableStats(cfg.db, "Artist"))LoginPage
def LoginPage(
req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(LoginPage())TableCard
def TableCard(
name, row_count, col_count, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(TableCard("Foobar", 123,123))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"))SearchForm
def SearchForm(
tbl, q:str='', req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(SearchForm("Artist"))Authentication
post
def post(
req, password:str, sess
):
get
def get(
req
):
post
def post(
req, sess
):
auth_check
def auth_check(
req, sess
):
DangerousQueryModal
def DangerousQueryModal(
tbl, sql, req:Request=<starlette.requests.Request object at 0x7f6d9becbc20>
):
p(DangerousQueryModal("Artist", "DROP TABLE Artist;"))Tables Overview
get
def get(
req, sess
):
Table Detail View
get
def get(
req, sess, tbl:str, pk:str, col:str
):
get
def get(
req, sess, tbl:str
):
put
def put(
req, sess, tbl:str, pk:str, col:str
):
post
def post(
req, sess, tbl:str
):
delete row route
delete
def delete(
req, sess, tbl:str, pk:str
):
That’s the route to make a sql query to the db.
execute_and_render_sql
def execute_and_render_sql(
db, sql
):
post
def post(
req, sess, tbl:str, sql:str
):
post
def post(
req, sess, tbl:str, sql:str
):
search_rows
def search_rows(
db, tbl, cols, text_cols, q
):
paginate
def paginate(
rows, page, per_page:int=25
):
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.routesparent.routes