This is an automated email from the ASF dual-hosted git repository.

choo121600 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 b63857e2f45 fix(ui): handle pools with unlimited (-1) slots in UI 
components (#62831)
b63857e2f45 is described below

commit b63857e2f4591daaea5f8ff7ff17f2092c30f04b
Author: Antonio Mello <[email protected]>
AuthorDate: Fri Mar 13 00:35:55 2026 -0300

    fix(ui): handle pools with unlimited (-1) slots in UI components (#62831)
    
    * fix(ui): handle pools with unlimited (-1) slots in UI components
    
    When a pool has slots set to -1 (unlimited), the UI now properly
    handles this case instead of showing broken bars or negative values.
    
    Changes:
    - PoolBar: render infinity symbol and proportional bar for unlimited pools
    - PoolBarCard: display ∞ instead of -1 in pool header
    - PoolSummary: correctly aggregate slots when any pool is unlimited
    - PoolForm: set min to -1 and add helper text explaining the convention
    
    Closes: #61115
    
    * fix(ui): simplify PoolBar unlimited slots computation per review
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * fix(ui): rename short identifier in PoolBar filter/reduce
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * fix(ui): resolve linting and typing errors caught by prek
    
    - Fix indentation in PoolBar.tsx map callback (formatting)
    - Fix JSX line break in PoolBarCard.tsx (formatting)
    - Widen slotType cast to TaskInstanceState | "open" since
      open_slots produces "open" which is not a TaskInstanceState
    - Add explicit cast for StateIcon prop where "open" is already
      filtered out by infoSlots
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../airflow/ui/public/i18n/locales/en/admin.json   |  3 +-
 .../src/airflow/ui/src/components/PoolBar.tsx      | 93 ++++++++++++----------
 .../pages/Dashboard/PoolSummary/PoolSummary.tsx    | 16 +++-
 .../src/airflow/ui/src/pages/Pools/PoolBarCard.tsx |  6 +-
 .../src/airflow/ui/src/pages/Pools/PoolForm.tsx    |  3 +-
 5 files changed, 72 insertions(+), 49 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json
index 1f0136b906d..1b02ee5b9b4 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/admin.json
@@ -120,7 +120,8 @@
       "includeDeferred": "Include Deferred",
       "nameMaxLength": "Name can contain a maximum of 256 characters",
       "nameRequired": "Name is required",
-      "slots": "Slots"
+      "slots": "Slots",
+      "slotsHelperText": "Use -1 for unlimited slots."
     },
     "noPoolsFound": "No pools found",
     "pool_one": "Pool",
diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx 
b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx
index e19cce8ecdd..30f43f73e11 100644
--- a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx
+++ b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx
@@ -26,6 +26,8 @@ import { Tooltip } from "src/components/ui";
 import { SearchParamsKeys } from "src/constants/searchParams";
 import { type Slots, slotConfigs } from "src/utils/slots";
 
+export const UNLIMITED_SLOTS = -1;
+
 export const PoolBar = ({
   pool,
   poolsWithSlotType,
@@ -37,6 +39,7 @@ export const PoolBar = ({
 }) => {
   const { t: translate } = useTranslation("common");
 
+  const isUnlimited = totalSlots === UNLIMITED_SLOTS;
   const isDashboard = Boolean(poolsWithSlotType);
   const includeDeferredInBar = "include_deferred" in pool && 
pool.include_deferred;
   const barSlots = ["running", "queued", "open"];
@@ -51,59 +54,69 @@ export const PoolBar = ({
   }
 
   const preparedSlots = slotConfigs.map((config) => {
-    const slotType = config.key.replace("_slots", "") as TaskInstanceState;
+    const slotType = config.key.replace("_slots", "") as TaskInstanceState | 
"open";
+    const rawValue = (pool[config.key] as number | undefined) ?? 0;
 
     return {
       ...config,
       label: translate(`common:states.${slotType}`),
       slotType,
-      slotValue: (pool[config.key] as number | undefined) ?? 0,
+      slotValue: slotType === "open" && rawValue === UNLIMITED_SLOTS ? 
Infinity : rawValue,
     };
   });
 
+  const displayedSlots = preparedSlots.filter(
+    (slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0,
+  );
+  const usedSlots = displayedSlots
+    .filter((slot) => slot.slotType !== "open")
+    .reduce((sum, slot) => sum + slot.slotValue, 0);
+
   return (
     <VStack align="stretch" gap={1} w="100%">
       <Flex bg="bg.muted" borderRadius="md" h="20px" overflow="hidden" 
w="100%">
-        {preparedSlots
-          .filter((slot) => barSlots.includes(slot.slotType) && slot.slotValue 
> 0)
-          .map((slot) => {
-            const flexValue = slot.slotValue / totalSlots || 0;
+        {displayedSlots.map((slot) => {
+          const flexValue = isUnlimited
+            ? slot.slotType === "open"
+              ? Math.max(1, usedSlots) // open takes at least as much space as 
all used slots combined
+              : slot.slotValue
+            : slot.slotValue / totalSlots || 0;
 
-            const poolContent = (
-              <Tooltip content={slot.label} key={slot.key} showArrow={true}>
-                <Flex
-                  alignItems="center"
-                  bg={`${slot.color}.solid`}
-                  color={`${slot.color}.contrast`}
-                  gap={1}
-                  h="100%"
-                  justifyContent="center"
-                  overflow="hidden"
-                  px={1}
-                  w="100%"
-                >
-                  {slot.icon}
-                  <Text fontSize="xs" fontWeight="bold" truncate>
-                    {slot.slotValue}
-                  </Text>
-                </Flex>
-              </Tooltip>
-            );
+          const poolContent = (
+            <Tooltip content={slot.label} key={slot.key} showArrow={true}>
+              <Flex
+                alignItems="center"
+                bg={`${slot.color}.solid`}
+                color={`${slot.color}.contrast`}
+                gap={1}
+                h="100%"
+                justifyContent="center"
+                overflow="hidden"
+                px={1}
+                w="100%"
+              >
+                {slot.icon}
+                <Text fontSize="xs" fontWeight="bold" truncate>
+                  {slot.slotValue === Infinity ? "∞" : slot.slotValue}
+                </Text>
+              </Flex>
+            </Tooltip>
+          );
 
-            return slot.color !== "success" && "name" in pool ? (
-              <Link asChild flex={flexValue} key={slot.key}>
-                <RouterLink
-                  
to={`/task_instances?${SearchParamsKeys.STATE}=${slot.color}&${SearchParamsKeys.POOL}=${pool.name}`}
-                >
-                  {poolContent}
-                </RouterLink>
-              </Link>
-            ) : (
-              <Box flex={flexValue} key={slot.key}>
+          return slot.color !== "success" && "name" in pool ? (
+            <Link asChild flex={flexValue} key={slot.key}>
+              <RouterLink
+                
to={`/task_instances?${SearchParamsKeys.STATE}=${slot.color}&${SearchParamsKeys.POOL}=${pool.name}`}
+              >
                 {poolContent}
-              </Box>
-            );
-          })}
+              </RouterLink>
+            </Link>
+          ) : (
+            <Box flex={flexValue} key={slot.key}>
+              {poolContent}
+            </Box>
+          );
+        })}
       </Flex>
 
       <HStack gap={4} wrap="wrap">
@@ -111,7 +124,7 @@ export const PoolBar = ({
           .filter((slot) => infoSlots.includes(slot.slotType) && 
slot.slotValue > 0)
           .map((slot) => (
             <HStack gap={1} key={slot.key}>
-              <StateIcon size={12} state={slot.slotType} />
+              <StateIcon size={12} state={slot.slotType as TaskInstanceState} 
/>
               <Text color="fg.muted" fontSize="xs" fontWeight="medium">
                 {slot.label}: {slot.slotValue}
               </Text>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx
index 65f4ba79095..9955ad82a8d 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.tsx
@@ -24,7 +24,7 @@ import { Link as RouterLink } from "react-router-dom";
 import { type PoolServiceGetPoolsDefaultResponse, 
useAuthLinksServiceGetAuthMenus } from "openapi/queries";
 import { usePoolServiceGetPools } from "openapi/queries/queries";
 import type { ApiError } from "openapi/requests";
-import { PoolBar } from "src/components/PoolBar";
+import { PoolBar, UNLIMITED_SLOTS } from "src/components/PoolBar";
 import { useAutoRefresh } from "src/utils";
 import { type Slots, slotKeys } from "src/utils/slots";
 
@@ -52,7 +52,10 @@ export const PoolSummary = () => {
   }
 
   const pools = data?.pools;
-  const totalSlots = pools?.reduce((sum, pool) => sum + pool.slots, 0) ?? 0;
+  const hasUnlimitedPool = pools?.some((pool) => pool.slots === 
UNLIMITED_SLOTS) ?? false;
+  const totalSlots = hasUnlimitedPool
+    ? UNLIMITED_SLOTS
+    : (pools?.reduce((sum, pool) => sum + pool.slots, 0) ?? 0);
   const aggregatePool: Slots = {
     deferred_slots: 0,
     open_slots: 0,
@@ -73,8 +76,13 @@ export const PoolSummary = () => {
     slotKeys.forEach((slotKey) => {
       const slotValue = pool[slotKey];
 
-      if (slotValue > 0) {
-        aggregatePool[slotKey] += slotValue;
+      if (slotValue === UNLIMITED_SLOTS) {
+        aggregatePool[slotKey] = UNLIMITED_SLOTS;
+        poolsWithSlotType[slotKey] += 1;
+      } else if (slotValue > 0) {
+        if (aggregatePool[slotKey] !== UNLIMITED_SLOTS) {
+          aggregatePool[slotKey] += slotValue;
+        }
         poolsWithSlotType[slotKey] += 1;
       }
     });
diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx
index ded363b4fc3..8be4f5d1d47 100644
--- a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx
@@ -20,7 +20,7 @@ import { Box, Flex, HStack, Text, VStack } from 
"@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 
 import type { PoolResponse } from "openapi/requests/types.gen";
-import { PoolBar } from "src/components/PoolBar";
+import { PoolBar, UNLIMITED_SLOTS } from "src/components/PoolBar";
 import { StateIcon } from "src/components/StateIcon";
 import { Tooltip } from "src/components/ui";
 
@@ -40,8 +40,8 @@ const PoolBarCard = ({ pool }: PoolBarCardProps) => {
         <VStack align="start" flex="1">
           <HStack justifyContent="space-between" width="100%">
             <Text fontSize="lg" fontWeight="bold" whiteSpace="normal" 
wordBreak="break-word">
-              {pool.name} ({pool.slots} {translate("pools.form.slots")})
-              {pool.team_name !== null && ` (${pool.team_name})`}
+              {pool.name} ({pool.slots === UNLIMITED_SLOTS ? "∞" : pool.slots} 
{translate("pools.form.slots")}
+              ){pool.team_name !== null && ` (${pool.team_name})`}
               {pool.include_deferred ? (
                 <Tooltip content={translate("pools.deferredSlotsIncluded")}>
                   <StateIcon size={18} state="deferred" style={{ display: 
"inline", marginLeft: 6 }} />
diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx 
b/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx
index 9025ddc330a..6fe5784d995 100644
--- a/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Pools/PoolForm.tsx
@@ -91,7 +91,7 @@ const PoolForm = ({ error, initialPool, isPending, 
manageMutate, setError }: Poo
           <Field.Root mt={4}>
             <Field.Label 
fontSize="md">{translate("pools.form.slots")}</Field.Label>
             <Input
-              min={initialPool.slots}
+              min={-1}
               onChange={(event) => {
                 const value = event.target.valueAsNumber;
 
@@ -101,6 +101,7 @@ const PoolForm = ({ error, initialPool, isPending, 
manageMutate, setError }: Poo
               type="number"
               value={field.value}
             />
+            
<Field.HelperText>{translate("pools.form.slotsHelperText")}</Field.HelperText>
           </Field.Root>
         )}
       />

Reply via email to