-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bump Misk, make full dashboard page layout to add per user services/b…
…ackfills list
- Loading branch information
Showing
3 changed files
with
237 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
186 changes: 186 additions & 0 deletions
186
service/src/main/kotlin/app/cash/backfila/ui/components/DashboardPageLayout.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<DashboardHomeUrl>, | ||
@AppName private val appName: String, | ||
private val allNavbarItem: List<DashboardNavbarItem>, | ||
private val allTabs: List<DashboardTab>, | ||
private val callerProvider: ActionScoped<MiskCaller?>, | ||
private val deployment: Deployment, | ||
private val clientHttpCall: ActionScoped<HttpCall>, | ||
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<MenuSection> { | ||
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 { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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:[email protected]" | ||
} | ||
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:[email protected]" | ||
// 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 = "/" | ||
|