refactor web-map to be SPA

This commit is contained in:
Adam 2022-09-09 17:21:09 +02:00
parent de419aeae8
commit a84c4a2f76
17 changed files with 329 additions and 267 deletions

View file

@ -173,3 +173,8 @@ val installThemeCss by tasks.registering(Copy::class) {
tasks.assemble {
dependsOn(installThemeCss)
}
tasks.withType<io.kvision.gradle.tasks.KVConvertPoTask>().configureEach {
enabled = false
}

View file

@ -1,2 +1,5 @@
FROM nginx
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./build/distributions /usr/share/nginx/html

View file

@ -0,0 +1,25 @@
http {
# what times to include
# include /etc/nginx/mime.types;
# what is the default one
#default_type application/octet-stream;
# Sets the path, format, and configuration for a buffered log write
#log_format compression '$remote_addr - $remote_user [$time_local] '
#'"$request" $status $upstream_addr '
#'"$http_referer" "$http_user_agent"';
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to redirecting to index.html
try_files $uri $uri/ $uri.html /index.html;
}
}
}

View file

@ -4,15 +4,19 @@ import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.webmap.layout.headerNav
import dev.adamko.kafkatorio.webmap.layout.homePage
import dev.adamko.kafkatorio.webmap.layout.servers.serverPage
import dev.adamko.kafkatorio.webmap.routing.SiteRouter
import dev.adamko.kafkatorio.webmap.routing.SiteView
import dev.adamko.kafkatorio.webmap.services.WebsocketService
import dev.adamko.kafkatorio.webmap.state.SiteAction
import dev.adamko.kafkatorio.webmap.state.SiteState
import io.kvision.Application
import io.kvision.BootstrapIconsModule
import io.kvision.BootstrapModule
import io.kvision.ChartModule
import io.kvision.CoreModule
import io.kvision.FontAwesomeModule
import io.kvision.html.Link
import io.kvision.html.div
import io.kvision.html.h3
import io.kvision.html.header
import io.kvision.html.main
import io.kvision.module
@ -20,7 +24,6 @@ import io.kvision.panel.root
import io.kvision.redux.ReduxStore
import io.kvision.redux.createReduxStore
import io.kvision.require
import io.kvision.routing.Routing
import io.kvision.startApplication
import io.kvision.state.bind
import kotlinx.coroutines.CoroutineName
@ -51,6 +54,7 @@ object App : Application() {
var siteStateStore: ReduxStore<SiteState, SiteAction> by appState
private set
init {
this.siteStateStore = createReduxStore(SiteState::plus, SiteState())
@ -65,27 +69,17 @@ object App : Application() {
}.launchIn(coroutineScope)
}
// private val gameState: FactorioGameState
// get() = reduxStore.getState()
// private val kvMaps: Maps
// get() = gameState.map.kvMap
override fun start(state: Map<String, Any>) {
Routing.init(useHash = false)
SiteRouter.init()
this.appState = state.toMutableMap()
SiteRouting.init()
root("kvapp") {
header().bind(siteStateStore) { state ->
headerNav(state)
}
h3("Kafkatorio Web Map")
main().bind(siteStateStore) { state ->
div(className = "container-fluid") {
when (state.view) {
@ -93,6 +87,7 @@ object App : Application() {
SiteView.SERVER -> serverPage(state)
}
}
SiteRouter.updatePageLinks()
}
}
@ -107,6 +102,7 @@ object App : Application() {
}
override fun dispose(): Map<String, Any> {
rootJob.cancelChildren()
rootJob.cancel("app dispose")
@ -119,7 +115,10 @@ object App : Application() {
fun main() {
startApplication(
{ App },
{
Link.useDataNavigoForLinks = true
App
},
module.hot,
BootstrapModule,
// BootstrapCssModule,

View file

@ -1,17 +1,16 @@
package dev.adamko.kafkatorio.webmap.layout
import dev.adamko.kafkatorio.webmap.SiteState
import dev.adamko.kafkatorio.webmap.SiteView
import dev.adamko.kafkatorio.webmap.routing.SiteView
import dev.adamko.kafkatorio.webmap.state.SiteState
import io.kvision.core.Container
import io.kvision.navbar.nav
import io.kvision.navbar.navLink
import io.kvision.navbar.navbar
fun Container.headerNav(state: SiteState) {
navbar {
navbar(label = "Kafkatorio", link = SiteView.HOME.path) {
nav {
navLink("Home", SiteView.HOME.path)
// navLink("Home", SiteView.HOME.path)
}
}
}

View file

@ -1,33 +1,89 @@
package dev.adamko.kafkatorio.webmap.layout
import dev.adamko.kafkatorio.webmap.SiteRouting
import dev.adamko.kafkatorio.webmap.SiteState
import dev.adamko.kafkatorio.webmap.state.SiteState
import io.kvision.core.Container
import io.kvision.html.ButtonSize
import io.kvision.html.ButtonStyle
import io.kvision.html.button
import io.kvision.html.ListType
import io.kvision.html.div
import io.kvision.html.h5
import io.kvision.html.span
import io.kvision.html.li
import io.kvision.html.link
import io.kvision.html.listTag
import io.kvision.html.p
fun Container.homePage(state: SiteState) {
div(className = "home-page") {
introduction()
servers(state)
}
}
private fun Container.introduction() {
div {
h5("Introduction")
// <a target="_blank" href="https://github.com/adamko-dev/kafkatorio">Kafkatorio</a>
p(
"""
Kafkatorio is a platform used for creating
<a target="_blank" href="https://www.factorio.com/">Factorio</a> mods that require communication with an external server.
""".trimIndent(),
rich = true,
)
p(
"""
It was created to explore the possibilities of using
<a target="_blank" href="https://kafka.apache.org/">Apache Kafka</a>
to process updates from a Factorio server.
""".trimIndent(),
rich = true,
)
p("To keep up to date on progress, check out:")
listTag(ListType.UL) {
link(
label = "Factorio Mod Portal",
url = "https://mods.factorio.com/mod/kafkatorio-events",
target = "_blank"
)
link(
label = "Factorio forum discussion",
url = "https://forums.factorio.com/viewtopic.php?f=190&t=102841&p=570598#p570598",
target = "_blank"
)
}
p(
"""
Development is ongoing. Presently it used to create a live-updating web-map of a Factorio
server.
""".trimIndent(),
rich = true,
)
}
}
private fun Container.servers(state: SiteState) {
div(className = "servers") {
h5("Servers")
h5("Factorio Servers Live maps")
if (state.serverIds != null) {
p("View live-updating maps of the following Factorio servers")
p("Available servers: ${state.serverIds.size}")
listTag(ListType.UL) {
state.serverIds.forEach { serverId ->
li {
div {
span("Available servers: ${state.serverIds.size}")
link(
label = "$serverId",
url = "/servers/$serverId",
// url = routing.link("servers/$serverId"),
)
}
}
}
}
div {
state.serverIds.forEach { serverId ->
// div {
// state.serverIds.forEach { serverId ->
// div {
// link(
// label = "$serverId",
@ -39,17 +95,17 @@ private fun Container.servers(state: SiteState) {
//// }
// }
// }
button(text = "$serverId", className = "btn btn-link") {
style = ButtonStyle.PRIMARY
size = ButtonSize.SMALL
onClick {
SiteRouting.serverView(serverId)
}
}
}
}
//
// button(text = "$serverId", className = "btn btn-link") {
// style = ButtonStyle.PRIMARY
// size = ButtonSize.SMALL
//
// onClick {
// SiteRouting.serverView(serverId)
// }
// }
// }
// }
} else {
div("no servers available")
}

View file

@ -1,6 +1,6 @@
package dev.adamko.kafkatorio.webmap.layout.servers
import dev.adamko.kafkatorio.webmap.SiteState
import dev.adamko.kafkatorio.webmap.state.SiteState
import io.kvision.core.Container
import io.kvision.html.div
import io.kvision.maps.externals.leaflet.layer.tile.GridLayer

View file

@ -1,174 +0,0 @@
package dev.adamko.kafkatorio.webmap
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.webmap.services.EventsServerClient
import dev.adamko.kafkatorio.webmap.state.FactorioGameState
import io.kvision.navigo.Match
import io.kvision.navigo.Navigo
import io.kvision.redux.RAction
import io.kvision.routing.routing
import kotlin.coroutines.CoroutineContext
import kotlin.js.RegExp
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
object SiteRouting : CoroutineScope {
override val coroutineContext: CoroutineContext =
Dispatchers.Default + SupervisorJob(rootJob) + CoroutineName("SiteRouting")
fun init() {
routing
.on(SiteView.HOME) { homepageView() }
.on(SiteView.SERVER) { match ->
val serverId = match.data[0] as? String ?: error("invalid server id")
serverView(FactorioServerId(serverId))
}
.resolve()
}
fun homepageView() {
App.siteStateStore.dispatch(SiteAction.HomePage)
launch {
val serverIds = EventsServerClient.serverIds()
App.siteStateStore.dispatch(SiteAction.ServerIdsLoaded(serverIds))
}
}
fun serverView(serverId: FactorioServerId) {
App.siteStateStore.dispatch(SiteAction.ServerPage(serverId))
}
}
data class SiteState(
val view: SiteView = SiteView.HOME,
val serverIds: List<FactorioServerId>? = null,
val factorioGameState: FactorioGameState? = null,
) : CoroutineScope {
override val coroutineContext: CoroutineContext =
Dispatchers.Default + SupervisorJob(rootJob) + CoroutineName("SiteState")
operator fun plus(action: SiteAction): SiteState = when (action) {
is SiteAction.HomePage -> copy(view = SiteView.HOME)
is SiteAction.ServerPage -> copy(
view = SiteView.SERVER,
factorioGameState = FactorioGameState(action.serverId),
)
is SiteAction.ServerIdsLoaded -> copy(serverIds = action.serverIds)
is SiteAction.FactorioUpdate -> copy(
factorioGameState = factorioGameState?.let { it + action }
)
is SiteAction.EventServerUpdate -> when (action.packet) {
is EventServerPacket.ChunkTileSaved -> {
if (factorioGameState?.map != null) {
launch {
factorioGameState.map.refreshUpdatedTilePng(action.packet.filename)
}
}
this
}
is EventServerPacket.Kafkatorio -> {
this
}
}
}
}
sealed interface SiteAction : RAction {
object HomePage : SiteAction
data class ServerPage(val serverId: FactorioServerId) : SiteAction
data class ServerIdsLoaded(val serverIds: List<FactorioServerId>) : SiteAction
/** Updates from `events-server` */
sealed class FactorioUpdate : SiteAction {
data class Player(val tick: Tick, val data: PlayerUpdate) : FactorioUpdate()
}
data class EventServerUpdate(val packet: EventServerPacket) : SiteAction
}
sealed interface SiteView {
sealed interface StaticPath : SiteView {
val path: String
}
sealed interface DynamicPath : SiteView {
val pathMatcher: RegExp
}
object HOME : StaticPath {
override val path: String = "/"
}
object SERVER : DynamicPath {
override val pathMatcher: RegExp = RegExp("^servers/${FactorioServerId.validIdRegex}")
}
}
private fun Navigo.on(view: SiteView, handler: (Match) -> Unit): Navigo {
return when (view) {
is SiteView.StaticPath -> on(view.path, { match: Match -> handler(match) })
is SiteView.DynamicPath -> on(view.pathMatcher, { match: Match -> handler(match) })
}
}
//private fun Navigo.on(
// view: SiteView,
// before: BeforeHook? = null,
// after: AfterHook? = null,
// leave: LeaveHook? = null,
// already: AlreadyHook? = null,
// handler: (Match) -> Unit,
//): Navigo {
//
// val hooks: RouteHooks? =
// if (before != null || after != null || leave != null || already != null) {
// jso {
// if (before != null) this@jso.before = before
// if (after != null) this@jso.after = after
// if (leave != null) this@jso.leave = leave
// if (already != null) this@jso.already = already
// }
// } else {
// null
// }
//
// return when (view) {
// is SiteView.StaticPath -> when (hooks) {
// null -> on(view.pathMatcher, { match: Match -> handler(match) })
// else -> on(view.pathMatcher, { match: Match -> handler(match) }, hooks)
// }
//
// is SiteView.DynamicPath -> when (hooks) {
// null -> on(view.pathMatcher, { match: Match -> handler(match) })
// else -> on(view.pathMatcher, { match: Match -> handler(match) }, hooks)
// }
// }
//}

View file

@ -0,0 +1,87 @@
package dev.adamko.kafkatorio.webmap.routing
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.webmap.App
import dev.adamko.kafkatorio.webmap.state.SiteAction
import dev.adamko.kafkatorio.webmap.rootJob
import dev.adamko.kafkatorio.webmap.services.EventsServerClient
import io.kvision.navigo.Match
import io.kvision.navigo.Navigo
import io.kvision.routing.Routing
import io.kvision.routing.Strategy
import kotlin.coroutines.CoroutineContext
import kotlinx.browser.window
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
object SiteRouter : CoroutineScope {
override val coroutineContext: CoroutineContext =
Dispatchers.Default + SupervisorJob(rootJob) + CoroutineName("SiteRouting")
private val navigo: Routing = Routing(
root = "/",
useHash = false,
strategy = Strategy.ONE
)
fun init() {
navigo
.on(SiteView.HOME) {
log("routing.on HOME")
homepageView()
}
.on(SiteView.SERVER) { match ->
println("routing.on SERVER ${JSON.stringify(match)}")
val serverId = match.data["serverId"] as? String ?: error("invalid server id")
println("routing.on SERVER serverId:$serverId")
serverView(FactorioServerId(serverId))
}
.resolve()
log("routing.lastResolved(): " + JSON.stringify(navigo.lastResolved()))
}
fun updatePageLinks() {
window.setTimeout(
handler = {
log("updating page links")
navigo.updatePageLinks()
},
timeout = 0,
)
}
fun homepageView() {
App.siteStateStore.dispatch(SiteAction.HomePage)
launch {
val serverIds = EventsServerClient.serverIds()
log("loaded server IDs $serverIds")
App.siteStateStore.dispatch(SiteAction.ServerIdsLoaded(serverIds))
}
}
fun serverView(serverId: FactorioServerId) {
App.siteStateStore.dispatch(SiteAction.ServerPage(serverId))
}
private fun log(message: String) = println("[SiteRouting] $message")
}
private fun Navigo.on(view: SiteView, handler: (Match) -> Unit): Navigo {
return when (view) {
is SiteView.StaticPath -> on(view.path, { match: Match -> handler(match) })
is SiteView.DynamicPath -> on(view.path, { match: Match -> handler(match) })
}
}

View file

@ -0,0 +1,22 @@
package dev.adamko.kafkatorio.webmap.routing
sealed interface SiteView {
sealed interface StaticPath : SiteView {
val path: String
}
sealed interface DynamicPath : SiteView {
val path: String
}
object HOME : StaticPath {
override val path: String = "/"
}
object SERVER : DynamicPath {
override val path: String = "/servers/:serverId/"
}
}

View file

@ -13,7 +13,7 @@ import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.schema.packets.PrototypesUpdate
import dev.adamko.kafkatorio.schema.packets.SurfaceUpdate
import dev.adamko.kafkatorio.webmap.App
import dev.adamko.kafkatorio.webmap.SiteAction
import dev.adamko.kafkatorio.webmap.state.SiteAction
import dev.adamko.kafkatorio.webmap.config.ApplicationProperties
import dev.adamko.kafkatorio.webmap.config.jsonMapper
import dev.adamko.kafkatorio.webmap.rootJob

View file

@ -0,0 +1,24 @@
package dev.adamko.kafkatorio.webmap.state
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import io.kvision.redux.RAction
sealed interface SiteAction : RAction {
object HomePage : SiteAction
data class ServerPage(val serverId: FactorioServerId) : SiteAction
data class ServerIdsLoaded(val serverIds: List<FactorioServerId>) : SiteAction
/** Updates from `events-server` */
sealed class FactorioUpdate : SiteAction {
data class Player(val tick: Tick, val data: PlayerUpdate) : FactorioUpdate()
}
data class EventServerUpdate(val packet: EventServerPacket) : SiteAction
}

View file

@ -0,0 +1,55 @@
package dev.adamko.kafkatorio.webmap.state
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.packets.EventServerPacket
import dev.adamko.kafkatorio.webmap.rootJob
import dev.adamko.kafkatorio.webmap.routing.SiteView
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
data class SiteState(
val view: SiteView = SiteView.HOME,
val serverIds: List<FactorioServerId>? = null,
val factorioGameState: FactorioGameState? = null,
) : CoroutineScope {
override val coroutineContext: CoroutineContext =
Dispatchers.Default + SupervisorJob(rootJob) + CoroutineName("SiteState")
operator fun plus(action: SiteAction): SiteState = when (action) {
is SiteAction.HomePage -> copy(view = SiteView.HOME)
is SiteAction.ServerPage -> copy(
view = SiteView.SERVER,
factorioGameState = FactorioGameState(action.serverId),
)
is SiteAction.ServerIdsLoaded -> copy(serverIds = action.serverIds)
is SiteAction.FactorioUpdate -> copy(
factorioGameState = factorioGameState?.let { it + action }
)
is SiteAction.EventServerUpdate -> when (action.packet) {
is EventServerPacket.ChunkTileSaved -> {
if (factorioGameState?.map != null) {
launch {
factorioGameState.map.refreshUpdatedTilePng(action.packet.filename)
}
}
this
}
is EventServerPacket.Kafkatorio -> {
this
}
}
}
}

View file

@ -3,7 +3,6 @@ package dev.adamko.kafkatorio.webmap.state
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.schema.common.Tick
import dev.adamko.kafkatorio.schema.packets.PlayerUpdate
import dev.adamko.kafkatorio.webmap.SiteAction
import io.kvision.maps.Maps
import io.kvision.maps.externals.leaflet.layer.vector.CircleMarker
import kotlinx.js.jso

View file

@ -3,7 +3,6 @@ package dev.adamko.kafkatorio.webmap.state
import dev.adamko.kafkatorio.schema.common.FactorioServerId
import dev.adamko.kafkatorio.schema.common.PlayerIndex
import dev.adamko.kafkatorio.webmap.FactorioMap
import dev.adamko.kafkatorio.webmap.SiteAction
import io.kvision.maps.externals.leaflet.layer.LayerGroup

View file

@ -4,12 +4,11 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kafkatorio Web Map</title>
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.1.0/dist/superhero/bootstrap.min.css" integrity="sha256-T8h7oW5tHp4MhFhmRoDI8xjnB2ld89qSgc3s9g8T0uU=" crossorigin="anonymous">-->
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/kafkatorio.css">
<link rel="icon" type="image/png" href="/img/kafkatorio-logo.png"/>
<!--suppress HtmlUnknownTarget -->
<script type="text/javascript" src="main.bundle.js"></script>
<script type="text/javascript" src="/main.bundle.js"></script>
</head>
<body>
<div id="kvapp"></div>

View file

@ -15,41 +15,5 @@ config.performance = {
},
};
// // dev server
// config.devServer = {
// open: false,
// // devMiddleware: {
// // index: false, // specify to enable root proxying
// // },
// port: 3000,
// proxy: {
// // context: () => true,
// // target: "http://localhost:12080",
// // secure: false,
// // ws: true,
// // changeOrigin: true,
//
// // "/tiles": {
// // target: "http://localhost:12080",
// // // "pathRewrite": { "^/tiles": "" },
// // // "logger": "console",
// // // "logLevel": "debug",
// // secure: false,
// // changeOrigin: true,
// // },
// // "/ws": {
// // target: "http://localhost:12080",
// // // pathRewrite: { "^/ws": "" },
// // // "logLevel": "debug",
// // // "logger": "console",
// // secure: false,
// // ws: true,
// // changeOrigin: true,
// // },
// },
// "static": [
// // "../../processedResources/js/main",
// "D:\\Users\\Adam\\Projects\\games\\kafkatorio\\modules\\web-map\\build\\processedResources\\frontend\\main",
// ],
// };
// required for single-page app
config.devServer.historyApiFallback = true