diff --git a/airflow/www/static/js/api/index.ts b/airflow/www/static/js/api/index.ts index b48fdc2dbadbd..487197d608761 100644 --- a/airflow/www/static/js/api/index.ts +++ b/airflow/www/static/js/api/index.ts @@ -56,6 +56,7 @@ import useCalendarData from "./useCalendarData"; import useCreateDatasetEvent from "./useCreateDatasetEvent"; import useRenderedK8s from "./useRenderedK8s"; import useTaskDetail from "./useTaskDetail"; +import useTIHistory from "./useTIHistory"; axios.interceptors.request.use((config) => { config.paramsSerializer = { @@ -108,4 +109,5 @@ export { useCreateDatasetEvent, useRenderedK8s, useTaskDetail, + useTIHistory, }; diff --git a/airflow/www/static/js/api/useTIHistory.ts b/airflow/www/static/js/api/useTIHistory.ts new file mode 100644 index 0000000000000..60e29e351e6f0 --- /dev/null +++ b/airflow/www/static/js/api/useTIHistory.ts @@ -0,0 +1,64 @@ +/*! + * 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 { useQuery } from "react-query"; +import { useAutoRefresh } from "src/context/autorefresh"; +import type { TaskInstance } from "src/types/api-generated"; + +import { getMetaValue } from "src/utils"; + +interface Props { + dagId: string; + runId: string; + taskId: string; + mapIndex?: number; + enabled?: boolean; +} + +export default function useTIHistory({ + dagId, + runId, + taskId, + mapIndex = -1, + enabled, +}: Props) { + const { isRefreshOn } = useAutoRefresh(); + return useQuery( + ["tiHistory", dagId, runId, taskId, mapIndex], + () => { + const tiHistoryUrl = getMetaValue("ti_history_url"); + + const params = { + dag_id: dagId, + run_id: runId, + task_id: taskId, + map_index: mapIndex, + }; + + return axios.get[]>(tiHistoryUrl, { + params, + }); + }, + { + enabled, + refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000, + } + ); +} diff --git a/airflow/www/static/js/dag/StatusBox.tsx b/airflow/www/static/js/dag/StatusBox.tsx index f84ce9c9e8877..85a84dc133a87 100644 --- a/airflow/www/static/js/dag/StatusBox.tsx +++ b/airflow/www/static/js/dag/StatusBox.tsx @@ -56,7 +56,7 @@ export const StatusWithNotes = ({ }; interface SimpleStatusProps extends BoxProps { - state: TaskState; + state: TaskState | undefined; } export const SimpleStatus = ({ state, ...rest }: SimpleStatusProps) => ( ; interface Props { - instance: TaskInstance; + instance: Instance; task: Task; } @@ -35,20 +40,18 @@ const GanttTooltip = ({ task, instance }: Props) => { // Calculate durations in ms const taskDuration = getDuration(instance?.startDate, instance?.endDate); const queuedDuration = - instance?.queuedDttm && - (instance?.startDate ? instance.queuedDttm < instance.startDate : true) - ? getDuration(instance.queuedDttm, instance?.startDate) + instance?.queuedWhen && + (instance?.startDate ? instance.queuedWhen < instance.startDate : true) + ? getDuration(instance.queuedWhen, instance?.startDate) : 0; return ( Task{isGroup ? " Group" : ""}: {task.label} - {!!instance?.tryNumber && instance.tryNumber > 1 && ( - Try Number: {instance.tryNumber} - )} + {!!instance?.tryNumber && Try Number: {instance.tryNumber}}
- {instance?.queuedDttm && ( + {instance?.queuedWhen && ( {isMappedOrGroupSummary && "Total "}Queued Duration:{" "} {formatDuration(queuedDuration)} @@ -59,10 +62,10 @@ const GanttTooltip = ({ task, instance }: Props) => { {formatDuration(taskDuration)}
- {instance?.queuedDttm && ( + {instance?.queuedWhen && ( {isMappedOrGroupSummary && "Earliest "}Queued At:{" "} - )} {instance?.startDate && ( diff --git a/airflow/www/static/js/dag/details/gantt/InstanceBar.tsx b/airflow/www/static/js/dag/details/gantt/InstanceBar.tsx new file mode 100644 index 0000000000000..8279cbff3c066 --- /dev/null +++ b/airflow/www/static/js/dag/details/gantt/InstanceBar.tsx @@ -0,0 +1,140 @@ +/*! + * 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 { Tooltip, Flex } from "@chakra-ui/react"; +import useSelection from "src/dag/useSelection"; +import { getDuration } from "src/datetime_utils"; +import { SimpleStatus } from "src/dag/StatusBox"; +import { useContainerRef } from "src/context/containerRef"; +import { hoverDelay } from "src/utils"; +import type { Task } from "src/types"; +import type { TaskInstance } from "src/types/api-generated"; +import GanttTooltip from "./GanttTooltip"; + +type Instance = Pick< + TaskInstance, + | "startDate" + | "endDate" + | "tryNumber" + | "queuedWhen" + | "dagRunId" + | "state" + | "taskId" +>; + +interface Props { + ganttWidth?: number; + task: Task; + instance: Instance; + ganttStartDate?: string | null; + ganttEndDate?: string | null; +} + +const InstanceBar = ({ + ganttWidth = 500, + task, + instance, + ganttStartDate, + ganttEndDate, +}: Props) => { + const { onSelect } = useSelection(); + const containerRef = useContainerRef(); + + const runDuration = getDuration(ganttStartDate, ganttEndDate); + const { queuedWhen } = instance; + + const hasValidQueuedDttm = + !!queuedWhen && + (instance?.startDate && queuedWhen + ? queuedWhen < instance.startDate + : true); + + // Calculate durations in ms + const taskDuration = getDuration(instance?.startDate, instance?.endDate); + const queuedDuration = hasValidQueuedDttm + ? getDuration(queuedWhen, instance?.startDate) + : 0; + const taskStartOffset = hasValidQueuedDttm + ? getDuration(ganttStartDate, queuedWhen || instance?.startDate) + : getDuration(ganttStartDate, instance?.startDate); + + // Percent of each duration vs the overall dag run + const taskDurationPercent = taskDuration / runDuration; + const taskStartOffsetPercent = taskStartOffset / runDuration; + const queuedDurationPercent = queuedDuration / runDuration; + + // Calculate the pixel width of the queued and task bars and the position in the graph + // Min width should be 5px + let width = ganttWidth * taskDurationPercent; + if (width < 5) width = 5; + let queuedWidth = hasValidQueuedDttm ? ganttWidth * queuedDurationPercent : 0; + if (hasValidQueuedDttm && queuedWidth < 5) queuedWidth = 5; + const offsetMargin = taskStartOffsetPercent * ganttWidth; + + if (!instance) return null; + + return ( + } + hasArrow + portalProps={{ containerRef }} + placement="top" + openDelay={hoverDelay} + > + { + onSelect({ + runId: instance.dagRunId, + taskId: instance.taskId, + }); + }} + > + {instance.state !== "queued" && hasValidQueuedDttm && ( + + )} + + + + ); +}; + +export default InstanceBar; diff --git a/airflow/www/static/js/dag/details/gantt/Row.tsx b/airflow/www/static/js/dag/details/gantt/Row.tsx index 1526e1713f0ad..56d1dafe7a918 100644 --- a/airflow/www/static/js/dag/details/gantt/Row.tsx +++ b/airflow/www/static/js/dag/details/gantt/Row.tsx @@ -18,14 +18,13 @@ */ import React from "react"; -import { Box, Tooltip, Flex } from "@chakra-ui/react"; +import { Box } from "@chakra-ui/react"; import useSelection from "src/dag/useSelection"; -import { getDuration } from "src/datetime_utils"; -import { SimpleStatus } from "src/dag/StatusBox"; -import { useContainerRef } from "src/context/containerRef"; -import { hoverDelay } from "src/utils"; +import { boxSize } from "src/dag/StatusBox"; +import { getMetaValue } from "src/utils"; import type { Task } from "src/types"; -import GanttTooltip from "./GanttTooltip"; +import { useTIHistory } from "src/api"; +import InstanceBar from "./InstanceBar"; interface Props { ganttWidth?: number; @@ -35,6 +34,8 @@ interface Props { ganttEndDate?: string | null; } +const dagId = getMetaValue("dag_id"); + const Row = ({ ganttWidth = 500, openGroupIds, @@ -44,42 +45,19 @@ const Row = ({ }: Props) => { const { selected: { runId, taskId }, - onSelect, } = useSelection(); - const containerRef = useContainerRef(); - - const runDuration = getDuration(ganttStartDate, ganttEndDate); const instance = task.instances.find((ti) => ti.runId === runId); - const isSelected = taskId === instance?.taskId; - const hasValidQueuedDttm = - !!instance?.queuedDttm && - (instance?.startDate && instance?.queuedDttm - ? instance.queuedDttm < instance.startDate - : true); - const isOpen = openGroupIds.includes(task.id || ""); - - // Calculate durations in ms - const taskDuration = getDuration(instance?.startDate, instance?.endDate); - const queuedDuration = hasValidQueuedDttm - ? getDuration(instance?.queuedDttm, instance?.startDate) - : 0; - const taskStartOffset = hasValidQueuedDttm - ? getDuration(ganttStartDate, instance?.queuedDttm || instance?.startDate) - : getDuration(ganttStartDate, instance?.startDate); - // Percent of each duration vs the overall dag run - const taskDurationPercent = taskDuration / runDuration; - const taskStartOffsetPercent = taskStartOffset / runDuration; - const queuedDurationPercent = queuedDuration / runDuration; + const { data: tiHistory } = useTIHistory({ + dagId, + taskId: task.id || "", + runId: runId || "", + enabled: !!(instance?.tryNumber && instance?.tryNumber > 1) && !!task.id, // Only try to look up task tries if try number > 1 + }); - // Calculate the pixel width of the queued and task bars and the position in the graph - // Min width should be 5px - let width = ganttWidth * taskDurationPercent; - if (width < 5) width = 5; - let queuedWidth = hasValidQueuedDttm ? ganttWidth * queuedDurationPercent : 0; - if (hasValidQueuedDttm && queuedWidth < 5) queuedWidth = 5; - const offsetMargin = taskStartOffsetPercent * ganttWidth; + const isSelected = taskId === instance?.taskId; + const isOpen = openGroupIds.includes(task.id || ""); return (
@@ -88,50 +66,35 @@ const Row = ({ borderBottomWidth={1} borderBottomColor={!!task.children && isOpen ? "gray.400" : "gray.200"} bg={isSelected ? "blue.100" : "inherit"} + position="relative" + width={ganttWidth} + height={`${boxSize + 9}px`} > - {instance ? ( - } - hasArrow - portalProps={{ containerRef }} - placement="top" - openDelay={hoverDelay} - > - { - onSelect({ - runId: instance.runId, - taskId: instance.taskId, - }); - }} - > - {instance.state !== "queued" && hasValidQueuedDttm && ( - - )} - - - - ) : ( - + {!!instance && ( + )} + {(tiHistory || []) + .filter( + (ti) => + ti.startDate !== instance?.startDate && // @ts-ignore + moment(ti.startDate).isAfter(ganttStartDate) + ) + .map((ti) => ( + + ))} {isOpen && !!task.children && diff --git a/airflow/www/static/js/dag/details/taskInstance/Details.tsx b/airflow/www/static/js/dag/details/taskInstance/Details.tsx index 17d5f13baa4c7..4e39b11c028d3 100644 --- a/airflow/www/static/js/dag/details/taskInstance/Details.tsx +++ b/airflow/www/static/js/dag/details/taskInstance/Details.tsx @@ -17,11 +17,12 @@ * under the License. */ -import React from "react"; +import React, { useState } from "react"; import { Text, Flex, Table, Tbody, Tr, Td, Code, Box } from "@chakra-ui/react"; import { snakeCase } from "lodash"; -import { getGroupAndMapSummary } from "src/utils"; +import { useTIHistory } from "src/api"; +import { getGroupAndMapSummary, getMetaValue } from "src/utils"; import { getDuration, formatDuration } from "src/datetime_utils"; import { SimpleStatus } from "src/dag/StatusBox"; import Time from "src/components/Time"; @@ -32,6 +33,7 @@ import type { TaskInstance as GridTaskInstance, TaskState, } from "src/types"; +import TrySelector from "./TrySelector"; interface Props { gridInstance?: GridTaskInstance; @@ -39,23 +41,44 @@ interface Props { group?: Task | null; } +const dagId = getMetaValue("dag_id"); + const Details = ({ gridInstance, taskInstance, group }: Props) => { const isGroup = !!group?.children; const summary: React.ReactNode[] = []; + const { + mapIndex, + runId, + taskId, + tryNumber: finalTryNumber, + } = gridInstance || {}; + + const { data: tiHistory } = useTIHistory({ + dagId, + taskId: taskId || "", + runId: runId || "", + mapIndex, + enabled: !!(finalTryNumber && finalTryNumber > 1) && !!taskId, // Only try to look up task tries if try number > 1 + }); + + const [selectedTryNumber, setSelectedTryNumber] = useState(0); + + const instance = + selectedTryNumber !== finalTryNumber + ? tiHistory?.find((ti) => ti.tryNumber === selectedTryNumber) + : gridInstance || taskInstance; + const state = - gridInstance?.state || - (taskInstance?.state === "none" ? null : taskInstance?.state) || + instance?.state || + (instance?.state === "none" ? null : instance?.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 startDate = instance?.startDate; + const endDate = instance?.endDate; const executor = taskInstance?.executor || ""; - const operator = group?.operator || taskInstance?.operator; + const operator = taskInstance?.operator || group?.operator; const mappedStates = !taskInstance ? gridInstance?.mappedStates : undefined; @@ -92,6 +115,8 @@ const Details = ({ gridInstance, taskInstance, group }: Props) => { }); } + const isTaskInstance = !isGroup && !(isMapped && mapIndex === undefined); + const taskIdTitle = isGroup ? "Task Group ID" : "Task ID"; const isStateFinal = state && @@ -100,9 +125,15 @@ const Details = ({ gridInstance, taskInstance, group }: Props) => { return ( - - Task Instance Details - + {isTaskInstance && ( + + )} {group?.tooltip && ( @@ -173,12 +204,6 @@ const Details = ({ gridInstance, taskInstance, group }: Props) => { )} - {!!taskInstance?.tryNumber && ( - - - - - )} {operator && ( 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 a10dbe2931382..e9c29ed4a8245 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 @@ -25,6 +25,9 @@ import type { UseQueryResult } from "react-query"; import * as utils from "src/utils"; import * as useTaskLogModule from "src/api/useTaskLog"; +import * as useTIHistory from "src/api/useTIHistory"; +import * as useTaskInstance from "src/api/useTaskInstance"; +import type { TaskInstance } from "src/types/api-generated"; import Logs from "./index"; @@ -55,11 +58,41 @@ describe("Test Logs Component.", () => { isSuccess: true, } as UseQueryResult; + const tiReturnValue = { + data: { tryNumber: 2, startDate: "2024-06-18T01:47:51.724946+00:00" }, + isSuccess: true, + } as UseQueryResult; + + const tiHistoryValue = { + data: [ + { + tryNumber: 1, + startDate: "2024-06-17T01:47:51.724946+00:00", + endDate: "2024-06-17T01:50:51.724946+00:00", + state: "failed", + }, + { + tryNumber: 2, + startDate: "2024-06-18T01:47:51.724946+00:00", + endDate: "2024-06-18T01:50:51.724946+00:00", + state: "failed", + }, + ], + isSuccess: true, + } as UseQueryResult[], unknown>; + beforeEach(() => { useTaskLogMock = jest .spyOn(useTaskLogModule, "default") .mockImplementation(() => returnValue); window.HTMLElement.prototype.scrollIntoView = jest.fn(); + + jest + .spyOn(useTaskInstance, "default") + .mockImplementation(() => tiReturnValue); + jest + .spyOn(useTIHistory, "default") + .mockImplementation(() => tiHistoryValue); }); test("Test Logs Content", () => { 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 8510bd32854ef..95553d5499ed8 100644 --- a/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx +++ b/airflow/www/static/js/dag/details/taskInstance/Logs/index.tsx @@ -18,16 +18,7 @@ */ import React, { useState, useEffect, useMemo } from "react"; -import { - Text, - Box, - Flex, - Button, - Checkbox, - Icon, - Spinner, - Select, -} from "@chakra-ui/react"; +import { Text, Box, Flex, Checkbox, Icon, Spinner } from "@chakra-ui/react"; import { MdWarning } from "react-icons/md"; import { getMetaValue } from "src/utils"; @@ -41,6 +32,7 @@ import URLSearchParamsWrapper from "src/utils/URLSearchParamWrapper"; import LogLink from "./LogLink"; import { LogLevel, logLevelColorMapping, parseLogs } from "./utils"; import LogBlock from "./LogBlock"; +import TrySelector from "../TrySelector"; interface LogLevelOption { label: LogLevel; @@ -58,26 +50,6 @@ const showExternalLogRedirect = const externalLogName = getMetaValue("external_log_name"); const logUrl = getMetaValue("log_url"); -const getLinkIndexes = ( - tryNumber: number | undefined -): Array> => { - const internalIndexes: Array = []; - const externalIndexes: Array = []; - - if (tryNumber) { - [...Array(tryNumber)].forEach((_, index) => { - const tryNum = index + 1; - if (showExternalLogRedirect) { - externalIndexes.push(tryNum); - } else { - internalIndexes.push(tryNum); - } - }); - } - - return [internalIndexes, externalIndexes]; -}; - const logLevelOptions: Array = Object.values(LogLevel).map( (value): LogLevelOption => ({ label: value, @@ -105,10 +77,7 @@ const Logs = ({ tryNumber, state, }: Props) => { - const [internalIndexes, externalIndexes] = getLinkIndexes(tryNumber); - const [selectedTryNumber, setSelectedTryNumber] = useState< - number | undefined - >(); + const [selectedTryNumber, setSelectedTryNumber] = useState(tryNumber || 1); const [wrap, setWrap] = useState(getMetaValue("default_wrap") === "True"); const [logLevelFilters, setLogLevelFilters] = useState>( [] @@ -119,13 +88,12 @@ const Logs = ({ const [unfoldedLogGroups, setUnfoldedLogGroup] = useState>([]); const { timezone } = useTimezone(); - const taskTryNumber = selectedTryNumber || tryNumber || 1; const { data, isLoading } = useTaskLog({ dagId, dagRunId, taskId, mapIndex, - taskTryNumber, + taskTryNumber: selectedTryNumber, state, }); @@ -154,14 +122,11 @@ const Logs = ({ [data, fileSourceFilters, logLevelFilters, timezone, unfoldedLogGroups] ); - const logAttemptDropdownLimit = 10; - const showDropdown = internalIndexes.length > logAttemptDropdownLimit; - useEffect(() => { // Reset fileSourceFilters and selected attempt when changing to // a task that do not have those filters anymore. - if (taskTryNumber > (tryNumber || 1)) { - setSelectedTryNumber(undefined); + if (selectedTryNumber > (tryNumber || 1)) { + setSelectedTryNumber(tryNumber || 1); } if ( @@ -175,16 +140,17 @@ const Logs = ({ ) { setFileSourceFilters([]); } - }, [data, fileSourceFilters, fileSources, taskTryNumber, tryNumber]); + }, [data, fileSourceFilters, fileSources, selectedTryNumber, tryNumber]); return ( <> - {externalLogName && externalIndexes.length > 0 && ( + {showExternalLogRedirect && externalLogName && ( View Logs in {externalLogName} (by attempts): - {externalIndexes.map((index) => ( + {[...Array(tryNumber || 1)].map((_, index) => ( )} - {!showDropdown && ( - - (by attempts) - - - {internalIndexes.map((index) => ( - - ))} - - - - )} + - {showDropdown && ( - - - - )} diff --git a/airflow/www/static/js/dag/details/taskInstance/TrySelector.tsx b/airflow/www/static/js/dag/details/taskInstance/TrySelector.tsx new file mode 100644 index 0000000000000..f458748cef8f9 --- /dev/null +++ b/airflow/www/static/js/dag/details/taskInstance/TrySelector.tsx @@ -0,0 +1,127 @@ +/*! + * 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 { Text, Box, Flex, Button, Select } from "@chakra-ui/react"; +import Tooltip from "src/components/Tooltip"; +import { useContainerRef } from "src/context/containerRef"; +import { useTIHistory, useTaskInstance } from "src/api"; +import { getMetaValue } from "src/utils"; +import { SimpleStatus } from "src/dag/StatusBox"; +import { formatDuration, getDuration } from "src/datetime_utils"; + +const dagId = getMetaValue("dag_id"); + +interface Props { + taskId?: string; + runId?: string; + mapIndex?: number; + selectedTryNumber?: number; + onSelectTryNumber?: (tryNumber: number) => void; +} + +const TrySelector = ({ + taskId, + runId, + mapIndex, + selectedTryNumber, + onSelectTryNumber, +}: Props) => { + const containerRef = useContainerRef(); + + const { data: taskInstance } = useTaskInstance({ + dagId, + dagRunId: runId || "", + taskId: taskId || "", + mapIndex, + }); + const finalTryNumber = taskInstance?.tryNumber; + const { data: tiHistory } = useTIHistory({ + dagId, + taskId: taskId || "", + runId: runId || "", + mapIndex, + enabled: !!(finalTryNumber && finalTryNumber > 1) && !!taskId, // Only try to look up task tries if try number > 1 + }); + + if (!finalTryNumber || finalTryNumber <= 1) return null; + + const logAttemptDropdownLimit = 10; + const showDropdown = finalTryNumber > logAttemptDropdownLimit; + + const tries = (tiHistory || []).filter( + (t) => t?.startDate !== taskInstance?.startDate + ); + tries?.push(taskInstance); + + return ( + + Task Tries + {!showDropdown && ( + + {tries.map(({ tryNumber, state, startDate, endDate }, i) => ( + + Status: {state} + + Duration: {formatDuration(getDuration(startDate, endDate))} + + + } + hasArrow + portalProps={{ containerRef }} + placement="top" + > + + + ))} + + )} + {showDropdown && ( + + )} + + ); +}; + +export default TrySelector; diff --git a/airflow/www/static/js/types/index.ts b/airflow/www/static/js/types/index.ts index 79ed13c9bb6a2..573072b5ffada 100644 --- a/airflow/www/static/js/types/index.ts +++ b/airflow/www/static/js/types/index.ts @@ -40,6 +40,7 @@ type TaskState = | "upstream_failed" | "skipped" | "deferred" + | "none" | null; interface Dag { diff --git a/airflow/www/templates/airflow/dag.html b/airflow/www/templates/airflow/dag.html index ebdae42f4fe0b..e18a1486bd90f 100644 --- a/airflow/www/templates/airflow/dag.html +++ b/airflow/www/templates/airflow/dag.html @@ -60,6 +60,7 @@ +
{taskInstance.renderedMapIndex}
Try Number{taskInstance.tryNumber}
Operator