Skip to content

Commit 43447ae

Browse files
authored
fix: show available client routes (#19050)
* fix: show available client routes Adds client views in the list of available routes when viewing "Route not found" page in development mode. Adds ClientRoutesProvider interface to get available client routes. Client routes means mainly Hilla views, but not limited to. Fixes: #18857 * chore: renamed a method * test: fixed test * chore: removed default ClientRoutesProvider Can't use @ConditionalOnMissingBean as the bean from Hilla dependencies is not reached from here leading to multiple beans which will break things. Therefore, allowing just one implementation of ClientRoutesProvider when Hilla is in the classpath. * chore: support multiple ClientRoutesProviders * chore: cleanup * chore: filter routes starting with $ * chore: filter $layout routes and trim $index * chore: remove default * chore: fixed test * chore: fixed test
1 parent cc01569 commit 43447ae

File tree

5 files changed

+158
-2
lines changed

5 files changed

+158
-2
lines changed

flow-server/src/main/java/com/vaadin/flow/router/AbstractRouteNotFoundError.java

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
import java.util.ArrayList;
2323
import java.util.List;
2424
import java.util.Map;
25+
import java.util.Objects;
26+
import java.util.Optional;
2527
import java.util.TreeMap;
28+
import java.util.regex.Pattern;
2629
import java.util.stream.Collectors;
2730

2831
import org.apache.commons.io.IOUtils;
@@ -35,7 +38,8 @@
3538
import com.vaadin.flow.component.Component;
3639
import com.vaadin.flow.component.Html;
3740
import com.vaadin.flow.component.Tag;
38-
import com.vaadin.flow.internal.hilla.EndpointRequestUtil;
41+
import com.vaadin.flow.di.Lookup;
42+
import com.vaadin.flow.router.internal.ClientRoutesProvider;
3943
import com.vaadin.flow.server.HttpStatusCode;
4044
import com.vaadin.flow.server.VaadinService;
4145
import com.vaadin.flow.server.frontend.FrontendUtils;
@@ -116,8 +120,10 @@ private static String readHtmlFile(String templateName) {
116120
}
117121

118122
private String getRoutes(BeforeEnterEvent event) {
123+
List<Element> routeElements = new ArrayList<>();
119124
List<RouteData> routes = event.getSource().getRegistry()
120125
.getRegisteredRoutes();
126+
121127
Map<String, Class<? extends Component>> routeTemplates = new TreeMap<>();
122128

123129
for (RouteData route : routes) {
@@ -127,14 +133,21 @@ private String getRoutes(BeforeEnterEvent event) {
127133
.put(alias.getTemplate(), alias.getNavigationTarget()));
128134
}
129135

130-
List<Element> routeElements = new ArrayList<>();
131136
routeTemplates.forEach(
132137
(k, v) -> routeElements.add(routeTemplateToHtml(k, v)));
133138

139+
routeElements.addAll(getClientRoutes());
134140
return routeElements.stream().map(Element::outerHtml)
135141
.collect(Collectors.joining());
136142
}
137143

144+
private List<Element> getClientRoutes() {
145+
return FrontendUtils.getClientRoutes().stream()
146+
.filter(route -> !route.contains("$layout"))
147+
.map(route -> route.replace("$index", ""))
148+
.map(this::clientRouteToHtml).toList();
149+
}
150+
138151
private Element routeTemplateToHtml(String routeTemplate,
139152
Class<? extends Component> navigationTarget) {
140153
String text = routeTemplate;
@@ -156,6 +169,23 @@ private Element routeTemplateToHtml(String routeTemplate,
156169
}
157170
}
158171

172+
private Element clientRouteToHtml(String route) {
173+
String text = route;
174+
if (text.isEmpty()) {
175+
text = "<root>";
176+
}
177+
if (!route.contains(":")) {
178+
return elementAsLink(route, text);
179+
} else {
180+
if (Pattern.compile(":\\w+\\?").matcher(route).find()) {
181+
text += " (supports optional parameter)";
182+
} else {
183+
text += " (requires parameter)";
184+
}
185+
return new Element(Tag.LI).text(text);
186+
}
187+
}
188+
159189
private Element elementAsLink(String url, String text) {
160190
Element link = new Element(Tag.A).attr("href", url).text(text);
161191
return new Element(Tag.LI).appendChild(link);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2000-2024 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.router.internal;
18+
19+
import java.io.Serializable;
20+
import java.util.List;
21+
22+
/**
23+
* Interface for providing client side routes.
24+
*/
25+
public interface ClientRoutesProvider extends Serializable {
26+
27+
/**
28+
* Get a list of client side routes.
29+
*
30+
* @return a list of client side routes. Not null.
31+
*/
32+
List<String> getClientRoutes();
33+
}

flow-server/src/main/java/com/vaadin/flow/server/frontend/FrontendUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import com.vaadin.flow.internal.Pair;
5454
import com.vaadin.flow.internal.StringUtil;
5555
import com.vaadin.flow.internal.hilla.EndpointRequestUtil;
56+
import com.vaadin.flow.router.internal.ClientRoutesProvider;
5657
import com.vaadin.flow.server.AbstractConfiguration;
5758
import com.vaadin.flow.server.Constants;
5859
import com.vaadin.flow.server.VaadinService;
@@ -1434,4 +1435,20 @@ public static boolean isReactModuleAvailable(Options options) {
14341435
return false;
14351436
}
14361437
}
1438+
1439+
/**
1440+
* Get all available client routes in a distinct list of route paths
1441+
* collected from all {@link ClientRoutesProvider} implementations found
1442+
* with Vaadin {@link Lookup}.
1443+
*
1444+
* @return a list of available client routes
1445+
*/
1446+
public static List<String> getClientRoutes() {
1447+
return Optional.ofNullable(VaadinService.getCurrent())
1448+
.map(VaadinService::getContext).stream()
1449+
.flatMap(ctx -> ctx.getAttribute(Lookup.class)
1450+
.lookupAll(ClientRoutesProvider.class).stream())
1451+
.flatMap(provider -> provider.getClientRoutes().stream())
1452+
.filter(Objects::nonNull).distinct().toList();
1453+
}
14371454
}

