From 975bfac19a5b97eead6c61d16515ccb334e3214b Mon Sep 17 00:00:00 2001 From: Bugen Zhao Date: Mon, 8 Jan 2024 12:07:43 +0800 Subject: [PATCH] refactor(dashboard): refine type annotation and renames for layout (#14383) Signed-off-by: Bugen Zhao --- ...yGraph.tsx => FragmentDependencyGraph.tsx} | 10 +- dashboard/components/FragmentGraph.tsx | 279 +++++++++--------- ...mGraph.tsx => RelationDependencyGraph.tsx} | 49 +-- dashboard/lib/layout.ts | 149 +++++----- dashboard/pages/dependency_graph.tsx | 8 +- dashboard/pages/fragment_graph.tsx | 20 +- 6 files changed, 252 insertions(+), 263 deletions(-) rename dashboard/components/{DependencyGraph.tsx => FragmentDependencyGraph.tsx} (96%) rename dashboard/components/{StreamGraph.tsx => RelationDependencyGraph.tsx} (82%) diff --git a/dashboard/components/DependencyGraph.tsx b/dashboard/components/FragmentDependencyGraph.tsx similarity index 96% rename from dashboard/components/DependencyGraph.tsx rename to dashboard/components/FragmentDependencyGraph.tsx index f85204e438fb..553c40ec53f9 100644 --- a/dashboard/components/DependencyGraph.tsx +++ b/dashboard/components/FragmentDependencyGraph.tsx @@ -3,11 +3,12 @@ import * as d3 from "d3" import { Dag, DagLink, DagNode, zherebko } from "d3-dag" import { cloneDeep } from "lodash" import { useCallback, useEffect, useRef, useState } from "react" +import { Position } from "../lib/layout" const nodeRadius = 5 const edgeRadius = 12 -export default function DependencyGraph({ +export default function FragmentDependencyGraph({ mvDependency, svgWidth, selectedId, @@ -18,7 +19,7 @@ export default function DependencyGraph({ selectedId: string | undefined onSelectedIdChange: (id: string) => void | undefined }) { - const svgRef = useRef() + const svgRef = useRef(null) const [svgHeight, setSvgHeight] = useState("0px") const MARGIN_X = 10 const MARGIN_Y = 2 @@ -47,7 +48,7 @@ export default function DependencyGraph({ // How to draw edges const curveStyle = d3.curveMonotoneY const line = d3 - .line<{ x: number; y: number }>() + .line() .curve(curveStyle) .x(({ x }) => x + MARGIN_X) .y(({ y }) => y) @@ -85,8 +86,7 @@ export default function DependencyGraph({ sel .attr( "transform", - ({ x, y }: { x: number; y: number }) => - `translate(${x + MARGIN_X}, ${y})` + ({ x, y }: Position) => `translate(${x + MARGIN_X}, ${y})` ) .attr("fill", (d: any) => isSelected(d) ? theme.colors.blue["500"] : theme.colors.gray["500"] diff --git a/dashboard/components/FragmentGraph.tsx b/dashboard/components/FragmentGraph.tsx index 891a6d21df4b..d9ca56fea69e 100644 --- a/dashboard/components/FragmentGraph.tsx +++ b/dashboard/components/FragmentGraph.tsx @@ -15,9 +15,10 @@ import * as d3 from "d3" import { cloneDeep } from "lodash" import { Fragment, useCallback, useEffect, useRef, useState } from "react" import { - ActorBox, - ActorBoxPosition, - generateBoxLinks, + FragmentBox, + FragmentBoxPosition, + Position, + generateBoxEdges, layout, } from "../lib/layout" import { PlanNodeDatum } from "../pages/fragment_graph" @@ -25,10 +26,17 @@ import BackPressureTable from "./BackPressureTable" const ReactJson = loadable(() => import("react-json-view")) -interface Point { - x: number - y: number -} +type FragmentLayout = { + id: string + layoutRoot: d3.HierarchyPointNode + width: number + height: number + actorIds: string[] +} & Position + +type Enter = Type extends d3.Selection + ? d3.Selection + : never function treeLayoutFlip( root: d3.HierarchyNode, @@ -40,10 +48,10 @@ function treeLayoutFlip( const treeRoot = tree(root) // Flip back x, y - treeRoot.each((d: Point) => ([d.x, d.y] = [d.y, d.x])) + treeRoot.each((d: Position) => ([d.x, d.y] = [d.y, d.x])) // LTR -> RTL - treeRoot.each((d: Point) => (d.x = -d.x)) + treeRoot.each((d: Position) => (d.x = -d.x)) return treeRoot } @@ -78,10 +86,10 @@ function boundBox( const nodeRadius = 12 const nodeMarginX = nodeRadius * 6 const nodeMarginY = nodeRadius * 4 -const actorMarginX = nodeRadius -const actorMarginY = nodeRadius -const actorDistanceX = nodeRadius * 5 -const actorDistanceY = nodeRadius * 5 +const fragmentMarginX = nodeRadius +const fragmentMarginY = nodeRadius +const fragmentDistanceX = nodeRadius * 5 +const fragmentDistanceY = nodeRadius * 5 export default function FragmentGraph({ planNodeDependencies, @@ -89,34 +97,27 @@ export default function FragmentGraph({ selectedFragmentId, }: { planNodeDependencies: Map> - fragmentDependency: ActorBox[] + fragmentDependency: FragmentBox[] selectedFragmentId: string | undefined }) { - const svgRef = useRef() + const svgRef = useRef(null) const { isOpen, onOpen, onClose } = useDisclosure() const [currentStreamNode, setCurrentStreamNode] = useState() const openPlanNodeDetail = useCallback( - () => (node: d3.HierarchyNode) => { - setCurrentStreamNode(node.data) + (node: PlanNodeDatum) => { + setCurrentStreamNode(node) onOpen() }, - [onOpen] - )() + [onOpen, setCurrentStreamNode] + ) const planNodeDependencyDagCallback = useCallback(() => { const deps = cloneDeep(planNodeDependencies) const fragmentDependencyDag = cloneDeep(fragmentDependency) - const layoutActorResult = new Map< - string, - { - layoutRoot: d3.HierarchyPointNode - width: number - height: number - extraInfo: string - } - >() + + const layoutFragmentResult = new Map() const includedFragmentIds = new Set() for (const [fragmentId, fragmentRoot] of deps) { const layoutRoot = treeLayoutFlip(fragmentRoot, { @@ -125,95 +126,86 @@ export default function FragmentGraph({ }) let { width, height } = boundBox(layoutRoot, { margin: { - left: nodeRadius * 4 + actorMarginX, - right: nodeRadius * 4 + actorMarginX, - top: nodeRadius * 3 + actorMarginY, - bottom: nodeRadius * 4 + actorMarginY, + left: nodeRadius * 4 + fragmentMarginX, + right: nodeRadius * 4 + fragmentMarginX, + top: nodeRadius * 3 + fragmentMarginY, + bottom: nodeRadius * 4 + fragmentMarginY, }, }) - layoutActorResult.set(fragmentId, { + layoutFragmentResult.set(fragmentId, { layoutRoot, width, height, - extraInfo: `Actor ${fragmentRoot.data.actor_ids?.join(", ")}` || "", + actorIds: fragmentRoot.data.actorIds ?? [], }) includedFragmentIds.add(fragmentId) } + const fragmentLayout = layout( fragmentDependencyDag.map(({ width: _1, height: _2, id, ...data }) => { - const { width, height } = layoutActorResult.get(id)! + const { width, height } = layoutFragmentResult.get(id)! return { width, height, id, ...data } }), - actorDistanceX, - actorDistanceY + fragmentDistanceX, + fragmentDistanceY ) - const fragmentLayoutPosition = new Map() - fragmentLayout.forEach(({ id, x, y }: ActorBoxPosition) => { + const fragmentLayoutPosition = new Map() + fragmentLayout.forEach(({ id, x, y }: FragmentBoxPosition) => { fragmentLayoutPosition.set(id, { x, y }) }) - const layoutResult = [] - for (const [fragmentId, result] of layoutActorResult) { + + const layoutResult: FragmentLayout[] = [] + for (const [fragmentId, result] of layoutFragmentResult) { const { x, y } = fragmentLayoutPosition.get(fragmentId)! layoutResult.push({ id: fragmentId, x, y, ...result }) } + let svgWidth = 0 let svgHeight = 0 layoutResult.forEach(({ x, y, width, height }) => { svgHeight = Math.max(svgHeight, y + height + 50) svgWidth = Math.max(svgWidth, x + width) }) - const links = generateBoxLinks(fragmentLayout) + const edges = generateBoxEdges(fragmentLayout) + return { layoutResult, - fragmentLayout, svgWidth, svgHeight, - links, + edges, includedFragmentIds, } }, [planNodeDependencies, fragmentDependency]) - type PlanNodeDesc = { - layoutRoot: d3.HierarchyPointNode - width: number - height: number - x: number - y: number - id: string - extraInfo: string - } - const { svgWidth, svgHeight, - links, - fragmentLayout: fragmentDependencyDag, - layoutResult: planNodeDependencyDag, + edges: fragmentEdgeLayout, + layoutResult: fragmentLayout, includedFragmentIds, } = planNodeDependencyDagCallback() useEffect(() => { - if (planNodeDependencyDag) { + if (fragmentLayout) { const svgNode = svgRef.current const svgSelection = d3.select(svgNode) // How to draw edges const treeLink = d3 - .linkHorizontal() - .x((d: Point) => d.x) - .y((d: Point) => d.y) + .linkHorizontal() + .x((d: Position) => d.x) + .y((d: Position) => d.y) - const isSelected = (d: any) => d === selectedFragmentId + const isSelected = (id: string) => id === selectedFragmentId - const applyActor = ( - gSel: d3.Selection - ) => { + // Fragments + const applyFragment = (gSel: FragmentSelection) => { gSel.attr("transform", ({ x, y }) => `translate(${x}, ${y})`) - // Actor text (fragment id) - let text = gSel.select(".actor-text-frag-id") + // Fragment text line 1 (fragment id) + let text = gSel.select(".text-frag-id") if (text.empty()) { - text = gSel.append("text").attr("class", "actor-text-frag-id") + text = gSel.append("text").attr("class", "text-frag-id") } text @@ -221,38 +213,38 @@ export default function FragmentGraph({ .text(({ id }) => `Fragment ${id}`) .attr("font-family", "inherit") .attr("text-anchor", "end") - .attr("dy", ({ height }) => height - actorMarginY + 12) - .attr("dx", ({ width }) => width - actorMarginX) + .attr("dy", ({ height }) => height - fragmentMarginY + 12) + .attr("dx", ({ width }) => width - fragmentMarginX) .attr("fill", "black") .attr("font-size", 12) - // Actor text (actors) - let text2 = gSel.select(".actor-text-actor-id") + // Fragment text line 2 (actor ids) + let text2 = gSel.select(".text-actor-id") if (text2.empty()) { - text2 = gSel.append("text").attr("class", "actor-text-actor-id") + text2 = gSel.append("text").attr("class", "text-actor-id") } text2 .attr("fill", "black") - .text(({ extraInfo }) => extraInfo) + .text(({ actorIds }) => `Actor ${actorIds.join(", ")}`) .attr("font-family", "inherit") .attr("text-anchor", "end") - .attr("dy", ({ height }) => height - actorMarginY + 24) - .attr("dx", ({ width }) => width - actorMarginX) + .attr("dy", ({ height }) => height - fragmentMarginY + 24) + .attr("dx", ({ width }) => width - fragmentMarginX) .attr("fill", "black") .attr("font-size", 12) - // Actor bounding box + // Fragment bounding box let boundingBox = gSel.select(".bounding-box") if (boundingBox.empty()) { boundingBox = gSel.append("rect").attr("class", "bounding-box") } boundingBox - .attr("width", ({ width }) => width - actorMarginX * 2) - .attr("height", ({ height }) => height - actorMarginY * 2) - .attr("x", actorMarginX) - .attr("y", actorMarginY) + .attr("width", ({ width }) => width - fragmentMarginX * 2) + .attr("height", ({ height }) => height - fragmentMarginY * 2) + .attr("x", fragmentMarginX) + .attr("y", fragmentMarginY) .attr("fill", "white") .attr("stroke-width", ({ id }) => (isSelected(id) ? 3 : 1)) .attr("rx", 5) @@ -260,56 +252,43 @@ export default function FragmentGraph({ isSelected(id) ? theme.colors.blue[500] : theme.colors.gray[500] ) - // Actor links - let linkSelection = gSel.select(".links") - if (linkSelection.empty()) { - linkSelection = gSel.append("g").attr("class", "links") + // Stream node edges + let edgeSelection = gSel.select(".edges") + if (edgeSelection.empty()) { + edgeSelection = gSel.append("g").attr("class", "edges") } - const applyLink = ( - sel: d3.Selection< - SVGPathElement, - d3.HierarchyPointLink, - SVGGElement, - PlanNodeDesc - > - ) => sel.attr("d", treeLink) - - const createLink = ( - sel: d3.Selection< - d3.EnterElement, - d3.HierarchyPointLink, - SVGGElement, - PlanNodeDesc - > - ) => { + const applyEdge = (sel: EdgeSelection) => sel.attr("d", treeLink) + + const createEdge = (sel: Enter) => { sel .append("path") .attr("fill", "none") .attr("stroke", theme.colors.gray[700]) .attr("stroke-width", 1.5) - .call(applyLink) + .call(applyEdge) return sel } - const links = linkSelection + const edges = edgeSelection .selectAll("path") .data(({ layoutRoot }) => layoutRoot.links()) + type EdgeSelection = typeof edges - links.enter().call(createLink) - links.call(applyLink) - links.exit().remove() + edges.enter().call(createEdge) + edges.call(applyEdge) + edges.exit().remove() - // Actor nodes + // Stream nodes in fragment let nodes = gSel.select(".nodes") if (nodes.empty()) { nodes = gSel.append("g").attr("class", "nodes") } - const applyActorNode = (g: any) => { - g.attr("transform", (d: any) => `translate(${d.x},${d.y})`) + const applyStreamNode = (g: StreamNodeSelection) => { + g.attr("transform", (d) => `translate(${d.x},${d.y})`) - let circle = g.select("circle") + let circle = g.select("circle") if (circle.empty()) { circle = g.append("circle") } @@ -318,16 +297,16 @@ export default function FragmentGraph({ .attr("fill", theme.colors.blue[500]) .attr("r", nodeRadius) .style("cursor", "pointer") - .on("click", (_d: any, i: any) => openPlanNodeDetail(i)) + .on("click", (_d, i) => openPlanNodeDetail(i.data)) - let text = g.select("text") + let text = g.select("text") if (text.empty()) { text = g.append("text") } text .attr("fill", "black") - .text((d: any) => d.data.name) + .text((d) => d.data.name) .attr("font-family", "inherit") .attr("text-anchor", "middle") .attr("dy", nodeRadius * 1.8) @@ -338,67 +317,77 @@ export default function FragmentGraph({ return g } - const createActorNode = (sel: any) => - sel.append("g").attr("class", "actor-node").call(applyActorNode) + const createStreamNode = (sel: Enter) => + sel.append("g").attr("class", "stream-node").call(applyStreamNode) - const actorNodeSelection = nodes - .selectAll(".actor-node") + const streamNodeSelection = nodes + .selectAll(".stream-node") .data(({ layoutRoot }) => layoutRoot.descendants()) + type StreamNodeSelection = typeof streamNodeSelection - actorNodeSelection.exit().remove() - actorNodeSelection.enter().call(createActorNode) - actorNodeSelection.call(applyActorNode) + streamNodeSelection.exit().remove() + streamNodeSelection.enter().call(createStreamNode) + streamNodeSelection.call(applyStreamNode) } - const createActor = ( - sel: d3.Selection - ) => { - const gSel = sel.append("g").attr("class", "actor").call(applyActor) + const createFragment = (sel: Enter) => { + const gSel = sel + .append("g") + .attr("class", "fragment") + .call(applyFragment) return gSel } - const actorSelection = svgSelection - .select(".actors") - .selectAll(".actor") - .data(planNodeDependencyDag) + const fragmentSelection = svgSelection + .select(".fragments") + .selectAll(".fragment") + .data(fragmentLayout) + type FragmentSelection = typeof fragmentSelection - actorSelection.enter().call(createActor) - actorSelection.call(applyActor) - actorSelection.exit().remove() + fragmentSelection.enter().call(createFragment) + fragmentSelection.call(applyFragment) + fragmentSelection.exit().remove() + // Fragment Edges const edgeSelection = svgSelection - .select(".actor-links") - .selectAll(".actor-link") - .data(links) + .select(".fragment-edges") + .selectAll(".fragment-edge") + .data(fragmentEdgeLayout) + type EdgeSelection = typeof edgeSelection const curveStyle = d3.curveMonotoneX const line = d3 - .line<{ x: number; y: number }>() + .line() .curve(curveStyle) .x(({ x }) => x) .y(({ y }) => y) - const applyEdge = (sel: any) => + const applyEdge = (sel: EdgeSelection) => sel - .attr("d", ({ points }: any) => line(points)) + .attr("d", ({ points }) => line(points)) .attr("fill", "none") - .attr("stroke-width", (d: any) => + .attr("stroke-width", (d) => isSelected(d.source) || isSelected(d.target) ? 2 : 1 ) - .attr("stroke", (d: any) => + .attr("stroke", (d) => isSelected(d.source) || isSelected(d.target) ? theme.colors.blue["500"] : theme.colors.gray["300"] ) - const createEdge = (sel: any) => - sel.append("path").attr("class", "actor-link").call(applyEdge) + const createEdge = (sel: Enter) => + sel.append("path").attr("class", "fragment-edge").call(applyEdge) edgeSelection.enter().call(createEdge) edgeSelection.call(applyEdge) edgeSelection.exit().remove() } - }, [planNodeDependencyDag, links, selectedFragmentId, openPlanNodeDetail]) + }, [ + fragmentLayout, + fragmentEdgeLayout, + selectedFragmentId, + openPlanNodeDetail, + ]) return ( @@ -431,8 +420,8 @@ export default function FragmentGraph({ - - + + diff --git a/dashboard/components/StreamGraph.tsx b/dashboard/components/RelationDependencyGraph.tsx similarity index 82% rename from dashboard/components/StreamGraph.tsx rename to dashboard/components/RelationDependencyGraph.tsx index 890bce6c98ad..99d40ca2615f 100644 --- a/dashboard/components/StreamGraph.tsx +++ b/dashboard/components/RelationDependencyGraph.tsx @@ -19,14 +19,15 @@ import { theme } from "@chakra-ui/react" import * as d3 from "d3" import { useCallback, useEffect, useRef } from "react" import { - ActorPoint, - ActorPointPosition, + FragmentPoint, + FragmentPointPosition, + Position, flipLayoutPoint, - generatePointLinks, + generatePointEdges, } from "../lib/layout" function boundBox( - actorPosition: ActorPointPosition[], + fragmentPosition: FragmentPointPosition[], nodeRadius: number ): { width: number @@ -34,7 +35,7 @@ function boundBox( } { let width = 0 let height = 0 - for (const { x, y, data } of actorPosition) { + for (const { x, y } of fragmentPosition) { width = Math.max(width, x + nodeRadius) height = Math.max(height, y + nodeRadius) } @@ -46,11 +47,11 @@ const rowMargin = 200 const nodeRadius = 10 const layoutMargin = 100 -export function StreamGraph({ +export default function RelationDependencyGraph({ nodes, selectedId, }: { - nodes: ActorPoint[] + nodes: FragmentPoint[] selectedId?: string }) { const svgRef = useRef() @@ -61,12 +62,15 @@ export function StreamGraph({ layerMargin, rowMargin, nodeRadius - ).map(({ x, y, ...data }) => ({ - x: x + layoutMargin, - y: y + layoutMargin, - ...data, - })) - const links = generatePointLinks(layoutMap) + ).map( + ({ x, y, ...data }) => + ({ + x: x + layoutMargin, + y: y + layoutMargin, + ...data, + } as FragmentPointPosition) + ) + const links = generatePointEdges(layoutMap) const { width, height } = boundBox(layoutMap, nodeRadius) return { layoutMap, @@ -85,7 +89,7 @@ export function StreamGraph({ const curveStyle = d3.curveMonotoneY const line = d3 - .line<{ x: number; y: number }>() + .line() .curve(curveStyle) .x(({ x }) => x) .y(({ y }) => y) @@ -120,13 +124,10 @@ export function StreamGraph({ edgeSelection.enter().call(createEdge) edgeSelection.call(applyEdge) - const applyNode = (g: any) => { - g.attr( - "transform", - ({ x, y }: ActorPointPosition) => `translate(${x},${y})` - ) + const applyNode = (g: NodeSelection) => { + g.attr("transform", ({ x, y }) => `translate(${x},${y})`) - let circle = g.select("circle") + let circle = g.select("circle") if (circle.empty()) { circle = g.append("circle") } @@ -134,18 +135,18 @@ export function StreamGraph({ circle .attr("r", nodeRadius) .style("cursor", "pointer") - .attr("fill", ({ id }: ActorPointPosition) => + .attr("fill", ({ id }) => isSelected(id) ? theme.colors.blue["500"] : theme.colors.gray["500"] ) - let text = g.select("text") + let text = g.select("text") if (text.empty()) { text = g.append("text") } text .attr("fill", "black") - .text(({ data: { name } }: ActorPointPosition) => name) + .text(({ name }) => name) .attr("font-family", "inherit") .attr("text-anchor", "middle") .attr("dy", nodeRadius * 2) @@ -161,6 +162,8 @@ export function StreamGraph({ const g = svgSelection.select(".boxes") const nodeSelection = g.selectAll(".node").data(layoutMap) + type NodeSelection = typeof nodeSelection + nodeSelection.enter().call(createNode) nodeSelection.call(applyNode) nodeSelection.exit().remove() diff --git a/dashboard/lib/layout.ts b/dashboard/lib/layout.ts index dd2871051e68..c0e61faeddbf 100644 --- a/dashboard/lib/layout.ts +++ b/dashboard/lib/layout.ts @@ -211,104 +211,103 @@ function dagLayout(nodes: GraphNode[]) { /** * @param fragments - * @returns Layer and row of the actor + * @returns Layer and row of the fragment */ function gridLayout( - fragments: Array -): Map { - // turn ActorBox to GraphNode - let actorBoxIdToActorBox = new Map() + fragments: Array +): Map { + // turn FragmentBox to GraphNode + let idToBox = new Map() for (let fragment of fragments) { - actorBoxIdToActorBox.set(fragment.id, fragment) + idToBox.set(fragment.id, fragment) } - let nodeToActorBoxId = new Map() - let actorBoxIdToNode = new Map() - const getActorBoxNode = (actorboxId: String): GraphNode => { - let rtn = actorBoxIdToNode.get(actorboxId) + let nodeToId = new Map() + let idToNode = new Map() + const getNode = (id: String): GraphNode => { + let rtn = idToNode.get(id) if (rtn !== undefined) { return rtn } let newNode = { nextNodes: new Array(), } - let ab = actorBoxIdToActorBox.get(actorboxId) + let ab = idToBox.get(id) if (ab === undefined) { - throw Error(`no such id ${actorboxId}`) + throw Error(`no such id ${id}`) } for (let id of ab.parentIds) { - // newNode.nextNodes.push(getActorBoxNode(id)) - getActorBoxNode(id).nextNodes.push(newNode) + getNode(id).nextNodes.push(newNode) } - actorBoxIdToNode.set(actorboxId, newNode) - nodeToActorBoxId.set(newNode, actorboxId) + idToNode.set(id, newNode) + nodeToId.set(newNode, id) return newNode } for (let fragment of fragments) { - getActorBoxNode(fragment.id) + getNode(fragment.id) } // run daglayout on GraphNode - let rtn = new Map() + let rtn = new Map() let allNodes = new Array() - for (let _n of nodeToActorBoxId.keys()) { + for (let _n of nodeToId.keys()) { allNodes.push(_n) } let resultMap = dagLayout(allNodes) for (let item of resultMap) { - let abId = nodeToActorBoxId.get(item[0]) - if (!abId) { - throw Error(`no corresponding actorboxid of node ${item[0]}`) + let id = nodeToId.get(item[0]) + if (!id) { + throw Error(`no corresponding fragment id of node ${item[0]}`) } - let ab = actorBoxIdToActorBox.get(abId) - if (!ab) { - throw Error(`actorbox id ${abId} is not present in actorBoxIdToActorBox`) + let fb = idToBox.get(id) + if (!fb) { + throw Error(`fragment id ${id} is not present in idToBox`) } - rtn.set(ab, item[1]) + rtn.set(fb, item[1]) } return rtn } -export interface ActorBox { +export interface FragmentBox { id: string name: string - order: number // preference order, actor box with larger order will be placed at right + order: number // preference order, fragment box with larger order will be placed at right width: number height: number parentIds: string[] fragment?: TableFragments_Fragment } -export interface ActorPoint { +export interface FragmentPoint { id: string name: string - order: number // preference order, actor box with larger order will be placed at right + order: number // preference order, fragment box with larger order will be placed at right parentIds: string[] } -export interface ActorBoxPosition { - id: string +export interface Position { x: number y: number - data: ActorBox } -export interface ActorPointPosition { - id: string - x: number - y: number - data: ActorPoint +export type FragmentBoxPosition = FragmentBox & Position +export type FragmentPointPosition = FragmentPoint & Position + +export interface Edge { + points: Array + source: string + target: string } /** * @param fragments - * @returns the coordination of the top-left corner of the actor box + * @returns the coordination of the top-left corner of the fragment box */ export function layout( - fragments: Array, + fragments: Array, layerMargin: number, rowMargin: number -): ActorBoxPosition[] { +): FragmentBoxPosition[] { let layoutMap = gridLayout(fragments) let layerRequiredWidth = new Map() let rowRequiredHeight = new Map() @@ -316,16 +315,16 @@ export function layout( maxRow = 0 for (let item of layoutMap) { - let ab = item[0], + let fb = item[0], layer = item[1][0], row = item[1][1] let currentWidth = layerRequiredWidth.get(layer) || 0 - if (ab.width > currentWidth) { - layerRequiredWidth.set(layer, ab.width) + if (fb.width > currentWidth) { + layerRequiredWidth.set(layer, fb.width) } let currentHeight = rowRequiredHeight.get(row) || 0 - if (ab.height > currentHeight) { - rowRequiredHeight.set(row, ab.height) + if (fb.height > currentHeight) { + rowRequiredHeight.set(row, fb.height) } maxLayer = max([layer, maxLayer]) || 0 @@ -373,17 +372,16 @@ export function layout( getCumulativeMargin(i, rowMargin, rowCumulativeHeight, rowRequiredHeight) } - let rtn: Array = [] + let rtn: Array = [] for (let [data, [layer, row]] of layoutMap) { let x = layerCumulativeWidth.get(layer) let y = rowCumulativeHeight.get(row) if (x !== undefined && y !== undefined) { rtn.push({ - id: data.id, x, y, - data, + ...data, }) } else { throw Error(`x of layer ${layer}: ${x}, y of row ${row}: ${y} `) @@ -393,30 +391,29 @@ export function layout( } export function flipLayout( - fragments: Array, + fragments: Array, layerMargin: number, rowMargin: number -): ActorBoxPosition[] { +): FragmentBoxPosition[] { const fragments_ = cloneDeep(fragments) for (let fragment of fragments_) { ;[fragment.width, fragment.height] = [fragment.height, fragment.width] } - const actorPosition = layout(fragments_, rowMargin, layerMargin) - return actorPosition.map(({ id, x, y, data }) => ({ - id, - data, + const fragmentPosition = layout(fragments_, rowMargin, layerMargin) + return fragmentPosition.map(({ x, y, ...data }) => ({ x: y, y: x, + ...data, })) } export function layoutPoint( - fragments: Array, + fragments: Array, layerMargin: number, rowMargin: number, nodeRadius: number -): ActorPointPosition[] { - const fragmentBoxes: Array = [] +): FragmentPointPosition[] { + const fragmentBoxes: Array = [] for (let { id, name, order, parentIds, ...others } of fragments) { fragmentBoxes.push({ id, @@ -429,42 +426,40 @@ export function layoutPoint( }) } const result = layout(fragmentBoxes, layerMargin, rowMargin) - return result.map(({ id, x, y, data }) => ({ - id, - data, + return result.map(({ x, y, ...data }) => ({ x: x + nodeRadius, y: y + nodeRadius, + ...data, })) } export function flipLayoutPoint( - fragments: Array, + fragments: Array, layerMargin: number, rowMargin: number, nodeRadius: number -): ActorPointPosition[] { - const actorPosition = layoutPoint( +): FragmentPointPosition[] { + const fragmentPosition = layoutPoint( fragments, rowMargin, layerMargin, nodeRadius ) - return actorPosition.map(({ id, x, y, data }) => ({ - id, - data, + return fragmentPosition.map(({ x, y, ...data }) => ({ x: y, y: x, + ...data, })) } -export function generatePointLinks(layoutMap: ActorPointPosition[]) { +export function generatePointEdges(layoutMap: FragmentPointPosition[]): Edge[] { const links = [] - const fragmentMap = new Map() + const fragmentMap = new Map() for (const x of layoutMap) { fragmentMap.set(x.id, x) } for (const fragment of layoutMap) { - for (const parentId of fragment.data.parentIds) { + for (const parentId of fragment.parentIds) { const parentFragment = fragmentMap.get(parentId)! links.push({ points: [ @@ -479,24 +474,24 @@ export function generatePointLinks(layoutMap: ActorPointPosition[]) { return links } -export function generateBoxLinks(layoutMap: ActorBoxPosition[]) { +export function generateBoxEdges(layoutMap: FragmentBoxPosition[]): Edge[] { const links = [] - const fragmentMap = new Map() + const fragmentMap = new Map() for (const x of layoutMap) { fragmentMap.set(x.id, x) } for (const fragment of layoutMap) { - for (const parentId of fragment.data.parentIds) { + for (const parentId of fragment.parentIds) { const parentFragment = fragmentMap.get(parentId)! links.push({ points: [ { - x: fragment.x + fragment.data.width / 2, - y: fragment.y + fragment.data.height / 2, + x: fragment.x + fragment.width / 2, + y: fragment.y + fragment.height / 2, }, { - x: parentFragment.x + parentFragment.data.width / 2, - y: parentFragment.y + parentFragment.data.height / 2, + x: parentFragment.x + parentFragment.width / 2, + y: parentFragment.y + parentFragment.height / 2, }, ], source: fragment.id, diff --git a/dashboard/pages/dependency_graph.tsx b/dashboard/pages/dependency_graph.tsx index 40a7d4c5c897..33a3a7f29b02 100644 --- a/dashboard/pages/dependency_graph.tsx +++ b/dashboard/pages/dependency_graph.tsx @@ -21,15 +21,15 @@ import Head from "next/head" import Link from "next/link" import { useRouter } from "next/router" import { Fragment, useCallback, useEffect, useState } from "react" -import { StreamGraph } from "../components/StreamGraph" +import RelationDependencyGraph from "../components/RelationDependencyGraph" import Title from "../components/Title" import useErrorToast from "../hook/useErrorToast" -import { ActorPoint } from "../lib/layout" +import { FragmentPoint } from "../lib/layout" import { Relation, getRelations, relationIsStreamingJob } from "./api/streaming" const SIDEBAR_WIDTH = "200px" -function buildDependencyAsEdges(list: Relation[]): ActorPoint[] { +function buildDependencyAsEdges(list: Relation[]): FragmentPoint[] { const edges = [] const relationSet = new Set(list.map((r) => r.id)) for (const r of reverse(sortBy(list, "id"))) { @@ -122,7 +122,7 @@ export default function StreamingGraph() { > Graph {mvDependency && ( - diff --git a/dashboard/pages/fragment_graph.tsx b/dashboard/pages/fragment_graph.tsx index c5e8accab51b..18042c6450dc 100644 --- a/dashboard/pages/fragment_graph.tsx +++ b/dashboard/pages/fragment_graph.tsx @@ -33,11 +33,11 @@ import _ from "lodash" import Head from "next/head" import { useRouter } from "next/router" import { Fragment, useCallback, useEffect, useState } from "react" -import DependencyGraph from "../components/DependencyGraph" +import FragmentDependencyGraph from "../components/FragmentDependencyGraph" import FragmentGraph from "../components/FragmentGraph" import Title from "../components/Title" import useErrorToast from "../hook/useErrorToast" -import { ActorBox } from "../lib/layout" +import { FragmentBox } from "../lib/layout" import { TableFragments, TableFragments_Fragment } from "../proto/gen/meta" import { Dispatcher, StreamNode } from "../proto/gen/stream_plan" import useFetch from "./api/fetch" @@ -53,7 +53,7 @@ export interface PlanNodeDatum { children?: PlanNodeDatum[] operatorId: string | number node: StreamNode | DispatcherNode - actor_ids?: string[] + actorIds?: string[] } function buildPlanNodeDependency( @@ -94,15 +94,17 @@ function buildPlanNodeDependency( return d3.hierarchy({ name: dispatcherName, - actor_ids: fragment.actors.map((a) => a.actorId.toString()), + actorIds: fragment.actors.map((a) => a.actorId.toString()), children: firstActor.nodes ? [hierarchyActorNode(firstActor.nodes)] : [], operatorId: "dispatcher", node: dispatcherNode, }) } -function buildFragmentDependencyAsEdges(fragments: TableFragments): ActorBox[] { - const nodes: ActorBox[] = [] +function buildFragmentDependencyAsEdges( + fragments: TableFragments +): FragmentBox[] { + const nodes: FragmentBox[] = [] const actorToFragmentMapping = new Map() for (const fragmentId in fragments.fragments) { const fragment = fragments.fragments[fragmentId] @@ -128,8 +130,8 @@ function buildFragmentDependencyAsEdges(fragments: TableFragments): ActorBox[] { width: 0, height: 0, order: fragment.fragmentId, - fragment: fragment, - } as ActorBox) + fragment, + } as FragmentBox) } return nodes } @@ -326,7 +328,7 @@ export default function Streaming() { Fragments {fragmentDependencyDag && ( -