Update static/dynamic rendering logic

This commit is contained in:
Alexander Nozik 2022-11-20 19:42:05 +03:00
parent 3c51060e2e
commit eae1316de5
No known key found for this signature in database
GPG Key ID: F7FCF2DD25C71357
17 changed files with 539 additions and 104 deletions

View File

@ -12,7 +12,7 @@ val fxVersion by extra("11")
allprojects {
group = "space.kscience"
version = "0.3.0-dev-3"
version = "0.3.0-dev-4"
}
subprojects {

View File

@ -1,6 +1,6 @@
package ru.mipt.npm.muon.monitor
import kotlinx.browser.document
import org.w3c.dom.Document
import react.dom.client.createRoot
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.fetch
@ -13,7 +13,7 @@ import space.kscience.visionforge.startApplication
private class MMDemoApp : Application {
override fun start(state: Map<String, Any>) {
override fun start(document: Document, state: Map<String, Any>) {
val context = Context("MM-demo") {
plugin(ThreePlugin)

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.jupyter.VFNotebookPlugin
import space.kscience.visionforge.markup.MarkupPlugin
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.ring.ThreeWithControlsPlugin
@ -11,4 +12,5 @@ fun main() = runVisionClient {
plugin(PlotlyPlugin)
plugin(MarkupPlugin)
plugin(TableVisionJsPlugin)
plugin(VFNotebookPlugin)
}

View File

@ -6,13 +6,13 @@ import space.kscience.dataforge.misc.DFExperimental
import space.kscience.gdml.Gdml
import space.kscience.plotly.Plot
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.jupyter.JupyterPluginBase
import space.kscience.visionforge.jupyter.VFIntegrationBase
import space.kscience.visionforge.plotly.PlotlyPlugin
import space.kscience.visionforge.plotly.asVision
import space.kscience.visionforge.solid.Solids
@DFExperimental
internal class VisionForgePlayGroundForJupyter : JupyterPluginBase(
internal class VisionForgePlayGroundForJupyter : VFIntegrationBase(
Context("VisionForge") {
plugin(Solids)
plugin(PlotlyPlugin)

View File

@ -11,7 +11,6 @@ import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Colors
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.server.DataServeMode
import space.kscience.visionforge.server.close
import space.kscience.visionforge.server.openInBrowser
import space.kscience.visionforge.server.serve
@ -37,7 +36,6 @@ fun main() {
}
val server = satContext.visionManager.serve {
dataMode = DataServeMode.UPDATE
page(VisionPage.threeJsHeader, VisionPage.styleSheetHeader("css/styles.css")) {
div("flex-column") {
h1 { +"Satellite detector demo" }

View File

@ -1,9 +1,9 @@
package space.kscience.visionforge.solid.demo
import kotlinx.browser.document
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import org.w3c.dom.Document
import space.kscience.visionforge.Application
import space.kscience.visionforge.solid.x
import space.kscience.visionforge.solid.y
@ -12,7 +12,7 @@ import kotlin.random.Random
private class ThreeDemoApp : Application {
override fun start(state: Map<String, Any>) {
override fun start(document: Document, state: Map<String, Any>) {
val element = document.getElementById("demo") ?: error("Element with id 'demo' not found on page")

View File

@ -0,0 +1,51 @@
package space.kscience.visionforge.jupyter
import kotlinx.browser.window
import org.w3c.dom.Element
import space.kscience.dataforge.context.AbstractPlugin
import space.kscience.dataforge.context.Context
import space.kscience.dataforge.context.PluginFactory
import space.kscience.dataforge.context.PluginTag
import space.kscience.dataforge.meta.Meta
import space.kscience.visionforge.VisionClient
import space.kscience.visionforge.renderAllVisions
import space.kscience.visionforge.renderAllVisionsById
import space.kscience.visionforge.renderAllVisionsIn
import kotlin.reflect.KClass
@JsExport
public class VFNotebookPlugin : AbstractPlugin() {
private val client by require(VisionClient)
public fun renderAllVisionsIn(element: Element) {
client.renderAllVisionsIn(element)
}
public fun renderAllVisionsById(id: String) {
client.renderAllVisionsById(id)
}
public fun renderAllVisions() {
client.renderAllVisions()
}
init {
//register VisionForge in the browser window
window.asDynamic().vf = this
window.asDynamic().VisionForge = this
}
@Suppress("NON_EXPORTABLE_TYPE")
override val tag: PluginTag get() = Companion.tag
@Suppress("NON_EXPORTABLE_TYPE")
public companion object : PluginFactory<VFNotebookPlugin> {
override fun build(context: Context, meta: Meta): VFNotebookPlugin = VFNotebookPlugin()
override val tag: PluginTag = PluginTag(name = "vision.notebook", group = PluginTag.DATAFORGE_GROUP)
override val type: KClass<out VFNotebookPlugin> = VFNotebookPlugin::class
}
}

View File

@ -1,11 +1,9 @@
package space.kscience.visionforge.jupyter
import io.ktor.server.engine.ApplicationEngine
import kotlinx.html.FORM
import kotlinx.html.TagConsumer
import kotlinx.html.p
import kotlinx.coroutines.CoroutineScope
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import kotlinx.html.style
import org.jetbrains.kotlinx.jupyter.api.HTML
import org.jetbrains.kotlinx.jupyter.api.MimeTypedResult
import space.kscience.dataforge.context.Context
@ -21,11 +19,21 @@ import space.kscience.visionforge.html.visionFragment
import space.kscience.visionforge.server.VisionServer
import space.kscience.visionforge.server.serve
import space.kscience.visionforge.visionManager
import kotlin.coroutines.CoroutineContext
import kotlin.random.Random
import kotlin.random.nextUInt
internal fun TagConsumer<*>.renderScriptForId(id: String) {
script {
type = "text/javascript"
unsafe { +"VisionForge.renderAllVisionsById(\"$id\");" }
}
}
/**
* A handler class that includes a server and common utilities
*/
public class VisionForgeForNotebook(override val context: Context) : ContextAware {
public class VFForNotebook(override val context: Context) : ContextAware, CoroutineScope {
private var counter = 0
private var engine: ApplicationEngine? = null
@ -33,6 +41,8 @@ public class VisionForgeForNotebook(override val context: Context) : ContextAwar
public var isolateFragments: Boolean = false
override val coroutineContext: CoroutineContext get() = context.coroutineContext
public fun legacyMode() {
isolateFragments = true
}
@ -68,15 +78,29 @@ public class VisionForgeForNotebook(override val context: Context) : ContextAwar
public fun stopServer() {
engine?.apply {
logger.info { "Stopping VisionForge server" }
}?.stop(1000, 2000)
stop(1000, 2000)
engine = null
server = null
}
}
private fun produceHtmlString(
fragment: HtmlVisionFragment,
): String = server?.serveVisionsFromFragment("content[${counter++}]", fragment)
?: createHTML().apply {
visionFragment(context, fragment = fragment)
}.finalize()
): String = createHTML().apply {
val server = server
val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]"
div {
this.id = id
if (server != null) {
//if server exist, serve dynamically
server.serveVisionsFromFragment(consumer, "content-${counter++}", fragment)
} else {
//if not, use static rendering
visionFragment(context, fragment = fragment)
}
}
renderScriptForId(id)
}.finalize()
public fun produceHtml(isolated: Boolean? = null, fragment: HtmlVisionFragment): MimeTypedResult =
HTML(produceHtmlString(fragment), isolated ?: isolateFragments)

View File

@ -1,8 +1,7 @@
package space.kscience.visionforge.jupyter
import kotlinx.html.p
import kotlinx.html.*
import kotlinx.html.stream.createHTML
import kotlinx.html.style
import org.jetbrains.kotlinx.jupyter.api.HTML
import org.jetbrains.kotlinx.jupyter.api.declare
import org.jetbrains.kotlinx.jupyter.api.libraries.JupyterIntegration
@ -11,11 +10,16 @@ import space.kscience.dataforge.context.ContextAware
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.Vision
import space.kscience.visionforge.html.*
import kotlin.random.Random
import kotlin.random.nextUInt
/**
* A base class for different Jupyter VF integrations
*/
@DFExperimental
public abstract class JupyterPluginBase(final override val context: Context) : JupyterIntegration(), ContextAware {
public abstract class VFIntegrationBase(final override val context: Context) : JupyterIntegration(), ContextAware {
protected val handler: VisionForgeForNotebook = VisionForgeForNotebook(context)
protected val handler: VFForNotebook = VFForNotebook(context)
protected abstract fun Builder.afterLoaded()
@ -50,7 +54,24 @@ public abstract class JupyterPluginBase(final override val context: Context) : J
}
render<VisionPage> { page ->
HTML(page.render(createHTML()), true)
HTML(createHTML().apply {
head {
meta {
charset = "utf-8"
}
page.pageHeaders.values.forEach {
fragment(it)
}
}
body {
val id = "fragment[${page.hashCode()}/${Random.nextUInt()}]"
div {
this.id = id
visionFragment(context, fragment = page.content)
}
renderScriptForId(id)
}
}.finalize(), true)
}
render<HtmlFormFragment> { fragment ->

View File

@ -5,11 +5,11 @@ import space.kscience.dataforge.context.Context
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.gdml.Gdml
import space.kscience.visionforge.gdml.toVision
import space.kscience.visionforge.jupyter.JupyterPluginBase
import space.kscience.visionforge.jupyter.VFIntegrationBase
import space.kscience.visionforge.solid.Solids
@DFExperimental
internal class GdmlForJupyter : JupyterPluginBase(
internal class GdmlForJupyter : VFIntegrationBase(
Context("GDML") {
plugin(Solids)
}

View File

@ -8,8 +8,6 @@ import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.Vision
import space.kscience.visionforge.VisionManager
import kotlin.random.Random
import kotlin.random.nextUInt
public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
@ -17,9 +15,6 @@ public typealias HtmlVisionFragment = VisionTagConsumer<*>.() -> Unit
public fun HtmlVisionFragment(content: VisionTagConsumer<*>.() -> Unit): HtmlVisionFragment = content
internal const val RENDER_FUNCTION_NAME = "renderAllVisionsById"
/**
* Render a fragment in the given consumer and return a map of extracted visions
* @param context a context used to create a vision fragment
@ -35,7 +30,6 @@ public fun TagConsumer<*>.visionFragment(
fetchDataUrl: String? = null,
fetchUpdatesUrl: String? = null,
idPrefix: String? = null,
renderScript: Boolean = true,
fragment: HtmlVisionFragment,
): Map<Name, Vision> {
val visionMap = HashMap<Name, Vision>()
@ -63,19 +57,9 @@ public fun TagConsumer<*>.visionFragment(
}
}
}
if (renderScript) {
val id = "fragment[${fragment.hashCode()}/${Random.nextUInt()}]"
div {
this.id = id
fragment(consumer)
}
script {
type = "text/javascript"
unsafe { +"window.${RENDER_FUNCTION_NAME}(\"$id\");" }
}
} else {
fragment(consumer)
}
fragment(consumer)
return visionMap
}
@ -83,16 +67,14 @@ public fun FlowContent.visionFragment(
context: Context = Global,
embedData: Boolean = true,
fetchDataUrl: String? = null,
fetchUpdatesUrl: String? = null,
flowDataUrl: String? = null,
idPrefix: String? = null,
renderScript: Boolean = true,
fragment: HtmlVisionFragment,
): Map<Name, Vision> = consumer.visionFragment(
context,
embedData,
fetchDataUrl,
fetchUpdatesUrl,
flowDataUrl,
idPrefix,
renderScript,
fragment
)

View File

@ -13,20 +13,6 @@ public data class VisionPage(
public val pageHeaders: Map<String, HtmlFragment> = emptyMap(),
public val content: HtmlVisionFragment,
) {
public fun <R> render(root: TagConsumer<R>): R = root.apply {
head {
meta {
charset = "utf-8"
}
pageHeaders.values.forEach {
fragment(it)
}
}
body {
visionFragment(context, fragment = content)
}
}.finalize()
public companion object{
/**
* Use a script with given [src] as a global header for all pages.

View File

@ -2,7 +2,7 @@ package space.kscience.visionforge
import kotlinx.browser.document
import kotlinx.coroutines.CoroutineScope
import kotlinx.dom.hasClass
import org.w3c.dom.Document
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
@ -36,7 +36,7 @@ public interface Application: CoroutineScope {
* Starting point for an application.
* @param state Initial state between Hot Module Replacement (HMR).
*/
public fun start(state: Map<String, Any>)
public fun start(document: Document, state: Map<String, Any>)
/**
* Ending point for an application.
@ -46,17 +46,13 @@ public interface Application: CoroutineScope {
}
public fun startApplication(builder: () -> Application) {
fun start(state: dynamic): Application? {
return if (document.body?.hasClass("application") == true) {
val application = builder()
fun start(document: Document, state: dynamic): Application{
val application = builder()
@Suppress("UnsafeCastFromDynamic")
application.start(state?.appState ?: emptyMap())
@Suppress("UnsafeCastFromDynamic")
application.start(document, state?.appState ?: emptyMap())
application
} else {
null
}
return application
}
var application: Application? = null
@ -73,9 +69,9 @@ public fun startApplication(builder: () -> Application) {
}
if (document.body != null) {
application = start(state)
application = start(document, state)
} else {
application = null
document.addEventListener("DOMContentLoaded", { application = start(state) })
document.addEventListener("DOMContentLoaded", { application = start(document, state) })
}
}

View File

@ -13,7 +13,6 @@ import space.kscience.dataforge.meta.MetaSerializer
import space.kscience.dataforge.meta.get
import space.kscience.dataforge.meta.int
import space.kscience.dataforge.names.Name
import space.kscience.visionforge.html.RENDER_FUNCTION_NAME
import space.kscience.visionforge.html.VisionTagConsumer
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_CONNECT_ATTRIBUTE
import space.kscience.visionforge.html.VisionTagConsumer.Companion.OUTPUT_ENDPOINT_ATTRIBUTE
@ -46,18 +45,16 @@ public class VisionClient : AbstractPlugin() {
return attribute?.value
}
private fun getRenderers() = context.gather<ElementVisionRenderer>(ElementVisionRenderer.TYPE).values
private val renderers by lazy { context.gather<ElementVisionRenderer>(ElementVisionRenderer.TYPE).values }
private fun findRendererFor(vision: Vision): ElementVisionRenderer? {
return getRenderers().mapNotNull {
val rating = it.rateVision(vision)
if (rating > 0) {
rating to it
} else {
null
}
}.maxByOrNull { it.first }?.second
}
private fun findRendererFor(vision: Vision): ElementVisionRenderer? = renderers.mapNotNull {
val rating = it.rateVision(vision)
if (rating > 0) {
rating to it
} else {
null
}
}.maxByOrNull { it.first }?.second
private fun Element.getEmbeddedData(className: String): String? = getElementsByClassName(className)[0]?.innerHTML
@ -78,7 +75,7 @@ public class VisionClient : AbstractPlugin() {
if (vision != null) {
vision.setAsRoot(visionManager)
val renderer = findRendererFor(vision)
?: error("Could not find renderer for ${visionManager.encodeToString(vision)}")
?: error("Could not find renderer for ${vision::class}")
renderer.render(element, vision, outputMeta)
element.attributes[OUTPUT_CONNECT_ATTRIBUTE]?.let { attr ->
@ -228,7 +225,7 @@ public class VisionClient : AbstractPlugin() {
private fun whenDocumentLoaded(block: Document.() -> Unit): Unit {
if (document.readyState == DocumentReadyState.COMPLETE) {
if (document.body != null) {
block(document)
} else {
document.addEventListener("DOMContentLoaded", { block(document) })
@ -267,14 +264,29 @@ public fun VisionClient.renderAllVisions(): Unit = whenDocumentLoaded {
renderAllVisionsIn(element)
}
public class VisionClientApplication(public val context: Context) : Application {
private val client = context.fetch(VisionClient)
override fun start(document: Document, state: Map<String, Any>) {
console.info("Starting Vision Client")
val element = document.body ?: error("Document does not have a body")
client.renderAllVisionsIn(element)
}
}
/**
* Create a vision client context and render all visions on the page.
*/
public fun runVisionClient(contextBuilder: ContextBuilder.() -> Unit) {
console.info("Starting VisionForge context")
val context = Context("VisionForge", contextBuilder)
val visionClient = context.fetch(VisionClient)
window.asDynamic()[RENDER_FUNCTION_NAME] = visionClient::renderAllVisionsById
//visionClient.renderAllVisions()
val context = Context("VisionForge") {
plugin(VisionClient)
contextBuilder()
}
startApplication {
VisionClientApplication(context)
}
}

View File

@ -1,9 +1,14 @@
package space.kscience.visionforge
import kotlinx.html.body
import kotlinx.html.head
import kotlinx.html.meta
import kotlinx.html.stream.createHTML
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.visionforge.html.HtmlFragment
import space.kscience.visionforge.html.VisionPage
import space.kscience.visionforge.html.fragment
import space.kscience.visionforge.html.visionFragment
import java.awt.Desktop
import java.nio.file.Files
import java.nio.file.Path
@ -56,20 +61,34 @@ import java.nio.file.Path
/**
* Export a [VisionPage] to a file
*
* @param fileHeaders additional file-system specific headers.
*/
@DFExperimental
public fun VisionPage.makeFile(
path: Path?,
defaultHeaders: ((Path) -> Map<String, HtmlFragment>)? = null,
fileHeaders: ((Path) -> Map<String, HtmlFragment>)? = null,
): Path {
val actualFile = path?.let {
Path.of(System.getProperty("user.home")).resolve(path)
} ?: Files.createTempFile("tempPlot", ".html")
val actualDefaultHeaders = defaultHeaders?.invoke(actualFile)
val actualPage = if (actualDefaultHeaders == null) this else copy(pageHeaders = actualDefaultHeaders + pageHeaders)
val actualDefaultHeaders = fileHeaders?.invoke(actualFile)
val actualHeaders = if (actualDefaultHeaders == null) pageHeaders else actualDefaultHeaders + pageHeaders
val htmlString = actualPage.render(createHTML())
val htmlString = createHTML().apply {
head {
meta {
charset = "utf-8"
}
actualHeaders.values.forEach {
fragment(it)
}
}
body {
visionFragment(context, fragment = content)
}
}.finalize()
Files.writeString(actualFile, htmlString)
return actualFile

View File

@ -115,7 +115,7 @@ public class VisionServer internal constructor(
context = visionManager.context,
embedData = dataMode == DataServeMode.EMBED,
fetchDataUrl = if (dataMode != DataServeMode.EMBED) "$serverUrl$pagePath/data" else null,
fetchUpdatesUrl = if (dataMode == DataServeMode.UPDATE) "$serverUrl$pagePath/ws" else null,
flowDataUrl = if (dataMode == DataServeMode.UPDATE) "$serverUrl$pagePath/ws" else null,
fragment = visionFragment
)
}
@ -192,18 +192,19 @@ public class VisionServer internal constructor(
* Compile a fragment to string and serve visions from it
*/
public fun serveVisionsFromFragment(
consumer: TagConsumer<*>,
route: String,
fragment: HtmlVisionFragment,
): String = createHTML().apply {
val visions = visionFragment(
): Unit {
val visions = consumer.visionFragment(
visionManager.context,
embedData = true,
fetchUpdatesUrl = "$serverUrl$route/ws",
renderScript = true,
fragment = fragment
)
serveVisions(route, visions)
}.finalize()
}
/**
* Serve a page, potentially containing any number of visions at a given [route] with given [header].
@ -248,7 +249,11 @@ public class VisionServer internal constructor(
title: String = "VisionForge server page '$route'",
visionFragment: HtmlVisionFragment,
) {
page(route, mapOf("title" to VisionPage.title(title)) + headers.associateBy { it.hashCode().toString() }, visionFragment)
page(
route,
mapOf("title" to VisionPage.title(title)) + headers.associateBy { it.hashCode().toString() },
visionFragment
)
}
/**