Skip to content

Commit 744fd0e

Browse files
committed
move state out of config
1 parent e3e4666 commit 744fd0e

File tree

2 files changed

+182
-104
lines changed

2 files changed

+182
-104
lines changed

service/src/main/kotlin/app/cash/backfila/ui/actions/BackfillShowButtonHandlerAction.kt

Lines changed: 162 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,25 @@ import app.cash.backfila.dashboard.UpdateBackfillRequest
1111
import app.cash.backfila.service.persistence.BackfillState
1212
import app.cash.backfila.ui.components.AlertError
1313
import app.cash.backfila.ui.components.DashboardPageLayout
14+
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.CANCEL_STATE_BUTTON_LABEL
15+
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.DELETE_STATE_BUTTON_LABEL
16+
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.PAUSE_STATE_BUTTON_LABEL
17+
import app.cash.backfila.ui.pages.BackfillShowAction.Companion.START_STATE_BUTTON_LABEL
18+
import java.time.Instant
1419
import javax.inject.Inject
1520
import javax.inject.Singleton
21+
import kotlinx.html.ButtonType
22+
import kotlinx.html.InputType
23+
import kotlinx.html.TagConsumer
24+
import kotlinx.html.button
1625
import kotlinx.html.div
26+
import kotlinx.html.form
27+
import kotlinx.html.h2
28+
import kotlinx.html.input
29+
import kotlinx.html.span
1730
import misk.security.authz.Authenticated
31+
import misk.tailwind.Link
32+
import misk.turbo.turbo_frame
1833
import misk.web.Get
1934
import misk.web.PathParam
2035
import misk.web.QueryParam
@@ -46,80 +61,171 @@ class BackfillShowButtonHandlerAction @Inject constructor(
4661
): Response<ResponseBody> {
4762
try {
4863
if (!field_id.isNullOrBlank()) {
49-
when (field_id) {
50-
"state" -> {
51-
when (field_value) {
52-
BackfillState.PAUSED.name -> {
53-
stopBackfillAction.stop(id.toLong(), StopBackfillRequest())
54-
}
55-
BackfillState.RUNNING.name -> {
56-
startBackfillAction.start(id.toLong(), StartBackfillRequest())
57-
}
58-
BackfillState.CANCELLED.name -> {
59-
cancelBackfillAction.cancel(id.toLong())
60-
}
61-
"soft_delete" -> {
62-
softDeleteBackfillAction.softDelete(id.toLong())
63-
}
64-
}
65-
}
64+
handleFieldUpdate(id.toLong(), field_id, field_value)
65+
}
66+
} catch (e: Exception) {
67+
return handleError(e)
68+
}
6669

67-
"num_threads" -> {
68-
val numThreads = field_value?.toIntOrNull()
69-
if (numThreads != null) {
70-
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(num_threads = numThreads))
71-
}
72-
}
70+
return when (field_id) {
71+
"state" -> handleStateFrameResponse(id, field_value)
72+
else -> handleRedirectResponse(id)
73+
}
74+
}
7375

74-
"scan_size" -> {
75-
val scanSize = field_value?.toLongOrNull()
76-
if (scanSize != null) {
77-
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(scan_size = scanSize))
78-
}
79-
}
76+
private fun handleFieldUpdate(id: Long, fieldId: String, fieldValue: String?) {
77+
when (fieldId) {
78+
"state" -> handleStateUpdate(id, fieldValue)
79+
else -> handleConfigUpdate(id, fieldId, fieldValue)
80+
}
81+
}
8082

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

88-
"extra_sleep_ms" -> {
89-
val extraSleepMs = field_value?.toLongOrNull()
90-
if (extraSleepMs != null) {
91-
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(extra_sleep_ms = extraSleepMs))
92-
}
93-
}
92+
private fun handleConfigUpdate(id: Long, fieldId: String, value: String?) {
93+
val request = when (fieldId) {
94+
"num_threads" -> value?.toIntOrNull()?.let { UpdateBackfillRequest(num_threads = it) }
95+
"scan_size" -> value?.toLongOrNull()?.let { UpdateBackfillRequest(scan_size = it) }
96+
"batch_size" -> value?.toLongOrNull()?.let { UpdateBackfillRequest(batch_size = it) }
97+
"extra_sleep_ms" -> value?.toLongOrNull()?.let { UpdateBackfillRequest(extra_sleep_ms = it) }
98+
"backoff_schedule" -> value?.let { UpdateBackfillRequest(backoff_schedule = it) }
99+
else -> null
100+
}
101+
request?.let { updateBackfillAction.update(id, it) }
102+
}
94103

