Display more info and accept / display messages.

This commit is contained in:
Mark Nellemann 2023-03-01 19:43:35 +01:00
parent b28ccc13ea
commit 6b6e4173cb
13 changed files with 15606 additions and 17 deletions

1
.gitignore vendored
View file

@ -14,3 +14,4 @@ bin/
.settings
.classpath
.factorypath
.vscode

View file

@ -2,21 +2,39 @@
Test micronaut web application.
## Container
## Usage
### Build application container
### Build container
```shell
docker build -t hellomicronaut .
```
### Run container
```shell
docker run --name hello1 -p 8080:8080 hellomicronaut
```
## Development
### Test with browser
Requires Java JDK version 11+.
Connect to the container on the specified port:
### Build
```shell
./gradlew build
```
### Run
```shell
./gradlew run -t
```
## Test with browser
Connect to the container or localhost on the specified port:
<http://localhost:8080/>

View file

@ -1,6 +1,7 @@
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
id("io.micronaut.application") version "3.7.3"
id("com.magnetichq.client-dependencies") version "2.0.0"
}
version = "0.1"
@ -17,7 +18,7 @@ dependencies {
implementation("jakarta.annotation:jakarta.annotation-api")
runtimeOnly("ch.qos.logback:logback-classic")
implementation("io.micronaut:micronaut-validation")
implementation("io.micronaut.views:micronaut-views-thymeleaf")
}
@ -40,4 +41,10 @@ micronaut {
}
clientDependencies {
installDir = 'src/main/resources/assets/vendor'
npm {
'bulma'('0.9.4', into: 'bulma') { include 'css/bulma.*' }
'htmx.org'('1.8.5', into: 'htmx') { include 'htmx.js', 'ext/preload.js' }
}
}

View file

@ -1,29 +1,72 @@
package hello;
import io.micronaut.http.MediaType;
import java.util.ArrayDeque;
import java.util.Deque;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Consumes;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.annotation.Post;
import io.micronaut.views.View;
import jakarta.annotation.PostConstruct;
@Controller("/")
public class HelloController {
@Get("/")
@Produces(MediaType.TEXT_PLAIN)
public HttpResponse<String> index() {
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
private static long counter = 0;
private static Deque<String> msgDeque = new ArrayDeque<String>();
private static String hostname;
@PostConstruct
void initialize() {
log.info("initialize()");
hostname = System.getenv("HOSTNAME");
}
@Get("/")
@View("index")
public HttpResponse<?> index() {
String osName = System.getProperty("os.name");
String osVersion = System.getProperty("os.version");
String osArch = System.getProperty("os.arch");
String javaName = System.getProperty("java.vm.name");
String javaVersion = System.getProperty("java.vm.version");
String helloMessage = String.format("%s v%s, running %s %s on %s.", javaName, javaVersion, osName, osVersion, osArch);
String helloMessage = String.format("Hello from %s v%s, running %s %s on %s.", javaName, javaVersion, osName, osVersion, osArch);
return HttpResponse.ok(helloMessage);
return HttpResponse.ok(CollectionUtils.mapOf("hostname", hostname, "info", helloMessage));
}
@Post("/ping")
@View("pong")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public HttpResponse<?> ping(@Nullable String message) {
if(message != null && !message.isEmpty()) {
log.info(message);
counter++;
msgDeque.push(message);
//msgDeque.add(message);
//messages.add(0, message);
if(msgDeque.size() > 25) {
log.debug("Message deque: {}", msgDeque.size());
msgDeque.removeLast();
}
}
return HttpResponse.ok(CollectionUtils.mapOf("hostname", hostname, "counter", counter, "messages", msgDeque));
}
}

View file

@ -1,6 +1,18 @@
micronaut:
application:
name: hello
router:
static-resources:
default:
enabled: true
mapping: /**/*.html
paths:
- classpath:public
assets:
enabled: true
mapping: /assets/**
paths:
- classpath:assets
netty:
default:
allocator:

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,144 @@
// This adds the "preload" extension to htmx. By default, this will
// preload the targets of any tags with `href` or `hx-get` attributes
// if they also have a `preload` attribute as well. See documentation
// for more details
htmx.defineExtension("preload", {
onEvent: function(name, event) {
// Only take actions on "htmx:afterProcessNode"
if (name !== "htmx:afterProcessNode") {
return;
}
// SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
// attr gets the closest non-empty value from the attribute.
var attr = function(node, property) {
if (node == undefined) {return undefined;}
return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property)
}
// load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
// preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
var load = function(node) {
// Called after a successful AJAX request, to mark the
// content as loaded (and prevent additional AJAX calls.)
var done = function(html) {
if (!node.preloadAlways) {
node.preloadState = "DONE"
}
if (attr(node, "preload-images") == "true") {
document.createElement("div").innerHTML = html // create and populate a node to load linked resources, too.
}
}
return function() {
// If this value has already been loaded, then do not try again.
if (node.preloadState !== "READY") {
return;
}
// Special handling for HX-GET - use built-in htmx.ajax function
// so that headers match other htmx requests, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future
var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
if (hxGet) {
htmx.ajax("GET", hxGet, {handler:function(elt, info) {
done(info.xhr.responseText);
}});
return;
}
// Otherwise, perform a standard xhr request, then set
// node.preloadState = TRUE so that requests are not duplicated
// in the future.
if (node.getAttribute("href")) {
var r = new XMLHttpRequest();
r.open("GET", node.getAttribute("href"));
r.onload = function() {done(r.responseText);};
r.send();
return;
}
}
}
// This function processes a specific node and sets up event handlers.
// We'll search for nodes and use it below.
var init = function(node) {
// If this node DOES NOT include a "GET" transaction, then there's nothing to do here.
if (node.getAttribute("href") + node.getAttribute("hx-get") + node.getAttribute("data-hx-get") == "") {
return;
}
// Guarantee that we only initialize each node once.
if (node.preloadState !== undefined) {
return;
}
// Get event name from config.
var on = attr(node, "preload") || "mousedown"
const always = on.indexOf("always") !== -1
if (always) {
on = on.replace('always', '').trim()
}
// FALL THROUGH to here means we need to add an EventListener
// Apply the listener to the node
node.addEventListener(on, function(evt) {
if (node.preloadState === "PAUSE") { // Only add one event listener
node.preloadState = "READY"; // Requred for the `load` function to trigger
// Special handling for "mouseover" events. Wait 100ms before triggering load.
if (on === "mouseover") {
window.setTimeout(load(node), 100);
} else {
load(node)() // all other events trigger immediately.
}
}
})
// Special handling for certain built-in event handlers
switch (on) {
case "mouseover":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
// WHhen the mouse leaves, immediately disable the preload
node.addEventListener("mouseout", function(evt) {
if ((evt.target === node) && (node.preloadState === "READY")) {
node.preloadState = "PAUSE";
}
})
break;
case "mousedown":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
break;
}
// Mark the node as ready to run.
node.preloadState = "PAUSE";
node.preloadAlways = always;
htmx.trigger(node, "preload:init") // This event can be used to load content immediately.
}
// Search for all child nodes that have a "preload" attribute
event.target.querySelectorAll("[preload]").forEach(function(node) {
// Initialize the node with the "preload" attribute
init(node)
// Initialize all child elements that are anchors or have `hx-get` (use with care)
node.querySelectorAll("a,[hx-get],[data-hx-get]").forEach(init)
})
}
})

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
<!DOCTYPE html>
<html th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<meta content="true" name="intercoolerjs:use-actual-http-method" />
<title th:replace="${title}">Hello Micronaut</title>
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico">
<link href="/assets/vendor/bulma/css/bulma.min.css" rel="stylesheet">
</head>
<body>
<!-- Navigation -->
<nav class="navbar is-light" role="navigation" aria-label="Navigation" id="header-nav">
<div class="navbar-brand">
<a href="/" class="navbar-item">
<!-- <img src="./images/bulma-logo.png" alt=""> -->
<!--<img src="/assets/images/app-logo.svg" alt="" width="32" height="32" />-->
</a>
</div>
<div class="navbar-menu">
<div class="navbar-start">
<p class="navbar-item">Hello Micronaut</p>
</div>
<div class="navbar-end">
<p class="navbar-item">
<a href="https://git.data.coop/nellemann/hellomicronaut" target="_blank">git.data.coop/nellemann/hellomicronaut</a>
</p>
</div>
</div>
</nav>
<!-- ./Navigation -->
<div th:replace="${content}">
<p>Layout content</p>
</div>
<script src="/assets/vendor/htmx/htmx.js" type="text/javascript"></script>
</body>
</html>

View file

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en" th:replace="~{defaultLayout :: layout(~{::title}, ~{::section})}" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello Micronaut</title>
</head>
<body>
<section class="section">
<div class="container">
<article class="message">
<div class="message-header">
<p>Hello Micronaut from <span th:text="${hostname}"></span></span></p>
<button class="delete" aria-label="delete"></button>
</div>
<div class="message-body" th:text="${info}">
</div>
</article>
<div class="columns">
<div class="column">
<form hx-post="/ping">
<div class="field has-addons" hx-target="#pong">
<div class="control">
<input name="message" class="input" type="text" placeholder="Enter message">
</div>
<div class="control">
<a class="button is-info" hx-post="/ping" hx-include="[msg='message']">
Submit
</a>
</div>
</div>
</form>
</div>
</div>
<div class="columns mt-4">
<div class="collumn" id="pong" hx-post="/ping" hx-trigger="load, queue:last, every 5s">
</div>
</div>
</div>
</section>
</body>
</html>

View file

@ -0,0 +1,15 @@
<span class="tag is-warning is-medium" th:text="${hostname}">
Host
</span>
<span class="tag is-danger is-medium" th:text="${counter}">
Count
</span>
<div th:each="message: ${messages}">
<article class="message mt-3">
<div class="message-body">
<span th:text="${message}">text</span>
</div>
</article>
</div>