quokka-admin 0.1.0

An admin panel for quokka
Documentation
use std::collections::HashMap;

use quokka::{
    config::TryFromModule,
    state::{Database, ProvideState, Templating},
};

use crate::{
    data::{AdminDashboardWidget, AdminNavigationGroup, AdminSidebarWidget},
    handler::{EntityHandler, TypeErasedEntityHandler},
    middleware::{
        AdminAuthProvider, AdminLoginProvider, AuthProviders, InnerAuthProvider,
        InnerLoginProvider, LoginProviders,
    },
    service::{
        page_loader::{AdminCreateFormPageLoader, AdminPageLoader, AdminUpdateFormPageLoader},
        AdminCreateForm, AdminListing, AdminUpdateForm, FormBuilder,
    },
};

#[derive(Clone)]
pub struct AdminState<S> {
    pub title: String,
    pub navigation_groups: HashMap<String, AdminNavigationGroup>,
    pub sidebar_widgets: Vec<AdminSidebarWidget>,
    pub dashboard_widgets: Vec<AdminDashboardWidget>,
    pub entities: Vec<std::sync::Arc<dyn TypeErasedEntityHandler<S> + Send + Sync>>,
    pub auth_providers: AuthProviders<S>,
    pub login_url: String,
    pub super_admin_group: Option<String>,
    pub login_providers: LoginProviders,
}

#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct AdminStateConfig {
    pub super_admin_group: Option<String>,
}

impl<S> AdminState<S>
where
    S: quokka::state::State + 'static,
{
    ///
    /// Adds a navigation section for your bundle
    ///
    pub fn add_navigation(&mut self, group: impl Into<AdminNavigationGroup>) {
        let group: AdminNavigationGroup = group.into();
        self.navigation_groups
            .entry(group.title.clone())
            .and_modify(|entry| {
                entry.items.extend(group.items.clone());
            })
            .or_insert(group);
    }

    ///
    /// Gets an ordered version of the navigation
    ///
    pub fn get_navigation(&self) -> Vec<AdminNavigationGroup> {
        let mut final_navigation_groups: Vec<AdminNavigationGroup> =
            self.navigation_groups.values().cloned().collect();

        final_navigation_groups.sort_by_key(|item| item.order);

        final_navigation_groups
    }

    ///
    /// Adds a sidebar widget, check [AdminSidebarWidget] for details
    ///
    pub fn add_sidebar_widget(&mut self, widget: impl Into<AdminSidebarWidget>) {
        self.sidebar_widgets.push(widget.into())
    }

    ///
    /// Adds a dashboard widget, check [AdminDashboardWidget] for details
    ///
    pub fn add_dashboard_widget(&mut self, widget: impl Into<AdminDashboardWidget>) {
        self.dashboard_widgets.push(widget.into())
    }

    ///
    /// Registers your custom entity to be managable in the admin-ui. Check the [EntityHandler] for more details.
    ///
    pub fn register_entity_handler<C, U, L>(&mut self, handler: EntityHandler<S, C, U, L>)
    where
        S: ProvideState<Database>,
        S: ProvideState<Templating>,
        S: ProvideState<FormBuilder<S>>,
        S: ProvideState<AdminCreateFormPageLoader<S, C>>,
        S: ProvideState<AdminUpdateFormPageLoader<S, U>>,
        S: ProvideState<AdminPageLoader>,
        C: AdminCreateForm<S>
            + serde::de::DeserializeOwned
            + serde::Serialize
            + std::fmt::Debug
            + Send
            + Sync
            + 'static,
        U: AdminUpdateForm<S>
            + serde::de::DeserializeOwned
            + serde::Serialize
            + std::fmt::Debug
            + Send
            + Sync
            + 'static,
        U::PrimaryKeys: serde::de::DeserializeOwned + Send + Sync + 'static,
        L: AdminListing<S> + Clone + Send + Sync + serde::Serialize + 'static,
        L::PrimaryKeys: serde::de::DeserializeOwned + Send + Sync + 'static,
        L::Entity: serde::Serialize
            + serde::de::DeserializeOwned
            + Clone
            + std::fmt::Debug
            + Send
            + 'static,
    {
        self.add_navigation(handler.get_navigation());

        self.entities.push(std::sync::Arc::new(handler));
    }

    ///
    /// Registers the route for the [AdminBundle]
    ///
    pub fn register_routes(&self, mut router: axum::Router<S>) -> axum::Router<S> {
        for entity in &self.entities {
            router = router.merge(entity.get_router());
        }

        router
    }

    ///
    /// Adds a custom auth provider. Check the [AdminAuthProvider] for more details.
    ///
    pub fn add_auth_provider<P: AdminAuthProvider<S> + InnerAuthProvider<S> + 'static>(
        &mut self,
        provider: P,
    ) {
        self.auth_providers
            .providers
            .push(std::sync::Arc::new(provider));
    }

    ///
    /// Adds a custom login provider. Check the [AdminLoginProvider] for details.
    ///
    pub fn add_login_provider<
        P: AdminLoginProvider + InnerLoginProvider + Send + Sync + 'static,
    >(
        &mut self,
        provider: P,
    ) {
        self.login_providers
            .providers
            .push(std::sync::Arc::new(provider));
    }
}

impl<S> TryFromModule for AdminState<S> {
    async fn try_from_module(module: &quokka::config::Module) -> quokka::Result<Option<Self>>
    where
        Self: Sized,
    {
        if module.module.ne("quokka-admin") {
            return Ok(None);
        }

        let config = module.build_config::<AdminStateConfig>()?;

        Ok(Some(Self {
            title: "Quokka Admin".to_string(),
            login_url: "/admin/login".to_string(),
            super_admin_group: config.super_admin_group,
            navigation_groups: Default::default(),
            sidebar_widgets: Default::default(),
            dashboard_widgets: Default::default(),
            entities: Default::default(), // The default derive fails here because of the State
            auth_providers: Default::default(),
            login_providers: Default::default(),
        }))
    }
}