diff --git a/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx b/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx index aa906a0b6ce34..4b656764a6510 100644 --- a/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx +++ b/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx @@ -19,12 +19,11 @@ import { HStack, Stat } from "@chakra-ui/react"; import type { ReactNode } from "react"; import { LiaSlashSolid } from "react-icons/lia"; -import { Link as RouterLink, useParams, useSearchParams } from "react-router-dom"; +import { Link as RouterLink, useParams } from "react-router-dom"; import { useDagRunServiceGetDagRun, useDagServiceGetDagDetails, - useTaskInstanceServiceGetMappedTaskInstance, useTaskServiceGetTask, } from "openapi/queries"; import { StateBadge } from "src/components/StateBadge"; @@ -33,11 +32,7 @@ import { TogglePause } from "src/components/TogglePause"; import { Breadcrumb } from "src/components/ui"; export const DagBreadcrumb = () => { - const { dagId = "", runId, taskId } = useParams(); - - const [searchParams] = useSearchParams(); - const mapIndexParam = searchParams.get("map_index"); - const mapIndex = parseInt(mapIndexParam ?? "-1", 10); + const { dagId = "", mapIndex = "-1", runId, taskId } = useParams(); const { data: dag } = useDagServiceGetDagDetails({ dagId, @@ -56,19 +51,6 @@ export const DagBreadcrumb = () => { const { data: task } = useTaskServiceGetTask({ dagId, taskId }, undefined, { enabled: Boolean(taskId) }); - const { data: taskInstance } = useTaskInstanceServiceGetMappedTaskInstance( - { - dagId, - dagRunId: runId ?? "", - mapIndex, - taskId: taskId ?? "", - }, - undefined, - { - enabled: Boolean(runId) && Boolean(taskId), - }, - ); - const links: Array<{ label: ReactNode | string; labelExtra?: ReactNode; title?: string; value?: string }> = [ { label: "Dags", value: "/dags" }, @@ -99,7 +81,18 @@ export const DagBreadcrumb = () => { // Add task breadcrumb if (runId !== undefined && taskId !== undefined) { - links.push({ label: taskInstance?.task_display_name ?? taskId, title: "Task" }); + if (task?.is_mapped) { + links.push({ + label: `${task.task_display_name ?? taskId} [ ]`, + title: "Task", + value: `/dags/${dagId}/runs/${runId}/tasks/${taskId}/mapped`, + }); + } else { + links.push({ + label: task?.task_display_name ?? taskId, + title: "Task", + }); + } } if (runId === undefined && taskId !== undefined) { @@ -107,8 +100,8 @@ export const DagBreadcrumb = () => { links.push({ label: task?.task_display_name ?? taskId, title: "Task" }); } - if (mapIndexParam !== null) { - links.push({ label: mapIndexParam, title: "Map Index" }); + if (mapIndex !== "-1") { + links.push({ label: mapIndex, title: "Map Index" }); } return ( diff --git a/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow/ui/src/layouts/Details/DetailsLayout.tsx index 0b2178e8f0a3d..e2fa8cf77b512 100644 --- a/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -92,7 +92,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { {children} - + diff --git a/airflow/ui/src/layouts/Details/Graph/TaskLink.tsx b/airflow/ui/src/layouts/Details/Graph/TaskLink.tsx index dab512d822a84..e933329b02901 100644 --- a/airflow/ui/src/layouts/Details/Graph/TaskLink.tsx +++ b/airflow/ui/src/layouts/Details/Graph/TaskLink.tsx @@ -25,13 +25,13 @@ type Props = { readonly id: string; } & TaskNameProps; -export const TaskLink = ({ id, isGroup, ...rest }: Props) => { +export const TaskLink = ({ id, isGroup, isMapped, ...rest }: Props) => { const { dagId = "", runId, taskId } = useParams(); const [searchParams] = useSearchParams(); // We don't have a task group details page to link to if (isGroup) { - return ; + return ; } return ( @@ -39,11 +39,11 @@ export const TaskLink = ({ id, isGroup, ...rest }: Props) => { - + ); diff --git a/airflow/ui/src/layouts/Details/Grid/GridTI.tsx b/airflow/ui/src/layouts/Details/Grid/GridTI.tsx index fc939eb8f2121..85a8342d8ac42 100644 --- a/airflow/ui/src/layouts/Details/Grid/GridTI.tsx +++ b/airflow/ui/src/layouts/Details/Grid/GridTI.tsx @@ -27,6 +27,7 @@ import { StateIcon } from "src/components/StateIcon"; type Props = { readonly dagId: string; readonly isGroup?: boolean; + readonly isMapped?: boolean | null; readonly label: string; readonly runId: string; readonly search: string; @@ -50,9 +51,9 @@ const onMouseLeave = (event: MouseEvent) => { }); }; -const Instance = ({ dagId, runId, search, state, taskId }: Props) => ( +const Instance = ({ dagId, isGroup, isMapped, runId, search, state, taskId }: Props) => ( ( transition="background-color 0.2s" zIndex={1} > - + {isGroup ? ( ( /> )} - + ) : ( + + + {state === undefined ? undefined : ( + + )} + + + )} ); diff --git a/airflow/ui/src/layouts/Details/Grid/TaskInstances.tsx b/airflow/ui/src/layouts/Details/Grid/TaskInstances.tsx index 4e0929e71823e..7c763999bacff 100644 --- a/airflow/ui/src/layouts/Details/Grid/TaskInstances.tsx +++ b/airflow/ui/src/layouts/Details/Grid/TaskInstances.tsx @@ -47,6 +47,7 @@ export const TaskInstances = ({ nodes, runId, taskInstances }: Props) => { ; }; -export const NavTabs = ({ keepSearch, tabs }: Props) => { - const [searchParams] = useSearchParams(); - +export const NavTabs = ({ tabs }: Props) => { const containerRef = useRef(null); const containerWidth = useContainerWidth(containerRef); @@ -42,8 +39,6 @@ export const NavTabs = ({ keepSearch, tabs }: Props) => { title={label} to={{ pathname: value, - // Preserve search params when navigating - search: keepSearch ? searchParams.toString() : undefined, }} > {({ isActive }) => ( diff --git a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvent.tsx b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvent.tsx index 9acc43e8d311e..e5bf931d17382 100644 --- a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvent.tsx +++ b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/AssetEvent.tsx @@ -58,7 +58,7 @@ export const AssetEvent = ({ event }: { readonly event: AssetEventResponse }) => Source: {source === "" ? ( -1 ? `/mapped/${event.source_map_index}` : ""}`} > {event.source_dag_id} diff --git a/airflow/ui/src/pages/MappedTaskInstance/Header.tsx b/airflow/ui/src/pages/MappedTaskInstance/Header.tsx new file mode 100644 index 0000000000000..9396802a2d845 --- /dev/null +++ b/airflow/ui/src/pages/MappedTaskInstance/Header.tsx @@ -0,0 +1,64 @@ +/*! + * 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 type { ReactNode } from "react"; +import { MdOutlineTask } from "react-icons/md"; + +import type { GridTaskInstanceSummary } from "openapi/requests/types.gen"; +import { HeaderCard } from "src/components/HeaderCard"; +import Time from "src/components/Time"; +import { getDuration } from "src/utils"; + +export const Header = ({ + isRefreshing, + taskInstance, +}: { + readonly isRefreshing?: boolean; + readonly taskInstance: GridTaskInstanceSummary; +}) => { + const entries: Array<{ label: string; value: number | ReactNode | string }> = []; + + if (taskInstance.child_states !== null) { + Object.entries(taskInstance.child_states).forEach(([state, count]) => { + if (count > 0) { + entries.push({ label: `Total ${state}`, value: count }); + } + }); + } + const stats = [ + { label: "Task Count", value: taskInstance.task_count }, + ...entries, + { label: "Start", value: