This is an automated email from the ASF dual-hosted git repository.
yaooqinn pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new 595ecd557939 [SPARK-56809][UI] Show SQL description and metadata on
the execution detail page
595ecd557939 is described below
commit 595ecd55793939e2c737dc49cfe7ffb5b397175b
Author: Kent Yao <[email protected]>
AuthorDate: Fri May 15 18:03:48 2026 +0800
[SPARK-56809][UI] Show SQL description and metadata on the execution detail
page
### What changes were proposed in this pull request?
Render the SQL execution detail page header as a single-row DataTable that
reuses the same column rendering as the SQL listing page. The summary at the
top of `/SQL/execution/?id=N` now shows ID, Query ID, Status, Description,
Submitted, Duration, Succeeded Jobs and Error Message in the same shape as the
listing page.
To avoid duplicating column logic, the shared helpers, the appId resolver,
the API base builder and the column factory are extracted into a new
`sql-table-utils.js`, sourced by both `AllExecutionsPage` and `ExecutionPage`.
The detail-page mode of the column factory:
1. skips truncation and self-links,
2. renders Description / Error Message as `<pre class="sql-cell-pre">` so
SQL formatting is preserved,
3. wraps long or multi-line values in a native `<details><summary>`
disclosure so the cells start collapsed and stay compact.
Parent / Sub Execution links remain on the detail page but move under the
new summary table as a small `text-muted` line, only when applicable.
### Why are the changes needed?
The SQL execution detail page currently shows an unstructured `<ul>`
summary that omits the description and the query id. Users have to bounce back
to the listing page to see what a query was about. Reusing the listing-page
columns gives a consistent, scannable header on every execution detail page and
avoids two parallel renderers drifting over time.
### Does this PR introduce _any_ user-facing change?
Yes, the SQL execution detail page header is restyled. The set of metadata
fields shown there is the union of what the listing page already shows. Long
descriptions and error messages can be expanded inline with the disclosure
triangle.
### How was this patch tested?
- `build/sbt sql/scalastyle` — clean
- `dev/lint-js` — clean
- `build/sbt 'sql/testOnly *AllExecutionsPageWithInMemoryStoreSuite'` — 5 /
5 passed; assertion added that the new `sql-table-utils.js` script tag is
rendered into the listing page.
- Manual browser verification on a local Spark UI: short single-line
descriptions render as plain `<pre>`; multi-line / long descriptions and long
error messages render as a collapsed `<details>` with a one-line summary; the
SQL listing page is unchanged.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: GitHub Copilot CLI 1.0.47 with Claude Opus 4.7
Closes #55861 from yaooqinn/SPARK-56809.
Authored-by: Kent Yao <[email protected]>
Signed-off-by: Kent Yao <[email protected]>
(cherry picked from commit a27b8fef8df69cc03591eef4165144a667319bfa)
Signed-off-by: Kent Yao <[email protected]>
---
.../sql/execution/ui/static/allexecutionspage.js | 184 +--------------
.../spark/sql/execution/ui/static/executionpage.js | 67 ++++++
.../sql/execution/ui/static/spark-sql-viz.css | 27 +++
.../sql/execution/ui/static/sql-table-utils.js | 262 +++++++++++++++++++++
.../spark/sql/execution/ui/AllExecutionsPage.scala | 2 +
.../spark/sql/execution/ui/ExecutionPage.scala | 90 +++----
.../sql/execution/ui/AllExecutionsPageSuite.scala | 1 +
7 files changed, 412 insertions(+), 221 deletions(-)
diff --git
a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
index b741a18789d6..ed4561c73224 100644
---
a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
+++
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/allexecutionspage.js
@@ -15,107 +15,11 @@
* limitations under the License.
*/
-/* global $, uiRoot, appBasePath */
-/* eslint-disable no-unused-vars */
-
-function formatDurationSql(milliseconds) {
- if (milliseconds < 100) return parseInt(milliseconds).toFixed(1) + " ms";
- var seconds = milliseconds / 1000;
- if (seconds < 1) return seconds.toFixed(1) + " s";
- if (seconds < 60) return seconds.toFixed(0) + " s";
- var minutes = seconds / 60;
- if (minutes < 10) return minutes.toFixed(1) + " min";
- if (minutes < 60) return minutes.toFixed(0) + " min";
- var hours = minutes / 60;
- return hours.toFixed(1) + " h";
-}
-
-function formatDateSql(dateStr) {
- if (!dateStr) return "";
- try {
- var dt = new Date(dateStr.replace("GMT", "Z"));
- if (isNaN(dt.getTime())) return dateStr;
- var pad = function(n) { return n < 10 ? "0" + n : n; };
- return dt.getFullYear() + "-" + pad(dt.getMonth() + 1) + "-" +
pad(dt.getDate()) + " " +
- pad(dt.getHours()) + ":" + pad(dt.getMinutes()) + ":" +
pad(dt.getSeconds());
- } catch (e) { return dateStr; }
-}
-function escapeHtml(str) {
- if (!str) return str;
- var div = document.createElement("div");
- div.appendChild(document.createTextNode(str));
- return div.innerHTML;
-}
-/* eslint-enable no-unused-vars */
-
-function createSQLTableEndPoint(appId) {
- var words = document.baseURI.split("/");
- var ind = words.indexOf("proxy");
- var newBaseURI;
- if (ind > 0) {
- appId = words[ind + 1];
- newBaseURI = words.slice(0, ind + 2).join("/");
- return newBaseURI + "/api/v1/applications/" + appId + "/sql/sqlTable";
- }
- ind = words.indexOf("history");
- if (ind > 0) {
- appId = words[ind + 1];
- var attemptId = words[ind + 2];
- newBaseURI = words.slice(0, ind).join("/");
- if (isNaN(attemptId)) {
- return newBaseURI + "/api/v1/applications/" + appId + "/sql/sqlTable";
- } else {
- return newBaseURI + "/api/v1/applications/" + appId + "/" +
- attemptId + "/sql/sqlTable";
- }
- }
- return uiRoot + "/api/v1/applications/" + appId + "/sql/sqlTable";
-}
-
-function statusBadge(status) {
- var cls = "bg-secondary";
- if (status === "COMPLETED") cls = "bg-success";
- else if (status === "RUNNING") cls = "bg-primary";
- else if (status === "FAILED") cls = "bg-danger";
- return '<span class="badge ' + cls + '">' + status + '</span>';
-}
-
-function jobIdLinks(ids) {
- if (!ids || ids.length === 0) return "";
- var basePath = uiRoot + appBasePath;
- return ids.map(function (id) {
- return '<a href="' + basePath + '/jobs/job/?id=' + id + '">' + id + '</a>';
- }).join(", ");
-}
-
-function descriptionHtml(exec) {
- var desc = exec.description || "";
- var basePath = uiRoot + appBasePath;
- var url = basePath + "/SQL/execution/?id=" + exec.id;
- if (desc.length > 100) {
- var short = escapeHtml(desc.substring(0, 100)) + "...";
- return '<a href="' + url + '" title="' + escapeHtml(desc) + '">' +
- short + '</a>';
- }
- return '<a href="' + url + '">' + (escapeHtml(desc) || exec.id) + '</a>';
-}
-
-// Remove client-side filter — status filtering is now server-side
+/* global $, uiRoot, appBasePath, createSqlApiBase, getSqlTableColumns,
+ withResolvedAppId, statusBadge, jobIdLinks, formatDurationSql,
+ descriptionHtml */
$(document).ready(function () {
- // Resolve appId: check proxy/history in URL, fallback to REST API
- var words = document.baseURI.split("/");
- var appId = "";
- var ind = words.indexOf("proxy");
- if (ind > 0) {
- appId = words[ind + 1];
- } else {
- ind = words.indexOf("history");
- if (ind > 0) {
- appId = words[ind + 1];
- }
- }
-
// Read the cluster-level grouping toggle rendered into the page by Scala
var groupSubExecEnabled = true;
var configEl = document.getElementById("group-sub-exec-config");
@@ -124,7 +28,7 @@ $(document).ready(function () {
}
function init(resolvedAppId) {
- var sqlTableEndPoint = createSQLTableEndPoint(resolvedAppId);
+ var sqlTableEndPoint = createSqlApiBase(resolvedAppId) + "/sqlTable";
var container = document.getElementById("sql-executions-table");
container.innerHTML =
@@ -138,74 +42,7 @@ $(document).ready(function () {
'<table id="sql-table" class="table table-striped compact cell-border" '
+
'style="width:100%"></table>';
- var columns = [
- {
- data: "id", name: "id", title: "ID",
- render: function (data, type) {
- if (type !== "display") return data;
- var basePath = uiRoot + appBasePath;
- return '<a href="' + basePath + '/SQL/execution/?id=' + data + '">' +
- data + '</a>';
- }
- },
- {
- data: "queryId", name: "queryId", title: "Query ID",
- orderable: false,
- render: function (data, type) {
- if (type !== "display" || !data) return data || "";
- var safe = escapeHtml(data);
- return '<span title="' + safe + '">' + escapeHtml(data.substring(0,
8)) + '...</span>';
- }
- },
- {
- data: "status", name: "status", title: "Status",
- render: function (data, type) {
- if (type !== "display") return data;
- return statusBadge(data);
- }
- },
- {
- data: "description", name: "description", title: "Description",
- render: function (data, type, row) {
- if (type !== "display") return data || "";
- return descriptionHtml({ id: row.id, description: data });
- }
- },
- {
- data: "submissionTime", name: "submissionTime", title: "Submitted",
- render: function (data, type) {
- if (type !== "display") return data;
- return formatDateSql(data);
- }
- },
- {
- data: "duration", name: "duration", title: "Duration",
- render: function (data, type) {
- if (type !== "display") return data;
- return formatDurationSql(data);
- }
- },
- {
- data: "jobIds", name: "jobIds", title: "Succeeded Jobs",
- orderable: false,
- render: function (data, type) {
- if (type !== "display") return (data || []).join(",");
- return jobIdLinks(data || []);
- }
- },
- {
- data: "errorMessage", name: "errorMessage", title: "Error Message",
- orderable: false,
- render: function (data, type) {
- if (type !== "display" || !data) return data || "";
- if (data.length > 100) {
- return '<span title="' + escapeHtml(data) + '">' +
- escapeHtml(data.substring(0, 100)) + '...</span>';
- }
- return escapeHtml(data);
- }
- }
- ];
+ var columns = getSqlTableColumns({ detail: false });
if (groupSubExecEnabled) {
// Trailing "Sub Executions" column matching the SPARK-41752 / 4.1
layout:
// shows "+N sub" when the root has children, blank otherwise. Click to
@@ -324,14 +161,5 @@ $(document).ready(function () {
} // end init
- if (appId) {
- init(appId);
- } else {
- // Standalone mode: fetch appId from REST API
- $.getJSON(uiRoot + "/api/v1/applications", function (response) {
- if (response && response.length > 0) {
- init(response[0].id);
- }
- });
- }
+ withResolvedAppId(init);
});
diff --git
a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/executionpage.js
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/executionpage.js
new file mode 100644
index 000000000000..4cb9a65c0510
--- /dev/null
+++
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/executionpage.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+/* global $, createSqlApiBase, getSqlTableColumns, withResolvedAppId */
+
+// Renders the single-row summary table at the top of the SQL execution detail
+// page using the same column layout as the SQL listing page. Fetches the
+// execution data from the v1 API and feeds it into a one-row DataTable.
+$(document).ready(function () {
+ var tableEl = document.getElementById("sql-execution-table");
+ if (!tableEl) return;
+
+ var executionId = tableEl.getAttribute("data-execution-id");
+ if (!executionId) return;
+
+ function init(resolvedAppId) {
+ var endpoint = createSqlApiBase(resolvedAppId) + "/" + executionId +
+ "?details=false&planDescription=false";
+ $.getJSON(endpoint, function (data) {
+ // ExecutionData fields: id, status, description, submissionTime,
duration,
+ // runningJobIds, successJobIds, failedJobIds, queryId, errorMessage,
+ // rootExecutionId. Map to the row shape consumed by
getSqlTableColumns:
+ // id, queryId, status, description, submissionTime, duration, jobIds,
+ // errorMessage.
+ var row = {
+ id: data.id,
+ queryId: data.queryId || "",
+ status: data.status,
+ description: data.description || "",
+ submissionTime: data.submissionTime,
+ duration: data.duration,
+ jobIds: data.successJobIds || [],
+ errorMessage: data.errorMessage || ""
+ };
+
+ $("#sql-execution-table").DataTable({
+ data: [row],
+ columns: getSqlTableColumns({ detail: true }),
+ paging: false,
+ searching: false,
+ info: false,
+ ordering: false,
+ dom: "t"
+ });
+ }).fail(function () {
+ $("#sql-execution-table").replaceWith(
+ '<div class="alert alert-warning">' +
+ 'Failed to load execution metadata.</div>');
+ });
+ }
+
+ withResolvedAppId(init);
+});
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 262b2f733242..738e70fdd127 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
@@ -216,3 +216,30 @@ svg g.node.search-dimmed,
svg g.cluster.search-dimmed {
opacity: 0.3 !important;
}
+
+/* Inline <pre> renderer used by the shared SQL DataTable column factory
+ * (sql-table-utils.js) on the execution detail page. Keeps long SQL/error
+ * text legible without stretching the row. */
+.sql-cell-pre {
+ font-size: 0.85rem;
+ max-height: 300px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+ margin: 0;
+ padding: 0.25rem 0.5rem;
+ background-color: transparent;
+}
+
+/* Collapsible disclosure used for long Description / Error Message cells.
+ * Uses the native <details>/<summary> element so no JavaScript is needed. */
+.sql-cell-details > summary {
+ cursor: pointer;
+ font-family: var(--bs-font-monospace);
+ font-size: 0.85rem;
+ list-style: revert;
+ user-select: none;
+}
+.sql-cell-details[open] > summary {
+ margin-bottom: 0.25rem;
+}
diff --git
a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/sql-table-utils.js
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/sql-table-utils.js
new file mode 100644
index 000000000000..7b0f4ffccde5
--- /dev/null
+++
b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/sql-table-utils.js
@@ -0,0 +1,262 @@
+/*
+ * 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.
+ */
+
+/* global $, uiRoot, appBasePath */
+/* eslint-disable no-unused-vars */
+
+function formatDurationSql(milliseconds) {
+ if (milliseconds < 100) return parseInt(milliseconds).toFixed(1) + " ms";
+ var seconds = milliseconds / 1000;
+ if (seconds < 1) return seconds.toFixed(1) + " s";
+ if (seconds < 60) return seconds.toFixed(0) + " s";
+ var minutes = seconds / 60;
+ if (minutes < 10) return minutes.toFixed(1) + " min";
+ if (minutes < 60) return minutes.toFixed(0) + " min";
+ var hours = minutes / 60;
+ return hours.toFixed(1) + " h";
+}
+
+function formatDateSql(dateStr) {
+ if (!dateStr) return "";
+ try {
+ var dt = new Date(dateStr.replace("GMT", "Z"));
+ if (isNaN(dt.getTime())) return dateStr;
+ var pad = function (n) { return n < 10 ? "0" + n : n; };
+ return dt.getFullYear() + "-" + pad(dt.getMonth() + 1) + "-" +
pad(dt.getDate()) + " " +
+ pad(dt.getHours()) + ":" + pad(dt.getMinutes()) + ":" +
pad(dt.getSeconds());
+ } catch (e) { return dateStr; }
+}
+
+function escapeHtml(str) {
+ if (!str) return str;
+ var div = document.createElement("div");
+ div.appendChild(document.createTextNode(str));
+ return div.innerHTML;
+}
+
+function statusBadge(status) {
+ var cls = "bg-secondary";
+ if (status === "COMPLETED") cls = "bg-success";
+ else if (status === "RUNNING") cls = "bg-primary";
+ else if (status === "FAILED") cls = "bg-danger";
+ return '<span class="badge ' + cls + '">' + status + '</span>';
+}
+
+function jobIdLinks(ids) {
+ if (!ids || ids.length === 0) return "";
+ var basePath = uiRoot + appBasePath;
+ return ids.map(function (id) {
+ return '<a href="' + basePath + '/jobs/job/?id=' + id + '">' + id + '</a>';
+ }).join(", ");
+}
+
+// Render a description cell.
+// exec - {id, description}
+// opts.detail - if true: render full text in <pre class="sql-cell-pre">,
+// no truncation, no self-link. Used by the SQL detail page.
+// Multi-line / long descriptions are wrapped in <details>
+// so the cell starts collapsed with a one-line summary.
+// if false (default): truncate to 100 chars, render as a
+// link to the execution detail page. Used by the list page
+// and by sub-execution child rows.
+function descriptionHtml(exec, opts) {
+ opts = opts || {};
+ var desc = exec.description || "";
+ if (opts.detail) {
+ if (!desc) {
+ return '<span class="text-muted">(no description)</span>';
+ }
+ return collapsiblePre(desc);
+ }
+ var basePath = uiRoot + appBasePath;
+ var url = basePath + "/SQL/execution/?id=" + exec.id;
+ if (desc.length > 100) {
+ var short = escapeHtml(desc.substring(0, 100)) + "...";
+ return '<a href="' + url + '" title="' + escapeHtml(desc) + '">' +
+ short + '</a>';
+ }
+ return '<a href="' + url + '">' + (escapeHtml(desc) || exec.id) + '</a>';
+}
+
+// Render a long, possibly multi-line value as either a plain <pre> when it
+// fits one short line, or a <details>/<summary>/<pre> disclosure block
+// otherwise. The summary shows the first 100 characters of the first line so
+// the cell stays compact when collapsed.
+function collapsiblePre(text) {
+ var firstLineBreak = text.indexOf("\n");
+ var firstLine = firstLineBreak >= 0 ? text.substring(0, firstLineBreak) :
text;
+ var multiLine = firstLineBreak >= 0;
+ if (!multiLine && text.length <= 100) {
+ return '<pre class="sql-cell-pre">' + escapeHtml(text) + '</pre>';
+ }
+ var summary = firstLine.length > 100 ?
+ firstLine.substring(0, 100) + "..." :
+ firstLine + (multiLine ? " ..." : "");
+ return '<details class="sql-cell-details">' +
+ '<summary>' + escapeHtml(summary) + '</summary>' +
+ '<pre class="sql-cell-pre">' + escapeHtml(text) + '</pre>' +
+ '</details>';
+}
+
+// Resolve the Spark applicationId for the current page, then invoke
+// callback(appId). Checks /proxy/<id> and /history/<id> path prefixes first;
+// falls back to the REST list endpoint for the local-mode UI.
+function withResolvedAppId(callback) {
+ var words = document.baseURI.split("/");
+ var appId = "";
+ var ind = words.indexOf("proxy");
+ if (ind > 0) {
+ appId = words[ind + 1];
+ } else {
+ ind = words.indexOf("history");
+ if (ind > 0) {
+ appId = words[ind + 1];
+ }
+ }
+ if (appId) {
+ callback(appId);
+ return;
+ }
+ $.getJSON(uiRoot + "/api/v1/applications", function (response) {
+ if (response && response.length > 0) {
+ callback(response[0].id);
+ }
+ });
+}
+
+// Build the base URL for SQL REST endpoints, accounting for proxy/history
paths.
+// Returns "<baseURI>/api/v1/applications/<resolvedAppId>/sql"; the caller can
+// append "/sqlTable", "/<id>", etc.
+function createSqlApiBase(appId) {
+ var words = document.baseURI.split("/");
+ var ind = words.indexOf("proxy");
+ var newBaseURI;
+ if (ind > 0) {
+ appId = words[ind + 1];
+ newBaseURI = words.slice(0, ind + 2).join("/");
+ return newBaseURI + "/api/v1/applications/" + appId + "/sql";
+ }
+ ind = words.indexOf("history");
+ if (ind > 0) {
+ appId = words[ind + 1];
+ var attemptId = words[ind + 2];
+ newBaseURI = words.slice(0, ind).join("/");
+ if (isNaN(attemptId)) {
+ return newBaseURI + "/api/v1/applications/" + appId + "/sql";
+ } else {
+ return newBaseURI + "/api/v1/applications/" + appId + "/" +
+ attemptId + "/sql";
+ }
+ }
+ return uiRoot + "/api/v1/applications/" + appId + "/sql";
+}
+
+// Factory for the shared SQL DataTable column definitions used on both the
+// SQL listing page (`allexecutionspage.js`) and the SQL execution detail page
+// (`executionpage.js`).
+// opts.detail - true on the detail page (single-row table): no truncation,
+// ID and Description rendered as plain text (no self-link),
+// description rendered as <pre> so SQL formatting is kept.
+// false on the list page (default): truncate long values,
+// ID and Description link to the detail page.
+function getSqlTableColumns(opts) {
+ opts = opts || {};
+ var detail = opts.detail === true;
+
+ var idColumn = {
+ data: "id", name: "id", title: "ID",
+ render: function (data, type) {
+ if (type !== "display") return data;
+ if (detail) return data;
+ var basePath = uiRoot + appBasePath;
+ return '<a href="' + basePath + '/SQL/execution/?id=' + data + '">' +
+ data + '</a>';
+ }
+ };
+
+ var queryIdColumn = {
+ data: "queryId", name: "queryId", title: "Query ID",
+ orderable: false,
+ render: function (data, type) {
+ if (type !== "display" || !data) return data || "";
+ var safe = escapeHtml(data);
+ if (detail) return safe;
+ return '<span title="' + safe + '">' +
+ escapeHtml(data.substring(0, 8)) + '...</span>';
+ }
+ };
+
+ var statusColumn = {
+ data: "status", name: "status", title: "Status",
+ render: function (data, type) {
+ if (type !== "display") return data;
+ return statusBadge(data);
+ }
+ };
+
+ var descriptionColumn = {
+ data: "description", name: "description", title: "Description",
+ render: function (data, type, row) {
+ if (type !== "display") return data || "";
+ return descriptionHtml({ id: row.id, description: data }, { detail:
detail });
+ }
+ };
+
+ var submissionColumn = {
+ data: "submissionTime", name: "submissionTime", title: "Submitted",
+ render: function (data, type) {
+ if (type !== "display") return data;
+ return formatDateSql(data);
+ }
+ };
+
+ var durationColumn = {
+ data: "duration", name: "duration", title: "Duration",
+ render: function (data, type) {
+ if (type !== "display") return data;
+ return formatDurationSql(data);
+ }
+ };
+
+ var jobsColumn = {
+ data: "jobIds", name: "jobIds", title: "Succeeded Jobs",
+ orderable: false,
+ render: function (data, type) {
+ if (type !== "display") return (data || []).join(",");
+ return jobIdLinks(data || []);
+ }
+ };
+
+ var errorColumn = {
+ data: "errorMessage", name: "errorMessage", title: "Error Message",
+ orderable: false,
+ render: function (data, type) {
+ if (type !== "display" || !data) return data || "";
+ if (detail) {
+ return collapsiblePre(data);
+ }
+ if (data.length > 100) {
+ return '<span title="' + escapeHtml(data) + '">' +
+ escapeHtml(data.substring(0, 100)) + '...</span>';
+ }
+ return escapeHtml(data);
+ }
+ };
+
+ return [idColumn, queryIdColumn, statusColumn, descriptionColumn,
+ submissionColumn, durationColumn, jobsColumn, errorColumn];
+}
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
index d919a0d556d3..5801aba8c769 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/AllExecutionsPage.scala
@@ -42,6 +42,8 @@ private[ui] class AllExecutionsPage(parent: SQLTab) extends
WebUIPage("") {
<div id="sql-executions-table">
{spinner}
</div>
+ <script src={UIUtils.prependBaseUri(
+ request, "/static/sql/sql-table-utils.js")}></script>
<script src={UIUtils.prependBaseUri(
request, "/static/sql/allexecutionspage.js")}></script>
</span>
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 9ff410e829e2..a556ee9339e0 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
@@ -46,53 +46,52 @@ class ExecutionPage(parent: SQLTab) extends
WebUIPage("execution") with Logging
val executionId = parameterExecutionId.toLong
val content = sqlStore.execution(executionId).map { executionUIData =>
- val currentTime = System.currentTimeMillis()
- val duration =
executionUIData.completionTime.map(_.getTime()).getOrElse(currentTime) -
- executionUIData.submissionTime
+ val isSubExec = executionUIData.rootExecutionId != executionId
+ val subExecutions = if (groupSubExecutionEnabled) {
+ sqlStore.executionsList()
+ .filter(e => e.rootExecutionId == executionId && e.executionId !=
executionId)
+ } else {
+ Seq.empty
+ }
+ // Headers and row data are rendered client-side by DataTables from the
+ // column definitions in static/sql-execution-detail.js -- intentionally
+ // no <thead>/<tbody> in the Scala template below. Adding markup headers
+ // here causes DataTables to double-render and breaks layout
(SPARK-56259).
val summary =
- <div>
- <ul class="list-unstyled">
- <li>
- <strong>Submitted Time:
</strong>{UIUtils.formatDate(executionUIData.submissionTime)}
- </li>
- <li>
- <strong>Duration: </strong>{UIUtils.formatDuration(duration)}
- </li>
- {
- Option(executionUIData.queryId).map { qId =>
- <li>
- <strong>Query ID: </strong>{qId}
- </li>
- }.getOrElse(Seq.empty)
- }
- {
- if (executionUIData.rootExecutionId != executionId) {
- <li>
- <strong>Parent Execution: </strong>
- <a href={"?id=" + executionUIData.rootExecutionId}>
- {executionUIData.rootExecutionId}
- </a>
- </li>
- }
- }
- {
- if (groupSubExecutionEnabled) {
- val subExecutions = sqlStore.executionsList()
- .filter(e => e.rootExecutionId == executionId &&
e.executionId != executionId)
- if (subExecutions.nonEmpty) {
- <li>
- <strong>Sub Executions: </strong>
- {
- subExecutions.map { e =>
- <a href={"?id=" +
e.executionId}>{e.executionId}</a><span> </span>
+ <div class="mb-3">
+ <table id="sql-execution-table" class="table table-striped compact
cell-border"
+ style="width:100%" data-execution-id={executionId.toString}>
+ </table>
+ {
+ if (isSubExec || subExecutions.nonEmpty) {
+ <ul class="list-unstyled small text-muted mb-2">
+ {
+ if (isSubExec) {
+ <li>
+ <strong>Parent Execution: </strong>
+ <a href={"?id=" + executionUIData.rootExecutionId}>
+ {executionUIData.rootExecutionId}
+ </a>
+ </li>
+ }
+ }
+ {
+ if (subExecutions.nonEmpty) {
+ <li>
+ <strong>Sub Executions: </strong>
+ {
+ subExecutions.map { e =>
+ <a href={"?id=" + e.executionId}>{e.executionId}</a>
+ <span> </span>
+ }
}
- }
- </li>
+ </li>
+ }
}
- }
+ </ul>
}
- </ul>
+ }
<div id="plan-viz-download-btn-container">
<select id="plan-viz-format-select">
<option value="svg">SVG</option>
@@ -109,6 +108,10 @@ class ExecutionPage(parent: SQLTab) extends
WebUIPage("execution") with Logging
type="button" title="Copy shareable link to this
execution">
🔗 Copy Link</button>
</div>
+ <script src={UIUtils.prependBaseUri(
+ request, "/static/sql/sql-table-utils.js")}></script>
+ <script src={UIUtils.prependBaseUri(
+ request, "/static/sql/executionpage.js")}></script>
</div>
val metrics = sqlStore.executionMetrics(executionId)
@@ -128,7 +131,8 @@ class ExecutionPage(parent: SQLTab) extends
WebUIPage("execution") with Logging
}
UIUtils.headerSparkPage(
- request, s"Details for Query $executionId", content, parent, useTimeline
= true)
+ request, s"Details for Query $executionId", content, parent,
+ useDataTables = true, useTimeline = true)
}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
index 2bc4e53611ba..635968838406 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ui/AllExecutionsPageSuite.scala
@@ -66,6 +66,7 @@ abstract class AllExecutionsPageSuite extends
SharedSparkSession with BeforeAndA
val page = new AllExecutionsPage(tab)
val html = page.render(request).toString().toLowerCase(Locale.ROOT)
assert(html.contains("sql-executions-table"))
+ assert(html.contains("sql-table-utils.js"))
assert(html.contains("allexecutionspage.js"))
assert(html.contains("datatables"))
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]