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 97cb24f0f57 Implement card view for tasks view of a dag (#44604)
97cb24f0f57 is described below

commit 97cb24f0f57c4475862b855688af292f3a8b397a
Author: Karthikeyan Singaravelan <[email protected]>
AuthorDate: Fri Dec 6 20:50:49 2024 +0530

    Implement card view for tasks view of a dag (#44604)
    
    * Initial commit for tasks tab.
    
    * Remove unused columns for card view.
    
    * Fetch latest dag run and then fetch corresponding task instances.
    
    * Make font to be consistent.
    
    * Pass dagRunsLimit to limit the number of dagruns.
    
    * Add recent task instance plot to card.
    
    * Fix merge conflicts and use Status component.
    
    * Make task name bold
    Change Last Run to Last Instance in header name
    Use start date and wrap it with Time tag
    Show try number only when greater than 1
    Use pluralize for task
    
    * Refactor tooltip to a separate component. Fix variable name casing.
    
    * Fix duration when null.
    
    * Use children prop to simplify TaskInstanceTooltip wrapping.
    
    * Omit content to reuse tooltip type.
---
 airflow/ui/src/components/TaskInstanceTooltip.tsx  |  62 +++++++++++
 .../ui/src/pages/DagsList/Dag/Tasks/TaskCard.tsx   |  88 ++++++++++++++++
 .../pages/DagsList/Dag/Tasks/TaskRecentRuns.tsx    |  76 ++++++++++++++
 airflow/ui/src/pages/DagsList/Dag/Tasks/Tasks.tsx  | 114 +++++++++++++++++++++
 airflow/ui/src/pages/DagsList/Dag/Tasks/index.ts   |  20 ++++
 airflow/ui/src/router.tsx                          |   3 +-
 6 files changed, 362 insertions(+), 1 deletion(-)

diff --git a/airflow/ui/src/components/TaskInstanceTooltip.tsx 
b/airflow/ui/src/components/TaskInstanceTooltip.tsx
new file mode 100644
index 00000000000..0b82567ae83
--- /dev/null
+++ b/airflow/ui/src/components/TaskInstanceTooltip.tsx
@@ -0,0 +1,62 @@
+/*!
+ * 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, Text } from "@chakra-ui/react";
+
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+import { Tooltip, type TooltipProps } from "src/components/ui";
+
+type Props = {
+  readonly taskInstance: TaskInstanceResponse;
+} & Omit<TooltipProps, "content">;
+
+const TaskInstanceTooltip = ({ children, taskInstance }: Props) => (
+  <Tooltip
+    content={
+      <Box>
+        <Text>Run ID: {taskInstance.dag_run_id}</Text>
+        <Text>Logical Date: {taskInstance.logical_date}</Text>
+        <Text>
+          Start Date: <Time datetime={taskInstance.start_date} />
+        </Text>
+        <Text>
+          End Date: <Time datetime={taskInstance.end_date} />
+        </Text>
+        {taskInstance.try_number > 1 && (
+          <Text>Try Number: {taskInstance.try_number}</Text>
+        )}
+        <Text>Duration: {taskInstance.duration?.toFixed(2) ?? 0}s</Text>
+        <Text>State: {taskInstance.state}</Text>
+      </Box>
+    }
+    key={taskInstance.dag_run_id}
+    positioning={{
+      offset: {
+        crossAxis: 5,
+        mainAxis: 5,
+      },
+      placement: "bottom-start",
+    }}
+    showArrow
+  >
+    {children}
+  </Tooltip>
+);
+
+export default TaskInstanceTooltip;
diff --git a/airflow/ui/src/pages/DagsList/Dag/Tasks/TaskCard.tsx 
b/airflow/ui/src/pages/DagsList/Dag/Tasks/TaskCard.tsx
new file mode 100644
index 00000000000..05726989a91
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/Dag/Tasks/TaskCard.tsx
@@ -0,0 +1,88 @@
+/*!
+ * 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 {
+  Heading,
+  VStack,
+  HStack,
+  Box,
+  SimpleGrid,
+  Text,
+} from "@chakra-ui/react";
+
+import type {
+  TaskResponse,
+  TaskInstanceResponse,
+} from "openapi/requests/types.gen";
+import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
+import Time from "src/components/Time";
+import { Status } from "src/components/ui";
+
+import { TaskRecentRuns } from "./TaskRecentRuns.tsx";
+
+type Props = {
+  readonly task: TaskResponse;
+  readonly taskInstances: Array<TaskInstanceResponse>;
+};
+
+export const TaskCard = ({ task, taskInstances }: Props) => (
+  <Box
+    borderColor="border.emphasized"
+    borderRadius={8}
+    borderWidth={1}
+    overflow="hidden"
+  >
+    <Text bg="bg.info" color="fg.info" fontWeight="bold" p={2}>
+      {task.task_display_name ?? task.task_id}
+      {task.is_mapped ? "[]" : undefined}
+    </Text>
+    <SimpleGrid columns={4} gap={4} height={20} px={3} py={2}>
+      <VStack align="flex-start" gap={1}>
+        <Heading color="fg.muted" fontSize="xs">
+          Operator
+        </Heading>
+        <Text fontSize="sm">{task.operator_name}</Text>
+      </VStack>
+      <VStack align="flex-start" gap={1}>
+        <Heading color="fg.muted" fontSize="xs">
+          Trigger Rule
+        </Heading>
+        <Text fontSize="sm">{task.trigger_rule}</Text>
+      </VStack>
+      <VStack align="flex-start" gap={1}>
+        <Heading color="fg.muted" fontSize="xs">
+          Last Instance
+        </Heading>
+        {taskInstances[0] ? (
+          <TaskInstanceTooltip taskInstance={taskInstances[0]}>
+            <HStack fontSize="sm">
+              <Time datetime={taskInstances[0].start_date} />
+              {taskInstances[0].state === null ? undefined : (
+                <Status state={taskInstances[0].state}>
+                  {taskInstances[0].state}
+                </Status>
+              )}
+            </HStack>
+          </TaskInstanceTooltip>
+        ) : undefined}
+      </VStack>
+      {/* TODO: Handled mapped tasks to not plot each map index as a task 
instance */}
+      {!task.is_mapped && <TaskRecentRuns taskInstances={taskInstances} />}
+    </SimpleGrid>
+  </Box>
+);
diff --git a/airflow/ui/src/pages/DagsList/Dag/Tasks/TaskRecentRuns.tsx 
b/airflow/ui/src/pages/DagsList/Dag/Tasks/TaskRecentRuns.tsx
new file mode 100644
index 00000000000..a69d7d72790
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/Dag/Tasks/TaskRecentRuns.tsx
@@ -0,0 +1,76 @@
+/*!
+ * 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 { Flex } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
+import { stateColor } from "src/utils/stateColor";
+
+dayjs.extend(duration);
+
+const BAR_HEIGHT = 60;
+
+export const TaskRecentRuns = ({
+  taskInstances,
+}: {
+  readonly taskInstances: Array<TaskInstanceResponse>;
+}) => {
+  if (!taskInstances.length) {
+    return undefined;
+  }
+
+  const taskInstancesWithDuration = taskInstances.map((taskInstance) => ({
+    ...taskInstance,
+    duration:
+      dayjs
+        .duration(dayjs(taskInstance.end_date).diff(taskInstance.start_date))
+        .asSeconds() || 0,
+  }));
+
+  const max = Math.max.apply(
+    undefined,
+    taskInstancesWithDuration.map((taskInstance) => taskInstance.duration),
+  );
+
+  return (
+    <Flex alignItems="flex-end" flexDirection="row-reverse">
+      {taskInstancesWithDuration.map((taskInstance) =>
+        taskInstance.state === null ? undefined : (
+          <TaskInstanceTooltip
+            key={taskInstance.dag_run_id}
+            taskInstance={taskInstance}
+          >
+            <Box p={1}>
+              <Box
+                bg={stateColor[taskInstance.state]}
+                borderRadius="4px"
+                height={`${(taskInstance.duration / max) * BAR_HEIGHT}px`}
+                minHeight={1}
+                width="4px"
+              />
+            </Box>
+          </TaskInstanceTooltip>
+        ),
+      )}
+    </Flex>
+  );
+};
diff --git a/airflow/ui/src/pages/DagsList/Dag/Tasks/Tasks.tsx 
b/airflow/ui/src/pages/DagsList/Dag/Tasks/Tasks.tsx
new file mode 100644
index 00000000000..4b844ad2b64
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/Dag/Tasks/Tasks.tsx
@@ -0,0 +1,114 @@
+/*!
+ * 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 { Heading, Skeleton, Box } from "@chakra-ui/react";
+import { useParams } from "react-router-dom";
+
+import {
+  useTaskServiceGetTasks,
+  useTaskInstanceServiceGetTaskInstances,
+  useDagsServiceRecentDagRuns,
+} from "openapi/queries";
+import type {
+  TaskResponse,
+  TaskInstanceResponse,
+} from "openapi/requests/types.gen";
+import { DataTable } from "src/components/DataTable";
+import type { CardDef } from "src/components/DataTable/types";
+import { ErrorAlert } from "src/components/ErrorAlert";
+import { pluralize } from "src/utils";
+
+import { TaskCard } from "./TaskCard";
+
+const cardDef = (
+  taskInstances?: Array<TaskInstanceResponse>,
+): CardDef<TaskResponse> => ({
+  card: ({ row }) => (
+    <TaskCard
+      task={row}
+      taskInstances={
+        taskInstances
+          ? taskInstances.filter(
+              (instance: TaskInstanceResponse) =>
+                instance.task_id === row.task_id,
+            )
+          : []
+      }
+    />
+  ),
+  meta: {
+    customSkeleton: <Skeleton height="120px" width="100%" />,
+  },
+});
+
+export const Tasks = () => {
+  const { dagId } = useParams();
+  const {
+    data,
+    error: tasksError,
+    isFetching,
+    isLoading,
+  } = useTaskServiceGetTasks({
+    dagId: dagId ?? "",
+  });
+
+  // TODO: Replace dagIdPattern with dagId once supported for better matching
+  const { data: runsData } = useDagsServiceRecentDagRuns(
+    { dagIdPattern: dagId ?? "", dagRunsLimit: 14 },
+    undefined,
+    {
+      enabled: Boolean(dagId),
+    },
+  );
+
+  const runs =
+    runsData?.dags.find((dagWithRuns) => dagWithRuns.dag_id === dagId)
+      ?.latest_dag_runs ?? [];
+
+  // TODO: Revisit this endpoint since only 100 task instances are returned and
+  // only duration is calculated with other attributes unused.
+  const { data: taskInstancesResponse } =
+    useTaskInstanceServiceGetTaskInstances(
+      {
+        dagId: dagId ?? "",
+        dagRunId: "~",
+        logicalDateGte: runs.at(-1)?.logical_date ?? "",
+      },
+      undefined,
+      { enabled: Boolean(runs[0]?.dag_run_id) },
+    );
+
+  return (
+    <Box>
+      <ErrorAlert error={tasksError} />
+      <Heading my={1} size="md">
+        {pluralize("Task", data ? data.total_entries : 0)}
+      </Heading>
+      <DataTable
+        cardDef={cardDef(taskInstancesResponse?.task_instances.reverse())}
+        columns={[]}
+        data={data ? data.tasks : []}
+        displayMode="card"
+        isFetching={isFetching}
+        isLoading={isLoading}
+        modelName="Task"
+        total={data ? data.total_entries : 0} // Todo : Disable pagination?
+      />
+    </Box>
+  );
+};
diff --git a/airflow/ui/src/pages/DagsList/Dag/Tasks/index.ts 
b/airflow/ui/src/pages/DagsList/Dag/Tasks/index.ts
new file mode 100644
index 00000000000..c5e32f6ebc6
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/Dag/Tasks/index.ts
@@ -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 { Tasks } from "./Tasks";
diff --git a/airflow/ui/src/router.tsx b/airflow/ui/src/router.tsx
index 6c5081c35e2..95b881e019e 100644
--- a/airflow/ui/src/router.tsx
+++ b/airflow/ui/src/router.tsx
@@ -24,6 +24,7 @@ import { Dag } from "src/pages/DagsList/Dag";
 import { Code } from "src/pages/DagsList/Dag/Code";
 import { Overview } from "src/pages/DagsList/Dag/Overview";
 import { Runs } from "src/pages/DagsList/Dag/Runs";
+import { Tasks } from "src/pages/DagsList/Dag/Tasks";
 import { Run } from "src/pages/DagsList/Run";
 import { Dashboard } from "src/pages/Dashboard";
 import { ErrorPage } from "src/pages/Error";
@@ -49,7 +50,7 @@ export const router = createBrowserRouter(
           children: [
             { element: <Overview />, index: true },
             { element: <Runs />, path: "runs" },
-            { element: <div>Tasks</div>, path: "tasks" },
+            { element: <Tasks />, path: "tasks" },
             { element: <Events />, path: "events" },
             { element: <Code />, path: "code" },
           ],

Reply via email to