Skip to content

Commit 282cc30

Browse files
authored
add Backfill Search Dropdown Implementation (#512)
This change was added to the legacy UI as an additional functionality. We want the new UI to have this functionality as well. https://github.com/user-attachments/assets/ba9ede4c-bbf8-4afc-807a-22efe0c62583 https://github.com/user-attachments/assets/a420c368-c8a1-45a2-8127-d9637bffee7c
1 parent b510b35 commit 282cc30

File tree

6 files changed

+198
-14
lines changed

6 files changed

+198
-14
lines changed

service/src/main/kotlin/app/cash/backfila/client/EnvoyCallbackConnectorProvider.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ import com.squareup.moshi.Moshi
66
import java.net.URL
77
import javax.inject.Inject
88
import javax.inject.Singleton
9+
import misk.client.EnvoyClientEndpointProvider
10+
import misk.client.HttpClientEnvoyConfig
911
import misk.client.HttpClientFactory
1012
import misk.client.HttpClientsConfig
1113
import misk.moshi.adapter
1214
import retrofit2.Retrofit
1315
import retrofit2.adapter.guava.GuavaCallAdapterFactory
1416
import retrofit2.converter.wire.WireConverterFactory
15-
import misk.client.EnvoyClientEndpointProvider
16-
import misk.client.HttpClientEnvoyConfig
1717

1818
@Singleton
1919
class EnvoyCallbackConnectorProvider @Inject constructor(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package app.cash.backfila.dashboard
2+
3+
import app.cash.backfila.service.persistence.BackfilaDb
4+
import app.cash.backfila.service.persistence.RegisteredBackfillQuery
5+
import app.cash.backfila.service.persistence.ServiceQuery
6+
import javax.inject.Inject
7+
import misk.exceptions.BadRequestException
8+
import misk.hibernate.Query
9+
import misk.hibernate.Transacter
10+
import misk.hibernate.newQuery
11+
import misk.security.authz.Authenticated
12+
import misk.web.Get
13+
import misk.web.PathParam
14+
import misk.web.ResponseContentType
15+
import misk.web.actions.WebAction
16+
import misk.web.mediatype.MediaTypes
17+
18+
data class GetBackfillNamesResponse(
19+
val backfill_names: List<String>,
20+
)
21+
22+
class GetBackfillNamesAction @Inject constructor(
23+
@BackfilaDb private val transacter: Transacter,
24+
private val queryFactory: Query.Factory,
25+
) : WebAction {
26+
@Get("/services/{service}/variants/{variant}/backfill-names")
27+
@ResponseContentType(MediaTypes.APPLICATION_JSON)
28+
@Authenticated(allowAnyUser = true)
29+
fun getBackfillNames(
30+
@PathParam service: String,
31+
@PathParam variant: String,
32+
): GetBackfillNamesResponse {
33+
return transacter.transaction { session ->
34+
val dbService = queryFactory.newQuery<ServiceQuery>()
35+
.registryName(service)
36+
.variant(variant)
37+
.uniqueResult(session) ?: throw BadRequestException("`$service`-`$variant` doesn't exist")
38+
39+
val backfillNames = queryFactory.newQuery<RegisteredBackfillQuery>()
40+
.serviceId(dbService.id)
41+
.list(session)
42+
.map { it.name }
43+
.distinct()
44+
.sorted()
45+
46+
GetBackfillNamesResponse(backfill_names = backfillNames)
47+
}
48+
}
49+
}

service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package app.cash.backfila.ui
22

3+
import app.cash.backfila.dashboard.GetBackfillNamesAction
34
import app.cash.backfila.ui.actions.BackfillCreateHandlerAction
45
import app.cash.backfila.ui.actions.BackfillShowButtonHandlerAction
56
import app.cash.backfila.ui.pages.BackfillCreateAction
@@ -28,6 +29,7 @@ class UiModule : KAbstractModule() {
2829
// Other
2930
install(WebActionModule.create<BackfillCreateHandlerAction>())
3031
install(WebActionModule.create<BackfillShowButtonHandlerAction>())
32+
install(WebActionModule.create<GetBackfillNamesAction>())
3133
}
3234
}
3335

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package app.cash.backfila.ui.components
2+
3+
import kotlinx.html.ButtonType
4+
import kotlinx.html.InputType
5+
import kotlinx.html.TagConsumer
6+
import kotlinx.html.button
7+
import kotlinx.html.div
8+
import kotlinx.html.form
9+
import kotlinx.html.input
10+
import kotlinx.html.label
11+
import kotlinx.html.script
12+
import kotlinx.html.unsafe
13+
14+
fun TagConsumer<*>.BackfillSearchForm(
15+
backfillName: String? = null,
16+
createdByUser: String? = null,
17+
serviceName: String,
18+
variantName: String,
19+
) {
20+
div("px-4 sm:px-6 lg:px-8 py-4 bg-gray-50 border-b border-gray-200") {
21+
form(classes = "flex flex-wrap gap-4 items-end") {
22+
method = kotlinx.html.FormMethod.get
23+
attributes["data-turbo-frame"] = "_top"
24+
25+
// Backfill Name Search with datalist (native autocomplete)
26+
div("flex-1 min-w-0") {
27+
label("block text-sm font-medium text-gray-700 mb-1") {
28+
htmlFor = "backfill_name"
29+
+"Backfill Name"
30+
}
31+
input(classes = "block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm") {
32+
type = InputType.text
33+
attributes["id"] = "backfill_name"
34+
name = "backfill_name"
35+
placeholder = "Type to search backfills..."
36+
value = backfillName ?: ""
37+
attributes["list"] = "backfill-names"
38+
attributes["autocomplete"] = "off"
39+
}
40+
// Native HTML5 datalist for autocomplete (using unsafe HTML since datalist isn't in kotlinx.html)
41+
unsafe {
42+
+"""<datalist id="backfill-names"></datalist>"""
43+
}
44+
}
45+
46+
// Created By User Search
47+
div("flex-1 min-w-0") {
48+
label("block text-sm font-medium text-gray-700 mb-1") {
49+
htmlFor = "created_by_user"
50+
+"Created by"
51+
}
52+
input(classes = "block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm") {
53+
type = InputType.text
54+
attributes["id"] = "created_by_user"
55+
name = "created_by_user"
56+
placeholder = "test.user"
57+
value = createdByUser ?: ""
58+
}
59+
}
60+
61+
// Search and Clear buttons
62+
div("flex gap-2") {
63+
button(classes = "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500") {
64+
type = ButtonType.submit
65+
+"FILTER"
66+
}
67+
68+
button(classes = "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500") {
69+
type = ButtonType.button
70+
attributes["id"] = "clear_filters_btn"
71+
+"CLEAR FILTERS"
72+
}
73+
}
74+
}
75+
76+
// JavaScript to populate datalist and handle clear filters
77+
script {
78+
unsafe {
79+
+"""
80+
function initBackfillForm() {
81+
const datalist = document.getElementById('backfill-names');
82+
const clearFiltersBtn = document.getElementById('clear_filters_btn');
83+
84+
if (!datalist) return;
85+
86+
// Fetch and populate backfill names
87+
const url = '/services/$serviceName/variants/$variantName/backfill-names';
88+
fetch(url)
89+
.then(response => response.json())
90+
.then(data => {
91+
datalist.innerHTML = '';
92+
if (data.backfill_names) {
93+
data.backfill_names.forEach(name => {
94+
const option = document.createElement('option');
95+
option.value = name;
96+
datalist.appendChild(option);
97+
});
98+
}
99+
})
100+
.catch(err => console.error('Error loading backfill names:', err));
101+
102+
// Clear filters button handler
103+
if (clearFiltersBtn && !clearFiltersBtn.hasAttribute('data-initialized')) {
104+
clearFiltersBtn.setAttribute('data-initialized', 'true');
105+
clearFiltersBtn.addEventListener('click', function() {
106+
let baseUrl = '/services/$serviceName';
107+
if ('$variantName' !== 'default') {
108+
baseUrl += '/$variantName';
109+
}
110+
window.location.href = baseUrl;
111+
});
112+
}
113+
}
114+
115+
// Run on page load
116+
if (document.readyState === 'loading') {
117+
document.addEventListener('DOMContentLoaded', initBackfillForm);
118+
} else {
119+
initBackfillForm();
120+
}
121+
""".trimIndent()
122+
}
123+
}
124+
}
125+
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,12 @@ class BackfillShowAction @Inject constructor(
211211
if (it.isNaN()) 0.0 else it
212212
}
213213
+"""${String.format("%.1f", percentage)}%"""
214-
} else
215-
if (backfill.state == BackfillState.COMPLETE) {
216-
+"""Complete"""
217214
} else {
218-
+"""Computing..."""
215+
if (backfill.state == BackfillState.COMPLETE) {
216+
+"""Complete"""
217+
} else {
218+
+"""Computing..."""
219+
}
219220
}
220221
}
221222
}

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package app.cash.backfila.ui.pages
22

