Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh 19 improve landing page experience #25

Merged
merged 5 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ notebook-profile-*
### pom release artefacts
release.properties
pom.xml.releaseBackup

# template classes generated by jte
jte-classes/
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
package tech.catheu.jnotebook;

import io.methvin.watcher.DirectoryChangeEvent;
import io.methvin.watcher.hashing.FileHash;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.subjects.PublishSubject;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tech.catheu.jnotebook.evaluate.GreedyInterpreter;
Expand All @@ -32,36 +35,38 @@ public class InteractiveNotebook {

private static final Logger LOG = LoggerFactory.getLogger(InteractiveNotebook.class);

private static final String RESOURCES_HELLO_WORLD_NOTEBOOK = "/jnb_interactive/hello_world.jsh";
private static final String RESOURCES_HELLO_WORLD_NOTEBOOK =
"/jnb_interactive/hello_world.jsh";
private static final String FILESYSTEM_HELLO_WORLD_NAME = "hello_world.jsh";

private final Main.InteractiveConfiguration configuration;
private static final String JSHELL_SUFFIX = ".jsh";
private final StaticParser staticParser;
private final Interpreter interpreter;
private final Renderer renderer;
private final InteractiveServer server;
private InteractiveServer server;

public InteractiveNotebook(final Main.InteractiveConfiguration configuration) {
this.configuration = configuration;
final ShellProvider shellProvider = new ShellProvider(configuration);
this.staticParser = new StaticParser(shellProvider);
this.interpreter = new GreedyInterpreter(shellProvider);
this.renderer = new Renderer(configuration);
this.server = new InteractiveServer(configuration);
}

public void run() throws IOException {
prepare();
server.start();
final Observable<DirectoryChangeEvent> notebookEvents =
final Observable<DirectoryChangeEvent> fileChangeEvents =
PathObservables.of(Paths.get(configuration.notebookPath))
.filter(e -> e.path().toString().endsWith(JSHELL_SUFFIX));
//.subscribe(s -> server.sendReload()); to subscribe on a side scheduler

final PublishSubject<DirectoryChangeEvent> manualTriggers = PublishSubject.create();
this.server = new InteractiveServer(configuration,
path -> manualTriggers.onNext(
directoryChangeEvent(path)));
server.start();
LOG.info("Notebook server started. Go to http://localhost:" + configuration.port);

notebookEvents
manualTriggers
.mergeWith(fileChangeEvents)
.doOnEach(e -> server.sendStatus(NotebookServerStatus.COMPUTE))
.map(staticParser::staticSnippets)
.doOnError(InteractiveNotebook::logError)
Expand All @@ -72,6 +77,17 @@ public void run() throws IOException {
.subscribe(server::sendUpdate, InteractiveNotebook::logError);
}

@NonNull
private static DirectoryChangeEvent directoryChangeEvent(final Path path) {
return new DirectoryChangeEvent(
DirectoryChangeEvent.EventType.MODIFY,
false,
path,
FileHash.fromLong(0),
0,
path.getRoot());
}

private void prepare() {
// ensure notebook folder exists, if not, create it.
final Path notebooksFolder = Paths.get(configuration.notebookPath);
Expand All @@ -91,7 +107,9 @@ private static void logError(Throwable e) {


public void stop() throws IOException {
server.stop();
if (server != null) {
server.stop();
}
staticParser.stop();
interpreter.stop();
renderer.stop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,15 @@ public void render(final Main.RenderConfiguration config) {
final Interpreted interpreted = interpreter.interpret(staticParsing);
final Rendering render = renderer.render(interpreted);
final HtmlTemplateEngine templateEngine = new HtmlTemplateEngine();
String html = templateEngine.render(config, false, render.html());
final HtmlTemplateEngine.TemplateData model =
new HtmlTemplateEngine.TemplateData(config, false, render.html(), null);
String html = templateEngine.render(model);
if (!config.noOptimize) {
html = optimizeHtml(html);
}

final String outputPath = optional(config.outputPath)
.orElse(Files.getNameWithoutExtension(config.inputPath) + ".html");
final String outputPath =
optional(config.outputPath).orElse(Files.getNameWithoutExtension(config.inputPath) + ".html");
final File outputFile = FileUtils.getFile(outputPath);
FileUtils.write(outputFile, html, StandardCharsets.UTF_8);
LOG.info("Notebook rendered successfully and written to {}", outputFile);
Expand Down Expand Up @@ -139,7 +141,8 @@ public void handle(HttpExchange exchange) throws IOException {
responseStream.close();
}
});
final HtmlFileServer result = new HtmlFileServer("http://localhost:" + port, server);
final HtmlFileServer result =
new HtmlFileServer("http://localhost:" + port, server);
return result;
} catch (Exception e) {
LOG.error("Failed to create a server: ", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import tech.catheu.jnotebook.utils.JavaUtils;

import java.nio.file.Path;
import java.util.List;

public class HtmlTemplateEngine {

Expand All @@ -36,15 +37,13 @@ public HtmlTemplateEngine() {
}

// render is the generated notebook html
public String render(final Main.SharedConfiguration config, final boolean interactive,
final String render) {
final TemplateModel model = new TemplateModel(config, interactive, render);
public String render(TemplateData model) {
final TemplateOutput output = new StringOutput();
delegate.render("index.jte", model, output);
return output.toString();
}

public record TemplateModel(Main.SharedConfiguration config, boolean interactive, String render) {}
public record TemplateData(Main.SharedConfiguration config, boolean interactive, String render, List<Path> notebooksInPath) {}


}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import io.undertow.util.Headers;
import io.undertow.websockets.WebSocketConnectionCallback;
import io.undertow.websockets.WebSocketProtocolHandshakeHandler;
import io.undertow.websockets.core.AbstractReceiveListener;
import io.undertow.websockets.core.BufferedTextMessage;
import io.undertow.websockets.core.WebSocketChannel;
import io.undertow.websockets.core.WebSockets;
import io.undertow.websockets.spi.WebSocketHttpExchange;
Expand All @@ -28,22 +30,30 @@
import tech.catheu.jnotebook.render.Rendering;

import java.io.IOException;
import java.util.ArrayList;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.function.Consumer;
import java.util.stream.Stream;

public class InteractiveServer {

private static final Logger LOG = LoggerFactory.getLogger(Main.class);
private static final Logger LOG = LoggerFactory.getLogger(InteractiveServer.class);

private final Main.InteractiveConfiguration configuration;
private final Consumer<Path> renderTrigger;

private Undertow server;
private final List<WebSocketChannel> channels = new ArrayList<>();
private final Queue<WebSocketChannel> channels = new ConcurrentLinkedQueue<>();
XnioWorker worker;
private Rendering lastUpdate;

public InteractiveServer(final Main.InteractiveConfiguration configuration) {
public InteractiveServer(final Main.InteractiveConfiguration configuration,
Consumer<Path> renderTrigger) {
this.configuration = configuration;
this.renderTrigger = renderTrigger;
}


Expand Down Expand Up @@ -94,7 +104,20 @@ private static class TemplatedHttpHandler implements HttpHandler {

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
final String html = templateEngine.render(this.configuration, true, null);
final List<Path> notebooksInPath;
try (Stream<Path> files = Files.find(Path.of(configuration.notebookPath),
10,
(path, attributes) -> attributes.isRegularFile() && path.toString()
.endsWith(
".jsh"))) {
notebooksInPath = files.toList();
}
final HtmlTemplateEngine.TemplateData model = new HtmlTemplateEngine.TemplateData(
this.configuration,
true,
null,
notebooksInPath);
final String html = templateEngine.render(model);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html");
exchange.getResponseSender().send(html);
}
Expand All @@ -119,7 +142,8 @@ private void sendMessage(final String message) {
}
}
if (!messageSent) {
LOG.error("ERROR: trying to send updates but no client is opened. Go to http://localhost:" + configuration.port);
LOG.error(
"ERROR: trying to send updates but no client is opened. Go to http://localhost:" + configuration.port);
}
}

Expand All @@ -144,10 +168,29 @@ public void onConnect(WebSocketHttpExchange webSocketHttpExchange,
channels.add(channel);
// resend to every channel - not very correct but simpler for the moment
sendStatus(NotebookServerStatus.CONNECTED);
setupReceiver(channel);
if (lastUpdate != null) {
// resend to every channel - not necessary but simpler for the moment
sendUpdate(lastUpdate);
}
}

private void setupReceiver(WebSocketChannel channel) {
channel.getReceiveSetter().set(new AbstractReceiveListener() {
@Override
protected void onFullTextMessage(WebSocketChannel channel,
BufferedTextMessage message) throws IOException {
final String messageText = message.getData();
if (messageText.startsWith("refresh_")) {
final String substring = messageText.substring(8);
LOG.info("Triggering refresh for file {}", substring);
renderTrigger.accept(Path.of(substring));
} else {
LOG.error("Received unsupported message from websocket: {}", messageText);
}
}
});
channel.resumeReceives();
}
}
}
Empty file.
41 changes: 20 additions & 21 deletions jnotebook-core/src/main/jte/index.jte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@import tech.catheu.jnotebook.Main.InteractiveConfiguration
@import tech.catheu.jnotebook.server.HtmlTemplateEngine.TemplateModel
@import tech.catheu.jnotebook.server.HtmlTemplateEngine.TemplateData
@import java.nio.file.Path
@import java.util.Optional
@param TemplateModel model
@param TemplateData model
<!--/*

Copyright 2023 Cyril de Catheu
Expand Down Expand Up @@ -646,7 +647,9 @@
background-color: #ffe4e4;
}

[x-cloak] { display: none !important; }
[x-cloak] {
display: none !important;
}
</style>

<script defer class="jnb-no-opti" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>
Expand All @@ -670,7 +673,7 @@
</head>
<body>
<div x-cloak x-data="{ tocOpen: window.innerWidth >= 768, notebookStatus: 'Connected'}" class="flex">
<button x-on:click="tocOpen = ! tocOpen"
<button x-on:click="tocOpen=!tocOpen"
class="toc-toggle z-20 fixed right-2 top-2 md:right-auto md:left-3 md:top-[7px] text-slate-400 font-sans text-xs hover:underline cursor-pointer flex items-center bg-white py-1 px-3 md:p-0 rounded-full md:rounded-none border md:border-0 border-slate-200 shadow md:shadow-none">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="20"
height="20">
Expand All @@ -687,7 +690,7 @@
<div class="px-3 mb-1 mt-1 md:mt-0 text-xs md:text-[12px] uppercase tracking-wider text-slate-500 font-medium px-3 mb-1 leading-none">
ToC
</div>
<div x-on:click="tocOpen = ! tocOpen"
<div x-on:click="tocOpen=!tocOpen"
class="toc-toggle text-slate-500 absolute right-2 top-[11px] cursor-pointer z-10">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="2" class="w-4 w-4">
Expand All @@ -697,23 +700,13 @@
<div id="toc"></div>
</aside>
<div x-bind:class="tocOpen ? 'toc-offset' : ''" class="flex-auto h-screen overflow-y-auto scroll-container">
@if(model.render() != null)
<div id="notebook" class="flex flex-col items-center viewer-notebook flex-auto">
<div id="notebook" class="flex flex-col items-center viewer-notebook flex-auto">
@if(model.render() != null)
$unsafe{model.render()}
</div>
@else
<div id="notebook" class="flex flex-col items-center viewer-notebook flex-auto">
<div id="helper" class="viewer viewer-result w-full max-w-prose px-8">
<div class="relative">
<div class="overflow-y-hidden">
<p>Welcome to <code>jnotebook</code>. Learn more on <a href="https://jnotebook.catheu.tech">jnotebook.catheu.tech</a>.
</p>
<p>Edit a <code>.jsh</code> file in your <code>${((InteractiveConfiguration) model.config()).notebookPath}</code> folder to
make your notebook appear.</p></div>
</div>
</div>
</div>
@endif
@else
@template.landing(model=model)
@endif
</div>
</div>
@if(model.interactive())
<script defer>
Expand Down Expand Up @@ -761,6 +754,12 @@
update(socketEvent.data);
}
};

// something like this for the message passing
const textAreas = document.querySelectorAll("button.notebook-in-path")
.forEach(e => e.addEventListener('click', ev => {
socket.send("refresh_" + e.textContent)
}));
}

const port = "${Optional.ofNullable(((InteractiveConfiguration) model.config()).port).orElse(5002)}";
Expand Down
34 changes: 34 additions & 0 deletions jnotebook-core/src/main/jte/landing.jte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@import tech.catheu.jnotebook.Main.InteractiveConfiguration
@param tech.catheu.jnotebook.server.HtmlTemplateEngine.TemplateData model

<div id="helper" class="viewer viewer-result w-full max-w-prose px-8">
<div class="relative">
<div class="overflow-y-hidden">
<p>Welcome to <code>jnotebook</code>. Learn more on <a href="https://jnotebook.catheu.tech">jnotebook.catheu.tech</a>.
</p>
<p>Edit a <code>.jsh</code> file in your
<code>${((InteractiveConfiguration) model.config()).notebookPath}</code> folder to
launch your notebook, or click on a notebook below.</p>
<div>
<table>
<tr>
<td>Notebooks</td>
</tr>
@if(model.notebooksInPath() == null || model.notebooksInPath().isEmpty())
<tr>
<td>
<i>There are no notebooks in the <code>${((InteractiveConfiguration) model.config()).notebookPath}</code> folder.</i>
</td>
</tr>
@else
@for(final java.nio.file.Path p: model.notebooksInPath())
<tr>
<td><button class="notebook-in-path"><a>${p.toString()}</a></button></td>
</tr>
@endfor
@endif
</table>
</div>
</div>
</div>
</div>