Skip to content

Commit

Permalink
fix: show available client routes (#19050)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
tltv committed Apr 3, 2024
1 parent cc01569 commit 43447ae
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 2 deletions.
Expand Up @@ -22,7 +22,10 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;
Expand All @@ -35,7 +38,8 @@
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.internal.hilla.EndpointRequestUtil;
import com.vaadin.flow.di.Lookup;
import com.vaadin.flow.router.internal.ClientRoutesProvider;
import com.vaadin.flow.server.HttpStatusCode;
import com.vaadin.flow.server.VaadinService;
import com.vaadin.flow.server.frontend.FrontendUtils;
Expand Down Expand Up @@ -116,8 +120,10 @@ private static String readHtmlFile(String templateName) {
}

private String getRoutes(BeforeEnterEvent event) {
List<Element> routeElements = new ArrayList<>();
List<RouteData> routes = event.getSource().getRegistry()
.getRegisteredRoutes();

Map<String, Class<? extends Component>> routeTemplates = new TreeMap<>();

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

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

routeElements.addAll(getClientRoutes());
return routeElements.stream().map(Element::outerHtml)
.collect(Collectors.joining());
}

private List<Element> getClientRoutes() {
return FrontendUtils.getClientRoutes().stream()
.filter(route -> !route.contains("$layout"))
.map(route -> route.replace("$index", ""))
.map(this::clientRouteToHtml).toList();
}

private Element routeTemplateToHtml(String routeTemplate,
Class<? extends Component> navigationTarget) {
String text = routeTemplate;
Expand All @@ -156,6 +169,23 @@ private Element routeTemplateToHtml(String routeTemplate,
}
}

private Element clientRouteToHtml(String route) {
String text = route;
if (text.isEmpty()) {
text = "<root>";
}
if (!route.contains(":")) {
return elementAsLink(route, text);
} else {
if (Pattern.compile(":\\w+\\?").matcher(route).find()) {
text += " (supports optional parameter)";
} else {
text += " (requires parameter)";
}
return new Element(Tag.LI).text(text);
}
}

private Element elementAsLink(String url, String text) {
Element link = new Element(Tag.A).attr("href", url).text(text);
return new Element(Tag.LI).appendChild(link);
Expand Down
@@ -0,0 +1,33 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed 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 com.vaadin.flow.router.internal;

import java.io.Serializable;
import java.util.List;

/**
* Interface for providing client side routes.
*/
public interface ClientRoutesProvider extends Serializable {

/**
* Get a list of client side routes.
*
* @return a list of client side routes. Not null.
*/
List<String> getClientRoutes();
}
Expand Up @@ -53,6 +53,7 @@
import com.vaadin.flow.internal.Pair;
import com.vaadin.flow.internal.StringUtil;
import com.vaadin.flow.internal.hilla.EndpointRequestUtil;
import com.vaadin.flow.router.internal.ClientRoutesProvider;
import com.vaadin.flow.server.AbstractConfiguration;
import com.vaadin.flow.server.Constants;
import com.vaadin.flow.server.VaadinService;
Expand Down Expand Up @@ -1434,4 +1435,20 @@ public static boolean isReactModuleAvailable(Options options) {
return false;
}
}

/**
* Get all available client routes in a distinct list of route paths
* collected from all {@link ClientRoutesProvider} implementations found
* with Vaadin {@link Lookup}.
*
* @return a list of available client routes
*/
public static List<String> getClientRoutes() {
return Optional.ofNullable(VaadinService.getCurrent())
.map(VaadinService::getContext).stream()
.flatMap(ctx -> ctx.getAttribute(Lookup.class)
.lookupAll(ClientRoutesProvider.class).stream())
.flatMap(provider -> provider.getClientRoutes().stream())
.filter(Objects::nonNull).distinct().toList();
}
}
Expand Up @@ -15,12 +15,17 @@
*/
package com.vaadin.flow.spring.test;

import java.util.List;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import com.vaadin.flow.router.internal.ClientRoutesProvider;

@SpringBootApplication
@Configuration
@EnableWebSecurity
Expand All @@ -30,4 +35,25 @@ public static void main(String[] args) {
SpringApplication.run(TestServletInitializer.class, args);
}

@Bean
public ClientRoutesProvider hillaClientRoutesProvider() {
return new ClientRoutesProvider() {
@Override
public List<String> getClientRoutes() {
return List.of("$index", "$layout", "/hilla",
"/hilla/person/:id", "/hilla/persons/:id?");
}
};
}

@Bean
public ClientRoutesProvider anotherhillaClientRoutesProvider() {
return new ClientRoutesProvider() {
@Override
public List<String> getClientRoutes() {
return List.of("/$layout", "/hilla", "/hilla/hilla/$index",
"/anotherhilla");
}
};
}
}
@@ -0,0 +1,50 @@
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* Licensed 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 com.vaadin.flow.spring.test;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;

public class HillaRoutesRegisteredIT extends AbstractSpringTest {

@Test
public void assertClientRoutesRegistered() {
String nonExistingRoutePath = "non-existing-route";
getDriver().get(getContextRootURL() + '/' + nonExistingRoutePath);
waitForDevServer();
Assert.assertTrue(getDriver().getPageSource().contains(String
.format("Could not navigate to '%s'", nonExistingRoutePath)));

if (getDriver().getPageSource().contains(
"This detailed message is only shown when running in development mode.")) {
var expectedClientRoutes = List.of("<a href=\"\">&lt;root&gt;</a>",
"<a href=\"/hilla\">/hilla</a>",
"<li>/hilla/person/:id (requires parameter)</li>",
"<li>/hilla/persons/:id? (supports optional parameter)</li>",
"<a href=\"/hilla/hilla/\">/hilla/hilla/</a>",
"<a href=\"/anotherhilla\">/anotherhilla</a>");
for (String route : expectedClientRoutes) {
Assert.assertTrue(
String.format("Expected client route %s is missing",
route),
getDriver().getPageSource().contains(route));
}
}
}
}

0 comments on commit 43447ae

Please sign in to comment.