This is an automated email from the ASF dual-hosted git repository.
jscheffl pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 44381b6282b Fix Edge3 provider navigation with webserver base_url
configuration (#56189)
44381b6282b is described below
commit 44381b6282b351fce16c05317d7f2ad89ab7a782
Author: Dheeraj Turaga <[email protected]>
AuthorDate: Wed Oct 8 11:04:31 2025 -0500
Fix Edge3 provider navigation with webserver base_url configuration (#56189)
* Fix Edge3 provider navigation with webserver base_url configuration
The Edge3 provider's BrowserRouter was not respecting the Airflow
webserver.base_url configuration, causing navigation links to generate
absolute paths from root instead of properly prefixed paths. This
resulted in 404 errors when Airflow is deployed with a base URL prefix
(e.g., my_company.com/airflow).
**Problem:**
- Edge3 provider pages were navigating to /plugin/edge_worker instead
of /airflow/plugin/edge_worker
- This broke navigation in deployments where nginx redirects traffic
from my_company.com/airflow to localhost:8080
- The issue was in JobsPage.tsx:94 where RouterLink generated links
without the base URL prefix
* Fix: Added _get_base_url_path() helper function and updated all URLs to
use it
* Fix Edge3 provider frontend API calls with webserver base_url
configuration
The Edge3 provider's frontend was not respecting Airflow's webserver
base_url configuration, causing API calls to fail when the webserver
was configured with a subpath (e.g., localhost:8080/airflow).
The OpenAPI-generated client was using an empty BASE configuration,
resulting in API calls to /edge_worker/ui/jobs instead of
/airflow/edge_worker/ui/jobs when base_url was set.
This fix initializes OpenAPI.BASE from the HTML <base> tag, following
the same pattern used in Airflow core, ensuring all API calls are
correctly prefixed with the configured base URL path
* Update
providers/edge3/src/airflow/providers/edge3/plugins/edge_executor_plugin.py
Co-authored-by: Jens Scheffler <[email protected]>
---------
Co-authored-by: Jens Scheffler <[email protected]>
---
.../edge3/plugins/edge_executor_plugin.py | 36 ++++++++++++++++------
.../providers/edge3/plugins/www/dist/main.umd.cjs | 36 +++++++++++-----------
.../edge3/plugins/www/src/layouts/EdgeLayout.tsx | 6 +++-
.../providers/edge3/plugins/www/src/main.tsx | 6 ++++
providers/edge3/www-hash.txt | 2 +-
5 files changed, 56 insertions(+), 30 deletions(-)
diff --git
a/providers/edge3/src/airflow/providers/edge3/plugins/edge_executor_plugin.py
b/providers/edge3/src/airflow/providers/edge3/plugins/edge_executor_plugin.py
index 6c8d5ebeda0..cd24e3e68dd 100644
---
a/providers/edge3/src/airflow/providers/edge3/plugins/edge_executor_plugin.py
+++
b/providers/edge3/src/airflow/providers/edge3/plugins/edge_executor_plugin.py
@@ -227,6 +227,22 @@ else:
RUNNING_ON_APISERVER = "gunicorn" in sys.argv[0] and "airflow-webserver"
in sys.argv
+def _get_base_url_path(path: str) -> str:
+ """Construct URL path with webserver base_url prefix."""
+ base_url = conf.get("api", "base_url", fallback="/")
+ # Extract pathname from base_url (handles both full URLs and path-only)
+ if base_url.startswith(("http://", "https://")):
+ from urllib.parse import urlparse
+
+ base_path = urlparse(base_url).path
+ else:
+ base_path = base_url
+
+ # Normalize paths: remove trailing slash from base, ensure leading slash
on path
+ base_path = base_path.rstrip("/")
+ return base_path + path
+
+
class EdgeExecutorPlugin(AirflowPlugin):
"""EdgeExecutor Plugin - provides API endpoints for Edge Workers in
Webserver."""
@@ -237,30 +253,30 @@ class EdgeExecutorPlugin(AirflowPlugin):
react_apps = [
{
"name": "Edge Worker",
- "bundle_url": "/edge_worker/static/main.umd.cjs",
+ "bundle_url":
_get_base_url_path("/edge_worker/static/main.umd.cjs"),
"destination": "nav",
"url_route": "edge_worker",
"category": "admin",
- "icon": "/edge_worker/res/cloud-computer.svg",
- "icon_dark_mode":
"/edge_worker/res/cloud-computer-dark.svg",
+ "icon":
_get_base_url_path("/edge_worker/res/cloud-computer.svg"),
+ "icon_dark_mode":
_get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
},
{
"name": "Edge Worker Jobs",
- "bundle_url": "/edge_worker/static/main.umd.cjs",
+ "bundle_url":
_get_base_url_path("/edge_worker/static/main.umd.cjs"),
"url_route": "edge_jobs",
"category": "admin",
- "icon": "/edge_worker/res/cloud-computer.svg",
- "icon_dark_mode":
"/edge_worker/res/cloud-computer-dark.svg",
+ "icon":
_get_base_url_path("/edge_worker/res/cloud-computer.svg"),
+ "icon_dark_mode":
_get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
},
]
external_views = [
{
"name": "Edge Worker API docs",
- "href": "/edge_worker/docs",
+ "href": _get_base_url_path("/edge_worker/docs"),
"destination": "nav",
"category": "docs",
- "icon": "/edge_worker/res/cloud-computer.svg",
- "icon_dark_mode":
"/edge_worker/res/cloud-computer-dark.svg",
+ "icon":
_get_base_url_path("/edge_worker/res/cloud-computer.svg"),
+ "icon_dark_mode":
_get_base_url_path("/edge_worker/res/cloud-computer-dark.svg"),
"url_route": "edge_worker_api_docs",
}
]
@@ -271,7 +287,7 @@ class EdgeExecutorPlugin(AirflowPlugin):
appbuilder_menu_items = [
{
"name": "Edge Worker API docs",
- "href": "/edge_worker/v1/ui",
+ "href": _get_base_url_path("/edge_worker/v1/ui"),
"category": "Docs",
}
]
diff --git
a/providers/edge3/src/airflow/providers/edge3/plugins/www/dist/main.umd.cjs
b/providers/edge3/src/airflow/providers/edge3/plugins/www/dist/main.umd.cjs
index 8677681e230..be28996e0ec 100644
--- a/providers/edge3/src/airflow/providers/edge3/plugins/www/dist/main.umd.cjs
+++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/dist/main.umd.cjs
@@ -1,4 +1,4 @@
-(function(P,Ft){typeof exports=="object"&&typeof
module<"u"?module.exports=Ft(require("react"),require("react-dom")):typeof
define=="function"&&define.amd?define(["react","react-dom"],Ft):(P=typeof
globalThis<"u"?globalThis:P||self,P.AirflowPlugin=Ft(P.React,P.ReactDOM))})(this,(function(P,Ft){"use
strict";function Sp(e){const
t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n
in e)if(n!=="default"){const
r=Object.getOwnPropertyDescriptor(e,n);Object.definePr [...]
+(function(P,Lt){typeof exports=="object"&&typeof
module<"u"?module.exports=Lt(require("react"),require("react-dom")):typeof
define=="function"&&define.amd?define(["react","react-dom"],Lt):(P=typeof
globalThis<"u"?globalThis:P||self,P.AirflowPlugin=Lt(P.React,P.ReactDOM))})(this,(function(P,Lt){"use
strict";function Ep(e){const
t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n
in e)if(n!=="default"){const
r=Object.getOwnPropertyDescriptor(e,n);Object.definePr [...]
* @license React
* react-jsx-runtime.production.js
*
@@ -6,28 +6,28 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */var ml;function Op(){if(ml)return hr;ml=1;var
e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function
n(r,o,i){var s=null;if(i!==void 0&&(s=""+i),o.key!==void
0&&(s=""+o.key),"key"in o){i={};for(var a in o)a!=="key"&&(i[a]=o[a])}else
i=o;return o=i.ref,{$$typeof:e,type:r,key:s,ref:o!==void
0?o:null,props:i}}return hr.Fragment=t,hr.jsx=n,hr.jsxs=n,hr}var vl;function
Pp(){return vl||(vl=1,qi.exports=Op()),qi.exports}var g=Pp();function bl(e){var
t=Object.creat [...]
+ */var ml;function Pp(){if(ml)return hr;ml=1;var
e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function
n(r,o,i){var s=null;if(i!==void 0&&(s=""+i),o.key!==void
0&&(s=""+o.key),"key"in o){i={};for(var a in o)a!=="key"&&(i[a]=o[a])}else
i=o;return o=i.ref,{$$typeof:e,type:r,key:s,ref:o!==void
0?o:null,props:i}}return hr.Fragment=t,hr.jsx=n,hr.jsxs=n,hr}var vl;function
Rp(){return vl||(vl=1,qi.exports=Pp()),qi.exports}var g=Rp();function bl(e){var
t=Object.creat [...]
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
- */var Il;function im(){if(Il)return Q;Il=1;var e=typeof
Symbol=="function"&&Symbol.for,t=e?Symbol.for("react.element"):60103,n=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,o=e?Symbol.for("react.strict_mode"):60108,i=e?Symbol.for("react.profiler"):60114,s=e?Symbol.for("react.provider"):60109,a=e?Symbol.for("react.context"):60110,l=e?Symbol.for("react.async_mode"):60111,c=e?Symbol.for("react.concurrent_mode"):60111,u=e?Symbol.for("react.forward_ref"):60112,h=e
[...]
+ */var Il;function sm(){if(Il)return Q;Il=1;var e=typeof
Symbol=="function"&&Symbol.for,t=e?Symbol.for("react.element"):60103,n=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,o=e?Symbol.for("react.strict_mode"):60108,i=e?Symbol.for("react.profiler"):60114,s=e?Symbol.for("react.provider"):60109,a=e?Symbol.for("react.context"):60110,l=e?Symbol.for("react.async_mode"):60111,c=e?Symbol.for("react.concurrent_mode"):60111,u=e?Symbol.for("react.forward_ref"):60112,h=e
[...]
<svg width="46" height="15" style="left: -15.5px; position: absolute;
top: 0; filter: drop-shadow(rgba(0, 0, 0, 0.4) 0px 1px 1.1px);">
<g transform="translate(2 3)">
<path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L
31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z" style="stroke-width: 2px; stroke:
white;"></path>
<path fill-rule="evenodd" d="M 15 4.5L 15 2L 11.5 5.5L 15 9L 15 6.5L
31 6.5L 31 9L 34.5 5.5L 31 2L 31 4.5Z"></path>
</g>
- </svg>`,n.body.appendChild(r)};function
fk(e,t){if(!(!e||!t.isActiveElement(e)))try{const{selectionStart:n,selectionEnd:r,value:o}=e,i=o.substring(0,n),s=o.substring(r);return{start:n,end:r,value:o,beforeTxt:i,afterTxt:s}}catch{}}function
gk(e,t,n){if(!(!e||!n.isActiveElement(e))){if(!t){e.setSelectionRange(e.value.length,e.value.length);return}try{const{value:r}=e,{beforeTxt:o="",afterTxt:i="",start:s}=t;let
a=r.length;if(r.endsWith(i))a=r.length-i.length;else if(r.startsWith(o))a [...]
+ </svg>`,n.body.appendChild(r)};function
gk(e,t,n){if(!(!e||!n.isActiveElement(e))){if(!t){const
r=e.value.length;e.setSelectionRange(r,r);return}try{const
r=e.value,{start:o,end:i,value:s}=t;if(r===s){e.setSelectionRange(o,i);return}const
a=Vd(s,r,o),l=o===i?a:Vd(s,r,i),c=Math.max(0,Math.min(a,r.length)),u=Math.max(c,Math.min(l,r.length));e.setSelectionRange(c,u)}catch{const
r=e.value.length;e.setSelectionRange(r,r)}}}function Vd(e,t,n){const
r=e.slice(0,n),o=e.slice(n);let i=0;con [...]
)+\\(\\s*max(-device)?-${e}`,"i"),min:new
RegExp(`\\(\\s*min(-device)?-${e}`,"i"),maxMin:new
RegExp(`(!?\\(\\s*max(-device)?-${e})(.|
-)+\\(\\s*min(-device)?-${e}`,"i"),max:new
RegExp(`\\(\\s*max(-device)?-${e}`,"i")}),yC=uh("width"),xC=uh("height"),dh=e=>({isMin:vh(e.minMax,e.maxMin,e.min),isMax:vh(e.maxMin,e.minMax,e.max)}),{isMin:Sa,isMax:hh}=dh(yC),{isMin:Ea,isMax:fh}=dh(xC),gh=/print/i,ph=/^print$/i,kC=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,CC=/(\d)/,$r=Number.MAX_VALUE,wC={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
mh(e){const t=kC.exec(e)||(Sa(e)||Ea(e)?CC.exec(e):null);if(!t)return
$r;if(t[0]==="0")return 0; [...]
-`).forEach(function(s){o=s.indexOf(":"),n=s.substring(0,o).trim().toLowerCase(),r=s.substring(o+1).trim(),!(!n||t[n]&&BO[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
"+r:r)}),t},og=Symbol("internals");function Kr(e){return
e&&String(e).trim().toLowerCase()}function Ei(e){return
e===!1||e==null?e:E.isArray(e)?e.map(Ei):String(e)}function HO(e){const
t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let
r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const UO=e=>/ [...]
-`)}getSetCookie(){return
this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static
from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const
r=new this(t);return n.forEach(o=>r.set(o)),r}static accessor(t){const
r=(this[og]=this[og]={accessors:{}}).accessors,o=this.prototype;function
i(s){const a=Kr(s);r[a]||(qO(o,s),r[a]=!0)}return
E.isArray(t)?t.forEach(i):i(t),this}};Me.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
[...]
-`+s.map(bg).join(`
-`):" "+bg(s[0]):"as no adapter specified";throw new H("There is no suitable
adapter to dispatch the request "+a,"ERR_NOT_SUPPORT")}return
o},adapters:Xa};function
Qa(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
new lr(null,e)}function xg(e){return
Qa(e),e.headers=Me.from(e.headers),e.data=Ya.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),yg.getAdapter(e.ad
[...]
-`+i):r.stack=i}catch{}}throw r}}_request(t,n){typeof
t=="string"?(n=n||{},n.url=t):n=t||{},n=In(this.defaults,n);const{transitional:r,paramsSerializer:o,headers:i}=n;r!==void
0&&Ii.assertOptions(r,{silentJSONParsing:mt.transitional(mt.boolean),forcedJSONParsing:mt.transitional(mt.boolean),clarifyTimeoutError:mt.transitional(mt.boolean)},!1),o!=null&&(E.isFunction(o)?n.paramsSerializer={serialize:o}:Ii.assertOptions(o,{encode:mt.function,serialize:mt.function},!0)),n.allowAbsoluteUrls!==v
[...]
- * react-router v7.9.2
+)+\\(\\s*min(-device)?-${e}`,"i"),max:new
RegExp(`\\(\\s*max(-device)?-${e}`,"i")}),yC=dh("width"),xC=dh("height"),hh=e=>({isMin:bh(e.minMax,e.maxMin,e.min),isMax:bh(e.maxMin,e.minMax,e.max)}),{isMin:Sa,isMax:fh}=hh(yC),{isMin:Ea,isMax:gh}=hh(xC),ph=/print/i,mh=/^print$/i,kC=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,CC=/(\d)/,$r=Number.MAX_VALUE,wC={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
vh(e){const t=kC.exec(e)||(Sa(e)||Ea(e)?CC.exec(e):null);if(!t)return
$r;if(t[0]==="0")return 0; [...]
+`).forEach(function(s){o=s.indexOf(":"),n=s.substring(0,o).trim().toLowerCase(),r=s.substring(o+1).trim(),!(!n||t[n]&&BO[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
"+r:r)}),t},ig=Symbol("internals");function Kr(e){return
e&&String(e).trim().toLowerCase()}function Ei(e){return
e===!1||e==null?e:E.isArray(e)?e.map(Ei):String(e)}function HO(e){const
t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let
r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const UO=e=>/ [...]
+`)}getSetCookie(){return
this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static
from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const
r=new this(t);return n.forEach(o=>r.set(o)),r}static accessor(t){const
r=(this[ig]=this[ig]={accessors:{}}).accessors,o=this.prototype;function
i(s){const a=Kr(s);r[a]||(qO(o,s),r[a]=!0)}return
E.isArray(t)?t.forEach(i):i(t),this}};Me.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
[...]
+`+s.map(yg).join(`
+`):" "+yg(s[0]):"as no adapter specified";throw new H("There is no suitable
adapter to dispatch the request "+a,"ERR_NOT_SUPPORT")}return
o},adapters:Xa};function
Qa(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
new lr(null,e)}function kg(e){return
Qa(e),e.headers=Me.from(e.headers),e.data=Ya.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),xg.getAdapter(e.ad
[...]
+`+i):r.stack=i}catch{}}throw r}}_request(t,n){typeof
t=="string"?(n=n||{},n.url=t):n=t||{},n=In(this.defaults,n);const{transitional:r,paramsSerializer:o,headers:i}=n;r!==void
0&&Ii.assertOptions(r,{silentJSONParsing:mt.transitional(mt.boolean),forcedJSONParsing:mt.transitional(mt.boolean),clarifyTimeoutError:mt.transitional(mt.boolean)},!1),o!=null&&(E.isFunction(o)?n.paramsSerializer={serialize:o}:Ii.assertOptions(o,{encode:mt.function,serialize:mt.function},!0)),n.allowAbsoluteUrls!==v
[...]
+ * react-router v7.9.3
*
* Copyright (c) Remix Software Inc.
*
@@ -35,9 +35,9 @@
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
- */var Pg="popstate";function wP(e={}){function
t(r,o){let{pathname:i,search:s,hash:a}=r.location;return
el("",{pathname:i,search:s,hash:a},o.state&&o.state.usr||null,o.state&&o.state.key||"default")}function
n(r,o){return typeof o=="string"?o:Yr(o)}return EP(t,n,null,e)}function
le(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function
vt(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new
Error(t)}catch{}}}function SP(){return Math.random().toString(36).substring(
[...]
+ */var Ig="popstate";function wP(e={}){function
t(r,o){let{pathname:i,search:s,hash:a}=r.location;return
el("",{pathname:i,search:s,hash:a},o.state&&o.state.usr||null,o.state&&o.state.key||"default")}function
n(r,o){return typeof o=="string"?o:Yr(o)}return EP(t,n,null,e)}function
le(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function
bt(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new
Error(t)}catch{}}}function SP(){return Math.random().toString(36).substring(
[...]
-Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}/*`}">.`)}let d=Nn(),m;if(t){let y=typeof
t=="string"?cr(t):t;le(u==="/"||y.pathname?.startsWith(u),`When overriding the
location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the
location pathname must begin with the portion of the URL pathname that was
matched by all parent routes. The current pathname base is "${u}" but pathname
"${y.pathname}" was given in the \`location\` prop.`),m=y}el [...]
+Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}/*`}">.`)}let d=Nn(),m;if(t){let y=typeof
t=="string"?cr(t):t;le(u==="/"||y.pathname?.startsWith(u),`When overriding the
location using \`<Routes location>\` or \`useRoutes(routes, location)\`, the
location pathname must begin with the portion of the URL pathname that was
matched by all parent routes. The current pathname base is "${u}" but pathname
"${y.pathname}" was given in the \`location\` prop.`),m=y}el [...]
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -54,7 +54,7 @@ Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
- */const _I=Fk({pauseOnPageIdle:!0,placement:"bottom-end"});/*!
+ */const _I=Lk({pauseOnPageIdle:!0,placement:"bottom-end"});/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -71,7 +71,7 @@ Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
- */const
VI=({block:e="start",inline:t="nearest"})=>{const[n,r]=P.useState(()=>window.location.hash);return
P.useEffect(()=>{const o=()=>r(window.location.hash);return
window.addEventListener("hashchange",o),()=>window.removeEventListener("hashchange",o)},[]),P.useEffect(()=>{if(n){const
o=document.getElementById(n.slice(1));o&&o.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},lp=({error:e})=>{const
t=e;if(!t)return;const n=t.body?.detail;let r;return n!==void 0&&(typ [...]
+ */const
VI=({block:e="start",inline:t="nearest"})=>{const[n,r]=P.useState(()=>window.location.hash);return
P.useEffect(()=>{const o=()=>r(window.location.hash);return
window.addEventListener("hashchange",o),()=>window.removeEventListener("hashchange",o)},[]),P.useEffect(()=>{if(n){const
o=document.getElementById(n.slice(1));o&&o.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},cp=({error:e})=>{const
t=e;if(!t)return;const n=t.body?.detail;let r;return n!==void 0&&(typ [...]
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -88,7 +88,7 @@ Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
- */const dp=5e3;/*!
+ */const hp=5e3;/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -122,7 +122,7 @@ Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
- */const hp="token",GI=()=>{const e=document.cookie.split(";");for(const t of
e){const[n,r]=t.split("=");if(n?.trim()==="_token"&&r!==void 0)return
localStorage.setItem(hp,r),document.cookie="_token=; expires=Sat, 01 Jan 2000
00:00:00 UTC; path=/;",r}},qI=e=>{const t=localStorage.getItem(hp)??GI();return
t!==void 0&&(e.headers.Authorization=`Bearer
${t}`),e},KI=()=>{const{data:e,error:t}=cI(void
0,{enabled:!0,refetchInterval:dp});return
e?.jobs&&e.jobs.length>0?g.jsx(xr,{p:2,children:g.j [...]
+ */const fp="token",GI=()=>{const e=document.cookie.split(";");for(const t of
e){const[n,r]=t.split("=");if(n?.trim()==="_token"&&r!==void 0)return
localStorage.setItem(fp,r),document.cookie="_token=; expires=Sat, 01 Jan 2000
00:00:00 UTC; path=/;",r}},qI=e=>{const t=localStorage.getItem(fp)??GI();return
t!==void 0&&(e.headers.Authorization=`Bearer
${t}`),e},KI=()=>{const{data:e,error:t}=cI(void
0,{enabled:!0,refetchInterval:hp});return
e?.jobs&&e.jobs.length>0?g.jsx(xr,{p:2,children:g.j [...]
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -139,4 +139,4 @@ Please change the parent <Route path="${y}"> to <Route
path="${y==="/"?"*":`${y}
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
- */const
U=(e,t="white")=>({solid:{value:`{colors.${e}.600}`},contrast:{value:{_light:"white",_dark:t}},fg:{value:{_light:`{colors.${e}.800}`,_dark:`{colors.${e}.200}`}},muted:{value:{_light:`{colors.${e}.200}`,_dark:`{colors.${e}.800}`}},subtle:{value:{_light:`{colors.${e}.100}`,_dark:`{colors.${e}.900}`}},emphasized:{value:{_light:`{colors.${e}.300}`,_dark:`{colors.${e}.700}`}},focusRing:{value:{_light:`{colors.${e}.800}`,_dark:`{colors.${e}.200}`}}}),fT=ka({theme:{tokens:{colors:{blac
[...]
+ */const
U=(e,t="white")=>({solid:{value:`{colors.${e}.600}`},contrast:{value:{_light:"white",_dark:t}},fg:{value:{_light:`{colors.${e}.800}`,_dark:`{colors.${e}.200}`}},muted:{value:{_light:`{colors.${e}.200}`,_dark:`{colors.${e}.800}`}},subtle:{value:{_light:`{colors.${e}.100}`,_dark:`{colors.${e}.900}`}},emphasized:{value:{_light:`{colors.${e}.300}`,_dark:`{colors.${e}.700}`}},focusRing:{value:{_light:`{colors.${e}.800}`,_dark:`{colors.${e}.200}`}}}),fT=ka({theme:{tokens:{colors:{blac
[...]
diff --git
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/layouts/EdgeLayout.tsx
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/layouts/EdgeLayout.tsx
index 00d04b0e225..8d75fdec30a 100644
---
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/layouts/EdgeLayout.tsx
+++
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/layouts/EdgeLayout.tsx
@@ -30,9 +30,13 @@ export const EdgeLayout = () => {
{ label: "Edge Jobs", value: "plugin/edge_jobs" },
];
+ // Extract base URL from HTML base element to handle webserver.base_url
configuration
+ const baseUrl = document.querySelector("base")?.href ??
"http://localhost:8080/";
+ const basename = new URL(baseUrl).pathname;
+
return (
<Box p={2} /* Compensate for parent padding from ExternalView */>
- <BrowserRouter>
+ <BrowserRouter basename={basename}>
<NavTabs tabs={tabs} />
<Routes>
<Route path="plugin/edge_worker" element={<WorkerPage />} />
diff --git
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/main.tsx
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/main.tsx
index 6d06ef6a8c5..71d23e738c1 100644
--- a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/main.tsx
+++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/main.tsx
@@ -19,6 +19,7 @@
import { ChakraProvider } from "@chakra-ui/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import axios from "axios";
+import { OpenAPI } from "openapi/requests/core/OpenAPI";
import { FC } from "react";
import { ColorModeProvider } from "src/context/colorMode";
@@ -33,6 +34,11 @@ export type PluginComponentProps = object;
* Main plugin component
*/
const PluginComponent: FC<PluginComponentProps> = () => {
+ // Set the base URL for OpenAPI client from the HTML base tag
+ const baseHref = document.querySelector("head > base")?.getAttribute("href")
?? "";
+ const baseUrl = new URL(baseHref, globalThis.location.origin);
+ OpenAPI.BASE = baseUrl.pathname.replace(/\/$/, ""); // Remove trailing slash
+
// ensure HTTP API calls are authenticated with current session token
axios.interceptors.request.use(tokenHandler);
diff --git a/providers/edge3/www-hash.txt b/providers/edge3/www-hash.txt
index 7d7c111098a..ce30fba4ffd 100644
--- a/providers/edge3/www-hash.txt
+++ b/providers/edge3/www-hash.txt
@@ -1 +1 @@
-719ccadaebac924011168dd33e1a46f7128362bfdd487ac43cefba7c365d717b
+b9d4cc61d6d7979b1b1cd1144d0a16663367137bb2e265a4fdd90a126dbbc78d