From 3caa22f8bf0bad62ea9d2b123e4f60a1b89eeaf1 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 6 Dec 2021 22:45:24 +0300 Subject: [PATCH] [WIP] Moving renderers to a common API --- .../visionforge-jupyter-base/build.gradle.kts | 25 ++++++ .../src/jvmMain/kotlin/JupyterPluginBase.kt | 84 +++++++++++++++++++ .../build.gradle.kts | 0 .../src/jsMain/kotlin/gdmlJupyter.kt | 0 .../src/jvmMain/kotlin/GdmlForJupyter.kt | 2 +- .../webpack.config.d/01.ring.js | 0 settings.gradle.kts | 16 +++- .../visionforge/html/HtmlVisionRenderer.kt | 44 +++++++--- .../visionforge/html/VisionTagConsumer.kt | 2 + .../kscience/visionforge/VisionClient.kt | 8 +- .../visionforge/three/server/VisionServer.kt | 67 +++++++-------- 11 files changed, 195 insertions(+), 53 deletions(-) create mode 100644 jupyter/visionforge-jupyter-base/build.gradle.kts create mode 100644 jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt rename jupyter/{visionforge-gdml-jupyter => visionforge-jupyter-gdml}/build.gradle.kts (100%) rename jupyter/{visionforge-gdml-jupyter => visionforge-jupyter-gdml}/src/jsMain/kotlin/gdmlJupyter.kt (100%) rename jupyter/{visionforge-gdml-jupyter => visionforge-jupyter-gdml}/src/jvmMain/kotlin/GdmlForJupyter.kt (98%) rename jupyter/{visionforge-gdml-jupyter => visionforge-jupyter-gdml}/webpack.config.d/01.ring.js (100%) diff --git a/jupyter/visionforge-jupyter-base/build.gradle.kts b/jupyter/visionforge-jupyter-base/build.gradle.kts new file mode 100644 index 00000000..3e5f671d --- /dev/null +++ b/jupyter/visionforge-jupyter-base/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("ru.mipt.npm.gradle.mpp") + id("org.jetbrains.kotlin.jupyter.api") +} + +description = "Common visionforge jupyter module" + +kotlin { + sourceSets { + commonMain{ + dependencies{ + api(projects.visionforge.visionforgeCore) + } + } + jvmMain { + dependencies { + implementation(project(":visionforge-server")) + } + } + } +} + +readme { + maturity = ru.mipt.npm.gradle.Maturity.EXPERIMENTAL +} \ No newline at end of file diff --git a/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt b/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt new file mode 100644 index 00000000..f912a88f --- /dev/null +++ b/jupyter/visionforge-jupyter-base/src/jvmMain/kotlin/JupyterPluginBase.kt @@ -0,0 +1,84 @@ +package space.kscience.visionforge.jupyter + +import io.ktor.server.cio.CIO +import io.ktor.server.engine.ApplicationEngine +import io.ktor.server.engine.embeddedServer +import kotlinx.html.stream.createHTML +import org.jetbrains.kotlinx.jupyter.api.HTML +import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration +import org.jetbrains.kotlinx.jupyter.api.libraries.resources +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.ContextAware +import space.kscience.dataforge.meta.get +import space.kscience.dataforge.meta.int +import space.kscience.dataforge.meta.string +import space.kscience.dataforge.misc.DFExperimental +import space.kscience.visionforge.Vision +import space.kscience.visionforge.html.HtmlVisionFragment +import space.kscience.visionforge.html.Page +import space.kscience.visionforge.html.embedAndRenderVisionFragment +import space.kscience.visionforge.three.server.VisionServer +import space.kscience.visionforge.three.server.visionServer +import space.kscience.visionforge.visionManager + +private const val DEFAULT_VISIONFORGE_PORT = 88898 + +@DFExperimental +public abstract class JupyterPluginBase( + override val context: Context, +) : JupyterIntegration(), ContextAware { + + private var counter = 0 + + private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().apply { + embedAndRenderVisionFragment(context.visionManager, counter++, fragment = fragment) + }.finalize() + + private var engine: ApplicationEngine? = null + private var server: VisionServer? = null + + override fun Builder.onLoaded() { + + onLoaded { + val host = context.properties["visionforge.host"].string ?: "localhost" + val port = context.properties["visionforge.port"].int ?: DEFAULT_VISIONFORGE_PORT + engine = context.embeddedServer(CIO, port, host) { + server = visionServer(context) + }.start() + } + + onShutdown { + engine?.stop(1000, 1000) + engine = null + server = null + } + + resources { + js("three") { + classPath("js/gdml-jupyter.js") + } + } + + import( + "kotlinx.html.*", + "space.kscience.visionforge.html.Page", + "space.kscience.visionforge.html.page", + ) + + render { vision -> + val server = this@JupyterPluginBase.server + if (server == null) { + HTML(produceHtmlVisionString { vision(vision) }) + } else { + val route = "route.${counter++}" + HTML(server.createHtmlAndServe(route,route, emptyList()){ + vision(vision) + }) + } + } + + render { page -> + //HTML(page.render(createHTML()), true) + } + } +} diff --git a/jupyter/visionforge-gdml-jupyter/build.gradle.kts b/jupyter/visionforge-jupyter-gdml/build.gradle.kts similarity index 100% rename from jupyter/visionforge-gdml-jupyter/build.gradle.kts rename to jupyter/visionforge-jupyter-gdml/build.gradle.kts diff --git a/jupyter/visionforge-gdml-jupyter/src/jsMain/kotlin/gdmlJupyter.kt b/jupyter/visionforge-jupyter-gdml/src/jsMain/kotlin/gdmlJupyter.kt similarity index 100% rename from jupyter/visionforge-gdml-jupyter/src/jsMain/kotlin/gdmlJupyter.kt rename to jupyter/visionforge-jupyter-gdml/src/jsMain/kotlin/gdmlJupyter.kt diff --git a/jupyter/visionforge-gdml-jupyter/src/jvmMain/kotlin/GdmlForJupyter.kt b/jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt similarity index 98% rename from jupyter/visionforge-gdml-jupyter/src/jvmMain/kotlin/GdmlForJupyter.kt rename to jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt index ce32f012..2aa1873b 100644 --- a/jupyter/visionforge-gdml-jupyter/src/jvmMain/kotlin/GdmlForJupyter.kt +++ b/jupyter/visionforge-jupyter-gdml/src/jvmMain/kotlin/GdmlForJupyter.kt @@ -25,7 +25,7 @@ internal class GdmlForJupyter : JupyterIntegration() { private var counter = 0 private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().apply { - embedAndRenderVisionFragment(context.visionManager, counter++, fragment) + embedAndRenderVisionFragment(context.visionManager, counter++, fragment = fragment) }.finalize() override fun Builder.onLoaded() { diff --git a/jupyter/visionforge-gdml-jupyter/webpack.config.d/01.ring.js b/jupyter/visionforge-jupyter-gdml/webpack.config.d/01.ring.js similarity index 100% rename from jupyter/visionforge-gdml-jupyter/webpack.config.d/01.ring.js rename to jupyter/visionforge-jupyter-gdml/webpack.config.d/01.ring.js diff --git a/settings.gradle.kts b/settings.gradle.kts index 4cdc6b82..088b448f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,19 @@ rootProject.name = "visionforge" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("VERSION_CATALOGS") +dependencyResolutionManagement { + repositories { + maven("https://repo.kotlin.link") + mavenCentral() + } + + versionCatalogs { + create("npmlibs") { + from("ru.mipt.npm:version-catalog:0.10.7") + } + } +} + include( // ":ui", ":ui:react", @@ -46,5 +59,6 @@ include( ":demo:jupyter-playground", ":demo:plotly-fx", ":demo:js-playground", - ":jupyter:visionforge-gdml-jupyter" + ":jupyter:visionforge-jupyter-base", + ":jupyter:visionforge-jupyter-gdml" ) diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt index e45e4f08..7bfe5012 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/HtmlVisionRenderer.kt @@ -7,9 +7,11 @@ import space.kscience.dataforge.names.Name import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionManager - public fun TagConsumer<*>.embedVisionFragment( manager: VisionManager, + embedData: Boolean = true, + fetchData: String? = null, + fetchUpdates: String? = null, idPrefix: String? = null, fragment: HtmlVisionFragment, ): Map { @@ -17,11 +19,23 @@ public fun TagConsumer<*>.embedVisionFragment( val consumer = object : VisionTagConsumer(this@embedVisionFragment, manager, idPrefix) { override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { visionMap[name] = vision - script { - type = "text/json" - attributes["class"] = OUTPUT_DATA_CLASS - unsafe { - +"\n${manager.encodeToString(vision)}\n" + // Toggle update mode + + fetchUpdates?.let { + attributes[OUTPUT_CONNECT_ATTRIBUTE] = it + } + + fetchData?.let { + attributes[OUTPUT_FETCH_ATTRIBUTE] = it + } + + if (embedData) { + script { + type = "text/json" + attributes["class"] = OUTPUT_DATA_CLASS + unsafe { + +"\n${manager.encodeToString(vision)}\n" + } } } } @@ -32,19 +46,29 @@ public fun TagConsumer<*>.embedVisionFragment( public fun FlowContent.embedVisionFragment( manager: VisionManager, + embedData: Boolean = true, + fetchDataUrl: String? = null, + fetchUpdatesUrl: String? = null, idPrefix: String? = null, fragment: HtmlVisionFragment, -): Map = consumer.embedVisionFragment(manager, idPrefix, fragment) +): Map = consumer.embedVisionFragment(manager, embedData, fetchDataUrl, fetchUpdatesUrl, idPrefix, fragment) internal const val RENDER_FUNCTION_NAME = "renderAllVisionsById" -@DFExperimental -public fun TagConsumer<*>.embedAndRenderVisionFragment(manager: VisionManager, id: Any, fragment: HtmlVisionFragment) { +public fun TagConsumer<*>.embedAndRenderVisionFragment( + manager: VisionManager, + id: Any, + embedData: Boolean = true, + fetchData: String? = null, + fetchUpdates: String? = null, + idPrefix: String? = null, + fragment: HtmlVisionFragment, +) { div { div { this.id = id.toString() - embedVisionFragment(manager, fragment = fragment) + embedVisionFragment(manager, embedData, fetchData, fetchUpdates, idPrefix, fragment) } script { type = "text/javascript" diff --git a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt index 035b6a6e..27725721 100644 --- a/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt +++ b/visionforge-core/src/commonMain/kotlin/space/kscience/visionforge/html/VisionTagConsumer.kt @@ -126,6 +126,8 @@ public abstract class VisionTagConsumer( public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint" public const val DEFAULT_ENDPOINT: String = "." + public const val AUTO_DATA_ATTRIBUTE: String = "@auto" + public const val DEFAULT_VISION_NAME: String = "vision" } } \ No newline at end of file diff --git a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt index 63f9a882..60f7a627 100644 --- a/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt +++ b/visionforge-core/src/jsMain/kotlin/space/kscience/visionforge/VisionClient.kt @@ -18,7 +18,6 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE import kotlin.reflect.KClass import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.ExperimentalTime public class VisionClient : AbstractPlugin() { override val tag: PluginTag get() = Companion.tag @@ -27,7 +26,7 @@ public class VisionClient : AbstractPlugin() { //private val visionMap = HashMap() /** - * Up-going tree traversal in search for endpoint attribute + * Up-going tree traversal in search for endpoint attribute. If element is null, return window URL */ private fun resolveEndpoint(element: Element?): String { if (element == null) return window.location.href @@ -57,14 +56,13 @@ public class VisionClient : AbstractPlugin() { private fun Element.getFlag(attribute: String): Boolean = attributes[attribute]?.value != null - @OptIn(ExperimentalTime::class) private fun renderVision(name: String, element: Element, vision: Vision?, outputMeta: Meta) { if (vision != null) { val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision") renderer.render(element, vision, outputMeta) element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr -> - val wsUrl = if (attr.value.isBlank() || attr.value == "auto") { + val wsUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) { val endpoint = resolveEndpoint(element) logger.info { "Vision server is resolved to $endpoint" } URL(endpoint).apply { @@ -154,7 +152,7 @@ public class VisionClient : AbstractPlugin() { element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> { val attr = element.attributes[OUTPUT_FETCH_ATTRIBUTE]!! - val fetchUrl = if (attr.value.isBlank() || attr.value == "auto") { + val fetchUrl = if (attr.value.isBlank() || attr.value == VisionTagConsumer.AUTO_DATA_ATTRIBUTE) { val endpoint = resolveEndpoint(element) logger.info { "Vision server is resolved to $endpoint" } URL(endpoint).apply { diff --git a/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt b/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt index 0018ab83..61c3fd84 100644 --- a/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt +++ b/visionforge-server/src/main/kotlin/space/kscience/visionforge/three/server/VisionServer.kt @@ -34,10 +34,7 @@ import space.kscience.visionforge.Vision import space.kscience.visionforge.VisionChange import space.kscience.visionforge.VisionManager import space.kscience.visionforge.flowChanges -import space.kscience.visionforge.html.HtmlFragment -import space.kscience.visionforge.html.HtmlVisionFragment -import space.kscience.visionforge.html.VisionTagConsumer -import space.kscience.visionforge.html.fragment +import space.kscience.visionforge.html.* import space.kscience.visionforge.three.server.VisionServer.Companion.DEFAULT_PAGE import java.awt.Desktop import java.net.URI @@ -71,36 +68,12 @@ public class VisionServer internal constructor( globalHeaders.add(block) } - private fun HTML.buildPage( - visionFragment: HtmlVisionFragment, + private fun HTML.visionPage( title: String, headers: List, + visionFragment: HtmlVisionFragment, ): Map { - val visionMap = HashMap() - - val consumer = object : VisionTagConsumer(consumer, visionManager) { - override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { - visionMap[name] = vision - // Toggle update mode - if (dataConnect) { - attributes[OUTPUT_CONNECT_ATTRIBUTE] = "auto" - } - - if (dataFetch) { - attributes[OUTPUT_FETCH_ATTRIBUTE] = "auto" - } - - if (dataEmbed) { - script { - type = "text/json" - attributes["class"] = OUTPUT_DATA_CLASS - unsafe { - +"\n${visionManager.encodeToString(vision)}\n" - } - } - } - } - } + var visionMap: Map? = null head { meta { @@ -113,16 +86,22 @@ public class VisionServer internal constructor( } body { //Load the fragment and remember all loaded visions - visionFragment(consumer) + visionMap = embedVisionFragment( + manager = visionManager, + embedData = true, + fetchDataUrl = VisionTagConsumer.AUTO_DATA_ATTRIBUTE, + fetchUpdatesUrl = VisionTagConsumer.AUTO_DATA_ATTRIBUTE, + fragment = visionFragment + ) } - return visionMap + return visionMap!! } /** * Server a map of visions without providing explicit html page for them */ - @OptIn(DFExperimental::class, ExperimentalTime::class) + @OptIn(DFExperimental::class) public fun serveVisions(route: Route, visions: Map): Unit = route { application.log.info("Serving visions $visions at $route") @@ -175,6 +154,22 @@ public class VisionServer internal constructor( } } + /** + * Create a static html page and serve visions produced in the process + */ + @DFExperimental + public fun createHtmlAndServe(route: String, title: String, headers: List, visionFragment: HtmlVisionFragment): String{ + val htmlString = createHTML().apply { + html { + visionPage(title, headers, visionFragment).also { + serveVisions(route, it) + } + } + }.finalize() + + return htmlString + } + /** * Serv visions in a given [route] without providing a page template */ @@ -203,7 +198,7 @@ public class VisionServer internal constructor( val cachedHtml: String? = if (cacheFragments) { //Create and cache page html and map of visions createHTML(true).html { - visions.putAll(buildPage(visionFragment, title, headers)) + visions.putAll(visionPage(title, headers, visionFragment)) } } else { null @@ -219,7 +214,7 @@ public class VisionServer internal constructor( //re-create html and vision list on each call call.respondHtml { visions.clear() - visions.putAll(buildPage(visionFragment, title, headers)) + visions.putAll(visionPage(title, headers, visionFragment)) } } else { //Use cached html