making admin work with a state, however it probably should go into a middleware instead.
This commit is contained in:
35
src/admin/domain.rs
Normal file
35
src/admin/domain.rs
Normal 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
13
src/admin/mod.rs
Normal 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
158
src/admin/state.rs
Normal 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
171
src/admin/views.rs
Normal 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()
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod admin;
|
||||
pub mod schema;
|
||||
pub mod service;
|
||||
pub mod state;
|
||||
|
||||
12
src/main.rs
12
src/main.rs
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
10
src/state.rs
10
src/state.rs
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user