quokka-admin 0.1.0

An admin panel for quokka
Documentation
use axum::routing::get;
use full::{
    auth::{TestAuthProvider, TestLoginProvider},
    ui::test::{TestEntityCreateForm, TestEntityListing, TestEntityUpdateForm},
};
use quokka::{
    bundle::Pouch,
    state::{
        Database, ProvideState, ProvideStateRef, Resources, Scripting, State, Styling, Templating,
    },
};
use quokka_admin::{
    data::AdminDashboardWidget,
    handler::EntityHandler,
    service::page_loader::{AdminNavigationGroup, AdminSidebarWidget, NavigationItem},
    state::AdminState,
    AdminBundle,
};

mod full;

pub const EXAMPLE_SUPER_USER_GROUP: &str = "super_example_user";

#[derive(Clone, State, ProvideState)]
pub struct ExampleState {
    templating: Templating,
    database: Database,
    styling: Styling,
    scripting: Scripting,
    resources: Resources,
    admin_state: AdminState<Self>,
}

struct ExampleBundle;

async fn test_widget() -> String {
    String::from("Example widget")
}

async fn time_widget() -> String {
    format!(
        "Current Server Time: {}",
        time::OffsetDateTime::now_local().unwrap_or_else(|_| time::OffsetDateTime::now_utc())
    )
}

impl<S> Pouch<S> for ExampleBundle
where
    S: ProvideState<Database>,
    S: ProvideState<Templating>,
    S: ProvideStateRef<AdminState<S>>,
    S: State + 'static,
{
    fn from_config(_: &quokka::config::Config) -> quokka::Result<Self>
    where
        Self: Sized,
    {
        Ok(Self)
    }

    fn get_router(&mut self, _state: &S) -> axum::Router<S> {
        axum::Router::new()
            .route("/example/admin/test_widget", get(test_widget))
            .route("/example/admin/time_widget", get(time_widget))
    }

    fn configure_state(&mut self, state: &mut S) -> quokka::Result<()> {
        let admin: &mut AdminState<_> = state.provide_mut();

        admin.super_admin_group = EXAMPLE_SUPER_USER_GROUP.to_string().into(); // This can be and usually should be set via the config
        admin.add_auth_provider(TestAuthProvider);
        admin.add_login_provider(TestLoginProvider);

        admin.add_navigation(
            AdminNavigationGroup::new("Example").add(
                NavigationItem::new("example_page", "Example Page").link("/admin/example/page"),
            ),
        );

        admin.add_sidebar_widget(AdminSidebarWidget::htmx("/example/admin/test_widget"));
        admin.add_sidebar_widget(AdminSidebarWidget::htmx("/example/admin/time_widget")); // .hx_trigger("load, every 1s") // This widget should be refreshed, but as it spams the console I leave this comment here so you know that it is possible

        admin.add_dashboard_widget(AdminDashboardWidget::htmx("/example/admin/test_widget"));
        admin.add_dashboard_widget(AdminDashboardWidget::htmx("/example/admin/test_widget"));
        admin.add_dashboard_widget(AdminDashboardWidget::htmx("/example/admin/test_widget"));
        admin.add_dashboard_widget(AdminDashboardWidget::htmx("/example/admin/test_widget"));

        admin.register_entity_handler(EntityHandler::<
            S,
            TestEntityCreateForm,
            TestEntityUpdateForm,
            TestEntityListing,
        >::new("Example"));

        Ok(())
    }

    fn get_migrations(&self) -> Option<sqlx::migrate::Migrator> {
        Some(sqlx::migrate::Migrator {
            migrations: vec![sqlx::migrate::Migration {
                version: 1,
                description: "Test migration".into(),
                migration_type: sqlx::migrate::MigrationType::Simple,
                sql: r#"CREATE TABLE IF NOT EXISTS "test" (
                        id SERIAL NOT NULL PRIMARY KEY,
                        name VARCHAR(255) NOT NULL,
                        age INTEGER NOT NULL,
                        created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
                        updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW())"#
                    .into(),
                checksum: vec![1, 2, 3, 4, 5, 6].into(),
                no_tx: true,
            }]
            .into(),
            ignore_missing: true,
            locking: true,
            no_tx: true,
        })
    }
}

#[tokio::main]
async fn main() -> quokka::Result<()> {
    quokka::Quokka::<ExampleState>::try_default()?
        .load::<ExampleBundle>()?
        .load::<AdminBundle>()?
        .serve()
        .await
}