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>
   );

Reply via email to