diff --git a/spark-ui/src/components/SqlFlow/SqlFlow.tsx b/spark-ui/src/components/SqlFlow/SqlFlow.tsx index 6a859af..729782c 100644 --- a/spark-ui/src/components/SqlFlow/SqlFlow.tsx +++ b/spark-ui/src/components/SqlFlow/SqlFlow.tsx @@ -1,24 +1,41 @@ import React, { FC, useCallback, useEffect, useState } from "react"; import ReactFlow, { + addEdge, ConnectionLineType, Controls, ReactFlowInstance, - addEdge, useEdgesState, useNodesState, } from "reactflow"; -import { Box, Drawer, FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import { + Box, + Drawer, + FormControl, + InputLabel, + MenuItem, + Select, +} from "@mui/material"; import "reactflow/dist/style.css"; import { useAppDispatch, useAppSelector } from "../../Hooks"; import { EnrichedSparkSQL, GraphFilter } from "../../interfaces/AppStore"; -import { setSQLMode, setSelectedStage } from "../../reducers/GeneralSlice"; -import SqlLayoutService from "./SqlLayoutService"; -import StageIconDrawer from "./StageIconDrawer"; -import { StageNode, StageNodeName } from "./StageNode"; +import { setSelectedStage, setSQLMode } from "../../reducers/GeneralSlice"; +import StageIconDrawer from "./drawerComponents/StageIconDrawer"; +import StageGroupNode, { + StageGroupNodeName, +} from "./flowComponents/StageGroupNode/StageGroupNode"; +import { StageNode, StageNodeName } from "./flowComponents/StageNode/StageNode"; +import { sqlElementsToLayout } from "./SqlLayoutService/SqlLayoutService"; const options = { hideAttribution: true }; -const nodeTypes = { [StageNodeName]: StageNode }; +const nodeTypes = { + [StageNodeName]: StageNode, + [StageGroupNodeName]: StageGroupNode, +}; + +// this is a mock to allow for use of external configuration +// const shouldUseGroupedLayout = true; +const shouldUseGroupedLayout = false; const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({ sparkSQL, @@ -31,26 +48,18 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({ const graphFilter = useAppSelector((state) => state.general.sqlMode); const selectedStage = useAppSelector((state) => state.general.selectedStage); - React.useEffect(() => { - if (!sparkSQL) return; - const { layoutNodes, layoutEdges } = SqlLayoutService.SqlElementsToLayout( - sparkSQL, - graphFilter, - ); - - setNodes(layoutNodes); - }, [sparkSQL.metricUpdateId]); - useEffect(() => { if (!sparkSQL) return; - const { layoutNodes, layoutEdges } = SqlLayoutService.SqlElementsToLayout( + + const { layoutNodes, layoutEdges } = sqlElementsToLayout( sparkSQL, graphFilter, + shouldUseGroupedLayout, ); setNodes(layoutNodes); setEdges(layoutEdges); - }, [sparkSQL.uniqueId, graphFilter]); + }, [sparkSQL.metricUpdateId, sparkSQL.uniqueId, graphFilter]); useEffect(() => { if (instance) { @@ -58,8 +67,6 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({ } }, [instance, edges]); - useEffect(() => { }, [nodes]); - const onConnect = useCallback( (params: any) => setEdges((eds) => @@ -119,10 +126,11 @@ const SqlFlow: FC<{ sparkSQL: EnrichedSparkSQL }> = ({ dispatch(setSelectedStage({ selectedStage: undefined }))} + onClose={() => + dispatch(setSelectedStage({ selectedStage: undefined })) + } > - + diff --git a/spark-ui/src/components/SqlFlow/SqlLayoutService.ts b/spark-ui/src/components/SqlFlow/SqlLayoutService.ts deleted file mode 100644 index 54f3c65..0000000 --- a/spark-ui/src/components/SqlFlow/SqlLayoutService.ts +++ /dev/null @@ -1,85 +0,0 @@ -import dagre from "dagre"; -import { Edge, Node, Position } from "reactflow"; -import { v4 as uuidv4 } from "uuid"; -import { - EnrichedSparkSQL, - EnrichedSqlEdge, - EnrichedSqlNode, - GraphFilter, -} from "../../interfaces/AppStore"; -import { StageNodeName } from "./StageNode"; - -const nodeWidth = 280; -const nodeHeight = 280; - -const getLayoutedElements = ( - nodes: Node[], - edges: Edge[], -): { layoutNodes: Node[]; layoutEdges: Edge[] } => { - const dagreGraph = new dagre.graphlib.Graph(); - dagreGraph.setDefaultEdgeLabel(() => ({})); - dagreGraph.setGraph({ rankdir: "LR" }); - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight }); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - nodes.forEach((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - node.targetPosition = Position.Left; - node.sourcePosition = Position.Right; - - // We are shifting the dagre node position (anchor=center center) to the top left - // so it matches the React Flow node anchor point (top left). - node.position = { - x: nodeWithPosition.x - nodeWidth / 2, - y: nodeWithPosition.y - nodeHeight / 2, - }; - - return node; - }); - - return { layoutNodes: nodes, layoutEdges: edges }; -}; - -class SqlLayoutService { - static SqlElementsToLayout( - sql: EnrichedSparkSQL, - graphFilter: GraphFilter, - ): { layoutNodes: Node[]; layoutEdges: Edge[] } { - const { nodesIds, edges } = sql.filters[graphFilter]; - - const flowNodes: Node[] = sql.nodes - .filter((node) => nodesIds.includes(node.nodeId)) - .map((node: EnrichedSqlNode) => { - return { - id: node.nodeId.toString(), - data: { sqlId: sql.id, node: node }, - type: StageNodeName, - position: { x: 0, y: 0 }, - }; - }); - const flowEdges: Edge[] = edges.map((edge: EnrichedSqlEdge) => { - return { - id: uuidv4(), - source: edge.fromId.toString(), - animated: true, - target: edge.toId.toString(), - }; - }); - - const { layoutNodes, layoutEdges } = getLayoutedElements( - flowNodes, - flowEdges, - ); - return { layoutNodes: layoutNodes, layoutEdges: layoutEdges }; - } -} - -export default SqlLayoutService; diff --git a/spark-ui/src/components/SqlFlow/SqlLayoutService/SqlLayoutService.ts b/spark-ui/src/components/SqlFlow/SqlLayoutService/SqlLayoutService.ts new file mode 100644 index 0000000..f4e3daa --- /dev/null +++ b/spark-ui/src/components/SqlFlow/SqlLayoutService/SqlLayoutService.ts @@ -0,0 +1,48 @@ +import { EnrichedSparkSQL, GraphFilter } from "../../../interfaces/AppStore"; +import { getLayoutElements } from "./dagreLayouts"; +import { + getFlowNodes, + getTopLevelNodes, + toFlowEdge, + transformEdgesToGroupEdges, +} from "./layoutServiceBuilders"; + +const getFlatFlowElements = ( + sql: EnrichedSparkSQL, + graphFilter: GraphFilter, +) => { + const { edges } = sql.filters[graphFilter]; + const flowNodes = getFlowNodes(sql, graphFilter); + const flowEdges = edges.map(toFlowEdge); + + return { flowNodes, flowEdges }; +}; + +const getGroupedFlowElements = ( + sql: EnrichedSparkSQL, + graphFilter: GraphFilter, +) => { + const { edges } = sql.filters[graphFilter]; + + const flowNodes = getFlowNodes(sql, graphFilter); + const topLevelNodes = getTopLevelNodes(flowNodes); + const topLevelEdges = transformEdgesToGroupEdges( + flowNodes, + topLevelNodes, + edges, + ); + + return { flowNodes: topLevelNodes, flowEdges: topLevelEdges }; +}; + +export function sqlElementsToLayout( + sql: EnrichedSparkSQL, + graphFilter: GraphFilter, + useGroupedLayout: boolean, +) { + const { flowNodes, flowEdges } = useGroupedLayout + ? getGroupedFlowElements(sql, graphFilter) + : getFlatFlowElements(sql, graphFilter); + + return getLayoutElements(flowNodes, flowEdges); +} diff --git a/spark-ui/src/components/SqlFlow/SqlLayoutService/dagreLayouts.ts b/spark-ui/src/components/SqlFlow/SqlLayoutService/dagreLayouts.ts new file mode 100644 index 0000000..51386ac --- /dev/null +++ b/spark-ui/src/components/SqlFlow/SqlLayoutService/dagreLayouts.ts @@ -0,0 +1,52 @@ +import dagre from "dagre"; +import { Edge, Node, Position } from "reactflow"; +import { isNodeAGroup } from "../flowComponents/StageGroupNode/StageGroupNode"; + +const buildDagreGraph = (rankdir: "LR" | "TB" = "LR") => { + const dagreGraph = new dagre.graphlib.Graph(); + dagreGraph.setDefaultEdgeLabel(() => ({})); + dagreGraph.setGraph({ rankdir }); + + return dagreGraph; +}; + +const nodeSize = 280; +export const nodeWidth = nodeSize; +export const nodeHeight = nodeSize; +const groupPadding = 40; +const groupWidth = nodeWidth + groupPadding * 2; + +export const getLayoutElements = ( + nodes: Node[], + edges: Edge[], +): { layoutNodes: Node[]; layoutEdges: Edge[] } => { + const dagreGraph = buildDagreGraph(); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { + width: isNodeAGroup(node) ? groupWidth : nodeWidth, + height: nodeHeight, + }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target); + }); + + dagre.layout(dagreGraph); + + nodes.forEach((node) => { + const nodeWithPosition = dagreGraph.node(node.id); + node.targetPosition = Position.Left; + node.sourcePosition = Position.Right; + + // We are shifting the dagre node position (anchor=center center) to the top left + // so it matches the React Flow node anchor point (top left). + node.position = { + x: nodeWithPosition.x - nodeWidth / 2, + y: nodeWithPosition.y - nodeHeight / 2, + }; + }); + + return { layoutNodes: nodes, layoutEdges: edges }; +}; diff --git a/spark-ui/src/components/SqlFlow/SqlLayoutService/layoutServiceBuilders.ts b/spark-ui/src/components/SqlFlow/SqlLayoutService/layoutServiceBuilders.ts new file mode 100644 index 0000000..3e6f692 --- /dev/null +++ b/spark-ui/src/components/SqlFlow/SqlLayoutService/layoutServiceBuilders.ts @@ -0,0 +1,134 @@ +import { Node } from "reactflow"; +import { v4 as uuidv4 } from "uuid"; +import { + EnrichedSparkSQL, + EnrichedSqlEdge, + EnrichedSqlNode, + GraphFilter, +} from "../../../interfaces/AppStore"; +import { StageGroupNodeName } from "../flowComponents/StageGroupNode/StageGroupNode"; +import { StageNodeName } from "../flowComponents/StageNode/StageNode"; + +const getPosition = (x = 0, y = 0) => ({ x, y }); +const getStageIdString = (id = "") => `stage-${id}`; + +const toFlowNode = (node: EnrichedSqlNode, sqlId: string) => ({ + id: node.nodeId.toString(), + data: { sqlId, node: node }, + type: StageNodeName, + position: getPosition(), +}); + +const toFlowGroupNode = (nodes: Node[], stageId: number) => ({ + type: StageGroupNodeName, + id: getStageIdString(stageId.toString()), + data: { + friendlyName: `Stage ${stageId}`, + nodes, + }, + position: getPosition(), +}); + +interface ToFlowEdgeParams { + fromId: string | number; + toId: string | number; +} +export const toFlowEdge = ({ fromId, toId }: ToFlowEdgeParams) => ({ + id: uuidv4(), + source: fromId.toString(), + animated: true, + target: toId.toString(), +}); + +export const getFlowNodes = ( + sql: EnrichedSparkSQL, + graphFilter: GraphFilter, +) => { + const { nodes } = sql; + const { nodesIds } = sql.filters[graphFilter]; + + return nodes + .filter((node) => nodesIds.includes(node.nodeId)) + .map((node: EnrichedSqlNode) => toFlowNode(node, sql.id)); +}; + +const buildAllNodeGroups = (flowNodes: Node[]): Map => { + const allGroupsWithNodes = flowNodes.reduce((stageGroups, node) => { + const stageId = node.data.node.stage?.stageId; + + if (stageId === undefined) return stageGroups; + + if (!stageGroups.has(stageId)) { + stageGroups.set(stageId, []); + } + + stageGroups.get(stageId)?.push(node); + + return stageGroups; + }, new Map()); + + return allGroupsWithNodes; +}; + +export const getTopLevelNodes = (flowNodes: Node[]): Node[] => { + const allGroupsWithNodes = buildAllNodeGroups(flowNodes); + + return Array.from(allGroupsWithNodes).map(([stageId, nodes]) => { + if (nodes.length === 1) { + return nodes[0]; + } else { + return toFlowGroupNode(nodes, stageId); + } + }); +}; + +const getNodeToGroupMap = (flowNodes: Node[]) => + flowNodes.reduce>((nodeToStageMap, node) => { + const stageId = node.data.node.stage?.stageId; + nodeToStageMap[node.id] = getStageIdString(stageId); + return nodeToStageMap; + }, {}); + +const getResolvedEdgeConnection = ( + nodeId: number, + nodeIdToStageGroupId: Record, + groupNodes: Node[], +) => { + const stageGroupId = nodeIdToStageGroupId[nodeId]; + const stageGroup = groupNodes.find((node) => node.id === stageGroupId); + + return stageGroup && stageGroup.data.nodes.length > 1 + ? stageGroupId + : String(nodeId); +}; + +export const transformEdgesToGroupEdges = ( + flowNodes: Node[], + groupNodes: Node[], + originalEdges: EnrichedSqlEdge[], +) => { + const nodeIdToStageGroupId = getNodeToGroupMap(flowNodes); + + return originalEdges + .filter(({ fromId, toId }) => { + const fromStageGroupId = nodeIdToStageGroupId[fromId]; + const toStageGroupId = nodeIdToStageGroupId[toId]; + + return fromStageGroupId !== toStageGroupId; + }) + .map(({ fromId, toId }) => { + const resolvedFromId = getResolvedEdgeConnection( + fromId, + nodeIdToStageGroupId, + groupNodes, + ); + + const resolvedToId = getResolvedEdgeConnection( + toId, + nodeIdToStageGroupId, + groupNodes, + ); + + return toFlowEdge({ fromId: resolvedFromId, toId: resolvedToId }); + }); +}; diff --git a/spark-ui/src/components/SqlFlow/BytesDistributionChart.tsx b/spark-ui/src/components/SqlFlow/drawerComponents/BytesDistributionChart.tsx similarity index 95% rename from spark-ui/src/components/SqlFlow/BytesDistributionChart.tsx rename to spark-ui/src/components/SqlFlow/drawerComponents/BytesDistributionChart.tsx index 1573449..47b6f27 100644 --- a/spark-ui/src/components/SqlFlow/BytesDistributionChart.tsx +++ b/spark-ui/src/components/SqlFlow/drawerComponents/BytesDistributionChart.tsx @@ -1,7 +1,7 @@ import { ApexOptions } from "apexcharts"; import React from "react"; import ReactApexChart from "react-apexcharts"; -import { humanFileSize } from "../../utils/FormatUtils"; +import { humanFileSize } from "../../../utils/FormatUtils"; export default function BytesDistributionChart({ bytesDist, diff --git a/spark-ui/src/components/SqlFlow/DurationDistributionChart.tsx b/spark-ui/src/components/SqlFlow/drawerComponents/DurationDistributionChart.tsx similarity index 95% rename from spark-ui/src/components/SqlFlow/DurationDistributionChart.tsx rename to spark-ui/src/components/SqlFlow/drawerComponents/DurationDistributionChart.tsx index e437c6b..b673d4d 100644 --- a/spark-ui/src/components/SqlFlow/DurationDistributionChart.tsx +++ b/spark-ui/src/components/SqlFlow/drawerComponents/DurationDistributionChart.tsx @@ -2,7 +2,7 @@ import { ApexOptions } from "apexcharts"; import { duration } from "moment"; import React from "react"; import ReactApexChart from "react-apexcharts"; -import { humanizeTimeDiff } from "../../utils/FormatUtils"; +import { humanizeTimeDiff } from "../../../utils/FormatUtils"; export default function DurationDistributionChart({ durationDist, diff --git a/spark-ui/src/components/SqlFlow/NumbersDistributionChart.tsx b/spark-ui/src/components/SqlFlow/drawerComponents/NumbersDistributionChart.tsx similarity index 100% rename from spark-ui/src/components/SqlFlow/NumbersDistributionChart.tsx rename to spark-ui/src/components/SqlFlow/drawerComponents/NumbersDistributionChart.tsx diff --git a/spark-ui/src/components/SqlFlow/StageIconDrawer.tsx b/spark-ui/src/components/SqlFlow/drawerComponents/StageIconDrawer.tsx similarity index 97% rename from spark-ui/src/components/SqlFlow/StageIconDrawer.tsx rename to spark-ui/src/components/SqlFlow/drawerComponents/StageIconDrawer.tsx index ba8129f..d04efe6 100644 --- a/spark-ui/src/components/SqlFlow/StageIconDrawer.tsx +++ b/spark-ui/src/components/SqlFlow/drawerComponents/StageIconDrawer.tsx @@ -7,18 +7,18 @@ import { } from "@mui/material"; import { duration } from "moment"; import React from "react"; -import { useAppSelector } from "../../Hooks"; +import { useAppSelector } from "../../../Hooks"; import { SQLNodeExchangeStageData, SQLNodeStageData, SparkStageStore, -} from "../../interfaces/AppStore"; -import { humanFileSize, humanizeTimeDiff } from "../../utils/FormatUtils"; -import { BASE_CURRENT_PAGE } from "../../utils/UrlConsts"; -import { getBaseAppUrl } from "../../utils/UrlUtils"; +} from "../../../interfaces/AppStore"; +import { humanFileSize, humanizeTimeDiff } from "../../../utils/FormatUtils"; +import { BASE_CURRENT_PAGE } from "../../../utils/UrlConsts"; +import { getBaseAppUrl } from "../../../utils/UrlUtils"; import BytesDistributionChart from "./BytesDistributionChart"; -import DurationDistributionChart from "./DurationDistributionChart"; import NumbersDistributionChart from "./NumbersDistributionChart"; +import DurationDistributionChart from "./DurationDistributionChart"; const linkToStage = (stageId: number) => { window.open( diff --git a/spark-ui/src/components/SqlFlow/flowComponents/StageGroupNode/StageGroupNode.module.css b/spark-ui/src/components/SqlFlow/flowComponents/StageGroupNode/StageGroupNode.module.css new file mode 100644 index 0000000..de0a35e --- /dev/null +++ b/spark-ui/src/components/SqlFlow/flowComponents/StageGroupNode/StageGroupNode.module.css @@ -0,0 +1,15 @@ +.groupContainer { + background-color: #ffffff50; + border-radius: 15px; + padding: 20px; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; +} + +.groupName { + text-decoration: underline; +} diff --git a/spark-ui/src/components/SqlFlow/flowComponents/StageGroupNode/StageGroupNode.tsx b/spark-ui/src/components/SqlFlow/flowComponents/StageGroupNode/StageGroupNode.tsx new file mode 100644 index 0000000..5b2ae83 --- /dev/null +++ b/spark-ui/src/components/SqlFlow/flowComponents/StageGroupNode/StageGroupNode.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { Handle, Node, Position } from "reactflow"; +import { StageNode } from "../StageNode/StageNode"; +import styles from "./StageGroupNode.module.css"; + +export const StageGroupNodeName: string = "stageGroupNode"; + +export const isNodeAGroup = (node: Node) => node.type === StageGroupNodeName; + +interface StageGroupNodeProps { + data: { + nodes: Node[]; + friendlyName: string; + }; +} + +export default function StageGroupNode({ data }: StageGroupNodeProps) { + const { friendlyName, nodes } = data; + + return ( +
+ +

{friendlyName}

+ {nodes.map((node) => { + return ; + })} + +
+ ); +} diff --git a/spark-ui/src/components/SqlFlow/StageIcon.tsx b/spark-ui/src/components/SqlFlow/flowComponents/StageNode/StageIcon.tsx similarity index 95% rename from spark-ui/src/components/SqlFlow/StageIcon.tsx rename to spark-ui/src/components/SqlFlow/flowComponents/StageNode/StageIcon.tsx index 5f3bff2..41e96da 100644 --- a/spark-ui/src/components/SqlFlow/StageIcon.tsx +++ b/spark-ui/src/components/SqlFlow/flowComponents/StageNode/StageIcon.tsx @@ -7,12 +7,12 @@ import { Typography } from "@mui/material"; import React from "react"; -import { useAppSelector } from "../../Hooks"; +import { useAppSelector } from "../../../../Hooks"; import { SQLNodeExchangeStageData, SQLNodeStageData -} from "../../interfaces/AppStore"; -import ExceptionIcon from "../ExceptionIcon"; +} from '../../../../interfaces/AppStore'; +import ExceptionIcon from "../../../ExceptionIcon"; function CircularProgressWithLabel( props: CircularProgressProps & { value: number }, diff --git a/spark-ui/src/components/SqlFlow/StageNode.tsx b/spark-ui/src/components/SqlFlow/flowComponents/StageNode/StageNode.tsx similarity index 96% rename from spark-ui/src/components/SqlFlow/StageNode.tsx rename to spark-ui/src/components/SqlFlow/flowComponents/StageNode/StageNode.tsx index 6cfc61f..902b411 100644 --- a/spark-ui/src/components/SqlFlow/StageNode.tsx +++ b/spark-ui/src/components/SqlFlow/flowComponents/StageNode/StageNode.tsx @@ -5,20 +5,20 @@ import React, { FC } from "react"; import SyntaxHighlighter from "react-syntax-highlighter"; import { a11yDark } from "react-syntax-highlighter/dist/esm/styles/hljs"; import { Handle, Position } from "reactflow"; -import { useAppDispatch, useAppSelector } from "../../Hooks"; -import { EnrichedSqlNode } from "../../interfaces/AppStore"; -import { SqlMetric } from "../../interfaces/SparkSQLs"; -import { setSelectedStage } from '../../reducers/GeneralSlice'; -import { truncateMiddle } from "../../reducers/PlanParsers/PlanParserUtils"; +import { useAppDispatch, useAppSelector } from "../../../../Hooks"; +import { EnrichedSqlNode } from "../../../../interfaces/AppStore"; +import { SqlMetric } from "../../../../interfaces/SparkSQLs"; +import { setSelectedStage } from '../../../../reducers/GeneralSlice'; +import { truncateMiddle } from "../../../../reducers/PlanParsers/PlanParserUtils"; import { calculatePercentage, humanFileSize, humanizeTimeDiff, parseBytesString, -} from "../../utils/FormatUtils"; -import AlertBadge, { TransperantTooltip } from "../AlertBadge/AlertBadge"; -import { ConditionalWrapper } from "../InfoBox/InfoBox"; -import StageIcon from "./StageIcon"; +} from "../../../../utils/FormatUtils"; +import AlertBadge, { TransperantTooltip } from "../../../AlertBadge/AlertBadge"; +import { ConditionalWrapper } from "../../../InfoBox/InfoBox"; +import StageIcon from './StageIcon'; import styles from "./node-style.module.css"; export const StageNodeName: string = "stageNode"; @@ -131,7 +131,7 @@ function handleAddedRemovedMetrics( } export const StageNode: FC<{ - data: { sqlId: string; node: EnrichedSqlNode }; + data: { sqlId: string; node: EnrichedSqlNode; }; }> = ({ data }): JSX.Element => { const dispatch = useAppDispatch(); const alerts = useAppSelector((state) => state.spark.alerts); @@ -516,7 +516,11 @@ export const StageNode: FC<{ return ( <> - +
diff --git a/spark-ui/src/components/SqlFlow/node-style.module.css b/spark-ui/src/components/SqlFlow/flowComponents/StageNode/node-style.module.css similarity index 100% rename from spark-ui/src/components/SqlFlow/node-style.module.css rename to spark-ui/src/components/SqlFlow/flowComponents/StageNode/node-style.module.css diff --git a/spark-ui/src/interfaces/AppStore.ts b/spark-ui/src/interfaces/AppStore.ts index 20e5248..56bac5d 100644 --- a/spark-ui/src/interfaces/AppStore.ts +++ b/spark-ui/src/interfaces/AppStore.ts @@ -274,6 +274,7 @@ export interface EnrichedSqlNode { export interface SQLNodeExchangeStageData { type: "exchange"; + stageId: number; writeStage: number; readStage: number; status: string; diff --git a/spark-ui/src/reducers/SQLNodeStageReducer.ts b/spark-ui/src/reducers/SQLNodeStageReducer.ts index f816080..d91be0b 100644 --- a/spark-ui/src/reducers/SQLNodeStageReducer.ts +++ b/spark-ui/src/reducers/SQLNodeStageReducer.ts @@ -63,6 +63,7 @@ export function calculateSQLNodeStage(sql: EnrichedSparkSQL): EnrichedSparkSQL { ...node, stage: { type: "exchange", + stageId: node.nodeId, writeStage: previousNode.stage?.type === "onestage" ? previousNode.stage.stageId diff --git a/spark-ui/src/tabs/SummaryTab.tsx b/spark-ui/src/tabs/SummaryTab.tsx index 67e5717..30bdeac 100644 --- a/spark-ui/src/tabs/SummaryTab.tsx +++ b/spark-ui/src/tabs/SummaryTab.tsx @@ -1,7 +1,7 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import BuildIcon from "@mui/icons-material/Build"; import { Box, Fade, IconButton, Tooltip, Typography } from "@mui/material"; -import * as React from "react"; +import React, { useEffect, useMemo, useState } from "react"; import SqlFlow from "../components/SqlFlow/SqlFlow"; import SqlTable from "../components/SqlTable/SqlTable"; import SummaryBar from "../components/SummaryBar"; @@ -13,19 +13,20 @@ import { getBaseAppUrl } from "../utils/UrlUtils"; export default function SummaryTab() { const sql = useAppSelector((state) => state.spark.sql); - const [selectedSqlId, setSelectedSqlId] = React.useState( + const [selectedSqlId, setSelectedSqlId] = useState( undefined, ); - const selectedSql = - selectedSqlId === undefined - ? undefined - : sql?.sqls.find((sql) => sql.id === selectedSqlId); - React.useEffect(() => { + const selectedSql = useMemo( + () => sql?.sqls.find((sql) => sql.id === selectedSqlId), + [selectedSqlId], + ); + + useEffect(() => { MixpanelService.TrackPageView(); }, []); - React.useEffect(() => { + useEffect(() => { function handleEscapeKey(event: KeyboardEvent) { if (event.code === "Escape") { setSelectedSqlId(undefined);