diff --git a/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx b/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx new file mode 100644 index 0000000000000..b1f608a445ddb --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/Graph/DirectionDropdown.tsx @@ -0,0 +1,78 @@ +/*! + * 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, Select, type SelectValueChangeDetails } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { useLocalStorage } from "usehooks-ts"; + +import { directionKey } from "src/constants/localStorage"; + +export type Direction = "DOWN" | "LEFT" | "RIGHT" | "UP"; + +export const DirectionDropdown = ({ graphId }: { readonly graphId: string }) => { + const { t: translate } = useTranslation(["components", "dag"]); + + const [direction, setDirection] = useLocalStorage(directionKey(graphId), "RIGHT"); + + const directionOptions = () => + createListCollection({ + items: [ + { label: translate("graph.directionRight"), value: "RIGHT" as Direction }, + { label: translate("graph.directionLeft"), value: "LEFT" as Direction }, + { label: translate("graph.directionUp"), value: "UP" as Direction }, + { label: translate("graph.directionDown"), value: "DOWN" as Direction }, + ], + }); + + const handleDirectionUpdate = ( + event: SelectValueChangeDetails<{ label: string; value: Array }>, + ) => { + if (event.value[0] !== undefined) { + setDirection(event.value[0] as Direction); + } + }; + + return ( + + {translate("dag:panel.graphDirection.label")} + + + + + + + + + + + {directionOptions().items.map((option) => ( + + {option.label} + + ))} + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/Graph/elkGraphUtils.ts b/airflow-core/src/airflow/ui/src/components/Graph/elkGraphUtils.ts index 0ab685cf37e84..b307482ba75dc 100644 --- a/airflow-core/src/airflow/ui/src/components/Graph/elkGraphUtils.ts +++ b/airflow-core/src/airflow/ui/src/components/Graph/elkGraphUtils.ts @@ -20,7 +20,7 @@ import type { ElkNode, ElkExtendedEdge, ElkShape } from "elkjs"; import type { EdgeResponse, NodeResponse } from "openapi/requests/types.gen"; -import type { Direction } from "./useGraphLayout"; +import type { Direction } from "./DirectionDropdown"; // --------------------------------------------------------------------------- // Types diff --git a/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts b/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts index 9ffeb0fb673fb..6d989e4f606c9 100644 --- a/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts +++ b/airflow-core/src/airflow/ui/src/components/Graph/useGraphLayout.ts @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { createListCollection } from "@chakra-ui/react"; import { useQuery } from "@tanstack/react-query"; import ELK, { type ElkNode } from "elkjs"; // ?raw imports the file content as a plain string without any transformation. @@ -26,10 +25,10 @@ import ELK, { type ElkNode } from "elkjs"; // URL-based worker approaches (?worker, ?worker&inline, new URL()) resolve to // the Vite origin which browsers reject for Workers. import ElkWorkerSource from "elkjs/lib/elk-worker.min.js?raw"; -import type { TFunction } from "i18next"; import type { NodeResponse, StructureDataResponse } from "openapi/requests/types.gen"; +import type { Direction } from "./DirectionDropdown"; import { generateElkGraph } from "./elkGraphUtils"; import { flattenGraph, formatFlowEdges } from "./reactflowUtils"; @@ -43,17 +42,6 @@ const elk = new ELK({ workerFactory: () => new Worker(elkWorkerBlobUrl, { type: "classic" }), }); -export type Direction = "DOWN" | "LEFT" | "RIGHT" | "UP"; -export const directionOptions = (translate: TFunction) => - createListCollection({ - items: [ - { label: translate("graph.directionRight"), value: "RIGHT" as Direction }, - { label: translate("graph.directionLeft"), value: "LEFT" as Direction }, - { label: translate("graph.directionUp"), value: "UP" as Direction }, - { label: translate("graph.directionDown"), value: "DOWN" as Direction }, - ], - }); - export type LayoutNode = ElkNode & NodeResponse; type LayoutProps = { diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx index 0cfb3e0d8e274..8c2ff6574530d 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Graph/Graph.tsx @@ -24,10 +24,11 @@ import { useParams } from "react-router-dom"; import { useLocalStorage } from "usehooks-ts"; import { useDagRunServiceGetDagRun, useStructureServiceStructureData } from "openapi/queries"; +import type { Direction } from "src/components/Graph/DirectionDropdown"; import { DownloadButton } from "src/components/Graph/DownloadButton"; import { edgeTypes, nodeTypes } from "src/components/Graph/graphTypes"; import type { CustomNodeProps } from "src/components/Graph/reactflowUtils"; -import { type Direction, useGraphLayout } from "src/components/Graph/useGraphLayout"; +import { useGraphLayout } from "src/components/Graph/useGraphLayout"; import { dependenciesKey, directionKey } from "src/constants/localStorage"; import { useColorMode } from "src/context/colorMode"; import { useGroups } from "src/context/groups"; 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 b8d825103393b..9bc1f7beeac2a 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -42,12 +42,12 @@ import { useParams } from "react-router-dom"; import { useLocalStorage } from "usehooks-ts"; import { DagVersionSelect } from "src/components/DagVersionSelect"; -import { directionOptions, type Direction } from "src/components/Graph/useGraphLayout"; +import { DirectionDropdown } from "src/components/Graph/DirectionDropdown"; import { GraphTaskFilters } from "src/components/GraphTaskFilters"; import { Tooltip } from "src/components/ui"; import { type ButtonGroupOption, ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; import type { DagView } from "src/constants/dagView"; -import { dependenciesKey, directionKey } from "src/constants/localStorage"; +import { dependenciesKey } from "src/constants/localStorage"; import type { VersionIndicatorOptions } from "src/constants/showVersionIndicatorOptions"; import { useContainerWidth } from "src/utils/useContainerWidth"; @@ -118,7 +118,6 @@ export const PanelButtons = ({ dependenciesKey(dagId), "tasks", ); - const [direction, setDirection] = useLocalStorage(directionKey(dagId), "RIGHT"); const containerRef = useRef(null); const containerWidth = useContainerWidth(containerRef); const handleLimitChange = (event: SelectValueChangeDetails<{ label: string; value: Array }>) => { @@ -148,14 +147,6 @@ export const PanelButtons = ({ } }; - const handleDirectionUpdate = ( - event: SelectValueChangeDetails<{ label: string; value: Array }>, - ) => { - if (event.value[0] !== undefined) { - setDirection(event.value[0] as Direction); - } - }; - const handleFocus = (view: string) => { if (panelGroupRef.current) { const newLayout = view === "graph" ? [70, 30] : [30, 70]; @@ -279,34 +270,7 @@ export const PanelButtons = ({ - - - {translate("dag:panel.graphDirection.label")} - - - - - - - - - - - - {directionOptions(translate).items.map((option) => ( - - {option.label} - - ))} - - - + ) : ( <> diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx index 5251117ccb74f..c0f5218e5c605 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetGraph.tsx @@ -20,12 +20,15 @@ import { useToken } from "@chakra-ui/react"; import { ReactFlow, Controls, Background, MiniMap, type Node as ReactFlowNode } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { useParams } from "react-router-dom"; +import { useLocalStorage } from "usehooks-ts"; import type { AssetResponse } from "openapi/requests/types.gen"; +import type { Direction } from "src/components/Graph/DirectionDropdown"; import { DownloadButton } from "src/components/Graph/DownloadButton"; import { edgeTypes, nodeTypes } from "src/components/Graph/graphTypes"; import type { CustomNodeProps } from "src/components/Graph/reactflowUtils"; import { useGraphLayout } from "src/components/Graph/useGraphLayout"; +import { directionKey } from "src/constants/localStorage"; import { useColorMode } from "src/context/colorMode"; import { useDependencyGraph } from "src/queries/useDependencyGraph"; import { getReactFlowThemeStyle } from "src/theme"; @@ -45,9 +48,11 @@ export const AssetGraph = ({ dependencyType, }); + const [direction] = useLocalStorage(directionKey(assetId ?? ""), "RIGHT"); + const { data: layoutData } = useGraphLayout({ ...graphData, - direction: "RIGHT", + direction, openGroupIds: [], }); diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetPanelButtons.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetPanelButtons.tsx index c8b3178fa2f31..c63a1dcafb2de 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetPanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetPanelButtons.tsx @@ -16,8 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Button, ButtonGroup } from "@chakra-ui/react"; +import { Box, Button, ButtonGroup, Flex, IconButton, Popover, Portal } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; +import { MdSettings } from "react-icons/md"; +import { useParams } from "react-router-dom"; + +import { DirectionDropdown } from "src/components/Graph/DirectionDropdown"; type Props = { readonly dependencyType: "data" | "scheduling"; @@ -26,27 +30,60 @@ type Props = { export const AssetPanelButtons = ({ dependencyType, setDependencyType }: Props) => { const { t: translate } = useTranslation(["assets"]); + const { assetId } = useParams(); return ( - - - - + + + + + + + + + + + + + + + + + + + + + + + ); }; diff --git a/airflow-core/src/airflow/ui/testsSetup.ts b/airflow-core/src/airflow/ui/testsSetup.ts index 1bb26b7d5ba7a..bbb226c7c91c4 100644 --- a/airflow-core/src/airflow/ui/testsSetup.ts +++ b/airflow-core/src/airflow/ui/testsSetup.ts @@ -28,7 +28,6 @@ import { handlers } from "src/mocks/handlers"; // happy-dom. Mock useGraphLayout so the Worker is never constructed. Any // test that specifically exercises graph layout should override this mock. vi.mock("src/components/Graph/useGraphLayout", () => ({ - directionOptions: () => ({ items: [] }), useGraphLayout: vi.fn().mockReturnValue({ data: undefined, isPending: false }), }));