flow-tests/vaadin-spring-tests/test-spring-boot-only-prepare/src/main/java/com/vaadin/flow/spring/test/TestServletInitializer.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,17 @@
1515
*/
1616
package com.vaadin.flow.spring.test;
1717

18+
import java.util.List;
19+
1820
import org.springframework.boot.SpringApplication;
1921
import org.springframework.boot.autoconfigure.SpringBootApplication;
22+
import org.springframework.context.annotation.Bean;
2023
import org.springframework.context.annotation.Configuration;
2124
import org.springframework.context.annotation.Import;
2225
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
2326

27+
import com.vaadin.flow.router.internal.ClientRoutesProvider;
28+
2429
@SpringBootApplication
2530
@Configuration
2631
@EnableWebSecurity
@@ -30,4 +35,25 @@ public static void main(String[] args) {
3035
SpringApplication.run(TestServletInitializer.class, args);
3136
}
3237

38+
@Bean
39+
public ClientRoutesProvider hillaClientRoutesProvider() {
40+
return new ClientRoutesProvider() {
41+
@Override
42+
public List<String> getClientRoutes() {
43+
return List.of("$index", "$layout", "/hilla",
44+
"/hilla/person/:id", "/hilla/persons/:id?");
45+
}
46+
};
47+
}
48+
49+
@Bean
50+
public ClientRoutesProvider anotherhillaClientRoutesProvider() {
51+
return new ClientRoutesProvider() {
52+
@Override
53+
public List<String> getClientRoutes() {
54+
return List.of("/$layout", "/hilla", "/hilla/hilla/$index",
55+
"/anotherhilla");
56+
}
57+
};
58+
}
3359
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2000-2024 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.vaadin.flow.spring.test;
18+
19+
import java.util.List;
20+
21+
import org.junit.Assert;
22+
import org.junit.Test;
23+
24+
public class HillaRoutesRegisteredIT extends AbstractSpringTest {
25+
26+
@Test
27+
public void assertClientRoutesRegistered() {
28+
String nonExistingRoutePath = "non-existing-route";
29+
getDriver().get(getContextRootURL() + '/' + nonExistingRoutePath);
30+
waitForDevServer();
31+
Assert.assertTrue(getDriver().getPageSource().contains(String
32+
.format("Could not navigate to '%s'", nonExistingRoutePath)));
33+
34+
if (getDriver().getPageSource().contains(
35+
"This detailed message is only shown when running in development mode.")) {
36+
var expectedClientRoutes = List.of("<a href=\"\">&lt;root&gt;</a>",
37+
"<a href=\"/hilla\">/hilla</a>",
38+
"<li>/hilla/person/:id (requires parameter)</li>",
39+
"<li>/hilla/persons/:id? (supports optional parameter)</li>",
40+
"<a href=\"/hilla/hilla/\">/hilla/hilla/</a>",
41+
"<a href=\"/anotherhilla\">/anotherhilla</a>");
42+
for (String route : expectedClientRoutes) {
43+
Assert.assertTrue(
44+
String.format("Expected client route %s is missing",
45+
route),
46+
getDriver().getPageSource().contains(route));
47+
}
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)