33
import app.cash.backfila.dashboard.GetBackfillRunsAction
44
import app.cash.backfila.ui.components.AutoReload
5+
import app.cash.backfila.ui.components.BackfillSearchForm
56
import app.cash.backfila.ui.components.BackfillsTable
67
import app.cash.backfila.ui.components.DashboardPageLayout
78
import app.cash.backfila.ui.components.PageTitle
@@ -47,6 +48,8 @@ class ServiceShowAction @Inject constructor(
4748
@QueryParam lastOffset: String? = null,
4849
@QueryParam history: String? = null,
4950
@QueryParam showDeleted: Boolean = false,
51+
@QueryParam backfill_name: String? = null,
52+
@QueryParam created_by_user: String? = null,
5053
): Response<ResponseBody> {
5154
if (service.isNullOrBlank()) {
5255
return Response(
@@ -62,6 +65,8 @@ class ServiceShowAction @Inject constructor(
6265
variant = variant,
6366
pagination_token = offset,
6467
show_deleted = showDeleted,
68+
backfill_name = backfill_name,
69+
created_by_user = created_by_user,
6570
)
6671

6772
// TODO show default if other variants and probably link to a switcher
@@ -73,18 +78,20 @@ class ServiceShowAction @Inject constructor(
7378
Link(label, path),
7479
)
7580
.buildHtmlResponseBody {
76-
AutoReload(frameId = "backfill-$service-status") {
77-
PageTitle("Service", label) {
78-
a {
79-
href = BackfillCreateServiceIndexAction.path(service, variantOrBlank)
81+
PageTitle("Service", label) {
82+
a {
83+
href = BackfillCreateServiceIndexAction.path(service, variantOrBlank)
8084

81-
button(classes = "rounded-full bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600") {
82-
type = ButtonType.button
83-
+"""Create"""
84-
}
85+
button(classes = "rounded-full bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600") {
86+
type = ButtonType.button
87+
+"""Create"""
8588
}
8689
}
90+
}
8791

92+
BackfillSearchForm(backfill_name, created_by_user, service, variant)
93+
94+
AutoReload(frameId = "backfill-$service-status") {
8895
BackfillsTable(true, backfillRuns.running_backfills)
8996
BackfillsTable(false, backfillRuns.paused_backfills, showDeleted)
9097
PaginationWithHistory(backfillRuns.next_pagination_token, offset, history, path(service, variantOrBlank))

0 commit comments

Comments
 (0)