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 f48e8efae8 Show abandoned tasks in Grid View (#38511)
f48e8efae8 is described below

commit f48e8efae8591741c5c25081a93c4216fff7d697
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Mar 26 14:12:22 2024 -0700

    Show abandoned tasks in Grid View (#38511)
    
    * Handle abandoned tasks in grid view
    
    * Prevent actions on abandoned tasks
    
    * Fix links from taskinstance/list
---
 airflow/www/static/js/dag/details/Header.tsx       |   4 +-
 .../www/static/js/dag/details/NotesAccordion.tsx   |   4 +-
 airflow/www/static/js/dag/details/index.tsx        |   5 +-
 .../static/js/dag/details/taskInstance/Details.tsx | 132 +++++-----
 .../js/dag/details/taskInstance/Logs/index.tsx     | 266 ++++++++++-----------
 .../static/js/dag/details/taskInstance/index.tsx   |  40 ++--
 airflow/www/utils.py                               |  14 +-
 airflow/www/views.py                               |  18 +-
 8 files changed, 261 insertions(+), 222 deletions(-)

diff --git a/airflow/www/static/js/dag/details/Header.tsx 
b/airflow/www/static/js/dag/details/Header.tsx
index 1903574630..06c1e2dd11 100644
--- a/airflow/www/static/js/dag/details/Header.tsx
+++ b/airflow/www/static/js/dag/details/Header.tsx
@@ -64,8 +64,6 @@ const Header = () => {
       clearSelection();
     } else if (runId && !dagRun) {
       onSelect({ taskId });
-    } else if (taskId && !group) {
-      onSelect({ runId });
     }
   }, [dagRun, taskId, group, runId, onSelect, clearSelection]);
 
