Skip to content

Commit

Permalink
Previously, if a Misk-Web tab is loaded and assets weren't
Browse files Browse the repository at this point in the history
present from a failed CI build or missing local build, the tab would
show up as an empty white screen in the dashboard and some browser
console errors were the only hint something was wrong.

We now have descriptive helper messages for debugging tab load failures
in local development and real environments in addition the browser
console errors to improve the developer experience for legacy Misk-Web
tabs.

# Staging / Production
<img width="1126" alt="Screenshot 2024-09-24 at 16 10 34"
src="https://github.com/user-attachments/assets/a6fb6bcb-c652-4b0e-9b3d-70bdbd243d87">

# Local Development
<img width="1114" alt="Screenshot 2024-09-24 at 16 11 01"
src="https://github.com/user-attachments/assets/e304b8ea-f712-4369-9737-2d42e2658b7e">
GitOrigin-RevId: 8663a7ffcf6115e91b166738694fedb49cce2525
  • Loading branch information
adrw authored and svc-squareup-copybara committed Sep 25, 2024
1 parent 285aac9 commit 73223cb
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 41 deletions.
10 changes: 0 additions & 10 deletions misk-admin/api/misk-admin.api
Original file line number Diff line number Diff line change
Expand Up @@ -985,16 +985,6 @@ public final class misk/web/metadata/webaction/WebActionsDashboardTabModule : mi
public fun <init> (Z)V
}

public final class misk/web/v2/DashboardHotwireTabAction : misk/web/actions/WebAction {
public fun <init> (Lmisk/scope/ActionScoped;Lmisk/web/v2/DashboardPageLayout;Ljava/util/List;)V
public final fun get (Ljava/lang/String;)Ljava/lang/String;
}

public final class misk/web/v2/DashboardIFrameTabAction : misk/web/actions/WebAction {
public fun <init> (Lmisk/scope/ActionScoped;Lmisk/web/v2/DashboardPageLayout;Ljava/util/List;)V
public final fun get (Ljava/lang/String;)Ljava/lang/String;
}

