From 7b4564709c242cb32a9f521f07fd216000eb5d3e Mon Sep 17 00:00:00 2001
From: Karthikeyan Singaravelan <tir.karthi@gmail.com>
Date: Tue, 28 May 2024 21:39:58 +0530
Subject: [PATCH 1/4] Add task documentation to details tab in grid view.

---
 airflow/api_connexion/openapi/v1.yaml         |  8 ++
 airflow/api_connexion/schemas/task_schema.py  |  1 +
 airflow/www/static/js/api/index.ts            |  2 +
 airflow/www/static/js/api/useTaskDetail.tsx   | 34 ++++++++
 .../taskInstance/TaskDocumentation.tsx        | 83 +++++++++++++++++++
 .../js/dag/details/taskInstance/index.tsx     |  2 +
 airflow/www/static/js/types/api-generated.ts  |  6 ++
 airflow/www/templates/airflow/dag.html        |  4 +-
 8 files changed, 138 insertions(+), 2 deletions(-)
 create mode 100644 airflow/www/static/js/api/useTaskDetail.tsx
 create mode 100644 airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx

diff --git a/airflow/api_connexion/openapi/v1.yaml b/airflow/api_connexion/openapi/v1.yaml
index 3efff1ebf5c71..63ac5768c33dc 100644
--- a/airflow/api_connexion/openapi/v1.yaml
+++ b/airflow/api_connexion/openapi/v1.yaml
@@ -4124,6 +4124,14 @@ components:
           readOnly: true
           items:
             type: string
+        doc_md:
+          type: string
+          readOnly: true
+          nullable: true
+          description: |
+            Task documentation.
+
+            *New in version 2.10.0*
 
     TaskCollection:
       type: object
diff --git a/airflow/api_connexion/schemas/task_schema.py b/airflow/api_connexion/schemas/task_schema.py
index 8d8f1e33f2985..03bf4b59ef2e2 100644
--- a/airflow/api_connexion/schemas/task_schema.py
+++ b/airflow/api_connexion/schemas/task_schema.py
@@ -65,6 +65,7 @@ class TaskSchema(Schema):
     downstream_task_ids = fields.List(fields.String(), dump_only=True)
     params = fields.Method("_get_params", dump_only=True)
     is_mapped = fields.Method("_get_is_mapped", dump_only=True)
+    doc_md = fields.String(dump_only=True)
 
     @staticmethod
     def _get_class_reference(obj):
diff --git a/airflow/www/static/js/api/index.ts b/airflow/www/static/js/api/index.ts
index 96532c3903781..b48fdc2dbadbd 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -55,6 +55,7 @@ import useEventLogs from "./useEventLogs";
 import useCalendarData from "./useCalendarData";
 import useCreateDatasetEvent from "./useCreateDatasetEvent";
 import useRenderedK8s from "./useRenderedK8s";
