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/04/19 21:43:28 UTC

[airflow] 01/01: Add queue date and duration timeline

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

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

commit f49baa39392a1e489c1f547730ce3fe9f7a789e2
Author: Brent Bovenzi <br...@gmail.com>
AuthorDate: Tue Apr 19 17:37:48 2022 -0400

    Add queue date and duration timeline
---
 .../tree/details/content/taskInstance/Details.jsx  |  23 ++--
 .../content/taskInstance/MappedInstances.jsx       |  24 ++--
 .../tree/details/content/taskInstance/Timeline.jsx | 122 +++++++++++++++++++++
 airflow/www/static/js/tree/details/index.jsx       |   2 +-
 airflow/www/utils.py                               |   5 +
 5 files changed, 154 insertions(+), 22 deletions(-)

diff --git a/airflow/www/static/js/tree/details/content/taskInstance/Details.jsx b/airflow/www/static/js/tree/details/content/taskInstance/Details.jsx
index 6d341e4c3c..d0686bb779 100644
--- a/airflow/www/static/js/tree/details/content/taskInstance/Details.jsx
+++ b/airflow/www/static/js/tree/details/content/taskInstance/Details.jsx
@@ -25,10 +25,10 @@ import {
 } from '@chakra-ui/react';
 
 import { finalStatesMap } from '../../../../utils';
-import { getDuration, formatDuration } from '../../../../datetime_utils';
 import { SimpleStatus } from '../../../StatusBox';
-import Time from '../../../Time';
 import { ClipboardText } from '../../../Clipboard';
