making admin work with a state, however it probably should go into a middleware instead.

This commit is contained in:
2023-12-19 21:53:35 +01:00
parent 26a1bee083
commit d89ff40d33
16 changed files with 3240 additions and 3 deletions

35
src/admin/domain.rs Normal file
View File

@@ -0,0 +1,35 @@
pub use dto::AdminApp;
pub use dto::AdminModel;
mod auth {
struct AdminUser {}
struct AdminRole {}
struct AdminGroup {}
struct AdminActionLog {}
}
mod dto {
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct AdminModel {
pub name: String,
pub object_name: String,
pub admin_url: String,
pub view_only: bool,
pub add_url: Option<String>,
}
#[derive(Deserialize, Serialize)]
pub struct AdminApp {
pub name: String,
pub app_label: String,
pub app_url: String,
pub models: Vec<AdminModel>,
}
}

13
src/admin/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
use axum::{routing::get, Router};
use crate::state::AppState;
pub mod domain;
pub mod state;
pub mod views;
/*
pub fn routes() -> Router<AppState> {
Router::new().route("/", get(views::index).post(views::index_action))
}
*/

158
src/admin/state.rs Normal file
View File

@@ -0,0 +1,158 @@
use crate::admin::domain::{AdminApp, AdminModel};
use axum::routing::MethodRouter;
use axum::{async_trait, response::IntoResponse};
use axum::{routing::get, Router};
use core::future::Future;
use std::collections::HashMap;
use std::sync::Arc;
pub type AdminState = Arc<AdminRegistry>;
// main registry.
#[derive(Clone)]
pub struct AdminRegistry {
base_path: String,
apps: HashMap<String, internal::DataNode>,
models: HashMap<String, internal::DataNode>,
}
impl AdminRegistry {
pub fn new(base_path: &str) -> Self {
AdminRegistry {
base_path: base_path.to_owned(),
apps: HashMap::new(),
models: HashMap::new(),
}
}
pub fn generate_router<T>(&self) -> Router<T>
where
T: Clone,
T: Send,
T: Sync,
T: 'static,
crate::service::templates::Templates: axum::extract::FromRef<T>,
AdminState: axum::extract::FromRef<T>,
{
routing::generate_routes::<T>(&self)
}
pub fn get_apps(&self) -> Vec<AdminApp> {
self.apps
.iter()
.map(|(key, node)| self.get_app(key, node))
.collect()
}
fn get_app(&self, name: &str, node: &internal::DataNode) -> AdminApp {
let label = node.label.as_ref().unwrap_or(&String::new()).clone();
AdminApp {
name: name.to_owned(),
app_label: label,
app_url: "".to_owned(),
models: vec![],
}
}
pub fn register_app(&mut self, name: &str, app_label: &str, app_url: &str) {
self.apps.insert(
name.to_owned(),
internal::DataNode {
object_name: name.to_owned(),
label: Some(app_label.to_owned()),
},
);
}
pub fn get_models(&self) -> Vec<AdminModel> {
vec![]
}
pub fn get_models_for_app(&self, name: &str) -> Vec<AdminModel> {
vec![]
}
pub fn register_model(&mut self, name: &str, config: AdminModelConfig) {}
}
// user uses this configuration object to register another model.
pub struct AdminModelConfig {
pub app: Arc<str>,
pub custom_index_handler: Option<MethodRouter>,
}
mod internal {
#[derive(Clone)]
pub struct DataNode {
pub object_name: String,
pub label: Option<String>,
}
}
mod routing {
use axum::{routing::get, Router};
pub fn generate_routes<T>(registry: &super::AdminRegistry) -> Router<T>
where
T: Clone,
T: Send,
T: Sync,
T: 'static,
crate::service::templates::Templates: axum::extract::FromRef<T>,
super::AdminState: axum::extract::FromRef<T>,
{
let mut router: Router<T> = Router::new();
let apps = registry.get_apps();
router = router.route(
&format!("/{}", registry.base_path),
get(crate::admin::views::index),
);
for app in apps {
let app_route = format!("/{}/{}", registry.base_path, app.app_url);
// Add a route for the app
router = router.route(
&app_route,
get(move || async move { format!("Admin panel for {}", app.app_label) }),
);
// Add routes for each model in the app
/*for model in data_node.models.iter() {
let model_route = format!("{}/{}", app_route, model.name);
router = router.route(
&model_route,
get(move || async move { format!("Model page for {}", model.name) }),
);
}*/
}
router
}
fn define_example_data(registry: &mut super::AdminRegistry) {
/*let models = vec![
AdminModel {
name: "User".to_owned(),
object_name: "User".to_owned(),
admin_url: "/admin/users/user".to_owned(),
view_only: false,
add_url: Some("/admin/users/user/add".to_owned()),
},
AdminModel {
name: "Group".to_owned(),
object_name: "Group".to_owned(),
admin_url: "/admin/users/group".to_owned(),
view_only: false,
add_url: None,
},
AdminModel {
name: "Permission".to_owned(),
object_name: "Permission".to_owned(),
admin_url: "/admin/users/permission".to_owned(),
view_only: true,
add_url: None,
},
];*/
registry.register_app("auth", "Authorities", "auth");
}
}

