@@ -128,18 +128,42 @@ def index() -> HTMLResponse:
128128 return HTMLResponse (_render_index (_list_jobs ()))
129129
130130
131- @app .get ("/{uuid}/" , response_class = HTMLResponse , dependencies = [Depends (_require_role )])
132- def job_index (uuid : str ) -> FileResponse :
133- job_dir = _resolve_in_public (uuid )
131+ def _render_empty_run (run_id : str ) -> str :
132+ return f"""<!doctype html>
133+ <html lang="en"><head>
134+ <meta charset="utf-8">
135+ <meta name="viewport" content="width=device-width, initial-scale=1">
136+ <title>Batch Image Results</title>
137+ <style>{ AIRFLOW_STYLE_CSS } </style>
138+ </head><body>
139+ <h1>Batch Image Results</h1>
140+ <div class="panel"><div class="empty">
141+ No batch image summary was generated for run
142+ <code class="mono">{ html .escape (run_id )} </code>.
143+ </div></div>
144+ </body></html>
145+ """
146+
147+
148+ @app .get ("/{run_id}/" , response_class = HTMLResponse , dependencies = [Depends (_require_role )])
149+ def job_index (run_id : str ):
150+ root = public_dir ().resolve ()
151+ job_dir = (root / run_id ).resolve ()
152+ try :
153+ job_dir .relative_to (root )
154+ except ValueError :
155+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
134156 index_html = job_dir / "index.html"
135157 if not index_html .is_file ():
136- raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
158+ # Return 200 with an empty-state page so the dag_run iframe tab on
159+ # non-summarise_job runs shows a clean message instead of a raw 404.
160+ return HTMLResponse (_render_empty_run (run_id ))
137161 return FileResponse (index_html , media_type = "text/html" )
138162
139163
140- @app .get ("/{uuid }/{filename:path}" , dependencies = [Depends (_require_role )])
141- def job_file (uuid : str , filename : str ) -> FileResponse :
142- return FileResponse (_resolve_in_public (uuid , filename ))
164+ @app .get ("/{run_id }/{filename:path}" , dependencies = [Depends (_require_role )])
165+ def job_file (run_id : str , filename : str ) -> FileResponse :
166+ return FileResponse (_resolve_in_public (run_id , filename ))
143167
144168
145169class PublicBrowserPlugin (AirflowPlugin ):
@@ -151,14 +175,23 @@ class PublicBrowserPlugin(AirflowPlugin):
151175 "name" : "Batch Image Results" ,
152176 },
153177 ]
154- # No ``category`` is set so the React UI routes this into ``topNavItems``
155- # (see ``airflow/ui/src/layouts/Nav/Nav.tsx``) instead of the "Browse"
156- # dropdown — rendering it as a top-level sidebar button next to DAGs/Assets.
178+ # Two views:
179+ # * ``nav`` \u2192 sidebar tab, cross-run listing (``topNavItems``
180+ # branch in ``airflow/ui/src/layouts/Nav/Nav.tsx``,
181+ # rendered as a top-level sidebar button).
182+ # * ``dag_run`` \u2192 tab on each DAG run detail page, deep-linking to
183+ # the per-run directory via ``{RUN_ID}`` templating.
157184 external_views = [
158185 {
159186 "name" : "Batch Image Results" ,
160187 "href" : f"{ URL_PREFIX } /" ,
161188 "destination" : "nav" ,
162189 "url_route" : "mokelumne_public_browser" ,
163190 },
191+ {
192+ "name" : "Batch Image Results" ,
193+ "href" : f"{ URL_PREFIX } /{{RUN_ID}}/" ,
194+ "destination" : "dag_run" ,
195+ "url_route" : "mokelumne_public_browser_run" ,
196+ },
164197 ]
0 commit comments