From c3a5c15dcb966b7e5e51699200a5feb23a384153 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Tue, 21 Feb 2023 13:56:53 +1100 Subject: [PATCH] Initial framework for dev-ui 2.0 Signed-off-by: Phillip Kruger --- bom/application/pom.xml | 353 +++++ .../StreamingLogHandlerBuildItem.java | 34 + .../logging/LoggingResourceProcessor.java | 21 +- .../runtime/logging/LoggingSetupRecorder.java | 27 +- extensions/arc/deployment/pom.xml | 4 + .../devconsole/ArcDevConsoleProcessor.java | 8 +- .../arc/deployment/devconsole/Name.java | 4 + .../devui/ArcBeanInfoBuildItem.java | 17 + .../deployment/devui/ArcDevUIProcessor.java | 113 ++ .../devui/DevBeanWithInterceptorInfo.java | 28 + .../resources/dev-ui/arc/qwc-arc-beans.js | 144 ++ .../dev-ui/arc/qwc-arc-decorators.js | 33 + .../dev-ui/arc/qwc-arc-fired-events.js | 130 ++ .../dev-ui/arc/qwc-arc-interceptors.js | 129 ++ .../dev-ui/arc/qwc-arc-invocation-trees.js | 84 + .../resources/dev-ui/arc/qwc-arc-observers.js | 147 ++ .../dev-ui/arc/qwc-arc-removed-components.js | 204 +++ .../arc/runtime/devmode/EventInfo.java | 67 + .../EventsMonitor.java | 78 +- .../arc/runtime/devmode/InvocationInfo.java | 15 + .../arc/runtime/devui/ArcJsonRPCService.java | 82 + .../io/quarkus/vertx/web/SimpleRouteTest.java | 2 +- .../test/ClasspathResourceTestCase.java | 27 +- .../StaticFileWithResourcesHttpRootTest.java | 3 + .../runtime/NotFoundExceptionMapper.java | 9 +- .../devui/SmallRyeGraphQLDevUIProcessor.java | 36 + .../smallrye-openapi/deployment/pom.xml | 4 + .../devui/OpenApiDevUIProcessor.java | 36 + extensions/vertx-http/deployment/pom.xml | 15 + .../deployment/BuildTimeConstBuildItem.java | 35 + .../deployment/BuiltTimeContentProcessor.java | 532 +++++++ .../devui/deployment/DevUIProcessor.java | 614 ++++++++ .../deployment/DevUIRoutesBuildItem.java | 35 + .../deployment/DevUIWebJarBuildItem.java | 23 + .../devui/deployment/ExtensionsBuildItem.java | 40 + .../InternalImportMapBuildItem.java | 30 + .../deployment/JsonRPCMethodsBuildItem.java | 23 + .../devui/deployment/MvnpmBuildItem.java | 21 + .../devui/deployment/ThemeVarsBuildItem.java | 38 + .../devui/deployment/extension/Codestart.java | 47 + .../devui/deployment/extension/Extension.java | 218 +++ .../deployment/extension/ExtensionGroup.java | 6 + .../logstream/LogStreamProcessor.java | 45 + .../deployment/StaticResourcesProcessor.java | 19 +- .../vertx-http/dev-ui-resources/pom.xml | 299 ++++ .../build-time/build-time-data.js | 4 + .../dev-ui-templates/build-time/index.html | 82 + .../resources/dev-ui/controller/jsonrpc.js | 279 ++++ .../dev-ui/controller/log-controller.js | 125 ++ .../resources/dev-ui/controller/notifier.js | 39 + .../dev-ui/controller/router-controller.js | 274 ++++ .../src/main/resources/dev-ui/favicon.ico | Bin 0 -> 4286 bytes .../dev-ui/font/red-hat-font.min.css | 1 + .../dev-ui/icon/font-awesome-brands.js | 477 ++++++ .../dev-ui/icon/font-awesome-regular.js | 174 ++ .../dev-ui/icon/font-awesome-solid.js | 1399 +++++++++++++++++ .../resources/dev-ui/icon/font-awesome.js | 7 + .../main/resources/dev-ui/quarkus-logo.png | Bin 0 -> 6206 bytes .../main/resources/dev-ui/qui/qui-badge.js | 126 ++ .../resources/dev-ui/qwc/qwc-build-steps.js | 29 + .../resources/dev-ui/qwc/qwc-configuration.js | 30 + .../dev-ui/qwc/qwc-continuous-testing.js | 31 + .../dev-ui/qwc/qwc-data-qute-page.js | 28 + .../resources/dev-ui/qwc/qwc-data-raw-page.js | 76 + .../dev-ui/qwc/qwc-data-table-page.js | 77 + .../resources/dev-ui/qwc/qwc-dev-services.js | 29 + .../dev-ui/qwc/qwc-extension-link.js | 97 ++ .../resources/dev-ui/qwc/qwc-extension.js | 244 +++ .../resources/dev-ui/qwc/qwc-extensions.js | 166 ++ .../resources/dev-ui/qwc/qwc-external-page.js | 112 ++ .../main/resources/dev-ui/qwc/qwc-footer.js | 333 ++++ .../main/resources/dev-ui/qwc/qwc-header.js | 245 +++ .../dev-ui/qwc/qwc-jsonrpc-messages.js | 191 +++ .../src/main/resources/dev-ui/qwc/qwc-menu.js | 215 +++ .../resources/dev-ui/qwc/qwc-server-log.js | 511 ++++++ .../resources/dev-ui/qwc/qwc-ws-status.js | 15 + .../dev-ui/state/connection-state.js | 59 + .../resources/dev-ui/state/devui-state.js | 64 + .../resources/dev-ui/state/theme-state.js | 57 + extensions/vertx-http/dev-ui-spi/pom.xml | 21 + .../devui/spi/AbstractDevUIBuildItem.java | 31 + .../io/quarkus/devui/spi/DevUIContent.java | 89 ++ .../devui/spi/JsonRPCProvidersBuildItem.java | 18 + .../spi/buildtime/QuteTemplateBuildItem.java | 61 + .../spi/buildtime/StaticContentBuildItem.java | 27 + .../devui/spi/page/AbstractPageBuildItem.java | 31 + .../spi/page/BuildTimeDataPageBuilder.java | 21 + .../java/io/quarkus/devui/spi/page/Card.java | 56 + .../devui/spi/page/CardPageBuildItem.java | 38 + .../devui/spi/page/ExternalPageBuilder.java | 61 + .../devui/spi/page/FooterPageBuildItem.java | 22 + .../devui/spi/page/MenuPageBuildItem.java | 22 + .../java/io/quarkus/devui/spi/page/Page.java | 166 ++ .../quarkus/devui/spi/page/PageBuilder.java | 119 ++ .../devui/spi/page/QuteDataPageBuilder.java | 36 + .../devui/spi/page/RawDataPageBuilder.java | 10 + .../devui/spi/page/TableDataPageBuilder.java | 28 + .../spi/page/WebComponentPageBuilder.java | 27 + extensions/vertx-http/pom.xml | 2 + .../runtime/DevUIBuildTimeStaticHandler.java | 116 ++ .../quarkus/devui/runtime/DevUIRecorder.java | 53 + .../quarkus/devui/runtime/DevUIWebSocket.java | 45 + .../quarkus/devui/runtime/MvnpmHandler.java | 100 ++ .../devui/runtime/VaadinRouterHandler.java | 36 + .../devui/runtime/comms/JsonRpcRouter.java | 161 ++ .../devui/runtime/comms/ReflectionInfo.java | 28 + .../devui/runtime/jsonrpc/JsonRpcKeys.java | 28 + .../devui/runtime/jsonrpc/JsonRpcMethod.java | 52 + .../runtime/jsonrpc/JsonRpcMethodName.java | 50 + .../devui/runtime/jsonrpc/JsonRpcReader.java | 68 + .../devui/runtime/jsonrpc/JsonRpcWriter.java | 46 + .../runtime/logstream/JsonFormatter.java | 151 ++ .../logstream/LogStreamBroadcaster.java | 43 + .../logstream/LogStreamJsonRPCService.java | 31 + .../runtime/logstream/LogStreamRecorder.java | 15 + .../runtime/logstream/MutinyLogHandler.java | 44 + .../http/runtime/devmode/ArcDevRecorder.java | 2 +- 117 files changed, 11364 insertions(+), 66 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/StreamingLogHandlerBuildItem.java create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcBeanInfoBuildItem.java create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/DevBeanWithInterceptorInfo.java create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-beans.js create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-decorators.js create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-fired-events.js create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-interceptors.js create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-invocation-trees.js create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-observers.js create mode 100644 extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-removed-components.js create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java rename extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/{devconsole => devmode}/EventsMonitor.java (59%) create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java create mode 100644 extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java create mode 100644 extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java create mode 100644 extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuiltTimeContentProcessor.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIRoutesBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIWebJarBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ExtensionsBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/InternalImportMapBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCMethodsBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/MvnpmBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ThemeVarsBuildItem.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Codestart.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/ExtensionGroup.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java create mode 100644 extensions/vertx-http/dev-ui-resources/pom.xml create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/build-time-data.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/index.html create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/log-controller.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/notifier.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/favicon.ico create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/font/red-hat-font.min.css create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-brands.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-regular.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-solid.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/quarkus-logo.png create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-qute-page.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-table-page.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-footer.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-jsonrpc-messages.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-menu.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-ws-status.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/connection-state.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/devui-state.js create mode 100644 extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/theme-state.js create mode 100644 extensions/vertx-http/dev-ui-spi/pom.xml create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/JsonRPCProvidersBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/StaticContentBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/BuildTimeDataPageBuilder.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Card.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/CardPageBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/ExternalPageBuilder.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/FooterPageBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/MenuPageBuildItem.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/QuteDataPageBuilder.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/RawDataPageBuilder.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/TableDataPageBuilder.java create mode 100644 extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/WebComponentPageBuilder.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/VaadinRouterHandler.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/ReflectionInfo.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcKeys.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethodName.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/JsonFormatter.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamBroadcaster.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index c4a8e8b189b52..936be271a94a2 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -218,6 +218,13 @@ 2.9.2 0.8.9 1.0.0 + + 1.0.6 + 2.6.1 + 1.7.0 + 23.3.7 + 1.7.4 + 2.1.0 @@ -1830,6 +1837,17 @@ quarkus-vertx-http-deployment-spi ${project.version} + + io.quarkus + quarkus-vertx-http-dev-ui-spi + ${project.version} + + + io.quarkus + quarkus-vertx-http-dev-ui-resources + ${project.version} + + io.quarkus quarkus-reactive-routes @@ -3174,6 +3192,341 @@ ${gizmo.version} + + + + org.mvnpm + importmap + ${importmap.version} + + + + org.mvnpm + lit + ${lit.version} + runtime + + + org.mvnpm + lit-element-state + ${lit-state.version} + runtime + + + org.mvnpm.at.vaadin + router + ${vaadin-router.version} + runtime + + + org.mvnpm.at.vaadin + accordion + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + avatar + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + horizontal-layout + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + button + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + combo-box + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + confirm-dialog + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + checkbox + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + checkbox-group + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + component-base + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + context-menu + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + custom-field + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + date-picker + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + date-time-picker + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + time-picker + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + details + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + dialog + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + email-field + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + password-field + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + number-field + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + field-base + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + grid + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + input-container + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + item + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + list-box + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + lit-renderer + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + menu-bar + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + message-list + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + message-input + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + multi-select-combo-box + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + radio-group + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + overlay + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + progress-bar + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + split-layout + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + text-area + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + tooltip + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + vaadin-list-mixin + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + vaadin-material-styles + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + vaadin-themable-mixin + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + vertical-layout + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + tabs + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + vaadin-lumo-styles + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + scroller + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + text-field + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + form-layout + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + tabsheet + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + icon + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + select + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + notification + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + upload + ${vaadin.version} + runtime + + + org.mvnpm.at.vaadin + virtual-list + ${vaadin.version} + runtime + + + + + org.mvnpm.at.vanillawc + wc-codemirror + ${wc-codemirror.version} + runtime + + biz.paluch.logging diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/StreamingLogHandlerBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/StreamingLogHandlerBuildItem.java new file mode 100644 index 0000000000000..c11d0f3fd052f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/StreamingLogHandlerBuildItem.java @@ -0,0 +1,34 @@ +package io.quarkus.deployment.builditem; + +import java.util.Optional; +import java.util.logging.Handler; + +import org.wildfly.common.Assert; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * A build item for adding the dev stream log via mutiny + */ +public final class StreamingLogHandlerBuildItem extends SimpleBuildItem { + private final RuntimeValue> handlerValue; + + /** + * Construct a new instance. + * + * @param handlerValue the handler value to add to the run time configuration + */ + public StreamingLogHandlerBuildItem(final RuntimeValue> handlerValue) { + this.handlerValue = Assert.checkNotNullParam("handlerValue", handlerValue); + } + + /** + * Get the handler value. + * + * @return the handler value + */ + public RuntimeValue> getHandlerValue() { + return handlerValue; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index acaf966faf7b0..237ca1e08668f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -68,6 +68,7 @@ import io.quarkus.deployment.builditem.NamedLogHandlersBuildItem; import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.ShutdownListenerBuildItem; +import io.quarkus.deployment.builditem.StreamingLogHandlerBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.WebSocketLogHandlerBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageSystemPropertyBuildItem; @@ -224,7 +225,8 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe LogBuildTimeConfig buildLog, CombinedIndexBuildItem combinedIndexBuildItem, LogCategoryMinLevelDefaultsBuildItem categoryMinLevelDefaults, - Optional logStreamHandlerBuildItem, + Optional streamingLogStreamHandlerBuildItem, + Optional wsLogStreamHandlerBuildItem, List handlerBuildItems, List namedHandlerBuildItems, List consoleFormatItems, @@ -252,11 +254,18 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe if (bannerBuildItem != null) { possibleSupplier = bannerBuildItem.getBannerSupplier(); } - // Dev UI Log Stream - RuntimeValue> devUiLogHandler = null; - if (logStreamHandlerBuildItem.isPresent()) { - devUiLogHandler = logStreamHandlerBuildItem.get().getHandlerValue(); + // Old Dev UI Log Stream + RuntimeValue> wsDevUiLogHandler = null; + if (wsLogStreamHandlerBuildItem.isPresent()) { + wsDevUiLogHandler = wsLogStreamHandlerBuildItem.get().getHandlerValue(); } + + // New Dev UI Log Stream + RuntimeValue> streamingDevUiLogHandler = null; + if (streamingLogStreamHandlerBuildItem.isPresent()) { + streamingDevUiLogHandler = streamingLogStreamHandlerBuildItem.get().getHandlerValue(); + } + boolean alwaysEnableLogStream = false; if (!logStreamBuildItems.isEmpty()) { alwaysEnableLogStream = true; @@ -279,7 +288,7 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(RecorderContext context, LoggingSe shutdownListenerBuildItemBuildProducer.produce(new ShutdownListenerBuildItem( recorder.initializeLogging(log, buildLog, discoveredLogComponents, categoryMinLevelDefaults.content, alwaysEnableLogStream, - devUiLogHandler, handlers, namedHandlers, + wsDevUiLogHandler, streamingDevUiLogHandler, handlers, namedHandlers, consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue) .collect(Collectors.toList()), possibleFileFormatters, diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 70c325c068037..57731859dbd23 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -78,7 +78,7 @@ public static void handleFailedStart(RuntimeValue>> ba new LoggingSetupRecorder(new RuntimeValue<>(consoleRuntimeConfig)).initializeLogging(config, buildConfig, DiscoveredLogComponents.ofEmpty(), Collections.emptyMap(), - false, null, + false, null, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), @@ -89,7 +89,8 @@ public ShutdownListener initializeLogging(LogConfig config, LogBuildTimeConfig b DiscoveredLogComponents discoveredLogComponents, final Map categoryDefaultMinLevels, final boolean enableWebStream, - final RuntimeValue> devUiConsoleHandler, + final RuntimeValue> wsDevUiConsoleHandler, + final RuntimeValue> streamingDevUiConsoleHandler, final List>> additionalHandlers, final List>> additionalNamedHandlers, final List>> possibleConsoleFormatters, @@ -179,10 +180,26 @@ public void close() throws SecurityException { } if ((launchMode.isDevOrTest() || enableWebStream) - && devUiConsoleHandler != null - && devUiConsoleHandler.getValue().isPresent()) { + && wsDevUiConsoleHandler != null + && wsDevUiConsoleHandler.getValue().isPresent()) { - Handler handler = devUiConsoleHandler.getValue().get(); + Handler handler = wsDevUiConsoleHandler.getValue().get(); + handler.setErrorManager(errorManager); + handler.setFilter(new LogCleanupFilter(filterElements, shutdownNotifier)); + + if (possibleBannerSupplier != null && possibleBannerSupplier.getValue().isPresent()) { + Supplier bannerSupplier = possibleBannerSupplier.getValue().get(); + String header = "\n" + bannerSupplier.get(); + handler.publish(new LogRecord(Level.INFO, header)); + } + handlers.add(handler); + } + + if ((launchMode.isDevOrTest()) + && streamingDevUiConsoleHandler != null + && streamingDevUiConsoleHandler.getValue().isPresent()) { + + Handler handler = streamingDevUiConsoleHandler.getValue().get(); handler.setErrorManager(errorManager); handler.setFilter(new LogCleanupFilter(filterElements, shutdownNotifier)); diff --git a/extensions/arc/deployment/pom.xml b/extensions/arc/deployment/pom.xml index c94e5e71adecf..bb95d4b482594 100644 --- a/extensions/arc/deployment/pom.xml +++ b/extensions/arc/deployment/pom.xml @@ -25,6 +25,10 @@ io.quarkus quarkus-vertx-http-dev-console-spi + + io.quarkus + quarkus-vertx-http-dev-ui-spi + io.quarkus quarkus-arc diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java index 3874e91b642ea..df0b3ce4ce5b9 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/ArcDevConsoleProcessor.java @@ -21,6 +21,7 @@ import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.devconsole.DependencyGraph.Link; +import io.quarkus.arc.deployment.devui.ArcBeanInfoBuildItem; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanDeploymentValidator; import io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext; @@ -34,11 +35,11 @@ import io.quarkus.arc.runtime.ArcContainerSupplier; import io.quarkus.arc.runtime.ArcRecorder; import io.quarkus.arc.runtime.BeanLookupSupplier; -import io.quarkus.arc.runtime.devconsole.EventsMonitor; import io.quarkus.arc.runtime.devconsole.InvocationInterceptor; import io.quarkus.arc.runtime.devconsole.InvocationTree; import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; import io.quarkus.arc.runtime.devconsole.Monitored; +import io.quarkus.arc.runtime.devmode.EventsMonitor; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -108,7 +109,8 @@ public void transform(TransformationContext transformationContext) { @BuildStep(onlyIf = IsDevelopment.class) public void collectBeanInfo(ValidationPhaseBuildItem validationPhaseBuildItem, CompletedApplicationClassPredicateBuildItem predicate, BuildProducer templates, - BuildProducer routes) { + BuildProducer routes, + BuildProducer arcBeanInfoProducer) { BeanDeploymentValidator.ValidationContext validationContext = validationPhaseBuildItem.getContext(); DevBeanInfos beanInfos = new DevBeanInfos(); for (BeanInfo bean : validationContext.beans()) { @@ -175,6 +177,8 @@ public void collectBeanInfo(ValidationPhaseBuildItem validationPhaseBuildItem, beanInfos.sort(); templates.produce(new DevConsoleTemplateInfoBuildItem("devBeanInfos", beanInfos)); + arcBeanInfoProducer.produce(new ArcBeanInfoBuildItem(beanInfos)); + routes.produce(new DevConsoleRouteBuildItem("toggleBeanDescription", "POST", new Handler() { @Override public void handle(RoutingContext context) { diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java index 027db93f79175..1c35b43503bed 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devconsole/Name.java @@ -41,6 +41,10 @@ public String getSimpleName() { return simpleName != null ? simpleName : name; } + public String getName() { + return name; + } + static String createSimpleName(Type type) { switch (type.kind()) { case CLASS: diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcBeanInfoBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcBeanInfoBuildItem.java new file mode 100644 index 0000000000000..8f90940b1b742 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcBeanInfoBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.arc.deployment.devui; + +import io.quarkus.arc.deployment.devconsole.DevBeanInfos; +import io.quarkus.builder.item.SimpleBuildItem; + +public final class ArcBeanInfoBuildItem extends SimpleBuildItem { + + private final DevBeanInfos beanInfos; + + public ArcBeanInfoBuildItem(DevBeanInfos beanInfos) { + this.beanInfos = beanInfos; + } + + public DevBeanInfos getBeanInfos() { + return beanInfos; + } +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java new file mode 100644 index 0000000000000..9cef7044ddf6f --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/ArcDevUIProcessor.java @@ -0,0 +1,113 @@ +package io.quarkus.arc.deployment.devui; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.arc.deployment.devconsole.DevBeanInfo; +import io.quarkus.arc.deployment.devconsole.DevBeanInfos; +import io.quarkus.arc.deployment.devconsole.DevDecoratorInfo; +import io.quarkus.arc.deployment.devconsole.DevInterceptorInfo; +import io.quarkus.arc.deployment.devconsole.DevObserverInfo; +import io.quarkus.arc.runtime.devui.ArcJsonRPCService; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; + +public class ArcDevUIProcessor { + + private static final String NAME = "ArC"; + + @BuildStep(onlyIf = IsDevelopment.class) + public CardPageBuildItem pages(ArcBeanInfoBuildItem arcBeanInfoBuildItem) { + DevBeanInfos beanInfos = arcBeanInfoBuildItem.getBeanInfos(); + + CardPageBuildItem pageBuildItem = new CardPageBuildItem(NAME); + + List beans = beanInfos.getBeans(); + if (!beans.isEmpty()) { + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:egg") + .componentLink("qwc-arc-beans.js") + .staticLabel(String.valueOf(beans.size()))); + + pageBuildItem.addBuildTimeData(BEANS, toDevBeanWithInterceptorInfo(beans, beanInfos)); + } + + List observers = beanInfos.getObservers(); + if (!observers.isEmpty()) { + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:eye") + .componentLink("qwc-arc-observers.js") + .staticLabel(String.valueOf(observers.size()))); + + pageBuildItem.addBuildTimeData(OBSERVERS, observers); + } + + List interceptors = beanInfos.getInterceptors(); + if (!interceptors.isEmpty()) { + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:traffic-light") + .componentLink("qwc-arc-interceptors.js") + .staticLabel(String.valueOf(interceptors.size()))); + + pageBuildItem.addBuildTimeData(INTERCEPTORS, interceptors); + } + + List decorators = beanInfos.getDecorators(); + if (!decorators.isEmpty()) { + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:traffic-light") + .componentLink("qwc-arc-decorators.js") + .staticLabel(String.valueOf(decorators.size()))); + + pageBuildItem.addBuildTimeData(DECORATORS, decorators); + } + + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:fire") + .componentLink("qwc-arc-fired-events.js")); + + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:diagram-project") + .componentLink("qwc-arc-invocation-trees.js")); + + int removedComponents = beanInfos.getRemovedComponents(); + if (removedComponents > 0) { + pageBuildItem.addPage(Page.webComponentPageBuilder() + .icon("font-awesome-solid:trash-can") + .componentLink("qwc-arc-removed-components.js") + .staticLabel(String.valueOf(removedComponents))); + + pageBuildItem.addBuildTimeData(REMOVED_BEANS, beanInfos.getRemovedBeans()); + pageBuildItem.addBuildTimeData(REMOVED_DECORATORS, beanInfos.getRemovedDecorators()); + pageBuildItem.addBuildTimeData(REMOVED_INTERCEPTORS, beanInfos.getRemovedInterceptors()); + } + + return pageBuildItem; + } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem createJsonRPCService() { + return new JsonRPCProvidersBuildItem(NAME, ArcJsonRPCService.class); + } + + private List toDevBeanWithInterceptorInfo(List beans, DevBeanInfos devBeanInfos) { + List l = new ArrayList<>(); + for (DevBeanInfo dbi : beans) { + l.add(new DevBeanWithInterceptorInfo(dbi, devBeanInfos)); + } + return l; + } + + private static final String BEANS = "beans"; + private static final String OBSERVERS = "observers"; + private static final String INTERCEPTORS = "interceptors"; + private static final String DECORATORS = "decorators"; + private static final String REMOVED_BEANS = "removedBeans"; + private static final String REMOVED_COMPONENTS = "removedComponents"; + private static final String REMOVED_DECORATORS = "removedDecorators"; + private static final String REMOVED_INTERCEPTORS = "removedInterceptors"; + +} diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/DevBeanWithInterceptorInfo.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/DevBeanWithInterceptorInfo.java new file mode 100644 index 0000000000000..80e21d19453a8 --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/devui/DevBeanWithInterceptorInfo.java @@ -0,0 +1,28 @@ +package io.quarkus.arc.deployment.devui; + +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.arc.deployment.devconsole.DevBeanInfo; +import io.quarkus.arc.deployment.devconsole.DevBeanInfos; +import io.quarkus.arc.deployment.devconsole.DevInterceptorInfo; + +public class DevBeanWithInterceptorInfo extends DevBeanInfo { + + private final List interceptorInfos = new ArrayList<>(); + + public DevBeanWithInterceptorInfo(DevBeanInfo beanInfo, DevBeanInfos beanInfos) { + super(beanInfo.getId(), beanInfo.getKind(), beanInfo.isApplicationBean(), beanInfo.getProviderType(), + beanInfo.getMemberName(), beanInfo.getTypes(), beanInfo.getQualifiers(), beanInfo.getScope(), + beanInfo.getDeclaringClass(), beanInfo.getInterceptors(), beanInfo.isGenerated()); + + for (String interceptorId : beanInfo.getInterceptors()) { + this.interceptorInfos.add(beanInfos.getInterceptor(interceptorId)); + } + } + + public List getInterceptorInfos() { + return interceptorInfos; + } + +} diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-beans.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-beans.js new file mode 100644 index 0000000000000..223ffb086bbc4 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-beans.js @@ -0,0 +1,144 @@ +import { LitElement, html, css} from 'lit'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import { beans } from 'arc-data'; +import '@vaadin/grid'; +import '@vaadin/vertical-layout'; +import 'qui-badge'; + +/** + * This component shows the Arc Beans + */ +export class QwcArcBeans extends LitElement { + + static styles = css` + .arctable { + height: 100%; + padding-bottom: 10px; + } + + code { + font-size: 85%; + } + + .annotation { + color: var(--lumo-contrast-50pct); + } + + .producer { + color: var(--lumo-primary-text-color); + } + `; + + static properties = { + _beans: {state: true}, + }; + + constructor() { + super(); + this._beans = beans; + } + + render() { + if (this._beans) { + + return html` + + + + + + + + + + `; + + } else { + return html`No beans found`; + } + } + + _beanRenderer(bean) { + return html` + @${bean.scope.simpleName} + ${bean.nonDefaultQualifiers.map(qualifier => + html`${this._qualifierRenderer(qualifier)}` + )} + ${bean.providerType.name} + `; + } + + _kindRenderer(bean) { + return html` + + ${this._kindBadgeRenderer(bean)} + ${this._kindClassRenderer(bean)} + + `; + } + + _kindBadgeRenderer(bean){ + let kind = this._camelize(bean.kind); + let level = null; + + if(bean.kind.toLowerCase() === "field"){ + kind = "Producer field"; + level = "success"; + }else if(bean.kind.toLowerCase() === "method"){ + kind = "Producer method"; + level = "success"; + }else if(bean.kind.toLowerCase() === "synthetic"){ + level = "contrast"; + } + + return html` + ${level + ? html`${kind}` + : html`${kind}` + }`; + } + + _kindClassRenderer(bean){ + return html` + ${bean.declaringClass + ? html`${bean.declaringClass.simpleName}.${bean.memberName}()` + : html`${bean.memberName}` + } + `; + } + + _interceptorsRenderer(bean) { + if (bean.interceptors && bean.interceptors.length > 0) { + return html` + ${bean.interceptorInfos.map(interceptor => + html`
+ ${interceptor.interceptorClass.name} + ${interceptor.priority} +
` + )} +
`; + } + } + + _qualifierRenderer(qualifier) { + return html`${qualifier.simpleName}`; + } + + _camelize(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { + if (+match === 0) + return ""; + return index === 0 ? match.toUpperCase() : match.toLowerCase(); + }); + } +} +customElements.define('qwc-arc-beans', QwcArcBeans); \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-decorators.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-decorators.js new file mode 100644 index 0000000000000..98dab06b8f02b --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-decorators.js @@ -0,0 +1,33 @@ +import { LitElement, html, css} from 'lit'; +import { decorators } from 'arc-data'; +/** + * This component shows the Arc Decorators + */ +export class QwcArcDecorators extends LitElement { + + static styles = css` + .todo { + font-size: small; + color: #4695EB; + padding-left: 10px; + background: white; + height: 100%; + }`; + + static properties = { + _decorators: {attribute: false}, + }; + + constructor() { + super(); + this._decorators = decorators; + } + + render() { + if (this._decorators) { + html`${this._decorators}`; + } + } + +} +customElements.define('qwc-arc-decorators', QwcArcDecorators); \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-fired-events.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-fired-events.js new file mode 100644 index 0000000000000..01c88140479c3 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-fired-events.js @@ -0,0 +1,130 @@ +import { LitElement, html, css} from 'lit'; +import { until } from 'lit/directives/until.js'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/details'; +import '@vaadin/vertical-layout'; +import '@vaadin/button'; +import '@vaadin/checkbox'; + +/** + * This component shows the Arc Fired Events + */ +export class QwcArcFiredEvents extends LitElement { + jsonRpc = new JsonRpc("ArC"); + + static styles = css` + .menubar { + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 5px; + } + .button { + background-color: transparent; + cursor: pointer; + } + .arctable { + height: 100%; + padding-bottom: 10px; + } + .payload { + color: grey; + font-size: small; + }`; + + static properties = { + _firedEvents: {state: true}, + _observer: {state:false}, + }; + + connectedCallback() { + super.connectedCallback(); + this._refresh(); + this._observer = this.jsonRpc.streamEvents().onNext(jsonRpcResponse => { + this._addToEvents(jsonRpcResponse.result); + }); + } + + disconnectedCallback() { + this._observer.cancel(); + super.disconnectedCallback(); + } + + render() { + return html`${until(this._renderFiredEvents(), html`Loading ArC fired event...`)}`; + } + + _renderFiredEvents(){ + if(this._firedEvents){ + return html` + + + + + + + + + + + `; + } + } + + _payloadRenderer(event) { + return html` + +
${event.type}
+ + + ${event.payload} + +
+ `; + } + + _refresh(){ + console.log("refresh"); + this.jsonRpc.getLastEvents().then(events => { + this._firedEvents = events.result; + }); + } + + _clear(){ + console.log("clear"); + this.jsonRpc.clearLastEvents().then(events => { + this._firedEvents = events.result; + }); + } + + _toggleContext(){ + console.log("context"); + } + + _addToEvents(event){ + this._firedEvents = [ + ...this._firedEvents, + event, + ]; + } +} +customElements.define('qwc-arc-fired-events', QwcArcFiredEvents); \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-interceptors.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-interceptors.js new file mode 100644 index 0000000000000..bd74386125b59 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-interceptors.js @@ -0,0 +1,129 @@ +import { LitElement, html, css} from 'lit'; +import { interceptors } from 'arc-data'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid'; +import '@vaadin/vertical-layout'; +import 'qui-badge'; + +/** + * This component shows the Arc Interceptors + */ +export class QwcArcInterceptors extends LitElement { + + static styles = css` + .arctable { + height: 100%; + padding-bottom: 10px; + } + + code { + font-size: 85%; + } + + .method { + color: var(--lumo-primary-text-color); + } + + .annotation { + color: var(--lumo-contrast-50pct); + } + `; + + static properties = { + _interceptors: {attribute: false} + }; + + constructor() { + super(); + this._interceptors = interceptors; + } + + render() { + if(this._interceptors){ + return html` + + + + + + + + + + + + + + `; + } + } + + _classRenderer(bean){ + return html` + ${bean.interceptorClass.name} + `; + } + + _priorityRenderer(bean){ + return html` + ${bean.priority} + `; + } + + _bindingsRenderer(bean){ + return html` + + ${bean.bindings.map(binding=> + html`${binding.simpleName}` + )} + `; + } + + _typeRenderer(bean){ + let i = JSON.stringify(bean.intercepts); + + const typeTemplates = []; + for (const [key, value] of Object.entries(bean.intercepts)) { + typeTemplates.push(html`${this._printIntercepterType(key)}`); + } + + return html` + + ${typeTemplates} +
+ ${bean.interceptorClass.simpleName} + #${bean.methodName}() +
+
`; + } + + _printIntercepterType(str){ + const p = str.split("_"); + let f = "@"; + p.forEach(w => { + f = f + this._camelize(w); + }); + return f; + } + + _camelize(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { + if (+match === 0) + return ""; + return index === 0 ? match.toUpperCase() : match.toLowerCase(); + }); + } +} +customElements.define('qwc-arc-interceptors', QwcArcInterceptors); \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-invocation-trees.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-invocation-trees.js new file mode 100644 index 0000000000000..6a9f9d678676b --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-invocation-trees.js @@ -0,0 +1,84 @@ +import { LitElement, html, css} from 'lit'; +import { until } from 'lit/directives/until.js'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-tree-column.js'; +import '@vaadin/button'; +import '@vaadin/checkbox'; + +/** + * This component shows the Arc Invocation Trees + */ +export class QwcArcInvocationTrees extends LitElement { + jsonRpc = new JsonRpc("ArC"); + + static styles = css` + .menubar { + display: flex; + justify-content: flex-start; + align-items: center; + padding-left: 5px; + } + .button { + background-color: transparent; + cursor: pointer; + } + .arctable { + height: 100%; + padding-bottom: 10px; + }`; + + static properties = { + _invocations: {state: true} + }; + + connectedCallback() { + super.connectedCallback(); + this._refresh(); + } + + render() { + return html`${until(this._renderInvocations(), html`Loading ArC invocation trees...`)}`; + } + + _renderInvocations(){ + if(this._invocations){ + return html` + + + + + `; + } + } + + _refresh(){ + console.log("refresh"); + this.jsonRpc.getLastInvocations().then(invocations => { + this._invocations = invocations.result; + }); + } + + _clear(){ + console.log("clear"); + this.jsonRpc.clearLastInvocations().then(invocations => { + this._invocations = invocations.result; + }); + } + + _toggleFilter(){ + console.log("filter"); + } +} +customElements.define('qwc-arc-invocation-trees', QwcArcInvocationTrees); \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-observers.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-observers.js new file mode 100644 index 0000000000000..a73d6a3172f22 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-observers.js @@ -0,0 +1,147 @@ +import { LitElement, html, css} from 'lit'; +import { observers } from 'arc-data'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid'; +import '@vaadin/vertical-layout'; +import 'qui-badge'; + +/** + * This component shows the Arc Observers + */ +export class QwcArcObservers extends LitElement { + + static styles = css` + .arctable { + height: 100%; + padding-bottom: 10px; + } + + code { + font-size: 85%; + } + + .text { + font-size: 85%; + } + + .method { + color: var(--lumo-primary-text-color); + } + + .annotation { + color: var(--lumo-contrast-50pct); + } + `; + + static properties = { + _observers: {attribute: false} + }; + + constructor() { + super(); + this._observers = observers; + } + + render() { + if(this._observers){ + + return html` + + + + + + + + + + + + + + + + + + + + + `; + } + } + + _sourceRenderer(bean){ + return html` + ${bean.declaringClass.name}#${bean.methodName}() + `; + } + + _typeRenderer(bean){ + return html` + ${bean.qualifiers.map(qualifier=> + html`${this._qualifierRenderer(qualifier)}` + )} + ${bean.observedType.name} + `; + } + + _qualifierRenderer(qualifier){ + if(qualifier){ + return html`${qualifier.simpleName}`; + } + } + + _priorityRenderer(bean){ + return html` + ${bean.priority} + `; + } + + _receptionRenderer(bean){ + return html` + ${this._camelize(bean.reception)} + `; + } + + _transactionPhaseRenderer(bean){ + return html` + ${this._camelize(bean.transactionPhase)} + `; + } + + _asyncRenderer(bean){ + if(bean.async !== false){ + return html` + + `; + } + } + + _camelize(str) { + const s = str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match, index) { + if (+match === 0) return ""; + return index === 0 ? match.toUpperCase() : match.toLowerCase(); + }); + + return s.replaceAll('_', ' '); + } +} +customElements.define('qwc-arc-observers', QwcArcObservers); \ No newline at end of file diff --git a/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-removed-components.js b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-removed-components.js new file mode 100644 index 0000000000000..2dcbf60c64aa1 --- /dev/null +++ b/extensions/arc/deployment/src/main/resources/dev-ui/arc/qwc-arc-removed-components.js @@ -0,0 +1,204 @@ +import { LitElement, html, css} from 'lit'; + +import '@vaadin/tabs'; +import '@vaadin/tabsheet'; +import '@vaadin/grid'; +import '@vaadin/vertical-layout'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import 'qui-badge'; +import { removedBeans } from 'arc-data'; +import { removedDecorators } from 'arc-data'; +import { removedInterceptors } from 'arc-data'; + +/** + * This component shows the Arc RemovedComponents + */ +export class QwcArcRemovedComponents extends LitElement { + static styles = css` + .fullHeight { + height: 100%; + } + code { + font-size: 85%; + } + + .annotation { + color: var(--lumo-contrast-50pct); + } + + .producer { + color: var(--lumo-primary-text-color); + } + `; + + static properties = { + _removedBeans: {state: true}, + _removedDecorators: {state: true}, + _removedInterceptors: {state: true}, + }; + + constructor() { + super(); + this._removedBeans = removedBeans; + this._removedDecorators = removedDecorators; + this._removedInterceptors = removedInterceptors; + } + + render() { + return html` + + + + Removed beans + ${this._removedBeans.length} + + + Removed decorators + ${this._removedDecorators.length} + + + Removed interceptors + ${this._removedInterceptors.length} + + + +
${this._renderRemovedBeans()}
+
${this._renderRemovedDecorators()}
+
${this._renderRemovedInterceptors()}
+
+ `; + } + + _renderRemovedBeans(){ + + if (this._removedBeans.length > 0) { + + return html` + + + + + + + `; + + } else { + return html`No beans removed`; + } + } + + _renderRemovedDecorators(){ + if (this._removedDecorators.length > 0) { + return html`TODO: Not yet implemented`; + + } else { + return html`No decorators removed`; + } + } + + _renderRemovedInterceptors(){ + if (this._removedInterceptors.length > 0) { + return html` + + + + + + + `; + + } else { + return html`No interceptors removed`; + } + } + + _interceptorRenderer(bean) { + return html`${this._nameRenderer(bean.interceptorClass)}`; + } + + _bindingsRenderer(bean) { + return html` + ${bean.bindings.map(binding => + html`${this._simpleNameRenderer(binding)}` + )} + `; + } + + _beanRenderer(bean) { + return html` + @${bean.scope.simpleName} + ${bean.nonDefaultQualifiers.map(qualifier => + html`${this._simpleNameRenderer(qualifier)}` + )} + ${bean.providerType.name} + `; + } + + _kindRenderer(bean) { + return html` + + ${this._kindBadgeRenderer(bean)} + ${this._kindClassRenderer(bean)} + + `; + } + + _kindBadgeRenderer(bean){ + let kind = this._camelize(bean.kind); + let level = null; + + if(bean.kind.toLowerCase() === "field"){ + kind = "Producer field"; + level = "success"; + }else if(bean.kind.toLowerCase() === "method"){ + kind = "Producer method"; + level = "success"; + }else if(bean.kind.toLowerCase() === "synthetic"){ + level = "contrast"; + } + + return html` + ${level + ? html`${kind}` + : html`${kind}` + }`; + } + + _kindClassRenderer(bean){ + return html` + ${bean.declaringClass + ? html`${bean.declaringClass.simpleName}.${bean.memberName}()` + : html`${bean.memberName}` + } + `; + } + + _simpleNameRenderer(name) { + return html`${name.simpleName}`; + } + + _nameRenderer(name) { + return html`${name.name}`; + } + + _camelize(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { + if (+match === 0) + return ""; + return index === 0 ? match.toUpperCase() : match.toLowerCase(); + }); + } +} +customElements.define('qwc-arc-removed-components', QwcArcRemovedComponents); \ No newline at end of file diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java new file mode 100644 index 0000000000000..fde1f35d48f42 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventInfo.java @@ -0,0 +1,67 @@ +package io.quarkus.arc.runtime.devmode; + +import java.util.List; + +public class EventInfo { + private String timestamp; + private String type; + private List qualifiers; + private boolean isContextEvent; + private Object payload; + + public EventInfo() { + } + + public EventInfo(String timestamp, String type, List qualifiers, boolean isContextEvent) { + this(timestamp, type, qualifiers, isContextEvent, null); + } + + public EventInfo(String timestamp, String type, List qualifiers, boolean isContextEvent, Object payload) { + this.timestamp = timestamp; + this.type = type; + this.qualifiers = qualifiers; + this.isContextEvent = isContextEvent; + this.payload = payload; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getQualifiers() { + return qualifiers; + } + + public void setQualifiers(List qualifiers) { + this.qualifiers = qualifiers; + } + + public boolean isIsContextEvent() { + return isContextEvent; + } + + public void setIsContextEvent(boolean isContextEvent) { + this.isContextEvent = isContextEvent; + } + + public Object getPayload() { + return payload; + } + + public void setPayload(Object payload) { + this.payload = payload; + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java similarity index 59% rename from extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java rename to extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java index e5e1e7298a1f6..2d85c7db162fa 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devconsole/EventsMonitor.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/EventsMonitor.java @@ -1,8 +1,9 @@ -package io.quarkus.arc.runtime.devconsole; +package io.quarkus.arc.runtime.devmode; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -16,13 +17,17 @@ import jakarta.enterprise.inject.spi.EventMetadata; import jakarta.inject.Singleton; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; + @Singleton public class EventsMonitor { private static final int DEFAULT_LIMIT = 500; private volatile boolean skipContextEvents = true; - private final List events = Collections.synchronizedList(new ArrayList(DEFAULT_LIMIT)); + private final List events = Collections.synchronizedList(new ArrayList<>(DEFAULT_LIMIT)); + private final BroadcastProcessor eventsStream = BroadcastProcessor.create(); void notify(@Observes Object payload, EventMetadata eventMetadata) { if (skipContextEvents && isContextEvent(eventMetadata)) { @@ -36,13 +41,19 @@ void notify(@Observes Object payload, EventMetadata eventMetadata) { } } } - events.add(EventInfo.from(eventMetadata)); + EventInfo eventInfo = toEventInfo(payload, eventMetadata); + eventsStream.onNext(eventInfo); + events.add(eventInfo); } public void clear() { events.clear(); } + public Multi streamEvents() { + return eventsStream; + } + public List getLastEvents() { List result = new ArrayList<>(events); Collections.reverse(result); @@ -74,47 +85,40 @@ boolean isContextEvent(EventMetadata eventMetadata) { return true; } - static class EventInfo { - - static EventInfo from(EventMetadata eventMetadata) { - List qualifiers; - if (eventMetadata.getQualifiers().size() == 1) { - // Just @Any - qualifiers = Collections.emptyList(); - } else { - qualifiers = new ArrayList<>(1); - for (Annotation qualifier : eventMetadata.getQualifiers()) { - // Skip @Any and @Default - if (!qualifier.annotationType().equals(Any.class) && !qualifier.annotationType().equals(Default.class)) { - qualifiers.add(qualifier); - } - } - } - return new EventInfo(eventMetadata.getType(), qualifiers); - } + private EventInfo toEventInfo(Object payload, EventMetadata eventMetadata) { + EventInfo eventInfo = new EventInfo(); - private final LocalDateTime timestamp; - private final Type type; - private final List qualifiers; + // Timestamp + eventInfo.setTimestamp(now()); - EventInfo(Type type, List qualifiers) { - this.timestamp = LocalDateTime.now(); - this.type = type; - this.qualifiers = qualifiers; - } + // Type + eventInfo.setType(eventMetadata.getType().getTypeName()); - public LocalDateTime getTimestamp() { - return timestamp; + // Qualifiers + List q = new ArrayList<>(); + if (eventMetadata.getQualifiers().size() > 1) { + for (Annotation qualifier : eventMetadata.getQualifiers()) { + // Skip @Any and @Default + if (!qualifier.annotationType().equals(Any.class) && !qualifier.annotationType().equals(Default.class)) { + q.add(qualifier.toString()); + } + } } + eventInfo.setQualifiers(q); - public String getType() { - return type.getTypeName(); - } + // ContextEvent + eventInfo.setIsContextEvent(isContextEvent(eventMetadata)); - public List getQualifiers() { - return qualifiers; - } + // Payload + eventInfo.setPayload(payload.toString()); + + return eventInfo; + } + private String now() { + LocalDateTime time = LocalDateTime.now(); + String timestamp = time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).replace("T", " "); + return timestamp.substring(0, timestamp.lastIndexOf(".")); } } diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java new file mode 100644 index 0000000000000..e681fd05e0af3 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devmode/InvocationInfo.java @@ -0,0 +1,15 @@ +package io.quarkus.arc.runtime.devmode; + +public class InvocationInfo { + + private String startTime; + + public String getStartTime() { + return startTime; + } + + public void setStartTime(String startTime) { + this.startTime = startTime; + } + +} diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java new file mode 100644 index 0000000000000..f3f4143017da5 --- /dev/null +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/devui/ArcJsonRPCService.java @@ -0,0 +1,82 @@ +package io.quarkus.arc.runtime.devui; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.runtime.devconsole.Invocation; +import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; +import io.quarkus.arc.runtime.devmode.EventInfo; +import io.quarkus.arc.runtime.devmode.EventsMonitor; +import io.quarkus.arc.runtime.devmode.InvocationInfo; +import io.smallrye.mutiny.Multi; + +public class ArcJsonRPCService { + + public Multi streamEvents() { + EventsMonitor eventsMonitor = Arc.container().instance(EventsMonitor.class).get(); + if (eventsMonitor != null) { + return eventsMonitor.streamEvents(); + } + return Multi.createFrom().empty(); + } + + public List getLastEvents() { + EventsMonitor eventsMonitor = Arc.container().instance(EventsMonitor.class).get(); + if (eventsMonitor != null) { + return eventsMonitor.getLastEvents(); + } + return List.of(); + } + + public List clearLastEvents() { + EventsMonitor eventsMonitor = Arc.container().instance(EventsMonitor.class).get(); + if (eventsMonitor != null) { + eventsMonitor.clear(); + return eventsMonitor.getLastEvents(); + } + return List.of(); + } + + public List getLastInvocations() { + InvocationsMonitor invocationsMonitor = Arc.container().instance(InvocationsMonitor.class).get(); + if (invocationsMonitor != null) { + List lastInvocations = invocationsMonitor.getLastInvocations(); + return toInvocationInfos(lastInvocations); + } + return List.of(); + } + + public List clearLastInvocations() { + InvocationsMonitor invocationsMonitor = Arc.container().instance(InvocationsMonitor.class).get(); + if (invocationsMonitor != null) { + invocationsMonitor.clear(); + return getLastInvocations(); + } + return List.of(); + } + + private List toInvocationInfos(List invocations) { + List infos = new ArrayList<>(); + for (Invocation invocation : invocations) { + infos.add(toInvocationInfo(invocation)); + } + return infos; + } + + private InvocationInfo toInvocationInfo(Invocation invocation) { + InvocationInfo info = new InvocationInfo(); + LocalDateTime starttime = LocalDateTime.ofInstant(Instant.ofEpochMilli(invocation.getStart()), ZoneId.systemDefault()); + info.setStartTime(timeString(starttime)); + return info; + } + + private String timeString(LocalDateTime time) { + String timestamp = time.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME).replace("T", " "); + return timestamp.substring(0, timestamp.lastIndexOf(".")); + } +} diff --git a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/SimpleRouteTest.java b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/SimpleRouteTest.java index b6bd6cbad75dc..2e737053d3a07 100644 --- a/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/SimpleRouteTest.java +++ b/extensions/reactive-routes/deployment/src/test/java/io/quarkus/vertx/web/SimpleRouteTest.java @@ -45,7 +45,7 @@ public void testSimpleRoute() { when().get("/hello-event-bus?name=ping").then().statusCode(200).body(is("Hello PING!")); when().get("/foo?name=foo").then().statusCode(200).body(is("Hello foo!")); when().get("/bar").then().statusCode(200).body(is("Hello bar!")); - when().get("/delete").then().statusCode(405); + when().post("/delete").then().statusCode(405); when().delete("/delete").then().statusCode(200).body(is("deleted")); when().get("/routes").then().statusCode(200) .body(Matchers.containsString("/hello-event-bus")); diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/ClasspathResourceTestCase.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/ClasspathResourceTestCase.java index 8396947a9d218..57477f8e100e7 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/ClasspathResourceTestCase.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/ClasspathResourceTestCase.java @@ -1,6 +1,7 @@ package io.quarkus.resteasy.test; -import org.hamcrest.Matchers; +import static org.hamcrest.Matchers.is; + import org.jboss.shrinkwrap.api.asset.StringAsset; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -19,10 +20,26 @@ public class ClasspathResourceTestCase { @Test public void testRootResource() { - RestAssured.when().get("/other/hello.txt").then().body(Matchers.is("hello")); - RestAssured.when().get("/stuff.html").then().body(Matchers.is("stuff")); - RestAssured.when().get("/index.html").then().body(Matchers.is("index")); - RestAssured.when().get("/").then().body(Matchers.is("index")); + + RestAssured.get("/other/hello.txt").then() + .log().all() + .statusCode(200) + .body(is("hello")); + + RestAssured.get("/stuff.html").then() + .log().all() + .statusCode(200) + .body(is("stuff")); + + RestAssured.get("/index.html").then() + .log().all() + .statusCode(200) + .body(is("index")); + + RestAssured.get("/").then() + .log().all() + .statusCode(200) + .body(is("index")); } } diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/files/StaticFileWithResourcesHttpRootTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/files/StaticFileWithResourcesHttpRootTest.java index 50956915d8707..b9fbe46dd8cc6 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/files/StaticFileWithResourcesHttpRootTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/files/StaticFileWithResourcesHttpRootTest.java @@ -29,14 +29,17 @@ public class StaticFileWithResourcesHttpRootTest { public void test() { RestAssured.get("/web/index.html").then() + .log().all() .statusCode(200) .body(containsString("

Hello

")); RestAssured.get("/lorem.txt").then() + .log().all() .statusCode(200) .body(containsString("Lorem")); RestAssured.get("/").then() + .log().all() .statusCode(200) .body(containsString("Root Resource")); } diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java index d85d703202e53..a0f1e2f5da713 100644 --- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java +++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/NotFoundExceptionMapper.java @@ -355,7 +355,7 @@ public void accept(java.nio.file.Path path) { } //limit to 1000 to not have to many files to display - return knownFiles.stream().filter(this::isHtmlFileName).limit(1000).distinct().sorted(Comparator.naturalOrder()) + return knownFiles.stream().filter(this::isValidHtmlFileName).limit(1000).distinct().sorted(Comparator.naturalOrder()) .collect(Collectors.toList()); } @@ -377,7 +377,12 @@ public FileVisitResult visitFile(java.nio.file.Path p, BasicFileAttributes attrs } } - private boolean isHtmlFileName(String fileName) { + private boolean isValidHtmlFileName(String fileName) { + // Filter out webjars and mvnpm + if (fileName.startsWith("webjars") || fileName.startsWith("_static")) { + return false; + } + return fileName.endsWith(".html") || fileName.endsWith(".htm"); } diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java new file mode 100644 index 0000000000000..eb07c0c2df213 --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/devui/SmallRyeGraphQLDevUIProcessor.java @@ -0,0 +1,36 @@ +package io.quarkus.smallrye.graphql.deployment.devui; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.devui.spi.page.PageBuilder; +import io.quarkus.smallrye.graphql.runtime.SmallRyeGraphQLConfig; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; + +public class SmallRyeGraphQLDevUIProcessor { + + SmallRyeGraphQLConfig graphQLConfig; + + @BuildStep(onlyIf = IsDevelopment.class) + CardPageBuildItem createCard(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem("SmallRye GraphQL"); + + // Generated GraphQL Schema + PageBuilder schemaPage = Page.externalPageBuilder("GraphQL Schema") + .icon("font-awesome-solid:diagram-project") + .url("/" + graphQLConfig.rootPath + "/schema.graphql"); + + // GraphiQL UI + String uiPath = nonApplicationRootPathBuildItem.resolvePath(graphQLConfig.ui.rootPath); + PageBuilder uiPage = Page.externalPageBuilder("GraphQL UI") + .icon("font-awesome-solid:table-columns") + .url(uiPath); + + cardPageBuildItem.addPage(schemaPage); + cardPageBuildItem.addPage(uiPage); + + return cardPageBuildItem; + } +} \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/pom.xml b/extensions/smallrye-openapi/deployment/pom.xml index 913b972a150ac..3c781f831bee5 100644 --- a/extensions/smallrye-openapi/deployment/pom.xml +++ b/extensions/smallrye-openapi/deployment/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-vertx-http-deployment
+ + io.quarkus + quarkus-vertx-http-dev-ui-spi + io.quarkus quarkus-smallrye-openapi-spi diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java new file mode 100644 index 0000000000000..d14c763217d25 --- /dev/null +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/devui/OpenApiDevUIProcessor.java @@ -0,0 +1,36 @@ +package io.quarkus.smallrye.openapi.deployment.devui; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; + +public class OpenApiDevUIProcessor { + + private static final String NAME = "Smallrye Openapi"; + + @BuildStep(onlyIf = IsDevelopment.class) + public CardPageBuildItem pages(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(NAME); + + cardPageBuildItem.addPage(Page.externalPageBuilder("Schema yaml") + .url(nonApplicationRootPathBuildItem.resolvePath("openapi")) + .isYamlContent() + .icon("font-awesome-solid:file-lines")); + + cardPageBuildItem.addPage(Page.externalPageBuilder("Schema json") + .url(nonApplicationRootPathBuildItem.resolvePath("openapi") + "?format=json") + .isJsonContent() + .icon("font-awesome-solid:file-code")); + + cardPageBuildItem.addPage(Page.externalPageBuilder("Swagger UI") + .url(nonApplicationRootPathBuildItem.resolvePath("swagger-ui")) + .isHtmlContent() + .icon("font-awesome-solid:signs-post")); + + return cardPageBuildItem; + } + +} diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index bc6f99b58d5db..444f15fd3dbd1 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -33,6 +33,21 @@ io.quarkus quarkus-kubernetes-spi + + + + io.quarkus + quarkus-vertx-http-dev-ui-spi + + + io.quarkus + quarkus-vertx-http-dev-ui-resources + + + org.mvnpm + importmap + + io.quarkus.qute diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java new file mode 100644 index 0000000000000..cdaa579589aea --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java @@ -0,0 +1,35 @@ +package io.quarkus.devui.deployment; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.devui.spi.AbstractDevUIBuildItem; + +/** + * Write javascript file containing const vars with build time data + */ +public final class BuildTimeConstBuildItem extends AbstractDevUIBuildItem { + + private final Map buildTimeData; + + public BuildTimeConstBuildItem(String extensionName) { + this(extensionName, new HashMap<>()); + } + + public BuildTimeConstBuildItem(String extensionName, Map buildTimeData) { + super(extensionName); + this.buildTimeData = buildTimeData; + } + + public void addBuildTimeData(String fieldName, Object fieldData) { + this.buildTimeData.put(fieldName, fieldData); + } + + public Map getBuildTimeData() { + return this.buildTimeData; + } + + public boolean hasBuildTimeData() { + return this.buildTimeData != null && !this.buildTimeData.isEmpty(); + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuiltTimeContentProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuiltTimeContentProcessor.java new file mode 100644 index 0000000000000..2b282c92aef33 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuiltTimeContentProcessor.java @@ -0,0 +1,532 @@ +package io.quarkus.devui.deployment; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.mvnpm.importmap.Aggregator; +import org.mvnpm.importmap.Location; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkus.builder.Version; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.util.IoUtil; +import io.quarkus.devui.deployment.extension.Extension; +import io.quarkus.devui.deployment.extension.ExtensionGroup; +import io.quarkus.devui.deployment.spi.DevUIContent; +import io.quarkus.devui.spi.AbstractDevUIBuildItem; +import io.quarkus.devui.spi.buildtime.QuteTemplateBuildItem; +import io.quarkus.devui.spi.buildtime.StaticContentBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.MenuPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.devui.spi.page.PageBuilder; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; + +/** + * This creates static content that is used in dev UI. For example the index.html and any other data (json) available on build + * time + */ +public class BuiltTimeContentProcessor { + private static final String SLASH = "/"; + private static final String DEV_UI = "dev-ui"; + private static final String BUILD_TIME_PATH = "dev-ui-templates" + File.separator + "build-time"; + + final Config config = ConfigProvider.getConfig(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Here we create references to internal dev ui files so that they can be imported by ref. + * This will be merged into the final importmap + */ + @BuildStep(onlyIf = IsDevelopment.class) + InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem) { + + String contextRoot = nonApplicationRootPathBuildItem.getNonApplicationRootPath() + DEV_UI + SLASH; + + InternalImportMapBuildItem internalImportMapBuildItem = new InternalImportMapBuildItem(); + + internalImportMapBuildItem.add("devui/", contextRoot + "/"); + internalImportMapBuildItem.add("qwc/", contextRoot + "qwc/"); + internalImportMapBuildItem.add("qui/", contextRoot + "qui/"); + internalImportMapBuildItem.add("qui-badge", contextRoot + "qui/qui-badge.js"); + internalImportMapBuildItem.add("icon/", contextRoot + "icon/"); + internalImportMapBuildItem.add("font/", contextRoot + "font/"); + internalImportMapBuildItem.add("controller/", contextRoot + "controller/"); + internalImportMapBuildItem.add("log-controller", contextRoot + "controller/log-controller.js"); + internalImportMapBuildItem.add("router-controller", contextRoot + "controller/router-controller.js"); + internalImportMapBuildItem.add("notifier", contextRoot + "controller/notifier.js"); + internalImportMapBuildItem.add("jsonrpc", contextRoot + "controller/jsonrpc.js"); + internalImportMapBuildItem.add("state/", contextRoot + "state/"); + internalImportMapBuildItem.add("theme-state", contextRoot + "state/theme-state.js"); + internalImportMapBuildItem.add("connection-state", contextRoot + "state/connection-state.js"); + internalImportMapBuildItem.add("devui-state", contextRoot + "state/devui-state.js"); + + return internalImportMapBuildItem; + } + + /** + * Here we map all the pages (as defined by the extensions) build time data + * + * @param pageBuildItems + * @param buildTimeConstProducer + */ + @BuildStep(onlyIf = IsDevelopment.class) + void mapPageBuildTimeData(List pageBuildItems, + BuildProducer buildTimeConstProducer) { + + for (CardPageBuildItem pageBuildItem : pageBuildItems) { + Map buildTimeData = getBuildTimeData(pageBuildItem); + if (!buildTimeData.isEmpty()) { + buildTimeConstProducer.produce( + new BuildTimeConstBuildItem(pageBuildItem.getExtensionName(), buildTimeData)); + } + } + } + + private Map getBuildTimeData(CardPageBuildItem pageBuildItem) { + Map m = new HashMap<>(); + if (pageBuildItem.hasBuildTimeData()) { + m.putAll(pageBuildItem.getBuildTimeData()); + } + + if (pageBuildItem.getOptionalCard().isPresent()) { + // Make the pages available for the custom card + List pages = new ArrayList<>(); + List pageBuilders = pageBuildItem.getPages(); + for (PageBuilder pageBuilder : pageBuilders) { + pageBuilder.extension(pageBuildItem.getExtensionName()); + pageBuilder.namespace(pageBuildItem.getExtensionName()); + pages.add(pageBuilder.build()); + } + + m.put("pages", pages); + } + return m; + } + + /** + * Here we find all build time data and make then available via a const + * + * js components can import the const with "import {constName} from '{ext}-data';" + * + * @param pageBuildItems + * @param quteTemplateProducer + * @param internalImportMapProducer + */ + @BuildStep(onlyIf = IsDevelopment.class) + void createBuildTimeConstJsTemplate( + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + List buildTimeConstBuildItems, + BuildProducer quteTemplateProducer, + BuildProducer internalImportMapProducer) { + + String contextRoot = nonApplicationRootPathBuildItem.getNonApplicationRootPath() + DEV_UI + SLASH; + + QuteTemplateBuildItem quteTemplateBuildItem = new QuteTemplateBuildItem( + QuteTemplateBuildItem.DEV_UI); + + InternalImportMapBuildItem internalImportMapBuildItem = new InternalImportMapBuildItem(); + + for (BuildTimeConstBuildItem buildTimeConstBuildItem : buildTimeConstBuildItems) { + Map data = new HashMap<>(); + if (buildTimeConstBuildItem.hasBuildTimeData()) { + for (Map.Entry pageData : buildTimeConstBuildItem.getBuildTimeData().entrySet()) { + try { + String key = pageData.getKey(); + String value = objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(pageData.getValue()); + data.put(key, value); + } catch (JsonProcessingException ex) { + ex.printStackTrace(); + } + } + } + if (!data.isEmpty()) { + Map qutedata = new HashMap<>(); + qutedata.put("buildTimeData", data); + String ref = buildTimeConstBuildItem.getExtensionPathName() + "-data"; + String file = ref + ".js"; + quteTemplateBuildItem.add("build-time-data.js", file, qutedata); + internalImportMapBuildItem.add(ref, contextRoot + file); + } + } + + quteTemplateProducer.produce(quteTemplateBuildItem); + internalImportMapProducer.produce(internalImportMapBuildItem); + } + + /** + * Here we find all the mvnpm jars + */ + @BuildStep(onlyIf = IsDevelopment.class) + void gatherMvnpmJars(BuildProducer mvnpmProducer, CurateOutcomeBuildItem curateOutcomeBuildItem) { + Set mvnpmJars = new HashSet<>(); + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + try { + Enumeration jarsWithImportMaps = tccl.getResources(Location.IMPORTMAP_PATH); + Set jarUrls = new HashSet(Collections.list(jarsWithImportMaps)); + for (URL jarUrl : jarUrls) { + final JarURLConnection connection = (JarURLConnection) jarUrl.openConnection(); + mvnpmJars.add(connection.getJarFileURL()); + } + mvnpmProducer.produce(new MvnpmBuildItem(mvnpmJars)); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + /** + * Here we create index.html + * We aggregate all import maps into one + * This includes import maps from 3rd party libs from mvnpm.org and internal ones defined above + * + * @return The QuteTemplate Build item that will create the end result + */ + @BuildStep(onlyIf = IsDevelopment.class) + QuteTemplateBuildItem createIndexHtmlTemplate( + MvnpmBuildItem mvnpmBuildItem, + ThemeVarsBuildItem themeVarsBuildItem, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + List internalImportMapBuildItems) { + QuteTemplateBuildItem quteTemplateBuildItem = new QuteTemplateBuildItem( + QuteTemplateBuildItem.DEV_UI); + + Aggregator aggregator = new Aggregator(mvnpmBuildItem.getMvnpmJars()); + + for (InternalImportMapBuildItem importMapBuildItem : internalImportMapBuildItems) { + Map importMap = importMapBuildItem.getImportMap(); + aggregator.addMappings(importMap); + } + String importmap = aggregator.aggregateAsJson(); + aggregator.reset(); + + String themeVars = themeVarsBuildItem.getTemplateValue(); + String contextRoot = nonApplicationRootPathBuildItem.getNonApplicationRootPath() + DEV_UI + SLASH; + + // TODO: Move version and name to build time data + + Map data = Map.of( + "contextRoot", contextRoot, + "importmap", importmap, + "themeVars", themeVars); + + quteTemplateBuildItem.add("index.html", data); + + return quteTemplateBuildItem; + } + + // Here load all templates + @BuildStep(onlyIf = IsDevelopment.class) + void loadAllBuildTimeTemplates(BuildProducer buildTimeContentProducer, + List templates) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + for (QuteTemplateBuildItem template : templates) { + + List contentPerExtension = new ArrayList<>(); + + if (template.isInternal()) { + List templatesWithData = template.getTemplateDatas(); + for (QuteTemplateBuildItem.TemplateData e : templatesWithData) { + + String templateName = e.getTemplateName(); // Relative to BUILD_TIME_PATH + Map data = e.getData(); + String resourceName = BUILD_TIME_PATH + File.separator + templateName; + String fileName = e.getFileName(); + // TODO: What if we find more than one ? + try (InputStream templateStream = cl.getResourceAsStream(resourceName)) { + if (templateStream != null) { + byte[] templateContent = IoUtil.readBytes(templateStream); + // Internal runs on "naked" namespace + DevUIContent content = DevUIContent.builder() + .fileName(fileName) + .template(templateContent) + .addData(data) + .build(); + contentPerExtension.add(content); + } + } catch (IOException ioe) { + throw new UncheckedIOException("An error occurred while processing " + resourceName, ioe); + } + } + buildTimeContentProducer.produce(new StaticContentBuildItem( + StaticContentBuildItem.DEV_UI, contentPerExtension)); + } + } + } + + /** + * Creates json data that is available in Javascript + */ + @BuildStep(onlyIf = IsDevelopment.class) + void createBuildTimeData(BuildProducer buildTimeConstProducer, + BuildProducer themeVarsProducer, + ExtensionsBuildItem extensionsBuildItem, + List menuPageBuildItems) { + + BuildTimeConstBuildItem internalBuildTimeData = new BuildTimeConstBuildItem(AbstractDevUIBuildItem.DEV_UI); + + // Theme details TODO: Allow configuration + Map> themes = new HashMap<>(); + Map dark = new HashMap<>(); + Map light = new HashMap<>(); + + // Quarkus logo colors + light.put("--quarkus-blue", QUARKUS_BLUE.toString()); + dark.put("--quarkus-blue", QUARKUS_BLUE.toString()); + + light.put("--quarkus-red", QUARKUS_RED.toString()); + dark.put("--quarkus-red", QUARKUS_RED.toString()); + + light.put("--quarkus-center", QUARKUS_DARK.toString()); + dark.put("--quarkus-center", QUARKUS_LIGHT.toString()); + + // Vaadin's Lumo (see https://vaadin.com/docs/latest/styling/lumo/design-tokens/color) + + // Base + light.put("--lumo-base-color", Color.from(0, 100, 100).toString()); + dark.put("--lumo-base-color", Color.from(210, 10, 23).toString()); + + // Grayscale + light.put("--lumo-contrast-5pct", Color.from(214, 61, 25, 0.05).toString()); + dark.put("--lumo-contrast-5pct", Color.from(214, 65, 85, 0.06).toString()); + light.put("--lumo-contrast-10pct", Color.from(214, 57, 24, 0.1).toString()); + dark.put("--lumo-contrast-10pct", Color.from(214, 60, 80, 0.14).toString()); + light.put("--lumo-contrast-20pct", Color.from(214, 53, 23, 0.16).toString()); + dark.put("--lumo-contrast-20pct", Color.from(214, 64, 82, 0.23).toString()); + light.put("--lumo-contrast-30pct", Color.from(214, 50, 22, 0.26).toString()); + dark.put("--lumo-contrast-30pct", Color.from(214, 69, 84, 0.32).toString()); + light.put("--lumo-contrast-40pct", Color.from(214, 47, 21, 0.38).toString()); + dark.put("--lumo-contrast-40pct", Color.from(214, 73, 86, 0.41).toString()); + light.put("--lumo-contrast-50pct", Color.from(214, 45, 20, 0.52).toString()); + dark.put("--lumo-contrast-50pct", Color.from(214, 78, 88, 0.50).toString()); + light.put("--lumo-contrast-60pct", Color.from(214, 43, 19, 0.6).toString()); + dark.put("--lumo-contrast-60pct", Color.from(214, 82, 90, 0.6).toString()); + light.put("--lumo-contrast-70pct", Color.from(214, 42, 18, 0.69).toString()); + dark.put("--lumo-contrast-70pct", Color.from(214, 87, 92, 0.7).toString()); + light.put("--lumo-contrast-80pct", Color.from(214, 41, 17, 0.83).toString()); + dark.put("--lumo-contrast-80pct", Color.from(214, 91, 94, 0.8).toString()); + light.put("--lumo-contrast-90pct", Color.from(214, 40, 16, 0.94).toString()); + dark.put("--lumo-contrast-90pct", Color.from(214, 96, 96, 0.9).toString()); + light.put("--lumo-contrast", Color.from(214, 35, 15).toString()); + dark.put("--lumo-contrast", Color.from(214, 100, 98).toString()); + + // Primary + light.put("--lumo-primary-color-10pct", Color.from(214, 100, 60, 0.13).toString()); + dark.put("--lumo-primary-color-10pct", Color.from(214, 90, 63, 0.1).toString()); + light.put("--lumo-primary-color-50pct", Color.from(QUARKUS_BLUE, 0.76).toString()); + dark.put("--lumo-primary-color-50pct", Color.from(QUARKUS_BLUE, 0.5).toString()); + light.put("--lumo-primary-color", QUARKUS_BLUE.toString()); + dark.put("--lumo-primary-color", QUARKUS_BLUE.toString()); + light.put("--lumo-primary-text-color", QUARKUS_BLUE.toString()); + dark.put("--lumo-primary-text-color", QUARKUS_BLUE.toString()); + light.put("--lumo-primary-contrast-color", Color.from(0, 100, 100).toString()); + dark.put("--lumo-primary-contrast-color", Color.from(0, 100, 100).toString()); + + // Error + light.put("--lumo-error-color-10pct", Color.from(3, 85, 49, 0.1).toString()); + dark.put("--lumo-error-color-10pct", Color.from(3, 90, 63, 0.1).toString()); + light.put("--lumo-error-color-50pct", Color.from(3, 85, 49, 0.5).toString()); + dark.put("--lumo-error-color-50pct", Color.from(3, 90, 63, 0.5).toString()); + light.put("--lumo-error-color", Color.from(3, 85, 48).toString()); + dark.put("--lumo-error-color", Color.from(3, 90, 63).toString()); + light.put("--lumo-error-text-color", Color.from(3, 89, 42).toString()); + dark.put("--lumo-error-text-color", Color.from(3, 100, 67).toString()); + light.put("--lumo-error-contrast-color", Color.from(0, 100, 100).toString()); + dark.put("--lumo-error-contrast-color", Color.from(0, 100, 100).toString()); + + // Success + light.put("--lumo-success-color-10pct", Color.from(145, 72, 31, 0.1).toString()); + dark.put("--lumo-success-color-10pct", Color.from(145, 65, 42, 0.1).toString()); + light.put("--lumo-success-color-50pct", Color.from(145, 72, 31, 0.5).toString()); + dark.put("--lumo-success-color-50pct", Color.from(145, 65, 42, 0.5).toString()); + light.put("--lumo-success-color", Color.from(145, 72, 30).toString()); + dark.put("--lumo-success-color", Color.from(145, 65, 42).toString()); + light.put("--lumo-success-text-color", Color.from(145, 85, 25).toString()); + dark.put("--lumo-success-text-color", Color.from(145, 85, 47).toString()); + light.put("--lumo-success-contrast-color", Color.from(0, 100, 100).toString()); + dark.put("--lumo-success-contrast-color", Color.from(0, 100, 100).toString()); + + // Text + light.put("--lumo-header-text-color", Color.from(214, 35, 15).toString()); + dark.put("--lumo-header-text-color", Color.from(214, 100, 98).toString()); + light.put("--lumo-body-text-color", Color.from(214, 40, 16, 0.94).toString()); + dark.put("--lumo-body-text-color", Color.from(214, 96, 96, 0.9).toString()); + light.put("--lumo-secondary-text-color", Color.from(214, 42, 18, 0.69).toString()); + dark.put("--lumo-secondary-text-color", Color.from(214, 87, 92, 0.7).toString()); + light.put("--lumo-tertiary-text-color", Color.from(214, 45, 20, 0.52).toString()); + dark.put("--lumo-tertiary-text-color", Color.from(214, 78, 88, 0.5).toString()); + light.put("--lumo-disabled-text-color", Color.from(214, 50, 22, 0.26).toString()); + dark.put("--lumo-disabled-text-color", Color.from(214, 69, 84, 0.32).toString()); + + themes.put("dark", dark); + themes.put("light", light); + + internalBuildTimeData.addBuildTimeData("themes", themes); + + // Extensions + Map> response = Map.of( + ExtensionGroup.active, extensionsBuildItem.getActiveExtensions(), + ExtensionGroup.inactive, extensionsBuildItem.getInactiveExtensions()); + + internalBuildTimeData.addBuildTimeData("extensions", response); + + // Sections Menu + Page extensions = Page.webComponentPageBuilder().internal() + .title("Extensions") + .icon("font-awesome-solid:puzzle-piece") + .componentLink("qwc-extensions.js").build(); + + Page configuration = Page.webComponentPageBuilder().internal() + .title("Configuration") + .icon("font-awesome-solid:sliders") + .componentLink("qwc-configuration.js").build(); + + internalBuildTimeData.addBuildTimeData("allConfiguration", "TODO: Configuration"); + + Page continuousTesting = Page.webComponentPageBuilder().internal() + .title("Continuous Testing") + .icon("font-awesome-solid:flask-vial") + .componentLink("qwc-continuous-testing.js").build(); + + internalBuildTimeData.addBuildTimeData("continuousTesting", "TODO: Continuous Testing"); + + Page devServices = Page.webComponentPageBuilder().internal() + .title("Dev services") + .icon("font-awesome-solid:wand-magic-sparkles") + .componentLink("qwc-dev-services.js").build(); + + internalBuildTimeData.addBuildTimeData("devServices", "TODO: Dev Services"); + + Page buildSteps = Page.webComponentPageBuilder().internal() + .title("Build steps") + .icon("font-awesome-solid:hammer") + .componentLink("qwc-build-steps.js").build(); + + internalBuildTimeData.addBuildTimeData("buildSteps", "TODO: Build Steps"); + + // Add default menu items + @SuppressWarnings("unchecked") + List sectionMenu = new ArrayList(List.of(extensions, configuration, continuousTesting, devServices, buildSteps)); + + // Add any Menus from extensions + for (Extension e : extensionsBuildItem.getSectionMenuExtensions()) { + List pagesFromExtension = e.getMenuPages(); + sectionMenu.addAll(pagesFromExtension); + } + + internalBuildTimeData.addBuildTimeData("menuItems", sectionMenu); + + // Add the Footer tabs + Page serverLog = Page.webComponentPageBuilder().internal() + .title("Server") + .icon("font-awesome-solid:server") + .componentLink("qwc-server-log.js").build(); + + Page devUiLog = Page.webComponentPageBuilder().internal() + .title("Dev UI") + .icon("font-awesome-solid:satellite-dish") + .componentLink("qwc-jsonrpc-messages.js").build(); + + @SuppressWarnings("unchecked") + List footerTabs = new ArrayList(List.of(serverLog, devUiLog)); + + // Add any Footer tabs from extensions + for (Extension e : extensionsBuildItem.getFooterTabsExtensions()) { + List tabsFromExtension = e.getFooterPages(); + footerTabs.addAll(tabsFromExtension); + } + + internalBuildTimeData.addBuildTimeData("footerTabs", footerTabs); + + // Add version info + Map applicationInfo = new HashMap<>(); + applicationInfo.put("quarkusVersion", Version.getVersion()); + applicationInfo.put("applicationName", config.getOptionalValue("quarkus.application.name", String.class).orElse("")); + applicationInfo.put("applicationVersion", + config.getOptionalValue("quarkus.application.version", String.class).orElse("")); + internalBuildTimeData.addBuildTimeData("applicationInfo", applicationInfo); + + buildTimeConstProducer.produce(internalBuildTimeData); + + themeVarsProducer.produce(new ThemeVarsBuildItem(light.keySet(), QUARKUS_BLUE.toString())); + } + + private static final Color QUARKUS_BLUE = Color.from(211, 63, 54); + private static final Color QUARKUS_RED = Color.from(343, 100, 50); + private static final Color QUARKUS_DARK = Color.from(180, 36, 5); + private static final Color QUARKUS_LIGHT = Color.from(0, 0, 90); + + /** + * This represents a HSLA color + * see https://www.w3schools.com/html/html_colors_hsl.asp + */ + static class Color { + private int hue; // Defines a degree on the color wheel (from 0 to 360) - 0 (or 360) is red, 120 is green, 240 is blue + private int saturation; // Defines the saturation; 0% is a shade of gray and 100% is the full color (full saturation) + private int lightness; // Defines the lightness; 0% is black, 50% is normal, and 100% is white + private double alpha; // Defines the opacity; 0 is fully transparent, 100 is not transparent at all + + private Color(int hue, int saturation, int lightness, double alpha) { + if (hue < 0 || hue > 360) { + throw new RuntimeException( + "Invalid hue, number needs to be between 0 and 360. Defines a degree on the color wheel"); + } + this.hue = hue; + + if (saturation < 0 || saturation > 100) { + throw new RuntimeException( + "Invalid saturation, number needs to be between 0 and 100. 0% is a shade of gray and 100% is the full color (full saturation)"); + } + this.saturation = saturation; + + if (lightness < 0 || lightness > 100) { + throw new RuntimeException( + "Invalid lightness, number needs to be between 0 and 100. 0% is black, 50% is normal, and 100% is white"); + } + this.lightness = lightness; + + if (alpha < 0 || alpha > 1) { + throw new RuntimeException( + "Invalid alpha, number needs to be between 0 and 1. 0 is fully transparent, 1 is not transparent at all"); + } + this.alpha = alpha; + } + + @Override + public String toString() { + return "hsla(" + this.hue + ", " + this.saturation + "%, " + this.lightness + "%, " + this.alpha + ")"; + } + + static Color from(Color color, double alpha) { + return new Color(color.hue, color.saturation, color.lightness, alpha); + } + + static Color from(int hue, int saturation, int lightness) { + return new Color(hue, saturation, lightness, 1); + } + + static Color from(int hue, int saturation, int lightness, double alpha) { + return new Color(hue, saturation, lightness, alpha); + } + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java new file mode 100644 index 0000000000000..b34caf2b57032 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -0,0 +1,614 @@ +package io.quarkus.devui.deployment; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Modifier; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; +import org.jboss.logging.Logger; +import org.yaml.snakeyaml.Yaml; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.devui.deployment.extension.Codestart; +import io.quarkus.devui.deployment.extension.Extension; +import io.quarkus.devui.deployment.spi.DevUIContent; +import io.quarkus.devui.runtime.DevUIRecorder; +import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.buildtime.StaticContentBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.FooterPageBuildItem; +import io.quarkus.devui.spi.page.MenuPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.devui.spi.page.PageBuilder; +import io.quarkus.devui.spi.page.QuteDataPageBuilder; +import io.quarkus.maven.dependency.GACT; +import io.quarkus.maven.dependency.GACTV; +import io.quarkus.qute.Qute; +import io.quarkus.runtime.util.ClassPathUtils; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarBuildItem; +import io.quarkus.vertx.http.deployment.webjar.WebJarResultsBuildItem; +import io.smallrye.mutiny.Multi; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Create the HTTP related Dev UI API Points. + * This includes the JsonRPC Websocket endpoint and the endpoints that deliver the generated and static content. + * + * This also find all jsonrpc methods and make them available in the jsonRPC Router + */ +public class DevUIProcessor { + + private static final String DEVUI = "dev-ui"; + private static final String SLASH = "/"; + private static final String DOT = "."; + private static final String SPACE = " "; + private static final String DASH = "-"; + private static final String DOUBLE_POINT = ":"; + private static final String DASH_DEPLOYMENT = "-deployment"; + private static final String SLASH_ALL = SLASH + "*"; + private static final String JSONRPC = "json-rpc-ws"; + + private static final String CONSTRUCTOR = ""; + + private final ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + + private static final String JAR = "jar"; + private static final GACT UI_JAR = new GACT("io.quarkus", "quarkus-vertx-http-dev-ui-resources", null, JAR); + private static final String YAML_FILE = "/META-INF/quarkus-extension.yaml"; + private static final String NAME = "name"; + private static final String DESCRIPTION = "description"; + private static final String ARTIFACT = "artifact"; + private static final String METADATA = "metadata"; + private static final String KEYWORDS = "keywords"; + private static final String SHORT_NAME = "short-name"; + private static final String GUIDE = "guide"; + private static final String CATEGORIES = "categories"; + private static final String STATUS = "status"; + private static final String BUILT_WITH = "built-with-quarkus-core"; + private static final String CONFIG = "config"; + private static final String EXTENSION_DEPENDENCIES = "extension-dependencies"; + private static final String CAPABILITIES = "capabilities"; + private static final String PROVIDES = "provides"; + private static final String UNLISTED = "unlisted"; + private static final String CODESTART = "codestart"; + private static final String LANGUAGES = "languages"; + + private static final Logger log = Logger.getLogger(DevUIProcessor.class); + + @BuildStep(onlyIf = IsDevelopment.class) + @Record(ExecutionTime.STATIC_INIT) + void registerDevUiHandlers( + MvnpmBuildItem mvnpmBuildItem, + List devUIRoutesBuildItems, + List staticContentBuildItems, + BuildProducer routeProducer, + DevUIRecorder recorder, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + ShutdownContextBuildItem shutdownContext) throws IOException { + + // Websocket for JsonRPC comms + routeProducer.produce( + nonApplicationRootPathBuildItem + .routeBuilder().route(DEVUI + SLASH + JSONRPC) + .handler(recorder.communicationHandler()) + .build()); + + // Static handler for components + for (DevUIRoutesBuildItem devUIRoutesBuildItem : devUIRoutesBuildItems) { + String route = devUIRoutesBuildItem.getPath(); + + String path = nonApplicationRootPathBuildItem.resolvePath(route); + Handler uihandler = recorder.uiHandler( + devUIRoutesBuildItem.getFinalDestination(), + path, + devUIRoutesBuildItem.getWebRootConfigurations(), + shutdownContext); + + NonApplicationRootPathBuildItem.Builder builder = nonApplicationRootPathBuildItem.routeBuilder() + .route(route) + .handler(uihandler); + + if (route.endsWith(DEVUI)) { + builder = builder.displayOnNotFoundPage("Dev UI 2.0"); + routeProducer.produce(builder.build()); + } + + routeProducer.produce( + nonApplicationRootPathBuildItem.routeBuilder().route(route + SLASH_ALL).handler(uihandler).build()); + } + + String basepath = nonApplicationRootPathBuildItem.resolvePath(DEVUI); + // For static content generated at build time + for (StaticContentBuildItem staticContentBuildItem : staticContentBuildItems) { + + Map urlAndPath = new HashMap<>(); + if (staticContentBuildItem.isInternal()) { + List content = staticContentBuildItem.getContent(); + for (DevUIContent c : content) { + String parsedContent = Qute.fmt(new String(c.getTemplate()), c.getData()); + Path tempFile = Files.createTempFile("quarkus-dev-ui-", c.getFileName()); + Files.write(tempFile, parsedContent.getBytes(StandardCharsets.UTF_8)); + + urlAndPath.put(c.getFileName(), tempFile.toString()); + } + Handler buildTimeStaticHandler = recorder.buildTimeStaticHandler(basepath, urlAndPath); + + routeProducer.produce( + nonApplicationRootPathBuildItem.routeBuilder().route(DEVUI + SLASH_ALL) + .handler(buildTimeStaticHandler) + .build()); + } + } + + // For the Vaadin router (So that bookmarks/url refreshes work) + for (DevUIRoutesBuildItem devUIRoutesBuildItem : devUIRoutesBuildItems) { + String route = devUIRoutesBuildItem.getPath(); + basepath = nonApplicationRootPathBuildItem.resolvePath(route); + Handler routerhandler = recorder.vaadinRouterHandler(basepath); + routeProducer.produce( + nonApplicationRootPathBuildItem.routeBuilder().route(route + SLASH_ALL).handler(routerhandler).build()); + } + + // Static mvnpm jars + routeProducer.produce(RouteBuildItem.builder() + .route("/_static" + SLASH_ALL) + .handler(recorder.mvnpmHandler(mvnpmBuildItem.getMvnpmJars())) + .build()); + + } + + /** + * This makes sure the JsonRPC Classes for both the internal Dev UI and extensions is available as a bean and on the index. + */ + @BuildStep(onlyIf = IsDevelopment.class) + void additionalBean(BuildProducer additionalBeanProducer, + BuildProducer additionalIndexProducer, + List jsonRPCProvidersBuildItems) { + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(JsonRpcRouter.class) + .setUnremovable().build()); + + // Make sure all JsonRPC Providers is in the index + for (JsonRPCProvidersBuildItem jsonRPCProvidersBuildItem : jsonRPCProvidersBuildItems) { + + Class c = jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass(); + additionalIndexProducer.produce(new AdditionalIndexedClassesBuildItem(c.getName())); + + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(c) + .setUnremovable().build()); + } + } + + /** + * This goes through all jsonRPC methods and discover the methods using Jandex + */ + @BuildStep(onlyIf = IsDevelopment.class) + void findAllJsonRPCMethods(BuildProducer jsonRPCMethodsProvider, + BuildProducer buildTimeConstProducer, + CombinedIndexBuildItem combinedIndexBuildItem, + List jsonRPCProvidersBuildItems) { + + IndexView index = combinedIndexBuildItem.getIndex(); + + Map> extensionMethodsMap = new HashMap<>(); // All methods so that we can build the reflection + + List requestResponseMethods = new ArrayList<>(); // All requestResponse methods for validation on the client side + List subscriptionMethods = new ArrayList<>(); // All subscription methods for validation on the client side + + // Let's use the Jandex index to find all methods + for (JsonRPCProvidersBuildItem jsonRPCProvidersBuildItem : jsonRPCProvidersBuildItems) { + + Class clazz = jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass(); + String extension = jsonRPCProvidersBuildItem.getExtensionName(); + + Map jsonRpcMethods = new HashMap<>(); + if (extensionMethodsMap.containsKey(extension)) { + jsonRpcMethods = extensionMethodsMap.get(extension); + } + + ClassInfo classInfo = index.getClassByName(DotName.createSimple(clazz.getName())); + + List methods = classInfo.methods(); + + for (MethodInfo method : methods) { + if (!method.name().equals(CONSTRUCTOR)) { // Ignore constructor + if (Modifier.isPublic(method.flags())) { // Only allow public methods + if (method.returnType().kind() != Type.Kind.VOID) { // Only allow method with response + + // Create list of available methods for the Javascript side. + if (method.returnType().name().equals(DotName.createSimple(Multi.class.getName()))) { + subscriptionMethods.add(extension + DOT + method.name()); + } else { + requestResponseMethods.add(extension + DOT + method.name()); + } + + // Also create the map to pass to the runtime for the relection calls + JsonRpcMethodName jsonRpcMethodName = new JsonRpcMethodName(method.name()); + if (method.parametersCount() > 0) { + Map params = new LinkedHashMap<>(); // Keep the order + for (int i = 0; i < method.parametersCount(); i++) { + Type parameterType = method.parameterType(i); + Class parameterClass = toClass(parameterType); + String parameterName = method.parameterName(i); + params.put(parameterName, parameterClass); + } + JsonRpcMethod jsonRpcMethod = new JsonRpcMethod(clazz, method.name(), params); + jsonRpcMethods.put(jsonRpcMethodName, jsonRpcMethod); + } else { + JsonRpcMethod jsonRpcMethod = new JsonRpcMethod(clazz, method.name(), null); + jsonRpcMethods.put(jsonRpcMethodName, jsonRpcMethod); + } + } + } + } + } + + if (!jsonRpcMethods.isEmpty()) { + extensionMethodsMap.put(extension, jsonRpcMethods); + } + } + + if (!extensionMethodsMap.isEmpty()) { + jsonRPCMethodsProvider.produce(new JsonRPCMethodsBuildItem(extensionMethodsMap)); + } + + BuildTimeConstBuildItem methodInfo = new BuildTimeConstBuildItem("devui-jsonrpc"); + + if (!subscriptionMethods.isEmpty()) { + methodInfo.addBuildTimeData("jsonRPCSubscriptions", subscriptionMethods); + } + if (!requestResponseMethods.isEmpty()) { + methodInfo.addBuildTimeData("jsonRPCMethods", requestResponseMethods); + } + + buildTimeConstProducer.produce(methodInfo); + + } + + @BuildStep(onlyIf = IsDevelopment.class) + @Record(ExecutionTime.STATIC_INIT) + void createJsonRpcRouter(DevUIRecorder recorder, + BeanContainerBuildItem beanContainer, + JsonRPCMethodsBuildItem jsonRPCMethodsBuildItem) { + + if (jsonRPCMethodsBuildItem != null) { + Map> extensionMethodsMap = jsonRPCMethodsBuildItem + .getExtensionMethodsMap(); + + recorder.createJsonRpcRouter(beanContainer.getValue(), extensionMethodsMap); + } + } + + /** + * This build all the pages for dev ui, based on the extension included + */ + @BuildStep(onlyIf = IsDevelopment.class) + @SuppressWarnings("unchecked") + void getAllExtensions(List cardPageBuildItems, + List menuPageBuildItems, + List footerPageBuildItems, + BuildProducer extensionsProducer, + BuildProducer webJarBuildProducer, + BuildProducer devUIWebJarProducer) { + + // First create the static resources for our own internal components + webJarBuildProducer.produce(WebJarBuildItem.builder() + .artifactKey(UI_JAR) + .root(DEVUI + SLASH).build()); + + devUIWebJarProducer.produce(new DevUIWebJarBuildItem(UI_JAR, DEVUI)); + + // Now go through all extensions and check them for active components + Map cardPagesMap = getCardPagesMap(cardPageBuildItems); + Map menuPagesMap = getMenuPagesMap(menuPageBuildItems); + Map footerPagesMap = getFooterPagesMap(footerPageBuildItems); + try { + final Yaml yaml = new Yaml(); + List activeExtensions = new ArrayList<>(); + List inactiveExtensions = new ArrayList<>(); + List sectionMenuExtensions = new ArrayList<>(); + List footerTabExtensions = new ArrayList<>(); + ClassPathUtils.consumeAsPaths(YAML_FILE, p -> { + try { + Extension extension = new Extension(); + final String extensionYaml; + try (Scanner scanner = new Scanner(Files.newBufferedReader(p, StandardCharsets.UTF_8))) { + scanner.useDelimiter("\\A"); + extensionYaml = scanner.hasNext() ? scanner.next() : null; + } + if (extensionYaml == null) { + // This is a internal extension (like this one, Dev UI) + return; + } + + final Map extensionMap = yaml.load(extensionYaml); + + if (extensionMap.containsKey(NAME)) { + String name = (String) extensionMap.get(NAME); + extension.setNamespace(getExtensionNamespace(extensionMap)); + extension.setName(name); + extension.setDescription((String) extensionMap.getOrDefault(DESCRIPTION, null)); + String artifactId = (String) extensionMap.getOrDefault(ARTIFACT, null); + extension.setArtifact(artifactId); + + Map metaData = (Map) extensionMap.getOrDefault(METADATA, null); + extension.setKeywords((List) metaData.getOrDefault(KEYWORDS, null)); + extension.setShortName((String) metaData.getOrDefault(SHORT_NAME, null)); + + if (metaData.containsKey(GUIDE)) { + String guide = (String) metaData.get(GUIDE); + try { + extension.setGuide(new URL(guide)); + } catch (MalformedURLException mue) { + log.warn("Could not set Guide URL [" + guide + "] for exception [" + name + "]"); + } + } + + extension.setCategories((List) metaData.getOrDefault(CATEGORIES, null)); + extension.setStatus((String) metaData.getOrDefault(STATUS, null)); + extension.setBuiltWith((String) metaData.getOrDefault(BUILT_WITH, null)); + extension.setConfigFilter((List) metaData.getOrDefault(CONFIG, null)); + extension.setExtensionDependencies((List) metaData.getOrDefault(EXTENSION_DEPENDENCIES, null)); + extension.setUnlisted(String.valueOf(metaData.getOrDefault(UNLISTED, false))); + + if (metaData.containsKey(CAPABILITIES)) { + Map capabilities = (Map) metaData.get(CAPABILITIES); + extension.setConfigFilter((List) capabilities.getOrDefault(PROVIDES, null)); + } + + if (metaData.containsKey(CODESTART)) { + Map codestartMap = (Map) metaData.get(metaData); + if (codestartMap != null) { + Codestart codestart = new Codestart(); + codestart.setName((String) codestartMap.getOrDefault(NAME, null)); + codestart.setLanguages((List) codestartMap.getOrDefault(LANGUAGES, null)); + codestart.setArtifact((String) codestartMap.getOrDefault(ARTIFACT, null)); + extension.setCodestart(codestart); + } + } + + String nameKey = name.toLowerCase().replaceAll(SPACE, DASH); + + if (!cardPagesMap.containsKey(nameKey)) { // Inactive + inactiveExtensions.add(extension); + } else { // Active + CardPageBuildItem cardPageBuildItem = cardPagesMap.get(nameKey); + + // Add all card links + List cardPageBuilders = cardPageBuildItem.getPages(); + + Map buildTimeData = cardPageBuildItem.getBuildTimeData(); + for (PageBuilder pageBuilder : cardPageBuilders) { + Page page = buildFinalPage(pageBuilder, extension, buildTimeData); + extension.addCardPage(page); + } + + // See if there is a custom card component + cardPageBuildItem.getOptionalCard().ifPresent((card) -> { + card.setNamespace(extension.getPathName()); + extension.setCard(card); + }); + + // Also make sure the static resources for that static resource is available + produceResources(artifactId, cardPageBuildItem.getExtensionPathName(), webJarBuildProducer, + devUIWebJarProducer); + + activeExtensions.add(extension); + } + + // Menus on the sections menu + if (menuPagesMap.containsKey(nameKey)) { + MenuPageBuildItem menuPageBuildItem = menuPagesMap.get(nameKey); + List menuPageBuilders = menuPageBuildItem.getPages(); + + Map buildTimeData = menuPageBuildItem.getBuildTimeData(); + for (PageBuilder pageBuilder : menuPageBuilders) { + Page page = buildFinalPage(pageBuilder, extension, buildTimeData); + extension.addMenuPage(page); + } + // Also make sure the static resources for that static resource is available + produceResources(artifactId, menuPageBuildItem.getExtensionPathName(), webJarBuildProducer, + devUIWebJarProducer); + + sectionMenuExtensions.add(extension); + } + + // Tabs in the footer + if (footerPagesMap.containsKey(nameKey)) { + FooterPageBuildItem footerPageBuildItem = footerPagesMap.get(nameKey); + List footerPageBuilders = footerPageBuildItem.getPages(); + + Map buildTimeData = footerPageBuildItem.getBuildTimeData(); + for (PageBuilder pageBuilder : footerPageBuilders) { + Page page = buildFinalPage(pageBuilder, extension, buildTimeData); + extension.addFooterPage(page); + } + // Also make sure the static resources for that static resource is available + produceResources(artifactId, footerPageBuildItem.getExtensionPathName(), webJarBuildProducer, + devUIWebJarProducer); + + footerTabExtensions.add(extension); + } + + } + + Collections.sort(activeExtensions, sortingComparator); + Collections.sort(inactiveExtensions, sortingComparator); + } catch (IOException | RuntimeException e) { + // don't abort, just log, to prevent a single extension from breaking entire dev ui + log.error("Failed to process extension descriptor " + p.toUri(), e); + } + }); + extensionsProducer.produce( + new ExtensionsBuildItem(activeExtensions, inactiveExtensions, sectionMenuExtensions, footerTabExtensions)); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + private void produceResources(String artifactId, String extensionPathName, + BuildProducer webJarBuildProducer, + BuildProducer devUIWebJarProducer) { + + GACT gact = getGACT(artifactId); + webJarBuildProducer.produce(WebJarBuildItem.builder() + .artifactKey(gact) + .root(DEVUI + SLASH + extensionPathName + SLASH).build()); + + devUIWebJarProducer.produce( + new DevUIWebJarBuildItem(gact, + DEVUI + SLASH + extensionPathName)); + } + + @BuildStep(onlyIf = IsDevelopment.class) + void createAllRoutes(WebJarResultsBuildItem webJarResultsBuildItem, + List devUIWebJarBuiltItems, + BuildProducer devUIRoutesProducer) { + + for (DevUIWebJarBuildItem devUIWebJarBuiltItem : devUIWebJarBuiltItems) { + WebJarResultsBuildItem.WebJarResult result = webJarResultsBuildItem + .byArtifactKey(devUIWebJarBuiltItem.getArtifactKey()); + if (result != null) { + devUIRoutesProducer.produce(new DevUIRoutesBuildItem(devUIWebJarBuiltItem.getPath(), + result.getFinalDestination(), result.getWebRootConfigurations())); + } + } + } + + private Page buildFinalPage(PageBuilder pageBuilder, Extension extension, Map buildTimeData) { + pageBuilder.namespace(extension.getPathName()); + pageBuilder.extension(extension.getName()); + + // TODO: Have a nice factory way to load this... + // Some preprocessing for certain builds + if (pageBuilder.getClass().equals(QuteDataPageBuilder.class)) { + return buildQutePage(pageBuilder, extension, buildTimeData); + } + + return pageBuilder.build(); + } + + private Page buildQutePage(PageBuilder pageBuilder, Extension extension, Map buildTimeData) { + try { + QuteDataPageBuilder quteDataPageBuilder = (QuteDataPageBuilder) pageBuilder; + String templatePath = quteDataPageBuilder.getTemplatePath(); + ClassPathUtils.consumeAsPaths(templatePath, p -> { + try { + String template = Files.readString(p); + String fragment = Qute.fmt(template, buildTimeData); + pageBuilder.metadata("htmlFragment", fragment); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + return pageBuilder.build(); + } + + private GACT getGACT(String artifactKey) { + String[] split = artifactKey.split(DOUBLE_POINT); + return new GACT(split[0], split[1] + DASH_DEPLOYMENT, null, JAR); + } + + private Class toClass(Type type) { + try { + return tccl.loadClass(type.name().toString()); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + } + + private Map getCardPagesMap(List pages) { + Map m = new HashMap<>(); + for (CardPageBuildItem pageBuildItem : pages) { + m.put(pageBuildItem.getExtensionPathName(), pageBuildItem); + } + return m; + } + + private Map getMenuPagesMap(List pages) { + Map m = new HashMap<>(); + for (MenuPageBuildItem pageBuildItem : pages) { + m.put(pageBuildItem.getExtensionPathName(), pageBuildItem); + } + return m; + } + + private Map getFooterPagesMap(List pages) { + Map m = new HashMap<>(); + for (FooterPageBuildItem pageBuildItem : pages) { + m.put(pageBuildItem.getExtensionPathName(), pageBuildItem); + } + return m; + } + + private String getExtensionNamespace(Map extensionMap) { + final String groupId; + final String artifactId; + final String artifact = (String) extensionMap.get("artifact"); + if (artifact == null) { + // trying quarkus 1.x format + groupId = (String) extensionMap.get("group-id"); + artifactId = (String) extensionMap.get("artifact-id"); + if (artifactId == null || groupId == null) { + throw new RuntimeException( + "Failed to locate 'artifact' or 'group-id' and 'artifact-id' among metadata keys " + + extensionMap.keySet()); + } + } else { + final GACTV coords = GACTV.fromString(artifact); + groupId = coords.getGroupId(); + artifactId = coords.getArtifactId(); + } + return groupId + "." + artifactId; + } + + // Sort extensions with Guide first and then alphabetical + private final Comparator sortingComparator = new Comparator() { + @Override + public int compare(Extension t, Extension t1) { + if (t.getGuide() != null && t1.getGuide() != null) { + return t.getName().compareTo(t1.getName()); + } else if (t.getGuide() == null) { + return 1; + } else { + return -1; + } + } + }; +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIRoutesBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIRoutesBuildItem.java new file mode 100644 index 0000000000000..87f74ea9b04ba --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIRoutesBuildItem.java @@ -0,0 +1,35 @@ +package io.quarkus.devui.deployment; + +import java.util.List; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.vertx.http.runtime.devmode.FileSystemStaticHandler; + +/** + * All the routes needed for Dev UI + */ +public final class DevUIRoutesBuildItem extends MultiBuildItem { + + private final String path; + private final String finalDestination; + private final List webRootConfigurations; + + public DevUIRoutesBuildItem(String path, String finalDestination, + List webRootConfigurations) { + this.path = path; + this.finalDestination = finalDestination; + this.webRootConfigurations = webRootConfigurations; + } + + public String getPath() { + return path; + } + + public String getFinalDestination() { + return finalDestination; + } + + public List getWebRootConfigurations() { + return webRootConfigurations; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIWebJarBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIWebJarBuildItem.java new file mode 100644 index 0000000000000..eefacb30ce6d6 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIWebJarBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.devui.deployment; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.maven.dependency.GACT; + +public final class DevUIWebJarBuildItem extends MultiBuildItem { + private final GACT artifactKey; + private final String path; + + public DevUIWebJarBuildItem(GACT artifactKey, String path) { + this.artifactKey = artifactKey; + this.path = path; + } + + public GACT getArtifactKey() { + return artifactKey; + } + + public String getPath() { + return path; + } + +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ExtensionsBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ExtensionsBuildItem.java new file mode 100644 index 0000000000000..9380d615b1ba6 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ExtensionsBuildItem.java @@ -0,0 +1,40 @@ +package io.quarkus.devui.deployment; + +import java.util.List; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.devui.deployment.extension.Extension; + +public final class ExtensionsBuildItem extends SimpleBuildItem { + + private final List activeExtensions; + private final List inactiveExtensions; + private final List sectionMenuExtensions; + private final List footerTabsExtensions; + + public ExtensionsBuildItem(List activeExtensions, + List inactiveExtensions, + List sectionMenuExtensions, + List footerTabsExtensions) { + this.activeExtensions = activeExtensions; + this.inactiveExtensions = inactiveExtensions; + this.sectionMenuExtensions = sectionMenuExtensions; + this.footerTabsExtensions = footerTabsExtensions; + } + + public List getActiveExtensions() { + return this.activeExtensions; + } + + public List getInactiveExtensions() { + return this.inactiveExtensions; + } + + public List getSectionMenuExtensions() { + return this.sectionMenuExtensions; + } + + public List getFooterTabsExtensions() { + return this.footerTabsExtensions; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/InternalImportMapBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/InternalImportMapBuildItem.java new file mode 100644 index 0000000000000..1e3f9f72b2c71 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/InternalImportMapBuildItem.java @@ -0,0 +1,30 @@ +package io.quarkus.devui.deployment; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Used internally to define some of our own imports + */ +public final class InternalImportMapBuildItem extends MultiBuildItem { + + private final Map importMap = new HashMap<>(); + + public InternalImportMapBuildItem() { + + } + + public void add(Map importMap) { + this.importMap.putAll(importMap); + } + + public void add(String key, String path) { + this.importMap.put(key, path); + } + + public Map getImportMap() { + return importMap; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCMethodsBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCMethodsBuildItem.java new file mode 100644 index 0000000000000..1cc0c40d7e0e3 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCMethodsBuildItem.java @@ -0,0 +1,23 @@ +package io.quarkus.devui.deployment; + +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; + +/** + * Simple holder for all discovered Json RPC Methods + */ +public final class JsonRPCMethodsBuildItem extends SimpleBuildItem { + + private final Map> extensionMethodsMap; + + public JsonRPCMethodsBuildItem(Map> extensionMethodsMap) { + this.extensionMethodsMap = extensionMethodsMap; + } + + public Map> getExtensionMethodsMap() { + return extensionMethodsMap; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/MvnpmBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/MvnpmBuildItem.java new file mode 100644 index 0000000000000..74b8a5576a057 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/MvnpmBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.devui.deployment; + +import java.net.URL; +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * All mvnpm jars used by Dev UI + */ +public final class MvnpmBuildItem extends SimpleBuildItem { + private final Set mvnpmJars; + + public MvnpmBuildItem(Set mvnpmJars) { + this.mvnpmJars = mvnpmJars; + } + + public Set getMvnpmJars() { + return mvnpmJars; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ThemeVarsBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ThemeVarsBuildItem.java new file mode 100644 index 0000000000000..cdafdbd7fe61a --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/ThemeVarsBuildItem.java @@ -0,0 +1,38 @@ +package io.quarkus.devui.deployment; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Set; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class ThemeVarsBuildItem extends SimpleBuildItem { + + private final Set themeVars; + private final String defaultValue; + + public ThemeVarsBuildItem(Set themeVars, String defaultValue) { + this.themeVars = themeVars; + this.defaultValue = defaultValue; + } + + public Set getThemeVars() { + return themeVars; + } + + public String getDefaultValue() { + return defaultValue; + } + + public String getTemplateValue() { + try (StringWriter sw = new StringWriter()) { + for (String line : themeVars) { + sw.write(line + ": " + defaultValue + ";"); + sw.write("\n"); + } + return sw.toString(); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Codestart.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Codestart.java new file mode 100644 index 0000000000000..5ca201a06c425 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Codestart.java @@ -0,0 +1,47 @@ +package io.quarkus.devui.deployment.extension; + +import java.util.List; + +public class Codestart { + private String name; + private List languages; + private String artifact; + + public Codestart() { + } + + public Codestart(String name, List languages, String artifact) { + this.name = name; + this.languages = languages; + this.artifact = artifact; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getLanguages() { + return languages; + } + + public void setLanguages(List languages) { + this.languages = languages; + } + + public String getArtifact() { + return artifact; + } + + public void setArtifact(String artifact) { + this.artifact = artifact; + } + + @Override + public String toString() { + return "Codestart{" + "name=" + name + ", languages=" + languages + ", artifact=" + artifact + '}'; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java new file mode 100644 index 0000000000000..61798c1a39e49 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/Extension.java @@ -0,0 +1,218 @@ +package io.quarkus.devui.deployment.extension; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import io.quarkus.devui.spi.page.Card; +import io.quarkus.devui.spi.page.Page; + +public class Extension { + private static final String SPACE = " "; + private static final String DASH = "-"; + + private String namespace; + private String artifact; + private String name; + private String shortName; + private String description; + private URL guide; + private List keywords; + private String status; + private List configFilter; + private List categories; + private String unlisted; + private String builtWith; + private List providesCapabilities; + private List extensionDependencies; + private Codestart codestart; + private final List cardPages = new ArrayList<>(); + private final List menuPages = new ArrayList<>(); + private final List footerPages = new ArrayList<>(); + private Card card = null; // Custom card + + public Extension() { + + } + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getArtifact() { + return artifact; + } + + public void setArtifact(String artifact) { + this.artifact = artifact; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPathName() { + return name.toLowerCase().replaceAll(SPACE, DASH); + } + + public String getShortName() { + return shortName; + } + + public void setShortName(String shortName) { + this.shortName = shortName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public URL getGuide() { + return guide; + } + + public void setGuide(URL guide) { + this.guide = guide; + } + + public List getKeywords() { + return keywords; + } + + public void setKeywords(List keywords) { + this.keywords = keywords; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public List getConfigFilter() { + return configFilter; + } + + public void setConfigFilter(List configFilter) { + this.configFilter = configFilter; + } + + public List getCategories() { + return categories; + } + + public void setCategories(List categories) { + this.categories = categories; + } + + public String getUnlisted() { + return unlisted; + } + + public void setUnlisted(String unlisted) { + this.unlisted = unlisted; + } + + public String getBuiltWith() { + return builtWith; + } + + public void setBuiltWith(String builtWith) { + this.builtWith = builtWith; + } + + public List getProvidesCapabilities() { + return providesCapabilities; + } + + public void setProvidesCapabilities(List providesCapabilities) { + this.providesCapabilities = providesCapabilities; + } + + public List getExtensionDependencies() { + return extensionDependencies; + } + + public void setExtensionDependencies(List extensionDependencies) { + this.extensionDependencies = extensionDependencies; + } + + public Codestart getCodestart() { + return codestart; + } + + public void setCodestart(Codestart codestart) { + this.codestart = codestart; + } + + public void addCardPage(Page page) { + this.cardPages.add(page); + } + + public void addCardPages(List pages) { + this.cardPages.addAll(pages); + } + + public List getCardPages() { + return cardPages; + } + + public void addMenuPage(Page page) { + this.menuPages.add(page); + } + + public void addMenuPages(List pages) { + this.menuPages.addAll(pages); + } + + public List getMenuPages() { + return menuPages; + } + + public void addFooterPage(Page page) { + this.footerPages.add(page); + } + + public void addFooterPages(List pages) { + this.footerPages.addAll(pages); + } + + public List getFooterPages() { + return footerPages; + } + + public void setCard(Card card) { + this.card = card; + } + + public Card getCard() { + return this.card; + } + + public boolean hasCard() { + return this.card != null; + } + + @Override + public String toString() { + return "Extension{" + "namespace=" + namespace + ", artifact=" + artifact + ", name=" + name + ", shortName=" + + shortName + ", description=" + description + ", guide=" + guide + ", keywords=" + keywords + ", status=" + + status + ", configFilter=" + configFilter + ", categories=" + categories + ", unlisted=" + unlisted + + ", builtWith=" + builtWith + ", providesCapabilities=" + providesCapabilities + ", extensionDependencies=" + + extensionDependencies + ", codestart=" + codestart + '}'; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/ExtensionGroup.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/ExtensionGroup.java new file mode 100644 index 0000000000000..cc79f0035194f --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/extension/ExtensionGroup.java @@ -0,0 +1,6 @@ +package io.quarkus.devui.deployment.extension; + +public enum ExtensionGroup { + active, + inactive +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java new file mode 100644 index 0000000000000..82570f81296ee --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java @@ -0,0 +1,45 @@ +package io.quarkus.devui.deployment.logstream; + +import java.util.Optional; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.StreamingLogHandlerBuildItem; +import io.quarkus.devui.runtime.logstream.LogStreamBroadcaster; +import io.quarkus.devui.runtime.logstream.LogStreamJsonRPCService; +import io.quarkus.devui.runtime.logstream.LogStreamRecorder; +import io.quarkus.devui.runtime.logstream.MutinyLogHandler; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.runtime.RuntimeValue; + +/** + * Processor for Log stream in Dev UI + */ +public class LogStreamProcessor { + + @BuildStep(onlyIf = IsDevelopment.class) + void additionalBean(BuildProducer additionalBeanProducer) { + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(LogStreamBroadcaster.class) + .setUnremovable().build()); + } + + @BuildStep(onlyIf = IsDevelopment.class) + @Record(ExecutionTime.STATIC_INIT) + @SuppressWarnings("unchecked") + public void handler(BuildProducer streamingLogHandlerBuildItem, + LogStreamRecorder recorder) { + RuntimeValue> mutinyLogHandler = recorder.mutinyLogHandler(); + streamingLogHandlerBuildItem.produce(new StreamingLogHandlerBuildItem((RuntimeValue) mutinyLogHandler)); + } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem createJsonRPCService() { + return new JsonRPCProvidersBuildItem("DevUI", LogStreamJsonRPCService.class); + } + +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java index c737487120cb4..d80af2bca4b0e 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/StaticResourcesProcessor.java @@ -16,6 +16,7 @@ import java.util.Set; import io.quarkus.arc.deployment.BeanContainerBuildItem; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; @@ -92,6 +93,10 @@ private Set getClasspathResources(ApplicationArc throws Exception { Set knownPaths = new HashSet<>(); + ClassPathUtils.consumeAsPaths(StaticResourcesRecorder.META_INF_RESOURCES, resource -> { + collectKnownPaths(resource, knownPaths); + }); + for (ApplicationArchive i : applicationArchivesBuildItem.getAllApplicationArchives()) { i.accept(tree -> { Path resource = tree.getPath(StaticResourcesRecorder.META_INF_RESOURCES); @@ -101,10 +106,6 @@ private Set getClasspathResources(ApplicationArc }); } - ClassPathUtils.consumeAsPaths(StaticResourcesRecorder.META_INF_RESOURCES, resource -> { - collectKnownPaths(resource, knownPaths); - }); - return knownPaths; } @@ -115,12 +116,16 @@ private void collectKnownPaths(Path resource, Set + + 4.0.0 + + io.quarkus + quarkus-vertx-http-parent + 999-SNAPSHOT + ../ + + quarkus-vertx-http-dev-ui-resources + Quarkus - Vert.x - HTTP - Dev UI Resources + + + + org.mvnpm + lit + runtime + + + org.mvnpm + lit-element-state + runtime + + + org.mvnpm.at.vaadin + router + runtime + + + org.mvnpm.at.vaadin + accordion + runtime + + + org.mvnpm.at.vaadin + avatar + runtime + + + org.mvnpm.at.vaadin + vertical-layout + runtime + + + org.mvnpm.at.vaadin + horizontal-layout + runtime + + + org.mvnpm.at.vaadin + button + runtime + + + org.mvnpm.at.vaadin + combo-box + runtime + + + org.mvnpm.at.vaadin + confirm-dialog + runtime + + + org.mvnpm.at.vaadin + checkbox + runtime + + + org.mvnpm.at.vaadin + checkbox-group + runtime + + + org.mvnpm.at.vaadin + component-base + runtime + + + org.mvnpm.at.vaadin + context-menu + runtime + + + org.mvnpm.at.vaadin + custom-field + runtime + + + org.mvnpm.at.vaadin + date-picker + runtime + + + org.mvnpm.at.vaadin + date-time-picker + runtime + + + org.mvnpm.at.vaadin + time-picker + runtime + + + org.mvnpm.at.vaadin + details + runtime + + + org.mvnpm.at.vaadin + dialog + runtime + + + org.mvnpm.at.vaadin + email-field + runtime + + + org.mvnpm.at.vaadin + form-layout + runtime + + + org.mvnpm.at.vaadin + password-field + runtime + + + org.mvnpm.at.vaadin + number-field + runtime + + + org.mvnpm.at.vaadin + field-base + runtime + + + org.mvnpm.at.vaadin + grid + runtime + + + org.mvnpm.at.vaadin + input-container + runtime + + + org.mvnpm.at.vaadin + item + runtime + + + org.mvnpm.at.vaadin + list-box + runtime + + + org.mvnpm.at.vaadin + lit-renderer + runtime + + + org.mvnpm.at.vaadin + menu-bar + runtime + + + org.mvnpm.at.vaadin + message-list + runtime + + + org.mvnpm.at.vaadin + message-input + runtime + + + org.mvnpm.at.vaadin + multi-select-combo-box + runtime + + + org.mvnpm.at.vaadin + radio-group + runtime + + + org.mvnpm.at.vaadin + overlay + runtime + + + org.mvnpm.at.vaadin + progress-bar + runtime + + + org.mvnpm.at.vaadin + split-layout + runtime + + + org.mvnpm.at.vaadin + text-area + runtime + + + org.mvnpm.at.vaadin + tooltip + runtime + + + org.mvnpm.at.vaadin + vaadin-list-mixin + runtime + + + org.mvnpm.at.vaadin + vaadin-material-styles + runtime + + + org.mvnpm.at.vaadin + vaadin-themable-mixin + runtime + + + org.mvnpm.at.vaadin + tabs + runtime + + + org.mvnpm.at.vaadin + scroller + runtime + + + org.mvnpm.at.vaadin + text-field + runtime + + + org.mvnpm.at.vaadin + tabsheet + runtime + + + org.mvnpm.at.vaadin + select + runtime + + + org.mvnpm.at.vaadin + notification + runtime + + + org.mvnpm.at.vaadin + upload + runtime + + + org.mvnpm.at.vaadin + virtual-list + runtime + + + + + org.mvnpm.at.vaadin + icon + runtime + + + org.mvnpm.at.vaadin + vaadin-lumo-styles + + + + + org.mvnpm.at.vaadin + vaadin-lumo-styles + runtime + + + org.mvnpm.at.vaadin + icon + + + + + org.mvnpm.at.vanillawc + wc-codemirror + runtime + + + \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/build-time-data.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/build-time-data.js new file mode 100644 index 0000000000000..ba25e172c8cd2 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/build-time-data.js @@ -0,0 +1,4 @@ +// Generated by Quarkus during Build +{#for d in buildTimeData} +export const {d.key} = {d.value}; +{/for} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/index.html b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/index.html new file mode 100644 index 0000000000000..818295a96afe8 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui-templates/build-time/index.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + Dev UI + + + + + + + + +
+ +
+ Quarkus +
+
+ + + + + + diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js new file mode 100644 index 0000000000000..7d6ff6c7c0c9f --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/jsonrpc.js @@ -0,0 +1,279 @@ +import { jsonRPCSubscriptions } from 'devui-jsonrpc-data'; +import { jsonRPCMethods } from 'devui-jsonrpc-data'; +import { connectionState } from 'connection-state'; + +class Level { + static Info = new Level("info"); + static Warning = new Level("warning"); + static Error = new Level("error"); + + constructor(level) { + this.level = level; + } + + toString() { + return this.level; + } +} + +class MessageDirection { + static Up = new MessageDirection("up"); + static Down = new MessageDirection("down"); + static Stationary = new MessageDirection("stationary"); + + constructor(direction) { + this.direction = direction; + } + + toString() { + return this.direction; + } +} + +class MessageType { + static Response = new MessageType("Response"); + static Void = new MessageType("Void"); + static SubscriptionMessage = new MessageType("SubscriptionMessage"); + + constructor(messageType) { + this.messageType = messageType; + } + + toString() { + return this.messageType; + } +} + +class Observer { + constructor(id) { + this.id = id; + } + + onNext(callback){ + this.onNextCallback = callback; + return this; + } + + onError(callback){ + this.onErrorCallback = callback; + return this; + } + + cancel(){ + JsonRpc.observerQueue.delete(this.id); + JsonRpc.cancelSubscription(this.id); + } + + toString() { + return "Observer for + " + this.id; + } +} + +/** + * This class allow a proxy to the JsonRPC messages. + * Callers will call the json-rpc method they want to call (even though the method does not exist on this class) and the proxy will translate that to + * a json RPC Message format and send it over web socket to the server, returning a promise, that will resolve once the websocket replies. + */ +export class JsonRpc { + static promiseQueue = new Map(); // Keep track of promise waiting for a response + static observerQueue = new Map(); // Keep track of subscriptions waiting for a responses + static initQueue = []; // If message came in and we do not have a connection yet, we queue here + static messageCounter = 0; + static webSocket; + static serverUri; + + _extensionName; + _logTraffic; + constructor(extensionName, logTraffic=true) { + this._extensionName = extensionName; + this._logTraffic = logTraffic; + if (!JsonRpc.webSocket) { + if (window.location.protocol === "https:") { + JsonRpc.serverUri = "wss:"; + } else { + JsonRpc.serverUri = "ws:"; + } + var currentPath = window.location.pathname; + currentPath = currentPath.substring(0, currentPath.indexOf('/dev')) + "/dev-ui"; + JsonRpc.serverUri += "//" + window.location.host + currentPath + "/json-rpc-ws"; + JsonRpc.connect(); + } + + return new Proxy(this, { + + get(target, prop) { + + const origMethod = target[prop]; + + if (typeof origMethod == 'undefined') { + return function (...args) { + var uid = JsonRpc.messageCounter++; + + let method = this._extensionName + "." + prop.toString(); + + let params = new Object(); + if (args.length > 0) { + params = args[0]; + } + + // Make a JsonRPC Call to the server + var message = new Object(); + message.jsonrpc = "2.0"; + message.method = method; + message.params = params; + message.id = uid; + + var jsonrpcpayload = JSON.stringify(message); + + if (jsonRPCSubscriptions.includes(method)) { + // Observer + var observer = new Observer(uid); + JsonRpc.observerQueue.set(uid, { + observer: observer, + log: this._logTraffic + }); + JsonRpc.sendJsonRPCMessage(jsonrpcpayload, this._logTraffic); + return observer; + } else if(jsonRPCMethods.includes(method)){ + // Promise + var _resolve, _reject; + var promise = new Promise((resolve, reject) => { + _reject = reject; + _resolve = resolve; + }); + promise.resolve_ex = (value) => { + _resolve(value); + }; + promise.reject_ex = (value) => { + _reject(value); + }; + JsonRpc.promiseQueue.set(uid, { + promise: promise, + log: this._logTraffic + }); + JsonRpc.sendJsonRPCMessage(jsonrpcpayload, this._logTraffic); + return promise; + } else { + // TODO: Send error ? + console.log("method not found " + method); + return Reflect.get(target, prop); + } + } + } else { + return Reflect.get(target, prop); + } + } + }) + } + + static sendJsonRPCMessage(jsonrpcpayload, log=true) { + if (JsonRpc.webSocket.readyState !== WebSocket.OPEN) { + JsonRpc.initQueue.push(jsonrpcpayload); + } else { + JsonRpc.webSocket.send(jsonrpcpayload); + if(log){ + JsonRpc.dispatchMessageLogEntry(Level.Info, MessageDirection.Up, jsonrpcpayload); + } + } + } + + static cancelSubscription(id, log=true) { + var message = new Object(); + message.jsonrpc = "2.0"; + message.method = "unsubscribe"; + message.params = {}; + message.id = id; + + var jsonrpcpayload = JSON.stringify(message); + JsonRpc.sendJsonRPCMessage(jsonrpcpayload, log); + } + + static connect() { + if(!connectionState.current.isConnecting && !connectionState.current.isConnected){ // Don't connect if already in progress or connected + connectionState.connecting(JsonRpc.serverUri); + JsonRpc.dispatchMessageLogEntry(Level.Info, MessageDirection.Stationary, "Connecting to " + JsonRpc.serverUri); + JsonRpc.webSocket = new WebSocket(JsonRpc.serverUri); + + JsonRpc.webSocket.onopen = function (event) { + connectionState.connected(JsonRpc.serverUri); + JsonRpc.dispatchMessageLogEntry(Level.Info, MessageDirection.Stationary, "Connected to " + JsonRpc.serverUri); + while (JsonRpc.initQueue.length > 0) { + JsonRpc.webSocket.send(JsonRpc.initQueue.pop()); + } + }; + + JsonRpc.webSocket.onmessage = function (event) { + var response = JSON.parse(event.data); + var devUiResponse = response.result; + var messageType = devUiResponse.messageType; + + if (messageType === MessageType.Void.toString()) { // Void response, typically used on initial subscription + // Do nothing + } else if (messageType === MessageType.Response.toString()) { // Normal Request-Response + if (JsonRpc.promiseQueue.has(response.id)) { + var saved = JsonRpc.promiseQueue.get(response.id); + var promise = saved.promise; + var log = saved.log; + var userData = devUiResponse.object; + response.result = userData; + + promise.resolve_ex(response); + JsonRpc.promiseQueue.delete(response.id); + if(log){ + var jsonrpcpayload = JSON.stringify(response); + JsonRpc.dispatchMessageLogEntry(Level.Info, MessageDirection.Down, jsonrpcpayload); + } + } else { + JsonRpc.dispatchMessageLogEntry(Level.Warning, MessageDirection.Down, "Initial normal request not found [ " + devUiResponse.messageType + "], " + event.data); + } + } else if (messageType === MessageType.SubscriptionMessage.toString()) { // Subscription message + if (JsonRpc.observerQueue.has(response.id)) { + var saved = JsonRpc.observerQueue.get(response.id); + var observer = saved.observer; + var log = saved.log; + var userData = devUiResponse.object; + response.result = userData; + observer.onNextCallback(response); + if(log){ + var jsonrpcpayload = JSON.stringify(response); + JsonRpc.dispatchMessageLogEntry(Level.Info, MessageDirection.Down, jsonrpcpayload); + } + } else { + // Let's cancel as we do not have someone interested in this anymore + JsonRpc.cancelSubscription(response.id); + JsonRpc.dispatchMessageLogEntry(Level.Warning, MessageDirection.Stationary, "Auto unsubscribe from [" + response.id + "] as no one is listening anymore "); + } + } else { + JsonRpc.dispatchMessageLogEntry(Level.Warning, MessageDirection.Down, "Unknown type [" + devUiResponse.messageType + "], " + event.data); + } + } + } + + JsonRpc.webSocket.onclose = function (event) { + connectionState.disconnected(JsonRpc.serverUri); + JsonRpc.dispatchMessageLogEntry(Level.Warning, MessageDirection.Stationary, "Closed connection to " + JsonRpc.serverUri); + setTimeout(function () { + JsonRpc.connect(); + }, 100); + }; + + JsonRpc.webSocket.onerror = function (error) { + JsonRpc.dispatchMessageLogEntry(Level.Error, MessageDirection.Stationary, "Error from " + JsonRpc.serverUri); + JsonRpc.webSocket.close(); + } + } + + static dispatchMessageLogEntry(level, direction, message) { + var logEntry = new Object(); + logEntry.id = Math.floor(Math.random() * 999999); + let now = new Date(); + logEntry.date = now.toDateString(); + logEntry.time = now.toLocaleTimeString('en-US'); + logEntry.direction = direction.toString(); + logEntry.connectionState = connectionState.current.name; + logEntry.level = level.toString(); + logEntry.message = message; + const event = new CustomEvent('jsonRPCLogEntryEvent', {detail: logEntry}); + document.dispatchEvent(event); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/log-controller.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/log-controller.js new file mode 100644 index 0000000000000..ba916d9e1e27b --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/log-controller.js @@ -0,0 +1,125 @@ +/** + * Control buttons for the log(s) at the bottom + */ +export class LogController { + static _controllers = new Map(); + + host; + tab; + items = []; + + constructor(host, tab) { + (this.host = host).addController(this); + this.tab = tab; + } + + hostConnected() { + LogController._controllers.set(this.tab, this); + } + + hostDisconnected() { + LogController._controllers.delete(this.tab); + } + + addItem(title, icon, color, callback){ + var item = { + component: this._createItem(icon, title, color), + callback: callback, + isToggle: false + }; + this.items.push(item); + return this; + } + + addToggle(title, selected, callback){ + var item = { + component: this._createToggle(title, selected), + callback: callback, + isToggle: true + }; + this.items.push(item); + return this; + } + + addFollow(title, selected, callback){ + var item = { + component: this._createFollow(title, selected), + callback: callback, + isToggle: true + }; + this.items.push(item); + return this; + } + + _createItem(icon, title, color) { + var style = `font-size: x-small;cursor: pointer;color: ${color};`; + const item = document.createElement('vaadin-context-menu-item'); + const vaadinicon = document.createElement('vaadin-icon'); + item.setAttribute('aria-label', `${title}`); + vaadinicon.setAttribute('icon', `${icon}`); + vaadinicon.setAttribute('style', `${style}`); + vaadinicon.setAttribute('title', `${title}`); + item.appendChild(vaadinicon); + return item; + } + + _createToggle(title, selected){ + var color = "var(--lumo-tertiary-text-color)"; + var icon = "font-awesome-solid:toggle-off"; + if(selected){ + color = "var(--lumo-primary-color)"; + icon = "font-awesome-solid:toggle-on"; + } + return this._createItem(icon,title,color); + } + + _createFollow(title, selected){ + var color = "var(--lumo-tertiary-text-color)"; + var icon = "font-awesome-regular:circle"; + if(selected){ + color = "var(--lumo-success-color)"; + icon = "font-awesome-regular:circle-dot"; + } + return this._createItem(icon,title,color); + } + + static getItemsForTab(tabName){ + + if(LogController._controllers.has(tabName)){ + return LogController._controllers.get(tabName).items; + }else { + return []; + } + } + + static fireCallback(e){ + if(e.detail.value.isToggle){ + if(e.detail.value.component.firstChild.icon.endsWith('-on')){ + // switching off + e.detail.value.component.firstChild.icon = "font-awesome-solid:toggle-off"; + e.detail.value.component.firstChild.style.color = "var(--lumo-tertiary-text-color)"; + e.detail.value.callback(false); + }else if(e.detail.value.component.firstChild.icon.endsWith('-off')){ + // switching on + e.detail.value.component.firstChild.icon = "font-awesome-solid:toggle-on"; + e.detail.value.component.firstChild.style.color = "var(--lumo-primary-color)"; + e.detail.value.callback(true); + }else if(e.detail.value.component.firstChild.icon.endsWith('circle-dot')){ + // switching off + e.detail.value.component.firstChild.icon = "font-awesome-regular:circle"; + e.detail.value.component.firstChild.style.color = "var(--lumo-tertiary-text-color)"; + e.detail.value.callback(false); + }else if(e.detail.value.component.firstChild.icon.endsWith('circle')){ + // switching on + e.detail.value.component.firstChild.icon = "font-awesome-regular:circle-dot"; + e.detail.value.component.firstChild.style.color = "var(--lumo-success-color)"; + e.detail.value.callback(true); + } + + }else{ + e.detail.value.callback(e); + } + + + } +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/notifier.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/notifier.js new file mode 100644 index 0000000000000..2230b3dee7c8c --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/notifier.js @@ -0,0 +1,39 @@ +import { Notification } from '@vaadin/notification'; +import { html} from 'lit'; +import '@vaadin/icon'; +import '@vaadin/horizontal-layout'; + +/* + * Show toast messages. + * TODO: Implement duration + */ +class Notifier { + + showInfoMessage(message, position = "bottom-start", duration = 5) { + this.showMessage("font-awesome-solid:circle-info", "primary", message, position, duration); + } + + showSuccessMessage(message, position = "bottom-start", duration = 5) { + this.showMessage("font-awesome-solid:circle-check", "success", message, position, duration); + } + + showWarningMessage(message, position = "bottom-start", duration = 5) { + this.showMessage("font-awesome-solid:triangle-exclamation", "contrast", message, position, duration); + } + + showErrorMessage(message, position = "bottom-start", duration = 5) { + this.showMessage("font-awesome-solid:circle-exclamation", "error", message, position, duration); + } + + showMessage(icon, theme, message, position = "bottom-start") { + + const notification = Notification.show(html` + ${message}`, { + position: position, + }); + + notification.setAttribute('theme', theme); + } +} + +export const notifier = new Notifier(); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js new file mode 100644 index 0000000000000..dabf7b27b5da4 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/controller/router-controller.js @@ -0,0 +1,274 @@ +import { Router } from '@vaadin/router'; + +let pageNode = document.querySelector('#page'); +pageNode.textContent = ''; + +export class RouterController { + + + static router = new Router(pageNode); + static pathContext = new Map(); // deprecated + static pageMap = new Map(); // deprecated + + /** + * Parse the event change event + */ + static parseLocationChangedEvent(event){ + var component = event.detail.location.route.component; + var path = event.detail.location.route.path; + var name = event.detail.location.route.name; + var title = RouterController.currentTitle(); + var subMenu = RouterController.currentSubMenu(); + + + + return { + 'component': component, + 'path': path, + 'name': name, + 'title': title, + 'subMenu': subMenu, + }; + } + + /** + * Get the header title for the current path + */ + static currentTitle(){ + var currentRoutePath = RouterController.currentRoutePath(); + if (currentRoutePath) { + return RouterController.titleForPath(currentRoutePath); + } + return null; + } + + /** + * Get the header title for a certain path + */ + static titleForPath(path){ + if(path.includes('/dev-ui/')){ + var metadata = RouterController.metaDataForPath(path); + if(metadata && metadata.extensionName){ + return metadata.extensionName; + }else{ + var currentPage = path.substring(path.indexOf('/dev-ui/') + 8); + if(currentPage.includes('/')){ + // This is a submenu + var extension = currentPage.substring(0, currentPage.lastIndexOf("/")); + return RouterController.displayTitle(extension); + }else{ + // This is a main section + return RouterController.displayTitle(currentPage); + } + } + } + return ""; + } + + /** + * Get the sub menu (if any) for the current certain path + */ + static currentSubMenu(){ + var currentRoutePath = RouterController.currentRoutePath(); + if (currentRoutePath) { + return RouterController.subMenuForPath(currentRoutePath); + } + return null; + } + + /** + * Get the sub menu (if any) for a certain path + */ + static subMenuForPath(path){ + + if(path.includes('/dev-ui/')){ + var currentPage = path.substring(path.indexOf('/dev-ui/') + 8); + if(currentPage.includes('/')){ + // This is a submenu + const links = []; + var startOfPath = path.substring(0, path.lastIndexOf("/")); + var routes = RouterController.router.getRoutes(); + + var counter = 0; + var index = 0; + routes.forEach((route) => { + var pageLink = route.path.substring(route.path.indexOf('/dev-ui/') + 8); + if(pageLink.includes('/')){ // To filter out section menu items + if(route.path.startsWith(startOfPath)){ + links.push(route); + if(route.name === RouterController.router.location.route.name){ + index = counter; + } + counter = counter + 1; + } + } + }); + + if (links && links.length > 1) { + return { + 'index': index, + 'links': links + }; + } + } + } + return null; + } + + /** + * Get the metadata for the current path + */ + static currentMetaData() { + var currentRoutePath = RouterController.currentRoutePath(); + if (currentRoutePath) { + return RouterController.metaDataForPath(currentRoutePath); + } + return null; + } + + static currentRoutePath(){ + var location = RouterController.router.location; + if (location.route) { + return location.route.path; + } + return null; + } + + static currentExtensionId(){ + var metadata = RouterController.currentMetaData(); + if(metadata){ + return metadata.extensionId; + } + return null; + } + + /** + * Get all the metadata for a certain path + */ + static metaDataForPath(path) { + if (RouterController.existingPath(path)) { + return RouterController.pathContext.get(path); + }else{ + return null; + } + } + + /** + * Check if we already know about this path + */ + static existingPath(path) { + if (RouterController.pathContext && RouterController.pathContext.size > 0 && RouterController.pathContext.has(path)) { + return true; + } + return false; + } + + /** + * Format a title + */ + static displayTitle(title) { + title = title.charAt(0).toUpperCase() + title.slice(1); + return title.split("-").join(" "); + } + + /** + * Creating the display Title for the Section Menu + */ + static displayMenuItem(pageName) { + pageName = pageName.substring(pageName.indexOf('-') + 1); + pageName = pageName.charAt(0).toUpperCase() + pageName.slice(1); + return pageName.replaceAll('-', ' '); + } + + /** + * This adds a route for Extensions (typically sub-menu pages) + */ + static addExtensionRoute(page){ + RouterController.addRoute(page.id, page.componentName, page.title, page); + } + + /** + * This adds a route for the Menu section + */ + static addMenuRoute(page, defaultSelection){ + var pageref = RouterController.pageRef(page.componentName); + RouterController.addRoute(pageref, page.componentName, page.title, page, defaultSelection); + } + + static basePath(){ + var base = window.location.pathname; + return base.substring(0, base.indexOf('/dev')) + "/dev-ui"; + } + + static pageRef(pageName) { + return pageName.substring(pageName.indexOf('-') + 1); + } + + static pageRefWithBase(pageName){ + return RouterController.basePath() + '/' + RouterController.pageRef(pageName); + } + + /** + * Add a route to the routes + */ + static addRoute(path, component, name, page, defaultRoute = false) { + var base = RouterController.basePath(); + path = base + '/' + path; + + if (!RouterController.existingPath(path)) { + RouterController.pathContext.set(path, page.metadata); // deprecated + RouterController.pageMap.set(path, page) + var routes = []; + var route = {}; + route.path = path; + route.component = component; + route.name = name; + + routes.push({...route}); + + RouterController.router.addRoutes(routes); + } + // TODO: Pass the other parameters along ? + var currentSelection = window.location.pathname; + + var relocationRequest = RouterController.from(); + if (relocationRequest) { + // We know and already loaded the requested location + if (relocationRequest === path) { + Router.go({pathname: path}); + } + } else { + // We know and already loaded the requested location + if (currentSelection === path) { + Router.go({pathname: path}); + // The default naked route + } else if (!RouterController.router.location.route && defaultRoute && currentSelection.endsWith('/dev-ui/')) { + Router.go({pathname: path}); + // We do not know and have not yet loaded the requested location + } else if (!RouterController.router.location.route && defaultRoute) { + Router.go({ + pathname: path, + search: '?from=' + currentSelection, + }); + } + } + } + + static queryParameters() { + const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + + return params; + } + + static from(){ + var params = RouterController.queryParameters(); + if(params){ + return params.from; + }else { + return null; + } + } + +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/favicon.ico b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b4ef4208a6f489de1c17dd3791a3691ae77dee0d GIT binary patch literal 4286 zcmeH~OGs2v7{|XECtu~LmF8KXiW zL}V0H3yD@?4`w0gq6cmw(o~dL)FP#^`FFlMoI7`%nH=YHqh?{~h(y>l*+ zPW&Y&6aS}&QABY>lrBW5C|?Ncl_uu?H}4Uc?kqBVyIIiq{z{&8;#MKV^`fN26boTp zk>1)Y^Li!iWlT+Uuz@{dGOtV4Kpu=xrYO>OA}HiL2FB=Q(4LlNP<;dbX+I z^Lm%2o%4ksx=N3c);)N0E@wfu04e#{5t>xyIhyTW%=0VKN%wl+WSdrP;8N7Sai79j0 zviYym5vs~sF*Z60*(6_HPmYhFx%QOEzvDd|sQhOp8PR`s2VOtDCs9g2pU=wPb_;gV zTSU@Umpb&P{`sBSQMn0Iw;jKOzVf*~O34lr-9-q+EBo+=e|&cWK40If)|Qq(Jw1)4 zrY1#rEF1gp-~&bU9f?%qH=pyjR>a`Ou?!^!%(vNWFq_Tj>gvMW+?*mjW*yHt_;zfe zZUj@Dngf6Q*VfPGga}-1s6|Ul3#?YFbXVD{>69P?PObAR`;-iPy-&*x9B<;94P=g%dbzqRKlAI6@}-%I-b@PR3x6OZ2? vB1e1% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +document.head.appendChild(template.content); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-regular.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-regular.js new file mode 100644 index 0000000000000..d5d279968f8f7 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-regular.js @@ -0,0 +1,174 @@ +import '@vaadin/icon'; + +const template = document.createElement('template'); + +template.innerHTML = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +document.head.appendChild(template.content); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-solid.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-solid.js new file mode 100644 index 0000000000000..70b8b40317e09 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome-solid.js @@ -0,0 +1,1399 @@ +import '@vaadin/icon'; + +const template = document.createElement('template'); + +template.innerHTML = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`; + +document.head.appendChild(template.content); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome.js new file mode 100644 index 0000000000000..7d92cc404d9e7 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/icon/font-awesome.js @@ -0,0 +1,7 @@ +import './font-awesome-brands.js'; +import './font-awesome-regular.js'; +import './font-awesome-solid.js'; + +export * from './font-awesome-brands.js'; +export * from './font-awesome-regular.js'; +export * from './font-awesome-solid.js'; diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/quarkus-logo.png b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/quarkus-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cd25591647a63155e1b28b08d6e9b3a6a46c82f9 GIT binary patch literal 6206 zcmV-E7{TX>P)lf6I{qwPlv4Wyt^)u7000T%BC^Ixsfzv_;9>q^r<7VMr4}T*000000Dd6} z7mi6pKv>yXE2U;isa47s2><{9fH2rZR-eSx4W5M)4@#+NM%N1f004j%I483De}8Jb=jA0ssI2I?De}d~FD<%ekU^*w6io00000pc$t`76g_bt|;6J0001h4pJ9c zGYBlN+#o*-0002!CT337P$~5T;r_lYVmmYh zs99%hOD_Nb0H7_j4LZlgwn%%jj>hkw20wr3U=hv{2`3kJ%!4eEKAo@}(>=Rx@Etca zzEA*+wGTIc zP^h#|3(>Tt+`bJhEQ`d$ug6LJ&GP@hB(nI}DiN=l#WN+iREPJOZ3f$JD60*Ixq(S- zJ{{-Bz2*OhTu(>zS+>pff#!?PakjG9m{jH`_niI+crE9s=+$JSZ{tAEXlZi-=)`xI zhxJ5;w9Da~7rJLp4nLEN!D_;gMCX>odRvDlj=%S-v5V`PcMzP0J6wXpC$H&|!TF?= z`kSH{OEN?#JM)@hB!LAkr!s7kZ-J#~Ox)1#|6$CbS7?zXHCDR}UGHr&rF$Zw)P~wK zT3kLg2%|RT_&K-qxyW?`+?q6jcwo`SVv9NTn?4y%Ze5!|U?qjF_4ydM9iW~=OYk0B{z<>AZp5}I!=ajT4_Z|Ax zCWW?3ZFsJvyvmi+B1@|FaN_GNU7TP?!}dL)rr}p~J@B%259u>U$k2^J$LEw(V4>i2 zI!JQo+U8Jqy!;vEGF~&Ke4e(^r`@EUCn;U4f3=`cT@%cN+jIAAOwX!GUe_aeW#Mt{ z21AD(>lH1+2ppto5o^HbHmjG{b+2CN=LHB zJwRY3$3kc$w5Mt5s0D^b>4&jJgufXVDTpjZll~j2%F1I{a!le%D^Y|yn=^G3SP#aD z!=~toFg1j-#6aAVsKY{7Hm-uHguJY6gUx8&m}rCHQfg&M2Bu`sy+mL=A@rOa(((+? z^xh_8xL%iRNb5;u%%R<>xp}HVbWE=4DCGRYMDae|kZ+L_;7Vaa9!Cksf&8w8I%>LP z!x!QE9}6#=(RFOz&Gngo@4>_Q3vDn)RBvh{_?bcB;-V4?ER3J@kfev)ksb53N!XH3 z8IisFaxU#DHIb$HbU~$N9TsewHw0eOckl>(4p2jFM#Uv1r8AZ|x?e0duas@kg^_&C ziVTIM=3l8IOHzy*0xM0%g6PKQmZKWF>`7K4OVO_L(J1`MF-f4LK#53V-N!ih$3O=d zlu@#Q+ky8GGAxoI)ucJahJ?d85q^y#wkN5`3vHuBPoP9#Z4e?%7A^%~9ndbFBxRAM zxuUS&^339>k8S>)P)UJOm{^RNNfqHh!Jfo7=h3=Bjiln>`a&vZJi=g}rlJj#jgms} zBY^?l(0A_IDe_$4YPm%fSSk~drMaPeEpvgKQ7sqbUA2Q`fPRj6VslRdiZcw+CmQgI zO=K-TIuolC_GA?&UnSmFJmlFx_=i$QZg97p#T^c?^VC72^B}Z}*S!+smUeJwuq}PIs zy!3<&ki&&k4hb2mx#0PuKxrA+#$0{ZYTLy^WSwR!2*KyvZb#CO5CRK$BXyCrqlD^; z4C%MpF7}%Kt)d<7ogj1sct(QyIhWSJ@(m{n2Po-~J+_2*b;1i{^U5Fr{ye1?9o78^ zZ610pu+SL*cp+7hb)dQvBYOQ{Vz0L}DTa=s<|~oWp`bNFRiS zE5W~v1PSk{p4Q_|lJp?7vc;D0t}G(UHYNrb4~@tzS7{+QfqsX+wpn5m#k~}SRth3( zM+3YfQS{BBpO%elD(_IyjsA>8MiHFRITe;kg}vakDe|)N#O8HcTk-?9%$R^T%blFO zJ(7NecPLt%E!MuO{mhQKP0t*|y`j|BKCq?09-$m-MJw>5Lt1w|+H`q;4a!iLH==+la} zLZ-Uu^sIn>Tt48$M-E;rB?xCgWaUAhNnCc#v9uZBTdV=U?hn^6In|ApwBa)ZR+-Ek zTI=afF{==ps}Le2*tQzg?A1v0lq66tUiTUBvFQ4qFz2pKSIqt4m))MEwpS-85=j`-P+Sy zaq)>npCYSIVux$y+yq@vbWE>15?m9S{6AVKWU6z-kgxS0rHCxw;}iJj(%M+(;Ogp> z8ZzwD+Bv0r@Q<)Q;epoK)Vx400!y>RTHXovG><=CP#UU78sIzWfX3^>pPugcSm?67 z1f8!{(dX&#V93veCkKG`;S%02J>j;n^^G{wdj$Z zf=8u= zxwOpzWx^%A`>b`eXL2+q9r1L8$ZKsAEfVWSZX&Qb$j<`GDAzEiDBME>y>fd_X!$x_ z$T2sv@+FnuL-NJ-v_Qdd_aGOegM5oT;gU&j1Fj!EGSsPgcXkcD>_b5ac9r28c%Zp{ zYICS_aQ;8$%-R>NVVMt`tueM0I+MJalXD%~c^Ai@hK$_Qp6Q&Q$1qnhG}cgUs{MK)cLI zY}s0JnY=nw6GLQ~R1OWGFXcdr6fyYoaH2CcBX|z39TGwV>5-}@rIZBtx!@3zualdp zA<56W>3Hc5U}sO`=(FHHU7H)0*dl8|hJECrP?!zfK)R2KWJTiHOFiGIFNmQ-WTi#l zcT-l6l#~F^g=4P^Z7Z}Fj<2sBEZqUzm4>r>aU`53YUZsb=BgE#M?KA69Qd_^YWzb$>Odrgcqv>PgzJIn!0znB<~K_{;XGU zO2w?8{H#=jU{?hxw`ToLtw}{>X>Q3t4e25!I}a1H70%Jv{oe`+F2(N5}~b{sDh?vrHoKhZ+CrPn20x4Vo)grpii83{a=DU@$W!C4Njv9+b=zKXtI$|6g1O-&dpa=(41FSn)Kt%^k0w~R^*qz>oJx=JuNOL9Uj zMNOF@3ptl*p*Nq*K<71T1uC^SxVrk z>J?l&OVJ$Q!lt(opF1Z~VyA6zpWpe;8Y&}^r5iY(dZ;-xpw1C)alK9`H)}|Fd^UVG z7nZ;+Ffl_h6(dMPERI~ldyJf*6=QTuN`O~n#JRRJw*~hCmF8#7BYB?pOmg*A#PP^Q z4h|4m|B{2q`a%QAw&?1z_iYw5?HY4|aE+a_U|t7DXwTc0G1!BMJ7n&2LwJxlkr~T_ z!hLROc}g?EJTKXs^&%)P88bhN6DqMH3S8u%3j&MVeEhlxDT^%E2F|H9pw6AWU~%mw zFq!6pG93?*`c!h_FUA5{G50G}Sk<-*UPtUJ%6<1z3X^K2N$@4NBby5j`^3u6;y&{> zf|qTW(5c{944HNG_Xayfk+q?dgiRYb>ju;x1@U%ZM{?BrD&mBy1f@C-5e)=)Uj~5} z?xEoGLR=3Qu8g@ra|!T+0)^H>Q1fS6Sg5p4m1us}R^T)U?PXgq^LG~^pE5L@P5JG7 zNJ(Vr2F@d4(l9sSm-L=c4X8^*vddMp_=IjpQBk=L$@^j*CEf}`*q;I|+8roTaxqg*mADVke&@!;$$8`kaBMJ&<7OG zU4zZ5aZ{jIXC+u8ZwgL=R}T4Eq1Tj^z?*eV?`w0TyAX(rO+Zg{jLptS@cefvd9qfN zcQtibSjP>z#c%2TA5goa5mP}pL6H&v-eP^7ik=@g(G^{93Q5Xa;m+hV@NBM)bQzzF9M;{C^Wu?o!y-^#+LYF4uGsmj zH7>Tunl;F~(hspoallmvV#RGS778sz3j2U>UAJOMgmYTvazW=8_AMTq-QM-s9#(K(KK4YA#` zA$=dchDbKFnx_aG4^Fr2XwB5Gz451wrfcbee(u%p&rM{V8es1u8`Lg|EOmj(wO zwvT?3o5Va=b$s2s1C4oC0eXtNfFYn2bIpGx_UtCqKlL2XEqOCyi!8~S*7kZ9TVyHW z;hGp5eOIA%jtz-|OV{2Q0!umQ78@H{mt6$qDb8;0b84$IJTtILZDN`#dm2o>4ia2@ z5?D`(3oK>aJ89VU96b*L>tHli4*{3({*>UelO({G*xOHyy;$z>X?a zmNdX()FL|=EthgT%%JO!eF!X4ceF6a@c7w`_AYW7f(=e&mFh{~()_Y9=s+Z4FFe^7 zqwS79RmYu%^v#2?o@L4GXmdZ|*3w)=7S|`K>uNeCd%FL+QFq&-VP}XdY0ITl(Ip%1 zJcJOrdv+)20ooQ^h4++;Tyhn+2L;!TlJqaVFT4`GA}@ucSZj>U-j)pghn8zW9!Old zmRxF+cVr7O5 zQCw*_Pjl*?$gYMAr5ztit^}`<R8Q}h&DV}t+R z_};#wDi^J#Ih-!!PMK(8op9*d>B1(AjnWK5%g$Gl%pKx%RSNV;c#Y`ql8*2D+Hqw7 zK)2Cr$k13z&hb)PLsAk{uJn!Xt?iA({_bn{;bOg$gUH%DoPdaN1RNsEAJTq=AJm%Jh&R%(a_002NCXptp7${pIUxrT*e7+x5A zzesDqKxhE~07x1wvZS$B*T&43P&FtLHV9nKVdij+T|VyWinJj$gcbk*fMn4k%S8Y@ zlDKHQCP>rd<`71=PoXVILcS)oUV16xQW+gW3jhE>lIXL-W^GkRt=Kj0RR9bg%(*#&~ZK|9)wBoT(@~&NUsOH0gMtxL|23p@JVXm8>dY3?9|761T%sGMpL{X)jccay~!NRrK8JT>Gs*-*VM z2rU0p!spdP=nwz^Kuk`FEJea&)Pnd3%|uu_!DnLn8XlBZ**k0kAhZAg07T=Q$WoN7 zJ)(r;T)kR7VFmc5hwr$fQ*dK=uv({y&89LP0dLeHL`Vj&E007`5O+^*}00000 cIEPZ||26LBnbg^%d;kCd07*qoM6N<$f+L@%+yDRo literal 0 HcmV?d00001 diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js new file mode 100644 index 0000000000000..c7f252b60be94 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-badge.js @@ -0,0 +1,126 @@ +import { LitElement, html, css} from 'lit'; +import '@vaadin/icon'; + +/** + * Badge UI Component based on the vaadin theme one + * see https://vaadin.com/docs/latest/components/badge + */ +export class QuiBadge extends LitElement { + static styles = css` + [theme~="badge"] { + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + padding: 0.4em calc(0.5em + var(--lumo-border-radius-s) / 4); + color: var(--lumo-primary-text-color); + background-color: var(--lumo-primary-color-10pct); + border-radius: var(--lumo-border-radius-s); + font-family: var(--lumo-font-family); + font-size: var(--lumo-font-size-s); + line-height: 1; + font-weight: 500; + text-transform: initial; + letter-spacing: initial; + min-width: calc(var (--lumo-line-height-xs) * 1em + 0.45em); + } + [theme~="success"] { + color: var(--lumo-success-text-color); + background-color: var(--lumo-success-color-10pct); + } + [theme~="error"] { + color: var(--lumo-error-text-color); + background-color: var(--lumo-error-color-10pct); + } + [theme~="contrast"] { + color: var(--lumo-contrast-80pct); + background-color: var(--lumo-contrast-5pct); + } + [theme~="small"] { + font-size: var(--lumo-font-size-xxs); + line-height: 1; + } + [theme~="tiny"] { + font-size: var(--lumo-font-size-xxs); + line-height: 1; + padding: 0.2em calc(0.2em + var(--lumo-border-radius-s) / 4); + } + [theme~="primary"] { + color: var(--lumo-success-contrast-color); + background-color: var(--lumo-success-color); + } + [theme~="pill"] { + --lumo-border-radius-s: 1em; + } + `; + + static properties = { + background: {type: String}, + color: {type: String}, + icon: {type: String}, + level: {type: String}, + small: {type: Boolean}, + tiny: {type: Boolean}, + primary: {type: Boolean}, + pill: {type: Boolean}, + _theme: {attribute: false}, + _style: {attribute: false}, + }; + + constructor(){ + super(); + this.icon = null; + this.level = null; + this.background = null; + this.color = null; + this.small = false; + this.small = false; + this.primary = false; + this.pill = false; + + this._theme = "badge"; + this._style = ""; + } + + connectedCallback() { + super.connectedCallback() + + if(this.level){ + this._theme = this._theme + " " + this.level; + } + if(this.small && !this.tiny){ + this._theme = this._theme + " small"; + } + if(this.tiny){ + this._theme = this._theme + " tiny"; + } + if(this.primary){ + this._theme = this._theme + " primary"; + } + if(this.pill){ + this._theme = this._theme + " pill"; + } + + if(this.background){ + this._style = this._style + "background: " + this.background + ";"; + } + if(this.color){ + this._style = this._style + "color: " + this.color + ";"; + } + } + + render() { + return html` + ${this._renderIcon()} + + `; + } + + _renderIcon(){ + if(this.icon){ + return html``; + } + } + +} +customElements.define('qui-badge', QuiBadge); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js new file mode 100644 index 0000000000000..066ac5e4ceb80 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-build-steps.js @@ -0,0 +1,29 @@ +import { LitElement, html, css} from 'lit'; +import { buildSteps } from 'devui-data'; + +/** + * This component shows the Build Steps Page + */ +export class QwcBuildSteps extends LitElement { + static styles = css` + .todo { + padding-left: 10px; + height: 100%; + }`; + + static properties = { + _steps: {state: true} + }; + + constructor() { + super(); + this._steps = buildSteps; + } + + render() { + if(this._steps){ + return html`
${this._steps}
`; + } + } +} +customElements.define('qwc-build-steps', QwcBuildSteps); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js new file mode 100644 index 0000000000000..23f97335d38fc --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration.js @@ -0,0 +1,30 @@ +import { LitElement, html, css} from 'lit'; +import { allConfiguration } from 'devui-data'; + +/** + * This component allows users to change the configuration + */ +export class QwcConfiguration extends LitElement { + static styles = css` + .todo { + padding-left: 10px; + height: 100%; + }`; + + static properties = { + _configurations: {state: true} + }; + + constructor() { + super(); + this._configurations = allConfiguration; + } + + render() { + if(this._configurations){ + return html`
${this._configurations}
`; + } + } + +} +customElements.define('qwc-configuration', QwcConfiguration); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js new file mode 100644 index 0000000000000..4f17fd2f3068a --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js @@ -0,0 +1,31 @@ +import { LitElement, html, css} from 'lit'; +import { continuousTesting } from 'devui-data'; + +/** + * This component shows the Continuous Testing Page + */ +export class QwcContinuousTesting extends LitElement { + + static styles = css` + .todo { + padding-left: 10px; + height: 100%; + }`; + + static properties = { + _tests: {state: true} + }; + + constructor() { + super(); + this._tests = continuousTesting; + } + + render() { + if(this._tests){ + return html`
${this._tests}
`; + } + } + +} +customElements.define('qwc-continuous-testing', QwcContinuousTesting); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-qute-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-qute-page.js new file mode 100644 index 0000000000000..d3afc8c1ff099 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-qute-page.js @@ -0,0 +1,28 @@ +import { LitElement, html, css} from 'lit'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { RouterController } from 'router-controller'; + +/** + * This component renders build time data using qute + */ +export class QwcDataQutePage extends LitElement { + + static styles = css``; + + static properties = { + _htmlFragment: {attribute: false}, + }; + + connectedCallback() { + super.connectedCallback(); + var metadata = RouterController.currentMetaData(); + if(metadata){ + this._htmlFragment = metadata.htmlFragment; + } + } + + render() { + return html`${unsafeHTML(this._htmlFragment)}`; + } +} +customElements.define('qwc-data-qute-page', QwcDataQutePage); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js new file mode 100644 index 0000000000000..4c9a3b422c49a --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-raw-page.js @@ -0,0 +1,76 @@ +import { LitElement, html, css} from 'lit'; +import { RouterController } from 'router-controller'; +import { observeState } from 'lit-element-state'; +import { themeState } from 'theme-state'; +import '@vanillawc/wc-codemirror'; +import '@vanillawc/wc-codemirror/mode/javascript/javascript.js'; + +/** + * This component renders build time data in raw json format + */ +export class QwcDataRawPage extends observeState(LitElement) { + + static styles = css` + .codeBlock { + display:flex; + gap: 10px; + flex-direction: column; + padding-left: 10px; + padding-right: 10px; + } + .jsondata { + height: 100%; + overflow: scroll; + padding-bottom: 100px; + } + `; + + static properties = { + _buildTimeDataKey: {attribute: false}, + _buildTimeData: {attribute: false}, + }; + + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + var extensionId = RouterController.currentExtensionId(); + if(extensionId){ + + var metadata = RouterController.currentMetaData(); + if(metadata){ + + this._buildTimeDataKey = metadata.buildTimeDataKey; + + let modulePath = extensionId + "-data"; + + import(modulePath) + .then(obj => { + this._buildTimeData = obj[this._buildTimeDataKey]; // TODO: Just use obj and allow multiple keys ? + }); + } + } + } + + + render() { + + var json = JSON.stringify(this._buildTimeData, null, '\t'); + + return html`
+ + + + +
`; + } + +} +customElements.define('qwc-data-raw-page', QwcDataRawPage); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-table-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-table-page.js new file mode 100644 index 0000000000000..f7d00cf59da19 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-data-table-page.js @@ -0,0 +1,77 @@ +import { LitElement, html, css} from 'lit'; +import { RouterController } from 'router-controller'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; + +/** + * This component renders build time data in a table + */ +export class QwcDataTablePage extends LitElement { + + static styles = css` + .datatable { + height: 100%; + padding-bottom: 10px; + } + `; + + static properties = { + _buildTimeDataKey: {attribute: false}, + _buildTimeData: {attribute: false}, + _cols: {attribute: false}, + }; + + connectedCallback() { + super.connectedCallback(); + var extensionId = RouterController.currentExtensionId(); + if(extensionId){ + + var metadata = RouterController.currentMetaData(); + if(metadata){ + + this._buildTimeDataKey = metadata.buildTimeDataKey; + + let modulePath = extensionId + "-data"; + + import(modulePath) + .then(obj => { + this._buildTimeData = obj[this._buildTimeDataKey]; + + if(metadata.cols){ + this._cols = metadata.cols.split(','); + }else{ + this._autodetectCols(); + } + }); + } + } + } + + + render() { + if(this._cols){ + const colTemplates = []; + + for (const col of this._cols) { + colTemplates.push(html``); + } + + return html` + ${colTemplates} + `; + } + } + + _autodetectCols(){ + if(this._buildTimeData){ + var row = this._buildTimeData[0]; + if(row){ + this._cols = Object.getOwnPropertyNames(row); + }else{ + this._cols = []; + } + } + } + +} +customElements.define('qwc-data-table-page', QwcDataTablePage); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js new file mode 100644 index 0000000000000..51128e9a5ed00 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-dev-services.js @@ -0,0 +1,29 @@ +import { LitElement, html, css} from 'lit'; +import { devServices } from 'devui-data'; + +/** + * This component shows the Dev Services Page + */ +export class QwcDevServices extends LitElement { + static styles = css` + .todo { + padding-left: 10px; + height: 100%; + }`; + + static properties = { + _services: {state: true} + }; + + constructor() { + super(); + this._services = devServices; + } + + render() { + if(this._services){ + return html`
${this._services}
`; + } + } +} +customElements.define('qwc-dev-services', QwcDevServices); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js new file mode 100644 index 0000000000000..61d19eea277d3 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension-link.js @@ -0,0 +1,97 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import 'qui-badge'; + +/** + * This component adds a custom link on the Extension card + */ +export class QwcExtensionLink extends LitElement { + + static styles = css` + .extensionLink { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + color: var(--lumo-contrast); + font-size: small; + padding: 2px 5px; + cursor: pointer; + text-decoration: none; + } + .extensionLink:hover { + filter: brightness(80%); + } + .icon { + padding-right: 5px; + } + .iconAndName { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + color: var(--lumo-contrast); + } + `; + + static properties = { + extensionName: {type: String}, + iconName: {type: String}, + displayName: {type: String}, + staticLabel: {type: String}, + dynamicLabel: {type: String}, + streamingLabel: {type: String}, + path: {type: String}, + webcomponent: {type: String}, + _effectiveLabel: {state: true}, + _observer: {state: false}, + }; + + connectedCallback() { + super.connectedCallback(); + if(this.streamingLabel){ + this.jsonRpc = new JsonRpc(this.extensionName); + this._observer = this.jsonRpc[this.streamingLabel]().onNext(jsonRpcResponse => { + this._effectiveLabel = jsonRpcResponse.result; + }); + }else if(this.dynamicLabel){ + this.jsonRpc = new JsonRpc(this.extensionName); + this.jsonRpc[this.dynamicLabel]().then(jsonRpcResponse => { + this._effectiveLabel = jsonRpcResponse.result; + }); + }else if(this.staticLabel){ + this._effectiveLabel = this.staticLabel; + } + } + + disconnectedCallback() { + if(this._observer){ + this._observer.cancel(); + } + super.disconnectedCallback() + } + + render() { + let routerIgnore = false; + if(this.webcomponent === ""){ + routerIgnore = true; + } + return html` + + + + ${this.displayName} + + ${this._renderBadge()} + + `; + } + + _renderBadge() { + if (this._effectiveLabel) { + return html`${this._effectiveLabel}`; + } + } +} +customElements.define('qwc-extension-link', QwcExtensionLink); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js new file mode 100644 index 0000000000000..d5baa21d332bf --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extension.js @@ -0,0 +1,244 @@ +import { LitElement, html, css} from 'lit'; +import '@vaadin/icon'; +import '@vaadin/dialog'; +import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; + +/** + * This component represent one extension + * It's a card on the extension board + */ +export class QwcExtension extends LitElement { + + static styles = css` + .card { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + border: 1px solid var(--lumo-contrast-10pct); + border-radius: 4px; + width: 300px; + filter: brightness(90%); + } + + .card-header { + height: 25px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 10px 10px; + background-color: var(--lumo-contrast-5pct); + border-bottom: 1px solid var(--lumo-contrast-10pct); + } + + .card-footer { + height: 20px; + padding: 10px 10px; + color: var(--lumo-contrast-50pct); + display: flex; + flex-direction: row; + justify-content: space-between; + visibility:hidden; + } + + .active:hover { + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + } + + .active .card-header{ + color: var(--lumo-contrast); + } + + .active:hover .card-footer, .active:hover .guide { + visibility:visible; + } + + .inactive:hover .card-footer, .inactive:hover .guide { + visibility:visible; + } + + .guide{ + visibility:hidden; + } + + .icon { + font-size: x-small; + cursor: pointer; + } + `; + + static properties = { + _dialogOpened: {state: true}, + name: {type: String}, + namespace: {type: String}, + description: {type: String}, + guide: {type: String}, + clazz: {type: String}, + artifact: {type: String}, + shortName: {type: String}, + keywords: {}, + status: {type: String}, + configFilter: {}, + categories: {}, + unlisted: {type: String}, + builtWith: {type: String}, + providesCapabilities: {}, + extensionDependencies: {}, + }; + + constructor() { + super(); + this._dialogOpened = false; + } + + render() { + + return html` + html` + + + + `, + [] + )} + ${dialogRenderer(() => this._renderDialog(), this.name)} + > + +
+ ${this._headerTemplate()} + + ${this._footerTemplate()} +
`; + } + + _headerTemplate() { + return html`
+

