From f75f16ec5e6d369e6cf8e33b6fba7979ac373cbc Mon Sep 17 00:00:00 2001 From: Andrew Alexander Date: Wed, 26 Feb 2025 12:26:45 -0800 Subject: [PATCH 1/2] Add pagintion to backfills index and service show pages --- .gitignore | 1 + gradle.properties | 8 ++ .../cash/backfila/ui/components/PageTitle.kt | 2 +- .../cash/backfila/ui/components/Pagination.kt | 54 ++++++++++++ .../ui/components/ServiceSelection.kt | 2 +- .../pages/BackfillCreateServiceIndexAction.kt | 2 +- .../backfila/ui/pages/BackfillIndexAction.kt | 28 ++++-- .../app/cash/backfila/ui/pages/IndexAction.kt | 2 +- .../backfila/ui/pages/ServiceShowAction.kt | 13 ++- .../app/cash/backfila/PaginationTest.kt | 88 +++++++++++++++++++ 10 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 service/src/main/kotlin/app/cash/backfila/ui/components/Pagination.kt create mode 100644 service/src/test/kotlin/app/cash/backfila/PaginationTest.kt diff --git a/.gitignore b/.gitignore index 6ba9a1ebd..f32719ca1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/ *.iml *.swp service/web/**/lib/ +**/.kotlin **/site/ docs/0.x/* diff --git a/gradle.properties b/gradle.properties index f325b3250..138465190 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,11 @@ GROUP=app.cash.backfila org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=5g -Dfile.encoding=UTF-8 + +org.gradle.caching=true +org.gradle.configuration-cache=true +org.gradle.vfs.watch=true +org.gradle.configureondemand=true +org.gradle.parallel=true + +misk.test.logging=false diff --git a/service/src/main/kotlin/app/cash/backfila/ui/components/PageTitle.kt b/service/src/main/kotlin/app/cash/backfila/ui/components/PageTitle.kt index 68244a0a7..d0b0aa3e5 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/components/PageTitle.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/components/PageTitle.kt @@ -7,7 +7,7 @@ import kotlinx.html.span fun TagConsumer<*>.PageTitle(title: String, subtitle: String? = null, smallerSubtitle: String? = null, floatRightBlock: TagConsumer<*>.() -> Unit = {}) { header { - div("mx-auto max-w-7xl px-200 sm:px-6 lg:px-8s py-10") { + div("mx-auto max-w-7xl px-200 sm:px-6 lg:px-8s py-5") { val maybeSubtitleSuffix = subtitle?.let { ": " } ?: "" span("text-3xl font-bold leading-tight tracking-tight text-gray-900") { +"$title$maybeSubtitleSuffix" } subtitle?.let { span("text-3xl font-bold leading-tight tracking-tight text-green-600") { +it } } diff --git a/service/src/main/kotlin/app/cash/backfila/ui/components/Pagination.kt b/service/src/main/kotlin/app/cash/backfila/ui/components/Pagination.kt new file mode 100644 index 000000000..5b3653458 --- /dev/null +++ b/service/src/main/kotlin/app/cash/backfila/ui/components/Pagination.kt @@ -0,0 +1,54 @@ +package app.cash.backfila.ui.components + +import kotlinx.html.TagConsumer +import kotlinx.html.a +import kotlinx.html.div +import kotlinx.html.nav +import misk.tailwind.icons.Heroicons +import misk.tailwind.icons.heroicon + +fun TagConsumer<*>.Pagination( + nextOffset: String?, + offset: String?, + lastOffset: String?, + basePath: String, +) { + nav("mt-12 flex items-center justify-between border-t border-gray-200 px-4 sm:px-0") { + lastOffset?.let { + div("-mt-px flex w-0 flex-1") { + a(classes = "inline-flex items-center border-t-2 border-transparent pt-4 pr-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700") { + href = basePath.appendOffsets(lastOffset) + heroicon(Heroicons.MINI_ARROW_LONG_LEFT) + +"""Previous""" + } + } + } + + if (nextOffset != null) { + div("-mt-px flex w-0 flex-1 justify-end") { + a(classes = "inline-flex items-center border-t-2 border-transparent pt-4 pl-1 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700") { + href = basePath.appendOffsets(nextOffset, offset ?: "") + +"""Next""" + heroicon(Heroicons.MINI_ARROW_LONG_RIGHT) + } + } + } + } +} + +fun String.appendOffsets(nextOffset: String? = null, lastOffset: String? = null): String { + val pathBuilder = StringBuilder(this) + if (nextOffset?.isNotBlank() == true) { + pathBuilder.append("?offset=").append(nextOffset) + } + + if (lastOffset != null) { + if (pathBuilder.contains("?")) { + pathBuilder.append("&") + } else { + pathBuilder.append("?") + } + pathBuilder.append("lastOffset=").append(lastOffset) + } + return pathBuilder.toString() +} diff --git a/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceSelection.kt b/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceSelection.kt index 6730a7062..a0094f40d 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceSelection.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/components/ServiceSelection.kt @@ -31,7 +31,7 @@ fun TagConsumer<*>.ServiceSelect( placeholder = "Search" } } - div("py-10") { + div("py-5") { ul("grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3") { role = "list" diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillCreateServiceIndexAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillCreateServiceIndexAction.kt index 6f1075680..d4cc2545b 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillCreateServiceIndexAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillCreateServiceIndexAction.kt @@ -85,7 +85,7 @@ class BackfillCreateServiceIndexAction @Inject constructor( AlertError("No backfills registered for this service. Check docs for how to register backfills.") } else { // If service + variant is set and valid, show registered backfills drop down - div("py-10") { + div("py-5") { ul("grid grid-cols-1 gap-6") { role = "list" diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillIndexAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillIndexAction.kt index 0c0c8149a..13c3aa4dc 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillIndexAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillIndexAction.kt @@ -5,6 +5,7 @@ import app.cash.backfila.service.persistence.BackfillRunQuery import app.cash.backfila.service.persistence.ServiceQuery import app.cash.backfila.ui.components.DashboardPageLayout import app.cash.backfila.ui.components.PageTitle +import app.cash.backfila.ui.components.Pagination import javax.inject.Inject import javax.inject.Singleton import kotlinx.html.ButtonType @@ -19,8 +20,13 @@ import kotlinx.html.ul import misk.hibernate.Query import misk.hibernate.Transacter import misk.hibernate.newQuery +import misk.hibernate.pagination.Offset +import misk.hibernate.pagination.Page +import misk.hibernate.pagination.idDescPaginator +import misk.hibernate.pagination.newPager import misk.security.authz.Authenticated import misk.web.Get +import misk.web.QueryParam import misk.web.Response import misk.web.ResponseBody import misk.web.ResponseContentType @@ -36,14 +42,18 @@ class BackfillIndexAction @Inject constructor( @Get(PATH) @ResponseContentType(MediaTypes.TEXT_HTML) @Authenticated(capabilities = ["users"]) - fun get(): Response { - val backfills = transacter.transaction { session -> + fun get( + @QueryParam offset: String? = null, + @QueryParam lastOffset: String? = null, + ): Response { + val (backfills, nextOffset) = transacter.transaction { session -> queryFactory.newQuery() - .orderByUpdatedAtDesc() - .apply { - maxRows = 10 - } - .list(session) + .newPager( + idDescPaginator(), + initialOffset = offset?.let { Offset(it) }, + pageSize = 12, + ) + .nextPage(session) ?: Page.empty() } return Response( @@ -62,7 +72,7 @@ class BackfillIndexAction @Inject constructor( } // List of Services - div("py-10") { + div("py-5") { ul("grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3") { role = "list" @@ -98,6 +108,8 @@ class BackfillIndexAction @Inject constructor( } } } + + Pagination(nextOffset?.offset, offset, lastOffset, PATH) } }, ) diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/IndexAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/IndexAction.kt index 67f6cfd15..fd55c0275 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/IndexAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/IndexAction.kt @@ -43,7 +43,7 @@ class IndexAction @Inject constructor( // Stats - div("py-10") { + div("py-5") { h3("text-base font-semibold text-gray-900") { +"""Last 30 days""" } dl("mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3") { div("overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6") { diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt index 18dc652cb..00dc61433 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt @@ -5,9 +5,7 @@ import app.cash.backfila.ui.components.AutoReload import app.cash.backfila.ui.components.BackfillsTable import app.cash.backfila.ui.components.DashboardPageLayout import app.cash.backfila.ui.components.PageTitle -import java.net.HttpURLConnection -import javax.inject.Inject -import javax.inject.Singleton +import app.cash.backfila.ui.components.Pagination import kotlinx.html.ButtonType import kotlinx.html.a import kotlinx.html.button @@ -17,6 +15,7 @@ import misk.tailwind.Link import misk.web.Get import misk.web.HttpCall import misk.web.PathParam +import misk.web.QueryParam import misk.web.Response import misk.web.ResponseBody import misk.web.ResponseContentType @@ -24,6 +23,9 @@ import misk.web.actions.WebAction import misk.web.mediatype.MediaTypes import misk.web.toResponseBody import okhttp3.Headers +import java.net.HttpURLConnection +import javax.inject.Inject +import javax.inject.Singleton @Singleton class ServiceShowAction @Inject constructor( @@ -41,6 +43,8 @@ class ServiceShowAction @Inject constructor( fun get( @PathParam service: String?, @PathParam variantOrBlank: String? = "", + @QueryParam offset: String? = null, + @QueryParam lastOffset: String? = null, ): Response { if (service.isNullOrBlank()) { return Response( @@ -51,7 +55,7 @@ class ServiceShowAction @Inject constructor( } val variant = variantOrBlank.orEmpty().ifBlank { "default" } - val backfillRuns = getBackfillRunsAction.backfillRuns(service, variant) + val backfillRuns = getBackfillRunsAction.backfillRuns(service, variant, offset) // TODO show default if other variants and probably link to a switcher val label = if (variant == "default") service else "$service ($variant)" @@ -76,6 +80,7 @@ class ServiceShowAction @Inject constructor( BackfillsTable(true, backfillRuns.running_backfills) BackfillsTable(false, backfillRuns.paused_backfills) + Pagination(backfillRuns.next_pagination_token, offset, lastOffset, path(service, variantOrBlank)) } } diff --git a/service/src/test/kotlin/app/cash/backfila/PaginationTest.kt b/service/src/test/kotlin/app/cash/backfila/PaginationTest.kt new file mode 100644 index 000000000..14795c3ca --- /dev/null +++ b/service/src/test/kotlin/app/cash/backfila/PaginationTest.kt @@ -0,0 +1,88 @@ +package app.cash.backfila + +import app.cash.backfila.api.ConfigureServiceAction +import app.cash.backfila.api.ConfigureServiceAction.Companion.RESERVED_VARIANT +import app.cash.backfila.client.Connectors +import app.cash.backfila.dashboard.CreateBackfillAction +import app.cash.backfila.protos.service.ConfigureServiceRequest +import app.cash.backfila.protos.service.CreateBackfillRequest +import app.cash.backfila.service.persistence.BackfilaDb +import app.cash.backfila.service.persistence.BackfillRunQuery +import jakarta.inject.Inject +import misk.hibernate.Query +import misk.hibernate.Transacter +import misk.hibernate.newQuery +import misk.hibernate.pagination.idDescPaginator +import misk.hibernate.pagination.newPager +import misk.scope.ActionScope +import misk.testing.MiskTest +import misk.testing.MiskTestModule +import org.junit.jupiter.api.Test + +@MiskTest(startService = true) +class PaginationTest { + @MiskTestModule + private val module = BackfilaTestingModule() + + @Inject @BackfilaDb + private lateinit var transacter: Transacter + + @Inject private lateinit var queryFactory: Query.Factory + + @Inject private lateinit var configureServiceAction: ConfigureServiceAction + + @Inject private lateinit var createBackfillAction: CreateBackfillAction + + @Inject private lateinit var scope: ActionScope + + @Test + fun happyPath() { + // Seed rows + seedData() + + // Test pagination + val allRows = transacter.transaction { session -> + queryFactory.newQuery() + .list(session) + } + val rows = transacter.transaction { session -> + val pager = queryFactory.newQuery() + .orderByUpdatedAtDesc() + .newPager(idDescPaginator(), pageSize = 10) + pager.nextPage(session) + ?.contents + } + + val a = "a" + } + + fun seedData() { + scope.fakeCaller(service = "deep-fryer") { + configureServiceAction.configureService( + ConfigureServiceRequest.Builder() + .backfills( + listOf( + ConfigureServiceRequest.BackfillData( + "ChickenSandwich", "Description", listOf(), null, + null, false, null, + ), + ), + ) + .connector_type(Connectors.ENVOY) + .build(), + ) + } + + for (i in 0..100) { + scope.fakeCaller(user = "molly") { + val response = createBackfillAction.create( + "deep-fryer", + RESERVED_VARIANT, + CreateBackfillRequest.Builder() + .backfill_name("ChickenSandwich") + .build(), + ) + } + } + } +} From e7322f467ae86266c4baf8b1e523aa255ebff6e4 Mon Sep 17 00:00:00 2001 From: Andrew Alexander Date: Wed, 26 Feb 2025 21:06:19 -0800 Subject: [PATCH 2/2] Remove PaginationTest --- gradle.properties | 8 -- .../backfila/ui/pages/ServiceShowAction.kt | 6 +- .../app/cash/backfila/PaginationTest.kt | 88 ------------------- 3 files changed, 3 insertions(+), 99 deletions(-) delete mode 100644 service/src/test/kotlin/app/cash/backfila/PaginationTest.kt diff --git a/gradle.properties b/gradle.properties index 138465190..f325b3250 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,3 @@ GROUP=app.cash.backfila org.gradle.jvmargs=-Xmx6g -XX:MaxMetaspaceSize=5g -Dfile.encoding=UTF-8 - -org.gradle.caching=true -org.gradle.configuration-cache=true -org.gradle.vfs.watch=true -org.gradle.configureondemand=true -org.gradle.parallel=true - -misk.test.logging=false diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt index 00dc61433..4aaace693 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceShowAction.kt @@ -6,6 +6,9 @@ import app.cash.backfila.ui.components.BackfillsTable import app.cash.backfila.ui.components.DashboardPageLayout import app.cash.backfila.ui.components.PageTitle import app.cash.backfila.ui.components.Pagination +import java.net.HttpURLConnection +import javax.inject.Inject +import javax.inject.Singleton import kotlinx.html.ButtonType import kotlinx.html.a import kotlinx.html.button @@ -23,9 +26,6 @@ import misk.web.actions.WebAction import misk.web.mediatype.MediaTypes import misk.web.toResponseBody import okhttp3.Headers -import java.net.HttpURLConnection -import javax.inject.Inject -import javax.inject.Singleton @Singleton class ServiceShowAction @Inject constructor( diff --git a/service/src/test/kotlin/app/cash/backfila/PaginationTest.kt b/service/src/test/kotlin/app/cash/backfila/PaginationTest.kt deleted file mode 100644 index 14795c3ca..000000000 --- a/service/src/test/kotlin/app/cash/backfila/PaginationTest.kt +++ /dev/null @@ -1,88 +0,0 @@ -package app.cash.backfila - -import app.cash.backfila.api.ConfigureServiceAction -import app.cash.backfila.api.ConfigureServiceAction.Companion.RESERVED_VARIANT -import app.cash.backfila.client.Connectors -import app.cash.backfila.dashboard.CreateBackfillAction -import app.cash.backfila.protos.service.ConfigureServiceRequest -import app.cash.backfila.protos.service.CreateBackfillRequest -import app.cash.backfila.service.persistence.BackfilaDb -import app.cash.backfila.service.persistence.BackfillRunQuery -import jakarta.inject.Inject -import misk.hibernate.Query -import misk.hibernate.Transacter -import misk.hibernate.newQuery -import misk.hibernate.pagination.idDescPaginator -import misk.hibernate.pagination.newPager -import misk.scope.ActionScope -import misk.testing.MiskTest -import misk.testing.MiskTestModule -import org.junit.jupiter.api.Test - -@MiskTest(startService = true) -class PaginationTest { - @MiskTestModule - private val module = BackfilaTestingModule() - - @Inject @BackfilaDb - private lateinit var transacter: Transacter - - @Inject private lateinit var queryFactory: Query.Factory - - @Inject private lateinit var configureServiceAction: ConfigureServiceAction - - @Inject private lateinit var createBackfillAction: CreateBackfillAction - - @Inject private lateinit var scope: ActionScope - - @Test - fun happyPath() { - // Seed rows - seedData() - - // Test pagination - val allRows = transacter.transaction { session -> - queryFactory.newQuery() - .list(session) - } - val rows = transacter.transaction { session -> - val pager = queryFactory.newQuery() - .orderByUpdatedAtDesc() - .newPager(idDescPaginator(), pageSize = 10) - pager.nextPage(session) - ?.contents - } - - val a = "a" - } - - fun seedData() { - scope.fakeCaller(service = "deep-fryer") { - configureServiceAction.configureService( - ConfigureServiceRequest.Builder() - .backfills( - listOf( - ConfigureServiceRequest.BackfillData( - "ChickenSandwich", "Description", listOf(), null, - null, false, null, - ), - ), - ) - .connector_type(Connectors.ENVOY) - .build(), - ) - } - - for (i in 0..100) { - scope.fakeCaller(user = "molly") { - val response = createBackfillAction.create( - "deep-fryer", - RESERVED_VARIANT, - CreateBackfillRequest.Builder() - .backfill_name("ChickenSandwich") - .build(), - ) - } - } - } -}