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 1c3789194c Add datasets to dag graph (#37604)
1c3789194c is described below

commit 1c3789194cf1d1af557cefd939395c047b2b122a
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu Feb 22 14:24:20 2024 -0500

    Add datasets to dag graph (#37604)
    
    * Add dataset nodes to dag graph
    
    * Clean up dataset nodes
---
 airflow/www/static/js/api/index.ts                 |   4 +-
 airflow/www/static/js/api/useDatasets.ts           |  60 ++----
 .../api/{useDatasets.ts => useDatasetsSummary.ts}  |   6 +-
 .../graph/{Node.test.tsx => DagNode.test.tsx}      |  12 +-
 .../js/dag/details/graph/{Node.tsx => DagNode.tsx} |  75 +------
 .../static/js/dag/details/graph/DatasetNode.tsx    | 112 +++++++++++
 airflow/www/static/js/dag/details/graph/Node.tsx   | 218 ++++-----------------
 airflow/www/static/js/dag/details/graph/index.tsx  |  61 +++++-
 airflow/www/static/js/datasets/List.test.tsx       |   2 +-
 airflow/www/static/js/datasets/List.tsx            |   6 +-
 airflow/www/static/js/types/index.ts               |   8 +-
 airflow/www/static/js/utils/graph.ts               |   3 +-
 airflow/www/templates/airflow/dag.html             |   1 +
 airflow/www/templates/airflow/datasets.html        |   2 +-
 14 files changed, 246 insertions(+), 324 deletions(-)

diff --git a/airflow/www/static/js/api/index.ts 
b/airflow/www/static/js/api/index.ts
index 6369a819d2..d703feaef9 100644
--- a/airflow/www/static/js/api/index.ts
+++ b/airflow/www/static/js/api/index.ts
@@ -33,6 +33,7 @@ import useGraphData from "./useGraphData";
 import useGridData from "./useGridData";
 import useMappedInstances from "./useMappedInstances";
 import useDatasets from "./useDatasets";
+import useDatasetsSummary from "./useDatasetsSummary";
 import useDataset from "./useDataset";
 import useDatasetDependencies from "./useDatasetDependencies";
 import useDatasetEvents from "./useDatasetEvents";
@@ -72,9 +73,10 @@ export {
   useDagRuns,
   useDags,
   useDataset,
+  useDatasets,
   useDatasetDependencies,
   useDatasetEvents,
-  useDatasets,
+  useDatasetsSummary,
   useExtraLinks,
   useGraphData,
   useGridData,
diff --git a/airflow/www/static/js/api/useDatasets.ts 
b/airflow/www/static/js/api/useDatasets.ts
index 6150f51ce3..db46415062 100644
--- a/airflow/www/static/js/api/useDatasets.ts
+++ b/airflow/www/static/js/api/useDatasets.ts
@@ -21,65 +21,29 @@ import axios, { AxiosResponse } from "axios";
 import { useQuery } from "react-query";
 
 import { getMetaValue } from "src/utils";
-import type { DatasetListItem } from "src/types";
-import type { unitOfTime } from "moment";
-
-export interface DatasetsData {
-  datasets: DatasetListItem[];
-  totalEntries: number;
-}
-
-export interface DateOption {
-  count: number;
-  unit: unitOfTime.DurationConstructor;
-}
+import type { API } from "src/types";
 
 interface Props {
-  limit?: number;
-  offset?: number;
-  order?: string;
-  uri?: string;
-  updatedAfter?: DateOption;
+  dagIds?: string[];
+  enabled?: boolean;
 }
 
-export default function useDatasets({
-  limit,
-  offset,
-  order,
-  uri,
-  updatedAfter,
-}: Props) {
-  const query = useQuery(
-    ["datasets", limit, offset, order, uri, updatedAfter],
+export default function useDatasets({ dagIds, enabled = true }: Props) {
+  return useQuery(
+    ["datasets", dagIds],
     () => {
       const datasetsUrl = getMetaValue("datasets_api");
-      const orderParam = order ? { order_by: order } : {};
-      const uriParam = uri ? { uri_pattern: uri } : {};
-      const updatedAfterParam =
-        updatedAfter && updatedAfter.count && updatedAfter.unit
-          ? {
-              // @ts-ignore
-              updated_after: moment()
-                .subtract(updatedAfter.count, updatedAfter.unit)
-                .toISOString(),
-            }
-          : {};
-      return axios.get<AxiosResponse, DatasetsData>(datasetsUrl, {
+      const dagIdsParam =
+        dagIds && dagIds.length ? { dag_ids: dagIds.join(",") } : {};
+
+      return axios.get<AxiosResponse, API.DatasetCollection>(datasetsUrl, {
         params: {
-          offset,
-          limit,
-          ...orderParam,
-          ...uriParam,
-          ...updatedAfterParam,
+          ...dagIdsParam,
         },
       });
     },
     {
-      keepPreviousData: true,
+      enabled,
     }
   );
-  return {
-    ...query,
-    data: query.data ?? { datasets: [], totalEntries: 0 },
-  };
 }
diff --git a/airflow/www/static/js/api/useDatasets.ts 
b/airflow/www/static/js/api/useDatasetsSummary.ts
similarity index 92%
copy from airflow/www/static/js/api/useDatasets.ts
copy to airflow/www/static/js/api/useDatasetsSummary.ts
index 6150f51ce3..6f902946f6 100644
--- a/airflow/www/static/js/api/useDatasets.ts
+++ b/airflow/www/static/js/api/useDatasetsSummary.ts
@@ -42,7 +42,7 @@ interface Props {
   updatedAfter?: DateOption;
 }
 
-export default function useDatasets({
+export default function useDatasetsSummary({
   limit,
   offset,
   order,
@@ -50,9 +50,9 @@ export default function useDatasets({
   updatedAfter,
 }: Props) {
   const query = useQuery(
-    ["datasets", limit, offset, order, uri, updatedAfter],
+    ["datasets_summary", limit, offset, order, uri, updatedAfter],
     () => {
-      const datasetsUrl = getMetaValue("datasets_api");
+      const datasetsUrl = getMetaValue("datasets_summary");
       const orderParam = order ? { order_by: order } : {};
       const uriParam = uri ? { uri_pattern: uri } : {};
       const updatedAfterParam =
diff --git a/airflow/www/static/js/dag/details/graph/Node.test.tsx 
b/airflow/www/static/js/dag/details/graph/DagNode.test.tsx
similarity index 91%
rename from airflow/www/static/js/dag/details/graph/Node.test.tsx
rename to airflow/www/static/js/dag/details/graph/DagNode.test.tsx
index a01114ec04..34ddac7506 100644
--- a/airflow/www/static/js/dag/details/graph/Node.test.tsx
+++ b/airflow/www/static/js/dag/details/graph/DagNode.test.tsx
@@ -26,7 +26,8 @@ import { Wrapper } from "src/utils/testUtils";
 
 import type { NodeProps } from "reactflow";
 import type { Task, TaskInstance } from "src/types";
-import { CustomNodeProps, BaseNode as Node } from "./Node";
+import type { CustomNodeProps } from "./Node";
+import DagNode from "./DagNode";
 
 const mockNode: NodeProps<CustomNodeProps> = {
   id: "task_id",
@@ -34,6 +35,7 @@ const mockNode: NodeProps<CustomNodeProps> = {
     label: "task_id",
     height: 50,
     width: 200,
+    class: "dag",
     instance: {
       state: "success",
       runId: "run_id",
@@ -65,7 +67,7 @@ const mockNode: NodeProps<CustomNodeProps> = {
 
 describe("Test Graph Node", () => {
   test("Renders normal task correctly", async () => {
-    const { getByText, getByTestId } = render(<Node {...mockNode} />, {
+    const { getByText, getByTestId } = render(<DagNode {...mockNode} />, {
       wrapper: Wrapper,
     });
 
@@ -77,7 +79,7 @@ describe("Test Graph Node", () => {
 
   test("Renders mapped task correctly", async () => {
     const { getByText } = render(
-      <Node
+      <DagNode
         {...mockNode}
         data={{
           ...mockNode.data,
@@ -99,7 +101,7 @@ describe("Test Graph Node", () => {
 
   test("Renders task group correctly", async () => {
     const { getByText } = render(
-      <Node
+      <DagNode
         {...mockNode}
         data={{ ...mockNode.data, childCount: 5, isOpen: false }}
       />,
@@ -114,7 +116,7 @@ describe("Test Graph Node", () => {
 
   test("Renders normal task correctly", async () => {
     const { getByTestId } = render(
-      <Node {...mockNode} data={{ ...mockNode.data, isActive: false }} />,
+      <DagNode {...mockNode} data={{ ...mockNode.data, isActive: false }} />,
       {
         wrapper: Wrapper,
       }
diff --git a/airflow/www/static/js/dag/details/graph/Node.tsx 
b/airflow/www/static/js/dag/details/graph/DagNode.tsx
similarity index 74%
copy from airflow/www/static/js/dag/details/graph/Node.tsx
copy to airflow/www/static/js/dag/details/graph/DagNode.tsx
index 4ce193066c..06809249ed 100644
--- a/airflow/www/static/js/dag/details/graph/Node.tsx
+++ b/airflow/www/static/js/dag/details/graph/DagNode.tsx
@@ -18,38 +18,20 @@
  */
 
 import React from "react";
-import { Box, Text, Flex } from "@chakra-ui/react";
-import { Handle, NodeProps, Position } from "reactflow";
+import { Box, Flex, Text } from "@chakra-ui/react";
+import type { NodeProps } from "reactflow";
 
 import { SimpleStatus } from "src/dag/StatusBox";
 import useSelection from "src/dag/useSelection";
-import type { DagRun, Task, TaskInstance } from "src/types";
 import { getGroupAndMapSummary, hoverDelay } from "src/utils";
 import Tooltip from "src/components/Tooltip";
 import InstanceTooltip from "src/dag/InstanceTooltip";
 import { useContainerRef } from "src/context/containerRef";
 import TaskName from "src/dag/TaskName";
 
-export interface CustomNodeProps {
-  label: string;
-  height?: number;
-  width?: number;
-  isJoinNode?: boolean;
-  instance?: TaskInstance;
-  task?: Task | null;
-  isSelected: boolean;
-  latestDagRunId: DagRun["runId"];
-  childCount?: number;
-  onToggleCollapse: () => void;
-  isOpen?: boolean;
-  isActive?: boolean;
-  setupTeardownType?: "setup" | "teardown";
-  labelStyle?: string;
-  style?: string;
-  isZoomedOut: boolean;
-}
+import type { CustomNodeProps } from "./Node";
 
-export const BaseNode = ({
+const DagNode = ({
   id,
   data: {
     label,
@@ -170,9 +152,7 @@ export const BaseNode = ({
                 maxWidth={`calc(${width}px - 12px)`}
                 fontWeight={400}
                 fontSize="md"
-                width="fit-content"
                 color={operatorTextColor}
-                px={1}
               >
                 {task.operator}
               </Text>
@@ -184,49 +164,4 @@ export const BaseNode = ({
   );
 };
 
-const Node = (props: NodeProps<CustomNodeProps>) => {
-  const {
-    data: { height, width, isJoinNode, task },
-  } = props;
-  if (isJoinNode) {
-    return (
-      <>
-        <Handle
-          type="target"
-          position={Position.Top}
-          style={{ visibility: "hidden" }}
-        />
-        <Box
-          height={`${height}px`}
-          width={`${width}px`}
-          borderRadius={width}
-          bg="gray.400"
-        />
-        <Handle
-          type="source"
-          position={Position.Bottom}
-          style={{ visibility: "hidden" }}
-        />
-      </>
-    );
-  }
-
-  if (!task) return null;
-  return (
-    <>
-      <Handle
-        type="target"
-        position={Position.Top}
-        style={{ visibility: "hidden" }}
-      />
-      <BaseNode {...props} />
-      <Handle
-        type="source"
-        position={Position.Bottom}
-        style={{ visibility: "hidden" }}
-      />
-    </>
-  );
-};
-
-export default Node;
+export default DagNode;
diff --git a/airflow/www/static/js/dag/details/graph/DatasetNode.tsx 
b/airflow/www/static/js/dag/details/graph/DatasetNode.tsx
new file mode 100644
index 0000000000..921341643b
--- /dev/null
+++ b/airflow/www/static/js/dag/details/graph/DatasetNode.tsx
@@ -0,0 +1,112 @@
+/*!
+ * 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 React from "react";
+import {
+  Box,
+  Link,
+  Popover,
+  PopoverArrow,
+  PopoverBody,
+  PopoverCloseButton,
+  PopoverContent,
+  PopoverHeader,
+  PopoverTrigger,
+  Portal,
+  Text,
+} from "@chakra-ui/react";
+import { HiDatabase } from "react-icons/hi";
+import type { NodeProps } from "reactflow";
+
+import { getMetaValue } from "src/utils";
+import { useContainerRef } from "src/context/containerRef";
+import type { CustomNodeProps } from "./Node";
+
+const datasetsUrl = getMetaValue("datasets_url");
+
+const DatasetNode = ({
+  data: { label, height, width, latestDagRunId, isZoomedOut },
+}: NodeProps<CustomNodeProps>) => {
+  const containerRef = useContainerRef();
+
+  return (
+    <Popover>
+      <PopoverTrigger>
+        <Box
+          borderRadius={isZoomedOut ? 10 : 5}
+          borderWidth={1}
+          borderColor="gray.400"
+          bg="white"
+          height={`${height}px`}
+          width={`${width}px`}
+          cursor={latestDagRunId ? "cursor" : "default"}
+          data-testid="node"
+          px={isZoomedOut ? 1 : 2}
+          mt={isZoomedOut ? -2 : 0}
+        >
+          <Text
+            fontWeight="bold"
+            mt={isZoomedOut ? -2 : 0}
+            noOfLines={2}
+            fontSize={isZoomedOut ? 24 : undefined}
+            textAlign="justify"
+          >
+            {label}
+          </Text>
+          {!isZoomedOut && (
+            <Text
+              maxWidth={`calc(${width}px - 12px)`}
+              fontWeight={400}
+              fontSize="md"
+              textAlign="justify"
+              color="gray.500"
+            >
+              <HiDatabase
+                size="16px"
+                style={{
+                  display: "inline",
+                  verticalAlign: "middle",
+                  marginRight: "3px",
+                }}
+              />
+              Dataset
+            </Text>
+          )}
+        </Box>
+      </PopoverTrigger>
+      <Portal containerRef={containerRef}>
+        <PopoverContent bg="gray.100">
+          <PopoverArrow bg="gray.100" />
+          <PopoverCloseButton />
+          <PopoverHeader>{label}</PopoverHeader>
+          <PopoverBody>
+            <Link
+              color="blue"
+              href={`${datasetsUrl}?uri=${encodeURIComponent(label)}`}
+            >
+              View Dataset
+            </Link>
+          </PopoverBody>
+        </PopoverContent>
+      </Portal>
+    </Popover>
+  );
+};
+
+export default DatasetNode;
diff --git a/airflow/www/static/js/dag/details/graph/Node.tsx 
b/airflow/www/static/js/dag/details/graph/Node.tsx
index 4ce193066c..04f1d56fa0 100644
--- a/airflow/www/static/js/dag/details/graph/Node.tsx
+++ b/airflow/www/static/js/dag/details/graph/Node.tsx
@@ -18,17 +18,13 @@
  */
 
 import React from "react";
-import { Box, Text, Flex } from "@chakra-ui/react";
+import { Box } from "@chakra-ui/react";
 import { Handle, NodeProps, Position } from "reactflow";
 
-import { SimpleStatus } from "src/dag/StatusBox";
-import useSelection from "src/dag/useSelection";
-import type { DagRun, Task, TaskInstance } from "src/types";
-import { getGroupAndMapSummary, hoverDelay } from "src/utils";
-import Tooltip from "src/components/Tooltip";
-import InstanceTooltip from "src/dag/InstanceTooltip";
-import { useContainerRef } from "src/context/containerRef";
-import TaskName from "src/dag/TaskName";
+import type { DepNode, DagRun, Task, TaskInstance } from "src/types";
+
+import DagNode from "./DagNode";
+import DatasetNode from "./DatasetNode";
 
 export interface CustomNodeProps {
   label: string;
@@ -47,186 +43,42 @@ export interface CustomNodeProps {
   labelStyle?: string;
   style?: string;
   isZoomedOut: boolean;
+  class: DepNode["value"]["class"];
 }
 
-export const BaseNode = ({
-  id,
-  data: {
-    label,
-    childCount,
-    height,
-    width,
-    instance,
-    task,
-    isSelected,
-    latestDagRunId,
-    onToggleCollapse,
-    isOpen,
-    isActive,
-    setupTeardownType,
-    labelStyle,
-    style,
-    isZoomedOut,
-  },
-}: NodeProps<CustomNodeProps>) => {
-  const { onSelect } = useSelection();
-  const containerRef = useContainerRef();
-
-  if (!task) return null;
-
-  const bg = isOpen ? "blackAlpha.50" : "white";
-  const { isMapped } = task;
-  const mappedStates = instance?.mappedStates;
-
-  const { totalTasks } = getGroupAndMapSummary({ group: task, mappedStates });
-
-  const taskName = isMapped
-    ? `${label} [${instance ? totalTasks : " "}]`
-    : label;
-
-  let operatorTextColor = "";
-  let operatorBG = "";
-  if (style) {
-    [, operatorBG] = style.split(":");
-  }
-
-  if (labelStyle) {
-    [, operatorTextColor] = labelStyle.split(":");
-  }
-  if (!operatorTextColor || operatorTextColor === "#000;")
-    operatorTextColor = "gray.500";
-
-  const nodeBorderColor =
-    instance?.state && stateColors[instance.state]
-      ? `${stateColors[instance.state]}.400`
-      : "gray.400";
-
-  return (
-    <Tooltip
-      label={
-        instance && task ? (
-          <InstanceTooltip instance={instance} group={task} />
-        ) : null
-      }
-      portalProps={{ containerRef }}
-      hasArrow
-      placement="top"
-      openDelay={hoverDelay}
-    >
-      <Box
-        borderRadius={isZoomedOut ? 10 : 5}
-        borderWidth={(isSelected ? 4 : 2) * (isZoomedOut ? 3 : 1)}
-        borderColor={nodeBorderColor}
-        bg={
-          !task.children?.length && operatorBG
-            ? // Fade the operator color to clash less with the task instance 
status
-              `color-mix(in srgb, ${operatorBG.replace(";", "")} 80%, white)`
-            : bg
-        }
-        height={`${height}px`}
-        width={`${width}px`}
-        cursor={latestDagRunId ? "cursor" : "default"}
-        opacity={isActive ? 1 : 0.3}
-        transition="opacity 0.2s"
-        data-testid="node"
-        onClick={() => {
-          if (latestDagRunId) {
-            onSelect({
-              runId: instance?.runId || latestDagRunId,
-              taskId: isSelected ? undefined : id,
-            });
-          }
-        }}
-        px={isZoomedOut ? 1 : 2}
-        mt={isZoomedOut ? -2 : 0}
-      >
-        <TaskName
-          label={taskName}
-          isOpen={isOpen}
-          isGroup={!!childCount}
-          onClick={(e) => {
-            e.stopPropagation();
-            onToggleCollapse();
-          }}
-          setupTeardownType={setupTeardownType}
-          fontWeight="bold"
-          isZoomedOut={isZoomedOut}
-          mt={isZoomedOut ? -2 : 0}
-          noOfLines={2}
-        />
-        {!isZoomedOut && (
-          <>
-            {!!instance && instance.state && (
-              <Flex alignItems="center">
-                <SimpleStatus state={instance.state} />
-                <Text ml={2} color="gray.500" fontWeight={400} fontSize="md">
-                  {instance.state}
-                </Text>
-              </Flex>
-            )}
-            {task?.operator && (
-              <Text
-                noOfLines={1}
-                maxWidth={`calc(${width}px - 12px)`}
-                fontWeight={400}
-                fontSize="md"
-                width="fit-content"
-                color={operatorTextColor}
-                px={1}
-              >
-                {task.operator}
-              </Text>
-            )}
-          </>
-        )}
-      </Box>
-    </Tooltip>
-  );
-};
-
 const Node = (props: NodeProps<CustomNodeProps>) => {
-  const {
-    data: { height, width, isJoinNode, task },
-  } = props;
-  if (isJoinNode) {
+  const { data } = props;
+
+  if (data.isJoinNode) {
     return (
-      <>
-        <Handle
-          type="target"
-          position={Position.Top}
-          style={{ visibility: "hidden" }}
-        />
-        <Box
-          height={`${height}px`}
-          width={`${width}px`}
-          borderRadius={width}
-          bg="gray.400"
-        />
-        <Handle
-          type="source"
-          position={Position.Bottom}
-          style={{ visibility: "hidden" }}
-        />
-      </>
+      <Box
+        height={`${data.height}px`}
+        width={`${data.width}px`}
+        borderRadius={data.width}
+        bg="gray.400"
+      />
     );
   }
 
-  if (!task) return null;
-  return (
-    <>
-      <Handle
-        type="target"
-        position={Position.Top}
-        style={{ visibility: "hidden" }}
-      />
-      <BaseNode {...props} />
-      <Handle
-        type="source"
-        position={Position.Bottom}
-        style={{ visibility: "hidden" }}
-      />
-    </>
-  );
+  if (data.class === "dataset") return <DatasetNode {...props} />;
+
+  return <DagNode {...props} />;
 };
 
-export default Node;
+const NodeWrapper = (props: NodeProps<CustomNodeProps>) => (
+  <>
+    <Handle
+      type="target"
+      position={Position.Top}
+      style={{ visibility: "hidden" }}
+    />
+    <Node {...props} />
+    <Handle
+      type="source"
+      position={Position.Bottom}
+      style={{ visibility: "hidden" }}
+    />
+  </>
+);
+
+export default NodeWrapper;
diff --git a/airflow/www/static/js/dag/details/graph/index.tsx 
b/airflow/www/static/js/dag/details/graph/index.tsx
index 4fb3d21f6c..84f71313a3 100644
--- a/airflow/www/static/js/dag/details/graph/index.tsx
+++ b/airflow/www/static/js/dag/details/graph/index.tsx
@@ -30,11 +30,12 @@ import ReactFlow, {
   Viewport,
 } from "reactflow";
 
-import { useGraphData, useGridData } from "src/api";
+import { useDatasets, useGraphData, useGridData } from "src/api";
 import useSelection from "src/dag/useSelection";
-import { useOffsetTop } from "src/utils";
+import { getMetaValue, useOffsetTop } from "src/utils";
 import { useGraphLayout } from "src/utils/graph";
 import Edge from "src/components/Graph/Edge";
+import type { DepNode, WebserverEdge } from "src/types";
 
 import Node from "./Node";
 import { buildEdges, nodeStrokeColor, nodeColor, flattenNodes } from "./utils";
@@ -48,6 +49,8 @@ interface Props {
   hoveredTaskState?: string | null;
 }
 
+const dagId = getMetaValue("dag_id");
+
 const Graph = ({ openGroupIds, onToggleGroups, hoveredTaskState }: Props) => {
   const graphRef = useRef(null);
   const { data } = useGraphData();
@@ -59,13 +62,63 @@ const Graph = ({ openGroupIds, onToggleGroups, 
hoveredTaskState }: Props) => {
     setArrange(data?.arrange || "LR");
   }, [data?.arrange]);
 
+  const { data: datasetsCollection } = useDatasets({
+    dagIds: [dagId],
+  });
+
+  const rawNodes =
+    data?.nodes && datasetsCollection?.datasets?.length
+      ? {
+          ...data.nodes,
+          children: [
+            ...(data.nodes.children || []),
+            ...(datasetsCollection?.datasets || []).map(
+              (dataset) =>
+                ({
+                  id: dataset?.id?.toString() || "",
+                  value: {
+                    class: "dataset",
+                    label: dataset.uri,
+                  },
+                } as DepNode)
+            ),
+          ],
+        }
+      : data?.nodes;
+
+  const datasetEdges: WebserverEdge[] = [];
+
+  datasetsCollection?.datasets?.forEach((dataset) => {
+    const producingTask = dataset?.producingTasks?.find(
+      (t) => t.dagId === dagId
+    );
+    const consumingDag = dataset?.consumingDags?.find((d) => d.dagId === 
dagId);
+    if (dataset.id) {
+      if (producingTask?.taskId) {
+        datasetEdges.push({
+          sourceId: producingTask.taskId,
+          targetId: dataset.id.toString(),
+        });
+      }
+      if (consumingDag && data?.nodes?.children?.length) {
+        datasetEdges.push({
+          sourceId: dataset.id.toString(),
+          // Point upstream datasets to the first task
+          targetId: data.nodes?.children[0].id,
+        });
+      }
+    }
+  });
+
   const { data: graphData } = useGraphLayout({
-    edges: data?.edges,
-    nodes: data?.nodes,
+    edges: [...(data?.edges || []), ...datasetEdges],
+    nodes: rawNodes,
     openGroupIds,
     arrange,
   });
+
   const { selected } = useSelection();
+
   const {
     data: { dagRuns, groups },
   } = useGridData();
diff --git a/airflow/www/static/js/datasets/List.test.tsx 
b/airflow/www/static/js/datasets/List.test.tsx
index c1cd0da344..f0c1523029 100644
--- a/airflow/www/static/js/datasets/List.test.tsx
+++ b/airflow/www/static/js/datasets/List.test.tsx
@@ -22,7 +22,7 @@
 import React from "react";
 import { render } from "@testing-library/react";
 
-import * as useDatasetsModule from "src/api/useDatasets";
+import * as useDatasetsModule from "src/api/useDatasetsSummary";
 import { Wrapper } from "src/utils/testUtils";
 
 import type { UseQueryResult } from "react-query";
diff --git a/airflow/www/static/js/datasets/List.tsx 
b/airflow/www/static/js/datasets/List.tsx
index a327936883..9d83406d7f 100644
--- a/airflow/www/static/js/datasets/List.tsx
+++ b/airflow/www/static/js/datasets/List.tsx
@@ -37,11 +37,11 @@ import type { Row, SortingRule } from "react-table";
 import { MdClose, MdSearch } from "react-icons/md";
 import { useSearchParams } from "react-router-dom";
 
-import { useDatasets } from "src/api";
+import { useDatasetsSummary } from "src/api";
 import { Table, TimeCell } from "src/components/Table";
 import type { API } from "src/types";
 import { getMetaValue } from "src/utils";
-import type { DateOption } from "src/api/useDatasets";
+import type { DateOption } from "src/api/useDatasetsSummary";
 
 interface Props {
   onSelect: (datasetId: string) => void;
@@ -99,7 +99,7 @@ const DatasetsList = ({ onSelect }: Props) => {
   const {
     data: { datasets, totalEntries },
     isLoading,
-  } = useDatasets({
+  } = useDatasetsSummary({
     limit,
     offset,
     order,
diff --git a/airflow/www/static/js/types/index.ts 
b/airflow/www/static/js/types/index.ts
index b9ada90370..926db4760d 100644
--- a/airflow/www/static/js/types/index.ts
+++ b/airflow/www/static/js/types/index.ts
@@ -135,13 +135,13 @@ interface DepNode {
     id?: string;
     class: "dag" | "dataset" | "trigger" | "sensor";
     label: string;
-    rx: number;
-    ry: number;
+    rx?: number;
+    ry?: number;
     isOpen?: boolean;
     isJoinNode?: boolean;
     childCount?: number;
-    labelStyle: string;
-    style: string;
+    labelStyle?: string;
+    style?: string;
     setupTeardownType?: "setup" | "teardown";
   };
   children?: DepNode[];
diff --git a/airflow/www/static/js/utils/graph.ts 
b/airflow/www/static/js/utils/graph.ts
index 71003b0f19..d1b4b47bde 100644
--- a/airflow/www/static/js/utils/graph.ts
+++ b/airflow/www/static/js/utils/graph.ts
@@ -174,6 +174,7 @@ const generateGraph = ({
     }
     const extraLabelLength =
       value.label.length > 20 ? value.label.length - 19 : 0;
+
     return {
       id,
       label: value.label,
@@ -218,7 +219,7 @@ export const useGraphLayout = ({
   return useQuery(
     [
       "graphLayout",
-      !!nodes?.children,
+      nodes?.children?.length,
       openGroupIds,
       arrange,
       root,
diff --git a/airflow/www/templates/airflow/dag.html 
b/airflow/www/templates/airflow/dag.html
index 6d6800e98c..5d854abe6e 100644
--- a/airflow/www/templates/airflow/dag.html
+++ b/airflow/www/templates/airflow/dag.html
@@ -79,6 +79,7 @@
   <meta name="dag_api" content="{{ 
url_for('/api/v1.airflow_api_connexion_endpoints_dag_endpoint_get_dag', 
dag_id=dag.dag_id) }}">
   <meta name="dag_source_api" content="{{ 
url_for('/api/v1.airflow_api_connexion_endpoints_dag_source_endpoint_get_dag_source',
 file_token='_FILE_TOKEN_') }}">
   <meta name="dag_details_api" content="{{ 
url_for('/api/v1.airflow_api_connexion_endpoints_dag_endpoint_get_dag_details', 
dag_id=dag.dag_id) }}">
+  <meta name="datasets_api" content="{{ 
url_for('/api/v1.airflow_api_connexion_endpoints_dataset_endpoint_get_datasets')
 }}">
 
   <!-- End Urls -->
   <meta name="is_paused" content="{{ dag_is_paused }}">
diff --git a/airflow/www/templates/airflow/datasets.html 
b/airflow/www/templates/airflow/datasets.html
index 0ffd11444a..164aaf0006 100644
--- a/airflow/www/templates/airflow/datasets.html
+++ b/airflow/www/templates/airflow/datasets.html
@@ -23,7 +23,7 @@
 
 {% block head_meta %}
   {{ super() }}
-  <meta name="datasets_api" content="{{ url_for('Airflow.datasets_summary') 
}}">
+  <meta name="datasets_summary" content="{{ 
url_for('Airflow.datasets_summary') }}">
   <meta name="dataset_api" content="{{ 
url_for('/api/v1.airflow_api_connexion_endpoints_dataset_endpoint_get_dataset', 
uri='__URI__') }}">
   <meta name="dataset_events_api" content="{{ 
url_for('/api/v1.airflow_api_connexion_endpoints_dataset_endpoint_get_dataset_events')
 }}">
   <meta name="grid_url" content="{{ url_for('Airflow.grid', 
dag_id='__DAG_ID__') }}">

Reply via email to