Add HTML5 setup SSE updates
This commit is contained in:
@@ -1038,7 +1038,13 @@ def render_html5_project_setup(*, project_id: str, projects: Iterable[object], s
|
|||||||
sources = getattr(setup, "import_sources", []) or []
|
sources = getattr(setup, "import_sources", []) or []
|
||||||
source_cards = "".join(_import_source_card(source) for source in sources)
|
source_cards = "".join(_import_source_card(source) for source in sources)
|
||||||
content = f"""
|
content = f"""
|
||||||
<main class="workspace setup-workspace" data-html5-page="setup" data-project-id="{escape(project_id)}">
|
<main
|
||||||
|
class="workspace setup-workspace"
|
||||||
|
data-html5-page="setup"
|
||||||
|
data-project-id="{escape(project_id)}"
|
||||||
|
hx-ext="sse"
|
||||||
|
sse-connect="/html5/projects/{quote(project_id)}/setup/events"
|
||||||
|
>
|
||||||
{_topbar(project_id, project_nav)}
|
{_topbar(project_id, project_nav)}
|
||||||
<section class="setup-layout">
|
<section class="setup-layout">
|
||||||
<aside class="panel">
|
<aside class="panel">
|
||||||
@@ -1187,7 +1193,7 @@ def render_html5_import_check(project_id: str, check: object | None = None) -> s
|
|||||||
def render_html5_import_job(project_id: str, job: object | None = None) -> str:
|
def render_html5_import_job(project_id: str, job: object | None = None) -> str:
|
||||||
if job is None:
|
if job is None:
|
||||||
return f"""
|
return f"""
|
||||||
<div class="import-job" data-html5-import-job>
|
<div class="import-job" data-html5-import-job sse-swap="setup-import-job" hx-swap="outerHTML">
|
||||||
<div class="panel-title flush">Фоновый импорт</div>
|
<div class="panel-title flush">Фоновый импорт</div>
|
||||||
<p class="muted padded">Фоновая задача импорта для проекта {escape(project_id)} еще не запускалась.</p>
|
<p class="muted padded">Фоновая задача импорта для проекта {escape(project_id)} еще не запускалась.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1199,14 +1205,14 @@ def render_html5_import_job(project_id: str, job: object | None = None) -> str:
|
|||||||
source = str(payload.get("source") or "")
|
source = str(payload.get("source") or "")
|
||||||
stage = str(payload.get("stage") or "")
|
stage = str(payload.get("stage") or "")
|
||||||
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
|
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
|
||||||
poll = ' hx-trigger="every 2s" hx-swap="outerHTML"' if status in {"QUEUED", "RUNNING"} else ""
|
|
||||||
logs_html = "".join(f"<li>{escape(str(item))}</li>" for item in logs[-6:])
|
logs_html = "".join(f"<li>{escape(str(item))}</li>" for item in logs[-6:])
|
||||||
return f"""
|
return f"""
|
||||||
<div
|
<div
|
||||||
class="import-job"
|
class="import-job"
|
||||||
data-html5-import-job
|
data-html5-import-job
|
||||||
hx-get="/html5/projects/{quote(project_id)}/setup/jobs/{quote(job_id)}"
|
hx-get="/html5/projects/{quote(project_id)}/setup/jobs/{quote(job_id)}"
|
||||||
{poll}
|
sse-swap="setup-import-job"
|
||||||
|
hx-swap="outerHTML"
|
||||||
>
|
>
|
||||||
<div class="panel-title flush">Фоновый импорт</div>
|
<div class="panel-title flush">Фоновый импорт</div>
|
||||||
<div class="check-head">
|
<div class="check-head">
|
||||||
@@ -1231,7 +1237,7 @@ def render_html5_setup_summary(project_id: str, setup: object) -> str:
|
|||||||
class="setup-summary"
|
class="setup-summary"
|
||||||
data-html5-setup-summary
|
data-html5-setup-summary
|
||||||
hx-get="/html5/projects/{quote(project_id)}/setup/summary"
|
hx-get="/html5/projects/{quote(project_id)}/setup/summary"
|
||||||
hx-trigger="every 5s"
|
sse-swap="setup-summary"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
>
|
>
|
||||||
<div class="section-title">
|
<div class="section-title">
|
||||||
|
|||||||
@@ -2021,6 +2021,24 @@ async def html5_project_setup_summary(project_id: str) -> Response:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/html5/projects/{project_id}/setup/events")
|
||||||
|
async def html5_project_setup_events(project_id: str, once: bool = False) -> StreamingResponse:
|
||||||
|
async def stream_setup():
|
||||||
|
while True:
|
||||||
|
setup = _project_setup_response(project_id)
|
||||||
|
yield _html5_sse_event("setup-summary", render_html5_setup_summary(project_id, setup))
|
||||||
|
yield _html5_sse_event("setup-import-job", render_html5_import_job(project_id, _html5_latest_import_job(project_id)))
|
||||||
|
if once:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
stream_setup(),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/html5/projects/{project_id}/setup/source")
|
@app.post("/html5/projects/{project_id}/setup/source")
|
||||||
async def html5_project_setup_source(project_id: str, request: Request) -> Response:
|
async def html5_project_setup_source(project_id: str, request: Request) -> Response:
|
||||||
form = await _html5_form_data(request)
|
form = await _html5_form_data(request)
|
||||||
@@ -8392,6 +8410,19 @@ def _html5_operation_jobs() -> list[OperationJob]:
|
|||||||
return sorted(_operations.jobs.values(), key=lambda job: job.updated_at, reverse=True)[:50]
|
return sorted(_operations.jobs.values(), key=lambda job: job.updated_at, reverse=True)[:50]
|
||||||
|
|
||||||
|
|
||||||
|
def _html5_latest_import_job(project_id: str) -> OperationJob | None:
|
||||||
|
jobs = [
|
||||||
|
job
|
||||||
|
for job in _operations.jobs.values()
|
||||||
|
if job.payload.get("project_id") == project_id and _operation_value(getattr(job, "kind", "")) == "SERVER_IMPORT"
|
||||||
|
]
|
||||||
|
return max(jobs, key=lambda job: job.updated_at) if jobs else None
|
||||||
|
|
||||||
|
|
||||||
|
def _operation_value(value: object) -> str:
|
||||||
|
return str(getattr(value, "value", value))
|
||||||
|
|
||||||
|
|
||||||
def _html5_sse_event(event: str, fragment: str) -> str:
|
def _html5_sse_event(event: str, fragment: str) -> str:
|
||||||
data = "\n".join(f"data: {line}" for line in fragment.splitlines())
|
data = "\n".join(f"data: {line}" for line in fragment.splitlines())
|
||||||
return f"event: {event}\n{data}\n\n"
|
return f"event: {event}\n{data}\n\n"
|
||||||
|
|||||||
@@ -533,6 +533,11 @@ def test_html5_project_setup_renders_server_fragments():
|
|||||||
assert "HTML5 Setup Demo" in setup.text
|
assert "HTML5 Setup Demo" in setup.text
|
||||||
assert "data-html5-settings-panel" in setup.text
|
assert "data-html5-settings-panel" in setup.text
|
||||||
assert "data-html5-setup-summary" in setup.text
|
assert "data-html5-setup-summary" in setup.text
|
||||||
|
assert 'hx-ext="sse"' in setup.text
|
||||||
|
assert f'sse-connect="/html5/projects/{project_id}/setup/events"' in setup.text
|
||||||
|
assert 'sse-swap="setup-summary"' in setup.text
|
||||||
|
assert 'sse-swap="setup-import-job"' in setup.text
|
||||||
|
assert 'hx-trigger="every 5s"' not in setup.text
|
||||||
assert f'hx-get="/html5/projects/{project_id}/setup/summary"' in setup.text
|
assert f'hx-get="/html5/projects/{project_id}/setup/summary"' in setup.text
|
||||||
assert f'hx-post="/html5/projects/{project_id}/setup/settings"' in setup.text
|
assert f'hx-post="/html5/projects/{project_id}/setup/settings"' in setup.text
|
||||||
assert f'hx-post="/html5/projects/{project_id}/setup/source"' in setup.text
|
assert f'hx-post="/html5/projects/{project_id}/setup/source"' in setup.text
|
||||||
@@ -582,6 +587,8 @@ def test_html5_project_setup_renders_server_fragments():
|
|||||||
assert "data-html5-import-job" in import_job.text
|
assert "data-html5-import-job" in import_job.text
|
||||||
assert "SERVER_IMPORT" not in import_job.text
|
assert "SERVER_IMPORT" not in import_job.text
|
||||||
assert "hx-get" in import_job.text
|
assert "hx-get" in import_job.text
|
||||||
|
assert 'sse-swap="setup-import-job"' in import_job.text
|
||||||
|
assert 'hx-trigger="every 2s"' not in import_job.text
|
||||||
assert "<html" not in import_job.text
|
assert "<html" not in import_job.text
|
||||||
|
|
||||||
jobs = client.get("/operations/jobs", params={"project_id": project_id, "kind": "SERVER_IMPORT"})
|
jobs = client.get("/operations/jobs", params={"project_id": project_id, "kind": "SERVER_IMPORT"})
|
||||||
@@ -591,6 +598,8 @@ def test_html5_project_setup_renders_server_fragments():
|
|||||||
assert job_fragment.status_code == 200
|
assert job_fragment.status_code == 200
|
||||||
assert "data-html5-import-job" in job_fragment.text
|
assert "data-html5-import-job" in job_fragment.text
|
||||||
assert job_id in job_fragment.text
|
assert job_id in job_fragment.text
|
||||||
|
assert 'sse-swap="setup-import-job"' in job_fragment.text
|
||||||
|
assert 'hx-trigger="every 2s"' not in job_fragment.text
|
||||||
assert "<html" not in job_fragment.text
|
assert "<html" not in job_fragment.text
|
||||||
|
|
||||||
html5_import = client.post(f"/html5/projects/{project_id}/setup/import")
|
html5_import = client.post(f"/html5/projects/{project_id}/setup/import")
|
||||||
@@ -611,8 +620,18 @@ def test_html5_project_setup_renders_server_fragments():
|
|||||||
assert "data-html5-setup-summary" in summary.text
|
assert "data-html5-setup-summary" in summary.text
|
||||||
assert "INDEXED" in summary.text
|
assert "INDEXED" in summary.text
|
||||||
assert "mock_indexed" in summary.text
|
assert "mock_indexed" in summary.text
|
||||||
|
assert 'sse-swap="setup-summary"' in summary.text
|
||||||
|
assert 'hx-trigger="every 5s"' not in summary.text
|
||||||
assert "<html" not in summary.text
|
assert "<html" not in summary.text
|
||||||
|
|
||||||
|
with client.stream("GET", f"/html5/projects/{project_id}/setup/events?once=1") as events:
|
||||||
|
first_chunk = next(events.iter_text())
|
||||||
|
assert "event: setup-summary" in first_chunk
|
||||||
|
assert "event: setup-import-job" in first_chunk
|
||||||
|
assert "data-html5-setup-summary" in first_chunk
|
||||||
|
assert "data-html5-import-job" in first_chunk
|
||||||
|
assert project_id in first_chunk
|
||||||
|
|
||||||
|
|
||||||
def test_html5_operations_renders_job_monitor_fragments():
|
def test_html5_operations_renders_job_monitor_fragments():
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|||||||
Reference in New Issue
Block a user