Script monitoring
This commit is contained in:
@@ -5,10 +5,11 @@ plugins {
|
||||
application
|
||||
alias(libs.plugins.serialization)
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.buildconfig)
|
||||
}
|
||||
|
||||
group = "com.jaytux.phoebench"
|
||||
version = PhoebenchVersion(1, 1, 1)
|
||||
version = 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")
|
||||
@@ -28,12 +29,15 @@ val generateVersion = tasks.register<Task>("serverVersion") {
|
||||
}
|
||||
}
|
||||
|
||||
val isDebug = envValue("PHOEBENCH_BUILD_RELEASE") == null
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(libs.clikt)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.auth)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.client.websocket)
|
||||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
implementation(project(":common"))
|
||||
@@ -42,6 +46,7 @@ dependencies {
|
||||
implementation(libs.slf4j.simple)
|
||||
implementation(libs.java.keystore)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
implementation(libs.process)
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -75,5 +80,24 @@ kotlin {
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
archiveFileName = "phoebench-cli.jar"
|
||||
archiveFileName = if(isDebug) "phoebench-cli-debug.jar" else "phoebench-cli.jar"
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
generateAtSync = false
|
||||
useKotlinOutput { internalVisibility = false }
|
||||
|
||||
println("Source sets: ${kotlin.sourceSets.toList().map { it.name }}")
|
||||
|
||||
className("PersistenceConstants")
|
||||
packageName("com.jaytux.phoebench.clients.cli")
|
||||
|
||||
if(isDebug) {
|
||||
println("Using debug CLI service")
|
||||
buildConfigField("SERVICE", "com.jaytux.phoebench.cli.debug")
|
||||
}
|
||||
else {
|
||||
println("Using release CLI service")
|
||||
buildConfigField("SERVICE", "com.jaytux.phoebench.cli")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.jaytux.phoebench.clients.cli
|
||||
|
||||
import com.github.ajalt.clikt.core.*
|
||||
import com.github.ajalt.clikt.parameters.arguments.argument
|
||||
import com.github.ajalt.clikt.parameters.arguments.multiple
|
||||
import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions
|
||||
import com.github.ajalt.clikt.parameters.groups.single
|
||||
import com.github.ajalt.clikt.parameters.options.*
|
||||
@@ -227,6 +229,24 @@ object CLI {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
class Monitor : CliktCommand(name = "monitor") {
|
||||
val finder by mutuallyExclusiveOptions<Project.IProjectIdentification>(
|
||||
option("--id", help = "Find a project by UUID.").convert { Project.ID(Uuid.parse(it)) },
|
||||
option("--name", help = "Find a project by name (formatted [user]/[project])").convert {
|
||||
val split = it.split('/')
|
||||
if(split.size != 2) throw IllegalArgumentException("Invalid format (expected [user]/[project])")
|
||||
Project.ProjectName(split[0], split[1])
|
||||
}
|
||||
).single()
|
||||
val command by argument("command", help = "The command to be run")
|
||||
val commandArgs by argument("arguments", help = "Arguments to pass to the command").multiple()
|
||||
|
||||
override fun run() {
|
||||
MonitorHandler.monitor(finder, command, commandArgs)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
class Version : CliktCommand(name = "version") {
|
||||
override fun run() {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.jaytux.phoebench.clients.cli
|
||||
|
||||
import com.jaytux.phoebench.common.ApiRoute
|
||||
import com.jaytux.phoebench.common.CSERoute
|
||||
import com.jaytux.phoebench.common.Either
|
||||
import com.jaytux.phoebench.common.ErrorResponse
|
||||
import com.jaytux.phoebench.common.IClient
|
||||
@@ -9,15 +10,31 @@ import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.TokenResponse
|
||||
import com.jaytux.phoebench.common.error
|
||||
import com.jaytux.phoebench.common.foldSuspend
|
||||
import com.jaytux.phoebench.common.value
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.ResponseException
|
||||
import io.ktor.client.plugins.auth.Auth
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import io.ktor.client.plugins.auth.providers.bearer
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.websocket.WebSocketException
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.sendSerialized
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.util.reflect.TypeInfo
|
||||
import io.ktor.util.reflect.typeInfo
|
||||
import io.ktor.utils.io.CancellationException
|
||||
import io.ktor.websocket.CloseReason
|
||||
import kotlinx.coroutines.asExecutor
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.URL
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
object Client {
|
||||
@@ -68,6 +85,10 @@ object Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(WebSockets) {
|
||||
contentConverter = KotlinxWebsocketSerializationConverter(Json)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -91,6 +112,19 @@ object Client {
|
||||
suspend fun <TReq: Any, TRes: Any> callRoute(route: ApiRoute<TReq, TRes>, body: TReq): Either<ErrorResponse, TRes> =
|
||||
callRoute(_client, route, body)
|
||||
|
||||
suspend fun <TParams: Any, TEvent: Any> callCSE(
|
||||
route: CSERoute<TParams, TEvent>, params: TParams,
|
||||
body: suspend (sender: suspend (TEvent) -> Unit) -> Unit
|
||||
): Either<ErrorResponse, Unit> {
|
||||
try {
|
||||
val client = IClient.Default(_client, _server ?: throw IllegalStateException("No server URL set."))
|
||||
return route.call(client, params, body)
|
||||
}
|
||||
catch(e: CancellationException) {
|
||||
return ErrorResponse("Event stream disconnected.").error()
|
||||
}
|
||||
}
|
||||
|
||||
fun onLogin(tokens: TokenResponse) {
|
||||
_refreshToken = tokens.refresh
|
||||
_accessToken = tokens.access
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.jaytux.phoebench.clients.cli
|
||||
|
||||
import com.github.pgreze.process.Redirect
|
||||
import com.github.pgreze.process.process
|
||||
import com.jaytux.phoebench.clients.cli.ProjectHandlers.toId
|
||||
import com.jaytux.phoebench.common.ANSI
|
||||
import com.jaytux.phoebench.common.ClientMonitorEvent
|
||||
import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.Stream
|
||||
import com.jaytux.phoebench.common.bind
|
||||
import com.jaytux.phoebench.common.fold
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.utils.io.CancellationException
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
object MonitorHandler {
|
||||
private fun filterAnsi(line: String): Pair<String, List<ANSI>> {
|
||||
val builder = StringBuilder()
|
||||
val codes = mutableSetOf<ANSI>()
|
||||
var remaining = line
|
||||
while(remaining.isNotEmpty()) {
|
||||
val next = remaining.indexOf("\u001B[")
|
||||
if(next < 0) {
|
||||
builder.append(remaining)
|
||||
remaining = ""
|
||||
}
|
||||
else {
|
||||
builder.append(remaining.substring(0, next))
|
||||
val end = remaining.indexOf('m', startIndex = next + 2)
|
||||
if(end == -1) return (line to listOf()) // invalid...
|
||||
codes += remaining.substring(next + 2, end).split(';').mapNotNull {
|
||||
it.toIntOrNull()?.let { i -> ANSI.ansiMapping[i] }
|
||||
}
|
||||
remaining = remaining.substring(end + 1)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.toString() to codes.toList()
|
||||
}
|
||||
|
||||
fun monitor(find: CLI.Commands.Project.IProjectIdentification?, command: String, args: List<String>) {
|
||||
val project = ProjectHandlers.ensureProjectIdentification(find)
|
||||
val combinedCommand = arrayOf(command, *args.toTypedArray())
|
||||
tryAuthenticated {
|
||||
project.toId().bind { id ->
|
||||
Client.callCSE(Routes.CSE.monitor, id) { sender ->
|
||||
try {
|
||||
val res = process(
|
||||
*combinedCommand,
|
||||
stdin = null,
|
||||
stdout = Redirect.Consume { flow ->
|
||||
flow.collect { line ->
|
||||
val (updLine, options) = filterAnsi(line)
|
||||
sender(ClientMonitorEvent.Message(msg = updLine, stream = Stream.STDOUT, options = options))
|
||||
println(line)
|
||||
}
|
||||
},
|
||||
stderr = Redirect.Consume { flow ->
|
||||
flow.collect { line ->
|
||||
val (updLine, options) = filterAnsi(line)
|
||||
sender(ClientMonitorEvent.Message(msg = updLine, stream = Stream.STDERR, options = options))
|
||||
System.err.println(line)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
sender(ClientMonitorEvent.ApplicationFinished(res.resultCode))
|
||||
}
|
||||
catch(e: CancellationException) {
|
||||
throw e
|
||||
}
|
||||
catch(e: Exception) {
|
||||
System.err.println("Launch failed: ${e.message} (${e::class.simpleName})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.fold({
|
||||
System.err.println("Failed to run: ${it.msg}")
|
||||
}) {}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import kotlin.uuid.Uuid
|
||||
|
||||
object PersistentStorage {
|
||||
private val json = Json
|
||||
const val SERVICE = "com.jaytux.phoebench.cli"
|
||||
const val SERVICE = PersistenceConstants.SERVICE
|
||||
private var _backend: IBackend = KeyringBackend
|
||||
|
||||
interface IBackend {
|
||||
|
||||
@@ -13,7 +13,7 @@ plugins {
|
||||
alias(libs.plugins.buildconfig)
|
||||
}
|
||||
|
||||
version = PhoebenchVersion(1, 1, 2)
|
||||
version = 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")
|
||||
@@ -142,23 +142,6 @@ compose.desktop {
|
||||
}
|
||||
}
|
||||
|
||||
fun envValue(key: String): String? {
|
||||
val fromEnv = providers.environmentVariable(key).orNull
|
||||
if(fromEnv != null) return fromEnv
|
||||
|
||||
val envFile = rootProject.file(".env")
|
||||
if(!envFile.exists()) return null
|
||||
|
||||
return envFile.useLines { lines ->
|
||||
lines.map { it.trim() }.filter { it.isNotBlank() && !it.startsWith('#') && '=' in it }
|
||||
.map { line ->
|
||||
val (k, v) = line.split('=', limit = 2)
|
||||
k.trim() to v.trim().removeSurrounding("\"").removeSurrounding("'")
|
||||
}
|
||||
.firstOrNull { it.first == key }?.second
|
||||
}
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
generateAtSync = false
|
||||
useKotlinOutput { internalVisibility = false }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,8 +4,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.HttpClientConfig
|
||||
import io.ktor.client.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.jaytux.phoebench.common.Either
|
||||
@@ -18,9 +24,10 @@ import kotlinx.datetime.format.MonthNames
|
||||
import kotlinx.datetime.format.Padding
|
||||
import kotlinx.datetime.format.char
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.Font
|
||||
import phoebench.clients.compose.generated.resources.*
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
import kotlin.random.nextInt
|
||||
import kotlin.time.Clock
|
||||
@@ -144,4 +151,16 @@ fun List<Float>.geomean(): Float {
|
||||
var prod = 1.0f
|
||||
for(value in this) prod *= value
|
||||
return prod.pow(1.0f / size.toFloat())
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextStyle.makeMonospaced(): TextStyle {
|
||||
val family = FontFamily(
|
||||
Font(Res.font.JetBrainsMono_Regular, weight = FontWeight.Normal, style = FontStyle.Normal),
|
||||
Font(Res.font.JetBrainsMono_Bold, weight = FontWeight.Bold, style = FontStyle.Normal),
|
||||
Font(Res.font.JetBrainsMono_Italic, weight = FontWeight.Normal, style = FontStyle.Italic),
|
||||
Font(Res.font.JetBrainsMono_BoldItalic, weight = FontWeight.Bold, style = FontStyle.Italic)
|
||||
)
|
||||
|
||||
return copy(fontFamily = family)
|
||||
}
|
||||
+5
@@ -38,6 +38,8 @@ interface IProjectRepo {
|
||||
unit: TimeUnit, input: String, hardware: String): Either<ErrorResponse, EntryResponse>
|
||||
suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit>
|
||||
|
||||
suspend fun deleteMonitorLogs(): Either<ErrorResponse, Unit>
|
||||
|
||||
companion object {
|
||||
class Default(private val _client: Client, private val _projectId: Uuid) : IProjectRepo {
|
||||
override suspend fun get(): Either<ErrorResponse, ProjectResponse> =
|
||||
@@ -74,6 +76,9 @@ interface IProjectRepo {
|
||||
|
||||
override suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Entry.delete, id).ignoreValue()
|
||||
|
||||
override suspend fun deleteMonitorLogs(): Either<ErrorResponse, Unit> =
|
||||
_client.callRoute(Routes.Project.rmMonitor, _projectId).ignoreValue()
|
||||
}
|
||||
|
||||
fun default(client: Client, projectId: Uuid) = Default(client, projectId)
|
||||
|
||||
@@ -7,12 +7,14 @@ import com.jaytux.phoebench.common.ErrorResponse
|
||||
import com.jaytux.phoebench.common.HomeEvent
|
||||
import com.jaytux.phoebench.common.ProjectEvent
|
||||
import com.jaytux.phoebench.common.Routes
|
||||
import com.jaytux.phoebench.common.ServerMonitorEvent
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
interface ISSERepo {
|
||||
suspend fun connectHome(onEvent: suspend (Either<ErrorResponse, HomeEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
suspend fun connectAdmin(onEvent: suspend (Either<ErrorResponse, AdminEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
suspend fun connectProject(id: Uuid, onEvent: suspend (Either<ErrorResponse, ProjectEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
suspend fun connectMonitor(id: Uuid, onEvent: suspend (Either<ErrorResponse, ServerMonitorEvent>) -> Unit): Either<ErrorResponse, Unit>
|
||||
|
||||
companion object {
|
||||
class Default(private val _client: Client) : ISSERepo {
|
||||
@@ -24,6 +26,9 @@ interface ISSERepo {
|
||||
|
||||
override suspend fun connectProject(id: Uuid, onEvent: suspend (Either<ErrorResponse, ProjectEvent>) -> Unit): Either<ErrorResponse, Unit> =
|
||||
_client.callSSE(Routes.SSE.projectSpecific, id, onEvent)
|
||||
|
||||
override suspend fun connectMonitor(id: Uuid, onEvent: suspend (Either<ErrorResponse, ServerMonitorEvent>) -> Unit): Either<ErrorResponse, Unit> =
|
||||
_client.callSSE(Routes.SSE.monitor, id, onEvent)
|
||||
}
|
||||
|
||||
fun default(client: Client) = Default(client)
|
||||
|
||||
+40
-1
@@ -1,5 +1,6 @@
|
||||
package com.jaytux.phoebench.clients.data
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -18,7 +19,9 @@ import com.jaytux.phoebench.common.BenchmarkResponse
|
||||
import com.jaytux.phoebench.common.EntryResponse
|
||||
import com.jaytux.phoebench.common.VersionResponse
|
||||
import com.jaytux.phoebench.common.ProjectEvent
|
||||
import com.jaytux.phoebench.common.ServerMonitorEvent
|
||||
import com.jaytux.phoebench.common.TimeUnit
|
||||
import com.jaytux.phoebench.common.fold
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
@@ -87,6 +90,7 @@ class ProjectVM(
|
||||
private val _currentBenchmark = mutableStateOf(0)
|
||||
private val _inputs = mutableStateOf(setOf<String>())
|
||||
private val _hardware = mutableStateOf(setOf<String>())
|
||||
private val _monitorMessages = mutableStateOf<List<ServerMonitorEvent.ITextEvent>?>(null)
|
||||
|
||||
val name = _name.immutable()
|
||||
val owner = _owner.immutable()
|
||||
@@ -97,8 +101,10 @@ class ProjectVM(
|
||||
val currentBenchmark = _currentBenchmark.immutable()
|
||||
val inputs = _inputs.immutable()
|
||||
val hardware = _hardware.immutable()
|
||||
val monitorMessages = _monitorMessages.immutable()
|
||||
|
||||
private var _job: Job? = null
|
||||
private var _monitorJob: Job? = null
|
||||
|
||||
init {
|
||||
_job = withScope {
|
||||
@@ -221,7 +227,7 @@ class ProjectVM(
|
||||
val (updated, idx) = _benchmarks.value.insortIdx(
|
||||
Benchmark.fromResponse(bench, _labels.value, { i -> _inputs.value += i }, { hw -> _hardware.value += hw })
|
||||
) { it.name }
|
||||
_currentBenchmark.value?.let { curr ->
|
||||
_currentBenchmark.value.let { curr ->
|
||||
if(idx != null) {
|
||||
if(idx < curr) _currentBenchmark.value = curr + 1
|
||||
}
|
||||
@@ -279,4 +285,37 @@ class ProjectVM(
|
||||
fun selectBenchmark(id: Uuid) {
|
||||
_currentBenchmark.value = maxOf(_benchmarks.value.indexOfFirst { it.id == id }, 0)
|
||||
}
|
||||
|
||||
fun openMonitor() {
|
||||
if(_monitorJob != null) return
|
||||
_monitorMessages.value = listOf()
|
||||
_monitorJob = withScope {
|
||||
_sseRepo.connectMonitor(_id) { ev ->
|
||||
ev.snackOr {
|
||||
when(it) {
|
||||
is ServerMonitorEvent.Backlog -> {
|
||||
val res = ArrayList<ServerMonitorEvent.ITextEvent>(it.messages.size + 2)
|
||||
it.start?.let { start -> res += start }
|
||||
res.addAll(it.messages)
|
||||
it.end?.let { end -> res += end }
|
||||
_monitorMessages.value = res
|
||||
}
|
||||
ServerMonitorEvent.Cleared -> _monitorMessages.value = listOf()
|
||||
is ServerMonitorEvent.ApplicationEnd, is ServerMonitorEvent.ApplicationStart, is ServerMonitorEvent.Message ->
|
||||
_monitorMessages.value = (_monitorMessages.value ?: listOf()) + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeMonitor() {
|
||||
_monitorJob?.cancel()
|
||||
_monitorJob = null
|
||||
_monitorMessages.value = null
|
||||
}
|
||||
|
||||
fun clearMonitor() = withScope {
|
||||
_repo.deleteMonitorLogs().snackOnError()
|
||||
}
|
||||
}
|
||||
+92
-9
@@ -18,12 +18,14 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalTextToolbar
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.composables.icons.lucide.*
|
||||
import com.jaytux.phoebench.clients.*
|
||||
@@ -57,6 +59,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
val versions by vm.versions
|
||||
val benchmarks by vm.benchmarks
|
||||
val currentBenchmark by vm.currentBenchmark
|
||||
val monitor by vm.monitorMessages
|
||||
|
||||
var editing by remember { mutableStateOf(false) }
|
||||
var deleting by remember { mutableStateOf(false) }
|
||||
@@ -64,16 +67,20 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
|
||||
Column(Modifier.padding(all = 15.dp)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Project ${name ?: "Unnamed Project"}", style = MaterialTheme.typography.headlineMedium)
|
||||
if(editable) {
|
||||
Spacer(Modifier.width(25.dp))
|
||||
IconButton({ editing = true }) {
|
||||
Icon(Lucide.Pencil, "Edit project details")
|
||||
Text("Project ${name ?: "Unnamed Project"}", style = MaterialTheme.typography.headlineMedium)
|
||||
if (editable) {
|
||||
Spacer(Modifier.width(25.dp))
|
||||
IconButton({ editing = true }) {
|
||||
Icon(Lucide.Pencil, "Edit project details")
|
||||
}
|
||||
IconButton({ deleting = true }) {
|
||||
Icon(Lucide.Trash, "Delete project")
|
||||
}
|
||||
IconButton({ vm.openMonitor() }) {
|
||||
Icon(Lucide.SquareTerminal, "Open monitor")
|
||||
}
|
||||
}
|
||||
IconButton({ deleting = true }) {
|
||||
Icon(Lucide.Trash, "Delete project")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
owner?.let { Text("${if(public) "Public" else "Private"} project by user $it") }
|
||||
Spacer(Modifier.height(15.dp))
|
||||
@@ -112,6 +119,82 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
||||
vm.mkBenchmark(it)
|
||||
addingBenchmark = false
|
||||
}
|
||||
|
||||
monitor?.let { MonitorDialog(name ?: "Unnamed Project", it, vm::closeMonitor, vm::clearMonitor) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonitorMessage(msg: ServerMonitorEvent.ITextEvent) = when(msg) {
|
||||
is ServerMonitorEvent.ApplicationEnd -> Text("[${msg.time.fmt()}] Application finished with exit code ${msg.exitCode}.", fontStyle = FontStyle.Italic)
|
||||
is ServerMonitorEvent.ApplicationStart -> Text("[${msg.time.fmt()}] Application started.", fontStyle = FontStyle.Italic)
|
||||
is ServerMonitorEvent.Message -> {
|
||||
val ansi = ANSI.unpack(msg.options).toSet()
|
||||
|
||||
val weight = if(ANSI.BOLD in ansi) FontWeight.Bold else null
|
||||
val decoration = if(ANSI.UNDERLINE in ansi) TextDecoration.Underline else null
|
||||
val color = when {
|
||||
ANSI.FG_BLACK in ansi -> Color.Black
|
||||
ANSI.FG_RED in ansi -> Color.Red
|
||||
ANSI.FG_GREEN in ansi -> Color.Green
|
||||
ANSI.FG_YELLOW in ansi -> Color.Yellow
|
||||
ANSI.FG_BLUE in ansi -> Color.Blue
|
||||
ANSI.FG_MAGENTA in ansi -> Color.Magenta
|
||||
ANSI.FG_CYAN in ansi -> Color.Cyan
|
||||
else -> LocalContentColor.current
|
||||
}
|
||||
|
||||
val background = when {
|
||||
ANSI.BG_BLACK in ansi -> Color.Black
|
||||
ANSI.BG_RED in ansi -> Color.Red
|
||||
ANSI.BG_GREEN in ansi -> Color.Green
|
||||
ANSI.BG_YELLOW in ansi -> Color.Yellow
|
||||
ANSI.BG_BLUE in ansi -> Color.Blue
|
||||
ANSI.BG_MAGENTA in ansi -> Color.Magenta
|
||||
ANSI.BG_CYAN in ansi -> Color.Cyan
|
||||
else -> null
|
||||
}?.let { Modifier.background(it) } ?: Modifier
|
||||
|
||||
Box(background.fillMaxWidth()) {
|
||||
Text(
|
||||
"[${msg.timeStamp.fmt()}] [${if (msg.stream == Stream.STDOUT) 'O' else 'E'}] ${msg.message}",
|
||||
fontWeight = weight, textDecoration = decoration, color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MonitorDialog(name: String, logs: List<ServerMonitorEvent.ITextEvent>, onClose: () -> Unit, onClear: () -> Unit) {
|
||||
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) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Text("Monitor for $name", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
if(logs.isEmpty()) {
|
||||
Box(Modifier.weight(1f)) {
|
||||
Box(Modifier.fillMaxHeight(0.33f).fillMaxWidth()) {
|
||||
Text("No monitor logs.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
CompositionLocalProvider(LocalTextStyle provides LocalTextStyle.current.makeMonospaced()) {
|
||||
LazyColumn(Modifier.weight(1f).padding(10.dp).background(MaterialTheme.colorScheme.surfaceDim)) {
|
||||
items(logs) {
|
||||
MonitorMessage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Row {
|
||||
Button(onClear, Modifier.weight(0.5f)) { Text("Clear Monitor") }
|
||||
Spacer(Modifier.width(10.dp))
|
||||
Button(onClose, Modifier.weight(0.5f)) { Text("Close Monitor") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+1
-5
@@ -2,11 +2,7 @@ package com.jaytux.phoebench.clients
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.draganddrop.DragAndDropEvent
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferAction
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferData
|
||||
import androidx.compose.ui.draganddrop.DragAndDropTransferable
|
||||
import androidx.compose.ui.draganddrop.awtTransferable
|
||||
import androidx.compose.ui.draganddrop.*
|
||||
import androidx.compose.ui.platform.ClipEntry
|
||||
import com.github.javakeyring.Keyring
|
||||
import com.jaytux.phoebench.clients.ui.DefaultServerSelect
|
||||
|
||||
-2
@@ -13,8 +13,6 @@ import kotlinx.browser.window
|
||||
import kotlinx.coroutines.await
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
import org.w3c.dom.DataTransfer
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PhoeBench</title>
|
||||
<link type="text/css" rel="stylesheet" href="styles.css">
|
||||
<link type="text/css" rel="stylesheet" href="styles.css" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-Regular.ttf" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-Bold.ttf" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-Italic.ttf" />
|
||||
<link rel="preload" href="composeResources/phoebench.clients.compose.generated.resources/font/JetBrainsMono-BoldItalic.ttf" />
|
||||
<script type="application/javascript" src="composeApp.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user