Add htmx HTML5 fragments
This commit is contained in:
@@ -9,7 +9,8 @@ from sir import NodeKind, SirSnapshot
|
||||
|
||||
|
||||
def render_html5_index(projects: Iterable[object]) -> str:
|
||||
project_rows = "\n".join(_project_row(project) for project in projects)
|
||||
project_list = list(projects)
|
||||
project_rows = "\n".join(_project_row(project) for project in project_list)
|
||||
if not project_rows:
|
||||
project_rows = '<tr><td colspan="4" class="muted">Проекты пока не настроены</td></tr>'
|
||||
return _page(
|
||||
@@ -23,7 +24,7 @@ def render_html5_index(projects: Iterable[object]) -> str:
|
||||
<p class="lead">Основной HTML собирает API-сервер. Браузер получает готовую страницу без React/Next runtime.</p>
|
||||
</div>
|
||||
<div class="hero-metrics">
|
||||
<strong>{len(list(projects))}</strong>
|
||||
<strong>{len(project_list)}</strong>
|
||||
<span>проектов</span>
|
||||
</div>
|
||||
</section>
|
||||
@@ -83,23 +84,15 @@ def render_html5_editor(
|
||||
NodeKind.DATA_PROCESSOR,
|
||||
}
|
||||
]
|
||||
tree_nodes = objects[:120] or modules[:120]
|
||||
selected_module = modules[0] if modules else None
|
||||
query = q.strip().lower()
|
||||
search_results = [
|
||||
node for node in snapshot.nodes
|
||||
if query and query in (node.qualified_name or node.name).lower()
|
||||
][:30]
|
||||
if not query:
|
||||
search_results = modules[:12] or snapshot.nodes[:12]
|
||||
|
||||
source_text = _node_source_text(selected_module)
|
||||
content = f"""
|
||||
<main class="workspace" data-html5-page="editor" data-project-id="{escape(project_id)}">
|
||||
{_topbar(project_id, project_nav)}
|
||||
<section class="layout">
|
||||
<aside class="panel tree" data-html5-tree>
|
||||
<div class="panel-title">Дерево объектов</div>
|
||||
<nav>{''.join(_tree_item(node) for node in objects[:120]) or '<p class="muted">Объекты не найдены</p>'}</nav>
|
||||
<nav>{''.join(_tree_item(project_id, node) for node in tree_nodes) or '<p class="muted">Объекты не найдены</p>'}</nav>
|
||||
</aside>
|
||||
<section class="editor" data-html5-editor>
|
||||
<div class="editor-head">
|
||||
@@ -107,12 +100,21 @@ def render_html5_editor(
|
||||
<p class="eyebrow">HTML5 editor</p>
|
||||
<h1>{escape(selected_module.qualified_name if selected_module else project_id)}</h1>
|
||||
</div>
|
||||
<form class="search" action="/html5/projects/{quote(project_id)}/editor" method="get" data-html5-search>
|
||||
<form
|
||||
class="search"
|
||||
action="/html5/projects/{quote(project_id)}/editor"
|
||||
method="get"
|
||||
data-html5-search
|
||||
hx-get="/html5/projects/{quote(project_id)}/symbols"
|
||||
hx-target="[data-html5-symbol-results]"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="false"
|
||||
>
|
||||
<input name="q" value="{escape(q)}" placeholder="Найти символ на сервере" />
|
||||
<button type="submit">Найти</button>
|
||||
</form>
|
||||
</div>
|
||||
<pre class="code" data-html5-source>{escape(source_text)}</pre>
|
||||
{render_html5_source(selected_module)}
|
||||
</section>
|
||||
<aside class="panel inspector" data-html5-inspector>
|
||||
<div class="panel-title">Серверный контекст</div>
|
||||
@@ -126,7 +128,7 @@ def render_html5_editor(
|
||||
<ul class="compact">{''.join(f'<li><span>{escape(kind)}</span><b>{count}</b></li>' for kind, count in counts.most_common(10))}</ul>
|
||||
<div class="panel-title">Результаты</div>
|
||||
<div data-html5-symbol-results>
|
||||
{''.join(_symbol_result(node) for node in search_results) or '<p class="muted">Нет результатов</p>'}
|
||||
{render_html5_symbols(snapshot, q)}
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
@@ -150,6 +152,29 @@ def render_html5_status(project_id: str, snapshot: SirSnapshot) -> str:
|
||||
)
|
||||
|
||||
|
||||
def html5_symbol_results(snapshot: SirSnapshot, q: str) -> list[object]:
|
||||
query = q.strip().lower()
|
||||
if not query:
|
||||
modules = [node for node in snapshot.nodes if node.kind == NodeKind.MODULE]
|
||||
return (modules[:12] or snapshot.nodes[:12])
|
||||
return [
|
||||
node for node in snapshot.nodes
|
||||
if query in (node.qualified_name or node.name).lower()
|
||||
][:30]
|
||||
|
||||
|
||||
def render_html5_symbols(snapshot: SirSnapshot, q: str) -> str:
|
||||
results = html5_symbol_results(snapshot, q)
|
||||
if not results:
|
||||
return '<p class="muted">Нет результатов</p>'
|
||||
return "".join(_symbol_result(node) for node in results)
|
||||
|
||||
|
||||
def render_html5_source(node: object | None) -> str:
|
||||
name = "source" if node is None else getattr(node, "qualified_name", None) or getattr(node, "name", "source")
|
||||
return f'<pre class="code" data-html5-source data-html5-source-name="{escape(str(name))}">{escape(_node_source_text(node))}</pre>'
|
||||
|
||||
|
||||
def _page(title: str, body: str) -> str:
|
||||
return f"""<!doctype html>
|
||||
<html lang="ru">
|
||||
@@ -196,11 +221,19 @@ def _topbar(project_id: str, project_nav: str) -> str:
|
||||
</header>"""
|
||||
|
||||
|
||||
def _tree_item(node: object) -> str:
|
||||
def _tree_item(project_id: str, node: object) -> str:
|
||||
name = getattr(node, "qualified_name", None) or getattr(node, "name", "")
|
||||
kind = getattr(node, "kind", "")
|
||||
kind_value = str(kind.value if hasattr(kind, "value") else kind)
|
||||
return f'<a class="tree-item" href="#{quote(str(name))}" data-html5-node-kind="{escape(kind_value)}"><span>{escape(str(name))}</span><small>{escape(kind_value)}</small></a>'
|
||||
lineage_id = str(getattr(node, "lineage_id", ""))
|
||||
return (
|
||||
f'<a class="tree-item" href="#{quote(str(name))}" '
|
||||
f'data-html5-node-kind="{escape(kind_value)}" '
|
||||
f'data-html5-lineage-id="{escape(lineage_id)}" '
|
||||
f'hx-get="/html5/projects/{quote(project_id)}/source/{quote(lineage_id, safe="")}" '
|
||||
'hx-target="[data-html5-source]" hx-swap="outerHTML">'
|
||||
f'<span>{escape(str(name))}</span><small>{escape(kind_value)}</small></a>'
|
||||
)
|
||||
|
||||
|
||||
def _symbol_result(node: object) -> str:
|
||||
|
||||
@@ -35,7 +35,7 @@ from fastapi.responses import PlainTextResponse, Response, StreamingResponse
|
||||
from neo4j import AsyncGraphDatabase
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from api_server.html5 import render_html5_editor, render_html5_index, render_html5_status
|
||||
from api_server.html5 import render_html5_editor, render_html5_index, render_html5_source, render_html5_status, render_html5_symbols
|
||||
from impact_engine import object_impact, routine_impact
|
||||
from incremental_indexer import rebuild_changed_file
|
||||
from integration_topology import IntegrationKind, build_integration_topology
|
||||
@@ -1618,6 +1618,27 @@ async def html5_project_events(project_id: str, once: bool = False) -> Streaming
|
||||
)
|
||||
|
||||
|
||||
@app.get("/html5/projects/{project_id}/symbols")
|
||||
async def html5_project_symbols(project_id: str, q: str = "") -> Response:
|
||||
snapshot = _project_snapshot_or_404(project_id)
|
||||
return Response(
|
||||
render_html5_symbols(snapshot, q),
|
||||
media_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/html5/projects/{project_id}/source/{lineage_id}")
|
||||
async def html5_project_source(project_id: str, lineage_id: str) -> Response:
|
||||
snapshot = _project_snapshot_or_404(project_id)
|
||||
node = _find_snapshot_node(snapshot, lineage_id)
|
||||
if node is None:
|
||||
raise HTTPException(status_code=404, detail=f"Lineage not found: {lineage_id}")
|
||||
return Response(
|
||||
render_html5_source(node),
|
||||
media_type="text/html; charset=utf-8",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/version")
|
||||
async def version() -> dict[str, str]:
|
||||
return {"name": "sfera", "version": "0.1.0"}
|
||||
|
||||
@@ -107,6 +107,10 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
||||
assert 'data-html5-page="editor"' in editor.text
|
||||
assert "data-html5-editor" in editor.text
|
||||
assert "data-html5-symbol-results" in editor.text
|
||||
assert 'hx-get="/html5/projects/' in editor.text
|
||||
assert 'hx-target="[data-html5-symbol-results]"' in editor.text
|
||||
assert 'hx-target="[data-html5-source]"' in editor.text
|
||||
assert 'hx-swap="outerHTML"' in editor.text
|
||||
assert "hx-ext=\"sse\"" in editor.text
|
||||
assert f"sse-connect=\"/html5/projects/{project_id}/events\"" in editor.text
|
||||
assert "htmx.org" in editor.text
|
||||
@@ -121,6 +125,22 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
||||
assert "data:" in first_chunk
|
||||
assert project_id in first_chunk
|
||||
|
||||
symbols = client.get(f"/html5/projects/{project_id}/symbols", params={"q": "Проверить"})
|
||||
assert symbols.status_code == 200
|
||||
assert "text/html" in symbols.headers["content-type"]
|
||||
assert 'data-html5-symbol' in symbols.text
|
||||
assert "Проверить" in symbols.text
|
||||
assert "<html" not in symbols.text
|
||||
|
||||
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
|
||||
module_node = next(node for node in snapshot["nodes"] if node["kind"] == "MODULE")
|
||||
source = client.get(f"/html5/projects/{project_id}/source/{module_node['lineage_id']}")
|
||||
assert source.status_code == 200
|
||||
assert "text/html" in source.headers["content-type"]
|
||||
assert "data-html5-source" in source.text
|
||||
assert "Проверить" in source.text
|
||||
assert "<html" not in source.text
|
||||
|
||||
|
||||
def test_project_setup_mock_import_indexes_project():
|
||||
client = TestClient(app)
|
||||
|
||||
Reference in New Issue
Block a user