+import useTaskDetail from "./useTaskDetail";
 
 axios.interceptors.request.use((config) => {
   config.paramsSerializer = {
@@ -106,4 +107,5 @@ export {
   useCalendarData,
   useCreateDatasetEvent,
   useRenderedK8s,
+  useTaskDetail,
 };
diff --git a/airflow/www/static/js/api/useTaskDetail.tsx b/airflow/www/static/js/api/useTaskDetail.tsx
new file mode 100644
index 0000000000000..ea9f184b04fb2
--- /dev/null
+++ b/airflow/www/static/js/api/useTaskDetail.tsx
@@ -0,0 +1,34 @@
+/*!
+ * 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 axios, { AxiosResponse } from "axios";
+import type { API } from "src/types";
+import { useQuery } from "react-query";
+import { getMetaValue } from "../utils";
+
+const taskDetailURI = getMetaValue("task_detail_api");
+
+export default function useTaskDetail({ taskId }: { taskId: string }) {
+  return useQuery(["taskDetails", taskId], async () => {
+    const url = taskDetailURI.replace("_TASK_ID_", taskId);
+
+    const datum = await axios.get<AxiosResponse, API.Task>(url);
+    return datum;
+  });
+}
diff --git a/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx b/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx
new file mode 100644
index 0000000000000..f0078aec90b1a
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx
@@ -0,0 +1,83 @@
+/*!
+ * 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 React from "react";
+import {
+  Accordion,
+  AccordionItem,
+  AccordionButton,
+  AccordionPanel,
+  AccordionIcon,
+  Alert,
+  AlertIcon,
+  Box,
+  Spinner,
+  Text,
+} from "@chakra-ui/react";
+
+import { useTaskDetail } from "src/api";
+import ReactMarkdown from "src/components/ReactMarkdown";
+
+interface Props {
+  taskId: string;
+}
+
+const TaskDocumentation = ({ taskId }: Props) => {
+  const { data, isLoading, error } = useTaskDetail({
+    taskId,
+  });
+
+  if (isLoading) {
+    return <Spinner size="md" thickness="4px" speed="0.65s" />;
+  }
+
+  if (error) {
+    return (
+      <Alert status="error" marginBottom="10px">
+        <AlertIcon />
+        An error occurred while fetching task documentation.
+      </Alert>
+    );
+  }
+
+  if (data && data.docMd) {
+    return (
+      <Box mt={3} flexGrow={1}>
+        <Accordion defaultIndex={[0]} allowToggle>
+          <AccordionItem>
+            <AccordionButton p={0} fontSize="inherit">
+              <Box flex="1" textAlign="left">
+                <Text as="strong" size="lg">
+                  Task Documentation:
+                </Text>
+              </Box>
+              <AccordionIcon />
+            </AccordionButton>
+            <AccordionPanel>
+              <ReactMarkdown>{data.docMd}</ReactMarkdown>
+            </AccordionPanel>
+          </AccordionItem>
+        </Accordion>
+      </Box>
+    );
+  }
+  return null;
+};
+
+export default TaskDocumentation;
diff --git a/airflow/www/static/js/dag/details/taskInstance/index.tsx b/airflow/www/static/js/dag/details/taskInstance/index.tsx
index 7dc3727ae4792..f33f3e23adbf0 100644
--- a/airflow/www/static/js/dag/details/taskInstance/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/index.tsx
@@ -31,6 +31,7 @@ import Details from "./Details";
 import DatasetUpdateEvents from "./DatasetUpdateEvents";
 import TriggererInfo from "./TriggererInfo";
 import TaskFailedDependency from "./TaskFailedDependency";
+import TaskDocumentation from "./TaskDocumentation";
 
 const dagId = getMetaValue("dag_id")!;
 
@@ -90,6 +91,7 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) => {
           operator={operator}
         />
       )}
+      {!isGroupOrMappedTaskSummary && <TaskDocumentation taskId={taskId} />}
       {!isGroupOrMappedTaskSummary && (
         <NotesAccordion
           dagId={dagId}
diff --git a/airflow/www/static/js/types/api-generated.ts b/airflow/www/static/js/types/api-generated.ts
index ff65443fe726c..6986020175894 100644
--- a/airflow/www/static/js/types/api-generated.ts
+++ b/airflow/www/static/js/types/api-generated.ts
@@ -1700,6 +1700,12 @@ export interface components {
       template_fields?: string[];
       sub_dag?: components["schemas"]["DAG"];
       downstream_task_ids?: string[];
+      /**
+       * @description Task documentation.
+       *
+       * *New in version 2.10.0*
+       */
+      doc_md?: string | null;
     };
     /** @description Collection of tasks. */
     TaskCollection: {
diff --git a/airflow/www/templates/airflow/dag.html b/airflow/www/templates/airflow/dag.html
index 81e5f59e004e6..ebdae42f4fe0b 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -85,8 +85,8 @@
   <meta name="audit_log_url" content="{{ url_for('LogModelView.list') }}">
   <meta name="dag_run_url" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_dag_run_endpoint_get_dag_run', dag_id='__DAG_ID__', dag_run_id='__DAG_RUN_ID__') }}">
   <meta name="task_dependency_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_task_instance_dependencies', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_') }}">
-    <meta name="mapped_task_dependency_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_mapped_task_instance_dependencies', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_', map_index=0) }}">
-
+  <meta name="mapped_task_dependency_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_mapped_task_instance_dependencies', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_', map_index=0) }}">
+  <meta name="task_detail_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_endpoint_get_task', dag_id=dag.dag_id, task_id='_TASK_ID_') }}">
 
   <!-- End Urls -->
   <meta name="is_paused" content="{{ dag_is_paused }}">

From 45edb1f2e7548c638fe0307f350b975054b785e0 Mon Sep 17 00:00:00 2001
From: Karthikeyan Singaravelan <tir.karthi@gmail.com>
Date: Tue, 28 May 2024 23:20:25 +0530
Subject: [PATCH 2/4] Fix tests.

---
 tests/api_connexion/endpoints/test_task_endpoint.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/tests/api_connexion/endpoints/test_task_endpoint.py b/tests/api_connexion/endpoints/test_task_endpoint.py
index 454b0db7525d1..64b7f4a8c5b7d 100644
--- a/tests/api_connexion/endpoints/test_task_endpoint.py
+++ b/tests/api_connexion/endpoints/test_task_endpoint.py
@@ -137,6 +137,7 @@ def test_should_respond_200(self):
             "wait_for_downstream": False,
             "weight_rule": "downstream",
             "is_mapped": False,
+            "doc_md": None,
         }
         response = self.client.get(
             f"/api/v1/dags/{self.dag_id}/tasks/{self.task_id}", environ_overrides={"REMOTE_USER": "test"}
@@ -172,6 +173,7 @@ def test_mapped_task(self):
             "ui_fgcolor": "#000",
             "wait_for_downstream": False,
             "weight_rule": "downstream",
+            "doc_md": None,
         }
         response = self.client.get(
             f"/api/v1/dags/{self.mapped_dag_id}/tasks/{self.mapped_task_id}",
@@ -225,6 +227,7 @@ def test_should_respond_200_serialized(self):
             "wait_for_downstream": False,
             "weight_rule": "downstream",
             "is_mapped": False,
+            "doc_md": None,
         }
         response = self.client.get(
             f"/api/v1/dags/{self.dag_id}/tasks/{self.task_id}", environ_overrides={"REMOTE_USER": "test"}
@@ -301,6 +304,7 @@ def test_should_respond_200(self):
                     "wait_for_downstream": False,
                     "weight_rule": "downstream",
                     "is_mapped": False,
+                    "doc_md": None,
                 },
                 {
                     "class_ref": {
@@ -332,6 +336,7 @@ def test_should_respond_200(self):
                     "wait_for_downstream": False,
                     "weight_rule": "downstream",
                     "is_mapped": False,
+                    "doc_md": None,
                 },
             ],
             "total_entries": 2,
@@ -372,6 +377,7 @@ def test_get_tasks_mapped(self):
                     "ui_fgcolor": "#000",
                     "wait_for_downstream": False,
                     "weight_rule": "downstream",
+                    "doc_md": None,
                 },
                 {
                     "class_ref": {
@@ -403,6 +409,7 @@ def test_get_tasks_mapped(self):
                     "wait_for_downstream": False,
                     "weight_rule": "downstream",
                     "is_mapped": False,
+                    "doc_md": None,
                 },
             ],
             "total_entries": 2,

From 83425803b26cd9cf6a7781e3db5ec5b2bfe4c947 Mon Sep 17 00:00:00 2001
From: Karthikeyan Singaravelan <tir.karthi@gmail.com>
Date: Tue, 28 May 2024 23:32:35 +0530
Subject: [PATCH 3/4] Fix tests.

---
 tests/api_connexion/schemas/test_task_schema.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tests/api_connexion/schemas/test_task_schema.py b/tests/api_connexion/schemas/test_task_schema.py
index e7abd814b8605..5748529b864af 100644
--- a/tests/api_connexion/schemas/test_task_schema.py
+++ b/tests/api_connexion/schemas/test_task_schema.py
@@ -60,6 +60,7 @@ def test_serialize(self):
             "wait_for_downstream": False,
             "weight_rule": "downstream",
             "is_mapped": False,
+            "doc_md": None,
         }
         assert expected == result
 
