Add HTML5 object runtime context
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-17 00:11:26 +03:00
parent 1da745c52e
commit b93fd88e81
3 changed files with 67 additions and 1 deletions
@@ -322,6 +322,7 @@ def render_html5_object_context(
impact: object | None,
access: object | None = None,
ui: object | None = None,
runtime: Iterable[object] | None = None,
) -> str:
if schema is None or impact is None:
return f"""
@@ -341,6 +342,7 @@ def render_html5_object_context(
jobs = getattr(impact, "jobs", []) or []
grants = getattr(access, "grants", []) if access is not None else []
ui_forms = getattr(ui, "forms", []) if ui is not None else []
runtime_items = list(runtime or [])
return f"""
<div class="object-context" data-html5-object-context data-html5-object-name="{escape(str(name))}">
<div class="panel-title">Object context</div>
@@ -355,12 +357,14 @@ def render_html5_object_context(
{_metric("Routines", len(routines))}
{_metric("Forms", len(ui_forms) or len(forms))}
{_metric("Roles", len(grants) or len(roles))}
{_metric("Runtime", len(runtime_items))}
</dl>
<div class="compact-list">
{''.join(_named_node_item("attr", item) for item in attributes[:6]) or '<p class="muted padded">Реквизиты не найдены</p>'}
{''.join(_tabular_section_item(item) for item in sections[:4])}
{''.join(_ui_form_item(item) for item in ui_forms[:4])}
{''.join(_role_access_item(item) for item in grants[:6])}
{''.join(_runtime_summary_item(item) for item in runtime_items[:6])}
{''.join(_named_node_item("routine", item) for item in routines[:6])}
{''.join(_named_node_item("job", item) for item in jobs[:4])}
</div>
@@ -1423,6 +1427,21 @@ def _ui_form_item(form_semantics: object) -> str:
"""
def _runtime_summary_item(item: object) -> str:
node = getattr(item, "node", None)
name = getattr(node, "qualified_name", None) or getattr(node, "name", "runtime")
signal_count = getattr(item, "signal_count", 0)
error_count = getattr(item, "error_count", 0)
max_duration = getattr(item, "max_duration_ms", None)
duration_text = f" · max {max_duration} ms" if max_duration is not None else ""
return f"""
<article class="object-context-item" data-html5-object-context-item="runtime">
<strong>{escape(str(name))}</strong>
<small>{escape(str(signal_count))} signals · {escape(str(error_count))} errors{escape(duration_text)}</small>
</article>
"""
def _authoring_diff_item(line: object) -> str:
kind = str(getattr(line, "kind", ""))
text = str(getattr(line, "text", ""))
+31 -1
View File
@@ -1747,8 +1747,9 @@ async def html5_project_object_context(project_id: str, object_name: str) -> Res
impact = await get_object_impact(project_id, object_name)
access = await get_object_access(project_id, object_name)
ui = await get_object_ui(project_id, object_name)
runtime = _runtime_for_object_context(project_id, impact)
return Response(
render_html5_object_context(project_id, schema, impact, access, ui),
render_html5_object_context(project_id, schema, impact, access, ui, runtime),
media_type="text/html; charset=utf-8",
)
@@ -8112,6 +8113,35 @@ def _html5_metadata_tabular_sections(raw: str) -> list[dict]:
return sections
def _runtime_for_object_context(project_id: str, impact: ObjectImpactResponse) -> list[RuntimeSummaryResponse]:
lineages = {
item.lineage_id
for group in [
[impact.object],
impact.modules,
impact.routines,
impact.forms,
impact.commands,
impact.jobs,
impact.writes,
]
for item in group
if item is not None
}
snapshot = _project_snapshot_or_404(project_id)
overlay = _overlays.get(project_id, RuntimeOverlay(project_id=project_id))
return [
RuntimeSummaryResponse(
node=_named_node(item.node),
signal_count=item.signal_count,
error_count=item.error_count,
max_duration_ms=item.max_duration_ms,
)
for item in summarize_runtime(snapshot, overlay)
if item.node.lineage_id in lineages
]
def _current_import_source(project_id: str) -> ImportSourceKind:
setup = _project_setup_response(project_id)
if setup.current_source is not None:
+17
View File
@@ -254,6 +254,20 @@ def test_html5_object_context_fragment(tmp_path: Path):
client = TestClient(app)
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
assert indexed.status_code == 200
snapshot = client.get(f"/projects/{project_id}/snapshot/export").json()
handler = next(node for node in snapshot["nodes"] if node["name"] == "ПровестиКоманда")
signal = client.post(
f"/projects/{project_id}/runtime/signals",
json={
"signal": {
"signal_id": "html5-runtime.1",
"lineage_id": handler["lineage_id"],
"kind": "ERROR",
"duration_ms": 125.0,
}
},
)
assert signal.status_code == 200
editor = client.get(f"/html5/projects/{project_id}/editor")
assert editor.status_code == 200
@@ -271,6 +285,9 @@ def test_html5_object_context_fragment(tmp_path: Path):
assert "ФормаДокумента" in context.text
assert "Провести" in context.text
assert "ПровестиКоманда" in context.text
assert "1 signals" in context.text
assert "1 errors" in context.text
assert "125.0 ms" in context.text
assert "Роль.Менеджер" in context.text
assert "read, write, post" in context.text or "post, read, write" in context.text
assert "<html" not in context.text