Add Airflow UI plugin: Blueprint tab showing YAML and blueprint code - #75
Add Airflow UI plugin: Blueprint tab showing YAML and blueprint code#75jlaneve wants to merge 1 commit into
Conversation
Airflow's Code tab shows the loader file that builds YAML DAGs, not the
YAML itself. This adds a plugin (registered via the airflow.plugins entry
point) that puts a Blueprint tab on dag, dag run, task, and task instance
pages in Airflow 3.1+:
- dag and dag run: the source YAML, with each blueprint's Python
underneath, syntax highlighted with line numbers
- task and task instance: that step's config and blueprint source
To make the lookups exact, build_all_airflow_dags now tags each DAG with
blueprint:<yaml path> (opt out with source_tags=False) and records the
path in each task's blueprint_step_config. Both survive serialization,
so the plugin reads per-run rendered fields first (config as the task
ran, param overrides included), then the serialized DAG, then the file
on disk, and labels which one it shows. Values Airflow truncated under
[core] max_templated_field_length fall through to the next source.
The pages are served by a FastAPI app mounted at /blueprint. They render
in Airflow's sandboxed iframes, so links drive the parent router through
its history API, and the pages follow Airflow's theme toggle. On task
group pages Airflow leaves {TASK_ID} unsubstituted; the group id is
recovered from the Referer. On Airflow 2 the plugin registers nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQnbVv8WAkqMzwKbQsbBER
|
Claude finished @jlaneve's task in 2m 39s —— View job Code Review: Add Airflow UI pluginTasks:
This is a solid, well-structured implementation. The provenance ladder (RTIF → serialized DAG → YAML on disk) is a clever design, and the graceful-degradation pattern throughout is consistent. Below are findings ranked by severity. SecurityPath traversal guard is correct but load-bearing — XSS via
<a href="/blueprint/dags/{{ dag_id }}/yaml"
>Jinja2's autoescaping HTML-encodes the value (so CorrectnessFragile "Truncated." heuristic — **
Code QualityPrivate function imported across module boundary — from blueprint.builder import SOURCE_TAG_PREFIX, _relative_source
Stale docstring reference — Module-level
Unbounded PerformanceDouble-parsing YAML in content_html = code_html(path.read_text(encoding="utf-8"), "yaml", ...)
code_sections = dag_code_sections(_parse_dag_yaml(path), folder)
Test CoverageCoverage is excellent overall. A few gaps worth noting:
Summary
The architecture is clean — the tag-first / scan-fallback resolution, the independent config and code provenance tracking, and the sandboxed iframe navigation approach are all well-considered. The builder changes ( |
What
Airflow's Code tab shows the loader file that builds YAML DAGs, not the YAML itself. This adds a UI plugin, registered automatically via the
airflow.pluginsentry point, that puts a Blueprint tab on dag, dag run, task, and task instance pages in Airflow 3.1+:How it resolves what to show
build_all_airflow_dags()now stamps each DAG with ablueprint:<yaml path>tag (opt out withsource_tags=False) and records the path in each task'sblueprint_step_config. Both survive DAG serialization, so the plugin needs one lookup instead of re-scanning the dags folder.Task-level content walks a ladder and labels which source you're seeing:
Values Airflow truncated under
[core] max_templated_field_length(blueprint source files usually exceed it) fall through to the next source instead of showing the placeholder.Notes
/blueprintunder the API server and render inside Airflow's sandboxed iframes: links drive the parent SPA's router through its history API (the sandbox blockstarget="_top"), and the pages follow Airflow's light/dark toggle.{TASK_ID}unsubstituted; the group id is recovered from the Referer, so groups get the tab too.pygmentsas a direct dependency (already present transitively viarich).Testing
tests/test_plugin.py,tests/test_builder.pyadditions)🤖 Generated with Claude Code
https://claude.ai/code/session_01TQnbVv8WAkqMzwKbQsbBER