Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,25 @@ import app.cash.backfila.dashboard.UpdateBackfillRequest
import app.cash.backfila.service.persistence.BackfillState
import app.cash.backfila.ui.components.AlertError
import app.cash.backfila.ui.components.DashboardPageLayout
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.CANCEL_STATE_BUTTON_LABEL
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.DELETE_STATE_BUTTON_LABEL
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.PAUSE_STATE_BUTTON_LABEL
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.START_STATE_BUTTON_LABEL
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.html.ButtonType
import kotlinx.html.InputType
import kotlinx.html.TagConsumer
import kotlinx.html.button
import kotlinx.html.div
import kotlinx.html.form
import kotlinx.html.h2
import kotlinx.html.input
import kotlinx.html.span
import misk.security.authz.Authenticated
import misk.tailwind.Link
import misk.turbo.turbo_frame
import misk.web.Get
import misk.web.PathParam
import misk.web.QueryParam
Expand Down Expand Up @@ -46,80 +61,171 @@ class BackfillShowButtonHandlerAction @Inject constructor(
): Response<ResponseBody> {
try {
if (!field_id.isNullOrBlank()) {
when (field_id) {
"state" -> {
when (field_value) {
BackfillState.PAUSED.name -> {
stopBackfillAction.stop(id.toLong(), StopBackfillRequest())
}
BackfillState.RUNNING.name -> {
startBackfillAction.start(id.toLong(), StartBackfillRequest())
}
BackfillState.CANCELLED.name -> {
cancelBackfillAction.cancel(id.toLong())
}
"soft_delete" -> {
softDeleteBackfillAction.softDelete(id.toLong())
}
}
}
handleFieldUpdate(id.toLong(), field_id, field_value)
}
} catch (e: Exception) {
return handleError(e)
}

"num_threads" -> {
val numThreads = field_value?.toIntOrNull()
if (numThreads != null) {
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(num_threads = numThreads))
}
}
return when (field_id) {
"state" -> handleStateFrameResponse(id, field_value)
else -> handleRedirectResponse(id)
}
}

"scan_size" -> {
val scanSize = field_value?.toLongOrNull()
if (scanSize != null) {
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(scan_size = scanSize))
}
}
private fun handleFieldUpdate(id: Long, fieldId: String, fieldValue: String?) {
when (fieldId) {
"state" -> handleStateUpdate(id, fieldValue)
else -> handleConfigUpdate(id, fieldId, fieldValue)
}
}

"batch_size" -> {
val batchSize = field_value?.toLongOrNull()
if (batchSize != null) {
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(batch_size = batchSize))
}
}
private fun handleStateUpdate(id: Long, value: String?) {
when (value) {
BackfillState.PAUSED.name -> stopBackfillAction.stop(id, StopBackfillRequest())
BackfillState.RUNNING.name -> startBackfillAction.start(id, StartBackfillRequest())
BackfillState.CANCELLED.name -> cancelBackfillAction.cancel(id)
"soft_delete" -> softDeleteBackfillAction.softDelete(id)
}
}

"extra_sleep_ms" -> {
val extraSleepMs = field_value?.toLongOrNull()
if (extraSleepMs != null) {
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(extra_sleep_ms = extraSleepMs))
}
}
private fun handleConfigUpdate(id: Long, fieldId: String, value: String?) {
val request = when (fieldId) {
"num_threads" -> value?.toIntOrNull()?.let { UpdateBackfillRequest(num_threads = it) }
"scan_size" -> value?.toLongOrNull()?.let { UpdateBackfillRequest(scan_size = it) }
"batch_size" -> value?.toLongOrNull()?.let { UpdateBackfillRequest(batch_size = it) }
"extra_sleep_ms" -> value?.toLongOrNull()?.let { UpdateBackfillRequest(extra_sleep_ms = it) }
"backoff_schedule" -> value?.let { UpdateBackfillRequest(backoff_schedule = it) }
else -> null
}
request?.let { updateBackfillAction.update(id, it) }
}

"backoff_schedule" -> {
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(backoff_schedule = field_value))
}
private fun handleError(e: Exception): Response<ResponseBody> {
logger.error(e) { "Update backfill field failed $e" }
val errorHtmlResponseBody = dashboardPageLayout.newBuilder()
.buildHtmlResponseBody {
div("py-20") {
AlertError(message = "Update backfill field failed: $e", label = "Try Again", onClick = "history.back(); return false;")
}
}
} catch (e: Exception) {
// Since this action is only hit from the UI, catch any validation errors and show them to the user
val errorHtmlResponseBody = dashboardPageLayout.newBuilder()
.buildHtmlResponseBody {
div("py-20") {
AlertError(message = "Update backfill field failed: $e", label = "Try Again", onClick = "history.back(); return false;")
return Response(
body = errorHtmlResponseBody,
statusCode = 200,
headers = Headers.headersOf("Content-Type", MediaTypes.TEXT_HTML),
)
}

private fun handleStateFrameResponse(id: String, fieldValue: String?): Response<ResponseBody> {
val currentState = when (fieldValue) {
BackfillState.PAUSED.name -> BackfillState.PAUSED
BackfillState.RUNNING.name -> BackfillState.RUNNING
BackfillState.CANCELLED.name -> BackfillState.CANCELLED
"soft_delete" -> BackfillState.COMPLETE
else -> BackfillState.RUNNING
}

val frameContent = dashboardPageLayout.newBuilder()
.buildHtmlResponseBody {
turbo_frame("backfill-$id-state") {
div("flex justify-between items-center") {
h2("text-base font-semibold leading-6 text-gray-900") { +"State" }
div("flex gap-2") {
span("text-gray-700") { +currentState.name }
renderStateButtons(id, currentState)
}
}
}
logger.error(e) { "Update backfill field failed $e" }
return Response(
body = errorHtmlResponseBody,
statusCode = 200,
headers = Headers.headersOf("Content-Type", MediaTypes.TEXT_HTML),
)
}

return Response(
body = frameContent,
statusCode = 200,
headers = Headers.headersOf("Content-Type", MediaTypes.TEXT_HTML),
)
}

fun TagConsumer<*>.renderStateButtons(id: String, currentState: BackfillState, deletedAt: Instant? = null) {
getStateButton(currentState)?.let { button ->
renderButton(id, "state", button, if (button.label == START_STATE_BUTTON_LABEL) "green" else "yellow")
}
getCancelButton(currentState)?.let { button ->
renderButton(id, "state", button, "red")
}
getDeleteButton(currentState, deletedAt)?.let { button ->
renderButton(id, "state", button, "gray")
}
}

fun TagConsumer<*>.renderButton(id: String, fieldId: String, button: Link, color: String) {
form {
action = path(id)
input {
type = InputType.hidden
name = "field_id"
value = fieldId
}
input {
type = InputType.hidden
name = "field_value"
value = button.href
}
button(
classes = "rounded-full bg-$color-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-$color-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-$color-600",
) {
type = ButtonType.submit
+button.label
}
}
}

private fun handleRedirectResponse(id: String): Response<ResponseBody> {
return Response(
body = "go to /backfills/$id".toResponseBody(),
statusCode = 303,
headers = Headers.headersOf("Location", "/backfills/$id"),
)
}

private fun getStateButton(state: BackfillState): Link? {
return when (state) {
BackfillState.PAUSED -> Link(
label = START_STATE_BUTTON_LABEL,
href = BackfillState.RUNNING.name,
)
// COMPLETE and CANCELLED represent final states.
BackfillState.COMPLETE -> null
BackfillState.CANCELLED -> null
else -> Link(
label = PAUSE_STATE_BUTTON_LABEL,
href = BackfillState.PAUSED.name,
)
}
}

private fun getCancelButton(state: BackfillState): Link? {
return when (state) {
BackfillState.PAUSED -> Link(
label = CANCEL_STATE_BUTTON_LABEL,
href = BackfillState.CANCELLED.name,
)
else -> null
}
}

private fun getDeleteButton(state: BackfillState, deletedAt: Instant?): Link? {
if (deletedAt != null) {
return null
}
return when (state) {
BackfillState.COMPLETE, BackfillState.CANCELLED -> Link(
label = DELETE_STATE_BUTTON_LABEL,
href = "soft_delete",
)
else -> null
}
}

companion object {
private val logger = getLogger<BackfillShowButtonHandlerAction>()

Expand Down
Loading