From 51b70e7d6e4c394668146111cf3c45121a3263f5 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 13 Oct 2024 10:11:57 -0300 Subject: [PATCH 1/2] [api][desktop]: Handle exception and show it as notification message. Fix #520 --- api/src/main/kotlin/nebulosa/api/Nebulosa.kt | 16 ++++ .../nebulosa/api/atlas/SkyAtlasController.kt | 2 +- .../confirmation/ConfirmationController.kt | 4 +- .../api/connection/ConnectionController.kt | 4 +- .../api/connection/ConnectionService.kt | 3 +- .../kotlin/nebulosa/api/core/ErrorResponse.kt | 20 +++++ .../nebulosa/api/framing/FramingController.kt | 7 +- .../api/guiding/GuideOutputController.kt | 4 +- .../kotlin/nebulosa/api/image/ImageService.kt | 3 +- .../nebulosa/api/mounts/MountController.kt | 32 ++++---- .../api/platesolver/PlateSolverController.kt | 4 +- .../api/platesolver/PlateSolverRequest.kt | 7 +- .../nebulosa/api/validators/Validators.kt | 79 +++++++++---------- .../nebulosa/api/wheels/WheelController.kt | 4 +- desktop/src/app/app.module.ts | 6 ++ desktop/src/app/atlas/atlas.component.ts | 2 +- .../app/filterwheel/filterwheel.component.ts | 3 +- .../app/flat-wizard/flat-wizard.component.ts | 2 +- desktop/src/app/framing/framing.component.ts | 6 -- desktop/src/app/home/home.component.ts | 8 -- desktop/src/app/mount/mount.component.ts | 8 +- .../src/app/sequencer/sequencer.component.ts | 4 +- .../device-list-menu.component.ts | 2 +- .../shared/interceptors/error.interceptor.ts | 28 +++++++ .../src/shared/services/angular.service.ts | 5 +- 25 files changed, 156 insertions(+), 107 deletions(-) create mode 100644 api/src/main/kotlin/nebulosa/api/core/ErrorResponse.kt create mode 100644 desktop/src/shared/interceptors/error.interceptor.ts diff --git a/api/src/main/kotlin/nebulosa/api/Nebulosa.kt b/api/src/main/kotlin/nebulosa/api/Nebulosa.kt index 00f96704e..a98359324 100644 --- a/api/src/main/kotlin/nebulosa/api/Nebulosa.kt +++ b/api/src/main/kotlin/nebulosa/api/Nebulosa.kt @@ -8,14 +8,18 @@ import com.fasterxml.jackson.module.kotlin.jsonMapper import com.github.rvesse.airline.annotations.Command import com.github.rvesse.airline.annotations.Option import io.javalin.Javalin +import io.javalin.http.Context +import io.javalin.http.HttpStatus.BAD_REQUEST import io.javalin.json.JavalinJackson import nebulosa.api.converters.modules.DeviceModule +import nebulosa.api.core.ErrorResponse import nebulosa.api.inject.* import nebulosa.json.PathModule import nebulosa.log.i import nebulosa.log.loggerFor import org.koin.core.context.startKoin import org.slf4j.LoggerFactory +import java.net.ConnectException @Command(name = "nebulosa") class Nebulosa : Runnable, AutoCloseable { @@ -52,6 +56,8 @@ class Nebulosa : Runnable, AutoCloseable { } }.start(host, port) + app.exception(Exception::class.java, ::handleException) + koinApp.modules(appModule(app)) koinApp.modules(objectMapperModule(OBJECT_MAPPER)) koinApp.modules(servicesModule()) @@ -61,6 +67,16 @@ class Nebulosa : Runnable, AutoCloseable { LOG.i("server is started at port: {}", app.port()) } + private fun handleException(ex: Exception, ctx: Context) { + val message = when (ex) { + is ConnectException -> "connection refused" + is NumberFormatException -> "invalid number: ${ex.message}" + else -> ex.message!! + } + + ctx.status(BAD_REQUEST).json(ErrorResponse.error(message.lowercase())) + } + override fun close() { app.stop() } diff --git a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt index 6bde600b5..905556bde 100644 --- a/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt +++ b/api/src/main/kotlin/nebulosa/api/atlas/SkyAtlasController.kt @@ -103,7 +103,7 @@ class SkyAtlasController( } private fun searchMinorPlanet(ctx: Context) { - val text = ctx.queryParam("text").notNull().notBlank() + val text = ctx.queryParam("text").notNullOrBlank() ctx.json(skyAtlasService.searchMinorPlanet(text)) } diff --git a/api/src/main/kotlin/nebulosa/api/confirmation/ConfirmationController.kt b/api/src/main/kotlin/nebulosa/api/confirmation/ConfirmationController.kt index 4a79aaa10..5eaa807f9 100644 --- a/api/src/main/kotlin/nebulosa/api/confirmation/ConfirmationController.kt +++ b/api/src/main/kotlin/nebulosa/api/confirmation/ConfirmationController.kt @@ -3,8 +3,8 @@ package nebulosa.api.confirmation import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.core.Controller -import nebulosa.api.validators.notBlank import nebulosa.api.validators.notNull +import nebulosa.api.validators.notNullOrBlank class ConfirmationController( override val app: Javalin, @@ -16,7 +16,7 @@ class ConfirmationController( } private fun confirm(ctx: Context) { - val idempotencyKey = ctx.pathParam("idempotencyKey").notNull().notBlank() + val idempotencyKey = ctx.pathParam("idempotencyKey").notNullOrBlank() val accepted = ctx.queryParam("accepted").notNull().toBoolean() confirmationService.confirm(idempotencyKey, accepted) } diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt index 35c86159a..6f1d7fc92 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionController.kt @@ -3,8 +3,8 @@ package nebulosa.api.connection import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.core.Controller -import nebulosa.api.validators.notBlank import nebulosa.api.validators.notNull +import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.range class ConnectionController( @@ -20,7 +20,7 @@ class ConnectionController( } private fun connect(ctx: Context) { - val host = ctx.queryParam("host").notNull().notBlank() + val host = ctx.queryParam("host").notNullOrBlank() val port = ctx.queryParam("port").notNull().toInt().range(1, 65535) val type = ctx.queryParam("type").notNull().let(ConnectionType::valueOf) diff --git a/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt b/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt index e5a5605bd..8b84a4d36 100644 --- a/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt +++ b/api/src/main/kotlin/nebulosa/api/connection/ConnectionService.kt @@ -1,6 +1,5 @@ package nebulosa.api.connection -import io.javalin.http.InternalServerErrorResponse import nebulosa.alpaca.indi.client.AlpacaClient import nebulosa.api.message.MessageService import nebulosa.indi.client.INDIClient @@ -81,7 +80,7 @@ class ConnectionService( return provider.id } catch (e: Throwable) { LOG.e("failed to connect", e) - throw InternalServerErrorResponse("Connection Failed") + throw e } } diff --git a/api/src/main/kotlin/nebulosa/api/core/ErrorResponse.kt b/api/src/main/kotlin/nebulosa/api/core/ErrorResponse.kt new file mode 100644 index 000000000..c360e60bf --- /dev/null +++ b/api/src/main/kotlin/nebulosa/api/core/ErrorResponse.kt @@ -0,0 +1,20 @@ +package nebulosa.api.core + +import nebulosa.api.notification.Severity + +data class ErrorResponse( + @JvmField val type: Severity, + @JvmField val message: String, +) { + + companion object { + + fun success(message: String) = ErrorResponse(Severity.SUCCESS, message) + + fun info(message: String) = ErrorResponse(Severity.INFO, message) + + fun warn(message: String) = ErrorResponse(Severity.WARNING, message) + + fun error(message: String) = ErrorResponse(Severity.ERROR, message) + } +} diff --git a/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt b/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt index a9a832d8c..7836682be 100644 --- a/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt +++ b/api/src/main/kotlin/nebulosa/api/framing/FramingController.kt @@ -4,8 +4,7 @@ import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.core.Controller import nebulosa.api.image.ImageService -import nebulosa.api.validators.notBlank -import nebulosa.api.validators.notNull +import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.range import nebulosa.math.deg import nebulosa.math.hours @@ -26,8 +25,8 @@ class FramingController( } private fun frame(ctx: Context) { - val rightAscension = ctx.queryParam("rightAscension").notNull().notBlank() - val declination = ctx.queryParam("declination").notNull().notBlank() + val rightAscension = ctx.queryParam("rightAscension").notNullOrBlank() + val declination = ctx.queryParam("declination").notNullOrBlank() val width = ctx.queryParam("width")?.toInt()?.range(1, 7680) ?: 1280 val height = ctx.queryParam("height")?.toInt()?.range(1, 4320) ?: 720 val fov = ctx.queryParam("fov")?.toDouble()?.range(0.0, 90.0) ?: 1.0 diff --git a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt index 3fb50df85..8a23eaee4 100644 --- a/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt +++ b/api/src/main/kotlin/nebulosa/api/guiding/GuideOutputController.kt @@ -4,8 +4,8 @@ import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.connection.ConnectionService import nebulosa.api.core.Controller -import nebulosa.api.validators.notBlank import nebulosa.api.validators.notNull +import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.range import nebulosa.guiding.GuideDirection import java.time.Duration @@ -49,7 +49,7 @@ class GuideOutputController( private fun pulse(ctx: Context) { val id = ctx.pathParam("id") val guideOutput = connectionService.guideOutput(id) ?: return - val direction = ctx.queryParam("direction").notNull().notBlank().let(GuideDirection::valueOf) + val direction = ctx.queryParam("direction").notNullOrBlank().let(GuideDirection::valueOf) val duration = ctx.queryParam("duration").notNull().toLong().range(0L, 1800000000L).times(1000L).let(Duration::ofNanos) guideOutputService.pulse(guideOutput, direction, duration) } diff --git a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt index 16cb06777..06b11022e 100644 --- a/api/src/main/kotlin/nebulosa/api/image/ImageService.kt +++ b/api/src/main/kotlin/nebulosa/api/image/ImageService.kt @@ -1,7 +1,6 @@ package nebulosa.api.image import com.fasterxml.jackson.databind.ObjectMapper -import io.javalin.http.NotFoundResponse import jakarta.servlet.http.HttpServletResponse import nebulosa.api.atlas.Location import nebulosa.api.atlas.SimbadEntityRepository @@ -280,7 +279,7 @@ class ImageService( require(save.path != null) var (image) = imageBucket.open(path).image?.transform(save.shouldBeTransformed, save.transformation, ImageOperation.SAVE) - ?: throw NotFoundResponse("Image not found") + ?: throw IllegalArgumentException("image not found") val (x, y, width, height) = save.subFrame.constrained(image.width, image.height) diff --git a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt index db4e0edd6..7a022907a 100644 --- a/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt +++ b/api/src/main/kotlin/nebulosa/api/mounts/MountController.kt @@ -79,8 +79,8 @@ class MountController( private fun sync(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val rightAscension = ctx.queryParam("rightAscension").notNull().notBlank() - val declination = ctx.queryParam("declination").notNull().notBlank() + val rightAscension = ctx.queryParam("rightAscension").notNullOrBlank() + val declination = ctx.queryParam("declination").notNullOrBlank() val j2000 = ctx.queryParam("j2000")?.toBoolean() ?: false mountService.sync(mount, rightAscension.hours, declination.deg, j2000) } @@ -88,8 +88,8 @@ class MountController( private fun slew(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val rightAscension = ctx.queryParam("rightAscension").notNull().notBlank() - val declination = ctx.queryParam("declination").notNull().notBlank() + val rightAscension = ctx.queryParam("rightAscension").notNullOrBlank() + val declination = ctx.queryParam("declination").notNullOrBlank() val j2000 = ctx.queryParam("j2000")?.toBoolean() ?: false val idempotencyKey = ctx.idempotencyKey() mountService.slewTo(mount, rightAscension.hours, declination.deg, j2000, idempotencyKey) @@ -98,8 +98,8 @@ class MountController( private fun goTo(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val rightAscension = ctx.queryParam("rightAscension").notNull().notBlank() - val declination = ctx.queryParam("declination").notNull().notBlank() + val rightAscension = ctx.queryParam("rightAscension").notNullOrBlank() + val declination = ctx.queryParam("declination").notNullOrBlank() val j2000 = ctx.queryParam("j2000")?.toBoolean() ?: false val idempotencyKey = ctx.idempotencyKey() mountService.goTo(mount, rightAscension.hours, declination.deg, j2000, idempotencyKey) @@ -120,21 +120,21 @@ class MountController( private fun trackMode(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val mode = ctx.queryParam("mode").notNull().notBlank().let(TrackMode::valueOf) + val mode = ctx.queryParam("mode").notNullOrBlank().let(TrackMode::valueOf) mountService.trackMode(mount, mode) } private fun slewRate(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val rate = ctx.queryParam("rate").notNull().notBlank() + val rate = ctx.queryParam("rate").notNullOrBlank() mountService.slewRate(mount, mount.slewRates.first { it.name == rate }) } private fun move(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val direction = ctx.queryParam("direction").notNull().notBlank().let(GuideDirection::valueOf) + val direction = ctx.queryParam("direction").notNullOrBlank().let(GuideDirection::valueOf) val enabled = ctx.queryParam("enabled").notNull().toBoolean() mountService.move(mount, direction, enabled) } @@ -154,8 +154,8 @@ class MountController( private fun coordinates(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val longitude = ctx.queryParam("longitude").notNull().notBlank() - val latitude = ctx.queryParam("latitude").notNull().notBlank() + val longitude = ctx.queryParam("longitude").notNullOrBlank() + val latitude = ctx.queryParam("latitude").notNullOrBlank() val elevation = ctx.queryParam("elevation")?.toDouble() ?: 0.0 mountService.coordinates(mount, longitude.deg, latitude.deg, elevation.m) } @@ -173,7 +173,7 @@ class MountController( private fun celestialLocation(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val type = ctx.pathParam("type").notNull().notBlank().let(CelestialLocationType::valueOf) + val type = ctx.pathParam("type").notNullOrBlank().let(CelestialLocationType::valueOf) val location = when (type) { CelestialLocationType.ZENITH -> mountService.computeZenithLocation(mount) @@ -191,8 +191,8 @@ class MountController( private fun location(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val rightAscension = ctx.queryParam("rightAscension").notNull().notBlank() - val declination = ctx.queryParam("declination").notNull().notBlank() + val rightAscension = ctx.queryParam("rightAscension").notNullOrBlank() + val declination = ctx.queryParam("declination").notNullOrBlank() val j2000 = ctx.queryParam("j2000")?.toBoolean() ?: false val equatorial = ctx.queryParam("equatorial")?.toBoolean() ?: true val horizontal = ctx.queryParam("horizontal")?.toBoolean() ?: true @@ -212,7 +212,7 @@ class MountController( private fun remoteControlStart(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val protocol = ctx.queryParam("protocol").notNull().notBlank().let(MountRemoteControlProtocol::valueOf) + val protocol = ctx.queryParam("protocol").notNullOrBlank().let(MountRemoteControlProtocol::valueOf) val host = ctx.queryParam("host")?.ifBlank { null } ?: "0.0.0.0" val port = ctx.queryParam("port")?.toInt()?.positive() ?: 10001 mountService.remoteControlStart(mount, protocol, host, port) @@ -221,7 +221,7 @@ class MountController( private fun remoteControlStop(ctx: Context) { val id = ctx.pathParam("id") val mount = connectionService.mount(id) ?: return - val protocol = ctx.queryParam("protocol").notNull().notBlank().let(MountRemoteControlProtocol::valueOf) + val protocol = ctx.queryParam("protocol").notNullOrBlank().let(MountRemoteControlProtocol::valueOf) mountService.remoteControlStop(mount, protocol) } diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt index fa153da2f..56542f54d 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverController.kt @@ -5,7 +5,7 @@ import io.javalin.http.Context import io.javalin.http.bodyAsClass import nebulosa.api.core.Controller import nebulosa.api.validators.exists -import nebulosa.api.validators.notNull +import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.path import nebulosa.api.validators.valid @@ -20,7 +20,7 @@ class PlateSolverController( } private fun start(ctx: Context) { - val path = ctx.queryParam("path")?.path().notNull().exists() + val path = ctx.queryParam("path").notNullOrBlank().path().exists() val solver = ctx.bodyAsClass().valid() ctx.json(plateSolverService.solveImage(solver, path)) } diff --git a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt index 26c6b7be2..7c60ff54a 100644 --- a/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt +++ b/api/src/main/kotlin/nebulosa/api/platesolver/PlateSolverRequest.kt @@ -6,9 +6,7 @@ import nebulosa.api.converters.angle.DegreesDeserializer import nebulosa.api.converters.angle.RightAscensionDeserializer import nebulosa.api.converters.time.DurationUnit import nebulosa.api.inject.Named -import nebulosa.api.validators.Validatable -import nebulosa.api.validators.max -import nebulosa.api.validators.positiveOrZero +import nebulosa.api.validators.* import nebulosa.astap.platesolver.AstapPlateSolver import nebulosa.astrometrynet.nova.NovaAstrometryNetService import nebulosa.astrometrynet.platesolver.LocalAstrometryNetPlateSolver @@ -44,6 +42,7 @@ data class PlateSolverRequest( ) : Validatable, KoinComponent, Supplier { override fun validate() { + executablePath.notNull(PLATE_SOLVER_IS_NOT_CONFIGURED).notBlank(PLATE_SOLVER_IS_NOT_CONFIGURED) timeout.positiveOrZero().max(5, TimeUnit.MINUTES) downsampleFactor.positiveOrZero() focalLength.positiveOrZero() @@ -70,6 +69,8 @@ data class PlateSolverRequest( companion object { + const val PLATE_SOLVER_IS_NOT_CONFIGURED = "plate solver is not configured" + @JvmStatic val EMPTY = PlateSolverRequest() @JvmStatic private val NOVA_ASTROMETRY_NET_CACHE = HashMap() } diff --git a/api/src/main/kotlin/nebulosa/api/validators/Validators.kt b/api/src/main/kotlin/nebulosa/api/validators/Validators.kt index 788daf393..f3aa1d1ee 100644 --- a/api/src/main/kotlin/nebulosa/api/validators/Validators.kt +++ b/api/src/main/kotlin/nebulosa/api/validators/Validators.kt @@ -17,74 +17,73 @@ inline fun T.validate(value: Boolean, lazyMessage: () -> String) = apply { r // ANY -inline fun T?.notNull() = requireNotNull(this) { "must not be null" } +inline fun T?.notNull(message: String? = null) = requireNotNull(this) { message ?: "must not be null" } // TEXT -inline fun String.notEmpty() = validate(isNotEmpty()) { "must not be empty" } -inline fun String.notBlank() = validate(isNotBlank()) { "must not be blank" } -inline fun String.minLength(min: Int) = validate(length >= min) { "length must be greater than or equal to $min" } -inline fun String.maxLength(max: Int) = validate(length <= max) { "length must be greater than or equal to $max" } -inline fun String.regex(pattern: Regex) = validate(pattern.matches(this)) { "must match \"$pattern\"" } +inline fun String.notEmpty(message: String? = null) = validate(isNotEmpty()) { message ?: "must not be empty" } +inline fun String.notBlank(message: String? = null) = validate(isNotBlank()) { message ?: "must not be blank" } +inline fun String?.notNullOrEmpty(message: String? = null) = if (isNullOrEmpty()) throw IllegalArgumentException(message ?: "must not be empty") else this +inline fun String?.notNullOrBlank(message: String? = null) = if (isNullOrBlank()) throw IllegalArgumentException(message ?: "must not be blank") else this +inline fun String.minLength(min: Int, message: String? = null) = validate(length >= min) { message ?: "length must be greater than or equal to $min" } +inline fun String.maxLength(max: Int, message: String? = null) = validate(length <= max) { message ?: "length must be greater than or equal to $max" } +inline fun String.regex(pattern: Regex, message: String? = null) = validate(pattern.matches(this)) { message ?: "must match \"$pattern\"" } // INT -inline fun Int.min(min: Int) = validate(this >= min) { "must be greater than or equal to $min" } -inline fun Int.max(max: Int) = validate(this <= max) { "must be less than or equal to $max" } -inline fun Int.range(min: Int, max: Int) = validate(this in min..max) { " must be between $min and $max" } -inline fun Int.positive() = validate(this > 0) { "must be greater than 0" } -inline fun Int.positiveOrZero() = validate(this >= 0) { "must be greater than or equal to 0" } +inline fun Int.min(min: Int, message: String? = null) = validate(this >= min) { message ?: "must be greater than or equal to $min" } +inline fun Int.max(max: Int, message: String? = null) = validate(this <= max) { message ?: "must be less than or equal to $max" } +inline fun Int.range(min: Int, max: Int, message: String? = null) = validate(this in min..max) { message ?: "must be between $min and $max" } +inline fun Int.positive(message: String? = null) = validate(this > 0) { message ?: "must be greater than 0" } +inline fun Int.positiveOrZero(message: String? = null) = validate(this >= 0) { message ?: "must be greater than or equal to 0" } // LONG -inline fun Long.min(min: Long) = validate(this >= min) { "must be greater than or equal to $min" } -inline fun Long.max(max: Long) = validate(this <= max) { "must be less than or equal to $max" } -inline fun Long.range(min: Long, max: Long) = validate(this in min..max) { " must be between $min and $max" } -inline fun Long.positive() = validate(this > 0) { "must be greater than 0" } -inline fun Long.positiveOrZero() = validate(this >= 0) { "must be greater than or equal to 0" } +inline fun Long.min(min: Long, message: String? = null) = validate(this >= min) { message ?: "must be greater than or equal to $min" } +inline fun Long.max(max: Long, message: String? = null) = validate(this <= max) { message ?: "must be less than or equal to $max" } +inline fun Long.range(min: Long, max: Long, message: String? = null) = validate(this in min..max) { message ?: "must be between $min and $max" } +inline fun Long.positive(message: String? = null) = validate(this > 0) { message ?: "must be greater than 0" } +inline fun Long.positiveOrZero(message: String? = null) = validate(this >= 0) { message ?: "must be greater than or equal to 0" } // DOUBLE -inline fun Double.min(min: Double) = validate(this >= min) { "must be greater than or equal to $min" } -inline fun Double.max(max: Double) = validate(this <= max) { "must be less than or equal to $max" } -inline fun Double.range(min: Double, max: Double) = validate(this in min..max) { " must be between $min and $max" } -inline fun Double.positive() = validate(this > 0) { "must be greater than 0" } -inline fun Double.positiveOrZero() = validate(this >= 0) { "must be greater than or equal to 0" } +inline fun Double.min(min: Double, message: String? = null) = validate(this >= min) { message ?: "must be greater than or equal to $min" } +inline fun Double.max(max: Double, message: String? = null) = validate(this <= max) { message ?: "must be less than or equal to $max" } +inline fun Double.range(min: Double, max: Double, message: String? = null) = validate(this in min..max) { message ?: "must be between $min and $max" } +inline fun Double.positive(message: String? = null) = validate(this > 0) { message ?: "must be greater than 0" } +inline fun Double.positiveOrZero(message: String? = null) = validate(this >= 0) { message ?: "must be greater than or equal to 0" } // PATH inline fun String.path() = Path(this) -inline fun Path.exists() = validate(exists()) { "must exist" } +inline fun Path.notBlank(message: String? = null) = validate("$this".isNotBlank()) { message ?: "path must not be blank" } +inline fun Path.exists(message: String? = null) = validate(exists()) { message ?: "path must exist" } // DURATION -inline fun Duration.positive() = validate(!isNegative && !isZero) { "must be greater than 0" } -inline fun Duration.positiveOrZero() = validate(!isNegative) { "must be greater than or equal to 0" } - -inline fun Duration.min(duration: Long, unit: TimeUnit) = - validate(toNanos() >= unit.toNanos(duration)) { "must be greater than or equal to $duration $unit" } - -inline fun Duration.max(duration: Long, unit: TimeUnit) = - validate(toNanos() <= unit.toNanos(duration)) { "must be less than or equal to $duration $unit" } +inline fun Duration.positive(message: String? = null) = validate(!isNegative && !isZero) { message ?: "must be greater than 0" } +inline fun Duration.positiveOrZero(message: String? = null) = validate(!isNegative) { message ?: "must be greater than or equal to 0" } +inline fun Duration.min(duration: Long, unit: TimeUnit, message: String? = null) = validate(toNanos() >= unit.toNanos(duration)) { message ?: "must be greater than or equal to $duration $unit" } +inline fun Duration.max(duration: Long, unit: TimeUnit, message: String? = null) = validate(toNanos() <= unit.toNanos(duration)) { message ?: "must be less than or equal to $duration $unit" } // COLLECTION -inline fun Collection.notEmpty() = validate(isNotEmpty()) { "must not be empty" } -inline fun Collection.minSize(min: Int) = validate(size >= min) { "size must be greater than or equal to $min" } +inline fun Collection.notEmpty(message: String? = null) = validate(isNotEmpty()) { message ?: "must not be empty" } +inline fun Collection.minSize(min: Int, message: String? = null) = validate(size >= min) { message ?: "size must be greater than or equal to $min" } // DATE & TIME inline fun String.localDate(): LocalDate = LocalDate.parse(this) -inline fun LocalDate.future() = validate(this > LocalDate.now(SystemClock)) { "must be a future date" } -inline fun LocalDate.futureOrPresent() = validate(this >= LocalDate.now(SystemClock)) { "must be a date in the present or in the future" } -inline fun LocalDate.past() = validate(this < LocalDate.now(SystemClock)) { "must be a past date" } -inline fun LocalDate.pastOrPresent() = validate(this <= LocalDate.now(SystemClock)) { "must be a date in the past or in the present" } +inline fun LocalDate.future(message: String? = null) = validate(this > LocalDate.now(SystemClock)) { message ?: "must be a future date" } +inline fun LocalDate.futureOrPresent(message: String? = null) = validate(this >= LocalDate.now(SystemClock)) { message ?: "must be a date in the present or in the future" } +inline fun LocalDate.past(message: String? = null) = validate(this < LocalDate.now(SystemClock)) { message ?: "must be a past date" } +inline fun LocalDate.pastOrPresent(message: String? = null) = validate(this <= LocalDate.now(SystemClock)) { message ?: "must be a date in the past or in the present" } inline fun String.localTime(): LocalTime = LocalTime.parse(this) -inline fun LocalTime.future() = validate(this > LocalTime.now(SystemClock)) { "must be a future time" } -inline fun LocalTime.futureOrPresent() = validate(this >= LocalTime.now(SystemClock)) { "must be a time in the present or in the future" } -inline fun LocalTime.past() = validate(this < LocalTime.now(SystemClock)) { "must be a past date" } -inline fun LocalTime.pastOrPresent() = validate(this <= LocalTime.now(SystemClock)) { "must be a time in the past or in the present" } +inline fun LocalTime.future(message: String? = null) = validate(this > LocalTime.now(SystemClock)) { message ?: "must be a future time" } +inline fun LocalTime.futureOrPresent(message: String? = null) = validate(this >= LocalTime.now(SystemClock)) { message ?: "must be a time in the present or in the future" } +inline fun LocalTime.past(message: String? = null) = validate(this < LocalTime.now(SystemClock)) { message ?: "must be a past date" } +inline fun LocalTime.pastOrPresent(message: String? = null) = validate(this <= LocalTime.now(SystemClock)) { message ?: "must be a time in the past or in the present" } // BODY diff --git a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt index 739355304..0be4d97b2 100644 --- a/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt +++ b/api/src/main/kotlin/nebulosa/api/wheels/WheelController.kt @@ -4,8 +4,8 @@ import io.javalin.Javalin import io.javalin.http.Context import nebulosa.api.connection.ConnectionService import nebulosa.api.core.Controller -import nebulosa.api.validators.notBlank import nebulosa.api.validators.notNull +import nebulosa.api.validators.notNullOrBlank import nebulosa.api.validators.positiveOrZero class WheelController( @@ -55,7 +55,7 @@ class WheelController( private fun sync(ctx: Context) { val id = ctx.pathParam("id") val wheel = connectionService.wheel(id) ?: return - val names = ctx.queryParam("names").notNull().notBlank() + val names = ctx.queryParam("names").notNullOrBlank() wheelService.sync(wheel, names.split(",")) } diff --git a/desktop/src/app/app.module.ts b/desktop/src/app/app.module.ts index c41432a0f..5d2fee002 100644 --- a/desktop/src/app/app.module.ts +++ b/desktop/src/app/app.module.ts @@ -64,6 +64,7 @@ import { NoDropdownDirective } from '../shared/directives/no-dropdown.directive' import { SpinnableNumberDirective } from '../shared/directives/spinnable-number.directive' import { StopPropagationDirective } from '../shared/directives/stop-propagation.directive' import { ConfirmationInterceptor } from '../shared/interceptors/confirmation.interceptor' +import { ErrorInterceptor } from '../shared/interceptors/error.interceptor' import { IdempotencyKeyInterceptor } from '../shared/interceptors/idempotency-key.interceptor' import { LocationInterceptor } from '../shared/interceptors/location.interceptor' import { AnglePipe } from '../shared/pipes/angle.pipe' @@ -225,6 +226,11 @@ import { StackerComponent } from './stacker/stacker.component' provide: LOCALE_ID, useValue: 'en-US', }, + { + provide: HTTP_INTERCEPTORS, + useClass: ErrorInterceptor, + multi: true, + }, { provide: HTTP_INTERCEPTORS, useClass: LocationInterceptor, diff --git a/desktop/src/app/atlas/atlas.component.ts b/desktop/src/app/atlas/atlas.component.ts index 215936687..81dacb000 100644 --- a/desktop/src/app/atlas/atlas.component.ts +++ b/desktop/src/app/atlas/atlas.component.ts @@ -510,7 +510,7 @@ export class AtlasComponent implements OnInit, AfterContentInit, AfterViewInit, this.minorPlanet.closeApproach.result = await this.api.closeApproachesOfMinorPlanets(this.minorPlanet.closeApproach.days, this.minorPlanet.closeApproach.lunarDistance, this.dateTimeAndLocation.dateTime) if (!this.minorPlanet.closeApproach.result.length) { - this.angularService.message('No close approaches found for the given days and lunar distance', 'warn') + this.angularService.message('No close approaches found for the given days and lunar distance', 'warning') } } finally { this.refresh.position = false diff --git a/desktop/src/app/filterwheel/filterwheel.component.ts b/desktop/src/app/filterwheel/filterwheel.component.ts index c5866a8d1..09abe984d 100644 --- a/desktop/src/app/filterwheel/filterwheel.component.ts +++ b/desktop/src/app/filterwheel/filterwheel.component.ts @@ -252,8 +252,7 @@ export class FilterWheelComponent implements AfterContentInit, OnDestroy, Tickab else await this.api.focuserMoveOut(this.focuser, offset) } } - } catch (e) { - console.error(e) + } catch { this.moving = false } } diff --git a/desktop/src/app/flat-wizard/flat-wizard.component.ts b/desktop/src/app/flat-wizard/flat-wizard.component.ts index bc32f121e..62f3e94c3 100644 --- a/desktop/src/app/flat-wizard/flat-wizard.component.ts +++ b/desktop/src/app/flat-wizard/flat-wizard.component.ts @@ -67,7 +67,7 @@ export class FlatWizardComponent implements AfterViewInit, OnDestroy, Tickable { if (event.state === 'CAPTURED') { this.angularService.message('Flat frame captured') } else if (event.state === 'FAILED') { - this.angularService.message('Failed to find an optimal exposure time from given parameters', 'error') + this.angularService.message('Failed to find an optimal exposure time from given parameters', 'danger') } } }) diff --git a/desktop/src/app/framing/framing.component.ts b/desktop/src/app/framing/framing.component.ts index d01f82a70..ecb5f765d 100644 --- a/desktop/src/app/framing/framing.component.ts +++ b/desktop/src/app/framing/framing.component.ts @@ -1,6 +1,5 @@ import { AfterViewInit, Component, HostListener, NgZone, OnDestroy } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -26,7 +25,6 @@ export class FramingComponent implements AfterViewInit, OnDestroy { private readonly browserWindowService: BrowserWindowService, private readonly electronService: ElectronService, private readonly preferenceService: PreferenceService, - private readonly angularService: AngularService, ngZone: NgZone, ) { app.title = 'Framing' @@ -83,10 +81,6 @@ export class FramingComponent implements AfterViewInit, OnDestroy { const title = `Framing ・ ${rightAscension} ・ ${declination}` this.frameId = await this.browserWindowService.openImage({ path, source: 'FRAMING', id: 'framing', title }) - } catch (e) { - console.error(e) - - this.angularService.message('Failed to retrieve the image', 'error') } finally { this.loading = false } diff --git a/desktop/src/app/home/home.component.ts b/desktop/src/app/home/home.component.ts index 55c493ae5..d7915723a 100644 --- a/desktop/src/app/home/home.component.ts +++ b/desktop/src/app/home/home.component.ts @@ -4,7 +4,6 @@ import nebulosa from '../../assets/data/nebulosa.json' import { DeviceChooserComponent } from '../../shared/components/device-chooser/device-chooser.component' import { DeviceConnectionCommandEvent, DeviceListMenuComponent } from '../../shared/components/device-list-menu/device-list-menu.component' import { MenuItem, SlideMenuItem } from '../../shared/components/menu-item/menu-item.component' -import { AngularService } from '../../shared/services/angular.service' import { ApiService } from '../../shared/services/api.service' import { BrowserWindowService } from '../../shared/services/browser-window.service' import { ElectronService } from '../../shared/services/electron.service' @@ -167,7 +166,6 @@ export class HomeComponent implements AfterContentInit { private readonly electronService: ElectronService, private readonly browserWindowService: BrowserWindowService, private readonly api: ApiService, - private readonly angularService: AngularService, private readonly preferenceService: PreferenceService, ngZone: NgZone, ) { @@ -468,10 +466,6 @@ export class HomeComponent implements AfterContentInit { if (this.connection && !this.connection.connected) { this.connection.id = await this.api.connect(this.connection.host, this.connection.port, this.connection.type) } - } catch (e) { - console.error(e) - - this.angularService.message('Connection failed', 'error') } finally { await this.updateConnection() } @@ -482,8 +476,6 @@ export class HomeComponent implements AfterContentInit { if (this.connection?.id && this.connection.connected) { await this.api.disconnect(this.connection.id) } - } catch (e) { - console.error(e) } finally { await this.updateConnection() } diff --git a/desktop/src/app/mount/mount.component.ts b/desktop/src/app/mount/mount.component.ts index 1707f0d14..b560fe80d 100644 --- a/desktop/src/app/mount/mount.component.ts +++ b/desktop/src/app/mount/mount.component.ts @@ -340,12 +340,8 @@ export class MountComponent implements AfterContentInit, OnDestroy, Tickable { } protected async startRemoteControl() { - try { - await this.api.mountRemoteControlStart(this.mount, this.remoteControl.protocol, this.remoteControl.host, this.remoteControl.port) - this.remoteControl.controls = await this.api.mountRemoteControlList(this.mount) - } catch { - this.angularService.message('Failed to start remote control', 'error') - } + await this.api.mountRemoteControlStart(this.mount, this.remoteControl.protocol, this.remoteControl.host, this.remoteControl.port) + this.remoteControl.controls = await this.api.mountRemoteControlList(this.mount) } protected async stopRemoteControl(protocol: MountRemoteControlProtocol) { diff --git a/desktop/src/app/sequencer/sequencer.component.ts b/desktop/src/app/sequencer/sequencer.component.ts index d783390b3..7a1e2b215 100644 --- a/desktop/src/app/sequencer/sequencer.component.ts +++ b/desktop/src/app/sequencer/sequencer.component.ts @@ -364,7 +364,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable private loadPlanFromJson(file: JsonFile) { if (!this.loadPlan(file.json)) { - this.angularService.message('No sequence found', 'warn') + this.angularService.message('No sequence found', 'warning') this.add() } @@ -387,7 +387,7 @@ export class SequencerComponent implements AfterContentInit, OnDestroy, Tickable return } - this.angularService.message('Failed to load the file', 'error') + this.angularService.message('Failed to load the file', 'danger') this.preference.loadPath = undefined this.savePreference() diff --git a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts index 5d879ee0e..5b56b9522 100644 --- a/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts +++ b/desktop/src/shared/components/device-list-menu/device-list-menu.component.ts @@ -57,7 +57,7 @@ export class DeviceListMenuComponent { return new Promise>((resolve) => { if (devices.length <= 0) { resolve(undefined) - this.angularService.message('Please connect your equipment first!', 'warn') + this.angularService.message('Please connect your equipment first!', 'warning') return } diff --git a/desktop/src/shared/interceptors/error.interceptor.ts b/desktop/src/shared/interceptors/error.interceptor.ts new file mode 100644 index 000000000..9018fb5fc --- /dev/null +++ b/desktop/src/shared/interceptors/error.interceptor.ts @@ -0,0 +1,28 @@ +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { catchError, Observable, throwError } from 'rxjs' +import { AngularService } from '../services/angular.service' +import { Severity } from '../types/angular.types' + +export interface ErrorResponse { + message: string + type: Severity +} + +@Injectable({ providedIn: 'root' }) +export class ErrorInterceptor implements HttpInterceptor { + constructor(private readonly angularService: AngularService) {} + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return next.handle(req).pipe( + catchError((e: HttpErrorResponse) => { + if (e.status === 400) { + const error = e.error as ErrorResponse + this.angularService.message(error.message, error.type) + } + + return throwError(() => e) + }), + ) + } +} diff --git a/desktop/src/shared/services/angular.service.ts b/desktop/src/shared/services/angular.service.ts index 43e5e5ab1..e26cd940f 100644 --- a/desktop/src/shared/services/angular.service.ts +++ b/desktop/src/shared/services/angular.service.ts @@ -2,6 +2,7 @@ import { Injectable, Type } from '@angular/core' import { MessageService } from 'primeng/api' import { DialogService, DynamicDialogConfig } from 'primeng/dynamicdialog' import { ConfirmDialog } from '../dialogs/confirm/confirm.dialog' +import { Severity } from '../types/angular.types' import { Undefinable } from '../utils/types' @Injectable({ providedIn: 'root' }) @@ -40,7 +41,7 @@ export class AngularService { return ConfirmDialog.open(this, message) } - message(text: string, severity: 'info' | 'warn' | 'error' | 'success' = 'success') { - this.messageService.add({ severity, detail: text, life: 8500 }) + message(text: string, severity: Severity = 'success') { + this.messageService.add({ severity, detail: text, life: 4000 }) } } From 9980bd8291f9aaa40141a6f1b14db4cc49001f03 Mon Sep 17 00:00:00 2001 From: tiagohm Date: Sun, 13 Oct 2024 10:22:56 -0300 Subject: [PATCH 2/2] [api]: Fix compilation error --- nebulosa-log/src/main/kotlin/nebulosa/log/Log.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nebulosa-log/src/main/kotlin/nebulosa/log/Log.kt b/nebulosa-log/src/main/kotlin/nebulosa/log/Log.kt index c7f7c3c93..cf6c9399e 100644 --- a/nebulosa-log/src/main/kotlin/nebulosa/log/Log.kt +++ b/nebulosa-log/src/main/kotlin/nebulosa/log/Log.kt @@ -131,6 +131,10 @@ inline fun Logger.dw(message: String, a0: Any?, a1: Any?) { if (isDebugEnabled) warn(message, a0, a1) } +inline fun Logger.dw(message: String, a0: Any?, a1: Any?, a2: Any?) { + if (isDebugEnabled) warn(message, a0, a1, a2) +} + // DEBUG INFO inline fun Logger.di(message: String) {