[WIP] Moving renderers to a common API

This commit is contained in:
Alexander Nozik 2021-12-06 22:45:24 +03:00
parent e7f0e1e4fc
commit 3caa22f8bf
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
11 changed files with 195 additions and 53 deletions

View File

@ -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
}

View File

@ -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> { 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> { page ->
//HTML(page.render(createHTML()), true)
}
}
}

View File

@ -25,7 +25,7 @@ internal class GdmlForJupyter : JupyterIntegration() {
private var counter = 0 private var counter = 0
private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().apply { private fun produceHtmlVisionString(fragment: HtmlVisionFragment) = createHTML().apply {
embedAndRenderVisionFragment(context.visionManager, counter++, fragment) embedAndRenderVisionFragment(context.visionManager, counter++, fragment = fragment)
}.finalize() }.finalize()
override fun Builder.onLoaded() { override fun Builder.onLoaded() {

View File

@ -22,6 +22,19 @@ rootProject.name = "visionforge"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
enableFeaturePreview("VERSION_CATALOGS") enableFeaturePreview("VERSION_CATALOGS")
dependencyResolutionManagement {
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
versionCatalogs {
create("npmlibs") {
from("ru.mipt.npm:version-catalog:0.10.7")
}
}
}
include( include(
// ":ui", // ":ui",
":ui:react", ":ui:react",
@ -46,5 +59,6 @@ include(
":demo:jupyter-playground", ":demo:jupyter-playground",
":demo:plotly-fx", ":demo:plotly-fx",
":demo:js-playground", ":demo:js-playground",
":jupyter:visionforge-gdml-jupyter" ":jupyter:visionforge-jupyter-base",
":jupyter:visionforge-jupyter-gdml"
) )

View File

@ -7,9 +7,11 @@ import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
public fun TagConsumer<*>.embedVisionFragment( public fun TagConsumer<*>.embedVisionFragment(
manager: VisionManager, manager: VisionManager,
embedData: Boolean = true,
fetchData: String? = null,
fetchUpdates: String? = null,
idPrefix: String? = null, idPrefix: String? = null,
fragment: HtmlVisionFragment, fragment: HtmlVisionFragment,
): Map<Name, Vision> { ): Map<Name, Vision> {
@ -17,6 +19,17 @@ public fun TagConsumer<*>.embedVisionFragment(
val consumer = object : VisionTagConsumer<Any?>(this@embedVisionFragment, manager, idPrefix) { val consumer = object : VisionTagConsumer<Any?>(this@embedVisionFragment, manager, idPrefix) {
override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) { override fun DIV.renderVision(name: Name, vision: Vision, outputMeta: Meta) {
visionMap[name] = vision visionMap[name] = vision
// Toggle update mode
fetchUpdates?.let {
attributes[OUTPUT_CONNECT_ATTRIBUTE] = it
}
fetchData?.let {
attributes[OUTPUT_FETCH_ATTRIBUTE] = it
}
if (embedData) {
script { script {
type = "text/json" type = "text/json"
attributes["class"] = OUTPUT_DATA_CLASS attributes["class"] = OUTPUT_DATA_CLASS
@ -26,25 +39,36 @@ public fun TagConsumer<*>.embedVisionFragment(
} }
} }
} }
}
fragment(consumer) fragment(consumer)
return visionMap return visionMap
} }
public fun FlowContent.embedVisionFragment( public fun FlowContent.embedVisionFragment(
manager: VisionManager, manager: VisionManager,
embedData: Boolean = true,
fetchDataUrl: String? = null,
fetchUpdatesUrl: String? = null,
idPrefix: String? = null, idPrefix: String? = null,
fragment: HtmlVisionFragment, fragment: HtmlVisionFragment,
): Map<Name, Vision> = consumer.embedVisionFragment(manager, idPrefix, fragment) ): Map<Name, Vision> = consumer.embedVisionFragment(manager, embedData, fetchDataUrl, fetchUpdatesUrl, idPrefix, fragment)
internal const val RENDER_FUNCTION_NAME = "renderAllVisionsById" internal const val RENDER_FUNCTION_NAME = "renderAllVisionsById"
@DFExperimental public fun TagConsumer<*>.embedAndRenderVisionFragment(
public fun TagConsumer<*>.embedAndRenderVisionFragment(manager: VisionManager, id: Any, fragment: HtmlVisionFragment) { manager: VisionManager,
id: Any,
embedData: Boolean = true,
fetchData: String? = null,
fetchUpdates: String? = null,
idPrefix: String? = null,
fragment: HtmlVisionFragment,
) {
div { div {
div { div {
this.id = id.toString() this.id = id.toString()
embedVisionFragment(manager, fragment = fragment) embedVisionFragment(manager, embedData, fetchData, fetchUpdates, idPrefix, fragment)
} }
script { script {
type = "text/javascript" type = "text/javascript"

View File

@ -126,6 +126,8 @@ public abstract class VisionTagConsumer<R>(
public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint" public const val OUTPUT_ENDPOINT_ATTRIBUTE: String = "data-output-endpoint"
public const val DEFAULT_ENDPOINT: String = "." public const val DEFAULT_ENDPOINT: String = "."
public const val AUTO_DATA_ATTRIBUTE: String = "@auto"
public const val DEFAULT_VISION_NAME: String = "vision" public const val DEFAULT_VISION_NAME: String = "vision"
} }
} }

View File

@ -18,7 +18,6 @@ import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_FETCH_
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_NAME_ATTRIBUTE
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
public class VisionClient : AbstractPlugin() { public class VisionClient : AbstractPlugin() {
override val tag: PluginTag get() = Companion.tag override val tag: PluginTag get() = Companion.tag
@ -27,7 +26,7 @@ public class VisionClient : AbstractPlugin() {
//private val visionMap = HashMap<Element, Vision>() //private val visionMap = HashMap<Element, Vision>()
/** /**
* 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 { private fun resolveEndpoint(element: Element?): String {
if (element == null) return window.location.href 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 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) { private fun renderVision(name: String, element: Element, vision: Vision?, outputMeta: Meta) {
if (vision != null) { if (vision != null) {
val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision") val renderer = findRendererFor(vision) ?: error("Could nof find renderer for $vision")
renderer.render(element, vision, outputMeta) renderer.render(element, vision, outputMeta)
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr -> 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) val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" } logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply { URL(endpoint).apply {
@ -154,7 +152,7 @@ public class VisionClient : AbstractPlugin() {
element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> { element.attributes[OUTPUT_FETCH_ATTRIBUTE] != null -> {
val attr = element.attributes[OUTPUT_FETCH_ATTRIBUTE]!! 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) val endpoint = resolveEndpoint(element)
logger.info { "Vision server is resolved to $endpoint" } logger.info { "Vision server is resolved to $endpoint" }
URL(endpoint).apply { URL(endpoint).apply {

View File

@ -34,10 +34,7 @@ import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionChange import space.kscience.visionforge.VisionChange
import space.kscience.visionforge.VisionManager import space.kscience.visionforge.VisionManager
import space.kscience.visionforge.flowChanges import space.kscience.visionforge.flowChanges
import space.kscience.visionforge.html.HtmlFragment import space.kscience.visionforge.html.*
import space.kscience.visionforge.html.HtmlVisionFragment
import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.fragment
import space.kscience.visionforge.three.server.VisionServer.Companion.DEFAULT_PAGE import space.kscience.visionforge.three.server.VisionServer.Companion.DEFAULT_PAGE
import java.awt.Desktop import java.awt.Desktop
import java.net.URI import java.net.URI
@ -71,36 +68,12 @@ public class VisionServer internal constructor(
globalHeaders.add(block) globalHeaders.add(block)
} }
private fun HTML.buildPage( private fun HTML.visionPage(
visionFragment: HtmlVisionFragment,
title: String, title: String,
headers: List<HtmlFragment>, headers: List<HtmlFragment>,
visionFragment: HtmlVisionFragment,
): Map<Name, Vision> { ): Map<Name, Vision> {
val visionMap = HashMap<Name, Vision>() var visionMap: Map<Name,Vision>? = null
val consumer = object : VisionTagConsumer<Any?>(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"
}
}
}
}
}
head { head {
meta { meta {
@ -113,16 +86,22 @@ public class VisionServer internal constructor(
} }
body { body {
//Load the fragment and remember all loaded visions //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 * 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<Name, Vision>): Unit = route { public fun serveVisions(route: Route, visions: Map<Name, Vision>): Unit = route {
application.log.info("Serving visions $visions at $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<HtmlFragment>, 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 * 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) { val cachedHtml: String? = if (cacheFragments) {
//Create and cache page html and map of visions //Create and cache page html and map of visions
createHTML(true).html { createHTML(true).html {
visions.putAll(buildPage(visionFragment, title, headers)) visions.putAll(visionPage(title, headers, visionFragment))
} }
} else { } else {
null null
@ -219,7 +214,7 @@ public class VisionServer internal constructor(
//re-create html and vision list on each call //re-create html and vision list on each call
call.respondHtml { call.respondHtml {
visions.clear() visions.clear()
visions.putAll(buildPage(visionFragment, title, headers)) visions.putAll(visionPage(title, headers, visionFragment))
} }
} else { } else {
//Use cached html //Use cached html