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

gopidesu 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 98ef18d7f0a Show HITL review tab only for review-enabled task 
instances (#63477)
98ef18d7f0a is described below

commit 98ef18d7f0a1af9ebf39abbbb203070c3f262ddf
Author: GPK <[email protected]>
AuthorDate: Fri Mar 20 04:31:58 2026 +0000

    Show HITL review tab only for review-enabled task instances (#63477)
    
    * Show HITL review tab only for review-enabled task instances
    
    * Resolve comments
    
    * Resolve comments
---
 .../src/airflow/ui/src/hooks/useHITLReviewTabs.ts  | 110 +++++++++++++++++++++
 .../ui/src/pages/TaskInstance/TaskInstance.tsx     |   8 +-
 2 files changed, 117 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/ui/src/hooks/useHITLReviewTabs.ts 
b/airflow-core/src/airflow/ui/src/hooks/useHITLReviewTabs.ts
new file mode 100644
index 00000000000..0de93b43932
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/hooks/useHITLReviewTabs.ts
@@ -0,0 +1,110 @@
+/*!
+ * 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 { useQuery } from "@tanstack/react-query";
+import axios, { type AxiosError } from "axios";
+import { useEffect } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
+
+import { OpenAPI } from "openapi/requests/core/OpenAPI";
+import type { TabItem } from "src/hooks/useRequiredActionTabs";
+
+const HITL_REVIEW_PLUGIN_TAB = "plugin/hitl-review";
+
+export type UseHITLReviewTabsOptions = {
+  enabled?: boolean;
+  mapIndex?: number;
+  refetchInterval?: number | false;
+};
+
+const filterHITLReviewTabs = (tabs: Array<TabItem>, hasHitlData: boolean): 
Array<TabItem> =>
+  tabs.filter((tab) => tab.value !== HITL_REVIEW_PLUGIN_TAB || hasHitlData);
+
+export const useHITLReviewTabs = (
+  {
+    dagId,
+    dagRunId,
+    taskId,
+  }: {
+    dagId: string;
+    dagRunId: string;
+    taskId: string;
+  },
+  tabs: Array<TabItem>,
+  options: UseHITLReviewTabsOptions = {},
+) => {
+  const { enabled = true, mapIndex = -1, refetchInterval } = options;
+  const hasHitlTab = tabs.some((tab) => tab.value === HITL_REVIEW_PLUGIN_TAB);
+  const location = useLocation();
+  const navigate = useNavigate();
+  const redirectPath =
+    Boolean(dagId) && Boolean(dagRunId) && Boolean(taskId)
+      ? `/dags/${dagId}/runs/${dagRunId}/tasks/${taskId}`
+      : location.pathname.replace("/plugin/hitl-review", "");
+
+  const { data: hasHITLReviewSession = false, isLoading: 
isLoadingHITLReviewSession } = useQuery({
+    enabled: enabled && hasHitlTab,
+    queryFn: () =>
+      axios
+        .get(`${OpenAPI.BASE}/hitl-review/sessions/find`, {
+          params: {
+            dag_id: dagId,
+            map_index: mapIndex,
+            run_id: dagRunId,
+            task_id: taskId,
+          },
+        })
+        .then(() => true)
+        .catch((error: unknown) => {
+          if (!axios.isAxiosError(error)) {
+            return Promise.reject(error);
+          }
+
+          const status = error.response?.status;
+
+          if (status === 404) {
+            return false;
+          }
+
+          // queryClient.ts reads error.status to decide whether 4xx responses 
should be retried.
+          if (status !== undefined) {
+            (error as { status?: number } & AxiosError).status = status;
+          }
+
+          return Promise.reject(error);
+        }),
+    queryKey: ["hitl-review-session", dagId, dagRunId, taskId, mapIndex],
+    refetchInterval,
+  });
+
+  useEffect(() => {
+    if (
+      !hasHITLReviewSession &&
+      !isLoadingHITLReviewSession &&
+      location.pathname.includes("plugin/hitl-review")
+    ) {
+      void Promise.resolve(navigate(redirectPath));
+    }
+  }, [hasHITLReviewSession, isLoadingHITLReviewSession, location.pathname, 
navigate, redirectPath]);
+
+  return {
+    hasHITLReviewSession,
+    isLoadingHITLReviewSession,
+    tabs: filterHITLReviewTabs(tabs, hasHITLReviewSession),
+  };
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
index 9f22e299c83..6a943f7b4c7 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
@@ -25,6 +25,7 @@ import { PiBracketsCurlyBold } from "react-icons/pi";
 import { useParams } from "react-router-dom";
 
 import { useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries";
+import { useHITLReviewTabs } from "src/hooks/useHITLReviewTabs";
 import { usePluginTabs } from "src/hooks/usePluginTabs";
 import { useRequiredActionTabs } from "src/hooks/useRequiredActionTabs";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
@@ -99,11 +100,16 @@ export const TaskInstance = () => {
     ];
   }
 
-  const { tabs: displayTabs } = useRequiredActionTabs({ dagId, dagRunId: 
runId, taskId }, newTabs, {
+  const { tabs: requiredActionTabs } = useRequiredActionTabs({ dagId, 
dagRunId: runId, taskId }, newTabs, {
     autoRedirect: true,
     refetchInterval: isStatePending(taskInstance?.state) ? refetchInterval : 
false,
   });
 
+  const { tabs: displayTabs } = useHITLReviewTabs({ dagId, dagRunId: runId, 
taskId }, requiredActionTabs, {
+    mapIndex: parsedMapIndex,
+    refetchInterval: isStatePending(taskInstance?.state) ? refetchInterval : 
false,
+  });
+
   return (
     <ReactFlowProvider>
       <DetailsLayout error={error} isLoading={isLoading} tabs={displayTabs}>

Reply via email to