This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 7880530d7c Hop Web: show a loading splash (#8182)
7880530d7c is described below
commit 7880530d7cbea17504e96f009704590f2b64aea5
Author: Lance <[email protected]>
AuthorDate: Mon Aug 31 16:15:43 2026 +0800
Hop Web: show a loading splash (#8182)
Signed-off-by: lance <[email protected]>
---
.../main/java/org/apache/hop/ui/hopgui/HopWeb.java | 36 +++--
rap/src/main/resources/splash.html | 175 +++++++++++++++++++++
.../org/apache/hop/ui/hopgui/HopWebSplashTest.java | 55 +++++++
3 files changed, 253 insertions(+), 13 deletions(-)
diff --git a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
index eb3ec1c19c..24597fd3a0 100644
--- a/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
+++ b/rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
@@ -61,6 +61,7 @@ public class HopWeb implements ApplicationConfiguration {
public static final String CONST_LIGHT = "light";
private static final String WEB_PAGE_TITLE = "Apache Hop Web";
+ private static final String RESOURCE_LOGO_ICON_PNG =
"ui/images/logo_icon.png";
/**
* Returns the browser page title for Hop Web, including the Apache Hop
version when it is
@@ -93,10 +94,8 @@ public class HopWeb implements ApplicationConfiguration {
//
GuiRegistry registry = GuiRegistry.getInstance();
Map<String, Map<String, GuiToolbarItem>> guiToolbarMap =
registry.getGuiToolbarMap();
- for (String toolbarId : guiToolbarMap.keySet()) {
- Map<String, GuiToolbarItem> itemMap = guiToolbarMap.get(toolbarId);
- for (String itemId : itemMap.keySet()) {
- final GuiToolbarItem item = itemMap.get(itemId);
+ for (Map<String, GuiToolbarItem> itemMap : guiToolbarMap.values()) {
+ for (GuiToolbarItem item : itemMap.values()) {
addResource(application, item.getImage(), item.getClassLoader());
}
}
@@ -133,7 +132,7 @@ public class HopWeb implements ApplicationConfiguration {
}
application.addResource(
- "ui/images/logo_icon.png",
+ RESOURCE_LOGO_ICON_PNG,
new ResourceLoader() {
@Override
public InputStream getResourceAsStream(String resourceName) {
@@ -152,6 +151,10 @@ public class HopWeb implements ApplicationConfiguration {
return new ByteArrayInputStream(outputStream.toByteArray());
}
});
+ application.addResource(
+ "ui/images/logo_hop.svg",
+ resourceName ->
+
HopWeb.class.getClassLoader().getResourceAsStream("ui/images/logo_hop.svg"));
application.addServiceHandler(
CanvasRenderServiceHandler.SERVICE_ID, new
CanvasRenderServiceHandler());
@@ -183,15 +186,17 @@ public class HopWeb implements ApplicationConfiguration {
Map<String, String> propertiesLight = new HashMap<>();
propertiesLight.put(WebClient.PAGE_TITLE, webPageTitle);
- propertiesLight.put(WebClient.FAVICON, "ui/images/logo_icon.png");
+ propertiesLight.put(WebClient.FAVICON, RESOURCE_LOGO_ICON_PNG);
propertiesLight.put(WebClient.THEME_ID, CONST_LIGHT);
propertiesLight.put(WebClient.HEAD_HTML,
readTextFromResource("head.html"));
+ propertiesLight.put(WebClient.BODY_HTML, splashBodyHtml(CONST_LIGHT));
Map<String, String> propertiesDark = new HashMap<>();
propertiesDark.put(WebClient.PAGE_TITLE, webPageTitle);
- propertiesDark.put(WebClient.FAVICON, "ui/images/logo_icon.png");
+ propertiesDark.put(WebClient.FAVICON, RESOURCE_LOGO_ICON_PNG);
propertiesDark.put(WebClient.THEME_ID, "dark");
propertiesDark.put(WebClient.HEAD_HTML, readTextFromResource("head.html"));
+ propertiesDark.put(WebClient.BODY_HTML, splashBodyHtml("dark"));
application.addEntryPoint("/ui", HopWebEntryPoint.class, propertiesLight);
application.addEntryPoint("/ui-dark", HopWebEntryPoint.class,
propertiesDark);
@@ -225,12 +230,7 @@ public class HopWeb implements ApplicationConfiguration {
String resourceName = "monaco/" + trimmed;
application.addResource(
resourceName,
- new ResourceLoader() {
- @Override
- public InputStream getResourceAsStream(String name) {
- return
HopWeb.class.getClassLoader().getResourceAsStream(classpathResource);
- }
- });
+ name ->
HopWeb.class.getClassLoader().getResourceAsStream(classpathResource));
}
} catch (Exception e) {
LogChannel.UI.logError("Failed to register Monaco editor resources", e);
@@ -263,6 +263,16 @@ public class HopWeb implements ApplicationConfiguration {
application.addResource(imageFilename, loader);
}
+ /**
+ * Startup overlay for {@link WebClient#BODY_HTML}. It stays up until the
main toolbar exists
+ * ({@code toolbar-10010-new}), covering the first-request {@code
HopGui.open()} work; client JS
+ * then drops the z-index so Welcome and the GUI stay clickable (issue
#8112).
+ */
+ static String splashBodyHtml(String theme) {
+ String safeTheme = CONST_LIGHT.equals(theme) ? CONST_LIGHT : "dark";
+ return readTextFromResource("splash.html").replace("{{theme}}", safeTheme);
+ }
+
private static String readTextFromResource(String resourceName) {
String result;
try {
diff --git a/rap/src/main/resources/splash.html
b/rap/src/main/resources/splash.html
new file mode 100644
index 0000000000..463222de1a
--- /dev/null
+++ b/rap/src/main/resources/splash.html
@@ -0,0 +1,175 @@
+<!--
+ ~ 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.
+ ~
+ -->
+<style>
+ #hop-web-splash {
+ position: fixed;
+ inset: 0;
+ z-index: 100000010;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont,
"Helvetica Neue", Arial, sans-serif;
+ transition: opacity 180ms ease-out;
+ }
+ #hop-web-splash.hop-web-splash--out {
+ opacity: 0;
+ pointer-events: none;
+ }
+ .hop-web-splash-card {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ gap: 12px;
+ }
+ .hop-web-splash-logo {
+ width: 72px;
+ height: 72px;
+ border-radius: 18px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
+ background: #033d5d;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .hop-web-splash-logo img {
+ width: 100%;
+ height: 100%;
+ display: block;
+ object-fit: cover;
+ object-position: center;
+ }
+ .hop-web-splash-title {
+ margin: 8px 0 0;
+ font-size: 1.35rem;
+ font-weight: 650;
+ letter-spacing: -0.02em;
+ }
+ .hop-web-splash-status {
+ margin: 0;
+ font-size: 0.95rem;
+ font-weight: 500;
+ }
+ .hop-web-splash-spinner {
+ width: 28px;
+ height: 28px;
+ margin-top: 8px;
+ border-radius: 50%;
+ border: 3px solid rgba(26, 143, 194, 0.25);
+ border-top-color: #1a8fc2;
+ animation: hop-web-splash-spin 0.8s linear infinite;
+ }
+ .hop-web-splash--light {
+ background: #f4f8fb;
+ color: #033d5d;
+ }
+ .hop-web-splash--light .hop-web-splash-status {
+ color: #5a6f7c;
+ }
+ .hop-web-splash--dark {
+ background: linear-gradient(145deg, #022a40 0%, #033d5d 42%, #0a5a82 100%);
+ color: #ffffff;
+ }
+ .hop-web-splash--dark .hop-web-splash-status {
+ color: #b9e5fa;
+ }
+ .hop-web-splash--dark .hop-web-splash-spinner {
+ border-color: rgba(185, 229, 250, 0.25);
+ border-top-color: #b9e5fa;
+ }
+ @keyframes hop-web-splash-spin {
+ to { transform: rotate(360deg); }
+ }
+ @media (prefers-reduced-motion: reduce) {
+ .hop-web-splash-spinner { animation: none; }
+ #hop-web-splash { transition: none; }
+ }
+</style>
+<div id="hop-web-splash" class="hop-web-splash--{{theme}}" role="status"
aria-live="polite" aria-busy="true">
+ <div class="hop-web-splash-card">
+ <div class="hop-web-splash-logo">
+ <img src="rwt-resources/ui/images/logo_hop.svg" width="72" height="72"
alt="" onerror="this.style.visibility='hidden'"/>
+ </div>
+ <p class="hop-web-splash-title">Apache Hop Web</p>
+ <p class="hop-web-splash-status">Loading …</p>
+ <div class="hop-web-splash-spinner" aria-hidden="true"></div>
+ </div>
+</div>
+<script>
+(function() {
+ var splashId = "hop-web-splash";
+ var hidden = false;
+ function hide() {
+ if (hidden) {
+ return;
+ }
+ var el = document.getElementById(splashId);
+ hidden = true;
+ if (!el) {
+ return;
+ }
+ // Drop z-index immediately so web-tests (HopGuiPage.SHELL_TITLES,
z>=100000)
+ // never treat this overlay as a RAP dialog, and Welcome stays clickable.
+ el.style.zIndex = "0";
+ el.style.pointerEvents = "none";
+ el.setAttribute("aria-busy", "false");
+ el.className += " hop-web-splash--out";
+ window.setTimeout(function() {
+ if (el.parentNode) {
+ el.parentNode.removeChild(el);
+ }
+ }, 200);
+ }
+ function hasRapUi() {
+ // Wait for the main toolbar, not an empty RAP Display/Shell. After Tomcat
+ // start the first /ui request spends seconds in HopGui.open() (class load
+
+ // GuiResource); RAP may paint a blank shell before that work is flushed.
+ return !!document.querySelector("[id$='-toolbar-10010-new']");
+ }
+ var hideScheduled = false;
+ function check() {
+ if (hidden || hideScheduled || !hasRapUi()) {
+ return;
+ }
+ hideScheduled = true;
+ // Two animation frames: RAP flushes layout after creating Display, and on
+ // a cold load that work often lands on the next frame.
+ var waitFrame = window.requestAnimationFrame || function(cb) {
window.setTimeout(cb, 16); };
+ waitFrame(function() {
+ waitFrame(function() {
+ hide();
+ });
+ });
+ }
+ function watch() {
+ if (!document.body) {
+ return;
+ }
+ var observer = new MutationObserver(check);
+ observer.observe(document.body, { childList: true, subtree: true });
+ check();
+ }
+ if (document.body) {
+ watch();
+ } else {
+ document.addEventListener("DOMContentLoaded", watch);
+ }
+ window.setTimeout(hide, 60000);
+})();
+</script>
diff --git a/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebSplashTest.java
b/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebSplashTest.java
new file mode 100644
index 0000000000..9401b3f744
--- /dev/null
+++ b/rap/src/test/java/org/apache/hop/ui/hopgui/HopWebSplashTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+package org.apache.hop.ui.hopgui;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link HopWeb} */
+class HopWebSplashTest {
+
+ @Test
+ void lightSplashUsesLightThemeClass() {
+ String html = HopWeb.splashBodyHtml("light");
+
+ assertTrue(html.contains("id=\"hop-web-splash\""));
+ assertTrue(html.contains("class=\"hop-web-splash--light\""));
+ assertFalse(html.contains("{{theme}}"));
+ }
+
+ @Test
+ void darkSplashUsesDarkThemeClass() {
+ String html = HopWeb.splashBodyHtml("dark");
+
+ assertTrue(html.contains("class=\"hop-web-splash--dark\""));
+ assertFalse(html.contains("class=\"hop-web-splash--light\""));
+ }
+
+ @Test
+ void overlaySitsAboveRapShellsThenDropsZIndexOnHide() {
+ String html = HopWeb.splashBodyHtml("light");
+
+ assertTrue(html.contains("z-index: 100000010"));
+ assertTrue(html.contains("el.style.zIndex = \"0\""));
+ assertTrue(html.contains("MutationObserver"));
+ assertTrue(html.contains("toolbar-10010-new"));
+ assertTrue(html.contains("requestAnimationFrame"));
+ }
+}