@@ -108,6 +109,7 @@ def test_serialize(self):
                     "wait_for_downstream": False,
                     "weight_rule": "downstream",
                     "is_mapped": False,
+                    "doc_md": None,
                 }
             ],
             "total_entries": 1,

From 7755ceb364e4c39ab6b7f10ac888fe171506b1c2 Mon Sep 17 00:00:00 2001
From: Karthikeyan Singaravelan <tir.karthi@gmail.com>
Date: Wed, 29 May 2024 16:25:14 +0530
Subject: [PATCH 4/4] Fix PR comments.

---
 airflow/api_connexion/openapi/v1.yaml                          | 2 +-
 airflow/www/static/js/api/useTaskDetail.tsx                    | 3 +--
 .../static/js/dag/details/taskInstance/TaskDocumentation.tsx   | 2 +-
 airflow/www/static/js/types/api-generated.ts                   | 2 +-
 4 files changed, 4 insertions(+), 5 deletions(-)

diff --git a/airflow/api_connexion/openapi/v1.yaml b/airflow/api_connexion/openapi/v1.yaml
index 63ac5768c33dc..c9b5a9d80852e 100644
--- a/airflow/api_connexion/openapi/v1.yaml
+++ b/airflow/api_connexion/openapi/v1.yaml
@@ -4129,7 +4129,7 @@ components:
           readOnly: true
           nullable: true
           description: |
