From c871a8bbd289ed0a37c803ae7e46e90bd943305d Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 17 May 2026 00:28:54 +0300 Subject: [PATCH] Add HTML5 object data flow context --- services/api-server/src/api_server/html5.py | 29 +++++++++++++++ services/api-server/src/api_server/main.py | 41 ++++++++++++++++++++- services/api-server/tests/test_api.py | 18 ++++++++- 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/services/api-server/src/api_server/html5.py b/services/api-server/src/api_server/html5.py index 8a03c4b..40681d3 100644 --- a/services/api-server/src/api_server/html5.py +++ b/services/api-server/src/api_server/html5.py @@ -325,6 +325,7 @@ def render_html5_object_context( runtime: Iterable[object] | None = None, knowledge: Iterable[object] | None = None, privacy: object | None = None, + integrations: Iterable[object] | None = None, ) -> str: if schema is None or impact is None: return f""" @@ -340,13 +341,18 @@ def render_html5_object_context( modules = getattr(impact, "modules", []) or [] routines = getattr(impact, "routines", []) or [] forms = getattr(impact, "forms", []) or [] + commands = getattr(impact, "commands", []) or [] roles = getattr(impact, "roles", []) or [] jobs = getattr(impact, "jobs", []) or [] + callees = getattr(impact, "callees", []) or [] + query_tables = getattr(impact, "query_tables", []) or [] + writes = getattr(impact, "writes", []) 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 []) knowledge_items = list(knowledge or []) privacy_markers = getattr(privacy, "markers", []) if privacy is not None else [] + integration_items = list(integrations or []) return f"""
Object context
@@ -360,7 +366,12 @@ def render_html5_object_context( {_metric("Modules", len(modules))} {_metric("Routines", len(routines))} {_metric("Forms", len(ui_forms) or len(forms))} + {_metric("Commands", len(commands))} {_metric("Roles", len(grants) or len(roles))} + {_metric("Reads", len(query_tables))} + {_metric("Writes", len(writes))} + {_metric("Calls", len(callees))} + {_metric("Integrations", len(integration_items))} {_metric("Runtime", len(runtime_items))} {_metric("Knowledge", len(knowledge_items))} {_metric("Privacy", len(privacy_markers))} @@ -370,6 +381,11 @@ def render_html5_object_context( {''.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(_integration_endpoint_item(item) for item in integration_items[:4])} + {''.join(_named_node_item("command", item) for item in commands[:6])} + {''.join(_named_node_item("read", item) for item in query_tables[:4])} + {''.join(_named_node_item("write", item) for item in writes[:4])} + {''.join(_named_node_item("call", item) for item in callees[:6])} {''.join(_runtime_summary_item(item) for item in runtime_items[:6])} {''.join(_knowledge_record_item(item) for item in knowledge_items[:6])} {''.join(_privacy_marker_item(item) for item in privacy_markers[:6])} @@ -1475,6 +1491,19 @@ def _privacy_marker_item(marker: object) -> str: """ +def _integration_endpoint_item(endpoint: object) -> str: + name = str(getattr(endpoint, "name", "") or "integration") + kind = _enum_text(getattr(endpoint, "kind", "UNKNOWN")) + direction = str(getattr(endpoint, "direction", "UNKNOWN") or "UNKNOWN") + owner = str(getattr(endpoint, "owner", "") or "owner unavailable") + return f""" +
+ {escape(name)} + {escape(kind)} · {escape(direction)} · {escape(owner)} +
+ """ + + def _authoring_diff_item(line: object) -> str: kind = str(getattr(line, "kind", "")) text = str(getattr(line, "text", "")) diff --git a/services/api-server/src/api_server/main.py b/services/api-server/src/api_server/main.py index 678a324..a122a43 100644 --- a/services/api-server/src/api_server/main.py +++ b/services/api-server/src/api_server/main.py @@ -1748,10 +1748,21 @@ async def html5_project_object_context(project_id: str, object_name: str) -> Res access = await get_object_access(project_id, object_name) ui = await get_object_ui(project_id, object_name) privacy = await object_privacy(project_id, object_name) + integrations = _integrations_for_object_context(project_id, impact) runtime = _runtime_for_object_context(project_id, impact) knowledge = _knowledge_for_object_context(schema, impact, ui) return Response( - render_html5_object_context(project_id, schema, impact, access, ui, runtime, knowledge, privacy), + render_html5_object_context( + project_id, + schema, + impact, + access, + ui, + runtime, + knowledge, + privacy, + integrations, + ), media_type="text/html; charset=utf-8", ) @@ -8178,6 +8189,34 @@ def _knowledge_for_object_context( return sorted(records, key=lambda item: item.title.lower())[:12] +def _integrations_for_object_context( + project_id: str, + impact: ObjectImpactResponse, +) -> list[IntegrationEndpointResponse]: + owner_names = { + name + for group in [impact.modules, impact.routines] + for item in group + for name in [item.qualified_name, item.name] + if name + } + if not owner_names: + return [] + snapshot = _project_snapshot_or_404(project_id) + return [ + IntegrationEndpointResponse( + endpoint_id=endpoint.endpoint_id, + name=endpoint.name, + kind=endpoint.kind.value, + direction=endpoint.direction, + owner=endpoint.owner, + attributes=endpoint.attributes, + ) + for endpoint in build_integration_topology(snapshot).endpoints + if endpoint.owner in owner_names + ] + + def _current_import_source(project_id: str) -> ImportSourceKind: setup = _project_setup_response(project_id) if setup.current_source is not None: diff --git a/services/api-server/tests/test_api.py b/services/api-server/tests/test_api.py index 2b4e156..c1e7f6b 100644 --- a/services/api-server/tests/test_api.py +++ b/services/api-server/tests/test_api.py @@ -250,7 +250,19 @@ def test_html5_object_context_fragment(tmp_path: Path): ) module = tmp_path / "Documents" / "ЗаказПокупателя" / "Ext" / "ObjectModule.bsl" module.parent.mkdir(parents=True) - module.write_text("Процедура ПровестиКоманда()\nКонецПроцедуры\n", encoding="utf-8") + module.write_text( + """ +Процедура ПровестиКоманда() + ПроверитьКонтрагента(); + Соединение = Новый HTTPСоединение("api.example.local"); + Адрес = "https://api.example.local/orders"; +КонецПроцедуры + +Процедура ПроверитьКонтрагента() +КонецПроцедуры +""", + encoding="utf-8", + ) client = TestClient(app) indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id}) assert indexed.status_code == 200 @@ -306,6 +318,10 @@ def test_html5_object_context_fragment(tmp_path: Path): assert "ФормаДокумента" in context.text assert "Провести" in context.text assert "ПровестиКоманда" in context.text + assert "ПроверитьКонтрагента" in context.text + assert "HTTPConnection" in context.text + assert "https://api.example.local/orders" in context.text + assert "OUTBOUND" in context.text assert "1 signals" in context.text assert "1 errors" in context.text assert "125.0 ms" in context.text