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 &hellip;</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"));
+  }
+}

Reply via email to