@@ -99,7 +97,7 @@ const Header = () => {
   const isMappedTaskDetails = runId && taskId && mapIndex !== undefined;
 
   return (
-    <Breadcrumb ml={3} separator={<Text color="gray.300">/</Text>}>
+    <Breadcrumb ml={3} pt={2} separator={<Text color="gray.300">/</Text>}>
       <BreadcrumbItem isCurrentPage={isDagDetails} mt={4}>
         <BreadcrumbLink
           onClick={clearSelection}
diff --git a/airflow/www/static/js/dag/details/NotesAccordion.tsx 
b/airflow/www/static/js/dag/details/NotesAccordion.tsx
index 5f1c00d7a2..dfe47a2a52 100644
--- a/airflow/www/static/js/dag/details/NotesAccordion.tsx
+++ b/airflow/www/static/js/dag/details/NotesAccordion.tsx
@@ -46,6 +46,7 @@ interface Props {
   taskId?: string;
   mapIndex?: number;
   initialValue?: string | null;
+  isAbandonedTask?: boolean;
 }
 
 const NotesAccordion = ({
@@ -54,8 +55,9 @@ const NotesAccordion = ({
   taskId,
   mapIndex,
   initialValue,
+  isAbandonedTask,
 }: Props) => {
-  const canEdit = getMetaValue("can_edit") === "True";
+  const canEdit = getMetaValue("can_edit") === "True" && !isAbandonedTask;
   const [note, setNote] = useState(initialValue ?? "");
   const [editMode, setEditMode] = useState(false);
   const [accordionIndexes, setAccordionIndexes] = useState<Array<number>>(
diff --git a/airflow/www/static/js/dag/details/index.tsx 
b/airflow/www/static/js/dag/details/index.tsx
index 0305fe0a2d..3e8b244c24 100644
--- a/airflow/www/static/js/dag/details/index.tsx
+++ b/airflow/www/static/js/dag/details/index.tsx
@@ -185,6 +185,8 @@ const Details = ({
 
   const showTaskDetails = !!taskId && !runId;
 
+  const isAbandonedTask = !!taskId && !group;
+
   const [searchParams, setSearchParams] = useSearchParams();
   const tab = searchParams.get(TAB_PARAM) || undefined;
   const tabIndex = tabToIndex(tab);
@@ -253,7 +255,7 @@ const Details = ({
               <MarkRunAs runId={runId} state={run?.state} />
             </>
           )}
-          {runId && taskId && (
+          {runId && taskId && !isAbandonedTask && (
             <>
               <ClearInstance
                 taskId={taskId}
@@ -379,6 +381,7 @@ const Details = ({
                 onChangeTab(0);
                 onSelect({ taskId });
               }}
+              isDisabled={isAbandonedTask}
             >
               <MdHourglassBottom size={16} />
               <Text as="strong" ml={1}>
diff --git a/airflow/www/static/js/dag/details/taskInstance/Details.tsx 
b/airflow/www/static/js/dag/details/taskInstance/Details.tsx
index cc4dea6e96..72d8273ce6 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Details.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Details.tsx
@@ -34,45 +34,62 @@ import type {
 } from "src/types";
 
 interface Props {
-  gridInstance: GridTaskInstance;
+  gridInstance?: GridTaskInstance;
   taskInstance?: API.TaskInstance;
-  group: Task;
+  group?: Task | null;
 }
 
 const Details = ({ gridInstance, taskInstance, group }: Props) => {
-  const isGroup = !!group.children;
+  const isGroup = !!group?.children;
   const summary: React.ReactNode[] = [];
 
-  const { taskId, runId, startDate, endDate, state } = gridInstance;
+  const state =
+    gridInstance?.state ||
+    (taskInstance?.state === "none" ? null : taskInstance?.state) ||
+    null;
+  const isMapped = group?.isMapped;
+  const runId = gridInstance?.runId || taskInstance?.dagRunId;
+  const startDate = gridInstance?.startDate || taskInstance?.startDate;
+  const endDate = gridInstance?.endDate || taskInstance?.endDate;
+  const taskId = gridInstance?.taskId || taskInstance?.taskId;
+  const mapIndex = gridInstance?.mapIndex || taskInstance?.mapIndex;
 
-  const mappedStates = !taskInstance ? gridInstance.mappedStates : undefined;
+  const operator = group?.operator || taskInstance?.operator;
 
-  const { isMapped, tooltip, operator, triggerRule } = group;
+  const mappedStates = !taskInstance ? gridInstance?.mappedStates : undefined;
 
-  const { totalTasks, childTaskMap } = getGroupAndMapSummary({
-    group,
-    runId,
-    mappedStates,
-  });
+  let totalTasks;
+  let childTaskMap;
 
-  childTaskMap.forEach((key, val) => {
-    const childState = snakeCase(val);
-    if (key > 0) {
-      summary.push(
-        <Tr key={childState}>
-          <Td />
-          <Td>
-            <Flex alignItems="center">
-              <SimpleStatus state={childState as TaskState} mx={2} />
-              {childState}
-              {": "}
-              {key}
-            </Flex>
-          </Td>
-        </Tr>
-      );
-    }
-  });
+  if (group) {
+    const groupAndMapSummary = getGroupAndMapSummary({
+      group,
+      runId,
+      mappedStates,
+    });
+
+    totalTasks = groupAndMapSummary.totalTasks;
+    childTaskMap = groupAndMapSummary.childTaskMap;
+
+    childTaskMap.forEach((key, val) => {
+      const childState = snakeCase(val);
+      if (key > 0) {
+        summary.push(
+          <Tr key={childState}>
+            <Td />
+            <Td>
+              <Flex alignItems="center">
+                <SimpleStatus state={childState as TaskState} mx={2} />
+                {childState}
+                {": "}
+                {key}
+              </Flex>
+            </Td>
+          </Tr>
+        );
+      }
+    });
+  }
 
   const taskIdTitle = isGroup ? "Task Group ID" : "Task ID";
   const isStateFinal =
@@ -87,9 +104,9 @@ const Details = ({ gridInstance, taskInstance, group }: 
Props) => {
       </Text>
       <Table variant="striped">
         <Tbody>
-          {tooltip && (
+          {group?.tooltip && (
             <Tr>
-              <Td colSpan={2}>{tooltip}</Td>
+              <Td colSpan={2}>{group.tooltip}</Td>
             </Tr>
           )}
           <Tr>
@@ -104,7 +121,7 @@ const Details = ({ gridInstance, taskInstance, group }: 
Props) => {
               </Flex>
             </Td>
           </Tr>
-          {!!group.setupTeardownType && (
+          {!!group?.setupTeardownType && (
             <Tr>
               <Td>Type</Td>
               <Td>
@@ -114,7 +131,7 @@ const Details = ({ gridInstance, taskInstance, group }: 
Props) => {
               </Td>
             </Tr>
           )}
-          {mappedStates && totalTasks > 0 && (
+          {mappedStates && !!totalTasks && totalTasks > 0 && (
             <Tr>
               <Td colSpan={2}>
                 {totalTasks} {isGroup ? "Task Group" : "Task"}
@@ -124,32 +141,37 @@ const Details = ({ gridInstance, taskInstance, group }: 
Props) => {
             </Tr>
           )}
           {summary.length > 0 && summary}
-          <Tr>
-            <Td>{taskIdTitle}</Td>
-            <Td>
-              <ClipboardText value={taskId} />
-            </Td>
-          </Tr>
-          <Tr>
-            <Td>Run ID</Td>
-            <Td>
-              <Text whiteSpace="nowrap">
-                <ClipboardText value={runId} />
-              </Text>
-            </Td>
-          </Tr>
-          {taskInstance?.mapIndex !== undefined && (
+          {!!taskId && (
             <Tr>
-              <Td>Map Index</Td>
-              <Td>{taskInstance.mapIndex}</Td>
+              <Td>{taskIdTitle}</Td>
+              <Td>
+                <ClipboardText value={taskId} />
+              </Td>
+            </Tr>
+          )}
+          {!!runId && (
+            <Tr>
+              <Td>Run ID</Td>
+              <Td>
+                <Text whiteSpace="nowrap">
+                  <ClipboardText value={runId} />
+                </Text>
+              </Td>
             </Tr>
           )}
-          {taskInstance?.renderedMapIndex !== undefined && (
+          {mapIndex !== undefined && (
             <Tr>
-              <Td>Rendered Map Index</Td>
-              <Td>{taskInstance.renderedMapIndex}</Td>
+              <Td>Map Index</Td>
+              <Td>{mapIndex}</Td>
             </Tr>
           )}
+          {taskInstance?.renderedMapIndex !== undefined &&
+            taskInstance?.renderedMapIndex !== null && (
+              <Tr>
+                <Td>Rendered Map Index</Td>
+                <Td>{taskInstance.renderedMapIndex}</Td>
+              </Tr>
+            )}
           {!!taskInstance?.tryNumber && (
             <Tr>
               <Td>Try Number</Td>
@@ -162,10 +184,10 @@ const Details = ({ gridInstance, taskInstance, group }: 
Props) => {
               <Td>{operator}</Td>
             </Tr>
           )}
-          {triggerRule && (
+          {group?.triggerRule && (
             <Tr>
               <Td>Trigger Rule</Td>
-              <Td>{triggerRule}</Td>
+              <Td>{group.triggerRule}</Td>
             </Tr>
           )}
           {startDate && (
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 9e664fda20..4a4013a2dd 100644
--- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx
@@ -201,146 +201,142 @@ const Logs = ({
           </Flex>
         </Box>
       )}
-      {tryNumber !== undefined && (
-        <>
+      <Box>
+        {!showDropdown && (
           <Box>
-            {!showDropdown && (
-              <Box>
-                <Text as="span"> (by attempts)</Text>
-                <Flex my={1} justifyContent="space-between">
-                  <Flex flexWrap="wrap">
-                    {internalIndexes.map((index) => (
-                      <Button
-                        key={index}
-                        variant={taskTryNumber === index ? "solid" : "ghost"}
-                        colorScheme="blue"
-                        onClick={() => setSelectedTryNumber(index)}
-                        data-testid={`log-attempt-select-button-${index}`}
-                      >
-                        {index}
-                      </Button>
-                    ))}
-                  </Flex>
-                </Flex>
-              </Box>
-            )}
-            <Flex my={1} justifyContent="space-between" flexWrap="wrap">
-              <Flex alignItems="center" flexGrow={1} mr={10}>
-                {showDropdown && (
-                  <Box width="100%" mr={2}>
-                    <Select
-                      size="sm"
-                      placeholder="Select log attempt"
-                      onChange={(e) => {
-                        setSelectedTryNumber(Number(e.target.value));
-                      }}
-                    >
-                      {internalIndexes.map((index) => (
-                        <option key={index} value={index}>
-                          {index}
-                        </option>
-                      ))}
-                    </Select>
-                  </Box>
-                )}
-                <Box width="100%" mr={2}>
-                  <MultiSelect
-                    size="sm"
-                    isMulti
-                    options={logLevelOptions}
-                    placeholder="All Levels"
-                    value={logLevelFilters}
-                    onChange={(options) => setLogLevelFilters([...options])}
-                    chakraStyles={{
-                      multiValue: (provided, ...rest) => ({
-                        ...provided,
-                        backgroundColor: rest[0].data.color,
-                      }),
-                      option: (provided, ...rest) => ({
-                        ...provided,
-                        borderLeft: "solid 4px black",
-                        borderColor: rest[0].data.color,
-                        mt: 2,
-                      }),
-                    }}
-                  />
-                </Box>
-                <Box width="100%">
-                  <MultiSelect
-                    size="sm"
-                    isMulti
-                    options={fileSources.map((fileSource) => ({
-                      label: fileSource,
-                      value: fileSource,
-                    }))}
-                    placeholder="All File Sources"
-                    value={fileSourceFilters}
-                    onChange={(options) => setFileSourceFilters([...options])}
-                  />
-                </Box>
-              </Flex>
-              <Flex alignItems="center" flexWrap="wrap">
-                <Checkbox
-                  isChecked={wrap}
-                  onChange={() => setWrap((previousState) => !previousState)}
-                  px={4}
-                  data-testid="wrap-checkbox"
-                >
-                  <Text as="strong">Wrap</Text>
-                </Checkbox>
-                <LogLink
-                  dagId={dagId}
-                  taskId={taskId}
-                  executionDate={executionDate}
-                  isInternal
-                  tryNumber={tryNumber}
-                  mapIndex={mapIndex}
-                />
-                <LinkButton href={`${logUrl}&${params.toString()}`}>
-                  See More
-                </LinkButton>
-                <IconButton
-                  variant="ghost"
-                  aria-label="Toggle full screen"
-                  title="Toggle full screen"
-                  onClick={toggleFullScreen}
-                  icon={
-                    isFullScreen ? (
-                      <BiCollapse height="24px" />
-                    ) : (
-                      <BiExpand height="24px" />
-                    )
-                  }
-                />
+            <Text as="span"> (by attempts)</Text>
+            <Flex my={1} justifyContent="space-between">
+              <Flex flexWrap="wrap">
+                {internalIndexes.map((index) => (
+                  <Button
+                    key={index}
+                    variant={taskTryNumber === index ? "solid" : "ghost"}
+                    colorScheme="blue"
+                    onClick={() => setSelectedTryNumber(index)}
+                    data-testid={`log-attempt-select-button-${index}`}
+                  >
+                    {index}
+                  </Button>
+                ))}
               </Flex>
             </Flex>
           </Box>
-          {!!warning && (
-            <Flex
-              bg="yellow.200"
-              borderRadius={2}
-              borderColor="gray.400"
-              alignItems="center"
-              p={2}
-            >
-              <Icon as={MdWarning} color="yellow.500" mr={2} />
-              <Text fontSize="sm">{warning}</Text>
-            </Flex>
-          )}
-          {isLoading ? (
-            <Spinner />
-          ) : (
-            !!parsedLogs && (
-              <LogBlock
-                parsedLogs={parsedLogs}
-                wrap={wrap}
-                tryNumber={taskTryNumber}
-                unfoldedGroups={unfoldedLogGroups}
-                setUnfoldedLogGroup={setUnfoldedLogGroup}
+        )}
+        <Flex my={1} justifyContent="space-between" flexWrap="wrap">
+          <Flex alignItems="center" flexGrow={1} mr={10}>
+            {showDropdown && (
+              <Box width="100%" mr={2}>
+                <Select
+                  size="sm"
+                  placeholder="Select log attempt"
+                  onChange={(e) => {
+                    setSelectedTryNumber(Number(e.target.value));
+                  }}
+                >
+                  {internalIndexes.map((index) => (
+                    <option key={index} value={index}>
+                      {index}
+                    </option>
+                  ))}
+                </Select>
+              </Box>
+            )}
+            <Box width="100%" mr={2}>
+              <MultiSelect
+                size="sm"
+                isMulti
+                options={logLevelOptions}
+                placeholder="All Levels"
+                value={logLevelFilters}
+                onChange={(options) => setLogLevelFilters([...options])}
+                chakraStyles={{
+                  multiValue: (provided, ...rest) => ({
+                    ...provided,
+                    backgroundColor: rest[0].data.color,
+                  }),
+                  option: (provided, ...rest) => ({
+                    ...provided,
+                    borderLeft: "solid 4px black",
+                    borderColor: rest[0].data.color,
+                    mt: 2,
+                  }),
+                }}
               />
-            )
-          )}
-        </>
+            </Box>
+            <Box width="100%">
+              <MultiSelect
+                size="sm"
+                isMulti
+                options={fileSources.map((fileSource) => ({
+                  label: fileSource,
+                  value: fileSource,
+                }))}
+                placeholder="All File Sources"
+                value={fileSourceFilters}
+                onChange={(options) => setFileSourceFilters([...options])}
+              />
+            </Box>
+          </Flex>
+          <Flex alignItems="center" flexWrap="wrap">
+            <Checkbox
+              isChecked={wrap}
+              onChange={() => setWrap((previousState) => !previousState)}
+              px={4}
+              data-testid="wrap-checkbox"
+            >
+              <Text as="strong">Wrap</Text>
+            </Checkbox>
+            <LogLink
+              dagId={dagId}
+              taskId={taskId}
+              executionDate={executionDate}
+              isInternal
+              tryNumber={tryNumber}
+              mapIndex={mapIndex}
+            />
+            <LinkButton href={`${logUrl}&${params.toString()}`}>
+              See More
+            </LinkButton>
+            <IconButton
+              variant="ghost"
+              aria-label="Toggle full screen"
+              title="Toggle full screen"
+              onClick={toggleFullScreen}
+              icon={
+                isFullScreen ? (
+                  <BiCollapse height="24px" />
+                ) : (
+                  <BiExpand height="24px" />
+                )
+              }
+            />
+          </Flex>
+        </Flex>
+      </Box>
+      {!!warning && (
+        <Flex
+          bg="yellow.200"
+          borderRadius={2}
+          borderColor="gray.400"
+          alignItems="center"
+          p={2}
+        >
+          <Icon as={MdWarning} color="yellow.500" mr={2} />
+          <Text fontSize="sm">{warning}</Text>
+        </Flex>
+      )}
+      {isLoading ? (
+        <Spinner />
+      ) : (
+        !!parsedLogs && (
+          <LogBlock
+            parsedLogs={parsedLogs}
+            wrap={wrap}
+            tryNumber={taskTryNumber}
+            unfoldedGroups={unfoldedLogGroups}
+            setUnfoldedLogGroup={setUnfoldedLogGroup}
+          />
+        )
       )}
     </>
   );
diff --git a/airflow/www/static/js/dag/details/taskInstance/index.tsx 
b/airflow/www/static/js/dag/details/taskInstance/index.tsx
index 1b8f0a54ba..83681cb070 100644
--- a/airflow/www/static/js/dag/details/taskInstance/index.tsx
+++ b/airflow/www/static/js/dag/details/taskInstance/index.tsx
@@ -65,11 +65,8 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) => 
{
     mapIndex,
     enabled: (!isGroup && !isMapped) || isMapIndexDefined,
   });
-  const gridInstance = group?.instances.find((ti) => ti.runId === runId);
-
-  if (!group || !run || !gridInstance) return null;
 
-  const { executionDate } = run;
+  const gridInstance = group?.instances.find((ti) => ti.runId === runId);
 
   return (
     <Box
@@ -79,12 +76,12 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) 
=> {
       ref={taskInstanceRef}
       overflowY="auto"
     >
-      {!isGroup && (
+      {!isGroup && run?.executionDate && (
         <TaskNav
           taskId={taskId}
           isMapped={isMapped}
           mapIndex={mapIndex}
-          executionDate={executionDate}
+          executionDate={run?.executionDate}
           operator={operator}
         />
       )}
@@ -93,22 +90,25 @@ const TaskInstance = ({ taskId, runId, mapIndex }: Props) 
=> {
           dagId={dagId}
           runId={runId}
           taskId={taskId}
-          mapIndex={gridInstance.mapIndex}
-          initialValue={gridInstance.note}
-          key={dagId + runId + taskId + gridInstance.mapIndex}
-        />
-      )}
-      {!!group.extraLinks?.length && !isGroupOrMappedTaskSummary && (
-        <ExtraLinks
-          taskId={taskId}
-          dagId={dagId}
-          mapIndex={isMapped && isMapIndexDefined ? mapIndex : undefined}
-          executionDate={executionDate}
-          extraLinks={group?.extraLinks}
-          tryNumber={taskInstance?.tryNumber || gridInstance.tryNumber}
+          mapIndex={mapIndex}
+          initialValue={gridInstance?.note || taskInstance?.note}
+          key={dagId + runId + taskId + mapIndex}
+          isAbandonedTask={!!taskId && !group}
         />
       )}
-      {group.hasOutletDatasets && (
+      {!!group?.extraLinks?.length &&
+        !isGroupOrMappedTaskSummary &&
+        run?.executionDate && (
+          <ExtraLinks
+            taskId={taskId}
+            dagId={dagId}
+            mapIndex={isMapped && isMapIndexDefined ? mapIndex : undefined}
+            executionDate={run.executionDate}
+            extraLinks={group.extraLinks}
+            tryNumber={taskInstance?.tryNumber || gridInstance?.tryNumber || 1}
+          />
+        )}
+      {group?.hasOutletDatasets && (
         <DatasetUpdateEvents taskId={taskId} runId={runId} />
       )}
       <TriggererInfo taskInstance={taskInstance} />
diff --git a/airflow/www/utils.py b/airflow/www/utils.py
index 8cbb19d176..d73bff9a64 100644
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -423,13 +423,17 @@ def task_instance_link(attr):
     dag_id = attr.get("dag_id")
     task_id = attr.get("task_id")
     run_id = attr.get("run_id")
-    execution_date = attr.get("dag_run.execution_date") or 
attr.get("execution_date") or timezone.utcnow()
+    map_index = attr.get("map_index", None)
+    if map_index == -1:
+        map_index = None
+
     url = url_for(
-        "Airflow.task",
+        "Airflow.grid",
         dag_id=dag_id,
         task_id=task_id,
-        execution_date=execution_date.isoformat(),
-        map_index=attr.get("map_index", -1),
+        dag_run_id=run_id,
+        map_index=map_index,
+        tab="graph",
     )
     url_root = url_for(
         "Airflow.grid",
@@ -437,8 +441,8 @@ def task_instance_link(attr):
         task_id=task_id,
         root=task_id,
         dag_run_id=run_id,
+        map_index=map_index,
         tab="graph",
-        map_index=attr.get("map_index", -1),
     )
     return Markup(
         """
diff --git a/airflow/www/views.py b/airflow/www/views.py
index 9d66fe5bda..17eabf3902 100644
--- a/airflow/www/views.py
+++ b/airflow/www/views.py
@@ -5187,10 +5187,24 @@ class TaskInstanceModelView(AirflowModelView):
 
     def log_url_formatter(self):
         """Format log URL."""
-        log_url = self.get("log_url")
+        dag_id = self.get("dag_id")
+        task_id = self.get("task_id")
+        run_id = self.get("run_id")
+        map_index = self.get("map_index", None)
+        if map_index == -1:
+            map_index = None
+
+        url = url_for(
+            "Airflow.grid",
+            dag_id=dag_id,
+            task_id=task_id,
+            dag_run_id=run_id,
+            map_index=map_index,
+            tab="logs",
+        )
         return Markup(
             '<a href="{log_url}"><span class="material-icons" 
aria-hidden="true">reorder</span></a>'
-        ).format(log_url=log_url)
+        ).format(log_url=url)
 
     def duration_f(self):
         """Format duration."""

Reply via email to