quokka-admin 0.1.0

An admin panel for quokka
Documentation
use axum::http::StatusCode;
use quokka::{
    extract::Extensions,
    handler::html::TemplateDataLoader,
    state::{FromState, ProvideState, Templating},
};

pub use crate::data::navigation::*;
pub use crate::data::sidebar_widget::*;
use crate::{data::Toast, state::AdminState};

pub const ADMIN_ERROR_TEMPLATE: &str = "quokka-admin/page/error/error.html.hbs";

#[derive(Clone, FromState)]
pub struct AdminPageLoader {
    templating: Templating,
    #[from_state(
        bounds = "State: ProvideState<AdminState<State>> + 'static",
        builder = ProvideState::<AdminState<_>>::provide(state).get_navigation(),
    )]
    navigation: Vec<AdminNavigationGroup>,
    #[from_state(
        bounds = "State: ProvideState<AdminState<State>> + 'static",
        builder = ProvideState::<AdminState<_>>::provide(state).sidebar_widgets,
    )]
    sidebar_widgets: Vec<AdminSidebarWidget>,
}

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct AdminErrorMessage {
    pub message: String,
    pub status_code: u16,
}

#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct AdminPageData<P> {
    pub title: String,
    pub subtitle: Option<String>,
    pub navigation: Vec<AdminNavigationGroup>,
    pub sidebar_widgets: Vec<AdminSidebarWidget>,
    pub toasts: Vec<Toast>,
    #[serde(flatten)]
    pub page: P,
    pub error: Option<AdminErrorMessage>,
}

impl<S: Send + Sync + 'static> TemplateDataLoader<S> for AdminPageLoader {
    type Args = Extensions<Toast>;

    type Data = AdminPageData<()>;

    #[tracing::instrument(
        skip_all,
        target = "quokka_admin::service::page_loader::admin_page_loader::AdminPageLoader"
    )]
    async fn load_data(&self, Extensions(toasts): Self::Args) -> quokka::Result<Self::Data> {
        Ok(AdminPageData {
            title: "Quokka Admin".to_string(),
            navigation: self.navigation.clone(),
            sidebar_widgets: self.sidebar_widgets.clone(),
            toasts,
            ..Default::default()
        })
    }

    #[tracing::instrument(
        skip_all,
        target = "quokka_admin::service::page_loader::admin_page_loader::AdminPageLoader"
    )]
    async fn render_error(&self, error: quokka::Error) -> impl axum::response::IntoResponse {
        tracing::error!(?error, "Caught error in AdminPageLoader::render_error");

        let mut message = error.message;

        if error.status_code == 403 {
            message = "Permission denied".to_string();
        }

        (
            StatusCode::from_u16(error.status_code)
                .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR),
            axum::response::Html(self.templating.render(
                ADMIN_ERROR_TEMPLATE,
                &AdminPageData::<()> {
                    title: "Quokka Admin".to_string(),
                    navigation: self.navigation.clone(),
                    sidebar_widgets: self.sidebar_widgets.clone(),
                    error: Some(AdminErrorMessage {
                        message,
                        status_code: error.status_code,
                    }),
                    ..Default::default()
                },
            )),
        )
    }
}

impl<P> AdminPageData<P> {
    pub fn page<P2>(self, page: P2) -> AdminPageData<P2> {
        AdminPageData {
            page,
            title: self.title,
            subtitle: self.subtitle,
            navigation: self.navigation,
            sidebar_widgets: self.sidebar_widgets,
            toasts: self.toasts,
            error: self.error,
        }
    }

    pub fn message(mut self, toast: impl Into<Toast>) -> Self {
        self.toasts.push(toast.into());

        self
    }
}