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