use topcoat::{
context::{Cx, try_context},
icon::icon,
router::{
Body, RouteFuture,
error::{
BadRequestError, ContentTooLargeError, ForbiddenError, InternalServerError,
MethodNotAllowedError, NotFoundError, RedirectError, RewriteError, SeeOther,
ServiceUnavailableError, TooManyRequestsError, UnauthorizedError, not_found, rewrite,
},
},
view::{BoxView, ViewExt, view},
};
use super::gate::panel_prefix;
pub(crate) const ERROR_SEGMENT: &str = "-/error";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PanelError {
NotFound,
Forbidden,
Failed,
}
impl PanelError {
pub(crate) fn of(error: &topcoat::Error) -> Option<Self> {
if error.is::<NotFoundError>() {
return Some(Self::NotFound);
}
if error.is::<ForbiddenError>() {
return Some(Self::Forbidden);
}
if error.is::<InternalServerError>() {
return Some(Self::Failed);
}
let answered = error.is::<RedirectError>()
|| error.is::<SeeOther>()
|| error.is::<RewriteError>()
|| error.is::<BadRequestError>()
|| error.is::<UnauthorizedError>()
|| error.is::<MethodNotAllowedError>()
|| error.is::<ContentTooLargeError>()
|| error.is::<TooManyRequestsError>()
|| error.is::<ServiceUnavailableError>();
(!answered).then_some(Self::Failed)
}
pub(crate) fn status(self) -> http::StatusCode {
match self {
Self::NotFound => http::StatusCode::NOT_FOUND,
Self::Forbidden => http::StatusCode::FORBIDDEN,
Self::Failed => http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub(crate) fn title(self) -> crate::Text {
crate::Text::message(match self {
Self::NotFound => "tablo-error-not-found",
Self::Forbidden => "tablo-error-forbidden",
Self::Failed => "tablo-error-failed",
})
}
fn description(self) -> crate::Text {
crate::Text::message(match self {
Self::NotFound => "tablo-error-not-found-description",
Self::Forbidden => "tablo-error-forbidden-description",
Self::Failed => "tablo-error-failed-description",
})
}
}
pub(crate) fn answer(cx: &Cx, error: topcoat::Error) -> topcoat::Error {
if try_context::<PanelError>(cx).is_some() || !crate::auth::page_load(cx) {
return error;
}
let Some(page) = PanelError::of(&error) else {
return error;
};
if page == PanelError::Failed {
tracing::error!(error = %error, "panel page failed; showing the error page");
}
rewrite(
format!("{}/{ERROR_SEGMENT}", panel_prefix(cx)),
Body::empty(),
)
.with(page)
.into()
}
pub(crate) fn current_error(cx: &Cx) -> Option<PanelError> {
try_context::<PanelError>(cx).copied()
}
pub(crate) fn error_page(cx: &Cx, _body: Body) -> BoxView<'_> {
let error = current_error(cx).unwrap_or(PanelError::NotFound);
let code = error.status().as_u16().to_string();
let home = crate::navigation::runtime_link(cx, &panel_prefix(cx));
let button =
tablo_ui::button_variants(tablo_ui::ButtonVariant::Outline, tablo_ui::ButtonSize::Md);
view! {
(error.status())
tablo_ui::page(
attrs: topcoat::view::attributes! { class="flex-1 justify-center" },
<div
class="flex flex-col items-center gap-3 py-16 text-center"
data-panel-error=(code.clone())
>
<p class="text-sm font-semibold text-muted-foreground">(code)</p>
<h1 class="text-2xl font-semibold tracking-tight text-foreground">
(error.title().localize(cx))
</h1>
<p class="max-w-md text-sm text-muted-foreground">
(error.description().localize(cx))
</p>
<a (home) class=(button) data-panel-error-home="">
icon(data: tablo_ui::icons::ARROW_LEFT)
(crate::t!(cx, "tablo-error-home"))
</a>
</div>
)
}
.boxed()
}
/// `{prefix}/{*rest}`, any method: a path no other route serves answers 404 behind the gate.
pub(crate) fn unmatched(_cx: &Cx, _body: Body) -> RouteFuture<'_> {
Box::pin(async { Err(not_found().into()) })
}
#[cfg(test)]
mod tests;