diff --git a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt index 3c8822c5..2ea325b7 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/UiModule.kt @@ -3,6 +3,7 @@ package app.cash.backfila.ui import app.cash.backfila.ui.actions.BackfillShowButtonHandlerAction import app.cash.backfila.ui.actions.ServiceAutocompleteAction import app.cash.backfila.ui.pages.BackfillShowAction +import app.cash.backfila.ui.pages.IndexAction import app.cash.backfila.ui.pages.ServiceIndexAction import app.cash.backfila.ui.pages.ServiceShowAction import misk.inject.KAbstractModule @@ -11,6 +12,7 @@ import misk.web.WebActionModule class UiModule : KAbstractModule() { override fun configure() { // Pages + install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) install(WebActionModule.create()) diff --git a/service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt b/service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt index 8e08b133..50d90dce 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt @@ -20,10 +20,12 @@ import misk.tailwind.Link import misk.tailwind.pages.MenuSection import misk.tailwind.pages.Navbar import misk.web.HttpCall +import misk.web.ResponseBody import misk.web.dashboard.DashboardHomeUrl import misk.web.dashboard.DashboardNavbarItem import misk.web.dashboard.DashboardTab import misk.web.dashboard.HtmlLayout +import okio.BufferedSink import wisp.deployment.Deployment /** @@ -113,6 +115,7 @@ class DashboardPageLayout @Inject constructor( menuSections = buildMenuSections( currentPath = path, ), + sortedMenuLinks = false, ) { div("py-10") { main { @@ -142,55 +145,78 @@ class DashboardPageLayout @Inject constructor( } } + fun buildHtmlResponseBody(block: TagConsumer<*>.() -> Unit): ResponseBody = object : ResponseBody { + override fun writeTo(sink: BufferedSink) { + sink.writeUtf8(build(block)) + } + } + private fun buildMenuSections( currentPath: String, ): List { return transacter.transaction { session -> - val runningBackfills = queryFactory.newQuery() + val runningBackfillsForCaller = queryFactory.newQuery() .createdByUser(callerProvider.get()!!.user!!) .state(BackfillState.RUNNING) .orderByUpdatedAtDesc() .list(session) // TODO get services from REgistry for user || group backfills by service + val services = runningBackfillsForCaller.groupBy { it.service } listOf( MenuSection( title = "Backfila", links = listOf( + Link( + label = "Home", + href = "/", + isSelected = currentPath == "/", + ), Link( label = "Services", href = "/services/", - isSelected = currentPath.startsWith("/services/"), + isSelected = currentPath == "/services/", ), Link( label = "Backfills", href = "/backfills/", - isSelected = currentPath.startsWith("/backfills/"), + isSelected = currentPath == "/backfills/", ), ), ), - MenuSection( - title = "Your Services", - links = listOf( - Link( - label = "Fine Dining", - href = "/services/?q=FineDining", - isSelected = currentPath.startsWith("/services/?q=FindDining"), - ), + ) + if (services.isNotEmpty()) { + listOf( + MenuSection( + title = "Your Services", + links = services.map { (service, backfills) -> + val variant = if (service.variant == "default") "" else service.variant + Link( + label = service.registry_name, + href = "/services/${service.registry_name}/$variant", + isSelected = currentPath.startsWith("/services/${service.registry_name}/$variant"), + ) + }, ), - ), - MenuSection( - title = "Your Backfills", - links = runningBackfills.map { backfill -> - Link( - label = backfill.service.registry_name + " #" + backfill.id, - href = "/backfills/${backfill.id}", - isSelected = currentPath.startsWith("/backfills/${backfill.id}"), - ) - }, - ), - ) + ) + } else { + listOf() + } + if (runningBackfillsForCaller.isNotEmpty()) { + listOf( + MenuSection( + title = "Your Backfills", + links = runningBackfillsForCaller.map { backfill -> + Link( + label = backfill.service.registry_name + " #" + backfill.id, + href = "/backfills/${backfill.id}", + isSelected = currentPath.startsWith("/backfills/${backfill.id}"), + ) + }, + ), + ) + } else { + listOf() + } } } diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillShowAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillShowAction.kt index 63468d8f..ffc0a37d 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillShowAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/BackfillShowAction.kt @@ -7,7 +7,7 @@ import app.cash.backfila.service.persistence.BackfillState import app.cash.backfila.ui.actions.BackfillShowButtonHandlerAction import app.cash.backfila.ui.components.AlertError import app.cash.backfila.ui.components.AlertSupport -import app.cash.backfila.ui.components.DashboardLayout +import app.cash.backfila.ui.components.DashboardPageLayout import app.cash.backfila.ui.components.PageTitle import app.cash.backfila.ui.components.ProgressBar import javax.inject.Inject @@ -30,7 +30,6 @@ import kotlinx.html.td import kotlinx.html.th import kotlinx.html.thead import kotlinx.html.tr -import misk.hotwire.buildHtmlResponseBody import misk.security.authz.Authenticated import misk.tailwind.Link import misk.web.Get @@ -45,6 +44,7 @@ import misk.web.mediatype.MediaTypes class BackfillShowAction @Inject constructor( private val config: BackfilaConfig, private val getBackfillStatusAction: GetBackfillStatusAction, + private val dashboardPageLayout: DashboardPageLayout, ) : WebAction { @Get(PATH) @ResponseContentType(MediaTypes.TEXT_HTML) @@ -54,25 +54,20 @@ class BackfillShowAction @Inject constructor( ): Response { if (id.toLongOrNull() == null) { return Response( - buildHtmlResponseBody { - DashboardLayout( - title = "Backfill $id | Backfila", - path = PATH, - ) { + dashboardPageLayout.newBuilder() + .title("Backfill $id | Backfila") + .buildHtmlResponseBody { PageTitle("Backfill", id) AlertError("Invalid Backfill Id [id=$id], must be of type Long.") AlertSupport(config.support_button_label, config.support_button_url) - } - }, + }, ) } val backfill = getBackfillStatusAction.status(id.toLong()) - val htmlResponseBody = buildHtmlResponseBody { - DashboardLayout( - title = "Backfill $id | Backfila", - path = PATH, - ) { + val htmlResponseBody = dashboardPageLayout.newBuilder() + .title("Backfill $id | Backfila") + .buildHtmlResponseBody { PageTitle("Backfill", id) // TODO add Header buttons / metrics @@ -306,7 +301,6 @@ class BackfillShowAction @Inject constructor( AlertSupport(config.support_button_label, config.support_button_url) } - } return Response(htmlResponseBody) } 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 new file mode 100644 index 00000000..f393290e --- /dev/null +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/IndexAction.kt @@ -0,0 +1,73 @@ +package app.cash.backfila.ui.pages + +import app.cash.backfila.service.BackfilaConfig +import app.cash.backfila.ui.actions.ServiceAutocompleteAction +import app.cash.backfila.ui.components.AlertSupport +import app.cash.backfila.ui.components.DashboardPageLayout +import javax.inject.Inject +import kotlinx.html.dd +import kotlinx.html.div +import kotlinx.html.dl +import kotlinx.html.dt +import kotlinx.html.h1 +import kotlinx.html.h3 +import kotlinx.html.span +import misk.MiskCaller +import misk.scope.ActionScoped +import misk.security.authz.Authenticated +import misk.web.Get +import misk.web.QueryParam +import misk.web.ResponseContentType +import misk.web.actions.WebAction +import misk.web.mediatype.MediaTypes + +class IndexAction @Inject constructor( + private val config: BackfilaConfig, + private val serviceAutocompleteAction: ServiceAutocompleteAction, + private val dashboardPageLayout: DashboardPageLayout, + private val callerProvider: ActionScoped, +) : WebAction { + @Get(PATH) + @ResponseContentType(MediaTypes.TEXT_HTML) + @Authenticated(capabilities = ["users"]) + fun get( + @QueryParam sc: String?, + ): String = dashboardPageLayout + .newBuilder() + .title("Backfila Home") + .build { + h1("text-2xl") { + +"""Welcome to Backfila, """ + span("font-bold font-mono") { +"""${callerProvider.get()?.user}""" } + +"""!""" + } + + // Stats + + div("py-10") { + 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") { + this@dl.dt("truncate text-sm font-medium text-gray-500") { +"""Total Records""" } + this@dl.dd("mt-1 text-3xl font-semibold tracking-tight text-gray-900") { +"""1,271,897""" } + } + div("overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6") { + this@dl.dt("truncate text-sm font-medium text-gray-500") { +"""Avg. Progress Rate""" } + this@dl.dd("mt-1 text-3xl font-semibold tracking-tight text-gray-900") { +"""58.16%""" } + } + div("overflow-hidden rounded-lg bg-white px-4 py-5 shadow sm:p-6") { + this@dl.dt("truncate text-sm font-medium text-gray-500") { +"""Avg. Records Per Second""" } + this@dl.dd("mt-1 text-3xl font-semibold tracking-tight text-gray-900") { +"""2457""" } + } + } + } + + // Running Backfills + + AlertSupport(config.support_button_label, config.support_button_url) + } + + companion object { + const val PATH = "/" + } +} diff --git a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceIndexAction.kt b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceIndexAction.kt index af532325..dbb6a540 100644 --- a/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceIndexAction.kt +++ b/service/src/main/kotlin/app/cash/backfila/ui/pages/ServiceIndexAction.kt @@ -129,6 +129,6 @@ class ServiceIndexAction @Inject constructor( } companion object { - const val PATH = "/" + const val PATH = "/services/" } } 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 9cbca166..f638d292 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 @@ -4,16 +4,16 @@ import app.cash.backfila.dashboard.GetBackfillRunsAction import app.cash.backfila.service.BackfilaConfig import app.cash.backfila.ui.components.AlertSupport import app.cash.backfila.ui.components.BackfillsTable -import app.cash.backfila.ui.components.DashboardLayout +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 kotlinx.html.role import kotlinx.html.ul -import misk.hotwire.buildHtmlResponseBody import misk.security.authz.Authenticated import misk.web.Get +import misk.web.PathParam import misk.web.QueryParam import misk.web.Response import misk.web.ResponseBody @@ -27,34 +27,32 @@ import okhttp3.Headers class ServiceShowAction @Inject constructor( private val config: BackfilaConfig, private val getBackfillRunsAction: GetBackfillRunsAction, + private val dashboardPageLayout: DashboardPageLayout, ) : WebAction { @Get(PATH) @ResponseContentType(MediaTypes.TEXT_HTML) @Authenticated(capabilities = ["users"]) fun get( - @QueryParam s: String?, + @PathParam service: String?, + @PathParam variantOrBlank: String = "", @QueryParam("experimental") experimental: Boolean? = false, ): Response { - if (s.isNullOrBlank()) { + if (service.isNullOrBlank()) { return Response( body = "go to /".toResponseBody(), statusCode = HttpURLConnection.HTTP_MOVED_TEMP, headers = Headers.headersOf("Location", "/"), ) } + val variant = variantOrBlank.ifBlank { "default" } - val serviceName = s.split("/").first() - val variant = s.split("/").last() + val backfillRuns = getBackfillRunsAction.backfillRuns(service, variant) - val backfillRuns = getBackfillRunsAction.backfillRuns(serviceName, variant) - - val htmlResponseBody = buildHtmlResponseBody { - // TODO show default if other variants and probably link to a switcher - val label = if (variant == "default") serviceName else "$serviceName ($variant)" - DashboardLayout( - title = "$label | Backfila", - path = PATH, - ) { + // TODO show default if other variants and probably link to a switcher + val label = if (variant == "default") service else "$service ($variant)" + val htmlResponseBody = dashboardPageLayout.newBuilder() + .title("$label | Backfila") + .buildHtmlResponseBody { PageTitle("Service", label) // TODO Add completed table @@ -68,12 +66,11 @@ class ServiceShowAction @Inject constructor( AlertSupport(config.support_button_label, config.support_button_url) } - } return Response(htmlResponseBody) } companion object { - const val PATH = "/services/" + const val PATH = "/services/{service}/{variantOrBlank}" } }