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 htpy import Node, Renderable
|
||||||
|
|
||||||
from repub.components import (
|
from repub.components import (
|
||||||
base_layout,
|
|
||||||
input_field,
|
input_field,
|
||||||
nav_link,
|
nav_link,
|
||||||
select_field,
|
select_field,
|
||||||
|
|
@ -451,7 +450,7 @@ def settings_panel() -> Renderable:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def admin_page(*, stylesheet_href: str) -> Renderable:
|
def admin_component() -> Renderable:
|
||||||
running_rows = (
|
running_rows = (
|
||||||
(
|
(
|
||||||
h.div(class_="font-semibold text-slate-950")["Pangea mobile articles"],
|
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(
|
return h.main(
|
||||||
page_title="Republisher Admin UI",
|
id="morph",
|
||||||
stylesheet_href=stylesheet_href,
|
class_="min-h-screen lg:grid lg:grid-cols-[18rem_minmax(0,1fr)]",
|
||||||
content=h.div(class_="min-h-screen lg:grid lg:grid-cols-[18rem_minmax(0,1fr)]")[
|
)[
|
||||||
sidebar(),
|
sidebar(),
|
||||||
h.main(class_="px-4 py-5 sm:px-6 lg:px-8 lg:py-8")[
|
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")[
|
h.div(class_="mx-auto max-w-7xl space-y-6")[
|
||||||
page_header(),
|
page_header(),
|
||||||
overview_section(),
|
overview_section(),
|
||||||
h.div(
|
h.div(
|
||||||
class_="grid gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(22rem,0.95fr)]"
|
class_="grid gap-6 xl:grid-cols-[minmax(0,1.35fr)_minmax(22rem,0.95fr)]"
|
||||||
)[
|
)[
|
||||||
h.div(class_="space-y-6")[
|
h.div(class_="space-y-6")[
|
||||||
source_form_section(),
|
source_form_section(),
|
||||||
configured_sources_section(),
|
configured_sources_section(),
|
||||||
job_table_section(
|
job_table_section(
|
||||||
title="Running executions",
|
title="Running executions",
|
||||||
subtitle="Operators can inspect active crawls and stop them if needed.",
|
subtitle="Operators can inspect active crawls and stop them if needed.",
|
||||||
rows=running_rows,
|
rows=running_rows,
|
||||||
),
|
),
|
||||||
job_table_section(
|
job_table_section(
|
||||||
title="Upcoming jobs",
|
title="Upcoming jobs",
|
||||||
subtitle="Schedule preview with enable, disable, run now, and delete affordances.",
|
subtitle="Schedule preview with enable, disable, run now, and delete affordances.",
|
||||||
rows=upcoming_rows,
|
rows=upcoming_rows,
|
||||||
),
|
),
|
||||||
job_table_section(
|
job_table_section(
|
||||||
title="Completed executions",
|
title="Completed executions",
|
||||||
subtitle="Recent history with direct access to text logs.",
|
subtitle="Recent history with direct access to text logs.",
|
||||||
rows=completed_rows,
|
rows=completed_rows,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
h.div(class_="space-y-6")[log_panel(), settings_panel()],
|
|
||||||
],
|
],
|
||||||
]
|
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 __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:
|
def create_app() -> Quart:
|
||||||
app = Quart(__name__)
|
app = Quart(__name__)
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def index() -> str:
|
async def index() -> Response:
|
||||||
return str(admin_page(stylesheet_href=url_for("static", filename="app.css")))
|
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
|
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