171
src/admin/views.rs Normal file
View File

@@ -0,0 +1,171 @@
use std::sync::Arc;
use axum::{extract::State, response::IntoResponse, Form};
use crate::admin::domain::{AdminApp, AdminModel};
use crate::admin::state;
use crate::service::templates;
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct Question {
question: String,
}
#[derive(Deserialize)]
pub struct ExampleData {
title: String,
content: String,
}
#[derive(Serialize, Deserialize)]
pub struct AdminRequest {
pub path: String,
}
#[derive(Deserialize, Serialize)]
pub struct AdminContext {
pub language_code: Option<String>,
pub language_bidi: Option<bool>,
pub user: Option<String>, // Todo: user type
pub site_url: Option<String>,
pub docsroot: Option<String>,
pub messages: Vec<String>, // Todo: message type
pub title: Option<String>,
pub subtitle: Option<String>,
pub content: String,
pub request: AdminRequest,
pub available_apps: Vec<AdminApp>,
}
impl Default for AdminContext {
fn default() -> Self {
AdminContext {
language_code: Some("en-us".to_string()), // Default language code
language_bidi: Some(false), // Default language bidi
user: None, //UserType::default(), // Assuming UserType has a Default impl
site_url: None,
docsroot: None,
messages: Vec::new(), // Empty vector for messages
title: None,
subtitle: None,
content: String::new(), // Empty string for content
available_apps: Vec::new(),
request: AdminRequest {
path: "".to_owned(),
},
}
}
}
// Index is the entry point of Admin. It should display the Dashboard to a logged in user.
pub async fn index_example(templates: State<templates::Templates>) -> impl IntoResponse {
let models = vec![
AdminModel {
name: "User".to_owned(),
object_name: "User".to_owned(),
admin_url: "/admin/users/user".to_owned(),
view_only: false,
add_url: Some("/admin/users/user/add".to_owned()),
},
AdminModel {
name: "Group".to_owned(),
object_name: "Group".to_owned(),
admin_url: "/admin/users/group".to_owned(),
view_only: false,
add_url: None,
},
AdminModel {
name: "Permission".to_owned(),
object_name: "Permission".to_owned(),
admin_url: "/admin/users/permission".to_owned(),
view_only: true,
add_url: None,
},
];
let core_app = AdminApp {
name: "Admin".to_owned(),
app_label: "admin".to_owned(),
app_url: "/admin/users/".to_owned(),
models: models,
};
templates.render_html(
"admin/index.html",
AdminContext {
available_apps: vec![core_app],
..Default::default()
},
)
}
pub async fn index(
templates: State<templates::Templates>,
administration: State<Arc<state::AdminRegistry>>,
) -> impl IntoResponse {
let models = vec![
AdminModel {
name: "User".to_owned(),
object_name: "User".to_owned(),
admin_url: "/admin/users/user".to_owned(),
view_only: false,
add_url: Some("/admin/users/user/add".to_owned()),
},
AdminModel {
name: "Group".to_owned(),
object_name: "Group".to_owned(),
admin_url: "/admin/users/group".to_owned(),
view_only: false,
add_url: None,
},
AdminModel {
name: "Permission".to_owned(),
object_name: "Permission".to_owned(),
admin_url: "/admin/users/permission".to_owned(),
view_only: true,
add_url: None,
},
];
let core_app = AdminApp {
name: "Admin".to_owned(),
app_label: "admin".to_owned(),
app_url: "/admin/users/".to_owned(),
models: models,
};
let mut available = vec![core_app];
available.extend(administration.get_apps());
templates.render_html(
"admin/index.html",
AdminContext {
available_apps: available,
..Default::default()
},
)
}
// Index Action is POST to the index site. We can anchor some general business code here.
pub async fn index_action(Form(example_data): Form<ExampleData>) -> impl IntoResponse {
"There is your answer!".to_owned()
}
// List Items renders the entire list item page.
pub async fn list_item_collection(templates: State<templates::Templates>) -> impl IntoResponse {
templates.render_html("admin/list_items.html", ())
}
// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input.
pub async fn item_collection_action(Form(question): Form<Question>) -> impl IntoResponse {
"There is your answer!".to_owned()
}
// Item Details shows one single dataset.
pub async fn item_details(templates: State<templates::Templates>) -> impl IntoResponse {
templates.render_html("admin/item_detail.html", ())
}
// Item Action allows running an action on one single dataset.
pub async fn item_action(Form(question): Form<Question>) -> impl IntoResponse {
"There is your answer!".to_owned()
}

