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 2f29eca1c92 Add queue management UI buttons for Edge workers (#55625)
2f29eca1c92 is described below
commit 2f29eca1c928ef9308102953f801518e3a46b1db
Author: Dheeraj Turaga <[email protected]>
AuthorDate: Sat Sep 13 11:29:53 2025 -0500
Add queue management UI buttons for Edge workers (#55625)
* Add queue management UI buttons for Edge workers
- Add AddQueueButton and RemoveQueueButton components with
LuListPlus/LuListMinus icons
- Create UI proxy endpoints for queue add/remove operations in
worker_api/routes/ui.py
- Add QueueUpdateRequest data model for queue operations
- Update OpenAPI spec and regenerate TypeScript client code
- Integrate buttons into WorkerOperations for idle/running workers
- Include proper error handling, validation, and toast notifications
- Connect to existing add_worker_queues/remove_worker_queues functions
* Jens Suggestions
---
.../providers/edge3/openapi/v2-edge-generated.yaml | 73 ++++++++++
.../providers/edge3/plugins/www/dist/main.umd.cjs | 38 +++---
.../plugins/www/openapi-gen/queries/common.ts | 2 +
.../plugins/www/openapi-gen/queries/queries.ts | 14 ++
.../www/openapi-gen/requests/services.gen.ts | 48 ++++++-
.../plugins/www/openapi-gen/requests/types.gen.ts | 42 ++++++
.../plugins/www/src/components/AddQueueButton.tsx | 138 +++++++++++++++++++
.../www/src/components/RemoveQueueButton.tsx | 151 +++++++++++++++++++++
.../www/src/components/WorkerOperations.tsx | 4 +
.../providers/edge3/worker_api/datamodels_ui.py | 6 +
.../providers/edge3/worker_api/routes/ui.py | 50 +++++++
providers/edge3/www-hash.txt | 2 +-
12 files changed, 547 insertions(+), 21 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 870ab123325..abfb61f1a54 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
@@ -751,6 +751,79 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /edge_worker/ui/worker/{worker_name}/queues/{queue_name}:
+ put:
+ tags:
+ - UI
+ summary: Add Worker Queue
+ description: Add a queue to a worker.
+ operationId: add_worker_queue
+ security:
+ - OAuth2PasswordBearer: []
+ - HTTPBearer: []
+ parameters:
+ - name: worker_name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Worker Name
+ - name: queue_name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Queue Name
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ type: 'null'
+ title: Response Add Worker Queue
+ '422':
+ description: Validation Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/HTTPValidationError'
+ delete:
+ tags:
+ - UI
+ summary: Remove Worker Queue
+ description: Remove a queue from a worker.
+ operationId: remove_worker_queue
+ security:
+ - OAuth2PasswordBearer: []
+ - HTTPBearer: []
+ parameters:
+ - name: worker_name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Worker Name
+ - name: queue_name
+ in: path
+ required: true
+ schema:
+ type: string
+ title: Queue Name
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ type: 'null'
+ title: Response Remove Worker Queue
+ '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 3b28f8b4654..2e7788801ee 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(P,St){typeof exports=="object"&&typeof
module<"u"?module.exports=St(require("react"),require("react-dom")):typeof
define=="function"&&define.amd?define(["react","react-dom"],St):(P=typeof
globalThis<"u"?globalThis:P||self,P.AirflowPlugin=St(P.React,P.ReactDOM))})(this,(function(P,St){"use
strict";function cp(e){const
t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n
in e)if(n!=="default"){const
r=Object.getOwnPropertyDescriptor(e,n);Object.definePr [...]
+(function(P,It){typeof exports=="object"&&typeof
module<"u"?module.exports=It(require("react"),require("react-dom")):typeof
define=="function"&&define.amd?define(["react","react-dom"],It):(P=typeof
globalThis<"u"?globalThis:P||self,P.AirflowPlugin=It(P.React,P.ReactDOM))})(this,(function(P,It){"use
strict";function gp(e){const
t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n
in e)if(n!=="default"){const
r=Object.getOwnPropertyDescriptor(e,n);Object.definePr [...]
* @license React
* react-jsx-runtime.production.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 il;function dp(){if(il)return Yn;il=1;var
e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function
n(r,o,i){var s=null;if(i!==void 0&&(s=""+i),o.key!==void
0&&(s=""+o.key),"key"in o){i={};for(var a in o)a!=="key"&&(i[a]=o[a])}else
i=o;return o=i.ref,{$$typeof:e,type:r,key:s,ref:o!==void
0?o:null,props:i}}return Yn.Fragment=t,Yn.jsx=n,Yn.jsxs=n,Yn}var sl;function
hp(){return sl||(sl=1,$i.exports=dp()),$i.exports}var m=hp();function al(e){var
t=Object.creat [...]
+ */var sl;function mp(){if(sl)return cr;sl=1;var
e=Symbol.for("react.transitional.element"),t=Symbol.for("react.fragment");function
n(r,o,i){var s=null;if(i!==void 0&&(s=""+i),o.key!==void
0&&(s=""+o.key),"key"in o){i={};for(var a in o)a!=="key"&&(i[a]=o[a])}else
i=o;return o=i.ref,{$$typeof:e,type:r,key:s,ref:o!==void
0?o:null,props:i}}return cr.Fragment=t,cr.jsx=n,cr.jsxs=n,cr}var al;function
vp(){return al||(al=1,Bi.exports=mp()),Bi.exports}var g=vp();function ll(e){var
t=Object.creat [...]
* 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 bl;function Hp(){if(bl)return J;bl=1;var e=typeof
Symbol=="function"&&Symbol.for,t=e?Symbol.for("react.element"):60103,n=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,o=e?Symbol.for("react.strict_mode"):60108,i=e?Symbol.for("react.profiler"):60114,s=e?Symbol.for("react.provider"):60109,a=e?Symbol.for("react.context"):60110,l=e?Symbol.for("react.async_mode"):60111,c=e?Symbol.for("react.concurrent_mode"):60111,u=e?Symbol.for("react.forward_ref"):60112,d=e
[...]
+ */var yl;function Yp(){if(yl)return J;yl=1;var e=typeof
Symbol=="function"&&Symbol.for,t=e?Symbol.for("react.element"):60103,n=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,o=e?Symbol.for("react.strict_mode"):60108,i=e?Symbol.for("react.profiler"):60114,s=e?Symbol.for("react.provider"):60109,a=e?Symbol.for("react.context"):60110,l=e?Symbol.for("react.async_mode"):60111,c=e?Symbol.for("react.concurrent_mode"):60111,u=e?Symbol.for("react.forward_ref"):60112,d=e
[...]
<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
J1(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
Z1(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
nk(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
rk(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")}),oC=Qd("width"),iC=Qd("height"),Jd=e=>({isMin:oh(e.minMax,e.maxMin,e.min),isMax:oh(e.maxMin,e.minMax,e.max)}),{isMin:ba,isMax:Zd}=Jd(oC),{isMin:ya,isMax:eh}=Jd(iC),th=/print/i,nh=/^print$/i,sC=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,aC=/(\d)/,Or=Number.MAX_VALUE,lC={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
rh(e){const t=sC.exec(e)||(ba(e)||ya(e)?aC.exec(e):null);if(!t)return
Or;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]&&EO[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
"+r:r)}),t},Uf=Symbol("internals");function Ur(e){return
e&&String(e).trim().toLowerCase()}function xi(e){return
e===!1||e==null?e:w.isArray(e)?e.map(xi):String(e)}function PO(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 IO=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[Uf]=this[Uf]={accessors:{}}).accessors,o=this.prototype;function
i(s){const a=Ur(s);r[a]||(TO(o,s),r[a]=!0)}return
w.isArray(t)?t.forEach(i):i(t),this}};De.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
[...]
-`+s.map(sg).join(`
-`):" "+sg(s[0]):"as no adapter specified";throw new H("There is no suitable
adapter to dispatch the request "+a,"ERR_NOT_SUPPORT")}return
o},adapters:Ha};function
Ua(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
new Hn(null,e)}function lg(e){return
Ua(e),e.headers=De.from(e.headers),e.data=Wa.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),ag.getAdapter(e.ad
[...]
-`+i):r.stack=i}catch{}}throw r}}_request(t,n){typeof
t=="string"?(n=n||{},n.url=t):n=t||{},n=cn(this.defaults,n);const{transitional:r,paramsSerializer:o,headers:i}=n;r!==void
0&&wi.assertOptions(r,{silentJSONParsing:mt.transitional(mt.boolean),forcedJSONParsing:mt.transitional(mt.boolean),clarifyTimeoutError:mt.transitional(mt.boolean)},!1),o!=null&&(w.isFunction(o)?n.paramsSerializer={serialize:o}:wi.assertOptions(o,{encode:mt.function,serialize:mt.function},!0)),n.allowAbsoluteUrls!==v
[...]
+)+\\(\\s*min(-device)?-${e}`,"i"),max:new
RegExp(`\\(\\s*max(-device)?-${e}`,"i")}),lC=eh("width"),cC=eh("height"),th=e=>({isMin:ah(e.minMax,e.maxMin,e.min),isMax:ah(e.maxMin,e.minMax,e.max)}),{isMin:ya,isMax:nh}=th(lC),{isMin:xa,isMax:rh}=th(cC),oh=/print/i,ih=/^print$/i,uC=/(-?\d*\.?\d+)(ch|em|ex|px|rem)/,dC=/(\d)/,zr=Number.MAX_VALUE,hC={ch:8.8984375,em:16,rem:16,ex:8.296875,px:1};function
sh(e){const t=uC.exec(e)||(ya(e)||xa(e)?dC.exec(e):null);if(!t)return
zr;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]&&NO[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+",
"+r:r)}),t},Kf=Symbol("internals");function Ur(e){return
e&&String(e).trim().toLowerCase()}function ki(e){return
e===!1||e==null?e:w.isArray(e)?e.map(ki):String(e)}function _O(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 VO=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[Kf]=this[Kf]={accessors:{}}).accessors,o=this.prototype;function
i(s){const a=Ur(s);r[a]||(LO(o,s),r[a]=!0)}return
w.isArray(t)?t.forEach(i):i(t),this}};Me.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-A
[...]
+`+s.map(cg).join(`
+`):" "+cg(s[0]):"as no adapter specified";throw new H("There is no suitable
adapter to dispatch the request "+a,"ERR_NOT_SUPPORT")}return
o},adapters:Ua};function
Ga(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw
new or(null,e)}function dg(e){return
Ga(e),e.headers=Me.from(e.headers),e.data=Ha.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),ug.getAdapter(e.ad
[...]
+`+i):r.stack=i}catch{}}throw r}}_request(t,n){typeof
t=="string"?(n=n||{},n.url=t):n=t||{},n=On(this.defaults,n);const{transitional:r,paramsSerializer:o,headers:i}=n;r!==void
0&&Ei.assertOptions(r,{silentJSONParsing:mt.transitional(mt.boolean),forcedJSONParsing:mt.transitional(mt.boolean),clarifyTimeoutError:mt.transitional(mt.boolean)},!1),o!=null&&(w.isFunction(o)?n.paramsSerializer={serialize:o}:Ei.assertOptions(o,{encode:mt.function,serialize:mt.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 Gr(){return
Gr=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},Gr.apply(this,arguments)}var
Lt;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(Lt||(Lt={}));const
pg="popstate";function iP(e){e===void 0&&(e={});function
t(r,o){let{pathname:i,search:s,hash:a}=r.location;return
Ka("",{pathname:i,search:s,hash:a},o.state&&o.state.usr|| [...]
+ */function Gr(){return
Gr=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},Gr.apply(this,arguments)}var
$t;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})($t||($t={}));const
bg="popstate";function dP(e){e===void 0&&(e={});function
t(r,o){let{pathname:i,search:s,hash:a}=r.location;return
Ya("",{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 qr(){return
qr=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},qr.apply(this,arguments)}const
Oi=O.createContext(null),Eg=O.createContext(null),zt=O.createContext(null),Pi=O.createContext(null),dn=O.createContext({outlet:null,matches:[],isDataRoute:!1}),Og=O.createContext(null);function
TP(e,t){let{relative:n}=t===void 0?{}:t;Kr()||le(!1);let{b [...]
+ */function qr(){return
qr=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},qr.apply(this,arguments)}const
Pi=O.createContext(null),Ig=O.createContext(null),Wt=O.createContext(null),Ii=O.createContext(null),In=O.createContext({outlet:null,matches:[],isDataRoute:!1}),Rg=O.createContext(null);function
LP(e,t){let{relative:n}=t===void 0?{}:t;Kr()||le(!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 Ri(){return
Ri=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},Ri.apply(this,arguments)}function Ag(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 YP(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function
XP(e,t){return e.button===0&&(!t||t==="_sel [...]
+ */function Ti(){return
Ti=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},Ti.apply(this,arguments)}function Fg(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 tI(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function
nI(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 KI=xk({pauseOnPageIdle:!0,placement:"bottom-end"});/*!
+ */const nR=wk({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
YI=({block:e="start",inline:t="nearest"})=>{const[n,r]=P.useState(()=>window.location.hash);return
P.useEffect(()=>{const o=()=>r(window.location.hash);return
window.addEventListener("hashchange",o),()=>window.removeEventListener("hashchange",o)},[]),P.useEffect(()=>{if(n){const
o=document.getElementById(n.slice(1));o&&o.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},qg=({error:e})=>{const
t=e;if(!t)return;const n=t.body?.detail;let r;return n!==void 0&&(typ [...]
+ */const
rR=({block:e="start",inline:t="nearest"})=>{const[n,r]=P.useState(()=>window.location.hash);return
P.useEffect(()=>{const o=()=>r(window.location.hash);return
window.addEventListener("hashchange",o),()=>window.removeEventListener("hashchange",o)},[]),P.useEffect(()=>{if(n){const
o=document.getElementById(n.slice(1));o&&o.scrollIntoView({behavior:"auto",block:e,inline:t})}},[n,e,t]),null},Jg=({error:e})=>{const
t=e;if(!t)return;const n=t.body?.detail;let r;return n!==void 0&&(typ [...]
* 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 Xg=5e3;/*!
+ */const tp=5e3;/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
@@ -121,7 +121,7 @@
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
- */const sR=e=>{const[t,n]=P.useState(0);return
P.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 gR=e=>{const[t,n]=P.useState(0);return
P.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 Qg="token",aR=()=>{const e=document.cookie.split(";");for(const t of
e){const[n,r]=t.split("=");if(n?.trim()==="_token"&&r!==void 0)return
localStorage.setItem(Qg,r),document.cookie="_token=; expires=Sat, 01 Jan 2000
00:00:00 UTC; path=/;",r}},lR=e=>{const t=localStorage.getItem(Qg)??aR();return
t!==void 0&&(e.headers.Authorization=`Bearer
${t}`),e},cR=()=>{const{data:e,error:t}=OI(void
0,{enabled:!0,refetchInterval:Xg});return
e?.jobs&&e.jobs.length>0?m.jsx(rr,{p:2,children:m.j [...]
+ */const np="token",pR=()=>{const e=document.cookie.split(";");for(const t of
e){const[n,r]=t.split("=");if(n?.trim()==="_token"&&r!==void 0)return
localStorage.setItem(np,r),document.cookie="_token=; expires=Sat, 01 Jan 2000
00:00:00 UTC; path=/;",r}},mR=e=>{const t=localStorage.getItem(np)??pR();return
t!==void 0&&(e.headers.Authorization=`Bearer
${t}`),e},vR=()=>{const{data:e,error:t}=AI(void
0,{enabled:!0,refetchInterval:tp});return
e?.jobs&&e.jobs.length>0?g.jsx(vr,{p:2,children:g.j [...]
* 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
U=(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}`}}}),OR=pa({theme:{tokens:{colors:{blac
[...]
+ */const
U=(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}`}}}),LR=ma({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 7d6ccb0f058..e6ef2ff1531 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
@@ -30,9 +30,11 @@ export type LogsServicePushLogsMutationResult =
Awaited<ReturnType<typeof LogsSe
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 UiServiceAddWorkerQueueMutationResult = Awaited<ReturnType<typeof
UiService.addWorkerQueue>>;
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 UiServiceUpdateWorkerMaintenanceMutationResult =
Awaited<ReturnType<typeof UiService.updateWorkerMaintenance>>;
export type UiServiceExitWorkerMaintenanceMutationResult =
Awaited<ReturnType<typeof UiService.exitWorkerMaintenance>>;
export type UiServiceDeleteWorkerMutationResult = Awaited<ReturnType<typeof
UiService.deleteWorker>>;
+export type UiServiceRemoveWorkerQueueMutationResult =
Awaited<ReturnType<typeof UiService.removeWorkerQueue>>;
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 c95cce5c8f5..565acd9bc53 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
@@ -62,6 +62,13 @@ export const useUiServiceRequestWorkerShutdown = <TData =
Common.UiServiceReques
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
workerName: string;
}, TContext>({ mutationFn: ({ workerName }) =>
UiService.requestWorkerShutdown({ workerName }) as unknown as Promise<TData>,
...options });
+export const useUiServiceAddWorkerQueue = <TData =
Common.UiServiceAddWorkerQueueMutationResult, TError = unknown, TContext =
unknown>(options?: Omit<UseMutationOptions<TData, TError, {
+ queueName: string;
+ workerName: string;
+}, TContext>, "mutationFn">) => useMutation<TData, TError, {
+ queueName: string;
+ workerName: string;
+}, TContext>({ mutationFn: ({ queueName, workerName }) =>
UiService.addWorkerQueue({ queueName, 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;
@@ -114,3 +121,10 @@ export const useUiServiceDeleteWorker = <TData =
Common.UiServiceDeleteWorkerMut
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
workerName: string;
}, TContext>({ mutationFn: ({ workerName }) => UiService.deleteWorker({
workerName }) as unknown as Promise<TData>, ...options });
+export const useUiServiceRemoveWorkerQueue = <TData =
Common.UiServiceRemoveWorkerQueueMutationResult, TError = unknown, TContext =
unknown>(options?: Omit<UseMutationOptions<TData, TError, {
+ queueName: string;
+ workerName: string;
+}, TContext>, "mutationFn">) => useMutation<TData, TError, {
+ queueName: string;
+ workerName: string;
+}, TContext>({ mutationFn: ({ queueName, workerName }) =>
UiService.removeWorkerQueue({ queueName, workerName }) as unknown as
Promise<TData>, ...options });
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 a26191d6099..45fe4554830 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,
UpdateWorkerMaintenanceData, UpdateWorkerMaintenanceResponse,
ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse,
RequestWorkerShutdownD [...]
+import type { FetchData, FetchResponse, StateData, StateResponse,
LogfilePathData, LogfilePathResponse, PushLogsData, PushLogsResponse,
RegisterData, RegisterResponse, SetStateData, SetStateResponse,
UpdateQueuesData, UpdateQueuesResponse, HealthResponse, WorkerResponse,
JobsResponse, RequestWorkerMaintenanceData, RequestWorkerMaintenanceResponse,
UpdateWorkerMaintenanceData, UpdateWorkerMaintenanceResponse,
ExitWorkerMaintenanceData, ExitWorkerMaintenanceResponse,
RequestWorkerShutdownD [...]
export class JobsService {
/**
@@ -397,4 +397,50 @@ export class UiService {
});
}
+ /**
+ * Add Worker Queue
+ * Add a queue to a worker.
+ * @param data The data for the request.
+ * @param data.workerName
+ * @param data.queueName
+ * @returns null Successful Response
+ * @throws ApiError
+ */
+ public static addWorkerQueue(data: AddWorkerQueueData):
CancelablePromise<AddWorkerQueueResponse> {
+ return __request(OpenAPI, {
+ method: 'PUT',
+ url: '/edge_worker/ui/worker/{worker_name}/queues/{queue_name}',
+ path: {
+ worker_name: data.workerName,
+ queue_name: data.queueName
+ },
+ errors: {
+ 422: 'Validation Error'
+ }
+ });
+ }
+
+ /**
+ * Remove Worker Queue
+ * Remove a queue from a worker.
+ * @param data The data for the request.
+ * @param data.workerName
+ * @param data.queueName
+ * @returns null Successful Response
+ * @throws ApiError
+ */
+ public static removeWorkerQueue(data: RemoveWorkerQueueData):
CancelablePromise<RemoveWorkerQueueResponse> {
+ return __request(OpenAPI, {
+ method: 'DELETE',
+ url: '/edge_worker/ui/worker/{worker_name}/queues/{queue_name}',
+ path: {
+ worker_name: data.workerName,
+ queue_name: data.queueName
+ },
+ 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 b613be3d4e8..9630cebd4f8 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
@@ -502,6 +502,20 @@ export type DeleteWorkerData = {
export type DeleteWorkerResponse = null;
+export type AddWorkerQueueData = {
+ queueName: string;
+ workerName: string;
+};
+
+export type AddWorkerQueueResponse = null;
+
+export type RemoveWorkerQueueData = {
+ queueName: string;
+ workerName: string;
+};
+
+export type RemoveWorkerQueueResponse = null;
+
export type $OpenApiTs = {
'/edge_worker/v1/jobs/fetch/{worker_name}': {
post: {
@@ -765,4 +779,32 @@ export type $OpenApiTs = {
};
};
};
+ '/edge_worker/ui/worker/{worker_name}/queues/{queue_name}': {
+ put: {
+ req: AddWorkerQueueData;
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: null;
+ /**
+ * Validation Error
+ */
+ 422: HTTPValidationError;
+ };
+ };
+ delete: {
+ req: RemoveWorkerQueueData;
+ 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/AddQueueButton.tsx
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/AddQueueButton.tsx
new file mode 100644
index 00000000000..cded43f5b78
--- /dev/null
+++
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/AddQueueButton.tsx
@@ -0,0 +1,138 @@
+/*!
+ * 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,
+ Input,
+ IconButton,
+ Portal,
+ Text,
+ useDisclosure,
+ VStack,
+} from "@chakra-ui/react";
+import { useUiServiceAddWorkerQueue } from "openapi/queries";
+import { useState } from "react";
+import { LuListPlus } from "react-icons/lu";
+
+interface AddQueueButtonProps {
+ onQueueUpdate: (toast: Record<string, string>) => void;
+ workerName: string;
+}
+
+export const AddQueueButton = ({ onQueueUpdate, workerName }:
AddQueueButtonProps) => {
+ const { onClose, onOpen, open } = useDisclosure();
+ const [queueName, setQueueName] = useState("");
+
+ const addQueueMutation = useUiServiceAddWorkerQueue({
+ onError: (error) => {
+ onQueueUpdate({
+ description: `Unable to add queue to worker ${workerName}: ${error}`,
+ title: "Add Queue Failed",
+ type: "error",
+ });
+ },
+ onSuccess: () => {
+ onQueueUpdate({
+ description: `Queue "${queueName}" was added to worker ${workerName}.`,
+ title: "Queue Added",
+ type: "success",
+ });
+ onClose();
+ setQueueName("");
+ },
+ });
+
+ const handleAddQueue = () => {
+ if (!queueName.trim()) {
+ onQueueUpdate({
+ description: "Please enter a queue name.",
+ title: "Invalid Input",
+ type: "error",
+ });
+ return;
+ }
+
+ addQueueMutation.mutate({
+ queueName: queueName.trim(),
+ workerName,
+ });
+ };
+
+ return (
+ <>
+ <IconButton
+ size="sm"
+ variant="ghost"
+ onClick={onOpen}
+ aria-label="Add Queue"
+ title="Add Queue"
+ colorPalette="success"
+ >
+ <LuListPlus />
+ </IconButton>
+
+ <Dialog.Root onOpenChange={onClose} open={open} size="md">
+ <Portal>
+ <Dialog.Backdrop />
+ <Dialog.Positioner>
+ <Dialog.Content>
+ <Dialog.Header>
+ <Dialog.Title>Add Queue to {workerName}</Dialog.Title>
+ </Dialog.Header>
+ <Dialog.Body>
+ <VStack gap={4} align="stretch">
+ <Text>Enter the name of the queue to add to this
worker:</Text>
+ <Input
+ placeholder="Queue name"
+ value={queueName}
+ onChange={(e) => setQueueName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ handleAddQueue();
+ }
+ }}
+ />
+ </VStack>
+ </Dialog.Body>
+ <Dialog.Footer>
+ <Dialog.ActionTrigger asChild>
+ <Button variant="outline">Cancel</Button>
+ </Dialog.ActionTrigger>
+ <Button
+ onClick={handleAddQueue}
+ colorPalette="success"
+ loading={addQueueMutation.isPending}
+ loadingText="Adding queue..."
+ disabled={!queueName.trim()}
+ >
+ <LuListPlus />
+ Add Queue
+ </Button>
+ </Dialog.Footer>
+ <Dialog.CloseTrigger asChild>
+ <CloseButton size="sm" />
+ </Dialog.CloseTrigger>
+ </Dialog.Content>
+ </Dialog.Positioner>
+ </Portal>
+ </Dialog.Root>
+ </>
+ );
+};
diff --git
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/RemoveQueueButton.tsx
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/RemoveQueueButton.tsx
new file mode 100644
index 00000000000..cc700ec832a
--- /dev/null
+++
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/RemoveQueueButton.tsx
@@ -0,0 +1,151 @@
+/*!
+ * 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,
+ VStack,
+ For,
+} from "@chakra-ui/react";
+import { useUiServiceRemoveWorkerQueue } from "openapi/queries";
+import type { Worker } from "openapi/requests/types.gen";
+import { useState } from "react";
+import { LuListMinus } from "react-icons/lu";
+
+interface RemoveQueueButtonProps {
+ onQueueUpdate: (toast: Record<string, string>) => void;
+ worker: Worker;
+}
+
+export const RemoveQueueButton = ({ onQueueUpdate, worker }:
RemoveQueueButtonProps) => {
+ const { onClose, onOpen, open } = useDisclosure();
+ const [selectedQueue, setSelectedQueue] = useState<string>("");
+
+ const removeQueueMutation = useUiServiceRemoveWorkerQueue({
+ onError: (error) => {
+ onQueueUpdate({
+ description: `Unable to remove queue from worker
${worker.worker_name}: ${error}`,
+ title: "Remove Queue Failed",
+ type: "error",
+ });
+ },
+ onSuccess: () => {
+ onQueueUpdate({
+ description: `Queue "${selectedQueue}" was removed from worker
${worker.worker_name}.`,
+ title: "Queue Removed",
+ type: "success",
+ });
+ onClose();
+ setSelectedQueue("");
+ },
+ });
+
+ const handleRemoveQueue = () => {
+ if (!selectedQueue) {
+ onQueueUpdate({
+ description: "Please select a queue to remove.",
+ title: "Invalid Selection",
+ type: "error",
+ });
+ return;
+ }
+
+ removeQueueMutation.mutate({
+ queueName: selectedQueue,
+ workerName: worker.worker_name,
+ });
+ };
+
+ const availableQueues = worker.queues || [];
+
+ // Don't render the button if there are no queues to remove
+ if (availableQueues.length === 0) {
+ return null;
+ }
+
+ return (
+ <>
+ <IconButton
+ size="sm"
+ variant="ghost"
+ onClick={onOpen}
+ aria-label="Remove Queue"
+ title="Remove Queue"
+ colorPalette="danger"
+ >
+ <LuListMinus />
+ </IconButton>
+
+ <Dialog.Root onOpenChange={onClose} open={open} size="md">
+ <Portal>
+ <Dialog.Backdrop />
+ <Dialog.Positioner>
+ <Dialog.Content>
+ <Dialog.Header>
+ <Dialog.Title>Remove Queue from
{worker.worker_name}</Dialog.Title>
+ </Dialog.Header>
+ <Dialog.Body>
+ <VStack gap={4} align="stretch">
+ <Text>Select a queue to remove from this worker:</Text>
+ <VStack gap={2} align="stretch">
+ <For each={availableQueues}>
+ {(queue) => (
+ <Button
+ key={queue}
+ variant={selectedQueue === queue ? "solid" :
"outline"}
+ colorPalette={selectedQueue === queue ? "blue" :
"gray"}
+ onClick={() => setSelectedQueue(queue)}
+ justifyContent="flex-start"
+ >
+ {queue}
+ </Button>
+ )}
+ </For>
+ </VStack>
+ </VStack>
+ </Dialog.Body>
+ <Dialog.Footer>
+ <Dialog.ActionTrigger asChild>
+ <Button variant="outline">Cancel</Button>
+ </Dialog.ActionTrigger>
+ <Button
+ onClick={handleRemoveQueue}
+ colorPalette="danger"
+ loading={removeQueueMutation.isPending}
+ loadingText="Removing queue..."
+ disabled={!selectedQueue}
+ >
+ <LuListMinus />
+ Remove Queue
+ </Button>
+ </Dialog.Footer>
+ <Dialog.CloseTrigger asChild>
+ <CloseButton size="sm" />
+ </Dialog.CloseTrigger>
+ </Dialog.Content>
+ </Dialog.Positioner>
+ </Portal>
+ </Dialog.Root>
+ </>
+ );
+};
diff --git
a/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx
b/providers/edge3/src/airflow/providers/edge3/plugins/www/src/components/WorkerOperations.tsx
index 5f949139171..0d0aa727eea 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
@@ -21,9 +21,11 @@ import type { Worker } from "openapi/requests/types.gen";
import { toaster } from "src/components/ui";
+import { AddQueueButton } from "./AddQueueButton";
import { MaintenanceEditCommentButton } from "./MaintenanceEditCommentButton";
import { MaintenanceEnterButton } from "./MaintenanceEnterButton";
import { MaintenanceExitButton } from "./MaintenanceExitButton";
+import { RemoveQueueButton } from "./RemoveQueueButton";
import { WorkerDeleteButton } from "./WorkerDeleteButton";
import { WorkerShutdownButton } from "./WorkerShutdownButton";
@@ -44,6 +46,8 @@ export const WorkerOperations = ({ onOperations, worker }:
WorkerOperationsProps
if (state === "idle" || state === "running") {
return (
<Flex justifyContent="end" gap={2}>
+ <AddQueueButton onQueueUpdate={onWorkerChange} workerName={workerName}
/>
+ <RemoveQueueButton onQueueUpdate={onWorkerChange} worker={worker} />
<MaintenanceEnterButton onEnterMaintenance={onWorkerChange}
workerName={workerName} />
<WorkerShutdownButton onShutdown={onWorkerChange}
workerName={workerName} />
</Flex>
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 70aa651d50c..3aa2e12e9df 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
@@ -71,3 +71,9 @@ class MaintenanceRequest(BaseModel):
"""Request body for maintenance operations."""
maintenance_comment: Annotated[str, Field(description="Comment describing
the maintenance reason.")]
+
+
+class QueueUpdateRequest(BaseModel):
+ """Request body for queue operations."""
+
+ queue_name: Annotated[str, Field(description="Name of the queue to add or
remove.")]
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 2ed91151789..11ea9b71e90 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
@@ -29,9 +29,11 @@ from airflow.api_fastapi.core_api.security import
GetUserDep, requires_access_vi
from airflow.providers.edge3.models.edge_job import EdgeJobModel
from airflow.providers.edge3.models.edge_worker import (
EdgeWorkerModel,
+ add_worker_queues,
change_maintenance_comment,
exit_maintenance,
remove_worker,
+ remove_worker_queues,
request_maintenance,
request_shutdown,
)
@@ -240,3 +242,51 @@ def delete_worker(
remove_worker(worker_name, session=session)
except Exception as e:
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e))
+
+
+@ui_router.put(
+ "/worker/{worker_name}/queues/{queue_name}",
+ dependencies=[
+ Depends(requires_access_view(access_view=AccessView.JOBS)),
+ ],
+)
+def add_worker_queue(
+ worker_name: str,
+ queue_name: str,
+ session: SessionDep,
+) -> None:
+ """Add a queue to 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:
+ add_worker_queues(worker_name, [queue_name], session=session)
+ except Exception as e:
+ raise HTTPException(status.HTTP_400_BAD_REQUEST, detail=str(e))
+
+
+@ui_router.delete(
+ "/worker/{worker_name}/queues/{queue_name}",
+ dependencies=[
+ Depends(requires_access_view(access_view=AccessView.JOBS)),
+ ],
+)
+def remove_worker_queue(
+ worker_name: str,
+ queue_name: str,
+ session: SessionDep,
+) -> None:
+ """Remove a queue from 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:
+ remove_worker_queues(worker_name, [queue_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 ad26951de75..8a05e7f76e0 100644
--- a/providers/edge3/www-hash.txt
+++ b/providers/edge3/www-hash.txt
@@ -1 +1 @@
-9ade83e065f7115a0426e3c234a6071e671de25f9c78782bd2a2d087fbe30270
+00633c53624d560176f73d3cccc880dce2af5bb4ba0bc2dc46e7327a47817051