This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 81ef598fd8d Improve Grid view UX (#54846)
81ef598fd8d is described below
commit 81ef598fd8dab4400ef06f1835e7212c574e2613
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Aug 27 17:16:41 2025 -0500
Improve Grid view UX (#54846)
* Improve grid kbd navigation
* Use chakra tooltip
* Fix static checks
* Fix static checks
* Upgrade keyboard commands to not conflict with regular arrow actions
* Remove jump hotkeys
* Remove english translation for jump
---
.../src/airflow/ui/public/i18n/locales/ar/dag.json | 1 -
.../src/airflow/ui/public/i18n/locales/de/dag.json | 3 +-
.../src/airflow/ui/public/i18n/locales/en/dag.json | 3 +-
.../src/airflow/ui/public/i18n/locales/he/dag.json | 1 -
.../src/airflow/ui/public/i18n/locales/ko/dag.json | 1 -
.../src/airflow/ui/public/i18n/locales/nl/dag.json | 1 -
.../src/airflow/ui/public/i18n/locales/pl/dag.json | 1 -
.../src/airflow/ui/public/i18n/locales/tr/dag.json | 1 -
.../airflow/ui/public/i18n/locales/zh-TW/dag.json | 1 -
.../src/airflow/ui/src/hooks/navigation/types.ts | 5 +-
.../src/hooks/navigation/useKeyboardNavigation.ts | 22 +-
.../ui/src/hooks/navigation/useNavigation.ts | 40 +--
.../ui/src/layouts/Details/DetailsLayout.tsx | 35 ++-
.../airflow/ui/src/layouts/Details/Grid/Grid.tsx | 49 +---
.../airflow/ui/src/layouts/Details/Grid/GridTI.tsx | 134 +++------
.../ui/src/layouts/Details/Grid/useGridStore.ts | 28 --
.../ui/src/layouts/Details/PanelButtons.tsx | 323 +++++++++++----------
.../ui/src/layouts/Details/ToggleGroups.tsx | 4 +-
18 files changed, 259 insertions(+), 394 deletions(-)
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
index 800daf360b7..3a9483a3faa 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ar/dag.json
@@ -40,7 +40,6 @@
"warning": "تحذير"
},
"navigation": {
- "jump": "الانتقال: Shift+{{arrow}}",
"navigation": "التنقل: {{arrow}}",
"toggleGroup": "تبديل المجموعة: المسافة"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
index adf8c6da98b..1c3a646b700 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
@@ -40,8 +40,7 @@
"warning": "WARNING"
},
"navigation": {
- "jump": "Springen: Umschalttaste+{{arrow}}",
- "navigation": "Navigation: {{arrow}}",
+ "navigation": "Navigation: Umschalttaste+{{arrow}}",
"toggleGroup": "Gruppen umschalten: Leertaste"
},
"overview": {
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 5a66838365d..215c4161d8c 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
@@ -67,8 +67,7 @@
"warning": "WARNING"
},
"navigation": {
- "jump": "Jump: Shift+{{arrow}}",
- "navigation": "Navigation: {{arrow}}",
+ "navigation": "Navigation: Shift+{{arrow}}",
"toggleGroup": "Toggle group: Space"
},
"overview": {
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
index d7b58d0d4d2..8e3fd86e062 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/he/dag.json
@@ -40,7 +40,6 @@
"warning": "WARNING"
},
"navigation": {
- "jump": "קפיצה: Shift+{{arrow}}",
"navigation": "ניווט: {{arrow}}",
"toggleGroup": "החלפת קבוצה: רווח"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
index 722b8d36734..ef7106c7d95 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
@@ -40,7 +40,6 @@
"warning": "경고"
},
"navigation": {
- "jump": "이동: Shift+{{arrow}}",
"navigation": "탐색: {{arrow}}",
"toggleGroup": "그룹 전환: Space"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
index 88f1305adea..9fc4737f8cb 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/nl/dag.json
@@ -40,7 +40,6 @@
"warning": "WARNING"
},
"navigation": {
- "jump": "Verspringen: Shift+{{arrow}}",
"navigation": "Navigatie: {{arrow}}",
"toggleGroup": "Groep in-/uitklappen: Spatie"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
index a8e48aa91fd..455a08cc9a3 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
@@ -40,7 +40,6 @@
"warning": "WARNING"
},
"navigation": {
- "jump": "Przeskocz: Shift+{{arrow}}",
"navigation": "Przewiń: {{arrow}}",
"toggleGroup": "Przełącz grupę: Space"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
index ab14e119fcd..242e09d72ae 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/tr/dag.json
@@ -40,7 +40,6 @@
"warning": "UYARI"
},
"navigation": {
- "jump": "Atla: Shift+{{arrow}}",
"navigation": "Gezin: {{arrow}}",
"toggleGroup": "Grubu aç/kapat: Space"
},
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
index 946efc199f7..0b7b06e7cfb 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
@@ -68,7 +68,6 @@
"warning": "WARNING"
},
"navigation": {
- "jump": "跳躍: Shift+{{arrow}}",
"navigation": "導航: {{arrow}}",
"toggleGroup": "展開/收合群組: 空白鍵"
},
diff --git a/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
b/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
index 890663da4a4..a3a7a1c9a31 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/types.ts
@@ -31,8 +31,6 @@ export type NavigationIndices = {
};
export type UseNavigationProps = {
- enabled?: boolean;
- onEscapePress?: () => void;
onToggleGroup?: (taskId: string) => void;
runs: Array<GridRunsResponse>;
tasks: Array<GridTask>;
@@ -41,8 +39,7 @@ export type UseNavigationProps = {
export type UseNavigationReturn = {
currentIndices: NavigationIndices;
currentTask: GridTask | undefined;
- enabled: boolean;
- handleNavigation: (direction: NavigationDirection, isJump?: boolean) => void;
+ handleNavigation: (direction: NavigationDirection) => void;
mode: NavigationMode;
setMode: (mode: NavigationMode) => void;
};
diff --git
a/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
b/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
index 99b8df2522d..1d8277d2c0c 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/useKeyboardNavigation.ts
@@ -21,13 +21,11 @@ import { useHotkeys } from "react-hotkeys-hook";
import type { ArrowKey, NavigationDirection } from "./types";
-const ARROW_KEYS = ["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"] as
const;
-const JUMP_KEYS = ["shift+ArrowDown", "shift+ArrowUp", "shift+ArrowLeft",
"shift+ArrowRight"] as const;
+const ARROW_KEYS = ["shift+ArrowDown", "shift+ArrowUp", "shift+ArrowLeft",
"shift+ArrowRight"] as const;
type Props = {
enabled?: boolean;
- onEscapePress?: () => void;
- onNavigate: (direction: NavigationDirection, isJump?: boolean) => void;
+ onNavigate: (direction: NavigationDirection) => void;
onToggleGroup?: () => void;
};
@@ -46,32 +44,24 @@ const mapKeyToDirection = (key: ArrowKey):
NavigationDirection => {
}
};
-export const useKeyboardNavigation = ({
- enabled = true,
- onEscapePress,
- onNavigate,
- onToggleGroup,
-}: Props) => {
+export const useKeyboardNavigation = ({ enabled = true, onNavigate,
onToggleGroup }: Props) => {
const createKeyHandler = useCallback(
- (isJump: boolean) => (event: KeyboardEvent) => {
+ () => (event: KeyboardEvent) => {
const direction = mapKeyToDirection(event.key as ArrowKey);
event.preventDefault();
event.stopPropagation();
- onNavigate(direction, isJump);
+ onNavigate(direction);
},
[onNavigate],
);
- const handleNormalKeyPress = createKeyHandler(false);
- const handleJumpKeyPress = createKeyHandler(true);
+ const handleNormalKeyPress = createKeyHandler();
const hotkeyOptions = { enabled, preventDefault: true };
useHotkeys(ARROW_KEYS.join(","), handleNormalKeyPress, hotkeyOptions,
[onNavigate]);
- useHotkeys(JUMP_KEYS.join(","), handleJumpKeyPress, hotkeyOptions,
[onNavigate]);
useHotkeys("space", () => onToggleGroup?.(), hotkeyOptions, [onToggleGroup]);
- useHotkeys("escape", () => onEscapePress?.(), hotkeyOptions,
[onEscapePress]);
};
diff --git a/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
b/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
index 13eec1751c7..9c0bb7bdd64 100644
--- a/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
+++ b/airflow-core/src/airflow/ui/src/hooks/navigation/useNavigation.ts
@@ -59,17 +59,8 @@ const isValidDirection = (direction: NavigationDirection,
mode: NavigationMode):
}
};
-const getNextIndex = (
- current: number,
- direction: number,
- options: { isJump: boolean; max: number },
-): number => {
- if (options.isJump) {
- return direction > 0 ? options.max - 1 : 0;
- }
-
- return Math.max(0, Math.min(options.max - 1, current + direction));
-};
+const getNextIndex = (current: number, direction: number, options: { max:
number }): number =>
+ Math.max(0, Math.min(options.max - 1, current + direction));
const buildPath = (params: {
dagId: string;
@@ -106,14 +97,9 @@ const buildPath = (params: {
}
};
-export const useNavigation = ({
- enabled = true,
- onEscapePress,
- onToggleGroup,
- runs,
- tasks,
-}: UseNavigationProps): UseNavigationReturn => {
+export const useNavigation = ({ onToggleGroup, runs, tasks }:
UseNavigationProps): UseNavigationReturn => {
const { dagId = "", groupId = "", mapIndex = "-1", runId = "", taskId = "" }
= useParams();
+ const enabled = Boolean(dagId) && (Boolean(runId) || Boolean(taskId) ||
Boolean(groupId));
const navigate = useNavigate();
const [mode, setMode] = useState<NavigationMode>("TI");
@@ -137,7 +123,7 @@ export const useNavigation = ({
const currentTask = useMemo(() => tasks[currentIndices.taskIndex], [tasks,
currentIndices.taskIndex]);
const handleNavigation = useCallback(
- (direction: NavigationDirection, isJump: boolean = false) => {
+ (direction: NavigationDirection) => {
if (!enabled || !dagId || !isValidDirection(direction, mode)) {
return;
}
@@ -151,7 +137,7 @@ export const useNavigation = ({
const isAtBoundary = boundaries[direction];
- if (!isJump && isAtBoundary) {
+ if (isAtBoundary) {
return;
}
@@ -171,11 +157,10 @@ export const useNavigation = ({
if (nav.index === "taskIndex") {
newIndices.taskIndex = getNextIndex(currentIndices.taskIndex,
nav.direction, {
- isJump,
max: nav.max,
});
} else {
- newIndices.runIndex = getNextIndex(currentIndices.runIndex,
nav.direction, { isJump, max: nav.max });
+ newIndices.runIndex = getNextIndex(currentIndices.runIndex,
nav.direction, { max: nav.max });
}
const { runIndex: newRunIndex, taskIndex: newTaskIndex } = newIndices;
@@ -191,14 +176,20 @@ export const useNavigation = ({
const path = buildPath({ dagId, mapIndex, mode, run, task });
navigate(path, { replace: true });
+
+ const grid =
document.querySelector(`[id='grid-${run.run_id}-${task.id}']`);
+
+ // Set the focus to the grid link to allow a user to continue tabbing
through with the keyboard
+ if (grid) {
+ (grid as HTMLLinkElement).focus();
+ }
}
},
[currentIndices, dagId, enabled, mapIndex, mode, runs, tasks, navigate],
);
useKeyboardNavigation({
- enabled: enabled && Boolean(dagId),
- onEscapePress,
+ enabled,
onNavigate: handleNavigation,
onToggleGroup: currentTask?.isGroup && onToggleGroup ? () =>
onToggleGroup(currentTask.id) : undefined,
});
@@ -206,7 +197,6 @@ export const useNavigation = ({
return {
currentIndices,
currentTask,
- enabled: enabled && Boolean(dagId),
handleNavigation,
mode,
setMode,
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 e536061cded..4a3714ac83b 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -90,15 +90,14 @@ export const DetailsLayout = ({ children, error, isLoading,
tabs }: Props) => {
<Tooltip content={translate("common:showDetailsPanel")}>
<IconButton
aria-label={translate("common:showDetailsPanel")}
- bg="bg.surface"
- borderRadius="full"
+ bg="fg.subtle"
+ borderRadius={direction === "ltr" ? "100% 0 0 100%" : "0 100%
100% 0"}
boxShadow="md"
- cursor="pointer"
- left={direction === "rtl" ? 0 : undefined}
+ left={direction === "rtl" ? "-5px" : undefined}
onClick={() => setIsRightPanelCollapsed(false)}
position="absolute"
- right={direction === "ltr" ? 0 : undefined}
- size="sm"
+ right={direction === "ltr" ? "-5px" : undefined}
+ size="2xs"
top="50%"
zIndex={10}
>
@@ -160,25 +159,31 @@ export const DetailsLayout = ({ children, error,
isLoading, tabs }: Props) => {
justifyContent="center"
position="relative"
w={0.5}
- >
+ // onClick={(e) => console.log(e)}
+ />
+ </PanelResizeHandle>
+
+ {/* Collapse button positioned next to the resize handle */}
+
+ <Panel defaultSize={dagView === "graph" ? 30 : 80}
id="details-panel" minSize={20} order={2}>
+ <Box display="flex" flexDirection="column" h="100%"
position="relative">
<Tooltip content={translate("common:collapseDetailsPanel")}>
<IconButton
aria-label={translate("common:collapseDetailsPanel")}
- bg="bg.surface"
- borderRadius="full"
+ bg="fg.subtle"
+ borderRadius={direction === "ltr" ? "0 100% 100% 0" :
"100% 0 0 100%"}
boxShadow="md"
- cursor="pointer"
+ left={direction === "ltr" ? "-5px" : undefined}
onClick={() => setIsRightPanelCollapsed(true)}
- size="xs"
+ position="absolute"
+ right={direction === "rtl" ? "-5px" : undefined}
+ size="2xs"
+ top="50%"
zIndex={2}
>
{direction === "ltr" ? <FaChevronRight /> :
<FaChevronLeft />}
</IconButton>
</Tooltip>
- </Box>
- </PanelResizeHandle>
- <Panel defaultSize={dagView === "graph" ? 30 : 80}
id="details-panel" minSize={20} order={2}>
- <Box display="flex" flexDirection="column" h="100%">
{children}
{Boolean(error) || (warningData?.dag_warnings.length ?? 0) >
0 ? (
<>
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 a5b1fb44298..8a56fc47441 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
@@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box, Flex, IconButton, Text } from "@chakra-ui/react";
+import { Box, Flex, IconButton } from "@chakra-ui/react";
import dayjs from "dayjs";
import dayjsDuration from "dayjs/plugin/duration";
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { FiChevronsRight } from "react-icons/fi";
import { Link, useParams } from "react-router-dom";
@@ -35,24 +35,10 @@ import { Bar } from "./Bar";
import { DurationAxis } from "./DurationAxis";
import { DurationTick } from "./DurationTick";
import { TaskNames } from "./TaskNames";
-import { useGridStore } from "./useGridStore";
import { flattenNodes } from "./utils";
dayjs.extend(dayjsDuration);
-const getArrowsForMode = (navigationMode: string) => {
- switch (navigationMode) {
- case "run":
- return "←→";
- case "task":
- return "↑↓";
- case "TI":
- return "↑↓←→";
- default:
- return "↑↓←→";
- }
-};
-
type Props = {
readonly limit: number;
readonly showGantt?: boolean;
@@ -61,7 +47,6 @@ type Props = {
export const Grid = ({ limit, showGantt }: Props) => {
const { t: translate } = useTranslation("dag");
const gridRef = useRef<HTMLDivElement>(null);
- const { isGridFocused, setIsGridFocused } = useGridStore();
const [selectedIsVisible, setSelectedIsVisible] = useState<boolean |
undefined>();
const [hasActiveRun, setHasActiveRun] = useState<boolean | undefined>();
@@ -106,21 +91,7 @@ export const Grid = ({ limit, showGantt }: Props) => {
const { flatNodes } = useMemo(() => flattenNodes(dagStructure,
openGroupIds), [dagStructure, openGroupIds]);
- const setGridFocus = useCallback(
- (focused: boolean) => {
- setIsGridFocused(focused);
- if (focused) {
- gridRef.current?.focus();
- } else {
- gridRef.current?.blur();
- }
- },
- [setIsGridFocused],
- );
-
- const { mode, setMode } = useNavigation({
- enabled: isGridFocused,
- onEscapePress: () => setGridFocus(false),
+ const { setMode } = useNavigation({
onToggleGroup: toggleGroupId,
runs: gridRuns ?? [],
tasks: flatNodes,
@@ -128,15 +99,7 @@ export const Grid = ({ limit, showGantt }: Props) => {
return (
<Flex
- _focus={{
- borderRadius: "4px",
- boxShadow: "0 0 0 2px rgba(59, 130, 246, 0.5)",
- }}
- cursor="pointer"
justifyContent="flex-start"
- onBlur={() => setGridFocus(false)}
- onFocus={() => setGridFocus(true)}
- onMouseDown={() => setGridFocus(true)}
outline="none"
position="relative"
pt={20}
@@ -144,12 +107,6 @@ export const Grid = ({ limit, showGantt }: Props) => {
tabIndex={0}
width={showGantt ? undefined : "100%"}
>
- <Box borderRadius="md" color="gray.400" fontSize="xs"
position="absolute" px={0} py={12} top={0}>
- <Text>{translate("navigation.navigation", { arrow:
getArrowsForMode(mode) })}</Text>
- <Text>{translate("navigation.jump", { arrow: getArrowsForMode(mode)
})}</Text>
- {mode !== "run" && <Text>{translate("navigation.toggleGroup")}</Text>}
- </Box>
-
<Box flexGrow={1} minWidth={7} position="relative" top="100px">
<TaskNames nodes={flatNodes} onRowClick={() => setMode("task")} />
</Box>
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
index 89ea4f30688..e03ff24d47b 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
@@ -16,15 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Badge, chakra, Flex } from "@chakra-ui/react";
+import { Badge, Flex } from "@chakra-ui/react";
import type { MouseEvent } from "react";
-import React, { useRef } from "react";
+import React from "react";
import { useTranslation } from "react-i18next";
import { Link, useParams } from "react-router-dom";
import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen";
import { StateIcon } from "src/components/StateIcon";
import Time from "src/components/Time";
+import { Tooltip } from "src/components/ui";
type Props = {
readonly dagId: string;
@@ -57,41 +58,6 @@ const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
const Instance = ({ dagId, instance, isGroup, isMapped, onClick, runId,
search, taskId }: Props) => {
const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams();
const { t: translate } = useTranslation();
- const debounceTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
- const tooltipRef = useRef<HTMLElement | undefined>(undefined);
-
- const onBadgeMouseEnter = (event: MouseEvent<HTMLDivElement>) => {
- // Clear any existing timeout
- if (debounceTimeoutRef.current) {
- clearTimeout(debounceTimeoutRef.current);
- }
-
- // Store reference to the tooltip element
- const tooltip = event.currentTarget.querySelector("#tooltip") as
HTMLElement;
-
- tooltipRef.current = tooltip;
-
- // Set a new timeout to show the tooltip after 200ms
- debounceTimeoutRef.current = setTimeout(() => {
- if (tooltipRef.current) {
- tooltipRef.current.style.visibility = "visible";
- }
- }, 200);
- };
-
- const onBadgeMouseLeave = () => {
- // Clear any existing timeout
- if (debounceTimeoutRef.current) {
- clearTimeout(debounceTimeoutRef.current);
- debounceTimeoutRef.current = undefined;
- }
-
- // Hide the tooltip immediately
- if (tooltipRef.current) {
- tooltipRef.current.style.visibility = "hidden";
- tooltipRef.current = undefined;
- }
- };
return (
<Flex
@@ -103,12 +69,13 @@ const Instance = ({ dagId, instance, isGroup, isMapped,
onClick, runId, search,
key={taskId}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
+ position="relative"
px="2px"
py={0}
transition="background-color 0.2s"
- zIndex={1}
>
<Link
+ id={`grid-${runId}-${taskId}`}
onClick={onClick}
replace
to={{
@@ -116,65 +83,42 @@ const Instance = ({ dagId, instance, isGroup, isMapped,
onClick, runId, search,
search,
}}
>
- <Badge
- borderRadius={4}
- colorPalette={instance.state ?? "none"}
- height="14px"
- minH={0}
- onMouseEnter={onBadgeMouseEnter}
- onMouseLeave={onBadgeMouseLeave}
- p={0}
- position="relative"
- variant="solid"
- width="14px"
+ <Tooltip
+ content={
+ <>
+ {translate("taskId")}: {taskId}
+ <br />
+ {translate("state")}: {instance.state}
+ {instance.min_start_date !== null && (
+ <>
+ <br />
+ {translate("startDate")}: <Time
datetime={instance.min_start_date} />
+ </>
+ )}
+ {instance.max_end_date !== null && (
+ <>
+ <br />
+ {translate("endDate")}: <Time
datetime={instance.max_end_date} />
+ </>
+ )}
+ </>
+ }
>
- <StateIcon
- size={10}
- state={instance.state}
- style={{
- marginLeft: "2px",
- }}
- />
- <chakra.span
- bg="bg.inverted"
- borderRadius={2}
- bottom={0}
- color="fg.inverted"
- id="tooltip"
- p={2}
- position="absolute"
- right={5}
- visibility="hidden"
- zIndex="tooltip"
+ <Badge
+ alignItems="center"
+ borderRadius={4}
+ colorPalette={instance.state ?? "none"}
+ display="flex"
+ height="14px"
+ justifyContent="center"
+ minH={0}
+ p={0}
+ variant="solid"
+ width="14px"
>
- {translate("taskId")}: {taskId}
- <br />
- {translate("state")}: {instance.state}
- {instance.min_start_date !== null && (
- <>
- <br />
- {translate("startDate")}: <Time
datetime={instance.min_start_date} />
- </>
- )}
- {instance.max_end_date !== null && (
- <>
- <br />
- {translate("endDate")}: <Time datetime={instance.max_end_date}
/>
- </>
- )}
- {/* Tooltip arrow pointing to the badge */}
- <chakra.div
- bg="bg.inverted"
- borderRadius={1}
- bottom={1}
- height={2}
- position="absolute"
- right="-3px"
- transform="rotate(45deg)"
- width={2}
- />
- </chakra.span>
- </Badge>
+ <StateIcon size={10} state={instance.state} />
+ </Badge>
+ </Tooltip>
</Link>
</Flex>
);
diff --git
a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridStore.ts
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridStore.ts
deleted file mode 100644
index 7f580649932..00000000000
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridStore.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*!
- * 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 { create } from "zustand";
-
-export const useGridStore = create<{ isGridFocused: boolean; setIsGridFocused:
(value: boolean) => void }>(
- (set) => ({
- // isGridFocused is shared between different pages (Run, GroupInstance,
MappedInstance, TaskInstance, etc.).
- // This will avoid many prop drilling and allow proper refocus of the grid
when navigating between these pages via grid links.
- isGridFocused: false,
- setIsGridFocused: (value: boolean) => set({ isGridFocused: value }),
- }),
-);
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 2684be54a64..75262ba3cf8 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx
@@ -28,17 +28,20 @@ import {
Portal,
Select,
VStack,
+ Text,
+ Box,
} from "@chakra-ui/react";
import { useReactFlow } from "@xyflow/react";
import { useTranslation } from "react-i18next";
import { FiChevronDown, FiGrid } from "react-icons/fi";
+import { LuKeyboard } from "react-icons/lu";
import { MdOutlineAccountTree } from "react-icons/md";
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 { Button } from "src/components/ui";
+import { Button, Tooltip } from "src/components/ui";
import { Checkbox } from "src/components/ui/Checkbox";
import { DagRunSelect } from "./DagRunSelect";
@@ -135,156 +138,174 @@ export const PanelButtons = ({
};
return (
- <Flex justifyContent="space-between" position="absolute" top={1}
width="100%" zIndex={1}>
- <ButtonGroup attached size="sm" variant="outline">
- <IconButton
- aria-label={translate("dag:panel.buttons.showGrid")}
- colorPalette="blue"
- onClick={() => {
- setDagView("grid");
- if (dagView === "grid") {
- handleFocus("grid");
- }
- }}
- title={translate("dag:panel.buttons.showGrid")}
- variant={dagView === "grid" ? "solid" : "outline"}
- >
- <FiGrid />
- </IconButton>
- <IconButton
- aria-label={translate("dag:panel.buttons.showGraph")}
- colorPalette="blue"
- onClick={() => {
- setDagView("graph");
- if (dagView === "graph") {
- handleFocus("graph");
- }
- }}
- title={translate("dag:panel.buttons.showGraph")}
- variant={dagView === "graph" ? "solid" : "outline"}
- >
- <MdOutlineAccountTree />
- </IconButton>
- </ButtonGroup>
- <Flex gap={1} mr={3}>
- <ToggleGroups />
- {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
- <Popover.Root autoFocus={false} positioning={{ placement: "bottom-end"
}}>
- <Popover.Trigger asChild>
- <Button size="sm" variant="outline">
- {translate("dag:panel.buttons.options")}
- <FiChevronDown size="0.5rem" />
- </Button>
- </Popover.Trigger>
- <Portal>
- <Popover.Positioner>
- <Popover.Content>
- <Popover.Arrow />
- <Popover.Body display="flex" flexDirection="column" gap={4}
p={2}>
- {dagView === "graph" ? (
- <>
- <DagVersionSelect />
- <DagRunSelect limit={limit} />
- <Select.Root
- // @ts-expect-error The expected option type is
incorrect
- collection={getOptions(translate)}
- data-testid="dependencies"
- onValueChange={handleDepsChange}
- size="sm"
- value={[dependencies]}
- >
- <Select.Label
fontSize="xs">{translate("dag:panel.dependencies.label")}</Select.Label>
- <Select.Control>
- <Select.Trigger>
- <Select.ValueText
placeholder={translate("dag:panel.dependencies.label")} />
- </Select.Trigger>
- <Select.IndicatorGroup>
- <Select.Indicator />
- </Select.IndicatorGroup>
- </Select.Control>
- <Select.Positioner>
- <Select.Content>
- {getOptions(translate).items.map((option) => (
- <Select.Item item={option} key={option.value}>
- {option.label}
- </Select.Item>
- ))}
- </Select.Content>
- </Select.Positioner>
- </Select.Root>
- <Select.Root
- // @ts-expect-error The expected option type is
incorrect
- collection={directionOptions(translate)}
- onValueChange={handleDirectionUpdate}
- size="sm"
- value={[direction]}
- >
- <Select.Label fontSize="xs">
- {translate("dag:panel.graphDirection.label")}
- </Select.Label>
- <Select.Control>
- <Select.Trigger>
- <Select.ValueText />
- </Select.Trigger>
- <Select.IndicatorGroup>
- <Select.Indicator />
- </Select.IndicatorGroup>
- </Select.Control>
- <Select.Positioner>
- <Select.Content>
- {directionOptions(translate).items.map((option) =>
(
- <Select.Item item={option} key={option.value}>
- {option.label}
- </Select.Item>
- ))}
- </Select.Content>
- </Select.Positioner>
- </Select.Root>
- </>
- ) : (
- <>
- <Select.Root
- // @ts-expect-error The expected option type is
incorrect
- collection={displayRunOptions}
- data-testid="display-dag-run-options"
- onValueChange={handleLimitChange}
- size="sm"
- value={[limit.toString()]}
- >
-
<Select.Label>{translate("dag:panel.dagRuns.label")}</Select.Label>
- <Select.Control>
- <Select.Trigger>
- <Select.ValueText />
- </Select.Trigger>
- <Select.IndicatorGroup>
- <Select.Indicator />
- </Select.IndicatorGroup>
- </Select.Control>
- <Select.Positioner>
- <Select.Content>
- {displayRunOptions.items.map((option) => (
- <Select.Item item={option} key={option.value}>
- {option.label}
- </Select.Item>
- ))}
- </Select.Content>
- </Select.Positioner>
- </Select.Root>
- {shouldShowToggleButtons ? (
- <VStack alignItems="flex-start" px={1}>
- <Checkbox checked={showGantt} onChange={() =>
setShowGantt(!showGantt)} size="sm">
- {translate("dag:panel.buttons.showGantt")}
- </Checkbox>
- </VStack>
- ) : undefined}
- </>
- )}
- </Popover.Body>
- </Popover.Content>
- </Popover.Positioner>
- </Portal>
- </Popover.Root>
+ <Box position="absolute" top={1} width="100%" zIndex={1}>
+ <Flex flexWrap="wrap" justifyContent="space-between">
+ <ButtonGroup attached size="sm" variant="outline">
+ <IconButton
+ aria-label={translate("dag:panel.buttons.showGrid")}
+ colorPalette="blue"
+ onClick={() => {
+ setDagView("grid");
+ if (dagView === "grid") {
+ handleFocus("grid");
+ }
+ }}
+ title={translate("dag:panel.buttons.showGrid")}
+ variant={dagView === "grid" ? "solid" : "outline"}
+ >
+ <FiGrid />
+ </IconButton>
+ <IconButton
+ aria-label={translate("dag:panel.buttons.showGraph")}
+ colorPalette="blue"
+ onClick={() => {
+ setDagView("graph");
+ if (dagView === "graph") {
+ handleFocus("graph");
+ }
+ }}
+ title={translate("dag:panel.buttons.showGraph")}
+ variant={dagView === "graph" ? "solid" : "outline"}
+ >
+ <MdOutlineAccountTree />
+ </IconButton>
+ </ButtonGroup>
+ <Flex gap={1}>
+ <ToggleGroups />
+ {/* eslint-disable-next-line jsx-a11y/no-autofocus */}
+ <Popover.Root autoFocus={false} positioning={{ placement:
"bottom-end" }}>
+ <Popover.Trigger asChild>
+ <Button size="sm" variant="outline">
+ {translate("dag:panel.buttons.options")}
+ <FiChevronDown size="0.5rem" />
+ </Button>
+ </Popover.Trigger>
+ <Portal>
+ <Popover.Positioner>
+ <Popover.Content>
+ <Popover.Arrow />
+ <Popover.Body display="flex" flexDirection="column" gap={4}
p={2}>
+ {dagView === "graph" ? (
+ <>
+ <DagVersionSelect />
+ <DagRunSelect limit={limit} />
+ <Select.Root
+ // @ts-expect-error The expected option type is
incorrect
+ collection={getOptions(translate)}
+ data-testid="dependencies"
+ onValueChange={handleDepsChange}
+ size="sm"
+ value={[dependencies]}
+ >
+ <Select.Label fontSize="xs">
+ {translate("dag:panel.dependencies.label")}
+ </Select.Label>
+ <Select.Control>
+ <Select.Trigger>
+ <Select.ValueText
placeholder={translate("dag:panel.dependencies.label")} />
+ </Select.Trigger>
+ <Select.IndicatorGroup>
+ <Select.Indicator />
+ </Select.IndicatorGroup>
+ </Select.Control>
+ <Select.Positioner>
+ <Select.Content>
+ {getOptions(translate).items.map((option) => (
+ <Select.Item item={option} key={option.value}>
+ {option.label}
+ </Select.Item>
+ ))}
+ </Select.Content>
+ </Select.Positioner>
+ </Select.Root>
+ <Select.Root
+ // @ts-expect-error The expected option type is
incorrect
+ collection={directionOptions(translate)}
+ onValueChange={handleDirectionUpdate}
+ size="sm"
+ value={[direction]}
+ >
+ <Select.Label fontSize="xs">
+ {translate("dag:panel.graphDirection.label")}
+ </Select.Label>
+ <Select.Control>
+ <Select.Trigger>
+ <Select.ValueText />
+ </Select.Trigger>
+ <Select.IndicatorGroup>
+ <Select.Indicator />
+ </Select.IndicatorGroup>
+ </Select.Control>
+ <Select.Positioner>
+ <Select.Content>
+ {directionOptions(translate).items.map((option)
=> (
+ <Select.Item item={option} key={option.value}>
+ {option.label}
+ </Select.Item>
+ ))}
+ </Select.Content>
+ </Select.Positioner>
+ </Select.Root>
+ </>
+ ) : (
+ <>
+ <Select.Root
+ // @ts-expect-error The expected option type is
incorrect
+ collection={displayRunOptions}
+ data-testid="display-dag-run-options"
+ onValueChange={handleLimitChange}
+ size="sm"
+ value={[limit.toString()]}
+ >
+
<Select.Label>{translate("dag:panel.dagRuns.label")}</Select.Label>
+ <Select.Control>
+ <Select.Trigger>
+ <Select.ValueText />
+ </Select.Trigger>
+ <Select.IndicatorGroup>
+ <Select.Indicator />
+ </Select.IndicatorGroup>
+ </Select.Control>
+ <Select.Positioner>
+ <Select.Content>
+ {displayRunOptions.items.map((option) => (
+ <Select.Item item={option} key={option.value}>
+ {option.label}
+ </Select.Item>
+ ))}
+ </Select.Content>
+ </Select.Positioner>
+ </Select.Root>
+ {shouldShowToggleButtons ? (
+ <VStack alignItems="flex-start" px={1}>
+ <Checkbox checked={showGantt} onChange={() =>
setShowGantt(!showGantt)} size="sm">
+ {translate("dag:panel.buttons.showGantt")}
+ </Checkbox>
+ </VStack>
+ ) : undefined}
+ </>
+ )}
+ </Popover.Body>
+ </Popover.Content>
+ </Popover.Positioner>
+ </Portal>
+ </Popover.Root>
+ </Flex>
</Flex>
- </Flex>
+ {dagView === "grid" && (
+ <Flex color="fg.muted" justifyContent="flex-end" mt={1}>
+ <Tooltip
+ content={
+ <Box>
+ <Text>{translate("dag:navigation.navigation", { arrow: "↑↓←→"
})}</Text>
+ <Text>{translate("dag:navigation.toggleGroup")}</Text>
+ </Box>
+ }
+ >
+ <LuKeyboard />
+ </Tooltip>
+ </Flex>
+ )}
+ </Box>
);
};
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
b/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
index 756a3298a9b..c55446c1c56 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/ToggleGroups.tsx
@@ -46,14 +46,13 @@ export const ToggleGroups = (props: ButtonGroupProps) => {
const collapseLabel = translate("dag:taskGroups.collapseAll");
return (
- <ButtonGroup attached size="sm" variant="surface" {...props}>
+ <ButtonGroup attached size="sm" variant="outline" {...props}>
<IconButton
aria-label={expandLabel}
disabled={isExpandDisabled}
onClick={onExpand}
size="sm"
title={expandLabel}
- variant="surface"
>
<MdExpand />
</IconButton>
@@ -63,7 +62,6 @@ export const ToggleGroups = (props: ButtonGroupProps) => {
onClick={onCollapse}
size="sm"
title={collapseLabel}
- variant="surface"
>
<MdCompress />
</IconButton>