Skip to main content

quokka_admin/service/page_loader/
admin_page_loader.rs

1use axum::http::StatusCode;
2use quokka::{
3    extract::Extensions,
4    handler::html::TemplateDataLoader,
5    state::{FromState, ProvideState, Templating},
6};
7
8pub use crate::data::navigation::*;
9pub use crate::data::sidebar_widget::*;
10use crate::{data::Toast, state::AdminState};
11
12pub const ADMIN_ERROR_TEMPLATE: &str = "quokka-admin/page/error/error.html.hbs";
13
14#[derive(Clone, FromState)]
15pub struct AdminPageLoader {
16    templating: Templating,
17    #[from_state(
18        bounds = "State: ProvideState<AdminState<State>> + 'static",
19        builder = ProvideState::<AdminState<_>>::provide(state).get_navigation(),
20    )]
21    navigation: Vec<AdminNavigationGroup>,
22    #[from_state(
23        bounds = "State: ProvideState<AdminState<State>> + 'static",
24        builder = ProvideState::<AdminState<_>>::provide(state).sidebar_widgets,
25    )]
26    sidebar_widgets: Vec<AdminSidebarWidget>,
27}
28
29#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
30pub struct AdminErrorMessage {
31    pub message: String,
32    pub status_code: u16,
33}
34
35#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
36pub struct AdminPageData<P> {
37    pub title: String,
38    pub subtitle: Option<String>,
39    pub navigation: Vec<AdminNavigationGroup>,
40    pub sidebar_widgets: Vec<AdminSidebarWidget>,
41    pub toasts: Vec<Toast>,
42    #[serde(flatten)]
43    pub page: P,
44    pub error: Option<AdminErrorMessage>,
45}
46
47impl<S: Send + Sync + 'static> TemplateDataLoader<S> for AdminPageLoader {
48    type Args = Extensions<Toast>;
49
50    type Data = AdminPageData<()>;
51
52    #[tracing::instrument(
53        skip_all,
54        target = "quokka_admin::service::page_loader::admin_page_loader::AdminPageLoader"
55    )]
56    async fn load_data(&self, Extensions(toasts): Self::Args) -> quokka::Result<Self::Data> {
57        Ok(AdminPageData {
58            title: "Quokka Admin".to_string(),
59            navigation: self.navigation.clone(),
60            sidebar_widgets: self.sidebar_widgets.clone(),
61            toasts,
62            ..Default::default()
63        })
64    }
65
66    #[tracing::instrument(
67        skip_all,
68        target = "quokka_admin::service::page_loader::admin_page_loader::AdminPageLoader"
69    )]
70    async fn render_error(&self, error: quokka::Error) -> impl axum::response::IntoResponse {
71        tracing::error!(?error, "Caught error in AdminPageLoader::render_error");
72
73        let mut message = error.message;
74
75        if error.status_code == 403 {
76            message = "Permission denied".to_string();
77        }
78
79        (
80            StatusCode::from_u16(error.status_code)
81                .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR),
82            axum::response::Html(self.templating.render(
83                ADMIN_ERROR_TEMPLATE,
84                &AdminPageData::<()> {
85                    title: "Quokka Admin".to_string(),
86                    navigation: self.navigation.clone(),
87                    sidebar_widgets: self.sidebar_widgets.clone(),
88                    error: Some(AdminErrorMessage {
89                        message,
90                        status_code: error.status_code,
91                    }),
92                    ..Default::default()
93                },
94            )),
95        )
96    }
97}
98
99impl<P> AdminPageData<P> {
100    pub fn page<P2>(self, page: P2) -> AdminPageData<P2> {
101        AdminPageData {
102            page,
103            title: self.title,
104            subtitle: self.subtitle,
105            navigation: self.navigation,
106            sidebar_widgets: self.sidebar_widgets,
107            toasts: self.toasts,
108            error: self.error,
109        }
110    }
111
112    pub fn message(mut self, toast: impl Into<Toast>) -> Self {
113        self.toasts.push(toast.into());
114
115        self
116    }
117}