diff --git a/.gitignore b/.gitignore index cadd981..9f5ae6f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ ObjectStore *.ipr *.iws .idea + +# VS Code +.vscode diff --git a/README.md b/README.md index 03ead7c..f5c0797 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,13 @@ mvn package ``` Run: ```bash -docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 \ +docker run --ulimit memlock=-1:-1 -it --rm=true \ --name postgres-quarkus-rest-http-crud \ -e POSTGRES_USER=restcrud \ -e POSTGRES_PASSWORD=restcrud \ -e POSTGRES_DB=rest-crud \ - -p 5432:5432 postgres:10.5 -java -jar target/todo-backend-1.0-SNAPSHOT-runner.jar + -p 5432:5432 postgres:14 +java -jar target/quarkus-app/quarkus-run.jar ``` Then, open: http://localhost:8080/ @@ -36,13 +36,13 @@ mvn clean package -Pnative ``` Run: ```bash -docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 \ +docker run --ulimit memlock=-1:-1 -it --rm=true \ --name postgres-quarkus-rest-http-crud \ -e POSTGRES_USER=restcrud \ -e POSTGRES_PASSWORD=restcrud \ -e POSTGRES_DB=rest-crud \ - -p 5432:5432 postgres:10.5 -target/todo-backend-*-runner + -p 5432:5432 postgres:14 +./target/todo-backend-1.0-SNAPSHOT-runner ``` ## Other links diff --git a/pom.xml b/pom.xml index 59013f1..37f254e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - - + + 4.0.0 io.quarkus.sample todo-backend @@ -14,8 +13,8 @@ UTF-8 UTF-8 quarkus-bom - io.quarkus.platform - 3.9.3 + io.quarkus + 999-SNAPSHOT true 3.1.2 2.22.2 @@ -73,20 +72,29 @@ io.quarkus quarkus-info + + io.quarkus + quarkus-websockets + + io.quarkus + quarkus-web-dependency-locator + + org.mvnpm.at.mvnpm vaadin-webcomponents - 24.3.10 - provided + 24.3.11 + + runtime - + io.quarkus diff --git a/src/main/java/io/quarkus/sample/TodoResource.java b/src/main/java/io/quarkus/sample/TodoResource.java index 733abc3..3af3d82 100644 --- a/src/main/java/io/quarkus/sample/TodoResource.java +++ b/src/main/java/io/quarkus/sample/TodoResource.java @@ -1,6 +1,9 @@ package io.quarkus.sample; +import io.quarkus.sample.audit.AuditType; import io.quarkus.panache.common.Sort; +import io.vertx.core.eventbus.EventBus; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; import jakarta.validation.Valid; @@ -22,6 +25,9 @@ @Tag(name = "Todo Resource", description = "All Todo Operations") public class TodoResource { + @Inject + EventBus bus; + @OPTIONS @Operation(hidden = true) public Response opt() { @@ -50,6 +56,7 @@ public Todo getOne(@PathParam("id") Long id) { @Operation(description = "Create a new todo") public Response create(@Valid Todo item) { item.persist(); + bus.publish(AuditType.TODO_ADDED.name(), item); return Response.status(Status.CREATED).entity(item).build(); } @@ -59,11 +66,18 @@ public Response create(@Valid Todo item) { @Operation(description = "Update an exiting todo") public Response update(@Valid Todo todo, @PathParam("id") Long id) { Todo entity = Todo.findById(id); + if(entity.completed!=todo.completed && todo.completed){ + bus.publish(AuditType.TODO_CHECKED.name(), todo); + }else if(entity.completed!=todo.completed && !todo.completed){ + bus.publish(AuditType.TODO_UNCHECKED.name(), todo); + } + entity.id = id; entity.completed = todo.completed; entity.order = todo.order; entity.title = todo.title; entity.url = todo.url; + return Response.ok(entity).build(); } @@ -85,6 +99,7 @@ public Response deleteOne(@PathParam("id") Long id) { throw new WebApplicationException("Todo with id of " + id + " does not exist.", Status.NOT_FOUND); } entity.delete(); + bus.publish(AuditType.TODO_REMOVED.name(), entity); return Response.noContent().build(); } diff --git a/src/main/java/io/quarkus/sample/audit/AuditLogEncoder.java b/src/main/java/io/quarkus/sample/audit/AuditLogEncoder.java new file mode 100644 index 0000000..c10bb6c --- /dev/null +++ b/src/main/java/io/quarkus/sample/audit/AuditLogEncoder.java @@ -0,0 +1,43 @@ +package io.quarkus.sample.audit; + +import jakarta.enterprise.inject.spi.CDI; +import jakarta.json.bind.Jsonb; +import jakarta.websocket.DecodeException; +import jakarta.websocket.Decoder; +import jakarta.websocket.EncodeException; +import jakarta.websocket.Encoder; +import jakarta.websocket.EndpointConfig; + +public class AuditLogEncoder implements Encoder.Text, Decoder.Text { + + private final Jsonb jsonb; + + public AuditLogEncoder() { + this.jsonb = CDI.current().select(Jsonb.class).get(); + } + + @Override + public String encode(AuditLogSocket.AuditLogEntry object) throws EncodeException { + return jsonb.toJson(object); + } + + @Override + public AuditLogSocket.AuditLogEntry decode(String s) throws DecodeException { + return jsonb.fromJson(s, AuditLogSocket.AuditLogEntry.class); + } + + @Override + public boolean willDecode(String s) { + return true; + } + + @Override + public void init(EndpointConfig config) { + + } + + @Override + public void destroy() { + + } +} diff --git a/src/main/java/io/quarkus/sample/audit/AuditLogSocket.java b/src/main/java/io/quarkus/sample/audit/AuditLogSocket.java new file mode 100644 index 0000000..9492b4a --- /dev/null +++ b/src/main/java/io/quarkus/sample/audit/AuditLogSocket.java @@ -0,0 +1,58 @@ +package io.quarkus.sample.audit; + +import io.quarkus.logging.Log; +import io.quarkus.sample.Todo; +import io.quarkus.vertx.ConsumeEvent; +import io.vertx.core.impl.ConcurrentHashSet; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.websocket.OnOpen; +import jakarta.websocket.Session; +import jakarta.websocket.server.ServerEndpoint; +import java.util.Set; + +@ServerEndpoint(value = "/audit", encoders = AuditLogEncoder.class, decoders = AuditLogEncoder.class) +@ApplicationScoped +public class AuditLogSocket { + + Set sessions = new ConcurrentHashSet<>(); + + public record AuditLogEntry(AuditType type, Todo todo) { + } + + @OnOpen + public void onOpen(Session session) { + sessions.add(session); + } + + @ConsumeEvent("TODO_ADDED") + public void add(Todo todo) { + log(new AuditLogEntry(AuditType.TODO_ADDED, todo)); + } + + @ConsumeEvent("TODO_CHECKED") + public void check(Todo todo) { + log(new AuditLogEntry(AuditType.TODO_CHECKED, todo)); + } + + @ConsumeEvent("TODO_UNCHECKED") + public void uncheck(Todo todo) { + log(new AuditLogEntry(AuditType.TODO_UNCHECKED, todo)); + } + + @ConsumeEvent("TODO_REMOVED") + public void remove(Todo todo) { + log(new AuditLogEntry(AuditType.TODO_REMOVED, todo)); + } + + private void log(AuditLogEntry entry){ + sessions.forEach(s -> { + s.getAsyncRemote().sendObject(entry, result -> { + if (result.getException() != null) { + Log.error("Unable to send message: " + result.getException()); + } + }); + }); + + } + +} diff --git a/src/main/java/io/quarkus/sample/audit/AuditType.java b/src/main/java/io/quarkus/sample/audit/AuditType.java new file mode 100644 index 0000000..cc9bdbf --- /dev/null +++ b/src/main/java/io/quarkus/sample/audit/AuditType.java @@ -0,0 +1,5 @@ +package io.quarkus.sample.audit; + +public enum AuditType { + TODO_ADDED, TODO_CHECKED, TODO_UNCHECKED, TODO_REMOVED +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 63f6d7e..1d8df2d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,3 +15,9 @@ quarkus.smallrye-openapi.info-contact-url=http://todos.com/contact quarkus.smallrye-openapi.info-license-name=Apache 2.0 quarkus.smallrye-openapi.info-license-url=https://www.apache.org/licenses/LICENSE-2.0.html quarkus.swagger-ui.always-include=true + +# DB (Prod mode) +%prod.quarkus.datasource.db-kind=postgresql +%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/rest-crud?loggerLevel=OFF +%prod.quarkus.datasource.password=restcrud +%prod.quarkus.datasource.username=restcrud \ No newline at end of file diff --git a/src/main/resources/web/app/todos-app.js b/src/main/resources/web/app/todos-app.js index 94b3847..95a2293 100644 --- a/src/main/resources/web/app/todos-app.js +++ b/src/main/resources/web/app/todos-app.js @@ -19,6 +19,7 @@ class TodosApp extends LitElement { display: flex; flex-direction: column; justify-content: flex-start; + height: 100%; } `; diff --git a/src/main/resources/web/app/todos-audit-log.js b/src/main/resources/web/app/todos-audit-log.js new file mode 100644 index 0000000..17fd0b0 --- /dev/null +++ b/src/main/resources/web/app/todos-audit-log.js @@ -0,0 +1,80 @@ +import {LitElement, html, css} from 'lit'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; + +class TodosAuditLog extends LitElement { + + static webSocket; + static serverUri; + + static styles = css` + `; + + static properties = { + _entries: {type: Array, state: true} + }; + + constructor() { + super(); + this._entries = []; + if (!TodosAuditLog.logWebSocket) { + if (window.location.protocol === "https:") { + TodosAuditLog.serverUri = "wss:"; + } else { + TodosAuditLog.serverUri = "ws:"; + } + TodosAuditLog.serverUri += "//" + window.location.host + "/audit"; + TodosAuditLog.connect(); + } + this._eventAuditEntry = (event) => this._receiveAuditEntry(event.detail); + } + + connectedCallback() { + super.connectedCallback(); + document.addEventListener('eventAuditEntryEvent', this._eventAuditEntry, false); + } + + disconnectedCallback() { + document.removeEventListener('eventAuditEntryEvent', this._eventAuditEntry, false); + super.disconnectedCallback(); + } + + render() { + return html` + + + + + `; + } + + _typeRenderer(entry) { + return html`${this._formatTodoType(entry.type)}`; + } + + _formatTodoType(str) { + return str.replace(/^TODO_(.*)$/, function(match, p1) { + return p1.toLowerCase(); + }); + } + + _receiveAuditEntry(entry) { + this._entries = [entry, ...this._entries]; + } + + static connect() { + TodosAuditLog.webSocket = new WebSocket(TodosAuditLog.serverUri); + TodosAuditLog.webSocket.onmessage = function (event) { + var auditentry = JSON.parse(event.data); + const eventAuditEntryEvent = new CustomEvent('eventAuditEntryEvent', {detail: auditentry}); + document.dispatchEvent(eventAuditEntryEvent); + } + TodosAuditLog.webSocket.onclose = function (event) { + setTimeout(function () { + TodosAuditLog.connect(); + }, 100); + }; + } + +} +customElements.define('todos-audit-log', TodosAuditLog); diff --git a/src/main/resources/web/app/todos-cards.js b/src/main/resources/web/app/todos-cards.js index 91e6b86..edd2b33 100644 --- a/src/main/resources/web/app/todos-cards.js +++ b/src/main/resources/web/app/todos-cards.js @@ -9,7 +9,11 @@ class TodosCards extends LitElement { static styles = css` :host { display: flex; - justify-content: center; + gap: 20px; + flex-direction: column; + align-items: center; + height: 100%; + justify-content: space-between; } .inputBar { display: flex; @@ -53,6 +57,7 @@ class TodosCards extends LitElement { hr { border-bottom: none; width: 100%; + color: var(--lumo-contrast-10pct); } .select-all-icon { @@ -105,40 +110,19 @@ class TodosCards extends LitElement { this._filter = "all"; } - render() { - return html`
- ${this._renderInput()} - ${this._renderItems()} - ${this._renderFooter()} -
`; - } - connectedCallback() { - super.connectedCallback() + super.connectedCallback(); this._fetchAllTasks(); } - - _fetchAllTasks(){ - fetch("/api") - .then(response => response.json()) - .then(response => this._setAll(response)); - } - - _setAll(tasks){ - this._tasks = tasks; - this._filterTasks(); - } - - _filterTasks(){ - if(this._filter === "active"){ - this._filteredTasks = this._tasks.filter(obj => obj.completed === false); - }else if(this._filter === "completed") { - this._filteredTasks = this._tasks.filter(obj => obj.completed === true); - }else{ - this._filteredTasks = this._tasks; - } + + render(){ + return html`
+ ${this._renderInput()} + ${this._renderItems()} + ${this._renderFooter()} +
`; } - + _renderInput(){ return html`
@@ -180,6 +164,27 @@ class TodosCards extends LitElement { Clear completed
`; } + + _fetchAllTasks(){ + fetch("/api") + .then(response => response.json()) + .then(response => this._setAll(response)); + } + + _setAll(tasks){ + this._tasks = tasks; + this._filterTasks(); + } + + _filterTasks(){ + if(this._filter === "active"){ + this._filteredTasks = this._tasks.filter(obj => obj.completed === false); + }else if(this._filter === "completed") { + this._filteredTasks = this._tasks.filter(obj => obj.completed === true); + }else{ + this._filteredTasks = this._tasks; + } + } _getFilterClass(forFilter){ if(this._filter === forFilter){ @@ -329,4 +334,4 @@ class TodosCards extends LitElement { }); } } -customElements.define('todos-cards', TodosCards); \ No newline at end of file +customElements.define('todos-cards', TodosCards); diff --git a/src/main/resources/web/app/todos-footer.js b/src/main/resources/web/app/todos-footer.js index 7430545..ca96b0f 100644 --- a/src/main/resources/web/app/todos-footer.js +++ b/src/main/resources/web/app/todos-footer.js @@ -13,6 +13,7 @@ class TodosFooter extends LitElement { return html`