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 00bf2ab219 Use reactflow for datasets graph (#31775)
00bf2ab219 is described below

commit 00bf2ab2191fd7ebce34583007a4c06b7561b360
Author: Brent Bovenzi <[email protected]>
AuthorDate: Sun Jul 2 11:47:29 2023 -0400

    Use reactflow for datasets graph (#31775)
---
 airflow/www/package.json                           |   2 -
 .../www/static/js/api/useDatasetDependencies.ts    |   5 +-
 airflow/www/static/js/api/useGraphData.ts          |   7 +-
 .../details/graph => components/Graph}/Edge.tsx    |  20 +-
 airflow/www/static/js/dag/details/graph/index.tsx  |   2 +-
 airflow/www/static/js/dag/details/graph/utils.ts   |   3 +-
 airflow/www/static/js/datasets/Graph/DagNode.tsx   |   5 +-
 airflow/www/static/js/datasets/Graph/Edge.tsx      |  63 ------
 airflow/www/static/js/datasets/Graph/Legend.tsx    |  70 ++-----
 airflow/www/static/js/datasets/Graph/Node.tsx      | 115 +++++------
 airflow/www/static/js/datasets/Graph/index.tsx     | 213 ++++++++++++---------
 airflow/www/static/js/datasets/index.tsx           |  31 ++-
 airflow/www/static/js/types/index.ts               |  12 ++
 airflow/www/static/js/utils/graph.ts               |   9 +-
 airflow/www/yarn.lock                              |  48 +----
 15 files changed, 252 insertions(+), 353 deletions(-)

diff --git a/airflow/www/package.json b/airflow/www/package.json
index 5eac374466..cf1727b45a 100644
--- a/airflow/www/package.json
+++ b/airflow/www/package.json
@@ -92,9 +92,7 @@
     "@emotion/react": "^11.9.3",
     "@emotion/styled": "^11",
     "@visx/group": "^2.10.0",
-    "@visx/marker": "^2.12.2",
     "@visx/shape": "^2.12.2",
-    "@visx/zoom": "^2.10.0",
     "axios": "^0.26.0",
     "bootstrap-3-typeahead": "^4.0.2",
     "camelcase-keys": "^7.0.0",
diff --git a/airflow/www/static/js/api/useDatasetDependencies.ts 
b/airflow/www/static/js/api/useDatasetDependencies.ts
index 3760cbfded..2680ee9a78 100644
--- a/airflow/www/static/js/api/useDatasetDependencies.ts
+++ b/airflow/www/static/js/api/useDatasetDependencies.ts
@@ -22,11 +22,10 @@ import { useQuery } from "react-query";
 import ELK, { ElkShape, ElkExtendedEdge } from "elkjs";
 
 import { getMetaValue } from "src/utils";
-import type { DepEdge, DepNode } from "src/types";
-import type { NodeType } from "src/datasets/Graph/Node";
-
 import { getTextWidth } from "src/utils/graph";
 
+import type { NodeType, DepEdge, DepNode } from "src/types";
+
 interface DatasetDependencies {
   edges: DepEdge[];
   nodes: DepNode[];
diff --git a/airflow/www/static/js/api/useGraphData.ts 
b/airflow/www/static/js/api/useGraphData.ts
index 9c0f089e9c..e7a94156ef 100644
--- a/airflow/www/static/js/api/useGraphData.ts
+++ b/airflow/www/static/js/api/useGraphData.ts
@@ -21,12 +21,12 @@ import { useQuery } from "react-query";
 import axios, { AxiosResponse } from "axios";
 
 import { getMetaValue } from "src/utils";
-import type { DepNode } from "src/types";
 import useFilters, {
   FILTER_DOWNSTREAM_PARAM,
   FILTER_UPSTREAM_PARAM,
   ROOT_PARAM,
 } from "src/dag/useFilters";
+import type { WebserverEdge, DepNode } from "src/types";
 
 const DAG_ID_PARAM = "dag_id";
 
@@ -38,11 +38,6 @@ interface GraphData {
   nodes: DepNode;
   arrange: string;
 }
-export interface WebserverEdge {
-  label?: string;
-  sourceId: string;
-  targetId: string;
-}
 
 const useGraphData = () => {
   const {
diff --git a/airflow/www/static/js/dag/details/graph/Edge.tsx 
b/airflow/www/static/js/components/Graph/Edge.tsx
similarity index 78%
rename from airflow/www/static/js/dag/details/graph/Edge.tsx
rename to airflow/www/static/js/components/Graph/Edge.tsx
index 561048f798..0205bf49cd 100644
--- a/airflow/www/static/js/dag/details/graph/Edge.tsx
+++ b/airflow/www/static/js/components/Graph/Edge.tsx
@@ -18,11 +18,11 @@
  */
 
 import React from "react";
-import { Text } from "@chakra-ui/react";
+import { Text, useTheme } from "@chakra-ui/react";
 import type { ElkEdgeSection, ElkLabel, ElkPoint, LayoutOptions } from "elkjs";
 
-import Edge from "src/datasets/Graph/Edge";
 import { Group } from "@visx/group";
+import { LinePath } from "@visx/shape";
 
 interface EdgeProps {
   data?: {
@@ -40,6 +40,7 @@ interface EdgeProps {
 }
 
 const CustomEdge = ({ data }: EdgeProps) => {
+  const { colors } = useTheme();
   if (!data) return null;
   const { rest } = data;
   return (
@@ -54,11 +55,16 @@ const CustomEdge = ({ data }: EdgeProps) => {
           </Group>
         );
       })}
-      <Edge
-        edge={rest}
-        showMarker={false}
-        isSelected={rest.isSelected || undefined}
-      />
+      {(rest.sections || []).map((s) => (
+        <LinePath
+          key={s.id}
+          stroke={rest.isSelected ? colors.blue[400] : colors.gray[400]}
+          strokeWidth={rest.isSelected ? 3 : 2}
+          x={(d) => d.x || 0}
+          y={(d) => d.y || 0}
+          data={[s.startPoint, ...(s.bendPoints || []), s.endPoint]}
+        />
+      ))}
     </>
   );
 };
diff --git a/airflow/www/static/js/dag/details/graph/index.tsx 
b/airflow/www/static/js/dag/details/graph/index.tsx
index 31578869cf..4dae9477bd 100644
--- a/airflow/www/static/js/dag/details/graph/index.tsx
+++ b/airflow/www/static/js/dag/details/graph/index.tsx
@@ -38,8 +38,8 @@ import { useGraphLayout } from "src/utils/graph";
 import Tooltip from "src/components/Tooltip";
 import { useContainerRef } from "src/context/containerRef";
 import useFilters from "src/dag/useFilters";
+import Edge from "src/components/Graph/Edge";
 
-import Edge from "./Edge";
 import Node, { CustomNodeProps } from "./Node";
 import { buildEdges, nodeStrokeColor, nodeColor, flattenNodes } from "./utils";
 
diff --git a/airflow/www/static/js/dag/details/graph/utils.ts 
b/airflow/www/static/js/dag/details/graph/utils.ts
index 703fb38b9c..998451e05b 100644
--- a/airflow/www/static/js/dag/details/graph/utils.ts
+++ b/airflow/www/static/js/dag/details/graph/utils.ts
@@ -23,8 +23,7 @@ import type { ElkExtendedEdge } from "elkjs";
 
 import type { SelectionProps } from "src/dag/useSelection";
 import { getTask } from "src/utils";
-import type { Task, TaskInstance } from "src/types";
-import type { NodeType } from "src/datasets/Graph/Node";
+import type { Task, TaskInstance, NodeType } from "src/types";
 
 import type { CustomNodeProps } from "./Node";
 
diff --git a/airflow/www/static/js/datasets/Graph/DagNode.tsx 
b/airflow/www/static/js/datasets/Graph/DagNode.tsx
index cb4900a5c3..aa12c57522 100644
--- a/airflow/www/static/js/datasets/Graph/DagNode.tsx
+++ b/airflow/www/static/js/datasets/Graph/DagNode.tsx
@@ -33,6 +33,7 @@ import {
   useTheme,
 } from "@chakra-ui/react";
 import { MdOutlineAccountTree } from "react-icons/md";
+
 import { useContainerRef } from "src/context/containerRef";
 import { getMetaValue } from "src/utils";
 
@@ -41,7 +42,7 @@ const DagNode = ({
   isHighlighted,
 }: {
   dagId: string;
-  isHighlighted: boolean;
+  isHighlighted?: boolean;
 }) => {
   const { colors } = useTheme();
   const containerRef = useContainerRef();
@@ -63,7 +64,7 @@ const DagNode = ({
           alignItems="center"
         >
           <MdOutlineAccountTree size="16px" />
-          <Text>{dagId}</Text>
+          <Text ml={2}>{dagId}</Text>
         </Flex>
       </PopoverTrigger>
       <Portal containerRef={containerRef}>
diff --git a/airflow/www/static/js/datasets/Graph/Edge.tsx 
b/airflow/www/static/js/datasets/Graph/Edge.tsx
deleted file mode 100644
index a5ef5fbfed..0000000000
--- a/airflow/www/static/js/datasets/Graph/Edge.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-/*!
- * 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 { LinePath } from "@visx/shape";
-import { MarkerArrow } from "@visx/marker";
-import { useTheme } from "@chakra-ui/react";
-
-interface Props {
-  edge: {
-    id: string;
-    sources: string[];
-    targets: string[];
-    sections: Record<string, any>[];
-  };
-  isSelected?: boolean;
-  showMarker?: boolean;
-}
-
-const Edge = ({ edge, isSelected = false, showMarker = true }: Props) => {
-  const { colors } = useTheme();
-  return (
-    <>
-      {showMarker && (
-        <MarkerArrow
-          id="marker-arrow"
-          fill={isSelected ? colors.blue[400] : colors.gray[400]}
-          refX={6}
-          size={6}
-        />
-      )}
-      {(edge.sections || []).map((s) => (
-        <LinePath
-          key={s.id}
-          stroke={isSelected ? colors.blue[400] : colors.gray[400]}
-          strokeWidth={isSelected ? 3 : 2}
-          x={(d) => d.x || 0}
-          y={(d) => d.y || 0}
-          data={[s.startPoint, ...(s.bendPoints || []), s.endPoint]}
-          markerEnd="url(#marker-arrow)"
-        />
-      ))}
-    </>
-  );
-};
-
-export default Edge;
diff --git a/airflow/www/static/js/datasets/Graph/Legend.tsx 
b/airflow/www/static/js/datasets/Graph/Legend.tsx
index 250d6852b9..3ecbed7664 100644
--- a/airflow/www/static/js/datasets/Graph/Legend.tsx
+++ b/airflow/www/static/js/datasets/Graph/Legend.tsx
@@ -18,59 +18,29 @@
  */
 
 import React from "react";
-import { Flex, Box, IconButton, Text } from "@chakra-ui/react";
-import {
-  MdOutlineZoomOutMap,
-  MdFilterCenterFocus,
-  MdOutlineAccountTree,
-} from "react-icons/md";
+import { Flex, Box, Text } from "@chakra-ui/react";
+import { MdOutlineAccountTree } from "react-icons/md";
 import { HiDatabase } from "react-icons/hi";
 
-interface Props {
-  zoom: any;
-  center: () => void;
-}
-
-const Legend = ({ zoom, center }: Props) => (
-  <Flex height="100%" flexDirection="column" justifyContent="space-between">
-    <Box>
-      <IconButton
-        onClick={zoom.reset}
-        fontSize="2xl"
-        m={2}
-        title="Reset zoom"
-        aria-label="Reset zoom"
-        icon={<MdOutlineZoomOutMap />}
-      />
-      <IconButton
-        onClick={center}
-        fontSize="2xl"
-        m={2}
-        title="Center"
-        aria-label="Center"
-        icon={<MdFilterCenterFocus />}
-      />
-    </Box>
-    <Box
-      backgroundColor="white"
-      p={2}
-      borderColor="gray.200"
-      borderRightWidth={1}
-      borderTopWidth={1}
-    >
-      <Text>Legend</Text>
-      <Flex>
-        <Flex mr={2} alignItems="center">
-          <MdOutlineAccountTree size="16px" />
-          <Text ml={1}>DAG</Text>
-        </Flex>
-        <Flex alignItems="center">
-          <HiDatabase size="16px" />
-          <Text ml={1}>Dataset</Text>
-        </Flex>
+const Legend = () => (
+  <Box
+    backgroundColor="white"
+    p={2}
+    borderColor="gray.200"
+    borderWidth={1}
+    fontSize={14}
+  >
+    <Flex>
+      <Flex mr={2} alignItems="center">
+        <MdOutlineAccountTree size="14px" />
+        <Text ml={1}>DAG</Text>
+      </Flex>
+      <Flex alignItems="center">
+        <HiDatabase size="14px" />
+        <Text ml={1}>Dataset</Text>
       </Flex>
-    </Box>
-  </Flex>
+    </Flex>
+  </Box>
 );
 
 export default Legend;
diff --git a/airflow/www/static/js/datasets/Graph/Node.tsx 
b/airflow/www/static/js/datasets/Graph/Node.tsx
index d692861b66..ed02653139 100644
--- a/airflow/www/static/js/datasets/Graph/Node.tsx
+++ b/airflow/www/static/js/datasets/Graph/Node.tsx
@@ -18,71 +18,76 @@
  */
 
 import React from "react";
-import { Flex, Text, useTheme } from "@chakra-ui/react";
-import { Group } from "@visx/group";
+import { Box, Text, Flex, useTheme } from "@chakra-ui/react";
+import { Handle, NodeProps, Position } from "reactflow";
 import { MdPlayArrow, MdSensors } from "react-icons/md";
 import { HiDatabase } from "react-icons/hi";
 
-import type { ElkShape } from "elkjs";
-import type { DepNode } from "src/types";
-
 import DagNode from "./DagNode";
 
-export interface NodeType extends ElkShape {
-  value: DepNode["value"];
-  children?: NodeType[];
-}
-
-interface Props {
-  node: NodeType;
-  onSelect: (datasetId: string) => void;
-  isSelected: boolean;
-  isHighlighted: boolean;
+export interface CustomNodeProps {
+  label: string;
+  type?: string;
+  height?: number;
+  width?: number;
+  isSelected?: boolean;
+  isHighlighted?: boolean;
+  onSelect: (datasetUri: string) => void;
+  isOpen?: boolean;
+  isActive?: boolean;
 }
 
-const Node = ({
-  node: { height, width, x, y, value },
-  onSelect,
-  isSelected,
-  isHighlighted,
-}: Props) => {
+const BaseNode = ({
+  data: { label, type, isSelected, isHighlighted, onSelect },
+}: NodeProps<CustomNodeProps>) => {
   const { colors } = useTheme();
+
   return (
-    <Group top={y} left={x} height={height} width={width}>
-      <foreignObject width={width} height={height}>
-        {value.class === "dag" && (
-          <DagNode dagId={value.label} isHighlighted={isHighlighted} />
-        )}
-        {value.class !== "dag" && (
-          <Flex
-            borderWidth={isSelected ? 4 : 2}
-            borderColor={
-              isHighlighted || isSelected ? colors.blue[400] : undefined
-            }
-            borderRadius={5}
-            p={2}
-            height="100%"
-            width="100%"
-            fontWeight={isSelected ? "bold" : "normal"}
-            onClick={(e) => {
-              e.preventDefault();
-              e.stopPropagation();
-              if (value.class === "dataset") onSelect(value.label);
-            }}
-            cursor="pointer"
-            fontSize={16}
-            justifyContent="space-between"
-            alignItems="center"
-          >
-            {value.class === "dataset" && <HiDatabase size="16px" />}
-            {value.class === "sensor" && <MdSensors size="16px" />}
-            {value.class === "trigger" && <MdPlayArrow size="16px" />}
-            <Text>{value.label}</Text>
-          </Flex>
-        )}
-      </foreignObject>
-    </Group>
+    <Box bg="white">
+      {type === "dag" && (
+        <DagNode dagId={label} isHighlighted={isHighlighted} />
+      )}
+      {type !== "dag" && (
+        <Flex
+          borderWidth={isSelected ? 4 : 2}
+          borderColor={isSelected ? colors.blue[400] : undefined}
+          borderRadius={5}
+          p={2}
+          fontWeight={isSelected ? "bold" : "normal"}
+          onClick={(e) => {
+            e.preventDefault();
+            e.stopPropagation();
+            if (type === "dataset") onSelect(label);
+          }}
+          cursor="pointer"
+          fontSize={16}
+          justifyContent="space-between"
+          alignItems="center"
+        >
+          {type === "dataset" && <HiDatabase size="16px" />}
+          {type === "sensor" && <MdSensors size="16px" />}
+          {type === "trigger" && <MdPlayArrow size="16px" />}
+          <Text ml={2}>{label}</Text>
+        </Flex>
+      )}
+    </Box>
   );
 };
 
+const Node = (props: NodeProps<CustomNodeProps>) => (
+  <>
+    <Handle
+      type="target"
+      position={Position.Top}
+      style={{ visibility: "hidden" }}
+    />
+    <BaseNode {...props} />
+    <Handle
+      type="source"
+      position={Position.Bottom}
+      style={{ visibility: "hidden" }}
+    />
+  </>
+);
+
 export default Node;
diff --git a/airflow/www/static/js/datasets/Graph/index.tsx 
b/airflow/www/static/js/datasets/Graph/index.tsx
index f869365945..f137be36d1 100644
--- a/airflow/www/static/js/datasets/Graph/index.tsx
+++ b/airflow/www/static/js/datasets/Graph/index.tsx
@@ -17,28 +17,50 @@
  * under the License.
  */
 
-import React, { RefObject } from "react";
-import { Box, Spinner } from "@chakra-ui/react";
-import { Zoom } from "@visx/zoom";
-import { Group } from "@visx/group";
+import React, { useEffect } from "react";
+import ReactFlow, {
+  ReactFlowProvider,
+  Controls,
+  Background,
+  MiniMap,
+  Node as ReactFlowNode,
+  useReactFlow,
+  ControlButton,
+  Panel,
+} from "reactflow";
+import { Box, Tooltip, useTheme } from "@chakra-ui/react";
+import { RiFocus3Line } from "react-icons/ri";
 
 import { useDatasetDependencies } from "src/api";
+import Edge from "src/components/Graph/Edge";
+import { useContainerRef } from "src/context/containerRef";
 
-import Node from "./Node";
-import Edge from "./Edge";
+import Node, { CustomNodeProps } from "./Node";
 import Legend from "./Legend";
 
 interface Props {
   onSelect: (datasetId: string) => void;
   selectedUri: string | null;
-  height: number;
-  width: number;
 }
 
-const Graph = ({ onSelect, selectedUri, height, width }: Props) => {
-  const { data, isLoading } = useDatasetDependencies();
+const nodeTypes = { custom: Node };
+const edgeTypes = { custom: Edge };
+
+const Graph = ({ onSelect, selectedUri }: Props) => {
+  const { data } = useDatasetDependencies();
+  const { colors } = useTheme();
+  const { setCenter, setViewport } = useReactFlow();
+  const containerRef = useContainerRef();
+
+  useEffect(() => {
+    setViewport({ x: 0, y: 0, zoom: 1 });
+  }, [selectedUri, setViewport]);
+
+  const nodeColor = ({
+    data: { isSelected },
+  }: ReactFlowNode<CustomNodeProps>) =>
+    isSelected ? colors.blue["300"] : colors.gray["300"];
 
-  if (isLoading && !data) return <Spinner />;
   if (!data || !data.fullGraph || !data.subGraphs) return null;
   const graph = selectedUri
     ? data.subGraphs.find((g) =>
@@ -46,91 +68,102 @@ const Graph = ({ onSelect, selectedUri, height, width }: 
Props) => {
       )
     : data.fullGraph;
   if (!graph) return null;
-  const { edges, children, width: graphWidth, height: graphHeight } = graph;
 
-  const initialTransform = {
-    scaleX: 1,
-    scaleY: 1,
-    translateX: 0,
-    translateY: 0,
-    skewX: 0,
-    skewY: 0,
-  };
+  const edges = graph.edges.map((e) => ({
+    id: e.id,
+    source: e.sources[0],
+    target: e.targets[0],
+    type: "custom",
+    data: {
+      rest: {
+        ...e,
+        isSelected: selectedUri && e.id.includes(selectedUri),
+      },
+    },
+  }));
 
-  const selectedEdges = selectedUri
-    ? edges?.filter(
-        ({ sources, targets }) =>
-          sources[0].includes(selectedUri) || targets[0].includes(selectedUri)
-      )
-    : [];
-  const highlightedNodes = children.filter((n) =>
-    selectedEdges.some(
-      ({ sources, targets }) => sources[0] === n.id || targets[0] === n.id
-    )
-  );
+  const nodes: ReactFlowNode<CustomNodeProps>[] = graph.children.map((c) => ({
+    id: c.id,
+    data: {
+      label: c.value.label,
+      type: c.value.class,
+      width: c.width,
+      height: c.height,
+      onSelect,
+      isSelected: selectedUri === c.value.label,
+      isHighlighted: edges.some(
+        (e) => e.data.rest.isSelected && e.id.includes(c.id)
+      ),
+    },
+    type: "custom",
+    position: {
+      x: c.x || 0,
+      y: c.y || 0,
+    },
+  }));
+
+  const focusNode = () => {
+    if (selectedUri) {
+      const node = nodes.find((n) => n.data.label === selectedUri);
+      if (!node || !node.position) return;
+      const { x, y } = node.position;
+      setCenter(
+        x + (node.data.width || 0) / 2,
+        y + (node.data.height || 0) / 2,
+        {
+          duration: 1000,
+        }
+      );
+    }
+  };
 
   return (
-    <Zoom
-      width={width}
-      height={height}
-      scaleXMin={1 / 4}
-      scaleXMax={1}
-      scaleYMin={1 / 4}
-      scaleYMax={1}
-      initialTransformMatrix={initialTransform}
+    <ReactFlow
+      nodes={nodes}
+      edges={edges}
+      nodeTypes={nodeTypes}
+      edgeTypes={edgeTypes}
+      nodesDraggable={false}
+      minZoom={0.25}
+      maxZoom={1}
+      onlyRenderVisibleElements
+      defaultEdgeOptions={{ zIndex: 1 }}
     >
-      {(zoom) => (
-        <Box>
-          <svg
-            id="GRAPH"
-            width={width}
-            height={height}
-            ref={zoom.containerRef as RefObject<SVGSVGElement>}
-            style={{
-              cursor: zoom.isDragging ? "grabbing" : "grab",
-              touchAction: "none",
-            }}
+      <Background />
+      <Controls showInteractive={false}>
+        <ControlButton onClick={focusNode} disabled={!selectedUri}>
+          <Tooltip
+            portalProps={{ containerRef }}
+            label="Center selected dataset"
+            placement="right"
           >
-            <g transform={zoom.toString()}>
-              <g height={graphHeight} width={graphWidth}>
-                {edges.map((edge) => (
-                  <Edge
-                    key={edge.id}
-                    edge={edge}
-                    isSelected={selectedEdges.some((e) => e.id === edge.id)}
-                  />
-                ))}
-                {children.map((node) => (
-                  <Node
-                    key={node.id}
-                    node={node}
-                    onSelect={onSelect}
-                    isSelected={node.id === `dataset:${selectedUri}`}
-                    isHighlighted={highlightedNodes.some(
-                      (n) => n.id === node.id
-                    )}
-                  />
-                ))}
-              </g>
-            </g>
-            <Group top={height - 100} left={0} height={100} width={width}>
-              <foreignObject width={150} height={100}>
-                <Legend
-                  zoom={zoom}
-                  center={() =>
-                    zoom.translateTo({
-                      x: (width - (graphWidth ?? 0)) / 2,
-                      y: (height - (graphHeight ?? 0)) / 2,
-                    })
-                  }
-                />
-              </foreignObject>
-            </Group>
-          </svg>
-        </Box>
-      )}
-    </Zoom>
+            <Box>
+              <RiFocus3Line
+                size={16}
+                style={{
+                  // override react-flow css
+                  maxWidth: "16px",
+                  maxHeight: "16px",
+                  color: colors.gray[800],
+                }}
+                aria-label="Center selected dataset"
+              />
+            </Box>
+          </Tooltip>
+        </ControlButton>
+      </Controls>
+      <Panel position="top-right">
+        <Legend />
+      </Panel>
+      <MiniMap nodeStrokeWidth={15} nodeColor={nodeColor} zoomable pannable />
+    </ReactFlow>
   );
 };
 
-export default Graph;
+const GraphWrapper = (props: Props) => (
+  <ReactFlowProvider>
+    <Graph {...props} />
+  </ReactFlowProvider>
+);
+
+export default GraphWrapper;
diff --git a/airflow/www/static/js/datasets/index.tsx 
b/airflow/www/static/js/datasets/index.tsx
index bd21ca26f4..166b3f26ec 100644
--- a/airflow/www/static/js/datasets/index.tsx
+++ b/airflow/www/static/js/datasets/index.tsx
@@ -23,9 +23,11 @@ import React, { useRef } from "react";
 import { createRoot } from "react-dom/client";
 import createCache from "@emotion/cache";
 import { useSearchParams } from "react-router-dom";
-import { Flex, Box, useDimensions } from "@chakra-ui/react";
+import { Flex, Box } from "@chakra-ui/react";
+import reactFlowStyle from "reactflow/dist/style.css";
 
 import App from "src/App";
+import { useOffsetTop } from "src/utils";
 
 import DatasetsList from "./List";
 import DatasetDetails from "./Details";
@@ -45,8 +47,9 @@ const DATASET_URI = "uri";
 const Datasets = () => {
   const [searchParams, setSearchParams] = useSearchParams();
   const contentRef = useRef<HTMLDivElement>(null);
-  const graphRef = useRef<HTMLDivElement>(null);
-  const dimensions = useDimensions(graphRef, true);
+  const offsetTop = useOffsetTop(contentRef);
+  // 60px for footer height
+  const height = `calc(100vh - ${offsetTop + 60}px)`;
 
   const onBack = () => {
     searchParams.delete(DATASET_URI);
@@ -66,26 +69,15 @@ const Datasets = () => {
       justifyContent="space-between"
       ref={contentRef}
     >
-      <Box minWidth="450px" height="100%" overflowY="auto">
+      <Box minWidth="450px" height={height} overflowY="auto">
         {datasetUri ? (
           <DatasetDetails uri={datasetUri} onBack={onBack} />
         ) : (
           <DatasetsList onSelect={onSelect} />
         )}
       </Box>
-      <Box
-        flex={1}
-        ref={graphRef}
-        height="calc(100vh - 68px)"
-        borderColor="gray.200"
-        borderWidth={1}
-      >
-        <Graph
-          selectedUri={datasetUri}
-          onSelect={onSelect}
-          height={dimensions?.contentBox.height || 0}
-          width={dimensions?.contentBox.width || 0}
-        />
+      <Box flex={1} height={height} borderColor="gray.200" borderWidth={1}>
+        <Graph selectedUri={datasetUri} onSelect={onSelect} />
       </Box>
     </Flex>
   );
@@ -93,6 +85,11 @@ const Datasets = () => {
 
 if (mainElement) {
   shadowRoot?.appendChild(mainElement);
+  const styleTag = document.createElement("style");
+  const style = reactFlowStyle.toString();
+  styleTag.innerHTML = style;
+  shadowRoot?.appendChild(styleTag);
+
   const reactRoot = createRoot(mainElement);
   reactRoot.render(
     <App cache={cache}>
diff --git a/airflow/www/static/js/types/index.ts 
b/airflow/www/static/js/types/index.ts
index 45b34c924f..08929d33c4 100644
--- a/airflow/www/static/js/types/index.ts
+++ b/airflow/www/static/js/types/index.ts
@@ -18,6 +18,7 @@
  */
 
 import type { CamelCase } from "type-fest";
+import type { ElkShape } from "elkjs";
 import type * as API from "./api-generated";
 
 type RunState = "success" | "running" | "queued" | "failed";
@@ -127,6 +128,17 @@ interface DepEdge {
   target: string;
 }
 
+export interface NodeType extends ElkShape {
+  value: DepNode["value"];
+  children?: NodeType[];
+}
+
+export interface WebserverEdge {
+  label?: string;
+  sourceId: string;
+  targetId: string;
+}
+
 interface DatasetListItem extends API.Dataset {
   lastDatasetUpdate: string | null;
   totalUpdates: number;
diff --git a/airflow/www/static/js/utils/graph.ts 
b/airflow/www/static/js/utils/graph.ts
index d2d2923c5a..e69a3045d9 100644
--- a/airflow/www/static/js/utils/graph.ts
+++ b/airflow/www/static/js/utils/graph.ts
@@ -19,8 +19,7 @@
 
 import ELK, { ElkExtendedEdge, ElkShape } from "elkjs";
 
-import type { DepNode } from "src/types";
-import type { NodeType } from "src/datasets/Graph/Node";
+import type { NodeType, DepNode, WebserverEdge } from "src/types";
 import { useQuery } from "react-query";
 import useFilters from "src/dag/useFilters";
 
@@ -32,12 +31,6 @@ interface GenerateProps {
   arrange: string;
 }
 
-interface WebserverEdge {
-  label?: string;
-  sourceId: string;
-  targetId: string;
-}
-
 interface Graph extends ElkShape {
   children: NodeType[];
   edges: ElkExtendedEdge[];
diff --git a/airflow/www/yarn.lock b/airflow/www/yarn.lock
index 6e1fce5334..1916d9c900 100644
--- a/airflow/www/yarn.lock
+++ b/airflow/www/yarn.lock
@@ -3575,18 +3575,6 @@
     "@typescript-eslint/types" "5.27.1"
     eslint-visitor-keys "^3.3.0"
 
-"@use-gesture/[email protected]":
-  version "10.2.17"
-  resolved 
"https://registry.yarnpkg.com/@use-gesture/core/-/core-10.2.17.tgz#dc78913cd5d105217c3f1d1c16a32ad6426a00ba";
-  integrity 
sha512-62hCybe4x6oGZ1/JA9gSYIdghV1FqxCdvYWt9SqCEAAikwT1OmVl2Q/Uu8CP636L57D+DfXtw6PWM+fdhr4oJQ==
-
-"@use-gesture/react@^10.0.0-beta.22":
-  version "10.2.17"
-  resolved 
"https://registry.yarnpkg.com/@use-gesture/react/-/react-10.2.17.tgz#00bc413da42a358dd3f9173c0631b54522e76614";
-  integrity 
sha512-Vfrp1KgdYn/kOEUAYNXtGBCl2dr38s3G6rru1TOPs+cVUjfNyNxvJK56grUyJ336N3rQLK8F9G7+FfrHuc3g/Q==
-  dependencies:
-    "@use-gesture/core" "10.2.17"
-
 "@visx/[email protected]":
   version "2.1.0"
   resolved 
"https://registry.yarnpkg.com/@visx/curve/-/curve-2.1.0.tgz#f614bfe3db66df7db7382db7a75ced1506b94602";
@@ -3595,14 +3583,6 @@
     "@types/d3-shape" "^1.3.1"
     d3-shape "^1.0.6"
 
-"@visx/[email protected]":
-  version "2.6.0"
-  resolved 
"https://registry.yarnpkg.com/@visx/event/-/event-2.6.0.tgz#0718eb1efabd5305cf659a153779c94ba4038996";
-  integrity 
sha512-WGp91g82s727g3NAnENF1ppC3ZAlvWg+Y+GG0WFg34NmmOZbvPI/PTOqTqZE3x6B8EUn8NJiMxRjxIMbi+IvRw==
-  dependencies:
-    "@types/react" "*"
-    "@visx/point" "2.6.0"
-
 "@visx/[email protected]", "@visx/group@^2.10.0":
   version "2.10.0"
   resolved 
"https://registry.yarnpkg.com/@visx/group/-/group-2.10.0.tgz#95839851832545621eb0d091866a61dafe552ae1";
@@ -3612,22 +3592,6 @@
     classnames "^2.3.1"
     prop-types "^15.6.2"
 
-"@visx/marker@^2.12.2":
-  version "2.12.2"
-  resolved 
"https://registry.yarnpkg.com/@visx/marker/-/marker-2.12.2.tgz#b81cea1a5d2b61c065aa97e4baccf9d0f17cab51";
-  integrity 
sha512-yvJDMBw9oKQDD2gX5q7O+raR9qk/NYqKFDZ0GtS4ZVH87PfNe0ZyTXt0vWbIaDaix/r58SMpv38GluIOxWE7jg==
-  dependencies:
-    "@types/react" "*"
-    "@visx/group" "2.10.0"
-    "@visx/shape" "2.12.2"
-    classnames "^2.3.1"
-    prop-types "^15.6.2"
-
-"@visx/[email protected]":
-  version "2.6.0"
-  resolved 
"https://registry.yarnpkg.com/@visx/point/-/point-2.6.0.tgz#c4316ca409b5b829c5455f07118d8c14a92cc633";
-  integrity 
sha512-amBi7yMz4S2VSchlPdliznN41TuES64506ySI22DeKQ+mc1s1+BudlpnY90sM1EIw4xnqbKmrghTTGfy6SVqvQ==
-
 "@visx/[email protected]":
   version "2.2.2"
   resolved 
"https://registry.yarnpkg.com/@visx/scale/-/scale-2.2.2.tgz#b8eafabdcf92bb45ab196058fe184772ad80fd25";
@@ -3640,7 +3604,7 @@
     d3-scale "^3.3.0"
     d3-time "^2.1.1"
 
-"@visx/[email protected]", "@visx/shape@^2.12.2":
+"@visx/shape@^2.12.2":
   version "2.12.2"
   resolved 
"https://registry.yarnpkg.com/@visx/shape/-/shape-2.12.2.tgz#81ed88bf823aa84a4f5f32a9c9daf8371a606897";
   integrity 
sha512-4gN0fyHWYXiJ+Ck8VAazXX0i8TOnLJvOc5jZBnaJDVxgnSIfCjJn0+Nsy96l9Dy/bCMTh4DBYUBv9k+YICBUOA==
@@ -3658,16 +3622,6 @@
     lodash "^4.17.21"
     prop-types "^15.5.10"
 
-"@visx/zoom@^2.10.0":
-  version "2.10.0"
-  resolved 
"https://registry.yarnpkg.com/@visx/zoom/-/zoom-2.10.0.tgz#143248813a35d2057eaf1a6336011d8650955533";
-  integrity 
sha512-sId1kuO3NvlzQTOorjeMWXRR3J44zQm8sofwKEt3O9IgaBZ49WzuPUm/owSdVT+YGsXnvxEr2qAdt26GRMzS7Q==
-  dependencies:
-    "@types/react" "*"
-    "@use-gesture/react" "^10.0.0-beta.22"
-    "@visx/event" "2.6.0"
-    prop-types "^15.6.2"
-
 "@webassemblyjs/[email protected]":
   version "1.11.1"
   resolved 
"https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7";

Reply via email to