From f48e7ec79f22ea2fb18d48289f44688a5b117b6d Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sat, 10 Jan 2026 15:12:31 +0900 Subject: [PATCH 1/4] 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 | 44 +++++-- .../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 | 82 +++++++++++- .../core_api/routes/ui/test_grid.py | 42 +++--- 17 files changed, 554 insertions(+), 50 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 a18042b4960f8..7dd1d0e0ef9ec 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 @@ -79,6 +79,8 @@ class GridRunsResponse(BaseModel): run_after: datetime state: DagRunState | None run_type: DagRunType + 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 b38c203825c53..5c1d5cecf36cf 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 @@ -31,6 +31,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 3eae26be31d68..8cf22ce7c92f7 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 @@ -1954,6 +1954,16 @@ components: - type: 'null' run_type: $ref: '#/components/schemas/DagRunType' + 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 @@ -2217,6 +2227,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 9c8a4e1782066..214621f01d448 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 select +from sqlalchemy import func, select from sqlalchemy.orm import joinedload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity @@ -255,17 +255,34 @@ def get_grid_runs( triggering_user: QueryDagRunTriggeringUserSearch, ) -> list[GridRunsResponse]: """Get info about a run for the grid.""" - # Retrieve, sort the previous DAG Runs - 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, - ).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, + 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()]: @@ -336,7 +353,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, @@ -359,6 +378,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 3b01f02b91630..3f869f5035440 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 @@ -69,11 +69,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 b44c2883a9551..3a8bbd82b4a74 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 @@ -7894,6 +7894,28 @@ export const $GridRunsResponse = { run_type: { '$ref': '#/components/schemas/DagRunType' }, + 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', @@ -8001,6 +8023,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 945062547ad96..747a9aeea1249 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 @@ -1944,6 +1944,8 @@ export type GridRunsResponse = { run_after: string; state: DagRunState | null; run_type: DagRunType; + bundle_version?: string | null; + dag_version_number?: number | null; readonly duration: number; }; @@ -1976,6 +1978,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 f101d9eb044c9..0dbd7bd94203c 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 @@ -115,6 +115,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 58497aa05d1b7..252a9ba5b3def 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -42,6 +42,8 @@ import { ProgressBar } from "src/components/ui"; import { Toaster } from "src/components/ui"; import ActionButton from "src/components/ui/ActionButton"; import { Tooltip } from "src/components/ui/Tooltip"; +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"; @@ -80,6 +82,11 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { ); const [showGantt, setShowGantt] = useLocalStorage(`show_gantt-${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(); @@ -150,8 +157,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" ? ( @@ -163,6 +172,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); @@ -100,7 +110,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, @@ -159,8 +175,14 @@ export const Grid = ({ dagRunState, limit, runType, showGantt, triggeringUser }: - {gridRuns?.map((dr: GridRunsResponse) => ( - + {runsWithVersionFlags?.map((dr) => ( + ))} {selectedIsVisible === undefined || !selectedIsVisible ? undefined : ( @@ -195,6 +217,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 4b05e40032a88..bc63b57aed8aa 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 { data: gridTISummaries } = useGridTiSummaries({ dagId, runId: run.run_id, state: run.state }); const { hoveredRunId, setHoveredRunId } = useHover(); @@ -45,12 +56,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 isSelected = runId === run.run_id; const isHovered = hoveredRunId === run.run_id; @@ -67,7 +93,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) { @@ -90,6 +116,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 f1fdaaf428ef3..a31b0972d6d37 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -22,6 +22,7 @@ import { Box, Button, ButtonGroup, + Circle, createListCollection, Flex, IconButton, @@ -36,7 +37,7 @@ import { useReactFlow } from "@xyflow/react"; import { useEffect, useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { useTranslation } from "react-i18next"; -import { FiChevronDown, FiGrid } from "react-icons/fi"; +import { FiChevronDown, FiGitCommit, FiGrid } from "react-icons/fi"; import { LuKeyboard } from "react-icons/lu"; import { MdOutlineAccountTree } from "react-icons/md"; import type { ImperativePanelGroupHandle } from "react-resizable-panels"; @@ -51,6 +52,12 @@ import { SearchBar } from "src/components/SearchBar"; import { StateBadge } from "src/components/StateBadge"; import { Tooltip } from "src/components/ui"; import { Checkbox } from "src/components/ui/Checkbox"; +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 +76,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 +127,10 @@ export const PanelButtons = ({ setLimit, setRunTypeFilter, setShowGantt, + setShowVersionIndicatorMode, setTriggeringUserFilter, showGantt, + showVersionIndicatorMode, triggeringUserFilter, }: Props) => { const { t: translate } = useTranslation(["components", "dag"]); @@ -131,6 +142,7 @@ export const PanelButtons = ({ "tasks", ); const [direction, setDirection] = useLocalStorage(`direction-${dagId}`, "RIGHT"); + const containerRef = useRef(null); const containerWidth = useContainerWidth(containerRef); const handleLimitChange = (event: SelectValueChangeDetails<{ label: string; value: Array }>) => { @@ -194,6 +206,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]; @@ -476,6 +498,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 7fbb81d1fa214..e739a9dd62cf1 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 @@ -60,6 +60,7 @@ GRID_RUN_1 = { "dag_id": "test_dag", + "dag_version_number": 1, "duration": 283996800.0, "end_date": "2024-12-31T00:00:00Z", "run_after": "2024-11-30T00:00:00Z", @@ -71,6 +72,7 @@ GRID_RUN_2 = { "dag_id": "test_dag", + "dag_version_number": 1, "duration": 283996800.0, "end_date": "2024-12-31T00:00:00Z", "run_after": "2024-11-30T00:00:00Z", @@ -605,28 +607,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", - "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", - "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"), @@ -696,6 +677,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "t1", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, @@ -703,6 +685,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "t2", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, @@ -710,11 +693,13 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "t7", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, { "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", @@ -724,11 +709,13 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "task_group-1.t6", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, { "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", @@ -738,6 +725,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "task_group-1.task_group-2.t3", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, @@ -745,6 +733,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "task_group-1.task_group-2.t4", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, @@ -752,6 +741,7 @@ def test_grid_ti_summaries_group(self, session, test_client): "state": "success", "task_id": "task_group-1.task_group-2.t5", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, @@ -783,6 +773,7 @@ def sort_dict(in_dict): expected = [ { "child_states": {"None": 1}, + "dag_version_number": 1, "task_id": "mapped_task_2", "max_end_date": None, "min_start_date": None, @@ -790,6 +781,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", @@ -799,6 +791,7 @@ def sort_dict(in_dict): "state": "running", "task_id": "mapped_task_group.subtask", "child_states": None, + "dag_version_number": 1, "max_end_date": None, "min_start_date": None, }, @@ -806,11 +799,13 @@ def sort_dict(in_dict): "state": "success", "task_id": "task", "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", "max_end_date": None, "min_start_date": None, @@ -818,6 +813,7 @@ def sort_dict(in_dict): }, { "child_states": {"None": 2}, + "dag_version_number": 1, "task_id": "task_group.inner_task_group", "max_end_date": None, "min_start_date": None, @@ -825,6 +821,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", "max_end_date": None, "min_start_date": None, @@ -832,6 +829,7 @@ def sort_dict(in_dict): }, { "child_states": {"None": 4}, + "dag_version_number": 1, "task_id": "task_group.mapped_task", "max_end_date": None, "min_start_date": None, From cf678a84ef3bba9929b93d937e45577b7851491e Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sun, 8 Feb 2026 00:09:46 +0900 Subject: [PATCH 2/4] 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 | 85 ++-------------- .../Details/VersionIndicatorSelect.tsx | 97 +++++++++++++++++++ 10 files changed, 151 insertions(+), 126 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 1b47a1e37ab92..89cb668f4e417 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 @@ -70,7 +70,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 471524b0149ce..33e8255d512db 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -41,7 +41,6 @@ import { TriggerDAGButton } from "src/components/TriggerDag/TriggerDAGButton"; import { ProgressBar } from "src/components/ui"; import { Toaster } from "src/components/ui"; import { Tooltip } from "src/components/ui/Tooltip"; -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"; @@ -81,8 +80,9 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { ); const [showGantt, setShowGantt] = useLocalStorage(`show_gantt-${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 bc63b57aed8aa..2001f9eb5f26f 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 ddc24f56935a0..558f9564132f0 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -20,7 +20,6 @@ */ import { Box, - Circle, createListCollection, Flex, IconButton, @@ -35,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 { 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"; @@ -51,18 +50,14 @@ import { StateBadge } from "src/components/StateBadge"; import { Tooltip } from "src/components/ui"; import { type ButtonGroupOption, ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { Checkbox } from "src/components/ui/Checkbox"; -import type { VersionIndicatorDisplayOption } from "src/constants/showVersionIndicatorOptions"; -import { - isVersionIndicatorDisplayOption, - showVersionIndicatorOptions, - VersionIndicatorDisplayOptions, -} from "src/constants/showVersionIndicatorOptions"; +import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { dagRunTypeOptions, dagRunStateOptions } from "src/constants/stateOptions"; 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; @@ -75,10 +70,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; }; @@ -205,16 +200,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]; @@ -493,64 +478,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 fdfccb9d2bde201d3d74d46b08fca3955473a550 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sun, 8 Feb 2026 02:23:30 +0900 Subject: [PATCH 3/4] fix static check --- .../src/airflow/api_fastapi/core_api/services/ui/grid.py | 4 +++- .../src/airflow/ui/src/layouts/Details/PanelButtons.tsx | 3 ++- .../ui/src/layouts/Details/VersionIndicatorSelect.tsx | 8 ++------ 3 files changed, 7 insertions(+), 8 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 89cb668f4e417..d931464953dc2 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 @@ -70,7 +70,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 558f9564132f0..5f7a107a14e71 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -50,7 +50,7 @@ import { StateBadge } from "src/components/StateBadge"; import { Tooltip } from "src/components/ui"; import { type ButtonGroupOption, ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import { Checkbox } from "src/components/ui/Checkbox"; -import { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; +import type { VersionIndicatorDisplayOptions } from "src/constants/showVersionIndicatorOptions"; import { dagRunTypeOptions, dagRunStateOptions } from "src/constants/stateOptions"; import { useContainerWidth } from "src/utils/useContainerWidth"; @@ -479,6 +479,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 8d3d8622111c9b1d06f0d90f90d0b4638b1c0f86 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Tue, 10 Feb 2026 03:07:48 +0900 Subject: [PATCH 4/4] 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 5f7a107a14e71..457f8f8178b6a 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -476,15 +476,14 @@ export const PanelButtons = ({ ) : undefined} + + + )} - - {/* eslint-disable-next-line react/jsx-max-depth */} - -