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>
)}
/>