From a90b2ebecaaa75075b62c0b81758f2a0e5912db4 Mon Sep 17 00:00:00 2001 From: jay-tux Date: Sun, 9 Aug 2026 20:18:11 +0200 Subject: [PATCH] Rename Label->Version, add version meta field --- build.gradle.kts | 2 +- .../com/jaytux/phoebench/clients/cli/CLI.kt | 19 ++-- .../phoebench/clients/cli/ProjectHandlers.kt | 39 ++++---- .../phoebench/clients/data/IProjectRepo.kt | 22 ++--- .../phoebench/clients/data/ProjectVM.kt | 42 ++++----- .../phoebench/clients/ui/ProjectView.kt | 91 ++++++++++--------- .../com/jaytux/phoebench/common/Events.kt | 6 +- .../com/jaytux/phoebench/common/Requests.kt | 5 +- .../com/jaytux/phoebench/common/Responses.kt | 12 +-- .../com/jaytux/phoebench/common/Routes.kt | 8 +- .../com/jaytux/phoebench/server/Main.kt | 6 +- .../com/jaytux/phoebench/server/db/DB.kt | 5 +- .../jaytux/phoebench/server/db/Entities.kt | 19 ++-- .../com/jaytux/phoebench/server/db/Tables.kt | 9 +- .../server/handlers/ProjectHandler.kt | 59 ++++++------ 15 files changed, 168 insertions(+), 176 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b8d1f31..9174dce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,4 +12,4 @@ repositories { mavenCentral() } -version = "0.1.0-dev" \ No newline at end of file +version = "0.2.0-dev" \ No newline at end of file diff --git a/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/CLI.kt b/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/CLI.kt index 20a1f76..50f765b 100644 --- a/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/CLI.kt +++ b/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/CLI.kt @@ -98,7 +98,7 @@ object CLI { sealed interface IProjectIdentification sealed interface ILabelIdentification 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 sealed interface IData { @@ -168,22 +168,23 @@ object CLI { } @Suppress("unused") - class AddLabel : CliktCommand(name = "add-label") { + class AddVersion : CliktCommand(name = "add-version") { val parent by requireObject() - val name by option("--name", help = "Set the label's name (*)") - val color by option("--color", help = "Set the label's color (*)").check("Color must be specified in RGB-hex-format (#ABCDEF)") { + val name by option("--name", help = "Set the version's name (*)") + 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" } } + 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") class AddData : CliktCommand(name = "add-data") { val parent by requireObject() - val label by mutuallyExclusiveOptions( - option("--label-id", help = "Set the label by UUID.").convert { ID(Uuid.parse(it)) }, - option("--label", help = "Set the label by name.").convert { LabelName(it) } + val version by mutuallyExclusiveOptions( + option("--version-id", help = "Set the version by UUID.").convert { ID(Uuid.parse(it)) }, + option("--version", help = "Set the version by name.").convert { VersionName(it) } ).single() val warmup by mutuallyExclusiveOptions>( option("--warmup", help = "Set warmup data directly.").float().split(",").transformAll { DirectData(it.flatten()) }, @@ -204,7 +205,7 @@ object CLI { 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) } } } diff --git a/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/ProjectHandlers.kt b/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/ProjectHandlers.kt index 1cb91e5..5006adc 100644 --- a/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/ProjectHandlers.kt +++ b/clients/cli/src/main/kotlin/com/jaytux/phoebench/clients/cli/ProjectHandlers.kt @@ -1,16 +1,11 @@ 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.common.ApiRoute import com.jaytux.phoebench.common.Either import com.jaytux.phoebench.common.EmptyRequest import com.jaytux.phoebench.common.EntryRequest import com.jaytux.phoebench.common.ErrorResponse -import com.jaytux.phoebench.common.LabelRequest -import com.jaytux.phoebench.common.LabelResponse +import com.jaytux.phoebench.common.VersionRequest import com.jaytux.phoebench.common.ProjectRequest import com.jaytux.phoebench.common.Routes 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.map 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.exists 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 useName = name.maybePrompt("label name") { it } val useColor = color.maybePrompt("color") { @@ -111,10 +103,11 @@ object ProjectHandlers { exitProcess(-1) } } + val useVersion = version.maybePrompt("version") { it } tryAuthenticated { id.toId().bind { projectId -> - Client.callRoute(Routes.Label.new, LabelRequest(useName, useColor, projectId)) + Client.callRoute(Routes.Version.new, VersionRequest(useName, useColor, useVersion, projectId)) } }.fold({ System.err.println("Could not create label: ${it.msg}") @@ -139,11 +132,11 @@ object ProjectHandlers { label: CLI.Commands.Project.ILabelIdentification?, warmup: CLI.Commands.Project.IData?, measurement: CLI.Commands.Project.IData?, - unit: TimeUnit? + unit: TimeUnit?, input: String?, hardware: String? ) { val projectId = ensureProjectIdentification(project) - val labelId = label.maybePrompt("label name") { - CLI.Commands.Project.LabelName(it) + val versionId = label.maybePrompt("version name") { + CLI.Commands.Project.VersionName(it) } val warmupData = warmup.ensure("warmup").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 { projectId.toId().bind { pId -> - when(labelId) { - is CLI.Commands.Project.ID -> labelId.id.value() - is CLI.Commands.Project.LabelName -> Client.callRoute(Routes.Project.get, pId).bind { - it.usedLabels.firstOrNull { l -> l.name == labelId.name }?.id?.value() - ?: ErrorResponse("Label ${labelId.name} does not exist in this project.").error() + when(versionId) { + is CLI.Commands.Project.ID -> versionId.id.value() + is CLI.Commands.Project.VersionName -> Client.callRoute(Routes.Project.get, pId).bind { + it.usedLabels.firstOrNull { l -> l.name == versionId.name }?.id?.value() + ?: ErrorResponse("Label ${versionId.name} does not exist in this project.").error() } }.map { pId to it } - }.bind { (pId, lId) -> + }.bind { (pId, vId) -> Client.callRoute(Routes.Entry.new, EntryRequest( - lId, Clock.System.now(), pId, - warmupData, measureData, timeUnit + vId, Clock.System.now(), pId, + warmupData, measureData, timeUnit, useInput, useHardware )) } } diff --git a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/IProjectRepo.kt b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/IProjectRepo.kt index e576aa0..b85a427 100644 --- a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/IProjectRepo.kt +++ b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/IProjectRepo.kt @@ -8,15 +8,13 @@ import com.jaytux.phoebench.common.Either import com.jaytux.phoebench.common.EntryRequest import com.jaytux.phoebench.common.EntryResponse import com.jaytux.phoebench.common.ErrorResponse -import com.jaytux.phoebench.common.LabelRequest -import com.jaytux.phoebench.common.LabelResponse -import com.jaytux.phoebench.common.PartialEntryRequest -import com.jaytux.phoebench.common.PartialLabelRequest +import com.jaytux.phoebench.common.VersionRequest +import com.jaytux.phoebench.common.VersionResponse import com.jaytux.phoebench.common.PartialProjectRequest +import com.jaytux.phoebench.common.PartialVersionRequest import com.jaytux.phoebench.common.ProjectResponse import com.jaytux.phoebench.common.Routes import com.jaytux.phoebench.common.TimeUnit -import kotlinx.datetime.TimeZone import kotlin.time.Instant import kotlin.uuid.Uuid @@ -25,8 +23,8 @@ interface IProjectRepo { suspend fun update(name: String? = null, isPublic: Boolean? = null): Either suspend fun delete(): Either - suspend fun newLabel(name: String, color: Color): Either - suspend fun updateLabel(id: Uuid, name: String? = null, color: Color? = null): Either + suspend fun newVersion(name: String, color: Color, meta: String): Either + suspend fun updateVersion(id: Uuid, name: String? = null, color: Color? = null, meta: String? = null): Either suspend fun deleteLabel(id: Uuid): Either suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List, measurements: List, @@ -44,14 +42,14 @@ interface IProjectRepo { override suspend fun delete(): Either = _client.callRoute(Routes.Project.delete, _projectId).ignoreValue() - override suspend fun newLabel(name: String, color: Color): Either = - _client.callRoute(Routes.Label.new, LabelRequest(name, color.hexString(), _projectId)) + override suspend fun newVersion(name: String, color: Color, meta: String): Either = + _client.callRoute(Routes.Version.new, VersionRequest(name, color.hexString(), meta, _projectId)) - override suspend fun updateLabel(id: Uuid, name: String?, color: Color?): Either = - _client.callRoute(Routes.Label.update, id to PartialLabelRequest(name, color?.hexString())).ignoreValue() + override suspend fun updateVersion(id: Uuid, name: String?, color: Color?, meta: String?): Either = + _client.callRoute(Routes.Version.update, id to PartialVersionRequest(name, color?.hexString(), meta)).ignoreValue() override suspend fun deleteLabel(id: Uuid): Either = - _client.callRoute(Routes.Label.delete, id).ignoreValue() + _client.callRoute(Routes.Version.delete, id).ignoreValue() override suspend fun newEntry(label: Uuid, timestamp: Instant, warmups: List, measurements: List, unit: TimeUnit, input: String, hardware: String diff --git a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt index fcdd27b..3fbef01 100644 --- a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt +++ b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/data/ProjectVM.kt @@ -11,13 +11,11 @@ import com.jaytux.phoebench.clients.SnackProvider.Companion.snackOr import com.jaytux.phoebench.clients.hexString import com.jaytux.phoebench.clients.immutable import com.jaytux.phoebench.clients.insort -import com.jaytux.phoebench.clients.systemTz import com.jaytux.phoebench.clients.withScope 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.TimeUnit -import com.jaytux.phoebench.common.foldSuspend import kotlinx.coroutines.Job import kotlin.time.Clock import kotlin.time.Instant @@ -32,7 +30,7 @@ class ProjectVM( private val _sseRepo: ISSERepo = ISSERepo.default(_client), private val _forceBack: () -> Unit ) : 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 { private val _snack by lazy { SnackProvider.get() } private val _errColor = Color(red = 252, green = 20, blue = 182) @@ -50,17 +48,17 @@ class ProjectVM( 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), "", _errColor.hexString(), _errColor) + val invalid = Version(Uuid.fromLongs(0L, 0L), "", _errColor.hexString(), "", _errColor) } } - data class Entry(val id: Uuid, val label: Label, val timeStamp: Instant, val warmups: List, + data class Entry(val id: Uuid, val label: Version, val timeStamp: Instant, val warmups: List, val measurements: List, val nativeUnit: TimeUnit, val input: String, val hardware: String) { companion object { - fun fromResponse(it: EntryResponse, map: Map) = Entry( - it.id, map[it.labelId] ?: Label.invalid, it.timestamp, + fun fromResponse(it: EntryResponse, map: Map) = Entry( + it.id, map[it.labelId] ?: Version.invalid, it.timestamp, it.warmups, it.measurements, it.unit, it.input, it.hardware ) } @@ -70,7 +68,7 @@ class ProjectVM( private val _owner = mutableStateOf(null) private val _public = mutableStateOf(false) private val _editable = mutableStateOf(false) - private val _labels = mutableStateOf(mapOf()) + private val _labels = mutableStateOf(mapOf()) private val _entries = mutableStateOf(listOf()) private val _inputs = mutableStateOf(setOf()) private val _hardware = mutableStateOf(setOf()) @@ -79,7 +77,7 @@ class ProjectVM( val owner = _owner.immutable() val public = _public.immutable() val editable = _editable.immutable() - val labels = _labels.immutable() + val versions = _labels.immutable() val entries = _entries.immutable() val inputs = _inputs.immutable() val hardware = _hardware.immutable() @@ -104,7 +102,7 @@ class ProjectVM( _owner.value = it.owner.name _public.value = it.isPublic _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) } val localInputs = mutableSetOf() val localHardware = mutableSetOf() _entries.value = it.entries.map { e -> @@ -122,14 +120,14 @@ class ProjectVM( when(event) { ProjectEvent.Deleted -> back() 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.LabelDeleted -> _labels.value = _labels.value.filter { it.value.id != event.id } + is ProjectEvent.VersionChanged -> _labels.value += (event.label.id to Version.fromResponse(event.label)) + 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) _inputs.value += event.entry.input _hardware.value += event.entry.hardware } - is ProjectEvent.NewLabel -> _labels.value += (event.label.id to Label.fromResponse(event.label)) + is ProjectEvent.NewVersion -> _labels.value += (event.label.id to Version.fromResponse(event.label)) is ProjectEvent.Updated -> { _name.value = event.changes.name _owner.value = event.changes.owner.name @@ -157,21 +155,21 @@ class ProjectVM( _forceBack() } - fun mkLabel(name: String, color: Color) { + fun mkLabel(name: String, color: Color, meta: String) { withScope { - _repo.newLabel(name, color).snackOr { + _repo.newVersion(name, color, meta).snackOr { if(_job == null) - _labels.value += it.id to Label(it.id, it.name, it.color) + _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 { val old = _labels.value[id] ?: return@withScope - _repo.updateLabel(id, name, color).snackOr { + _repo.updateVersion(id, name, color).snackOr { if(_job == null) - _labels.value += id to Label(id, name ?: old.name, color?.hexString() ?: old.colorStr, color ?: old.uiColor) + _labels.value += id to Version(id, name ?: old.name, color?.hexString() ?: old.colorStr, meta ?: old.meta, color ?: old.uiColor) } } } @@ -185,7 +183,7 @@ class ProjectVM( } } - fun mkEntry(label: Label, warmups: List, measurements: List, unit: TimeUnit, input: String, hardware: String) { + fun mkEntry(label: Version, warmups: List, measurements: List, unit: TimeUnit, input: String, hardware: String) { withScope { _repo.newEntry(label.id, Clock.System.now(), warmups, measurements, unit, input, hardware).snackOr { if(_job == null) { diff --git a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt index 2883329..c583e4c 100644 --- a/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt +++ b/clients/compose/src/commonMain/kotlin/com/jaytux/phoebench/clients/ui/ProjectView.kt @@ -20,14 +20,11 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.toLowerCase import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.lifecycle.viewmodel.compose.viewModel import com.composables.icons.lucide.* -import com.jaytux.phoebench.clients.darken import com.jaytux.phoebench.clients.data.ProjectVM import com.jaytux.phoebench.clients.data.mutableStateSetFrom import com.jaytux.phoebench.clients.data.mutableStateSetOf @@ -52,8 +49,6 @@ import io.github.koalaplot.core.xygraph.AxisContent import io.github.koalaplot.core.xygraph.DefaultPoint import io.github.koalaplot.core.xygraph.XYGraph 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.rememberFloatLinearAxisModel import io.github.koalaplot.core.xygraph.rememberGridStyle @@ -70,7 +65,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) { val owner by vm.owner val public by vm.public val editable by vm.editable - val labels by vm.labels + val labels by vm.versions var editing by remember { mutableStateOf(false) } var deleting by remember { mutableStateOf(false) } @@ -152,8 +147,8 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) { vm.delete() } - if(addingLabel) AddLabelDialog({ addingLabel = false; addOpen = false }) { name, color -> - vm.mkLabel(name, color) + if(addingLabel) AddVersionDialog({ addingLabel = false; addOpen = false }) { name, color, meta -> + vm.mkLabel(name, color, meta) addOpen = false } @@ -162,7 +157,7 @@ fun ProjectView(id: Uuid, forceBack: () -> Unit) { if(addingData) AddEntryDialog(labels, usedInputs, usedHardware, 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, input, hardware -> vm.mkEntry(label, warmups, measurements, unit, input, hardware) addOpen = false @@ -212,29 +207,32 @@ fun ConfirmDeleteProjectDialog(name: String, onCancel: () -> Unit, onDelete: () } @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) { var name by remember { mutableStateOf("") } var color by remember { mutableStateOf(randomColor()) } + var meta by remember { mutableStateOf("") } Surface(Modifier.padding(15.dp).widthIn(400.dp), shape = MaterialTheme.shapes.medium) { 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") }) Spacer(Modifier.height(10.dp)) KolorPicker(color, { color = it }, alphaTrackVisible = false, modifier = Modifier.aspectRatio(1f)) Spacer(Modifier.height(10.dp)) + OutlinedTextField(meta, { meta = it }, Modifier.fillMaxWidth(), label = { Text("Additional information") }) + Spacer(Modifier.height(10.dp)) CancelConfirmXRow(onCancel, { - onAdd(name.trim(), color) - }, confirmText = "Create", canConfirm = name.trim().isNotBlank()) + onAdd(name.trim(), color, meta.trim()) + }, confirmText = "Create", canConfirm = name.trim().isNotBlank() && meta.trim().isNotBlank()) } } } } @Composable -fun QuickLabel(lbl: ProjectVM.Label, hollow: Boolean = false) { +fun QuickVersion(lbl: ProjectVM.Version, hollow: Boolean = false) { Row(Modifier.height(IntrinsicSize.Min).padding(vertical = 3.dp), verticalAlignment = Alignment.Bottom) { Box(Modifier.fillMaxHeight().aspectRatio(1f).background(lbl.uiColor)) { if(hollow) { @@ -242,20 +240,29 @@ fun QuickLabel(lbl: ProjectVM.Label, hollow: Boolean = false) { } } Spacer(Modifier.width(15.dp)) - Text(lbl.name) - Text(" ${lbl.colorStr}", style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current.copy(alpha = 0.75f)) + Column { + 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 fun AddEntryDialog( - labels: Map, usedInputs: Set, usedHardware: Set, - onCancel: () -> Unit, onAddLbl: (name: String, color: Color) -> Unit, - onAdd: (label: ProjectVM.Label, warmups: List, measurements: List, unit: TimeUnit, input: String, hardware: String) -> Unit + labels: Map, usedInputs: Set, usedHardware: Set, + onCancel: () -> Unit, onAddLbl: (name: String, color: Color, meta: String) -> Unit, + onAdd: (label: ProjectVM.Version, warmups: List, measurements: List, unit: TimeUnit, input: String, hardware: String) -> Unit ) { Dialog(onDismissRequest = onCancel) { - var label by remember { mutableStateOf(null) } + var label by remember { mutableStateOf(null) } var warmups by remember { mutableStateOf("") } var measurements by remember { mutableStateOf("") } var unit by remember { mutableStateOf(TimeUnit.SECONDS) } @@ -295,12 +302,12 @@ fun AddEntryDialog( tonalElevation = 2.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 }) { labels.values.forEach { lbl -> - DropdownMenuItem({ QuickLabel(lbl) }, { label = lbl; dropDownOpen = false }) + DropdownMenuItem({ QuickVersion(lbl) }, { label = lbl; dropDownOpen = false }) } HorizontalDivider(Modifier.height(1.dp)) @@ -378,14 +385,14 @@ fun AddEntryDialog( } } - if(addingLabel) AddLabelDialog({ addingLabel = false }, onAddLbl) + if(addingLabel) AddVersionDialog({ addingLabel = false }, onAddLbl) } } @Composable fun ProjectPlotArea(vm: ProjectVM){ val entries by vm.entries - val labels by vm.labels + val versions by vm.versions val editable by vm.editable var displayWarmups by remember { mutableStateOf(false) } @@ -400,8 +407,8 @@ fun ProjectPlotArea(vm: ProjectVM){ var timeFilter by remember { mutableStateOf(0f..1f) } var timeFilterString by remember { mutableStateOf("") } - val labelFilter = remember(labels) { mutableStateSetFrom(labels.values) } - val labelFilterKey by labelFilter.revision + val versionFilter = remember(versions) { mutableStateSetFrom(versions.values) } + val labelVersionFilter by versionFilter.revision val allInputs by vm.inputs val allHardware by vm.hardware @@ -410,7 +417,7 @@ fun ProjectPlotArea(vm: ProjectVM){ var filterInput by remember { mutableStateOf(null) } var filterHardware by remember { mutableStateOf(null) } - LaunchedEffect(entries, displayWarmups, currentUnit, timeFilter, timeMin, timeMax, labelFilterKey, filterInput, filterHardware) { + LaunchedEffect(entries, displayWarmups, currentUnit, timeFilter, timeMin, timeMax, labelVersionFilter, filterInput, filterHardware) { var maxX = 0f var minY = 0f var maxY = 0f @@ -421,7 +428,7 @@ fun ProjectPlotArea(vm: ProjectVM){ renderableEntries = entries.mapNotNull { entry -> val use = if(displayWarmups) entry.warmups else entry.measurements - if(entry.label !in labelFilter) return@mapNotNull null + 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 @@ -547,32 +554,32 @@ fun ProjectPlotArea(vm: ProjectVM){ Row(Modifier.weight(0.33f).fillMaxHeight()) { Column(Modifier.weight(0.5f)) { - Text("Labels", style = MaterialTheme.typography.headlineSmall) - Text("Click a label to toggle its visibility.", fontStyle = FontStyle.Italic, + 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)) - if(labels.isEmpty()) { + if(versions.isEmpty()) { 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 { 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 { - if(labelFilter.size == labels.size) { - labelFilter.clear() - labelFilter.add(lbl) + if(versionFilter.size == versions.size) { + versionFilter.clear() + versionFilter.add(lbl) } else { - labelFilter.toggle(lbl) - if(labelFilter.isEmpty()) labelFilter.addAll(labels.values) + versionFilter.toggle(lbl) + if(versionFilter.isEmpty()) versionFilter.addAll(versions.values) } }) { Box { - QuickLabel(lbl, lbl !in labelFilter) + QuickVersion(lbl, lbl !in versionFilter) - if(lbl !in labelFilter) { + if(lbl !in versionFilter) { Box(Modifier.matchParentSize()) { HorizontalDivider( Modifier.fillMaxWidth().align(Alignment.Center), @@ -607,7 +614,9 @@ fun ProjectPlotArea(vm: ProjectVM){ Text(it, Modifier.padding(start = 5.dp, top = 2.dp, bottom = 2.dp), color = LocalContentColor.current.copy(0.66f)) } - item { Spacer(Modifier.height(15.dp)) } + 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 { diff --git a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Events.kt b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Events.kt index fa65b98..76911ad 100644 --- a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Events.kt +++ b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Events.kt @@ -43,13 +43,13 @@ sealed class ProjectEvent { data class Updated(val changes: HomeResponse.ProjectSummary) : ProjectEvent() @Serializable - data class NewLabel(val label: LabelResponse) : ProjectEvent() + data class NewVersion(val label: VersionResponse) : ProjectEvent() @Serializable - data class LabelChanged(val label: LabelResponse) : ProjectEvent() + data class VersionChanged(val label: VersionResponse) : ProjectEvent() @Serializable - data class LabelDeleted(val id: Uuid) : ProjectEvent() + data class VersionDeleted(val id: Uuid) : ProjectEvent() @Serializable data class NewEntry(val entry: EntryResponse) : ProjectEvent() diff --git a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Requests.kt b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Requests.kt index 5d15eea..5bba870 100644 --- a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Requests.kt +++ b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Requests.kt @@ -1,6 +1,5 @@ package com.jaytux.phoebench.common -import kotlinx.datetime.LocalDateTime import kotlinx.serialization.Serializable import kotlin.time.Instant import kotlin.uuid.Uuid @@ -23,10 +22,10 @@ data class RefreshRequest(val refreshToken: Uuid) data class ProjectRequest(val name: String, var isPublic: Boolean) @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 -data class EntryRequest(val label: Uuid, val timestamp: Instant, val projectId: Uuid, val warmups: List, +data class EntryRequest(val version: Uuid, val timestamp: Instant, val projectId: Uuid, val warmups: List, val measurements: List, val unit: TimeUnit, val input: String, val hardware: String) @Serializable diff --git a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Responses.kt b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Responses.kt index abf1b73..ba5ec47 100644 --- a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Responses.kt +++ b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Responses.kt @@ -1,14 +1,6 @@ package com.jaytux.phoebench.common -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.serialization.KSerializer 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.uuid.Uuid @@ -47,10 +39,10 @@ data class HomeResponse(val username: String, val isAdmin: Boolean, val projectL @Serializable data class ProjectResponse(val id: Uuid, val name: String, val owner: NamedID, val isPublic: Boolean, val isEditable: Boolean, - val usedLabels: List, val entries: List) + val usedLabels: List, val entries: List) @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 data class EntryResponse(val id: Uuid, val labelId: Uuid, val timestamp: Instant, val warmups: List, diff --git a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Routes.kt b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Routes.kt index 41ebaee..c94e8b9 100644 --- a/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Routes.kt +++ b/common/src/commonMain/kotlin/com/jaytux/phoebench/common/Routes.kt @@ -30,10 +30,10 @@ object Routes { val delete = ApiRoute.deleteUuidNoRes("/project", Elevation.AUTH) } - object Label { - val new = ApiRoute.post("/label", Elevation.AUTH) - val update = ApiRoute.patchUuidNoRes("/label", Elevation.AUTH) - val delete = ApiRoute.deleteUuidNoRes("/label", Elevation.AUTH) + object Version { + val new = ApiRoute.post("/version", Elevation.AUTH) + val update = ApiRoute.patchUuidNoRes("/version", Elevation.AUTH) + val delete = ApiRoute.deleteUuidNoRes("/version", Elevation.AUTH) } object Entry { diff --git a/server/src/main/kotlin/com/jaytux/phoebench/server/Main.kt b/server/src/main/kotlin/com/jaytux/phoebench/server/Main.kt index b901dc6..fc97236 100644 --- a/server/src/main/kotlin/com/jaytux/phoebench/server/Main.kt +++ b/server/src/main/kotlin/com/jaytux/phoebench/server/Main.kt @@ -129,9 +129,9 @@ fun Application.module() { patchAuth(Routes.Project.update, ProjectHandler::updateProject) deleteAuth(Routes.Project.delete, ProjectHandler::deleteProject) - postAuth(Routes.Label.new, ProjectHandler::createLabel) - patchAuth(Routes.Label.update, ProjectHandler::updateLabel) - deleteAuth(Routes.Label.delete, ProjectHandler::deleteLabel) + postAuth(Routes.Version.new, ProjectHandler::createVersion) + patchAuth(Routes.Version.update, ProjectHandler::updateVersion) + deleteAuth(Routes.Version.delete, ProjectHandler::deleteVersion) postAuth(Routes.Entry.new, ProjectHandler::createEntry) deleteAuth(Routes.Entry.delete, ProjectHandler::deleteEntry) diff --git a/server/src/main/kotlin/com/jaytux/phoebench/server/db/DB.kt b/server/src/main/kotlin/com/jaytux/phoebench/server/db/DB.kt index 05992cc..50639ee 100644 --- a/server/src/main/kotlin/com/jaytux/phoebench/server/db/DB.kt +++ b/server/src/main/kotlin/com/jaytux/phoebench/server/db/DB.kt @@ -1,7 +1,6 @@ package com.jaytux.phoebench.server.db 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.SchemaUtils import org.jetbrains.exposed.v1.jdbc.transactions.transaction @@ -16,9 +15,9 @@ object DB { ) 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 { migration.forEach { exec(it) diff --git a/server/src/main/kotlin/com/jaytux/phoebench/server/db/Entities.kt b/server/src/main/kotlin/com/jaytux/phoebench/server/db/Entities.kt index 15d0e00..e99b2e4 100644 --- a/server/src/main/kotlin/com/jaytux/phoebench/server/db/Entities.kt +++ b/server/src/main/kotlin/com/jaytux/phoebench/server/db/Entities.kt @@ -42,25 +42,26 @@ class Project(id: EntityID) : Entity(id) { var isPublic by Projects.isPublic 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 } -class Label(id: EntityID) : Entity(id) { - companion object : EntityClass(Labels) +class Version(id: EntityID) : Entity(id) { + companion object : EntityClass(Versions) - var label by Labels.label - var color by Labels.color - var projectId by Labels.projectId + var label by Versions.label + var color by Versions.color + 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) : Entity(id) { companion object : EntityClass(Entries) var projectId by Entries.projectId - var labelId by Entries.labelId + var versionId by Entries.versionId var timestamp by Entries.timestamp var warmups by Entries.warmups var measurements by Entries.measurements @@ -69,5 +70,5 @@ class Entry(id: EntityID) : Entity(id) { var hardware by Entries.hardware var project by Project referencedOn Entries.projectId - var label by Label referencedOn Entries.labelId + var version by Version referencedOn Entries.versionId } \ No newline at end of file diff --git a/server/src/main/kotlin/com/jaytux/phoebench/server/db/Tables.kt b/server/src/main/kotlin/com/jaytux/phoebench/server/db/Tables.kt index 85966b7..d4b9366 100644 --- a/server/src/main/kotlin/com/jaytux/phoebench/server/db/Tables.kt +++ b/server/src/main/kotlin/com/jaytux/phoebench/server/db/Tables.kt @@ -34,19 +34,20 @@ object Projects : UuidTable() { } } -object Labels : UuidTable() { +object Versions : UuidTable() { val label = varchar("label", 255) val color = varchar("color", 7) + val meta = text("meta", eagerLoading = true) val projectId = reference("project_id", Projects, onDelete = ReferenceOption.CASCADE, onUpdate = ReferenceOption.CASCADE) } object Entries : UuidTable() { 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 warmups = list("warmups") val measurements = list("measurements") val unit = enumeration("unit") - val input = varchar("input", 255).default("") - val hardware = varchar("hardware", 255).default("") + val input = varchar("input", 255) + val hardware = varchar("hardware", 255) } \ No newline at end of file diff --git a/server/src/main/kotlin/com/jaytux/phoebench/server/handlers/ProjectHandler.kt b/server/src/main/kotlin/com/jaytux/phoebench/server/handlers/ProjectHandler.kt index bebb3bf..191d0d3 100644 --- a/server/src/main/kotlin/com/jaytux/phoebench/server/handlers/ProjectHandler.kt +++ b/server/src/main/kotlin/com/jaytux/phoebench/server/handlers/ProjectHandler.kt @@ -6,27 +6,24 @@ import com.jaytux.phoebench.common.EntryRequest import com.jaytux.phoebench.common.EntryResponse import com.jaytux.phoebench.common.HomeEvent import com.jaytux.phoebench.common.HomeResponse -import com.jaytux.phoebench.common.LabelRequest -import com.jaytux.phoebench.common.LabelResponse +import com.jaytux.phoebench.common.VersionRequest +import com.jaytux.phoebench.common.VersionResponse 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.PartialVersionRequest import com.jaytux.phoebench.common.ProjectEvent import com.jaytux.phoebench.common.ProjectRequest import com.jaytux.phoebench.common.ProjectResponse 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.Entry -import com.jaytux.phoebench.server.db.Label -import com.jaytux.phoebench.server.db.Labels +import com.jaytux.phoebench.server.db.Version +import com.jaytux.phoebench.server.db.Versions import com.jaytux.phoebench.server.db.Project import com.jaytux.phoebench.server.db.Projects import com.jaytux.phoebench.server.db.User import com.jaytux.phoebench.server.handlers.RouteError.Companion.success import io.ktor.http.HttpStatusCode -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.exposed.v1.core.SortOrder import org.jetbrains.exposed.v1.core.Transaction @@ -55,8 +52,8 @@ object ProjectHandler { context(trns: Transaction) private fun Project.toResponse(user: User) = ProjectResponse( 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) }, - entries.orderBy(Entries.timestamp to SortOrder.ASC).map { EntryResponse(it.id.value, it.label.id.value, it.timestamp, it.warmups, it.measurements, it.unit, it.input, it.hardware) }) + 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.version.id.value, it.timestamp, it.warmups, it.measurements, it.unit, it.input, it.hardware) }) fun home(user: User, req: EmptyRequest) = transaction { val own = user.projects.orderBy(Projects.name to SortOrder.ASC).map { @@ -127,49 +124,51 @@ object ProjectHandler { success(EmptyResponse()) } - fun createLabel(user: User, req: LabelRequest) = transaction { + fun createVersion(user: User, req: VersionRequest) = transaction { val proj = accessibleProject(user, req.projectId, true) 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 color = req.color 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 { - Buses.projectBus(req.projectId).sendAll(ProjectEvent.NewLabel(res)) + Buses.projectBus(req.projectId).sendAll(ProjectEvent.NewVersion(res)) } success(res) } - fun updateLabel(user: User, req: Pair) = transaction { - val lbl = Label.findById(req.first) ?: throw RouteError("Invalid label ID.", HttpStatusCode.NotFound) - lbl.project.isAccessible(user, true) + fun updateVersion(user: User, req: Pair) = transaction { + val ver = Version.findById(req.first) ?: throw RouteError("Invalid version ID.", HttpStatusCode.NotFound) + ver.project.isAccessible(user, true) val changes = req.second - changes.name?.let { lbl.label = it } + changes.name?.let { ver.label = it } changes.color?.let { 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 { - Buses.projectBus(lbl.projectId.value).sendAll(ProjectEvent.LabelChanged( - LabelResponse(lbl.id.value, lbl.label, lbl.color) + Buses.projectBus(ver.projectId.value).sendAll(ProjectEvent.VersionChanged( + VersionResponse(ver.id.value, ver.label, ver.color, ver.meta) )) } success(EmptyResponse()) } - fun deleteLabel(user: User, req: Uuid) = transaction { - val lbl = Label.findById(req) ?: throw RouteError("Invalid label ID.", HttpStatusCode.NotFound) - lbl.project.isAccessible(user, true) - lbl.delete() + fun deleteVersion(user: User, req: Uuid) = transaction { + val ver = Version.findById(req) ?: throw RouteError("Invalid version ID.", HttpStatusCode.NotFound) + ver.project.isAccessible(user, true) + ver.delete() ServerScope.launch { - Buses.projectBus(lbl.id.value).sendAll(ProjectEvent.LabelDeleted(req)) + Buses.projectBus(ver.id.value).sendAll(ProjectEvent.VersionDeleted(req)) } success(EmptyResponse()) @@ -178,9 +177,9 @@ object ProjectHandler { fun createEntry(user: User, req: EntryRequest) = transaction { val proj = accessibleProject(user, req.projectId, true) val entry = Entry.new { - label = when(val l = Label.findById(req.label)) { - null -> throw RouteError("Invalid label ID.", HttpStatusCode.NotFound) - is Label if l.projectId.value != proj.id.value -> throw RouteError("Label is attached to a different project.", HttpStatusCode.Conflict) + version = when(val l = Version.findById(req.version)) { + null -> throw RouteError("Invalid version ID.", HttpStatusCode.NotFound) + is Version if l.projectId.value != proj.id.value -> throw RouteError("Version is attached to a different project.", HttpStatusCode.Conflict) else -> l } project = proj @@ -192,7 +191,7 @@ object ProjectHandler { hardware = req.hardware } - val response = EntryResponse(entry.id.value, entry.label.id.value, entry.timestamp, entry.warmups, entry.measurements, entry.unit, entry.input, entry.hardware) + val response = EntryResponse(entry.id.value, entry.version.id.value, entry.timestamp, entry.warmups, entry.measurements, entry.unit, entry.input, entry.hardware) ServerScope.launch { Buses.projectBus(proj.id.value).sendAll(ProjectEvent.NewEntry(response)) }