This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 6dee0b84b88 Fix dashboard pool summary deferred slot usage (#67818)
6dee0b84b88 is described below
commit 6dee0b84b88aab59291749bf6481aaeb8665e113
Author: deepinsight coder <[email protected]>
AuthorDate: Mon Jun 1 11:42:36 2026 -0700
Fix dashboard pool summary deferred slot usage (#67818)
* Fix dashboard pool summary deferred slot usage
* Add newsfragment for pool summary deferred slots
* Remove unnecessary pool summary newsfragment
* Stabilize Firefox Dag Calendar tooltip e2e test
* Hover calendar tooltip trigger in Firefox e2e test
* Stabilize calendar tooltip hover in Firefox e2e
* Show excluded deferred slots in pool summary
* Fix pool summary UI lint formatting
---
.../ui/public/i18n/locales/en/dashboard.json | 2 +
.../src/airflow/ui/src/components/PoolBar.test.tsx | 65 +++++++++
.../Dashboard/PoolSummary/PoolSummary.test.tsx | 150 +++++++++++++++++++++
.../pages/Dashboard/PoolSummary/PoolSummary.tsx | 26 +++-
4 files changed, 240 insertions(+), 3 deletions(-)
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
index 4504856227f..224bd8d8575 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dashboard.json
@@ -1,4 +1,6 @@
{
+ "deferredSlotsNotCounted": "Deferred not counted in slots: {{count}}",
+ "deferredSlotsNotCountedTooltip": "Deferred tasks shown in the bar are
counted against pool slots. Deferred tasks shown below the bar are from pools
that do not count deferred tasks against slots.",
"favorite": {
"favoriteDags_one": "First {{count}} favorite Dag",
"favoriteDags_other": "First {{count}} favorite Dags",
diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx
b/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx
new file mode 100644
index 00000000000..0352210b066
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx
@@ -0,0 +1,65 @@
+/*!
+ * 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 "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import type { PoolResponse } from "openapi/requests/types.gen";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { PoolBar } from "./PoolBar";
+
+const createPool = (pool: Partial<PoolResponse>): PoolResponse => ({
+ deferred_slots: 1,
+ description: null,
+ include_deferred: false,
+ name: "default_pool",
+ occupied_slots: 1,
+ open_slots: 128,
+ queued_slots: 0,
+ running_slots: 0,
+ scheduled_slots: 0,
+ slots: 128,
+ team_name: null,
+ ...pool,
+});
+
+describe("PoolBar", () => {
+ it("shows deferred slots as secondary info when they do not consume pool
slots", () => {
+ const { container } = render(
+ <PoolBar pool={createPool({ include_deferred: false })} totalSlots={128}
/>,
+ {
+ wrapper: Wrapper,
+ },
+ );
+
+
expect(container.querySelector('a[href*="task_state=deferred"]')).not.toBeInTheDocument();
+
expect(screen.getByText(/common:states\.deferred/u)).toHaveTextContent("1");
+ });
+
+ it("shows deferred slots in the usage bar when they consume pool slots", ()
=> {
+ const { container } = render(
+ <PoolBar pool={createPool({ include_deferred: true, open_slots: 127 })}
totalSlots={128} />,
+ { wrapper: Wrapper },
+ );
+
+
expect(container.querySelector('a[href*="task_state=deferred"]')).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ });
+});
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
new file mode 100644
index 00000000000..39bcbbcb181
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
@@ -0,0 +1,150 @@
+/*!
+ * 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 "@testing-library/jest-dom";
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import type { PoolResponse } from "openapi/requests/types.gen";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { PoolSummary } from "./PoolSummary";
+
+const mocks = vi.hoisted(() => ({
+ deferredSlotsNotCountedTooltip:
+ "Deferred tasks shown in the bar are counted against pool slots. " +
+ "Deferred tasks shown below the bar are from pools that do not count
deferred tasks against slots.",
+ useAuthLinksServiceGetAuthMenus: vi.fn(),
+ usePoolServiceGetPools: vi.fn(),
+}));
+
+vi.mock("openapi/queries", () => ({
+ useAuthLinksServiceGetAuthMenus: mocks.useAuthLinksServiceGetAuthMenus,
+}));
+
+vi.mock("openapi/queries/queries", () => ({
+ usePoolServiceGetPools: mocks.usePoolServiceGetPools,
+}));
+
+vi.mock("src/utils", () => ({
+ useAutoRefresh: () => false,
+}));
+
+vi.mock("react-i18next", () => ({
+ useTranslation: (namespace: string) => ({
+ // eslint-disable-next-line id-length
+ t: (key: string, options?: { count?: number }) => {
+ if (namespace === "dashboard" && key === "deferredSlotsNotCounted") {
+ return `Deferred not counted in slots: ${options?.count}`;
+ }
+
+ if (namespace === "dashboard" && key ===
"deferredSlotsNotCountedTooltip") {
+ return mocks.deferredSlotsNotCountedTooltip;
+ }
+
+ return `${namespace}:${key}`;
+ },
+ }),
+}));
+
+const createPool = (pool: Partial<PoolResponse>): PoolResponse => ({
+ deferred_slots: 1,
+ description: null,
+ include_deferred: false,
+ name: "default_pool",
+ occupied_slots: 1,
+ open_slots: 128,
+ queued_slots: 0,
+ running_slots: 0,
+ scheduled_slots: 0,
+ slots: 128,
+ team_name: null,
+ ...pool,
+});
+
+const renderPoolSummary = (pools: Array<PoolResponse>) => {
+ mocks.useAuthLinksServiceGetAuthMenus.mockReturnValue({
+ data: { authorized_menu_items: ["Pools"] },
+ });
+ mocks.usePoolServiceGetPools.mockReturnValue({
+ data: { pools, total_entries: pools.length },
+ error: undefined,
+ isLoading: false,
+ });
+
+ return render(<PoolSummary />, { wrapper: Wrapper });
+};
+
+describe("PoolSummary", () => {
+ beforeEach(() => {
+ mocks.useAuthLinksServiceGetAuthMenus.mockReset();
+ mocks.usePoolServiceGetPools.mockReset();
+ });
+
+ it("shows non-consuming deferred slots below the dashboard usage bar", () =>
{
+ renderPoolSummary([createPool({ deferred_slots: 32, include_deferred:
false })]);
+
+ expect(screen.getByText("128")).toBeInTheDocument();
+ expect(screen.queryByText("32")).not.toBeInTheDocument();
+ expect(screen.getByText("Deferred not counted in slots:
32")).toBeInTheDocument();
+ });
+
+ it("aggregates consuming deferred slots into the dashboard usage bar", () =>
{
+ renderPoolSummary([createPool({ include_deferred: true, open_slots: 127
})]);
+
+ expect(screen.getByText("127")).toBeInTheDocument();
+ expect(screen.getByText("1")).toBeInTheDocument();
+ expect(screen.queryByText(/Deferred not counted in
slots/u)).not.toBeInTheDocument();
+ });
+
+ it("shows both counted and non-counted deferred slots for mixed pools", ()
=> {
+ renderPoolSummary([
+ createPool({ deferred_slots: 32, include_deferred: false }),
+ createPool({ deferred_slots: 3, include_deferred: true, name:
"counting_pool", open_slots: 125 }),
+ ]);
+
+ expect(screen.getByText("253")).toBeInTheDocument();
+ expect(screen.getByText("3")).toBeInTheDocument();
+ expect(screen.queryByText("35")).not.toBeInTheDocument();
+ expect(screen.getByText("Deferred not counted in slots:
32")).toBeInTheDocument();
+ });
+
+ it("explains the dashboard deferred slot distinction in a tooltip", async ()
=> {
+ vi.useFakeTimers();
+
+ renderPoolSummary([createPool({ deferred_slots: 32, include_deferred:
false })]);
+
+ const deferredSlotsInfo = screen
+ .getByText("Deferred not counted in slots: 32")
+ .closest('[data-part="trigger"]');
+
+ expect(deferredSlotsInfo).not.toBeNull();
+
+ try {
+ await act(async () => {
+ fireEvent.focus(deferredSlotsInfo as Element);
+ fireEvent.pointerEnter(deferredSlotsInfo as Element);
+ await vi.advanceTimersByTimeAsync(500);
+ });
+
+
expect(screen.getByText(mocks.deferredSlotsNotCountedTooltip)).toBeInTheDocument();
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+});
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 b306bcfe3cd..065c43949d6 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
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box, Heading, Flex, Skeleton } from "@chakra-ui/react";
+import { Box, Flex, Heading, HStack, Skeleton, Text } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { BiTargetLock } from "react-icons/bi";
@@ -24,7 +24,8 @@ import { type PoolServiceGetPoolsDefaultResponse,
useAuthLinksServiceGetAuthMenu
import { usePoolServiceGetPools } from "openapi/queries/queries";
import type { ApiError } from "openapi/requests";
import { PoolBar, UNLIMITED_SLOTS } from "src/components/PoolBar";
-import { RouterLink } from "src/components/ui";
+import { StateIcon } from "src/components/StateIcon";
+import { RouterLink, Tooltip } from "src/components/ui";
import { useAutoRefresh } from "src/utils";
import { type Slots, slotKeys } from "src/utils/slots";
@@ -71,9 +72,16 @@ export const PoolSummary = () => {
running_slots: 0,
scheduled_slots: 0,
};
+ let deferredSlotsNotCounted = 0;
pools?.forEach((pool) => {
slotKeys.forEach((slotKey) => {
+ if (slotKey === "deferred_slots" && !pool.include_deferred) {
+ deferredSlotsNotCounted += pool.deferred_slots;
+
+ return;
+ }
+
const slotValue = pool[slotKey];
if (slotValue === UNLIMITED_SLOTS) {
@@ -107,7 +115,19 @@ export const PoolSummary = () => {
{isLoading ? (
<Skeleton borderRadius="full" h={8} w="100%" />
) : (
- <PoolBar pool={aggregatePool} poolsWithSlotType={poolsWithSlotType}
totalSlots={totalSlots} />
+ <>
+ <PoolBar pool={aggregatePool} poolsWithSlotType={poolsWithSlotType}
totalSlots={totalSlots} />
+ {deferredSlotsNotCounted > 0 ? (
+ <Tooltip content={translate("deferredSlotsNotCountedTooltip")}>
+ <HStack gap={1} mt={1} w="fit-content">
+ <StateIcon size={12} state="deferred" />
+ <Text color="fg.muted" fontSize="xs" fontWeight="medium">
+ {translate("deferredSlotsNotCounted", { count:
deferredSlotsNotCounted })}
+ </Text>
+ </HStack>
+ </Tooltip>
+ ) : undefined}
+ </>
)}
</Box>
);