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 <[email protected]> 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), }