View File

@@ -1,3 +1,4 @@
pub mod admin;
pub mod schema;
pub mod service;
pub mod state;

View File

@@ -1,3 +1,4 @@
mod admin;
mod howto;
mod service;
mod state;
@@ -8,6 +9,7 @@ use axum::{
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
};
use std::net::SocketAddr;
use std::sync::Arc;
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
templates.render_html("index.html", ())
@@ -21,12 +23,20 @@ async fn hello_world(templates: State<templates::Templates>) -> impl IntoRespons
async fn main() {
// Prepare App State
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
let state: AppState = AppState { templates: tmpl };
let mut admin = admin::state::AdminRegistry::new("admin");
admin.register_app("auth", "Authorities", "auth");
let admin_router = admin.generate_router();
let state: AppState = AppState {
templates: tmpl,
admin: Arc::new(admin),
};
// Application Route
let app = Router::new()
.route("/", get(home))
.route("/hello", get(hello_world))
.merge(admin_router)
.nest("/howto", howto::routes())
.route_service("/static/*file", handlers::static_handler.into_service())
.fallback(handlers::not_found_handler)

View File

@@ -17,6 +17,12 @@ impl Templates {
let template_path = "templates";
environment.set_loader(path_loader(&template_path));
environment.add_filter("markdown", markdown);
environment.add_filter("yesno", filter_yesno);
environment.add_function("static", tpl_static);
environment.add_function("url", tpl_url);
environment.add_function("csrf_token", tpl_to_be_implemented);
environment.add_function("translate", tpl_translate);
notifier.watch_path(template_path, true);
Ok(environment)
});
@@ -58,3 +64,23 @@ fn markdown(value: String) -> Value {
pulldown_cmark::html::push_html(&mut html, parser);
Value::from_safe_string(html)
}
fn filter_yesno(value: String, yesno: String) -> String {
"".into()
}
fn tpl_url(value: String) -> Value {
Value::from_safe_string(value.into())
}
fn tpl_translate(value: String) -> Value {
Value::from_safe_string(value.into())
}
fn tpl_to_be_implemented(value: String) -> Value {
Value::from_safe_string("".into())
}
fn tpl_static(value: String) -> Value {
Value::from_safe_string("".into())
}

View File

@@ -1,10 +1,14 @@
use std::sync::Arc;
use axum::extract::FromRef;
use crate::admin::state::AdminRegistry;
use crate::service::templates;
#[derive(Clone)]
pub struct AppState {
pub templates: templates::Templates,
pub admin: Arc<AdminRegistry>,
}
impl FromRef<AppState> for templates::Templates {
@@ -12,3 +16,9 @@ impl FromRef<AppState> for templates::Templates {
app_state.templates.clone()
}
}
impl FromRef<AppState> for Arc<AdminRegistry> {
fn from_ref(app_state: &AppState) -> Arc<AdminRegistry> {
app_state.admin.clone()
}
}