This is an automated email from the ASF dual-hosted git repository. kaxilnaik 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 068f3fce84e Feature/edge maintenance plugin beautification (#55348) 068f3fce84e is described below commit 068f3fce84e1d75752a852ba714e12d8f33f6f4f Author: Jens Scheffler <95105677+jsche...@users.noreply.github.com> AuthorDate: Tue Sep 9 00:13:54 2025 +0200 Feature/edge maintenance plugin beautification (#55348) @dheerajturaga somehow I was not able to pugh to your repo directly git gave me an error :-( ``` error: Authentication error: Authentication required: You must have push access to verify locks error: failed to push some refs to 'github.com:dheerajturaga/airflow.git' ``` ...anyway, another way around, openeing a draft PR here, can you cherry-pick the commits down to your PR? The last commit [Rework maintenance dialog to be a dialog](https://github.com/apache/airflow/commit/7e6d13cf316711f286e3262fa8552b2d62ede965) has a problem though: Dialog is not opening, it makes a JS script error which is not much helpful: ``` Uncaught TypeError: can't access property "flushSync", ne is undefined R http://localhost:28080/edge_worker/static/main.umd.cjs:16 ``` --- airflow-core/src/airflow/ui/src/main.tsx | 4 +- .../providers/edge3/plugins/www/dist/main.umd.cjs | 51 ++++--- .../www/src/components/MaintenanceEnterButton.tsx | 101 ++++++++++++++ .../www/src/components/MaintenanceExitButton.tsx | 61 +++++++++ .../plugins/www/src/components/OperationsCell.tsx | 147 --------------------- .../www/src/components/WorkerOperations.tsx | 65 +++++++++ .../components/ui/{index.ts => createToaster.ts} | 6 +- .../edge3/plugins/www/src/components/ui/index.ts | 1 + .../edge3/plugins/www/src/pages/WorkerPage.tsx | 54 +------- .../providers/edge3/plugins/www/vite.config.ts | 1 + .../providers/edge3/worker_api/routes/ui.py | 12 +- providers/edge3/www-hash.txt | 2 +- 12 files changed, 282 insertions(+), 223 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx index 2768d84809a..41f5d1bd0f6 100644 --- a/airflow-core/src/airflow/ui/src/main.tsx +++ b/airflow-core/src/airflow/ui/src/main.tsx @@ -21,6 +21,7 @@ import { QueryClientProvider } from "@tanstack/react-query"; import axios, { type AxiosError } from "axios"; import { StrictMode } from "react"; import React from "react"; +import * as ReactDOM from "react-dom"; import { createRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; import { RouterProvider } from "react-router-dom"; @@ -37,10 +38,11 @@ import { client } from "./queryClient"; import { system } from "./theme"; import { clearToken, tokenHandler } from "./utils/tokenHandler"; -// Set React and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins. +// Set React, ReactDOM, and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins. // Only one instance of React should be used. // Reflect will avoid type checking. Reflect.set(globalThis, "React", React); +Reflect.set(globalThis, "ReactDOM", ReactDOM); Reflect.set(globalThis, "ReactJSXRuntime", ReactJSXRuntime); // redirect to login page if the API responds with unauthorized or forbidden errors 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 871159e89c8..766376f7f11 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(T,Y){typeof exports=="object"&&typeof module<"u"?module.exports=Y(require("react"),require("react-dom")):typeof define=="function"&&define.amd?define(["react","react-dom"],Y):(T=typeof globalThis<"u"?globalThis:T||self,T.AirflowPlugin=Y(T.React))})(this,function(T){"use strict";var ZT=Object.defineProperty;var uv=T=>{throw TypeError(T)};var eN=(T,Y,w)=>Y in T?ZT(T,Y,{enumerable:!0,configurable:!0,writable:!0,value:w}):T[Y]=w;var Ke=(T,Y,w)=>eN(T,typeof Y!="symbol"?Y+"":Y,w),Vl= [...] +(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 [...] * @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 pv=T,mv=Symbol.for("react.element"),vv=Symbol.for("react.fragment"),bv=Object.prototype.hasOwnProperty,yv=pv.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,xv={key:!0,ref:!0,__self:!0,__source:!0};function Wl(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)bv.call(t,r)&&!xv.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 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 [...] * 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 xe=typeof Symbol=="function"&&Symbol.for,Os=xe?Symbol.for("react.element"):60103,Is=xe?Symbol.for("react.portal"):60106,$i=xe?Symbol.for("react.fragment"):60107,Bi=xe?Symbol.for("react.strict_mode"):60108,ji=xe?Symbol.for("react.profiler"):60114,Wi=xe?Symbol.for("react.provider"):60109,Hi=xe?Symbol.for("react.context"):60110,Rs=xe?Symbol.for("react.async_mode"):60111,Ui=xe?Symbol.for("react.concurrent_mode"):60111,Gi=xe?Symbol.for("react.forward_ref"):60112,qi=xe?Symbol.for("react [...] + */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 [...] <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 MS(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 $S(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 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 [...] )+\\(\\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")}),Dw=mh("width"),Mw=mh("height"),vh=e=>({isMin:wh(e.minMax,e.maxMin,e.min),isMax:wh(e.maxMin,e.minMax,e.max)}),{isMin:Ga,isMax:bh}=vh(Dw),{isMin:qa,isMax:yh}=vh(Mw),xh=/print/i,Ch=/^print$/i,$w=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,Bw=/(\d)/,di=Number.MAX_VALUE,jw={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function Sh(e){const t=$w.exec(e)||(Ga(e)||qa(e)?Bw.exec(e):null);if(!t)return di;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]&&e2[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)}),t},lg=Symbol("internals");function yi(e){return e&&String(e).trim().toLowerCase()}function Yo(e){return e===!1||e==null?e:k.isArray(e)?e.map(Yo):String(e)}function n2(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 r2=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[lg]=this[lg]={accessors:{}}).accessors,i=this.prototype;function o(s){const a=yi(s);r[a]||(o2(i,s),r[a]=!0)}return k.isArray(t)?t.forEach(o):o(t),this}};Ue.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A [...] -`+o.map(xg).join(` -`):" "+xg(o[0]):"as no adapter specified";throw new W("There is no suitable adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return r},adapters:xl};function Cl(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Cr(null,e)}function Sg(e){return Cl(e),e.headers=Ue.from(e.headers),e.data=bl.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Cg.getAdapter(e.ad [...] -`+o):r.stack=o}catch{}}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=zn(this.defaults,n);const{transitional:r,paramsSerializer:i,headers:o}=n;r!==void 0&&ts.assertOptions(r,{silentJSONParsing:Pt.transitional(Pt.boolean),forcedJSONParsing:Pt.transitional(Pt.boolean),clarifyTimeoutError:Pt.transitional(Pt.boolean)},!1),i!=null&&(k.isFunction(i)?n.paramsSerializer={serialize:i}:ts.assertOptions(i,{encode:Pt.function,serialize:Pt.function},!0)),n.allowAbsoluteUrls!==v [...] +)+\\(\\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 [...] * @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 xi(){return xi=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},xi.apply(this,arguments)}var tn;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(tn||(tn={}));const Pg="popstate";function V2(e){e===void 0&&(e={});function t(r,i){let{pathname:o,search:s,hash:a}=r.location;return kl("",{pathname:o,search:s,hash:a},i.state&&i.state.usr|| [...] + */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|| [...] * 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 Ci(){return Ci=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},Ci.apply(this,arguments)}const is=w.createContext(null),Mg=w.createContext(null),rn=w.createContext(null),os=w.createContext(null),Mn=w.createContext({outlet:null,matches:[],isDataRoute:!1}),$g=w.createContext(null);function oP(e,t){let{relative:n}=t===void 0?{}:t;Si()||de(!1);let{b [...] + */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 [...] * 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 Gg(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 kP(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function EP(e,t){return e.button===0&&(!t||t==="_sel [...] + */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 [...] * 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 ap=5e3;/*! + */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 [...] * 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 RT=e=>{const[t,n]=T.useState(0);return T.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 @@ -104,7 +104,7 @@ * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. - */const lp="token",PT=()=>{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(lp,r),document.cookie="_token=; expires=Sat, 01 Jan 2000 00:00:00 UTC; path=/;",r}},TT=e=>{const t=localStorage.getItem(lp)??PT();return t!==void 0&&(e.headers.Authorization=`Bearer ${t}`),e},NT=()=>{const{data:e,error:t}=nT(void 0,{enabled:!0,refetchInterval:ap});return e?S.jsx(At,{p:2,children:S.jsxs(mf, [...] + */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};/*! * 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,4 +121,21 @@ * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. - */const on=(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}`}}}),qT=ja({theme:{tokens:{colors:{suc [...] + */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, [...] + * 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}`}}}),HA=Cl({theme:{tokens:{colors:{suc [...] diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/MaintenanceEnterButton.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/MaintenanceEnterButton.tsx new file mode 100644 index 00000000000..a7d526c0095 --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/MaintenanceEnterButton.tsx @@ -0,0 +1,101 @@ +/*! + * 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 { Button, CloseButton, Dialog, IconButton, Portal, Textarea, useDisclosure } from "@chakra-ui/react"; +import { useUiServiceRequestWorkerMaintenance } from "openapi/queries"; +import { useState } from "react"; +import { HiOutlineWrenchScrewdriver } from "react-icons/hi2"; + +interface MaintenanceEnterButtonProps { + onEnterMaintenance: (toast: Record<string, string>) => void; + workerName: string; +} + +export const MaintenanceEnterButton = ({ onEnterMaintenance, workerName }: MaintenanceEnterButtonProps) => { + const { onClose, onOpen, open } = useDisclosure(); + const [comment, setComment] = useState(""); + + const enterMaintenanceMutation = useUiServiceRequestWorkerMaintenance({ + onError: (error) => { + onEnterMaintenance({ + description: `Unable to set worker ${workerName} to maintenance mode: ${error}`, + title: "Setting Maintenance Mode failed", + type: "error", + }); + }, + onSuccess: () => { + onEnterMaintenance({ + description: `Worker ${workerName} was requested to be in maintenance mode.`, + title: "Maintenance Mode activated", + type: "success", + }); + }, + }); + + const enterMaintenance = () => { + enterMaintenanceMutation.mutate({ requestBody: { maintenance_comment: comment }, workerName }); + }; + + return ( + <> + <IconButton + size="sm" + variant="ghost" + aria-label="Enter Maintenance" + title="Enter Maintenance" + onClick={onOpen} + > + <HiOutlineWrenchScrewdriver /> + </IconButton> + + <Dialog.Root onOpenChange={onClose} open={open} size="md"> + <Portal> + <Dialog.Backdrop /> + <Dialog.Positioner> + <Dialog.Content> + <Dialog.Header> + <Dialog.Title>Set maintenance for worker {workerName}</Dialog.Title> + </Dialog.Header> + <Dialog.Body> + <Textarea + placeholder="Enter maintenance comment (required)" + value={comment} + onChange={(e) => setComment(e.target.value)} + required + maxLength={1024} + size="sm" + /> + </Dialog.Body> + <Dialog.Footer> + <Dialog.ActionTrigger asChild> + <Button variant="outline">Cancel</Button> + </Dialog.ActionTrigger> + <Button onClick={enterMaintenance} disabled={!comment.trim()}> + Confirm Maintenance + </Button> + </Dialog.Footer> + <Dialog.CloseTrigger asChild> + <CloseButton size="sm" /> + </Dialog.CloseTrigger> + </Dialog.Content> + </Dialog.Positioner> + </Portal> + </Dialog.Root> + </> + ); +}; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/MaintenanceExitButton.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/MaintenanceExitButton.tsx new file mode 100644 index 00000000000..7ebb04c1572 --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/MaintenanceExitButton.tsx @@ -0,0 +1,61 @@ +/*! + * 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 { IconButton } from "@chakra-ui/react"; +import { useUiServiceExitWorkerMaintenance } from "openapi/queries"; +import { IoMdExit } from "react-icons/io"; + +interface MaintenanceExitButtonProps { + onExitMaintenance: (toast: Record<string, string>) => void; + workerName: string; +} + +export const MaintenanceExitButton = ({ onExitMaintenance, workerName }: MaintenanceExitButtonProps) => { + const exitMaintenanceMutation = useUiServiceExitWorkerMaintenance({ + onError: (error) => { + onExitMaintenance({ + description: `Unable to exit ${workerName} from maintenance mode: ${error}`, + title: "Exit Maintenance Mode failed", + type: "error", + }); + }, + onSuccess: () => { + onExitMaintenance({ + description: `Worker ${workerName} was requested to exit maintenance mode.`, + title: "Maintenance Mode deactivated", + type: "success", + }); + }, + }); + + const exitMaintenance = () => { + exitMaintenanceMutation.mutate({ workerName }); + }; + + return ( + <IconButton + size="sm" + variant="ghost" + onClick={() => exitMaintenance()} + aria-label="Exit Maintenance" + title="Exit Maintenance" + > + <IoMdExit /> + </IconButton> + ); +}; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/OperationsCell.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/OperationsCell.tsx deleted file mode 100644 index bf7fe3b4d94..00000000000 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/OperationsCell.tsx +++ /dev/null @@ -1,147 +0,0 @@ -/*! - * 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 { Box, Flex, HStack, IconButton, Textarea, VStack } from "@chakra-ui/react"; -import type { Worker } from "openapi/requests/types.gen"; -import { useState } from "react"; -import { FcCheckmark } from "react-icons/fc"; -import { HiOutlineWrenchScrewdriver } from "react-icons/hi2"; -import { ImCross } from "react-icons/im"; -import { IoMdExit } from "react-icons/io"; - -interface MaintenanceFormProps { - onSubmit: (comment: string) => void; - onCancel: () => void; -} - -const MaintenanceForm = ({ onCancel, onSubmit }: MaintenanceFormProps) => { - const [comment, setComment] = useState(""); - - const handleSubmit = () => { - if (comment.trim()) { - onSubmit(comment.trim()); - } - }; - - return ( - <VStack gap={2} align="stretch"> - <Textarea - placeholder="Enter maintenance comment (required)" - value={comment} - onChange={(e) => setComment(e.target.value)} - required - maxLength={1024} - size="sm" - /> - <HStack gap={2}> - <IconButton - size="sm" - colorScheme="green" - onClick={handleSubmit} - disabled={!comment.trim()} - aria-label="Confirm Maintenance" - title="Confirm Maintenance" - > - <FcCheckmark /> - </IconButton> - <IconButton - size="sm" - colorScheme="red" - variant="outline" - onClick={onCancel} - aria-label="Cancel" - title="Cancel" - > - <ImCross /> - </IconButton> - </HStack> - </VStack> - ); -}; - -interface OperationsCellProps { - worker: Worker; - activeMaintenanceForm: string | null; - onSetActiveMaintenanceForm: (workerName: string | null) => void; - onRequestMaintenance: (workerName: string, comment: string) => void; - onExitMaintenance: (workerName: string) => void; -} - -export const OperationsCell = ({ - activeMaintenanceForm, - onExitMaintenance, - onRequestMaintenance, - onSetActiveMaintenanceForm, - worker, -}: OperationsCellProps) => { - const workerName = worker.worker_name; - const state = worker.state; - - let cellContent = null; - - if (state === "idle" || state === "running") { - if (activeMaintenanceForm === workerName) { - cellContent = ( - <MaintenanceForm - onSubmit={(comment) => onRequestMaintenance(workerName, comment)} - onCancel={() => onSetActiveMaintenanceForm(null)} - /> - ); - } else { - cellContent = ( - <Flex justifyContent="end"> - <IconButton - size="sm" - variant="ghost" - onClick={() => onSetActiveMaintenanceForm(workerName)} - aria-label="Enter Maintenance" - title="Enter Maintenance" - > - <HiOutlineWrenchScrewdriver /> - </IconButton> - </Flex> - ); - } - } else if ( - state === "maintenance pending" || - state === "maintenance mode" || - state === "maintenance request" || - state === "offline maintenance" - ) { - cellContent = ( - <VStack gap={2} align="stretch"> - <Box fontSize="sm" whiteSpace="pre-wrap"> - {worker.maintenance_comments || "No comment"} - </Box> - <Flex justifyContent="end"> - <IconButton - size="sm" - variant="ghost" - onClick={() => onExitMaintenance(workerName)} - aria-label="Exit Maintenance" - title="Exit Maintenance" - > - <IoMdExit /> - </IconButton> - </Flex> - </VStack> - ); - } - - return cellContent; -}; diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx new file mode 100644 index 00000000000..34ce5fd6676 --- /dev/null +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx @@ -0,0 +1,65 @@ +/*! + * 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 { Box, Flex, VStack } from "@chakra-ui/react"; +import type { Worker } from "openapi/requests/types.gen"; + +import { toaster } from "src/components/ui"; + +import { MaintenanceEnterButton } from "./MaintenanceEnterButton"; +import { MaintenanceExitButton } from "./MaintenanceExitButton"; + +interface WorkerOperationsProps { + onOperations: () => void; + worker: Worker; +} + +export const WorkerOperations = ({ onOperations, worker }: WorkerOperationsProps) => { + const workerName = worker.worker_name; + const state = worker.state; + + const onWorkerChange = (toast: Record<string, string>) => { + toaster.create(toast); + onOperations(); + }; + + if (state === "idle" || state === "running") { + return ( + <Flex justifyContent="end"> + <MaintenanceEnterButton onEnterMaintenance={onWorkerChange} workerName={workerName} /> + </Flex> + ); + } else if ( + state === "maintenance pending" || + state === "maintenance mode" || + state === "maintenance request" || + state === "offline maintenance" + ) { + return ( + <VStack gap={2} align="stretch"> + <Box fontSize="sm" whiteSpace="pre-wrap"> + {worker.maintenance_comments || "No comment"} + </Box> + <Flex justifyContent="end"> + <MaintenanceExitButton onExitMaintenance={onWorkerChange} workerName={workerName} /> + </Flex> + </VStack> + ); + } + 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/createToaster.ts similarity index 84% copy from providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/index.ts copy to providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/ui/createToaster.ts index 62ff4eda180..9fa1c393c77 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/createToaster.ts @@ -16,5 +16,9 @@ * specific language governing permissions and limitations * under the License. */ +import { createToaster } from "@chakra-ui/react"; -export * from "./Alert"; +export const toaster = createToaster({ + pauseOnPageIdle: true, + placement: "bottom-end", +}); 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 62ff4eda180..95ef0889d86 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 @@ -18,3 +18,4 @@ */ export * from "./Alert"; +export * from "./createToaster"; 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 ff6a1745532..0351b2d608e 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 @@ -17,15 +17,10 @@ * under the License. */ import { Box, Table } from "@chakra-ui/react"; -import { - useUiServiceWorker, - useUiServiceRequestWorkerMaintenance, - useUiServiceExitWorkerMaintenance, -} from "openapi/queries"; -import { useState } from "react"; +import { useUiServiceWorker } from "openapi/queries"; import { ErrorAlert } from "src/components/ErrorAlert"; -import { OperationsCell } from "src/components/OperationsCell"; +import { WorkerOperations } from "src/components/WorkerOperations"; import { WorkerStateBadge } from "src/components/WorkerStateBadge"; import { autoRefreshInterval } from "src/utils"; @@ -34,43 +29,6 @@ export const WorkerPage = () => { enabled: true, refetchInterval: autoRefreshInterval, }); - const [activeMaintenanceForm, setActiveMaintenanceForm] = useState<string | null>(null); - - const requestMaintenanceMutation = useUiServiceRequestWorkerMaintenance({ - onError: (error) => { - console.error("Error requesting maintenance:", error); - alert(`Error requesting maintenance: ${error}`); - }, - onSuccess: () => { - console.log("Maintenance request successful"); - setActiveMaintenanceForm(null); - refetch(); - }, - }); - - const exitMaintenanceMutation = useUiServiceExitWorkerMaintenance({ - onError: (error) => { - console.error("Error exiting maintenance:", error); - alert(`Error exiting maintenance: ${error}`); - }, - onSuccess: () => { - console.log("Exit maintenance successful"); - refetch(); - }, - }); - - const requestMaintenance = (workerName: string, comment: string) => { - console.log(`Requesting maintenance for worker: ${workerName}, comment: ${comment}`); - requestMaintenanceMutation.mutate({ - requestBody: { maintenance_comment: comment }, - workerName, - }); - }; - - const exitMaintenance = (workerName: string) => { - console.log(`Exiting maintenance for worker: ${workerName}`); - exitMaintenanceMutation.mutate({ workerName }); - }; // TODO to make it proper // Use DataTable as component from Airflow-Core UI @@ -129,13 +87,7 @@ export const WorkerPage = () => { )} </Table.Cell> <Table.Cell> - <OperationsCell - worker={worker} - activeMaintenanceForm={activeMaintenanceForm} - onSetActiveMaintenanceForm={setActiveMaintenanceForm} - onRequestMaintenance={requestMaintenance} - onExitMaintenance={exitMaintenance} - /> + <WorkerOperations worker={worker} onOperations={refetch} /> </Table.Cell> </Table.Row> ))} diff --git a/providers/edge3/src/airflow/providers/edge3/plugins/www/vite.config.ts b/providers/edge3/src/airflow/providers/edge3/plugins/www/vite.config.ts index e364349872f..37ebf1eca9e 100644 --- a/providers/edge3/src/airflow/providers/edge3/plugins/www/vite.config.ts +++ b/providers/edge3/src/airflow/providers/edge3/plugins/www/vite.config.ts @@ -42,6 +42,7 @@ export default defineConfig(({ command }) => { output: { globals: { react: "React", + "react-dom": "ReactDOM", "react/jsx-runtime": "ReactJSXRuntime", }, }, diff --git a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py index 11027310768..0b452691765 100644 --- a/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py +++ b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py @@ -19,7 +19,7 @@ from __future__ import annotations from datetime import datetime -from fastapi import Depends, HTTPException +from fastapi import Depends, HTTPException, status from sqlalchemy import select from airflow.api_fastapi.auth.managers.models.resource_details import AccessView @@ -122,7 +122,9 @@ def request_worker_maintenance( worker_query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name) worker = session.scalar(worker_query) if not worker: - raise HTTPException(status_code=404, detail=f"Worker {worker_name} not found") + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Worker {worker_name} not found") + if not maintenance_request.maintenance_comment: + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Maintenance comment is required") # Format the comment with timestamp and username (username will be added by plugin layer) formatted_comment = f"[{datetime.now().strftime('%Y-%m-%d %H:%M')}] - {user.get_name()} put node into maintenance mode\nComment: {maintenance_request.maintenance_comment}" @@ -130,7 +132,7 @@ def request_worker_maintenance( try: request_maintenance(worker_name, formatted_comment, session=session) except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) @ui_router.delete( @@ -148,9 +150,9 @@ def exit_worker_maintenance( worker_query = select(EdgeWorkerModel).where(EdgeWorkerModel.worker_name == worker_name) worker = session.scalar(worker_query) if not worker: - raise HTTPException(status_code=404, detail=f"Worker {worker_name} not found") + raise HTTPException(status.HTTP_404_NOT_FOUND, detail=f"Worker {worker_name} not found") try: exit_maintenance(worker_name, session=session) except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) + raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e)) diff --git a/providers/edge3/www-hash.txt b/providers/edge3/www-hash.txt index 0e053e9262d..e77d9fccb14 100644 --- a/providers/edge3/www-hash.txt +++ b/providers/edge3/www-hash.txt @@ -1 +1 @@ -ed85f7d6558cdc8c5edf498fcd96e187484327b877e021314f94ae640d4634f2 +d3d458dbc15ae801bb6bf8f128e38a1a65fd81e3ecc8c87dd30a79acc9a4041a