Skip to content

Commit

Permalink
Add error UI for tab load and link failures
Browse files Browse the repository at this point in the history
GitOrigin-RevId: 6cbb1496aff5a80b3d1b30b48b8d6deb65d74281
  • Loading branch information
adrw authored and svc-squareup-copybara committed Feb 13, 2025
1 parent dde6299 commit fe24704
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 12 deletions.
2 changes: 1 addition & 1 deletion misk-admin/api/misk-admin.api
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ public final class misk/web/v2/DashboardIndexAccessBlock {
public final class misk/web/v2/DashboardIndexAction : misk/web/actions/WebAction {
public static final field Companion Lmisk/web/v2/DashboardIndexAction$Companion;
public fun <init> (Lmisk/scope/ActionScoped;Lmisk/web/v2/DashboardPageLayout;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lwisp/deployment/Deployment;)V
public final fun get ()Ljava/lang/String;
public final fun get (Ljava/lang/String;)Ljava/lang/String;
}

public final class misk/web/v2/DashboardIndexAction$Companion {
Expand Down
79 changes: 74 additions & 5 deletions misk-admin/src/main/kotlin/misk/web/v2/DashboardIFrameTabAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import misk.web.mediatype.MediaTypes
import misk.web.proxy.WebProxyAction
import misk.web.resources.StaticResourceAction
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import wisp.deployment.Deployment
import wisp.logging.getLogger

Expand Down Expand Up @@ -53,6 +55,9 @@ internal class DashboardIFrameTabAction @Inject constructor(
if (iframeTab != null) {
// If the tab is found, render the iframe

val iframeSrc = "${iframeTab.iframePath}$suffix"
val hostname = "http://localhost/"

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

Expand All @@ -72,8 +77,8 @@ internal class DashboardIFrameTabAction @Inject constructor(
// 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())
val tabEntrypointJsResponse = webProxyAction
.getResponse((hostname / tabEntrypointJs).toHttpUrl())
if (tabEntrypointJsResponse.statusCode != 200) {
div("container mx-auto p-8") {
AlertError("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
Expand All @@ -83,21 +88,78 @@ internal class DashboardIFrameTabAction @Inject constructor(
} 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())
.getResponse((hostname / tabEntrypointJs).toHttpUrl())
if (tabEntrypointJsResponse.statusCode != 200) {
logger.info("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel} Responsse: $tabEntrypointJsResponse")
logger.info("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel} Response: $tabEntrypointJsResponse")
div("container mx-auto p-8") {
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.")
}
}
}
}
} else if (iframeTab.iframePath.endsWith("index.html")) {
// If tab is a non Misk-Web frontend, 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) {
div("container mx-auto p-8") {
AlertError("No tab found for slug: $slug")
AlertInfo("Check your DashboardModule installation to ensure that the slug, urlPathPrefix, and iframePath matches your frontend location.")
}
} else {
// 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((hostname / iframeTab.iframePath).toHttpUrl())
if (tabEntrypointJsResponse.statusCode != 200) {
div("container mx-auto p-8") {
AlertError("Failed to load tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
if (iframeTab.iframePath == "/_tab/web-actions-v4/index.html") {
AlertInfo("In local development, this can be from not having your local dev server (ie. Webpack) running or not doing an initial local frontend build to generate the necessary web assets. Try running in your Terminal: \$ gradle :misk:misk-admin:web-actions:buildWebActionsTab.")
} else {
AlertInfo("In local development, this can be from not having your local dev server (ie. Webpack) running or not doing an initial local frontend 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((hostname / iframeTab.iframePath).toHttpUrl())
if (tabEntrypointJsResponse.statusCode != 200) {
logger.info("Failed to load tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel} Response: $tabEntrypointJsResponse")
div("container mx-auto p-8") {
AlertError("Failed to load 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.")
}
}
}
}
} else {
// If tab is not Misk-Web, show generic error message if src doesn't resolve
try {
OkHttpClient.Builder().build().newCall(Request((hostname / iframeSrc).toHttpUrl()))
.execute()
.use { response ->
if (!response.isSuccessful) {
div("container mx-auto p-8") {
AlertError("Failed to load tab at $iframeSrc")
}
}
}
} catch (e: Exception) {
div("container mx-auto p-8") {
AlertError("Failed to load tab at $iframeSrc")
}
}
}

// Always still show iframe so that full load errors show up in browser console
iframe(classes = "h-full w-full") {
src = "${iframeTab.iframePath}$suffix"
src = iframeSrc
}
} else {
div("container mx-auto p-8") {
Expand All @@ -107,6 +169,13 @@ internal class DashboardIFrameTabAction @Inject constructor(
}
}

// Allow easier concatenation of URL paths with auto-stripping of extra slashes
private operator fun String.div(other: String): String = StringBuilder()
.append(this.removeSuffix("/"))
.append("/")
.append(other.removePrefix("/"))
.toString()

companion object {
private val logger = getLogger<DashboardIFrameTabAction>()
}
Expand Down
14 changes: 11 additions & 3 deletions misk-admin/src/main/kotlin/misk/web/v2/DashboardIndexAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import kotlinx.html.span
import misk.MiskCaller
import misk.scope.ActionScoped
import misk.security.authz.Unauthenticated
import misk.tailwind.components.AlertError
import misk.tailwind.components.AlertInfo
import misk.web.Get
import misk.web.PathParam
import misk.web.ResponseContentType
import misk.web.actions.WebAction
import misk.web.dashboard.DashboardTab
Expand All @@ -34,11 +37,18 @@ class DashboardIndexAction @Inject constructor(
@Get("/{rest:.*}")
@ResponseContentType(MediaTypes.TEXT_HTML)
@Unauthenticated
fun get(): String = dashboardPageLayout
fun get(@PathParam rest: String?): String = dashboardPageLayout
.newBuilder()
.title { appName, dashboardHomeUrl, _ -> "Home | $appName ${dashboardHomeUrl?.dashboardAnnotationKClass?.titlecase() ?: ""}" }
.build { appName, dashboardHomeUrl, _ ->
div("center container p-8") {
if (rest?.isNotBlank() == true) {
// Show 404 message if the tab is not found.
AlertError("""Dashboard tab not found for: ${rest.removeSuffix("/")}""")
AlertInfo("Check your DashboardModule installation to ensure that the slug, urlPathPrefix, and iframePath matches your frontend location.")
}

// Welcome
h1("text-2xl") {
+"""Welcome, """
span("font-bold font-mono") { +"""${callerProvider.get()?.user}""" }
Expand All @@ -50,8 +60,6 @@ class DashboardIndexAction @Inject constructor(
+"""."""
}

// TODO setup better 404 for dashboard /*

// Access notice block.
val dashboardTabs =
allTabs.filter { it.dashboard_slug == dashboardHomeUrl?.dashboard_slug }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,41 @@ class ExemplarDashboardModule(private val deployment: Deployment) : KAbstractMod

// Custom Admin Dashboard Tab at /_admin/... which doesn't exist and shows graceful failure 404
install(WebActionModule.create<AlphaIndexAction>())

// Tests 404 error message for misconfigured tabs
install(
DashboardModule.createIFrameTab<AdminDashboard, AdminDashboardAccess>(
slug = "not-found",
urlPathPrefix = "/_admin/not-found-iframe/",
iframePath = "/path/to/not-found-iframe/index.html",
menuLabel = "Not Found IFrame",
menuCategory = "Admin Tools"
)
)
install(
DashboardModule.createMiskWebTab<AdminDashboard, AdminDashboardAccess>(
isDevelopment = deployment.isLocalDevelopment,
slug = "not-found",
urlPathPrefix = "/_admin/not-found/",
slug = "not-found-misk-web",
urlPathPrefix = "/_admin/not-found-misk-web/",
developmentWebProxyUrl = "http://localhost:3000/",
menuLabel = "Not Found",
menuLabel = "Not Found Misk-Web",
menuCategory = "Admin Tools"
)
)
install(
DashboardModule.createIFrameTab<AdminDashboard, AdminDashboardAccess>(
slug = "not-found-react",
urlPathPrefix = "/_admin/not-found-react/",
iframePath = "/_tab/not-found-react/index.html",
menuLabel = "Not Found React",
menuCategory = "Admin Tools"
)
)
install(DashboardModule.createMenuLink<AdminDashboard, AdminDashboardAccess>(
label = "Not Found Link",
url = "/_admin/not-found-link/",
category = "Admin Tools"
))
}
}

Expand Down

0 comments on commit fe24704

Please sign in to comment.