diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index 186ebb5f6f19e..8008307fabf40 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -35,8 +35,9 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import type { TaskInstanceResponse, GridRunsResponse } from "openapi/requests/types.gen"; +import { useTimezone } from "src/context/timezone"; import { getComputedCSSVariableValue } from "src/theme"; -import { DEFAULT_DATETIME_FORMAT, renderDuration } from "src/utils/datetimeUtils"; +import { DEFAULT_DATETIME_FORMAT, formatDate, renderDuration } from "src/utils/datetimeUtils"; import { buildTaskInstanceUrl } from "src/utils/links"; ChartJS.register( @@ -69,15 +70,35 @@ const getDuration = (start: string, end: string | null) => { return dayjs.duration(endDate.diff(startDate)).asSeconds(); }; +const getTickLabelFormat = (entries: Array): string => { + if (entries.length < 2) { + return "HH:mm:ss"; + } + + const first = dayjs(entries[0]?.run_after); + const last = dayjs(entries[entries.length - 1]?.run_after); + + if (!first.isValid() || !last.isValid()) { + return "MMM DD"; + } + + const diffInDays = Math.abs(last.diff(first, "day")); + + return diffInDays < 1 ? "HH:mm:ss" : "MMM DD HH:mm"; +}; + export const DurationChart = ({ entries, + isAutoRefreshing = false, kind, }: { readonly entries: Array | undefined; + readonly isAutoRefreshing?: boolean; readonly kind: "Dag Run" | "Task Instance"; }) => { const { t: translate } = useTranslation(["components", "common"]); const navigate = useNavigate(); + const { selectedTimezone } = useTimezone(); const [queuedColorToken] = useToken("colors", ["queued.solid"]); // Get states and create color tokens for them @@ -175,6 +196,7 @@ export const DurationChart = ({ }} datasetIdKey="id" options={{ + animation: isAutoRefreshing ? false : undefined, onClick: (_event, elements) => { const [element] = elements; @@ -239,6 +261,8 @@ export const DurationChart = ({ x: { stacked: true, ticks: { + callback: (_value, index) => + formatDate(entries[index]?.run_after, selectedTimezone, getTickLabelFormat(entries)), maxTicksLimit: 3, }, title: { align: "end", display: true, text: translate("common:dagRun.runAfter") }, diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index 043ae4d1b1509..7ea155fe84761 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -36,6 +36,7 @@ import { TrendCountButton } from "src/components/TrendCountButton"; import { dagRunsLimitKey } from "src/constants/localStorage"; import { SearchParamsKeys } from "src/constants/searchParams"; import { useGridRuns } from "src/queries/useGridRuns.ts"; +import { isStatePending, useAutoRefresh } from "src/utils"; const FailedLogs = lazy(() => import("./FailedLogs")); @@ -68,6 +69,9 @@ export const Overview = () => { state: ["failed"], }); const { data: gridRuns, isLoading: isLoadingRuns } = useGridRuns({ limit }); + const refetchInterval = useAutoRefresh({ dagId }); + const isAutoRefreshing = + Boolean(refetchInterval) && (gridRuns ?? []).some((run) => isStatePending(run.state)); const { data: assetEventsData, isLoading: isLoadingAssetEvents } = useAssetServiceGetAssetEvents({ limit, orderBy: [assetSortBy], @@ -125,7 +129,11 @@ export const Overview = () => { {isLoadingRuns ? ( ) : ( - + )} {assetEventsData && assetEventsData.total_entries > 0 ? (