diff --git a/extensions/liquibase/deployment/pom.xml b/extensions/liquibase/deployment/pom.xml index 204165e919458f..7a82d54cf07dd9 100644 --- a/extensions/liquibase/deployment/pom.xml +++ b/extensions/liquibase/deployment/pom.xml @@ -33,6 +33,10 @@ io.quarkus quarkus-liquibase + + io.quarkus + quarkus-vertx-http-dev-ui-spi + io.quarkus quarkus-junit5-internal diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devconsole/LiquibaseDevConsoleProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devconsole/LiquibaseDevConsoleProcessor.java index 0ad7a0390b3f03..fcf6e15cc0b437 100644 --- a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devconsole/LiquibaseDevConsoleProcessor.java +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devconsole/LiquibaseDevConsoleProcessor.java @@ -9,7 +9,7 @@ import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; import io.quarkus.devconsole.spi.DevConsoleRuntimeTemplateInfoBuildItem; import io.quarkus.liquibase.runtime.devconsole.LiquibaseDevConsoleRecorder; -import io.quarkus.liquibase.runtime.devconsole.LiquibaseFactoriesSupplier; +import io.quarkus.liquibase.runtime.devui.LiquibaseFactoriesSupplier; public class LiquibaseDevConsoleProcessor { diff --git a/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devui/LiquibaseDevUIProcessor.java b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devui/LiquibaseDevUIProcessor.java new file mode 100644 index 00000000000000..d7d0f6a92ea179 --- /dev/null +++ b/extensions/liquibase/deployment/src/main/java/io/quarkus/liquibase/deployment/devui/LiquibaseDevUIProcessor.java @@ -0,0 +1,61 @@ +package io.quarkus.liquibase.deployment.devui; + +import java.io.InputStream; +import java.net.URL; +import java.util.jar.Manifest; + +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +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; +import io.quarkus.liquibase.runtime.devui.LiquibaseJsonRpcService; +import liquibase.changelog.DatabaseChangeLog; + +/** + * Dev UI card for displaying important details such as the library version. + */ +public class LiquibaseDevUIProcessor { + + private static final String EXTENSION_NAME = "Liquibase"; + + @BuildStep(onlyIf = IsDevelopment.class) + void createCard(BuildProducer cardPageBuildItemBuildProducer) { + final CardPageBuildItem card = new CardPageBuildItem(EXTENSION_NAME); + + // card + card.setCustomCard("qwc-liquibase-card.js"); + + // pages + card.addPage(Page.externalPageBuilder("Version") + .icon("font-awesome-solid:book") + .url("https://www.liquibase.org/") + .doNotEmbed() + .staticLabel(getManifest(DatabaseChangeLog.class).getMainAttributes().getValue("Bundle-Version"))); + + card.addPage(Page.webComponentPageBuilder().title("Datasources") + .componentLink("qwc-liquibase-datasources.js") + .icon("font-awesome-solid:database") + .dynamicLabelJsonRPCMethodName("getDatasourceCount")); + + cardPageBuildItemBuildProducer.produce(card); + } + + @BuildStep(onlyIf = IsDevelopment.class) + JsonRPCProvidersBuildItem registerJsonRpcBackend() { + return new JsonRPCProvidersBuildItem(EXTENSION_NAME, LiquibaseJsonRpcService.class); + } + + private static Manifest getManifest(Class clz) { + String resource = "/" + clz.getName().replace(".", "/") + ".class"; + String fullPath = clz.getResource(resource).toString(); + String archivePath = fullPath.substring(0, fullPath.length() - resource.length()); + + try (InputStream input = new URL(archivePath + "/META-INF/MANIFEST.MF").openStream()) { + return new Manifest(input); + } catch (Exception e) { + throw new RuntimeException("Loading MANIFEST for class " + clz + " failed!", e); + } + } +} diff --git a/extensions/liquibase/deployment/src/main/resources/dev-ui/liquibase/qwc-liquibase-card.js b/extensions/liquibase/deployment/src/main/resources/dev-ui/liquibase/qwc-liquibase-card.js new file mode 100644 index 00000000000000..b175f9f0c6e055 --- /dev/null +++ b/extensions/liquibase/deployment/src/main/resources/dev-ui/liquibase/qwc-liquibase-card.js @@ -0,0 +1,84 @@ +import { LitElement, html, css} from 'lit'; +import { pages } from 'liquibase-data'; +import 'qwc/qwc-extension-link.js'; + +const NAME = "Liquibase"; +export class QwcLiquibaseCard extends LitElement { + + static styles = css` + .identity { + display: flex; + justify-content: flex-start; + } + + .description { + padding-bottom: 10px; + } + + .logo { + padding-bottom: 10px; + margin-right: 5px; + } + + .card-content { + color: var(--lumo-contrast-90pct); + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 2px 2px; + height: 100%; + } + + .card-content slot { + display: flex; + flex-flow: column wrap; + padding-top: 5px; + } + `; + + static properties = { + description: {type: String} + }; + + constructor() { + super(); + } + + connectedCallback() { + super.connectedCallback(); + } + + render() { + return html`
+
+ +
${this.description}
+
+ ${this._renderCardLinks()} +
+ `; + } + + _renderCardLinks(){ + return html`${pages.map(page => html` + + + `)}`; + } + +} +customElements.define('qwc-liquibase-card', QwcLiquibaseCard); \ No newline at end of file diff --git a/extensions/liquibase/deployment/src/main/resources/dev-ui/liquibase/qwc-liquibase-datasources.js b/extensions/liquibase/deployment/src/main/resources/dev-ui/liquibase/qwc-liquibase-datasources.js new file mode 100644 index 00000000000000..81ed30f10d13f7 --- /dev/null +++ b/extensions/liquibase/deployment/src/main/resources/dev-ui/liquibase/qwc-liquibase-datasources.js @@ -0,0 +1,138 @@ +import { LitElement, html, css} from 'lit'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/icon'; +import '@vaadin/button'; +import '@vaadin/confirm-dialog'; +import '@vaadin/text-field'; +import '@vaadin/text-area'; +import '@vaadin/form-layout'; +import '@vaadin/progress-bar'; +import '@vaadin/checkbox'; +import '@vaadin/grid'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import 'qui-alert'; +import { until } from 'lit/directives/until.js'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; + +export class QwcLiquibaseDatasources extends LitElement { + + jsonRpc = new JsonRpc("Liquibase"); + + static styles = css` + .button { + background-color: transparent; + cursor: pointer; + } + .clearIcon { + color: red; + } + .migrateIcon { + color: yellow; + } + .message { + padding: 15px; + text-align: center; + margin-left: 20%; + margin-right: 20%; + border: 2px solid orange; + border-radius: 10px; + font-size: large; + } + `; + + static properties = { + "_factories": {state: true}, + "_ds": {state: true}, + "_message": {state: true}, + "_dialogOpened": {state: true} + } + + connectedCallback() { + super.connectedCallback(); + this.jsonRpc.getLiquibaseFactories().then(jsonRpcResponse => { + this._factories = jsonRpcResponse.result; + }); + } + + render() { + return html`${until(this._renderDataSourceTable(), html`Loading datasources...`)}`; + } + + + _renderDataSourceTable() { + if (this._factories) { + return html` + ${this._message} + + + + + + + + + + This will drop all objects (tables, views, procedures, triggers, ...) in the configured schema. Do you want to continue? + + `; + } + } + + _actionRenderer(ds) { + return html` + this._confirm(ds)} class="button"> + Clear + + this._migrate(ds)} class="button"> + Migrate + + `; + } + + _nameRenderer(ds) { + return html`${ds.dataSourceName}`; + } + + _confirm(ds) { + this._message = ''; + this._ds = ds; + this._dialogOpened = true; + } + + _clear(ds) { + this._message = ''; + this.jsonRpc.clear({ds: ds.dataSourceName}).then(jsonRpcResponse => { + this._message = html` + The datasource ${ds.dataSourceName} has been cleared. + ` + }); + this._ds = null; + } + + _migrate(ds) { + this._message = ''; + this.jsonRpc.migrate({ds: ds.dataSourceName}).then(jsonRpcResponse => { + this._message = html` + The datasource ${ds.dataSourceName} has been migrated. + ` + }); + } + +} +customElements.define('qwc-liquibase-datasources', QwcLiquibaseDatasources); \ No newline at end of file diff --git a/extensions/liquibase/runtime/pom.xml b/extensions/liquibase/runtime/pom.xml index 18b16d9264c4ae..8af78fbd568118 100644 --- a/extensions/liquibase/runtime/pom.xml +++ b/extensions/liquibase/runtime/pom.xml @@ -60,6 +60,11 @@ quarkus-vertx-http true
+ + io.quarkus + quarkus-vertx-http-dev-console-runtime-spi + true + diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devconsole/LiquibaseFactoriesSupplier.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java similarity index 97% rename from extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devconsole/LiquibaseFactoriesSupplier.java rename to extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java index 0ec1d9285ef545..ccd6b438635425 100644 --- a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devconsole/LiquibaseFactoriesSupplier.java +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseFactoriesSupplier.java @@ -1,4 +1,4 @@ -package io.quarkus.liquibase.runtime.devconsole; +package io.quarkus.liquibase.runtime.devui; import java.util.Collection; import java.util.Collections; diff --git a/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseJsonRpcService.java b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseJsonRpcService.java new file mode 100644 index 00000000000000..65547a58678885 --- /dev/null +++ b/extensions/liquibase/runtime/src/main/java/io/quarkus/liquibase/runtime/devui/LiquibaseJsonRpcService.java @@ -0,0 +1,52 @@ +package io.quarkus.liquibase.runtime.devui; + +import java.util.Collection; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkus.liquibase.LiquibaseFactory; +import liquibase.Liquibase; + +@ApplicationScoped +public class LiquibaseJsonRpcService { + + private Collection factories; + + @PostConstruct + void init() { + factories = new LiquibaseFactoriesSupplier().get(); + } + + public boolean clear(String ds) throws Exception { + for (LiquibaseFactory lf : factories) { + if (ds.equalsIgnoreCase(lf.getDataSourceName())) { + try (Liquibase liquibase = lf.createLiquibase()) { + liquibase.dropAll(); + } + return true; + } + } + return false; + } + + public boolean migrate(String ds) throws Exception { + for (LiquibaseFactory lf : factories) { + if (ds.equalsIgnoreCase(lf.getDataSourceName())) { + try (Liquibase liquibase = lf.createLiquibase()) { + liquibase.update(lf.createContexts(), lf.createLabels()); + } + return true; + } + } + return false; + } + + public Integer getDatasourceCount() { + return factories.size(); + } + + public Collection getLiquibaseFactories() { + return factories; + } +}