Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -60,10 +60,12 @@ class GetBackfillRunsAction @Inject constructor(
@QueryParam pagination_token: String? = null,
@QueryParam backfill_name: String? = null,
@QueryParam created_by_user: String? = null,
@QueryParam show_deleted: Boolean = false,
): GetBackfillRunsResponse {
val filterArgs = FilterArgs(
backfillName = backfill_name,
createdByUser = created_by_user,
showDeleted = show_deleted,
)
return search(service, variant, pagination_token, filterArgs)
}
Expand Down Expand Up @@ -102,6 +104,7 @@ class GetBackfillRunsAction @Inject constructor(
val (pausedBackfills, nextOffset) = queryFactory.newQuery<BackfillRunQuery>()
.serviceId(dbService.id)
.stateNot(BackfillState.RUNNING)
.apply { if (!filterArgs.showDeleted) notSoftDeleted() }
.filterByArgs(filterArgs)
.newPager(
idDescPaginator(),
Expand Down Expand Up @@ -218,5 +221,6 @@ class GetBackfillRunsAction @Inject constructor(
private data class FilterArgs(
val backfillName: String? = null,
val createdByUser: String? = null,
val showDeleted: Boolean = false,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ data class GetBackfillStatusResponse(
val backoff_schedule: String?,
val partitions: List<UiPartition>,
val event_logs: List<UiEventLog>,
val deleted_at: Instant?,
val next_offset: String?,
)

Expand Down Expand Up @@ -124,6 +125,7 @@ class GetBackfillStatusAction @Inject constructor(
event.extra_data,
)
},
run.deleted_at,
nextOffset?.offset,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package app.cash.backfila.dashboard

import app.cash.backfila.service.persistence.BackfilaDb
import app.cash.backfila.service.persistence.BackfillState
import app.cash.backfila.service.persistence.DbBackfillRun
import app.cash.backfila.service.persistence.DbEventLog
import javax.inject.Inject
import misk.MiskCaller
import misk.exceptions.BadRequestException
import misk.hibernate.Id
import misk.hibernate.Transacter
import misk.hibernate.load
import misk.scope.ActionScoped
import misk.security.authz.Authenticated
import misk.web.PathParam
import misk.web.Post
import misk.web.RequestContentType
import misk.web.ResponseContentType
import misk.web.actions.WebAction
import misk.web.mediatype.MediaTypes

class SoftDeleteBackfillAction @Inject constructor(
@BackfilaDb private val transacter: Transacter,
private val caller: @JvmSuppressWildcards ActionScoped<MiskCaller?>,
) : WebAction {
@Post("/backfill/delete/{id}")
@RequestContentType(MediaTypes.APPLICATION_JSON)
@ResponseContentType(MediaTypes.APPLICATION_JSON)
@Authenticated(capabilities = ["users"])
fun softDelete(
@PathParam id: Long,
) {
transacter.transaction { session ->
val backfillRun = session.load<DbBackfillRun>(Id(id))

// Only allow soft delete for COMPLETE or CANCELLED backfills
if (backfillRun.state != BackfillState.COMPLETE && backfillRun.state != BackfillState.CANCELLED) {
throw BadRequestException("Can only delete completed or cancelled backfills")
}

backfillRun.deleted_at = java.time.Instant.now()

// Log the deletion event
session.save(
DbEventLog(
backfillRun.id,
partition_id = null,
user = caller.get()?.principal ?: "",
type = DbEventLog.Type.STATE_CHANGE,
message = "backfill soft deleted",
),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,7 @@ interface BackfillRunQuery : Query<DbBackfillRun> {

@Order("updated_at", asc = false)
fun orderByUpdatedAtDesc(): BackfillRunQuery

@Constraint("deleted_at", Operator.IS_NULL)
fun notSoftDeleted(): BackfillRunQuery
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ class DbBackfillRun() : DbUnsharded<DbBackfillRun>, DbTimestampedEntity {
@Column(nullable = false)
var dry_run: Boolean = false

@Column(nullable = true)
var deleted_at: Instant? = null

/** Comma separated list of delays for consecutive retries in milliseconds, e.g. 1000,2000 */
@Column
var backoff_schedule: String? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.cash.backfila.ui.actions

import app.cash.backfila.dashboard.CancelBackfillAction
import app.cash.backfila.dashboard.SoftDeleteBackfillAction
import app.cash.backfila.dashboard.StartBackfillAction
import app.cash.backfila.dashboard.StartBackfillRequest
import app.cash.backfila.dashboard.StopBackfillAction
Expand Down Expand Up @@ -33,6 +34,7 @@ class BackfillShowButtonHandlerAction @Inject constructor(
private val stopBackfillAction: StopBackfillAction,
private val updateBackfillAction: UpdateBackfillAction,
private val cancelBackfillAction: CancelBackfillAction,
private val softDeleteBackfillAction: SoftDeleteBackfillAction,
) : WebAction {
@Get(PATH)
@ResponseContentType(MediaTypes.TEXT_HTML)
Expand All @@ -56,6 +58,9 @@ class BackfillShowButtonHandlerAction @Inject constructor(
BackfillState.CANCELLED.name -> {
cancelBackfillAction.cancel(id.toLong())
}
"soft_delete" -> {
softDeleteBackfillAction.softDelete(id.toLong())
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,46 @@ package app.cash.backfila.ui.components

import app.cash.backfila.dashboard.UiBackfillRun
import app.cash.backfila.ui.pages.BackfillShowAction
import kotlinx.html.InputType
import kotlinx.html.TagConsumer
import kotlinx.html.a
import kotlinx.html.div
import kotlinx.html.form
import kotlinx.html.h1
import kotlinx.html.input
import kotlinx.html.label
import kotlinx.html.section
import kotlinx.html.table
import kotlinx.html.tbody
import kotlinx.html.td
import kotlinx.html.th
import kotlinx.html.thead
import kotlinx.html.tr

fun TagConsumer<*>.BackfillsTable(running: Boolean, backfills: List<UiBackfillRun>) {
fun TagConsumer<*>.BackfillsTable(
running: Boolean,
backfills: List<UiBackfillRun>,
showDeleted: Boolean = false,
) {
val title = if (running) "Running" else "Paused"

div("px-4 sm:px-6 lg:px-8 py-5") {
div("sm:flex sm:items-center") {
div("sm:flex-auto") {
h1("text-base font-semibold leading-6 text-gray-900") { +"""Backfills ($title)""" }
section("sm:flex sm:items-center justify-between") {
h1("text-base font-semibold leading-6 text-gray-900") { +"""Backfills ($title)""" }

if (!running) { // Only show toggle for Paused backfills
form {
label("text-sm font-medium text-gray-600 flex items-center") {
+"Show deleted"
input(classes = "ml-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600") {
type = InputType.checkBox
name = "showDeleted"
value = "true"
if (showDeleted) attributes["checked"] = "checked"
attributes["onchange"] = "this.form.submit()"
}
}
}
}
}
div("mt-8 flow-root") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import app.cash.backfila.ui.components.PageTitle
import app.cash.backfila.ui.components.Pagination
import app.cash.backfila.ui.components.ProgressBar
import app.cash.backfila.ui.pages.BackfillCreateAction.BackfillCreateField.CUSTOM_PARAMETER_PREFIX
import java.time.Instant
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.html.ButtonType
Expand Down Expand Up @@ -268,6 +269,7 @@ class BackfillShowAction @Inject constructor(
val button: Link? = null,
val updateFieldId: String? = null,
val cancelButton: Link? = null,
val deleteButton: Link? = null,
)

private fun getStateButton(state: BackfillState): Link? {
Expand Down Expand Up @@ -296,13 +298,27 @@ class BackfillShowAction @Inject constructor(
}
}

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
}
}

private fun GetBackfillStatusResponse.toConfigurationRows(id: Long) = listOf(
DescriptionListRow(
label = "State",
description = state.name,
button = getStateButton(state),
updateFieldId = "state",
cancelButton = getCancelButton(state),
deleteButton = getDeleteButton(state, deleted_at),
),
DescriptionListRow(
label = "Dry Run",
Expand Down Expand Up @@ -541,6 +557,35 @@ class BackfillShowAction @Inject constructor(
}
}
}

it.deleteButton?.let { deleteButton ->
span("ml-2") {
form {
action = BackfillShowButtonHandlerAction.path(id)

it.updateFieldId?.let {
input {
type = InputType.hidden
name = "field_id"
value = it
}

input {
type = InputType.hidden
name = "field_value"
value = deleteButton.href
}
}

button(
classes = "rounded-full bg-gray-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-gray-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-600",
) {
type = ButtonType.submit
+deleteButton.label
}
}
}
}
}
}
}
Expand Down Expand Up @@ -590,6 +635,7 @@ class BackfillShowAction @Inject constructor(
const val START_STATE_BUTTON_LABEL = "Start"
const val PAUSE_STATE_BUTTON_LABEL = "Pause"
const val CANCEL_STATE_BUTTON_LABEL = "Cancel"
const val DELETE_STATE_BUTTON_LABEL = "Delete"
const val UPDATE_BUTTON_LABEL = "Update"
const val VIEW_LOGS_BUTTON_LABEL = "View Logs"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class ServiceShowAction @Inject constructor(
@PathParam variantOrBlank: String? = "",
@QueryParam offset: String? = null,
@QueryParam lastOffset: String? = null,
@QueryParam showDeleted: Boolean = false,
): Response<ResponseBody> {
if (service.isNullOrBlank()) {
return Response(
Expand All @@ -55,7 +56,12 @@ class ServiceShowAction @Inject constructor(
}
val variant = variantOrBlank.orEmpty().ifBlank { "default" }

val backfillRuns = getBackfillRunsAction.backfillRuns(service, variant, offset)
val backfillRuns = getBackfillRunsAction.backfillRuns(
service = service,
variant = variant,
pagination_token = offset,
show_deleted = showDeleted,
)

// TODO show default if other variants and probably link to a switcher
val label = if (variant == "default") service else "$service ($variant)"
Expand All @@ -79,7 +85,7 @@ class ServiceShowAction @Inject constructor(
}

BackfillsTable(true, backfillRuns.running_backfills)
BackfillsTable(false, backfillRuns.paused_backfills)
BackfillsTable(false, backfillRuns.paused_backfills, showDeleted)
Pagination(backfillRuns.next_pagination_token, offset, lastOffset, path(service, variantOrBlank))
}
}
Expand Down
2 changes: 2 additions & 0 deletions service/src/main/resources/migrations/v019__backfila.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE backfill_runs
ADD COLUMN `deleted_at` timestamp(3) NULL DEFAULT NULL;
Loading