You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by bb...@apache.org on 2022/08/14 22:37:21 UTC

[airflow] branch main updated: Grid logs for mapped instances (#25610)

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

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 88292837ba Grid logs for mapped instances (#25610)
88292837ba is described below

commit 88292837ba219c99f94bf3e03c5befda470c83b4
Author: pierrejeambrun <pi...@gmail.com>
AuthorDate: Mon Aug 15 00:36:53 2022 +0200

    Grid logs for mapped instances (#25610)
    
    * Add logs for mapped tasks
    
    * Mapped Task for download link, see more and paginated Table.
    
    * Handle refresh on select mapped task instance + other fix
    
    * Add useSelection tests for mapIndex
    
    * Stop propagation on mapped instance icon clicked
    
    * Add nav link for mapped task instances
    
    * Add Back to Dynamic Task Summary button
    
    * Move MappedInstances to its own tab
    
    * Update following code review
    
    * Simplifies the logic by using get_task_instance endpoint
    
    * Pull instance either from useGridData or useTaskInstance
    
    * Update test
---
 airflow/www/static/js/api/index.ts                 |  22 +--
 .../js/api/{useTaskLog.tsx => useTaskInstance.tsx} |  40 ++++--
 airflow/www/static/js/api/useTaskLog.tsx           |   7 +-
 .../www/static/js/dag/details/BreadcrumbText.tsx   |  11 +-
 airflow/www/static/js/dag/details/Header.tsx       |  14 +-
 airflow/www/static/js/dag/details/index.tsx        |   4 +-
 .../BackToTaskSummary.tsx}                         |  33 +++--
 .../static/js/dag/details/taskInstance/Details.tsx |   7 +
 .../dag/details/taskInstance/Logs/LogLink.test.tsx |   4 +-
 .../js/dag/details/taskInstance/Logs/LogLink.tsx   |   4 +-
 .../dag/details/taskInstance/Logs/index.test.tsx   |  37 +++++
 .../js/dag/details/taskInstance/Logs/index.tsx     |  16 ++-
 .../dag/details/taskInstance/MappedInstances.tsx   |  84 +++---------
 .../www/static/js/dag/details/taskInstance/Nav.tsx |   8 +-
 .../static/js/dag/details/taskInstance/index.tsx   | 152 +++++++++++----------
 .../{ActionButton.jsx => ActionButton.tsx}         |   9 +-
 .../dag/details/taskInstance/taskActions/index.tsx |  76 +++++++++++
 .../taskActions/types.tsx}                         |  24 +---
 airflow/www/static/js/dag/useSelection.test.tsx    |  12 +-
 airflow/www/static/js/dag/useSelection.ts          |  11 +-
 airflow/www/static/js/types/index.ts               |   1 +
 airflow/www/templates/airflow/dag.html             |   1 +
 22 files changed, 363 insertions(+), 214 deletions(-)

diff --git a/airflow/www/static/js/api/index.ts b/airflow/www/static/js/api/index.ts
index 5bc3a1d4af..35b2eacbd2 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -36,6 +36,7 @@ import useDatasets from './useDatasets';
 import useDataset from './useDataset';
 import useDatasetEvents from './useDatasetEvents';
 import useUpstreamDatasetEvents from './useUpstreamDatasetEvents';
+import useTaskInstance from './useTaskInstance';
 
 axios.interceptors.response.use(
   (res: AxiosResponse) => (res.data ? camelcaseKeys(res.data, { deep: true }) : res),
@@ -45,19 +46,20 @@ axios.defaults.headers.common.Accept = 'application/json';
 
 export {
   useClearRun,
-  useQueueRun,
-  useMarkFailedRun,
-  useMarkSuccessRun,
-  useRunTask,
   useClearTask,
-  useMarkFailedTask,
-  useMarkSuccessTask,
-  useExtraLinks,
   useConfirmMarkTask,
-  useGridData,
-  useMappedInstances,
-  useDatasets,
   useDataset,
   useDatasetEvents,
+  useDatasets,
+  useExtraLinks,
+  useGridData,
+  useMappedInstances,
+  useMarkFailedRun,
+  useMarkFailedTask,
+  useMarkSuccessRun,
+  useMarkSuccessTask,
+  useQueueRun,
+  useRunTask,
+  useTaskInstance,
   useUpstreamDatasetEvents,
 };
diff --git a/airflow/www/static/js/api/useTaskLog.tsx b/airflow/www/static/js/api/useTaskInstance.tsx
similarity index 54%
copy from airflow/www/static/js/api/useTaskLog.tsx
copy to airflow/www/static/js/api/useTaskInstance.tsx
index 43e7f957c2..1678bb85e0 100644
--- a/airflow/www/static/js/api/useTaskLog.tsx
+++ b/airflow/www/static/js/api/useTaskInstance.tsx
@@ -18,37 +18,53 @@
  */
 
 import axios, { AxiosResponse } from 'axios';
+import type { API, TaskInstance } from 'src/types';
 import { useQuery } from 'react-query';
 import { useAutoRefresh } from 'src/context/autorefresh';
 
 import { getMetaValue } from 'src/utils';
 
-const taskLogApi = getMetaValue('task_log_api');
+/* GridData.TaskInstance and API.TaskInstance are not compatible at the moment.
+ * Remove this function when changing the api response for grid_data_url to comply
+ * with API.TaskInstance.
+ */
+const convertTaskInstance = (
+  ti:
+  API.TaskInstance,
+) => ({ ...ti, runId: ti.dagRunId }) as TaskInstance;
+
+const taskInstanceApi = getMetaValue('task_instance_api');
 
-const useTaskLog = ({
-  dagId, dagRunId, taskId, taskTryNumber, fullContent,
+const useTaskInstance = ({
+  dagId, dagRunId, taskId, mapIndex, enabled,
 }: {
   dagId: string,
   dagRunId: string,
-  taskId: string,
-  taskTryNumber: number,
-  fullContent: boolean,
+  taskId: string | null,
+  mapIndex?: number,
+  enabled: boolean
 }) => {
   let url: string = '';
-  if (taskLogApi) {
-    url = taskLogApi.replace('_DAG_RUN_ID_', dagRunId).replace('_TASK_ID_', taskId).replace(/-1$/, taskTryNumber.toString());
+  if (taskInstanceApi) {
+    url = taskInstanceApi.replace('_DAG_RUN_ID_', dagRunId).replace('_TASK_ID_', taskId || '');
+  }
+
+  if (mapIndex !== undefined && mapIndex >= 0) {
+    url += `/${mapIndex.toString()}`;
   }
 
   const { isRefreshOn } = useAutoRefresh();
 
   return useQuery(
-    ['taskLogs', dagId, dagRunId, taskId, taskTryNumber, fullContent],
-    () => axios.get<AxiosResponse, string>(url, { headers: { Accept: 'text/plain' }, params: { full_content: fullContent } }),
+    ['taskIntance', dagId, dagRunId, taskId, mapIndex],
+    () => axios.get<AxiosResponse, API.TaskInstance>(url, { headers: { Accept: 'text/plain' } }),
     {
-      placeholderData: '',
+      placeholderData: {},
       refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000,
+      enabled,
+      select: convertTaskInstance,
     },
   );
 };
 
-export default useTaskLog;
+export default useTaskInstance;
diff --git a/airflow/www/static/js/api/useTaskLog.tsx b/airflow/www/static/js/api/useTaskLog.tsx
index 43e7f957c2..f389f4c118 100644
--- a/airflow/www/static/js/api/useTaskLog.tsx
+++ b/airflow/www/static/js/api/useTaskLog.tsx
@@ -26,12 +26,13 @@ import { getMetaValue } from 'src/utils';
 const taskLogApi = getMetaValue('task_log_api');
 
 const useTaskLog = ({
-  dagId, dagRunId, taskId, taskTryNumber, fullContent,
+  dagId, dagRunId, taskId, taskTryNumber, mapIndex, fullContent,
 }: {
   dagId: string,
   dagRunId: string,
   taskId: string,
   taskTryNumber: number,
+  mapIndex?: number,
   fullContent: boolean,
 }) => {
   let url: string = '';
@@ -42,8 +43,8 @@ const useTaskLog = ({
   const { isRefreshOn } = useAutoRefresh();
 
   return useQuery(
-    ['taskLogs', dagId, dagRunId, taskId, taskTryNumber, fullContent],
-    () => axios.get<AxiosResponse, string>(url, { headers: { Accept: 'text/plain' }, params: { full_content: fullContent } }),
+    ['taskLogs', dagId, dagRunId, taskId, mapIndex, taskTryNumber, fullContent],
+    () => axios.get<AxiosResponse, string>(url, { headers: { Accept: 'text/plain' }, params: { map_index: mapIndex, full_content: fullContent } }),
     {
       placeholderData: '',
       refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000,
diff --git a/airflow/www/static/js/dag/details/BreadcrumbText.tsx b/airflow/www/static/js/dag/details/BreadcrumbText.tsx
index 85d5f6dec5..b0cf19161a 100644
--- a/airflow/www/static/js/dag/details/BreadcrumbText.tsx
+++ b/airflow/www/static/js/dag/details/BreadcrumbText.tsx
@@ -30,7 +30,16 @@ interface Props {
 
 const BreadcrumbText = ({ label, value }: Props) => (
   <Box position="relative">
-    <Heading as="h5" size="sm" color="gray.300" position="absolute" top="-12px">{label}</Heading>
+    <Heading
+      as="h5"
+      size="sm"
+      color="gray.300"
+      position="absolute"
+      top="-12px"
+      whiteSpace="nowrap"
+    >
+      {label}
+    </Heading>
     <Heading as="h3" size="md">{value}</Heading>
   </Box>
 );
diff --git a/airflow/www/static/js/dag/details/Header.tsx b/airflow/www/static/js/dag/details/Header.tsx
index 502dba5942..3b957f6a17 100644
--- a/airflow/www/static/js/dag/details/Header.tsx
+++ b/airflow/www/static/js/dag/details/Header.tsx
@@ -38,7 +38,7 @@ const dagId = getMetaValue('dag_id');
 const Header = () => {
   const { data: { dagRuns, groups } } = useGridData();
 
-  const { selected: { taskId, runId }, onSelect, clearSelection } = useSelection();
+  const { selected: { taskId, runId, mapIndex }, onSelect, clearSelection } = useSelection();
   const dagRun = dagRuns.find((r) => r.runId === runId);
 
   // clearSelection if the current selected dagRun is
@@ -75,7 +75,8 @@ const Header = () => {
 
   const isDagDetails = !runId && !taskId;
   const isRunDetails = !!(runId && !taskId);
-  const isTaskDetails = runId && taskId;
+  const isTaskDetails = runId && taskId && mapIndex === null;
+  const isMappedTaskDetails = runId && taskId && mapIndex !== null;
 
   return (
     <Breadcrumb separator={<Text color="gray.300">/</Text>}>
@@ -93,11 +94,18 @@ const Header = () => {
       )}
       {taskId && (
         <BreadcrumbItem isCurrentPage mt={4}>
-          <BreadcrumbLink _hover={isTaskDetails ? { cursor: 'default' } : undefined}>
+          <BreadcrumbLink onClick={() => onSelect({ runId, taskId })} _hover={isTaskDetails ? { cursor: 'default' } : undefined}>
             <BreadcrumbText label="Task" value={`${taskName}${group?.isMapped ? ' []' : ''}`} />
           </BreadcrumbLink>
         </BreadcrumbItem>
       )}
+      {mapIndex !== null && (
+        <BreadcrumbItem isCurrentPage mt={4}>
+          <BreadcrumbLink _hover={isMappedTaskDetails ? { cursor: 'default' } : undefined}>
+            <BreadcrumbText label="Map Index" value={mapIndex} />
+          </BreadcrumbLink>
+        </BreadcrumbItem>
+      )}
     </Breadcrumb>
   );
 };
diff --git a/airflow/www/static/js/dag/details/index.tsx b/airflow/www/static/js/dag/details/index.tsx
index 9ba19cabf2..557bc1b8b9 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -32,7 +32,7 @@ import DagRunContent from './dagRun';
 import DagContent from './Dag';
 
 const Details = () => {
-  const { selected: { runId, taskId } } = useSelection();
+  const { selected: { runId, taskId, mapIndex }, onSelect } = useSelection();
   return (
     <Flex flexDirection="column" pl={3} mr={3} flexGrow={1} maxWidth="750px">
       <Header />
@@ -46,6 +46,8 @@ const Details = () => {
         <TaskInstanceContent
           runId={runId}
           taskId={taskId}
+          mapIndex={mapIndex === null ? undefined : mapIndex}
+          onSelect={onSelect}
         />
         )}
       </Box>
diff --git a/airflow/www/static/js/dag/details/BreadcrumbText.tsx b/airflow/www/static/js/dag/details/taskInstance/BackToTaskSummary.tsx
similarity index 64%
copy from airflow/www/static/js/dag/details/BreadcrumbText.tsx
copy to airflow/www/static/js/dag/details/taskInstance/BackToTaskSummary.tsx
index 85d5f6dec5..26918d5cb5 100644
--- a/airflow/www/static/js/dag/details/BreadcrumbText.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/BackToTaskSummary.tsx
@@ -18,21 +18,28 @@
  */
 
 import React from 'react';
-import {
-  Box,
-  Heading,
-} from '@chakra-ui/react';
+import { Button, Flex } from '@chakra-ui/react';
 
 interface Props {
-  label: string;
-  value: React.ReactNode;
+  isMapIndexDefined: boolean;
+  onClick: () => void;
 }
 
-const BreadcrumbText = ({ label, value }: Props) => (
-  <Box position="relative">
-    <Heading as="h5" size="sm" color="gray.300" position="absolute" top="-12px">{label}</Heading>
-    <Heading as="h3" size="md">{value}</Heading>
-  </Box>
-);
+const BackToTaskSummary = ({ isMapIndexDefined, onClick }: Props) => {
+  if (!isMapIndexDefined) return null;
 
-export default BreadcrumbText;
+  return (
+    <Flex justifyContent="right">
+      <Button
+        variant="ghost"
+        colorScheme="blue"
+        onClick={onClick}
+        size="lg"
+      >
+        Back to Dynamic Task Summary
+      </Button>
+    </Flex>
+  );
+};
+
+export default BackToTaskSummary;
diff --git a/airflow/www/static/js/dag/details/taskInstance/Details.tsx b/airflow/www/static/js/dag/details/taskInstance/Details.tsx
index 91d453b3c4..ed0174b72b 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Details.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Details.tsx
@@ -52,6 +52,7 @@ const Details = ({ instance, group }: Props) => {
     endDate,
     state,
     mappedStates,
+    mapIndex,
   } = instance;
 
   const {
@@ -142,6 +143,12 @@ const Details = ({ instance, group }: Props) => {
             <Td>Run ID</Td>
             <Td><Text whiteSpace="nowrap"><ClipboardText value={runId} /></Text></Td>
           </Tr>
+          {mapIndex !== undefined && (
+            <Tr>
+              <Td>Map Index</Td>
+              <Td>{mapIndex}</Td>
+            </Tr>
+          )}
           {operator && (
             <Tr>
               <Td>Operator</Td>
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.test.tsx b/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.test.tsx
index c6ae0ce49d..e3e235596e 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.test.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.test.tsx
@@ -41,7 +41,7 @@ describe('Test LogLink Component.', () => {
     expect(linkElement).toBeDefined();
     expect(linkElement).not.toHaveAttribute('target');
     expect(linkElement?.href.includes(
-      `?dag_id=dummyDagId&task_id=dummyTaskId&execution_date=2020%3A01%3A01T01%3A00%2B00%3A00&format=file&try_number=${tryNumber}`,
+      `?dag_id=dummyDagId&task_id=dummyTaskId&execution_date=2020%3A01%3A01T01%3A00%2B00%3A00&map_index=-1&format=file&try_number=${tryNumber}`,
     )).toBeTruthy();
   });
 
@@ -61,7 +61,7 @@ describe('Test LogLink Component.', () => {
     expect(linkElement).toBeDefined();
     expect(linkElement).toHaveAttribute('target', '_blank');
     expect(linkElement?.href.includes(
-      `?dag_id=dummyDagId&task_id=dummyTaskId&execution_date=2020%3A01%3A01T01%3A00%2B00%3A00&try_number=${tryNumber}`,
+      `?dag_id=dummyDagId&task_id=dummyTaskId&execution_date=2020%3A01%3A01T01%3A00%2B00%3A00&map_index=-1&try_number=${tryNumber}`,
     )).toBeTruthy();
   });
 });
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.tsx b/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.tsx
index 49683112b5..9ae2704ce6 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/LogLink.tsx
@@ -32,15 +32,17 @@ interface Props {
   executionDate: DagRun['executionDate'];
   isInternal?: boolean;
   tryNumber: TaskInstance['tryNumber'];
+  mapIndex?: TaskInstance['mapIndex'];
 }
 
 const LogLink = ({
-  dagId, taskId, executionDate, isInternal, tryNumber,
+  dagId, taskId, executionDate, isInternal, tryNumber, mapIndex,
 }: Props) => {
   let fullMetadataUrl = `${isInternal ? logsWithMetadataUrl : externalLogUrl
   }?dag_id=${encodeURIComponent(dagId)
   }&task_id=${encodeURIComponent(taskId)
   }&execution_date=${encodeURIComponent(executionDate)
+  }&map_index=${encodeURIComponent(mapIndex?.toString() ?? '-1')
   }`;
 
   if (isInternal && tryNumber) {
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/index.test.tsx b/airflow/www/static/js/dag/details/taskInstance/Logs/index.test.tsx
index 1f748a1df7..6c11c59305 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.test.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.test.tsx
@@ -78,6 +78,43 @@ describe('Test Logs Component.', () => {
       { exact: false },
     )).toBeDefined();
     expect(getByText('AIRFLOW_CTX_DAG_ID=test_ui_grid', { exact: false })).toBeDefined();
+
+    expect(useTaskLogMock).toHaveBeenLastCalledWith({
+      dagId: 'dummyDagId',
+      dagRunId: 'dummyDagRunId',
+      fullContent: false,
+      taskId: 'dummyTaskId',
+      taskTryNumber: 1,
+    });
+  });
+
+  test('Test Logs Content Mapped Task', () => {
+    const tryNumber = 2;
+    const { getByText } = render(
+      <Logs
+        dagId="dummyDagId"
+        dagRunId="dummyDagRunId"
+        taskId="dummyTaskId"
+        executionDate="2020:01:01T01:00+00:00"
+        mapIndex={1}
+        tryNumber={tryNumber}
+      />,
+    );
+    expect(getByText('[2022-06-04, 00:00:01 UTC] {taskinstance.py:1329} INFO -', { exact: false })).toBeDefined();
+    expect(getByText(
+      '[2022-06-04, 00:00:01 UTC] {standard_task_runner.py:81} INFO - Job 1626: Subtask section_1.get_entry_group',
+      { exact: false },
+    )).toBeDefined();
+    expect(getByText('AIRFLOW_CTX_DAG_ID=test_ui_grid', { exact: false })).toBeDefined();
+
+    expect(useTaskLogMock).toHaveBeenLastCalledWith({
+      dagId: 'dummyDagId',
+      dagRunId: 'dummyDagRunId',
+      fullContent: false,
+      mapIndex: 1,
+      taskId: 'dummyTaskId',
+      taskTryNumber: 1,
+    });
   });
 
   test('Test Logs Attempt Select Button', () => {
diff --git a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
index 58bb030ed8..9a2b33205a 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
@@ -84,6 +84,7 @@ interface Props {
   dagId: Dag['id'];
   dagRunId: DagRun['runId'];
   taskId: TaskInstance['taskId'];
+  mapIndex?: TaskInstance['mapIndex'];
   executionDate: DagRun['executionDate'];
   tryNumber: TaskInstance['tryNumber'];
 }
@@ -92,6 +93,7 @@ const Logs = ({
   dagId,
   dagRunId,
   taskId,
+  mapIndex,
   executionDate,
   tryNumber,
 }: Props) => {
@@ -106,6 +108,7 @@ const Logs = ({
     dagId,
     dagRunId,
     taskId,
+    mapIndex,
     taskTryNumber: selectedAttempt,
     fullContent: shouldRequestFullContent,
   });
@@ -113,7 +116,11 @@ const Logs = ({
   const params = new URLSearchParams({
     task_id: taskId,
     execution_date: executionDate,
-  }).toString();
+  });
+
+  if (mapIndex !== undefined) {
+    params.append('map_index', mapIndex.toString());
+  }
 
   const { parsedLogs, fileSources = [] } = useMemo(
     () => parseLogs(
@@ -136,7 +143,7 @@ const Logs = ({
   useEffect(() => {
     // Reset fileSourceFilters and selected attempt when changing to
     // a task that do not have those filters anymore.
-    if (!internalIndexes.includes(selectedAttempt)) {
+    if (!internalIndexes.includes(selectedAttempt) && internalIndexes.length) {
       setSelectedAttempt(internalIndexes[0]);
     }
 
@@ -151,7 +158,7 @@ const Logs = ({
 
   return (
     <>
-      {tryNumber! > 0 && (
+      {tryNumber !== undefined && (
         <>
           <Text as="span"> (by attempts)</Text>
           <Flex my={1} justifyContent="space-between">
@@ -227,9 +234,10 @@ const Logs = ({
                 executionDate={executionDate}
                 isInternal
                 tryNumber={tryNumber}
+                mapIndex={mapIndex}
               />
               <LinkButton
-                href={`${logUrl}&${params}`}
+                href={`${logUrl}&${params.toString()}`}
               >
                 See More
               </LinkButton>
diff --git a/airflow/www/static/js/dag/details/taskInstance/MappedInstances.tsx b/airflow/www/static/js/dag/details/taskInstance/MappedInstances.tsx
index 8b45a4be2c..9796814fcc 100644
--- a/airflow/www/static/js/dag/details/taskInstance/MappedInstances.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/MappedInstances.tsx
@@ -17,51 +17,32 @@
  * under the License.
  */
 
-import React, { useState, useMemo } from 'react';
+import React, {
+  useState, useMemo,
+} from 'react';
 import {
   Flex,
   Text,
   Box,
-  Link,
-  IconButton,
-  IconButtonProps,
 } from '@chakra-ui/react';
 import { snakeCase } from 'lodash';
-import {
-  MdDetails, MdCode, MdSyncAlt, MdReorder,
-} from 'react-icons/md';
-import type { SortingRule } from 'react-table';
+import type { Row, SortingRule } from 'react-table';
 
-import { getMetaValue } from 'src/utils';
 import { formatDuration, getDuration } from 'src/datetime_utils';
 import { useMappedInstances } from 'src/api';
 import { SimpleStatus } from 'src/dag/StatusBox';
 import { Table } from 'src/components/Table';
 import Time from 'src/components/Time';
 
-const canEdit = getMetaValue('can_edit') === 'True';
-const renderedTemplatesUrl = getMetaValue('rendered_templates_url');
-const logUrl = getMetaValue('log_url');
-const taskUrl = getMetaValue('task_url');
-const xcomUrl = getMetaValue('xcom_url');
-
-interface IconLinkProps extends IconButtonProps {
-  href: string;
-}
-
-const IconLink = (props: IconLinkProps) => (
-  <IconButton as={Link} variant="ghost" colorScheme="blue" fontSize="3xl" {...props} />
-);
-
 interface Props {
   dagId: string;
   runId: string;
   taskId: string;
-  selectRows: (selectedRows: number[]) => void;
+  onRowClicked: (row: Row) => void;
 }
 
 const MappedInstances = ({
-  dagId, runId, taskId, selectRows,
+  dagId, runId, taskId, onRowClicked,
 }: Props) => {
   const limit = 25;
   const [offset, setOffset] = useState(0);
@@ -78,41 +59,18 @@ const MappedInstances = ({
     dagId, runId, taskId, limit, offset, order,
   });
 
-  const data = useMemo(
-    () => taskInstances.map((mi) => {
-      const params = new URLSearchParams({
-        dag_id: dagId.toString(),
-        task_id: mi.taskId || '',
-        execution_date: mi.executionDate || '',
-        map_index: (mi.mapIndex || -1).toString(),
-      }).toString();
-      const detailsLink = `${taskUrl}&${params}`;
-      const renderedLink = `${renderedTemplatesUrl}&${params}`;
-      const logLink = `${logUrl}&${params}`;
-      const xcomLink = `${xcomUrl}&${params}`;
-      return {
-        ...mi,
-        state: (
-          <Flex alignItems="center">
-            <SimpleStatus state={mi.state === undefined || mi.state === 'none' ? null : mi.state} mx={2} />
-            {mi.state || 'no status'}
-          </Flex>
-        ),
-        duration: mi.duration && formatDuration(getDuration(mi.startDate, mi.endDate)),
-        startDate: <Time dateTime={mi.startDate} />,
-        endDate: <Time dateTime={mi.endDate} />,
-        links: (
-          <Flex alignItems="center">
-            <IconLink mr={1} title="Details" aria-label="Details" icon={<MdDetails />} href={detailsLink} />
-            <IconLink mr={1} title="Rendered Templates" aria-label="Rendered Templates" icon={<MdCode />} href={renderedLink} />
-            <IconLink mr={1} title="Log" aria-label="Log" icon={<MdReorder />} href={logLink} />
-            <IconLink title="XCom" fontWeight="bold" aria-label="XCom" icon={<MdSyncAlt />} href={xcomLink} />
-          </Flex>
-        ),
-      };
-    }),
-    [dagId, taskInstances],
-  );
+  const data = useMemo(() => taskInstances.map((mi) => ({
+    ...mi,
+    state: (
+      <Flex alignItems="center">
+        <SimpleStatus state={mi.state === undefined || mi.state === 'none' ? null : mi.state} mx={2} />
+        {mi.state || 'no status'}
+      </Flex>
+    ),
+    duration: mi.duration && formatDuration(getDuration(mi.startDate, mi.endDate)),
+    startDate: <Time dateTime={mi.startDate} />,
+    endDate: <Time dateTime={mi.endDate} />,
+  })), [taskInstances]);
 
   const columns = useMemo(
     () => [
@@ -139,10 +97,6 @@ const MappedInstances = ({
         accessor: 'endDate',
         disableSortBy: true,
       },
-      {
-        disableSortBy: true,
-        accessor: 'links',
-      },
     ],
     [],
   );
@@ -165,7 +119,7 @@ const MappedInstances = ({
           sortBy,
         }}
         isLoading={isLoading}
-        selectRows={canEdit ? selectRows : undefined}
+        onRowClicked={onRowClicked}
       />
     </Box>
   );
diff --git a/airflow/www/static/js/dag/details/taskInstance/Nav.tsx b/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
index 36782f067e..6a2c314930 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Nav.tsx
@@ -46,16 +46,18 @@ interface Props {
   executionDate: string;
   operator?: string;
   isMapped?: boolean;
+  mapIndex?: number;
 }
 
 const Nav = ({
-  runId, taskId, executionDate, operator, isMapped = false,
+  runId, taskId, executionDate, operator, isMapped = false, mapIndex,
 }: Props) => {
   if (!taskId) return null;
   const params = new URLSearchParams({
     task_id: taskId,
     execution_date: executionDate,
-  }).toString();
+    map_index: mapIndex?.toString() ?? '-1',
+  });
   const detailsLink = `${taskUrl}&${params}`;
   const renderedLink = `${renderedTemplatesUrl}&${params}`;
   const logLink = `${logUrl}&${params}`;
@@ -90,7 +92,7 @@ const Nav = ({
   return (
     <>
       <Flex flexWrap="wrap">
-        {!isMapped && (
+        {(!isMapped || mapIndex !== undefined) && (
         <>
           <LinkButton href={detailsLink}>Task Instance Details</LinkButton>
           <LinkButton href={renderedLink}>Rendered Template</LinkButton>
diff --git a/airflow/www/static/js/dag/details/taskInstance/index.tsx b/airflow/www/static/js/dag/details/taskInstance/index.tsx
index e0490fd2e6..40248e2428 100644
--- a/airflow/www/static/js/dag/details/taskInstance/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/index.tsx
@@ -22,9 +22,6 @@
 import React, { useState } from 'react';
 import {
   Box,
-  VStack,
-  Divider,
-  StackDivider,
   Text,
   Tabs,
   TabList,
@@ -33,19 +30,20 @@ import {
   TabPanel,
 } from '@chakra-ui/react';
 
-import { useGridData } from 'src/api';
+import { useGridData, useTaskInstance } from 'src/api';
 import { getMetaValue, getTask } from 'src/utils';
-import type { Task, DagRun } from 'src/types';
+import type {
+  Task, DagRun, TaskInstance as TaskInstanceType,
+} from 'src/types';
 
-import RunAction from './taskActions/Run';
-import ClearAction from './taskActions/Clear';
-import MarkFailedAction from './taskActions/MarkFailed';
-import MarkSuccessAction from './taskActions/MarkSuccess';
+import type { SelectionProps } from 'src/dag/useSelection';
 import ExtraLinks from './ExtraLinks';
 import Logs from './Logs';
 import TaskNav from './Nav';
 import Details from './Details';
 import MappedInstances from './MappedInstances';
+import TaskActions from './taskActions';
+import BackToTaskSummary from './BackToTaskSummary';
 
 const detailsPanelActiveTabIndex = 'detailsPanelActiveTabIndex';
 
@@ -54,10 +52,15 @@ const dagId = getMetaValue('dag_id')!;
 interface Props {
   taskId: Task['id'];
   runId: DagRun['runId'];
+  mapIndex: TaskInstanceType['mapIndex'];
+  onSelect: (selectionProps: SelectionProps) => void;
 }
 
-const TaskInstance = ({ taskId, runId }: Props) => {
-  const [selectedRows, setSelectedRows] = useState<number[]>([]);
+const TaskInstance = ({
+  taskId, runId, mapIndex, onSelect,
+}: Props) => {
+  const isMapIndexDefined = !(mapIndex === undefined);
+  const actionsMapIndexes = isMapIndexDefined ? [mapIndex] : [];
   const { data: { dagRuns, groups } } = useGridData();
 
   const storageTabIndex = parseInt(localStorage.getItem(detailsPanelActiveTabIndex) || '0', 10);
@@ -66,17 +69,28 @@ const TaskInstance = ({ taskId, runId }: Props) => {
   const group = getTask({ taskId, task: groups });
   const run = dagRuns.find((r) => r.runId === runId);
 
-  if (!group || !run) return null;
+  const children = group?.children;
+  const isMapped = group?.isMapped;
+  const operator = group?.operator;
 
-  const { children, isMapped, operator } = group;
+  const isMappedTaskSummary = !!isMapped && !isMapIndexDefined && taskId;
+  const isGroup = !!children;
+  const isGroupOrMappedTaskSummary = (isGroup || isMappedTaskSummary);
+
+  const { data: mappedTaskInstance } = useTaskInstance({
+    dagId, dagRunId: runId, taskId, mapIndex, enabled: isMapIndexDefined,
+  });
+
+  const instance = isMapIndexDefined
+    ? mappedTaskInstance
+    : group?.instances.find((ti) => ti.runId === runId);
 
   const handleTabsChange = (index: number) => {
     localStorage.setItem(detailsPanelActiveTabIndex, index.toString());
     setPreferedTabIndex(index);
   };
 
-  const isGroup = !!children;
-  const isSimpleTask = !isMapped && !isGroup;
+  if (!group || !run || !instance) return null;
 
   let isPreferedTabDisplayed = false;
 
@@ -85,7 +99,7 @@ const TaskInstance = ({ taskId, runId }: Props) => {
       isPreferedTabDisplayed = true;
       break;
     case 1:
-      isPreferedTabDisplayed = isSimpleTask;
+      isPreferedTabDisplayed = !isGroup;
       break;
     default:
       isPreferedTabDisplayed = false;
@@ -95,12 +109,9 @@ const TaskInstance = ({ taskId, runId }: Props) => {
 
   const { executionDate } = run;
 
-  const instance = group.instances.find((ti) => ti.runId === runId);
-  if (!instance) return null;
-
   let taskActionsTitle = 'Task Actions';
   if (isMapped) {
-    taskActionsTitle += ` for ${selectedRows.length || 'all'} mapped task${selectedRows.length !== 1 ? 's' : ''}`;
+    taskActionsTitle += ` for ${actionsMapIndexes.length || 'all'} mapped task${actionsMapIndexes.length !== 1 ? 's' : ''}`;
   }
 
   return (
@@ -110,6 +121,7 @@ const TaskInstance = ({ taskId, runId }: Props) => {
           taskId={taskId}
           runId={runId}
           isMapped={isMapped}
+          mapIndex={mapIndex}
           executionDate={executionDate}
           operator={operator}
         />
@@ -119,49 +131,37 @@ const TaskInstance = ({ taskId, runId }: Props) => {
           <Tab>
             <Text as="strong">Details</Text>
           </Tab>
-          { isSimpleTask && (
+          {isMappedTaskSummary && (
+            <Tab>
+              <Text as="strong">Mapped Tasks</Text>
+            </Tab>
+          )}
+          {!isGroupOrMappedTaskSummary && (
             <Tab>
               <Text as="strong">Logs</Text>
             </Tab>
           )}
         </TabList>
+
+        <BackToTaskSummary
+          isMapIndexDefined={isMapIndexDefined}
+          onClick={() => onSelect({ runId, taskId })}
+        />
+
         <TabPanels>
+
           {/* Details Tab */}
-          <TabPanel>
+          <TabPanel pt={isMapIndexDefined ? '0px' : undefined}>
             <Box py="4px">
               {!isGroup && (
-                <Box my={3}>
-                  <Text as="strong">{taskActionsTitle}</Text>
-                  <Divider my={2} />
-                  <VStack justifyContent="center" divider={<StackDivider my={3} />}>
-                    <RunAction
-                      runId={runId}
-                      taskId={taskId}
-                      dagId={dagId}
-                      mapIndexes={selectedRows}
-                    />
-                    <ClearAction
-                      runId={runId}
-                      taskId={taskId}
-                      dagId={dagId}
-                      executionDate={executionDate}
-                      mapIndexes={selectedRows}
-                    />
-                    <MarkFailedAction
-                      runId={runId}
-                      taskId={taskId}
-                      dagId={dagId}
-                      mapIndexes={selectedRows}
-                    />
-                    <MarkSuccessAction
-                      runId={runId}
-                      taskId={taskId}
-                      dagId={dagId}
-                      mapIndexes={selectedRows}
-                    />
-                  </VStack>
-                  <Divider my={2} />
-                </Box>
+                <TaskActions
+                  title={taskActionsTitle}
+                  runId={runId}
+                  taskId={taskId}
+                  dagId={dagId}
+                  executionDate={executionDate}
+                  mapIndexes={actionsMapIndexes}
+                />
               )}
               <Details instance={instance} group={group} />
               {!isMapped && (
@@ -172,28 +172,36 @@ const TaskInstance = ({ taskId, runId }: Props) => {
                   extraLinks={group?.extraLinks || []}
                 />
               )}
-              {isMapped && taskId && (
-                <MappedInstances
-                  dagId={dagId}
-                  runId={runId}
-                  taskId={taskId}
-                  selectRows={setSelectedRows}
-                />
-              )}
             </Box>
           </TabPanel>
+
           {/* Logs Tab */}
-          { isSimpleTask && (
-          <TabPanel>
-            <Logs
-              dagId={dagId}
-              dagRunId={runId}
-              taskId={taskId!}
-              executionDate={executionDate}
-              tryNumber={instance?.tryNumber}
-            />
-          </TabPanel>
+          {!isGroupOrMappedTaskSummary && (
+            <TabPanel pt={isMapIndexDefined ? '0px' : undefined}>
+              <Logs
+                dagId={dagId}
+                dagRunId={runId}
+                taskId={taskId!}
+                mapIndex={mapIndex}
+                executionDate={executionDate}
+                tryNumber={instance?.tryNumber}
+              />
+            </TabPanel>
           )}
+
+          {/* Mapped Task Instances Tab */}
+          {
+            isMappedTaskSummary && (
+            <TabPanel>
+              <MappedInstances
+                dagId={dagId}
+                runId={runId}
+                taskId={taskId}
+                onRowClicked={(row) => onSelect({ runId, taskId, mapIndex: row.values.mapIndex })}
+              />
+            </TabPanel>
+            )
+          }
         </TabPanels>
       </Tabs>
     </Box>
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.jsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx
similarity index 79%
rename from airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.jsx
rename to airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx
index d25db9eb53..09c189b49c 100644
--- a/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.jsx
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/ActionButton.tsx
@@ -18,7 +18,7 @@
  */
 
 import React from 'react';
-import { Button } from '@chakra-ui/react';
+import { Button, ButtonProps } from '@chakra-ui/react';
 
 const titleMap = {
   past: 'Also include past task instances when clearing this one',
@@ -29,8 +29,11 @@ const titleMap = {
   failed: 'Only consider failed task instances when clearing this one',
 };
 
-const ActionButton = ({ name, ...rest }) => (
-  <Button title={titleMap[name.toLowerCase()]} {...rest}>{name}</Button>
+type KeysOfTitleMap = keyof (typeof titleMap);
+
+type Props = ButtonProps & { name: Capitalize<KeysOfTitleMap> } ;
+const ActionButton = ({ name, ...rest }: Props) => (
+  <Button title={titleMap[name.toLowerCase() as KeysOfTitleMap]} {...rest}>{name}</Button>
 );
 
 export default ActionButton;
diff --git a/airflow/www/static/js/dag/details/taskInstance/taskActions/index.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/index.tsx
new file mode 100644
index 0000000000..2ba97a1e54
--- /dev/null
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/index.tsx
@@ -0,0 +1,76 @@
+/*!
+ * 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 {
+  Box,
+  VStack,
+  Divider,
+  StackDivider,
+  Text,
+} from '@chakra-ui/react';
+
+import type { CommonActionProps } from './types';
+import RunAction from './Run';
+import ClearAction from './Clear';
+import MarkFailedAction from './MarkFailed';
+import MarkSuccessAction from './MarkSuccess';
+
+type Props = {
+  title: string;
+} & CommonActionProps;
+
+const TaskActions = ({
+  title, runId, taskId, dagId, executionDate, mapIndexes,
+}: Props) => (
+  <Box my={3}>
+    <Text as="strong">{title}</Text>
+    <Divider my={2} />
+    <VStack justifyContent="center" divider={<StackDivider my={3} />}>
+      <RunAction
+        runId={runId}
+        taskId={taskId}
+        dagId={dagId}
+        mapIndexes={mapIndexes}
+      />
+      <ClearAction
+        runId={runId}
+        taskId={taskId}
+        dagId={dagId}
+        executionDate={executionDate}
+        mapIndexes={mapIndexes}
+      />
+      <MarkFailedAction
+        runId={runId}
+        taskId={taskId}
+        dagId={dagId}
+        mapIndexes={mapIndexes}
+      />
+      <MarkSuccessAction
+        runId={runId}
+        taskId={taskId}
+        dagId={dagId}
+        mapIndexes={mapIndexes}
+      />
+    </VStack>
+    <Divider my={2} />
+  </Box>
+);
+
+export default TaskActions;
diff --git a/airflow/www/static/js/dag/details/BreadcrumbText.tsx b/airflow/www/static/js/dag/details/taskInstance/taskActions/types.tsx
similarity index 66%
copy from airflow/www/static/js/dag/details/BreadcrumbText.tsx
copy to airflow/www/static/js/dag/details/taskInstance/taskActions/types.tsx
index 85d5f6dec5..c5bacf3302 100644
--- a/airflow/www/static/js/dag/details/BreadcrumbText.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/taskActions/types.tsx
@@ -17,22 +17,12 @@
  * under the License.
  */
 
-import React from 'react';
-import {
-  Box,
-  Heading,
-} from '@chakra-ui/react';
+import type { Dag, DagRun, TaskInstance } from 'src/types';
 
-interface Props {
-  label: string;
-  value: React.ReactNode;
+export interface CommonActionProps {
+  runId: DagRun['runId'],
+  taskId: TaskInstance['taskId'] | null,
+  dagId: Dag['id'],
+  executionDate: DagRun['executionDate'],
+  mapIndexes: number[]
 }
-
-const BreadcrumbText = ({ label, value }: Props) => (
-  <Box position="relative">
-    <Heading as="h5" size="sm" color="gray.300" position="absolute" top="-12px">{label}</Heading>
-    <Heading as="h3" size="md">{value}</Heading>
-  </Box>
-);
-
-export default BreadcrumbText;
diff --git a/airflow/www/static/js/dag/useSelection.test.tsx b/airflow/www/static/js/dag/useSelection.test.tsx
index 3cfe61d5db..d7959e37dc 100644
--- a/airflow/www/static/js/dag/useSelection.test.tsx
+++ b/airflow/www/static/js/dag/useSelection.test.tsx
@@ -38,17 +38,21 @@ describe('Test useSelection hook', () => {
       selected: {
         runId,
         taskId,
+        mapIndex,
       },
     } = result.current;
 
     expect(runId).toBeNull();
     expect(taskId).toBeNull();
+    expect(mapIndex).toBeNull();
   });
 
   test.each([
-    { taskId: 'task_1', runId: 'run_1' },
-    { runId: 'run_1', taskId: null },
-    { taskId: 'task_1', runId: null },
+    { taskId: 'task_1', runId: 'run_1', mapIndex: 2 },
+    { taskId: null, runId: 'run_1', mapIndex: null },
+    { taskId: 'task_2', runId: null, mapIndex: 1 },
+    { taskId: 'task_3', runId: null, mapIndex: -1 },
+    { taskId: 'task_4', runId: null, mapIndex: 0 },
   ])('Test onSelect() and clearSelection()', async (selected) => {
     const { result } = renderHook(() => useSelection(), { wrapper: Wrapper });
 
@@ -58,6 +62,7 @@ describe('Test useSelection hook', () => {
 
     expect(result.current.selected.taskId).toBe(selected.taskId);
     expect(result.current.selected.runId).toBe(selected.runId);
+    expect(result.current.selected.mapIndex).toBe(selected.mapIndex);
 
     // clearSelection
     await act(async () => {
@@ -66,5 +71,6 @@ describe('Test useSelection hook', () => {
 
     expect(result.current.selected.taskId).toBeNull();
     expect(result.current.selected.runId).toBeNull();
+    expect(result.current.selected.mapIndex).toBeNull();
   });
 });
diff --git a/airflow/www/static/js/dag/useSelection.ts b/airflow/www/static/js/dag/useSelection.ts
index c4f5290b11..790e584e83 100644
--- a/airflow/www/static/js/dag/useSelection.ts
+++ b/airflow/www/static/js/dag/useSelection.ts
@@ -21,10 +21,12 @@ import { useSearchParams } from 'react-router-dom';
 
 const RUN_ID = 'dag_run_id';
 const TASK_ID = 'task_id';
+const MAP_INDEX = 'map_index';
 
 export interface SelectionProps {
   runId?: string | null ;
   taskId?: string | null;
+  mapIndex?: number | null;
 }
 
 const useSelection = () => {
@@ -34,10 +36,11 @@ const useSelection = () => {
   const clearSelection = () => {
     searchParams.delete(RUN_ID);
     searchParams.delete(TASK_ID);
+    searchParams.delete(MAP_INDEX);
     setSearchParams(searchParams);
   };
 
-  const onSelect = ({ runId, taskId }: SelectionProps) => {
+  const onSelect = ({ runId, taskId, mapIndex }: SelectionProps) => {
     const params = new URLSearchParams(searchParams);
 
     if (runId) params.set(RUN_ID, runId);
@@ -46,16 +49,22 @@ const useSelection = () => {
     if (taskId) params.set(TASK_ID, taskId);
     else params.delete(TASK_ID);
 
+    if (mapIndex || mapIndex === 0) params.set(MAP_INDEX, mapIndex.toString());
+    else params.delete(MAP_INDEX);
+
     setSearchParams(params);
   };
 
   const runId = searchParams.get(RUN_ID);
   const taskId = searchParams.get(TASK_ID);
+  const mapIndexParam = searchParams.get(MAP_INDEX);
+  const mapIndex = mapIndexParam !== null ? parseInt(mapIndexParam, 10) : null;
 
   return {
     selected: {
       runId,
       taskId,
+      mapIndex,
     },
     clearSelection,
     onSelect,
diff --git a/airflow/www/static/js/types/index.ts b/airflow/www/static/js/types/index.ts
index 60a8c8a9bb..6918320f15 100644
--- a/airflow/www/static/js/types/index.ts
+++ b/airflow/www/static/js/types/index.ts
@@ -63,6 +63,7 @@ interface TaskInstance {
   mappedStates?: {
     [key: string]: number;
   },
+  mapIndex?: number;
   tryNumber?: number;
 }
 
diff --git a/airflow/www/templates/airflow/dag.html b/airflow/www/templates/airflow/dag.html
index 20a2f06e54..860dfaeb8f 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -72,6 +72,7 @@
   <meta name="mapped_instances_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_mapped_task_instances', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_') }}">
   <meta name="task_log_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_log_endpoint_get_log', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_', task_try_number='-1') }}">
   <meta name="upstream_dataset_events_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_dag_run_endpoint_get_upstream_dataset_events', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_') }}">
+  <meta name="task_instance_api" content="{{ url_for('/api/v1.airflow_api_connexion_endpoints_task_instance_endpoint_get_task_instance', dag_id=dag.dag_id, dag_run_id='_DAG_RUN_ID_', task_id='_TASK_ID_') }}">
   <!-- End Urls -->
   <meta name="is_paused" content="{{ dag_is_paused }}">
   <meta name="csrf_token" content="{{ csrf_token() }}">