This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 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 250fbb22be1 Add notification UX for HITL actions (#68346)
250fbb22be1 is described below

commit 250fbb22be1145201077c9a15bf5390521680fbc
Author: hojeong park <[email protected]>
AuthorDate: Wed Jun 17 02:26:15 2026 +0900

    Add notification UX for HITL actions (#68346)
    
    * Add notification UX for HITL actions
    
    related: #64847
    
    * UI: Adjust HITL review list column widths
    
    Restore changes dropped during branch sync
    
    * UI: Simplify HITL review selection hook
    
    Restore changes dropped during branch sync
    
    * UI: Adjust Modal layout styling
    
    * UI: Replace task button with link
    
    * UI: Add horizontal scrolling to HITL review list
    
    * UI: Add horizontal scrolling to HITL review modal header
    
    * Add tooltip and reviewAll button on daglist
    
    ---------
    
    Co-authored-by: pierrejeambrun <[email protected]>
---
 .../airflow/ui/public/i18n/locales/en/hitl.json    |  14 ++
 .../src/components/HITLReview/HITLReviewDetail.tsx |  55 ++++++++
 .../HITLReview/HITLReviewDetailSummary.tsx         |  74 ++++++++++
 .../src/components/HITLReview/HITLReviewDrawer.tsx |  65 +++++++++
 .../src/components/HITLReview/HITLReviewList.tsx   | 115 +++++++++++++++
 .../HITLReview/HITLReviewListSection.tsx           |  57 ++++++++
 .../components/HITLReview/HITLReviewModal.test.tsx | 109 +++++++++++++++
 .../src/components/HITLReview/HITLReviewModal.tsx  | 155 +++++++++++++++++++++
 .../useHITLReviewModalRouteSync.test.tsx           |  78 +++++++++++
 .../HITLReview/useHITLReviewModalRouteSync.ts      |  58 ++++++++
 .../HITLReview/useHITLReviewModalSelection.test.ts |  72 ++++++++++
 .../HITLReview/useHITLReviewModalSelection.ts      |  54 +++++++
 .../airflow/ui/src/components/NeedsReviewBadge.tsx |  42 ++++--
 .../ui/src/components/NeedsReviewButton.tsx        | 141 ++++++++++++++++++-
 airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx  |  13 +-
 .../src/airflow/ui/src/pages/Dag/Header.tsx        |   2 +
 .../airflow/ui/src/pages/Dag/Overview/Overview.tsx |   2 -
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  |  10 +-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |   4 +-
 .../airflow/ui/src/pages/Dashboard/Stats/Stats.tsx |   4 +-
 .../pages/HITLTaskInstances/HITLResponseForm.tsx   |   9 +-
 .../pages/HITLTaskInstances/HITLTaskInstances.tsx  |  81 ++++++++++-
 .../src/airflow/ui/src/pages/Run/Header.tsx        |   2 +
 airflow-core/src/airflow/ui/src/pages/Run/Run.tsx  |  10 +-
 .../airflow/ui/src/queries/useUpdateHITLDetail.ts  |  11 +-
 airflow-core/src/airflow/ui/src/router.tsx         |  10 +-
 .../ui/tests/e2e/components/HITLReviewDrawer.ts    |  32 +++++
 .../ui/tests/e2e/components/HITLReviewModal.ts     |  32 +++++
 .../src/airflow/ui/tests/e2e/fixtures/data.ts      |  26 ++++
 .../src/airflow/ui/tests/e2e/fixtures/pom.ts       |  10 ++
 .../airflow/ui/tests/e2e/pages/DagDetailPage.ts    |  48 +++++++
 .../src/airflow/ui/tests/e2e/pages/DagRunPage.ts   |  48 +++++++
 .../src/airflow/ui/tests/e2e/pages/DagsPage.ts     |  21 +++
 .../src/airflow/ui/tests/e2e/pages/HomePage.ts     |   5 +
 .../ui/tests/e2e/pages/RequiredActionsPage.ts      |  29 +++-
 .../airflow/ui/tests/e2e/specs/dag-detail.spec.ts  |  43 ++++++
 .../src/airflow/ui/tests/e2e/specs/dag-run.spec.ts |  43 ++++++
 .../airflow/ui/tests/e2e/specs/dags-list.spec.ts   |  42 ++++++
 .../ui/tests/e2e/specs/home-dashboard.spec.ts      |  15 ++
 .../ui/tests/e2e/specs/requiredAction.spec.ts      |  11 ++
 .../src/airflow/ui/tests/e2e/utils/api/hitl.ts     |  22 +++
 41 files changed, 1610 insertions(+), 64 deletions(-)

diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json
index a2c6e95dc08..c0fd74bbfd4 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/hitl.json
@@ -24,6 +24,20 @@
     "success": "{{taskId}} response successful",
     "title": "Human Task Instance - {{taskId}}"
   },
+  "review": {
+    "detail": {
+      "selectRequiredAction": "Select a required action to see details"
+    },
+    "list": {
+      "completedRequiredActions": "Completed required actions ({{count}})",
+      "loadError": "Unable to load required actions",
+      "loadingActions": "Loading required actions...",
+      "pendingRequiredActions": "Pending required actions ({{count}})"
+    },
+    "openReviewDrawer": "Open Review Drawer",
+    "pageLimitHint": "Only the first few of actions are shown here. Use 
\"$t(review.viewAll)\" to see all of them.",
+    "viewAll": "View all required actions"
+  },
   "state": {
     "approvalReceived": "Approval Received",
     "approvalRequired": "Approval Required",
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetail.tsx 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetail.tsx
new file mode 100644
index 00000000000..b16bf656bea
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetail.tsx
@@ -0,0 +1,55 @@
+/*!
+ * 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 { Text, VStack } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+import { HITLResponseForm } from 
"src/pages/HITLTaskInstances/HITLResponseForm.tsx";
+
+import { HITLReviewDetailSummary } from "./HITLReviewDetailSummary.tsx";
+
+export const HITLReviewDetail = ({
+  detail,
+  onOpenTask,
+  onResponded,
+}: {
+  readonly detail?: HITLDetail;
+  readonly onOpenTask: () => void;
+  readonly onResponded: () => void;
+}) => {
+  const { t: translate } = useTranslation("hitl");
+
+  if (detail === undefined) {
+    return (
+      <VStack py={20}>
+        <Text 
color="fg.muted">{translate("review.detail.selectRequiredAction")}</Text>
+      </VStack>
+    );
+  }
+
+  const ti = detail.task_instance;
+
+  return (
+    <VStack alignItems="stretch" gap={4}>
+      <HITLReviewDetailSummary detail={detail} onOpenTask={onOpenTask} />
+
+      <HITLResponseForm hitlDetail={detail} key={ti.id} namespace={ti.id} 
onResponded={onResponded} />
+    </VStack>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
new file mode 100644
index 00000000000..f6d02117bc5
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDetailSummary.tsx
@@ -0,0 +1,74 @@
+/*!
+ * 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 { Table, Text } from "@chakra-ui/react";
+import type { ReactNode } from "react";
+import { useTranslation } from "react-i18next";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+import Time from "src/components/Time.tsx";
+import { RouterLink } from "src/components/ui/RouterLink.tsx";
+import { getRelativeTime } from "src/utils/datetimeUtils.ts";
+import { getTaskInstanceLink } from "src/utils/links.ts";
+
+const HITLReviewRow = ({ label, value }: { readonly label: string; readonly 
value: ReactNode }) => (
+  <Table.Row>
+    <Table.Cell w="30%">{label}</Table.Cell>
+    <Table.Cell>{value}</Table.Cell>
+  </Table.Row>
+);
+
+export const HITLReviewDetailSummary = ({
+  detail,
+  onOpenTask,
+}: {
+  readonly detail: HITLDetail;
+  readonly onOpenTask: () => void;
+}) => {
+  const { t: translate } = useTranslation(["hitl", "common"]);
+  const ti = detail.task_instance;
+  const mappedIndex = ti.rendered_map_index ?? (ti.map_index >= 0 ? 
ti.map_index : undefined);
+
+  return (
+    <Table.Root>
+      <Table.Body>
+        <HITLReviewRow label={translate("common:dagId")} value={ti.dag_id} />
+        <HITLReviewRow label={translate("common:dagRunId")} 
value={ti.dag_run_id} />
+        <HITLReviewRow label={translate("common:mapIndex")} value={mappedIndex 
?? "-"} />
+        <HITLReviewRow
+          label={translate("common:taskId")}
+          value={
+            <RouterLink onClick={onOpenTask} 
to={`${getTaskInstanceLink(ti)}/required_actions`}>
+              {ti.task_id}
+            </RouterLink>
+          }
+        />
+        <HITLReviewRow
+          label={translate("common:table.createdAt")}
+          value={
+            <Text>
+              <Time datetime={detail.created_at} />
+              {` (${getRelativeTime(detail.created_at)})`}
+            </Text>
+          }
+        />
+        <HITLReviewRow label={translate("common:tryNumber")} 
value={ti.try_number} />
+      </Table.Body>
+    </Table.Root>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDrawer.tsx 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDrawer.tsx
new file mode 100644
index 00000000000..df0b450d128
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewDrawer.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 { CloseButton, Drawer, Portal } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+
+import { HITLReviewDetail } from "./HITLReviewDetail.tsx";
+
+type HITLReviewDrawerProps = {
+  readonly detail?: HITLDetail;
+  readonly onClose: () => void;
+  readonly open: boolean;
+};
+
+export const HITLReviewDrawer = ({ detail, onClose, open }: 
HITLReviewDrawerProps) => {
+  const { t: translate } = useTranslation("hitl");
+
+  return (
+    <Drawer.Root
+      lazyMount
+      onOpenChange={(event) => {
+        if (!event.open) {
+          onClose();
+        }
+      }}
+      open={open}
+      size="xl"
+      unmountOnExit
+    >
+      <Portal>
+        <Drawer.Backdrop />
+        <Drawer.Positioner>
+          <Drawer.Content>
+            <Drawer.Header>
+              <Drawer.Title>{translate("requiredAction_other")}</Drawer.Title>
+            </Drawer.Header>
+            <Drawer.CloseTrigger asChild>
+              <CloseButton />
+            </Drawer.CloseTrigger>
+            <Drawer.Body>
+              <HITLReviewDetail detail={detail} onOpenTask={onClose} 
onResponded={onClose} />
+            </Drawer.Body>
+          </Drawer.Content>
+        </Drawer.Positioner>
+      </Portal>
+    </Drawer.Root>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewList.tsx 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewList.tsx
new file mode 100644
index 00000000000..9f33d0600d4
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewList.tsx
@@ -0,0 +1,115 @@
+/*!
+ * 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 { Table } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+import Time from "src/components/Time.tsx";
+
+const TableColumnHeader = ({ children, width }: { readonly children: string; 
readonly width?: string }) => (
+  <Table.ColumnHeader w={width}>{children}</Table.ColumnHeader>
+);
+
+const HITL_GROUP_COLORS = ["green.solid", "purple.solid"] as const;
+
+const getHitlGroupColor = (details: Array<HITLDetail>, index: number) => {
+  let groupIndex = 0;
+
+  for (let currentIndex = 0; currentIndex <= index; currentIndex += 1) {
+    const detail = details[currentIndex];
+    const previous = details[currentIndex - 1];
+
+    if (
+      detail !== undefined &&
+      previous !== undefined &&
+      detail.task_instance.dag_id !== previous.task_instance.dag_id
+    ) {
+      groupIndex += 1;
+    }
+  }
+
+  return HITL_GROUP_COLORS[groupIndex % HITL_GROUP_COLORS.length] ?? 
HITL_GROUP_COLORS[0];
+};
+
+export const HITLReviewList = ({
+  details,
+  onSelect,
+  selectedDetail,
+}: {
+  readonly details: Array<HITLDetail>;
+  readonly onSelect: (selection: HITLDetail) => void;
+  readonly selectedDetail?: HITLDetail;
+}) => {
+  const { t: translate } = useTranslation(["hitl", "common"]);
+
+  return (
+    <Table.Root minW="max-content">
+      <Table.Header>
+        <Table.Row>
+          <TableColumnHeader 
width="30%">{translate("common:dagId")}</TableColumnHeader>
+          <TableColumnHeader 
width="170px">{translate("common:dagRun_one")}</TableColumnHeader>
+          <TableColumnHeader 
width="90px">{translate("common:mapIndex")}</TableColumnHeader>
+          <TableColumnHeader>{translate("common:taskId")}</TableColumnHeader>
+          <TableColumnHeader 
width="170px">{translate("common:table.createdAt")}</TableColumnHeader>
+        </Table.Row>
+      </Table.Header>
+      <Table.Body>
+        {details.length === 0
+          ? null
+          : details.map((detail, index) => {
+              const isSelected = selectedDetail?.task_instance.id === 
detail.task_instance.id;
+              const ti = detail.task_instance;
+
+              return (
+                <Table.Row
+                  _hover={{ bg: isSelected ? "bg.muted" : "bg.subtle" }}
+                  aria-pressed={isSelected}
+                  bg={isSelected ? "bg.muted" : undefined}
+                  cursor="pointer"
+                  key={detail.task_instance.id}
+                  onClick={() => onSelect(detail)}
+                  onKeyDown={(event) => {
+                    if (event.key === "Enter" || event.key === " ") {
+                      event.preventDefault();
+                      onSelect(detail);
+                    }
+                  }}
+                  py={2}
+                  tabIndex={0}
+                >
+                  <Table.Cell borderLeftColor={getHitlGroupColor(details, 
index)} borderLeftWidth={3}>
+                    {ti.dag_id}
+                  </Table.Cell>
+                  <Table.Cell>
+                    <Time datetime={ti.run_after} />
+                  </Table.Cell>
+                  <Table.Cell>
+                    {ti.rendered_map_index ?? (ti.map_index === -1 ? undefined 
: String(ti.map_index))}
+                  </Table.Cell>
+                  <Table.Cell>{ti.task_id}</Table.Cell>
+                  <Table.Cell>
+                    <Time datetime={detail.created_at} />
+                  </Table.Cell>
+                </Table.Row>
+              );
+            })}
+      </Table.Body>
+    </Table.Root>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewListSection.tsx
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewListSection.tsx
new file mode 100644
index 00000000000..c3273727423
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewListSection.tsx
@@ -0,0 +1,57 @@
+/*!
+ * 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 { Heading, Text, VStack } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+
+import { HITLReviewList } from "./HITLReviewList.tsx";
+
+export const HITLReviewListSection = ({
+  details,
+  heading,
+  isError = false,
+  isLoading = false,
+  onSelect,
+  selectedDetail,
+}: {
+  readonly details?: Array<HITLDetail>;
+  readonly heading: string;
+  readonly isError?: boolean;
+  readonly isLoading?: boolean;
+  readonly onSelect: (selection: HITLDetail) => void;
+  readonly selectedDetail?: HITLDetail;
+}) => {
+  const { t: translate } = useTranslation("hitl");
+
+  return (
+    <VStack alignItems="stretch">
+      <Heading size="md">{heading}</Heading>
+      <VStack alignItems="stretch" borderRadius="md" borderWidth={1} 
overflowX="auto">
+        {isLoading ? (
+          <Text 
color="fg.muted">{translate("review.list.loadingActions")}</Text>
+        ) : isError ? (
+          <Text color="fg.error">{translate("review.list.loadError")}</Text>
+        ) : (
+          <HITLReviewList details={details ?? []} onSelect={onSelect} 
selectedDetail={selectedDetail} />
+        )}
+      </VStack>
+    </VStack>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.test.tsx
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.test.tsx
new file mode 100644
index 00000000000..ed10180ad41
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.test.tsx
@@ -0,0 +1,109 @@
+/*!
+ * 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/vitest";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { HITLReviewModal } from "./HITLReviewModal";
+
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    // eslint-disable-next-line id-length
+    t: (key: string) => key,
+  }),
+}));
+
+vi.mock("src/components/HITLReview/HITLReviewDetail.tsx", () => ({
+  HITLReviewDetail: () => null,
+}));
+
+const mockHitl = {
+  created_at: "2024-01-01T00:00:00Z",
+  options: ["Approve", "Reject"],
+  subject: "Test subject",
+  task_instance: {
+    dag_id: "test_dag",
+    dag_run_id: "test_run",
+    id: "pending",
+    map_index: -1,
+    rendered_map_index: null,
+    run_after: "2024-01-01T00:00:00Z",
+    state: "awaiting_input",
+    task_id: "task-pending",
+    try_number: 1,
+  },
+} as HITLDetail;
+
+const completedHitl = {
+  ...mockHitl,
+  task_instance: {
+    ...mockHitl.task_instance,
+    id: "completed",
+    task_id: "task-completed",
+  },
+};
+
+const renderModal = ({ withCompleted = false }: { readonly withCompleted?: 
boolean } = {}) =>
+  render(
+    <HITLReviewModal
+      completedHitl={withCompleted ? { data: [completedHitl] } : undefined}
+      onClose={vi.fn()}
+      open
+      pendingHitl={{ data: [mockHitl] }}
+    />,
+    { wrapper: Wrapper },
+  );
+
+describe("HITLReviewModal", () => {
+  it("renders the required actions dialog", () => {
+    renderModal();
+
+    expect(screen.getByRole("dialog", { name: "requiredAction_other" 
})).toBeInTheDocument();
+  });
+
+  it("does not render the completed filter when completed HITL data is not 
provided", () => {
+    renderModal();
+
+    expect(screen.queryByRole("button", { name: "filters.response.all" 
})).toBeNull();
+  });
+
+  it("renders the completed filter when completed HITL data is provided", () 
=> {
+    renderModal({ withCompleted: true });
+
+    expect(screen.getByRole("button", { name: "filters.response.all" 
})).toBeInTheDocument();
+  });
+
+  it("shows only pending HITL rows by default", () => {
+    renderModal({ withCompleted: true });
+
+    expect(screen.getByText("task-pending")).toBeInTheDocument();
+    expect(screen.queryByText("task-completed")).toBeNull();
+  });
+
+  it("shows completed HITL rows when switching to all required actions", () => 
{
+    renderModal({ withCompleted: true });
+
+    fireEvent.click(screen.getByRole("button", { name: "filters.response.all" 
}));
+
+    expect(screen.getByText("task-completed")).toBeInTheDocument();
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.tsx 
b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.tsx
new file mode 100644
index 00000000000..6da3e60f71d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/HITLReview/HITLReviewModal.tsx
@@ -0,0 +1,155 @@
+/*!
+ * 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 { Box, HStack, Icon, VStack } from "@chakra-ui/react";
+import type { ReactNode } from "react";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { FiInfo } from "react-icons/fi";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+import { HITLReviewDetail } from 
"src/components/HITLReview/HITLReviewDetail.tsx";
+import { Tooltip } from "src/components/ui";
+import { ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle";
+import { Dialog } from "src/components/ui/Dialog";
+
+import { HITLReviewListSection } from "./HITLReviewListSection.tsx";
+import { useHITLReviewModalSelection } from "./useHITLReviewModalSelection.ts";
+
+enum HITLReviewFilterMode {
+  ALL = "all",
+  PENDING = "pending",
+}
+
+type HITLReviewListState = {
+  readonly data: Array<HITLDetail>;
+  readonly isError?: boolean;
+  readonly isLoading?: boolean;
+  readonly total?: number;
+};
+
+const HITL_REVIEW_FILTER_OPTIONS: Array<{ labelKey: string; value: 
HITLReviewFilterMode }> = [
+  { labelKey: "filters.response.pending", value: HITLReviewFilterMode.PENDING 
},
+  { labelKey: "filters.response.all", value: HITLReviewFilterMode.ALL },
+];
+
+export const HITLReviewModal = ({
+  completedHitl,
+  headerAction,
+  onClose,
+  open,
+  pendingHitl,
+}: {
+  readonly completedHitl?: HITLReviewListState;
+  readonly headerAction?: ReactNode;
+  readonly onClose: () => void;
+  readonly open: boolean;
+  readonly pendingHitl: HITLReviewListState;
+}) => {
+  const { t: translate } = useTranslation("hitl");
+  const [selectedFilter, setSelectedFilter] = 
useState<HITLReviewFilterMode>(HITLReviewFilterMode.PENDING);
+  const shouldShowCompletedHitl = completedHitl !== undefined;
+  const visibleHitls =
+    shouldShowCompletedHitl && selectedFilter === HITLReviewFilterMode.ALL
+      ? [...pendingHitl.data, ...completedHitl.data]
+      : pendingHitl.data;
+
+  const { onNext, onSelect, selectedDetail } = useHITLReviewModalSelection({
+    hitlDetails: visibleHitls,
+  });
+  const handleClose = () => {
+    setSelectedFilter(HITLReviewFilterMode.PENDING);
+    onClose();
+  };
+
+  return (
+    <Dialog.Root
+      lazyMount
+      onOpenChange={(event) => {
+        if (!event.open) {
+          handleClose();
+        }
+      }}
+      open={open}
+      scrollBehavior="inside"
+      unmountOnExit
+    >
+      <Dialog.Content maxW="1440px" p={4}>
+        <Dialog.Header>
+          <HStack justifyContent="space-between" overflowX="auto" width="100%">
+            <HStack flexShrink={0} gap={1}>
+              <Dialog.Title>{translate("requiredAction_other")}</Dialog.Title>
+              <Tooltip content={translate("review.pageLimitHint")}>
+                <Icon color="fg.muted">
+                  <FiInfo />
+                </Icon>
+              </Tooltip>
+            </HStack>
+            <HStack gap={3}>
+              {headerAction}
+              {shouldShowCompletedHitl ? (
+                <ButtonGroupToggle<HITLReviewFilterMode>
+                  onChange={setSelectedFilter}
+                  options={HITL_REVIEW_FILTER_OPTIONS.map((option) => ({
+                    label: translate(option.labelKey),
+                    value: option.value,
+                  }))}
+                  value={selectedFilter}
+                />
+              ) : undefined}
+            </HStack>
+          </HStack>
+        </Dialog.Header>
+        <Dialog.CloseTrigger />
+        <Dialog.Body>
+          <HStack alignItems="stretch" flexDirection={{ base: "column", lg: 
"row" }} gap={6}>
+            <Box flex="2">
+              <VStack alignItems="stretch" gap={4}>
+                <HITLReviewListSection
+                  details={pendingHitl.data}
+                  heading={translate("review.list.pendingRequiredActions", {
+                    count: pendingHitl.total ?? pendingHitl.data.length,
+                  })}
+                  isError={pendingHitl.isError}
+                  isLoading={pendingHitl.isLoading}
+                  onSelect={onSelect}
+                  selectedDetail={selectedDetail}
+                />
+                {shouldShowCompletedHitl && selectedFilter === 
HITLReviewFilterMode.ALL ? (
+                  <HITLReviewListSection
+                    details={completedHitl.data}
+                    heading={translate("review.list.completedRequiredActions", 
{
+                      count: completedHitl.total ?? completedHitl.data.length,
+                    })}
+                    isError={completedHitl.isError}
+                    isLoading={completedHitl.isLoading}
+                    onSelect={onSelect}
+                    selectedDetail={selectedDetail}
+                  />
+                ) : null}
+              </VStack>
+            </Box>
+            <Box borderRadius="md" borderWidth={1} flex="1" mt={8} p={3}>
+              <HITLReviewDetail detail={selectedDetail} 
onOpenTask={handleClose} onResponded={onNext} />
+            </Box>
+          </HStack>
+        </Dialog.Body>
+      </Dialog.Content>
+    </Dialog.Root>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalRouteSync.test.tsx
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalRouteSync.test.tsx
new file mode 100644
index 00000000000..a557a4b2305
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalRouteSync.test.tsx
@@ -0,0 +1,78 @@
+/*!
+ * 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 { act, renderHook } from "@testing-library/react";
+import type { PropsWithChildren } from "react";
+import type * as ReactRouterDom from "react-router-dom";
+import { MemoryRouter } from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { BaseWrapper } from "src/utils/Wrapper";
+
+import { useHITLReviewModalRouteSync } from "./useHITLReviewModalRouteSync";
+
+const navigate = vi.hoisted(() => vi.fn());
+
+vi.mock("react-router-dom", async (importOriginal) => {
+  const actual = await importOriginal<typeof ReactRouterDom>();
+
+  return {
+    ...actual,
+    useNavigate: () => navigate,
+  };
+});
+
+const createWrapper =
+  (initialEntry = "/") =>
+  ({ children }: PropsWithChildren) => (
+    <BaseWrapper>
+      <MemoryRouter initialEntries={[initialEntry]}>{children}</MemoryRouter>
+    </BaseWrapper>
+  );
+
+beforeEach(() => {
+  navigate.mockClear();
+});
+
+describe("useHITLReviewModalRouteSync", () => {
+  it("opens the modal when the route points to required actions", () => {
+    const onClose = vi.fn();
+    const onOpen = vi.fn();
+
+    renderHook(() => useHITLReviewModalRouteSync({ onClose, onOpen }), {
+      wrapper: 
createWrapper("/dags/example_dag/tasks/example_task/required_actions"),
+    });
+
+    expect(onOpen).toHaveBeenCalledTimes(1);
+    expect(onClose).not.toHaveBeenCalled();
+  });
+
+  it("removes the required actions route when closing the HITL review", () => {
+    const onClose = vi.fn();
+    const { result } = renderHook(() => useHITLReviewModalRouteSync({ onClose, 
onOpen: vi.fn() }), {
+      wrapper: 
createWrapper("/dags/example_dag/tasks/example_task/required_actions"),
+    });
+
+    act(() => {
+      result.current.onCloseHITLReview();
+    });
+
+    expect(onClose).toHaveBeenCalledTimes(1);
+    
expect(navigate).toHaveBeenCalledWith("/dags/example_dag/tasks/example_task", { 
replace: true });
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalRouteSync.ts
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalRouteSync.ts
new file mode 100644
index 00000000000..a4778e1ef6a
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalRouteSync.ts
@@ -0,0 +1,58 @@
+/*!
+ * 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 { useEffect, useRef } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+
+export const useHITLReviewModalRouteSync = ({
+  onClose,
+  onOpen,
+}: {
+  readonly onClose: () => void;
+  readonly onOpen: () => void;
+}) => {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const wasOpenedFromRouteRef = useRef(false);
+  const isHITLReviewRoute = location.pathname.endsWith("/required_actions");
+
+  useEffect(() => {
+    if (isHITLReviewRoute) {
+      wasOpenedFromRouteRef.current = true;
+      onOpen();
+    } else if (wasOpenedFromRouteRef.current) {
+      wasOpenedFromRouteRef.current = false;
+      onClose();
+    }
+  }, [isHITLReviewRoute, onClose, onOpen]);
+
+  const onCloseHITLReview = () => {
+    wasOpenedFromRouteRef.current = false;
+    onClose();
+
+    if (isHITLReviewRoute) {
+      const redirectPath = location.pathname.replace(/\/required_actions$/u, 
"") || "/";
+
+      void Promise.resolve(navigate(redirectPath, { replace: true }));
+    }
+  };
+
+  return {
+    onCloseHITLReview,
+  };
+};
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalSelection.test.ts
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalSelection.test.ts
new file mode 100644
index 00000000000..cb544c7377c
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalSelection.test.ts
@@ -0,0 +1,72 @@
+/*!
+ * 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 { act, renderHook } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+
+import { useHITLReviewModalSelection } from "./useHITLReviewModalSelection";
+
+const hitl = (id: string) =>
+  ({
+    task_instance: { id },
+  }) as HITLDetail;
+
+describe("useHITLReviewModalSelection", () => {
+  it("does not select a HITL detail when the list is empty", () => {
+    const { result } = renderHook(() => useHITLReviewModalSelection({ 
hitlDetails: [] }));
+
+    expect(result.current.selectedDetail).toBeUndefined();
+  });
+
+  it("selects the first HITL detail by default", () => {
+    const details = [hitl("first"), hitl("second")];
+
+    const { result } = renderHook(() => useHITLReviewModalSelection({ 
hitlDetails: details }));
+
+    expect(result.current.selectedDetail).toBe(details[0]);
+  });
+
+  it("moves selection forward", () => {
+    const details = [hitl("first"), hitl("second")];
+
+    const { result } = renderHook(() => useHITLReviewModalSelection({ 
hitlDetails: details }));
+
+    act(() => result.current.onNext());
+    expect(result.current.selectedDetail).toBe(details[1]);
+  });
+
+  it("falls back to the first HITL detail when the selected key is no longer 
visible", () => {
+    const first = hitl("first");
+    const second = hitl("second");
+    const details = [first, second];
+
+    const { rerender, result } = renderHook(
+      ({ hitlDetails }: { readonly hitlDetails: Array<HITLDetail> }) =>
+        useHITLReviewModalSelection({ hitlDetails }),
+      { initialProps: { hitlDetails: details } },
+    );
+
+    act(() => result.current.onNext());
+    expect(result.current.selectedDetail).toBe(second);
+
+    rerender({ hitlDetails: [first] });
+    expect(result.current.selectedDetail).toBe(first);
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalSelection.ts
 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalSelection.ts
new file mode 100644
index 00000000000..11e704407e9
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/HITLReview/useHITLReviewModalSelection.ts
@@ -0,0 +1,54 @@
+/*!
+ * 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 { useState } from "react";
+
+import type { HITLDetail } from "openapi/requests/types.gen.ts";
+
+export const useHITLReviewModalSelection = ({ hitlDetails }: { readonly 
hitlDetails: Array<HITLDetail> }) => {
+  const [selectedHITLDetailKey, setSelectedHITLDetailKey] = useState<string | 
undefined>(undefined);
+
+  const selectedIndex = (() => {
+    if (hitlDetails.length === 0) {
+      return -1;
+    }
+
+    const selectedIndexFromKey = hitlDetails.findIndex(
+      (hitlDetail) => hitlDetail.task_instance.id === selectedHITLDetailKey,
+    );
+
+    return selectedIndexFromKey === -1 ? 0 : selectedIndexFromKey;
+  })();
+  const isSelected = selectedIndex !== -1;
+
+  const hasNext = isSelected && selectedIndex < hitlDetails.length - 1;
+
+  const onSelect = (hitl?: HITLDetail) => 
setSelectedHITLDetailKey(hitl?.task_instance.id);
+
+  const onNext = () => {
+    if (hasNext) {
+      onSelect(hitlDetails[selectedIndex + 1]);
+    }
+  };
+
+  return {
+    onNext,
+    onSelect,
+    selectedDetail: isSelected ? hitlDetails[selectedIndex] : undefined,
+  };
+};
diff --git a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
index e270a32e455..4302a54b2b4 100644
--- a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
@@ -16,38 +16,50 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { Button, useDisclosure } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 import { LuUserRoundPen } from "react-icons/lu";
-import { Link as RouterLink } from "react-router-dom";
+import { Link } from "react-router-dom";
 
 import type { HITLDetail } from "openapi/requests/types.gen";
+import { HITLReviewModal } from 
"src/components/HITLReview/HITLReviewModal.tsx";
 import { StateBadge } from "src/components/StateBadge";
 import { Tooltip } from "src/components/ui";
-import { SearchParamsKeys } from "src/constants/searchParams";
 
 type Props = {
-  readonly dagId: string;
   readonly pendingActions: Array<HITLDetail>;
 };
 
-export const NeedsReviewBadge = ({ dagId, pendingActions }: Props) => {
+export const NeedsReviewBadge = ({ pendingActions }: Props) => {
   const { t: translate } = useTranslation("hitl");
+  const { onClose, onOpen, open } = useDisclosure();
 
   if (pendingActions.length === 0) {
     return undefined;
   }
 
   return (
-    <Tooltip content={translate("requiredActionCount", { count: 
pendingActions.length })}>
-      <RouterLink
-        data-testid="needs-review-badge"
-        
to={`/dags/${dagId}/required_actions?${SearchParamsKeys.RESPONSE_RECEIVED}=false`}
-      >
-        <StateBadge colorPalette="awaiting_input" fontSize="md" 
variant="solid">
-          <LuUserRoundPen />
-          {pendingActions.length}
-        </StateBadge>
-      </RouterLink>
-    </Tooltip>
+    <>
+      <Tooltip content={translate("requiredActionCount", { count: 
pendingActions.length })}>
+        <Button data-testid="needs-review-badge" onClick={onOpen} 
variant="plain">
+          <StateBadge colorPalette="awaiting_input" fontSize="md" 
variant="solid">
+            <LuUserRoundPen />
+            {pendingActions.length}
+          </StateBadge>
+        </Button>
+      </Tooltip>
+      <HITLReviewModal
+        headerAction={
+          <Button asChild size="sm" variant="outline">
+            <Link onClick={onClose} 
to="/required_actions?response_received=false">
+              {translate("review.viewAll")}
+            </Link>
+          </Button>
+        }
+        onClose={onClose}
+        open={open}
+        pendingHitl={{ data: pendingActions, isError: false, isLoading: false 
}}
+      />
+    </>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
index ade24b26f78..a48252b2d8f 100644
--- a/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewButton.tsx
@@ -16,16 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box } from "@chakra-ui/react";
+import { Box, Button, useDisclosure } from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 import { LuUserRoundPen } from "react-icons/lu";
+import { Link } from "react-router-dom";
 
 import { useTaskInstanceServiceGetHitlDetails } from "openapi/queries";
+import { HITLReviewModal } from 
"src/components/HITLReview/HITLReviewModal.tsx";
+import { useHITLReviewModalRouteSync } from 
"src/components/HITLReview/useHITLReviewModalRouteSync.ts";
 import { useAutoRefresh } from "src/utils/query";
 
 import { StatsCard } from "./StatsCard";
 
-export const NeedsReviewButton = ({
+const usePendingHitl = ({
   dagId,
   runId,
   taskId,
@@ -36,10 +39,15 @@ export const NeedsReviewButton = ({
 }) => {
   const refetchInterval = useAutoRefresh({ checkPendingRuns: true, dagId });
 
-  const { data: hitlStatsData, isLoading } = 
useTaskInstanceServiceGetHitlDetails(
+  const {
+    data: pendingHitlData,
+    isError,
+    isLoading,
+  } = useTaskInstanceServiceGetHitlDetails(
     {
       dagId: dagId ?? "~",
       dagRunId: runId ?? "~",
+      orderBy: ["dag_id", "run_after", "created_at", "task_display_name"],
       responseReceived: false,
       state: ["deferred", "awaiting_input"],
       taskId,
@@ -50,7 +58,53 @@ export const NeedsReviewButton = ({
     },
   );
 
-  const hitlTIsCount = hitlStatsData?.hitl_details.length ?? 0;
+  return {
+    isError,
+    isLoading,
+    pendingHitlData,
+  };
+};
+
+const useCompletedHitl = ({
+  dagId,
+  enabled,
+  runId,
+}: {
+  readonly dagId?: string;
+  readonly enabled: boolean;
+  readonly runId?: string;
+}) => {
+  const {
+    data: completedHitlData,
+    isError,
+    isLoading,
+  } = useTaskInstanceServiceGetHitlDetails(
+    {
+      dagId: dagId ?? "~",
+      dagRunId: runId ?? "~",
+      orderBy: ["dag_id", "run_after", "created_at", "task_display_name"],
+      responseReceived: true,
+    },
+    undefined,
+    {
+      enabled,
+    },
+  );
+
+  return { completedHitlData, isError, isLoading };
+};
+
+const NeedsReviewButtonCard = ({
+  hitlTIsCount,
+  isLoading,
+  link,
+  onClick,
+}: {
+  readonly hitlTIsCount: number;
+  readonly isLoading: boolean;
+  readonly link?: string;
+  readonly onClick?: () => void;
+}) => {
   const { i18n, t: translate } = useTranslation("hitl");
 
   const isRTL = i18n.dir() === "rtl";
@@ -64,8 +118,85 @@ export const NeedsReviewButton = ({
         isLoading={isLoading}
         isRTL={isRTL}
         label={translate("requiredAction_other")}
-        link="required_actions?response_received=false"
+        link={link}
+        onClick={onClick}
       />
     </Box>
   ) : undefined;
 };
+
+export const NeedsReviewButton = ({
+  dagId,
+  runId,
+  taskId,
+}: {
+  readonly dagId?: string;
+  readonly runId?: string;
+  readonly taskId?: string;
+}) => {
+  const { isLoading, pendingHitlData } = usePendingHitl({ dagId, runId, taskId 
});
+  const hitlTIsCount = pendingHitlData?.total_entries ?? 0;
+
+  return (
+    <NeedsReviewButtonCard
+      hitlTIsCount={hitlTIsCount}
+      isLoading={isLoading}
+      link="required_actions?response_received=false"
+    />
+  );
+};
+
+export const NeedsReviewButtonWithModal = ({
+  dagId,
+  runId,
+}: {
+  readonly dagId?: string;
+  readonly runId?: string;
+}) => {
+  const { onClose, onOpen, open } = useDisclosure();
+  const { onCloseHITLReview } = useHITLReviewModalRouteSync({
+    onClose,
+    onOpen,
+  });
+  const { isError: pendingHitlIsError, isLoading, pendingHitlData } = 
usePendingHitl({ dagId, runId });
+  const {
+    completedHitlData,
+    isError: completedHitlIsError,
+    isLoading: isLoadingCompletedHitl,
+  } = useCompletedHitl({
+    dagId,
+    enabled: open,
+    runId,
+  });
+  const { t: translate } = useTranslation("hitl");
+  const hitlTIsCount = pendingHitlData?.total_entries ?? 0;
+
+  return (
+    <>
+      <NeedsReviewButtonCard hitlTIsCount={hitlTIsCount} isLoading={isLoading} 
onClick={onOpen} />
+      <HITLReviewModal
+        completedHitl={{
+          data: completedHitlData?.hitl_details ?? [],
+          isError: completedHitlIsError,
+          isLoading: isLoadingCompletedHitl,
+          total: completedHitlData?.total_entries,
+        }}
+        headerAction={
+          <Button asChild size="sm" variant="outline">
+            <Link onClick={onCloseHITLReview} 
to="/required_actions?response_received=false">
+              {translate("review.viewAll")}
+            </Link>
+          </Button>
+        }
+        onClose={onCloseHITLReview}
+        open={open}
+        pendingHitl={{
+          data: pendingHitlData?.hitl_details ?? [],
+          isError: pendingHitlIsError,
+          isLoading,
+          total: pendingHitlData?.total_entries,
+        }}
+      />
+    </>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx
index ff1bc4b9495..2d1f3bd8f6a 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx
@@ -19,7 +19,7 @@
 import { ReactFlowProvider } from "@xyflow/react";
 import { useEffect, useRef, useState } from "react";
 import { useTranslation } from "react-i18next";
-import { FiBarChart, FiCode, FiUser, FiCalendar } from "react-icons/fi";
+import { FiBarChart, FiCode, FiCalendar } from "react-icons/fi";
 import { LuChartColumn } from "react-icons/lu";
 import { MdDetails, MdOutlineEventNote } from "react-icons/md";
 import { RiArrowGoBackFill } from "react-icons/ri";
@@ -29,7 +29,6 @@ import { useDagServiceGetDagDetails, 
useDagServiceGetLatestRunInfo } from "opena
 import { ApiError } from "openapi/requests/core/ApiError";
 import { TaskIcon } from "src/assets/TaskIcon";
 import { usePluginTabs } from "src/hooks/usePluginTabs";
-import { useRequiredActionTabs } from "src/hooks/useRequiredActionTabs";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
 import { useRefreshOnNewDagRuns } from "src/queries/useRefreshOnNewDagRuns";
 import { isStatePending, useAutoRefresh, useDocumentTitle } from "src/utils";
@@ -49,7 +48,6 @@ export const Dag = () => {
     { icon: <FiBarChart />, label: translate("tabs.runs"), value: "runs" },
     { icon: <TaskIcon />, label: translate("tabs.tasks"), value: "tasks" },
     { icon: <FiCalendar />, label: translate("tabs.calendar"), value: 
"calendar" },
-    { icon: <FiUser />, label: translate("tabs.requiredActions"), value: 
"required_actions" },
     { icon: <RiArrowGoBackFill />, label: translate("tabs.backfills"), value: 
"backfills" },
     { icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: 
"events" },
     { icon: <FiCode />, label: translate("tabs.code"), value: "code" },
@@ -123,14 +121,7 @@ export const Dag = () => {
     },
   );
 
-  const { tabs: processedTabs } = useRequiredActionTabs({ dagId }, tabs, {
-    refetchInterval:
-      (dag?.active_runs_count ?? 0) > 0 || (latestRun && 
isStatePending(latestRun.state))
-        ? refetchInterval
-        : false,
-  });
-
-  const displayTabs = processedTabs.filter((tab) => {
+  const displayTabs = tabs.filter((tab) => {
     if (dag?.timetable_summary === null && tab.value === "backfills") {
       return false;
     }
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
index 9e3d915b079..419db668c81 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
@@ -29,6 +29,7 @@ import DagRunInfo from "src/components/DagRunInfo";
 import { DagVersion } from "src/components/DagVersion";
 import DisplayMarkdownButton from "src/components/DisplayMarkdownButton";
 import { HeaderCard } from "src/components/HeaderCard";
+import { NeedsReviewButtonWithModal } from "src/components/NeedsReviewButton";
 import { TogglePause } from "src/components/TogglePause";
 import { RouterLink } from "src/components/ui";
 
@@ -131,6 +132,7 @@ export const Header = ({
         dag === undefined ? undefined : (
           <>
             <DeadlineAlertsBadge dagId={dag.dag_id} />
+            <NeedsReviewButtonWithModal dagId={dag.dag_id} />
             {dag.doc_md === null ? undefined : (
               <DisplayMarkdownButton
                 header={translate("dagDetails.documentation")}
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
index 0d81a888031..d66be6299c4 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx
@@ -30,7 +30,6 @@ import {
 } from "openapi/queries";
 import { AssetEvents } from "src/components/Assets/AssetEvents";
 import { DurationChart } from "src/components/DurationChart";
-import { NeedsReviewButton } from "src/components/NeedsReviewButton";
 import TimeRangeSelector from "src/components/TimeRangeSelector";
 import { TrendCountButton } from "src/components/TrendCountButton";
 import { dagRunsLimitKey } from "src/constants/localStorage";
@@ -87,7 +86,6 @@ export const Overview = () => {
 
   return (
     <Box m={4} spaceY={4}>
-      <NeedsReviewButton dagId={dagId} />
       <Box my={2}>
         <TimeRangeSelector
           defaultValue={defaultHour}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index aefe80c5aaa..9eef74269d6 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -45,7 +45,13 @@ export const DagCard = ({ dag }: Props) => {
   const refetchInterval = useAutoRefresh({});
 
   return (
-    <Box borderColor="border.emphasized" borderRadius={8} borderWidth={1} 
overflow="hidden">
+    <Box
+      borderColor="border.emphasized"
+      borderRadius={8}
+      borderWidth={1}
+      data-testid="dag-card"
+      overflow="hidden"
+    >
       <Flex alignItems="center" bg="bg.muted" justifyContent="space-between" 
px={3} py={1}>
         <HStack>
           <Tooltip content={dag.description} 
disabled={!Boolean(dag.description)}>
@@ -56,7 +62,7 @@ export const DagCard = ({ dag }: Props) => {
           <DagTags tags={dag.tags} />
         </HStack>
         <HStack gap={1}>
-          <NeedsReviewBadge dagId={dag.dag_id} 
pendingActions={dag.pending_actions} />
+          <NeedsReviewBadge pendingActions={dag.pending_actions} />
           <TogglePause dagDisplayName={dag.dag_display_name} 
dagId={dag.dag_id} isPaused={dag.is_paused} />
           <TriggerDAGButton
             allowedRunTypes={dag.allowed_run_types}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index 0e26e363c66..eaedd94406a 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -133,9 +133,7 @@ const createColumns = (
   },
   {
     accessorKey: "pending_actions",
-    cell: ({ row: { original: dag } }) => (
-      <NeedsReviewBadge dagId={dag.dag_id} 
pendingActions={dag.pending_actions} />
-    ),
+    cell: ({ row: { original: dag } }) => <NeedsReviewBadge 
pendingActions={dag.pending_actions} />,
     enableSorting: false,
     header: "",
   },
diff --git a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
index 6fb64d2659d..25545dfac50 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.tsx
@@ -21,7 +21,7 @@ import { useTranslation } from "react-i18next";
 import { FiClipboard, FiZap } from "react-icons/fi";
 
 import { useDashboardServiceDagStats } from "openapi/queries";
-import { NeedsReviewButton } from "src/components/NeedsReviewButton";
+import { NeedsReviewButtonWithModal } from "src/components/NeedsReviewButton";
 import { StatsCard } from "src/components/StatsCard";
 import { useAutoRefresh } from "src/utils";
 
@@ -52,7 +52,7 @@ export const Stats = () => {
       </Flex>
 
       <Flex flexWrap="wrap" gap={4}>
-        <NeedsReviewButton />
+        <NeedsReviewButtonWithModal />
 
         <StatsCard
           colorScheme="failed"
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx
index c8b1fd4db64..b6eda5ccaa6 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLResponseForm.tsx
@@ -34,6 +34,8 @@ type HITLResponseFormProps = {
   readonly hitlDetail: {
     task_instance: TaskInstanceHistoryResponse;
   } & Omit<HITLDetailHistory, "task_instance">;
+  readonly namespace?: string;
+  readonly onResponded?: () => void;
 };
 
 const isHighlightOption = (
@@ -54,11 +56,11 @@ const isHighlightOption = (
   return isSelected ?? isDefault ?? !Boolean(hitlDetail.defaults);
 };
 
-export const HITLResponseForm = ({ hitlDetail }: HITLResponseFormProps) => {
+export const HITLResponseForm = ({ hitlDetail, namespace = "hitl", onResponded 
}: HITLResponseFormProps) => {
   const { t: translate } = useTranslation("hitl");
   const [errors, setErrors] = useState<boolean>(false);
   const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
-  const { paramsDict } = useParamStore("hitl");
+  const { paramsDict } = useParamStore(namespace);
   const [searchParams] = useSearchParams();
   const { preloadedHITLOptions } = getPreloadHITLFormData(searchParams, 
hitlDetail);
 
@@ -76,6 +78,7 @@ export const HITLResponseForm = ({ hitlDetail }: 
HITLResponseFormProps) => {
     dagId: hitlDetail.task_instance.dag_id,
     dagRunId: hitlDetail.task_instance.dag_run_id,
     mapIndex: hitlDetail.task_instance.map_index,
+    onSuccess: onResponded,
     taskId: hitlDetail.task_instance.task_id,
   });
 
@@ -118,7 +121,7 @@ export const HITLResponseForm = ({ hitlDetail }: 
HITLResponseFormProps) => {
           }}
           isHITL
           key={hitlDetail.subject}
-          namespace="hitl"
+          namespace={namespace}
           noAccordion
           setError={setErrors}
         />
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
index 4ad3c945cf4..71a0faaaf7b 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
@@ -16,10 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { VStack } from "@chakra-ui/react";
+import { HStack, VStack } from "@chakra-ui/react";
 import type { ColumnDef } from "@tanstack/react-table";
 import type { TFunction } from "i18next";
+import type { ReactNode } from "react";
+import { useState } from "react";
 import { useTranslation } from "react-i18next";
+import { LuPanelRightOpen } from "react-icons/lu";
 import { useParams, useSearchParams } from "react-router-dom";
 
 import { useTaskInstanceServiceGetHitlDetails } from "openapi/queries";
@@ -27,10 +30,11 @@ import type { HITLDetail } from 
"openapi/requests/types.gen";
 import { DataTable } from "src/components/DataTable";
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
+import { HITLReviewDrawer } from 
"src/components/HITLReview/HITLReviewDrawer.tsx";
 import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { TruncatedText } from "src/components/TruncatedText";
-import { RouterLink } from "src/components/ui";
+import { IconButton, RouterLink } from "src/components/ui";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
 import { useAutoRefresh } from "src/utils";
@@ -54,13 +58,50 @@ const {
   TASK_ID_PATTERN,
 }: SearchParamsKeysType = SearchParamsKeys;
 
+const HITLReviewDrawerButton = ({
+  detail,
+  onOpen,
+}: {
+  readonly detail: HITLDetail;
+  readonly onOpen: (detail: HITLDetail) => void;
+}) => {
+  const { t: translate } = useTranslation("hitl");
+
+  return (
+    <IconButton label={translate("review.openReviewDrawer")} onClick={() => 
onOpen(detail)}>
+      <LuPanelRightOpen />
+    </IconButton>
+  );
+};
+
+const useHITLReviewDrawer = () => {
+  const [selectedDetail, setSelectedDetail] = useState<HITLDetail | 
undefined>(undefined);
+
+  const openHITLReviewDrawer = (detail: HITLDetail) => {
+    setSelectedDetail(detail);
+  };
+
+  const closeHITLReviewDrawer = () => {
+    setSelectedDetail(undefined);
+  };
+
+  return {
+    closeHITLReviewDrawer,
+    isHITLReviewDrawerOpen: selectedDetail !== undefined,
+    openHITLReviewDrawer,
+    selectedDetail,
+  };
+};
+
 const taskInstanceColumns = ({
   dagId,
+  renderHITLReviewDrawerButton,
   runId,
   taskId,
   translate,
 }: {
   dagId?: string;
+  renderHITLReviewDrawerButton?: (detail: HITLDetail) => ReactNode;
   runId?: string;
   taskId?: string;
   translate: TFunction;
@@ -68,14 +109,21 @@ const taskInstanceColumns = ({
   {
     accessorKey: "task_instance_state",
     cell: ({ row: { original } }: HITLRow) => (
-      <StateBadge 
state={original.task_instance.state}>{getHITLState(translate, 
original)}</StateBadge>
+      <HStack justifyContent="space-between">
+        <StateBadge 
state={original.task_instance.state}>{getHITLState(translate, 
original)}</StateBadge>
+        {renderHITLReviewDrawerButton?.(original)}
+      </HStack>
     ),
     header: translate("requiredActionState"),
   },
   {
     accessorKey: "subject",
     cell: ({ row: { original } }: HITLRow) => (
-      <RouterLink fontWeight="bold" 
to={`${getTaskInstanceLink(original.task_instance)}/required_actions`}>
+      <RouterLink
+        fontWeight="bold"
+        onClick={(event) => event.stopPropagation()}
+        to={`${getTaskInstanceLink(original.task_instance)}/required_actions`}
+      >
         <TruncatedText text={original.subject} />
       </RouterLink>
     ),
@@ -87,7 +135,10 @@ const taskInstanceColumns = ({
         {
           accessorKey: "task_instance.dag_id",
           cell: ({ row: { original } }: HITLRow) => (
-            <RouterLink to={`/dags/${original.task_instance.dag_id}`}>
+            <RouterLink
+              onClick={(event) => event.stopPropagation()}
+              to={`/dags/${original.task_instance.dag_id}`}
+            >
               <TruncatedText text={original.task_instance.dag_display_name} />
             </RouterLink>
           ),
@@ -102,6 +153,7 @@ const taskInstanceColumns = ({
           accessorKey: "run_id",
           cell: ({ row: { original } }: HITLRow) => (
             <RouterLink
+              onClick={(event) => event.stopPropagation()}
               
to={`/dags/${original.task_instance.dag_id}/runs/${original.task_instance.dag_run_id}`}
             >
               <TruncatedText text={original.task_instance.dag_run_id} />
@@ -127,6 +179,7 @@ const taskInstanceColumns = ({
           cell: ({ row: { original } }: HITLRow) => (
             <RouterLink
               fontWeight="bold"
+              onClick={(event) => event.stopPropagation()}
               
to={`${getTaskInstanceLink(original.task_instance)}/required_actions`}
             >
               <TruncatedText text={original.task_instance.task_display_name} />
@@ -162,9 +215,15 @@ const taskInstanceColumns = ({
   },
 ];
 
-export const HITLTaskInstances = () => {
+export const HITLTaskInstances = ({
+  enableHITLReviewDrawer = false,
+}: {
+  readonly enableHITLReviewDrawer?: boolean;
+}) => {
   const { t: translate } = useTranslation("hitl");
   const { dagId, runId, taskId } = useParams();
+  const { closeHITLReviewDrawer, isHITLReviewDrawerOpen, openHITLReviewDrawer, 
selectedDetail } =
+    useHITLReviewDrawer();
   const [searchParams, setSearchParams] = useSearchParams();
   const { setTableURLState, tableURLState } = useTableURLState();
   const { pagination, sorting } = tableURLState;
@@ -248,6 +307,9 @@ export const HITLTaskInstances = () => {
 
   const columns = taskInstanceColumns({
     dagId,
+    renderHITLReviewDrawerButton: enableHITLReviewDrawer
+      ? (detail) => <HITLReviewDrawerButton detail={detail} 
onOpen={openHITLReviewDrawer} />
+      : undefined,
     runId,
     taskId,
     translate,
@@ -266,6 +328,13 @@ export const HITLTaskInstances = () => {
         onStateChange={setTableURLState}
         total={data?.total_entries}
       />
+      {enableHITLReviewDrawer ? (
+        <HITLReviewDrawer
+          detail={selectedDetail}
+          onClose={closeHITLReviewDrawer}
+          open={isHITLReviewDrawerOpen}
+        />
+      ) : null}
     </VStack>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
index d9a56dd796d..acba855305f 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx
@@ -27,6 +27,7 @@ import { DagVersion } from "src/components/DagVersion";
 import { HeaderCard } from "src/components/HeaderCard";
 import { LimitedItemsList } from "src/components/LimitedItemsList";
 import { MarkRunAsButton } from "src/components/MarkAs";
+import { NeedsReviewButtonWithModal } from "src/components/NeedsReviewButton";
 import NotePreview from "src/components/NotePreview";
 import { RunTypeIcon } from "src/components/RunTypeIcon";
 import Time from "src/components/Time";
@@ -53,6 +54,7 @@ export const Header = ({ dagRun }: { readonly dagRun: 
DAGRunResponse }) => {
       <HeaderCard
         actions={
           <>
+            <NeedsReviewButtonWithModal dagId={dagId} runId={dagRunId} />
             <ClearRunButton dagRun={dagRun} isHotkeyEnabled />
             <MarkRunAsButton dagRun={dagRun} isHotkeyEnabled />
             <DeleteRunButton dagRun={dagRun} />
diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx 
b/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
index 2ec4fb8fc6c..ea49ef73b7c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx
@@ -18,13 +18,12 @@
  */
 import { ReactFlowProvider } from "@xyflow/react";
 import { useTranslation } from "react-i18next";
-import { FiCode, FiDatabase, FiUser } from "react-icons/fi";
+import { FiCode, FiDatabase } from "react-icons/fi";
 import { MdDetails, MdOutlineEventNote, MdOutlineTask } from "react-icons/md";
 import { useParams } from "react-router-dom";
 
 import { useDagRunServiceGetDagRun } from "openapi/queries";
 import { usePluginTabs } from "src/hooks/usePluginTabs";
-import { useRequiredActionTabs } from "src/hooks/useRequiredActionTabs";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
 import { isStatePending, useAutoRefresh } from "src/utils";
 
@@ -39,7 +38,6 @@ export const Run = () => {
 
   const tabs = [
     { icon: <MdOutlineTask />, label: translate("tabs.taskInstances"), value: 
"" },
-    { icon: <FiUser />, label: translate("tabs.requiredActions"), value: 
"required_actions" },
     { icon: <FiDatabase />, label: translate("tabs.assetEvents"), value: 
"asset_events" },
     { icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: 
"events" },
     { icon: <FiCode />, label: translate("tabs.code"), value: "code" },
@@ -64,13 +62,9 @@ export const Run = () => {
     },
   );
 
-  const { tabs: displayTabs } = useRequiredActionTabs({ dagId, dagRunId: runId 
}, tabs, {
-    refetchInterval: isStatePending(dagRun?.state) ? refetchInterval : false,
-  });
-
   return (
     <ReactFlowProvider>
-      <DetailsLayout error={error} isLoading={isLoading} tabs={displayTabs}>
+      <DetailsLayout error={error} isLoading={isLoading} tabs={tabs}>
         {dagRun === undefined ? undefined : <Header dagRun={dagRun} />}
       </DetailsLayout>
     </ReactFlowProvider>
diff --git a/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts 
b/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts
index b75bca8cfbd..283a8a10233 100644
--- a/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useUpdateHITLDetail.ts
@@ -22,6 +22,7 @@ import { useTranslation } from "react-i18next";
 
 import {
   UseDagRunServiceGetDagRunKeyFn,
+  useDagServiceGetDagsUiKey,
   useDagRunServiceGetDagRunsKey,
   UseGanttServiceGetGanttDataKeyFn,
   useTaskInstanceServiceGetHitlDetailsKey,
@@ -41,23 +42,26 @@ export const useUpdateHITLDetail = ({
   dagId,
   dagRunId,
   mapIndex,
+  onSuccess,
   taskId,
 }: {
   dagId: string;
   dagRunId: string;
   mapIndex: number | undefined;
+  onSuccess?: () => void;
   taskId: string;
 }) => {
   const queryClient = useQueryClient();
   const [error, setError] = useState<unknown>(undefined);
   const { t: translate } = useTranslation("hitl");
-  const onSuccess = async () => {
+  const handleSuccess = async () => {
     const queryKeys = [
       UseDagRunServiceGetDagRunKeyFn({ dagId, dagRunId }),
       [useDagRunServiceGetDagRunsKey],
       [useTaskInstanceServiceGetTaskInstancesKey, { dagId, dagRunId }],
       [useTaskInstanceServiceGetTaskInstanceKey, { dagId, dagRunId, mapIndex, 
taskId }],
-      [useTaskInstanceServiceGetHitlDetailsKey, { dagIdPrefixPattern: dagId, 
dagRunId }],
+      [useDagServiceGetDagsUiKey],
+      [useTaskInstanceServiceGetHitlDetailsKey],
       [useTaskInstanceServiceGetHitlDetailKey, { dagId, dagRunId }],
       [useTaskInstanceServiceGetHitlDetailTryDetailKey, { dagId, dagRunId }],
       UseGanttServiceGetGanttDataKeyFn({ dagId, runId: dagRunId }),
@@ -73,6 +77,7 @@ export const useUpdateHITLDetail = ({
       title: translate("response.success", { taskId }),
       type: "success",
     });
+    onSuccess?.();
   };
 
   const onError = (apiError: unknown) => {
@@ -81,7 +86,7 @@ export const useUpdateHITLDetail = ({
 
   const { isPending, mutate } = useTaskInstanceServiceUpdateHitlDetail({
     onError,
-    onSuccess,
+    onSuccess: handleSuccess,
   });
 
   const updateHITLResponse = (updateHITLResponseRequestBody: 
HITLResponseParams) => {
diff --git a/airflow-core/src/airflow/ui/src/router.tsx 
b/airflow-core/src/airflow/ui/src/router.tsx
index 2245fcc0421..d0e5ad25658 100644
--- a/airflow-core/src/airflow/ui/src/router.tsx
+++ b/airflow-core/src/airflow/ui/src/router.tsx
@@ -102,7 +102,7 @@ export const routerConfig = [
         index: true,
       },
       {
-        element: <HITLTaskInstances />,
+        element: <HITLTaskInstances enableHITLReviewDrawer />,
         path: "required_actions",
       },
       {
@@ -188,7 +188,9 @@ export const routerConfig = [
           { element: <DagRuns />, path: "runs" },
           { element: <Tasks />, path: "tasks" },
           { element: <Calendar />, path: "calendar" },
-          { element: <HITLTaskInstances />, path: "required_actions" },
+          // The Required Actions tab is now a button + modal; this keeps old 
/required_actions
+          // deep links alive by rendering the overview, where the route sync 
opens the modal.
+          { element: <Overview />, path: "required_actions" },
           { element: <Backfills />, path: "backfills" },
           { element: <Events />, path: "events" },
           { element: <Code />, path: "code" },
@@ -201,7 +203,9 @@ export const routerConfig = [
       {
         children: [
           { element: <TaskInstances />, index: true },
-          { element: <HITLTaskInstances />, path: "required_actions" },
+          // The Required Actions tab is now a button + modal; this keeps old 
/required_actions
+          // deep links alive by rendering the task instances, where the route 
sync opens the modal.
+          { element: <TaskInstances />, path: "required_actions" },
           { element: <Events />, path: "events" },
           { element: <Code />, path: "code" },
           { element: <DagRunDetails />, path: "details" },
diff --git 
a/airflow-core/src/airflow/ui/tests/e2e/components/HITLReviewDrawer.ts 
b/airflow-core/src/airflow/ui/tests/e2e/components/HITLReviewDrawer.ts
new file mode 100644
index 00000000000..09990c6cd07
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/components/HITLReviewDrawer.ts
@@ -0,0 +1,32 @@
+/*!
+ * 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 { expect, type Locator, type Page } from "@playwright/test";
+
+export class HITLReviewDrawer {
+  private readonly root: Locator;
+
+  public constructor(page: Page) {
+    this.root = page.getByRole("dialog", { name: "Required Actions" });
+  }
+
+  public async expectOpenWith(dagId: string): Promise<void> {
+    await expect(this.root).toBeVisible();
+    await expect(this.root).toContainText(dagId);
+  }
+}
diff --git 
a/airflow-core/src/airflow/ui/tests/e2e/components/HITLReviewModal.ts 
b/airflow-core/src/airflow/ui/tests/e2e/components/HITLReviewModal.ts
new file mode 100644
index 00000000000..b9975762c0e
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/components/HITLReviewModal.ts
@@ -0,0 +1,32 @@
+/*!
+ * 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 { expect, type Locator, type Page } from "@playwright/test";
+
+export class HITLReviewModal {
+  private readonly root: Locator;
+
+  public constructor(page: Page) {
+    this.root = page.getByRole("dialog", { name: "Required Actions" });
+  }
+
+  public async expectOpenWith(dagId: string): Promise<void> {
+    await expect(this.root).toBeVisible();
+    await expect(this.root).toContainText(dagId, { timeout: 60_000 });
+  }
+}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/fixtures/data.ts 
b/airflow-core/src/airflow/ui/tests/e2e/fixtures/data.ts
index bc0352916d2..adf0ac37a82 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/fixtures/data.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/fixtures/data.ts
@@ -37,6 +37,7 @@ import {
   waitForDagReady,
   waitForDagRunStatus,
 } from "../utils/api/dag-runs";
+import { setupPendingHITLFlowViaAPI } from "../utils/api/hitl";
 import { uniqueRunId } from "../utils/shared";
 import { test as base } from "./pom";
 
@@ -54,11 +55,18 @@ export type SuccessAndFailedRunsData = {
   successRun: DagRunFixtureData;
 };
 
+export type HITLRunFixtureData = {
+  dagId: string;
+  runId: string;
+};
+
 export type DataWorkerFixtures = {
   /** Ensures the default test Dag is parsed and ready. Worker-scoped, no 
cleanup needed. */
   dagReady: string;
   /** A Dag run triggered via scheduler and completed. Worker-scoped with 
auto-cleanup. */
   executedDagRun: DagRunFixtureData;
+  /** A pending HITL Dag run. Worker-scoped with auto-cleanup. */
+  pendingHITLRun: HITLRunFixtureData;
   /** Two Dag runs: one success, one failed. Worker-scoped with auto-cleanup. 
*/
   successAndFailedRuns: SuccessAndFailedRunsData;
   /** A Dag run in "success" state (API-only, no scheduler). Worker-scoped 
with auto-cleanup. */
@@ -146,6 +154,24 @@ export const test = base.extend<DataTestFixtures, 
DataWorkerFixtures>({
     { scope: "worker", timeout: 180_000 },
   ],
 
+  pendingHITLRun: [
+    async ({ authenticatedRequest }, use) => {
+      const dagId = testConfig.testDag.hitlId;
+      let runId: string | undefined;
+
+      try {
+        runId = await setupPendingHITLFlowViaAPI(authenticatedRequest, dagId);
+
+        await use({ dagId, runId });
+      } finally {
+        if (runId !== undefined) {
+          await safeCleanupDagRun(authenticatedRequest, dagId, runId);
+        }
+      }
+    },
+    { scope: "worker", timeout: 120_000 },
+  ],
+
   successAndFailedRuns: [
     async ({ authenticatedRequest }, use, workerInfo) => {
       const dagId = testConfig.testDag.id;
diff --git a/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts 
b/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
index d561be27faa..ccd72dc82c4 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/fixtures/pom.ts
@@ -33,6 +33,8 @@ import { ConfigurationPage } from 
"../pages/ConfigurationPage";
 import { ConnectionsPage } from "../pages/ConnectionsPage";
 import { DagCalendarTab } from "../pages/DagCalendarTab";
 import { DagCodePage } from "../pages/DagCodePage";
+import { DagDetailPage } from "../pages/DagDetailPage";
+import { DagRunPage } from "../pages/DagRunPage";
 import { DagRunsPage } from "../pages/DagRunsPage";
 import { DagRunsTabPage } from "../pages/DagRunsTabPage";
 import { DagsPage } from "../pages/DagsPage";
@@ -60,6 +62,8 @@ export type PomFixtures = {
   connectionsPage: ConnectionsPage;
   dagCalendarTab: DagCalendarTab;
   dagCodePage: DagCodePage;
+  dagDetailPage: DagDetailPage;
+  dagRunPage: DagRunPage;
   dagRunsPage: DagRunsPage;
   dagRunsTabPage: DagRunsTabPage;
   dagsPage: DagsPage;
@@ -111,6 +115,12 @@ export const test = base.extend<PomFixtures, 
PomWorkerFixtures>({
   dagCodePage: async ({ page }, use) => {
     await use(new DagCodePage(page));
   },
+  dagDetailPage: async ({ page }, use) => {
+    await use(new DagDetailPage(page));
+  },
+  dagRunPage: async ({ page }, use) => {
+    await use(new DagRunPage(page));
+  },
   dagRunsPage: async ({ page }, use) => {
     await use(new DagRunsPage(page));
   },
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagDetailPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagDetailPage.ts
new file mode 100644
index 00000000000..2a3335b679d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagDetailPage.ts
@@ -0,0 +1,48 @@
+/*!
+ * 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 type { Locator, Page } from "@playwright/test";
+import { HITLReviewModal } from "tests/e2e/components/HITLReviewModal";
+import { BasePage } from "tests/e2e/pages/BasePage";
+
+export class DagDetailPage extends BasePage {
+  public readonly hitlReviewModal: HITLReviewModal;
+  public readonly requiredActionsButton: Locator;
+
+  public constructor(page: Page) {
+    super(page);
+    this.hitlReviewModal = new HITLReviewModal(page);
+    this.requiredActionsButton = page.getByRole("button", { name: "Required 
Actions" });
+  }
+
+  public static getDagDetailRequiredActionsUrl(dagId: string): string {
+    return this.getDagDetailUrl(dagId) + `/required_actions`;
+  }
+
+  public static getDagDetailUrl(dagId: string): string {
+    return `/dags/${dagId}`;
+  }
+
+  public async navigateToDagDetail(dagId: string): Promise<void> {
+    await this.navigateTo(DagDetailPage.getDagDetailUrl(dagId));
+  }
+
+  public async navigateToDagDetailRequiredActions(dagId: string): 
Promise<void> {
+    await this.navigateTo(DagDetailPage.getDagDetailRequiredActionsUrl(dagId));
+  }
+}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts
new file mode 100644
index 00000000000..d6d915593fd
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagRunPage.ts
@@ -0,0 +1,48 @@
+/*!
+ * 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 type { Locator, Page } from "@playwright/test";
+import { HITLReviewModal } from "tests/e2e/components/HITLReviewModal";
+import { BasePage } from "tests/e2e/pages/BasePage";
+
+export class DagRunPage extends BasePage {
+  public readonly hitlReviewModal: HITLReviewModal;
+  public readonly requiredActionsButton: Locator;
+
+  public constructor(page: Page) {
+    super(page);
+    this.hitlReviewModal = new HITLReviewModal(page);
+    this.requiredActionsButton = page.getByRole("button", { name: "Required 
Actions" });
+  }
+
+  public static getDagRunRequiredActionsUrl(dagId: string, dagRunId: string): 
string {
+    return this.getDagRunUrl(dagId, dagRunId) + `/required_actions`;
+  }
+
+  public static getDagRunUrl(dagId: string, dagRunId: string): string {
+    return `/dags/${dagId}/runs/${dagRunId}`;
+  }
+
+  public async navigateToDagRun(dagId: string, dagRunId: string): 
Promise<void> {
+    await this.navigateTo(DagRunPage.getDagRunUrl(dagId, dagRunId));
+  }
+
+  public async navigateToDagRunRequiredActions(dagId: string, dagRunId: 
string): Promise<void> {
+    await this.navigateTo(DagRunPage.getDagRunRequiredActionsUrl(dagId, 
dagRunId));
+  }
+}
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
index 564e5d420bd..f83134d6f93 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { expect, type Locator, type Page, type Response } from 
"@playwright/test";
+import { HITLReviewModal } from "tests/e2e/components/HITLReviewModal";
 import { BasePage } from "tests/e2e/pages/BasePage";
 
 import type { DAGRunResponse } from "openapi/requests/types.gen";
@@ -32,6 +33,8 @@ export class DagsPage extends BasePage {
   public readonly cardViewButton: Locator;
   public readonly confirmButton: Locator;
   public readonly failedFilter: Locator;
+  public readonly hitlReviewModal: HITLReviewModal;
+  public readonly needsReviewBadges: Locator;
   public readonly needsReviewFilter: Locator;
   public readonly operatorFilter: Locator;
   public readonly queuedFilter: Locator;
@@ -63,6 +66,8 @@ export class DagsPage extends BasePage {
     this.tableViewButton = page.getByRole("button", { name: "Show table view" 
});
     this.successFilter = page.getByRole("button", { name: "Success" });
     this.failedFilter = page.getByRole("button", { name: "Failed" });
+    this.hitlReviewModal = new HITLReviewModal(page);
+    this.needsReviewBadges = page.getByTestId("needs-review-badge");
     this.runningFilter = page.getByRole("button", { name: "Running" });
     this.queuedFilter = page.getByRole("button", { name: "Queued" });
     // Uses testId because this button's text is driven by an i18n key.
@@ -195,6 +200,22 @@ export class DagsPage extends BasePage {
     return texts.map((text) => text.trim()).filter((text) => text !== "");
   }
 
+  public async getDagNeedsReviewBadgeOnCard(dagId: string): Promise<Locator> {
+    const dagCard = this.page.getByTestId("dag-card").filter({ hasText: dagId 
});
+
+    await expect(dagCard).toBeVisible({ timeout: 60_000 });
+
+    return dagCard.getByTestId("needs-review-badge");
+  }
+
+  public async getDagNeedsReviewBadgeOnTable(dagId: string): Promise<Locator> {
+    const dagRow = 
this.page.getByTestId("table-list").getByRole("row").filter({ hasText: dagId });
+
+    await expect(dagRow).toBeVisible({ timeout: 60_000 });
+
+    return dagRow.getByTestId("needs-review-badge");
+  }
+
   /**
    * Get count of Dags on current page
    */
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts
index 299b7a1cfd7..b1367458de5 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/HomePage.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { expect, type Locator, type Page } from "@playwright/test";
+import { HITLReviewModal } from "tests/e2e/components/HITLReviewModal";
 import { BasePage } from "tests/e2e/pages/BasePage";
 
 /**
@@ -35,8 +36,10 @@ export class HomePage extends BasePage {
 
   public readonly healthSection: Locator;
   public readonly historicalMetricsSection: Locator;
+  public readonly hitlReviewModal: HITLReviewModal;
   public readonly metaDatabaseHealth: Locator;
   public readonly poolSummarySection: Locator;
+  public readonly requiredActionsButton: Locator;
   public readonly runningDagsCard: Locator;
 
   public readonly schedulerHealth: Locator;
@@ -55,10 +58,12 @@ export class HomePage extends BasePage {
     this.runningDagsCard = page.getByRole("link", { name: /running/i });
     this.activeDagsCard = page.getByRole("link", { name: /active/i });
     this.dagImportErrorsCard = page.getByRole("button", { name: "Dag Import 
Errors" });
+    this.requiredActionsButton = page.getByRole("button", { name: "Required 
Actions" });
 
     // Navigate to parent via ".." since there are no ARIA landmark/region 
roles on these sections.
     this.statsSection = page.getByRole("heading", { name: "Stats" 
}).locator("..");
     this.healthSection = page.getByRole("heading", { name: "Health" 
}).locator("..");
+    this.hitlReviewModal = new HITLReviewModal(page);
     this.metaDatabaseHealth = page.getByText("Metadatabase").first();
     this.schedulerHealth = page.getByText("Scheduler").first();
     this.triggererHealth = page.getByText("Triggerer").first();
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts
index d524c9f7756..750a077f420 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts
@@ -16,8 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { expect, type APIRequestContext, type Locator, type Page } from 
"@playwright/test";
+import { type APIRequestContext, expect, type Locator, type Page } from 
"@playwright/test";
 import { testConfig } from "playwright.config";
+import { HITLReviewDrawer } from "tests/e2e/components/HITLReviewDrawer";
 import { apiTriggerDagRun, waitForDagReady } from 
"tests/e2e/utils/api/dag-runs";
 
 import { BasePage } from "./BasePage";
@@ -25,6 +26,7 @@ import { BasePage } from "./BasePage";
 export class RequiredActionsPage extends BasePage {
   public readonly actionsTable: Locator;
   public readonly emptyStateMessage: Locator;
+  public readonly hitlReviewDrawer: HITLReviewDrawer;
   public readonly pageHeading: Locator;
 
   // Standalone API context — page.request degrades after many navigations in 
WebKit.
@@ -36,16 +38,41 @@ export class RequiredActionsPage extends BasePage {
     this.pageHeading = page.getByRole("heading").filter({ hasText: /required 
action/i });
     this.actionsTable = page.getByTestId("table-list");
     this.emptyStateMessage = page.getByText(/no required actions found/i);
+    this.hitlReviewDrawer = new HITLReviewDrawer(page);
+  }
+
+  public static getPendingRequiredActionsUrl(): string {
+    return `${this.getRequiredActionsUrl()}?response_received=false`;
   }
 
   public static getRequiredActionsUrl(): string {
     return "/required_actions";
   }
 
+  public async clickReviewDrawerButton(dagId: string): Promise<void> {
+    const actionRow = this.getActionRow(dagId);
+    const reviewDrawerButton = actionRow.getByRole("button", { name: "Open 
Review Drawer" });
+
+    await expect(reviewDrawerButton).toBeVisible({ timeout: 30_000 });
+    await reviewDrawerButton.click();
+  }
+
+  public getActionRow(dagId: string): Locator {
+    return this.actionsTable
+      .getByRole("row")
+      .filter({ has: this.page.getByRole("cell", { name: dagId }) })
+      .first();
+  }
+
   public async isEmptyStateDisplayed(): Promise<boolean> {
     return this.emptyStateMessage.isVisible();
   }
 
+  public async navigateToPendingRequiredActionsPage(): Promise<void> {
+    await this.navigateTo(RequiredActionsPage.getPendingRequiredActionsUrl());
+    await expect(this.pageHeading).toBeVisible({ timeout: 30_000 });
+  }
+
   public async navigateToRequiredActionsPage(): Promise<void> {
     await expect(async () => {
       await this.navigateTo(RequiredActionsPage.getRequiredActionsUrl());
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-detail.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-detail.spec.ts
new file mode 100644
index 00000000000..bf35acc4d0d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-detail.spec.ts
@@ -0,0 +1,43 @@
+/*!
+ * 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 { expect, test } from "tests/e2e/fixtures";
+
+test.describe("Dag Detail Page", () => {
+  test("verify HITL review modal opens from Dag detail", async ({ 
dagDetailPage, pendingHITLRun }) => {
+    test.slow();
+
+    await dagDetailPage.navigateToDagDetail(pendingHITLRun.dagId);
+
+    await expect(dagDetailPage.requiredActionsButton).toBeVisible({ timeout: 
60_000 });
+    await dagDetailPage.requiredActionsButton.click();
+
+    await dagDetailPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
+  });
+
+  test("verify HITL review modal opens from the Dag required actions route", 
async ({
+    dagDetailPage,
+    pendingHITLRun,
+  }) => {
+    test.slow();
+
+    await 
dagDetailPage.navigateToDagDetailRequiredActions(pendingHITLRun.dagId);
+
+    await dagDetailPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
+  });
+});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts
new file mode 100644
index 00000000000..704d4682f2d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-run.spec.ts
@@ -0,0 +1,43 @@
+/*!
+ * 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 { expect, test } from "tests/e2e/fixtures";
+
+test.describe("Dag Run Page", () => {
+  test("verify HITL review modal opens from Dag run details", async ({ 
dagRunPage, pendingHITLRun }) => {
+    test.slow();
+
+    await dagRunPage.navigateToDagRun(pendingHITLRun.dagId, 
pendingHITLRun.runId);
+
+    await expect(dagRunPage.requiredActionsButton).toBeVisible({ timeout: 
60_000 });
+    await dagRunPage.requiredActionsButton.click();
+
+    await dagRunPage.hitlReviewModal.expectOpenWith(pendingHITLRun.runId);
+  });
+
+  test("verify HITL review modal opens from the required actions route", async 
({
+    dagRunPage,
+    pendingHITLRun,
+  }) => {
+    test.slow();
+
+    await dagRunPage.navigateToDagRunRequiredActions(pendingHITLRun.dagId, 
pendingHITLRun.runId);
+
+    await dagRunPage.hitlReviewModal.expectOpenWith(pendingHITLRun.runId);
+  });
+});
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
index 8b91cf44847..798a910a52e 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
@@ -99,6 +99,48 @@ test.describe("Dags List Display", () => {
     await dagsPage.waitForDagList();
     await expect(dagsPage.getDagLink(testDagId)).toBeVisible();
   });
+
+  test("verify HITL review modal opens from the needs review badge in table 
view", async ({
+    dagsPage,
+    pendingHITLRun,
+  }) => {
+    test.slow();
+
+    await dagsPage.navigate();
+    await dagsPage.waitForDagList();
+    await dagsPage.switchToTableView();
+
+    await expect(dagsPage.needsReviewFilter).toBeVisible({ timeout: 30_000 });
+    await dagsPage.needsReviewFilter.click();
+
+    const needsReviewBadge = await 
dagsPage.getDagNeedsReviewBadgeOnTable(pendingHITLRun.dagId);
+
+    await expect(needsReviewBadge).toBeVisible({ timeout: 30_000 });
+    await needsReviewBadge.click();
+
+    await dagsPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
+  });
+
+  test("verify HITL review modal opens from the needs review badge in card 
view", async ({
+    dagsPage,
+    pendingHITLRun,
+  }) => {
+    test.slow();
+
+    await dagsPage.navigate();
+    await dagsPage.waitForDagList();
+    await dagsPage.switchToCardView();
+
+    await expect(dagsPage.needsReviewFilter).toBeVisible({ timeout: 30_000 });
+    await dagsPage.needsReviewFilter.click();
+
+    const needsReviewBadge = await 
dagsPage.getDagNeedsReviewBadgeOnCard(pendingHITLRun.dagId);
+
+    await expect(needsReviewBadge).toBeVisible({ timeout: 30_000 });
+    await needsReviewBadge.click();
+
+    await dagsPage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
+  });
 });
 
 test.describe("Dags View Toggle", () => {
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
index 97f22068504..1c03a2c6995 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/home-dashboard.spec.ts
@@ -74,6 +74,21 @@ test.describe("Dashboard Metrics Display", () => {
     await expect(homePage.welcomeHeading).toBeVisible();
   });
 
+  test("verify HITL review modal opens from the Required Actions button", 
async ({
+    homePage,
+    pendingHITLRun,
+  }) => {
+    test.slow();
+
+    await homePage.navigate();
+    await homePage.waitForDashboardLoad();
+
+    await expect(homePage.requiredActionsButton).toBeVisible({ timeout: 30_000 
});
+    await homePage.requiredActionsButton.click();
+
+    await homePage.hitlReviewModal.expectOpenWith(pendingHITLRun.dagId);
+  });
+
   test("should update metrics when Dag is triggered", async ({ dagRunCleanup, 
dagsPage, homePage }) => {
     test.slow();
 
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts
index b55dc5ec330..f8e9f4f5a0d 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/requiredAction.spec.ts
@@ -53,4 +53,15 @@ test.describe("Verify Required Action page", () => {
     await expect(page.locator("th").filter({ hasText: "Response created at" 
})).toBeVisible();
     await expect(page.locator("th").filter({ hasText: "Response received at" 
})).toBeVisible();
   });
+
+  test("verify HITL Review drawer opens from clicking a pending action state", 
async ({
+    pendingHITLRun,
+    requiredActionsPage,
+  }) => {
+    await requiredActionsPage.navigateToPendingRequiredActionsPage();
+
+    await requiredActionsPage.clickReviewDrawerButton(pendingHITLRun.dagId);
+
+    await 
requiredActionsPage.hitlReviewDrawer.expectOpenWith(pendingHITLRun.dagId);
+  });
 });
diff --git a/airflow-core/src/airflow/ui/tests/e2e/utils/api/hitl.ts 
b/airflow-core/src/airflow/ui/tests/e2e/utils/api/hitl.ts
index de3113f9c9a..159dd0b05c7 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/utils/api/hitl.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/utils/api/hitl.ts
@@ -192,3 +192,25 @@ export async function setupHITLFlowViaAPI(
 
   return dagRunId;
 }
+
+export async function setupPendingHITLFlowViaAPI(source: RequestLike, dagId: 
string): Promise<string> {
+  const request = getRequestContext(source);
+
+  await waitForDagReady(request, dagId);
+  const response = await request.patch(`${baseUrl}/api/v2/dags/${dagId}`, { 
data: { is_paused: false } });
+
+  if (!response.ok()) {
+    throw new Error(`HITL response failed (${response.status()})`);
+  }
+
+  const { dagRunId } = await apiTriggerDagRun(request, dagId);
+
+  await waitForTaskInstanceState(request, {
+    dagId,
+    expectedState: "awaiting_input",
+    runId: dagRunId,
+    taskId: "wait_for_input",
+  });
+
+  return dagRunId;
+}

Reply via email to