diff --git a/dev/eslint.js b/dev/eslint.js index 24b5170b436a..abb06526fe96 100644 --- a/dev/eslint.js +++ b/dev/eslint.js @@ -40,6 +40,7 @@ module.exports = { "dataTables.rowsGroup.js" ], "parserOptions": { - "sourceType": "module" + "sourceType": "module", + "ecmaVersion": "latest" } } diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css index d6a498e93872..032957940681 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css @@ -15,35 +15,35 @@ * limitations under the License. */ -#plan-viz-graph .label { +svg g.label { font-size: 0.85rem; font-weight: normal; text-shadow: none; color: #333; } -#plan-viz-graph svg g.cluster rect { +svg g.cluster rect { fill: #A0DFFF; stroke: #3EC0FF; stroke-width: 1px; } -#plan-viz-graph svg g.node rect { +svg g.node rect { fill: #C3EBFF; stroke: #3EC0FF; stroke-width: 1px; } /* Highlight the SparkPlan node name */ -#plan-viz-graph svg text :first-child:not(.stageId-and-taskId-metrics) { +svg text :first-child:not(.stageId-and-taskId-metrics) { font-weight: bold; } -#plan-viz-graph svg text { +svg text { fill: #333; } -#plan-viz-graph svg path { +svg path { stroke: #444; stroke-width: 1.5px; } @@ -58,19 +58,19 @@ word-wrap: break-word; } -#plan-viz-graph svg g.node rect.selected { +svg g.node rect.selected { fill: #E25A1CFF; stroke: #317EACFF; stroke-width: 2px; } -#plan-viz-graph svg g.node rect.linked { +svg g.node rect.linked { fill: #FFC106FF; stroke: #317EACFF; stroke-width: 2px; } -#plan-viz-graph svg path.linked { +svg path.linked { fill: #317EACFF; stroke: #317EACFF; stroke-width: 2px; diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js index d4cc45a1639a..37bae5e4b774 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js @@ -312,3 +312,36 @@ function collectLinks(map, key, value) { } map.get(key).add(value); } + +function downloadPlanBlob(b, ext) { + const link = document.createElement("a"); + link.href = URL.createObjectURL(b); + link.download = `plan.${ext}`; + link.click(); +} + +document.getElementById("plan-viz-download-btn").addEventListener("click", async function () { + const format = document.getElementById("plan-viz-format-select").value; + let blob; + if (format === "svg") { + const svg = planVizContainer().select("svg").node().cloneNode(true); + let css = ""; + try { + css = await fetch("/static/sql/spark-sql-viz.css").then((resp) => resp.text()); + } catch (e) { + console.error("Failed to fetch CSS for SVG download", e); + } + d3.select(svg).insert("style", ":first-child").text(css); + const svgData = new XMLSerializer().serializeToString(svg); + blob = new Blob([svgData], { type: "image/svg+xml" }); + } else if (format === "dot") { + const dot = d3.select("#plan-viz-metadata .dot-file").text().trim(); + blob = new Blob([dot], { type: "text/plain" }); + } else if (format === "txt") { + const txt = d3.select("#physical-plan-details pre").text().trim(); + blob = new Blob([txt], { type: "text/plain" }); + } else { + return; + } + downloadPlanBlob(blob, format); +}); diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala index bbdf9b4c4bd8..e705b5cf46ed 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala @@ -75,6 +75,16 @@ class ExecutionPage(parent: SQLTab) extends WebUIPage("execution") with Logging {jobLinks(JobExecutionStatus.SUCCEEDED, "Succeeded Jobs:")} {jobLinks(JobExecutionStatus.FAILED, "Failed Jobs:")} +