From 2ef9975a41bd39ed11491cbd5b71af55391daf46 Mon Sep 17 00:00:00 2001 From: Phillip Kruger Date: Mon, 29 May 2023 15:26:39 +1000 Subject: [PATCH] Dev UI: Add config editor Signed-off-by: Phillip Kruger --- .../menu/ConfigurationProcessor.java | 9 ++ .../console/ConfigEditorProcessor.java | 2 +- .../resources/dev-ui/qui/qui-code-block.js | 45 ++++---- .../dev-ui/qwc/qwc-configuration-editor.js | 102 +++++++++++++++++- .../runtime/config/ConfigJsonRPCService.java | 63 +++++++++++ 5 files changed, 198 insertions(+), 23 deletions(-) diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java index 4f03b2b204568..9da8b7f13141b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java @@ -13,8 +13,10 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ConfigDescriptionBuildItem; import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem; +import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.devui.deployment.InternalPageBuildItem; import io.quarkus.devui.spi.page.Page; +import io.quarkus.vertx.http.deployment.devmode.console.ConfigEditorProcessor; import io.quarkus.vertx.http.runtime.devmode.ConfigDescription; /** @@ -43,6 +45,13 @@ InternalPageBuildItem createConfigurationPages(List configurationPages.addBuildTimeData("allConfiguration", getAllConfig(configDescriptionBuildItems, devServicesLauncherConfig)); + // TODO: Move method to JsonRPCProvidersBuildItem method once that PR is in + DevConsoleManager.register("config-set-properties", value -> { + String content = value.get("content"); + ConfigEditorProcessor.setConfig(content); + return null; + }); + return configurationPages; } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java index 07c324325043b..6198f4f56f51b 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java @@ -262,7 +262,7 @@ public static void updateConfig(Map values) { } } - static void setConfig(String value) { + public static void setConfig(String value) { try { Path configPath = getConfigPath(); try (BufferedWriter writer = Files.newBufferedWriter(configPath)) { diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-code-block.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-code-block.js index fb1286ff9e47b..1632c807470d3 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-code-block.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qui/qui-code-block.js @@ -1,5 +1,5 @@ -import {LitElement, html, css} from 'lit'; - +import { LitElement, html, css } from 'lit'; +import { nothing } from 'lit'; import { observeState } from 'lit-element-state'; import { themeState } from 'theme-state'; import '@vanillawc/wc-codemirror'; @@ -14,7 +14,9 @@ export class QuiCodeBlock extends observeState(LitElement) { static properties = { mode: {type: String}, // yaml / js / etc src: {type: String}, // src (optional) - content: {type: String} // content (optional) + content: {type: String}, // content (optional), + value: {type: String, reflect: true }, // up to date value + editable: {type: Boolean} // readonly }; constructor() { @@ -22,29 +24,34 @@ export class QuiCodeBlock extends observeState(LitElement) { this.mode = null; this.src = null; this.content = null; + this.value = null; + this.editable = false; } render() { let currentPath = window.location.pathname; currentPath = currentPath.substring(0, currentPath.indexOf('/dev')); - - if(this.src){ - return html` - - `; - }else if(this.content){ - return html` - - - `; - } + + return html` + + ${this._renderContent()} + `; + + } + + _persistValue(event){ + this.value = event.target.value; } + _renderContent(){ + if(this.content){ + return html``; + } + } } customElements.define('qui-code-block', QuiCodeBlock); diff --git a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js index f59c003c354a7..c1e7285349342 100644 --- a/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js +++ b/extensions/vertx-http/dev-ui-resources/src/main/resources/dev-ui/qwc/qwc-configuration-editor.js @@ -1,23 +1,119 @@ import { LitElement, html, css } from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import { notifier } from 'notifier'; +import 'qui-code-block'; +import '@vaadin/button'; +import '@vaadin/icon'; +import '@vaadin/progress-bar'; /** * This component allows users to change the configuration in an online editor */ export class QwcConfigurationEditor extends LitElement { + jsonRpc = new JsonRpc(this); static styles = css` + :host { + display: flex; + flex-direction:column; + gap: 20px; + } + .toolbar { + display: flex; + gap: 20px; + align-items: center; + } `; - static properties = { + static properties = { + _type: {state: true}, + _value: {state: true}, + _error: {state: true}, + _inProgress: {state: true, type: Boolean} }; constructor() { super(); + this._error = null; + this._value = null; + this._type = null; + this._inProgress = false; + } + + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getProjectProperties().then(e => { + if(e.result.error){ + this._error = e.result.error; + }else{ + this._type = e.result.type; + this._value = e.result.value; + } + }); + + this.addEventListener('keydown', this._handleCtrlS); + } + + disconnectedCallback() { + this.removeEventListener('keydown', this._handleCtrlS); + super.disconnectedCallback(); + } + + _handleCtrlS(e){ + if (e.ctrlKey && e.key === 's') { + e.preventDefault(); + this._save(); + } } render() { - return html`TODO: Configuration properties editor`; + if(this._error){ + return html`Error: ${this._error}`; + } + + if(this._value){ + return html` + ${this._renderToolbar()} + + `; + } + } + + _renderToolbar(){ + return html`
+ application.${this._type} + ${this._renderProgressOrButton()} +
`; + } + + _renderProgressOrButton(){ + if(this._inProgress){ + return html``; + }else{ + return html` + + Save + `; + } + } + + _save(){ + this._inProgress = true; + let newValue = this.shadowRoot.getElementById('code').getAttribute('value'); + this.jsonRpc.updateProperties({content: newValue, type: this._type}).then(jsonRpcResponse => { + this._inProgress = false; + if(jsonRpcResponse.result === false){ + notifier.showErrorMessage("Configuration failed to update. See log file for details"); + }else{ + notifier.showSuccessMessage("Configuration successfully updated"); + } + }); } } -customElements.define('qwc-configuration-editor', QwcConfigurationEditor); \ No newline at end of file +customElements.define('qwc-configuration-editor', QwcConfigurationEditor); + diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java index bd00385cf577d..9f9cca4151164 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java @@ -1,23 +1,50 @@ package io.quarkus.devui.runtime.config; +import java.io.IOException; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; import jakarta.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; import io.quarkus.dev.console.DevConsoleManager; import io.vertx.core.json.JsonObject; @ApplicationScoped public class ConfigJsonRPCService { + private static final Logger LOG = Logger.getLogger(ConfigJsonRPCService.class.getName()); public boolean updateProperty(String name, String value) { DevConsoleManager.invoke("config-update-property", Map.of("name", name, "value", value)); return true; } + public boolean updateProperties(String content, String type) { + + if (type.equalsIgnoreCase("properties")) { + Properties p = new Properties(); + try (StringReader sr = new StringReader(content)) { + p.load(sr); // Validate + Map m = Map.of("content", content, "type", type); + DevConsoleManager.invoke("config-set-properties", m); + return true; + } catch (IOException ex) { + LOG.error("Could not update properties", ex); + return false; + } + } + return false; + } + public JsonObject getAllValues() { JsonObject values = new JsonObject(); Config config = ConfigProvider.getConfig(); @@ -26,4 +53,40 @@ public JsonObject getAllValues() { } return values; } + + public JsonObject getProjectProperties() { + JsonObject response = new JsonObject(); + try { + List resourcesDir = DevConsoleManager.getHotReplacementContext().getResourcesDir(); + if (resourcesDir.isEmpty()) { + response.put("error", "Unable to manage configurations - no resource directory found"); + } else { + + // In the current project only + Path path = resourcesDir.get(0); + Path configPropertiesPath = path.resolve("application.properties"); + if (Files.exists(configPropertiesPath)) { + // Properties file + response.put("type", "properties"); + String value = new String(Files.readAllBytes(configPropertiesPath)); + response.put("value", value); + } else { + response.put("type", "properties"); + response.put("value", ""); + } + } + + } catch (Throwable t) { + throw new RuntimeException(t); + } + return response; + } + + private Map toMap(Properties prop) { + return prop.entrySet().stream().collect( + Collectors.toMap( + e -> String.valueOf(e.getKey()), + e -> String.valueOf(e.getValue()), + (prev, next) -> next, HashMap::new)); + } }