diff --git a/airflow/ui/src/assets/TaskIcon.tsx b/airflow/ui/src/assets/TaskIcon.tsx
new file mode 100644
index 0000000000000..cd8f4440f09d3
--- /dev/null
+++ b/airflow/ui/src/assets/TaskIcon.tsx
@@ -0,0 +1,41 @@
+/*!
+ * 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 { createIcon } from "@chakra-ui/react";
+
+export const TaskIcon = createIcon({
+ defaultProps: {
+ height: "1em",
+ width: "1em",
+ },
+ displayName: "Task Icon",
+ path: (
+
+
+
+
+
+ ),
+ viewBox: "0 0 16 16",
+});
diff --git a/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx b/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
index 05726989a91d6..d8a2ffe158c24 100644
--- a/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
+++ b/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
@@ -16,14 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
-import {
- Heading,
- VStack,
- HStack,
- Box,
- SimpleGrid,
- Text,
-} from "@chakra-ui/react";
+import { Heading, VStack, Box, SimpleGrid, Text, Link } from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
import type {
TaskResponse,
@@ -36,22 +30,27 @@ import { Status } from "src/components/ui";
import { TaskRecentRuns } from "./TaskRecentRuns.tsx";
type Props = {
+ readonly dagId: string;
readonly task: TaskResponse;
readonly taskInstances: Array;
};
-export const TaskCard = ({ task, taskInstances }: Props) => (
+export const TaskCard = ({ dagId, task, taskInstances }: Props) => (
-
- {task.task_display_name ?? task.task_id}
- {task.is_mapped ? "[]" : undefined}
-
-
+
+
+ {task.task_display_name ?? task.task_id}
+ {task.is_mapped ? "[]" : undefined}
+
+
+
Operator
@@ -70,14 +69,18 @@ export const TaskCard = ({ task, taskInstances }: Props) => (
{taskInstances[0] ? (
-
-
- {taskInstances[0].state === null ? undefined : (
-
- {taskInstances[0].state}
-
- )}
-
+
+
+
+ {taskInstances[0].state === null ? undefined : (
+
+ {taskInstances[0].state}
+
+ )}
+
+
) : undefined}
diff --git a/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx b/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
index 4b844ad2b64f9..4b76110099e15 100644
--- a/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
+++ b/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
@@ -36,10 +36,12 @@ import { pluralize } from "src/utils";
import { TaskCard } from "./TaskCard";
const cardDef = (
+ dagId: string,
taskInstances?: Array,
): CardDef => ({
card: ({ row }) => (
{
- const { dagId } = useParams();
+ const { dagId = "" } = useParams();
const {
data,
error: tasksError,
isFetching,
isLoading,
} = useTaskServiceGetTasks({
- dagId: dagId ?? "",
+ dagId,
});
// TODO: Replace dagIdPattern with dagId once supported for better matching
const { data: runsData } = useDagsServiceRecentDagRuns(
- { dagIdPattern: dagId ?? "", dagRunsLimit: 14 },
+ { dagIdPattern: dagId, dagRunsLimit: 14 },
undefined,
{
enabled: Boolean(dagId),
@@ -85,7 +87,7 @@ export const Tasks = () => {
const { data: taskInstancesResponse } =
useTaskInstanceServiceGetTaskInstances(
{
- dagId: dagId ?? "",
+ dagId,
dagRunId: "~",
logicalDateGte: runs.at(-1)?.logical_date ?? "",
},
@@ -100,7 +102,10 @@ export const Tasks = () => {
{pluralize("Task", data ? data.total_entries : 0)}
{
{latestRun ? (
-
+
+
+
+
+
) : undefined}
diff --git a/airflow/ui/src/pages/Run/TaskInstances.tsx b/airflow/ui/src/pages/Run/TaskInstances.tsx
index 9503b2e20ad2e..95b1ebf3c92f7 100644
--- a/airflow/ui/src/pages/Run/TaskInstances.tsx
+++ b/airflow/ui/src/pages/Run/TaskInstances.tsx
@@ -41,16 +41,8 @@ const columns: Array> = [
),
- header: "Task ID",
- },
- {
- accessorKey: "map_index",
- header: "Map Index",
- },
- {
- accessorKey: "try_number",
enableSorting: false,
- header: "Try Number",
+ header: "Task ID",
},
{
accessorKey: "state",
@@ -61,11 +53,6 @@ const columns: Array> = [
}) => {state},
header: () => "State",
},
- {
- accessorKey: "operator",
- enableSorting: false,
- header: "Operator",
- },
{
accessorKey: "start_date",
cell: ({ row: { original } }) => ,
@@ -76,6 +63,22 @@ const columns: Array> = [
cell: ({ row: { original } }) => ,
header: "End Date",
},
+ {
+ accessorKey: "map_index",
+ header: "Map Index",
+ },
+
+ {
+ accessorKey: "try_number",
+ enableSorting: false,
+ header: "Try Number",
+ },
+ {
+ accessorKey: "operator",
+ enableSorting: false,
+ header: "Operator",
+ },
+
{
cell: ({ row: { original } }) =>
`${dayjs.duration(dayjs(original.end_date).diff(original.start_date)).asSeconds().toFixed(2)}s`,
diff --git a/airflow/ui/src/pages/Task/Header.tsx b/airflow/ui/src/pages/Task/Header.tsx
new file mode 100644
index 0000000000000..3bfb10479f4a1
--- /dev/null
+++ b/airflow/ui/src/pages/Task/Header.tsx
@@ -0,0 +1,53 @@
+/*!
+ * 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, Flex, Heading, HStack, SimpleGrid, Text } from "@chakra-ui/react";
+
+import type { TaskResponse } from "openapi/requests/types.gen";
+import { TaskIcon } from "src/assets/TaskIcon";
+import DagDocumentation from "src/components/DagDocumentation";
+import { Stat } from "src/components/Stat";
+
+export const Header = ({ task }: { readonly task: TaskResponse }) => (
+
+
+
+
+
+ Task:
+ {task.task_display_name}
+ {task.is_mapped ? "[ ]" : ""}
+
+
+
+
+
+ {task.doc_md === null ? undefined : (
+
+ )}
+
+
+
+ {task.operator_name}
+
+
+ {task.trigger_rule}
+
+
+
+);
diff --git a/airflow/ui/src/pages/Task/Instances.tsx b/airflow/ui/src/pages/Task/Instances.tsx
new file mode 100644
index 0000000000000..af72c865c8901
--- /dev/null
+++ b/airflow/ui/src/pages/Task/Instances.tsx
@@ -0,0 +1,129 @@
+/*!
+ * 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, Link } from "@chakra-ui/react";
+import type { ColumnDef } from "@tanstack/react-table";
+import dayjs from "dayjs";
+import { Link as RouterLink, useParams } from "react-router-dom";
+
+import {
+ useTaskInstanceServiceGetTaskInstances,
+ useTaskServiceGetTask,
+} from "openapi/queries";
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import { DataTable } from "src/components/DataTable";
+import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { ErrorAlert } from "src/components/ErrorAlert";
+import Time from "src/components/Time";
+import { Status } from "src/components/ui";
+
+const columns = (
+ isMapped?: boolean,
+): Array> => [
+ {
+ accessorKey: "dag_run_id",
+ cell: ({ row: { original } }) => (
+
+
+ {original.dag_run_id}
+
+
+ ),
+ enableSorting: false,
+ header: "Dag Run ID",
+ },
+ {
+ accessorKey: "state",
+ cell: ({
+ row: {
+ original: { state },
+ },
+ }) => {state},
+ header: () => "State",
+ },
+ {
+ accessorKey: "start_date",
+ cell: ({ row: { original } }) => ,
+ header: "Start Date",
+ },
+ {
+ accessorKey: "end_date",
+ cell: ({ row: { original } }) => ,
+ header: "End Date",
+ },
+ ...(isMapped
+ ? [
+ {
+ accessorKey: "map_index",
+ header: "Map Index",
+ },
+ ]
+ : []),
+ {
+ accessorKey: "try_number",
+ enableSorting: false,
+ header: "Try Number",
+ },
+ {
+ cell: ({ row: { original } }) =>
+ `${dayjs.duration(dayjs(original.end_date).diff(original.start_date)).asSeconds().toFixed(2)}s`,
+ header: "Duration",
+ },
+];
+
+export const Instances = () => {
+ const { dagId = "", taskId } = useParams();
+ const { setTableURLState, tableURLState } = useTableURLState();
+ const { pagination, sorting } = tableURLState;
+ const [sort] = sorting;
+ const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : undefined;
+
+ const {
+ data: task,
+ error: taskError,
+ isLoading: isTaskLoading,
+ } = useTaskServiceGetTask({ dagId, taskId });
+
+ const { data, error, isFetching, isLoading } =
+ useTaskInstanceServiceGetTaskInstances({
+ dagId,
+ dagRunId: "~",
+ limit: pagination.pageSize,
+ offset: pagination.pageIndex * pagination.pageSize,
+ orderBy,
+ taskId,
+ });
+
+ return (
+
+ }
+ initialState={tableURLState}
+ isFetching={isFetching}
+ isLoading={isLoading || isTaskLoading}
+ modelName="Task Instance"
+ onStateChange={setTableURLState}
+ total={data?.total_entries}
+ />
+
+ );
+};
diff --git a/airflow/ui/src/pages/Task/Task.tsx b/airflow/ui/src/pages/Task/Task.tsx
new file mode 100644
index 0000000000000..dde59584af7bf
--- /dev/null
+++ b/airflow/ui/src/pages/Task/Task.tsx
@@ -0,0 +1,90 @@
+/*!
+ * 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 { LiaSlashSolid } from "react-icons/lia";
+import { useParams, Link as RouterLink } from "react-router-dom";
+
+import {
+ useDagServiceGetDagDetails,
+ useTaskServiceGetTask,
+} from "openapi/queries";
+import { Breadcrumb } from "src/components/ui";
+import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
+
+import { Header } from "./Header";
+
+const tabs = [
+ { label: "Task Instances", value: "" },
+ { label: "Events", value: "events" },
+];
+
+export const Task = () => {
+ const { dagId = "", taskId = "" } = useParams();
+
+ const {
+ data: task,
+ error,
+ isLoading,
+ } = useTaskServiceGetTask({ dagId, taskId });
+
+ const {
+ data: dag,
+ error: dagError,
+ isLoading: isDagLoading,
+ } = useDagServiceGetDagDetails({
+ dagId,
+ });
+
+ const links = [
+ { label: "Dags", value: "/dags" },
+ { label: dag?.dag_display_name ?? dagId, value: `/dags/${dagId}` },
+ { label: task?.task_display_name ?? taskId },
+ ];
+
+ return (
+
+ }>
+ {links.map((link, index) => {
+ if (index === links.length - 1) {
+ return (
+
+ {link.label}
+
+ );
+ }
+
+ return link.value === undefined ? (
+
+ {link.label}
+
+ ) : (
+
+ {link.label}
+
+ );
+ })}
+
+ {task === undefined ? undefined : }
+
+ );
+};
diff --git a/airflow/ui/src/pages/Task/index.ts b/airflow/ui/src/pages/Task/index.ts
new file mode 100644
index 0000000000000..99691b18e5261
--- /dev/null
+++ b/airflow/ui/src/pages/Task/index.ts
@@ -0,0 +1,21 @@
+/*!
+ * 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 * from "./Task";
+export * from "./Instances";
diff --git a/airflow/ui/src/router.tsx b/airflow/ui/src/router.tsx
index 938bc91c8413a..cd8da392dbc6f 100644
--- a/airflow/ui/src/router.tsx
+++ b/airflow/ui/src/router.tsx
@@ -30,6 +30,7 @@ import { ErrorPage } from "src/pages/Error";
import { Events } from "src/pages/Events";
import { Run } from "src/pages/Run";
import { TaskInstances } from "src/pages/Run/TaskInstances";
+import { Task, Instances } from "src/pages/Task";
import { TaskInstance } from "src/pages/TaskInstance";
import { Variables } from "./pages/Variables";
@@ -85,6 +86,14 @@ export const router = createBrowserRouter(
element: ,
path: "dags/:dagId/runs/:runId/tasks/:taskId",
},
+ {
+ children: [
+ { element: , index: true },
+ { element: , path: "events" },
+ ],
+ element: ,
+ path: "dags/:dagId/tasks/:taskId",
+ },
],
element: ,
errorElement: (