This is an automated email from the ASF dual-hosted git repository.
yao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push:
new 983ccfb9aa63 [SPARK-55771][UI] Modernize progress bars using Bootstrap
5 Progress component
983ccfb9aa63 is described below
commit 983ccfb9aa632bf8ff6a025a4d80e1941159d1ed
Author: Kent Yao <[email protected]>
AuthorDate: Mon Mar 2 22:51:42 2026 +0800
[SPARK-55771][UI] Modernize progress bars using Bootstrap 5 Progress
component
### What changes were proposed in this pull request?
Replace the custom progress bar implementation with Bootstrap 5's
`progress-stacked` component for proper accessibility and modern CSS.
**Changes:**
- **CSS** (`webui.css`): Remove 21 lines of IE9 vendor-prefixed gradients
(`-moz-`, `-webkit-`, `-o-`, `filter:progid`), legacy `box-shadow` border, and
`background-repeat`. Replace with clean `background: linear-gradient()` rules
targeting `.progress-stacked` selector.
- **Scala** (`UIUtils.scala`): Use BS5 `progress-stacked` wrapper with
proper ARIA attributes (`role="progressbar"`, `aria-valuenow/min/max`). Replace
inline styles with BS5 utility classes (`position-absolute`, `d-flex`,
`align-items-center`, etc.). Extract label text to a `progressLabel` variable
for clarity.
- **Tests**: Update `UIUtilsSuite` assertions for new BS5 markup structure.
Update `UISeleniumSuite` CSS selector from `.progress` to `.progress-stacked`.
**Before → After (HTML output):**
```html
<div class="progress">
<span style="display:flex;align-items:center;...">3/4</span>
<div class="progress-bar progress-completed" style="width:75%"></div>
<div class="progress-bar progress-started" style="width:25%"></div>
</div>
<div class="progress-stacked" title="3/4 (2 running)">
<span class="position-absolute w-100 h-100 d-flex align-items-center
justify-content-center">3/4</span>
<div class="progress" role="progressbar" aria-label="Completed"
aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" style="width:75%">
<div class="progress-bar progress-completed"></div>
</div>
<div class="progress" role="progressbar" aria-label="Running"
aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="width:25%">
<div class="progress-bar progress-started"></div>
</div>
</div>
```
### Why are the changes needed?
1. **Accessibility**: Progress bars lacked ARIA attributes, making them
invisible to screen readers
2. **Dead code**: IE9 vendor prefixes (`-moz-`, `-webkit-`, `-o-`,
`filter:progid`) are no longer needed
3. **BS5 alignment**: Use the standard BS5 Progress component API instead
of custom markup
4. **Maintainability**: Inline styles replaced with BS5 utility classes
### Does this PR introduce _any_ user-facing change?
Yes — progress bars now have proper ARIA attributes for accessibility.
Visual appearance is unchanged.
### How was this patch tested?
- `UIUtilsSuite` — progress bar markup assertions updated and passing
- `UISeleniumSuite` — CSS selector updated for new structure
- Scalastyle checks passing
### Was this patch authored or co-authored using generative AI tooling?
Yes, GitHub Copilot CLI was used.
Closes #54564 from yaooqinn/SPARK-55771.
Authored-by: Kent Yao <[email protected]>
Signed-off-by: Kent Yao <[email protected]>
---
.../resources/org/apache/spark/ui/static/webui.css | 25 +++++-----------------
.../main/scala/org/apache/spark/ui/UIUtils.scala | 24 +++++++++++++--------
.../org/apache/spark/ui/UISeleniumSuite.scala | 2 +-
.../scala/org/apache/spark/ui/UIUtilsSuite.scala | 11 ++++++++--
4 files changed, 30 insertions(+), 32 deletions(-)
diff --git a/core/src/main/resources/org/apache/spark/ui/static/webui.css
b/core/src/main/resources/org/apache/spark/ui/static/webui.css
index 3f7ed3be2a51..fd00c2fd2920 100755
--- a/core/src/main/resources/org/apache/spark/ui/static/webui.css
+++ b/core/src/main/resources/org/apache/spark/ui/static/webui.css
@@ -108,34 +108,19 @@ table.sortable td {
max-width: 600px;
}
-.progress {
+.progress-stacked {
font-size: 1rem;
height: 1.42rem;
margin-bottom: 0px;
position: relative;
- box-shadow: inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15);
}
-.progress .progress-bar.progress-started {
- background-color: #A0DFFF;
- background-image: -moz-linear-gradient(top, #A4EDFF, #94DDFF);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#A4EDFF),
to(#94DDFF));
- background-image: -webkit-linear-gradient(top, #A4EDFF, #94DDFF);
- background-image: -o-linear-gradient(top, #A4EDFF, #94DDFF);
- background-image: linear-gradient(to bottom, #A4EDFF, #94DDFF);
- background-repeat: repeat-x;
- filter:
progid:dximagetransform.microsoft.gradient(startColorstr='#FFA4EDFF',
endColorstr='#FF94DDFF', GradientType=0);
+.progress-stacked .progress-bar.progress-started {
+ background: linear-gradient(to bottom, #A4EDFF, #94DDFF);
}
-.progress .progress-bar.progress-completed {
- background-color: #3EC0FF;
- background-image: -moz-linear-gradient(top, #44CBFF, #34B0EE);
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#44CBFF),
to(#34B0EE));
- background-image: -webkit-linear-gradient(top, #44CBFF, #34B0EE);
- background-image: -o-linear-gradient(top, #44CBFF, #34B0EE);
- background-image: linear-gradient(to bottom, #64CBFF, #54B0EE);
- background-repeat: repeat-x;
- filter:
progid:dximagetransform.microsoft.gradient(startColorstr='#FF44CBFF',
endColorstr='#FF34B0EE', GradientType=0);
+.progress-stacked .progress-bar.progress-completed {
+ background: linear-gradient(to bottom, #64CBFF, #54B0EE);
}
a.kill-link {
diff --git a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
index 4cb206d80755..74cd4791399e 100644
--- a/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
+++ b/core/src/main/scala/org/apache/spark/ui/UIUtils.scala
@@ -472,17 +472,23 @@ private[spark] object UIUtils extends Logging {
if (skipped > 0) s" ($skipped skipped)" else ""
} + killTaskReasonText
+ val progressLabel = s"$completed/$total" +
+ (if (failed == 0 && skipped == 0 && started > 0) s" ($started running)"
else "") +
+ (if (failed > 0) s" ($failed failed)" else "") +
+ (if (skipped > 0) s" ($skipped skipped)" else "") +
+ killTaskReasonText
+
// scalastyle:off line.size.limit
- <div class="progress">
- <span style="display: flex; align-items: center; justify-content:
center; position:absolute; width:100%; height:100%; text-align:center;"
title={progressTitle}>
- { s"$completed/$total" +
- (if (failed == 0 && skipped == 0 && started > 0) s" ($started
running)" else "") +
- (if (failed > 0) s" ($failed failed)" else "") +
- (if (skipped > 0) s" ($skipped skipped)" else "") +
- killTaskReasonText }
+ <div class="progress-stacked" title={progressTitle}>
+ <span class="position-absolute w-100 h-100 d-flex align-items-center
justify-content-center">
+ {progressLabel}
</span>
- <div class="progress-bar progress-completed" style={completeWidth}></div>
- <div class="progress-bar progress-started" style={startWidth}></div>
+ <div class="progress" role="progressbar" aria-label="Completed"
aria-valuenow={ratio.toInt.toString} aria-valuemin="0" aria-valuemax="100"
style={completeWidth}>
+ <div class="progress-bar progress-completed"></div>
+ </div>
+ <div class="progress" role="progressbar" aria-label="Running"
aria-valuenow={startRatio.toInt.toString} aria-valuemin="0" aria-valuemax="100"
style={startWidth}>
+ <div class="progress-bar progress-started"></div>
+ </div>
</div>
// scalastyle:on line.size.limit
}
diff --git a/core/src/test/scala/org/apache/spark/ui/UISeleniumSuite.scala
b/core/src/test/scala/org/apache/spark/ui/UISeleniumSuite.scala
index 5d02dd753845..9096076f8212 100644
--- a/core/src/test/scala/org/apache/spark/ui/UISeleniumSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/UISeleniumSuite.scala
@@ -362,7 +362,7 @@ class UISeleniumSuite extends SparkFunSuite with WebBrowser
with Matchers {
eventually(timeout(5.seconds), interval(50.milliseconds)) {
goToUi(sc, "/jobs")
find(cssSelector(".stage-progress-cell")).get.text should be ("2/2 (1
failed)")
- find(cssSelector(".progress-cell .progress")).get.text should be ("2/2
(1 failed)")
+ find(cssSelector(".progress-cell .progress-stacked")).get.text should
be ("2/2 (1 failed)")
}
val jobJson = getJson(sc.ui.get, "jobs")
(jobJson \\ "numTasks").extract[Int]should be (2)
diff --git a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
index c0753a2af8be..ebb22de7c538 100644
--- a/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
+++ b/core/src/test/scala/org/apache/spark/ui/UIUtilsSuite.scala
@@ -126,10 +126,17 @@ class UIUtilsSuite extends SparkFunSuite {
test("SPARK-11906: Progress bar should not overflow because of speculative
tasks") {
val generated = makeProgressBar(2, 3, 0, 0, Map.empty,
4).head.child.filter(_.label == "div")
+ // BS5 progress-stacked: each segment is a .progress wrapper with a
.progress-bar
+ // scalastyle:off line.size.limit
val expected = Seq(
- <div class="progress-bar progress-completed" style="width: 75.0%"></div>,
- <div class="progress-bar progress-started" style="width: 25.0%"></div>
+ <div class="progress" role="progressbar" aria-label="Completed"
aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" style="width: 75.0%">
+ <div class="progress-bar progress-completed"></div>
+ </div>,
+ <div class="progress" role="progressbar" aria-label="Running"
aria-valuenow="25" aria-valuemin="0" aria-valuemax="100" style="width: 25.0%">
+ <div class="progress-bar progress-started"></div>
+ </div>
)
+ // scalastyle:on line.size.limit
assert(generated.sameElements(expected),
s"\nRunning progress bar should round
down\n\nExpected:\n$expected\nGenerated:\n$generated")
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]