+import Timeline from './Timeline';
+import Time from '../../../Time';
 
 const Details = ({ instance, group, operator }) => {
   const isGroup = !!group.children;
@@ -37,11 +37,11 @@ const Details = ({ instance, group, operator }) => {
   const {
     taskId,
     runId,
-    duration,
     startDate,
     endDate,
     state,
     mappedStates,
+    queueDate,
   } = instance;
 
   const {
@@ -81,8 +81,8 @@ const Details = ({ instance, group, operator }) => {
   });
 
   const taskIdTitle = isGroup ? 'Task Group Id: ' : 'Task Id: ';
-  const isStateFinal = ['success', 'failed', 'upstream_failed', 'skipped'].includes(state);
   const isOverall = (isMapped || isGroup) && 'Overall ';
+  const isStateFinal = ['success', 'failed', 'upstream_failed', 'skipped'].includes(state);
 
   return (
     <Flex flexWrap="wrap" justifyContent="space-between">
@@ -130,12 +130,21 @@ const Details = ({ instance, group, operator }) => {
           </Text>
         )}
         <br />
+        <Timeline
+          startDate={startDate}
+          endDate={endDate}
+          queueDate={queueDate}
+          state={state}
+          isOverall={isOverall}
+        />
+        <br />
+        {queueDate && (
         <Text>
-          {isOverall}
-          Duration:
+          Queued:
           {' '}
-          {formatDuration(duration || getDuration(startDate, endDate))}
+          <Time dateTime={queueDate} />
         </Text>
+        )}
         {startDate && (
         <Text>
           Started:
diff --git a/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx b/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx
index 42bbdca66f..74d01a45da 100644
--- a/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx
+++ b/airflow/www/static/js/tree/details/content/taskInstance/MappedInstances.jsx
@@ -31,10 +31,10 @@ import {
 } from 'react-icons/md';
 
 import { getMetaValue } from '../../../../utils';
-import { formatDateTime, formatDuration } from '../../../../datetime_utils';
 import { useMappedInstances } from '../../../api';
 import { SimpleStatus } from '../../../StatusBox';
 import Table from '../../../Table';
+import Timeline from './Timeline';
 
 const renderedTemplatesUrl = getMetaValue('rendered_templates_url');
 const logUrl = getMetaValue('log_url');
@@ -83,9 +83,15 @@ const MappedInstances = ({
             {mi.state || 'no status'}
           </Flex>
         ),
-        duration: mi.duration && formatDuration(mi.duration),
-        startDate: mi.startDate && formatDateTime(mi.startDate),
-        endDate: mi.endDate && formatDateTime(mi.endDate),
+        duration: (
+          <Timeline
+            queueDate={mi.queuedWhen}
+            startDate={mi.startDate}
+            endDate={mi.endDate}
+            state={mi.state}
+            showDates
+          />
+        ),
         links: (
           <Flex alignItems="center">
             <IconLink mr={1} title="Details" aria-label="Details" icon={<MdDetails />} href={detailsLink} />
@@ -114,16 +120,6 @@ const MappedInstances = ({
         accessor: 'duration',
         disableSortBy: true,
       },
-      {
-        Header: 'Start Date',
-        accessor: 'startDate',
-        disableSortBy: true,
-      },
-      {
-        Header: 'End Date',
-        accessor: 'endDate',
-        disableSortBy: true,
-      },
       {
         disableSortBy: true,
         accessor: 'links',
diff --git a/airflow/www/static/js/tree/details/content/taskInstance/Timeline.jsx b/airflow/www/static/js/tree/details/content/taskInstance/Timeline.jsx
new file mode 100644
index 0000000000..d9287d8940
--- /dev/null
+++ b/airflow/www/static/js/tree/details/content/taskInstance/Timeline.jsx
@@ -0,0 +1,122 @@
+/*!
+ * 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.
+ */
+
+/* global stateColors */
+
+import React from 'react';
+import {
+  Flex,
+  Text,
+  Box,
+  Tooltip,
+  Center,
+} from '@chakra-ui/react';
+
+import { useContainerRef } from '../../../context/containerRef';
+import { getDuration, formatDuration } from '../../../../datetime_utils';
+import Time from '../../../Time';
+
+const Timeline = ({
+  startDate,
+  endDate,
+  queueDate,
+  state,
+  width = 100,
+  isOverall = false,
+  showDates = false,
+}) => {
+  const containerRef = useContainerRef();
+
+  const isStateFinal = ['success', 'failed', 'upstream_failed', 'skipped'].includes(state);
+  const queuedTime = getDuration(queueDate, startDate);
+  const executionTime = getDuration(startDate, endDate);
+  const elapsedTime = getDuration(queueDate, endDate);
+  return (
+    <Center position="relative" width="150px" mb={6} color="gray.400">
+      <Box position="absolute" left="3px" bottom="-22px" textAlign="center">
+        <Text fontSize="xs">|</Text>
+        <Text fontSize="sm"><Time dateTime={queueDate} format="HH:mm:ss" /></Text>
+      </Box>
+      <Box position="absolute" right="3px" bottom="-22px" textAlign="center">
+        <Text fontSize="xs">|</Text>
+        <Text fontSize="sm"><Time dateTime={endDate} format="HH:mm:ss" /></Text>
+      </Box>
+      <Tooltip
+        label={(
+          <>
+            {!queueDate && !startDate && !endDate && (<Text>Instance has not started yet.</Text>)}
+            {showDates && (
+              <>
+                {queueDate && (
+                <Text>
+                  Queued:
+                  {' '}
+                  <Time dateTime={queueDate} />
+                </Text>
+                )}
+                {startDate && (
+                <Text>
+                  Started:
+                  {' '}
+                  <Time dateTime={startDate} />
+                </Text>
+                )}
+                {endDate && isStateFinal && (
+                <Text>
+                  Ended:
+                  {' '}
+                  <Time dateTime={endDate} />
+                </Text>
+                )}
+                <br />
+              </>
+            )}
+            <Text>
+              Queued Time:
+              {' '}
+              {formatDuration(getDuration(queueDate, startDate))}
+            </Text>
+            <Text>
+              {isOverall}
+              Duration:
+              {' '}
+              {formatDuration(getDuration(startDate, endDate))}
+            </Text>
+            <Text>
+              Total Elapsed Time:
+              {' '}
+              {formatDuration(getDuration(queueDate, endDate))}
+            </Text>
+          </>
+      )}
+        hasArrow
+        portalProps={{ containerRef }}
+        placement="bottom"
+        openDelay={100}
+      >
+        <Flex maxWidth={`${width}px`} height={6} borderRadius={3}>
+          <Box height="100%" bg={stateColors.queued} opacity={0.3} width={`${(queuedTime / elapsedTime) * width}px`} minWidth={2} />
+          <Box height="100%" bg={stateColors[state]} width={`${(executionTime / elapsedTime) * width}px`} minWidth={2} />
+        </Flex>
+      </Tooltip>
+    </Center>
+  );
+};
+
+export default Timeline;
diff --git a/airflow/www/static/js/tree/details/index.jsx b/airflow/www/static/js/tree/details/index.jsx
index ffe8b0ff74..18cd329950 100644
--- a/airflow/www/static/js/tree/details/index.jsx
+++ b/airflow/www/static/js/tree/details/index.jsx
@@ -36,7 +36,7 @@ const Details = () => {
     <Flex borderLeftWidth="1px" flexDirection="column" pl={3} mr={3} flexGrow={1} maxWidth="750px">
       <Header />
       <Divider my={2} />
-      <Box minWidth="750px">
+      <Box minWidth="550px">
         {!selected.runId && !selected.taskId && <DagContent />}
         {selected.runId && !selected.taskId && (
           <DagRunContent runId={selected.runId} />
diff --git a/airflow/www/utils.py b/airflow/www/utils.py
index 8a5cc3c717..73722511c6 100644
--- a/airflow/www/utils.py
+++ b/airflow/www/utils.py
@@ -103,6 +103,9 @@ def get_mapped_summary(parent_instance, task_instances):
             group_state = state
             break
 
+    group_queue_date = datetime_to_string(
+        min((ti.queued_dttm for ti in task_instances if ti.queued_dttm), default=None)
+    )
     group_start_date = datetime_to_string(
         min((ti.start_date for ti in task_instances if ti.start_date), default=None)
     )
@@ -120,6 +123,7 @@ def get_mapped_summary(parent_instance, task_instances):
         'run_id': parent_instance.run_id,
         'state': group_state,
         'start_date': group_start_date,
+        'queue_date': group_queue_date,
         'end_date': group_end_date,
         'mapped_states': mapped_states,
         'try_number': try_count,
@@ -149,6 +153,7 @@ def encode_ti(
         'start_date': datetime_to_string(task_instance.start_date),
         'end_date': datetime_to_string(task_instance.end_date),
         'try_number': try_count,
+        'queue_date': datetime_to_string(task_instance.queued_dttm),
     }