Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -93,10 +94,8 @@ public void configure(Application application) {
//
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());
}
}
Expand Down Expand Up @@ -133,7 +132,7 @@ public void configure(Application application) {
}

application.addResource(
"ui/images/logo_icon.png",
RESOURCE_LOGO_ICON_PNG,
new ResourceLoader() {
@Override
public InputStream getResourceAsStream(String resourceName) {
Expand All @@ -152,6 +151,10 @@ public InputStream getResourceAsStream(String resourceName) {
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());

Expand Down Expand Up @@ -183,15 +186,17 @@ public InputStream getResourceAsStream(String resourceName) {

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);
Expand Down Expand Up @@ -225,12 +230,7 @@ private void registerMonacoResources(Application application) {
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);
Expand Down Expand Up @@ -263,6 +263,16 @@ private void addResource(
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 {
Expand Down
175 changes: 175 additions & 0 deletions rap/src/main/resources/splash.html
Original file line number Diff line number Diff line change
@@ -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>
55 changes: 55 additions & 0 deletions rap/src/test/java/org/apache/hop/ui/hopgui/HopWebSplashTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading