14 changed files with 139 additions and 17 deletions

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ plugins {
} }
group = "com.jaytux.phoebench" group = "com.jaytux.phoebench"
version = rootProject.version as PhoebenchVersion version = PhoebenchVersion(1, 2, 1, "patch0")
if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion)) if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion))
throw GradleException("CLI Client version must be at least as high as protocol/common version") throw GradleException("CLI Client version must be at least as high as protocol/common version")
@@ -57,6 +57,7 @@ object PersistentStorage {
override fun write(key: String, value: String) { override fun write(key: String, value: String) {
prefs.put(key, value) prefs.put(key, value)
prefs.flush()
} }
override fun erase(key: String) { override fun erase(key: String) {
+2 -1
View File
@@ -13,7 +13,7 @@ plugins {
alias(libs.plugins.buildconfig) alias(libs.plugins.buildconfig)
} }
version = rootProject.version as PhoebenchVersion version = PhoebenchVersion(1, 2, 2, "csv-export")
if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion)) if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion))
throw GradleException("UI Clients version must be at least as high as protocol/common version") throw GradleException("UI Clients version must be at least as high as protocol/common version")
@@ -96,6 +96,7 @@ kotlin {
implementation(libs.lucide) implementation(libs.lucide)
implementation(libs.koala) implementation(libs.koala)
implementation(libs.kolor) implementation(libs.kolor)
implementation(libs.filekit)
} }
desktopMain.dependencies { desktopMain.dependencies {
implementation(compose.desktop.currentOs) implementation(compose.desktop.currentOs)
@@ -38,3 +38,5 @@ expect fun <T> drag(data: T, serializer: KSerializer<T>): DragAndDropTransferDat
inline fun <reified T> drop(data: DragAndDropEvent): T? = drop(data, serializer<T>()) inline fun <reified T> drop(data: DragAndDropEvent): T? = drop(data, serializer<T>())
expect fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T? expect fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T?
expect suspend fun onDownload(contents: String, nameHint: String, extension: String)
@@ -1,6 +1,5 @@
package com.jaytux.phoebench.clients.data package com.jaytux.phoebench.clients.data
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -13,6 +12,7 @@ import com.jaytux.phoebench.clients.hexString
import com.jaytux.phoebench.clients.immutable import com.jaytux.phoebench.clients.immutable
import com.jaytux.phoebench.clients.insort import com.jaytux.phoebench.clients.insort
import com.jaytux.phoebench.clients.insortIdx import com.jaytux.phoebench.clients.insortIdx
import com.jaytux.phoebench.clients.onDownload
import com.jaytux.phoebench.clients.replace import com.jaytux.phoebench.clients.replace
import com.jaytux.phoebench.clients.withScope import com.jaytux.phoebench.clients.withScope
import com.jaytux.phoebench.common.BenchmarkResponse import com.jaytux.phoebench.common.BenchmarkResponse
@@ -21,7 +21,7 @@ import com.jaytux.phoebench.common.VersionResponse
import com.jaytux.phoebench.common.ProjectEvent import com.jaytux.phoebench.common.ProjectEvent
import com.jaytux.phoebench.common.ServerMonitorEvent import com.jaytux.phoebench.common.ServerMonitorEvent
import com.jaytux.phoebench.common.TimeUnit import com.jaytux.phoebench.common.TimeUnit
import com.jaytux.phoebench.common.fold import io.ktor.http.quote
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
@@ -138,7 +138,11 @@ class ProjectVM(
when(event) { when(event) {
ProjectEvent.Deleted -> back() ProjectEvent.Deleted -> back()
is ProjectEvent.EntryDeleted -> _benchmarks.value = _benchmarks.value.replace({ it.id == event.benchmarkId }) { it.remove(event.id) } is ProjectEvent.EntryDeleted -> _benchmarks.value = _benchmarks.value.replace({ it.id == event.benchmarkId }) { it.remove(event.id) }
is ProjectEvent.VersionChanged -> _labels.value += (event.label.id to Version.fromResponse(event.label)) is ProjectEvent.VersionChanged -> {
val updVersion = Version.fromResponse(event.label)
_labels.value += (event.label.id to updVersion)
_benchmarks.value = _benchmarks.value.map { b -> b.copy(entries = b.entries.map { e -> if(e.label.id == event.label.id) e.copy(label = updVersion) else e }) }
}
is ProjectEvent.VersionDeleted -> { is ProjectEvent.VersionDeleted -> {
_labels.value = _labels.value.filter { it.value.id != event.id } _labels.value = _labels.value.filter { it.value.id != event.id }
_benchmarks.value = _benchmarks.value.map { it.copy(entries = it.entries.filter { e -> e.label.id != event.id }) } _benchmarks.value = _benchmarks.value.map { it.copy(entries = it.entries.filter { e -> e.label.id != event.id }) }
@@ -318,4 +322,31 @@ class ProjectVM(
fun clearMonitor() = withScope { fun clearMonitor() = withScope {
_repo.deleteMonitorLogs().snackOnError() _repo.deleteMonitorLogs().snackOnError()
} }
fun triggerCsvDownload() = withScope {
data class CsvEntry(val id: Uuid, val bench: Uuid, val benchName: String, val runner: Uuid, val runnerName: String, val runnerHex: String, val input: String, val hardware: String, val timing: List<Float>)
val benchmarks = _benchmarks.value.flatMap { bench ->
bench.entries.map {
CsvEntry(
id = it.id, bench = bench.id, benchName = bench.name,
runner = it.label.id, runnerName = it.label.name, runnerHex = it.label.colorStr,
input = it.input, hardware = it.hardware, timing = it.measurements
)
}
}
val dataCount = benchmarks.maxOf { it.timing.size }
val header = listOf("id", "benchmark-id", "benchmark-name", "runner-id", "runner-name", "runner-hex", "input", "hardware") + List(dataCount) { "entry-$it" }
val body = benchmarks.map {
listOf(
it.id.toHexDashString(), it.bench.toHexDashString(), it.benchName.quote(),
it.runner.toHexDashString(), it.runnerName.quote(), it.runnerHex,
it.input.quote(), it.hardware.quote()
) + List(dataCount) { i -> it.timing.getOrNull(i)?.toString() ?: "" }
}
val csv = "${header.joinToString(",")}\n${body.joinToString("\n"){ it.joinToString(",") }}"
onDownload(csv, name.value ?: "PhoeBench-project", "csv")
}
} }
@@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -79,8 +80,10 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
IconButton({ vm.openMonitor() }) { IconButton({ vm.openMonitor() }) {
Icon(Lucide.SquareTerminal, "Open monitor") Icon(Lucide.SquareTerminal, "Open monitor")
} }
IconButton({ vm.triggerCsvDownload() }) {
Icon(Lucide.FileDown, "Download CSV")
}
} }
} }
owner?.let { Text("${if(public) "Public" else "Private"} project by user $it") } owner?.let { Text("${if(public) "Public" else "Private"} project by user $it") }
Spacer(Modifier.height(15.dp)) Spacer(Modifier.height(15.dp))
@@ -161,6 +164,15 @@ fun MonitorMessage(msg: ServerMonitorEvent.ITextEvent) = when(msg) {
@Composable @Composable
fun MonitorDialog(name: String, logs: List<ServerMonitorEvent.ITextEvent>, onClose: () -> Unit, onClear: () -> Unit) { fun MonitorDialog(name: String, logs: List<ServerMonitorEvent.ITextEvent>, onClose: () -> Unit, onClear: () -> Unit) {
var autoFollow by remember { mutableStateOf(true) }
val state = rememberLazyListState()
LaunchedEffect(autoFollow, logs) {
if(autoFollow) {
state.animateScrollToItem(logs.size - 1)
}
}
Dialog(onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false)) { Dialog(onDismissRequest = onClose, properties = DialogProperties(usePlatformDefaultWidth = false)) {
Surface(Modifier.padding(15.dp).fillMaxHeight(0.8f).widthIn(min = 300.dp, max = 2000.dp), shape = MaterialTheme.shapes.medium) { Surface(Modifier.padding(15.dp).fillMaxHeight(0.8f).widthIn(min = 300.dp, max = 2000.dp), shape = MaterialTheme.shapes.medium) {
Column(Modifier.padding(8.dp)) { Column(Modifier.padding(8.dp)) {
@@ -175,11 +187,22 @@ fun MonitorDialog(name: String, logs: List<ServerMonitorEvent.ITextEvent>, onClo
} }
else { else {
CompositionLocalProvider(LocalTextStyle provides LocalTextStyle.current.makeMonospaced()) { CompositionLocalProvider(LocalTextStyle provides LocalTextStyle.current.makeMonospaced()) {
LazyColumn(Modifier.weight(1f).padding(10.dp).background(MaterialTheme.colorScheme.surfaceDim)) { Box(Modifier.weight(1f)) {
LazyColumn(Modifier.fillMaxHeight().fillMaxWidth().padding(10.dp).background(MaterialTheme.colorScheme.surfaceDim), state) {
items(logs) { items(logs) {
MonitorMessage(it) MonitorMessage(it)
} }
} }
Box(Modifier.align(Alignment.TopEnd).padding(25.dp)) {
val colors = if(autoFollow)
IconButtonDefaults.iconButtonColors(containerColor = LocalContentColor.current, contentColor = MaterialTheme.colorScheme.surface)
else IconButtonDefaults.iconButtonColors(containerColor = MaterialTheme.colorScheme.surface, contentColor = LocalContentColor.current)
IconButton({ autoFollow = !autoFollow }, colors = colors) {
Icon(Lucide.ArrowDownToLine, "Follow")
}
}
}
} }
} }
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
@@ -199,6 +222,7 @@ fun ProjectPanel(vm: ProjectVM, editable: Boolean, versions: Map<Uuid, ProjectVM
var addingLabel by remember { mutableStateOf(false) } var addingLabel by remember { mutableStateOf(false) }
var addingData by remember { mutableStateOf(false) } var addingData by remember { mutableStateOf(false) }
var editing by remember { mutableStateOf(false) } var editing by remember { mutableStateOf(false) }
var deleting by remember { mutableStateOf(false) }
Surface(Modifier.fillMaxSize().padding(8.dp), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) { Surface(Modifier.fillMaxSize().padding(8.dp), tonalElevation = 10.dp, shape = MaterialTheme.shapes.medium) {
Column(Modifier.fillMaxSize()) { Column(Modifier.fillMaxSize()) {
@@ -217,6 +241,9 @@ fun ProjectPanel(vm: ProjectVM, editable: Boolean, versions: Map<Uuid, ProjectVM
IconButton({ editing = true }) { IconButton({ editing = true }) {
Icon(Lucide.Pencil, "Edit benchmark") Icon(Lucide.Pencil, "Edit benchmark")
} }
IconButton({ deleting = true }) {
Icon(Lucide.Trash, "Remove benchmark")
}
Icon(Lucide.ChevronDown, "Select benchmark...") Icon(Lucide.ChevronDown, "Select benchmark...")
} }
} }
@@ -315,6 +342,8 @@ fun ProjectPanel(vm: ProjectVM, editable: Boolean, versions: Map<Uuid, ProjectVM
} }
if(editing) EditBenchmarkDialog(currentBenchmark, { editing = false }) { vm.updateBenchmark(currentBenchmark.id, it) } if(editing) EditBenchmarkDialog(currentBenchmark, { editing = false }) { vm.updateBenchmark(currentBenchmark.id, it) }
if(deleting) ConfirmDeleteBenchmarkDialog(currentBenchmark, { deleting = false }) { vm.deleteBenchmark(currentBenchmark.id); deleting = false }
} }
@Composable @Composable
@@ -359,6 +388,21 @@ fun ConfirmDeleteProjectDialog(name: String, onCancel: () -> Unit, onDelete: ()
} }
} }
@Composable
fun ConfirmDeleteBenchmarkDialog(benchmark: ProjectVM.Benchmark, onCancel: () -> Unit, onDelete: () -> Unit) {
Dialog(onDismissRequest = onCancel) {
Surface(Modifier.padding(15.dp), shape = MaterialTheme.shapes.medium) {
Column(Modifier.padding(8.dp).widthIn(min = 250.dp).width(IntrinsicSize.Min)) {
Text("Confirm deletion", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
Spacer(Modifier.height(10.dp))
Text("Are you sure you want to delete ${benchmark.name}?")
Spacer(Modifier.height(10.dp))
CancelConfirmXRow(onCancel, onDelete, confirmText = "Delete")
}
}
}
}
@Composable @Composable
fun AddBenchmarkDialog(onCancel: () -> Unit, onAdd: (name: String) -> Unit) { fun AddBenchmarkDialog(onCancel: () -> Unit, onAdd: (name: String) -> Unit) {
Dialog(onDismissRequest = onCancel) { Dialog(onDismissRequest = onCancel) {
@@ -6,6 +6,10 @@ import androidx.compose.ui.draganddrop.*
import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.ClipEntry
import com.github.javakeyring.Keyring import com.github.javakeyring.Keyring
import com.jaytux.phoebench.clients.ui.DefaultServerSelect import com.jaytux.phoebench.clients.ui.DefaultServerSelect
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.dialogs.FileKitDialogException
import io.github.vinceglb.filekit.dialogs.openFileSaver
import io.github.vinceglb.filekit.writeString
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
@@ -78,3 +82,13 @@ actual fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T? =
if(data.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor)) if(data.awtTransferable.isDataFlavorSupported(DataFlavor.stringFlavor))
Json.decodeFromString(serializer, data.awtTransferable.getTransferData(DataFlavor.stringFlavor) as String) Json.decodeFromString(serializer, data.awtTransferable.getTransferData(DataFlavor.stringFlavor) as String)
else null else null
actual suspend fun onDownload(contents: String, nameHint: String, extension: String) {
try {
val file = FileKit.openFileSaver(suggestedName = nameHint, defaultExtension = extension)
file?.writeString(contents)
}
catch(e: FileKitDialogException) {
SnackProvider.get().send("Failed to save CSV: ${e.message}")
}
}
@@ -7,6 +7,8 @@ import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.draganddrop.domDataTransferOrNull import androidx.compose.ui.draganddrop.domDataTransferOrNull
import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.ClipEntry
import com.jaytux.phoebench.clients.ui.constantServerSelect import com.jaytux.phoebench.clients.ui.constantServerSelect
import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.download
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.js.* import io.ktor.client.engine.js.*
import kotlinx.browser.window import kotlinx.browser.window
@@ -94,3 +96,7 @@ actual fun <T> drop(data: DragAndDropEvent, serializer: KSerializer<T>): T? =
data.transferData?.domDataTransferOrNull?.getData("application/json")?.let { data.transferData?.domDataTransferOrNull?.getData("application/json")?.let {
Json.decodeFromString(serializer, it) Json.decodeFromString(serializer, it)
} }
actual suspend fun onDownload(contents: String, nameHint: String, extension: String) {
FileKit.download(contents.encodeToByteArray(), fileName = "$nameHint.$extension")
}
+4 -2
View File
@@ -7,8 +7,8 @@ exposed = "1.3.1"
ksoup = "0.2.6" ksoup = "0.2.6"
sqlite = "3.53.2.0" sqlite = "3.53.2.0"
mariadb = "3.5.9" mariadb = "3.5.9"
androidx-lifecycle = "2.10.0" androidx-lifecycle = "2.11.0"
compose-multiplatform = "1.11.1" compose-multiplatform = "1.12.2"
kotlin = "2.4.10" kotlin = "2.4.10"
kotlinx-coroutines = "1.11.0" kotlinx-coroutines = "1.11.0"
ktor = "3.5.1" ktor = "3.5.1"
@@ -24,6 +24,7 @@ shadow = "9.3.0"
clikt = "5.0.3" clikt = "5.0.3"
buildconfig = "6.0.10" buildconfig = "6.0.10"
process = "1.5.1" process = "1.5.1"
filekit = "0.17.0"
[libraries] [libraries]
androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel", version.ref = "androidx-lifecycle" }
@@ -91,6 +92,7 @@ java-keystore = { module = "com.github.javakeyring:java-keyring", version.ref =
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" } clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
process = { module = "com.github.pgreze:kotlin-process", version.ref = "process" } process = { module = "com.github.pgreze:kotlin-process", version.ref = "process" }
filekit = { module = "io.github.vinceglb:filekit-dialogs-compose", version.ref = "filekit" }
[plugins] [plugins]
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
+3
View File
@@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
+1 -1
View File
@@ -7,7 +7,7 @@ plugins {
} }
group = "com.jaytux.phoebench" group = "com.jaytux.phoebench"
version = rootProject.version as PhoebenchVersion version = PhoebenchVersion(1, 2, 1, "patch1")
if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion)) if((version as PhoebenchVersion) < (rootProject.version as PhoebenchVersion))
throw GradleException("Server version must be at least as high as protocol/common version") throw GradleException("Server version must be at least as high as protocol/common version")
@@ -185,7 +185,7 @@ fun Application.module() {
cseAuth(Routes.CSE.monitor, cseAuth(Routes.CSE.monitor,
{ id, user -> transaction { accessibleProjectCSE(user, id, true) } }, { id, user -> transaction { accessibleProjectCSE(user, id, true) } },
ProjectHandler::monitorSetup) { _, _, bus, event -> ProjectHandler::monitorSetup, ProjectHandler::monitorTeardown) { _, _, bus, event ->
try { try {
when(event) { when(event) {
is ClientMonitorEvent.ApplicationFinished -> bus.end(ServerMonitorEvent.ApplicationEnd(Clock.System.now(), event.exitCode)) is ClientMonitorEvent.ApplicationFinished -> bus.end(ServerMonitorEvent.ApplicationEnd(Clock.System.now(), event.exitCode))
@@ -282,6 +282,7 @@ inline fun <reified TParams: Any, reified TEvent: Any, TInter, TExtra> Route.wra
api: CSERoute<TParams, TEvent>, api: CSERoute<TParams, TEvent>,
noinline extra: suspend (ApplicationCall, TParams) -> TInter, noinline extra: suspend (ApplicationCall, TParams) -> TInter,
noinline setup: suspend (TParams, TInter) -> TExtra, noinline setup: suspend (TParams, TInter) -> TExtra,
noinline onClose: suspend (TParams, TInter, TExtra) -> Unit,
noinline handler: suspend (TParams, TInter, TExtra, TEvent) -> Unit noinline handler: suspend (TParams, TInter, TExtra, TEvent) -> Unit
) { ) {
webSocket(api.pattern) { webSocket(api.pattern) {
@@ -292,11 +293,15 @@ inline fun <reified TParams: Any, reified TEvent: Any, TInter, TExtra> Route.wra
val converter = KotlinxWebsocketSerializationConverter(Json) val converter = KotlinxWebsocketSerializationConverter(Json)
val extra = setup(params, inter) val extra = setup(params, inter)
try {
for (frame in incoming) { for (frame in incoming) {
handler(params, inter, extra, converter.deserialize<TEvent>(frame)) handler(params, inter, extra, converter.deserialize<TEvent>(frame))
} }
} }
catch(e: ClosedReceiveChannelException) {} finally {
onClose(params, inter, extra)
}
}
catch(e: RouteError.CSERouteError) { catch(e: RouteError.CSERouteError) {
close(CloseReason(e.code, e.message ?: "Unknown error")) close(CloseReason(e.code, e.message ?: "Unknown error"))
} }
@@ -310,6 +315,7 @@ inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.wrapAuthCSE
api: CSERoute<TParams, TEvent>, api: CSERoute<TParams, TEvent>,
noinline verifyUser: suspend (TParams, User) -> Unit, noinline verifyUser: suspend (TParams, User) -> Unit,
noinline setup: suspend (TParams, User) -> TExtra, noinline setup: suspend (TParams, User) -> TExtra,
noinline onClose: suspend (TParams, User, TExtra) -> Unit,
noinline handler: suspend (TParams, User, TExtra, TEvent) -> Unit noinline handler: suspend (TParams, User, TExtra, TEvent) -> Unit
) = wrapCSE<TParams, TEvent, User, TExtra>( ) = wrapCSE<TParams, TEvent, User, TExtra>(
api = api, api = api,
@@ -327,28 +333,32 @@ inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.wrapAuthCSE
user user
}, },
setup = setup, setup = setup,
onClose = onClose,
handler = handler handler = handler
) )
inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cse( inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cse(
api: CSERoute<TParams, TEvent>, api: CSERoute<TParams, TEvent>,
noinline setup: suspend (TParams, Unit) -> TExtra, noinline setup: suspend (TParams) -> TExtra,
noinline onClose: suspend (TParams, TExtra) -> Unit,
noinline handler: suspend (TParams, TExtra, TEvent) -> Unit noinline handler: suspend (TParams, TExtra, TEvent) -> Unit
) { ) {
if(api.elevation != Elevation.UN_AUTH) throw IllegalArgumentException("CSE ${api.pattern} can only be used with ${api.elevation}") if(api.elevation != Elevation.UN_AUTH) throw IllegalArgumentException("CSE ${api.pattern} can only be used with ${api.elevation}")
wrapCSE(api, { _, _ -> }, setup) { params, _, extra, event -> handler(params, extra, event) } wrapCSE(api, { _, _ -> }, { it, _ -> setup(it) }, { p, _, extra -> onClose(p, extra) }) { params, _, extra, event -> handler(params, extra, event) }
} }
inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cseAuth( inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cseAuth(
api: CSERoute<TParams, TEvent>, noinline verifyUser: suspend (TParams, User) -> Unit, api: CSERoute<TParams, TEvent>, noinline verifyUser: suspend (TParams, User) -> Unit,
noinline setup: suspend (TParams, User) -> TExtra, noinline handler: suspend (TParams, User, TExtra, TEvent) -> Unit noinline setup: suspend (TParams, User) -> TExtra, noinline onClose: suspend (TParams, User, TExtra) -> Unit,
noinline handler: suspend (TParams, User, TExtra, TEvent) -> Unit
) { ) {
if(api.elevation != Elevation.AUTH) throw IllegalArgumentException("CSE ${api.pattern} can only be used with ${api.elevation}") if(api.elevation != Elevation.AUTH) throw IllegalArgumentException("CSE ${api.pattern} can only be used with ${api.elevation}")
wrapAuthCSE(api, verifyUser, setup, handler) wrapAuthCSE(api, verifyUser, setup, onClose, handler)
} }
inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cseAdmin( inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cseAdmin(
api: CSERoute<TParams, TEvent>, noinline setup: suspend (TParams, User) -> TExtra, api: CSERoute<TParams, TEvent>, noinline setup: suspend (TParams, User) -> TExtra,
noinline onClose: suspend (TParams, User, TExtra) -> Unit,
noinline handler: suspend (TParams, User, TExtra, TEvent) -> Unit noinline handler: suspend (TParams, User, TExtra, TEvent) -> Unit
) { ) {
if(api.elevation != Elevation.ADMIN) throw IllegalArgumentException("CSE ${api.pattern} can only be used with ${api.elevation}") if(api.elevation != Elevation.ADMIN) throw IllegalArgumentException("CSE ${api.pattern} can only be used with ${api.elevation}")
@@ -356,5 +366,5 @@ inline fun <reified TParams: Any, reified TEvent: Any, TExtra> Route.cseAdmin(
if(!user.isAdmin) { if(!user.isAdmin) {
throw RouteError.CSERouteError("Admin access required", CloseReasons.NOT_AUTHORIZED) throw RouteError.CSERouteError("Admin access required", CloseReasons.NOT_AUTHORIZED)
} }
}, setup, handler) }, setup, onClose, handler)
} }
@@ -402,6 +402,14 @@ object ProjectHandler {
} }
} }
suspend fun monitorTeardown(
id: Uuid, user: User,
bus: SSEBus.MonitorSSEBus<ServerMonitorEvent, ServerMonitorEvent.ApplicationStart, ServerMonitorEvent.Message,
ServerMonitorEvent.ApplicationEnd, ServerMonitorEvent.Backlog>
) {
bus.end(ServerMonitorEvent.ApplicationEnd(Clock.System.now(), -1))
}
fun deleteMonitor(user: User, req: Uuid) = transaction { fun deleteMonitor(user: User, req: Uuid) = transaction {
checkMigration(user) checkMigration(user)
accessibleProject(user, req, true) accessibleProject(user, req, true)