Skip to content

Commit fe24704

Browse files
adrwsvc-squareup-copybara
authored andcommitted
Add error UI for tab load and link failures
GitOrigin-RevId: 6cbb1496aff5a80b3d1b30b48b8d6deb65d74281
1 parent dde6299 commit fe24704

File tree

4 files changed

+114
-12
lines changed

4 files changed

+114
-12
lines changed

misk-admin/api/misk-admin.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1001,7 +1001,7 @@ public final class misk/web/v2/DashboardIndexAccessBlock {
10011001
public final class misk/web/v2/DashboardIndexAction : misk/web/actions/WebAction {
10021002
public static final field Companion Lmisk/web/v2/DashboardIndexAction$Companion;
10031003
public fun <init> (Lmisk/scope/ActionScoped;Lmisk/web/v2/DashboardPageLayout;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lwisp/deployment/Deployment;)V
1004-
public final fun get ()Ljava/lang/String;
1004+
public final fun get (Ljava/lang/String;)Ljava/lang/String;
10051005
}
10061006

10071007
public final class misk/web/v2/DashboardIndexAction$Companion {

misk-admin/src/main/kotlin/misk/web/v2/DashboardIFrameTabAction.kt

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import misk.web.mediatype.MediaTypes
2121
import misk.web.proxy.WebProxyAction
2222
import misk.web.resources.StaticResourceAction
2323
import okhttp3.HttpUrl.Companion.toHttpUrl
24+
import okhttp3.OkHttpClient
25+
import okhttp3.Request
2426
import wisp.deployment.Deployment
2527
import wisp.logging.getLogger
2628

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

58+
val iframeSrc = "${iframeTab.iframePath}$suffix"
59+
val hostname = "http://localhost/"
60+
5661
if (iframeTab.iframePath.startsWith(MiskWebTabIndexAction.PATH)) {
5762
// If tab is Misk-Web, do extra validation to show a more helpful error message
5863

@@ -72,8 +77,8 @@ internal class DashboardIFrameTabAction @Inject constructor(
7277
// If tab is Misk-Web do additional checks and show separate development and real errors
7378
if (deployment.isLocalDevelopment) {
7479
// If local development, check web proxy action and show fuller development 404 message
75-
val tabEntrypointJsResponse =
76-
webProxyAction.getResponse(("http://localhost" + tabEntrypointJs).toHttpUrl())
80+
val tabEntrypointJsResponse = webProxyAction
81+
.getResponse((hostname / tabEntrypointJs).toHttpUrl())
7782
if (tabEntrypointJsResponse.statusCode != 200) {
7883
div("container mx-auto p-8") {
7984
AlertError("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
@@ -83,21 +88,78 @@ internal class DashboardIFrameTabAction @Inject constructor(
8388
} else if (deployment.isReal) {
8489
// If real environment, only check static resource action and show limited 404 message
8590
val tabEntrypointJsResponse = staticResourceAction
86-
.getResponse(("http://localhost" + tabEntrypointJs).toHttpUrl())
91+
.getResponse((hostname / tabEntrypointJs).toHttpUrl())
8792
if (tabEntrypointJsResponse.statusCode != 200) {
88-
logger.info("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel} Responsse: $tabEntrypointJsResponse")
93+
logger.info("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel} Response: $tabEntrypointJsResponse")
8994
div("container mx-auto p-8") {
9095
AlertError("Failed to load Misk-Web tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
9196
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.")
9297
}
9398
}
9499
}
95100
}
101+
} else if (iframeTab.iframePath.endsWith("index.html")) {
102+
// If tab is a non Misk-Web frontend, do extra validation to show a more helpful error message
103+
104+
val slug = iframeTab.urlPathPrefix.split("/").last { it.isNotBlank() }
105+
val dashboardTab = dashboardTabs.firstOrNull { slug == it.slug }
106+
107+
if (dashboardTab == null) {
108+
div("container mx-auto p-8") {
109+
AlertError("No tab found for slug: $slug")
110+
AlertInfo("Check your DashboardModule installation to ensure that the slug, urlPathPrefix, and iframePath matches your frontend location.")
111+
}
112+
} else {
113+
// If tab is Misk-Web do additional checks and show separate development and real errors
114+
if (deployment.isLocalDevelopment) {
115+
// If local development, check web proxy action and show fuller development 404 message
116+
val tabEntrypointJsResponse = webProxyAction
117+
.getResponse((hostname / iframeTab.iframePath).toHttpUrl())
118+
if (tabEntrypointJsResponse.statusCode != 200) {
119+
div("container mx-auto p-8") {
120+
AlertError("Failed to load tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
121+
if (iframeTab.iframePath == "/_tab/web-actions-v4/index.html") {
122+
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.")
123+
} else {
124+
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.")
125+
}
126+
}
127+
}
128+
} else if (deployment.isReal) {
129+
// If real environment, only check static resource action and show limited 404 message
130+
val tabEntrypointJsResponse = staticResourceAction
131+
.getResponse((hostname / iframeTab.iframePath).toHttpUrl())
132+
if (tabEntrypointJsResponse.statusCode != 200) {
133+
logger.info("Failed to load tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel} Response: $tabEntrypointJsResponse")
134+
div("container mx-auto p-8") {
135+
AlertError("Failed to load tab: ${dashboardTab.menuCategory} / ${dashboardTab.menuLabel}")
136+
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.")
137+
}
138+
}
139+
}
140+
}
141+
} else {
142+
// If tab is not Misk-Web, show generic error message if src doesn't resolve
143+
try {
144+
OkHttpClient.Builder().build().newCall(Request((hostname / iframeSrc).toHttpUrl()))
145+
.execute()
146+
.use { response ->
147+
if (!response.isSuccessful) {
148+
div("container mx-auto p-8") {
149+
AlertError("Failed to load tab at $iframeSrc")
150+
}
151+
}
152+
}
153+
} catch (e: Exception) {
154+
div("container mx-auto p-8") {
155+
AlertError("Failed to load tab at $iframeSrc")
156+
}
157+
}
96158
}
97159

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

172+
// Allow easier concatenation of URL paths with auto-stripping of extra slashes
173+
private operator fun String.div(other: String): String = StringBuilder()
174+
.append(this.removeSuffix("/"))
175+
.append("/")
176+
.append(other.removePrefix("/"))
177+
.toString()
178+
110179
companion object {
111180
private val logger = getLogger<DashboardIFrameTabAction>()
112181
}

misk-admin/src/main/kotlin/misk/web/v2/DashboardIndexAction.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import kotlinx.html.span
1010
import misk.MiskCaller
1111
import misk.scope.ActionScoped
1212
import misk.security.authz.Unauthenticated
13+
import misk.tailwind.components.AlertError
14+
import misk.tailwind.components.AlertInfo
1315
import misk.web.Get
16+
import misk.web.PathParam
1417
import misk.web.ResponseContentType
1518
import misk.web.actions.WebAction
1619
import misk.web.dashboard.DashboardTab
@@ -34,11 +37,18 @@ class DashboardIndexAction @Inject constructor(
3437
@Get("/{rest:.*}")
3538
@ResponseContentType(MediaTypes.TEXT_HTML)
3639
@Unauthenticated
37-
fun get(): String = dashboardPageLayout
40+
fun get(@PathParam rest: String?): String = dashboardPageLayout
3841
.newBuilder()
3942
.title { appName, dashboardHomeUrl, _ -> "Home | $appName ${dashboardHomeUrl?.dashboardAnnotationKClass?.titlecase() ?: ""}" }
4043
.build { appName, dashboardHomeUrl, _ ->
4144
div("center container p-8") {
45+
if (rest?.isNotBlank() == true) {
46+
// Show 404 message if the tab is not found.
47+
AlertError("""Dashboard tab not found for: ${rest.removeSuffix("/")}""")
48+
AlertInfo("Check your DashboardModule installation to ensure that the slug, urlPathPrefix, and iframePath matches your frontend location.")
49+
}
50+
51+
// Welcome
4252
h1("text-2xl") {
4353
+"""Welcome, """
4454
span("font-bold font-mono") { +"""${callerProvider.get()?.user}""" }
@@ -50,8 +60,6 @@ class DashboardIndexAction @Inject constructor(
5060
+"""."""
5161
}
5262

53-
// TODO setup better 404 for dashboard /*
54-
5563
// Access notice block.
5664
val dashboardTabs =
5765
allTabs.filter { it.dashboard_slug == dashboardHomeUrl?.dashboard_slug }

samples/exemplar/src/main/kotlin/com/squareup/exemplar/dashboard/ExemplarDashboardModule.kt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,41 @@ class ExemplarDashboardModule(private val deployment: Deployment) : KAbstractMod
9999

100100
// Custom Admin Dashboard Tab at /_admin/... which doesn't exist and shows graceful failure 404
101101
install(WebActionModule.create<AlphaIndexAction>())
102+
103+
// Tests 404 error message for misconfigured tabs
104+
install(
105+
DashboardModule.createIFrameTab<AdminDashboard, AdminDashboardAccess>(
106+
slug = "not-found",
107+
urlPathPrefix = "/_admin/not-found-iframe/",
108+
iframePath = "/path/to/not-found-iframe/index.html",
109+
menuLabel = "Not Found IFrame",
110+
menuCategory = "Admin Tools"
111+
)
112+
)
102113
install(
103114
DashboardModule.createMiskWebTab<AdminDashboard, AdminDashboardAccess>(
104115
isDevelopment = deployment.isLocalDevelopment,
105-
slug = "not-found",
106-
urlPathPrefix = "/_admin/not-found/",
116+
slug = "not-found-misk-web",
117+
urlPathPrefix = "/_admin/not-found-misk-web/",
107118
developmentWebProxyUrl = "http://localhost:3000/",
108-
menuLabel = "Not Found",
119+
menuLabel = "Not Found Misk-Web",
109120
menuCategory = "Admin Tools"
110121
)
111122
)
123+
install(
124+
DashboardModule.createIFrameTab<AdminDashboard, AdminDashboardAccess>(
125+
slug = "not-found-react",
126+
urlPathPrefix = "/_admin/not-found-react/",
127+
iframePath = "/_tab/not-found-react/index.html",
128+
menuLabel = "Not Found React",
129+
menuCategory = "Admin Tools"
130+
)
131+
)
132+
install(DashboardModule.createMenuLink<AdminDashboard, AdminDashboardAccess>(
133+
label = "Not Found Link",
134+
url = "/_admin/not-found-link/",
135+
category = "Admin Tools"
136+
))
112137
}
113138
}
114139

0 commit comments

Comments
 (0)