From 7409649b0165711f45ce161a65fb3e4713f7488d Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Sat, 27 Jul 2024 09:44:58 +1000 Subject: [PATCH] Allow extensions to contribute actions to the error page Signed-off-by: Phillip Kruger --- .../io/quarkus/runtime/ErrorPageAction.java | 5 +++ .../quarkus/runtime/TemplateHtmlBuilder.java | 31 ++++++++++++++----- .../META-INF/template-html-builder.css | 19 ++++++++++++ .../deployment/ErrorPageActionsBuildItem.java | 29 +++++++++++++++++ .../http/deployment/VertxHttpProcessor.java | 11 ++++++- .../http/runtime/QuarkusErrorHandler.java | 13 +++++++- .../vertx/http/runtime/VertxHttpRecorder.java | 9 +++--- .../runtime/devmode/ReplacementDebugPage.java | 2 +- .../runtime/devmode/ResourceNotFoundData.java | 2 +- 9 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ErrorPageActionsBuildItem.java diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java b/core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java new file mode 100644 index 0000000000000..4dc3967abf23a --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/ErrorPageAction.java @@ -0,0 +1,5 @@ +package io.quarkus.runtime; + +public record ErrorPageAction(String name, String url) { + +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java index 4d8b4b9315749..22b3cce91e9f2 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/TemplateHtmlBuilder.java @@ -115,6 +115,7 @@ public class TemplateHtmlBuilder { + "
\n" + "
\n" + "

%2$s

\n" + + "
%3$s
\n" + "
\n" + "
\n" + "
\n"; @@ -176,25 +177,37 @@ public class TemplateHtmlBuilder { private String baseUrl; public TemplateHtmlBuilder(String title, String subTitle, String details) { - this(null, title, subTitle, details, null, Collections.emptyList()); + this(null, title, subTitle, details, Collections.emptyList(), null, Collections.emptyList()); } - public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details) { - this(baseUrl, title, subTitle, details, null, Collections.emptyList()); + public TemplateHtmlBuilder(String title, String subTitle, String details, List actions) { + this(null, title, subTitle, details, actions, null, Collections.emptyList()); } - public TemplateHtmlBuilder(String title, String subTitle, String details, String redirect, + public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details, List actions) { + this(baseUrl, title, subTitle, details, actions, null, Collections.emptyList()); + } + + public TemplateHtmlBuilder(String title, String subTitle, String details, List actions, String redirect, List config) { - this(null, title, subTitle, details, null, Collections.emptyList()); + this(null, title, subTitle, details, actions, null, Collections.emptyList()); } - public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details, String redirect, + public TemplateHtmlBuilder(String baseUrl, String title, String subTitle, String details, List actions, + String redirect, List config) { this.baseUrl = baseUrl; + loadCssFile(); + + StringBuilder actionLinks = new StringBuilder(); + for (ErrorPageAction epa : actions) { + actionLinks.append(buildLink(epa.name(), epa.url())); + } + result = new StringBuilder(String.format(HTML_TEMPLATE_START, escapeHtml(title), subTitle == null || subTitle.isEmpty() ? "" : " - " + escapeHtml(subTitle), CSS)); - result.append(String.format(HEADER_TEMPLATE, escapeHtml(title), escapeHtml(details))); + result.append(String.format(HEADER_TEMPLATE, escapeHtml(title), escapeHtml(details), actionLinks.toString())); if (!config.isEmpty()) { result.append(String.format(CONFIG_EDITOR_HEAD, redirect)); for (CurrentConfig i : config) { @@ -379,4 +392,8 @@ public void loadCssFile() { } } } + + private String buildLink(String name, String url) { + return "" + name + ""; + } } diff --git a/core/runtime/src/main/resources/META-INF/template-html-builder.css b/core/runtime/src/main/resources/META-INF/template-html-builder.css index 2d76259b11b96..7760fd6de8743 100644 --- a/core/runtime/src/main/resources/META-INF/template-html-builder.css +++ b/core/runtime/src/main/resources/META-INF/template-html-builder.css @@ -34,6 +34,25 @@ ul { .exception-message { background: #960031; font-size: 1.5rem; + display: flex; + align-items: center; + padding-right: 50px; +} + +.actions { + font-size: 1.1rem; + display: flex; + gap: 10px; +} + +.actions a { + + padding: 1px 6px; + border: 1px outset #180011; + border-radius: 4px; + color: white !important; + background-color: #041437; + text-decoration: none !important; } h1, h2 { diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ErrorPageActionsBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ErrorPageActionsBuildItem.java new file mode 100644 index 0000000000000..fe1f1bc70ddae --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/ErrorPageActionsBuildItem.java @@ -0,0 +1,29 @@ +package io.quarkus.vertx.http.deployment; + +import java.util.List; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.ErrorPageAction; + +/** + * Allows extensions to contribute an action (button) to the error page + */ +public final class ErrorPageActionsBuildItem extends MultiBuildItem { + private final List actions; + + public ErrorPageActionsBuildItem(String name, String url) { + this(new ErrorPageAction(name, url)); + } + + public ErrorPageActionsBuildItem(ErrorPageAction errorPageAction) { + this(List.of(errorPageAction)); + } + + public ErrorPageActionsBuildItem(List errorPageAction) { + this.actions = errorPageAction; + } + + public List getActions() { + return actions; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 495e331d6257a..53d8bfb19f20f 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -51,6 +51,7 @@ import io.quarkus.deployment.pkg.steps.NoopNativeImageBuildRunner; import io.quarkus.kubernetes.spi.KubernetesPortBuildItem; import io.quarkus.netty.runtime.virtual.VirtualServerChannel; +import io.quarkus.runtime.ErrorPageAction; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.LiveReloadConfig; import io.quarkus.runtime.RuntimeValue; @@ -338,6 +339,7 @@ ServiceStartBuildItem finalizeRouter( HttpBuildTimeConfig httpBuildTimeConfig, List requireBodyHandlerBuildItems, BodyHandlerBuildItem bodyHandlerBuildItem, + List errorPageActionsBuildItems, BuildProducer shutdownListenerBuildItemBuildProducer, ShutdownConfig shutdownConfig, LiveReloadConfig lrc, @@ -391,6 +393,12 @@ ServiceStartBuildItem finalizeRouter( } } + // Combine all error actions from exceptions + List combinedActions = new ArrayList<>(); + for (ErrorPageActionsBuildItem errorPageActionsBuildItem : errorPageActionsBuildItems) { + combinedActions.addAll(errorPageActionsBuildItem.getActions()); + } + recorder.finalizeRouter(beanContainer.getValue(), defaultRoute.map(DefaultRouteBuildItem::getRoute).orElse(null), listOfFilters, listOfManagementInterfaceFilters, @@ -401,7 +409,8 @@ ServiceStartBuildItem finalizeRouter( nonApplicationRootPathBuildItem.getNonApplicationRootPath(), launchMode.getLaunchMode(), getBodyHandlerRequiredConditions(requireBodyHandlerBuildItems), bodyHandlerBuildItem.getHandler(), - gracefulShutdownFilter, shutdownConfig, executorBuildItem.getExecutorProxy()); + gracefulShutdownFilter, shutdownConfig, executorBuildItem.getExecutorProxy(), + combinedActions); return new ServiceStartBuildItem("vertx-http"); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java index 20c10be2eb202..8405fe52bfe00 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/QuarkusErrorHandler.java @@ -18,6 +18,7 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; +import io.quarkus.runtime.ErrorPageAction; import io.quarkus.runtime.TemplateHtmlBuilder; import io.quarkus.security.AuthenticationException; import io.quarkus.security.ForbiddenException; @@ -43,12 +44,20 @@ public class QuarkusErrorHandler implements Handler { private final boolean showStack; private final boolean decorateStack; private final Optional contentTypeDefault; + private final List actions; public QuarkusErrorHandler(boolean showStack, boolean decorateStack, Optional contentTypeDefault) { + this(showStack, decorateStack, contentTypeDefault, List.of()); + } + + public QuarkusErrorHandler(boolean showStack, boolean decorateStack, + Optional contentTypeDefault, + List actions) { this.showStack = showStack; this.decorateStack = decorateStack; this.contentTypeDefault = contentTypeDefault; + this.actions = actions; } @Override @@ -200,10 +209,12 @@ private void jsonResponse(RoutingContext event, String contentType, String detai private void htmlResponse(RoutingContext event, String details, Throwable exception) { event.response().headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=utf-8"); - final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details); + final TemplateHtmlBuilder htmlBuilder = new TemplateHtmlBuilder("Internal Server Error", details, details, + this.actions); if (showStack && exception != null) { htmlBuilder.stack(exception); } + writeResponse(event, htmlBuilder.toString()); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index 0a221237122a8..156aabc83880f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -54,6 +54,7 @@ import io.quarkus.netty.runtime.virtual.VirtualAddress; import io.quarkus.netty.runtime.virtual.VirtualChannel; import io.quarkus.netty.runtime.virtual.VirtualServerChannel; +import io.quarkus.runtime.ErrorPageAction; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.LiveReloadConfig; import io.quarkus.runtime.QuarkusBindException; @@ -377,7 +378,8 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute LaunchMode launchMode, BooleanSupplier[] requireBodyHandlerConditions, Handler bodyHandler, GracefulShutdownFilter gracefulShutdownFilter, ShutdownConfig shutdownConfig, - Executor executor) { + Executor executor, + List actions) { HttpConfiguration httpConfiguration = this.httpConfiguration.getValue(); // install the default route at the end Router httpRouteRouter = httpRouterRuntimeValue.getValue(); @@ -415,8 +417,7 @@ public void finalizeRouter(BeanContainer container, Consumer defaultRoute applyCompression(httpBuildTimeConfig.enableCompression, httpRouteRouter); httpRouteRouter.route().last().failureHandler( new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, httpConfiguration), - httpConfiguration.unhandledErrorContentTypeDefault)); - + httpConfiguration.unhandledErrorContentTypeDefault, actions)); for (BooleanSupplier requireBodyHandlerCondition : requireBodyHandlerConditions) { if (requireBodyHandlerCondition.getAsBoolean()) { //if this is set then everything needs the body handler installed @@ -535,7 +536,7 @@ public void handle(RoutingContext event) { mr.route().last().failureHandler( new QuarkusErrorHandler(launchMode.isDevOrTest(), decorateStacktrace(launchMode, httpConfiguration), - httpConfiguration.unhandledErrorContentTypeDefault)); + httpConfiguration.unhandledErrorContentTypeDefault, actions)); mr.route().order(RouteConstants.ROUTE_ORDER_BODY_HANDLER_MANAGEMENT) .handler(createBodyHandlerForManagementInterface()); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ReplacementDebugPage.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ReplacementDebugPage.java index 1ad26a274de46..5cc7647580bfc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ReplacementDebugPage.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ReplacementDebugPage.java @@ -64,7 +64,7 @@ public static String generateHtml(final Throwable exception, String currentUri) } TemplateHtmlBuilder builder = new TemplateHtmlBuilder("Error restarting Quarkus", exception.getClass().getName(), - generateHeaderMessage(exception), currentUri, toEdit); + generateHeaderMessage(exception), List.of(), currentUri, toEdit); builder.stack(exception); return builder.toString(); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java index ad0e6bab207f5..b9f28a85dee5a 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -75,7 +75,7 @@ public String getHTMLContent() { List combinedRoutes = getCombinedRoutes(); TemplateHtmlBuilder builder = new TemplateHtmlBuilder(this.baseUrl, - HEADING, "", "Resources overview"); + HEADING, "", "Resources overview", List.of()); builder.resourcesStart(RESOURCE_ENDPOINTS);