diff --git a/Cargo.lock b/Cargo.lock
index 296b15e..531ef91 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1847,6 +1847,8 @@ dependencies = [
"sqlformat",
"strinto",
"tokio",
+ "tower-http",
+ "tracing",
]
[[package]]
@@ -3477,6 +3479,23 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "tower-http"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e"
+dependencies = [
+ "bitflags 2.4.0",
+ "bytes",
+ "http 1.0.0",
+ "http-body 1.0.0",
+ "http-body-util",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
[[package]]
name = "tower-layer"
version = "0.3.2"
@@ -3491,11 +3510,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
-version = "0.1.37"
+version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
- "cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
@@ -3515,9 +3533,9 @@ dependencies = [
[[package]]
name = "tracing-core"
-version = "0.1.31"
+version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
]
diff --git a/Cargo.toml b/Cargo.toml
index 45ed198..22cca55 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,3 +52,5 @@ serde_json = "1.0.108"
slug = "0.1.5"
derive_builder = "0.13.0"
async-trait = "0.1.77"
+tracing = "0.1.40"
+tower-http = { version = "0.5.1", features = ["trace"] }
diff --git a/NOTES.md b/NOTES.md
index 8d21adf..2644699 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -156,6 +156,7 @@ As I started by quickly using a django admin template with CSS to jump start the
### UiKit
in general really nice, but I got annoyed by uk- prefixes pretty fast.
+ Psychologically, "ui grid" seems nicer than "uk-grid", weird.
It would however seem to align with hx- prefixes of htmx?
### Tailwind/DaisyUI
@@ -175,11 +176,13 @@ As I started by quickly using a django admin template with CSS to jump start the
I think it is important to go into form validation or interactive things quickly with htmx to properly sort out which to use.
+ Why fomantic won was the lack of needing a btn class.
+
### Builder Pattern etc.
- // consider builder: https://docs.rs/derive_builder/latest/derive_builder/
- // downside of builders as macro: no intellisense!
- // each repository has to implement a repo info.
+consider builder: https://docs.rs/derive_builder/latest/derive_builder/
+downside of builders as macro: no intellisense!
+
//#[derive(Builder)]
//#[builder(setter(into))]
@@ -235,4 +238,28 @@ let dynamic_field = Field::widget(Cow::Owned(dynamic_label));
- "object safe"
- the async dynamic dispatch.
- the building pattern debacle here
-
\ No newline at end of file
+
+
+### Async Trait Adventure
+
+What was not noted here was the few days gone by researching async Trait dilemmas.
+Given I use dynamic dispatch, the only solution is async_trait crate for now
+However I am aware that this is an ongoing topic in the rust world.
+
+For the admin this works well, and in general, it makes sense in the axum world I guess.
+
+Other solutions if this stops being the case:
+ - Channels like in Go with crossbeam: https://gsquire.github.io/static/post/a-rusty-go-at-channels/
+ - Watch Rust Development here: https://rust-lang.github.io/async-fundamentals-initiative/index.html
+
+### For the occasional HTMX Reload on Error bug
+
+The header being in the history, returns a partial that cannot insert itself into the browser's error page.
+
+Maybe a hint for a cache control problem actually?
+ https://github.com/bigskysoftware/htmx/issues/854
+
+Make axum log requests!
+
+There is another crate, that uses some MIME detection middleware for cache-control, called axum-cc, which could inspire middlewares here.
+
diff --git a/TODOS.md b/TODOS.md
index 8cee66c..4365c7a 100644
--- a/TODOS.md
+++ b/TODOS.md
@@ -39,3 +39,18 @@ auth:
In the widget section, we need to add more abilities to adjust attributes of html elements directly.
Probably options should be transmitted via JSON data.
+
+## Form Foomatics
+
+ - multi-item form fields (as in multiple inputs for one field, or multiple fields inside one field)
+ ```html
+
+
+
+
+ ...
+
+
+
+```
+ -
diff --git a/src/admin/domain.rs b/src/admin/domain.rs
index f9134ca..464bc71 100644
--- a/src/admin/domain.rs
+++ b/src/admin/domain.rs
@@ -60,9 +60,18 @@ pub mod repository {
pub type RepositoryContext = AdminModel;
impl RepositoryContext {
+ pub fn get_default_detail_url(&self, key: &str) -> Option {
+ Some(format!("{}/detail/{}", self.admin_url, key))
+ }
+
+ pub fn get_default_change_url(&self, key: &str) -> Option {
+ 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,
+ pub change_url: Option,
}
pub enum RepositoryList {
diff --git a/src/admin/mod.rs b/src/admin/mod.rs
index 4048ccc..4e36cb4 100644
--- a/src/admin/mod.rs
+++ b/src/admin/mod.rs
@@ -15,8 +15,9 @@ pub fn routes() -> Router {
"/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),
)
}
diff --git a/src/admin/views.rs b/src/admin/views.rs
index 6b8b6bc..e9e9a94 100644
--- a/src/admin/views.rs
+++ b/src/admin/views.rs
@@ -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,
registry: State>,
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,
+ registry: State>,
+ 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)>,
diff --git a/src/admin_examples/empty_repository.rs b/src/admin_examples/empty_repository.rs
index 0aeb9be..02952cb 100644
--- a/src/admin_examples/empty_repository.rs
+++ b/src/admin_examples/empty_repository.rs
@@ -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 {}
diff --git a/src/admin_examples/static_repository.rs b/src/admin_examples/static_repository.rs
index 9613d7c..678437c 100644
--- a/src/admin_examples/static_repository.rs
+++ b/src/admin_examples/static_repository.rs
@@ -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 {
@@ -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));
}
}
diff --git a/src/main.rs b/src/main.rs
index c8044b2..7b01dac 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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) -> 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::()
+ .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")
diff --git a/templates/admin/base.jinja b/templates/admin/base.jinja
index 38dc478..e631b66 100644
--- a/templates/admin/base.jinja
+++ b/templates/admin/base.jinja
@@ -113,8 +113,9 @@
{{ model.name }}
{% endif %}
- {% if model.add_url %}
- {{ translate( 'Add') }}
+ {% if model.add_url %}
+ {% if captions %}{{ translate( 'Add') }}{% endif %}
{% endif %}
{% endfor %}
diff --git a/templates/admin/items/item_change.jinja b/templates/admin/items/item_change.jinja
new file mode 100644
index 0000000..d48f730
--- /dev/null
+++ b/templates/admin/items/item_change.jinja
@@ -0,0 +1,10 @@
+{% extends base|none("admin/base.jinja") %}
+
+{% block content %}
+
+
Update {{item_model.name}} in {{item_info.name}}
+ {% set fields = item_info.fields %}
+ {% set form = { 'action': item.change_url } %}
+ {% include "/admin/items/item_change_form.jinja" %}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/templates/admin/items/item_change_form.jinja b/templates/admin/items/item_change_form.jinja
new file mode 100644
index 0000000..67797df
--- /dev/null
+++ b/templates/admin/items/item_change_form.jinja
@@ -0,0 +1,16 @@
+
\ No newline at end of file
diff --git a/templates/admin/items/item_create.jinja b/templates/admin/items/item_create.jinja
index af1a4af..0e10d1e 100644
--- a/templates/admin/items/item_create.jinja
+++ b/templates/admin/items/item_create.jinja
@@ -1,22 +1,17 @@
{% extends base|none("admin/base.jinja") %}
{% block content %}
-{% from "/admin/items/items.jinja" import field_widget %}
Create {{item_model.name}} in {{item_info.name}}
-
+ {% block navigation_bar %}
+
+
+
+ {% endblock %}
+
+ {% set fields = item_info.fields %}
+ {% set form = { 'action': item_model.add_url } %}
+ {% include "/admin/items/item_change_form.jinja" %}
{% endblock content %}
\ No newline at end of file
diff --git a/templates/admin/items/item_list.jinja b/templates/admin/items/item_list.jinja
index c20983d..b7ccb20 100644
--- a/templates/admin/items/item_list.jinja
+++ b/templates/admin/items/item_list.jinja
@@ -1,57 +1,84 @@
{% extends base|none("admin/base.jinja") %}
{% block content %}
+
+
List of {{ item_info.name }}
-{% block search_bar %}
-
-
-
-
-{% endblock %}
+ {% block navigation_bar %}
+
+
+
+ {% endblock %}
-{% if item_list %}
-{% set item_keys = item_info.display_list or item_list|table_keys %}
+ {% block search_bar %}
+
+
+
+
+ {% endblock %}
-{% if item_info.lookup_key in item_keys %}
-{% set primary_key = item_info.lookup_key %}
-{% endif %}
+ {% if item_list %}
+ {% set item_keys = item_info.display_list or item_list|table_keys %}
-
-
-
{{ item_info.name|none(content) }}
-
-
- {% for key in item_keys %}
- {% if key==primary_key %}
-
- {{ key|capitalize }}
-
- {% else %}
-
{{ key|capitalize }}
- {% endif %}
+ {% if item_info.lookup_key in item_keys %}
+ {% set primary_key = item_info.lookup_key %}
+ {% endif %}
- {% endfor %}
-
-
+
+
+
{{ item_info.name }}
+
+
+
+ {% for key in item_keys %}
+ {% if key==primary_key %}
+
+ {{ key|capitalize }}
+
+ {% else %}
+
{{ key|capitalize }}
+ {% endif %}
-
- {% for item in item_list %}
-
- {% for key in item_keys %}
- {% if key==primary_key %}
-
\ No newline at end of file
diff --git a/templates/admin/widgets/input_textarea.jinja b/templates/admin/widgets/input_textarea.jinja
index 334e99a..0092dde 100644
--- a/templates/admin/widgets/input_textarea.jinja
+++ b/templates/admin/widgets/input_textarea.jinja
@@ -1,4 +1,4 @@
-