-            Task documentation.
+            Task documentation in markdown.
 
             *New in version 2.10.0*
 
diff --git a/airflow/www/static/js/api/useTaskDetail.tsx b/airflow/www/static/js/api/useTaskDetail.tsx
index ea9f184b04fb2..d7e0ce81c1c43 100644
--- a/airflow/www/static/js/api/useTaskDetail.tsx
+++ b/airflow/www/static/js/api/useTaskDetail.tsx
@@ -28,7 +28,6 @@ export default function useTaskDetail({ taskId }: { taskId: string }) {
   return useQuery(["taskDetails", taskId], async () => {
     const url = taskDetailURI.replace("_TASK_ID_", taskId);
 
-    const datum = await axios.get<AxiosResponse, API.Task>(url);
-    return datum;
+    return axios.get<AxiosResponse, API.Task>(url);
   });
 }
diff --git a/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx b/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx
index f0078aec90b1a..a4ac20b8d1490 100644
--- a/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/TaskDocumentation.tsx
@@ -64,7 +64,7 @@ const TaskDocumentation = ({ taskId }: Props) => {
             <AccordionButton p={0} fontSize="inherit">
               <Box flex="1" textAlign="left">
                 <Text as="strong" size="lg">
-                  Task Documentation:
+                  Task Documentation
                 </Text>
               </Box>
               <AccordionIcon />
diff --git a/airflow/www/static/js/types/api-generated.ts b/airflow/www/static/js/types/api-generated.ts
index 6986020175894..a04c1cd0e07b4 100644
--- a/airflow/www/static/js/types/api-generated.ts
+++ b/airflow/www/static/js/types/api-generated.ts
@@ -1701,7 +1701,7 @@ export interface components {
       sub_dag?: components["schemas"]["DAG"];
       downstream_task_ids?: string[];
       /**
-       * @description Task documentation.
+       * @description Task documentation in markdown.
        *
        * *New in version 2.10.0*
        */
