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"