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 acc17657858 Add worker maintenance mode functionality to Edge3 
provider UI (#55301)
acc17657858 is described below

commit acc17657858c950cfe9046c06167a9cdb200477b
Author: Dheeraj Turaga <dheerajtur...@gmail.com>
AuthorDate: Sun Sep 7 15:34:24 2025 -0500

    Add worker maintenance mode functionality to Edge3 provider UI (#55301)
    
    * Add worker maintenance mode functionality to Edge3 provider UI
    
      - Add POST /edge_worker/ui/worker/{worker_name}/maintenance endpoint to 
request maintenance mode
      - Add DELETE /edge_worker/ui/worker/{worker_name}/maintenance endpoint to 
exit maintenance mode
      - Add maintenance request form with required comment field (max 1024 
chars)
      - Add maintenance state handling for all maintenance-related worker states
      - Add proper error handling and user feedback for maintenance operations
      - Add worker existence validation and database session management
      - Remove authentication dependencies for UI maintenance endpoints
      - Update frontend to show maintenance controls based on worker state
      - Format maintenance comments with timestamps for audit trail
    
    * Move MaintenanceRequest to datamodels_ui.py for better code organization
    Remove explicit session.commit() calls - let SQLAlchemy handle transactions
    
    * refactor: extract OperationsCell into separate component
    
      - Move MaintenanceRequest class to datamodels_ui.py for better 
organization
      - Remove explicit session.commit() calls, let SQLAlchemy handle 
transactions
      - Extract OperationsCell and MaintenanceForm into separate component file
      - Clean up unused imports in WorkerPage.tsx
      - Improve code modularity and maintainability
    
    * refactor: replace manual fetch with generated axios wrappers for 
maintenance operations
    
      - Use useUiServiceRequestWorkerMaintenance and 
useUiServiceExitWorkerMaintenance hooks
      - Remove manual fetch implementation with CSRF token handling
      - Simplify error handling using React Query's onSuccess/onError callbacks
      - Improve type safety with generated API client
      - Reduce bundle size and code complexity
      - Maintain same functionality with cleaner, more maintainable code
    
    * Use react icons instead of old school buttons
    
    * Add tooltips to buttons
    
    * Have exit button not if it is in maintenance exit already
    
    * Add authentication to (new) API endpoints
    
    ---------
    
    Co-authored-by: Jens Scheffler <jsche...@apache.org>
---
 .../providers/edge3/openapi/v2-edge-generated.yaml |  78 +++++++++++
 .../providers/edge3/plugins/www/dist/main.umd.cjs  |  34 ++---
 .../plugins/www/openapi-gen/queries/common.ts      |   2 +
 .../plugins/www/openapi-gen/queries/queries.ts     |  14 +-
 .../www/openapi-gen/requests/schemas.gen.ts        |  14 ++
 .../www/openapi-gen/requests/services.gen.ts       |  47 ++++++-
 .../plugins/www/openapi-gen/requests/types.gen.ts  |  51 +++++++
 .../plugins/www/src/components/OperationsCell.tsx  | 147 +++++++++++++++++++++
 .../edge3/plugins/www/src/pages/WorkerPage.tsx     |  58 +++++++-
 .../providers/edge3/worker_api/datamodels_ui.py    |   6 +
 .../providers/edge3/worker_api/routes/ui.py        |  60 ++++++++-
 providers/edge3/www-hash.txt                       |   2 +-
 12 files changed, 486 insertions(+), 27 deletions(-)

diff --git 
a/providers/edge3/src/airflow/providers/edge3/openapi/v2-edge-generated.yaml 
b/providers/edge3/src/airflow/providers/edge3/openapi/v2-edge-generated.yaml
index 6eabd5901f9..4619fc22235 100644
--- a/providers/edge3/src/airflow/providers/edge3/openapi/v2-edge-generated.yaml
+++ b/providers/edge3/src/airflow/providers/edge3/openapi/v2-edge-generated.yaml
@@ -586,6 +586,73 @@ paths:
       security:
       - OAuth2PasswordBearer: []
       - HTTPBearer: []
+  /edge_worker/ui/worker/{worker_name}/maintenance:
+    post:
+      tags:
+      - UI
+      summary: Request Worker Maintenance
+      description: Put a worker into maintenance mode.
+      operationId: request_worker_maintenance
+      security:
+      - OAuth2PasswordBearer: []
+      - HTTPBearer: []
+      parameters:
+      - name: worker_name
+        in: path
+        required: true
+        schema:
+          type: string
+          title: Worker Name
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/MaintenanceRequest'
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                type: 'null'
+                title: Response Request Worker Maintenance
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+    delete:
+      tags:
+      - UI
+      summary: Exit Worker Maintenance
+      description: Exit a worker from maintenance mode.
+      operationId: exit_worker_maintenance
+      security:
+      - OAuth2PasswordBearer: []
+      - HTTPBearer: []
+      parameters:
+      - name: worker_name
+        in: path
+        required: true
+        schema:
+          type: string
+          title: Worker Name
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                type: 'null'
+                title: Response Exit Worker Maintenance
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
 components:
   schemas:
     BundleInfo:
@@ -794,6 +861,17 @@ components:
       - total_entries
       title: JobCollectionResponse
       description: Job Collection serializer.
+    MaintenanceRequest:
+      properties:
+        maintenance_comment:
+          type: string
+          title: Maintenance Comment
+          description: Comment describing the maintenance reason.
+      type: object
+      required:
+      - maintenance_comment
+      title: MaintenanceRequest
+      description: Request body for maintenance operations.
     PushLogsBody:
       properties:
         log_chunk_time:
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 8a897c96b35..871159e89c8 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(A,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):(A=typeof 
globalThis<"u"?globalThis:A||self,A.AirflowPlugin=Y(A.React))})(this,function(A){"use
 strict";var dT=Object.defineProperty;var Vm=A=>{throw TypeError(A)};var 
hT=(A,Y,w)=>Y in 
A?dT(A,Y,{enumerable:!0,configurable:!0,writable:!0,value:w}):A[Y]=w;var 
qe=(A,Y,w)=>hT(A,typeof Y!="symbol"?Y+"":Y,w),bl= [...]
+(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= [...]
  * @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 
Mm=A,Bm=Symbol.for("react.element"),$m=Symbol.for("react.fragment"),jm=Object.prototype.hasOwnProperty,Wm=Mm.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Hm={key:!0,ref:!0,__self:!0,__source:!0};function
 Ol(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)jm.call(t,r)&&!Hm.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 
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 [...]
  * 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 ye=typeof 
Symbol=="function"&&Symbol.for,hs=ye?Symbol.for("react.element"):60103,fs=ye?Symbol.for("react.portal"):60106,Ni=ye?Symbol.for("react.fragment"):60107,_i=ye?Symbol.for("react.strict_mode"):60108,Ai=ye?Symbol.for("react.profiler"):60114,Vi=ye?Symbol.for("react.provider"):60109,Li=ye?Symbol.for("react.context"):60110,gs=ye?Symbol.for("react.async_mode"):60111,Fi=ye?Symbol.for("react.concurrent_mode"):60111,zi=ye?Symbol.for("react.forward_ref"):60112,Di=ye?Symbol.for("react
 [...]
+ */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
 [...]
       <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 
tC(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
 
nC(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 
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  [...]
 )+\\(\\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")}),ew=Kd("width"),tw=Kd("height"),Xd=e=>({isMin:th(e.minMax,e.maxMin,e.min),isMax:th(e.maxMin,e.minMax,e.max)}),{isMin:Na,isMax:Yd}=Xd(ew),{isMin:_a,isMax:Qd}=Xd(tw),Jd=/print/i,Zd=/^print$/i,nw=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,rw=/(\d)/,ti=Number.MAX_VALUE,iw={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
 eh(e){const t=nw.exec(e)||(Na(e)||_a(e)?rw.exec(e):null);if(!t)return 
ti;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]&&bR[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
 "+r:r)}),t},_f=Symbol("internals");function ui(e){return 
e&&String(e).trim().toLowerCase()}function Bo(e){return 
e===!1||e==null?e:k.isArray(e)?e.map(Bo):String(e)}function xR(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 SR=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[_f]=this[_f]={accessors:{}}).accessors,i=this.prototype;function 
o(s){const a=ui(s);r[a]||(wR(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(Hf).join(`
-`):" "+Hf(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:ol};function 
sl(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
 new dr(null,e)}function Gf(e){return 
sl(e),e.headers=Ue.from(e.headers),e.data=rl.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Uf.getAdapter(e.ad
 [...]
-`+o):r.stack=o}catch{}}throw r}}_request(t,n){typeof 
t=="string"?(n=n||{},n.url=t):n=t||{},n=Pn(this.defaults,n);const{transitional:r,paramsSerializer:i,headers:o}=n;r!==void
 
0&&Uo.assertOptions(r,{silentJSONParsing:It.transitional(It.boolean),forcedJSONParsing:It.transitional(It.boolean),clarifyTimeoutError:It.transitional(It.boolean)},!1),i!=null&&(k.isFunction(i)?n.paramsSerializer={serialize:i}:Uo.assertOptions(i,{encode:It.function,serialize:It.function},!0)),n.allowAbsoluteUrls!==v
 [...]
+)+\\(\\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
 [...]
  * @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 di(){return 
di=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},di.apply(this,arguments)}var 
Jt;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(Jt||(Jt={}));const
 Zf="popstate";function JR(e){e===void 0&&(e={});function 
t(r,i){let{pathname:o,search:s,hash:a}=r.location;return 
cl("",{pathname:o,search:s,hash:a},i.state&&i.state.usr|| [...]
+ */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|| [...]
  * 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 hi(){return 
hi=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},hi.apply(this,arguments)}const 
Ko=w.createContext(null),cg=w.createContext(null),en=w.createContext(null),Xo=w.createContext(null),Nn=w.createContext({outlet:null,matches:[],isDataRoute:!1}),ug=w.createContext(null);function
 wP(e,t){let{relative:n}=t===void 0?{}:t;fi()||ue(!1);let{b [...]
+ */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 [...]
  * 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 Qo(){return 
Qo=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},Qo.apply(this,arguments)}function mg(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 jP(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function 
WP(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 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 [...]
  * 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 Vg=5e3;/*!
+ */const ap=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
@@ -87,7 +87,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const H2=e=>{const[t,n]=A.useState(0);return 
A.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 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};/*!
  * 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 Lg="token",U2=()=>{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(Lg,r),document.cookie="_token=; expires=Sat, 01 
Jan 2000 00:00:00 UTC; path=/;",r}},G2=e=>{const 
t=localStorage.getItem(Lg)??U2();return t!==void 
0&&(e.headers.Authorization=`Bearer 
${t}`),e},q2=()=>{const{data:e,error:t}=x2(void 
0,{enabled:!0,refetchInterval:Vg});return e?C.jsx($t,{p:2,children:C.jsxs(Hh, 
[...]
+ */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, 
[...]
  * 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,4 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const 
tn=(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}`}}}),oT=Ia({theme:{tokens:{colors:{suc
 [...]
+ */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
 [...]
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts
index a6bd6241897..43908424fb0 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/common.ts
@@ -28,6 +28,8 @@ export const UseUiServiceJobsKeyFn = (queryKey?: 
Array<unknown>) => [useUiServic
 export type JobsServiceFetchMutationResult = Awaited<ReturnType<typeof 
JobsService.fetch>>;
 export type LogsServicePushLogsMutationResult = Awaited<ReturnType<typeof 
LogsService.pushLogs>>;
 export type WorkerServiceRegisterMutationResult = Awaited<ReturnType<typeof 
WorkerService.register>>;
+export type UiServiceRequestWorkerMaintenanceMutationResult = 
Awaited<ReturnType<typeof UiService.requestWorkerMaintenance>>;
 export type JobsServiceStateMutationResult = Awaited<ReturnType<typeof 
JobsService.state>>;
 export type WorkerServiceSetStateMutationResult = Awaited<ReturnType<typeof 
WorkerService.setState>>;
 export type WorkerServiceUpdateQueuesMutationResult = 
Awaited<ReturnType<typeof WorkerService.updateQueues>>;
+export type UiServiceExitWorkerMaintenanceMutationResult = 
Awaited<ReturnType<typeof UiService.exitWorkerMaintenance>>;
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts
index cb94b352977..c23397614c2 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/queries/queries.ts
@@ -2,7 +2,7 @@
 
 import { UseMutationOptions, UseQueryOptions, useMutation, useQuery } from 
"@tanstack/react-query";
 import { JobsService, LogsService, MonitorService, UiService, WorkerService } 
from "../requests/services.gen";
-import { PushLogsBody, TaskInstanceState, WorkerQueueUpdateBody, 
WorkerQueuesBody, WorkerStateBody } from "../requests/types.gen";
+import { MaintenanceRequest, PushLogsBody, TaskInstanceState, 
WorkerQueueUpdateBody, WorkerQueuesBody, WorkerStateBody } from 
"../requests/types.gen";
 import * as Common from "./common";
 export const useLogsServiceLogfilePath = <TData = 
Common.LogsServiceLogfilePathDefaultResponse, TError = unknown, TQueryKey 
extends Array<unknown> = unknown[]>({ authorization, dagId, mapIndex, runId, 
taskId, tryNumber }: {
   authorization: string;
@@ -50,6 +50,13 @@ export const useWorkerServiceRegister = <TData = 
Common.WorkerServiceRegisterMut
   requestBody: WorkerStateBody;
   workerName: string;
 }, TContext>({ mutationFn: ({ authorization, requestBody, workerName }) => 
WorkerService.register({ authorization, requestBody, workerName }) as unknown 
as Promise<TData>, ...options });
+export const useUiServiceRequestWorkerMaintenance = <TData = 
Common.UiServiceRequestWorkerMaintenanceMutationResult, TError = unknown, 
TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
+  requestBody: MaintenanceRequest;
+  workerName: string;
+}, TContext>, "mutationFn">) => useMutation<TData, TError, {
+  requestBody: MaintenanceRequest;
+  workerName: string;
+}, TContext>({ mutationFn: ({ requestBody, workerName }) => 
UiService.requestWorkerMaintenance({ requestBody, workerName }) as unknown as 
Promise<TData>, ...options });
 export const useJobsServiceState = <TData = 
Common.JobsServiceStateMutationResult, TError = unknown, TContext = 
unknown>(options?: Omit<UseMutationOptions<TData, TError, {
   authorization: string;
   dagId: string;
@@ -85,3 +92,8 @@ export const useWorkerServiceUpdateQueues = <TData = 
Common.WorkerServiceUpdateQ
   requestBody: WorkerQueueUpdateBody;
   workerName: string;
 }, TContext>({ mutationFn: ({ authorization, requestBody, workerName }) => 
WorkerService.updateQueues({ authorization, requestBody, workerName }) as 
unknown as Promise<TData>, ...options });
+export const useUiServiceExitWorkerMaintenance = <TData = 
Common.UiServiceExitWorkerMaintenanceMutationResult, TError = unknown, TContext 
= unknown>(options?: Omit<UseMutationOptions<TData, TError, {
+  workerName: string;
+}, TContext>, "mutationFn">) => useMutation<TData, TError, {
+  workerName: string;
+}, TContext>({ mutationFn: ({ workerName }) => 
UiService.exitWorkerMaintenance({ workerName }) as unknown as Promise<TData>, 
...options });
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts
index f542a791a9f..d605e434669 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/schemas.gen.ts
@@ -252,6 +252,20 @@ export const $JobCollectionResponse = {
     description: 'Job Collection serializer.'
 } as const;
 
+export const $MaintenanceRequest = {
+    properties: {
+        maintenance_comment: {
+            type: 'string',
+            title: 'Maintenance Comment',
+            description: 'Comment describing the maintenance reason.'
+        }
+    },
+    type: 'object',
+    required: ['maintenance_comment'],
+    title: 'MaintenanceRequest',
+    description: 'Request body for maintenance operations.'
+} as const;
+
 export const $PushLogsBody = {
     properties: {
         log_chunk_time: {
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts
index 4d7a7b0b951..728a1ae5507 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/services.gen.ts
@@ -3,7 +3,7 @@
 import type { CancelablePromise } from './core/CancelablePromise';
 import { OpenAPI } from './core/OpenAPI';
 import { request as __request } from './core/request';
-import type { FetchData, FetchResponse, StateData, StateResponse, 
LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse, 
RegisterData, RegisterResponse, SetStateData, SetStateResponse, 
UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerResponse, 
JobsResponse } from './types.gen';
+import type { FetchData, FetchResponse, StateData, StateResponse, 
LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse, 
RegisterData, RegisterResponse, SetStateData, SetStateResponse, 
UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerResponse, 
JobsResponse, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse, 
ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse } from './types.gen';
 
 export class JobsService {
     /**
@@ -286,4 +286,49 @@ export class UiService {
         });
     }
     
+    /**
+     * Request Worker Maintenance
+     * Put a worker into maintenance mode.
+     * @param data The data for the request.
+     * @param data.workerName
+     * @param data.requestBody
+     * @returns null Successful Response
+     * @throws ApiError
+     */
+    public static requestWorkerMaintenance(data: 
RequestWorkerMaintenanceData): 
CancelablePromise<RequestWorkerMaintenanceResponse> {
+        return __request(OpenAPI, {
+            method: 'POST',
+            url: '/edge_worker/ui/worker/{worker_name}/maintenance',
+            path: {
+                worker_name: data.workerName
+            },
+            body: data.requestBody,
+            mediaType: 'application/json',
+            errors: {
+                422: 'Validation Error'
+            }
+        });
+    }
+    
+    /**
+     * Exit Worker Maintenance
+     * Exit a worker from maintenance mode.
+     * @param data The data for the request.
+     * @param data.workerName
+     * @returns null Successful Response
+     * @throws ApiError
+     */
+    public static exitWorkerMaintenance(data: ExitWorkerMaintenanceData): 
CancelablePromise<ExitWorkerMaintenanceResponse> {
+        return __request(OpenAPI, {
+            method: 'DELETE',
+            url: '/edge_worker/ui/worker/{worker_name}/maintenance',
+            path: {
+                worker_name: data.workerName
+            },
+            errors: {
+                422: 'Validation Error'
+            }
+        });
+    }
+    
 }
\ No newline at end of file
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts
index 53198713128..61bf7663a9a 100644
--- 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/openapi-gen/requests/types.gen.ts
@@ -126,6 +126,16 @@ export type JobCollectionResponse = {
     total_entries: number;
 };
 
+/**
+ * Request body for maintenance operations.
+ */
+export type MaintenanceRequest = {
+    /**
+     * Comment describing the maintenance reason.
+     */
+    maintenance_comment: string;
+};
+
 /**
  * Incremental new log content from worker.
  */
@@ -460,6 +470,19 @@ export type WorkerResponse = WorkerCollectionResponse;
 
 export type JobsResponse = JobCollectionResponse;
 
+export type RequestWorkerMaintenanceData = {
+    requestBody: MaintenanceRequest;
+    workerName: string;
+};
+
+export type RequestWorkerMaintenanceResponse = null;
+
+export type ExitWorkerMaintenanceData = {
+    workerName: string;
+};
+
+export type ExitWorkerMaintenanceResponse = null;
+
 export type $OpenApiTs = {
     '/edge_worker/v1/jobs/fetch/{worker_name}': {
         post: {
@@ -652,4 +675,32 @@ export type $OpenApiTs = {
             };
         };
     };
+    '/edge_worker/ui/worker/{worker_name}/maintenance': {
+        post: {
+            req: RequestWorkerMaintenanceData;
+            res: {
+                /**
+                 * Successful Response
+                 */
+                200: null;
+                /**
+                 * Validation Error
+                 */
+                422: HTTPValidationError;
+            };
+        };
+        delete: {
+            req: ExitWorkerMaintenanceData;
+            res: {
+                /**
+                 * Successful Response
+                 */
+                200: null;
+                /**
+                 * Validation Error
+                 */
+                422: HTTPValidationError;
+            };
+        };
+    };
 };
\ No newline at end of file
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
new file mode 100644
index 00000000000..bf7fe3b4d94
--- /dev/null
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/OperationsCell.tsx
@@ -0,0 +1,147 @@
+/*!
+ * 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/pages/WorkerPage.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/pages/WorkerPage.tsx
index 2af2adb6ecf..ff6a1745532 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,21 +17,63 @@
  * under the License.
  */
 import { Box, Table } from "@chakra-ui/react";
-import { useUiServiceWorker } from "openapi/queries";
+import {
+  useUiServiceWorker,
+  useUiServiceRequestWorkerMaintenance,
+  useUiServiceExitWorkerMaintenance,
+} from "openapi/queries";
+import { useState } from "react";
 
 import { ErrorAlert } from "src/components/ErrorAlert";
+import { OperationsCell } from "src/components/OperationsCell";
 import { WorkerStateBadge } from "src/components/WorkerStateBadge";
 import { autoRefreshInterval } from "src/utils";
 
 export const WorkerPage = () => {
-  const { data, error } = useUiServiceWorker(undefined, {
+  const { data, error, refetch } = useUiServiceWorker(undefined, {
     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
-  // Add actions for maintenance / delete of orphan worker
   // Add sorting
   // Add filtering
   // Add links to see jobs on worker
@@ -86,7 +128,15 @@ export const WorkerPage = () => {
                     "N/A"
                   )}
                 </Table.Cell>
-                <Table.Cell>{worker.maintenance_comments}</Table.Cell>
+                <Table.Cell>
+                  <OperationsCell
+                    worker={worker}
+                    activeMaintenanceForm={activeMaintenanceForm}
+                    onSetActiveMaintenanceForm={setActiveMaintenanceForm}
+                    onRequestMaintenance={requestMaintenance}
+                    onExitMaintenance={exitMaintenance}
+                  />
+                </Table.Cell>
               </Table.Row>
             ))}
           </Table.Body>
diff --git 
a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py 
b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py
index f2e270809f2..70aa651d50c 100644
--- a/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py
+++ b/providers/edge3/src/airflow/providers/edge3/worker_api/datamodels_ui.py
@@ -65,3 +65,9 @@ class JobCollectionResponse(BaseModel):
 
     jobs: list[Job]
     total_entries: int
+
+
+class MaintenanceRequest(BaseModel):
+    """Request body for maintenance operations."""
+
+    maintenance_comment: Annotated[str, Field(description="Comment describing 
the maintenance reason.")]
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 620c0df8b25..11027310768 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
@@ -17,18 +17,21 @@
 
 from __future__ import annotations
 
-from fastapi import Depends
+from datetime import datetime
+
+from fastapi import Depends, HTTPException
 from sqlalchemy import select
 
 from airflow.api_fastapi.auth.managers.models.resource_details import 
AccessView
 from airflow.api_fastapi.common.db.common import SessionDep  # noqa: TC001
 from airflow.api_fastapi.common.router import AirflowRouter
-from airflow.api_fastapi.core_api.security import requires_access_view
+from airflow.api_fastapi.core_api.security import GetUserDep, 
requires_access_view
 from airflow.providers.edge3.models.edge_job import EdgeJobModel
-from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel
+from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, 
exit_maintenance, request_maintenance
 from airflow.providers.edge3.worker_api.datamodels_ui import (
     Job,
     JobCollectionResponse,
+    MaintenanceRequest,
     Worker,
     WorkerCollectionResponse,
 )
@@ -100,3 +103,54 @@ def jobs(
         jobs=result,
         total_entries=len(result),
     )
+
+
+@ui_router.post(
+    "/worker/{worker_name}/maintenance",
+    dependencies=[
+        Depends(requires_access_view(access_view=AccessView.JOBS)),
+    ],
+)
+def request_worker_maintenance(
+    worker_name: str,
+    maintenance_request: MaintenanceRequest,
+    session: SessionDep,
+    user: GetUserDep,
+) -> None:
+    """Put a worker into maintenance mode."""
+    # Check if worker exists first
+    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")
+
+    # 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}"
+
+    try:
+        request_maintenance(worker_name, formatted_comment, session=session)
+    except Exception as e:
+        raise HTTPException(status_code=400, detail=str(e))
+
+
+@ui_router.delete(
+    "/worker/{worker_name}/maintenance",
+    dependencies=[
+        Depends(requires_access_view(access_view=AccessView.JOBS)),
+    ],
+)
+def exit_worker_maintenance(
+    worker_name: str,
+    session: SessionDep,
+) -> None:
+    """Exit a worker from maintenance mode."""
+    # Check if worker exists first
+    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")
+
+    try:
+        exit_maintenance(worker_name, session=session)
+    except Exception as e:
+        raise HTTPException(status_code=400, detail=str(e))
diff --git a/providers/edge3/www-hash.txt b/providers/edge3/www-hash.txt
index 41772bc2d03..0e053e9262d 100644
--- a/providers/edge3/www-hash.txt
+++ b/providers/edge3/www-hash.txt
@@ -1 +1 @@
-d780858c70355a081b2486871c18d5f3c579f04129b3b15e7c6f691a1a3a2c12
+ed85f7d6558cdc8c5edf498fcd96e187484327b877e021314f94ae640d4634f2


Reply via email to