Add publisher dashboard routes
This commit is contained in:
parent
96551c2788
commit
e4a5246ab3
31 changed files with 1603 additions and 516 deletions
24
repub/web/admin/__init__.py
Normal file
24
repub/web/admin/__init__.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from quart import Quart
|
||||
|
||||
from repub.web.admin.actions import register_admin_actions
|
||||
from repub.web.admin.pages.dashboard import register_dashboard_routes
|
||||
from repub.web.admin.pages.logs import register_log_routes
|
||||
from repub.web.admin.pages.runs import register_runs_routes
|
||||
from repub.web.admin.pages.settings import register_settings_routes
|
||||
from repub.web.admin.pages.sources import register_source_routes
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_admin_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
register_dashboard_routes(app, admin_required=admin_required)
|
||||
register_source_routes(app, admin_required=admin_required)
|
||||
register_runs_routes(app, admin_required=admin_required)
|
||||
register_settings_routes(app, admin_required=admin_required)
|
||||
register_log_routes(app, admin_required=admin_required)
|
||||
register_admin_actions(app, admin_required=admin_required)
|
||||
181
repub/web/admin/actions.py
Normal file
181
repub/web/admin/actions.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, cast
|
||||
|
||||
from datastar_py import ServerSentEventGenerator as SSE
|
||||
from datastar_py.quart import DatastarResponse, read_signals
|
||||
from peewee import IntegrityError
|
||||
from quart import Quart, Response
|
||||
|
||||
from repub.jobs import clear_completed_executions
|
||||
from repub.model import (
|
||||
create_source,
|
||||
delete_job_source,
|
||||
delete_source,
|
||||
load_job_enabled,
|
||||
save_setting,
|
||||
source_slug_exists,
|
||||
update_source,
|
||||
)
|
||||
from repub.web.app import (
|
||||
RUNS_TAB_STATE_KEY,
|
||||
_read_optional_signals,
|
||||
_read_tab_id,
|
||||
get_job_runtime,
|
||||
get_tab_state_store,
|
||||
run_job_now_response,
|
||||
trigger_refresh,
|
||||
validate_settings_form,
|
||||
validate_source_form,
|
||||
)
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_admin_actions(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.post("/admin/actions/sources/create")
|
||||
@admin_required
|
||||
async def admin_create_source_action() -> DatastarResponse:
|
||||
signals = cast(dict[str, object], await read_signals())
|
||||
source, error = validate_source_form(
|
||||
signals,
|
||||
slug_exists=source_slug_exists,
|
||||
)
|
||||
if error is not None:
|
||||
return DatastarResponse(
|
||||
SSE.patch_signals({"_formError": error, "_formSuccess": ""})
|
||||
)
|
||||
|
||||
assert source is not None
|
||||
try:
|
||||
create_source(**source)
|
||||
except IntegrityError:
|
||||
return DatastarResponse(
|
||||
SSE.patch_signals(
|
||||
{"_formError": "Slug must be unique.", "_formSuccess": ""}
|
||||
)
|
||||
)
|
||||
get_job_runtime(app).sync_jobs()
|
||||
trigger_refresh(app)
|
||||
return DatastarResponse(SSE.redirect("/admin/sources"))
|
||||
|
||||
@app.post("/admin/actions/sources/<string:slug>/edit")
|
||||
@admin_required
|
||||
async def admin_edit_source_action(slug: str) -> DatastarResponse:
|
||||
signals = cast(dict[str, object], await read_signals())
|
||||
source, error = validate_source_form(
|
||||
signals,
|
||||
slug_exists=lambda candidate: candidate != slug
|
||||
and source_slug_exists(candidate),
|
||||
immutable_slug=slug,
|
||||
)
|
||||
if error is not None:
|
||||
return DatastarResponse(
|
||||
SSE.patch_signals({"_formError": error, "_formSuccess": ""})
|
||||
)
|
||||
|
||||
assert source is not None
|
||||
if update_source(slug, **source) is None:
|
||||
return DatastarResponse(
|
||||
SSE.patch_signals(
|
||||
{"_formError": "Source does not exist.", "_formSuccess": ""}
|
||||
)
|
||||
)
|
||||
get_job_runtime(app).sync_jobs()
|
||||
trigger_refresh(app)
|
||||
return DatastarResponse(SSE.redirect("/admin/sources"))
|
||||
|
||||
@app.post("/admin/actions/sources/<string:slug>/delete")
|
||||
@admin_required
|
||||
async def admin_delete_source_action(slug: str) -> Response:
|
||||
delete_source(slug)
|
||||
get_job_runtime(app).sync_jobs()
|
||||
trigger_refresh(app)
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/settings")
|
||||
@admin_required
|
||||
async def admin_update_settings_action() -> DatastarResponse:
|
||||
signals = cast(dict[str, object], await read_signals())
|
||||
settings, error = validate_settings_form(signals)
|
||||
if error is not None:
|
||||
return DatastarResponse(
|
||||
SSE.patch_signals({"_formError": error, "_formSuccess": ""})
|
||||
)
|
||||
|
||||
assert settings is not None
|
||||
save_setting("max_concurrent_jobs", settings["max_concurrent_jobs"])
|
||||
save_setting("feed_url", settings["feed_url"])
|
||||
trigger_refresh(app)
|
||||
return DatastarResponse(SSE.redirect("/admin/settings"))
|
||||
|
||||
@app.post("/admin/actions/runs/completed-page/<int:page>")
|
||||
@admin_required
|
||||
async def admin_set_completed_runs_page_action(page: int) -> Response:
|
||||
signals = await _read_optional_signals()
|
||||
tab_id = _read_tab_id(signals)
|
||||
if tab_id is None:
|
||||
return Response(status=400)
|
||||
get_tab_state_store(app).update_page_state(
|
||||
tab_id,
|
||||
RUNS_TAB_STATE_KEY,
|
||||
lambda state: {**state, "completed_page": max(1, page)},
|
||||
)
|
||||
trigger_refresh(app, tab_id=tab_id)
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/jobs/<int:job_id>/run-now")
|
||||
@admin_required
|
||||
async def admin_run_job_now_action(job_id: int) -> Response:
|
||||
return run_job_now_response(app, job_id)
|
||||
|
||||
@app.post("/admin/actions/jobs/<int:job_id>/toggle-enabled")
|
||||
@admin_required
|
||||
async def admin_toggle_job_enabled_action(job_id: int) -> Response:
|
||||
enabled = load_job_enabled(job_id)
|
||||
if enabled is not None:
|
||||
get_job_runtime(app).set_job_enabled(job_id, enabled=not enabled)
|
||||
trigger_refresh(app)
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/jobs/<int:job_id>/delete")
|
||||
@admin_required
|
||||
async def admin_delete_job_action(job_id: int) -> Response:
|
||||
delete_job_source(job_id)
|
||||
get_job_runtime(app).sync_jobs()
|
||||
trigger_refresh(app)
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/executions/<int:execution_id>/cancel")
|
||||
@admin_required
|
||||
async def admin_cancel_execution_action(execution_id: int) -> Response:
|
||||
get_job_runtime(app).request_execution_cancel(execution_id)
|
||||
trigger_refresh(app)
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/queued-executions/<int:execution_id>/cancel")
|
||||
@admin_required
|
||||
async def admin_cancel_queued_execution_action(execution_id: int) -> Response:
|
||||
get_job_runtime(app).cancel_queued_execution(execution_id)
|
||||
trigger_refresh(app)
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/queued-executions/<int:execution_id>/move-up")
|
||||
@admin_required
|
||||
async def admin_move_queued_execution_up_action(execution_id: int) -> Response:
|
||||
get_job_runtime(app).move_queued_execution(execution_id, direction="up")
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/queued-executions/<int:execution_id>/move-down")
|
||||
@admin_required
|
||||
async def admin_move_queued_execution_down_action(execution_id: int) -> Response:
|
||||
get_job_runtime(app).move_queued_execution(execution_id, direction="down")
|
||||
return Response(status=204)
|
||||
|
||||
@app.post("/admin/actions/completed-executions/clear")
|
||||
@admin_required
|
||||
async def admin_clear_completed_executions_action() -> Response:
|
||||
clear_completed_executions(log_dir=app.config["REPUB_LOG_DIR"])
|
||||
trigger_refresh(app)
|
||||
return Response(status=204)
|
||||
1
repub/web/admin/pages/__init__.py
Normal file
1
repub/web/admin/pages/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from __future__ import annotations
|
||||
26
repub/web/admin/pages/dashboard.py
Normal file
26
repub/web/admin/pages/dashboard.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from quart import Quart, Response
|
||||
|
||||
from repub.web.app import _page_patch_response, _shim_page_response, render_dashboard
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_dashboard_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.get("/admin")
|
||||
@admin_required
|
||||
async def admin_dashboard_home() -> Response:
|
||||
return _shim_page_response(current_path="/admin", static_prefix="/admin")
|
||||
|
||||
@app.post("/admin")
|
||||
@admin_required
|
||||
async def admin_dashboard_patch() -> DatastarResponse:
|
||||
return await _page_patch_response(
|
||||
app,
|
||||
lambda _tab_id: render_dashboard(app, path_prefix="/admin"),
|
||||
)
|
||||
42
repub/web/admin/pages/logs.py
Normal file
42
repub/web/admin/pages/logs.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from htpy import Renderable
|
||||
from quart import Quart, Response
|
||||
|
||||
from repub.web.app import (
|
||||
_page_patch_response,
|
||||
_shim_page_response,
|
||||
render_execution_logs,
|
||||
)
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_log_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.get("/admin/job/<int:job_id>/execution/<int:execution_id>/logs")
|
||||
@admin_required
|
||||
async def admin_logs_home(job_id: int, execution_id: int) -> Response:
|
||||
return _shim_page_response(
|
||||
current_path=f"/admin/job/{job_id}/execution/{execution_id}/logs",
|
||||
static_prefix="/admin",
|
||||
)
|
||||
|
||||
@app.post("/admin/job/<int:job_id>/execution/<int:execution_id>/logs")
|
||||
@admin_required
|
||||
async def admin_logs_patch(
|
||||
job_id: int,
|
||||
execution_id: int,
|
||||
) -> DatastarResponse:
|
||||
async def render() -> Renderable:
|
||||
return await render_execution_logs(
|
||||
app,
|
||||
job_id=job_id,
|
||||
execution_id=execution_id,
|
||||
path_prefix="/admin",
|
||||
)
|
||||
|
||||
return await _page_patch_response(app, lambda _tab_id: render())
|
||||
26
repub/web/admin/pages/runs.py
Normal file
26
repub/web/admin/pages/runs.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from quart import Quart, Response
|
||||
|
||||
from repub.web.app import _page_patch_response, _shim_page_response, render_runs
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_runs_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.get("/admin/runs")
|
||||
@admin_required
|
||||
async def admin_runs_home() -> Response:
|
||||
return _shim_page_response(current_path="/admin/runs", static_prefix="/admin")
|
||||
|
||||
@app.post("/admin/runs")
|
||||
@admin_required
|
||||
async def admin_runs_patch() -> DatastarResponse:
|
||||
return await _page_patch_response(
|
||||
app,
|
||||
lambda tab_id: render_runs(app, tab_id=tab_id, path_prefix="/admin"),
|
||||
)
|
||||
26
repub/web/admin/pages/settings.py
Normal file
26
repub/web/admin/pages/settings.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from quart import Quart, Response
|
||||
|
||||
from repub.web.app import _page_patch_response, _shim_page_response, render_settings
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_settings_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.get("/admin/settings")
|
||||
@admin_required
|
||||
async def admin_settings_home() -> Response:
|
||||
return _shim_page_response(
|
||||
current_path="/admin/settings",
|
||||
static_prefix="/admin",
|
||||
)
|
||||
|
||||
@app.post("/admin/settings")
|
||||
@admin_required
|
||||
async def admin_settings_patch() -> DatastarResponse:
|
||||
return await _page_patch_response(app, lambda _tab_id: render_settings(app))
|
||||
48
repub/web/admin/pages/sources.py
Normal file
48
repub/web/admin/pages/sources.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from datastar_py.quart import DatastarResponse
|
||||
from quart import Quart, Response, request
|
||||
|
||||
from repub.web.app import (
|
||||
_page_patch_response,
|
||||
_shim_page_response,
|
||||
render_create_source,
|
||||
render_edit_source,
|
||||
render_sources,
|
||||
)
|
||||
|
||||
RouteGuard = Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]
|
||||
|
||||
|
||||
def register_source_routes(app: Quart, *, admin_required: RouteGuard) -> None:
|
||||
@app.get("/admin/sources")
|
||||
@app.get("/admin/sources/create")
|
||||
@app.get("/admin/sources/<string:slug>/edit")
|
||||
@admin_required
|
||||
async def admin_sources_shim(slug: str | None = None) -> Response:
|
||||
del slug
|
||||
return _shim_page_response(current_path=request.path, static_prefix="/admin")
|
||||
|
||||
@app.post("/admin/sources")
|
||||
@admin_required
|
||||
async def admin_sources_patch() -> DatastarResponse:
|
||||
return await _page_patch_response(app, lambda _tab_id: render_sources(app))
|
||||
|
||||
@app.post("/admin/sources/create")
|
||||
@admin_required
|
||||
async def admin_create_source_patch() -> DatastarResponse:
|
||||
return await _page_patch_response(
|
||||
app,
|
||||
lambda _tab_id: render_create_source(app),
|
||||
)
|
||||
|
||||
@app.post("/admin/sources/<string:slug>/edit")
|
||||
@admin_required
|
||||
async def admin_edit_source_patch(slug: str) -> DatastarResponse:
|
||||
return await _page_patch_response(
|
||||
app,
|
||||
lambda _tab_id: render_edit_source(slug, app),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue