Compare commits
14
Commits
768bb0c650
...
v1.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84cc52e3c
|
||
|
|
62243161f3
|
||
|
|
eb9ed2a208
|
||
|
|
6e21b1038b | ||
|
|
9a32bb8590 | ||
|
|
589649d9f6
|
||
|
|
ae7724b3c4
|
||
|
|
ef5ccb9274
|
||
|
|
9e010c7873
|
||
|
|
fce1c097d4
|
||
|
|
b5c4e150a5
|
||
|
|
126a3bfff2
|
||
|
|
563ee98d03
|
||
|
|
7f92df1ca5
|
No files matched your search
+1
-1
@@ -14,4 +14,4 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
version = PhoebenchVersion(1, 2, 0, "")
|
version = PhoebenchVersion(1, 2, 1, "")
|
||||||
@@ -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")
|
||||||
@@ -80,7 +80,9 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<ShadowJar> {
|
tasks.withType<ShadowJar> {
|
||||||
archiveFileName = if(isDebug) "phoebench-cli-debug.jar" else "phoebench-cli.jar"
|
val archive = if(isDebug) "phoebench-cli-debug.jar" else "phoebench-cli.jar"
|
||||||
|
println("Using archive '$archive'")
|
||||||
|
archiveFileName = archive
|
||||||
}
|
}
|
||||||
|
|
||||||
buildConfig {
|
buildConfig {
|
||||||
|
|||||||
@@ -6,16 +6,21 @@ import kotlin.system.exitProcess
|
|||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
object AuthHandlers {
|
object AuthHandlers {
|
||||||
|
suspend fun checkServer() =
|
||||||
|
Client.callRoute(Routes.handshake, EmptyRequest()).bind { handshake ->
|
||||||
|
if(handshake.version != ProtocolVersion.VERSION) {
|
||||||
|
val server = Client.getServer() ?: "<unknown server ULR>"
|
||||||
|
Client.onClearServer()
|
||||||
|
ErrorResponse("Cannot use $server: protocol version mismatch (server: ${handshake.version}, expected: ${ProtocolVersion.VERSION})").error()
|
||||||
|
}
|
||||||
|
Unit.value()
|
||||||
|
}
|
||||||
|
|
||||||
fun serverPrompt(server: String?): Either<ErrorResponse, Unit> {
|
fun serverPrompt(server: String?): Either<ErrorResponse, Unit> {
|
||||||
val useServer = server.maybePrompt("server") { it }
|
val useServer = server.maybePrompt("server") { it }
|
||||||
Client.onSelectServer(useServer)
|
Client.onSelectServer(useServer)
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
Client.callRoute(Routes.handshake, EmptyRequest()).bind { handshake ->
|
checkServer().bind { Client.onSelectServer(useServer).value() }
|
||||||
if (handshake.version != ProtocolVersion.VERSION) {
|
|
||||||
Client.onClearServer()
|
|
||||||
ErrorResponse("Mismatched protocol version (server: ${handshake.version}, client: ${ProtocolVersion.VERSION})").error()
|
|
||||||
} else Unit.value()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.jaytux.phoebench.clients.cli
|
|||||||
|
|
||||||
import com.github.javakeyring.BackendNotSupportedException
|
import com.github.javakeyring.BackendNotSupportedException
|
||||||
import com.github.javakeyring.Keyring
|
import com.github.javakeyring.Keyring
|
||||||
|
import com.github.javakeyring.PasswordAccessException
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
@@ -22,7 +23,11 @@ object PersistentStorage {
|
|||||||
private object KeyringBackend : IBackend {
|
private object KeyringBackend : IBackend {
|
||||||
override fun read(key: String): String? = try {
|
override fun read(key: String): String? = try {
|
||||||
Keyring.create().use { it.getPassword(SERVICE, key) }
|
Keyring.create().use { it.getPassword(SERVICE, key) }
|
||||||
} catch(e: Exception) {
|
}
|
||||||
|
catch(e: PasswordAccessException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
catch(e: BackendNotSupportedException) {
|
||||||
_backend = PreferencesBackend
|
_backend = PreferencesBackend
|
||||||
_backend.read(key)
|
_backend.read(key)
|
||||||
}
|
}
|
||||||
@@ -52,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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
+34
-3
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ import androidx.compose.material3.DropdownMenuItem
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -237,10 +238,12 @@ fun SignupView(server: String?, error: String?, onSwitchLogin: () -> Unit, onSig
|
|||||||
@Composable
|
@Composable
|
||||||
fun AuthenticatedRoot() {
|
fun AuthenticatedRoot() {
|
||||||
val snacks = SnackProvider.get()
|
val snacks = SnackProvider.get()
|
||||||
|
val auth = AuthProvider.get()
|
||||||
val state = remember { SnackbarHostState() }
|
val state = remember { SnackbarHostState() }
|
||||||
val vm = viewModel { HomeVM() }
|
val vm = viewModel { HomeVM() }
|
||||||
var loggingOut by remember { mutableStateOf(false) }
|
var loggingOut by remember { mutableStateOf(false) }
|
||||||
var currentProject by remember { mutableStateOf<Uuid?>(null) }
|
var currentProject by remember { mutableStateOf<Uuid?>(null) }
|
||||||
|
val server by auth.server
|
||||||
|
|
||||||
LaunchedEffect(snacks) {
|
LaunchedEffect(snacks) {
|
||||||
snacks.snacks.collect { state.showSnackbar(it) }
|
snacks.snacks.collect { state.showSnackbar(it) }
|
||||||
@@ -254,7 +257,12 @@ fun AuthenticatedRoot() {
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("PhoeBench v${ClientVersion.VERSION}", style = MaterialTheme.typography.headlineLarge) },
|
title = {
|
||||||
|
Column {
|
||||||
|
Text("PhoeBench v${ClientVersion.VERSION}", style = MaterialTheme.typography.headlineLarge)
|
||||||
|
server?.let { Text("Connected to $it", style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(0.6f)) }
|
||||||
|
}
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
|||||||
+53
-10
@@ -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
|
||||||
@@ -64,6 +65,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
|||||||
var editing by remember { mutableStateOf(false) }
|
var editing by remember { mutableStateOf(false) }
|
||||||
var deleting by remember { mutableStateOf(false) }
|
var deleting by remember { mutableStateOf(false) }
|
||||||
var addingBenchmark by remember { mutableStateOf(false) }
|
var addingBenchmark by remember { mutableStateOf(false) }
|
||||||
|
var downloading by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
Column(Modifier.padding(all = 15.dp)) {
|
Column(Modifier.padding(all = 15.dp)) {
|
||||||
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
||||||
@@ -79,8 +81,10 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
|||||||
IconButton({ vm.openMonitor() }) {
|
IconButton({ vm.openMonitor() }) {
|
||||||
Icon(Lucide.SquareTerminal, "Open monitor")
|
Icon(Lucide.SquareTerminal, "Open monitor")
|
||||||
}
|
}
|
||||||
|
IconButton({ downloading = true }) {
|
||||||
|
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))
|
||||||
@@ -120,6 +124,8 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
|||||||
addingBenchmark = false
|
addingBenchmark = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(downloading) vm.triggerCsvDownload()
|
||||||
|
|
||||||
monitor?.let { MonitorDialog(name ?: "Unnamed Project", it, vm::closeMonitor, vm::clearMonitor) }
|
monitor?.let { MonitorDialog(name ?: "Unnamed Project", it, vm::closeMonitor, vm::clearMonitor) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,24 +139,20 @@ fun MonitorMessage(msg: ServerMonitorEvent.ITextEvent) = when(msg) {
|
|||||||
val weight = if(ANSI.BOLD in ansi) FontWeight.Bold else null
|
val weight = if(ANSI.BOLD in ansi) FontWeight.Bold else null
|
||||||
val decoration = if(ANSI.UNDERLINE in ansi) TextDecoration.Underline else null
|
val decoration = if(ANSI.UNDERLINE in ansi) TextDecoration.Underline else null
|
||||||
val color = when {
|
val color = when {
|
||||||
ANSI.FG_BLACK in ansi -> Color.Black
|
|
||||||
ANSI.FG_RED in ansi -> Color.Red
|
ANSI.FG_RED in ansi -> Color.Red
|
||||||
ANSI.FG_GREEN in ansi -> Color.Green
|
ANSI.FG_GREEN in ansi -> Color.Green
|
||||||
ANSI.FG_YELLOW in ansi -> Color.Yellow
|
ANSI.FG_YELLOW in ansi -> Color.Yellow
|
||||||
ANSI.FG_BLUE in ansi -> Color.Blue
|
ANSI.FG_BLUE in ansi -> Color.Blue
|
||||||
ANSI.FG_MAGENTA in ansi -> Color.Magenta
|
ANSI.FG_GRAY in ansi -> Color.Gray
|
||||||
ANSI.FG_CYAN in ansi -> Color.Cyan
|
|
||||||
else -> LocalContentColor.current
|
else -> LocalContentColor.current
|
||||||
}
|
}
|
||||||
|
|
||||||
val background = when {
|
val background = when {
|
||||||
ANSI.BG_BLACK in ansi -> Color.Black
|
|
||||||
ANSI.BG_RED in ansi -> Color.Red
|
ANSI.BG_RED in ansi -> Color.Red
|
||||||
ANSI.BG_GREEN in ansi -> Color.Green
|
ANSI.BG_GREEN in ansi -> Color.Green
|
||||||
ANSI.BG_YELLOW in ansi -> Color.Yellow
|
ANSI.BG_YELLOW in ansi -> Color.Yellow
|
||||||
ANSI.BG_BLUE in ansi -> Color.Blue
|
ANSI.BG_BLUE in ansi -> Color.Blue
|
||||||
ANSI.BG_MAGENTA in ansi -> Color.Magenta
|
ANSI.BG_GRAY in ansi -> Color.Gray
|
||||||
ANSI.BG_CYAN in ansi -> Color.Cyan
|
|
||||||
else -> null
|
else -> null
|
||||||
}?.let { Modifier.background(it) } ?: Modifier
|
}?.let { Modifier.background(it) } ?: Modifier
|
||||||
|
|
||||||
@@ -165,6 +167,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)) {
|
||||||
@@ -179,9 +190,20 @@ 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)) {
|
||||||
items(logs) {
|
LazyColumn(Modifier.fillMaxHeight().fillMaxWidth().padding(10.dp).background(MaterialTheme.colorScheme.surfaceDim), state) {
|
||||||
MonitorMessage(it)
|
items(logs) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,6 +225,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()) {
|
||||||
@@ -221,6 +244,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...")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,6 +345,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
|
||||||
@@ -363,6 +391,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) {
|
||||||
|
|||||||
+14
@@ -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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -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")
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ sealed class CSERoute<TParams, TEvent: Any>(val path: String, val elevation: Ele
|
|||||||
abstract fun extractParams(reqParams: Parameters): TParams?
|
abstract fun extractParams(reqParams: Parameters): TParams?
|
||||||
|
|
||||||
suspend fun call(client: IClient, params: TParams, body: suspend (sender: suspend (TEvent) -> Unit) -> Unit): Either<ErrorResponse, Unit> {
|
suspend fun call(client: IClient, params: TParams, body: suspend (sender: suspend (TEvent) -> Unit) -> Unit): Either<ErrorResponse, Unit> {
|
||||||
val server = client.serverUrl.replace("https", "ws").replace("http", "ws")
|
val server = if(client.serverUrl.startsWith("http")) client.serverUrl.replace("http", "ws") else "ws://${client.serverUrl}"
|
||||||
var error: ErrorResponse? = null
|
var error: ErrorResponse? = null
|
||||||
try {
|
try {
|
||||||
client.client.webSocket("$server${buildUrl(params)}", {}) {
|
client.client.webSocket("$server${buildUrl(params)}", {}) {
|
||||||
|
|||||||
@@ -78,13 +78,11 @@ sealed class ProjectEvent {
|
|||||||
enum class ANSI(val bitIdx: Int, val ansiCode: Int) {
|
enum class ANSI(val bitIdx: Int, val ansiCode: Int) {
|
||||||
BOLD(0, 1), UNDERLINE(1, 4),
|
BOLD(0, 1), UNDERLINE(1, 4),
|
||||||
|
|
||||||
FG_BLACK(2, 30), FG_RED(3, 31), FG_GREEN(4, 32),
|
FG_RED(2, 31), FG_GREEN(3, 32), FG_YELLOW(4, 33),
|
||||||
FG_YELLOW(5, 33), FG_BLUE(6, 34), FG_MAGENTA(7, 35),
|
FG_BLUE(5, 34), FG_GRAY(6, 90),
|
||||||
FG_CYAN(8, 36),
|
|
||||||
|
|
||||||
BG_BLACK(9, 40), BG_RED(10, 41), BG_GREEN(11, 42),
|
BG_RED(7, 41), BG_GREEN(8, 42), BG_YELLOW(9, 43),
|
||||||
BG_YELLOW(12, 43), BG_BLUE(13, 44), BG_MAGENTA(14, 45),
|
BG_BLUE(10, 44), BG_GRAY(11, 100);
|
||||||
BG_CYAN(15, 46);
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val _mapping: Map<Int, ANSI>
|
private val _mapping: Map<Int, ANSI>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ 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.10.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" }
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -179,14 +179,13 @@ fun Application.module() {
|
|||||||
) { _, id, sender ->
|
) { _, id, sender ->
|
||||||
val bus = Buses.monitorBus(id)
|
val bus = Buses.monitorBus(id)
|
||||||
val backlog = bus.onConnect()
|
val backlog = bus.onConnect()
|
||||||
println("Bus [$id] connected; sending backlog (${backlog.start != null} // ${backlog.messages.size} // ${backlog.end != null})")
|
|
||||||
sender(bus.bus.serializer, backlog)
|
sender(bus.bus.serializer, backlog)
|
||||||
bus.bus
|
bus.bus
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|||||||
@@ -11,21 +11,18 @@ class MutableBackLog<TStart, TEvent, TEnd> {
|
|||||||
fun registerStart(event: TStart) {
|
fun registerStart(event: TStart) {
|
||||||
if(start != null) throw IllegalStateException("Start is already set.")
|
if(start != null) throw IllegalStateException("Start is already set.")
|
||||||
start = event
|
start = event
|
||||||
println("[BACKLOG]: start '$event'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEvent(event: TEvent) {
|
fun onEvent(event: TEvent) {
|
||||||
if(start == null) throw IllegalStateException("Start is not set yet.")
|
if(start == null) throw IllegalStateException("Start is not set yet.")
|
||||||
if(end != null) throw IllegalStateException("End is already set.")
|
if(end != null) throw IllegalStateException("End is already set.")
|
||||||
_events += event
|
_events += event
|
||||||
println("[BACKLOG]: event '$event'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerEnd(event: TEnd) {
|
fun registerEnd(event: TEnd) {
|
||||||
if(start == null) throw IllegalStateException("Start is not set yet.")
|
if(start == null) throw IllegalStateException("Start is not set yet.")
|
||||||
if(end != null) throw IllegalStateException("End is already set.")
|
if(end != null) throw IllegalStateException("End is already set.")
|
||||||
end = event
|
end = event
|
||||||
println("[BACKLOG]: end '$event'")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
|
|||||||
@@ -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)
|
||||||
for(frame in incoming) {
|
try {
|
||||||
handler(params, inter, extra, converter.deserialize<TEvent>(frame))
|
for (frame in incoming) {
|
||||||
|
handler(params, inter, extra, converter.deserialize<TEvent>(frame))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
onClose(params, inter, extra)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(e: ClosedReceiveChannelException) {}
|
|
||||||
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)
|
||||||
|
|||||||
Reference in new issue
Block a user