${this.name}

+ ${this.guide? + html``: + html`` + } +
+ `; + } + + _footerTemplate() { + return html` + + `; + } + + _renderDialog(){ + return html` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name${this.name}
Namespace${this.namespace}
Description${this.description}
Guide${this._renderGuideDetails()}
Artifact${this._renderArtifact()}
Short name${this.shortName}
Keywords${this._renderKeywordsDetails()}
Status${this.status}
Config Filter${this.configFilter}
Categories${this.categories}
Unlisted${this.unlisted}
Built with${this.builtWith}
Provides capabilities${this.providesCapabilities}
Extension dependencies${this._renderExtensionDependencies()}
+ `; + } + + _renderGuideDetails() { + return this.guide + ? html`${this.guide}` + : html``; + } + + _renderKeywordsDetails() { + return this._renderCommaString(this.keywords); + } + + _renderExtensionDependencies() { + return this._renderCommaString(this.extensionDependencies); + } + + _renderArtifact(){ + if(this.artifact){ + return html`${this.artifact}`; + }else{ + return html``; + } + } + + _renderCommaString(cs){ + if(cs) { + var arr = cs.split(','); + return html`
    ${arr.map(v => + html`
  • ${v}
  • ` + )}
`; + }else{ + return html``; + } + } + + _guide(e) { + window.open(this.guide, '_blank').focus(); + } + + _configuration(e) { + console.log("Show config with filter: " + this.configFilter); + } +} + +customElements.define('qwc-extension', QwcExtension); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js new file mode 100644 index 0000000000000..c5e5f9a9ed40f --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-extensions.js @@ -0,0 +1,166 @@ +import { LitElement, html, css} from 'lit'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { RouterController } from 'router-controller'; +import { devuiState } from 'devui-state'; +import { observeState } from 'lit-element-state'; +import 'qwc/qwc-extension.js'; +import 'qwc/qwc-extension-link.js'; + +/** + * This component create cards of all the extensions + */ +export class QwcExtensions extends observeState(LitElement) { + + static styles = css` + .grid { + display: flex; + flex-wrap: wrap; + gap: 20px; + padding-left: 5px; + padding-right: 10px; + } + + .description { + padding-bottom: 10px; + } + + .card-content { + color: var(--lumo-contrast-90pct); + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 10px 10px; + height: 100%; + } + + .card-content slot { + display: flex; + flex-flow: column wrap; + padding-top: 5px; + } + `; + + constructor() { + super(); + // TODO: Change this to use state + window.addEventListener('vaadin-router-location-changed', (event) => { + var pageDetails = RouterController.parseLocationChangedEvent(event); + }); + } + + render() { + return html`
+ ${devuiState.cards.active.map(extension => this._renderActive(extension))} + ${devuiState.cards.inactive.map(extension => this._renderInactive(extension))} +
`; + } + + _renderActive(extension){ + extension.cardPages.forEach(page => { + if(page.embed){ // we need to register with the router + import(page.componentRef); + RouterController.addExtensionRoute(page); + } + }); + + return html` + + + ${this._renderCardContent(extension)} + + + + `; + } + + _renderCardContent(extension){ + if(extension.card){ + return this._renderCustomCardContent(extension); + } else { + return this._renderDefaultCardContent(extension); + } + } + + _renderCustomCardContent(extension){ + import(extension.card.componentRef); + + // TODO: Pass description and links along ${this._renderCardLinks(extension)} + let customCardCode = `<${extension.card.componentName} + class="card-content" + slot="content" + description="${extension.description}"> + + `; + + return html`${unsafeHTML(customCardCode)}`; + + } + + _renderDefaultCardContent(extension){ + + return html`
+ ${extension.description} + ${this._renderCardLinks(extension)} +
`; + } + + _renderCardLinks(extension){ + + return html`${extension.cardPages.map(page => html` + + + `)}`; + } + + _renderInactive(extension){ + if(extension.unlisted === "false"){ + return html` + +
+ ${extension.description} +
+
+ `; + } + } + +} +customElements.define('qwc-extensions', QwcExtensions); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js new file mode 100644 index 0000000000000..43b7931d04188 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-external-page.js @@ -0,0 +1,112 @@ +import { LitElement, html, css} from 'lit'; +import { until } from 'lit/directives/until.js'; +import { RouterController } from 'router-controller'; +import { observeState } from 'lit-element-state'; +import { themeState } from 'theme-state'; +import '@vanillawc/wc-codemirror'; +import '@vanillawc/wc-codemirror/mode/yaml/yaml.js'; +import '@vanillawc/wc-codemirror/mode/properties/properties.js'; +import '@vanillawc/wc-codemirror/mode/javascript/javascript.js'; +import '@vaadin/icon'; + +/** + * This component loads an external page + */ +export class QwcExternalPage extends observeState(LitElement) { + + static styles = css` + .codeBlock { + display:flex; + gap: 10px; + flex-direction: column; + padding-left: 10px; + padding-right: 10px; + } + .download { + padding-left: 6px; + color: var(--lumo-contrast-50pct); + font-size: small; + cursor: pointer; + } + `; + + static properties = { + _externalUrl: {type: String}, + _mode: {type: String}, + _mimeType: {type: String} + }; + + connectedCallback() { + super.connectedCallback(); + var metadata = RouterController.currentMetaData(); + if(metadata){ + this._externalUrl = metadata.externalUrl; + if(metadata.mimeType){ + this._mimeType = metadata.mimeType; + this._deriveModeFromMimeType(this._mimeType); + }else{ + this._autoDetectMimeType(); + } + } + } + + render() { + return html`${until(this._loadExternal(), html`Loading with ${themeState.theme.name} theme`)}`; + } + + _autoDetectMimeType(){ + if(this._externalUrl){ + fetch(this._externalUrl) + .then((res) => { + this._mimeType = res.headers.get('content-type'); + this._deriveModeFromMimeType(this._mimeType); + } + ); + } + } + + _deriveModeFromMimeType(mimeType){ + if(mimeType.startsWith('application/yaml')){ + this._mode = "yaml"; + }else if(mimeType.startsWith('application/json')){ + this._mode = "javascript"; + }else if(mimeType.startsWith('text/html')){ + this._mode = "html"; + }else if(mimeType.startsWith('application/pdf')){ + this._mode = "pdf"; + }else{ + this._mode = "properties"; + } + } + + _loadExternal(){ + if(this._mode){ + if(this._mode == "html" || this._mode == "pdf"){ + return html` + `; + } else { + return html`
+ + + Download + + + + +
+ `; + } + } + } + + _download(e) { + window.open(this._externalUrl, '_blank').focus(); + } +} +customElements.define('qwc-external-page', QwcExternalPage); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-footer.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-footer.js new file mode 100644 index 0000000000000..e0647a0ee80a6 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-footer.js @@ -0,0 +1,333 @@ +import { LitElement, html, css} from 'lit'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; +import { LogController } from 'log-controller'; +import { devuiState } from 'devui-state'; +import { observeState } from 'lit-element-state'; +import '@vaadin/tabsheet'; +import '@vaadin/tabs'; +import '@vaadin/icon'; +import '@vaadin/menu-bar'; +import 'qwc/qwc-ws-status.js'; + +/** + * This component shows the Bottom Drawer + * + */ +export class QwcFooter extends observeState(LitElement) { + + static styles = css` + + vaadin-menu-bar { + --lumo-size-m: 10px; + --lumo-space-xs: 0.5rem; + --_lumo-button-background-color: transparent; + } + + .openIcon { + cursor: pointer; + font-size: small; + } + + .openIcon:hover { + color: var(--quarkus-blue); + } + + #footer { + background: var(--lumo-contrast-5pct); + overflow: hidden; + margin-right: 5px; + margin-left: 5px; + border-radius: 15px 15px 0px 0px; + display: flex; + flex-direction: column; + } + .footerOpen { + + } + .footerClose { + max-height: 38px; + overflow: hidden; + } + + .dragOpen { + overflow: hidden; + height: 3px; + cursor: row-resize; + background: var(--lumo-contrast-10pct); + } + + .dragOpen:hover { + background: var(--quarkus-blue); + } + + .dragClose { + display:none; + } + + vaadin-tabsheet { + overflow: hidden; + } + .tabsheetOpen { + height: 100%; + } + .tabsheetClose { + max-height: 38px; + justify-content: flex-start; + } + + vaadin-tabs { + max-width: 100%; + overflow: hidden; + } + .tabsOpen { + height: 100%; + } + + .tabsClose { + max-height: 0px; + visibility: collapse; + } + + vaadin-tab { + overflow: hidden; + } + .tabOpen { + + } + .tabClose { + max-height: 0px; + visibility: hidden; + } + + .controlsOpen { + } + + .controlsClose { + display:none; + } + + .tabContentOpen { + font-size: var(--lumo-font-size-s); + overflow: auto; + } + + .tabContentClose { + font-size: var(--lumo-font-size-s); + overflow: hidden; + max-height: 0px; + visibility: hidden; + } + + `; + + static properties = { + _isOpen: {state: true}, + _footerClass: {state: false}, + _tabsheetClass: {state: false}, + _tabsClass: {state: false}, + _tabClass: {state: false}, + _tabContentClass: {state: false}, + _dragClass: {state: false}, + _controlsClass: {state: false}, + _arrow: {state: false}, + _controlButtons: {state: true}, + _selectedTab: {state: false}, + _height: {state: false}, + _originalHeight: {state: false}, + _originalMouseY: {state: false}, + }; + + connectedCallback() { + super.connectedCallback(); + this._controlButtons = []; + this._originalMouseY = 0; + + this._restoreState(); + this._restoreHeight(); + this._restoreSelectedTab(); + } + + _restoreHeight(){ + const storedHeight = localStorage.getItem("qwc-footer-height"); + if(storedHeight){ + this._height = storedHeight; + }else { + this._height = 250; // Default initial height + } + this._originalHeight = this._height; + } + + _restoreState(){ + const storedState = localStorage.getItem("qwc-footer-state"); + if(storedState && storedState === "open"){ + this._open(); + }else { + this._close(); + } + } + + _restoreSelectedTab(){ + const storedTab = localStorage.getItem("qwc-footer-selected-tab"); + if(storedTab){ + this._tabSelected(storedTab); + }else { + this._tabSelected(0); + } + } + + render() { + + var selectedComponentName = devuiState.footer[this._selectedTab]; + if(!selectedComponentName){ // Might have been removed + this._tabSelected(0); + } + + return html``; + } + + _renderTabHeaders(){ + return html`${devuiState.footer.map((footerTab, index) => + this._renderTabHeader(footerTab, index) + )}`; + } + + _renderTabHeader(footerTab, index){ + import(footerTab.componentRef); + return html` this._tabSelected(index)}>${footerTab.title}`; + } + + _renderControls(){ + return html` + `; + } + + _renderTabBodies(){ + + return html`${devuiState.footer.map((footerTab, index) => + html`
+ ${this._renderTabBody(footerTab)} +
` + )}`; + } + + _renderTabBody(footerTab){ + return html`${unsafeHTML('<' + footerTab.componentName + '>')}`; + } + + _tabSelected(index){ + this._selectedTab = index; + var selectedComponentName = devuiState.footer[this._selectedTab]; + if(selectedComponentName){ + this._controlButtons = LogController.getItemsForTab(devuiState.footer[this._selectedTab].componentName); + }else{ + this._controlButtons = LogController.getItemsForTab(devuiState.footer[0].componentName); + this._selectedTab = 0; + } + localStorage.setItem('qwc-footer-selected-tab', this._selectedTab); + } + + _mousedown(e){ + this._originalHeight = this._height; + this._originalMouseY = e.y; + document.addEventListener('mousemove', this._mousemove, true); + document.addEventListener('mouseup', this._mouseup, true); + } + + _mousemove = (e) => { + const height = this._originalHeight - (e.y - this._originalMouseY); + this._height = height; + + if(this._height<=70){ + this._height = this._originalHeight; + this._doubleClicked(e); + this._mouseup(); + } + } + + _mouseup = (e) => { + document.removeEventListener('mousemove', this._mousemove, true); + document.removeEventListener('mouseup', this._mouseup, true); + + if(this._height){ + localStorage.setItem('qwc-footer-height', this._height); + } + } + + _doubleClicked(e) { + if(e.target.tagName.toLowerCase() === "vaadin-tabs" + || e.target.tagName.toLowerCase() === "vaadin-tabsheet" + || e.target.tagName.toLowerCase() === "vaadin-icon"){ + if(this._isOpen){ + this._close(); + }else { + this._open(); + } + } + + // Initial load of control buttons + if (this._controlButtons.length === 0) { + this._initControlButtons(); + } + } + + _initControlButtons(){ + if (this._controlButtons.length === 0) { + if(this._selectedTab){ + this._tabSelected(this._selectedTab); + }else{ + this._tabSelected(0); + } + } + } + + _open(){ + this._arrow = "down"; + this._footerClass = "footerOpen"; + this._tabsheetClass = "tabsheetOpen"; + this._tabsClass = "tabsOpen"; + this._tabClass = "tabOpen"; + this._tabContentClass = "tabContentOpen"; + this._dragClass = "dragOpen"; + this._controlsClass = "controlsOpen"; + this._isOpen=true; + localStorage.setItem('qwc-footer-state', "open"); + } + + _close(){ + this._arrow = "up"; + this._footerClass = "footerClose"; + this._tabsheetClass = "tabsheetClose"; + this._tabsClass = "tabsClose"; + this._tabClass = "tabClose"; + this._tabContentClass = "tabContentClose"; + this._dragClass = "dragClose"; + this._controlsClass = "controlsClose"; + this._isOpen=false; + localStorage.setItem('qwc-footer-state', "close"); + } + + _controlButtonClicked(e){ + LogController.fireCallback(e); + } +} +customElements.define('qwc-footer', QwcFooter); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js new file mode 100644 index 0000000000000..7c8f1f7c35a6e --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-header.js @@ -0,0 +1,245 @@ +import { LitElement, html, css } from 'lit'; +import { RouterController } from 'router-controller'; +import { observeState } from 'lit-element-state'; +import { themeState } from 'theme-state'; +import { devuiState } from 'devui-state'; +import '@vaadin/menu-bar'; +import '@vaadin/tabs'; + +/** + * This component represent the Dev UI Header + */ +export class QwcHeader extends observeState(LitElement) { + + static styles = css` + + .top-bar { + height: 70px; + display: flex; + align-items: center; + flex-direction: row; + justify-content: space-between; + } + .right-bar { + display: flex; + justify-content: flex-end; + align-items: center; + } + + .logo-title { + display: flex; + align-items: center; + flex-direction: row; + } + .top-bar svg { + height: 45px; + padding: 8px; + } + + .logo-right-actions { + display: flex; + align-items:center; + padding-right: 10px; + } + + .logo-reload-click { + cursor: pointer; + display: flex; + align-items:center; + font-size: xx-large; + } + + .logo-reload-click:hover { + filter: brightness(90%); + } + + .title { + display: flex; + align-items:center; + font-size: x-large; + padding-left: 100px; + } + + .logo-text { + line-height: 1; + } + + .app-info { + font-size: small; + padding-right: 10px; + } + + .themeDropdown { + padding-right: 15px; + padding-left: 15px; + } + + `; + + static properties = { + _title: {state: true}, + _rightSideNav: {state: true}, + _selectedTheme: {state: true}, + _themeOptions: {state: true}, + _desktopTheme: {state: true} + }; + + constructor() { + super(); + this._title = "Extensions"; + this._rightSideNav = ""; + + this._createThemeItems(); + this._restoreThemePreference(); + this._createThemeOptions(); + + window.addEventListener('vaadin-router-location-changed', (event) => { + this._updateHeader(event); + }); + } + + connectedCallback() { + super.connectedCallback(); + // Get desktop theme setting + this._desktopTheme = "dark"; + if(window.matchMedia){ + if(window.matchMedia('(prefers-color-scheme: light)').matches){ + this._desktopTheme = "light"; + } + + // Change theme setting when OS theme change + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', e => { + if(e.matches){ + this._desktopTheme = "light"; + }else{ + this._desktopTheme = "dark"; + } + this._changeToSelectedTheme(); + }); + } + + this._changeToSelectedTheme(); + } + + _restoreThemePreference() { + const storedValue = localStorage.getItem("qwc-header-theme-preference"); + if(storedValue){ + this._selectedTheme = storedValue; + }else { + this._selectedTheme = "desktop"; + } + } + + render() { + return html` +
+
+
+ Quarkus + Dev UI +
+ ${this._title} +
+
+ ${this._rightSideNav} + ${this._renderThemeOptions()} + +
+
+ `; + } + + _renderThemeOptions(){ + return html` + `; + } + + _changeThemeOption(e){ + this._selectedTheme = e.detail.value.name; + this._createThemeOptions(); + this._changeToSelectedTheme(); + localStorage.setItem('qwc-header-theme-preference', this._selectedTheme); + } + + _changeToSelectedTheme(){ + if(this._selectedTheme === "desktop"){ + themeState.changeTo(this._desktopTheme); + }else { + themeState.changeTo(this._selectedTheme); + } + } + + _createThemeOptions(){ + + let selectedComponent = this._desktopThemeItem; + if(this._selectedTheme === "dark"){ + selectedComponent = this._darkThemeItem; + }else if(this._selectedTheme === "light"){ + selectedComponent = this._lightThemeItem; + } + + this._themeOptions = [ + { + component: selectedComponent, + children: [ + { + component: this._darkThemeItem, + name: "dark" + }, + { + component: this._lightThemeItem, + name: "light" + }, + { + component: this._desktopThemeItem, + name: "desktop" + } + ] + } + + ]; + } + + _createThemeItems() { + this._darkThemeItem = this._createThemeItem("moon", "dark"); + this._lightThemeItem = this._createThemeItem("sun", "light"); + this._desktopThemeItem = this._createThemeItem("desktop", "desktop"); + } + + _createThemeItem(iconName, ariaLabel) { + const item = document.createElement('vaadin-context-menu-item'); + const icon = document.createElement('vaadin-icon'); + item.setAttribute('aria-label', ariaLabel); + icon.setAttribute('icon', `font-awesome-solid:${iconName}`); + item.appendChild(icon); + return item; + } + + _toggleTheme(){ + themeState.toggle(); + } + + _updateHeader(event){ + var pageDetails = RouterController.parseLocationChangedEvent(event); + + this._title = pageDetails.title; + var subMenu = pageDetails.subMenu; + + if(subMenu){ + this._rightSideNav = html` + ${subMenu.links.map(link => + html`${link.name}` + )} + `; + }else{ + this._rightSideNav = html`
${devuiState.applicationInfo.applicationName} ${devuiState.applicationInfo.applicationVersion}
`; // default + } + } + + _reload(e) { + console.log("TODO: Reload"); + } +} +customElements.define('qwc-header', QwcHeader); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-jsonrpc-messages.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-jsonrpc-messages.js new file mode 100644 index 0000000000000..f0a7f532e411c --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-jsonrpc-messages.js @@ -0,0 +1,191 @@ +import { LitElement, html, css} from 'lit'; +import { repeat } from 'lit/directives/repeat.js'; +import { LogController } from 'log-controller'; + +/** + * This component represent the Dev UI Json RPC Message log + */ +export class QwcJsonrpcMessages extends LitElement { + + logControl = new LogController(this, "qwc-jsonrpc-messages"); + + static styles = css` + .log { + width: 100%; + height: 100%; + max-height: 100%; + display: flex; + flex-direction:column; + } + .error { + color:var(--lumo-error-color); + } + .warning { + color:var(--lumo-error-text-color); + } + .info { + color:var(--lumo-success-color); + } + .timestamp { + color: var(--lumo-secondary-text-color); + } + .line { + margin-top: 10px; + margin-bottom: 10px; + border-top: 1px dashed var(--lumo-primary-color-50pct); + color: transparent; + } + `; + + static properties = { + _messages: {state:true}, + _zoom: {state:true}, + _increment: {state: false}, + _followLog: {state: false}, + }; + + constructor() { + super(); + this.logControl + .addToggle("On/off switch", true, (e) => { + this._toggleOnOffClicked(e); + }).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "grey", (e) => { + this._zoomOut(); + }).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "grey", (e) => { + this._zoomIn(); + }).addItem("Clear", "font-awesome-solid:trash-can", "#FF004A", (e) => { + this._clearLog(); + }).addFollow("Follow log", true , (e) => { + this._toggleFollowLog(e); + }); + + this._messages = []; + this._zoom = parseFloat(1.0); + this._increment = parseFloat(0.05); + this._followLog = true; + this._jsonRPCLogEntryEvent = (event) => this._addLogEntry(event.detail); + + } + + connectedCallback() { + super.connectedCallback(); + this._toggleOnOff(true); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._toggleOnOff(false); + } + + render() { + + return html` + ${repeat( + this._messages, + (message) => message.id, + (message, index) => html` +
+ ${this._renderLogEntry(message)} +
+ ` + )} +
`; + + } + + _renderLogEntry(message){ + if(message.isLine){ + return html`
`; + }else{ + return html` + ${this._renderTimestamp(message.time)} + ${this._renderDirection(message.level, message.direction)} + ${this._renderMessage(message.level, message.message)} + `; + } + } + + _renderDirection(level, direction){ + let icon = "minus"; + if(direction === "up"){ + icon = "chevron-right"; + }else if(direction === "down"){ + icon = "chevron-left"; + } + + return html``; + } + + _renderTimestamp(time){ + return html`${time}`; + } + + _renderMessage(level, message){ + return html`${message}`; + } + + _toggleOnOffClicked(e){ + this._toggleOnOff(e); + // Add line on stop + if(!e){ + var stopEntry = new Object(); + stopEntry.id = Math.floor(Math.random() * 999999); + stopEntry.isLine = true; + this._addLogEntry(stopEntry); + } + } + + _toggleOnOff(e){ + if(e){ + document.addEventListener('jsonRPCLogEntryEvent', this._jsonRPCLogEntryEvent, false); + }else{ + document.removeEventListener('jsonRPCLogEntryEvent', this._jsonRPCLogEntryEvent, false); + } + } + + _toggleFollowLog(e){ + this._followLog = e; + this._scrollToBottom(); + } + + _addLogEntry(entry){ + this._messages = [ + ...this._messages, + entry + ]; + + this._scrollToBottom(); + } + + async _scrollToBottom(){ + if(this._followLog){ + await this.updateComplete; + + const last = Array.from( + this.shadowRoot.querySelectorAll('.logEntry') + ).pop(); + + if(last){ + last.scrollIntoView({ + behavior: "smooth", + block: "end" + }); + } + } + } + + + _clearLog(){ + this._messages = []; + } + + _zoomOut(){ + this._zoom = parseFloat(parseFloat(this._zoom) - parseFloat(this._increment)).toFixed(2); + } + + _zoomIn(){ + this._zoom = parseFloat(parseFloat(this._zoom) + parseFloat(this._increment)).toFixed(2); + } +} + +customElements.define('qwc-jsonrpc-messages', QwcJsonrpcMessages); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-menu.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-menu.js new file mode 100644 index 0000000000000..af4e907cd57ab --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-menu.js @@ -0,0 +1,215 @@ +import { LitElement, html, css} from 'lit'; +import { devuiState } from 'devui-state'; +import { observeState } from 'lit-element-state'; +import { RouterController } from 'router-controller'; +import '@vaadin/icon'; + +/** + * This component represent the Dev UI left menu + * It creates the menuItems during build and dynamically add the routes and import the relevant components + */ +export class QwcMenu extends observeState(LitElement) { + + static styles = css` + .left { + height: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .menu { + height: 100%; + display: flex; + flex-direction: column; + } + + .menuSizeControl { + align-self: flex-end; + cursor: pointer; + color: var(--lumo-contrast-10pct); + height: 60px; + width: 30px; + padding-top:30px; + } + + .menuSizeControl:hover { + color: var(--lumo-primary-color-50pct); + } + + .item { + display: flex; + flex-direction: row; + align-items:center; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 5px; + gap: 10px; + cursor: pointer; + border-left: 5px solid transparent; + color: var(--lumo-contrast); + height:30px; + text-decoration: none; + } + + .item:hover{ + border-left: 5px solid var(--lumo-primary-color); + background-color: var(--lumo-primary-color-10pct); + } + + .selected { + border-left: 5px solid var(--lumo-primary-color); + cursor: default; + background-color: var(--lumo-primary-color-10pct); + } + + .quarkusVersion { + padding-bottom: 10px; + padding-left: 15px; + width: 100%; + } + + .quarkusVersion span { + cursor: pointer; + font-size: small; + color: var(--lumo-contrast-50pct); + } + + .quarkusVersion span:hover { + color: var(--lumo-primary-color-50pct); + } + `; + + static properties = { + _show: {state: true}, + _selectedPage: {attribute: false}, + _selectedPageLabel: {attribute: false}, + _width: {state: true}, + }; + + constructor() { + super(); + // TODO, Use state for location + window.addEventListener('vaadin-router-location-changed', (event) => { + this._updateSelection(event); + }); + } + + connectedCallback() { + super.connectedCallback(); + this._selectedPage = "qwc-extensions"; // default + this._selectedPageLabel = "Extensions"; // default + this._restoreState(); + } + + _restoreState(){ + const storedState = localStorage.getItem("qwc-menu-state"); + if(storedState && storedState === "small"){ + this._smaller(); + }else{ + this._larger(); + } + } + + _updateSelection(event){ + var pageDetails = RouterController.parseLocationChangedEvent(event); + this._selectedPage = pageDetails.component; + this._selectedPageLabel = pageDetails.title; + } + + render() { + return html` +
+ + + ${this._renderVersion()} +
`; + } + + _renderVersion(){ + if(this._show){ + return html`
+ Quarkus ${devuiState.applicationInfo.quarkusVersion} +
`; + } + } + + _renderItem(page, index){ + + var pagename = page.componentName; + var defaultSelection = false; + if(index===0)defaultSelection = true; + import(page.componentRef); + RouterController.addMenuRoute(page, defaultSelection); + + let displayName = ""; + if(this._show){ + displayName = page.title; + } + let pageRef = RouterController.pageRefWithBase(page.componentName); + + const selected = this._selectedPage == page.componentName; + let classnames = "item"; + if(selected){ + classnames = "item selected"; + } + + return html` + + + ${displayName} + + `; + } + + _renderIcon(icon, action){ + if((action == "smaller" && this._show) || (action == "larger" && !this._show)){ + return html` + + `; + } + } + + _doubleClicked(e) { + if(e.target.tagName.toLowerCase() === "div"){ + if(this._show){ + this._smaller(); + }else { + this._larger(); + } + } + } + + _changeMenuSize(e){ + if(e.target.dataset.action === "smaller"){ + this._smaller(); + }else{ + this._larger(); + } + this.requestUpdate(); + } + + _smaller() { + this._show = false; + this._width = 50; + localStorage.setItem('qwc-menu-state', "small"); + } + + _larger() { + this._show = true; + this._width = 250; + localStorage.setItem('qwc-menu-state', "large"); + } + + _quarkus(e) { + window.open("https://quarkus.io", '_blank').focus(); + } +} + +customElements.define('qwc-menu', QwcMenu); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js new file mode 100644 index 0000000000000..ea24480806f63 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-server-log.js @@ -0,0 +1,511 @@ +import { LitElement, html, css} from 'lit'; +import { repeat } from 'lit/directives/repeat.js'; +import { LogController } from 'log-controller'; +import { JsonRpc } from 'jsonrpc'; +import { unsafeHTML } from 'lit-html/directives/unsafe-html.js'; +import '@vaadin/icon'; +import '@vaadin/dialog'; +import '@vaadin/checkbox'; +import '@vaadin/checkbox-group'; +import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; + +/** + * This component represent the Server Log + */ +export class QwcServerLog extends LitElement { + + logControl = new LogController(this, "qwc-server-log"); + jsonRpc = new JsonRpc("DevUI", false); + + static styles = css` + .log { + width: 100%; + height: 100%; + max-height: 100%; + display: flex; + flex-direction:column; + } + + a, a:link, a:visited, a:hover, a:active{ + color: var(--lumo-primary-color); + } + + .line { + margin-top: 10px; + margin-bottom: 10px; + border-top: 1px dashed var(--lumo-primary-color); + color: transparent; + } + + .badge { + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 700; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25rem; + transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; + } + .badge-info { + color: #fff; + background-color: #17a2b8; + } + + .text-warn { + color: var(--lumo-error-color-50pct); + } + .text-error{ + color: var(--lumo-error-text-color); + } + .text-info{ + + } + .icon-info { + color: var(--lumo-primary-text-color); + } + + .text-debug{ + color: var(--lumo-success-text-color); + } + .text-normal{ + color: var(--lumo-success-color-50pct); + } + .text-logger{ + color: var(--lumo-primary-color-50pct); + } + .text-source{ + color: var(--lumo-primary-text-color); + } + .text-file { + color: var(--lumo-contrast-40pct); + } + .text-process{ + color: var(--lumo-primary-text-color); + } + .text-thread{ + color: var(--lumo-success-color-50pct); + } + + .columnsDialog{ + + } + + .levelsDialog{ + + } + `; + + static properties = { + _messages: {state:true}, + _zoom: {state:true}, + _increment: {state: false}, + _followLog: {state: false}, + _observer: {state:false}, + _levelsDialogOpened: {state: true}, + _columnsDialogOpened: {state: true}, + _selectedColumns: {state: true}, + }; + + constructor() { + super(); + this.logControl + .addToggle("On/off switch", true, (e) => { + this._toggleOnOffClicked(e); + }).addItem("Log levels", "font-awesome-solid:layer-group", "var(--lumo-tertiary-text-color)", (e) => { + this._logLevels(); + }).addItem("Columns", "font-awesome-solid:table-columns", "var(--lumo-tertiary-text-color)", (e) => { + this._columns(); + }).addItem("Zoom out", "font-awesome-solid:magnifying-glass-minus", "var(--lumo-tertiary-text-color)", (e) => { + this._zoomOut(); + }).addItem("Zoom in", "font-awesome-solid:magnifying-glass-plus", "var(--lumo-tertiary-text-color)", (e) => { + this._zoomIn(); + }).addItem("Clear", "font-awesome-solid:trash-can", "var(--lumo-error-color)", (e) => { + this._clearLog(); + }).addFollow("Follow log", true , (e) => { + this._toggleFollowLog(e); + }); + + this._messages = []; + this._zoom = parseFloat(1.0); + this._increment = parseFloat(0.05); + this._followLog = true; + this._levelsDialogOpened = false; + this._columnsDialogOpened = false; + this._selectedColumns = ['0','3','4','5','10','18','19']; + } + + connectedCallback() { + super.connectedCallback(); + this._toggleOnOff(true); + this.jsonRpc.history().then(jsonRpcResponse => { + jsonRpcResponse.result.forEach(entry => { + this._addLogEntry(entry); + }); + }); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._toggleOnOff(false); + } + + render() { + + return html` + html` + + + + `, + [] + )} + ${dialogRenderer(() => this._renderLevelsDialog(), "levels")} + > + html` + + + + `, + [] + )} + ${dialogRenderer(() => this._renderColumnsDialog(), "columns")} + > + + ${repeat( + this._messages, + (message) => message.sequenceNumber, + (message, index) => html` +
+ ${this._renderLogEntry(message)} +
+ ` + )} +
`; + + } + + _renderLogEntry(message){ + if(message.type === "line"){ + return html`
`; + }else{ + var level = message.level.toUpperCase(); + if (level === "WARNING" || level === "WARN"){ + level = "warn"; + }else if (level === "SEVERE" || level === "ERROR"){ + level = "error"; + }else if (level === "INFO"){ + level = "info"; + }else if (level === "DEBUG"){ + level = "debug"; + }else { + level = "normal"; + } + + return html` + ${this._renderLevelIcon(level)} + ${this._renderSequenceNumber(message.sequenceNumber)} + ${this._renderHostName(message.hostName)} + ${this._renderDate(message.timestamp)} + ${this._renderTime(message.timestamp)} + ${this._renderLevel(level, message.level)} + ${this._renderLoggerNameShort(message.loggerNameShort)} + ${this._renderLoggerName(message.loggerName)} + ${this._renderLoggerClassName(message.loggerClassName)} + ${this._renderSourceClassNameFull(message.sourceClassNameFull)} + ${this._renderSourceClassNameFullShort(message.sourceClassNameFullShort)} + ${this._renderSourceClassName(message.sourceClassName)} + ${this._renderSourceMethodName(message.sourceMethodName)} + ${this._renderSourceFileName(message.sourceFileName)} + ${this._renderSourceLineNumber(message.sourceLineNumber)} + ${this._renderProcessId(message.processId)} + ${this._renderProcessName(message.processName)} + ${this._renderThreadId(message.threadId)} + ${this._renderThreadName(message.threadName)} + ${this._renderMessage(level, message.formattedMessage)} + `; + } + } + + _renderLevelIcon(level){ + if(this._selectedColumns.includes('0')){ + if (level === "warn"){ + return html``; + }else if (level === "error"){ + return html``; + }else if (level === "info"){ + return html``; + }else if (level === "debug"){ + return html``; + }else { + return html``; + } + } + } + + _renderSequenceNumber(sequenceNumber){ + if(this._selectedColumns.includes('1')){ + return html`${sequenceNumber}`; + } + } + + _renderHostName(hostName){ + if(this._selectedColumns.includes('2')){ + return html`${hostName}`; + } + } + + _renderDate(timestamp){ + if(this._selectedColumns.includes('3')){ + return html`${timestamp.slice(0, 10)}`; + } + } + + _renderTime(timestamp){ + if(this._selectedColumns.includes('4')){ + return html`${timestamp.slice(11, 23).replace(".", ",")}`; + } + } + + _renderLevel(level, leveldisplay){ + if(this._selectedColumns.includes('5')){ + return html`${leveldisplay}`; + } + } + + _renderLoggerNameShort(loggerNameShort){ + if(this._selectedColumns.includes('6')){ + return html`[${loggerNameShort}]`; + } + } + + _renderLoggerName(loggerName){ + if(this._selectedColumns.includes('7')){ + return html`[${loggerName}]`; + } + } + + _renderLoggerClassName(loggerClassName){ + if(this._selectedColumns.includes('8')){ + return html`[${loggerClassName}]`; + } + } + + _renderSourceClassNameFull(sourceClassNameFull){ + if(this._selectedColumns.includes('9')){ + return html`[${sourceClassNameFull}]`; + } + } + + _renderSourceClassNameFullShort(sourceClassNameFullShort){ + if(this._selectedColumns.includes('10')){ + return html`[${sourceClassNameFullShort}]`; + } + } + + _renderSourceClassName(sourceClassName){ + if(this._selectedColumns.includes('11')){ + return html`[${sourceClassName}]`; + } + } + + _renderSourceMethodName(sourceMethodName){ + if(this._selectedColumns.includes('12')){ + return html`${sourceMethodName}`; + } + } + + _renderSourceFileName(sourceFileName){ + if(this._selectedColumns.includes('13')){ + return html`${sourceFileName}`; + } + } + + _renderSourceLineNumber(sourceLineNumber){ + if(this._selectedColumns.includes('14')){ + return html`(line:${sourceLineNumber})`; + } + } + + _renderProcessId(processId){ + if(this._selectedColumns.includes('15')){ + return html`(${processId})`; + } + } + + _renderProcessName(processName){ + if(this._selectedColumns.includes('16')){ + return html`(${processName})`; + } + } + + _renderThreadId(threadId){ + if(this._selectedColumns.includes('17')){ + return html`(${threadId})`; + } + } + + _renderThreadName(threadName){ + if(this._selectedColumns.includes('18')){ + return html`(${threadName})`; + } + } + + _renderMessage(level, message){ + if(this._selectedColumns.includes('19')){ + // Make links clickable + if(message.includes("http://")){ + message = this._makeLink(message, "http://"); + } + if(message.includes("https://")){ + message = this._makeLink(message, "https://"); + } + + // Make sure multi line is supported + if(message.includes('\n')){ + var htmlifiedLines = []; + var lines = message.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + line = line.replace(/ /g, '\u00a0'); + if(i === lines.length-1){ + htmlifiedLines.push(line); + }else{ + htmlifiedLines.push(line + '
'); + } + } + message = htmlifiedLines.join(''); + } + + return html`${unsafeHTML(message)}`; + } + } + + _renderLevelsDialog(){ + return html` + Hello levels + `; + } + + _renderColumnsDialog(){ + return html` + + + + + + + + + + + + + + + + + + + + + `; + } + + _makeLink(message, protocol){ + var url = message.substring(message.indexOf(protocol)); + if(url.includes(" ")){ + url = url.substr(0,url.indexOf(' ')); + } + var link = "" + url + ""; + + return message.replace(url, link); + } + + _toggleOnOffClicked(e){ + this._toggleOnOff(e); + // Add line on stop + if(!e){ + var stopEntry = new Object(); + stopEntry.id = Math.floor(Math.random() * 999999); + stopEntry.type = "line"; + this._addLogEntry(stopEntry); + } + } + + _toggleOnOff(e){ + if(e){ + this._observer = this.jsonRpc.streamLog().onNext(jsonRpcResponse => { + this._addLogEntry(jsonRpcResponse.result); + }); + }else{ + this._observer.cancel(); + } + } + + _toggleFollowLog(e){ + this._followLog = e; + this._scrollToBottom(); + } + + _addLogEntry(entry){ + this._messages = [ + ...this._messages, + entry + ]; + + this._scrollToBottom(); + } + + async _scrollToBottom(){ + if(this._followLog){ + await this.updateComplete; + + const last = Array.from( + this.shadowRoot.querySelectorAll('.logEntry') + ).pop(); + + if(last){ + last.scrollIntoView({ + behavior: "smooth", + block: "end" + }); + } + } + } + + _clearLog(){ + this._messages = []; + } + + _zoomOut(){ + this._zoom = parseFloat(parseFloat(this._zoom) - parseFloat(this._increment)).toFixed(2); + } + + _zoomIn(){ + this._zoom = parseFloat(parseFloat(this._zoom) + parseFloat(this._increment)).toFixed(2); + } + + _logLevels(){ + this._levelsDialogOpened = true; + } + + _columns(){ + this._columnsDialogOpened = true; + } +} + +customElements.define('qwc-server-log', QwcServerLog); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-ws-status.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-ws-status.js new file mode 100644 index 0000000000000..f58249f11497d --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-ws-status.js @@ -0,0 +1,15 @@ +import { LitElement, html} from 'lit'; +import { connectionState } from 'connection-state'; +import { observeState } from 'lit-element-state'; + +/** + * This component shows the status of the Web socket connection + */ +export class QwcWsStatus extends observeState(LitElement) { + + render() { + return html``; + } +} + +customElements.define('qwc-ws-status', QwcWsStatus); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/connection-state.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/connection-state.js new file mode 100644 index 0000000000000..92630dc4de57f --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/connection-state.js @@ -0,0 +1,59 @@ +import { LitState } from 'lit-element-state'; + +/** + * This keeps state of the JsonRPC Connection + */ +class ConnectionState extends LitState { + + constructor() { + super(); + } + + static get stateVars() { + return { + current: {} + }; + } + + disconnected(serverUri){ + const newState = new Object(); + newState.name = "disconnected"; + newState.icon = "plug-circle-exclamation"; + newState.color = "var(--lumo-error-color)"; + newState.message = "Disconnected from " + serverUri; + newState.serverUri = serverUri; + newState.isConnected = false; + newState.isDisconnected = true; + newState.isConnecting = false; + connectionState.current = newState; + } + + connecting(serverUri){ + const newState = new Object(); + newState.name = "connecting"; + newState.icon = "plug-circle-bolt"; + newState.color = "var(--lumo-primary-color)"; + newState.message = "Connecting to " + serverUri; + newState.serverUri = serverUri; + newState.isConnected = false; + newState.isDisconnected = true; + newState.isConnecting = true; + connectionState.current = newState; + } + + connected(serverUri){ + const newState = new Object(); + newState.name = "connected"; + newState.icon = "plug-circle-check"; + newState.color = "var(--lumo-success-color)"; + newState.message = "Connected to " + serverUri; + newState.serverUri = serverUri; + newState.isConnected = true; + newState.isDisconnected = false; + newState.isConnecting = false; + + connectionState.current = newState; + } +} + +export const connectionState = new ConnectionState(); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/devui-state.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/devui-state.js new file mode 100644 index 0000000000000..5b38377919e91 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/devui-state.js @@ -0,0 +1,64 @@ +import { extensions } from 'devui-data'; +import { menuItems } from 'devui-data'; +import { footerTabs } from 'devui-data'; +import { applicationInfo } from 'devui-data'; +import { connectionState } from 'connection-state'; +import { LitState } from 'lit-element-state'; + +/** + * This keeps track of the build time data of Dev UI + * + * TODO: Find a way to abstract this so that any build time data can reuse this in an easy way + * TODO: Import map needs to be reloaded too + * TODO: Hot reload should trigger this too (not only ws connection drops) + */ +class DevUIState extends LitState { + + constructor() { + super(); + document.title = "Dev UI | " + applicationInfo.applicationName + " " + applicationInfo.applicationVersion; + this.connectionStateObserver = () => this.reload(); + connectionState.addObserver(this.connectionStateObserver); + } + + static get stateVars() { + return { + cards: extensions, + menu: menuItems, + footer: footerTabs, + applicationInfo: applicationInfo, + }; + } + + reload(){ + + if(connectionState.current.isConnected){ + import(`devui/devui-data.js?${Date.now()}`).then(devUIData => { + + // Check Card changes + if(devUIData.extensions.active !== devuiState.cards.active || + devUIData.extensions.inactive !== devuiState.cards.inactive ){ // TODO: Do a finer check if something changed + devuiState.cards = devUIData.extensions; + } + + // Check Menu changes + if(devUIData.menuItems !== devuiState.menuItems){ // TODO: Do a finer check if something changed + devuiState.menu = devUIData.menuItems; + } + + // Check Footer changes + if(devUIData.footerTabs !== devuiState.footerTabs){ // TODO: Do a finer check if something changed + devuiState.footer = devUIData.footerTabs; + } + + // Check application info for updates + if(devUIData.applicationInfo !== devuiState.applicationInfo){ + devuiState.applicationInfo = devUIData.applicationInfo; + document.title = "Dev UI | " + devuiState.applicationInfo.applicationName + " " + devuiState.applicationInfo.applicationVersion; + } + }); + } + } +} + +export const devuiState = new DevUIState(); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/theme-state.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/theme-state.js new file mode 100644 index 0000000000000..2eb5e6fe82f8d --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/state/theme-state.js @@ -0,0 +1,57 @@ +import { themes } from 'devui-data'; +import { LitState } from 'lit-element-state'; + +/** + * This keeps state of the theme (dark/light) + */ +class ThemeState extends LitState { + + constructor() { + super(); + this._themes = themes; + } + + static get stateVars() { + return { + theme: {} + }; + } + + toggle(){ + if(themeState.theme.name === "dark"){ + this.changeTo("light"); + }else{ + this.changeTo("dark"); + } + } + + changeTo(themeName){ + const newTheme = new Object(); + newTheme.name = themeName; + + var colorMap; + + if(themeName==="dark"){ + newTheme.icon = "moon"; + colorMap = this._themes.dark; + }else{ + newTheme.icon = "sun"; + colorMap = this._themes.light; + } + + for (const [key, value] of Object.entries(colorMap)) { + document.body.style.setProperty(key, value); + if(key === "--quarkus-blue"){ + newTheme.quarkusBlue = value; + }else if(key === "--quarkus-red"){ + newTheme.quarkusRed = value; + }else if(key === "--quarkus-center"){ + newTheme.quarkusCenter = value; + } + } + + themeState.theme = newTheme; + } +} + +export const themeState = new ThemeState(); \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/pom.xml b/extensions/vertx-http/dev-ui-spi/pom.xml new file mode 100644 index 0000000000000..13b458a6e2e33 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + quarkus-vertx-http-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-vertx-http-dev-ui-spi + Quarkus - Vert.x - HTTP - Dev UI SPI + + + + io.quarkus + quarkus-core-deployment + + + + \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java new file mode 100644 index 0000000000000..36fe9862f57a5 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java @@ -0,0 +1,31 @@ +package io.quarkus.devui.spi; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * For All DEV UI Build Item, we need to distinguish between the extensions, and the internal usage of Dev UI + */ +public abstract class AbstractDevUIBuildItem extends MultiBuildItem { + private static final String SPACE = " "; + private static final String DASH = "-"; + + protected final String extensionName; + + public AbstractDevUIBuildItem(String extensionName) { + this.extensionName = extensionName; + } + + public String getExtensionName() { + return extensionName; + } + + public String getExtensionPathName() { + return extensionName.toLowerCase().replaceAll(SPACE, DASH); + } + + public boolean isInternal() { + return this.extensionName == DEV_UI; + } + + public static final String DEV_UI = "DevUI"; +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java new file mode 100644 index 0000000000000..6ce24bf72ccec --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java @@ -0,0 +1,89 @@ +package io.quarkus.devui.deployment.spi; + +import java.util.HashMap; +import java.util.Map; + +/** + * Content that is made available in the DEV UI + */ +public class DevUIContent { + private final String fileName; + private final byte[] template; + private final Map data; + + private DevUIContent(DevUIContent.Builder builder) { + this.fileName = builder.fileName; + this.template = builder.template; + this.data = builder.data; + } + + public String getFileName() { + return fileName; + } + + public byte[] getTemplate() { + return template; + } + + public Map getData() { + return data; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String fileName; + private byte[] template; + private Map data; + + private Builder() { + this.data = new HashMap<>(); + } + + public Builder fileName(String fileName) { + if (fileName == null || fileName.isEmpty()) { + throw new RuntimeException("Invalid fileName"); + } + this.fileName = fileName; + return this; + } + + public Builder template(byte[] template) { + if (template == null || template.length == 0) { + throw new RuntimeException("Invalid template"); + } + + this.template = template; + return this; + } + + public Builder addData(Map data) { + this.data.putAll(data); + return this; + } + + public Builder addData(String key, Object value) { + this.data.put(key, value); + return this; + } + + public DevUIContent build() { + if (fileName == null) { + throw new RuntimeException( + ERROR + " FileName is mandatory, for example 'index.html'"); + } + + if (template == null) { + template = DEFAULT_TEMPLATE; + } + + return new DevUIContent(this); + } + + private static final String ERROR = "Not enough information to create Dev UI content."; + private static final byte[] DEFAULT_TEMPLATE = "Here the template of your page. Set your own by providing the template() in the DevUIContent" + .getBytes(); + } +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/JsonRPCProvidersBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/JsonRPCProvidersBuildItem.java new file mode 100644 index 0000000000000..9bcdf44d98a85 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/JsonRPCProvidersBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.devui.spi; + +/** + * This allows you to register a class that will provide data during runtime for JsonRPC Requests + */ +public final class JsonRPCProvidersBuildItem extends AbstractDevUIBuildItem { + + private final Class jsonRPCMethodProviderClass; + + public JsonRPCProvidersBuildItem(String extensionName, Class jsonRPCMethodProviderClass) { + super(extensionName); + this.jsonRPCMethodProviderClass = jsonRPCMethodProviderClass; + } + + public Class getJsonRPCMethodProviderClass() { + return jsonRPCMethodProviderClass; + } +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java new file mode 100644 index 0000000000000..c9db5ef99730c --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java @@ -0,0 +1,61 @@ +package io.quarkus.devui.spi.buildtime; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.quarkus.devui.spi.AbstractDevUIBuildItem; + +/** + * Contains info on the build time template used to build static content for Dev UI + * All files are relative to dev-ui-templates/build-time/{extensionName} (in src/main/resources) + * + * This contain the fileName to the template, and the template data (variables) + * + * This allows extensions developers to add "static files" that they generate with Qute at build time. + * From a runtime p.o.v this is file served from "disk" + */ +public final class QuteTemplateBuildItem extends AbstractDevUIBuildItem { + private final List templateDatas; + + public QuteTemplateBuildItem(String extensionName) { + super(extensionName); + this.templateDatas = new ArrayList<>(); + } + + public List getTemplateDatas() { + return templateDatas; + } + + public void add(String templatename, Map data) { + templateDatas.add(new TemplateData(templatename, templatename, data)); // By default the template is used for only one file. + } + + public void add(String templatename, String fileName, Map data) { + templateDatas.add(new TemplateData(templatename, fileName, data)); + } + + public static class TemplateData { + final String templateName; + final String fileName; + final Map data; + + private TemplateData(String templateName, String fileName, Map data) { + this.templateName = templateName; + this.fileName = fileName; + this.data = data; + } + + public String getTemplateName() { + return templateName; + } + + public String getFileName() { + return fileName; + } + + public Map getData() { + return data; + } + } +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/StaticContentBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/StaticContentBuildItem.java new file mode 100644 index 0000000000000..f89afa24effd8 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/buildtime/StaticContentBuildItem.java @@ -0,0 +1,27 @@ +package io.quarkus.devui.spi.buildtime; + +import java.util.List; + +import io.quarkus.devui.deployment.spi.DevUIContent; +import io.quarkus.devui.spi.AbstractDevUIBuildItem; + +/** + * Static Content generated at build time + * + * This is used to generate components that will be available in Dev UI, but generated during build. + * This contains the final content (no more generation) and will be served as is + */ +public final class StaticContentBuildItem extends AbstractDevUIBuildItem { + + private final List content; + + public StaticContentBuildItem(String extensionName, List content) { + super(extensionName); + this.content = content; + } + + public List getContent() { + return content; + } + +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java new file mode 100644 index 0000000000000..16dc8cc10bd94 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java @@ -0,0 +1,31 @@ +package io.quarkus.devui.spi.page; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.devui.spi.AbstractDevUIBuildItem; + +/** + * Any of card, menu or footer pages + */ +public abstract class AbstractPageBuildItem extends AbstractDevUIBuildItem { + + protected final Map buildTimeData; + + public AbstractPageBuildItem(String extensionName) { + super(extensionName); + this.buildTimeData = new HashMap<>(); + } + + public void addBuildTimeData(String fieldName, Object fieldData) { + this.buildTimeData.put(fieldName, fieldData); + } + + public Map getBuildTimeData() { + return this.buildTimeData; + } + + public boolean hasBuildTimeData() { + return this.buildTimeData != null && !this.buildTimeData.isEmpty(); + } +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/BuildTimeDataPageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/BuildTimeDataPageBuilder.java new file mode 100644 index 0000000000000..3104e82007dcf --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/BuildTimeDataPageBuilder.java @@ -0,0 +1,21 @@ +package io.quarkus.devui.spi.page; + +public abstract class BuildTimeDataPageBuilder extends PageBuilder { + private static final String BUILD_TIME_DATA_KEY = "buildTimeDataKey"; + + protected BuildTimeDataPageBuilder(String title) { + super(); + super.title = title; + super.internalComponent = true;// As external page runs on "internal" namespace + } + + @SuppressWarnings("unchecked") + public T buildTimeDataKey(String key) { + if (key == null || key.isEmpty()) { + throw new RuntimeException("Invalid build time data key, can not be empty"); + } + super.metadata.put(BUILD_TIME_DATA_KEY, key); + return (T) this; + } + +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Card.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Card.java new file mode 100644 index 0000000000000..040114d8b6b77 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Card.java @@ -0,0 +1,56 @@ +package io.quarkus.devui.spi.page; + +/** + * Define a card in Dev UI. + * This is only used when an extension wants to supply a custom card (i.e. the default with links is not sufficient) + */ +public class Card { + + private final String componentName; // This is the name (tagName) of the component + private final String componentLink; // This is a link to the component, excluding namespace + private String namespace; // The namespace (a.k.a extension path) + + public Card(String componentLink) { + if (componentLink.endsWith(DOT_JS)) { + this.componentLink = componentLink; + } else { + this.componentLink = componentLink + DOT_JS; + } + + this.componentName = this.componentLink.substring(0, this.componentLink.length() - 3); + } + + public String getComponentRef() { + if (this.namespace != null) { + return DOT + SLASH + DOT + DOT + SLASH + this.namespace + SLASH + this.componentLink; + } + // TODO: Create a not found component to display here ? + throw new RuntimeException("Could not find component reference"); + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getNamespace() { + return this.namespace; + } + + public String getComponentLink() { + return componentLink; + } + + public String getComponentName() { + return componentName; + } + + @Override + public String toString() { + return "Card {\n\tnamespace=" + namespace + + ", \n\tcomponentLink=" + componentLink + "\n}"; + } + + private static final String SLASH = "/"; + private static final String DOT = "."; + private static final String DOT_JS = DOT + "js"; +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/CardPageBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/CardPageBuildItem.java new file mode 100644 index 0000000000000..14c832067072b --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/CardPageBuildItem.java @@ -0,0 +1,38 @@ +package io.quarkus.devui.spi.page; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * Add a page (or section) to the Dev UI. This is typically the middle part of the screen. + * This will also add links to this pages + */ +public final class CardPageBuildItem extends AbstractPageBuildItem { + + private final List pageBuilders; + private Optional optionalCard = Optional.empty(); + + public CardPageBuildItem(String extensionName) { + super(extensionName); + this.pageBuilders = new ArrayList<>(); + } + + public void addPage(PageBuilder page) { + this.pageBuilders.add(page); + } + + public void setCustomCard(String cardComponent) { + if (cardComponent != null) { + this.optionalCard = Optional.of(new Card(cardComponent)); + } + } + + public List getPages() { + return this.pageBuilders; + } + + public Optional getOptionalCard() { + return this.optionalCard; + } +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/ExternalPageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/ExternalPageBuilder.java new file mode 100644 index 0000000000000..8747fca2f93ce --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/ExternalPageBuilder.java @@ -0,0 +1,61 @@ +package io.quarkus.devui.spi.page; + +import io.quarkus.logging.Log; + +public class ExternalPageBuilder extends PageBuilder { + private static final String QWC_EXTERNAL_PAGE_JS = "qwc-external-page.js"; + private static final String EXTERNAL_URL = "externalUrl"; + private static final String MIME_TYPE = "mimeType"; + + public static final String MIME_TYPE_HTML = "text/html"; + public static final String MIME_TYPE_JSON = "application/json"; + public static final String MIME_TYPE_YAML = "application/yaml"; + public static final String MIME_TYPE_PDF = "application/pdf"; + + protected ExternalPageBuilder(String title) { + super(); + super.title = title; + super.componentLink = QWC_EXTERNAL_PAGE_JS; + super.internalComponent = true;// As external page runs on "internal" namespace + } + + public ExternalPageBuilder url(String url) { + if (url == null || url.isEmpty()) { + throw new RuntimeException("Invalid external URL, can not be empty"); + } + super.metadata.put(EXTERNAL_URL, url); + return this; + } + + public ExternalPageBuilder isHtmlContent() { + return mimeType(MIME_TYPE_HTML); + } + + public ExternalPageBuilder isJsonContent() { + return mimeType(MIME_TYPE_JSON); + } + + public ExternalPageBuilder isYamlContent() { + return mimeType(MIME_TYPE_YAML); + } + + public ExternalPageBuilder isPdfContent() { + return mimeType(MIME_TYPE_PDF); + } + + public ExternalPageBuilder mimeType(String mimeType) { + if (mimeType == null || mimeType.isEmpty()) { + throw new RuntimeException("Invalid mimeType, can not be empty"); + } + if (super.metadata.containsKey(MIME_TYPE)) { + Log.warn("MimeType already set to " + super.metadata.get(MIME_TYPE) + ", overriding with new value"); + } + super.metadata.put(MIME_TYPE, mimeType); + return this; + } + + public ExternalPageBuilder doNotEmbed() { + super.embed = false; + return this; + } +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/FooterPageBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/FooterPageBuildItem.java new file mode 100644 index 0000000000000..45da2850ed8d5 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/FooterPageBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkus.devui.spi.page; + +import java.util.Arrays; +import java.util.List; + +/** + * Add a footer tab to the Dev UI. + */ +public final class FooterPageBuildItem extends AbstractPageBuildItem { + + private final List pageBuilders; + + public FooterPageBuildItem(String extensionName, PageBuilder... pageBuilder) { + super(extensionName); + this.pageBuilders = Arrays.asList(pageBuilder); + } + + public List getPages() { + return this.pageBuilders; + } + +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/MenuPageBuildItem.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/MenuPageBuildItem.java new file mode 100644 index 0000000000000..61f28eca8424a --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/MenuPageBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkus.devui.spi.page; + +import java.util.Arrays; +import java.util.List; + +/** + * Add a menu (or section) to the Dev UI. + */ +public final class MenuPageBuildItem extends AbstractPageBuildItem { + + private final List pageBuilders; + + public MenuPageBuildItem(String extensionName, PageBuilder... pageBuilder) { + super(extensionName); + this.pageBuilders = Arrays.asList(pageBuilder); + } + + public List getPages() { + return this.pageBuilders; + } + +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java new file mode 100644 index 0000000000000..a96a2d44832c1 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/Page.java @@ -0,0 +1,166 @@ +package io.quarkus.devui.spi.page; + +import java.util.Map; + +/** + * Define a page in Dev UI. + * This is not a full web page, but rather the section in the middle where extensions can display data. + * All pages (fragments) are rendered using Web components, but different builders exist to make it easy to define a page + * + * Navigation to this page is also defined here. + */ +public class Page { + private final String icon; // Any font awesome icon + private final String title; // This is the display name and link title for the page + private final String staticLabel; // This is optional extra info that might be displayed next to the link + private final String dynamicLabel; // This is optional extra info that might be displayed next to the link. This will override above static label. This expects a jsonRPC method name + private final String streamingLabel; // This is optional extra info that might be displayed next to the link. This will override above dynamic label. This expects a jsonRPC Multi method name + + private final String componentName; // This is name of the component + private final String componentLink; // This is a link to the component, excluding namespace + private final Map metadata; // Key value Metadata + + private final boolean embed; // if the component is embeded in the page. true in all cases except maybe external pages + private final boolean internalComponent; // True f this component is provided by dev-ui (usually provided by the extension) + + private String namespace = null; // The namespace can be the extension path or, if internal, qwc + + protected Page(String icon, + String title, + String staticLabel, + String dynamicLabel, + String streamingLabel, + String componentName, + String componentLink, + Map metadata, + boolean embed, + boolean internalComponent, + String namespace) { + + this.icon = icon; + this.title = title; + this.staticLabel = staticLabel; + this.dynamicLabel = dynamicLabel; + this.streamingLabel = streamingLabel; + this.componentName = componentName; + this.componentLink = componentLink; + this.metadata = metadata; + this.embed = embed; + this.internalComponent = internalComponent; + this.namespace = namespace; + } + + public String getId() { + String id = this.title.toLowerCase().replaceAll(SPACE, DASH); + if (this.namespace != null) { + id = this.namespace + SLASH + id; + } + return id; + } + + public String getComponentRef() { + if (internalComponent) { + return DOT + SLASH + DOT + DOT + SLASH + "qwc" + SLASH + this.componentLink; + } else if (this.namespace != null) { + return DOT + SLASH + DOT + DOT + SLASH + this.namespace + SLASH + this.componentLink; + } + // TODO: Create a not found component to display here ? + throw new RuntimeException("Could not find component reference"); + } + + public String getNamespace() { + return this.namespace; + } + + public String getIcon() { + return icon; + } + + public String getTitle() { + return title; + } + + public String getStaticLabel() { + return staticLabel; + } + + public String getDynamicLabel() { + return dynamicLabel; + } + + public String getStreamingLabel() { + return streamingLabel; + } + + public String getComponentName() { + return componentName; + } + + public String getComponentLink() { + return componentLink; + } + + public boolean isEmbed() { + return embed; + } + + public Map getMetadata() { + return metadata; + } + + @Override + public String toString() { + return "Page {\n\ticon=" + icon + + ", \n\ttitle=" + title + + ", \n\tstaticLabel=" + staticLabel + + ", \n\tdynamicLabel=" + dynamicLabel + + ", \n\tstreamingLabel=" + streamingLabel + + ", \n\tnamespace=" + namespace + + ", \n\tcomponentName=" + componentName + + ", \n\tcomponentLink=" + componentLink + + ", \n\tembed=" + embed + "\n}"; + } + + /** + * Here you provide the Web Component that should be rendered. You have full control over the page. + * You can use build time data if you made it available + */ + public static WebComponentPageBuilder webComponentPageBuilder() { + return new WebComponentPageBuilder(); + } + + /** + * Here you provide a url to an external resource. When code/markup, if can be displayed in a code view, when HTML it can + * render the HTML + */ + public static ExternalPageBuilder externalPageBuilder(String name) { + return new ExternalPageBuilder(name); + } + + /** + * Here you provide the data that should be rendered in raw json format + */ + public static RawDataPageBuilder rawDataPageBuilder(String name) { + return new RawDataPageBuilder(name); + } + + /** + * Here you can render the data with a qute template + */ + public static QuteDataPageBuilder quteDataPageBuilder(String name) { + return new QuteDataPageBuilder(name); + } + + /** + * Here you provide the data that should be rendered in a table + */ + public static TableDataPageBuilder tableDataPageBuilder(String name) { + return new TableDataPageBuilder(name); + } + + private static final String SPACE = " "; + private static final String DASH = "-"; + private static final String SLASH = "/"; + private static final String DOT = "."; + +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java new file mode 100644 index 0000000000000..046372c9383b0 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/PageBuilder.java @@ -0,0 +1,119 @@ +package io.quarkus.devui.spi.page; + +import java.util.HashMap; +import java.util.Map; + +public abstract class PageBuilder { + protected static final String EMPTY = ""; + protected static final String SPACE = " "; + protected static final String DASH = "-"; + protected static final String DOT = "."; + protected static final String JS = "js"; + protected static final String QWC_DASH = "qwc-"; + protected static final String DOT_JS = DOT + JS; + + protected String icon = "font-awesome-solid:arrow-right"; + protected String title = null; + protected String staticLabel = null; + protected String dynamicLabel = null; + protected String streamingLabel = null; + protected String componentName; + protected String componentLink; + protected Map metadata = new HashMap<>(); + protected boolean embed = true; // default + protected boolean internalComponent = false; // default + protected String namespace = null; + protected Class preprocessor = null; + + @SuppressWarnings("unchecked") + public T icon(String icon) { + this.icon = icon; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T title(String title) { + this.title = title; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T staticLabel(String staticLabel) { + this.staticLabel = staticLabel; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T dynamicLabelJsonRPCMethodName(String methodName) { + this.dynamicLabel = methodName; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T streamingLabelJsonRPCMethodName(String methodName) { + this.streamingLabel = methodName; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T metadata(String key, String value) { + this.metadata.put(key, value); + return (T) this; + } + + @SuppressWarnings("unchecked") + public T namespace(String namespace) { + if (this.namespace == null) { + this.namespace = namespace; + } + return (T) this; + } + + @SuppressWarnings("unchecked") + public T internal() { + this.internalComponent = true; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T extension(String extension) { + this.metadata.put("extensionName", extension); + this.metadata.put("extensionId", extension.toLowerCase().replaceAll(SPACE, DASH)); + if (this.namespace == null) { + this.namespace = extension.toLowerCase().replaceAll(SPACE, DASH); + } + return (T) this; + } + + public Page build() { + if (this.componentName == null && this.componentLink == null && this.title == null) { + throw new RuntimeException( + "Not enough information to build the page. Set at least one of componentLink and/or componentName and/or title"); + } + + // Guess the component link from the component name or title + if (this.componentLink == null) { + if (this.componentName != null) { + this.componentLink = this.componentName + DOT_JS; + } else if (this.title != null) { + this.componentLink = QWC_DASH + this.title.toLowerCase().replaceAll(SPACE, DASH) + DOT_JS; + } + } + + // Guess the component name from the componentlink or title + if (this.componentName == null) { + this.componentName = this.componentLink.substring(0, this.componentLink.lastIndexOf(DOT)); // Remove the file extension (.js) + } + + // Guess the title + if (this.title == null) { + String n = this.componentName.replaceAll(QWC_DASH, EMPTY); // Remove the qwc- + n = n.substring(n.indexOf(DASH) + 1); // Remove the namespace- + n = n.replaceAll(DASH, SPACE); + this.title = n.substring(0, 1).toUpperCase() + n.substring(1); // Capitalize first letter + } + + return new Page(icon, title, staticLabel, dynamicLabel, streamingLabel, componentName, componentLink, metadata, embed, + internalComponent, namespace); + } +} diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/QuteDataPageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/QuteDataPageBuilder.java new file mode 100644 index 0000000000000..595ad61283569 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/QuteDataPageBuilder.java @@ -0,0 +1,36 @@ +package io.quarkus.devui.spi.page; + +public class QuteDataPageBuilder extends PageBuilder { + private static final String DOT_HTML = ".html"; + private static final String QWC_DATA_QUTE_PAGE_JS = "qwc-data-qute-page.js"; + private String templateLink; + + protected QuteDataPageBuilder(String title) { + super(); + super.title = title; + super.internalComponent = true;// As external page runs on "internal" namespace + super.componentLink = QWC_DATA_QUTE_PAGE_JS; + } + + public QuteDataPageBuilder templateLink(String templateLink) { + if (templateLink == null || templateLink.isEmpty() || !templateLink.endsWith(DOT_HTML)) { + throw new RuntimeException( + "Invalid template link [" + templateLink + "] - Expeting a link that ends with .html"); + } + + this.templateLink = templateLink; + return this; + } + + @Override + public Page build() { + + super.metadata("templatePath", getTemplatePath()); + return super.build(); + } + + public String getTemplatePath() { + return "/dev-ui/" + super.namespace + "/" + this.templateLink; + } + +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/RawDataPageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/RawDataPageBuilder.java new file mode 100644 index 0000000000000..89db5df4f8449 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/RawDataPageBuilder.java @@ -0,0 +1,10 @@ +package io.quarkus.devui.spi.page; + +public class RawDataPageBuilder extends BuildTimeDataPageBuilder { + private static final String QWC_DATA_RAW_PAGE_JS = "qwc-data-raw-page.js"; + + protected RawDataPageBuilder(String title) { + super(title); + super.componentLink = QWC_DATA_RAW_PAGE_JS; + } +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/TableDataPageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/TableDataPageBuilder.java new file mode 100644 index 0000000000000..f7391349f7af1 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/TableDataPageBuilder.java @@ -0,0 +1,28 @@ +package io.quarkus.devui.spi.page; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class TableDataPageBuilder extends BuildTimeDataPageBuilder { + private static final String QWC_DATA_TABLE_PAGE_JS = "qwc-data-table-page.js"; + private static final String COLS = "cols"; + private static final String COMMA = ","; + + protected TableDataPageBuilder(String title) { + super(title); + super.componentLink = QWC_DATA_TABLE_PAGE_JS; + } + + public TableDataPageBuilder showColumn(String path) { + List headerPaths = new ArrayList<>(); + if (super.metadata.containsKey(COLS)) { + String csl = super.metadata.get(COLS); + headerPaths = new ArrayList<>(Arrays.asList(csl.split(COMMA))); + } + headerPaths.add(path); + String csl = String.join(COMMA, headerPaths); + super.metadata(COLS, csl); + return this; + } +} \ No newline at end of file diff --git a/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/WebComponentPageBuilder.java b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/WebComponentPageBuilder.java new file mode 100644 index 0000000000000..845cae3659075 --- /dev/null +++ b/extensions/vertx-http/dev-ui-spi/src/main/java/io/quarkus/devui/spi/page/WebComponentPageBuilder.java @@ -0,0 +1,27 @@ +package io.quarkus.devui.spi.page; + +public class WebComponentPageBuilder extends PageBuilder { + + protected WebComponentPageBuilder() { + super(); + } + + public WebComponentPageBuilder componentName(String componentName) { + if (componentName == null || componentName.isEmpty()) { + throw new RuntimeException("Invalid component [" + componentName + "]"); + } + + super.componentName = componentName; + return this; + } + + public WebComponentPageBuilder componentLink(String componentLink) { + if (componentLink == null || componentLink.isEmpty() || !componentLink.endsWith(DOT_JS)) { + throw new RuntimeException( + "Invalid component link [" + componentLink + "] - Expeting a link that ends with .js"); + } + + super.componentLink = componentLink; + return this; + } +} \ No newline at end of file diff --git a/extensions/vertx-http/pom.xml b/extensions/vertx-http/pom.xml index e4290f2dc4e3b..794f5f2c19d59 100644 --- a/extensions/vertx-http/pom.xml +++ b/extensions/vertx-http/pom.xml @@ -19,5 +19,7 @@ dev-console-spi dev-console-runtime-spi deployment-spi + dev-ui-spi + dev-ui-resources diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java new file mode 100644 index 0000000000000..c66a937cda0eb --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java @@ -0,0 +1,116 @@ +package io.quarkus.devui.runtime; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler to return the "static" content created a build time + */ +public class DevUIBuildTimeStaticHandler implements Handler { + private Map urlAndPath; + private String basePath; // Like /q/dev-ui + + public DevUIBuildTimeStaticHandler() { + + } + + public DevUIBuildTimeStaticHandler(String basePath, Map urlAndPath) { + this.basePath = basePath; + this.urlAndPath = urlAndPath; + } + + public Map getUrlAndPath() { + return urlAndPath; + } + + public void setUrlAndPath(Map urlAndPath) { + this.urlAndPath = urlAndPath; + } + + public String getBasePath() { + return basePath; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + @Override + public void handle(RoutingContext event) { + String normalizedPath = event.normalizedPath(); + if (normalizedPath.contains(SLASH)) { + int si = normalizedPath.lastIndexOf(SLASH) + 1; + String path = normalizedPath.substring(0, si); + String fileName = normalizedPath.substring(si); + // TODO: Handle params ? + + if (path.startsWith(basePath) && this.urlAndPath.containsKey(fileName)) { + String pathOnDisk = this.urlAndPath.get(fileName); + + try { + byte[] content = Files.readAllBytes(Path.of(pathOnDisk)); + event.response() + .setStatusCode(STATUS) + .setStatusMessage(OK) + .putHeader(CONTENT_TYPE, getMimeType(fileName)) + .end(Buffer.buffer(content)); + } catch (IOException ex) { + ex.printStackTrace(); + event.next(); + } + } else { + event.next(); + } + } else { + event.next(); + } + } + + private String getMimeType(String fileName) { + if (fileName.contains(DOT)) { + // Detect the mimeType from the file extension + int dotIndex = fileName.lastIndexOf(DOT) + 1; + String ext = fileName.substring(dotIndex); + if (!ext.isEmpty()) { + if (ext.equalsIgnoreCase(FileExtension.HTML) || ext.equalsIgnoreCase(FileExtension.HTM)) { + return MimeType.HTML; + } else if (ext.equalsIgnoreCase(FileExtension.JS)) { + return MimeType.JS; + } else if (ext.equalsIgnoreCase(FileExtension.CSS)) { + return MimeType.CSS; + } else if (ext.equalsIgnoreCase(FileExtension.JSON)) { + return MimeType.JSON; + } + } + } + return MimeType.PLAIN; + } + + private static final int STATUS = 200; + private static final String OK = "OK"; + private static final String SLASH = "/"; + private static final String DOT = "."; + private static final String CONTENT_TYPE = "Content-Type"; + + public static interface FileExtension { + public static final String HTML = "html"; + public static final String HTM = "htm"; + public static final String JS = "js"; + public static final String JSON = "json"; + public static final String CSS = "css"; + } + + public static interface MimeType { + public static final String HTML = "text/html"; + public static final String JS = "text/javascript"; + public static final String JSON = "application/json"; + public static final String CSS = "text/css"; + public static final String PLAIN = "text/plain"; + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java new file mode 100644 index 0000000000000..a17057a1aff02 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java @@ -0,0 +1,53 @@ +package io.quarkus.devui.runtime; + +import java.net.URL; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.quarkus.arc.runtime.BeanContainer; +import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; +import io.quarkus.runtime.ShutdownContext; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.vertx.http.runtime.devmode.FileSystemStaticHandler; +import io.quarkus.vertx.http.runtime.webjar.WebJarStaticHandler; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class DevUIRecorder { + + public void createJsonRpcRouter(BeanContainer beanContainer, + Map> extensionMethodsMap) { + JsonRpcRouter jsonRpcRouter = beanContainer.beanInstance(JsonRpcRouter.class); + jsonRpcRouter.populateJsonRPCMethods(extensionMethodsMap); + } + + public Handler communicationHandler() { + return new DevUIWebSocket(); + } + + public Handler uiHandler(String finalDestination, + String path, + List webRootConfigurations, + ShutdownContext shutdownContext) { + + WebJarStaticHandler handler = new WebJarStaticHandler(finalDestination, path, webRootConfigurations); + shutdownContext.addShutdownTask(new ShutdownContext.CloseRunnable(handler)); + return handler; + } + + public Handler buildTimeStaticHandler(String basePath, Map urlAndPath) { + return new DevUIBuildTimeStaticHandler(basePath, urlAndPath); + } + + public Handler vaadinRouterHandler(String basePath) { + return new VaadinRouterHandler(basePath); + } + + public Handler mvnpmHandler(Set mvnpmJarFiles) { + return new MvnpmHandler(mvnpmJarFiles); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java new file mode 100644 index 0000000000000..f31226d4b4965 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java @@ -0,0 +1,45 @@ +package io.quarkus.devui.runtime; + +import jakarta.enterprise.inject.spi.CDI; + +import org.jboss.logging.Logger; + +import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.vertx.core.AsyncResult; +import io.vertx.core.Handler; +import io.vertx.core.http.ServerWebSocket; +import io.vertx.ext.web.RoutingContext; + +/** + * This is the main entry point for Dev UI Json RPC communication + */ +public class DevUIWebSocket implements Handler { + private static final Logger LOG = Logger.getLogger(DevUIWebSocket.class.getName()); + + @Override + public void handle(RoutingContext event) { + if (WEBSOCKET.equalsIgnoreCase(event.request().getHeader(UPGRADE)) && !event.request().isEnded()) { + event.request().toWebSocket(new Handler>() { + @Override + public void handle(AsyncResult event) { + if (event.succeeded()) { + ServerWebSocket socket = event.result(); + addSocket(socket); + } else { + LOG.debug("Failed to connect to dev ui communication server", event.cause()); + } + } + }); + return; + } + event.next(); + } + + private void addSocket(ServerWebSocket session) { + JsonRpcRouter jsonRpcRouter = CDI.current().select(JsonRpcRouter.class).get(); + jsonRpcRouter.addSocket(session); + } + + private static final String UPGRADE = "Upgrade"; + private static final String WEBSOCKET = "websocket"; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java new file mode 100644 index 0000000000000..9c8f99e8f5a1e --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/MvnpmHandler.java @@ -0,0 +1,100 @@ +package io.quarkus.devui.runtime; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Set; + +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpHeaders; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler to load mvnpm jars + */ +public class MvnpmHandler implements Handler { + + private final URLClassLoader mvnpmLoader; + + public MvnpmHandler(Set mvnpmJars) { + this.mvnpmLoader = new URLClassLoader(mvnpmJars.toArray(new URL[] {})); + } + + @Override + public void handle(RoutingContext event) { + // Find the "filename" and see if it has a file extension + String fullPath = event.normalizedPath(); + String parts[] = fullPath.split(SLASH); + String fileName = parts[parts.length - 1]; + + if (!fileName.contains(DOT)) { + fullPath = fullPath + DOT_JS;// Default to js. Some modules reference other module without the extension + } + + try { + InputStream is = mvnpmLoader.getResourceAsStream(BASE_DIR + fullPath); + if (is != null) { + byte[] contents = is.readAllBytes(); + event.response() + .putHeader(HttpHeaders.CONTENT_TYPE, getContentType(fileName)) + .end(Buffer.buffer(contents)); + return; + } + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + event.next(); + } + + private String getContentType(String filename) { + String f = filename.toLowerCase(); + if (f.endsWith(DOT_JS)) { + return CONTENT_TYPE_JAVASCRIPT; + } else if (f.endsWith(DOT_JSON)) { + return CONTENT_TYPE_JSON; + } else if (f.endsWith(DOT_HTML) || f.endsWith(DOT_HTM)) { + return CONTENT_TYPE_HTML; + } else if (f.endsWith(DOT_XHTML)) { + return CONTENT_TYPE_XHTML; + } else if (f.endsWith(DOT_CSS)) { + return CONTENT_TYPE_CSS; + } else if (f.endsWith(DOT_XML)) { + return CONTENT_TYPE_XML; + } + // .csv Comma-separated values (CSV) text/csv + // .gif Graphics Interchange Format (GIF) image/gif + // .ico Icon format image/vnd.microsoft.icon + // .jpeg, .jpg JPEG images image/jpeg + // .png Portable Network Graphics image/png + // .svg Scalable Vector Graphics (SVG) image/svg+xml + // .ttf TrueType Font font/ttf + // .woff Web Open Font Format (WOFF) font/woff + // .woff2 Web Open Font Format (WOFF) font/woff2 + + return CONTENT_TYPE_TEXT; // default + + } + + private static final String SLASH = "/"; + private static final String BASE_DIR = "META-INF/resources"; + private static final String DOT = "."; + private static final String DOT_JS = ".js"; + private static final String DOT_JSON = ".json"; + private static final String DOT_HTML = ".html"; + private static final String DOT_HTM = ".htm"; + private static final String DOT_XHTML = ".xhtml"; + private static final String DOT_CSS = ".css"; + private static final String DOT_XML = ".xml"; + + private static final String CONTENT_TYPE_JAVASCRIPT = "application/javascript"; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String CONTENT_TYPE_HTML = "text/html"; + private static final String CONTENT_TYPE_XHTML = "application/xhtml+xml"; + private static final String CONTENT_TYPE_XML = "application/xml"; + private static final String CONTENT_TYPE_CSS = "text/css"; + private static final String CONTENT_TYPE_TEXT = "text/plain"; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/VaadinRouterHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/VaadinRouterHandler.java new file mode 100644 index 0000000000000..41c991c71ec43 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/VaadinRouterHandler.java @@ -0,0 +1,36 @@ +package io.quarkus.devui.runtime; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +/** + * Handler to enable Vaadin router. + */ +public class VaadinRouterHandler implements Handler { + private String basePath; + + public VaadinRouterHandler() { + + } + + public VaadinRouterHandler(String basePath) { + this.basePath = basePath; + } + + public String getBasePath() { + return basePath; + } + + public void setBasePath(String basePath) { + this.basePath = basePath; + } + + @Override + public void handle(RoutingContext event) { + if (event.normalizedPath().startsWith(basePath)) { + event.reroute(basePath + "/index.html"); + return; + } + event.next(); + } +} \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java new file mode 100644 index 0000000000000..0a36e927a8b0e --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java @@ -0,0 +1,161 @@ +package io.quarkus.devui.runtime.comms; + +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MessageType; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.arc.Arc; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcReader; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcWriter; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.subscription.Cancellable; +import io.vertx.core.http.ServerWebSocket; +import io.vertx.core.json.JsonObject; + +/** + * Route JsonRPC message to the correct method + */ +@ApplicationScoped +public class JsonRpcRouter { + + private final Map subscriptions = new ConcurrentHashMap<>(); + + // Map json-rpc method to java + private final Map jsonRpcToJava = new HashMap<>(); + + /** + * This gets called on build to build into of the classes we are going to call in runtime + * + * @param extensionMethodsMap + */ + public void populateJsonRPCMethods(Map> extensionMethodsMap) { + for (Map.Entry> extension : extensionMethodsMap.entrySet()) { + String extensionName = extension.getKey(); + Map jsonRpcMethods = extension.getValue(); + for (Map.Entry method : jsonRpcMethods.entrySet()) { + JsonRpcMethodName methodName = method.getKey(); + JsonRpcMethod jsonRpcMethod = method.getValue(); + + @SuppressWarnings("unchecked") + Object providerInstance = Arc.container().select(jsonRpcMethod.getClazz()).get(); + + try { + Method javaMethod; + Map params = null; + if (jsonRpcMethod.hasParams()) { + params = jsonRpcMethod.getParams(); + javaMethod = providerInstance.getClass().getMethod(jsonRpcMethod.getMethodName(), + params.values().toArray(new Class[] {})); + } else { + javaMethod = providerInstance.getClass().getMethod(jsonRpcMethod.getMethodName()); + } + ReflectionInfo reflectionInfo = new ReflectionInfo(jsonRpcMethod.getClazz(), providerInstance, javaMethod, + params); + String jsonRpcMethodName = extensionName + DOT + methodName; + jsonRpcToJava.put(jsonRpcMethodName, reflectionInfo); + } catch (NoSuchMethodException | SecurityException ex) { + throw new RuntimeException(ex); + } + } + } + } + + public void addSocket(ServerWebSocket socket) { + socket.textMessageHandler((e) -> { + socket.writeTextMessage(route(e, socket)); + }); + } + + private String route(String message, ServerWebSocket s) { + JsonRpcReader jsonRpcRequest = JsonRpcReader.read(message); + JsonObject jsonRpcResponse = route(jsonRpcRequest, s); + return jsonRpcResponse.encodePrettily(); + } + + @SuppressWarnings("unchecked") + private JsonObject route(JsonRpcReader jsonRpcRequest, ServerWebSocket s) { + + String jsonRpcMethodName = jsonRpcRequest.getMethod(); + + // First check some internal methods + if (jsonRpcMethodName.equalsIgnoreCase(UNSUBSCRIBE)) { + JsonObject jsonRpcResponse = JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), null, MessageType.Void); + + if (this.subscriptions.containsKey(jsonRpcRequest.getId())) { + Cancellable cancellable = this.subscriptions.remove(jsonRpcRequest.getId()); + cancellable.cancel(); + } + return jsonRpcResponse; + + } else if (this.jsonRpcToJava.containsKey(jsonRpcMethodName)) { // Route to extension + ReflectionInfo reflectionInfo = this.jsonRpcToJava.get(jsonRpcMethodName); + Object providerInstance = Arc.container().select(reflectionInfo.bean).get(); + try { + Object result; + if (jsonRpcRequest.hasParams()) { + Object[] args = getArgsAsObjects(reflectionInfo.params, jsonRpcRequest); + result = reflectionInfo.method.invoke(providerInstance, args); + } else { + result = reflectionInfo.method.invoke(providerInstance); + } + + // Here wrap in our own object that contain some more metadata + JsonObject jsonRpcResponse; + if (reflectionInfo.isSubscription()) { + // Subscription + Multi subscription = (Multi) result; + + // TODO: If Jackson is on the classpath ? + + Cancellable cancellable = subscription.subscribe().with((t) -> { + JsonObject jsonResponse = JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), t, + MessageType.SubscriptionMessage); + s.writeTextMessage(jsonResponse.encodePrettily()); + }); + + this.subscriptions.put(jsonRpcRequest.getId(), cancellable); + + jsonRpcResponse = JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), null, MessageType.Void); + + } else { + // Normal response + jsonRpcResponse = JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), result, MessageType.Response); + } + + return jsonRpcResponse; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + throw new RuntimeException(ex); + } + } + + // Method not found + return JsonRpcWriter.writeMethodNotFoundResponse(jsonRpcRequest.getId(), jsonRpcMethodName); + + } + + private Object[] getArgsAsObjects(Map params, JsonRpcReader jsonRpcRequest) { + List objects = new ArrayList<>(); + for (Map.Entry expectedParams : params.entrySet()) { + String paramName = expectedParams.getKey(); + Class paramType = expectedParams.getValue(); + Object param = jsonRpcRequest.getParam(paramName); + Object casted = paramType.cast(param); + objects.add(casted); + } + return objects.toArray(Object[]::new); + } + + private static final String DOT = "."; + private static final String UNSUBSCRIBE = "unsubscribe"; + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/ReflectionInfo.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/ReflectionInfo.java new file mode 100644 index 0000000000000..9f17ad15cdeb5 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/ReflectionInfo.java @@ -0,0 +1,28 @@ +package io.quarkus.devui.runtime.comms; + +import java.lang.reflect.Method; +import java.util.Map; + +import io.smallrye.mutiny.Multi; + +/** + * Contains reflection info on the beans that needs to be called from the jsonrpc router + */ +public class ReflectionInfo { + public Class bean; + public Object instance; + public Method method; + public Map params; + + public ReflectionInfo(Class bean, Object instance, Method method, Map params) { + this.bean = bean; + this.instance = instance; + this.method = method; + this.params = params; + } + + public boolean isSubscription() { + Class returnType = this.method.getReturnType(); + return returnType.getName().equals(Multi.class.getName()); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcKeys.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcKeys.java new file mode 100644 index 0000000000000..5f85a35f96519 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcKeys.java @@ -0,0 +1,28 @@ +package io.quarkus.devui.runtime.jsonrpc; + +public interface JsonRpcKeys { + + public static final String VERSION = "2.0"; + public static final String JSONRPC = "jsonrpc"; + public static final String OBJECT = "object"; + public static final String MESSAGE_TYPE = "messageType"; + public static final String ID = "id"; + public static final String RESULT = "result"; + public static final String MESSAGE = "message"; + public static final String CODE = "code"; + public static final String ERROR = "error"; + public static final String METHOD = "method"; + public static final String PARAMS = "params"; + + public static final int PARSE_ERROR = -32700; // Parse error. Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. + public static final int INVALID_REQUEST = -32600; // Invalid Request. The JSON sent is not a valid Request object. + public static final int METHOD_NOT_FOUND = -32601; // Method not found. The method does not exist / is not available. + public static final int INVALID_PARAMS = -32602; // Invalid params. Invalid method parameter(s). + public static final int INTERNAL_ERROR = -32603; // Internal error. Internal JSON-RPC error. + + public static enum MessageType { + Void, + Response, + SubscriptionMessage + } +} \ No newline at end of file diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java new file mode 100644 index 0000000000000..e7b508af00024 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java @@ -0,0 +1,52 @@ +package io.quarkus.devui.runtime.jsonrpc; + +import java.util.Map; + +public final class JsonRpcMethod { + private Class clazz; + private String methodName; + private Map params; + + public JsonRpcMethod() { + } + + public JsonRpcMethod(Class clazz, String methodName, Map params) { + this.clazz = clazz; + this.methodName = methodName; + this.params = params; + } + + public Class getClazz() { + return clazz; + } + + public String getMethodName() { + return methodName; + } + + public Map getParams() { + return params; + } + + public boolean hasParams() { + return this.params != null && !this.params.isEmpty(); + } + + public void setClazz(Class clazz) { + this.clazz = clazz; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public void setParams(Map params) { + this.params = params; + } + + @Override + public String toString() { + return clazz.getName() + ":" + methodName + "(" + params + ")"; + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethodName.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethodName.java new file mode 100644 index 0000000000000..a0ed7a069d589 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethodName.java @@ -0,0 +1,50 @@ +package io.quarkus.devui.runtime.jsonrpc; + +import java.util.Objects; + +public final class JsonRpcMethodName { + private String name; + + public JsonRpcMethodName() { + } + + public JsonRpcMethodName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 73 * hash + Objects.hashCode(this.name); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final JsonRpcMethodName other = (JsonRpcMethodName) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public String toString() { + return name; + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java new file mode 100644 index 0000000000000..4e59f83ab9100 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java @@ -0,0 +1,68 @@ +package io.quarkus.devui.runtime.jsonrpc; + +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ID; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.JSONRPC; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.METHOD; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.PARAMS; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.VERSION; + +import java.util.Map; + +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; + +public class JsonRpcReader { + + private final JsonObject jsonObject; + + private JsonRpcReader(JsonObject jsonObject) { + this.jsonObject = jsonObject; + } + + public static JsonRpcReader read(String json) { + return new JsonRpcReader((JsonObject) Json.decodeValue(json)); + } + + public int getId() { + return jsonObject.getInteger(ID); + } + + public String getJsonrpc() { + return jsonObject.getString(JSONRPC, VERSION); + } + + public String getMethod() { + return jsonObject.getString(METHOD); + } + + public boolean isMethod(String m) { + return this.getMethod().equalsIgnoreCase(m); + } + + public boolean hasParams() { + return this.getParams() != null; + } + + public Map getParams() { + JsonObject paramsObject = jsonObject.getJsonObject(PARAMS); + if (paramsObject != null && paramsObject.getMap() != null && !paramsObject.getMap().isEmpty()) { + return paramsObject.getMap(); + } + return null; + } + + @SuppressWarnings({ "unchecked", "unchecked" }) + public T getParam(String key) { + Map params = getParams(); + if (params == null || !params.containsKey(key)) { + return null; + } + return (T) params.get(key); + } + + @Override + public String toString() { + return jsonObject.encodePrettily(); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java new file mode 100644 index 0000000000000..80abf4094df86 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java @@ -0,0 +1,46 @@ +package io.quarkus.devui.runtime.jsonrpc; + +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.CODE; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ERROR; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ID; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.JSONRPC; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MESSAGE; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MESSAGE_TYPE; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.METHOD_NOT_FOUND; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MessageType; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.OBJECT; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.RESULT; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.VERSION; + +import io.vertx.core.json.JsonObject; + +public class JsonRpcWriter { + + private JsonRpcWriter() { + } + + public static JsonObject writeResponse(int id, Object object, MessageType messageType) { + JsonObject result = JsonObject.of(); + if (object != null) { + result.put(OBJECT, object); + } + result.put(MESSAGE_TYPE, messageType.name()); + + return JsonObject.of( + ID, id, + JSONRPC, VERSION, + RESULT, result); + } + + public static JsonObject writeMethodNotFoundResponse(int id, String jsonRpcMethodName) { + JsonObject jsonRpcError = JsonObject.of( + CODE, METHOD_NOT_FOUND, + MESSAGE, "Method [" + jsonRpcMethodName + "] not found"); + + return JsonObject.of( + ID, id, + JSONRPC, VERSION, + ERROR, jsonRpcError); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/JsonFormatter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/JsonFormatter.java new file mode 100644 index 0000000000000..a63cba40bde3e --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/JsonFormatter.java @@ -0,0 +1,151 @@ +package io.quarkus.devui.runtime.logstream; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; + +import org.jboss.logmanager.ExtFormatter; +import org.jboss.logmanager.ExtLogRecord; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +/** + * Formatting log records into a json format + */ +public class JsonFormatter extends ExtFormatter { + + @Override + public String format(final ExtLogRecord logRecord) { + return toJsonObject(logRecord).encodePrettily(); + } + + JsonObject toJsonObject(ExtLogRecord logRecord) { + String formattedMessage = formatMessage(logRecord); + + JsonObject jsonObject = JsonObject.of(); + + jsonObject.put(TYPE, LOG_LINE); + if (logRecord.getLoggerName() != null) { + jsonObject.put(LOGGER_NAME_SHORT, getShortFullClassName(logRecord.getLoggerName(), "")); + jsonObject.put(LOGGER_NAME, logRecord.getLoggerName()); + } + if (logRecord.getLoggerClassName() != null) { + jsonObject.put(LOGGER_CLASS_NAME, logRecord.getLoggerClassName()); + } + if (logRecord.getHostName() != null) { + jsonObject.put(HOST_NAME, logRecord.getHostName()); + } + if (logRecord.getLevel() != null) { + jsonObject.put(LEVEL, logRecord.getLevel().getName()); + } + if (formattedMessage != null) { + jsonObject.put(FORMATTED_MESSAGE, formattedMessage); + } + if (logRecord.getMessage() != null) { + jsonObject.put(MESSAGE, logRecord.getMessage()); + } + jsonObject.put(SOURCE_LINE_NUMBER, logRecord.getSourceLineNumber()); + if (logRecord.getSourceClassName() != null) { + String justClassName = getJustClassName(logRecord.getSourceClassName()); + jsonObject.put(SOURCE_CLASS_NAME_FULL_SHORT, getShortFullClassName(logRecord.getSourceClassName(), justClassName)); + jsonObject.put(SOURCE_CLASS_NAME_FULL, logRecord.getSourceClassName()); + jsonObject.put(SOURCE_CLASS_NAME, justClassName); + } + if (logRecord.getSourceFileName() != null) { + jsonObject.put(SOURCE_FILE_NAME, logRecord.getSourceFileName()); + } + if (logRecord.getSourceMethodName() != null) { + jsonObject.put(SOURCE_METHOD_NAME, logRecord.getSourceMethodName()); + } + if (logRecord.getThrown() != null) { + jsonObject.put(STACKTRACE, getStacktraces(logRecord.getThrown())); + } + jsonObject.put(THREAD_ID, logRecord.getThreadID()); + jsonObject.put(THREAD_NAME, logRecord.getThreadName()); + jsonObject.put(PROCESS_ID, logRecord.getProcessId()); + jsonObject.put(PROCESS_NAME, logRecord.getProcessName()); + jsonObject.put(TIMESTAMP, Instant.ofEpochMilli(logRecord.getMillis()).toString()); + jsonObject.put(SEQUENCE_NUMBER, logRecord.getSequenceNumber()); + return jsonObject; + } + + private JsonArray getStacktraces(Throwable t) { + List traces = new LinkedList<>(); + addStacktrace(traces, t); + + JsonArray jsonArray = JsonArray.of(); + + traces.forEach((trace) -> { + jsonArray.add(trace); + }); + return jsonArray; + } + + private void addStacktrace(List traces, Throwable t) { + traces.add(getStacktrace(t)); + if (t.getCause() != null) + addStacktrace(traces, t.getCause()); + } + + private String getStacktrace(Throwable t) { + try (StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw)) { + t.printStackTrace(pw); + return sw.toString(); + } catch (IOException ex) { + return null; + } + } + + private String getJustClassName(String fullName) { + int lastDot = fullName.lastIndexOf(DOT) + 1; + return fullName.substring(lastDot); + } + + private String getShortFullClassName(String fullName, String justClassName) { + String[] parts = fullName.split("\\" + DOT); + try (StringWriter buffer = new StringWriter()) { + for (int i = 0; i < parts.length - 1; i++) { + String part = parts[i]; + if (part.equals(justClassName) || part.length() < 3) { + buffer.write(part); + } else { + buffer.write(part.substring(0, 3)); + } + buffer.write(DOT); + } + buffer.write(parts[parts.length - 1]); + return buffer.toString(); + } catch (IOException ex) { + return fullName; + } + } + + private static final String TYPE = "type"; + private static final String LEVEL = "level"; + private static final String MESSAGE = "message"; + private static final String FORMATTED_MESSAGE = "formattedMessage"; + private static final String LOGGER_NAME_SHORT = "loggerNameShort"; + private static final String LOGGER_NAME = "loggerName"; + private static final String LOGGER_CLASS_NAME = "loggerClassName"; + private static final String HOST_NAME = "hostName"; + private static final String SOURCE_LINE_NUMBER = "sourceLineNumber"; + private static final String SOURCE_CLASS_NAME_FULL = "sourceClassNameFull"; + private static final String SOURCE_CLASS_NAME_FULL_SHORT = "sourceClassNameFullShort"; + private static final String SOURCE_CLASS_NAME = "sourceClassName"; + private static final String SOURCE_FILE_NAME = "sourceFileName"; + private static final String SOURCE_METHOD_NAME = "sourceMethodName"; + private static final String THREAD_ID = "threadId"; + private static final String THREAD_NAME = "threadName"; + private static final String PROCESS_ID = "processId"; + private static final String PROCESS_NAME = "processName"; + private static final String TIMESTAMP = "timestamp"; + private static final String STACKTRACE = "stacktrace"; + private static final String SEQUENCE_NUMBER = "sequenceNumber"; + private static final String DOT = "."; + private static final String LOG_LINE = "logLine"; +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamBroadcaster.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamBroadcaster.java new file mode 100644 index 0000000000000..c250718564f61 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamBroadcaster.java @@ -0,0 +1,43 @@ +package io.quarkus.devui.runtime.logstream; + +import java.util.concurrent.LinkedBlockingQueue; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; +import io.vertx.core.json.JsonObject; + +@ApplicationScoped +public class LogStreamBroadcaster { + + private final LinkedBlockingQueue history = new LinkedBlockingQueue<>(60); + private final BroadcastProcessor logStream = BroadcastProcessor.create(); + + public BroadcastProcessor getLogStream() { + return this.logStream; + } + + public void onNext(JsonObject message) { + recordHistory(message); + this.logStream.onNext(message); + } + + public LinkedBlockingQueue getHistory() { + return history; + } + + private void recordHistory(final JsonObject message) { + synchronized (this) { + try { + if (history.remainingCapacity() == 0) { + history.take(); + } + history.add(message); + } catch (InterruptedException ex) { + ex.printStackTrace(); + Thread.currentThread().interrupt(); + } + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java new file mode 100644 index 0000000000000..425d3f8b810ec --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java @@ -0,0 +1,31 @@ +package io.quarkus.devui.runtime.logstream; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +import io.quarkus.arc.Arc; +import io.smallrye.mutiny.Multi; +import io.vertx.core.json.JsonObject; + +/** + * This makes the log file available via json RPC + */ +public class LogStreamJsonRPCService { + + public String ping() { + return "pong"; + } + + public List history() { + LogStreamBroadcaster logStreamBroadcaster = Arc.container().instance(LogStreamBroadcaster.class).get(); + LinkedBlockingQueue history = logStreamBroadcaster.getHistory(); + return new ArrayList<>(history); + } + + public Multi streamLog() { + LogStreamBroadcaster logStreamBroadcaster = Arc.container().instance(LogStreamBroadcaster.class).get(); + return logStreamBroadcaster.getLogStream(); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java new file mode 100644 index 0000000000000..989be2fc5ef06 --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamRecorder.java @@ -0,0 +1,15 @@ +package io.quarkus.devui.runtime.logstream; + +import java.util.Optional; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LogStreamRecorder { + + public RuntimeValue> mutinyLogHandler() { + return new RuntimeValue<>(Optional.of(new MutinyLogHandler())); + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java new file mode 100644 index 0000000000000..dbe414c9db0de --- /dev/null +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/logstream/MutinyLogHandler.java @@ -0,0 +1,44 @@ +package io.quarkus.devui.runtime.logstream; + +import org.jboss.logmanager.ExtHandler; +import org.jboss.logmanager.ExtLogRecord; + +import io.quarkus.arc.Arc; +import io.vertx.core.json.JsonObject; + +/** + * Log handler for Logger Manager + */ +public class MutinyLogHandler extends ExtHandler { + + private LogStreamBroadcaster logStreamBroadcaster; + + public MutinyLogHandler() { + setFormatter(new JsonFormatter()); + } + + @Override + public final void doPublish(final ExtLogRecord record) { + // Don't log empty messages + if (record.getMessage() == null || record.getMessage().isEmpty()) { + return; + } + + if (isLoggable(record)) { + LogStreamBroadcaster broadcaster = getBroadcaster(); + if (broadcaster != null) { + JsonObject message = ((JsonFormatter) getFormatter()).toJsonObject(record); + broadcaster.onNext(message); + } + } + } + + private LogStreamBroadcaster getBroadcaster() { + synchronized (this) { + if (this.logStreamBroadcaster == null && Arc.container() != null) { + this.logStreamBroadcaster = Arc.container().instance(LogStreamBroadcaster.class).get(); + } + } + return this.logStreamBroadcaster; + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java index c5c18b76d2555..2552b9de3885f 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ArcDevRecorder.java @@ -17,8 +17,8 @@ import io.quarkus.arc.InjectableObserverMethod; import io.quarkus.arc.RemovedBean; import io.quarkus.arc.impl.ArcContainerImpl; -import io.quarkus.arc.runtime.devconsole.EventsMonitor; import io.quarkus.arc.runtime.devconsole.InvocationsMonitor; +import io.quarkus.arc.runtime.devmode.EventsMonitor; import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler; import io.quarkus.runtime.annotations.Recorder;