From e29a5b09b83f8cdb8243b0c6e322647f2334d35c Mon Sep 17 00:00:00 2001 From: Andrew Alexander Date: Thu, 9 Jan 2025 17:07:47 -0500 Subject: [PATCH] Bump Misk, make full dashboard page layout to add per user services/backfills list --- gradle/libs.versions.toml | 2 +- .../ui/components/DashboardPageLayout.kt | 186 ++++++++++++++++++ .../backfila/ui/pages/ServiceIndexAction.kt | 104 +++++----- 3 files changed, 237 insertions(+), 55 deletions(-) create mode 100644 service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 809eb60d1..66e485d8d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ jooq = "3.18.1" ktlint = "0.47.1" kotlin = "1.9.23" -misk = "2024.06.26.052833-88986ac" +misk = "2025.01.09.184726-680bca2" okhttp = "5.0.0-alpha.14" wire = "5.0.0" sqldelight = "2.0.2" 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 new file mode 100644 index 000000000..ed66a2f9e --- /dev/null +++ b/service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt @@ -0,0 +1,186 @@ +package app.cash.backfila.ui.components + +import app.cash.backfila.dashboard.GetBackfillRunsAction +import jakarta.inject.Inject +import kotlinx.html.TagConsumer +import kotlinx.html.div +import kotlinx.html.main +import kotlinx.html.script +import misk.MiskCaller +import misk.config.AppName +import misk.hotwire.buildHtml +import misk.scope.ActionScoped +import misk.tailwind.Link +import misk.tailwind.pages.MenuSection +import misk.tailwind.pages.Navbar +import misk.web.HttpCall +import misk.web.dashboard.DashboardHomeUrl +import misk.web.dashboard.DashboardNavbarItem +import misk.web.dashboard.DashboardTab +import misk.web.dashboard.HtmlLayout +import wisp.deployment.Deployment + +/** + * Builds dashboard UI for index homepage. + * + * Must be called within a Web Action. + */ +class DashboardPageLayout @Inject constructor( + private val allHomeUrls: List, + @AppName private val appName: String, + private val allNavbarItem: List, + private val allTabs: List, + private val callerProvider: ActionScoped, + private val deployment: Deployment, + private val clientHttpCall: ActionScoped, + private val getBackfillRunsAction: GetBackfillRunsAction, +) { + private var newBuilder = false + private var headBlock: TagConsumer<*>.() -> Unit = {} + private var title: String = "Backfila" + + private val path by lazy { + clientHttpCall.get().url.encodedPath + } + private val dashboardHomeUrl by lazy { + allHomeUrls.firstOrNull { path.startsWith(it.url) } + } + private val homeUrl by lazy { + dashboardHomeUrl?.url ?: "/" + } + + private fun setNewBuilder() = apply { newBuilder = true } + + fun newBuilder(): DashboardPageLayout = DashboardPageLayout( + allHomeUrls = allHomeUrls, + appName = appName, + allNavbarItem = allNavbarItem, + allTabs = allTabs, + callerProvider = callerProvider, + deployment = deployment, + clientHttpCall = clientHttpCall, + getBackfillRunsAction = getBackfillRunsAction, + ).setNewBuilder() + + fun title(title: String) = apply { + this.title = title + } + + fun headBlock(block: TagConsumer<*>.() -> Unit) = apply { this.headBlock = block } + + @JvmOverloads + fun build(block: TagConsumer<*>.() -> Unit = { }): String { + check(newBuilder) { + "You must call newBuilder() before calling build() to prevent builder reuse." + } + newBuilder = false + + return buildHtml { + HtmlLayout( + appRoot = "/", + title = title, + // TODO only use play CDN in development, using it always for demo purporses to avoid UI bugs +// playCdn = deployment.isLocalDevelopment, + playCdn = true, + headBlock = { + script { + type = "module" + src = "/static/js/autocomplete_controller.js" + } + script { + type = "module" + src = "/static/js/search_bar_controller.js" + } + }, + ) { + div("min-h-full") { + if (true) { + // Uses Misk's Navbar with sidebar + Navbar( + appName = "Backfila", + deployment = deployment, + homeHref = "/", + menuSections = buildMenuSections( + currentPath = path, + ), + ) { + div("py-10") { + main { + div("mx-auto max-w-7xl sm:px-6 lg:px-8") { + // TODO remove when new UI is stable and preferred + UseOldUIAlert() + block() + } + } + } + } + } else { + // Old UI + NavBar(path) + div("py-10") { + main { + div("mx-auto max-w-7xl sm:px-6 lg:px-8") { + // TODO remove when new UI is stable and preferred + UseOldUIAlert() + block() + } + } + } + } + } + } + } + } + + private fun buildMenuSections( + currentPath: String, + ): List { + val callerBackfills = getBackfillRunsAction.backfillRuns(serviceName, variant) + + return listOf( + MenuSection( + title = "Backfila", + links = listOf( + Link( + label = "Services", + href = "/services/", + isSelected = currentPath.startsWith("/services/"), + ), + Link( + label = "Backfills", + href = "/backfills/", + isSelected = currentPath.startsWith("/backfills/"), + ), + ), + ), + MenuSection( + title = "Your Services", + links = listOf( + Link( + label = "Fine Dining", + href = "/services/?q=FineDining", + isSelected = currentPath.startsWith("/services/?q=FindDining"), + ), + ), + ), + MenuSection( + title = "Your Backfills", + links = listOf( + Link( + label = "FineDining #0034", + href = "/services/", + isSelected = currentPath.startsWith("/backfill/"), + ), + Link( + label = "FineDining #0067", + href = "/backfill/", + isSelected = currentPath.startsWith("/backfill/"), + ), + ), + ), + ) + } + + companion object { + } +} 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 d2a233162..af532325b 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 @@ -4,7 +4,7 @@ import app.cash.backfila.service.BackfilaConfig import app.cash.backfila.ui.PathBuilder import app.cash.backfila.ui.actions.ServiceAutocompleteAction 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 javax.inject.Inject import kotlinx.html.InputType @@ -17,7 +17,6 @@ import kotlinx.html.p import kotlinx.html.role import kotlinx.html.span import kotlinx.html.ul -import misk.hotwire.buildHtml import misk.security.authz.Authenticated import misk.web.Get import misk.web.QueryParam @@ -28,62 +27,61 @@ import misk.web.mediatype.MediaTypes class ServiceIndexAction @Inject constructor( private val config: BackfilaConfig, private val serviceAutocompleteAction: ServiceAutocompleteAction, + private val dashboardPageLayout: DashboardPageLayout, ) : WebAction { @Get(PATH) @ResponseContentType(MediaTypes.TEXT_HTML) @Authenticated(capabilities = ["users"]) fun get( @QueryParam sc: String?, - ): String { - return buildHtml { - DashboardLayout( - title = "Backfila", - path = PATH, - ) { - PageTitle("Services") - val pathBuilder = PathBuilder(path = PATH) + ): String = dashboardPageLayout + .newBuilder() + .title("Backfila Home") + .build { + PageTitle("Services") + val pathBuilder = PathBuilder(path = PATH) - div { - attributes["data-controller"] = "search-bar" + div { + attributes["data-controller"] = "search-bar" - // Search Bar - div { - input( - type = InputType.search, - classes = "flex h-10 w-full bg-gray-100 hover:bg-gray-200 duration-500 border-none rounded-lg text-sm", - ) { - attributes["data-action"] = "input->search-bar#search" - placeholder = "Search" - } + // Search Bar + div { + input( + type = InputType.search, + classes = "flex h-10 w-full bg-gray-100 hover:bg-gray-200 duration-500 border-none rounded-lg text-sm", + ) { + attributes["data-action"] = "input->search-bar#search" + placeholder = "Search" } + } - // List of Services - val services = serviceAutocompleteAction.getFlattenedServices() - div("py-10") { - ul("grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3") { - role = "list" + // List of Services + val services = serviceAutocompleteAction.getFlattenedServices() + div("py-10") { + ul("grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3") { + role = "list" - services.map { (path, service) -> - li("registration col-span-1 divide-y divide-gray-200 rounded-lg bg-white shadow") { - div("flex w-full items-center justify-between space-x-6 p-6") { - div("flex-1 truncate") { - div("flex items-center space-x-3") { - // Don't include default variant in label, only for unique variants - val label = if (path.split("/").last() == "default") service.name else path - val variant = if (path.split("/").last() == "default") null else path.split("/").last() - h3("truncate text-sm font-medium text-gray-900") { - +"""$label (${service.running_backfills})""" - } - variant?.let { span("inline-flex shrink-0 items-center rounded-full bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20") { +it } } + services.map { (path, service) -> + li("registration col-span-1 divide-y divide-gray-200 rounded-lg bg-white shadow") { + div("flex w-full items-center justify-between space-x-6 p-6") { + div("flex-1 truncate") { + div("flex items-center space-x-3") { + // Don't include default variant in label, only for unique variants + val label = if (path.split("/").last() == "default") service.name else path + val variant = if (path.split("/").last() == "default") null else path.split("/").last() + h3("truncate text-sm font-medium text-gray-900") { + +"""$label (${service.running_backfills})""" } - p("mt-1 truncate text-sm text-gray-500") { +"""Regional Paradigm Technician""" } + variant?.let { span("inline-flex shrink-0 items-center rounded-full bg-green-50 px-1.5 py-0.5 text-xs font-medium text-green-700 ring-1 ring-inset ring-green-600/20") { +it } } } + p("mt-1 truncate text-sm text-gray-500") { +"""Regional Paradigm Technician""" } } - div { - div("-mt-px flex divide-x divide-gray-200") { - div("flex w-0 flex-1") { - a(classes = "relative -mr-px inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-gray-900") { - href = "mailto:janecooper@example.com" + } + div { + div("-mt-px flex divide-x divide-gray-200") { + div("flex w-0 flex-1") { + a(classes = "relative -mr-px inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-bl-lg border border-transparent py-4 text-sm font-semibold text-gray-900") { + href = "mailto:janecooper@example.com" // svg("size-5 text-gray-400") { // viewbox = "0 0 20 20" // fill = "currentColor" @@ -98,12 +96,12 @@ class ServiceIndexAction @Inject constructor( // "m19 8.839-7.77 3.885a2.75 2.75 0 0 1-2.46 0L1 8.839V14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V8.839Z" // } // } - +"""Email""" - } + +"""Email""" } - div("-ml-px flex w-0 flex-1") { - a(classes = "relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-gray-900") { - href = "tel:+1-202-555-0170" + } + div("-ml-px flex w-0 flex-1") { + a(classes = "relative inline-flex w-0 flex-1 items-center justify-center gap-x-3 rounded-br-lg border border-transparent py-4 text-sm font-semibold text-gray-900") { + href = "tel:+1-202-555-0170" // svg("size-5 text-gray-400") { // viewbox = "0 0 20 20" // fill = "currentColor" @@ -116,8 +114,7 @@ class ServiceIndexAction @Inject constructor( // attributes["clip-rule"] = "evenodd" // } // } - +"""Call""" - } + +"""Call""" } } } @@ -126,11 +123,10 @@ class ServiceIndexAction @Inject constructor( } } } - - AlertSupport(config.support_button_label, config.support_button_url) } + + AlertSupport(config.support_button_label, config.support_button_url) } - } companion object { const val PATH = "/"