diff --git a/bom/dev-ui/pom.xml b/bom/dev-ui/pom.xml index 75d047481735d..bab44f4111ee9 100644 --- a/bom/dev-ui/pom.xml +++ b/bom/dev-ui/pom.xml @@ -13,7 +13,7 @@ Dependency management for dev-ui. Importable by third party extension developers. - 24.3.7 + 24.3.8 3.1.2 4.0.4 3.1.2 diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java index 44c413466984f..1108c14e08f20 100644 --- a/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/DevConsoleManager.java @@ -119,6 +119,17 @@ public static void register(String name, Function, T> ac actions.put(name, action); } + /** + * Invokes a registered action + * + * @param name the name of the action + * @return the result of the invocation. An empty map is returned for action not returning any result. + */ + @SuppressWarnings("unchecked") + public static T invoke(String name) { + return DevConsoleManager.invoke(name, Map.of()); + } + /** * Invokes a registered action * diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java index ea82358c6b1d1..4b3d9a182ddd6 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java @@ -102,6 +102,7 @@ InternalImportMapBuildItem createKnownInternalImportMap(NonApplicationRootPathBu internalImportMapBuildItem.add("qwc/", contextRoot + "qwc/"); internalImportMapBuildItem.add("qwc-no-data", contextRoot + "qwc/qwc-no-data.js"); internalImportMapBuildItem.add("qwc-hot-reload-element", contextRoot + "qwc/qwc-hot-reload-element.js"); + internalImportMapBuildItem.add("qwc-abstract-log-element", contextRoot + "qwc/qwc-abstract-log-element.js"); internalImportMapBuildItem.add("qwc-server-log", contextRoot + "qwc/qwc-server-log.js"); internalImportMapBuildItem.add("qwc-extension-link", contextRoot + "qwc/qwc-extension-link.js"); // Quarkus UI 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 index dcfc6882b7427..dec094531cc70 100644 --- 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 @@ -1,5 +1,6 @@ package io.quarkus.devui.deployment.logstream; +import java.util.Map; import java.util.Optional; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -8,7 +9,12 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.StreamingLogHandlerBuildItem; +import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; +import io.quarkus.deployment.dev.testing.TestSupport; +import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.dev.spi.DevModeType; import io.quarkus.devui.runtime.logstream.LogStreamBroadcaster; import io.quarkus.devui.runtime.logstream.LogStreamJsonRPCService; import io.quarkus.devui.runtime.logstream.LogStreamRecorder; @@ -38,8 +44,97 @@ public void handler(BuildProducer streamingLogHand } @BuildStep(onlyIf = IsDevelopment.class) - JsonRPCProvidersBuildItem createJsonRPCService() { + JsonRPCProvidersBuildItem createJsonRPCService(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + + DevConsoleManager.register("logstream-force-restart", ignored -> { + RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); + return Map.of(); + }); + + DevConsoleManager.register("logstream-rerun-all-tests", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + if (ts.get().isStarted()) { + ts.get().runAllTests(); + return Map.of(); + } else { + ts.get().start(); + return Map.of("running", ts.get().isRunning()); + } + }); + + DevConsoleManager.register("logstream-rerun-failed-tests", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + ts.get().runFailedTests(); + return Map.of(); + }); + + DevConsoleManager.register("logstream-toggle-broken-only", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean brokenOnlyMode = ts.get().toggleBrokenOnlyMode(); + return Map.of("brokenOnlyMode", brokenOnlyMode); + }); + + DevConsoleManager.register("logstream-print-failures", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + ts.get().printFullResults(); + return Map.of(); + }); + + DevConsoleManager.register("logstream-toggle-test-output", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean isTestOutput = ts.get().toggleTestOutput(); + return Map.of("isTestOutput", isTestOutput); + }); + + DevConsoleManager.register("logstream-toggle-instrumentation-reload", ignored -> { + boolean instrumentationEnabled = RuntimeUpdatesProcessor.INSTANCE.toggleInstrumentation(); + return Map.of("instrumentationEnabled", instrumentationEnabled); + }); + + DevConsoleManager.register("logstream-pause-tests", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + if (ts.get().isStarted()) { + ts.get().stop(); + return Map.of("running", ts.get().isRunning()); + } + return Map.of(); + }); + + DevConsoleManager.register("logstream-toggle-live-reload", ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean liveReloadEnabled = ts.get().toggleLiveReloadEnabled(); + return Map.of("liveReloadEnabled", liveReloadEnabled); + }); + + DevConsoleManager.register("logstream-toggle-live-reload", ignored -> { + + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean liveReloadEnabled = ts.get().toggleLiveReloadEnabled(); + return Map.of("liveReloadEnabled", liveReloadEnabled); + }); + return new JsonRPCProvidersBuildItem("devui-logstream", LogStreamJsonRPCService.class); } + private boolean testsDisabled(LaunchModeBuildItem launchModeBuildItem, Optional ts) { + return ts.isEmpty() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL; + } + } 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 index 454235bf3100f..2d2366d46f230 100644 --- 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 @@ -57,7 +57,7 @@ export class LogController { } _createItem(icon, title, color) { - var style = `font-size: small;cursor: pointer;color: ${color};`; + var style = `font-size: small;cursor: pointer;color: ${color};background-color: transparent;`; const item = document.createElement('vaadin-context-menu-item'); const vaadinicon = document.createElement('vaadin-icon'); item.setAttribute('aria-label', `${title}`); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-abstract-log-element.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-abstract-log-element.js new file mode 100644 index 0000000000000..541ca69ccde19 --- /dev/null +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-abstract-log-element.js @@ -0,0 +1,54 @@ +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; +export * from 'lit'; + +/** + * This is an abstract component that handle some common event for logs + */ +class QwcAbstractLogElement extends QwcHotReloadElement { + + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + this.setAttribute('tabindex', '0'); + this.bindKeyHandler = this._handleKeyPress.bind(this); + this.bindScrollHandler = this._handleScroll.bind(this); + this.addEventListener('keydown', this.bindKeyHandler); + this.addEventListener('wheel', this.bindScrollHandler, { passive: false }); + } + + disconnectedCallback() { + this.removeEventListener('keydown', this.bindKeyHandler); + this.removeEventListener('wheel', this.bindScrollHandler); + super.disconnectedCallback(); + } + + _handleScroll(event){ + if (event.ctrlKey) { + // Prevent the default zoom action when Ctrl + scroll is detected + event.preventDefault(); + if (event.deltaY < 0) { + this._handleZoomIn(event); + } else if (event.deltaY > 0) { + this._handleZoomOut(event); + } + } + } + + _handleKeyPress(event) { + throw new Error("Method '_handleKeyPress(event)' must be implemented."); + } + + _handleZoomIn(event){ + throw new Error("Method '_handleZoomIn(event)' must be implemented."); + } + + _handleZoomOut(event){ + throw new Error("Method '_handleZoomOut(event)' must be implemented."); + } + +} + +export { QwcAbstractLogElement }; 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 index fa293774b4938..7ac8c1b3b4305 100644 --- 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 @@ -101,9 +101,13 @@ export class QwcFooter extends observeState(LitElement) { } vaadin-menu-bar-button { - background: var(--lumo-contrast-5pct); + background-color: transparent; } + vaadin-menu-bar-button:hover { + background-color: transparent; + } + .dragOpen:hover { background: var(--quarkus-blue); } 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 index cf329656943f9..c1b5839681415 100644 --- 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 @@ -1,4 +1,4 @@ -import { LitElement, html, css} from 'lit'; +import { QwcAbstractLogElement, html, css} from 'qwc-abstract-log-element'; import { repeat } from 'lit/directives/repeat.js'; import { LogController } from 'log-controller'; import { StorageController } from 'storage-controller'; @@ -6,7 +6,7 @@ import { StorageController } from 'storage-controller'; /** * This component represent the Dev UI Json RPC Message log */ -export class QwcJsonrpcMessages extends LitElement { +export class QwcJsonrpcMessages extends QwcAbstractLogElement { logControl = new LogController(this); storageControl = new StorageController(this); @@ -44,7 +44,7 @@ export class QwcJsonrpcMessages extends LitElement { _zoom: {state:true}, _increment: {state: false}, _followLog: {state: false}, - _isOn: {state: false}, + _isOn: {state: false} }; constructor() { @@ -83,8 +83,8 @@ export class QwcJsonrpcMessages extends LitElement { } disconnectedCallback() { - super.disconnectedCallback(); this._toggleOnOff(false); + super.disconnectedCallback(); } render() { @@ -106,6 +106,8 @@ export class QwcJsonrpcMessages extends LitElement { _renderLogEntry(message){ if(message.isLine){ return html`
`; + }else if(message.isBlank){ + return html`
`; }else{ return html` ${this._renderTimestamp(message.time)} @@ -201,6 +203,30 @@ export class QwcJsonrpcMessages extends LitElement { _zoomIn(){ this._zoom = parseFloat(parseFloat(this._zoom) + parseFloat(this._increment)).toFixed(2); } + + hotReload(){ + + } + + _handleZoomIn(event){ + this._zoomIn(); + } + + _handleZoomOut(event){ + this._zoomOut(); + } + + _handleKeyPress(event) { + if (event.key === 'Enter') { + // Create a blank line in the console. + var blankEntry = new Object(); + blankEntry.id = Math.floor(Math.random() * 999999); + blankEntry.isBlank = true; + this._addLogEntry(blankEntry); + } + } + + } customElements.define('qwc-jsonrpc-messages', QwcJsonrpcMessages); 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 index 8d0e32979669d..d9a63017d3525 100644 --- 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 @@ -1,4 +1,4 @@ -import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; +import { QwcAbstractLogElement, html, css} from 'qwc-abstract-log-element'; import { repeat } from 'lit/directives/repeat.js'; import { LogController } from 'log-controller'; import { JsonRpc } from 'jsonrpc'; @@ -20,7 +20,7 @@ import { loggerLevels } from 'devui-data'; /** * This component represent the Server Log */ -export class QwcServerLog extends QwcHotReloadElement { +export class QwcServerLog extends QwcAbstractLogElement { logControl = new LogController(this); jsonRpc = new JsonRpc("devui-logstream", false); @@ -80,7 +80,6 @@ export class QwcServerLog extends QwcHotReloadElement { .text-thread{ color: var(--lumo-success-color-50pct); } - `; static properties = { @@ -146,8 +145,8 @@ export class QwcServerLog extends QwcHotReloadElement { } disconnectedCallback() { - super.disconnectedCallback(); this._toggleOnOff(false); + super.disconnectedCallback(); } _loadAllLoggers(){ @@ -207,6 +206,28 @@ export class QwcServerLog extends QwcHotReloadElement { _renderLogEntry(message){ if(message.type === "line"){ return html`
`; + }else if(message.type === "blank"){ + return html`
`; + }else if(message.type === "help"){ + return html`
+ The following commands are available:
+
+ == Continuous Testing
+
+ [r] - Resume testing / Re-run all tests
+ [f] - Re-run failed tests
+ [b] - Toggle 'broken only' mode, where only failing tests are run
+ [v] - Print failures from the last test run
+ [p] - Pause tests
+ [o] - Toggle test output
+
+ == System
+
+ [s] - Force restart
+ [i] - Toggle instrumentation based reload
+ [l] - Toggle live reload
+ [h] - Show this help
+ `; }else{ var level = message.level.toUpperCase(); if (level === "WARNING" || level === "WARN"){ @@ -577,12 +598,14 @@ export class QwcServerLog extends QwcHotReloadElement { }); }else{ this._observer.cancel(); + this._observer = null; } } hotReload(){ - this._toggleOnOffClicked(false); - this._toggleOnOffClicked(true); + // Stop then start / start then stop + this._stopStartLog(); + this._stopStartLog(); this._loadAllLoggers(); } @@ -642,6 +665,69 @@ export class QwcServerLog extends QwcHotReloadElement { _columns(){ this._columnsDialogOpened = true; } + + _handleZoomIn(event){ + this._zoomIn(); + } + + _handleZoomOut(event){ + this._zoomOut(); + } + + _handleKeyPress(event) { + if (event.key === 'Enter') { + this._keyPressEnter(); + } else if (event.ctrlKey && event.key === 'c') { + this._stopStartLog(); + } else if (event.key === 's') { + this.jsonRpc.forceRestart(); + } else if (event.key === 'r') { + this.jsonRpc.rerunAllTests(); + } else if (event.key === 'f') { + this.jsonRpc.rerunFailedTests(); + } else if (event.key === 'b') { + this.jsonRpc.toggleBrokenOnly(); + } else if (event.key === 'v') { + this.jsonRpc.printFailures(); + } else if (event.key === 'o') { + this.jsonRpc.toggleTestOutput(); + } else if (event.key === 'i') { + this.jsonRpc.toggleInstrumentationReload(); + } else if (event.key === 'p') { + this.jsonRpc.pauseTests(); + } else if (event.key === 'l') { + this.jsonRpc.toggleLiveReload(); + } else if (event.key === 'h'){ + this._printHelp(); + } + } + + _keyPressEnter(){ + // Create a blank line in the console. + var blankEntry = new Object(); + blankEntry.id = Math.floor(Math.random() * 999999); + blankEntry.type = "blank"; + this._addLogEntry(blankEntry); + } + + _printHelp(){ + var helpEntry = new Object(); + helpEntry.id = Math.floor(Math.random() * 999999); + helpEntry.type = "help"; + this._addLogEntry(helpEntry); + } + + + _stopStartLog(){ + if(this._observer){ + // stop + this._toggleOnOffClicked(false); + }else{ + // start + this._toggleOnOffClicked(true); + } + } + } customElements.define('qwc-server-log', QwcServerLog); \ No newline at end of file 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 index 7f2f7e369e76d..433d03a7d75b8 100644 --- 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 @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.List; +import java.util.Map; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; @@ -10,6 +11,7 @@ import org.jboss.logmanager.Logger; import io.quarkus.arc.Arc; +import io.quarkus.dev.console.DevConsoleManager; import io.smallrye.common.annotation.NonBlocking; import io.smallrye.mutiny.Multi; import io.vertx.core.json.JsonObject; @@ -25,6 +27,51 @@ public String ping() { return "pong"; } + @NonBlocking + public Map forceRestart() { + return DevConsoleManager.invoke("logstream-force-restart"); + } + + @NonBlocking + public Map rerunAllTests() { + return DevConsoleManager.invoke("logstream-rerun-all-tests"); + } + + @NonBlocking + public Map rerunFailedTests() { + return DevConsoleManager.invoke("logstream-rerun-failed-tests"); + } + + @NonBlocking + public Map toggleBrokenOnly() { + return DevConsoleManager.invoke("logstream-toggle-broken-only"); + } + + @NonBlocking + public Map printFailures() { + return DevConsoleManager.invoke("logstream-print-failures"); + } + + @NonBlocking + public Map toggleTestOutput() { + return DevConsoleManager.invoke("logstream-toggle-test-output"); + } + + @NonBlocking + public Map toggleInstrumentationReload() { + return DevConsoleManager.invoke("logstream-toggle-instrumentation-reload"); + } + + @NonBlocking + public Map pauseTests() { + return DevConsoleManager.invoke("logstream-pause-tests"); + } + + @NonBlocking + public Map toggleLiveReload() { + return DevConsoleManager.invoke("logstream-toggle-live-reload"); + } + @NonBlocking public List history() { LogStreamBroadcaster logStreamBroadcaster = Arc.container().instance(LogStreamBroadcaster.class).get();