add datastar and render shim
This commit is contained in:
parent
9ce576e7e8
commit
2accb26546
6 changed files with 173 additions and 42 deletions
|
|
@ -1 +1,4 @@
|
|||
from repub.pages.dashboard import admin_page
|
||||
from repub.pages.dashboard import admin_component
|
||||
from repub.pages.shim import shim_page
|
||||
|
||||
__all__ = ["admin_component", "shim_page"]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import htpy as h
|
|||
from htpy import Node, Renderable
|
||||
|
||||
from repub.components import (
|
||||
base_layout,
|
||||
input_field,
|
||||
nav_link,
|
||||
select_field,
|
||||
|
|
@ -451,7 +450,7 @@ def settings_panel() -> Renderable:
|
|||
]
|
||||
|
||||
|
||||
def admin_page(*, stylesheet_href: str) -> Renderable:
|
||||
def admin_component() -> Renderable:
|
||||
running_rows = (
|
||||
(
|
||||
h.div(class_="font-semibold text-slate-950")["Pangea mobile articles"],
|
||||
|
|
@ -559,40 +558,39 @@ def admin_page(*, stylesheet_href: str) -> Renderable:
|
|||
),
|
||||
)
|
||||
|
||||
return base_layout(
|
||||
page_title="Republisher Admin UI",
|
||||
stylesheet_href=stylesheet_href,
|
||||
content=h.div(class_="min-h-screen lg:grid lg:grid-cols-[18rem_minmax(0,1fr)]")[
|
||||
sidebar(),
|
||||
h.main(class_="px-4 py-5 sm:px-6 lg:px-8 lg:py-8")[
|
||||
h.div(class_="mx-auto max-w-7xl space-y-6")[
|
||||
page_header(),
|
||||
overview_section(),
|
||||
h.div(
|
||||
class_="grid gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(22rem,0.95fr)]"
|
||||
)[
|
||||
h.div(class_="space-y-6")[
|
||||
source_form_section(),
|
||||
configured_sources_section(),
|
||||
job_table_section(
|
||||
title="Running executions",
|
||||
subtitle="Operators can inspect active crawls and stop them if needed.",
|
||||
rows=running_rows,
|
||||
),
|
||||
job_table_section(
|
||||
title="Upcoming jobs",
|
||||
subtitle="Schedule preview with enable, disable, run now, and delete affordances.",
|
||||
rows=upcoming_rows,
|
||||
),
|
||||
job_table_section(
|
||||
title="Completed executions",
|
||||
subtitle="Recent history with direct access to text logs.",
|
||||
rows=completed_rows,
|
||||
),
|
||||
],
|
||||
h.div(class_="space-y-6")[log_panel(), settings_panel()],
|
||||
return h.main(
|
||||
id="morph",
|
||||
class_="min-h-screen lg:grid lg:grid-cols-[18rem_minmax(0,1fr)]",
|
||||
)[
|
||||
sidebar(),
|
||||
h.div(class_="px-4 py-5 sm:px-6 lg:px-8 lg:py-8")[
|
||||
h.div(class_="mx-auto max-w-7xl space-y-6")[
|
||||
page_header(),
|
||||
overview_section(),
|
||||
h.div(
|
||||
class_="grid gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(22rem,0.95fr)]"
|
||||
)[
|
||||
h.div(class_="space-y-6")[
|
||||
source_form_section(),
|
||||
configured_sources_section(),
|
||||
job_table_section(
|
||||
title="Running executions",
|
||||
subtitle="Operators can inspect active crawls and stop them if needed.",
|
||||
rows=running_rows,
|
||||
),
|
||||
job_table_section(
|
||||
title="Upcoming jobs",
|
||||
subtitle="Schedule preview with enable, disable, run now, and delete affordances.",
|
||||
rows=upcoming_rows,
|
||||
),
|
||||
job_table_section(
|
||||
title="Completed executions",
|
||||
subtitle="Recent history with direct access to text logs.",
|
||||
rows=completed_rows,
|
||||
),
|
||||
],
|
||||
]
|
||||
],
|
||||
h.div(class_="space-y-6")[log_panel(), settings_panel()],
|
||||
],
|
||||
]
|
||||
],
|
||||
)
|
||||
]
|
||||
|
|
|
|||
34
repub/pages/shim.py
Normal file
34
repub/pages/shim.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import htpy as h
|
||||
from htpy import Node, Renderable
|
||||
|
||||
ON_LOAD_JS = (
|
||||
"@post(window.location.pathname + "
|
||||
"(window.location.search + '&u=').replace(/^&/,'?'), "
|
||||
"{retryMaxCount: Infinity})"
|
||||
)
|
||||
|
||||
TAB_ID_JS = "self.crypto.randomUUID().substring(0,8)"
|
||||
|
||||
|
||||
def shim_page(*, datastar_src: str, head: Node | None = None) -> Renderable:
|
||||
return h.html(lang="en")[
|
||||
h.head[
|
||||
h.meta(charset="UTF-8"),
|
||||
head,
|
||||
h.script(id="js", defer=True, type="module", src=datastar_src),
|
||||
h.meta(name="viewport", content="width=device-width, initial-scale=1.0"),
|
||||
],
|
||||
h.body[
|
||||
h.div({"data-signals:tabid": TAB_ID_JS}),
|
||||
h.div(
|
||||
{
|
||||
"data-init": ON_LOAD_JS,
|
||||
"data-on:online__window": ON_LOAD_JS,
|
||||
}
|
||||
),
|
||||
h.noscript["Your browser does not support JavaScript!"],
|
||||
h.main(id="morph"),
|
||||
],
|
||||
]
|
||||
9
repub/static/datastar@1.0.0-RC.8.js
Normal file
9
repub/static/datastar@1.0.0-RC.8.js
Normal file
File diff suppressed because one or more lines are too long
36
repub/web.py
36
repub/web.py
|
|
@ -1,15 +1,43 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from quart import Quart, url_for
|
||||
import hashlib
|
||||
|
||||
from repub.pages import admin_page
|
||||
import htpy as h
|
||||
from quart import Quart, Response, request, url_for
|
||||
|
||||
from repub.pages import admin_component, shim_page
|
||||
|
||||
|
||||
def _render_shim_page(*, stylesheet_href: str, datastar_src: str) -> tuple[str, str]:
|
||||
head = (
|
||||
h.title["Republisher Admin UI"],
|
||||
h.link(rel="stylesheet", href=stylesheet_href),
|
||||
)
|
||||
body = str(shim_page(datastar_src=datastar_src, head=head))
|
||||
etag = hashlib.sha256(body.encode("utf-8")).hexdigest()
|
||||
return body, etag
|
||||
|
||||
|
||||
def create_app() -> Quart:
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.get("/")
|
||||
async def index() -> str:
|
||||
return str(admin_page(stylesheet_href=url_for("static", filename="app.css")))
|
||||
async def index() -> Response:
|
||||
body, etag = _render_shim_page(
|
||||
stylesheet_href=url_for("static", filename="app.css"),
|
||||
datastar_src=url_for("static", filename="datastar@1.0.0-RC.8.js"),
|
||||
)
|
||||
if request.if_none_match.contains(etag):
|
||||
response = Response(status=304)
|
||||
response.set_etag(etag)
|
||||
return response
|
||||
|
||||
response = Response(body, mimetype="text/html")
|
||||
response.set_etag(etag)
|
||||
return response
|
||||
|
||||
@app.post("/")
|
||||
async def index_patch() -> Response:
|
||||
return Response(str(admin_component()), mimetype="text/html")
|
||||
|
||||
return app
|
||||
|
|
|
|||
59
tests/test_web.py
Normal file
59
tests/test_web.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from repub.web import create_app
|
||||
|
||||
|
||||
def test_root_get_serves_datastar_shim() -> None:
|
||||
async def run() -> None:
|
||||
client = create_app().test_client()
|
||||
|
||||
response = await client.get("/")
|
||||
body = await response.get_data(as_text=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["ETag"]
|
||||
assert body.startswith("<!doctype html>")
|
||||
assert (
|
||||
'<script id="js" defer type="module" src="/static/datastar@1.0.0-RC.8.js"></script>'
|
||||
in body
|
||||
)
|
||||
assert 'data-signals:tabid="self.crypto.randomUUID().substring(0,8)"' in body
|
||||
assert 'data-init="@post(window.location.pathname +' in body
|
||||
assert "retryMaxCount: Infinity" in body
|
||||
assert "data-on:online__window=" in body
|
||||
assert '<main id="morph"></main>' in body
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_root_get_honors_if_none_match() -> None:
|
||||
async def run() -> None:
|
||||
client = create_app().test_client()
|
||||
|
||||
initial = await client.get("/")
|
||||
etag = initial.headers["ETag"]
|
||||
|
||||
response = await client.get("/", headers={"If-None-Match": etag})
|
||||
|
||||
assert response.status_code == 304
|
||||
assert response.headers["ETag"] == etag
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_root_post_serves_morph_component() -> None:
|
||||
async def run() -> None:
|
||||
client = create_app().test_client()
|
||||
|
||||
response = await client.post("/?u=shim")
|
||||
body = await response.get_data(as_text=True)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.content_type == "text/html; charset=utf-8"
|
||||
assert body.startswith('<main id="morph"')
|
||||
assert "Admin UI" in body
|
||||
assert "All on one page for the v1 spike" not in body
|
||||
|
||||
asyncio.run(run())
|
||||
Loading…
Add table
Add a link
Reference in a new issue