95-
"backoff_schedule" -> {
96-
updateBackfillAction.update(id.toLong(), UpdateBackfillRequest(backoff_schedule = field_value))
97-
}
104+
private fun handleError(e: Exception): Response<ResponseBody> {
105+
logger.error(e) { "Update backfill field failed $e" }
106+
val errorHtmlResponseBody = dashboardPageLayout.newBuilder()
107+
.buildHtmlResponseBody {
108+
div("py-20") {
109+
AlertError(message = "Update backfill field failed: $e", label = "Try Again", onClick = "history.back(); return false;")
98110
}
99111
}
100-
} catch (e: Exception) {
101-
// Since this action is only hit from the UI, catch any validation errors and show them to the user
102-
val errorHtmlResponseBody = dashboardPageLayout.newBuilder()
103-
.buildHtmlResponseBody {
104-
div("py-20") {
105-
AlertError(message = "Update backfill field failed: $e", label = "Try Again", onClick = "history.back(); return false;")
112+
return Response(
113+
body = errorHtmlResponseBody,
114+
statusCode = 200,
115+
headers = Headers.headersOf("Content-Type", MediaTypes.TEXT_HTML),
116+
)
117+
}
118+
119+
private fun handleStateFrameResponse(id: String, fieldValue: String?): Response<ResponseBody> {
120+
val currentState = when (fieldValue) {
121+
BackfillState.PAUSED.name -> BackfillState.PAUSED
122+
BackfillState.RUNNING.name -> BackfillState.RUNNING
123+
BackfillState.CANCELLED.name -> BackfillState.CANCELLED
124+
"soft_delete" -> BackfillState.COMPLETE
125+
else -> BackfillState.RUNNING
126+
}
127+
128+
val frameContent = dashboardPageLayout.newBuilder()
129+
.buildHtmlResponseBody {
130+
turbo_frame("backfill-$id-state") {
131+
div("flex justify-between items-center") {
132+
h2("text-base font-semibold leading-6 text-gray-900") { +"State" }
133+
div("flex gap-2") {
134+
span("text-gray-700") { +currentState.name }
135+
renderStateButtons(id, currentState)
136+
}
106137
}
107138
}
108-
logger.error(e) { "Update backfill field failed $e" }
109-
return Response(
110-
body = errorHtmlResponseBody,
111-
statusCode = 200,
112-
headers = Headers.headersOf("Content-Type", MediaTypes.TEXT_HTML),
113-
)
139+
}
140+
141+
return Response(
142+
body = frameContent,
143+
statusCode = 200,
144+
headers = Headers.headersOf("Content-Type", MediaTypes.TEXT_HTML),
145+
)
146+
}
147+
148+
fun TagConsumer<*>.renderStateButtons(id: String, currentState: BackfillState, deletedAt: Instant? = null) {
149+
getStateButton(currentState)?.let { button ->
150+
renderButton(id, "state", button, if (button.label == START_STATE_BUTTON_LABEL) "green" else "yellow")
151+
}
152+
getCancelButton(currentState)?.let { button ->
153+
renderButton(id, "state", button, "red")
154+
}
155+
getDeleteButton(currentState, deletedAt)?.let { button ->
156+
renderButton(id, "state", button, "gray")
157+
}
158+
}
159+
160+
fun TagConsumer<*>.renderButton(id: String, fieldId: String, button: Link, color: String) {
161+
form {
162+
action = path(id)
163+
input {
164+
type = InputType.hidden
165+
name = "field_id"
166+
value = fieldId
167+
}
168+
input {
169+
type = InputType.hidden
170+
name = "field_value"
171+
value = button.href
172+
}
173+
button(
174+
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",
175+
) {
176+
type = ButtonType.submit
177+
+button.label
178+
}
114179
}
180+
}
115181

182+
private fun handleRedirectResponse(id: String): Response<ResponseBody> {
116183
return Response(
117184
body = "go to /backfills/$id".toResponseBody(),
118185
statusCode = 303,
119186
headers = Headers.headersOf("Location", "/backfills/$id"),
120187
)
121188
}
122189

190+
private fun getStateButton(state: BackfillState): Link? {
191+
return when (state) {
192+
BackfillState.PAUSED -> Link(
193+
label = START_STATE_BUTTON_LABEL,
194+
href = BackfillState.RUNNING.name,
195+
)
196+
// COMPLETE and CANCELLED represent final states.
197+
BackfillState.COMPLETE -> null
198+
BackfillState.CANCELLED -> null
199+
else -> Link(
200+
label = PAUSE_STATE_BUTTON_LABEL,
201+
href = BackfillState.PAUSED.name,
202+
)
203+
}
204+
}
205+
206+
private fun getCancelButton(state: BackfillState): Link? {
207+
return when (state) {
208+
BackfillState.PAUSED -> Link(
209+
label = CANCEL_STATE_BUTTON_LABEL,
210+
href = BackfillState.CANCELLED.name,
211+
)
212+
else -> null
213+
}
214+
}
215+
216+
private fun getDeleteButton(state: BackfillState, deletedAt: Instant?): Link? {
217+
if (deletedAt != null) {
218+
return null
219+
}
220+
return when (state) {
221+
BackfillState.COMPLETE, BackfillState.CANCELLED -> Link(
222+
label = DELETE_STATE_BUTTON_LABEL,
223+
href = "soft_delete",
224+
)
225+
else -> null
226+
}
227+
}
228+
123229
companion object {
124230
private val logger = getLogger<BackfillShowButtonHandlerAction>()
125231

service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillShowAction.kt

Lines changed: 20 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import app.cash.backfila.ui.components.PageTitle
1212
import app.cash.backfila.ui.components.Pagination
1313
import app.cash.backfila.ui.components.ProgressBar
1414
import app.cash.backfila.ui.pages.BackfillCreateAction.BackfillCreateField.CUSTOM_PARAMETER_PREFIX
15-
import java.time.Instant
1615
import javax.inject.Inject
1716
import javax.inject.Singleton
1817
import kotlinx.html.ButtonType
@@ -37,6 +36,7 @@ import kotlinx.html.thead
3736
import kotlinx.html.tr
3837
import misk.security.authz.Authenticated
3938
import misk.tailwind.Link
39+
import misk.turbo.turbo_frame
4040
import misk.web.Get
4141
import misk.web.PathParam
4242
import misk.web.QueryParam
@@ -51,6 +51,7 @@ class BackfillShowAction @Inject constructor(
5151
private val getBackfillStatusAction: GetBackfillStatusAction,
5252
private val dashboardPageLayout: DashboardPageLayout,
5353
private val viewLogsAction: ViewLogsAction,
54+
private val backfillShowButtonHandlerAction: BackfillShowButtonHandlerAction,
5455
) : WebAction {
5556
@Get(PATH)
5657
@ResponseContentType(MediaTypes.TEXT_HTML)
@@ -102,6 +103,24 @@ class BackfillShowAction @Inject constructor(
102103
}
103104
}
104105

106+
// State section with its own auto-reload
107+
AutoReload(frameId = "backfill-$id-state") {
108+
Card {
109+
turbo_frame("backfill-$id-state") {
110+
div("flex justify-between items-center") {
111+
h2("text-base font-semibold leading-6 text-gray-900") { +"State" }
112+
div("flex gap-2") {
113+
span("text-gray-700") { +backfill.state.name }
114+
with(backfillShowButtonHandlerAction) {
115+
renderStateButtons(id.toString(), backfill.state, backfill.deleted_at)
116+
}
117+
}
118+
}
119+
}
120+
}
121+
}
122+
123+
// Configuration section
105124
Card {
106125
div("mx-auto grid max-w-2xl grid-cols-1 grid-rows-1 items-start gap-x-24 gap-y-8 lg:mx-0 lg:max-w-none lg:grid-cols-2") {
107126
// <!-- Left Column -->"""
@@ -298,54 +317,7 @@ class BackfillShowAction @Inject constructor(
298317
val deleteButton: Link? = null,
299318
)
300319

301-
private fun getStateButton(state: BackfillState): Link? {
302-
return when (state) {
303-
BackfillState.PAUSED -> Link(
304-
label = START_STATE_BUTTON_LABEL,
305-
href = BackfillState.RUNNING.name,
306-
)
307-
// COMPLETE and CANCELLED represent final states.
308-
BackfillState.COMPLETE -> null
309-
BackfillState.CANCELLED -> null
310-
else -> Link(
311-
label = PAUSE_STATE_BUTTON_LABEL,
312-
href = BackfillState.PAUSED.name,
313-
)
314-
}
315-
}
316-
317-
private fun getCancelButton(state: BackfillState): Link? {
318-
return when (state) {
319-
BackfillState.PAUSED -> Link(
320-
label = CANCEL_STATE_BUTTON_LABEL,
321-
href = BackfillState.CANCELLED.name,
322-
)
323-
else -> null
324-
}
325-
}
326-
327-
private fun getDeleteButton(state: BackfillState, deletedAt: Instant?): Link? {
328-
if (deletedAt != null) {
329-
return null
330-
}
331-
return when (state) {
332-
BackfillState.COMPLETE, BackfillState.CANCELLED -> Link(
333-
label = DELETE_STATE_BUTTON_LABEL,
334-
href = "soft_delete",
335-
)
336-
else -> null
337-
}
338-
}
339-
340320
private fun GetBackfillStatusResponse.toConfigurationRows(id: Long) = listOf(
341-
DescriptionListRow(
342-
label = "State",
343-
description = state.name,
344-
button = getStateButton(state),
345-
updateFieldId = "state",
346-
cancelButton = getCancelButton(state),
347-
deleteButton = getDeleteButton(state, deleted_at),
348-
),
349321
DescriptionListRow(
350322
label = "Dry Run",
351323
description = if (dry_run) "dry run" else "wet run",

0 commit comments

Comments
 (0)