public final class misk/web/v2/DashboardIndexAccessBlock {
public fun <init> (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function6;)V
public final fun component1 ()Lkotlin/reflect/KClass;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package misk.web.dashboard

import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.html.body
import kotlinx.html.div
import kotlinx.html.head
Expand All @@ -15,14 +17,14 @@ import misk.web.PathParam
import misk.web.ResponseContentType
import misk.web.actions.WebAction
import misk.web.mediatype.MediaTypes
import jakarta.inject.Inject
import jakarta.inject.Singleton

/**
* Kotlin backed tab loader, equivalent to /_tab/slug/index.html
*/
@Singleton
class MiskWebTabIndexAction @Inject constructor(
// TODO this probably shouldn't be limited to AdminDashboard tabs only but Misk-Web is
// deprecated so this can be solved if it's raised as a problem
@AdminDashboard private val dashboardTabs: List<DashboardTab>,
) : WebAction {
@Get("$PATH/{slug}/{rest:.*}")
Expand All @@ -33,6 +35,8 @@ class MiskWebTabIndexAction @Inject constructor(
?: throw NotFoundException("No Misk-Web tab found for slug: $slug")
// TODO remove this hack when new Web Actions tab lands and old ones removed, v1 and v2 are in the same web-actions tab
val normalizedSlug = if (dashboardTab.slug == "web-actions-v1") "web-actions" else dashboardTab.slug
val tabEntrypointJs = "/_tab/${normalizedSlug}/tab_${normalizedSlug}.js"

return buildHtml {
html {
head {
Expand Down Expand Up @@ -73,7 +77,7 @@ class MiskWebTabIndexAction @Inject constructor(
// Tab specific resources
script {
type = "text/javascript"
src = "/_tab/${normalizedSlug}/tab_${normalizedSlug}.js"
src = tabEntrypointJs
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import misk.web.dashboard.AdminDashboardAccess
* Builds dashboard UI and loads Hotwire tab.
*/
@Singleton
class DashboardHotwireTabAction @Inject constructor(
internal class DashboardHotwireTabAction @Inject constructor(
@JvmSuppressWildcards private val clientHttpCall: ActionScoped<HttpCall>,
private val dashboardPageLayout: DashboardPageLayout,
private val entries: List<DashboardTabLoaderEntry>,
Expand Down
76 changes: 67 additions & 9 deletions misk-admin/src/main/kotlin/misk/web/v2/DashboardIFrameTabAction.kt
Original file line number Diff line number Diff line change
@@ -1,28 +1,40 @@
package misk.web.v2

import kotlinx.html.h1
import jakarta.inject.Inject
import jakarta.inject.Singleton
import kotlinx.html.div
import kotlinx.html.iframe
import misk.scope.ActionScoped
import misk.tailwind.components.AlertError
import misk.tailwind.components.AlertInfo
import misk.web.Get
import misk.web.HttpCall
import misk.web.PathParam
import misk.web.ResponseContentType
import misk.web.actions.WebAction
import misk.web.dashboard.AdminDashboardAccess
import misk.web.dashboard.DashboardTab
import misk.web.dashboard.DashboardTabLoader
import misk.web.dashboard.DashboardTabLoaderEntry
import misk.web.dashboard.MiskWebTabIndexAction
import misk.web.mediatype.MediaTypes
import jakarta.inject.Inject
import jakarta.inject.Singleton
import misk.web.dashboard.AdminDashboardAccess
import misk.web.proxy.WebProxyAction
import misk.web.resources.StaticResourceAction
import okhttp3.HttpUrl.Companion.toHttpUrl
import wisp.deployment.Deployment

/**
* Builds dashboard UI and loads IFrame tab.
*/
@Singleton
class DashboardIFrameTabAction @Inject constructor(
internal class DashboardIFrameTabAction @Inject constructor(
@JvmSuppressWildcards private val clientHttpCall: ActionScoped<HttpCall>,
private val dashboardPageLayout: DashboardPageLayout,
private val dashboardTabs: List<DashboardTab>,
private val deployment: Deployment,
private val entries: List<DashboardTabLoaderEntry>,
private val staticResourceAction: StaticResourceAction,
private val webProxyAction: WebProxyAction,
) : WebAction {
@Get("/{suffix:.*}")
@ResponseContentType(MediaTypes.TEXT_HTML)
Expand All @@ -34,10 +46,56 @@ class DashboardIFrameTabAction @Inject constructor(
val entry = entries
.filter { it.loader is DashboardTabLoader.IframeTab }
.firstOrNull { fullPath.startsWith(it.urlPathPrefix) }
(entry?.loader as? DashboardTabLoader.IframeTab)?.let {
iframe(classes = "h-full w-full") {
src = "${it.iframePath}$suffix"

val iframeTab = entry?.loader as? DashboardTabLoader.IframeTab

div("container mx-auto p-8") {
if (iframeTab != null) {
// If the tab is found, render the iframe

if (iframeTab.iframePath.startsWith(MiskWebTabIndexAction.PATH)) {
// If tab is Misk-Web, do extra validation to show a more helpful error message

val slug = iframeTab.urlPathPrefix.split("/").last { it.isNotBlank() }
val dashboardTab = dashboardTabs.firstOrNull { slug == it.slug }

if (dashboardTab == null) {
AlertError("No Misk-Web tab found for slug: $slug. Check your install bindings or Misk-Web build.")
} else {
// TODO remove this hack when new Web Actions tab lands and old ones removed, v1 and v2 are in the same web-actions tab
val normalizedSlug =
if (dashboardTab.slug == "web-actions-v1") "web-actions" else dashboardTab.slug
val tabEntrypointJs = "/_tab/${normalizedSlug}/tab_${normalizedSlug}.js"

// If tab is Misk-Web do additional checks and show separate development and real errors
if (deployment.isLocalDevelopment) {
// If local development, check web proxy action and show fuller development 404 message
val tabEntrypointJsResponse =
webProxyAction.getResponse(("http://localhost/" + tabEntrypointJs).toHttpUrl())
if (tabEntrypointJsResponse.statusCode != 200) {
AlertError("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
AlertInfo("In local development, this can be from not having your local dev server (ie. Webpack) running or not doing an initial local Misk-Web build to generate the necessary web assets. Try running in your Terminal: \$ gradle buildMiskWeb OR \$ misk-web ci-build -e.")
}
} else if (deployment.isReal) {
// If real environment, only check static resource action and show limited 404 message
val tabEntrypointJsResponse =
staticResourceAction.getResponse(("http://localhost/" + tabEntrypointJs).toHttpUrl())
if (tabEntrypointJsResponse.statusCode != 200) {
AlertError("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
AlertInfo("In real environments, this is usually because of a web build failure in CI. Try checking CI logs and report this bug to your platform team. If the CI web build fails or is not run, the web assets will be missing from the Docker context when deployed and fail to load.")
}
}
}
}

// Always still show iframe so that full load errors show up in browser console
iframe(classes = "h-full w-full") {
src = "${iframeTab.iframePath}$suffix"
}
} else {
// If tab is not found show alert error
AlertError("""Dashboard tab not found at $fullPath""")
}
} ?: h1 { +"""Dashboard tab not found at $fullPath""" }
}
}
}
3 changes: 2 additions & 1 deletion misk/api/misk.api
Original file line number Diff line number Diff line change
Expand Up @@ -2296,6 +2296,7 @@ public final class misk/web/proxy/OptionalBinder {
public final class misk/web/proxy/WebProxyAction : misk/web/actions/WebAction {
public fun <init> (Lmisk/web/proxy/OptionalBinder;Lmisk/scope/ActionScoped;Lmisk/web/resources/StaticResourceAction;Lmisk/web/resources/ResourceEntryFinder;)V
public final fun action ()Lmisk/web/Response;
public final fun getResponse (Lokhttp3/HttpUrl;)Lmisk/web/Response;
}

public final class misk/web/proxy/WebProxyEntry : misk/web/dashboard/ValidWebEntry {
Expand Down Expand Up @@ -2327,7 +2328,7 @@ public final class misk/web/resources/ResourceEntryFinder {
public final class misk/web/resources/StaticResourceAction : misk/web/actions/WebAction {
public fun <init> (Lmisk/scope/ActionScoped;Lmisk/resources/ResourceLoader;Lmisk/web/resources/ResourceEntryFinder;)V
public final fun action ()Lmisk/web/Response;
public final fun getResponse (Lmisk/web/HttpCall;)Lmisk/web/Response;
public final fun getResponse (Lokhttp3/HttpUrl;)Lmisk/web/Response;
}

public final class misk/web/resources/StaticResourceEntry : misk/web/dashboard/ValidWebEntry {
Expand Down
14 changes: 9 additions & 5 deletions misk/src/main/kotlin/misk/web/proxy/WebProxyAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,15 @@ class WebProxyAction @Inject constructor(
@Unauthenticated
fun action(): Response<ResponseBody> {
val httpCall = clientHttpCall.get()
val matchedEntry = resourceEntryFinder.webProxy(httpCall.url) as WebProxyEntry?
?: return NotFoundAction.response(httpCall.url.toString())
return getResponse(httpCall.url)
}

fun getResponse(url: HttpUrl): Response<ResponseBody> {
val matchedEntry = resourceEntryFinder.webProxy(url) as WebProxyEntry?
?: return NotFoundAction.response(url.toString())
val proxyUrl = matchedEntry.web_proxy_url.newBuilder()
.encodedPath(httpCall.url.encodedPath)
.query(httpCall.url.query)
.encodedPath(url.encodedPath)
.query(url.query)
.build()
return forwardRequestTo(proxyUrl)
}
Expand All @@ -70,7 +74,7 @@ class WebProxyAction @Inject constructor(
return try {
optionalBinder.proxyClient.newCall(proxyRequest).execute().toMisk()
} catch (e: IOException) {
staticResourceAction.getResponse(httpCall)
staticResourceAction.getResponse(httpCall.url)
}
}

Expand Down
20 changes: 10 additions & 10 deletions misk/src/main/kotlin/misk/web/resources/StaticResourceAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@ class StaticResourceAction @Inject constructor(
@Unauthenticated // TODO(adrw) https://github.com/square/misk/issues/429
fun action(): Response<ResponseBody> {
val httpCall = clientHttpCall.get()
return getResponse(httpCall)
return getResponse(httpCall.url)
}

fun getResponse(httpCall: HttpCall): Response<ResponseBody> {
fun getResponse(url: HttpUrl): Response<ResponseBody> {
val staticResourceEntry =
resourceEntryFinder.staticResource(httpCall.url) as StaticResourceEntry?
?: return NotFoundAction.response(httpCall.url.encodedPath.drop(1))
return MatchedResource(staticResourceEntry).getResponse(httpCall)
resourceEntryFinder.staticResource(url) as StaticResourceEntry?
?: return NotFoundAction.response(url.encodedPath.drop(1))
return MatchedResource(staticResourceEntry).getResponse(url)
}

private enum class Kind {
Expand All @@ -68,11 +68,11 @@ class StaticResourceAction @Inject constructor(
}

private inner class MatchedResource(var matchedEntry: StaticResourceEntry) {
fun getResponse(httpCall: HttpCall): Response<ResponseBody> {
val urlPath = httpCall.url.encodedPath
fun getResponse(url: HttpUrl): Response<ResponseBody> {
val urlPath = url.encodedPath
return when (exists(urlPath)) {
Kind.NO_MATCH -> when {
!urlPath.endsWith("/") -> redirectResponse(normalizePathWithQuery(httpCall.url))
!urlPath.endsWith("/") -> redirectResponse(normalizePathWithQuery(url))
// actually return the resource, don't redirect. Path must stay the same since this will be handled by React router
urlPath.endsWith("/") -> resourceResponse(
normalizePath(matchedEntry.url_path_prefix)
Expand All @@ -82,8 +82,8 @@ class StaticResourceAction @Inject constructor(
}

Kind.RESOURCE -> resourceResponse(urlPath)
Kind.RESOURCE_DIRECTORY -> resourceResponse(normalizePathWithQuery(httpCall.url))
} ?: NotFoundAction.response(httpCall.url.encodedPath.drop(1))
Kind.RESOURCE_DIRECTORY -> resourceResponse(normalizePathWithQuery(url))
} ?: NotFoundAction.response(url.encodedPath.drop(1))
}

/** Returns true if the mapped path exists on either the resource path or file system. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ fun main(args: Array<String>) {
ConfigModule.create("exemplar", config),
DeploymentModule(deployment),
ExemplarAccessModule(),
ExemplarDashboardModule(),
ExemplarDashboardModule(deployment),
ExemplarMetadataModule(),
ExemplarWebActionsModule(),
ExemplarCronModule(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import misk.web.dashboard.DashboardModule
import misk.web.metadata.config.ConfigMetadataAction
import misk.web.resources.StaticResourceAction
import misk.web.resources.StaticResourceEntry
import wisp.deployment.Deployment

class ExemplarDashboardModule : KAbstractModule() {
class ExemplarDashboardModule(private val deployment: Deployment) : KAbstractModule() {
override fun configure() {
// Favicon.ico and any other shared static assets available at /static/*
multibind<StaticResourceEntry>()
Expand Down Expand Up @@ -95,6 +96,19 @@ class ExemplarDashboardModule : KAbstractModule() {
url = { appName, deployment -> "https://internal-tool.cash.app/?app=$appName&deployment=$deployment" },
category = "Internal"
))

// Custom Admin Dashboard Tab at /_admin/... which doesn't exist and shows graceful failure 404
install(WebActionModule.create<AlphaIndexAction>())
install(
DashboardModule.createMiskWebTab<AdminDashboard, AdminDashboardAccess>(
isDevelopment = deployment.isLocalDevelopment,
slug = "not-found",
urlPathPrefix = "/_admin/not-found/",
developmentWebProxyUrl = "http://localhost:3000/",
menuLabel = "Not Found",
menuCategory = "Admin Tools"
)
)
}
}

Expand Down

0 comments on commit 73223cb

Please sign in to comment.