code: list+create improvements, expanded notes, added logging

This commit is contained in:
2024-02-07 13:37:35 +01:00
parent fdbc37db06
commit 1c6317808a
18 changed files with 293 additions and 90 deletions

View File

@@ -60,9 +60,18 @@ pub mod repository {
pub type RepositoryContext = AdminModel;
impl RepositoryContext {
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
Some(format!("{}/detail/{}", self.admin_url, key))
}
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
Some(format!("{}/change/{}", self.admin_url, key))
}
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
RepositoryItem {
detail_url: Some(format!("{}/detail/{}", self.admin_url, key)),
detail_url: self.get_default_detail_url(key),
change_url: self.get_default_change_url(key),
fields: fields,
}
}
@@ -217,6 +226,7 @@ pub mod repository {
pub struct RepositoryItem {
pub fields: Value,
pub detail_url: Option<String>,
pub change_url: Option<String>,
}
pub enum RepositoryList {

View File

@@ -15,8 +15,9 @@ pub fn routes() -> Router<AppState> {
"/app/:app/model/:model/add",
get(views::new_item).post(views::create_item),
)
.route("/app/:app/model/:model/change/:id", get(views::change_item))
.route(
"/app/:app/model/:model/detail/:id",
get(views::item_details),
get(views::view_item_details),
)
}

View File

@@ -116,9 +116,7 @@ pub async fn list_item_collection(
let repo = repo.lock().await;
let admin_model = registry
.get_model(&app_key, &model_key)
.expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
// Note: AdminModel contains Registry Data, while Repository only contains user data; however, both could be retrieved more reliably from each other.
// Another solution would be a clear "AdminRepositoryContext", that contains information about the current model.
.expect("Admin Model not found?");
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
@@ -145,7 +143,7 @@ pub async fn item_collection_action(
}
// Item Details shows one single dataset.
pub async fn item_details(
pub async fn view_item_details(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
@@ -155,9 +153,7 @@ pub async fn item_details(
let repo = repo.lock().await;
let admin_model = registry
.get_model(&app_key, &model_key)
.expect("Admin Model not found?"); // TODO: do we really need to differentiate between AdminModel and Repository, if both need to co-exist
// Note: AdminModel contains Registry Data, while Repository only contains user data; however, both could be retrieved more reliably from each other.
// Another solution would be a clear "AdminRepositoryContext", that contains information about the current model.
.expect("Admin Model not found?");
let key: LookupKey = id.into();
AdminContext {
base: base_template(&headers),
@@ -243,6 +239,37 @@ pub async fn create_item(
templates.render_html("admin/items/item_create.jinja", context)
}
pub async fn change_item(
templates: State<templates::Templates>,
registry: State<Arc<state::AdminRegistry>>,
headers: HeaderMap,
Path((app_key, model_key, id)): Path<(String, String, String)>,
) -> impl IntoResponse {
let context = if let Ok(repo) = registry.get_repository(&app_key, &model_key) {
let repo = repo.lock().await;
let admin_model = registry
.get_model(&app_key, &model_key)
.expect("Admin Model not found?");
let key: LookupKey = id.into();
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
item_info: Some(repo.info(&admin_model).await),
item_list: repo.list(&admin_model).await,
item: repo.get(&admin_model, key).await,
item_model: Some(admin_model),
..Default::default()
}
} else {
AdminContext {
base: base_template(&headers),
available_apps: registry.get_apps(),
..Default::default()
}
};
templates.render_html("admin/items/item_change.jinja", context)
}
// Item Action allows running an action on one single dataset.
pub async fn item_action(
Path((app_key, model_key, model_id)): Path<(String, String, String)>,

View File

@@ -1,8 +1,6 @@
use crate::admin::domain::*;
use crate::admin::state::AdminRegistry;
use async_trait::async_trait;
use log::{debug, warn};
use serde_json::{json, Value};
use serde_json::Value;
struct Repository {}

View File

@@ -44,8 +44,8 @@ impl AdminRepository for MyStaticRepository {
fields: &["name", "level", "age", "powers"],
}
.build()
.set_widget("age", Widget::default().as_password().labeled("hi there."))
.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
//.set_widget("age", Widget::default().as_password().labeled("hi there."))
//.set_widget("name", Widget::textarea().options(&[("disabled", "true")]))
}
async fn get(&self, model: &RepositoryContext, id: LookupKey) -> Option<RepositoryItem> {
@@ -117,10 +117,7 @@ impl AdminRepository for MyStaticRepository {
*item = data.clone();
if let Some(item_id) = item.get("id") {
return Some(RepositoryItem {
detail_url: Some(format!("{}/detail/{}", model.admin_url, item_id)),
fields: data,
});
return Some(model.build_item(&*item_id.to_string(), data));
}
}

View File

@@ -7,6 +7,12 @@ mod state;
use crate::service::{handlers, templates};
use crate::state::AppState;
use admin_examples::static_repository;
use axum::{
body::Bytes,
extract::MatchedPath,
http::{HeaderMap, Request},
response::Response,
};
use axum::{
extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router,
};
@@ -15,7 +21,10 @@ use log::info;
use std::env;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpListener;
use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
use tracing::{info_span, Span};
async fn home(templates: State<templates::Templates>) -> impl IntoResponse {
templates.render_html("index.html", ())
@@ -51,7 +60,48 @@ async fn main() {
.nest("/howto", howto::routes())
.route_service("/static/*file", handlers::static_handler.into_service())
.fallback(handlers::not_found_handler)
.with_state(state);
.with_state(state)
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {
// Log the matched route's path (with placeholders not filled in).
// Use request.uri() or OriginalUri if you want the real path.
let matched_path = request
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str);
info_span!(
"http_request",
method = ?request.method(),
uri = ?request.uri(),
matched_path,
some_other_field = tracing::field::Empty,
)
})
.on_request(|_request: &Request<_>, _span: &Span| {
// You can use `_span.record("some_other_field", value)` in one of these
// closures to attach a value to the initially empty field in the info_span
// created above.
info!("Request: {:?}", _request);
})
.on_response(|_response: &Response, _latency: Duration, _span: &Span| {
// ...
info!("Response: {:?}", _response);
})
.on_body_chunk(|_chunk: &Bytes, _latency: Duration, _span: &Span| {
// ...
})
.on_eos(
|_trailers: Option<&HeaderMap>, _stream_duration: Duration, _span: &Span| {
// ...
},
)
.on_failure(
|_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
// ...
},
),
);
// Run Server
let app_host: std::net::IpAddr = env::var("APP_HOST")