quokka_admin/service/page_loader/
admin_page_loader.rs1use 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}