Skip to content

Commit

Permalink
Dev UI: Migrate Flyway UI
Browse files Browse the repository at this point in the history
Signed-off-by: Phillip Kruger <[email protected]>
  • Loading branch information
phillip-kruger committed Jul 28, 2023
1 parent b1626f4 commit 5b8a371
Show file tree
Hide file tree
Showing 4 changed files with 495 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package io.quarkus.flyway.devui;

import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
import static java.util.List.of;

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.function.Function;
import java.util.function.Supplier;

import io.quarkus.agroal.spi.JdbcInitialSQLGeneratorBuildItem;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.dev.config.CurrentConfig;
import io.quarkus.dev.console.DevConsoleManager;
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.devui.spi.page.Page;
import io.quarkus.flyway.runtime.FlywayBuildTimeConfig;
import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig;
import io.quarkus.flyway.runtime.devconsole.FlywayDevConsoleRecorder;
import io.quarkus.flyway.runtime.devconsole.FlywayJsonRpcService;
import io.quarkus.runtime.configuration.ConfigUtils;

public class FlywayDevUIProcessor {

@BuildStep(onlyIf = IsDevelopment.class)
@Record(value = RUNTIME_INIT, optional = true)
CardPageBuildItem create(FlywayDevConsoleRecorder recorder, FlywayBuildTimeConfig buildTimeConfig,
List<JdbcInitialSQLGeneratorBuildItem> generatorBuildItem,
CurateOutcomeBuildItem curateOutcomeBuildItem) {

Map<String, Supplier<String>> initialSqlSuppliers = new HashMap<>();
for (JdbcInitialSQLGeneratorBuildItem buildItem : generatorBuildItem) {
initialSqlSuppliers.put(buildItem.getDatabaseName(), buildItem.getSqlSupplier());
}

recorder.setInitialSqlSuppliers(initialSqlSuppliers);

boolean isBaselineOnMigrateConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.baseline-on-migrate");
boolean isMigrateAtStartConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.migrate-at-start");
boolean isCleanAtStartConfigured = ConfigUtils.isPropertyPresent("quarkus.flyway.clean-at-start");
String artifactId = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().getArtifactId();

DevConsoleManager.register("flyway-create-initial-migration",
createInitialMigration(buildTimeConfig,
artifactId,
isBaselineOnMigrateConfigured,
isMigrateAtStartConfigured,
isCleanAtStartConfigured));

CardPageBuildItem card = new CardPageBuildItem();

card.addPage(Page.webComponentPageBuilder()
.componentLink("qwc-flyway-datasources.js")
.dynamicLabelJsonRPCMethodName("getNumberOfDatasources")
.icon("font-awesome-solid:database"));
return card;
}

@BuildStep(onlyIf = IsDevelopment.class)
JsonRPCProvidersBuildItem registerJsonRpcBackend() {
return new JsonRPCProvidersBuildItem(FlywayJsonRpcService.class);
}

private Function<Map<String, String>, String> createInitialMigration(FlywayBuildTimeConfig buildTimeConfig,
String artifactId,
boolean isBaselineOnMigrateConfigured,
boolean isMigrateAtStartConfigured,
boolean isCleanAtStartConfigured) {
return (map -> {
String name = map.get("ds");
String script = map.get("script");
if (name != null) {
try {
return createInitialMigrationScript(name, script,
buildTimeConfig,
artifactId,
isBaselineOnMigrateConfigured,
isMigrateAtStartConfigured,
isCleanAtStartConfigured);
} catch (Exception ex) {
return ex.getMessage();
}
}
return "Datasource parameter not provided";
});
}

private String createInitialMigrationScript(String name, String script,
FlywayBuildTimeConfig buildTimeConfig,
String artifactId,
boolean isBaselineOnMigrateConfigured,
boolean isMigrateAtStartConfigured,
boolean isCleanAtStartConfigured) throws Exception {

FlywayDataSourceBuildTimeConfig config = buildTimeConfig.getConfigForDataSourceName(name);
if (config.locations.isEmpty()) {
return "Error: Datasource has no locations configured";
}

List<Path> resourcesDir = DevConsoleManager.getHotReplacementContext().getResourcesDir();
if (resourcesDir.isEmpty()) {
return "Error: No resource directory found";
}

// In the current project only
Path path = resourcesDir.get(0);

Path migrationDir = path.resolve(config.locations.get(0));
Files.createDirectories(migrationDir);
Path file = migrationDir.resolve(
"V1.0.0__" + artifactId + ".sql");

Files.writeString(file, script);

Map<String, String> newConfig = new HashMap<>();
if (!isBaselineOnMigrateConfigured) {
newConfig.put("quarkus.flyway.baseline-on-migrate", "true");
}
if (!isMigrateAtStartConfigured) {
newConfig.put("quarkus.flyway.migrate-at-start", "true");
}
for (var profile : of("test", "dev")) {
if (!isCleanAtStartConfigured) {
newConfig.put("%" + profile + ".quarkus.flyway.clean-at-start", "true");
}
}
CurrentConfig.EDITOR.accept(newConfig);
//force a scan, to make sure everything is up-to-date
DevConsoleManager.getHotReplacementContext().doScan(true);

return "Initial migration created, Flyway will now manage this datasource";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element';
import { JsonRpc } from 'jsonrpc';
import '@vaadin/icon';
import '@vaadin/button';
import '@vaadin/text-field';
import '@vaadin/text-area';
import '@vaadin/form-layout';
import '@vaadin/progress-bar';
import '@vaadin/checkbox';
import '@vaadin/grid';
import 'qui-alert';
import { columnBodyRenderer } from '@vaadin/grid/lit.js';
import { dialogRenderer } from '@vaadin/dialog/lit.js';
import '@vaadin/grid/vaadin-grid-sort-column.js';
import '@vaadin/progress-bar';
import { notifier } from 'notifier';

export class QwcFlywayDatasources extends QwcHotReloadElement {

jsonRpc = new JsonRpc(this);

static styles = css`
.button {
cursor: pointer;
}
.clearIcon {
color: var(--lumo-warning-text-color);
}`;

static properties = {
_ds: {state: true},
_selectedDs: {state: true},
_createDialogOpened: {state: true}
}

constructor() {
super();
this._ds = null;
this._selectedDs = null;
this._createDialogOpened = false;
}

connectedCallback() {
super.connectedCallback();
this.hotReload();
}

hotReload(){
this.jsonRpc.getDatasources().then(jsonRpcResponse => {
this._ds = jsonRpcResponse.result;
});
}

render() {
if (this._ds) {
return this._renderDataSourceTable();
} else {
return html`<vaadin-progress-bar class="progress" indeterminate></vaadin-progress-bar>`;
}
}

_renderDataSourceTable() {
return html`${this._renderCreateDialog()}
<vaadin-grid .items="${this._ds}" class="datatable" theme="no-border">
<vaadin-grid-column auto-width
header="Name"
${columnBodyRenderer(this._nameRenderer, [])}>
</vaadin-grid-column>
<vaadin-grid-column auto-width
header="Action"
${columnBodyRenderer(this._actionRenderer, [])}
resizable>
</vaadin-grid-column>
</vaadin-grid>`;
}

_actionRenderer(ds) {
return html`${this._renderMigrationButtons(ds)}
${this._renderCreateButton(ds)}`;
}

_renderMigrationButtons(ds) {
if(ds.hasMigrations){
return html`
<vaadin-button theme="small" @click=${() => this._clean(ds)} class="button">
<vaadin-icon class="clearIcon" icon="font-awesome-solid:broom"></vaadin-icon> Clean
</vaadin-button>
<vaadin-button theme="small" @click=${() => this._migrate(ds)} class="button">
<vaadin-icon icon="font-awesome-solid:arrow-right-arrow-left"></vaadin-icon> Migrate
</vaadin-button>`;
}
}

_renderCreateButton(ds) {
if(ds.createPossible){
return html`
<vaadin-button theme="small" @click=${() => this._showCreateDialog(ds)} class="button" title="Set up basic files for Flyway migrations to work. Initial file in db/migrations will be created and you can then add additional migration files">
<vaadin-icon icon="font-awesome-solid:plus"></vaadin-icon> Create Initial Migration File
</vaadin-button>`;
}
}

_nameRenderer(ds) {
return html`${ds.name}`;
}

_showCreateDialog(ds){
this._selectedDs = ds;
this._createDialogOpened = true;
}

_renderCreateDialog(){
return html`<vaadin-dialog class="createDialog"
header-title="Create"
.opened="${this._createDialogOpened}"
@opened-changed="${(e) => (this._createDialogOpened = e.detail.value)}"
${dialogRenderer(() => this._renderCreateDialogForm(), "Create")}
></vaadin-dialog>`;
}

_renderCreateDialogForm(){
let title = this._selectedDs.name + " Datasource";
return html`<b>${title}</b></br>
Set up an initial file from Hibernate ORM schema generation for Flyway migrations to work.<br/>
If you say yes, an initial file in <code>db/migrations</code> will be <br/>
created and you can then add additional migration files as documented.
${this._renderDialogButtons(this._selectedDs)}
`;
}

_renderDialogButtons(ds){
return html`<div style="display: flex; flex-direction: row-reverse; gap: 10px;">
<vaadin-button theme="secondary" @click=${() => this._create(this._selectedDs)}>Create</vaadin-button>
<vaadin-button theme="secondary error" @click=${this._cancelCreate}>Cancel</vaadin-button>
</div>`;
}

_clean(ds) {
this.jsonRpc.clean({ds: ds.name}).then(jsonRpcResponse => {
this._showResultNotification(jsonRpcResponse.result);
});
}

_migrate(ds) {
this.jsonRpc.migrate({ds: ds.name}).then(jsonRpcResponse => {
this._showResultNotification(jsonRpcResponse.result);
});
}

_create(ds) {
this.jsonRpc.create({ds: ds.name}).then(jsonRpcResponse => {
this._showResultNotification(jsonRpcResponse.result);
this._selectedDs = null;
this._createDialogOpened = false;
this.hotReload();
});
}

_cancelCreate(){
this._selectedDs = null;
this._createDialogOpened = false;
}

_showResultNotification(response){
if(response.type === "success"){
notifier.showInfoMessage(response.message + " (" + response.number + ")");
}else{
notifier.showWarningMessage(response.message);
}
}

}
customElements.define('qwc-flyway-datasources', QwcFlywayDatasources);
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.arc.Arc;
import io.quarkus.dev.config.CurrentConfig;
import io.quarkus.dev.console.DevConsoleManager;
import io.quarkus.devconsole.runtime.spi.DevConsolePostHandler;
Expand All @@ -22,6 +23,7 @@
import io.quarkus.flyway.runtime.FlywayContainer;
import io.quarkus.flyway.runtime.FlywayContainersSupplier;
import io.quarkus.flyway.runtime.FlywayDataSourceBuildTimeConfig;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
Expand All @@ -30,6 +32,12 @@
@Recorder
public class FlywayDevConsoleRecorder {

public RuntimeValue<Boolean> setInitialSqlSuppliers(Map<String, Supplier<String>> initialSqlSuppliers) {
FlywayJsonRpcService rpcService = Arc.container().instance(FlywayJsonRpcService.class).get();
rpcService.setInitialSqlSuppliers(initialSqlSuppliers);
return new RuntimeValue<>(true);
}

public Handler<RoutingContext> datasourcesHandler() {
return new DevConsolePostHandler() {
@Override
Expand Down
Loading

0 comments on commit 5b8a371

Please sign in to comment.