Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 16 additions & 23 deletions airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand All @@ -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" },
Expand Down Expand Up @@ -99,16 +81,27 @@ 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) {
links.push({ label: "All Runs", title: "Dag Run", value: `/dags/${dagId}/runs/` });
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 (
Expand Down
2 changes: 1 addition & 1 deletion airflow/ui/src/layouts/Details/DetailsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => {
{children}
<ErrorAlert error={error} />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<NavTabs keepSearch tabs={tabs} />
<NavTabs tabs={tabs} />
<Box h="100%" overflow="auto" px={2}>
<Outlet />
</Box>
Expand Down
8 changes: 4 additions & 4 deletions airflow/ui/src/layouts/Details/Graph/TaskLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@ 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 <TaskName isGroup={true} {...rest} />;
return <TaskName isGroup={true} isMapped={isMapped} {...rest} />;
}

return (
<Link asChild data-testid={id}>
<RouterLink
to={{
// Do not include runId if there is no selected run, clicking a second time will deselect a task id
pathname: `/dags/${dagId}/${runId === undefined ? "" : `runs/${runId}/`}${taskId === id ? "" : `tasks/${id}`}`,
pathname: `/dags/${dagId}/${runId === undefined ? "" : `runs/${runId}/`}${taskId === id ? "" : `tasks/${id}`}${isMapped ? "/mapped" : ""}`,
search: searchParams.toString(),
}}
>
<TaskName {...rest} />
<TaskName isMapped={isMapped} {...rest} />
</RouterLink>
</Link>
);
Expand Down
44 changes: 34 additions & 10 deletions airflow/ui/src/layouts/Details/Grid/GridTI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -50,9 +51,9 @@ const onMouseLeave = (event: MouseEvent<HTMLDivElement>) => {
});
};

const Instance = ({ dagId, runId, search, state, taskId }: Props) => (
const Instance = ({ dagId, isGroup, isMapped, runId, search, state, taskId }: Props) => (
<Flex
alignItems="flex-end"
alignItems="center"
height="20px"
id={taskId.replaceAll(".", "-")}
justifyContent="center"
Expand All @@ -64,13 +65,7 @@ const Instance = ({ dagId, runId, search, state, taskId }: Props) => (
transition="background-color 0.2s"
zIndex={1}
>
<Link
replace
to={{
pathname: `/dags/${dagId}/runs/${runId}/tasks/${taskId}`,
search,
}}
>
{isGroup ? (
<Badge
borderRadius={4}
colorPalette={state === null ? "none" : state}
Expand All @@ -91,7 +86,36 @@ const Instance = ({ dagId, runId, search, state, taskId }: Props) => (
/>
)}
</Badge>
</Link>
) : (
<Link
replace
to={{
pathname: `/dags/${dagId}/runs/${runId}/tasks/${taskId}${isMapped ? "/mapped" : ""}`,
search,
}}
>
<Badge
borderRadius={4}
colorPalette={state === null ? "none" : state}
height="14px"
minH={0}
opacity={state === "success" ? 0.6 : 1}
p={0}
variant="solid"
width="14px"
>
{state === undefined ? undefined : (
<StateIcon
size={10}
state={state}
style={{
marginLeft: "2px",
}}
/>
)}
</Badge>
</Link>
)}
</Flex>
);

Expand Down
1 change: 1 addition & 0 deletions airflow/ui/src/layouts/Details/Grid/TaskInstances.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const TaskInstances = ({ nodes, runId, taskInstances }: Props) => {
<GridTI
dagId={dagId}
isGroup={node.isGroup}
isMapped={node.is_mapped}
key={node.id}
label={node.label}
runId={runId}
Expand Down
9 changes: 2 additions & 7 deletions airflow/ui/src/layouts/Details/NavTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,15 @@
*/
import { Center, Flex } from "@chakra-ui/react";
import { useRef, type ReactNode } from "react";
import { NavLink, useSearchParams } from "react-router-dom";
import { NavLink } from "react-router-dom";

import { useContainerWidth } from "src/utils";

type Props = {
readonly keepSearch?: boolean;
readonly tabs: Array<{ icon?: ReactNode; label: string; value: string }>;
};

export const NavTabs = ({ keepSearch, tabs }: Props) => {
const [searchParams] = useSearchParams();

export const NavTabs = ({ tabs }: Props) => {
const containerRef = useRef<HTMLDivElement>(null);
const containerWidth = useContainerWidth(containerRef);

Expand All @@ -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 }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const AssetEvent = ({ event }: { readonly event: AssetEventResponse }) =>
<MdOutlineAccountTree /> <Text> Source: </Text>
{source === "" ? (
<Link
to={`/dags/${event.source_dag_id}/runs/${event.source_run_id}/tasks/${event.source_task_id}?map_index=${event.source_map_index}`}
to={`/dags/${event.source_dag_id}/runs/${event.source_run_id}/tasks/${event.source_task_id}${event.source_map_index > -1 ? `/mapped/${event.source_map_index}` : ""}`}
>
<Text color="fg.info"> {event.source_dag_id} </Text>
</Link>
Expand Down
64 changes: 64 additions & 0 deletions airflow/ui/src/pages/MappedTaskInstance/Header.tsx
Original file line number Diff line number Diff line change
@@ -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: <Time datetime={taskInstance.start_date} /> },
{ label: "End", value: <Time datetime={taskInstance.end_date} /> },
{ label: "Duration", value: `${getDuration(taskInstance.start_date, taskInstance.end_date)}s` },
];

return (
<Box>
<HeaderCard
icon={<MdOutlineTask />}
isRefreshing={isRefreshing}
state={taskInstance.state}
stats={stats}
subTitle={<Time datetime={taskInstance.start_date} />}
title={`${taskInstance.task_id} [ ]`}
/>
</Box>
);
};
70 changes: 70 additions & 0 deletions airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*!
* 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 { ReactFlowProvider } from "@xyflow/react";
import { MdOutlineTask } from "react-icons/md";
import { useParams } from "react-router-dom";

import { useDagServiceGetDagDetails, useGridServiceGridData } from "openapi/queries";
import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
import { isStatePending, useAutoRefresh } from "src/utils";

import { Header } from "./Header";

const tabs = [{ icon: <MdOutlineTask />, label: "Task Instances", value: "" }];

export const MappedTaskInstance = () => {
const { dagId = "", runId = "", taskId = "" } = useParams();
const refetchInterval = useAutoRefresh({ dagId });

const {
data: dag,
error: dagError,
isLoading: isDagLoading,
} = useDagServiceGetDagDetails({
dagId,
});

const { data, error, isLoading } = useGridServiceGridData(
{
dagId,
},
undefined,
{
refetchInterval: (query) =>
query.state.data?.dag_runs.some((dr) => isStatePending(dr.state)) ? refetchInterval : false,
},
);

const taskInstance = data?.dag_runs
.find((dr) => dr.dag_run_id === runId)
?.task_instances.find((ti) => ti.task_id === taskId);

return (
<ReactFlowProvider>
<DetailsLayout dag={dag} error={error ?? dagError} isLoading={isLoading || isDagLoading} tabs={tabs}>
{taskInstance === undefined ? undefined : (
<Header
isRefreshing={Boolean(isStatePending(taskInstance.state) && Boolean(refetchInterval))}
taskInstance={taskInstance}
/>
)}
</DetailsLayout>
</ReactFlowProvider>
);
};
20 changes: 20 additions & 0 deletions airflow/ui/src/pages/MappedTaskInstance/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*!
* 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.
*/

export { MappedTaskInstance } from "./MappedTaskInstance";
Loading