Skip to content

Commit

Permalink
AIP 64: Add TI try history to Task Instance Details, Logs, and Gantt …
Browse files Browse the repository at this point in the history
…chart (#40304)
  • Loading branch information
bbovenzi authored Jun 20, 2024
1 parent ddee71f commit 25132c9
Show file tree
Hide file tree
Showing 12 changed files with 487 additions and 192 deletions.
2 changes: 2 additions & 0 deletions airflow/www/static/js/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -108,4 +109,5 @@ export {
useCreateDatasetEvent,
useRenderedK8s,
useTaskDetail,
useTIHistory,
};
64 changes: 64 additions & 0 deletions airflow/www/static/js/api/useTIHistory.ts
Original file line number Diff line number Diff line change
@@ -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<AxiosResponse, Partial<TaskInstance>[]>(tiHistoryUrl, {
params,
});
},
{
enabled,
refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000,
}
);
}
2 changes: 1 addition & 1 deletion airflow/www/static/js/dag/StatusBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const StatusWithNotes = ({
};

interface SimpleStatusProps extends BoxProps {
state: TaskState;
state: TaskState | undefined;
}
export const SimpleStatus = ({ state, ...rest }: SimpleStatusProps) => (
<Box
Expand Down
25 changes: 14 additions & 11 deletions airflow/www/static/js/dag/details/gantt/GanttTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,15 @@ import React from "react";
import { Box, Text } from "@chakra-ui/react";
import { getDuration, formatDuration } from "src/datetime_utils";
import Time from "src/components/Time";
import type { Task, TaskInstance } from "src/types";
import type { Task, API } from "src/types";

type Instance = Pick<
API.TaskInstance,
"startDate" | "endDate" | "tryNumber" | "queuedWhen"
>;

interface Props {
instance: TaskInstance;
instance: Instance;
task: Task;
}

Expand All @@ -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 (
<Box>
<Text>
Task{isGroup ? " Group" : ""}: {task.label}
</Text>
{!!instance?.tryNumber && instance.tryNumber > 1 && (
<Text>Try Number: {instance.tryNumber}</Text>
)}
{!!instance?.tryNumber && <Text>Try Number: {instance.tryNumber}</Text>}
<br />
{instance?.queuedDttm && (
{instance?.queuedWhen && (
<Text>
{isMappedOrGroupSummary && "Total "}Queued Duration:{" "}
{formatDuration(queuedDuration)}
Expand All @@ -59,10 +62,10 @@ const GanttTooltip = ({ task, instance }: Props) => {
{formatDuration(taskDuration)}
</Text>
<br />
{instance?.queuedDttm && (
{instance?.queuedWhen && (
<Text>
{isMappedOrGroupSummary && "Earliest "}Queued At:{" "}
<Time dateTime={instance?.queuedDttm} />
<Time dateTime={instance?.queuedWhen} />
</Text>
)}
{instance?.startDate && (
Expand Down
140 changes: 140 additions & 0 deletions airflow/www/static/js/dag/details/gantt/InstanceBar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Tooltip
label={<GanttTooltip task={task} instance={instance} />}
hasArrow
portalProps={{ containerRef }}
placement="top"
openDelay={hoverDelay}
>
<Flex
width={`${width + queuedWidth}px`}
position="absolute"
top="4px"
left={`${offsetMargin}px`}
cursor="pointer"
pointerEvents="auto"
onClick={() => {
onSelect({
runId: instance.dagRunId,
taskId: instance.taskId,
});
}}
>
{instance.state !== "queued" && hasValidQueuedDttm && (
<SimpleStatus
state="queued"
width={`${queuedWidth}px`}
borderRightRadius={0}
// The normal queued color is too dark when next to the actual task's state
opacity={0.6}
/>
)}
<SimpleStatus
state={
!instance.state || instance?.state === "none"
? null
: instance.state
}
width={`${width}px`}
borderLeftRadius={
instance.state !== "queued" && hasValidQueuedDttm ? 0 : undefined
}
/>
</Flex>
</Tooltip>
);
};

export default InstanceBar;
Loading

0 comments on commit 25132c9

Please sign in to comment.