Skip to content

Commit

Permalink
Dev UI: Add config editor
Browse files Browse the repository at this point in the history
Signed-off-by: Phillip Kruger <[email protected]>
  • Loading branch information
phillip-kruger committed Jun 1, 2023
1 parent ea88a3c commit 329058d
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ JsonRPCProvidersBuildItem registerJsonRpcService() {
ConfigEditorProcessor.updateConfig(values, false);
return null;
});
DevConsoleManager.register("config-set-properties", value -> {
String content = value.get("content");
ConfigEditorProcessor.setConfig(content);
return null;
});
return new JsonRPCProvidersBuildItem("devui-configuration", ConfigJsonRPCService.class);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -14,37 +14,44 @@ 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() {
super();
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`<wc-codemirror mode='${this.mode}'
src='${this.src}'
theme='base16-${themeState.theme.name}'
readonly>
<link rel='stylesheet' href='${currentPath}/_static/wc-codemirror/theme/base16-${themeState.theme.name}.css'>
</wc-codemirror>`;
}else if(this.content){
return html`<wc-codemirror mode='${this.mode}'
theme='base16-${themeState.theme.name}'
readonly>
<link rel='stylesheet' href='${currentPath}/_static/wc-codemirror/theme/base16-${themeState.theme.name}.css'>
<script type='wc-content'>${this.content}</script>
</wc-codemirror>`;
}

return html`<wc-codemirror mode='${this.mode}'
src='${this.src || nothing}'
theme='base16-${themeState.theme.name}'
?readonly=${!this.editable}
@keyup=${this._persistValue}>
<link rel='stylesheet' href='${currentPath}/_static/wc-codemirror/theme/base16-${themeState.theme.name}.css'>
${this._renderContent()}
</wc-codemirror>`;

}

_persistValue(event){
this.value = event.target.value;
}

_renderContent(){
if(this.content){
return html`<script type='wc-content'>${this.content}</script>`;
}
}
}

customElements.define('qui-code-block', QuiCodeBlock);
Original file line number Diff line number Diff line change
@@ -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`<span>TODO: Configuration properties editor</span>`;
if(this._error){
return html`<span>Error: ${this._error}</span>`;
}

if(this._value){
return html`
${this._renderToolbar()}
<qui-code-block id="code"
mode='${this._type}'
content='${this._value}'
value='${this._value}'
editable>
</qui-code-block>`;
}
}

_renderToolbar(){
return html`<div class="toolbar">
<code>application.${this._type}</code>
${this._renderProgressOrButton()}
</div>`;
}

_renderProgressOrButton(){
if(this._inProgress){
return html`<vaadin-progress-bar class="progress" indeterminate></vaadin-progress-bar>`;
}else{
return html`<vaadin-button @click="${() => this._save()}">
<vaadin-icon icon="font-awesome-solid:floppy-disk" slot="prefix"></vaadin-icon>
Save
</vaadin-button>`;
}
}

_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);
customElements.define('qwc-configuration-editor', QwcConfigurationEditor);

Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
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.quarkus.devui.runtime.comms.JsonRpcMessage;
Expand All @@ -14,12 +23,30 @@

@ApplicationScoped
public class ConfigJsonRPCService {
private static final Logger LOG = Logger.getLogger(ConfigJsonRPCService.class.getName());

public JsonRpcMessage<Boolean> updateProperty(String name, String value) {
DevConsoleManager.invoke("config-update-property", Map.of("name", name, "value", value));
return new JsonRpcMessage(true, MessageType.HotReload);
}

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<String, String> 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();
Expand All @@ -28,4 +55,40 @@ public JsonObject getAllValues() {
}
return values;
}

public JsonObject getProjectProperties() {
JsonObject response = new JsonObject();
try {
List<Path> 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<String, String> 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));
}
}

0 comments on commit 329058d

Please sign in to comment.