From a7674923883d137dc6e0822f70930c7375b1285f Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sat, 10 Jan 2026 15:12:31 +0900 Subject: [PATCH 1/9] feat: add version indicator for DAG and bundle versions in Grid view --- .../core_api/datamodels/ui/common.py | 2 + .../core_api/datamodels/ui/grid.py | 1 + .../core_api/openapi/_private_ui.yaml | 15 +++ .../api_fastapi/core_api/routes/ui/grid.py | 45 +++++-- .../api_fastapi/core_api/services/ui/grid.py | 4 + .../ui/openapi-gen/requests/schemas.gen.ts | 33 +++++ .../ui/openapi-gen/requests/types.gen.ts | 3 + .../ui/public/i18n/locales/en/dag.json | 9 ++ .../ui/src/components/ui/VersionIndicator.tsx | 123 ++++++++++++++++++ .../constants/showVersionIndicatorOptions.ts | 48 +++++++ .../ui/src/layouts/Details/DetailsLayout.tsx | 10 ++ .../ui/src/layouts/Details/Grid/Bar.tsx | 21 ++- .../ui/src/layouts/Details/Grid/Grid.tsx | 33 ++++- .../Details/Grid/TaskInstancesColumn.tsx | 63 ++++++++- .../Grid/useGridRunsWithVersionFlags.ts | 71 ++++++++++ .../ui/src/layouts/Details/PanelButtons.tsx | 83 +++++++++++- .../core_api/routes/ui/test_grid.py | 44 +++---- 17 files changed, 556 insertions(+), 52 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx create mode 100644 airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts create mode 100644 airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py index 2c5832f3246e8..05467eda10c0a 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py @@ -80,6 +80,8 @@ class GridRunsResponse(BaseModel): state: DagRunState | None run_type: DagRunType has_missed_deadline: bool + bundle_version: str | None = None + dag_version_number: int | None = None @computed_field def duration(self) -> float: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py index 46e275a803c3e..70b0c590c3f3c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py @@ -32,6 +32,7 @@ class LightGridTaskInstanceSummary(BaseModel): child_states: dict[TaskInstanceState | None, int] | None min_start_date: datetime | None max_end_date: datetime | None + dag_version_number: int | None = None class GridTISummaries(BaseModel): diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index c3f307afbe73b..fa6a1d8e48092 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -2186,6 +2186,16 @@ components: has_missed_deadline: type: boolean title: Has Missed Deadline + bundle_version: + anyOf: + - type: string + - type: 'null' + title: Bundle Version + dag_version_number: + anyOf: + - type: integer + - type: 'null' + title: Dag Version Number duration: type: number title: Duration @@ -2453,6 +2463,11 @@ components: format: date-time - type: 'null' title: Max End Date + dag_version_number: + anyOf: + - type: integer + - type: 'null' + title: Dag Version Number type: object required: - task_id diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py index 7762656415ecf..59f962b5d4a40 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py @@ -22,7 +22,7 @@ import structlog from fastapi import Depends, HTTPException, status -from sqlalchemy import exists, select +from sqlalchemy import exists, func, select from sqlalchemy.orm import joinedload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity @@ -282,17 +282,35 @@ def get_grid_runs( .correlate(DagRun) .label("has_missed_deadline") ) - base_query = select( - DagRun.dag_id, - DagRun.run_id, - DagRun.queued_at, - DagRun.start_date, - DagRun.end_date, - DagRun.run_after, - DagRun.state, - DagRun.run_type, - has_missed_deadline, - ).where(DagRun.dag_id == dag_id) + # get the highest dag_version_number from TIs for each run + latest_ti_version = ( + select( + TaskInstance.run_id, + func.max(DagVersion.version_number).label("version_number"), + ) + .join(DagVersion, TaskInstance.dag_version_id == DagVersion.id) + .where(TaskInstance.dag_id == dag_id) + .group_by(TaskInstance.run_id) + .subquery() + ) + + base_query = ( + select( + DagRun.dag_id, + DagRun.run_id, + DagRun.queued_at, + DagRun.start_date, + DagRun.end_date, + DagRun.run_after, + DagRun.state, + DagRun.run_type, + has_missed_deadline, + DagRun.bundle_version, + latest_ti_version.c.version_number.label("dag_version_number"), + ) + .outerjoin(latest_ti_version, DagRun.run_id == latest_ti_version.c.run_id) + .where(DagRun.dag_id == dag_id) + ) # This comparison is to fall back to DAG timetable when no order_by is provided if order_by.value == [order_by.get_primary_key_string()]: @@ -363,7 +381,9 @@ def get_grid_ti_summaries( TaskInstance.dag_version_id, TaskInstance.start_date, TaskInstance.end_date, + DagVersion.version_number, ) + .outerjoin(DagVersion, TaskInstance.dag_version_id == DagVersion.id) .where(TaskInstance.dag_id == dag_id) .where( TaskInstance.run_id == run_id, @@ -386,6 +406,7 @@ def get_grid_ti_summaries( "state": ti.state, "start_date": ti.start_date, "end_date": ti.end_date, + "dag_version_number": ti.version_number, } ) serdag = _get_serdag( diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py index 46ec2c45d98ae..17f73f67e6839 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py @@ -72,11 +72,15 @@ def _get_aggs_for_node(detail): max_end_date = max(x["end_date"] for x in detail if x["end_date"]) except ValueError: max_end_date = None + + dag_version_number = detail[0].get("dag_version_number") + return { "state": agg_state(states), "min_start_date": min_start_date, "max_end_date": max_end_date, "child_states": dict(Counter(states)), + "dag_version_number": dag_version_number, } diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index b9e305f07c983..4a27bce51c5e3 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -8147,6 +8147,28 @@ export const $GridRunsResponse = { type: 'boolean', title: 'Has Missed Deadline' }, + bundle_version: { + anyOf: [ + { + type: 'string' + }, + { + type: 'null' + } + ], + title: 'Bundle Version' + }, + dag_version_number: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Dag Version Number' + }, duration: { type: 'number', title: 'Duration', @@ -8258,6 +8280,17 @@ export const $LightGridTaskInstanceSummary = { } ], title: 'Max End Date' + }, + dag_version_number: { + anyOf: [ + { + type: 'integer' + }, + { + type: 'null' + } + ], + title: 'Dag Version Number' } }, type: 'object', diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index aa596d50253fb..c8e278b4ed553 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -2000,6 +2000,8 @@ export type GridRunsResponse = { state: DagRunState | null; run_type: DagRunType; has_missed_deadline: boolean; + bundle_version?: string | null; + dag_version_number?: number | null; readonly duration: number; }; @@ -2033,6 +2035,7 @@ export type LightGridTaskInstanceSummary = { } | null; min_start_date: string | null; max_end_date: string | null; + dag_version_number?: number | null; }; /** diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json index 04380b149a8b2..c38b2a888702d 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json @@ -121,6 +121,15 @@ "graphDirection": { "label": "Graph Direction" }, + "showVersionIndicator": { + "label": "Show Version Indicator", + "options": { + "hideAll": "Hide All", + "showAll": "Show All", + "showBundleVersion": "Show Bundle Version", + "showDagVersion": "Show Dag Version" + } + }, "taskStreamFilter": { "activeFilter": "Active filter", "clearFilter": "Clear Filter", diff --git a/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx b/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx new file mode 100644 index 0000000000000..21d73e4712a19 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx @@ -0,0 +1,123 @@ +/*! + * 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 { Box } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { FiGitCommit } from "react-icons/fi"; + +import { Tooltip } from "src/components/ui"; + +type BundleVersionIndicatorProps = { + readonly bundleVersion: string | undefined; +}; + +export const BundleVersionIndicator = ({ bundleVersion }: BundleVersionIndicatorProps) => { + const { t: translate } = useTranslation("components"); + + return ( + + + + + + ); +}; + +type DagVersionIndicatorProps = { + readonly dagVersionNumber: number | undefined; + readonly orientation?: "horizontal" | "vertical"; +}; + +export const DagVersionIndicator = ({ + dagVersionNumber, + orientation = "vertical", +}: DagVersionIndicatorProps) => { + const isVertical = orientation === "vertical"; + + const containerStyles = { + horizontal: { + height: 0.5, + left: "50%", + top: 0, + transform: "translate(-50%, -50%)", + width: 4.5, + }, + vertical: { + height: 104, + left: -1.25, + top: -1.5, + width: 0.5, + }, + } as const; + + const circleStyles = { + horizontal: { + height: 1.5, + left: "50%", + top: "50%", + transform: "translate(-50%, -50%)", + width: 1.5, + }, + vertical: { + height: 1.5, + left: "50%", + top: -1, + transform: "translateX(-50%)", + width: 1.5, + }, + } as const; + + const currentContainerStyle = containerStyles[orientation]; + const currentCircleStyle = circleStyles[orientation]; + + return ( + + + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts b/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts new file mode 100644 index 0000000000000..90ea231f592ff --- /dev/null +++ b/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts @@ -0,0 +1,48 @@ +/*! + * 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 { createListCollection } from "@chakra-ui/react"; + +export enum VersionIndicatorDisplayOptions { + ALL = "all", + BUNDLE = "bundle", + DAG = "dag", + NONE = "none", +} + +export type VersionIndicatorDisplayOption = VersionIndicatorDisplayOptions; + +const validOptions = new Set(Object.values(VersionIndicatorDisplayOptions)); + +export const isVersionIndicatorDisplayOption = (value: unknown): value is VersionIndicatorDisplayOption => + typeof value === "string" && validOptions.has(value); + +export const showVersionIndicatorOptions = createListCollection({ + items: [ + { label: "dag:panel.showVersionIndicator.options.showAll", value: VersionIndicatorDisplayOptions.ALL }, + { + label: "dag:panel.showVersionIndicator.options.showBundleVersion", + value: VersionIndicatorDisplayOptions.BUNDLE, + }, + { + label: "dag:panel.showVersionIndicator.options.showDagVersion", + value: VersionIndicatorDisplayOptions.DAG, + }, + { label: "dag:panel.showVersionIndicator.options.hideAll", value: VersionIndicatorDisplayOptions.NONE }, + ], +}); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 09d116a8370e7..f26ff6f4a0038 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -50,6 +50,8 @@ import { showGanttKey, triggeringUserFilterKey, } from "src/constants/localStorage"; +import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { HoverProvider } from "src/context/hover"; import { OpenGroupsProvider } from "src/context/openGroups"; @@ -88,6 +90,11 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { ); const [showGantt, setShowGantt] = useLocalStorage(showGanttKey(dagId), false); + const [showVersionIndicatorMode, setShowVersionIndicatorMode] = + useLocalStorage( + `version_indicator_display_mode`, + VersionIndicatorDisplayOptions.ALL, + ); const { fitView, getZoom } = useReactFlow(); const { data: warningData } = useDagWarningServiceListDagWarnings({ dagId }); const { onClose, onOpen, open } = useDisclosure(); @@ -161,8 +168,10 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { setLimit={setLimit} setRunTypeFilter={setRunTypeFilter} setShowGantt={setShowGantt} + setShowVersionIndicatorMode={setShowVersionIndicatorMode} setTriggeringUserFilter={setTriggeringUserFilter} showGantt={showGantt} + showVersionIndicatorMode={showVersionIndicatorMode} triggeringUserFilter={triggeringUserFilter} /> {dagView === "graph" ? ( @@ -174,6 +183,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { limit={limit} runType={runTypeFilter} showGantt={Boolean(runId) && showGantt} + showVersionIndicatorMode={showVersionIndicatorMode} triggeringUser={triggeringUserFilter} /> {showGantt ? ( diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx index 6b52198f51025..8dfe1873e1a00 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx @@ -19,21 +19,25 @@ import { Flex, Box } from "@chakra-ui/react"; import { useParams, useSearchParams } from "react-router-dom"; -import type { GridRunsResponse } from "openapi/requests"; import { RunTypeIcon } from "src/components/RunTypeIcon"; +import { BundleVersionIndicator, DagVersionIndicator } from "src/components/ui/VersionIndicator"; +import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { useHover } from "src/context/hover"; import { GridButton } from "./GridButton"; +import type { GridRunWithVersionFlags } from "./useGridRunsWithVersionFlags"; const BAR_HEIGHT = 100; type Props = { readonly max: number; readonly onClick?: () => void; - readonly run: GridRunsResponse; + readonly run: GridRunWithVersionFlags; + readonly showVersionIndicatorMode?: VersionIndicatorDisplayOption; }; -export const Bar = ({ max, onClick, run }: Props) => { +export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => { const { dagId = "", runId } = useParams(); const [searchParams] = useSearchParams(); const { hoveredRunId, setHoveredRunId } = useHover(); @@ -53,6 +57,17 @@ export const Bar = ({ max, onClick, run }: Props) => { position="relative" transition="background-color 0.2s" > + {run.isBundleVersionChange && + (showVersionIndicatorMode === VersionIndicatorDisplayOptions.BUNDLE || + showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) ? ( + + ) : undefined} + {run.isDagVersionChange && + (showVersionIndicatorMode === VersionIndicatorDisplayOptions.DAG || + showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) ? ( + + ) : undefined} + { +export const Grid = ({ + dagRunState, + limit, + runType, + showGantt, + showVersionIndicatorMode, + triggeringUser, +}: Props) => { const { t: translate } = useTranslation("dag"); const gridRef = useRef(null); const scrollContainerRef = useRef(null); @@ -107,7 +117,13 @@ export const Grid = ({ dagRunState, limit, runType, showGantt, triggeringUser }: .filter((duration: number | null): duration is number => duration !== null), ); - const { flatNodes } = flattenNodes(dagStructure, openGroupIds); + // calculate version change flags + const runsWithVersionFlags = useGridRunsWithVersionFlags({ + gridRuns, + showVersionIndicatorMode, + }); + + const { flatNodes } = useMemo(() => flattenNodes(dagStructure, openGroupIds), [dagStructure, openGroupIds]); const { setMode } = useNavigation({ onToggleGroup: toggleGroupId, @@ -166,8 +182,14 @@ export const Grid = ({ dagRunState, limit, runType, showGantt, triggeringUser }: - {gridRuns?.map((dr: GridRunsResponse) => ( - + {runsWithVersionFlags?.map((dr) => ( + ))} {selectedIsVisible === undefined || !selectedIsVisible ? undefined : ( @@ -202,6 +224,7 @@ export const Grid = ({ dagRunState, limit, runType, showGantt, triggeringUser }: nodes={flatNodes} onCellClick={handleCellClick} run={dr} + showVersionIndicatorMode={showVersionIndicatorMode} virtualItems={virtualItems} /> ))} diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx index dcbf7cc212053..78c1b586a3c27 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx @@ -18,10 +18,14 @@ */ import { Box } from "@chakra-ui/react"; import type { VirtualItem } from "@tanstack/react-virtual"; +import { useMemo } from "react"; import { useParams } from "react-router-dom"; import type { GridRunsResponse } from "openapi/requests"; import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; +import { DagVersionIndicator } from "src/components/ui/VersionIndicator"; +import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { useHover } from "src/context/hover"; import { useGridTiSummaries } from "src/queries/useGridTISummaries.ts"; @@ -32,12 +36,19 @@ type Props = { readonly nodes: Array; readonly onCellClick?: () => void; readonly run: GridRunsResponse; + readonly showVersionIndicatorMode?: VersionIndicatorDisplayOption; readonly virtualItems?: Array; }; const ROW_HEIGHT = 20; -export const TaskInstancesColumn = ({ nodes, onCellClick, run, virtualItems }: Props) => { +export const TaskInstancesColumn = ({ + nodes, + onCellClick, + run, + showVersionIndicatorMode, + virtualItems, +}: Props) => { const { dagId = "", runId } = useParams(); const isSelected = runId === run.run_id; const { data: gridTISummaries } = useGridTiSummaries({ @@ -51,12 +62,27 @@ export const TaskInstancesColumn = ({ nodes, onCellClick, run, virtualItems }: P const itemsToRender = virtualItems ?? nodes.map((_, index) => ({ index, size: ROW_HEIGHT, start: index * ROW_HEIGHT })); - const taskInstances = gridTISummaries?.task_instances ?? []; - const taskInstanceMap = new Map(); + const taskInstances = useMemo( + () => gridTISummaries?.task_instances ?? [], + [gridTISummaries?.task_instances], + ); + const taskInstanceMap = useMemo(() => { + const map = new Map(); + + for (const ti of taskInstances) { + map.set(ti.task_id, ti); + } + + return map; + }, [taskInstances]); - for (const ti of taskInstances) { - taskInstanceMap.set(ti.task_id, ti); - } + const hasMixedVersions = useMemo(() => { + const versionNumbers = new Set( + taskInstances.map((ti) => ti.dag_version_number).filter((vn) => vn !== null && vn !== undefined), + ); + + return versionNumbers.size > 1; + }, [taskInstances]); const isHovered = hoveredRunId === run.run_id; @@ -72,7 +98,7 @@ export const TaskInstancesColumn = ({ nodes, onCellClick, run, virtualItems }: P transition="background-color 0.2s" width="18px" > - {itemsToRender.map((virtualItem) => { + {itemsToRender.map((virtualItem, idx) => { const node = nodes[virtualItem.index]; if (!node) { @@ -95,6 +121,23 @@ export const TaskInstancesColumn = ({ nodes, onCellClick, run, virtualItems }: P ); } + let hasVersionChangeFlag = false; + + if ( + hasMixedVersions && + (showVersionIndicatorMode === VersionIndicatorDisplayOptions.DAG || + showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) && + idx > 0 + ) { + const prevVirtualItem = itemsToRender[idx - 1]; + const prevNode = prevVirtualItem ? nodes[prevVirtualItem.index] : undefined; + const prevTaskInstance = prevNode ? taskInstanceMap.get(prevNode.id) : undefined; + + hasVersionChangeFlag = Boolean( + prevTaskInstance && prevTaskInstance.dag_version_number !== taskInstance.dag_version_number, + ); + } + return ( + {hasVersionChangeFlag && ( + + )} | undefined; + showVersionIndicatorMode?: VersionIndicatorDisplayOption; +}; + +// Hook to calculate version change flags for grid runs. +export const useGridRunsWithVersionFlags = ({ + gridRuns, + showVersionIndicatorMode, +}: UseGridRunsWithVersionFlagsParams): Array | undefined => { + const isVersionIndicatorEnabled = showVersionIndicatorMode !== VersionIndicatorDisplayOptions.NONE; + + return useMemo(() => { + if (!gridRuns) { + return undefined; + } + + if (!isVersionIndicatorEnabled) { + return gridRuns.map((run) => ({ ...run, isBundleVersionChange: false, isDagVersionChange: false })); + } + + return gridRuns.map((run, index) => { + const prevRun = gridRuns[index + 1]; + + const isBundleVersionChange = Boolean( + prevRun && + run.bundle_version !== null && + prevRun.bundle_version !== null && + run.bundle_version !== prevRun.bundle_version, + ); + + const isDagVersionChange = Boolean( + prevRun && + run.dag_version_number !== null && + prevRun.dag_version_number !== null && + run.dag_version_number !== prevRun.dag_version_number, + ); + + return { ...run, isBundleVersionChange, isDagVersionChange }; + }); + }, [gridRuns, isVersionIndicatorEnabled]); +}; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index cdcaa90f3fc1e..703d801bafbd4 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -20,6 +20,9 @@ */ import { Box, + Button, + ButtonGroup, + Circle, createListCollection, Flex, IconButton, @@ -34,7 +37,7 @@ import { useReactFlow } from "@xyflow/react"; import { useEffect, useMemo, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; -import { FiGrid } from "react-icons/fi"; +import { FiChevronDown, FiGitCommit, FiGrid } from "react-icons/fi"; import { LuKeyboard } from "react-icons/lu"; import { MdOutlineAccountTree, MdSettings } from "react-icons/md"; import type { ImperativePanelGroupHandle } from "react-resizable-panels"; @@ -51,6 +54,12 @@ import { Tooltip } from "src/components/ui"; import { type ButtonGroupOption, ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { Checkbox } from "src/components/ui/Checkbox"; import { dependenciesKey, directionKey } from "src/constants/localStorage"; +import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; +import { + isVersionIndicatorDisplayOption, + showVersionIndicatorOptions, + VersionIndicatorDisplayOptions, +} from "src/constants/showVersionIndicatorOptions"; import { dagRunTypeOptions, dagRunStateOptions } from "src/constants/stateOptions"; import { useContainerWidth } from "src/utils/useContainerWidth"; @@ -69,8 +78,10 @@ type Props = { readonly setLimit: React.Dispatch>; readonly setRunTypeFilter: React.Dispatch>; readonly setShowGantt: React.Dispatch>; + readonly setShowVersionIndicatorMode: React.Dispatch>; readonly setTriggeringUserFilter: React.Dispatch>; readonly showGantt: boolean; + readonly showVersionIndicatorMode: VersionIndicatorDisplayOption; readonly triggeringUserFilter: string | undefined; }; @@ -118,8 +129,10 @@ export const PanelButtons = ({ setLimit, setRunTypeFilter, setShowGantt, + setShowVersionIndicatorMode, setTriggeringUserFilter, showGantt, + showVersionIndicatorMode, triggeringUserFilter, }: Props) => { const { t: translate } = useTranslation(["components", "dag"]); @@ -194,6 +207,16 @@ export const PanelButtons = ({ setTriggeringUserFilter(trimmedValue === "" ? undefined : trimmedValue); }; + const handleShowVersionIndicatorChange = ( + event: SelectValueChangeDetails<{ label: string; value: Array }>, + ) => { + const [selectedDisplayMode] = event.value; + + if (isVersionIndicatorDisplayOption(selectedDisplayMode)) { + setShowVersionIndicatorMode(selectedDisplayMode); + } + }; + const handleFocus = (view: string) => { if (panelGroupRef.current) { const newLayout = view === "graph" ? [70, 30] : [30, 70]; @@ -472,6 +495,64 @@ export const PanelButtons = ({ ) : undefined} )} + {/* eslint-disable react/jsx-max-depth */} + + + + {translate("dag:panel.showVersionIndicator.label")} + + + + + + {(showVersionIndicatorMode === VersionIndicatorDisplayOptions.BUNDLE || + showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {(showVersionIndicatorMode === VersionIndicatorDisplayOptions.DAG || + showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {translate( + showVersionIndicatorOptions.items.find( + (item) => item.value === showVersionIndicatorMode, + )?.label ?? "", + )} + + + + + + + + + + {showVersionIndicatorOptions.items.map((option) => ( + + + {(option.value === VersionIndicatorDisplayOptions.BUNDLE || + option.value === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {(option.value === VersionIndicatorDisplayOptions.DAG || + option.value === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {translate(option.label)} + + + ))} + + + + + {/* eslint-enable react/jsx-max-depth */} diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py index ed4d6fb2e0e43..7eaafb5d63e12 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py @@ -61,6 +61,7 @@ GRID_RUN_1 = { "dag_id": "test_dag", + "dag_version_number": 1, "duration": 283996800.0, "end_date": "2024-12-31T00:00:00Z", "has_missed_deadline": False, @@ -73,6 +74,7 @@ GRID_RUN_2 = { "dag_id": "test_dag", + "dag_version_number": 1, "duration": 283996800.0, "end_date": "2024-12-31T00:00:00Z", "has_missed_deadline": False, @@ -608,30 +610,7 @@ def test_get_grid_runs(self, session, test_client): with assert_queries_count(5): response = test_client.get(f"/grid/runs/{DAG_ID}?limit=5") assert response.status_code == 200 - assert response.json() == [ - { - "dag_id": "test_dag", - "duration": 283996800.0, - "end_date": "2024-12-31T00:00:00Z", - "has_missed_deadline": False, - "run_after": "2024-11-30T00:00:00Z", - "run_id": "run_1", - "run_type": "scheduled", - "start_date": "2016-01-01T00:00:00Z", - "state": "success", - }, - { - "dag_id": "test_dag", - "duration": 283996800.0, - "end_date": "2024-12-31T00:00:00Z", - "has_missed_deadline": False, - "run_after": "2024-11-30T00:00:00Z", - "run_id": "run_2", - "run_type": "manual", - "start_date": "2016-01-01T00:00:00Z", - "state": "failed", - }, - ] + assert response.json() == [GRID_RUN_1, GRID_RUN_2] @pytest.mark.parametrize( ("endpoint", "run_type", "expected"), @@ -702,6 +681,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "t1", "task_display_name": "t1", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:00Z", "min_start_date": "2025-03-01T23:59:58Z", }, @@ -710,6 +690,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "t2", "task_display_name": "t2", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:02Z", "min_start_date": "2025-03-02T00:00:00Z", }, @@ -718,11 +699,13 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "t7", "task_display_name": "t7", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:04Z", "min_start_date": "2025-03-02T00:00:02Z", }, { "child_states": {"success": 4}, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:12Z", "min_start_date": "2025-03-02T00:00:04Z", "state": "success", @@ -734,11 +717,13 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "task_group-1.t6", "task_display_name": "task_group-1.t6", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:06Z", "min_start_date": "2025-03-02T00:00:04Z", }, { "child_states": {"success": 3}, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:12Z", "min_start_date": "2025-03-02T00:00:06Z", "state": "success", @@ -750,6 +735,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "task_group-1.task_group-2.t3", "task_display_name": "task_group-1.task_group-2.t3", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:08Z", "min_start_date": "2025-03-02T00:00:06Z", }, @@ -758,6 +744,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "task_group-1.task_group-2.t4", "task_display_name": "task_group-1.task_group-2.t4", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:10Z", "min_start_date": "2025-03-02T00:00:08Z", }, @@ -766,6 +753,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "task_id": "task_group-1.task_group-2.t5", "task_display_name": "task_group-1.task_group-2.t5", "child_states": None, + "dag_version_number": 1, "max_end_date": "2025-03-02T00:00:12Z", "min_start_date": "2025-03-02T00:00:10Z", }, @@ -797,6 +785,7 @@ def sort_dict(in_dict): expected = [ { "child_states": {"None": 1}, + "dag_version_number": 1, "task_id": "mapped_task_2", "task_display_name": "mapped_task_2", "max_end_date": None, @@ -805,6 +794,7 @@ def sort_dict(in_dict): }, { "child_states": {"success": 1, "running": 1, "None": 1}, + "dag_version_number": 1, "max_end_date": "2024-12-30T01:02:03Z", "min_start_date": "2024-12-30T01:00:00Z", "state": "running", @@ -816,6 +806,7 @@ def sort_dict(in_dict): "task_id": "mapped_task_group.subtask", "task_display_name": "mapped_task_group.subtask", "child_states": None, + "dag_version_number": 1, "max_end_date": "2024-12-30T01:02:03Z", "min_start_date": "2024-12-30T01:00:00Z", }, @@ -824,11 +815,13 @@ def sort_dict(in_dict): "task_id": "task", "task_display_name": "A Beautiful Task Name \U0001f680", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, { "child_states": {"None": 6}, + "dag_version_number": 1, "task_id": "task_group", "task_display_name": "task_group", "max_end_date": None, @@ -837,6 +830,7 @@ def sort_dict(in_dict): }, { "child_states": {"None": 2}, + "dag_version_number": 1, "task_id": "task_group.inner_task_group", "task_display_name": "task_group.inner_task_group", "max_end_date": None, @@ -845,6 +839,7 @@ def sort_dict(in_dict): }, { "child_states": {"None": 2}, + "dag_version_number": 1, "task_id": "task_group.inner_task_group.inner_task_group_sub_task", "task_display_name": "Inner Task Group Sub Task Label", "max_end_date": None, @@ -853,6 +848,7 @@ def sort_dict(in_dict): }, { "child_states": {"None": 4}, + "dag_version_number": 1, "task_id": "task_group.mapped_task", "task_display_name": "task_group.mapped_task", "max_end_date": None, From 7674e16f82a2034c94b7bf8655cc013021d1d050 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sun, 8 Feb 2026 00:09:46 +0900 Subject: [PATCH 2/9] Refactor version indicator handling and add VersionIndicatorSelect component --- .../api_fastapi/core_api/services/ui/grid.py | 3 +- .../ui/src/components/ui/VersionIndicator.tsx | 71 +++++++------- .../constants/showVersionIndicatorOptions.ts | 4 +- .../ui/src/layouts/Details/DetailsLayout.tsx | 4 +- .../ui/src/layouts/Details/Grid/Bar.tsx | 3 +- .../ui/src/layouts/Details/Grid/Grid.tsx | 4 +- .../Details/Grid/TaskInstancesColumn.tsx | 3 +- .../Grid/useGridRunsWithVersionFlags.ts | 3 +- .../ui/src/layouts/Details/PanelButtons.tsx | 81 ++-------------- .../Details/VersionIndicatorSelect.tsx | 97 +++++++++++++++++++ 10 files changed, 150 insertions(+), 123 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py index 17f73f67e6839..efc8f98f385b5 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py @@ -73,7 +73,8 @@ def _get_aggs_for_node(detail): except ValueError: max_end_date = None - dag_version_number = detail[0].get("dag_version_number") + dag_version_numbers = [x.get("dag_version_number") for x in detail if x.get("dag_version_number") is not None] + dag_version_number = max(dag_version_numbers) if dag_version_numbers else None return { "state": agg_state(states), diff --git a/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx b/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx index 21d73e4712a19..1b75b6b2112ff 100644 --- a/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx +++ b/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx @@ -38,6 +38,39 @@ export const BundleVersionIndicator = ({ bundleVersion }: BundleVersionIndicator ); }; +const CONTAINER_STYLES = { + horizontal: { + height: 0.5, + left: "50%", + top: 0, + transform: "translate(-50%, -50%)", + width: 4.5, + }, + vertical: { + height: 104, + left: -1.25, + top: -1.5, + width: 0.5, + }, +} as const; + +const CIRCLE_STYLES = { + horizontal: { + height: 1.5, + left: "50%", + top: "50%", + transform: "translate(-50%, -50%)", + width: 1.5, + }, + vertical: { + height: 1.5, + left: "50%", + top: -1, + transform: "translateX(-50%)", + width: 1.5, + }, +} as const; + type DagVersionIndicatorProps = { readonly dagVersionNumber: number | undefined; readonly orientation?: "horizontal" | "vertical"; @@ -48,42 +81,8 @@ export const DagVersionIndicator = ({ orientation = "vertical", }: DagVersionIndicatorProps) => { const isVertical = orientation === "vertical"; - - const containerStyles = { - horizontal: { - height: 0.5, - left: "50%", - top: 0, - transform: "translate(-50%, -50%)", - width: 4.5, - }, - vertical: { - height: 104, - left: -1.25, - top: -1.5, - width: 0.5, - }, - } as const; - - const circleStyles = { - horizontal: { - height: 1.5, - left: "50%", - top: "50%", - transform: "translate(-50%, -50%)", - width: 1.5, - }, - vertical: { - height: 1.5, - left: "50%", - top: -1, - transform: "translateX(-50%)", - width: 1.5, - }, - } as const; - - const currentContainerStyle = containerStyles[orientation]; - const currentCircleStyle = circleStyles[orientation]; + const currentContainerStyle = CONTAINER_STYLES[orientation]; + const currentCircleStyle = CIRCLE_STYLES[orientation]; return ( (Object.values(VersionIndicatorDisplayOptions)); -export const isVersionIndicatorDisplayOption = (value: unknown): value is VersionIndicatorDisplayOption => +export const isVersionIndicatorDisplayOption = (value: unknown): value is VersionIndicatorDisplayOptions => typeof value === "string" && validOptions.has(value); export const showVersionIndicatorOptions = createListCollection({ diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index f26ff6f4a0038..4ee46df9f1681 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -50,7 +50,6 @@ import { showGanttKey, triggeringUserFilterKey, } from "src/constants/localStorage"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { HoverProvider } from "src/context/hover"; import { OpenGroupsProvider } from "src/context/openGroups"; @@ -90,8 +89,9 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { ); const [showGantt, setShowGantt] = useLocalStorage(showGanttKey(dagId), false); + // Global setting: applies to all Dags (intentionally not scoped to dagId) const [showVersionIndicatorMode, setShowVersionIndicatorMode] = - useLocalStorage( + useLocalStorage( `version_indicator_display_mode`, VersionIndicatorDisplayOptions.ALL, ); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx index 8dfe1873e1a00..752c3f255c62d 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx @@ -21,7 +21,6 @@ import { useParams, useSearchParams } from "react-router-dom"; import { RunTypeIcon } from "src/components/RunTypeIcon"; import { BundleVersionIndicator, DagVersionIndicator } from "src/components/ui/VersionIndicator"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { useHover } from "src/context/hover"; @@ -34,7 +33,7 @@ type Props = { readonly max: number; readonly onClick?: () => void; readonly run: GridRunWithVersionFlags; - readonly showVersionIndicatorMode?: VersionIndicatorDisplayOption; + readonly showVersionIndicatorMode?: VersionIndicatorDisplayOptions; }; export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => { diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx index 8da6153898a4d..203d3464442d1 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx @@ -26,7 +26,7 @@ import { FiChevronsRight } from "react-icons/fi"; import { Link, useParams, useSearchParams } from "react-router-dom"; import type { DagRunState, DagRunType, GridRunsResponse } from "openapi/requests"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; +import type { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { useOpenGroups } from "src/context/openGroups"; import { NavigationModes, useNavigation } from "src/hooks/navigation"; import { useGridRuns } from "src/queries/useGridRuns.ts"; @@ -54,7 +54,7 @@ type Props = { readonly limit: number; readonly runType?: DagRunType | undefined; readonly showGantt?: boolean; - readonly showVersionIndicatorMode?: VersionIndicatorDisplayOption; + readonly showVersionIndicatorMode?: VersionIndicatorDisplayOptions; readonly triggeringUser?: string | undefined; }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx index 78c1b586a3c27..679a17b3b4b25 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx @@ -24,7 +24,6 @@ import { useParams } from "react-router-dom"; import type { GridRunsResponse } from "openapi/requests"; import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; import { DagVersionIndicator } from "src/components/ui/VersionIndicator"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { useHover } from "src/context/hover"; import { useGridTiSummaries } from "src/queries/useGridTISummaries.ts"; @@ -36,7 +35,7 @@ type Props = { readonly nodes: Array; readonly onCellClick?: () => void; readonly run: GridRunsResponse; - readonly showVersionIndicatorMode?: VersionIndicatorDisplayOption; + readonly showVersionIndicatorMode?: VersionIndicatorDisplayOptions; readonly virtualItems?: Array; }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts index f6500f19748f8..71da7e439794d 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts @@ -19,7 +19,6 @@ import { useMemo } from "react"; import type { GridRunsResponse } from "openapi/requests"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; export type GridRunWithVersionFlags = { @@ -29,7 +28,7 @@ export type GridRunWithVersionFlags = { type UseGridRunsWithVersionFlagsParams = { gridRuns: Array | undefined; - showVersionIndicatorMode?: VersionIndicatorDisplayOption; + showVersionIndicatorMode?: VersionIndicatorDisplayOptions; }; // Hook to calculate version change flags for grid runs. diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index 703d801bafbd4..6e05c14b8aee6 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -20,9 +20,6 @@ */ import { Box, - Button, - ButtonGroup, - Circle, createListCollection, Flex, IconButton, @@ -37,7 +34,7 @@ import { useReactFlow } from "@xyflow/react"; import { useEffect, useMemo, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; -import { FiChevronDown, FiGitCommit, FiGrid } from "react-icons/fi"; +import { FiGrid } from "react-icons/fi"; import { LuKeyboard } from "react-icons/lu"; import { MdOutlineAccountTree, MdSettings } from "react-icons/md"; import type { ImperativePanelGroupHandle } from "react-resizable-panels"; @@ -54,7 +51,6 @@ import { Tooltip } from "src/components/ui"; import { type ButtonGroupOption, ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { Checkbox } from "src/components/ui/Checkbox"; import { dependenciesKey, directionKey } from "src/constants/localStorage"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; import { isVersionIndicatorDisplayOption, showVersionIndicatorOptions, @@ -66,6 +62,7 @@ import { useContainerWidth } from "src/utils/useContainerWidth"; import { DagRunSelect } from "./DagRunSelect"; import { TaskStreamFilter } from "./TaskStreamFilter"; import { ToggleGroups } from "./ToggleGroups"; +import { VersionIndicatorSelect } from "./VersionIndicatorSelect"; type Props = { readonly dagRunStateFilter: DagRunState | undefined; @@ -78,10 +75,10 @@ type Props = { readonly setLimit: React.Dispatch>; readonly setRunTypeFilter: React.Dispatch>; readonly setShowGantt: React.Dispatch>; - readonly setShowVersionIndicatorMode: React.Dispatch>; + readonly setShowVersionIndicatorMode: React.Dispatch>; readonly setTriggeringUserFilter: React.Dispatch>; readonly showGantt: boolean; - readonly showVersionIndicatorMode: VersionIndicatorDisplayOption; + readonly showVersionIndicatorMode: VersionIndicatorDisplayOptions; readonly triggeringUserFilter: string | undefined; }; @@ -207,16 +204,6 @@ export const PanelButtons = ({ setTriggeringUserFilter(trimmedValue === "" ? undefined : trimmedValue); }; - const handleShowVersionIndicatorChange = ( - event: SelectValueChangeDetails<{ label: string; value: Array }>, - ) => { - const [selectedDisplayMode] = event.value; - - if (isVersionIndicatorDisplayOption(selectedDisplayMode)) { - setShowVersionIndicatorMode(selectedDisplayMode); - } - }; - const handleFocus = (view: string) => { if (panelGroupRef.current) { const newLayout = view === "graph" ? [70, 30] : [30, 70]; @@ -495,64 +482,12 @@ export const PanelButtons = ({ ) : undefined} )} - {/* eslint-disable react/jsx-max-depth */} - - - {translate("dag:panel.showVersionIndicator.label")} - - - - - - {(showVersionIndicatorMode === VersionIndicatorDisplayOptions.BUNDLE || - showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) && ( - - )} - {(showVersionIndicatorMode === VersionIndicatorDisplayOptions.DAG || - showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) && ( - - )} - {translate( - showVersionIndicatorOptions.items.find( - (item) => item.value === showVersionIndicatorMode, - )?.label ?? "", - )} - - - - - - - - - - {showVersionIndicatorOptions.items.map((option) => ( - - - {(option.value === VersionIndicatorDisplayOptions.BUNDLE || - option.value === VersionIndicatorDisplayOptions.ALL) && ( - - )} - {(option.value === VersionIndicatorDisplayOptions.DAG || - option.value === VersionIndicatorDisplayOptions.ALL) && ( - - )} - {translate(option.label)} - - - ))} - - - + - {/* eslint-enable react/jsx-max-depth */} diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx new file mode 100644 index 0000000000000..30ec62a4d986a --- /dev/null +++ b/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx @@ -0,0 +1,97 @@ +/*! + * 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 { Circle, Flex, Select, type SelectValueChangeDetails } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { FiGitCommit } from "react-icons/fi"; + +import { + isVersionIndicatorDisplayOption, + showVersionIndicatorOptions, + VersionIndicatorDisplayOptions, +} from "src/constants/showVersionIndicatorOptions"; + +type VersionIndicatorSelectProps = { + readonly onChange: (value: VersionIndicatorDisplayOptions) => void; + readonly value: VersionIndicatorDisplayOptions; +}; + +export const VersionIndicatorSelect = ({ onChange, value }: VersionIndicatorSelectProps) => { + const { t: translate } = useTranslation(["components", "dag"]); + + const handleChange = (event: SelectValueChangeDetails<{ label: string; value: Array }>) => { + const [selectedDisplayMode] = event.value; + + if (isVersionIndicatorDisplayOption(selectedDisplayMode)) { + onChange(selectedDisplayMode); + } + }; + + return ( + + {translate("dag:panel.showVersionIndicator.label")} + + + + + {(value === VersionIndicatorDisplayOptions.BUNDLE || + value === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {(value === VersionIndicatorDisplayOptions.DAG || + value === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {translate( + showVersionIndicatorOptions.items.find((item) => item.value === value)?.label ?? "", + )} + + + + + + + + + + {showVersionIndicatorOptions.items.map((option) => ( + + + {(option.value === VersionIndicatorDisplayOptions.BUNDLE || + option.value === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {(option.value === VersionIndicatorDisplayOptions.DAG || + option.value === VersionIndicatorDisplayOptions.ALL) && ( + + )} + {translate(option.label)} + + + ))} + + + + ); +}; From 7b94ff559f4bf2cf5283e0c9db63524780f5f617 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sun, 8 Feb 2026 02:23:30 +0900 Subject: [PATCH 3/9] fix static check --- .../src/airflow/api_fastapi/core_api/services/ui/grid.py | 4 +++- .../src/airflow/ui/src/layouts/Details/PanelButtons.tsx | 1 + .../ui/src/layouts/Details/VersionIndicatorSelect.tsx | 8 ++------ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py index efc8f98f385b5..4ee93f4f10e69 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py @@ -73,7 +73,9 @@ def _get_aggs_for_node(detail): except ValueError: max_end_date = None - dag_version_numbers = [x.get("dag_version_number") for x in detail if x.get("dag_version_number") is not None] + dag_version_numbers = [ + x.get("dag_version_number") for x in detail if x.get("dag_version_number") is not None + ] dag_version_number = max(dag_version_numbers) if dag_version_numbers else None return { diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index 6e05c14b8aee6..91b44cbeec5c5 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -483,6 +483,7 @@ export const PanelButtons = ({ )} + {/* eslint-disable-next-line react/jsx-max-depth */} )} {(value === VersionIndicatorDisplayOptions.DAG || - value === VersionIndicatorDisplayOptions.ALL) && ( - - )} - {translate( - showVersionIndicatorOptions.items.find((item) => item.value === value)?.label ?? "", - )} + value === VersionIndicatorDisplayOptions.ALL) && } + {translate(showVersionIndicatorOptions.items.find((item) => item.value === value)?.label ?? "")} From b2c5396da809bbe571a7a4a96bca4e5d3ada0303 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Tue, 10 Feb 2026 03:07:48 +0900 Subject: [PATCH 4/9] Refactor VersionIndicatorSelect placement in PanelButtons component --- .../airflow/ui/src/layouts/Details/PanelButtons.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index 91b44cbeec5c5..75efc109156c2 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -480,15 +480,14 @@ export const PanelButtons = ({ ) : undefined} + + + )} - - {/* eslint-disable-next-line react/jsx-max-depth */} - - From a4b4c77f5404b1f1024472ab6c4b1c640966be90 Mon Sep 17 00:00:00 2001 From: Yeonguk Choo Date: Thu, 19 Feb 2026 15:08:41 +0900 Subject: [PATCH 5/9] Update airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts Co-authored-by: Pierre Jeambrun --- .../ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts index 71da7e439794d..13b02c5008ecc 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts @@ -48,7 +48,7 @@ export const useGridRunsWithVersionFlags = ({ } return gridRuns.map((run, index) => { - const prevRun = gridRuns[index + 1]; + const nextRun = gridRuns[index + 1]; const isBundleVersionChange = Boolean( prevRun && From dedec3161bb14c27fe766775648f7b5112075567 Mon Sep 17 00:00:00 2001 From: Yeonguk Choo Date: Thu, 19 Feb 2026 15:08:57 +0900 Subject: [PATCH 6/9] Update airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts Co-authored-by: Pierre Jeambrun --- .../airflow/ui/src/constants/showVersionIndicatorOptions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts b/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts index b22fa38455ed7..fe025afe9a32a 100644 --- a/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts +++ b/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts @@ -20,8 +20,8 @@ import { createListCollection } from "@chakra-ui/react"; export enum VersionIndicatorDisplayOptions { ALL = "all", - BUNDLE = "bundle", - DAG = "dag", + BUNDLE_VERSION = "bundle", + DAG_VERSION = "dag", NONE = "none", } From 03e92cdf7c1a45e66be3052ae36c0fff3f326223 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Fri, 20 Feb 2026 15:22:29 +0900 Subject: [PATCH 7/9] Refactor version indicator options to unify naming and improve consistency across components --- .../constants/showVersionIndicatorOptions.ts | 14 +++---- .../ui/src/layouts/Details/DetailsLayout.tsx | 11 +++--- .../ui/src/layouts/Details/Grid/Bar.tsx | 14 +++---- .../ui/src/layouts/Details/Grid/Grid.tsx | 4 +- .../Details/Grid/TaskInstancesColumn.tsx | 38 +++++++------------ .../Details/Grid}/VersionIndicator.tsx | 0 .../Grid/useGridRunsWithVersionFlags.ts | 18 ++++----- .../ui/src/layouts/Details/PanelButtons.tsx | 10 ++--- .../Details/VersionIndicatorSelect.tsx | 27 ++++++------- 9 files changed, 61 insertions(+), 75 deletions(-) rename airflow-core/src/airflow/ui/src/{components/ui => layouts/Details/Grid}/VersionIndicator.tsx (100%) diff --git a/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts b/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts index fe025afe9a32a..ceaa3c0d246b5 100644 --- a/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts +++ b/airflow-core/src/airflow/ui/src/constants/showVersionIndicatorOptions.ts @@ -18,29 +18,29 @@ */ import { createListCollection } from "@chakra-ui/react"; -export enum VersionIndicatorDisplayOptions { +export enum VersionIndicatorOptions { ALL = "all", BUNDLE_VERSION = "bundle", DAG_VERSION = "dag", NONE = "none", } -const validOptions = new Set(Object.values(VersionIndicatorDisplayOptions)); +const validOptions = new Set(Object.values(VersionIndicatorOptions)); -export const isVersionIndicatorDisplayOption = (value: unknown): value is VersionIndicatorDisplayOptions => +export const isVersionIndicatorOption = (value: unknown): value is VersionIndicatorOptions => typeof value === "string" && validOptions.has(value); export const showVersionIndicatorOptions = createListCollection({ items: [ - { label: "dag:panel.showVersionIndicator.options.showAll", value: VersionIndicatorDisplayOptions.ALL }, + { label: "dag:panel.showVersionIndicator.options.showAll", value: VersionIndicatorOptions.ALL }, { label: "dag:panel.showVersionIndicator.options.showBundleVersion", - value: VersionIndicatorDisplayOptions.BUNDLE, + value: VersionIndicatorOptions.BUNDLE_VERSION, }, { label: "dag:panel.showVersionIndicator.options.showDagVersion", - value: VersionIndicatorDisplayOptions.DAG, + value: VersionIndicatorOptions.DAG_VERSION, }, - { label: "dag:panel.showVersionIndicator.options.hideAll", value: VersionIndicatorDisplayOptions.NONE }, + { label: "dag:panel.showVersionIndicator.options.hideAll", value: VersionIndicatorOptions.NONE }, ], }); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 4ee46df9f1681..8d5f87a8da160 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -50,7 +50,7 @@ import { showGanttKey, triggeringUserFilterKey, } from "src/constants/localStorage"; -import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { HoverProvider } from "src/context/hover"; import { OpenGroupsProvider } from "src/context/openGroups"; @@ -90,11 +90,10 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { const [showGantt, setShowGantt] = useLocalStorage(showGanttKey(dagId), false); // Global setting: applies to all Dags (intentionally not scoped to dagId) - const [showVersionIndicatorMode, setShowVersionIndicatorMode] = - useLocalStorage( - `version_indicator_display_mode`, - VersionIndicatorDisplayOptions.ALL, - ); + const [showVersionIndicatorMode, setShowVersionIndicatorMode] = useLocalStorage( + `version_indicator_display_mode`, + VersionIndicatorOptions.ALL, + ); const { fitView, getZoom } = useReactFlow(); const { data: warningData } = useDagWarningServiceListDagWarnings({ dagId }); const { onClose, onOpen, open } = useDisclosure(); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx index 752c3f255c62d..71a5e7e116bfc 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx @@ -20,11 +20,11 @@ import { Flex, Box } from "@chakra-ui/react"; import { useParams, useSearchParams } from "react-router-dom"; import { RunTypeIcon } from "src/components/RunTypeIcon"; -import { BundleVersionIndicator, DagVersionIndicator } from "src/components/ui/VersionIndicator"; -import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { useHover } from "src/context/hover"; import { GridButton } from "./GridButton"; +import { BundleVersionIndicator, DagVersionIndicator } from "./VersionIndicator"; import type { GridRunWithVersionFlags } from "./useGridRunsWithVersionFlags"; const BAR_HEIGHT = 100; @@ -33,7 +33,7 @@ type Props = { readonly max: number; readonly onClick?: () => void; readonly run: GridRunWithVersionFlags; - readonly showVersionIndicatorMode?: VersionIndicatorDisplayOptions; + readonly showVersionIndicatorMode?: VersionIndicatorOptions; }; export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => { @@ -57,13 +57,13 @@ export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => { transition="background-color 0.2s" > {run.isBundleVersionChange && - (showVersionIndicatorMode === VersionIndicatorDisplayOptions.BUNDLE || - showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) ? ( + (showVersionIndicatorMode === VersionIndicatorOptions.BUNDLE_VERSION || + showVersionIndicatorMode === VersionIndicatorOptions.ALL) ? ( ) : undefined} {run.isDagVersionChange && - (showVersionIndicatorMode === VersionIndicatorDisplayOptions.DAG || - showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) ? ( + (showVersionIndicatorMode === VersionIndicatorOptions.DAG_VERSION || + showVersionIndicatorMode === VersionIndicatorOptions.ALL) ? ( ) : undefined} diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx index 203d3464442d1..517f5d9f7d4e6 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx @@ -26,7 +26,7 @@ import { FiChevronsRight } from "react-icons/fi"; import { Link, useParams, useSearchParams } from "react-router-dom"; import type { DagRunState, DagRunType, GridRunsResponse } from "openapi/requests"; -import type { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; +import type { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { useOpenGroups } from "src/context/openGroups"; import { NavigationModes, useNavigation } from "src/hooks/navigation"; import { useGridRuns } from "src/queries/useGridRuns.ts"; @@ -54,7 +54,7 @@ type Props = { readonly limit: number; readonly runType?: DagRunType | undefined; readonly showGantt?: boolean; - readonly showVersionIndicatorMode?: VersionIndicatorDisplayOptions; + readonly showVersionIndicatorMode?: VersionIndicatorOptions; readonly triggeringUser?: string | undefined; }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx index 679a17b3b4b25..7727748fba7bd 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx @@ -18,24 +18,23 @@ */ import { Box } from "@chakra-ui/react"; import type { VirtualItem } from "@tanstack/react-virtual"; -import { useMemo } from "react"; import { useParams } from "react-router-dom"; import type { GridRunsResponse } from "openapi/requests"; import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; -import { DagVersionIndicator } from "src/components/ui/VersionIndicator"; -import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { useHover } from "src/context/hover"; import { useGridTiSummaries } from "src/queries/useGridTISummaries.ts"; import { GridTI } from "./GridTI"; +import { DagVersionIndicator } from "./VersionIndicator"; import type { GridTask } from "./utils"; type Props = { readonly nodes: Array; readonly onCellClick?: () => void; readonly run: GridRunsResponse; - readonly showVersionIndicatorMode?: VersionIndicatorDisplayOptions; + readonly showVersionIndicatorMode?: VersionIndicatorOptions; readonly virtualItems?: Array; }; @@ -61,27 +60,18 @@ export const TaskInstancesColumn = ({ const itemsToRender = virtualItems ?? nodes.map((_, index) => ({ index, size: ROW_HEIGHT, start: index * ROW_HEIGHT })); - const taskInstances = useMemo( - () => gridTISummaries?.task_instances ?? [], - [gridTISummaries?.task_instances], - ); - const taskInstanceMap = useMemo(() => { - const map = new Map(); - - for (const ti of taskInstances) { - map.set(ti.task_id, ti); - } + const taskInstances = gridTISummaries?.task_instances ?? []; - return map; - }, [taskInstances]); + const taskInstanceMap = new Map(); - const hasMixedVersions = useMemo(() => { - const versionNumbers = new Set( - taskInstances.map((ti) => ti.dag_version_number).filter((vn) => vn !== null && vn !== undefined), - ); + for (const ti of taskInstances) { + taskInstanceMap.set(ti.task_id, ti); + } - return versionNumbers.size > 1; - }, [taskInstances]); + const versionNumbers = new Set( + taskInstances.map((ti) => ti.dag_version_number).filter((vn) => vn !== null && vn !== undefined), + ); + const hasMixedVersions = versionNumbers.size > 1; const isHovered = hoveredRunId === run.run_id; @@ -124,8 +114,8 @@ export const TaskInstancesColumn = ({ if ( hasMixedVersions && - (showVersionIndicatorMode === VersionIndicatorDisplayOptions.DAG || - showVersionIndicatorMode === VersionIndicatorDisplayOptions.ALL) && + (showVersionIndicatorMode === VersionIndicatorOptions.DAG_VERSION || + showVersionIndicatorMode === VersionIndicatorOptions.ALL) && idx > 0 ) { const prevVirtualItem = itemsToRender[idx - 1]; diff --git a/airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/VersionIndicator.tsx similarity index 100% rename from airflow-core/src/airflow/ui/src/components/ui/VersionIndicator.tsx rename to airflow-core/src/airflow/ui/src/layouts/Details/Grid/VersionIndicator.tsx diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts index 13b02c5008ecc..aff82e562a4ea 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts @@ -19,7 +19,7 @@ import { useMemo } from "react"; import type { GridRunsResponse } from "openapi/requests"; -import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; export type GridRunWithVersionFlags = { isBundleVersionChange: boolean; @@ -28,7 +28,7 @@ export type GridRunWithVersionFlags = { type UseGridRunsWithVersionFlagsParams = { gridRuns: Array | undefined; - showVersionIndicatorMode?: VersionIndicatorDisplayOptions; + showVersionIndicatorMode?: VersionIndicatorOptions; }; // Hook to calculate version change flags for grid runs. @@ -36,7 +36,7 @@ export const useGridRunsWithVersionFlags = ({ gridRuns, showVersionIndicatorMode, }: UseGridRunsWithVersionFlagsParams): Array | undefined => { - const isVersionIndicatorEnabled = showVersionIndicatorMode !== VersionIndicatorDisplayOptions.NONE; + const isVersionIndicatorEnabled = showVersionIndicatorMode !== VersionIndicatorOptions.NONE; return useMemo(() => { if (!gridRuns) { @@ -51,17 +51,17 @@ export const useGridRunsWithVersionFlags = ({ const nextRun = gridRuns[index + 1]; const isBundleVersionChange = Boolean( - prevRun && + nextRun && run.bundle_version !== null && - prevRun.bundle_version !== null && - run.bundle_version !== prevRun.bundle_version, + nextRun.bundle_version !== null && + run.bundle_version !== nextRun.bundle_version, ); const isDagVersionChange = Boolean( - prevRun && + nextRun && run.dag_version_number !== null && - prevRun.dag_version_number !== null && - run.dag_version_number !== prevRun.dag_version_number, + nextRun.dag_version_number !== null && + run.dag_version_number !== nextRun.dag_version_number, ); return { ...run, isBundleVersionChange, isDagVersionChange }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index 75efc109156c2..3d6a335f9d326 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -51,11 +51,7 @@ import { Tooltip } from "src/components/ui"; import { type ButtonGroupOption, ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { Checkbox } from "src/components/ui/Checkbox"; import { dependenciesKey, directionKey } from "src/constants/localStorage"; -import { - isVersionIndicatorDisplayOption, - showVersionIndicatorOptions, - VersionIndicatorDisplayOptions, -} from "src/constants/showVersionIndicatorOptions"; +import type { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { dagRunTypeOptions, dagRunStateOptions } from "src/constants/stateOptions"; import { useContainerWidth } from "src/utils/useContainerWidth"; @@ -75,10 +71,10 @@ type Props = { readonly setLimit: React.Dispatch>; readonly setRunTypeFilter: React.Dispatch>; readonly setShowGantt: React.Dispatch>; - readonly setShowVersionIndicatorMode: React.Dispatch>; + readonly setShowVersionIndicatorMode: React.Dispatch>; readonly setTriggeringUserFilter: React.Dispatch>; readonly showGantt: boolean; - readonly showVersionIndicatorMode: VersionIndicatorDisplayOptions; + readonly showVersionIndicatorMode: VersionIndicatorOptions; readonly triggeringUserFilter: string | undefined; }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx index bd9fa1f936e22..3d4cf9a51c70a 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/VersionIndicatorSelect.tsx @@ -21,14 +21,14 @@ import { useTranslation } from "react-i18next"; import { FiGitCommit } from "react-icons/fi"; import { - isVersionIndicatorDisplayOption, + isVersionIndicatorOption, showVersionIndicatorOptions, - VersionIndicatorDisplayOptions, + VersionIndicatorOptions, } from "src/constants/showVersionIndicatorOptions"; type VersionIndicatorSelectProps = { - readonly onChange: (value: VersionIndicatorDisplayOptions) => void; - readonly value: VersionIndicatorDisplayOptions; + readonly onChange: (value: VersionIndicatorOptions) => void; + readonly value: VersionIndicatorOptions; }; export const VersionIndicatorSelect = ({ onChange, value }: VersionIndicatorSelectProps) => { @@ -37,7 +37,7 @@ export const VersionIndicatorSelect = ({ onChange, value }: VersionIndicatorSele const handleChange = (event: SelectValueChangeDetails<{ label: string; value: Array }>) => { const [selectedDisplayMode] = event.value; - if (isVersionIndicatorDisplayOption(selectedDisplayMode)) { + if (isVersionIndicatorOption(selectedDisplayMode)) { onChange(selectedDisplayMode); } }; @@ -55,12 +55,13 @@ export const VersionIndicatorSelect = ({ onChange, value }: VersionIndicatorSele - {(value === VersionIndicatorDisplayOptions.BUNDLE || - value === VersionIndicatorDisplayOptions.ALL) && ( + {(value === VersionIndicatorOptions.BUNDLE_VERSION || + value === VersionIndicatorOptions.ALL) && ( )} - {(value === VersionIndicatorDisplayOptions.DAG || - value === VersionIndicatorDisplayOptions.ALL) && } + {(value === VersionIndicatorOptions.DAG_VERSION || value === VersionIndicatorOptions.ALL) && ( + + )} {translate(showVersionIndicatorOptions.items.find((item) => item.value === value)?.label ?? "")} @@ -74,12 +75,12 @@ export const VersionIndicatorSelect = ({ onChange, value }: VersionIndicatorSele {showVersionIndicatorOptions.items.map((option) => ( - {(option.value === VersionIndicatorDisplayOptions.BUNDLE || - option.value === VersionIndicatorDisplayOptions.ALL) && ( + {(option.value === VersionIndicatorOptions.BUNDLE_VERSION || + option.value === VersionIndicatorOptions.ALL) && ( )} - {(option.value === VersionIndicatorDisplayOptions.DAG || - option.value === VersionIndicatorDisplayOptions.ALL) && ( + {(option.value === VersionIndicatorOptions.DAG_VERSION || + option.value === VersionIndicatorOptions.ALL) && ( )} {translate(option.label)} From 703bd93a5c3d7ff95837542c5add5be6cfbbef83 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sun, 22 Feb 2026 00:46:01 +0900 Subject: [PATCH 8/9] Refactor GridRunsResponse to use a list of DagVersionResponse for versioning details --- .../core_api/datamodels/ui/common.py | 4 +- .../core_api/openapi/_private_ui.yaml | 16 ++--- .../api_fastapi/core_api/routes/ui/grid.py | 60 +++++++++--------- .../ui/openapi-gen/requests/schemas.gen.ts | 30 +++------ .../ui/openapi-gen/requests/types.gen.ts | 3 +- .../ui/src/layouts/Details/Grid/Bar.tsx | 13 ++-- .../layouts/Details/Grid/VersionIndicator.tsx | 28 +++++++-- .../ui/src/layouts/Details/Grid/constants.ts | 10 ++- .../Grid/useGridRunsWithVersionFlags.ts | 28 +++++---- .../core_api/routes/ui/test_grid.py | 62 ++++++++++++++----- 10 files changed, 149 insertions(+), 105 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py index 05467eda10c0a..86dd7d74bd23d 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py @@ -24,6 +24,7 @@ from airflow._shared.timezones import timezone from airflow.api_fastapi.core_api.base import BaseModel +from airflow.api_fastapi.core_api.datamodels.dag_versions import DagVersionResponse from airflow.utils.state import DagRunState from airflow.utils.types import DagRunType @@ -79,9 +80,8 @@ class GridRunsResponse(BaseModel): run_after: datetime state: DagRunState | None run_type: DagRunType + dag_versions: list[DagVersionResponse] = [] has_missed_deadline: bool - bundle_version: str | None = None - dag_version_number: int | None = None @computed_field def duration(self) -> float: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml index fa6a1d8e48092..e8c97b6cac773 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml @@ -2183,19 +2183,15 @@ components: - type: 'null' run_type: $ref: '#/components/schemas/DagRunType' + dag_versions: + items: + $ref: '#/components/schemas/DagVersionResponse' + type: array + title: Dag Versions + default: [] has_missed_deadline: type: boolean title: Has Missed Deadline - bundle_version: - anyOf: - - type: string - - type: 'null' - title: Bundle Version - dag_version_number: - anyOf: - - type: integer - - type: 'null' - title: Dag Version Number duration: type: number title: Duration diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py index 59f962b5d4a40..a23fee49d76de 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py @@ -22,8 +22,8 @@ import structlog from fastapi import Depends, HTTPException, status -from sqlalchemy import exists, func, select -from sqlalchemy.orm import joinedload +from sqlalchemy import exists, select +from sqlalchemy.orm import joinedload, load_only, noload, selectinload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity from airflow.api_fastapi.common.db.common import SessionDep, paginated_select @@ -58,6 +58,7 @@ get_task_group_children_getter, task_group_to_dict_grid, ) +from airflow.models.dag import DagModel from airflow.models.dag_version import DagVersion from airflow.models.dagrun import DagRun from airflow.models.deadline import Deadline @@ -282,34 +283,29 @@ def get_grid_runs( .correlate(DagRun) .label("has_missed_deadline") ) - # get the highest dag_version_number from TIs for each run - latest_ti_version = ( - select( - TaskInstance.run_id, - func.max(DagVersion.version_number).label("version_number"), - ) - .join(DagVersion, TaskInstance.dag_version_id == DagVersion.id) - .where(TaskInstance.dag_id == dag_id) - .group_by(TaskInstance.run_id) - .subquery() - ) - base_query = ( - select( - DagRun.dag_id, - DagRun.run_id, - DagRun.queued_at, - DagRun.start_date, - DagRun.end_date, - DagRun.run_after, - DagRun.state, - DagRun.run_type, - has_missed_deadline, - DagRun.bundle_version, - latest_ti_version.c.version_number.label("dag_version_number"), - ) - .outerjoin(latest_ti_version, DagRun.run_id == latest_ti_version.c.run_id) + select(DagRun, has_missed_deadline) .where(DagRun.dag_id == dag_id) + .options( + load_only( + DagRun.dag_id, + DagRun.run_id, + DagRun.queued_at, + DagRun.start_date, + DagRun.end_date, + DagRun.run_after, + DagRun.state, + DagRun.run_type, + DagRun.bundle_version, + ), + joinedload(DagRun.dag_model).load_only(DagModel._dag_display_property_value), + joinedload(DagRun.created_dag_version).joinedload(DagVersion.bundle), + selectinload(DagRun.task_instances) + .load_only(TaskInstance.dag_version_id) + .joinedload(TaskInstance.dag_version) + .joinedload(DagVersion.bundle), + noload(DagRun.task_instances_histories), + ) ) # This comparison is to fall back to DAG timetable when no order_by is provided @@ -327,8 +323,14 @@ def get_grid_runs( offset=offset, filters=[run_after, run_type, state, triggering_user], limit=limit, + return_total_entries=False, ) - return [GridRunsResponse(**row._mapping) for row in session.execute(dag_runs_select_filter)] + results = session.execute(dag_runs_select_filter).unique().all() + grid_runs = [] + for run, has_missed in results: + run.has_missed_deadline = has_missed + grid_runs.append(GridRunsResponse.model_validate(run, from_attributes=True)) + return grid_runs @grid_router.get( diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts index 4a27bce51c5e3..e54f4579caadf 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts @@ -8143,32 +8143,18 @@ export const $GridRunsResponse = { run_type: { '$ref': '#/components/schemas/DagRunType' }, + dag_versions: { + items: { + '$ref': '#/components/schemas/DagVersionResponse' + }, + type: 'array', + title: 'Dag Versions', + default: [] + }, has_missed_deadline: { type: 'boolean', title: 'Has Missed Deadline' }, - bundle_version: { - anyOf: [ - { - type: 'string' - }, - { - type: 'null' - } - ], - title: 'Bundle Version' - }, - dag_version_number: { - anyOf: [ - { - type: 'integer' - }, - { - type: 'null' - } - ], - title: 'Dag Version Number' - }, duration: { type: 'number', title: 'Duration', diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts index c8e278b4ed553..79387a6c3f56e 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts @@ -1999,9 +1999,8 @@ export type GridRunsResponse = { run_after: string; state: DagRunState | null; run_type: DagRunType; + dag_versions?: Array; has_missed_deadline: boolean; - bundle_version?: string | null; - dag_version_number?: number | null; readonly duration: number; }; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx index 71a5e7e116bfc..c2bb3ed6d04b3 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx @@ -25,9 +25,12 @@ import { useHover } from "src/context/hover"; import { GridButton } from "./GridButton"; import { BundleVersionIndicator, DagVersionIndicator } from "./VersionIndicator"; -import type { GridRunWithVersionFlags } from "./useGridRunsWithVersionFlags"; - -const BAR_HEIGHT = 100; +import { BAR_HEIGHT } from "./constants"; +import { + getBundleVersion, + getMaxVersionNumber, + type GridRunWithVersionFlags, +} from "./useGridRunsWithVersionFlags"; type Props = { readonly max: number; @@ -59,12 +62,12 @@ export const Bar = ({ max, onClick, run, showVersionIndicatorMode }: Props) => { {run.isBundleVersionChange && (showVersionIndicatorMode === VersionIndicatorOptions.BUNDLE_VERSION || showVersionIndicatorMode === VersionIndicatorOptions.ALL) ? ( - + ) : undefined} {run.isDagVersionChange && (showVersionIndicatorMode === VersionIndicatorOptions.DAG_VERSION || showVersionIndicatorMode === VersionIndicatorOptions.ALL) ? ( - + ) : undefined} { @@ -31,8 +39,14 @@ export const BundleVersionIndicator = ({ bundleVersion }: BundleVersionIndicator return ( - - + + ); @@ -47,7 +61,7 @@ const CONTAINER_STYLES = { width: 4.5, }, vertical: { - height: 104, + height: DAG_VERSION_INDICATOR_HEIGHT, left: -1.25, top: -1.5, width: 0.5, @@ -89,7 +103,7 @@ export const DagVersionIndicator = ({ aria-label={`Version ${dagVersionNumber} indicator`} as="output" position="absolute" - zIndex={1} + zIndex={VERSION_INDICATOR_Z_INDEX} {...currentContainerStyle} > diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts index 7286818def786..99bd035896de0 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts @@ -20,7 +20,7 @@ // Grid layout constants - shared between Grid and Gantt for alignment export const ROW_HEIGHT = 20; export const GRID_OUTER_PADDING_PX = 64; // pt={16} = 16 * 4 = 64px -export const GRID_HEADER_PADDING_PX = 8; // pt={2} = 2 * 4 = 8px +export const GRID_HEADER_PADDING_PX = 16; // pt={4} = 4 * 4 = 16px export const GRID_HEADER_HEIGHT_PX = 100; // height="100px" for duration bars // Gantt chart's x-axis height (time labels at top of chart) @@ -30,3 +30,11 @@ export const GANTT_AXIS_HEIGHT_PX = 36; // minus the Gantt axis height since the chart includes its own top axis export const GRID_BODY_OFFSET_PX = GRID_OUTER_PADDING_PX + GRID_HEADER_PADDING_PX + GRID_HEADER_HEIGHT_PX - GANTT_AXIS_HEIGHT_PX; + +// Version indicator constants +export const BAR_HEIGHT = GRID_HEADER_HEIGHT_PX; // Duration bar height matches grid header +export const BUNDLE_VERSION_INDICATOR_TOP = 93; // Position from top for bundle version icon +export const BUNDLE_VERSION_INDICATOR_LEFT = -2; // Position from left for bundle version icon +export const BUNDLE_VERSION_ICON_SIZE = 15; // Size of the git commit icon +export const DAG_VERSION_INDICATOR_HEIGHT = 104; // Height of the vertical line indicator +export const VERSION_INDICATOR_Z_INDEX = 1; // Z-index for version indicators diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts index aff82e562a4ea..e60012e58ba80 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridRunsWithVersionFlags.ts @@ -31,6 +31,12 @@ type UseGridRunsWithVersionFlagsParams = { showVersionIndicatorMode?: VersionIndicatorOptions; }; +export const getMaxVersionNumber = (run: GridRunsResponse): number | undefined => + run.dag_versions?.at(-1)?.version_number; + +export const getBundleVersion = (run: GridRunsResponse): string | null | undefined => + run.dag_versions?.at(-1)?.bundle_version; + // Hook to calculate version change flags for grid runs. export const useGridRunsWithVersionFlags = ({ gridRuns, @@ -50,19 +56,17 @@ export const useGridRunsWithVersionFlags = ({ return gridRuns.map((run, index) => { const nextRun = gridRuns[index + 1]; - const isBundleVersionChange = Boolean( - nextRun && - run.bundle_version !== null && - nextRun.bundle_version !== null && - run.bundle_version !== nextRun.bundle_version, - ); + const currentBundleVersion = getBundleVersion(run); + const nextBundleVersion = nextRun ? getBundleVersion(nextRun) : undefined; + const isBundleVersionChange = + currentBundleVersion !== undefined && + nextBundleVersion !== undefined && + currentBundleVersion !== nextBundleVersion; - const isDagVersionChange = Boolean( - nextRun && - run.dag_version_number !== null && - nextRun.dag_version_number !== null && - run.dag_version_number !== nextRun.dag_version_number, - ); + const currentVersion = getMaxVersionNumber(run); + const nextVersion = nextRun ? getMaxVersionNumber(nextRun) : undefined; + const isDagVersionChange = + currentVersion !== undefined && nextVersion !== undefined && currentVersion !== nextVersion; return { ...run, isBundleVersionChange, isDagVersionChange }; }); diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py index 7eaafb5d63e12..c9642f7c4922e 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py @@ -61,7 +61,15 @@ GRID_RUN_1 = { "dag_id": "test_dag", - "dag_version_number": 1, + "dag_versions": [ + { + "version_number": 1, + "dag_id": "test_dag", + "bundle_name": "dag_maker", + "created_at": "2024-12-31T00:00:00Z", + "dag_display_name": "test_dag", + } + ], "duration": 283996800.0, "end_date": "2024-12-31T00:00:00Z", "has_missed_deadline": False, @@ -74,7 +82,15 @@ GRID_RUN_2 = { "dag_id": "test_dag", - "dag_version_number": 1, + "dag_versions": [ + { + "version_number": 1, + "dag_id": "test_dag", + "bundle_name": "dag_maker", + "created_at": "2024-12-31T00:00:00Z", + "dag_display_name": "test_dag", + } + ], "duration": 283996800.0, "end_date": "2024-12-31T00:00:00Z", "has_missed_deadline": False, @@ -85,6 +101,18 @@ "state": "failed", } + +def _strip_dag_version_ids(data): + """Strip dynamic `id` fields from dag_versions for deterministic comparison.""" + if isinstance(data, list): + return [_strip_dag_version_ids(item) for item in data] + if isinstance(data, dict) and "dag_versions" in data: + result = dict(data) + result["dag_versions"] = [{k: v for k, v in dv.items() if k != "id"} for dv in result["dag_versions"]] + return result + return data + + GRID_NODES = [ { "children": [{"id": "mapped_task_group.subtask", "is_mapped": True, "label": "subtask"}], @@ -359,10 +387,10 @@ def _freeze_time_for_dagruns(time_machine): @pytest.mark.usefixtures("_freeze_time_for_dagruns") class TestGetGridDataEndpoint: def test_should_response_200(self, test_client): - with assert_queries_count(5): + with assert_queries_count(6): response = test_client.get(f"/grid/runs/{DAG_ID}") assert response.status_code == 200 - assert response.json() == [ + assert _strip_dag_version_ids(response.json()) == [ GRID_RUN_1, GRID_RUN_2, ] @@ -401,10 +429,10 @@ def test_should_response_200(self, test_client): ], ) def test_should_response_200_order_by(self, test_client, order_by, expected): - with assert_queries_count(5): + with assert_queries_count(6): response = test_client.get(f"/grid/runs/{DAG_ID}", params={"order_by": order_by}) assert response.status_code == 200 - assert response.json() == expected + assert _strip_dag_version_ids(response.json()) == expected @pytest.mark.parametrize( ("limit", "expected"), @@ -420,10 +448,10 @@ def test_should_response_200_order_by(self, test_client, order_by, expected): ], ) def test_should_response_200_limit(self, test_client, limit, expected): - with assert_queries_count(5): + with assert_queries_count(6): response = test_client.get(f"/grid/runs/{DAG_ID}", params={"limit": limit}) assert response.status_code == 200 - assert response.json() == expected + assert _strip_dag_version_ids(response.json()) == expected @pytest.mark.parametrize( ("params", "expected"), @@ -445,13 +473,13 @@ def test_should_response_200_limit(self, test_client, limit, expected): ], ) def test_runs_should_response_200_date_filters(self, test_client, params, expected): - with assert_queries_count(5): + with assert_queries_count(6): response = test_client.get( f"/grid/runs/{DAG_ID}", params=params, ) assert response.status_code == 200 - assert response.json() == expected + assert _strip_dag_version_ids(response.json()) == expected @pytest.mark.parametrize( ("params", "expected", "expected_queries_count"), @@ -607,10 +635,10 @@ def test_get_dag_structure(self, session, test_client): def test_get_grid_runs(self, session, test_client): session.commit() - with assert_queries_count(5): + with assert_queries_count(6): response = test_client.get(f"/grid/runs/{DAG_ID}?limit=5") assert response.status_code == 200 - assert response.json() == [GRID_RUN_1, GRID_RUN_2] + assert _strip_dag_version_ids(response.json()) == [GRID_RUN_1, GRID_RUN_2] @pytest.mark.parametrize( ("endpoint", "run_type", "expected"), @@ -625,7 +653,7 @@ def test_filter_by_run_type(self, session, test_client, endpoint, run_type, expe session.commit() response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?run_type={run_type}") assert response.status_code == 200 - assert response.json() == expected + assert _strip_dag_version_ids(response.json()) == expected @pytest.mark.parametrize( ("endpoint", "triggering_user", "expected"), @@ -639,14 +667,14 @@ def test_filter_by_triggering_user(self, session, test_client, endpoint, trigger session.commit() response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?triggering_user={triggering_user}") assert response.status_code == 200 - assert response.json() == expected + assert _strip_dag_version_ids(response.json()) == expected def test_get_grid_runs_filter_by_run_type_and_triggering_user(self, session, test_client): session.commit() - with assert_queries_count(5): + with assert_queries_count(6): response = test_client.get(f"/grid/runs/{DAG_ID}?run_type=manual&triggering_user=user2") assert response.status_code == 200 - assert response.json() == [GRID_RUN_2] + assert _strip_dag_version_ids(response.json()) == [GRID_RUN_2] @pytest.mark.parametrize( ("endpoint", "state", "expected"), @@ -662,7 +690,7 @@ def test_filter_by_state(self, session, test_client, endpoint, state, expected): session.commit() response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?state={state}") assert response.status_code == 200 - assert response.json() == expected + assert _strip_dag_version_ids(response.json()) == expected def test_grid_ti_summaries_group(self, session, test_client): run_id = "run_4-1" From 709870c165b68b262b34efc4844d62c715eae4b2 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Tue, 24 Feb 2026 15:10:19 +0900 Subject: [PATCH 9/9] Refactor version indicator handling to improve consistency and clarity in local storage and component usage --- .../src/airflow/api_fastapi/core_api/routes/ui/grid.py | 8 ++++++-- airflow-core/src/airflow/ui/src/constants/localStorage.ts | 1 + .../src/airflow/ui/src/layouts/Details/DetailsLayout.tsx | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py index a23fee49d76de..ca363c3adab02 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py @@ -23,7 +23,7 @@ import structlog from fastapi import Depends, HTTPException, status from sqlalchemy import exists, select -from sqlalchemy.orm import joinedload, load_only, noload, selectinload +from sqlalchemy.orm import joinedload, load_only, selectinload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity from airflow.api_fastapi.common.db.common import SessionDep, paginated_select @@ -64,6 +64,7 @@ from airflow.models.deadline import Deadline from airflow.models.serialized_dag import SerializedDagModel from airflow.models.taskinstance import TaskInstance +from airflow.models.taskinstancehistory import TaskInstanceHistory log = structlog.get_logger(logger_name=__name__) grid_router = AirflowRouter(prefix="/grid", tags=["Grid"]) @@ -304,7 +305,10 @@ def get_grid_runs( .load_only(TaskInstance.dag_version_id) .joinedload(TaskInstance.dag_version) .joinedload(DagVersion.bundle), - noload(DagRun.task_instances_histories), + selectinload(DagRun.task_instances_histories) + .load_only(TaskInstanceHistory.dag_version_id) + .joinedload(TaskInstanceHistory.dag_version) + .joinedload(DagVersion.bundle), ) ) diff --git a/airflow-core/src/airflow/ui/src/constants/localStorage.ts b/airflow-core/src/airflow/ui/src/constants/localStorage.ts index 46482baddd369..72fd47b0909c5 100644 --- a/airflow-core/src/airflow/ui/src/constants/localStorage.ts +++ b/airflow-core/src/airflow/ui/src/constants/localStorage.ts @@ -26,6 +26,7 @@ export const CALENDAR_VIEW_MODE_KEY = "calendar-view-mode"; export const LOG_WRAP_KEY = "log_wrap"; export const LOG_SHOW_TIMESTAMP_KEY = "log_show_timestamp"; export const LOG_SHOW_SOURCE_KEY = "log_show_source"; +export const VERSION_INDICATOR_DISPLAY_MODE_KEY = "version_indicator_display_mode"; // Dag-scoped keys export const dagViewKey = (dagId: string) => `dag_view-${dagId}`; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 8d5f87a8da160..71635ea6dbf1d 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -1,3 +1,5 @@ +/* eslint-disable max-lines */ + /*! * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file