From 39f3ba5919dfc2a397c37f23f34715effc061fc7 Mon Sep 17 00:00:00 2001 From: Will Gan Date: Thu, 13 Aug 2026 22:40:42 +0000 Subject: [PATCH 1/2] Fix filter and min,med,max table interaction --- .../sql/execution/ui/static/spark-sql-viz.js | 14 ++-- ui-test/tests/spark-sql-viz.test.js | 80 +++++++++++++++++++ 2 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 ui-test/tests/spark-sql-viz.test.js 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 7c1391c9d2b4f..38cdd6a7bcd9b 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 @@ -500,15 +500,19 @@ function updateDetailsPanel(nodeId, nodeDetails) { var searchBox = document.getElementById("metric-search"); if (searchBox) { searchBox.addEventListener("input", function () { - var query = this.value.toLowerCase(); - bodyEl.querySelectorAll("table tbody tr").forEach(function (row) { - var metricName = row.cells[0] ? row.cells[0].textContent.toLowerCase() : ""; - row.style.display = metricName.indexOf(query) >= 0 ? "" : "none"; - }); + filterMetricRows(bodyEl, this.value); }); } } +function filterMetricRows(container, query) { + var q = query.toLowerCase(); + container.querySelectorAll("table.sortable > tbody > tr").forEach(function (row) { + var metricName = row.cells[0] ? row.cells[0].textContent.toLowerCase() : ""; + row.style.display = metricName.indexOf(q) >= 0 ? "" : "none"; + }); +} + function htmlEscape(str) { return str.replace(/&/g, "&").replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); diff --git a/ui-test/tests/spark-sql-viz.test.js b/ui-test/tests/spark-sql-viz.test.js new file mode 100644 index 0000000000000..d757c1c21f1ac --- /dev/null +++ b/ui-test/tests/spark-sql-viz.test.js @@ -0,0 +1,80 @@ +/* + * 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. + */ + +/** + * @jest-environment jsdom + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// spark-sql-viz.js is a classic (non-module) script, so read it and return the helpers under test. +const __dirname = dirname(fileURLToPath(import.meta.url)); +const vizPath = join( + __dirname, + '../../sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js'); +const src = readFileSync(vizPath, 'utf8'); + +global.$ = function () {}; +document.body.innerHTML = ''; +const { buildMetricsTable, filterMetricRows } = new Function( + src + '\nreturn { buildMetricsTable, filterMetricRows };')(); + +// A size metric renders a nested Total/Min/Med/Max stat sub-table inside its value cell. +const SIZE_VALUE = + 'total (min, med, max (stageId: taskId))\n25.0 MiB (3.8 MiB, N/A, 4.8 MiB (stage 2.0: task 4))'; + +function renderPanel(metrics) { + document.body.innerHTML = buildMetricsTable(metrics, false, true); + return document.body; +} + +test('filterMetricRows keeps the matched metric value visible', function () { + const body = renderPanel([ + { name: 'shuffle bytes written', value: SIZE_VALUE, type: 'size' }, + { name: 'number of output rows', value: '10', type: 'sum' } + ]); + + filterMetricRows(body, 'shuffle'); + + const outerRows = [...body.querySelectorAll('table.sortable > tbody > tr')]; + const matched = outerRows.find(r => /shuffle bytes written/.test(r.cells[0].textContent)); + const other = outerRows.find(r => /output rows/.test(r.cells[0].textContent)); + + expect(matched.style.display).toBe(''); + expect(other.style.display).toBe('none'); + + // The nested stat sub-table rows must not be hidden, else the value renders blank. + const statRows = matched.querySelectorAll('td table tr'); + expect(statRows.length).toBeGreaterThan(0); + statRows.forEach(r => expect(r.style.display).not.toBe('none')); + expect(matched.cells[1].textContent).toContain('25.0 MiB'); +}); + +test('filterMetricRows restores all rows when the query is cleared', function () { + const body = renderPanel([ + { name: 'shuffle bytes written', value: SIZE_VALUE, type: 'size' }, + { name: 'number of output rows', value: '10', type: 'sum' } + ]); + + filterMetricRows(body, 'shuffle'); + filterMetricRows(body, ''); + + body.querySelectorAll('table.sortable > tbody > tr') + .forEach(r => expect(r.style.display).toBe('')); +}); From 180dbd83400199a2fb7ca5185dd569fdded9c2bc Mon Sep 17 00:00:00 2001 From: Will Gan Date: Sat, 15 Aug 2026 17:56:41 +0000 Subject: [PATCH 2/2] fix lint --- ui-test/tests/spark-sql-viz.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-test/tests/spark-sql-viz.test.js b/ui-test/tests/spark-sql-viz.test.js index d757c1c21f1ac..1597b8619b424 100644 --- a/ui-test/tests/spark-sql-viz.test.js +++ b/ui-test/tests/spark-sql-viz.test.js @@ -30,7 +30,7 @@ const vizPath = join( '../../sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js'); const src = readFileSync(vizPath, 'utf8'); -global.$ = function () {}; +window.$ = function () {}; document.body.innerHTML = ''; const { buildMetricsTable, filterMetricRows } = new Function( src + '\nreturn { buildMetricsTable, filterMetricRows };')();