Skip to content

Commit 01e3c6d

Browse files
committed
Hop Web: show a loading splash
Signed-off-by: lance <leehaut@gmail.com>
1 parent 75fc6b3 commit 01e3c6d

3 files changed

Lines changed: 253 additions & 13 deletions

File tree

rap/src/main/java/org/apache/hop/ui/hopgui/HopWeb.java

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class HopWeb implements ApplicationConfiguration {
6161
public static final String CONST_LIGHT = "light";
6262

6363
private static final String WEB_PAGE_TITLE = "Apache Hop Web";
64+
private static final String RESOURCE_LOGO_ICON_PNG = "ui/images/logo_icon.png";
6465

6566
/**
6667
* Returns the browser page title for Hop Web, including the Apache Hop version when it is
@@ -93,10 +94,8 @@ public void configure(Application application) {
9394
//
9495
GuiRegistry registry = GuiRegistry.getInstance();
9596
Map<String, Map<String, GuiToolbarItem>> guiToolbarMap = registry.getGuiToolbarMap();
96-
for (String toolbarId : guiToolbarMap.keySet()) {
97-
Map<String, GuiToolbarItem> itemMap = guiToolbarMap.get(toolbarId);
98-
for (String itemId : itemMap.keySet()) {
99-
final GuiToolbarItem item = itemMap.get(itemId);
97+
for (Map<String, GuiToolbarItem> itemMap : guiToolbarMap.values()) {
98+
for (GuiToolbarItem item : itemMap.values()) {
10099
addResource(application, item.getImage(), item.getClassLoader());
101100
}
102101
}
@@ -133,7 +132,7 @@ public void configure(Application application) {
133132
}
134133

135134
application.addResource(
136-
"ui/images/logo_icon.png",
135+
RESOURCE_LOGO_ICON_PNG,
137136
new ResourceLoader() {
138137
@Override
139138
public InputStream getResourceAsStream(String resourceName) {
@@ -152,6 +151,10 @@ public InputStream getResourceAsStream(String resourceName) {
152151
return new ByteArrayInputStream(outputStream.toByteArray());
153152
}
154153
});
154+
application.addResource(
155+
"ui/images/logo_hop.svg",
156+
resourceName ->
157+
HopWeb.class.getClassLoader().getResourceAsStream("ui/images/logo_hop.svg"));
155158
application.addServiceHandler(
156159
CanvasRenderServiceHandler.SERVICE_ID, new CanvasRenderServiceHandler());
157160

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

184187
Map<String, String> propertiesLight = new HashMap<>();
185188
propertiesLight.put(WebClient.PAGE_TITLE, webPageTitle);
186-
propertiesLight.put(WebClient.FAVICON, "ui/images/logo_icon.png");
189+
propertiesLight.put(WebClient.FAVICON, RESOURCE_LOGO_ICON_PNG);
187190
propertiesLight.put(WebClient.THEME_ID, CONST_LIGHT);
188191
propertiesLight.put(WebClient.HEAD_HTML, readTextFromResource("head.html"));
192+
propertiesLight.put(WebClient.BODY_HTML, splashBodyHtml(CONST_LIGHT));
189193

190194
Map<String, String> propertiesDark = new HashMap<>();
191195
propertiesDark.put(WebClient.PAGE_TITLE, webPageTitle);
192-
propertiesDark.put(WebClient.FAVICON, "ui/images/logo_icon.png");
196+
propertiesDark.put(WebClient.FAVICON, RESOURCE_LOGO_ICON_PNG);
193197
propertiesDark.put(WebClient.THEME_ID, "dark");
194198
propertiesDark.put(WebClient.HEAD_HTML, readTextFromResource("head.html"));
199+
propertiesDark.put(WebClient.BODY_HTML, splashBodyHtml("dark"));
195200

196201
application.addEntryPoint("/ui", HopWebEntryPoint.class, propertiesLight);
197202
application.addEntryPoint("/ui-dark", HopWebEntryPoint.class, propertiesDark);
@@ -225,12 +230,7 @@ private void registerMonacoResources(Application application) {
225230
String resourceName = "monaco/" + trimmed;
226231
application.addResource(
227232
resourceName,
228-
new ResourceLoader() {
229-
@Override
230-
public InputStream getResourceAsStream(String name) {
231-
return HopWeb.class.getClassLoader().getResourceAsStream(classpathResource);
232-
}
233-
});
233+
name -> HopWeb.class.getClassLoader().getResourceAsStream(classpathResource));
234234
}
235235
} catch (Exception e) {
236236
LogChannel.UI.logError("Failed to register Monaco editor resources", e);
@@ -263,6 +263,16 @@ private void addResource(
263263
application.addResource(imageFilename, loader);
264264
}
265265

266+
/**
267+
* Startup overlay for {@link WebClient#BODY_HTML}. It stays up until the main toolbar exists
268+
* ({@code toolbar-10010-new}), covering the first-request {@code HopGui.open()} work; client JS
269+
* then drops the z-index so Welcome and the GUI stay clickable (issue #8112).
270+
*/
271+
static String splashBodyHtml(String theme) {
272+
String safeTheme = CONST_LIGHT.equals(theme) ? CONST_LIGHT : "dark";
273+
return readTextFromResource("splash.html").replace("{{theme}}", safeTheme);
274+
}
275+
266276
private static String readTextFromResource(String resourceName) {
267277
String result;
268278
try {

rap/src/main/resources/splash.html

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<!--
2+
~ Licensed to the Apache Software Foundation (ASF) under one or more
3+
~ contributor license agreements. See the NOTICE file distributed with
4+
~ this work for additional information regarding copyright ownership.
5+
~ The ASF licenses this file to You under the Apache License, Version 2.0
6+
~ (the "License"); you may not use this file except in compliance with
7+
~ the License. You may obtain a copy of the License at
8+
~
9+
~ http://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
~
17+
-->
18+
<style>
19+
#hop-web-splash {
20+
position: fixed;
21+
inset: 0;
22+
z-index: 100000010;
23+
display: flex;
24+
align-items: center;
25+
justify-content: center;
26+
font-family: "Segoe UI", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
27+
transition: opacity 180ms ease-out;
28+
}
29+
#hop-web-splash.hop-web-splash--out {
30+
opacity: 0;
31+
pointer-events: none;
32+
}
33+
.hop-web-splash-card {
34+
display: flex;
35+
flex-direction: column;
36+
align-items: center;
37+
text-align: center;
38+
gap: 12px;
39+
}
40+
.hop-web-splash-logo {
41+
width: 72px;
42+
height: 72px;
43+
border-radius: 18px;
44+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
45+
background: #033d5d;
46+
overflow: hidden;
47+
display: flex;
48+
align-items: center;
49+
justify-content: center;
50+
}
51+
.hop-web-splash-logo img {
52+
width: 100%;
53+
height: 100%;
54+
display: block;
55+
object-fit: cover;
56+
object-position: center;
57+
}
58+
.hop-web-splash-title {
59+
margin: 8px 0 0;
60+
font-size: 1.35rem;
61+
font-weight: 650;
62+
letter-spacing: -0.02em;
63+
}
64+
.hop-web-splash-status {
65+
margin: 0;
66+
font-size: 0.95rem;
67+
font-weight: 500;
68+
}
69+
.hop-web-splash-spinner {
70+
width: 28px;
71+
height: 28px;
72+
margin-top: 8px;
73+
border-radius: 50%;
74+
border: 3px solid rgba(26, 143, 194, 0.25);
75+
border-top-color: #1a8fc2;
76+
animation: hop-web-splash-spin 0.8s linear infinite;
77+
}
78+
.hop-web-splash--light {
79+
background: #f4f8fb;
80+
color: #033d5d;
81+
}
82+
.hop-web-splash--light .hop-web-splash-status {
83+
color: #5a6f7c;
84+
}
85+
.hop-web-splash--dark {
86+
background: linear-gradient(145deg, #022a40 0%, #033d5d 42%, #0a5a82 100%);
87+
color: #ffffff;
88+
}
89+
.hop-web-splash--dark .hop-web-splash-status {
90+
color: #b9e5fa;
91+
}
92+
.hop-web-splash--dark .hop-web-splash-spinner {
93+
border-color: rgba(185, 229, 250, 0.25);
94+
border-top-color: #b9e5fa;
95+
}
96+
@keyframes hop-web-splash-spin {
97+
to { transform: rotate(360deg); }
98+
}
99+
@media (prefers-reduced-motion: reduce) {
100+
.hop-web-splash-spinner { animation: none; }
101+
#hop-web-splash { transition: none; }
102+
}
103+
</style>
104+
<div id="hop-web-splash" class="hop-web-splash--{{theme}}" role="status" aria-live="polite" aria-busy="true">
105+
<div class="hop-web-splash-card">
106+
<div class="hop-web-splash-logo">
107+
<img src="rwt-resources/ui/images/logo_hop.svg" width="72" height="72" alt="" onerror="this.style.visibility='hidden'"/>
108+
</div>
109+
<p class="hop-web-splash-title">Apache Hop Web</p>
110+
<p class="hop-web-splash-status">Loading &hellip;</p>
111+
<div class="hop-web-splash-spinner" aria-hidden="true"></div>
112+
</div>
113+
</div>
114+
<script>
115+
(function() {
116+
var splashId = "hop-web-splash";
117+
var hidden = false;
118+
function hide() {
119+
if (hidden) {
120+
return;
121+
}
122+
var el = document.getElementById(splashId);
123+
hidden = true;
124+
if (!el) {
125+
return;
126+
}
127+
// Drop z-index immediately so web-tests (HopGuiPage.SHELL_TITLES, z>=100000)
128+
// never treat this overlay as a RAP dialog, and Welcome stays clickable.
129+
el.style.zIndex = "0";
130+
el.style.pointerEvents = "none";
131+
el.setAttribute("aria-busy", "false");
132+
el.className += " hop-web-splash--out";
133+
window.setTimeout(function() {
134+
if (el.parentNode) {
135+
el.parentNode.removeChild(el);
136+
}
137+
}, 200);
138+
}
139+
function hasRapUi() {
140+
// Wait for the main toolbar, not an empty RAP Display/Shell. After Tomcat
141+
// start the first /ui request spends seconds in HopGui.open() (class load +
142+
// GuiResource); RAP may paint a blank shell before that work is flushed.
143+
return !!document.querySelector("[id$='-toolbar-10010-new']");
144+
}
145+
var hideScheduled = false;
146+
function check() {
147+
if (hidden || hideScheduled || !hasRapUi()) {
148+
return;
149+
}
150+
hideScheduled = true;
151+
// Two animation frames: RAP flushes layout after creating Display, and on
152+
// a cold load that work often lands on the next frame.
153+
var waitFrame = window.requestAnimationFrame || function(cb) { window.setTimeout(cb, 16); };
154+
waitFrame(function() {
155+
waitFrame(function() {
156+
hide();
157+
});
158+
});
159+
}
160+
function watch() {
161+
if (!document.body) {
162+
return;
163+
}
164+
var observer = new MutationObserver(check);
165+
observer.observe(document.body, { childList: true, subtree: true });
166+
check();
167+
}
168+
if (document.body) {
169+
watch();
170+
} else {
171+
document.addEventListener("DOMContentLoaded", watch);
172+
}
173+
window.setTimeout(hide, 60000);
174+
})();
175+
</script>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hop.ui.hopgui;
19+
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import org.junit.jupiter.api.Test;
24+
25+
/** Unit test for {@link HopWeb} */
26+
class HopWebSplashTest {
27+
28+
@Test
29+
void lightSplashUsesLightThemeClass() {
30+
String html = HopWeb.splashBodyHtml("light");
31+
32+
assertTrue(html.contains("id=\"hop-web-splash\""));
33+
assertTrue(html.contains("class=\"hop-web-splash--light\""));
34+
assertFalse(html.contains("{{theme}}"));
35+
}
36+
37+
@Test
38+
void darkSplashUsesDarkThemeClass() {
39+
String html = HopWeb.splashBodyHtml("dark");
40+
41+
assertTrue(html.contains("class=\"hop-web-splash--dark\""));
42+
assertFalse(html.contains("class=\"hop-web-splash--light\""));
43+
}
44+
45+
@Test
46+
void overlaySitsAboveRapShellsThenDropsZIndexOnHide() {
47+
String html = HopWeb.splashBodyHtml("light");
48+
49+
assertTrue(html.contains("z-index: 100000010"));
50+
assertTrue(html.contains("el.style.zIndex = \"0\""));
51+
assertTrue(html.contains("MutationObserver"));
52+
assertTrue(html.contains("toolbar-10010-new"));
53+
assertTrue(html.contains("requestAnimationFrame"));
54+
}
55+
}

0 commit comments

Comments
 (0)