Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev UI: Add source editor for configuration #33661

Merged
merged 1 commit into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, false);
return null;
});
return new JsonRPCProvidersBuildItem("devui-configuration", ConfigJsonRPCService.class);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,11 @@ public static void updateConfig(Map<String, String> values, boolean preventKill)
}
}

static void setConfig(String value) {
public static void setConfig(String value) {
setConfig(value, true);
}

static void setConfig(String value, boolean preventKill) {
public static void setConfig(String value, boolean preventKill) {
try {
Path configPath = getConfigPath();
try (BufferedWriter writer = Files.newBufferedWriter(configPath)) {
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));
}
}