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


Reply via email to