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}>