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 abdb7880d13 Add Links to Edge React UI (#55356)
abdb7880d13 is described below

commit abdb7880d130e8d63685516ff814ebd7a6402396
Author: Jens Scheffler <[email protected]>
AuthorDate: Wed Sep 10 17:29:21 2025 +0200

    Add Links to Edge React UI (#55356)
    
    * Add Links to Edge React UI
    
    * Turn off live updates for time-ago
    
    * Copilot feedback
---
 .../providers/edge3/plugins/www/dist/main.umd.cjs  | 53 ++++++++++++++--------
 .../providers/edge3/plugins/www/package.json       |  3 +-
 .../providers/edge3/plugins/www/pnpm-lock.yaml     | 12 +++++
 .../www/src/components/WorkerStateBadge.tsx        | 33 ++++++++++++++
 .../www/src/components/ui/ScrollToAnchor.tsx       | 49 ++++++++++++++++++++
 .../edge3/plugins/www/src/components/ui/index.ts   |  1 +
 .../edge3/plugins/www/src/pages/JobsPage.tsx       | 46 +++++++++++++++----
 .../edge3/plugins/www/src/pages/WorkerPage.tsx     | 18 ++++++--
 providers/edge3/www-hash.txt                       |  2 +-
 9 files changed, 182 insertions(+), 35 deletions(-)

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 766376f7f11..6a912664875 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(E,te){typeof exports=="object"&&typeof 
module<"u"?module.exports=te(require("react"),require("react-dom")):typeof 
define=="function"&&define.amd?define(["react","react-dom"],te):(E=typeof 
globalThis<"u"?globalThis:E||self,E.AirflowPlugin=te(E.React,E.ReactDOM))})(this,function(E,te){"use
 strict";var YA=Object.defineProperty;var hb=E=>{throw TypeError(E)};var 
QA=(E,te,ve)=>te in 
E?YA(E,te,{enumerable:!0,configurable:!0,writable:!0,value:ve}):E[te]=ve;var 
Je=(E,te,ve)=>QA(E,typeo [...]
+(function(E,te){typeof exports=="object"&&typeof 
module<"u"?module.exports=te(require("react"),require("react-dom")):typeof 
define=="function"&&define.amd?define(["react","react-dom"],te):(E=typeof 
globalThis<"u"?globalThis:E||self,E.AirflowPlugin=te(E.React,E.ReactDOM))})(this,function(E,te){"use
 strict";var dA=Object.defineProperty;var Eb=E=>{throw TypeError(E)};var 
hA=(E,te,ve)=>te in 
E?dA(E,te,{enumerable:!0,configurable:!0,writable:!0,value:ve}):E[te]=ve;var 
Je=(E,te,ve)=>hA(E,typeo [...]
  * @license React
  * react-jsx-runtime.production.min.js
  *
@@ -6,27 +6,27 @@
  *
  * This source code is licensed under the MIT license found in the
  * LICENSE file in the root directory of this source tree.
- */var 
vb=E,bb=Symbol.for("react.element"),yb=Symbol.for("react.fragment"),xb=Object.prototype.hasOwnProperty,Cb=vb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Sb={key:!0,ref:!0,__self:!0,__source:!0};function
 kc(e,t,n){var r,i={},o=null,s=null;n!==void 0&&(o=""+n),t.key!==void 
0&&(o=""+t.key),t.ref!==void 0&&(s=t.ref);for(r in 
t)xb.call(t,r)&&!Sb.hasOwnProperty(r)&&(i[r]=t[r]);if(e&&e.defaultProps)for(r 
in t=e.defaultProps,t)i[r]===void 0&&(i[r]=t[r]);return{$$t [...]
+ */var 
Rb=E,Tb=Symbol.for("react.element"),Nb=Symbol.for("react.fragment"),_b=Object.prototype.hasOwnProperty,Ab=Rb.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Vb={key:!0,ref:!0,__self:!0,__source:!0};function
 Vc(e,t,n){var r,i={},o=null,s=null;n!==void 0&&(o=""+n),t.key!==void 
0&&(o=""+t.key),t.ref!==void 0&&(s=t.ref);for(r in 
t)_b.call(t,r)&&!Vb.hasOwnProperty(r)&&(i[r]=t[r]);if(e&&e.defaultProps)for(r 
in t=e.defaultProps,t)i[r]===void 0&&(i[r]=t[r]);return{$$t [...]
  * 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 ke=typeof 
Symbol=="function"&&Symbol.for,Js=ke?Symbol.for("react.element"):60103,Zs=ke?Symbol.for("react.portal"):60106,Zi=ke?Symbol.for("react.fragment"):60107,eo=ke?Symbol.for("react.strict_mode"):60108,to=ke?Symbol.for("react.profiler"):60114,no=ke?Symbol.for("react.provider"):60109,ro=ke?Symbol.for("react.context"):60110,ea=ke?Symbol.for("react.async_mode"):60111,io=ke?Symbol.for("react.concurrent_mode"):60111,oo=ke?Symbol.for("react.forward_ref"):60112,so=ke?Symbol.for("react
 [...]
+ */var ke=typeof 
Symbol=="function"&&Symbol.for,sa=ke?Symbol.for("react.element"):60103,aa=ke?Symbol.for("react.portal"):60106,ro=ke?Symbol.for("react.fragment"):60107,io=ke?Symbol.for("react.strict_mode"):60108,oo=ke?Symbol.for("react.profiler"):60114,so=ke?Symbol.for("react.provider"):60109,ao=ke?Symbol.for("react.context"):60110,la=ke?Symbol.for("react.async_mode"):60111,lo=ke?Symbol.for("react.concurrent_mode"):60111,co=ke?Symbol.for("react.forward_ref"):60112,uo=ke?Symbol.for("react
 [...]
       <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 
pE(e){if(!(!e||e.ownerDocument.activeElement!==e))try{const{selectionStart:t,selectionEnd:n,value:r}=e,i=r.substring(0,t),o=r.substring(n);return{start:t,end:n,value:r,beforeTxt:i,afterTxt:o}}catch{}}function
 
mE(e,t){if(!(!e||e.ownerDocument.activeElement!==e)){if(!t){e.setSelectionRange(e.value.length,e.value.length);return}try{const{value:n}=e,{beforeTxt:r="",afterTxt:i="",start:o}=t;let
 s=n.length;if(n.endsWith(i))s=n.length-i.length;else  [...]
+      </svg>`,n.body.appendChild(r)};function 
IE(e){if(!(!e||e.ownerDocument.activeElement!==e))try{const{selectionStart:t,selectionEnd:n,value:r}=e,i=r.substring(0,t),o=r.substring(n);return{start:t,end:n,value:r,beforeTxt:i,afterTxt:o}}catch{}}function
 
PE(e,t){if(!(!e||e.ownerDocument.activeElement!==e)){if(!t){e.setSelectionRange(e.value.length,e.value.length);return}try{const{value:n}=e,{beforeTxt:r="",afterTxt:i="",start:o}=t;let
 s=n.length;if(n.endsWith(i))s=n.length-i.length;else  [...]
 )+\\(\\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")}),Sk=Cf("width"),wk=Cf("height"),Sf=e=>({isMin:Pf(e.minMax,e.maxMin,e.min),isMax:Pf(e.maxMin,e.minMax,e.max)}),{isMin:kl,isMax:wf}=Sf(Sk),{isMin:Ol,isMax:Ef}=Sf(wk),kf=/print/i,Of=/^print$/i,Ek=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,kk=/(\d)/,Oi=Number.MAX_VALUE,Ok={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
 If(e){const t=Ek.exec(e)||(kl(e)||Ol(e)?kk.exec(e):null);if(!t)return 
Oi;if(t[0]==="0")return 0; [...]
-`).forEach(function(s){i=s.indexOf(":"),n=s.substring(0,i).trim().toLowerCase(),r=s.substring(i+1).trim(),!(!n||t[n]&&QT[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
 "+r:r)}),t},up=Symbol("internals");function _i(e){return 
e&&String(e).trim().toLowerCase()}function Cs(e){return 
e===!1||e==null?e:k.isArray(e)?e.map(Cs):String(e)}function ZT(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 e2=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(i=>r.set(i)),r}static accessor(t){const 
r=(this[up]=this[up]={accessors:{}}).accessors,i=this.prototype;function 
o(s){const a=_i(s);r[a]||(n2(i,s),r[a]=!0)}return 
k.isArray(t)?t.forEach(o):o(t),this}};Xe.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
 [...]
-`+o.map(Sp).join(`
-`):" "+Sp(o[0]):"as no adapter specified";throw new Q("There is no suitable 
adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return 
r},adapters:tc};function 
nc(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
 new Vr(null,e)}function Ep(e){return 
nc(e),e.headers=Xe.from(e.headers),e.data=Zl.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),wp.getAdapter(e.ad
 [...]
-`+o):r.stack=o}catch{}}throw r}}_request(t,n){typeof 
t=="string"?(n=n||{},n.url=t):n=t||{},n=qn(this.defaults,n);const{transitional:r,paramsSerializer:i,headers:o}=n;r!==void
 
0&&Os.assertOptions(r,{silentJSONParsing:Vt.transitional(Vt.boolean),forcedJSONParsing:Vt.transitional(Vt.boolean),clarifyTimeoutError:Vt.transitional(Vt.boolean)},!1),i!=null&&(k.isFunction(i)?n.paramsSerializer={serialize:i}:Os.assertOptions(i,{encode:Vt.function,serialize:Vt.function},!0)),n.allowAbsoluteUrls!==v
 [...]
+)+\\(\\s*min(-device)?-${e}`,"i"),max:new 
RegExp(`\\(\\s*max(-device)?-${e}`,"i")}),Vk=Tf("width"),Fk=Tf("height"),Nf=e=>({isMin:Df(e.minMax,e.maxMin,e.min),isMax:Df(e.maxMin,e.minMax,e.max)}),{isMin:Al,isMax:_f}=Nf(Vk),{isMin:Vl,isMax:Af}=Nf(Fk),Vf=/print/i,Ff=/^print$/i,Lk=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,Dk=/(\d)/,Ii=Number.MAX_VALUE,zk={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
 Lf(e){const t=Lk.exec(e)||(Al(e)||Vl(e)?Dk.exec(e):null);if(!t)return 
Ii;if(t[0]==="0")return 0; [...]
+`).forEach(function(s){i=s.indexOf(":"),n=s.substring(0,i).trim().toLowerCase(),r=s.substring(i+1).trim(),!(!n||t[n]&&u2[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
 "+r:r)}),t},yp=Symbol("internals");function Vi(e){return 
e&&String(e).trim().toLowerCase()}function Os(e){return 
e===!1||e==null?e:k.isArray(e)?e.map(Os):String(e)}function h2(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 f2=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(i=>r.set(i)),r}static accessor(t){const 
r=(this[yp]=this[yp]={accessors:{}}).accessors,i=this.prototype;function 
o(s){const a=Vi(s);r[a]||(p2(i,s),r[a]=!0)}return 
k.isArray(t)?t.forEach(o):o(t),this}};Xe.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
 [...]
+`+o.map(Np).join(`
+`):" "+Np(o[0]):"as no adapter specified";throw new Q("There is no suitable 
adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return 
r},adapters:cc};function 
uc(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
 new Fr(null,e)}function Ap(e){return 
uc(e),e.headers=Xe.from(e.headers),e.data=ac.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),_p.getAdapter(e.ad
 [...]
+`+o):r.stack=o}catch{}}throw r}}_request(t,n){typeof 
t=="string"?(n=n||{},n.url=t):n=t||{},n=qn(this.defaults,n);const{transitional:r,paramsSerializer:i,headers:o}=n;r!==void
 
0&&Ns.assertOptions(r,{silentJSONParsing:Vt.transitional(Vt.boolean),forcedJSONParsing:Vt.transitional(Vt.boolean),clarifyTimeoutError:Vt.transitional(Vt.boolean)},!1),i!=null&&(k.isFunction(i)?n.paramsSerializer={serialize:i}:Ns.assertOptions(i,{encode:Vt.function,serialize:Vt.function},!0)),n.allowAbsoluteUrls!==v
 [...]
  * @remix-run/router v1.23.0
  *
  * Copyright (c) Remix Software Inc.
@@ -35,7 +35,7 @@
  * LICENSE.md file in the root directory of this source tree.
  *
  * @license MIT
- */function Vi(){return 
Vi=Object.assign?Object.assign.bind():function(e){for(var 
t=1;t<arguments.length;t++){var n=arguments[t];for(var r in 
n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return 
e},Vi.apply(this,arguments)}var 
cn;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(cn||(cn={}));const
 Np="popstate";function N2(e){e===void 0&&(e={});function 
t(r,i){let{pathname:o,search:s,hash:a}=r.location;return 
oc("",{pathname:o,search:s,hash:a},i.state&&i.state.usr|| [...]
+ */function Fi(){return 
Fi=Object.assign?Object.assign.bind():function(e){for(var 
t=1;t<arguments.length;t++){var n=arguments[t];for(var r in 
n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return 
e},Fi.apply(this,arguments)}var 
cn;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(cn||(cn={}));const
 $p="popstate";function H2(e){e===void 0&&(e={});function 
t(r,i){let{pathname:o,search:s,hash:a}=r.location;return 
fc("",{pathname:o,search:s,hash:a},i.state&&i.state.usr|| [...]
  * React Router v6.30.1
  *
  * Copyright (c) Remix Software Inc.
@@ -44,7 +44,7 @@
  * LICENSE.md file in the root directory of this source tree.
  *
  * @license MIT
- */function Fi(){return 
Fi=Object.assign?Object.assign.bind():function(e){for(var 
t=1;t<arguments.length;t++){var n=arguments[t];for(var r in 
n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return 
e},Fi.apply(this,arguments)}const 
Rs=O.createContext(null),Bp=O.createContext(null),dn=O.createContext(null),Ts=O.createContext(null),Xn=O.createContext({outlet:null,matches:[],isDataRoute:!1}),jp=O.createContext(null);function
 nN(e,t){let{relative:n}=t===void 0?{}:t;Li()||pe(!1);let{b [...]
+ */function Li(){return 
Li=Object.assign?Object.assign.bind():function(e){for(var 
t=1;t<arguments.length;t++){var n=arguments[t];for(var r in 
n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return 
e},Li.apply(this,arguments)}const 
Vs=O.createContext(null),Yp=O.createContext(null),dn=O.createContext(null),Fs=O.createContext(null),Xn=O.createContext({outlet:null,matches:[],isDataRoute:!1}),Qp=O.createContext(null);function
 pN(e,t){let{relative:n}=t===void 0?{}:t;Di()||pe(!1);let{b [...]
  * React Router DOM v6.30.1
  *
  * Copyright (c) Remix Software Inc.
@@ -53,7 +53,7 @@
  * LICENSE.md file in the root directory of this source tree.
  *
  * @license MIT
- */function As(){return 
As=Object.assign?Object.assign.bind():function(e){for(var 
t=1;t<arguments.length;t++){var n=arguments[t];for(var r in 
n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return 
e},As.apply(this,arguments)}function Kp(e,t){if(e==null)return{};var 
n={},r=Object.keys(e),i,o;for(o=0;o<r.length;o++)i=r[o],!(t.indexOf(i)>=0)&&(n[i]=e[i]);return
 n}function CN(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function 
SN(e,t){return e.button===0&&(!t||t==="_sel [...]
+ */function Ds(){return 
Ds=Object.assign?Object.assign.bind():function(e){for(var 
t=1;t<arguments.length;t++){var n=arguments[t];for(var r in 
n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return 
e},Ds.apply(this,arguments)}function rm(e,t){if(e==null)return{};var 
n={},r=Object.keys(e),i,o;for(o=0;o<r.length;o++)i=r[o],!(t.indexOf(i)>=0)&&(n[i]=e[i]);return
 n}function VN(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function 
FN(e,t){return e.button===0&&(!t||t==="_sel [...]
  * 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
@@ -70,7 +70,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const 
mA=zE({pauseOnPageIdle:!0,placement:"bottom-end"}),sm=({error:e})=>{var i;const 
t=e;if(!t)return;const n=(i=t.body)==null?void 0:i.detail;let r;return n!==void 
0&&(typeof n=="string"?r=n:Array.isArray(n)?r=n.map(o=>`${o.loc.join(".")} 
${o.msg}`):r=Object.keys(n).map(o=>`${o}: 
${n[o]}`)),y.jsx(pA,{status:"error",children:y.jsxs(fR,{align:"start",flexDirection:"column",gap:2,mt:-1,children:[t.status,"
 ",t.message,r===t.message?void 
0:y.jsx(Wx,{whiteSpace:"preserve",wordBreak:"brea [...]
+ */const R_=YE({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
@@ -87,7 +87,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const cm=5e3;/*!
+ */const 
T_=({block:e="start",inline:t="nearest"})=>{const[n,r]=E.useState(()=>window.location.hash);return
 E.useEffect(()=>{const i=()=>r(window.location.hash);return 
window.addEventListener("hashchange",i),()=>window.removeEventListener("hashchange",i)},[]),E.useEffect(()=>{if(n){const
 
i=document.getElementById(n.slice(1));i&&i.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},bm=({error:e})=>{var
 i;const t=e;if(!t)return;const n=(i=t.body)==null?void 0:i.detail;let r [...]
  * 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
@@ -104,7 +104,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const OA=e=>{const[t,n]=E.useState(0);return 
E.useEffect(()=>{if(!e.current)return;const r=new ResizeObserver(i=>{for(const 
o of i)n(o.contentRect.width)});return 
r.observe(e.current),()=>{r.disconnect()}},[e]),t};/*!
+ */const Cm=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
@@ -121,7 +121,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const um="token",IA=()=>{const e=document.cookie.split(";");for(const t of 
e){const[n,r]=t.split("=");if((n==null?void 0:n.trim())==="_token"&&r!==void 
0)return localStorage.setItem(um,r),document.cookie="_token=; expires=Sat, 01 
Jan 2000 00:00:00 UTC; path=/;",r}},PA=e=>{const 
t=localStorage.getItem(um)??IA();return t!==void 
0&&(e.headers.Authorization=`Bearer 
${t}`),e},RA=()=>{const{data:e,error:t}=ZN(void 
0,{enabled:!0,refetchInterval:cm});return e?y.jsx(zt,{p:2,children:y.jsxs(bg, 
[...]
+ */const $_=e=>{const[t,n]=E.useState(0);return 
E.useEffect(()=>{if(!e.current)return;const r=new ResizeObserver(i=>{for(const 
o of i)n(o.contentRect.width)});return 
r.observe(e.current),()=>{r.disconnect()}},[e]),t};/*!
  * 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
@@ -138,4 +138,21 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const 
hn=(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}`}}}),HA=Cl({theme:{tokens:{colors:{suc
 [...]
+ */const Sm="token",B_=()=>{const e=document.cookie.split(";");for(const t of 
e){const[n,r]=t.split("=");if((n==null?void 0:n.trim())==="_token"&&r!==void 
0)return localStorage.setItem(Sm,r),document.cookie="_token=; expires=Sat, 01 
Jan 2000 00:00:00 UTC; path=/;",r}},j_=e=>{const 
t=localStorage.getItem(Sm)??B_();return t!==void 
0&&(e.headers.Authorization=`Bearer 
${t}`),e},W_=()=>{const{data:e,error:t}=d_(void 
0,{enabled:!0,refetchInterval:Cm});return e?v.jsx(zt,{p:2,children:v.jsxs(Ig, 
[...]
+ * 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
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */const 
hn=(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}`}}}),oA=Rl({theme:{tokens:{colors:{suc
 [...]
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json
index 48ea8f420f9..aa9d61d0690 100644
--- a/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json
+++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/package.json
@@ -37,7 +37,8 @@
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
     "react-icons": "^5.5.0",
-    "react-router-dom": "^6.30.0"
+    "react-router-dom": "^6.30.0",
+    "react-timeago": "^8.3.0"
   },
   "devDependencies": {
     "@7nohe/openapi-react-query-codegen": "^1.6.2",
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml
index c8e8979ad51..800ead46d1d 100644
--- a/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml
+++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/pnpm-lock.yaml
@@ -35,6 +35,9 @@ importers:
       react-router-dom:
         specifier: ^6.30.0
         version: 6.30.1([email protected]([email protected]))([email protected])
+      react-timeago:
+        specifier: ^8.3.0
+        version: 8.3.0([email protected])
     devDependencies:
       '@7nohe/openapi-react-query-codegen':
         specifier: ^1.6.2
@@ -2562,6 +2565,11 @@ packages:
     peerDependencies:
       react: '>=16.8'
 
+  [email protected]:
+    resolution: {integrity: 
sha512-BeR0hj/5qqTc2+zxzBSQZMky6MmqwOtKseU3CSmcjKR5uXerej2QY34v2d+cdz11PoeVfAdWLX+qjM/UdZkUUg==}
+    peerDependencies:
+      react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
   [email protected]:
     resolution: {integrity: 
sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
     engines: {node: '>=0.10.0'}
@@ -6039,6 +6047,10 @@ snapshots:
       '@remix-run/router': 1.23.0
       react: 18.3.1
 
+  [email protected]([email protected]):
+    dependencies:
+      react: 18.3.1
+
   [email protected]:
     dependencies:
       loose-envify: 1.4.0
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerStateBadge.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerStateBadge.tsx
index d4269b2d2ab..71ec46ea7ce 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerStateBadge.tsx
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerStateBadge.tsx
@@ -48,6 +48,38 @@ const state2Color = (state: EdgeWorkerState | null | 
undefined) => {
   }
 };
 
+const state2TooltipText = (state: EdgeWorkerState | null | undefined) => {
+  switch (state) {
+    // see enum mapping from 
providers/edge3/src/airflow/providers/edge3/models/edge_worker.py:EdgeWorkerState
+    case "starting":
+      return "Edge Worker is in initialization.";
+    case "running":
+      return "Edge Worker is actively running a task.";
+    case "idle":
+      return "Edge Worker is active and waiting for a task.";
+    case "shutdown request":
+      return "Request to shutdown Edge Worker.";
+    case "terminating":
+      return "Edge Worker is completing work and stopping.";
+    case "offline":
+      return "Edge Worker was shut down.";
+    case "unknown":
+      return "No heartbeat signal from worker for some time, Edge Worker 
probably down.";
+    case "maintenance request":
+      return "Worker was requested to enter maintenance mode. Once worker 
receives this it will pause fetching jobs.";
+    case "maintenance pending":
+      return "Edge worker received the request for maintenance, waiting for 
jobs to finish. Once jobs are finished will move to 'maintenance mode'.";
+    case "maintenance mode":
+      return "Edge worker is in maintenance mode. It is online but pauses 
fetching jobs.";
+    case "maintenance exit":
+      return "Request worker to exit maintenance mode. Once the worker 
receives this state it will un-pause and fetch new jobs.";
+    case "offline maintenance":
+      return "Worker was shut down in maintenance mode. It will be in 
maintenance mode when restarted.";
+    default:
+      return undefined;
+  }
+};
+
 export type Props = {
   readonly state?: EdgeWorkerState | null;
 } & BadgeProps;
@@ -61,6 +93,7 @@ export const WorkerStateBadge = 
React.forwardRef<HTMLDivElement, Props>(
       px={children === undefined ? 1 : 2}
       py={1}
       ref={ref}
+      title={state2TooltipText(state)}
       variant="solid"
       {...rest}
     >
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/ScrollToAnchor.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/ScrollToAnchor.tsx
new file mode 100644
index 00000000000..c06766d6e48
--- /dev/null
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/ScrollToAnchor.tsx
@@ -0,0 +1,49 @@
+/*!
+ * 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
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useEffect, useState } from "react";
+
+interface ScrollToAnchorProps {
+  inline?: ScrollLogicalPosition;
+  block?: ScrollLogicalPosition;
+}
+
+export const ScrollToAnchor = ({ block = "start", inline = "nearest" }: 
ScrollToAnchorProps): null => {
+  const [hash, setHash] = useState(() => window.location.hash);
+
+  useEffect(() => {
+    const onHashChange = () => setHash(window.location.hash);
+    window.addEventListener("hashchange", onHashChange);
+    return () => window.removeEventListener("hashchange", onHashChange);
+  }, []);
+
+  useEffect(() => {
+    if (hash) {
+      const element = document.getElementById(hash.slice(1));
+      if (element) {
+        element.scrollIntoView({
+          behavior: "auto",
+          block: block,
+          inline: inline,
+        });
+      }
+    }
+  }, [hash, block, inline]);
+
+  return null;
+};
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/index.ts
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/index.ts
index 95ef0889d86..5f6d9ea424c 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/index.ts
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/index.ts
@@ -19,3 +19,4 @@
 
 export * from "./Alert";
 export * from "./createToaster";
+export * from "./ScrollToAnchor";
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/JobsPage.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/JobsPage.tsx
index 5ace842f5dd..344860c5083 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/JobsPage.tsx
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/JobsPage.tsx
@@ -16,8 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Table } from "@chakra-ui/react";
+import { Box, Link, Table } from "@chakra-ui/react";
 import { useUiServiceJobs } from "openapi/queries";
+import { Link as RouterLink } from "react-router-dom";
+import TimeAgo from "react-timeago";
 
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { StateBadge } from "src/components/StateBadge";
@@ -33,8 +35,7 @@ export const JobsPage = () => {
   // Use DataTable as component from Airflow-Core UI
   // Add sorting
   // Add filtering
-  // Add links to see job details / jobs list
-  // Translation
+  // Translation?
   if (data)
     return (
       <Box p={2}>
@@ -58,18 +59,43 @@ export const JobsPage = () => {
               <Table.Row
                 
key={`${job.dag_id}.${job.run_id}.${job.task_id}.${job.map_index}.${job.try_number}`}
               >
-                <Table.Cell>{job.dag_id}</Table.Cell>
-                <Table.Cell>{job.run_id}</Table.Cell>
-                <Table.Cell>{job.task_id}</Table.Cell>
-                <Table.Cell>{job.map_index}</Table.Cell>
+                <Table.Cell>
+                  {/* TODO Check why <Link to={`/dags/${job.dag_id}`}> is not 
working via react-router-dom! */}
+                  <Link href={`../dags/${job.dag_id}`}>{job.dag_id}</Link>
+                </Table.Cell>
+                <Table.Cell>
+                  <Link 
href={`../dags/${job.dag_id}/runs/${job.run_id}`}>{job.run_id}</Link>
+                </Table.Cell>
+                <Table.Cell>
+                  {job.map_index >= 0 ? (
+                    <Link
+                      
href={`../dags/${job.dag_id}/runs/${job.run_id}/tasks/${job.task_id}/mapped/${job.map_index}?try_number=${job.try_number}`}
+                    >
+                      {job.task_id}
+                    </Link>
+                  ) : (
+                    <Link
+                      
href={`../dags/${job.dag_id}/runs/${job.run_id}/tasks/${job.task_id}?try_number=${job.try_number}`}
+                    >
+                      {job.task_id}
+                    </Link>
+                  )}
+                </Table.Cell>
+                <Table.Cell>{job.map_index >= 0 ? job.map_index : 
"-"}</Table.Cell>
                 <Table.Cell>{job.try_number}</Table.Cell>
                 <Table.Cell>
                   <StateBadge state={job.state}>{job.state}</StateBadge>
                 </Table.Cell>
                 <Table.Cell>{job.queue}</Table.Cell>
-                <Table.Cell>{job.queued_dttm}</Table.Cell>
-                <Table.Cell>{job.edge_worker}</Table.Cell>
-                <Table.Cell>{job.last_update}</Table.Cell>
+                <Table.Cell>
+                  {job.queued_dttm ? <TimeAgo date={job.queued_dttm} 
live={false} /> : undefined}
+                </Table.Cell>
+                <Table.Cell>
+                  <RouterLink 
to={`/plugin/edge_worker#${job.edge_worker}`}>{job.edge_worker}</RouterLink>
+                </Table.Cell>
+                <Table.Cell>
+                  {job.last_update ? <TimeAgo date={job.last_update} 
live={false} /> : undefined}
+                </Table.Cell>
               </Table.Row>
             ))}
           </Table.Body>
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx
index 0351b2d608e..dd1d059acb4 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx
@@ -18,10 +18,12 @@
  */
 import { Box, Table } from "@chakra-ui/react";
 import { useUiServiceWorker } from "openapi/queries";
+import TimeAgo from "react-timeago";
 
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { WorkerOperations } from "src/components/WorkerOperations";
 import { WorkerStateBadge } from "src/components/WorkerStateBadge";
+import { ScrollToAnchor } from "src/components/ui";
 import { autoRefreshInterval } from "src/utils";
 
 export const WorkerPage = () => {
@@ -34,8 +36,9 @@ export const WorkerPage = () => {
   // Use DataTable as component from Airflow-Core UI
   // Add sorting
   // Add filtering
-  // Add links to see jobs on worker
-  // Translation
+  // Add links with filter to see jobs on worker
+  // Add time zone support for time display
+  // Translation?
   if (data)
     return (
       <Box p={2}>
@@ -54,7 +57,7 @@ export const WorkerPage = () => {
           </Table.Header>
           <Table.Body>
             {data.workers.map((worker) => (
-              <Table.Row key={worker.worker_name}>
+              <Table.Row key={worker.worker_name} id={worker.worker_name}>
                 <Table.Cell>{worker.worker_name}</Table.Cell>
                 <Table.Cell>
                   <WorkerStateBadge 
state={worker.state}>{worker.state}</WorkerStateBadge>
@@ -70,8 +73,12 @@ export const WorkerPage = () => {
                     "(default)"
                   )}
                 </Table.Cell>
-                <Table.Cell>{worker.first_online}</Table.Cell>
-                <Table.Cell>{worker.last_heartbeat}</Table.Cell>
+                <Table.Cell>
+                  {worker.first_online ? <TimeAgo date={worker.first_online} 
live={false} /> : undefined}
+                </Table.Cell>
+                <Table.Cell>
+                  {worker.last_heartbeat ? <TimeAgo 
date={worker.last_heartbeat} live={false} /> : undefined}
+                </Table.Cell>
                 <Table.Cell>{worker.jobs_active}</Table.Cell>
                 <Table.Cell>
                   {worker.sysinfo ? (
@@ -93,6 +100,7 @@ export const WorkerPage = () => {
             ))}
           </Table.Body>
         </Table.Root>
+        <ScrollToAnchor />
       </Box>
     );
   if (error) {
diff --git a/providers/edge3/www-hash.txt b/providers/edge3/www-hash.txt
index e77d9fccb14..f1107f40c8a 100644
--- a/providers/edge3/www-hash.txt
+++ b/providers/edge3/www-hash.txt
@@ -1 +1 @@
-d3d458dbc15ae801bb6bf8f128e38a1a65fd81e3ecc8c87dd30a79acc9a4041a
+597e7860a9617d30432916dd89ed0018eca967c71184b4454690e36de31d0120

Reply via email to