@@ -11,10 +11,25 @@ import app.cash.backfila.dashboard.UpdateBackfillRequest
11
11
import app.cash.backfila.service.persistence.BackfillState
12
12
import app.cash.backfila.ui.components.AlertError
13
13
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
14
19
import javax.inject.Inject
15
20
import javax.inject.Singleton
21
+ import kotlinx.html.ButtonType
22
+ import kotlinx.html.InputType
23
+ import kotlinx.html.TagConsumer
24
+ import kotlinx.html.button
16
25
import kotlinx.html.div
26
+ import kotlinx.html.form
27
+ import kotlinx.html.h2
28
+ import kotlinx.html.input
29
+ import kotlinx.html.span
17
30
import misk.security.authz.Authenticated
31
+ import misk.tailwind.Link
32
+ import misk.turbo.turbo_frame
18
33
import misk.web.Get
19
34
import misk.web.PathParam
20
35
import misk.web.QueryParam
@@ -46,80 +61,171 @@ class BackfillShowButtonHandlerAction @Inject constructor(
46
61
): Response <ResponseBody > {
47
62
try {
48
63
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
+ }
66
69
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
+ }
73
75
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
+ }
80
82
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
+ }
87
91
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
+ }
94
103
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;" )
98
110
}
99
111
}
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
+ }
106
137
}
107
138
}
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
+ }
114
179
}
180
+ }
115
181
182
+ private fun handleRedirectResponse (id : String ): Response <ResponseBody > {
116
183
return Response (
117
184
body = " go to /backfills/$id " .toResponseBody(),
118
185
statusCode = 303 ,
119
186
headers = Headers .headersOf(" Location" , " /backfills/$id " ),
120
187
)
121
188
}
122
189
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
+
123
229
companion object {
124
230
private val logger = getLogger<BackfillShowButtonHandlerAction >()
125
231
0 commit comments