From 31846e3718bbfcb3abae5c9872cee6bed7786d79 Mon Sep 17 00:00:00 2001 From: Antonio Date: Tue, 3 Mar 2026 20:54:02 -0300 Subject: [PATCH 1/4] fix(ui): handle pools with unlimited (-1) slots in UI components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ui/public/i18n/locales/en/admin.json | 3 ++- .../src/airflow/ui/src/components/PoolBar.tsx | 17 ++++++++++++++--- .../pages/Dashboard/PoolSummary/PoolSummary.tsx | 16 ++++++++++++---- .../airflow/ui/src/pages/Pools/PoolBarCard.tsx | 4 ++-- .../src/airflow/ui/src/pages/Pools/PoolForm.tsx | 3 ++- 5 files changed, 32 insertions(+), 11 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 1f0136b906d25..1b02ee5b9b448 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 e19cce8ecdd00..07262113b1f82 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"]; @@ -52,12 +55,13 @@ export const PoolBar = ({ const preparedSlots = slotConfigs.map((config) => { const slotType = config.key.replace("_slots", "") as TaskInstanceState; + 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, }; }); @@ -67,7 +71,14 @@ export const PoolBar = ({ {preparedSlots .filter((slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0) .map((slot) => { - const flexValue = slot.slotValue / totalSlots || 0; + const usedSlots = preparedSlots + .filter((s) => barSlots.includes(s.slotType) && s.slotValue > 0 && s.slotType !== "open") + .reduce((sum, s) => sum + s.slotValue, 0); + 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 = ( @@ -84,7 +95,7 @@ export const PoolBar = ({ > {slot.icon} - {slot.slotValue} + {slot.slotValue === Infinity ? "∞" : slot.slotValue} 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 65f4ba790958c..9955ad82a8d10 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 ded363b4fc34a..1088a0229a138 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,7 +40,7 @@ const PoolBarCard = ({ pool }: PoolBarCardProps) => { - {pool.name} ({pool.slots} {translate("pools.form.slots")}) + {pool.name} ({pool.slots === UNLIMITED_SLOTS ? "∞" : pool.slots} {translate("pools.form.slots")}) {pool.team_name !== null && ` (${pool.team_name})`} {pool.include_deferred ? ( 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 9025ddc330a90..6fe5784d99551 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 {translate("pools.form.slots")} { const value = event.target.valueAsNumber; @@ -101,6 +101,7 @@ const PoolForm = ({ error, initialPool, isPending, manageMutate, setError }: Poo type="number" value={field.value} /> + {translate("pools.form.slotsHelperText")} )} /> From 9f72b2f690ffbfda18e1cfc1dafa570c8a92eb07 Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 6 Mar 2026 14:57:07 -0300 Subject: [PATCH 2/4] fix(ui): simplify PoolBar unlimited slots computation per review Co-Authored-By: Claude Opus 4.6 --- .../src/airflow/ui/src/components/PoolBar.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx index 07262113b1f82..fbe5324cebab7 100644 --- a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx @@ -65,15 +65,17 @@ export const PoolBar = ({ }; }); + const displayedSlots = preparedSlots.filter( + (slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0, + ); + const usedSlots = displayedSlots + .filter((s) => s.slotType !== "open") + .reduce((sum, s) => sum + s.slotValue, 0); + return ( - {preparedSlots - .filter((slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0) - .map((slot) => { - const usedSlots = preparedSlots - .filter((s) => barSlots.includes(s.slotType) && s.slotValue > 0 && s.slotType !== "open") - .reduce((sum, s) => sum + s.slotValue, 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 From 1e07bb4d114fee29be4c532853c5c9613a041e1f Mon Sep 17 00:00:00 2001 From: Antonio Date: Wed, 11 Mar 2026 07:56:25 -0300 Subject: [PATCH 3/4] fix(ui): rename short identifier in PoolBar filter/reduce Co-Authored-By: Claude Opus 4.6 --- airflow-core/src/airflow/ui/src/components/PoolBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx index fbe5324cebab7..329c38278ed3b 100644 --- a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx @@ -69,8 +69,8 @@ export const PoolBar = ({ (slot) => barSlots.includes(slot.slotType) && slot.slotValue > 0, ); const usedSlots = displayedSlots - .filter((s) => s.slotType !== "open") - .reduce((sum, s) => sum + s.slotValue, 0); + .filter((slot) => slot.slotType !== "open") + .reduce((sum, slot) => sum + slot.slotValue, 0); return ( From 6ae58d3aadf2f056e2e1ebdf38f6c3d4e8d0449d Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 12 Mar 2026 13:40:53 -0300 Subject: [PATCH 4/4] 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 --- .../src/airflow/ui/src/components/PoolBar.tsx | 80 +++++++++---------- .../ui/src/pages/Pools/PoolBarCard.tsx | 4 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx index 329c38278ed3b..30f43f73e11c3 100644 --- a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx @@ -54,7 +54,7 @@ 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 { @@ -76,47 +76,47 @@ export const PoolBar = ({ {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 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 = ( - - - {slot.icon} - - {slot.slotValue === Infinity ? "∞" : slot.slotValue} - - - - ); + const poolContent = ( + + + {slot.icon} + + {slot.slotValue === Infinity ? "∞" : slot.slotValue} + + + + ); - return slot.color !== "success" && "name" in pool ? ( - - - {poolContent} - - - ) : ( - + return slot.color !== "success" && "name" in pool ? ( + + {poolContent} - - ); - })} + + + ) : ( + + {poolContent} + + ); + })} @@ -124,7 +124,7 @@ export const PoolBar = ({ .filter((slot) => infoSlots.includes(slot.slotType) && slot.slotValue > 0) .map((slot) => ( - + {slot.label}: {slot.slotValue} 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 1088a0229a138..8be4f5d1d4732 100644 --- a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx @@ -40,8 +40,8 @@ const PoolBarCard = ({ pool }: PoolBarCardProps) => { - {pool.name} ({pool.slots === UNLIMITED_SLOTS ? "∞" : 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 ? (