Compare commits
2
Commits
8f4c6cc630
...
a90b2ebeca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a90b2ebeca
|
||
|
|
60b1a69bc6
|
+1
-1
@@ -12,4 +12,4 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
version = "0.1.0-dev"
|
version = "0.2.0-dev"
|
||||||
@@ -98,7 +98,7 @@ object CLI {
|
|||||||
sealed interface IProjectIdentification
|
sealed interface IProjectIdentification
|
||||||
sealed interface ILabelIdentification
|
sealed interface ILabelIdentification
|
||||||
data class ProjectName(val user: String, val project: String) : IProjectIdentification
|
data class ProjectName(val user: String, val project: String) : IProjectIdentification
|
||||||
data class LabelName(val name: String): ILabelIdentification
|
data class VersionName(val name: String): ILabelIdentification
|
||||||
data class ID(val id: Uuid) : IProjectIdentification, ILabelIdentification
|
data class ID(val id: Uuid) : IProjectIdentification, ILabelIdentification
|
||||||
|
|
||||||
sealed interface IData<T> {
|
sealed interface IData<T> {
|
||||||
@@ -168,22 +168,23 @@ object CLI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class AddLabel : CliktCommand(name = "add-label") {
|
class AddVersion : CliktCommand(name = "add-version") {
|
||||||
val parent by requireObject<Project>()
|
val parent by requireObject<Project>()
|
||||||
val name by option("--name", help = "Set the label's name (*)")
|
val name by option("--name", help = "Set the version's name (*)")
|
||||||
val color by option("--color", help = "Set the label's color (*)").check("Color must be specified in RGB-hex-format (#ABCDEF)") {
|
val color by option("--color", help = "Set the version's color (*)").check("Color must be specified in RGB-hex-format (#ABCDEF)") {
|
||||||
it.length == 7 && it[0] == '#' && it.substring(1, it.length).all { c -> c.isDigit() || c in "ABCDEF" }
|
it.length == 7 && it[0] == '#' && it.substring(1, it.length).all { c -> c.isDigit() || c in "ABCDEF" }
|
||||||
}
|
}
|
||||||
|
val meta by option("--meta", help = "Add metadata to the version (*)")
|
||||||
|
|
||||||
override fun run() = ProjectHandlers.newLabel(name, color, parent.finder)
|
override fun run() = ProjectHandlers.newVersion(name, color, parent.finder, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class AddData : CliktCommand(name = "add-data") {
|
class AddData : CliktCommand(name = "add-data") {
|
||||||
val parent by requireObject<Project>()
|
val parent by requireObject<Project>()
|
||||||
val label by mutuallyExclusiveOptions<ILabelIdentification>(
|
val version by mutuallyExclusiveOptions<ILabelIdentification>(
|
||||||
option("--label-id", help = "Set the label by UUID.").convert { ID(Uuid.parse(it)) },
|
option("--version-id", help = "Set the version by UUID.").convert { ID(Uuid.parse(it)) },
|
||||||
option("--label", help = "Set the label by name.").convert { LabelName(it) }
|
option("--version", help = "Set the version by name.").convert { VersionName(it) }
|
||||||
).single()
|
).single()
|
||||||
val warmup by mutuallyExclusiveOptions<IData<Float>>(
|
val warmup by mutuallyExclusiveOptions<IData<Float>>(
|
||||||
option("--warmup", help = "Set warmup data directly.").float().split(",").transformAll { DirectData(it.flatten()) },
|
option("--warmup", help = "Set warmup data directly.").float().split(",").transformAll { DirectData(it.flatten()) },
|
||||||
@@ -201,8 +202,10 @@ object CLI {
|
|||||||
option("--min", "--m", "--minutes", help = "Set the time unit to minutes").flag().convert { TimeUnit.MINUTES },
|
option("--min", "--m", "--minutes", help = "Set the time unit to minutes").flag().convert { TimeUnit.MINUTES },
|
||||||
option("--h", "--hour", help = "Set the time unit to hours").flag().convert { TimeUnit.HOURS }
|
option("--h", "--hour", help = "Set the time unit to hours").flag().convert { TimeUnit.HOURS }
|
||||||
).single()
|
).single()
|
||||||
|
val input by option("--input", help = "Set the input used to benchmark")
|
||||||
|
val hardware by option("--hardware", "--hw", help = "Set the hardware on which the benchmark was run")
|
||||||
|
|
||||||
override fun run() = ProjectHandlers.newData(parent.finder, label, warmup, measurement, unit)
|
override fun run() = ProjectHandlers.newData(parent.finder, version, warmup, measurement, unit, input, hardware)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
package com.jaytux.phoebench.clients.cli
|
package com.jaytux.phoebench.clients.cli
|
||||||
|
|
||||||
import com.github.ajalt.clikt.parameters.options.convert
|
|
||||||
import com.github.ajalt.clikt.parameters.options.flag
|
|
||||||
import com.github.ajalt.clikt.parameters.options.option
|
|
||||||
import com.jaytux.phoebench.clients.cli.CLI.Commands.Project.ProjectName
|
import com.jaytux.phoebench.clients.cli.CLI.Commands.Project.ProjectName
|
||||||
import com.jaytux.phoebench.common.ApiRoute
|
|
||||||
import com.jaytux.phoebench.common.Either
|
import com.jaytux.phoebench.common.Either
|
||||||
import com.jaytux.phoebench.common.EmptyRequest
|
import com.jaytux.phoebench.common.EmptyRequest
|
||||||
import com.jaytux.phoebench.common.EntryRequest
|
import com.jaytux.phoebench.common.EntryRequest
|
||||||
import com.jaytux.phoebench.common.ErrorResponse
|
import com.jaytux.phoebench.common.ErrorResponse
|
||||||
import com.jaytux.phoebench.common.LabelRequest
|
import com.jaytux.phoebench.common.VersionRequest
|
||||||
import com.jaytux.phoebench.common.LabelResponse
|
|
||||||
import com.jaytux.phoebench.common.ProjectRequest
|
import com.jaytux.phoebench.common.ProjectRequest
|
||||||
import com.jaytux.phoebench.common.Routes
|
import com.jaytux.phoebench.common.Routes
|
||||||
import com.jaytux.phoebench.common.TimeUnit
|
import com.jaytux.phoebench.common.TimeUnit
|
||||||
@@ -19,9 +14,6 @@ import com.jaytux.phoebench.common.error
|
|||||||
import com.jaytux.phoebench.common.fold
|
import com.jaytux.phoebench.common.fold
|
||||||
import com.jaytux.phoebench.common.map
|
import com.jaytux.phoebench.common.map
|
||||||
import com.jaytux.phoebench.common.value
|
import com.jaytux.phoebench.common.value
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.datetime.toLocalDateTime
|
|
||||||
import java.nio.file.OpenOption
|
|
||||||
import kotlin.io.path.Path
|
import kotlin.io.path.Path
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
import kotlin.io.path.inputStream
|
import kotlin.io.path.inputStream
|
||||||
@@ -100,7 +92,7 @@ object ProjectHandlers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newLabel(name: String?, color: String?, project: CLI.Commands.Project.IProjectIdentification?) {
|
fun newVersion(name: String?, color: String?, project: CLI.Commands.Project.IProjectIdentification?, version: String?) {
|
||||||
val id = ensureProjectIdentification(project)
|
val id = ensureProjectIdentification(project)
|
||||||
val useName = name.maybePrompt("label name") { it }
|
val useName = name.maybePrompt("label name") { it }
|
||||||
val useColor = color.maybePrompt("color") {
|
val useColor = color.maybePrompt("color") {
|
||||||
@@ -111,10 +103,11 @@ object ProjectHandlers {
|
|||||||
exitProcess(-1)
|
exitProcess(-1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val useVersion = version.maybePrompt("version") { it }
|
||||||
|
|
||||||
tryAuthenticated {
|
tryAuthenticated {
|
||||||
id.toId().bind { projectId ->
|
id.toId().bind { projectId ->
|
||||||
Client.callRoute(Routes.Label.new, LabelRequest(useName, useColor, projectId))
|
Client.callRoute(Routes.Version.new, VersionRequest(useName, useColor, useVersion, projectId))
|
||||||
}
|
}
|
||||||
}.fold({
|
}.fold({
|
||||||
System.err.println("Could not create label: ${it.msg}")
|
System.err.println("Could not create label: ${it.msg}")
|
||||||
@@ -139,11 +132,11 @@ object ProjectHandlers {
|
|||||||
label: CLI.Commands.Project.ILabelIdentification?,
|
label: CLI.Commands.Project.ILabelIdentification?,
|
||||||
warmup: CLI.Commands.Project.IData<Float>?,
|
warmup: CLI.Commands.Project.IData<Float>?,
|
||||||
measurement: CLI.Commands.Project.IData<Float>?,
|
measurement: CLI.Commands.Project.IData<Float>?,
|
||||||
unit: TimeUnit?
|
unit: TimeUnit?, input: String?, hardware: String?
|
||||||
) {
|
) {
|
||||||
val projectId = ensureProjectIdentification(project)
|
val projectId = ensureProjectIdentification(project)
|
||||||
val labelId = label.maybePrompt("label name") {
|
val versionId = label.maybePrompt("version name") {
|
||||||
CLI.Commands.Project.LabelName(it)
|
CLI.Commands.Project.VersionName(it)
|
||||||
}
|
}
|
||||||
val warmupData = warmup.ensure("warmup").toList()
|
val warmupData = warmup.ensure("warmup").toList()
|
||||||
val measureData = measurement.ensure("measurements").toList()
|
val measureData = measurement.ensure("measurements").toList()
|
||||||
@@ -162,20 +155,22 @@ object ProjectHandlers {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val useInput = input.maybePrompt("benchmark input") { it }
|
||||||
|
val useHardware = hardware.maybePrompt("hardware description") { it }
|
||||||
|
|
||||||
tryAuthenticated {
|
tryAuthenticated {
|
||||||
projectId.toId().bind { pId ->
|
projectId.toId().bind { pId ->
|
||||||
when(labelId) {
|
when(versionId) {
|
||||||
is CLI.Commands.Project.ID -> labelId.id.value()
|
is CLI.Commands.Project.ID -> versionId.id.value()
|
||||||
is CLI.Commands.Project.LabelName -> Client.callRoute(Routes.Project.get, pId).bind {
|
is CLI.Commands.Project.VersionName -> Client.callRoute(Routes.Project.get, pId).bind {
|
||||||
it.usedLabels.firstOrNull { l -> l.name == labelId.name }?.id?.value()
|
it.usedLabels.firstOrNull { l -> l.name == versionId.name }?.id?.value()
|
||||||
?: ErrorResponse("Label ${labelId.name} does not exist in this project.").error()
|
?: ErrorResponse("Label ${versionId.name} does not exist in this project.").error()
|
||||||
}
|
}
|
||||||
}.map { pId to it }
|
}.map { pId to it }
|
||||||
}.bind { (pId, lId) ->
|
}.bind { (pId, vId) ->
|
||||||
Client.callRoute(Routes.Entry.new, EntryRequest(
|
Client.callRoute(Routes.Entry.new, EntryRequest(
|
||||||
lId, Clock.System.now(), pId,
|
vId, Clock.System.now(), pId,
|
||||||
warmupData, measureData, timeUnit
|
warmupData, measureData, timeUnit, useInput, useHardware
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class Client private constructor(private val _auth: AuthProvider) {
|
|||||||
println("Call to ${route.pattern} [$ctr] was cancelled")
|
println("Call to ${route.pattern} [$ctr] was cancelled")
|
||||||
ErrorResponse(COROUTINE_CANCELLED).error()
|
ErrorResponse(COROUTINE_CANCELLED).error()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Call to ${route.pattern} [$ctr] ran into an exception")
|
println("Call to ${route.pattern} [$ctr] ran into an exception: ${e.message}")
|
||||||
ErrorResponse(e.message ?: "Unknown error.").error()
|
ErrorResponse(e.message ?: "Unknown error.").error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-15
@@ -8,15 +8,13 @@ import com.jaytux.phoebench.common.Either
|
|||||||
import com.jaytux.phoebench.common.EntryRequest
|
import com.jaytux.phoebench.common.EntryRequest
|
||||||
import com.jaytux.phoebench.common.EntryResponse
|
import com.jaytux.phoebench.common.EntryResponse
|
||||||
import com.jaytux.phoebench.common.ErrorResponse
|
import com.jaytux.phoebench.common.ErrorResponse
|
||||||
import com.jaytux.phoebench.common.LabelRequest
|
import com.jaytux.phoebench.common.VersionRequest
|
||||||
import com.jaytux.phoebench.common.LabelResponse
|
import com.jaytux.phoebench.common.VersionResponse
|
||||||
import com.jaytux.phoebench.common.PartialEntryRequest
|
|
||||||
import com.jaytux.phoebench.common.PartialLabelRequest
|
|
||||||
import com.jaytux.phoebench.common.PartialProjectRequest
|
import com.jaytux.phoebench.common.PartialProjectRequest
|
||||||
|
import com.jaytux.phoebench.common.PartialVersionRequest
|
||||||
import com.jaytux.phoebench.common.ProjectResponse
|
import com.jaytux.phoebench.common.ProjectResponse
|
||||||
import com.jaytux.phoebench.common.Routes
|
import com.jaytux.phoebench.common.Routes
|
||||||
import com.jaytux.phoebench.common.TimeUnit
|
import com.jaytux.phoebench.common.TimeUnit
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@@ -25,12 +23,12 @@ interface IProjectRepo {
|
|||||||
suspend fun update(name: String? = null, isPublic: Boolean? = null): Either<ErrorResponse, Unit>
|
suspend fun update(name: String? = null, isPublic: Boolean? = null): Either<ErrorResponse, Unit>
|
||||||
suspend fun delete(): Either<ErrorResponse, Unit>
|
suspend fun delete(): Either<ErrorResponse, Unit>
|
||||||
|
|
||||||
suspend fun newLabel(name: String, color: Color): Either<ErrorResponse, LabelResponse>
|
suspend fun newVersion(name: String, color: Color, meta: String): Either<ErrorResponse, VersionResponse>
|
||||||
suspend fun updateLabel(id: Uuid, name: String? = null, color: Color? = null): Either<ErrorResponse, Unit>
|
suspend fun updateVersion(id: Uuid, name: String? = null, color: Color? = null, meta: String? = null): Either<ErrorResponse, Unit>
|
||||||
suspend fun deleteLabel(id: Uuid): Either<ErrorResponse, Unit>
|
suspend fun deleteLabel(id: Uuid): Either<ErrorResponse, Unit>
|
||||||
|
|
||||||
suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>, measurements: List<Float>,
|
suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>, measurements: List<Float>,
|
||||||
unit: TimeUnit): Either<ErrorResponse, EntryResponse>
|
unit: TimeUnit, input: String, hardware: String): Either<ErrorResponse, EntryResponse>
|
||||||
suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit>
|
suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -44,19 +42,19 @@ interface IProjectRepo {
|
|||||||
override suspend fun delete(): Either<ErrorResponse, Unit> =
|
override suspend fun delete(): Either<ErrorResponse, Unit> =
|
||||||
_client.callRoute(Routes.Project.delete, _projectId).ignoreValue()
|
_client.callRoute(Routes.Project.delete, _projectId).ignoreValue()
|
||||||
|
|
||||||
override suspend fun newLabel(name: String, color: Color): Either<ErrorResponse, LabelResponse> =
|
override suspend fun newVersion(name: String, color: Color, meta: String): Either<ErrorResponse, VersionResponse> =
|
||||||
_client.callRoute(Routes.Label.new, LabelRequest(name, color.hexString(), _projectId))
|
_client.callRoute(Routes.Version.new, VersionRequest(name, color.hexString(), meta, _projectId))
|
||||||
|
|
||||||
override suspend fun updateLabel(id: Uuid, name: String?, color: Color?): Either<ErrorResponse, Unit> =
|
override suspend fun updateVersion(id: Uuid, name: String?, color: Color?, meta: String?): Either<ErrorResponse, Unit> =
|
||||||
_client.callRoute(Routes.Label.update, id to PartialLabelRequest(name, color?.hexString())).ignoreValue()
|
_client.callRoute(Routes.Version.update, id to PartialVersionRequest(name, color?.hexString(), meta)).ignoreValue()
|
||||||
|
|
||||||
override suspend fun deleteLabel(id: Uuid): Either<ErrorResponse, Unit> =
|
override suspend fun deleteLabel(id: Uuid): Either<ErrorResponse, Unit> =
|
||||||
_client.callRoute(Routes.Label.delete, id).ignoreValue()
|
_client.callRoute(Routes.Version.delete, id).ignoreValue()
|
||||||
|
|
||||||
override suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>,
|
override suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List<Float>,
|
||||||
measurements: List<Float>, unit: TimeUnit
|
measurements: List<Float>, unit: TimeUnit, input: String, hardware: String
|
||||||
): Either<ErrorResponse, EntryResponse> =
|
): Either<ErrorResponse, EntryResponse> =
|
||||||
_client.callRoute(Routes.Entry.new, EntryRequest(label, timestamp, _projectId, warmups, measurements, unit))
|
_client.callRoute(Routes.Entry.new, EntryRequest(label, timestamp, _projectId, warmups, measurements, unit, input, hardware))
|
||||||
|
|
||||||
override suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit> =
|
override suspend fun deleteEntry(id: Uuid): Either<ErrorResponse, Unit> =
|
||||||
_client.callRoute(Routes.Entry.delete, id).ignoreValue()
|
_client.callRoute(Routes.Entry.delete, id).ignoreValue()
|
||||||
|
|||||||
+55
-31
@@ -11,13 +11,11 @@ import com.jaytux.phoebench.clients.SnackProvider.Companion.snackOr
|
|||||||
import com.jaytux.phoebench.clients.hexString
|
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.systemTz
|
|
||||||
import com.jaytux.phoebench.clients.withScope
|
import com.jaytux.phoebench.clients.withScope
|
||||||
import com.jaytux.phoebench.common.EntryResponse
|
import com.jaytux.phoebench.common.EntryResponse
|
||||||
import com.jaytux.phoebench.common.LabelResponse
|
import com.jaytux.phoebench.common.VersionResponse
|
||||||
import com.jaytux.phoebench.common.ProjectEvent
|
import com.jaytux.phoebench.common.ProjectEvent
|
||||||
import com.jaytux.phoebench.common.TimeUnit
|
import com.jaytux.phoebench.common.TimeUnit
|
||||||
import com.jaytux.phoebench.common.foldSuspend
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
@@ -32,7 +30,7 @@ class ProjectVM(
|
|||||||
private val _sseRepo: ISSERepo = ISSERepo.default(_client),
|
private val _sseRepo: ISSERepo = ISSERepo.default(_client),
|
||||||
private val _forceBack: () -> Unit
|
private val _forceBack: () -> Unit
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
data class Label(val id: Uuid, val name: String, val colorStr: String, val uiColor: Color = parseColor(colorStr)) {
|
data class Version(val id: Uuid, val name: String, val colorStr: String, val meta: String, val uiColor: Color = parseColor(colorStr)) {
|
||||||
companion object {
|
companion object {
|
||||||
private val _snack by lazy { SnackProvider.get() }
|
private val _snack by lazy { SnackProvider.get() }
|
||||||
private val _errColor = Color(red = 252, green = 20, blue = 182)
|
private val _errColor = Color(red = 252, green = 20, blue = 182)
|
||||||
@@ -50,18 +48,18 @@ class ProjectVM(
|
|||||||
return Color(red = r, green = g, blue = b)
|
return Color(red = r, green = g, blue = b)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun fromResponse(it: LabelResponse) = Label(it.id, it.name, it.color)
|
fun fromResponse(it: VersionResponse) = Version(it.id, it.name, it.color, it.meta)
|
||||||
|
|
||||||
val invalid = Label(Uuid.fromLongs(0L, 0L), "<invalid>", _errColor.hexString(), _errColor)
|
val invalid = Version(Uuid.fromLongs(0L, 0L), "<invalid>", _errColor.hexString(), "", _errColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Entry(val id: Uuid, val label: Label, val timeStamp: Instant, val warmups: List<Float>,
|
data class Entry(val id: Uuid, val label: Version, val timeStamp: Instant, val warmups: List<Float>,
|
||||||
val measurements: List<Float>, val nativeUnit: TimeUnit) {
|
val measurements: List<Float>, val nativeUnit: TimeUnit, val input: String, val hardware: String) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromResponse(it: EntryResponse, map: Map<Uuid, Label>) = Entry(
|
fun fromResponse(it: EntryResponse, map: Map<Uuid, Version>) = Entry(
|
||||||
it.id, map[it.labelId] ?: Label.invalid, it.timestamp,
|
it.id, map[it.labelId] ?: Version.invalid, it.timestamp,
|
||||||
it.warmups, it.measurements, it.unit
|
it.warmups, it.measurements, it.unit, it.input, it.hardware
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,15 +68,19 @@ class ProjectVM(
|
|||||||
private val _owner = mutableStateOf<String?>(null)
|
private val _owner = mutableStateOf<String?>(null)
|
||||||
private val _public = mutableStateOf(false)
|
private val _public = mutableStateOf(false)
|
||||||
private val _editable = mutableStateOf(false)
|
private val _editable = mutableStateOf(false)
|
||||||
private val _labels = mutableStateOf(mapOf<Uuid, Label>())
|
private val _labels = mutableStateOf(mapOf<Uuid, Version>())
|
||||||
private val _entries = mutableStateOf(listOf<Entry>())
|
private val _entries = mutableStateOf(listOf<Entry>())
|
||||||
|
private val _inputs = mutableStateOf(setOf<String>())
|
||||||
|
private val _hardware = mutableStateOf(setOf<String>())
|
||||||
|
|
||||||
val name = _name.immutable()
|
val name = _name.immutable()
|
||||||
val owner = _owner.immutable()
|
val owner = _owner.immutable()
|
||||||
val public = _public.immutable()
|
val public = _public.immutable()
|
||||||
val editable = _editable.immutable()
|
val editable = _editable.immutable()
|
||||||
val labels = _labels.immutable()
|
val versions = _labels.immutable()
|
||||||
val entries = _entries.immutable()
|
val entries = _entries.immutable()
|
||||||
|
val inputs = _inputs.immutable()
|
||||||
|
val hardware = _hardware.immutable()
|
||||||
|
|
||||||
private var _job: Job? = null
|
private var _job: Job? = null
|
||||||
|
|
||||||
@@ -100,8 +102,16 @@ class ProjectVM(
|
|||||||
_owner.value = it.owner.name
|
_owner.value = it.owner.name
|
||||||
_public.value = it.isPublic
|
_public.value = it.isPublic
|
||||||
_editable.value = it.isEditable
|
_editable.value = it.isEditable
|
||||||
_labels.value = it.usedLabels.associate { l -> l.id to Label.fromResponse(l) }
|
_labels.value = it.usedLabels.associate { l -> l.id to Version.fromResponse(l) }
|
||||||
_entries.value = it.entries.map { e -> Entry.fromResponse(e, _labels.value) }
|
val localInputs = mutableSetOf<String>()
|
||||||
|
val localHardware = mutableSetOf<String>()
|
||||||
|
_entries.value = it.entries.map { e ->
|
||||||
|
localInputs += e.input
|
||||||
|
localHardware += e.hardware
|
||||||
|
Entry.fromResponse(e, _labels.value)
|
||||||
|
}
|
||||||
|
_inputs.value = localInputs
|
||||||
|
_hardware.value = localHardware
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,10 +120,14 @@ class ProjectVM(
|
|||||||
when(event) {
|
when(event) {
|
||||||
ProjectEvent.Deleted -> back()
|
ProjectEvent.Deleted -> back()
|
||||||
is ProjectEvent.EntryDeleted -> _entries.value = _entries.value.filter { it.id != event.id }
|
is ProjectEvent.EntryDeleted -> _entries.value = _entries.value.filter { it.id != event.id }
|
||||||
is ProjectEvent.LabelChanged -> _labels.value += (event.label.id to Label.fromResponse(event.label))
|
is ProjectEvent.VersionChanged -> _labels.value += (event.label.id to Version.fromResponse(event.label))
|
||||||
is ProjectEvent.LabelDeleted -> _labels.value = _labels.value.filter { it.value.id != event.id }
|
is ProjectEvent.VersionDeleted -> _labels.value = _labels.value.filter { it.value.id != event.id }
|
||||||
is ProjectEvent.NewEntry -> _entries.value = _entries.value.insort(Entry.fromResponse(event.entry, _labels.value), Entry::timeStamp)
|
is ProjectEvent.NewEntry -> {
|
||||||
is ProjectEvent.NewLabel -> _labels.value += (event.label.id to Label.fromResponse(event.label))
|
_entries.value = _entries.value.insort(Entry.fromResponse(event.entry, _labels.value), Entry::timeStamp)
|
||||||
|
_inputs.value += event.entry.input
|
||||||
|
_hardware.value += event.entry.hardware
|
||||||
|
}
|
||||||
|
is ProjectEvent.NewVersion -> _labels.value += (event.label.id to Version.fromResponse(event.label))
|
||||||
is ProjectEvent.Updated -> {
|
is ProjectEvent.Updated -> {
|
||||||
_name.value = event.changes.name
|
_name.value = event.changes.name
|
||||||
_owner.value = event.changes.owner.name
|
_owner.value = event.changes.owner.name
|
||||||
@@ -124,7 +138,9 @@ class ProjectVM(
|
|||||||
|
|
||||||
fun update(name: String?, isPublic: Boolean?) {
|
fun update(name: String?, isPublic: Boolean?) {
|
||||||
withScope {
|
withScope {
|
||||||
_repo.update(name, isPublic).snackOr { refresh() }
|
_repo.update(name, isPublic).snackOr {
|
||||||
|
if(_job == null) refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,19 +155,21 @@ class ProjectVM(
|
|||||||
_forceBack()
|
_forceBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mkLabel(name: String, color: Color) {
|
fun mkLabel(name: String, color: Color, meta: String) {
|
||||||
withScope {
|
withScope {
|
||||||
_repo.newLabel(name, color).snackOr {
|
_repo.newVersion(name, color, meta).snackOr {
|
||||||
_labels.value += it.id to Label(it.id, it.name, it.color)
|
if(_job == null)
|
||||||
|
_labels.value += it.id to Version(it.id, it.name, it.color, it.meta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLabel(id: Uuid, name: String?, color: Color?) {
|
fun updateLabel(id: Uuid, name: String?, color: Color?, meta: String?) {
|
||||||
withScope {
|
withScope {
|
||||||
val old = _labels.value[id] ?: return@withScope
|
val old = _labels.value[id] ?: return@withScope
|
||||||
_repo.updateLabel(id, name, color).snackOr {
|
_repo.updateVersion(id, name, color).snackOr {
|
||||||
_labels.value += id to Label(id, name ?: old.name, color?.hexString() ?: old.colorStr, color ?: old.uiColor)
|
if(_job == null)
|
||||||
|
_labels.value += id to Version(id, name ?: old.name, color?.hexString() ?: old.colorStr, meta ?: old.meta, color ?: old.uiColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,15 +177,20 @@ class ProjectVM(
|
|||||||
fun deleteLabel(id: Uuid) {
|
fun deleteLabel(id: Uuid) {
|
||||||
withScope {
|
withScope {
|
||||||
_repo.deleteLabel(id).snackOr {
|
_repo.deleteLabel(id).snackOr {
|
||||||
_labels.value -= id
|
if(_job == null)
|
||||||
|
_labels.value -= id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mkEntry(label: Label, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit) {
|
fun mkEntry(label: Version, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit, input: String, hardware: String) {
|
||||||
withScope {
|
withScope {
|
||||||
_repo.newEntry(label.id, Clock.System.now(), warmups, measurements, unit).snackOr {
|
_repo.newEntry(label.id, Clock.System.now(), warmups, measurements, unit, input, hardware).snackOr {
|
||||||
_entries.value += Entry.fromResponse(it, _labels.value)
|
if(_job == null) {
|
||||||
|
_entries.value += Entry.fromResponse(it, _labels.value)
|
||||||
|
_inputs.value += input
|
||||||
|
_hardware.value += hardware
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +198,8 @@ class ProjectVM(
|
|||||||
fun deleteEntry(id: Uuid) {
|
fun deleteEntry(id: Uuid) {
|
||||||
withScope {
|
withScope {
|
||||||
_repo.deleteEntry(id).snackOr {
|
_repo.deleteEntry(id).snackOr {
|
||||||
_entries.value = _entries.value.filter { it.id != id }
|
if(_job == null)
|
||||||
|
_entries.value = _entries.value.filter { it.id != id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+165
-42
@@ -13,6 +13,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.backhandler.BackHandler
|
import androidx.compose.ui.backhandler.BackHandler
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
@@ -24,9 +25,9 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.composables.icons.lucide.*
|
import com.composables.icons.lucide.*
|
||||||
import com.jaytux.phoebench.clients.darken
|
|
||||||
import com.jaytux.phoebench.clients.data.ProjectVM
|
import com.jaytux.phoebench.clients.data.ProjectVM
|
||||||
import com.jaytux.phoebench.clients.data.mutableStateSetFrom
|
import com.jaytux.phoebench.clients.data.mutableStateSetFrom
|
||||||
|
import com.jaytux.phoebench.clients.data.mutableStateSetOf
|
||||||
import com.jaytux.phoebench.clients.dualLerp
|
import com.jaytux.phoebench.clients.dualLerp
|
||||||
import com.jaytux.phoebench.clients.fmt
|
import com.jaytux.phoebench.clients.fmt
|
||||||
import com.jaytux.phoebench.clients.fmtRange
|
import com.jaytux.phoebench.clients.fmtRange
|
||||||
@@ -48,8 +49,6 @@ import io.github.koalaplot.core.xygraph.AxisContent
|
|||||||
import io.github.koalaplot.core.xygraph.DefaultPoint
|
import io.github.koalaplot.core.xygraph.DefaultPoint
|
||||||
import io.github.koalaplot.core.xygraph.XYGraph
|
import io.github.koalaplot.core.xygraph.XYGraph
|
||||||
import io.github.koalaplot.core.xygraph.autoScaleRange
|
import io.github.koalaplot.core.xygraph.autoScaleRange
|
||||||
import io.github.koalaplot.core.xygraph.autoScaleXRange
|
|
||||||
import io.github.koalaplot.core.xygraph.autoScaleYRange
|
|
||||||
import io.github.koalaplot.core.xygraph.rememberAxisStyle
|
import io.github.koalaplot.core.xygraph.rememberAxisStyle
|
||||||
import io.github.koalaplot.core.xygraph.rememberFloatLinearAxisModel
|
import io.github.koalaplot.core.xygraph.rememberFloatLinearAxisModel
|
||||||
import io.github.koalaplot.core.xygraph.rememberGridStyle
|
import io.github.koalaplot.core.xygraph.rememberGridStyle
|
||||||
@@ -66,7 +65,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
|||||||
val owner by vm.owner
|
val owner by vm.owner
|
||||||
val public by vm.public
|
val public by vm.public
|
||||||
val editable by vm.editable
|
val editable by vm.editable
|
||||||
val labels by vm.labels
|
val labels by vm.versions
|
||||||
|
|
||||||
var editing by remember { mutableStateOf(false) }
|
var editing by remember { mutableStateOf(false) }
|
||||||
var deleting by remember { mutableStateOf(false) }
|
var deleting by remember { mutableStateOf(false) }
|
||||||
@@ -148,16 +147,19 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) {
|
|||||||
vm.delete()
|
vm.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
if(addingLabel) AddLabelDialog({ addingLabel = false; addOpen = false }) { name, color ->
|
if(addingLabel) AddVersionDialog({ addingLabel = false; addOpen = false }) { name, color, meta ->
|
||||||
vm.mkLabel(name, color)
|
vm.mkLabel(name, color, meta)
|
||||||
addOpen = false
|
addOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if(addingData) AddEntryDialog(labels,
|
val usedInputs by vm.inputs
|
||||||
|
val usedHardware by vm.hardware
|
||||||
|
|
||||||
|
if(addingData) AddEntryDialog(labels, usedInputs, usedHardware,
|
||||||
onCancel = { addingData = false; addOpen = false },
|
onCancel = { addingData = false; addOpen = false },
|
||||||
onAddLbl = { name, lbl -> vm.mkLabel(name, lbl) }
|
onAddLbl = { name, lbl, meta -> vm.mkLabel(name, lbl, meta) }
|
||||||
) { label, warmups, measurements, unit ->
|
) { label, warmups, measurements, unit, input, hardware ->
|
||||||
vm.mkEntry(label, warmups, measurements, unit)
|
vm.mkEntry(label, warmups, measurements, unit, input, hardware)
|
||||||
addOpen = false
|
addOpen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,48 +207,67 @@ fun ConfirmDeleteProjectDialog(name: String, onCancel: () -> Unit, onDelete: ()
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AddLabelDialog(onCancel: () -> Unit, onAdd: (name: String, color: Color) -> Unit) {
|
fun AddVersionDialog(onCancel: () -> Unit, onAdd: (name: String, color: Color, meta: String) -> Unit) {
|
||||||
Dialog(onDismissRequest = onCancel) {
|
Dialog(onDismissRequest = onCancel) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf("") }
|
||||||
var color by remember { mutableStateOf(randomColor()) }
|
var color by remember { mutableStateOf(randomColor()) }
|
||||||
|
var meta by remember { mutableStateOf("") }
|
||||||
|
|
||||||
Surface(Modifier.padding(15.dp).widthIn(400.dp), shape = MaterialTheme.shapes.medium) {
|
Surface(Modifier.padding(15.dp).widthIn(400.dp), shape = MaterialTheme.shapes.medium) {
|
||||||
Column(Modifier.padding(8.dp).width(IntrinsicSize.Min)) {
|
Column(Modifier.padding(8.dp).width(IntrinsicSize.Min)) {
|
||||||
Text("Create label", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
Text("Create version", Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.headlineMedium)
|
||||||
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text("Name") })
|
OutlinedTextField(name, { name = it }, Modifier.fillMaxWidth(), label = { Text("Name") })
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
KolorPicker(color, { color = it }, alphaTrackVisible = false, modifier = Modifier.aspectRatio(1f))
|
KolorPicker(color, { color = it }, alphaTrackVisible = false, modifier = Modifier.aspectRatio(1f))
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
|
OutlinedTextField(meta, { meta = it }, Modifier.fillMaxWidth(), label = { Text("Additional information") })
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
|
||||||
CancelConfirmXRow(onCancel, {
|
CancelConfirmXRow(onCancel, {
|
||||||
onAdd(name.trim(), color)
|
onAdd(name.trim(), color, meta.trim())
|
||||||
}, confirmText = "Create", canConfirm = name.trim().isNotBlank())
|
}, confirmText = "Create", canConfirm = name.trim().isNotBlank() && meta.trim().isNotBlank())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun QuickLabel(lbl: ProjectVM.Label) {
|
fun QuickVersion(lbl: ProjectVM.Version, hollow: Boolean = false) {
|
||||||
Row(Modifier.height(IntrinsicSize.Min).padding(vertical = 3.dp), verticalAlignment = Alignment.Bottom) {
|
Row(Modifier.height(IntrinsicSize.Min).padding(vertical = 3.dp), verticalAlignment = Alignment.Bottom) {
|
||||||
Box(Modifier.fillMaxHeight().aspectRatio(1f).background(lbl.uiColor)) {}
|
Box(Modifier.fillMaxHeight().aspectRatio(1f).background(lbl.uiColor)) {
|
||||||
|
if(hollow) {
|
||||||
|
Box(Modifier.fillMaxHeight(0.75f).aspectRatio(1f).align(Alignment.Center).background(Color.Gray.copy(alpha = 0.5f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
Spacer(Modifier.width(15.dp))
|
Spacer(Modifier.width(15.dp))
|
||||||
Text(lbl.name)
|
Column {
|
||||||
Text(" ${lbl.colorStr}", style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(alpha = 0.75f))
|
Row {
|
||||||
|
Text(lbl.name)
|
||||||
|
Text(" ${lbl.colorStr}", Modifier.align(Alignment.Bottom),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalContentColor.current.copy(alpha = 0.75f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(lbl.meta, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = LocalContentColor.current.copy(0.75f))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AddEntryDialog(
|
fun AddEntryDialog(
|
||||||
labels: Map<Uuid, ProjectVM.Label>,
|
labels: Map<Uuid, ProjectVM.Version>, usedInputs: Set<String>, usedHardware: Set<String>,
|
||||||
onCancel: () -> Unit, onAddLbl: (name: String, color: Color) -> Unit,
|
onCancel: () -> Unit, onAddLbl: (name: String, color: Color, meta: String) -> Unit,
|
||||||
onAdd: (label: ProjectVM.Label, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit) -> Unit
|
onAdd: (label: ProjectVM.Version, warmups: List<Float>, measurements: List<Float>, unit: TimeUnit, input: String, hardware: String) -> Unit
|
||||||
) {
|
) {
|
||||||
Dialog(onDismissRequest = onCancel) {
|
Dialog(onDismissRequest = onCancel) {
|
||||||
var label by remember { mutableStateOf<ProjectVM.Label?>(null) }
|
var label by remember { mutableStateOf<ProjectVM.Version?>(null) }
|
||||||
var warmups by remember { mutableStateOf("") }
|
var warmups by remember { mutableStateOf("") }
|
||||||
var measurements by remember { mutableStateOf("") }
|
var measurements by remember { mutableStateOf("") }
|
||||||
var unit by remember { mutableStateOf(TimeUnit.SECONDS) }
|
var unit by remember { mutableStateOf(TimeUnit.SECONDS) }
|
||||||
|
var input by remember { mutableStateOf("") }
|
||||||
|
var hardware by remember { mutableStateOf("") }
|
||||||
|
|
||||||
var addingLabel by remember { mutableStateOf(false) }
|
var addingLabel by remember { mutableStateOf(false) }
|
||||||
var warmupParsed by remember { mutableStateOf<Either<String, List<Float>>>(listOf<Float>().value()) }
|
var warmupParsed by remember { mutableStateOf<Either<String, List<Float>>>(listOf<Float>().value()) }
|
||||||
@@ -281,12 +302,12 @@ fun AddEntryDialog(
|
|||||||
tonalElevation = 2.dp
|
tonalElevation = 2.dp
|
||||||
) {
|
) {
|
||||||
Box(Modifier.fillMaxWidth().clickable { dropDownOpen = true }.padding(8.dp)) {
|
Box(Modifier.fillMaxWidth().clickable { dropDownOpen = true }.padding(8.dp)) {
|
||||||
label?.let { QuickLabel(it) } ?: Text("Select label...", fontStyle = FontStyle.Italic)
|
label?.let { QuickVersion(it) } ?: Text("Select version...", fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DropdownMenu(dropDownOpen, { dropDownOpen = false }) {
|
DropdownMenu(dropDownOpen, { dropDownOpen = false }) {
|
||||||
labels.values.forEach { lbl ->
|
labels.values.forEach { lbl ->
|
||||||
DropdownMenuItem({ QuickLabel(lbl) }, { label = lbl; dropDownOpen = false })
|
DropdownMenuItem({ QuickVersion(lbl) }, { label = lbl; dropDownOpen = false })
|
||||||
}
|
}
|
||||||
HorizontalDivider(Modifier.height(1.dp))
|
HorizontalDivider(Modifier.height(1.dp))
|
||||||
|
|
||||||
@@ -308,6 +329,39 @@ fun AddEntryDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
var inputFocused by remember { mutableStateOf(false) }
|
||||||
|
var hardwareFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box(Modifier.weight(0.5f)) {
|
||||||
|
ExposedDropdownMenuBox(inputFocused, {}) {
|
||||||
|
OutlinedTextField(input, { input = it },
|
||||||
|
Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable).onFocusChanged { inputFocused = it.isFocused },
|
||||||
|
label = { Text("Input") }
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(inputFocused, {}) {
|
||||||
|
usedInputs.filter { input.lowercase() in it.lowercase() }.sorted().forEach { inp ->
|
||||||
|
DropdownMenuItem(text = { Text(inp) }, onClick = { input = inp })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(5.dp))
|
||||||
|
Box(Modifier.weight(0.5f)) {
|
||||||
|
ExposedDropdownMenuBox(hardwareFocused, { inputFocused = false }) {
|
||||||
|
OutlinedTextField(hardware, { hardware = it },
|
||||||
|
Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable).onFocusChanged { hardwareFocused = it.isFocused },
|
||||||
|
label = { Text("Hardware") }
|
||||||
|
)
|
||||||
|
ExposedDropdownMenu(hardwareFocused, { hardwareFocused = false }) {
|
||||||
|
usedHardware.filter { hardware.lowercase() in it.lowercase() }.sorted().forEach { hw ->
|
||||||
|
DropdownMenuItem(text = { Text(hw) }, onClick = { hardware = hw })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(7.dp))
|
Spacer(Modifier.height(7.dp))
|
||||||
Text("Data (separate data points by commas)")
|
Text("Data (separate data points by commas)")
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
@@ -326,19 +380,19 @@ fun AddEntryDialog(
|
|||||||
|
|
||||||
Spacer(Modifier.height(5.dp))
|
Spacer(Modifier.height(5.dp))
|
||||||
CancelConfirmXRow(onCancel, {
|
CancelConfirmXRow(onCancel, {
|
||||||
onAdd(label!!, warmupParsed.asValue()!!, measureParsed.asValue()!!, unit)
|
onAdd(label!!, warmupParsed.asValue()!!, measureParsed.asValue()!!, unit, input, hardware)
|
||||||
}, confirmText = "Create", canConfirm = label != null && warmupParsed.isValue() && measureParsed.isValue())
|
}, confirmText = "Create", canConfirm = label != null && warmupParsed.isValue() && measureParsed.isValue() && input.isNotBlank() && hardware.isNotBlank())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(addingLabel) AddLabelDialog({ addingLabel = false }, onAddLbl)
|
if(addingLabel) AddVersionDialog({ addingLabel = false }, onAddLbl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProjectPlotArea(vm: ProjectVM){
|
fun ProjectPlotArea(vm: ProjectVM){
|
||||||
val entries by vm.entries
|
val entries by vm.entries
|
||||||
val labels by vm.labels
|
val versions by vm.versions
|
||||||
val editable by vm.editable
|
val editable by vm.editable
|
||||||
|
|
||||||
var displayWarmups by remember { mutableStateOf(false) }
|
var displayWarmups by remember { mutableStateOf(false) }
|
||||||
@@ -353,20 +407,36 @@ fun ProjectPlotArea(vm: ProjectVM){
|
|||||||
var timeFilter by remember { mutableStateOf(0f..1f) }
|
var timeFilter by remember { mutableStateOf(0f..1f) }
|
||||||
var timeFilterString by remember { mutableStateOf("") }
|
var timeFilterString by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val labelFilter = remember(labels) { mutableStateSetFrom(labels.values) }
|
val versionFilter = remember(versions) { mutableStateSetFrom(versions.values) }
|
||||||
val labelFilterKey by labelFilter.revision
|
val labelVersionFilter by versionFilter.revision
|
||||||
|
|
||||||
LaunchedEffect(entries, displayWarmups, currentUnit, timeFilter, timeMin, timeMax, labelFilterKey) {
|
val allInputs by vm.inputs
|
||||||
|
val allHardware by vm.hardware
|
||||||
|
val enabledInputs = remember { mutableStateSetOf<String>() }
|
||||||
|
val enabledHardware = remember { mutableStateSetOf<String>() }
|
||||||
|
var filterInput by remember { mutableStateOf<String?>(null) }
|
||||||
|
var filterHardware by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(entries, displayWarmups, currentUnit, timeFilter, timeMin, timeMax, labelVersionFilter, filterInput, filterHardware) {
|
||||||
var maxX = 0f
|
var maxX = 0f
|
||||||
var minY = 0f
|
var minY = 0f
|
||||||
var maxY = 0f
|
var maxY = 0f
|
||||||
val timeRange = dualLerp(timeMin, timeMax, timeFilter.start, timeFilter.endInclusive)
|
val timeRange = dualLerp(timeMin, timeMax, timeFilter.start, timeFilter.endInclusive)
|
||||||
|
enabledInputs.clear()
|
||||||
|
enabledHardware.clear()
|
||||||
|
|
||||||
renderableEntries = entries.mapNotNull { entry ->
|
renderableEntries = entries.mapNotNull { entry ->
|
||||||
val use = if(displayWarmups) entry.warmups else entry.measurements
|
val use = if(displayWarmups) entry.warmups else entry.measurements
|
||||||
|
|
||||||
|
if(entry.label !in versionFilter) return@mapNotNull null
|
||||||
|
if(!(entry.timeStamp inRange timeRange)) return@mapNotNull null
|
||||||
|
if(filterInput != null && entry.input != filterInput) return@mapNotNull null
|
||||||
|
if(filterHardware != null && entry.hardware != filterHardware) return@mapNotNull null
|
||||||
|
|
||||||
maxX = maxOf(maxX, use.size.toFloat())
|
maxX = maxOf(maxX, use.size.toFloat())
|
||||||
|
|
||||||
if(entry.label !in labelFilter) return@mapNotNull null
|
enabledInputs.add(entry.input)
|
||||||
if(!(entry.timeStamp inRange timeRange)) return@mapNotNull null
|
enabledHardware.add(entry.hardware)
|
||||||
|
|
||||||
use.mapIndexed { idx, it ->
|
use.mapIndexed { idx, it ->
|
||||||
val converted = entry.nativeUnit.convertTo(currentUnit, it)
|
val converted = entry.nativeUnit.convertTo(currentUnit, it)
|
||||||
@@ -482,23 +552,34 @@ fun ProjectPlotArea(vm: ProjectVM){
|
|||||||
|
|
||||||
Spacer(Modifier.width(20.dp))
|
Spacer(Modifier.width(20.dp))
|
||||||
|
|
||||||
Box(Modifier.weight(0.33f).fillMaxHeight()) {
|
Row(Modifier.weight(0.33f).fillMaxHeight()) {
|
||||||
Column {
|
Column(Modifier.weight(0.5f)) {
|
||||||
Text("Labels", style = MaterialTheme.typography.headlineSmall)
|
Text("Versions", style = MaterialTheme.typography.headlineSmall)
|
||||||
|
Text("Click a version to toggle its visibility.", fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalContentColor.current.copy(alpha = 0.5f), style = MaterialTheme.typography.bodySmall)
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
if(labels.isEmpty()) {
|
if(versions.isEmpty()) {
|
||||||
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f)) {
|
Box(Modifier.fillMaxWidth().fillMaxHeight(0.25f)) {
|
||||||
Text("No labels yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
Text("No versions yet.", Modifier.align(Alignment.Center), fontStyle = FontStyle.Italic)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
LazyColumn(Modifier.padding(start = 5.dp)) {
|
LazyColumn(Modifier.padding(start = 5.dp)) {
|
||||||
items(labels.toList().sortedBy { it.second.name }) { (_, lbl) ->
|
items(versions.toList().sortedBy { it.second.name }) { (_, lbl) ->
|
||||||
Box(Modifier.fillMaxWidth().clickable { labelFilter.toggle(lbl) }) {
|
Box(Modifier.fillMaxWidth().clickable {
|
||||||
|
if(versionFilter.size == versions.size) {
|
||||||
|
versionFilter.clear()
|
||||||
|
versionFilter.add(lbl)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
versionFilter.toggle(lbl)
|
||||||
|
if(versionFilter.isEmpty()) versionFilter.addAll(versions.values)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
Box {
|
Box {
|
||||||
QuickLabel(lbl)
|
QuickVersion(lbl, lbl !in versionFilter)
|
||||||
|
|
||||||
if(lbl !in labelFilter) {
|
if(lbl !in versionFilter) {
|
||||||
Box(Modifier.matchParentSize()) {
|
Box(Modifier.matchParentSize()) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
Modifier.fillMaxWidth().align(Alignment.Center),
|
Modifier.fillMaxWidth().align(Alignment.Center),
|
||||||
@@ -513,6 +594,48 @@ fun ProjectPlotArea(vm: ProjectVM){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.width(15.dp))
|
||||||
|
LazyColumn(Modifier.weight(0.5f)) {
|
||||||
|
item { Text("Inputs", style = MaterialTheme.typography.headlineSmall) }
|
||||||
|
item {
|
||||||
|
Text("Click an input to set or clear the filter.", fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(10.dp)) }
|
||||||
|
|
||||||
|
items((allInputs intersect enabledInputs.toSet()).toList()) {
|
||||||
|
Row(Modifier.fillMaxWidth().clickable { filterInput = if(filterInput == null) it else null }) {
|
||||||
|
Text(it, Modifier.padding(start = 5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items((allInputs - enabledInputs.toSet()).toList()) {
|
||||||
|
Text(it, Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp), color = LocalContentColor.current.copy(0.66f))
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(10.dp)) }
|
||||||
|
item { HorizontalDivider(Modifier.height(1.dp)) }
|
||||||
|
item { Spacer(Modifier.height(10.dp)) }
|
||||||
|
|
||||||
|
item { Text("Hardware", style = MaterialTheme.typography.headlineSmall) }
|
||||||
|
item {
|
||||||
|
Text("Click an item to set or clear the filter.", fontStyle = FontStyle.Italic,
|
||||||
|
color = LocalContentColor.current.copy(0.5f), style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(10.dp)) }
|
||||||
|
|
||||||
|
items((allHardware intersect enabledHardware.toSet()).toList()) {
|
||||||
|
Row(Modifier.fillMaxWidth().clickable { filterHardware = if(filterHardware == null) it else null }) {
|
||||||
|
Text(it, Modifier.padding(start = 5.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items((allHardware - enabledHardware.toSet()).toList()) {
|
||||||
|
Text(it, Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp), color = LocalContentColor.current.copy(0.66f))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ sealed class ProjectEvent {
|
|||||||
data class Updated(val changes: HomeResponse.ProjectSummary) : ProjectEvent()
|
data class Updated(val changes: HomeResponse.ProjectSummary) : ProjectEvent()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NewLabel(val label: LabelResponse) : ProjectEvent()
|
data class NewVersion(val label: VersionResponse) : ProjectEvent()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LabelChanged(val label: LabelResponse) : ProjectEvent()
|
data class VersionChanged(val label: VersionResponse) : ProjectEvent()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LabelDeleted(val id: Uuid) : ProjectEvent()
|
data class VersionDeleted(val id: Uuid) : ProjectEvent()
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class NewEntry(val entry: EntryResponse) : ProjectEvent()
|
data class NewEntry(val entry: EntryResponse) : ProjectEvent()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.jaytux.phoebench.common
|
package com.jaytux.phoebench.common
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
@@ -23,11 +22,11 @@ data class RefreshRequest(val refreshToken: Uuid)
|
|||||||
data class ProjectRequest(val name: String, var isPublic: Boolean)
|
data class ProjectRequest(val name: String, var isPublic: Boolean)
|
||||||
|
|
||||||
@Serializable @ToPartialize
|
@Serializable @ToPartialize
|
||||||
data class LabelRequest(val name: String, val color: String, val projectId: Uuid)
|
data class VersionRequest(val name: String, val color: String, val meta: String, val projectId: Uuid)
|
||||||
|
|
||||||
@Serializable @ToPartialize
|
@Serializable @ToPartialize
|
||||||
data class EntryRequest(val label: Uuid, val timestamp: Instant, val projectId: Uuid, val warmups: List<Float>,
|
data class EntryRequest(val version: Uuid, val timestamp: Instant, val projectId: Uuid, val warmups: List<Float>,
|
||||||
val measurements: List<Float>, val unit: TimeUnit)
|
val measurements: List<Float>, val unit: TimeUnit, val input: String, val hardware: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LogoutRequest(val refresh: Uuid)
|
data class LogoutRequest(val refresh: Uuid)
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
package com.jaytux.phoebench.common
|
package com.jaytux.phoebench.common
|
||||||
|
|
||||||
import kotlinx.datetime.LocalDateTime
|
|
||||||
import kotlinx.datetime.TimeZone
|
|
||||||
import kotlinx.serialization.KSerializer
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
|
||||||
import kotlinx.serialization.encoding.Decoder
|
|
||||||
import kotlinx.serialization.encoding.Encoder
|
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlin.uuid.Uuid
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
@@ -47,14 +39,14 @@ data class HomeResponse(val username: String, val isAdmin: Boolean, val projectL
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ProjectResponse(val id: Uuid, val name: String, val owner: NamedID, val isPublic: Boolean, val isEditable: Boolean,
|
data class ProjectResponse(val id: Uuid, val name: String, val owner: NamedID, val isPublic: Boolean, val isEditable: Boolean,
|
||||||
val usedLabels: List<LabelResponse>, val entries: List<EntryResponse>)
|
val usedLabels: List<VersionResponse>, val entries: List<EntryResponse>)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LabelResponse(val id: Uuid, val name: String, val color: String)
|
data class VersionResponse(val id: Uuid, val name: String, val color: String, val meta: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EntryResponse(val id: Uuid, val labelId: Uuid, val timestamp: Instant, val warmups: List<Float>,
|
data class EntryResponse(val id: Uuid, val labelId: Uuid, val timestamp: Instant, val warmups: List<Float>,
|
||||||
val measurements: List<Float>, val unit: TimeUnit)
|
val measurements: List<Float>, val unit: TimeUnit, val input: String, val hardware: String)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HandshakeResponse(val version: String = ProtocolVersion.VERSION) {
|
data class HandshakeResponse(val version: String = ProtocolVersion.VERSION) {
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ object Routes {
|
|||||||
val delete = ApiRoute.deleteUuidNoRes("/project", Elevation.AUTH)
|
val delete = ApiRoute.deleteUuidNoRes("/project", Elevation.AUTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Label {
|
object Version {
|
||||||
val new = ApiRoute.post<LabelRequest, LabelResponse>("/label", Elevation.AUTH)
|
val new = ApiRoute.post<VersionRequest, VersionResponse>("/version", Elevation.AUTH)
|
||||||
val update = ApiRoute.patchUuidNoRes<PartialLabelRequest>("/label", Elevation.AUTH)
|
val update = ApiRoute.patchUuidNoRes<PartialVersionRequest>("/version", Elevation.AUTH)
|
||||||
val delete = ApiRoute.deleteUuidNoRes("/label", Elevation.AUTH)
|
val delete = ApiRoute.deleteUuidNoRes("/version", Elevation.AUTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Entry {
|
object Entry {
|
||||||
|
|||||||
@@ -129,9 +129,9 @@ fun Application.module() {
|
|||||||
patchAuth(Routes.Project.update, ProjectHandler::updateProject)
|
patchAuth(Routes.Project.update, ProjectHandler::updateProject)
|
||||||
deleteAuth(Routes.Project.delete, ProjectHandler::deleteProject)
|
deleteAuth(Routes.Project.delete, ProjectHandler::deleteProject)
|
||||||
|
|
||||||
postAuth(Routes.Label.new, ProjectHandler::createLabel)
|
postAuth(Routes.Version.new, ProjectHandler::createVersion)
|
||||||
patchAuth(Routes.Label.update, ProjectHandler::updateLabel)
|
patchAuth(Routes.Version.update, ProjectHandler::updateVersion)
|
||||||
deleteAuth(Routes.Label.delete, ProjectHandler::deleteLabel)
|
deleteAuth(Routes.Version.delete, ProjectHandler::deleteVersion)
|
||||||
|
|
||||||
postAuth(Routes.Entry.new, ProjectHandler::createEntry)
|
postAuth(Routes.Entry.new, ProjectHandler::createEntry)
|
||||||
deleteAuth(Routes.Entry.delete, ProjectHandler::deleteEntry)
|
deleteAuth(Routes.Entry.delete, ProjectHandler::deleteEntry)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.jaytux.phoebench.server.db
|
package com.jaytux.phoebench.server.db
|
||||||
|
|
||||||
import com.jaytux.phoebench.server.DotEnv
|
import com.jaytux.phoebench.server.DotEnv
|
||||||
import io.github.cdimascio.dotenv.Dotenv
|
|
||||||
import org.jetbrains.exposed.v1.jdbc.Database
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
@@ -16,9 +15,9 @@ object DB {
|
|||||||
)
|
)
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
SchemaUtils.create(Users, Invites, RefreshTokens, Projects, Labels, Entries)
|
SchemaUtils.create(Users, Invites, RefreshTokens, Projects, Versions, Entries)
|
||||||
|
|
||||||
val migration = MigrationUtils.statementsRequiredForDatabaseMigration(Users, Invites, RefreshTokens, Projects, Labels, Entries)
|
val migration = MigrationUtils.statementsRequiredForDatabaseMigration(Users, Invites, RefreshTokens, Projects, Versions, Entries)
|
||||||
try {
|
try {
|
||||||
migration.forEach {
|
migration.forEach {
|
||||||
exec(it)
|
exec(it)
|
||||||
|
|||||||
@@ -42,30 +42,33 @@ class Project(id: EntityID<Uuid>) : Entity<Uuid>(id) {
|
|||||||
var isPublic by Projects.isPublic
|
var isPublic by Projects.isPublic
|
||||||
|
|
||||||
var owner by User referencedOn Projects.ownerId
|
var owner by User referencedOn Projects.ownerId
|
||||||
val labels by Label referrersOn Labels.projectId
|
val labels by Version referrersOn Versions.projectId
|
||||||
val entries by Entry referrersOn Entries.projectId
|
val entries by Entry referrersOn Entries.projectId
|
||||||
}
|
}
|
||||||
|
|
||||||
class Label(id: EntityID<Uuid>) : Entity<Uuid>(id) {
|
class Version(id: EntityID<Uuid>) : Entity<Uuid>(id) {
|
||||||
companion object : EntityClass<Uuid, Label>(Labels)
|
companion object : EntityClass<Uuid, Version>(Versions)
|
||||||
|
|
||||||
var label by Labels.label
|
var label by Versions.label
|
||||||
var color by Labels.color
|
var color by Versions.color
|
||||||
var projectId by Labels.projectId
|
var meta by Versions.meta
|
||||||
|
var projectId by Versions.projectId
|
||||||
|
|
||||||
var project by Project referencedOn Labels.projectId
|
var project by Project referencedOn Versions.projectId
|
||||||
}
|
}
|
||||||
|
|
||||||
class Entry(id: EntityID<Uuid>) : Entity<Uuid>(id) {
|
class Entry(id: EntityID<Uuid>) : Entity<Uuid>(id) {
|
||||||
companion object : EntityClass<Uuid, Entry>(Entries)
|
companion object : EntityClass<Uuid, Entry>(Entries)
|
||||||
|
|
||||||
var projectId by Entries.projectId
|
var projectId by Entries.projectId
|
||||||
var labelId by Entries.labelId
|
var versionId by Entries.versionId
|
||||||
var timestamp by Entries.timestamp
|
var timestamp by Entries.timestamp
|
||||||
var warmups by Entries.warmups
|
var warmups by Entries.warmups
|
||||||
var measurements by Entries.measurements
|
var measurements by Entries.measurements
|
||||||
var unit by Entries.unit
|
var unit by Entries.unit
|
||||||
|
var input by Entries.input
|
||||||
|
var hardware by Entries.hardware
|
||||||
|
|
||||||
var project by Project referencedOn Entries.projectId
|
var project by Project referencedOn Entries.projectId
|
||||||
var label by Label referencedOn Entries.labelId
|
var version by Version referencedOn Entries.versionId
|
||||||
}
|
}
|
||||||
@@ -34,17 +34,20 @@ object Projects : UuidTable() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Labels : UuidTable() {
|
object Versions : UuidTable() {
|
||||||
val label = varchar("label", 255)
|
val label = varchar("label", 255)
|
||||||
val color = varchar("color", 7)
|
val color = varchar("color", 7)
|
||||||
|
val meta = text("meta", eagerLoading = true)
|
||||||
val projectId = reference("project_id", Projects, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
val projectId = reference("project_id", Projects, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||||
}
|
}
|
||||||
|
|
||||||
object Entries : UuidTable() {
|
object Entries : UuidTable() {
|
||||||
val projectId = reference("project_id", Projects, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
val projectId = reference("project_id", Projects, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||||
val labelId = reference("label", Labels, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
val versionId = reference("version", Versions, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE)
|
||||||
val timestamp = timestamp("timestamp").default(Clock.System.now())
|
val timestamp = timestamp("timestamp").default(Clock.System.now())
|
||||||
val warmups = list<Float>("warmups")
|
val warmups = list<Float>("warmups")
|
||||||
val measurements = list<Float>("measurements")
|
val measurements = list<Float>("measurements")
|
||||||
val unit = enumeration<TimeUnit>("unit")
|
val unit = enumeration<TimeUnit>("unit")
|
||||||
|
val input = varchar("input", 255)
|
||||||
|
val hardware = varchar("hardware", 255)
|
||||||
}
|
}
|
||||||
@@ -6,27 +6,24 @@ import com.jaytux.phoebench.common.EntryRequest
|
|||||||
import com.jaytux.phoebench.common.EntryResponse
|
import com.jaytux.phoebench.common.EntryResponse
|
||||||
import com.jaytux.phoebench.common.HomeEvent
|
import com.jaytux.phoebench.common.HomeEvent
|
||||||
import com.jaytux.phoebench.common.HomeResponse
|
import com.jaytux.phoebench.common.HomeResponse
|
||||||
import com.jaytux.phoebench.common.LabelRequest
|
import com.jaytux.phoebench.common.VersionRequest
|
||||||
import com.jaytux.phoebench.common.LabelResponse
|
import com.jaytux.phoebench.common.VersionResponse
|
||||||
import com.jaytux.phoebench.common.NamedID
|
import com.jaytux.phoebench.common.NamedID
|
||||||
import com.jaytux.phoebench.common.PartialEntryRequest
|
|
||||||
import com.jaytux.phoebench.common.PartialLabelRequest
|
|
||||||
import com.jaytux.phoebench.common.PartialProjectRequest
|
import com.jaytux.phoebench.common.PartialProjectRequest
|
||||||
|
import com.jaytux.phoebench.common.PartialVersionRequest
|
||||||
import com.jaytux.phoebench.common.ProjectEvent
|
import com.jaytux.phoebench.common.ProjectEvent
|
||||||
import com.jaytux.phoebench.common.ProjectRequest
|
import com.jaytux.phoebench.common.ProjectRequest
|
||||||
import com.jaytux.phoebench.common.ProjectResponse
|
import com.jaytux.phoebench.common.ProjectResponse
|
||||||
import com.jaytux.phoebench.server.Buses
|
import com.jaytux.phoebench.server.Buses
|
||||||
import com.jaytux.phoebench.server.SSEBus
|
|
||||||
import com.jaytux.phoebench.server.db.Entries
|
import com.jaytux.phoebench.server.db.Entries
|
||||||
import com.jaytux.phoebench.server.db.Entry
|
import com.jaytux.phoebench.server.db.Entry
|
||||||
import com.jaytux.phoebench.server.db.Label
|
import com.jaytux.phoebench.server.db.Version
|
||||||
import com.jaytux.phoebench.server.db.Labels
|
import com.jaytux.phoebench.server.db.Versions
|
||||||
import com.jaytux.phoebench.server.db.Project
|
import com.jaytux.phoebench.server.db.Project
|
||||||
import com.jaytux.phoebench.server.db.Projects
|
import com.jaytux.phoebench.server.db.Projects
|
||||||
import com.jaytux.phoebench.server.db.User
|
import com.jaytux.phoebench.server.db.User
|
||||||
import com.jaytux.phoebench.server.handlers.RouteError.Companion.success
|
import com.jaytux.phoebench.server.handlers.RouteError.Companion.success
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.exposed.v1.core.SortOrder
|
import org.jetbrains.exposed.v1.core.SortOrder
|
||||||
import org.jetbrains.exposed.v1.core.Transaction
|
import org.jetbrains.exposed.v1.core.Transaction
|
||||||
@@ -55,8 +52,8 @@ object ProjectHandler {
|
|||||||
context(trns: Transaction)
|
context(trns: Transaction)
|
||||||
private fun Project.toResponse(user: User) = ProjectResponse(
|
private fun Project.toResponse(user: User) = ProjectResponse(
|
||||||
id.value, name, NamedID(owner.username, owner.id.value), isPublic, isEditableBy(user),
|
id.value, name, NamedID(owner.username, owner.id.value), isPublic, isEditableBy(user),
|
||||||
labels.orderBy(Labels.label to SortOrder.ASC).map { LabelResponse(it.id.value, it.label, it.color) },
|
labels.orderBy(Versions.label to SortOrder.ASC).map { VersionResponse(it.id.value, it.label, it.color, it.meta) },
|
||||||
entries.orderBy(Entries.timestamp to SortOrder.ASC).map { EntryResponse(it.id.value, it.label.id.value, it.timestamp, it.warmups, it.measurements, it.unit) })
|
entries.orderBy(Entries.timestamp to SortOrder.ASC).map { EntryResponse(it.id.value, it.version.id.value, it.timestamp, it.warmups, it.measurements, it.unit, it.input, it.hardware) })
|
||||||
|
|
||||||
fun home(user: User, req: EmptyRequest) = transaction {
|
fun home(user: User, req: EmptyRequest) = transaction {
|
||||||
val own = user.projects.orderBy(Projects.name to SortOrder.ASC).map {
|
val own = user.projects.orderBy(Projects.name to SortOrder.ASC).map {
|
||||||
@@ -127,49 +124,51 @@ object ProjectHandler {
|
|||||||
success(EmptyResponse())
|
success(EmptyResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createLabel(user: User, req: LabelRequest) = transaction {
|
fun createVersion(user: User, req: VersionRequest) = transaction {
|
||||||
val proj = accessibleProject(user, req.projectId, true)
|
val proj = accessibleProject(user, req.projectId, true)
|
||||||
if(req.color.length != 7) throw RouteError("Expected Hex-RGB color (7 characters).", HttpStatusCode.BadRequest)
|
if(req.color.length != 7) throw RouteError("Expected Hex-RGB color (7 characters).", HttpStatusCode.BadRequest)
|
||||||
val lbl = Label.new {
|
val ver = Version.new {
|
||||||
label = req.name
|
label = req.name
|
||||||
color = req.color
|
color = req.color
|
||||||
project = proj
|
project = proj
|
||||||
|
meta = req.meta
|
||||||
}
|
}
|
||||||
|
|
||||||
val res = LabelResponse(lbl.id.value, lbl.label, lbl.color)
|
val res = VersionResponse(ver.id.value, ver.label, ver.color, ver.meta)
|
||||||
ServerScope.launch {
|
ServerScope.launch {
|
||||||
Buses.projectBus(req.projectId).sendAll(ProjectEvent.NewLabel(res))
|
Buses.projectBus(req.projectId).sendAll(ProjectEvent.NewVersion(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
success(res)
|
success(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLabel(user: User, req: Pair<Uuid, PartialLabelRequest>) = transaction {
|
fun updateVersion(user: User, req: Pair<Uuid, PartialVersionRequest>) = transaction {
|
||||||
val lbl = Label.findById(req.first) ?: throw RouteError("Invalid label ID.", HttpStatusCode.NotFound)
|
val ver = Version.findById(req.first) ?: throw RouteError("Invalid version ID.", HttpStatusCode.NotFound)
|
||||||
lbl.project.isAccessible(user, true)
|
ver.project.isAccessible(user, true)
|
||||||
val changes = req.second
|
val changes = req.second
|
||||||
changes.name?.let { lbl.label = it }
|
changes.name?.let { ver.label = it }
|
||||||
changes.color?.let {
|
changes.color?.let {
|
||||||
if(it.length != 7) throw RouteError("Expected Hex-RGB color (7 characters).", HttpStatusCode.BadRequest)
|
if(it.length != 7) throw RouteError("Expected Hex-RGB color (7 characters).", HttpStatusCode.BadRequest)
|
||||||
lbl.color = it
|
ver.color = it
|
||||||
}
|
}
|
||||||
|
changes.meta?.let { ver.meta = it }
|
||||||
|
|
||||||
ServerScope.launch {
|
ServerScope.launch {
|
||||||
Buses.projectBus(lbl.projectId.value).sendAll(ProjectEvent.LabelChanged(
|
Buses.projectBus(ver.projectId.value).sendAll(ProjectEvent.VersionChanged(
|
||||||
LabelResponse(lbl.id.value, lbl.label, lbl.color)
|
VersionResponse(ver.id.value, ver.label, ver.color, ver.meta)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
success(EmptyResponse())
|
success(EmptyResponse())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteLabel(user: User, req: Uuid) = transaction {
|
fun deleteVersion(user: User, req: Uuid) = transaction {
|
||||||
val lbl = Label.findById(req) ?: throw RouteError("Invalid label ID.", HttpStatusCode.NotFound)
|
val ver = Version.findById(req) ?: throw RouteError("Invalid version ID.", HttpStatusCode.NotFound)
|
||||||
lbl.project.isAccessible(user, true)
|
ver.project.isAccessible(user, true)
|
||||||
lbl.delete()
|
ver.delete()
|
||||||
|
|
||||||
ServerScope.launch {
|
ServerScope.launch {
|
||||||
Buses.projectBus(lbl.id.value).sendAll(ProjectEvent.LabelDeleted(req))
|
Buses.projectBus(ver.id.value).sendAll(ProjectEvent.VersionDeleted(req))
|
||||||
}
|
}
|
||||||
|
|
||||||
success(EmptyResponse())
|
success(EmptyResponse())
|
||||||
@@ -178,9 +177,9 @@ object ProjectHandler {
|
|||||||
fun createEntry(user: User, req: EntryRequest) = transaction {
|
fun createEntry(user: User, req: EntryRequest) = transaction {
|
||||||
val proj = accessibleProject(user, req.projectId, true)
|
val proj = accessibleProject(user, req.projectId, true)
|
||||||
val entry = Entry.new {
|
val entry = Entry.new {
|
||||||
label = when(val l = Label.findById(req.label)) {
|
version = when(val l = Version.findById(req.version)) {
|
||||||
null -> throw RouteError("Invalid label ID.", HttpStatusCode.NotFound)
|
null -> throw RouteError("Invalid version ID.", HttpStatusCode.NotFound)
|
||||||
is Label if l.projectId.value != proj.id.value -> throw RouteError("Label is attached to a different project.", HttpStatusCode.Conflict)
|
is Version if l.projectId.value != proj.id.value -> throw RouteError("Version is attached to a different project.", HttpStatusCode.Conflict)
|
||||||
else -> l
|
else -> l
|
||||||
}
|
}
|
||||||
project = proj
|
project = proj
|
||||||
@@ -188,9 +187,11 @@ object ProjectHandler {
|
|||||||
timestamp = req.timestamp
|
timestamp = req.timestamp
|
||||||
warmups = req.warmups
|
warmups = req.warmups
|
||||||
unit = req.unit
|
unit = req.unit
|
||||||
|
input = req.input
|
||||||
|
hardware = req.hardware
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = EntryResponse(entry.id.value, entry.label.id.value, entry.timestamp, entry.warmups, entry.measurements, entry.unit)
|
val response = EntryResponse(entry.id.value, entry.version.id.value, entry.timestamp, entry.warmups, entry.measurements, entry.unit, entry.input, entry.hardware)
|
||||||
ServerScope.launch {
|
ServerScope.launch {
|
||||||
Buses.projectBus(proj.id.value).sendAll(ProjectEvent.NewEntry(response))
|
Buses.projectBus(proj.id.value).sendAll(ProjectEvent.NewEntry(response))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user