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 8a51c7351d5 feat: Add shutdown button for edge workers with 
confirmation dialog (#55513)
8a51c7351d5 is described below

commit 8a51c7351d5ca99017ff49d9543490660cbf03a9
Author: Dheeraj Turaga <[email protected]>
AuthorDate: Thu Sep 11 11:58:31 2025 -0500

    feat: Add shutdown button for edge workers with confirmation dialog (#55513)
    
    - Add shutdown API endpoint at /edge_worker/ui/worker/{worker_name}/shutdown
      - Create WorkerShutdownButton component with FaPowerOff icon in red
      - Implement confirmation dialog warning about worker termination
      - Integrate with existing request_shutdown function from edge_worker 
models
      - Add shutdown button to WorkerOperations for idle/running/maintenance 
workers
      - Update OpenAPI spec and regenerate TypeScript client code
      - Position shutdown button after maintenance operations for better UX
---
 .../providers/edge3/openapi/v2-edge-generated.yaml |  31 +++++++
 .../providers/edge3/plugins/www/dist/main.umd.cjs  |  34 +++----
 .../plugins/www/openapi-gen/queries/common.ts      |   1 +
 .../plugins/www/openapi-gen/queries/queries.ts     |   5 +
 .../www/openapi-gen/requests/services.gen.ts       |  23 ++++-
 .../plugins/www/openapi-gen/requests/types.gen.ts  |  21 +++++
 .../www/src/components/WorkerOperations.tsx        |   7 +-
 .../www/src/components/WorkerShutdownButton.tsx    | 102 +++++++++++++++++++++
 .../providers/edge3/worker_api/routes/ui.py        |  30 +++++-
 providers/edge3/www-hash.txt                       |   2 +-
 10 files changed, 234 insertions(+), 22 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 4619fc22235..30662aee091 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
@@ -653,6 +653,37 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
+  /edge_worker/ui/worker/{worker_name}/shutdown:
+    post:
+      tags:
+      - UI
+      summary: Request Worker Shutdown
+      description: Request shutdown of a worker.
+      operationId: request_worker_shutdown
+      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 Request Worker Shutdown
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
 components:
   schemas:
     BundleInfo:
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 302b9e9cf6a..edb5b6e3252 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(w,ne){typeof exports=="object"&&typeof 
module<"u"?module.exports=ne(require("react"),require("react-dom")):typeof 
define=="function"&&define.amd?define(["react","react-dom"],ne):(w=typeof 
globalThis<"u"?globalThis:w||self,w.AirflowPlugin=ne(w.React,w.ReactDOM))})(this,function(w,ne){"use
 strict";var LN=Object.defineProperty;var lv=w=>{throw TypeError(w)};var 
FN=(w,ne,be)=>ne in 
w?LN(w,ne,{enumerable:!0,configurable:!0,writable:!0,value:be}):w[ne]=be;var 
Ze=(w,ne,be)=>FN(w,typeo [...]
+(function(w,ne){typeof exports=="object"&&typeof 
module<"u"?module.exports=ne(require("react"),require("react-dom")):typeof 
define=="function"&&define.amd?define(["react","react-dom"],ne):(w=typeof 
globalThis<"u"?globalThis:w||self,w.AirflowPlugin=ne(w.React,w.ReactDOM))})(this,function(w,ne){"use
 strict";var DN=Object.defineProperty;var cv=w=>{throw TypeError(w)};var 
MN=(w,ne,ye)=>ne in 
w?DN(w,ne,{enumerable:!0,configurable:!0,writable:!0,value:ye}):w[ne]=ye;var 
Ze=(w,ne,ye)=>MN(w,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 
fv=w,gv=Symbol.for("react.element"),pv=Symbol.for("react.fragment"),mv=Object.prototype.hasOwnProperty,vv=fv.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,bv={key:!0,ref:!0,__self:!0,__source:!0};function
 Ac(e,t,n){var r,o={},i=null,s=null;n!==void 0&&(i=""+n),t.key!==void 
0&&(i=""+t.key),t.ref!==void 0&&(s=t.ref);for(r in 
t)mv.call(t,r)&&!bv.hasOwnProperty(r)&&(o[r]=t[r]);if(e&&e.defaultProps)for(r 
in t=e.defaultProps,t)o[r]===void 0&&(o[r]=t[r]);return{$$t [...]
+ */var 
gv=w,pv=Symbol.for("react.element"),mv=Symbol.for("react.fragment"),vv=Object.prototype.hasOwnProperty,bv=gv.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,yv={key:!0,ref:!0,__self:!0,__source:!0};function
 Kc(e,t,n){var r,o={},i=null,s=null;n!==void 0&&(i=""+n),t.key!==void 
0&&(i=""+t.key),t.ref!==void 0&&(s=t.ref);for(r in 
t)vv.call(t,r)&&!yv.hasOwnProperty(r)&&(o[r]=t[r]);if(e&&e.defaultProps)for(r 
in t=e.defaultProps,t)o[r]===void 0&&(o[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 Oe=typeof 
Symbol=="function"&&Symbol.for,sa=Oe?Symbol.for("react.element"):60103,aa=Oe?Symbol.for("react.portal"):60106,oi=Oe?Symbol.for("react.fragment"):60107,ii=Oe?Symbol.for("react.strict_mode"):60108,si=Oe?Symbol.for("react.profiler"):60114,ai=Oe?Symbol.for("react.provider"):60109,li=Oe?Symbol.for("react.context"):60110,la=Oe?Symbol.for("react.async_mode"):60111,ci=Oe?Symbol.for("react.concurrent_mode"):60111,ui=Oe?Symbol.for("react.forward_ref"):60112,di=Oe?Symbol.for("react
 [...]
+ */var Oe=typeof 
Symbol=="function"&&Symbol.for,aa=Oe?Symbol.for("react.element"):60103,la=Oe?Symbol.for("react.portal"):60106,li=Oe?Symbol.for("react.fragment"):60107,ci=Oe?Symbol.for("react.strict_mode"):60108,ui=Oe?Symbol.for("react.profiler"):60114,di=Oe?Symbol.for("react.provider"):60109,hi=Oe?Symbol.for("react.context"):60110,ca=Oe?Symbol.for("react.async_mode"):60111,fi=Oe?Symbol.for("react.concurrent_mode"):60111,gi=Oe?Symbol.for("react.forward_ref"):60112,pi=Oe?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 
cw(e){if(!(!e||e.ownerDocument.activeElement!==e))try{const{selectionStart:t,selectionEnd:n,value:r}=e,o=r.substring(0,t),i=r.substring(n);return{start:t,end:n,value:r,beforeTxt:o,afterTxt:i}}catch{}}function
 
uw(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:o="",start:i}=t;let
 s=n.length;if(n.endsWith(o))s=n.length-o.length;else  [...]
+      </svg>`,n.body.appendChild(r)};function 
uw(e){if(!(!e||e.ownerDocument.activeElement!==e))try{const{selectionStart:t,selectionEnd:n,value:r}=e,o=r.substring(0,t),i=r.substring(n);return{start:t,end:n,value:r,beforeTxt:o,afterTxt:i}}catch{}}function
 
dw(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:o="",start:i}=t;let
 s=n.length;if(n.endsWith(o))s=n.length-o.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")}),pE=_f("width"),mE=_f("height"),Af=e=>({isMin:Mf(e.minMax,e.maxMin,e.min),isMax:Mf(e.maxMin,e.minMax,e.max)}),{isMin:Al,isMax:Vf}=Af(pE),{isMin:Vl,isMax:Lf}=Af(mE),Ff=/print/i,Df=/^print$/i,vE=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,bE=/(\d)/,Io=Number.MAX_VALUE,yE={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
 zf(e){const t=vE.exec(e)||(Al(e)||Vl(e)?bE.exec(e):null);if(!t)return 
Io;if(t[0]==="0")return 0; [...]
-`).forEach(function(s){o=s.indexOf(":"),n=s.substring(0,o).trim().toLowerCase(),r=s.substring(o+1).trim(),!(!n||t[n]&&VR[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
 "+r:r)}),t},Ap=Symbol("internals");function Lo(e){return 
e&&String(e).trim().toLowerCase()}function Os(e){return 
e===!1||e==null?e:E.isArray(e)?e.map(Os):String(e)}function FR(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 DR=e=>/ [...]
-`)}getSetCookie(){return 
this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static
 from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const 
r=new this(t);return n.forEach(o=>r.set(o)),r}static accessor(t){const 
r=(this[Ap]=this[Ap]={accessors:{}}).accessors,o=this.prototype;function 
i(s){const a=Lo(s);r[a]||(MR(o,s),r[a]=!0)}return 
E.isArray(t)?t.forEach(i):i(t),this}};Ye.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
 [...]
+)+\\(\\s*min(-device)?-${e}`,"i"),max:new 
RegExp(`\\(\\s*max(-device)?-${e}`,"i")}),mE=Hf("width"),vE=Hf("height"),Uf=e=>({isMin:Qf(e.minMax,e.maxMin,e.min),isMax:Qf(e.maxMin,e.minMax,e.max)}),{isMin:Fl,isMax:Gf}=Uf(mE),{isMin:zl,isMax:qf}=Uf(vE),Kf=/print/i,Xf=/^print$/i,bE=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,yE=/(\d)/,Po=Number.MAX_VALUE,xE={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
 Yf(e){const t=bE.exec(e)||(Fl(e)||zl(e)?yE.exec(e):null);if(!t)return 
Po;if(t[0]==="0")return 0; [...]
+`).forEach(function(s){o=s.indexOf(":"),n=s.substring(0,o).trim().toLowerCase(),r=s.substring(o+1).trim(),!(!n||t[n]&&LR[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
 "+r:r)}),t},Ap=Symbol("internals");function Fo(e){return 
e&&String(e).trim().toLowerCase()}function Ts(e){return 
e===!1||e==null?e:E.isArray(e)?e.map(Ts):String(e)}function zR(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 DR=e=>/ [...]
+`)}getSetCookie(){return 
this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static
 from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const 
r=new this(t);return n.forEach(o=>r.set(o)),r}static accessor(t){const 
r=(this[Ap]=this[Ap]={accessors:{}}).accessors,o=this.prototype;function 
i(s){const a=Fo(s);r[a]||($R(o,s),r[a]=!0)}return 
E.isArray(t)?t.forEach(i):i(t),this}};Ye.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
 [...]
 `+i.map(Up).join(`
-`):" "+Up(i[0]):"as no adapter specified";throw new Q("There is no suitable 
adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return 
r},adapters:lc};function 
cc(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
 new Vr(null,e)}function qp(e){return 
cc(e),e.headers=Ye.from(e.headers),e.data=sc.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Gp.getAdapter(e.ad
 [...]
-`+i):r.stack=i}catch{}}throw r}}_request(t,n){typeof 
t=="string"?(n=n||{},n.url=t):n=t||{},n=Gn(this.defaults,n);const{transitional:r,paramsSerializer:o,headers:i}=n;r!==void
 
0&&Ns.assertOptions(r,{silentJSONParsing:Vt.transitional(Vt.boolean),forcedJSONParsing:Vt.transitional(Vt.boolean),clarifyTimeoutError:Vt.transitional(Vt.boolean)},!1),o!=null&&(E.isFunction(o)?n.paramsSerializer={serialize:o}:Ns.assertOptions(o,{encode:Vt.function,serialize:Vt.function},!0)),n.allowAbsoluteUrls!==v
 [...]
+`):" "+Up(i[0]):"as no adapter specified";throw new Q("There is no suitable 
adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return 
r},adapters:Sc};function 
wc(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
 new Lr(null,e)}function qp(e){return 
wc(e),e.headers=Ye.from(e.headers),e.data=Cc.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Gp.getAdapter(e.ad
 [...]
+`+i):r.stack=i}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:o,headers:i}=n;r!==void
 
0&&Ls.assertOptions(r,{silentJSONParsing:Vt.transitional(Vt.boolean),forcedJSONParsing:Vt.transitional(Vt.boolean),clarifyTimeoutError:Vt.transitional(Vt.boolean)},!1),o!=null&&(E.isFunction(o)?n.paramsSerializer={serialize:o}:Ls.assertOptions(o,{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 Fo(){return 
Fo=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},Fo.apply(this,arguments)}var 
cn;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(cn||(cn={}));const
 em="popstate";function gT(e){e===void 0&&(e={});function 
t(r,o){let{pathname:i,search:s,hash:a}=r.location;return 
hc("",{pathname:i,search:s,hash:a},o.state&&o.state.usr|| [...]
+ */function zo(){return 
zo=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},zo.apply(this,arguments)}var 
cn;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(cn||(cn={}));const
 em="popstate";function pT(e){e===void 0&&(e={});function 
t(r,o){let{pathname:i,search:s,hash:a}=r.location;return 
Ic("",{pathname:i,search:s,hash:a},o.state&&o.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 Do(){return 
Do=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},Do.apply(this,arguments)}const 
Vs=O.createContext(null),um=O.createContext(null),dn=O.createContext(null),Ls=O.createContext(null),Kn=O.createContext({outlet:null,matches:[],isDataRoute:!1}),dm=O.createContext(null);function
 MT(e,t){let{relative:n}=t===void 0?{}:t;zo()||me(!1);let{b [...]
+ */function Do(){return 
Do=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},Do.apply(this,arguments)}const 
Ds=O.createContext(null),um=O.createContext(null),dn=O.createContext(null),Ms=O.createContext(null),Xn=O.createContext({outlet:null,matches:[],isDataRoute:!1}),dm=O.createContext(null);function
 $T(e,t){let{relative:n}=t===void 0?{}:t;Mo()||me(!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 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 vm(e,t){if(e==null)return{};var 
n={},r=Object.keys(e),o,i;for(i=0;i<r.length;i++)o=r[i],!(t.indexOf(o)>=0)&&(n[o]=e[o]);return
 n}function o5(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function 
i5(e,t){return e.button===0&&(!t||t==="_sel [...]
+ */function js(){return 
js=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},js.apply(this,arguments)}function vm(e,t){if(e==null)return{};var 
n={},r=Object.keys(e),o,i;for(i=0;i<r.length;i++)o=r[i],!(t.indexOf(o)>=0)&&(n[o]=e[o]);return
 n}function i5(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function 
s5(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 Z5=Nw({pauseOnPageIdle:!0,placement:"bottom-end"});/*!
+ */const tN=_w({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 
eN=({block:e="start",inline:t="nearest"})=>{const[n,r]=w.useState(()=>window.location.hash);return
 w.useEffect(()=>{const o=()=>r(window.location.hash);return 
window.addEventListener("hashchange",o),()=>window.removeEventListener("hashchange",o)},[]),w.useEffect(()=>{if(n){const
 
o=document.getElementById(n.slice(1));o&&o.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},_m=({error:e})=>{var
 o;const t=e;if(!t)return;const n=(o=t.body)==null?void 0:o.detail;let r [...]
+ */const 
nN=({block:e="start",inline:t="nearest"})=>{const[n,r]=w.useState(()=>window.location.hash);return
 w.useEffect(()=>{const o=()=>r(window.location.hash);return 
window.addEventListener("hashchange",o),()=>window.removeEventListener("hashchange",o)},[]),w.useEffect(()=>{if(n){const
 
o=document.getElementById(n.slice(1));o&&o.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},_m=({error:e})=>{var
 o;const t=e;if(!t)return;const n=(o=t.body)==null?void 0:o.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
@@ -121,7 +121,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const uN=e=>{const[t,n]=w.useState(0);return 
w.useEffect(()=>{if(!e.current)return;const r=new ResizeObserver(o=>{for(const 
i of o)n(i.contentRect.width)});return 
r.observe(e.current),()=>{r.disconnect()}},[e]),t};/*!
+ */const hN=e=>{const[t,n]=w.useState(0);return 
w.useEffect(()=>{if(!e.current)return;const r=new ResizeObserver(o=>{for(const 
i of o)n(i.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,7 +138,7 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const Fm="token",dN=()=>{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(Fm,r),document.cookie="_token=; expires=Sat, 01 
Jan 2000 00:00:00 UTC; path=/;",r}},hN=e=>{const 
t=localStorage.getItem(Fm)??dN();return t!==void 
0&&(e.headers.Authorization=`Bearer 
${t}`),e},fN=()=>{const{data:e,error:t}=L5(void 
0,{enabled:!0,refetchInterval:Lm});return e?v.jsx(zt,{p:2,children:v.jsxs(jg, 
[...]
+ */const Fm="token",fN=()=>{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(Fm,r),document.cookie="_token=; expires=Sat, 01 
Jan 2000 00:00:00 UTC; path=/;",r}},gN=e=>{const 
t=localStorage.getItem(Fm)??fN();return t!==void 
0&&(e.headers.Authorization=`Bearer 
${t}`),e},pN=()=>{const{data:e,error:t}=F5(void 
0,{enabled:!0,refetchInterval:Lm});return e?v.jsx(Dt,{p:2,children:v.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
@@ -155,4 +155,4 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
- */const 
J=(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}`}}}),RN=Rl({theme:{tokens:{colors:{blac
 [...]
+ */const 
J=(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}`}}}),_N=_l({theme:{tokens:{colors:{blac
 [...]
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 43908424fb0..15a00f291fe 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
@@ -29,6 +29,7 @@ export type JobsServiceFetchMutationResult = 
Awaited<ReturnType<typeof JobsServi
 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 UiServiceRequestWorkerShutdownMutationResult = 
Awaited<ReturnType<typeof UiService.requestWorkerShutdown>>;
 export type JobsServiceStateMutationResult = Awaited<ReturnType<typeof 
JobsService.state>>;
 export type WorkerServiceSetStateMutationResult = Awaited<ReturnType<typeof 
WorkerService.setState>>;
 export type WorkerServiceUpdateQueuesMutationResult = 
Awaited<ReturnType<typeof WorkerService.updateQueues>>;
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 c23397614c2..e5756164d9e 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
@@ -57,6 +57,11 @@ export const useUiServiceRequestWorkerMaintenance = <TData = 
Common.UiServiceReq
   requestBody: MaintenanceRequest;
   workerName: string;
 }, TContext>({ mutationFn: ({ requestBody, workerName }) => 
UiService.requestWorkerMaintenance({ requestBody, workerName }) as unknown as 
Promise<TData>, ...options });
+export const useUiServiceRequestWorkerShutdown = <TData = 
Common.UiServiceRequestWorkerShutdownMutationResult, TError = unknown, TContext 
= unknown>(options?: Omit<UseMutationOptions<TData, TError, {
+  workerName: string;
+}, TContext>, "mutationFn">) => useMutation<TData, TError, {
+  workerName: string;
+}, TContext>({ mutationFn: ({ workerName }) => 
UiService.requestWorkerShutdown({ 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;
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 728a1ae5507..494d7c48b51 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, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse, 
ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse } 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, 
RequestWorkerShutdownData, RequestWorkerShutdownResponse } from './types.gen';
 
 export class JobsService {
     /**
@@ -331,4 +331,25 @@ export class UiService {
         });
     }
     
+    /**
+     * Request Worker Shutdown
+     * Request shutdown of a worker.
+     * @param data The data for the request.
+     * @param data.workerName
+     * @returns null Successful Response
+     * @throws ApiError
+     */
+    public static requestWorkerShutdown(data: RequestWorkerShutdownData): 
CancelablePromise<RequestWorkerShutdownResponse> {
+        return __request(OpenAPI, {
+            method: 'POST',
+            url: '/edge_worker/ui/worker/{worker_name}/shutdown',
+            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 61bf7663a9a..d32521f3215 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
@@ -483,6 +483,12 @@ export type ExitWorkerMaintenanceData = {
 
 export type ExitWorkerMaintenanceResponse = null;
 
+export type RequestWorkerShutdownData = {
+    workerName: string;
+};
+
+export type RequestWorkerShutdownResponse = null;
+
 export type $OpenApiTs = {
     '/edge_worker/v1/jobs/fetch/{worker_name}': {
         post: {
@@ -703,4 +709,19 @@ export type $OpenApiTs = {
             };
         };
     };
+    '/edge_worker/ui/worker/{worker_name}/shutdown': {
+        post: {
+            req: RequestWorkerShutdownData;
+            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/WorkerOperations.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx
index 34ce5fd6676..9cf157d8dd6 100644
--- 
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
@@ -23,6 +23,7 @@ import { toaster } from "src/components/ui";
 
 import { MaintenanceEnterButton } from "./MaintenanceEnterButton";
 import { MaintenanceExitButton } from "./MaintenanceExitButton";
+import { WorkerShutdownButton } from "./WorkerShutdownButton";
 
 interface WorkerOperationsProps {
   onOperations: () => void;
@@ -40,8 +41,9 @@ export const WorkerOperations = ({ onOperations, worker }: 
WorkerOperationsProps
 
   if (state === "idle" || state === "running") {
     return (
-      <Flex justifyContent="end">
+      <Flex justifyContent="end" gap={2}>
         <MaintenanceEnterButton onEnterMaintenance={onWorkerChange} 
workerName={workerName} />
+        <WorkerShutdownButton onShutdown={onWorkerChange} 
workerName={workerName} />
       </Flex>
     );
   } else if (
@@ -55,8 +57,9 @@ export const WorkerOperations = ({ onOperations, worker }: 
WorkerOperationsProps
         <Box fontSize="sm" whiteSpace="pre-wrap">
           {worker.maintenance_comments || "No comment"}
         </Box>
-        <Flex justifyContent="end">
+        <Flex justifyContent="end" gap={2}>
           <MaintenanceExitButton onExitMaintenance={onWorkerChange} 
workerName={workerName} />
+          <WorkerShutdownButton onShutdown={onWorkerChange} 
workerName={workerName} />
         </Flex>
       </VStack>
     );
diff --git 
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerShutdownButton.tsx
 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerShutdownButton.tsx
new file mode 100644
index 00000000000..dbbd828896d
--- /dev/null
+++ 
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerShutdownButton.tsx
@@ -0,0 +1,102 @@
+/*!
+ * 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, Text, useDisclosure 
} from "@chakra-ui/react";
+import { useUiServiceRequestWorkerShutdown } from "openapi/queries";
+import { FaPowerOff } from "react-icons/fa";
+
+interface WorkerShutdownButtonProps {
+  onShutdown: (toast: Record<string, string>) => void;
+  workerName: string;
+}
+
+export const WorkerShutdownButton = ({ onShutdown, workerName }: 
WorkerShutdownButtonProps) => {
+  const { onClose, onOpen, open } = useDisclosure();
+
+  const shutdownMutation = useUiServiceRequestWorkerShutdown({
+    onError: (error) => {
+      onShutdown({
+        description: `Unable to request shutdown for worker ${workerName}: 
${error}`,
+        title: "Shutdown Request Failed",
+        type: "error",
+      });
+    },
+    onSuccess: () => {
+      onShutdown({
+        description: `Worker ${workerName} was requested to shutdown.`,
+        title: "Shutdown Request Sent",
+        type: "success",
+      });
+      onClose();
+    },
+  });
+
+  const handleShutdown = () => {
+    shutdownMutation.mutate({ workerName });
+  };
+
+  return (
+    <>
+      <IconButton
+        size="sm"
+        variant="ghost"
+        onClick={onOpen}
+        aria-label="Shutdown Worker"
+        title="Shutdown Worker"
+        color="red.500"
+      >
+        <FaPowerOff />
+      </IconButton>
+
+      <Dialog.Root onOpenChange={onClose} open={open} size="md">
+        <Portal>
+          <Dialog.Backdrop />
+          <Dialog.Positioner>
+            <Dialog.Content>
+              <Dialog.Header>
+                <Dialog.Title>Shutdown worker {workerName}</Dialog.Title>
+              </Dialog.Header>
+              <Dialog.Body>
+                <Text>Are you sure you want to request shutdown for worker 
{workerName}?</Text>
+                <Text fontSize="sm" color="red.500" mt={2}>
+                  This will terminate the worker process on the remote edge 
site.
+                </Text>
+              </Dialog.Body>
+              <Dialog.Footer>
+                <Dialog.ActionTrigger asChild>
+                  <Button variant="outline">Cancel</Button>
+                </Dialog.ActionTrigger>
+                <Button
+                  onClick={handleShutdown}
+                  colorScheme="red"
+                  loading={shutdownMutation.isPending}
+                  loadingText="Shutting down..."
+                >
+                  Shutdown Worker
+                </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/worker_api/routes/ui.py 
b/providers/edge3/src/airflow/providers/edge3/worker_api/routes/ui.py
index 0b452691765..019195ade65 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
@@ -27,7 +27,12 @@ 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 GetUserDep, 
requires_access_view
 from airflow.providers.edge3.models.edge_job import EdgeJobModel
-from airflow.providers.edge3.models.edge_worker import EdgeWorkerModel, 
exit_maintenance, request_maintenance
+from airflow.providers.edge3.models.edge_worker import (
+    EdgeWorkerModel,
+    exit_maintenance,
+    request_maintenance,
+    request_shutdown,
+)
 from airflow.providers.edge3.worker_api.datamodels_ui import (
     Job,
     JobCollectionResponse,
@@ -156,3 +161,26 @@ def exit_worker_maintenance(
         exit_maintenance(worker_name, session=session)
     except Exception as e:
         raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e))
+
+
+@ui_router.post(
+    "/worker/{worker_name}/shutdown",
+    dependencies=[
+        Depends(requires_access_view(access_view=AccessView.JOBS)),
+    ],
+)
+def request_worker_shutdown(
+    worker_name: str,
+    session: SessionDep,
+) -> None:
+    """Request shutdown of a worker."""
+    # 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.HTTP_404_NOT_FOUND, detail=f"Worker 
{worker_name} not found")
+
+    try:
+        request_shutdown(worker_name, session=session)
+    except Exception as 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 bf1b12c54fb..cef9155df64 100644
--- a/providers/edge3/www-hash.txt
+++ b/providers/edge3/www-hash.txt
@@ -1 +1 @@
-5ac4d4783165960a231642baa97f22ddbb39ae0473188ffe6bfe09f1d3699e0f
+3279a0aacccafb0389ade571b71fae3cba5a5058a1a74a1566bf1d07e31857b7


Reply via email to