add a datastar action
This commit is contained in:
parent
33dbb143fd
commit
3fc999a69b
3 changed files with 142 additions and 2 deletions
|
|
@ -87,7 +87,8 @@ def page_header() -> Renderable:
|
||||||
class_="rounded-full border border-white/15 bg-white/5 px-4 py-2.5 text-sm font-semibold text-white hover:bg-white/10",
|
class_="rounded-full border border-white/15 bg-white/5 px-4 py-2.5 text-sm font-semibold text-white hover:bg-white/10",
|
||||||
)["Run scheduler health check"],
|
)["Run scheduler health check"],
|
||||||
],
|
],
|
||||||
]
|
],
|
||||||
|
demo_action_panel(),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -121,6 +122,61 @@ def overview_section(*, active_jobs: str) -> Renderable:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def demo_action_panel() -> Renderable:
|
||||||
|
return h.div(
|
||||||
|
{"data-signals": "{decrementAmount: '1', decrementError: ''}"},
|
||||||
|
id="demo-decrement-panel",
|
||||||
|
class_="mt-6 rounded-[1.75rem] border border-white/10 bg-white/5 p-5 ring-1 ring-white/10",
|
||||||
|
)[
|
||||||
|
h.div(class_="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between")[
|
||||||
|
h.div(class_="max-w-2xl")[
|
||||||
|
h.p(
|
||||||
|
class_="text-xs font-semibold uppercase tracking-[0.22em] text-amber-300"
|
||||||
|
)["Demo action"],
|
||||||
|
h.h2(class_="mt-2 text-lg font-semibold text-white")[
|
||||||
|
"Decrement active jobs"
|
||||||
|
],
|
||||||
|
h.p(class_="mt-2 text-sm text-slate-300")[
|
||||||
|
"Uses Datastar signals plus a server action. Enter an odd number and the server will validate it before mutating state."
|
||||||
|
],
|
||||||
|
],
|
||||||
|
h.div(class_="flex flex-col gap-3 sm:flex-row sm:items-end")[
|
||||||
|
h.div(class_="min-w-40")[
|
||||||
|
h.label(
|
||||||
|
for_="decrement-amount",
|
||||||
|
class_="block text-xs font-semibold uppercase tracking-[0.18em] text-slate-300",
|
||||||
|
)["Odd decrement"],
|
||||||
|
h.input(
|
||||||
|
{
|
||||||
|
"data-bind:decrement-amount": True,
|
||||||
|
"data-preserve-attr:value": True,
|
||||||
|
},
|
||||||
|
id="decrement-amount",
|
||||||
|
name="decrement-amount",
|
||||||
|
type="number",
|
||||||
|
min="1",
|
||||||
|
step="1",
|
||||||
|
inputmode="numeric",
|
||||||
|
class_="mt-2 block w-full rounded-2xl border border-white/10 bg-slate-950/70 px-3.5 py-2.5 text-sm text-white shadow-sm placeholder:text-slate-500 focus:outline-hidden focus:ring-2 focus:ring-amber-400",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
h.button(
|
||||||
|
{"data-on:click": "@post('/demo/decrement')"},
|
||||||
|
type="button",
|
||||||
|
class_="cursor-pointer rounded-full bg-amber-400 px-4 py-2.5 text-sm font-semibold text-slate-950 shadow-sm hover:bg-amber-300",
|
||||||
|
)["Decrement"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
h.p(
|
||||||
|
{
|
||||||
|
"data-show": "$decrementError !== ''",
|
||||||
|
"data-text": "$decrementError",
|
||||||
|
},
|
||||||
|
class_="mt-3 text-sm font-medium text-rose-300",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def source_form_section() -> Renderable:
|
def source_form_section() -> Renderable:
|
||||||
return h.section(
|
return h.section(
|
||||||
class_="rounded-[2rem] bg-white/90 shadow-sm ring-1 ring-slate-200"
|
class_="rounded-[2rem] bg-white/90 shadow-sm ring-1 ring-slate-200"
|
||||||
|
|
|
||||||
30
repub/web.py
30
repub/web.py
|
|
@ -7,7 +7,8 @@ from contextlib import suppress
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
import htpy as h
|
import htpy as h
|
||||||
from datastar_py.quart import DatastarResponse
|
from datastar_py import ServerSentEventGenerator as SSE
|
||||||
|
from datastar_py.quart import DatastarResponse, read_signals
|
||||||
from datastar_py.sse import DatastarEvent
|
from datastar_py.sse import DatastarEvent
|
||||||
from htpy import Renderable
|
from htpy import Renderable
|
||||||
from quart import Quart, Response, request, url_for
|
from quart import Quart, Response, request, url_for
|
||||||
|
|
@ -77,6 +78,16 @@ def create_app(*, enable_demo_refresh: bool = True) -> Quart:
|
||||||
)
|
)
|
||||||
return DatastarResponse(_unsubscribe_on_close(queue, stream, app))
|
return DatastarResponse(_unsubscribe_on_close(queue, stream, app))
|
||||||
|
|
||||||
|
@app.post("/demo/decrement")
|
||||||
|
async def demo_decrement() -> DatastarResponse:
|
||||||
|
amount, error = _validated_decrement_amount(await read_signals())
|
||||||
|
if error is not None:
|
||||||
|
return DatastarResponse(SSE.patch_signals({"decrementError": error}))
|
||||||
|
|
||||||
|
set_active_jobs(app, max(0, get_active_jobs(app) - amount))
|
||||||
|
trigger_refresh(app)
|
||||||
|
return DatastarResponse(SSE.patch_signals({"decrementError": ""}))
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -115,3 +126,20 @@ async def _demo_refresh_loop(app: Quart) -> None:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
set_active_jobs(app, get_active_jobs(app) + 1)
|
set_active_jobs(app, get_active_jobs(app) + 1)
|
||||||
trigger_refresh(app)
|
trigger_refresh(app)
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_decrement_amount(
|
||||||
|
signals: dict[str, object] | None,
|
||||||
|
) -> tuple[int, str | None]:
|
||||||
|
raw_amount = (
|
||||||
|
"" if signals is None else str(signals.get("decrementAmount", "")).strip()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
amount = int(raw_amount)
|
||||||
|
except ValueError:
|
||||||
|
return 0, "Decrement amount must be an odd integer."
|
||||||
|
|
||||||
|
if amount < 1 or amount % 2 == 0:
|
||||||
|
return 0, "Decrement amount must be an odd integer."
|
||||||
|
|
||||||
|
return amount, None
|
||||||
|
|
|
||||||
|
|
@ -134,5 +134,61 @@ def test_render_dashboard_uses_active_jobs_from_app_state() -> None:
|
||||||
|
|
||||||
assert "27" in body
|
assert "27" in body
|
||||||
assert "Temporary live demo counter for Datastar refresh testing" in body
|
assert "Temporary live demo counter for Datastar refresh testing" in body
|
||||||
|
assert "/demo/decrement" in body
|
||||||
|
assert "data-bind:decrement-amount" in body
|
||||||
|
|
||||||
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_decrement_action_decrements_active_jobs() -> None:
|
||||||
|
async def run() -> None:
|
||||||
|
app = create_app(enable_demo_refresh=False)
|
||||||
|
broker = get_refresh_broker(app)
|
||||||
|
queue = broker.subscribe()
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/demo/decrement",
|
||||||
|
headers={"Datastar-Request": "true"},
|
||||||
|
json={"decrementAmount": "3"},
|
||||||
|
)
|
||||||
|
body = await response.get_data(as_text=True)
|
||||||
|
event = await asyncio.wait_for(queue.get(), timeout=1)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert get_active_jobs(app) == 9
|
||||||
|
assert event == "refresh-event"
|
||||||
|
assert 'data: signals {"decrementError":""}' in body
|
||||||
|
broker.unsubscribe(queue)
|
||||||
|
|
||||||
|
asyncio.run(run())
|
||||||
|
|
||||||
|
|
||||||
|
def test_demo_decrement_action_validates_odd_amount() -> None:
|
||||||
|
async def run() -> None:
|
||||||
|
app = create_app(enable_demo_refresh=False)
|
||||||
|
broker = get_refresh_broker(app)
|
||||||
|
queue = broker.subscribe()
|
||||||
|
client = app.test_client()
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
"/demo/decrement",
|
||||||
|
headers={"Datastar-Request": "true"},
|
||||||
|
json={"decrementAmount": "2"},
|
||||||
|
)
|
||||||
|
body = await response.get_data(as_text=True)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert get_active_jobs(app) == 12
|
||||||
|
assert "odd integer" in body
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(queue.get(), timeout=0.1)
|
||||||
|
except TimeoutError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError("invalid decrement should not publish a refresh")
|
||||||
|
finally:
|
||||||
|
broker.unsubscribe(queue)
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue