Display more info and accept / display messages.
This commit is contained in:
parent
b28ccc13ea
commit
6b6e4173cb
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,3 +14,4 @@ bin/
|
||||||
.settings
|
.settings
|
||||||
.classpath
|
.classpath
|
||||||
.factorypath
|
.factorypath
|
||||||
|
.vscode
|
||||||
|
|
30
README.md
30
README.md
|
@ -2,21 +2,39 @@
|
||||||
|
|
||||||
Test micronaut web application.
|
Test micronaut web application.
|
||||||
|
|
||||||
|
## Container
|
||||||
|
|
||||||
## Usage
|
### Build container
|
||||||
|
|
||||||
### Build application container
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build -t hellomicronaut .
|
docker build -t hellomicronaut .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run container
|
||||||
|
|
||||||
|
```shell
|
||||||
docker run --name hello1 -p 8080:8080 hellomicronaut
|
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/>
|
<http://localhost:8080/>
|
||||||
|
|
||||||
|
|
11
build.gradle
11
build.gradle
|
@ -1,6 +1,7 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||||
id("io.micronaut.application") version "3.7.3"
|
id("io.micronaut.application") version "3.7.3"
|
||||||
|
id("com.magnetichq.client-dependencies") version "2.0.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
|
@ -17,7 +18,7 @@ dependencies {
|
||||||
implementation("jakarta.annotation:jakarta.annotation-api")
|
implementation("jakarta.annotation:jakarta.annotation-api")
|
||||||
runtimeOnly("ch.qos.logback:logback-classic")
|
runtimeOnly("ch.qos.logback:logback-classic")
|
||||||
implementation("io.micronaut:micronaut-validation")
|
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' }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,29 +1,72 @@
|
||||||
package hello;
|
package hello;
|
||||||
|
|
||||||
import io.micronaut.http.MediaType;
|
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.HttpResponse;
|
||||||
|
import io.micronaut.http.annotation.Consumes;
|
||||||
import io.micronaut.http.annotation.Controller;
|
import io.micronaut.http.annotation.Controller;
|
||||||
import io.micronaut.http.annotation.Get;
|
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("/")
|
@Controller("/")
|
||||||
public class HelloController {
|
public class HelloController {
|
||||||
|
|
||||||
@Get("/")
|
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
|
||||||
@Produces(MediaType.TEXT_PLAIN)
|
|
||||||
public HttpResponse<String> index() {
|
|
||||||
|
|
||||||
|
|
||||||
|
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 osName = System.getProperty("os.name");
|
||||||
String osVersion = System.getProperty("os.version");
|
String osVersion = System.getProperty("os.version");
|
||||||
String osArch = System.getProperty("os.arch");
|
String osArch = System.getProperty("os.arch");
|
||||||
|
|
||||||
String javaName = System.getProperty("java.vm.name");
|
String javaName = System.getProperty("java.vm.name");
|
||||||
String javaVersion = System.getProperty("java.vm.version");
|
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(CollectionUtils.mapOf("hostname", hostname, "info", helloMessage));
|
||||||
|
|
||||||
return HttpResponse.ok(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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
micronaut:
|
micronaut:
|
||||||
application:
|
application:
|
||||||
name: hello
|
name: hello
|
||||||
|
router:
|
||||||
|
static-resources:
|
||||||
|
default:
|
||||||
|
enabled: true
|
||||||
|
mapping: /**/*.html
|
||||||
|
paths:
|
||||||
|
- classpath:public
|
||||||
|
assets:
|
||||||
|
enabled: true
|
||||||
|
mapping: /assets/**
|
||||||
|
paths:
|
||||||
|
- classpath:assets
|
||||||
netty:
|
netty:
|
||||||
default:
|
default:
|
||||||
allocator:
|
allocator:
|
||||||
|
|
11851
src/main/resources/assets/vendor/bulma/css/bulma.css
vendored
Normal file
11851
src/main/resources/assets/vendor/bulma/css/bulma.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
1
src/main/resources/assets/vendor/bulma/css/bulma.css.map
vendored
Normal file
1
src/main/resources/assets/vendor/bulma/css/bulma.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/main/resources/assets/vendor/bulma/css/bulma.min.css
vendored
Normal file
1
src/main/resources/assets/vendor/bulma/css/bulma.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
144
src/main/resources/assets/vendor/htmx/ext/preload.js
vendored
Normal file
144
src/main/resources/assets/vendor/htmx/ext/preload.js
vendored
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
3399
src/main/resources/assets/vendor/htmx/htmx.js
vendored
Normal file
3399
src/main/resources/assets/vendor/htmx/htmx.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
45
src/main/resources/views/defaultLayout.html
Normal file
45
src/main/resources/views/defaultLayout.html
Normal 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>
|
52
src/main/resources/views/index.html
Normal file
52
src/main/resources/views/index.html
Normal 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>
|
15
src/main/resources/views/pong.html
Normal file
15
src/main/resources/views/pong.html
Normal 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>
|
Loading…
Reference in a new issue