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..30f43f73e11c3 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 ( - {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 = ( - - - {slot.icon} - - {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} + + ); + })} @@ -111,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/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..8be4f5d1d4732 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) => { - {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 ? ( 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")} )} />