refactor: using word depot in rear instead of admin

This commit is contained in:
Gabor Körber 2024-07-20 09:34:51 +02:00
parent a26e17a064
commit 966291dbd9
38 changed files with 1112 additions and 72 deletions

78
rear/src/depot/context.rs Normal file
View File

@ -0,0 +1,78 @@
use serde::{Deserialize, Serialize};
use super::repository::{RepositoryInfo, RepositoryItem, RepositoryList};
// representation of a Model in the template.
#[derive(Deserialize, Serialize)]
pub struct DepotModel {
pub key: String,
pub name: String,
pub model_url: String,
pub view_only: bool,
pub add_url: Option<String>,
}
// representation of a Section in the template.
#[derive(Deserialize, Serialize)]
pub struct DepotSection {
pub key: String,
pub name: String,
pub section_url: String,
pub models: Vec<DepotModel>,
}
#[derive(Serialize, Deserialize)]
pub struct DepotRequest {
pub path: String,
}
#[derive(Serialize)]
pub struct DepotContext {
pub base: Option<String>,
pub language_code: Option<String>,
pub language_bidi: Option<bool>,
pub user: Option<String>, // Todo: user type
pub depot_url: String,
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: DepotRequest,
pub sections: Vec<DepotSection>,
pub item_model: Option<DepotModel>,
pub item_info: Option<RepositoryInfo>,
pub item_list: RepositoryList,
pub item: Option<RepositoryItem>,
}
impl Default for DepotContext {
fn default() -> Self {
DepotContext {
base: None, // TODO: what is this used for?
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
depot_url: "/depot".to_owned(),
site_url: None,
docsroot: None,
messages: Vec::new(), // Empty vector for messages
title: None,
subtitle: None,
content: String::new(), // Empty string for content
sections: Vec::new(),
request: DepotRequest {
path: "".to_owned(),
},
item_model: None,
item_info: None,
item_list: RepositoryList::Empty,
item: None,
}
}
}

31
rear/src/depot/mod.rs Normal file
View File

@ -0,0 +1,31 @@
use axum::{routing::get, Router};
mod context;
pub mod prelude;
mod registry;
pub mod repository;
pub mod state;
pub mod views;
pub mod widgets;
pub fn routes<S: state::DepotState + Clone + Send + Sync + 'static>() -> Router<S> {
Router::new()
.route("/", get(views::index::<S>).post(views::index_action::<S>))
.route("/app/:app", get(views::list_app::<S>))
.route(
"/app/:app/model/:model",
get(views::list_item_collection::<S>),
)
.route(
"/app/:app/model/:model/add",
get(views::new_item::<S>).post(views::create_item::<S>),
)
.route(
"/app/:app/model/:model/change/:id",
get(views::change_item::<S>).patch(views::update_item::<S>),
)
.route(
"/app/:app/model/:model/detail/:id",
get(views::view_item_details::<S>),
)
}

View File

@ -0,0 +1,7 @@
pub use super::context::{DepotContext, DepotModel};
pub use super::registry::DepotRegistry;
pub use super::repository::{
DepotModelConfig, DepotRepository, RepoInfo, RepositoryContext, RepositoryInfo, RepositoryItem,
RepositoryList,
};
pub use super::widgets::Widget;

164
rear/src/depot/registry.rs Normal file
View File

@ -0,0 +1,164 @@
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use super::{
context::{DepotModel, DepotSection},
repository::{DepotModelConfig, DepotRepository, DepotRepositoryWrapper, DynDepotRepository},
};
pub struct DepotRegistry {
base_path: String,
sections: HashMap<String, internal::DepotSectionInfo>,
models: HashMap<String, internal::DepotModelInfo>,
repositories: HashMap<String, Arc<Mutex<dyn DynDepotRepository>>>,
}
impl DepotRegistry {
pub fn new(base_path: &str) -> Self {
DepotRegistry {
base_path: base_path.to_owned(),
sections: HashMap::new(),
models: HashMap::new(),
repositories: HashMap::new(),
}
}
pub fn get_sections(&self) -> Vec<DepotSection> {
self.sections
.iter()
.map(|(key, section_info)| self.get_section(key, section_info))
.collect()
}
fn get_section(&self, key: &str, section_info: &internal::DepotSectionInfo) -> DepotSection {
let my_models = self.get_models(key);
DepotSection {
key: key.to_owned(),
name: section_info.name.to_owned(),
section_url: format!("/{}/section/{}", self.base_path, key.to_owned()),
models: my_models,
}
}
pub fn register_section(&mut self, name: &str) -> String {
let key = self.get_key(name);
self.sections.insert(
key.to_owned(),
internal::DepotSectionInfo {
key: key.to_owned(),
name: name.to_owned(),
},
);
key
}
fn get_key(&self, name: &str) -> String {
slug::slugify(name)
}
fn model_from_model_info(&self, model_info: &internal::DepotModelInfo) -> DepotModel {
let model_url = format!(
"/{}/section/{}/model/{}",
self.base_path, model_info.section_key, model_info.model_key
);
DepotModel {
key: model_info.model_key.clone(),
name: model_info.name.clone(),
view_only: false,
add_url: Some(format!("{}/add", model_url)),
model_url: model_url,
}
}
pub fn get_models(&self, section_key: &str) -> Vec<DepotModel> {
self.models
.iter()
.filter(|(key, _)| key.starts_with(&format!("{}.", section_key)))
.map(|(_, model)| self.model_from_model_info(model))
.collect()
}
pub fn get_model(&self, section_key: &str, model_key: &str) -> Option<DepotModel> {
let full_model_key = format!("{}.{}", section_key, model_key);
let internal_model = self.models.get(&full_model_key)?;
Some(self.model_from_model_info(internal_model))
}
fn register_model_config(&mut self, model: DepotModelConfig) -> Result<String, String> {
let local_config = internal::DepotModelInfo::from(model);
if local_config.model_key.is_empty() {
return Err("No model name".to_owned());
}
let local_config_name = format!("{}.{}", local_config.section_key, local_config.model_key);
if self.models.contains_key(&local_config_name) {
return Err(format!("Model {} already exists", local_config_name));
}
let full_model_key = local_config_name.clone();
self.models.insert(local_config_name, local_config);
Ok(full_model_key)
}
pub fn register_model<R: DepotRepository + 'static>(
&mut self,
model: DepotModelConfig,
repository: R,
) -> Result<(), String> {
let model_key = self.register_model_config(model)?;
let repository = DepotRepositoryWrapper::new(repository);
self.repositories
.insert(model_key, Arc::new(Mutex::new(repository)));
Ok(())
}
pub fn get_repository(
&self,
section_key: &str,
model_key: &str,
) -> Result<Arc<Mutex<dyn DynDepotRepository>>, String> {
let full_model_key = format!("{}.{}", section_key, model_key);
if let Some(repo) = self.repositories.get(&full_model_key) {
// Clone the Arc to return a reference to the repository
return Ok(Arc::clone(repo));
} else {
return Err("Couldn't find repository".to_owned());
}
}
}
mod internal {
// how the registry saves data internally.
use super::super::repository::DepotModelConfig;
#[derive(Clone)]
pub(super) struct DepotSectionInfo {
pub key: String,
pub name: String,
}
#[derive(Clone)]
pub(super) struct DepotModelInfo {
pub section_key: String,
pub model_key: String,
pub name: String,
}
impl From<DepotModelConfig> for DepotModelInfo {
fn from(value: DepotModelConfig) -> Self {
DepotModelInfo {
section_key: value.section_key,
model_key: slug::slugify(value.name.clone()),
name: value.name,
}
}
}
impl From<(&str, &str)> for DepotModelInfo {
fn from(value: (&str, &str)) -> Self {
DepotModelInfo {
section_key: value.0.to_owned(),
model_key: slug::slugify(value.1.to_owned()),
name: value.1.to_owned(),
}
}
}
}

View File

@ -0,0 +1,361 @@
use async_trait::async_trait;
use serde::{Serialize, Serializer};
use serde_json::Value;
use std::any::Any;
use std::fmt::Debug;
use std::vec::IntoIter;
use super::context::DepotModel;
use super::widgets::Widget;
// user uses this configuration object to register another model.
pub struct DepotModelConfig {
pub name: String,
pub section_key: String,
}
impl From<(&str, &str)> for DepotModelConfig {
fn from(value: (&str, &str)) -> Self {
DepotModelConfig {
section_key: value.0.to_owned(),
name: value.1.to_owned(),
}
}
}
// May become it's own structure.
pub type RepositoryContext = DepotModel;
impl RepositoryContext {
pub fn get_default_detail_url(&self, key: &str) -> Option<String> {
Some(format!("{}/detail/{}", self.model_url, key))
}
pub fn get_default_change_url(&self, key: &str) -> Option<String> {
Some(format!("{}/change/{}", self.model_url, key))
}
pub fn build_item(&self, key: &str, fields: Value) -> RepositoryItem {
RepositoryItem {
detail_url: self.get_default_detail_url(key),
change_url: self.get_default_change_url(key),
fields: fields,
}
}
}
#[derive(Debug, Serialize)]
pub struct Field {
widget: String,
label: Option<String>,
#[serde(rename = "type")]
field_type: String,
readonly: bool,
required: bool,
options: Value,
}
impl From<Widget> for Field {
fn from(value: Widget) -> Self {
Field {
widget: value.widget.to_string(),
label: value.label.map(|s| s.to_string()),
field_type: value.field_type.to_string(),
readonly: value.readonly,
required: value.required,
options: value
.options
.iter()
.map(|(key, val)| (key.to_string(), serde_json::json!(val)))
.collect::<serde_json::Map<String, Value>>()
.into(),
}
}
}
#[derive(Serialize)]
pub struct RepositoryItem {
pub fields: Value,
pub detail_url: Option<String>,
pub change_url: Option<String>,
}
pub enum RepositoryList {
Empty,
List {
values: Vec<RepositoryItem>,
},
Page {
values: Vec<RepositoryItem>,
offset: usize,
total: usize,
},
Stream {
values: Vec<RepositoryItem>,
next_index: Option<String>,
},
}
impl IntoIterator for RepositoryList {
type Item = RepositoryItem;
type IntoIter = IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
match self {
RepositoryList::Empty => vec![].into_iter(),
RepositoryList::List { values } => values.into_iter(),
RepositoryList::Page { values, .. } => values.into_iter(),
RepositoryList::Stream { values, .. } => values.into_iter(),
}
}
}
impl Serialize for RepositoryList {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
RepositoryList::Empty => serializer.serialize_unit(),
RepositoryList::List { values }
| RepositoryList::Page { values, .. }
| RepositoryList::Stream { values, .. } => values.serialize(serializer),
}
}
}
/// Static initializer for RepositoryInfo.
pub struct RepoInfo {
pub name: &'static str,
pub lookup_key: &'static str,
pub display_list: &'static [&'static str],
pub fields: &'static [&'static str],
}
impl RepoInfo {
pub fn build(self) -> RepositoryInfo {
self.into()
}
}
#[derive(Serialize)]
pub struct RepositoryInfo {
name: String,
lookup_key: String,
display_list: Vec<String>,
fields: Vec<(String, Field)>,
}
impl RepositoryInfo {
pub fn new(name: &str, lookup_key: &str) -> Self {
RepositoryInfo {
name: name.to_owned(),
lookup_key: lookup_key.to_owned(),
display_list: vec![],
fields: vec![],
}
}
// self mutating builder pattern
pub fn display_list(mut self, display_list: &[&str]) -> Self {
self.display_list = display_list.iter().map(|&e| e.to_string()).collect();
self
}
pub fn set_widget<T: Into<Field>>(mut self, name: &str, item: T) -> Self {
let field = item.into(); // Convert the input into a Field
// Find the index of the existing entry with the same name, if it exists
let pos = self
.fields
.iter()
.position(|(existing_name, _)| existing_name == name);
match pos {
Some(index) => {
self.fields[index].1 = field;
}
None => {
self.fields.push((name.to_owned(), field));
}
}
self
}
}
impl From<RepoInfo> for RepositoryInfo {
fn from(repo_info: RepoInfo) -> Self {
RepositoryInfo {
name: repo_info.name.to_string(),
lookup_key: repo_info.lookup_key.to_string(),
display_list: repo_info
.display_list
.iter()
.map(|&s| s.to_string())
.collect(),
fields: repo_info
.fields
.iter()
.map(|x| (x.to_string(), Field::from(Widget::default())))
.collect(),
}
}
}
pub trait PrimaryKeyType: Any + Debug + Send + Sync {
fn as_any(&self) -> &dyn Any;
}
impl PrimaryKeyType for i64 {
fn as_any(&self) -> &dyn Any {
self
}
}
impl PrimaryKeyType for String {
fn as_any(&self) -> &dyn Any {
self
}
}
#[async_trait]
pub trait DepotRepository: Send + Sync {
type Key: PrimaryKeyType;
fn key_from_string(&self, s: String) -> Option<Self::Key>;
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
async fn get(&self, context: &RepositoryContext, id: &Self::Key) -> Option<RepositoryItem>;
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem>;
async fn update(
&mut self,
context: &RepositoryContext,
id: &Self::Key,
data: Value,
) -> Option<RepositoryItem>;
async fn replace(
&mut self,
context: &RepositoryContext,
id: &Self::Key,
data: Value,
) -> Option<RepositoryItem>;
async fn delete(&mut self, context: &RepositoryContext, id: &Self::Key) -> Option<Value>;
}
#[async_trait]
pub(crate) trait DynDepotRepository: Send + Sync {
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>>;
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo;
async fn list(&self, context: &RepositoryContext) -> RepositoryList;
async fn get(
&self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<RepositoryItem>;
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem>;
async fn update(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem>;
async fn replace(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem>;
async fn delete(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<Value>;
}
pub struct DepotRepositoryWrapper<T: DepotRepository> {
inner: T,
}
impl<T: DepotRepository> DepotRepositoryWrapper<T> {
pub fn new(inner: T) -> Self {
Self { inner }
}
fn key_from_string(&self, s: String) -> Option<<T as DepotRepository>::Key> {
self.inner.key_from_string(s)
}
}
#[async_trait]
impl<T: DepotRepository> DynDepotRepository for DepotRepositoryWrapper<T> {
fn key_from_string(&self, s: String) -> Option<Box<dyn PrimaryKeyType>> {
if let Some(key) = self.inner.key_from_string(s) {
Some(Box::new(key))
} else {
None
}
}
async fn info(&self, context: &RepositoryContext) -> RepositoryInfo {
self.inner.info(context).await
}
async fn list(&self, context: &RepositoryContext) -> RepositoryList {
self.inner.list(context).await
}
async fn get(
&self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.get(context, key).await
} else {
None
}
}
async fn create(&mut self, context: &RepositoryContext, data: Value) -> Option<RepositoryItem> {
self.inner.create(context, data).await
}
async fn update(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.update(context, key, data).await
} else {
None
}
}
async fn replace(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
data: Value,
) -> Option<RepositoryItem> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.replace(context, key, data).await
} else {
None
}
}
async fn delete(
&mut self,
context: &RepositoryContext,
id: &dyn PrimaryKeyType,
) -> Option<Value> {
if let Some(key) = id.as_any().downcast_ref::<T::Key>() {
self.inner.delete(context, key).await
} else {
None
}
}
}

11
rear/src/depot/state.rs Normal file
View File

@ -0,0 +1,11 @@
use crate::service::templates::Templates;
use std::sync::Arc;
use super::registry::DepotRegistry;
pub trait DepotState {
fn get_templates(&self) -> &Templates;
fn get_registry(&self) -> SharedDepotRegistry;
}
pub type SharedDepotRegistry = Arc<DepotRegistry>;

294
rear/src/depot/views.rs Normal file
View File

@ -0,0 +1,294 @@
use axum::extract::Path;
use axum::http::HeaderMap;
use axum::Form;
use axum::{extract::State, response::IntoResponse};
use log::info;
use serde_json::Value;
use super::context::DepotContext;
use super::state::DepotState;
pub fn base_template(headers: &HeaderMap) -> Option<String> {
let hx_request = headers.get("HX-Request").is_some();
if hx_request {
Some("depot/base_hx.jinja".to_string())
} else {
None
}
}
pub async fn index<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
) -> impl IntoResponse {
let templates = depot.get_templates();
let registry = depot.get_registry();
templates.render_html(
"depot/index.html",
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
},
)
}
// Index Action is POST to the index site. We can anchor some general business code here.
pub async fn index_action<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
) -> impl IntoResponse {
"There is your answer!".to_owned()
}
pub async fn list_app<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
Path(depot_key): Path<String>,
) -> impl IntoResponse {
let templates = depot.get_templates();
templates.render_html("depot/depot.jinja", ())
}
// List Items renders the entire list item page.
pub async fn list_item_collection<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
Path((depot_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse {
info!("list_item_collection {} for model {}", depot_key, model_key);
let templates = depot.get_templates();
let registry = depot.get_registry();
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
let repo = repo.lock().await;
let depot_model = registry
.get_model(&depot_key, &model_key)
.expect("depot Model not found?"); // we will need a proper error route; so something that implements IntoResponse and can be substituted in the unwraps and expects.
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await,
item_model: Some(depot_model),
..Default::default()
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
};
templates.render_html("depot/items/item_list.jinja", context)
}
// 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<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
Path((depot_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse {
"There is your answer!".to_owned()
}
// Item Details shows one single dataset.
pub async fn view_item_details<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
Path((depot_key, model_key, id)): Path<(String, String, String)>,
) -> impl IntoResponse {
let templates = depot.get_templates();
let registry = depot.get_registry();
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
let repo = repo.lock().await;
let depot_model = registry
.get_model(&depot_key, &model_key)
.expect("depot Model not found?");
if let Some(key) = repo.key_from_string(id) {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await,
item: repo.get(&depot_model, key.as_ref()).await,
item_model: Some(depot_model),
..Default::default()
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
};
templates.render_html("depot/items/item_detail.jinja", context)
}
pub async fn new_item<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
Path((depot_key, model_key)): Path<(String, String)>,
) -> impl IntoResponse {
let templates = depot.get_templates();
let registry = depot.get_registry();
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
let repo = repo.lock().await;
let depot_model = registry
.get_model(&depot_key, &model_key)
.expect("depot Model not found?");
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await,
item_model: Some(depot_model),
..Default::default()
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
};
templates.render_html("depot/items/item_create.jinja", context)
}
pub async fn create_item<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
Path((depot_key, model_key)): Path<(String, String)>,
Form(form): Form<Value>,
) -> impl IntoResponse {
let templates = depot.get_templates();
let registry = depot.get_registry();
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
let mut repo = repo.lock().await;
let depot_model = registry
.get_model(&depot_key, &model_key)
.expect("Depot Model not found?");
// create our item.
let result = repo.create(&depot_model, form).await;
// TODO: refactor run over these views, way too much repetition.
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await,
item_model: Some(depot_model),
item: result,
..Default::default()
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
};
templates.render_html("depot/items/item_create.jinja", context)
}
/// Change is the GET version.
pub async fn change_item<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
Path((depot_key, model_key, id)): Path<(String, String, String)>,
) -> impl IntoResponse {
let templates = depot.get_templates();
let registry = depot.get_registry();
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
let repo = repo.lock().await;
let depot_model = registry
.get_model(&depot_key, &model_key)
.expect("depot Model not found?");
if let Some(key) = repo.key_from_string(id) {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await,
item: repo.get(&depot_model, key.as_ref()).await,
item_model: Some(depot_model),
..Default::default()
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
};
templates.render_html("depot/items/item_change.jinja", context)
}
pub async fn update_item<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
headers: HeaderMap,
Path((depot_key, model_key, id)): Path<(String, String, String)>,
Form(form): Form<Value>,
) -> impl IntoResponse {
let templates = depot.get_templates();
let registry = depot.get_registry();
let context = if let Ok(repo) = registry.get_repository(&depot_key, &model_key) {
let mut repo = repo.lock().await;
let depot_model = registry
.get_model(&depot_key, &model_key)
.expect("depot Model not found?");
if let Some(key) = repo.key_from_string(id) {
let result = repo.update(&depot_model, key.as_ref(), form).await;
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
item_info: Some(repo.info(&depot_model).await),
item_list: repo.list(&depot_model).await,
item: result,
item_model: Some(depot_model),
..Default::default()
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
}
} else {
DepotContext {
base: base_template(&headers),
sections: registry.get_sections(),
..Default::default()
}
};
let response = templates.render_html("depot/items/item_change.jinja", context);
response
}
// Item Action allows running an action on one single dataset.
pub async fn item_action<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
Path((depot_key, model_key, model_id)): Path<(String, String, String)>,
) -> impl IntoResponse {
"There is your answer!".to_owned()
}
pub async fn debug_view<S: DepotState + Clone + Send + Sync + 'static>(
depot: State<S>,
Path(data): Path<String>,
) -> impl IntoResponse {
println!("debug: {}", data);
"Debug!".to_owned()
}

79
rear/src/depot/widgets.rs Normal file
View File

@ -0,0 +1,79 @@
use serde::Serialize;
/// This is a static configuration object.
/// It might be changed in the future to have a dynamic counterpart.
///
/// ## Example:
/// Creating a simple required and readonly text input field with a label:
///
/// ```ignore
/// let my_field = Field::widget("/admin/widgets/input_text.jinja")
/// .labelled("Username")
/// .required()
/// .readonly();
/// ```
#[derive(Debug, Serialize, Clone, Copy)]
pub struct Widget {
pub widget: &'static str,
pub label: Option<&'static str>,
#[serde(rename = "type")]
pub field_type: &'static str,
pub required: bool,
pub readonly: bool,
pub options: &'static [(&'static str, &'static str)],
}
impl Widget {
pub fn widget(widget: &'static str) -> Self {
Widget {
widget: widget,
label: None,
field_type: "text",
required: false,
readonly: false,
options: &[],
}
}
pub fn default() -> Self {
Self::widget("/admin/widgets/input_text.jinja")
}
pub fn textarea() -> Self {
Self::widget("/admin/widgets/input_textarea.jinja")
}
pub fn checkbox() -> Self {
Self::widget("/admin/widgets/checkbox_toggle.jinja")
}
pub fn required(mut self) -> Self {
self.required = true;
self
}
pub fn readonly(mut self) -> Self {
self.readonly = true;
self
}
pub fn labeled(mut self, label: &'static str) -> Self {
self.label = Some(label);
self
}
pub fn as_password(mut self) -> Self {
self.field_type = "password";
self
}
pub fn as_hidden(mut self) -> Self {
self.field_type = "hidden";
self
}
pub fn options(mut self, options: &'static [(&'static str, &'static str)]) -> Self {
self.options = options;
self
}
}

View File

@ -1,2 +1,2 @@
pub mod admin; pub mod depot;
pub mod service; pub mod service;

View File

@ -3,7 +3,23 @@ use axum::response::Html;
use minijinja::{path_loader, Environment, Value}; use minijinja::{path_loader, Environment, Value};
use minijinja_autoreload::AutoReloader; use minijinja_autoreload::AutoReloader;
use pulldown_cmark::Event; use pulldown_cmark::Event;
use std::sync::Arc; use std::{path::Path, sync::Arc};
pub fn composite_loader<P: AsRef<Path>>(
dirs: Vec<P>,
) -> impl Fn(&str) -> Result<Option<String>, minijinja::Error> + Send + Sync + 'static {
let loaders: Vec<_> = dirs.into_iter().map(|dir| path_loader(dir)).collect();
move |name| {
for loader in &loaders {
match loader(name) {
Ok(Some(template)) => return Ok(Some(template)),
Ok(None) => continue,
Err(err) => return Err(err.into()),
}
}
Ok(None)
}
}
#[derive(Clone)] #[derive(Clone)]
pub struct Templates { pub struct Templates {
@ -15,7 +31,8 @@ impl Templates {
let reloader = AutoReloader::new(move |notifier| { let reloader = AutoReloader::new(move |notifier| {
let mut environment = Environment::new(); let mut environment = Environment::new();
let template_path = "templates"; let template_path = "templates";
environment.set_loader(path_loader(&template_path)); let loader = composite_loader(vec![template_path]);
environment.set_loader(loader);
environment.add_filter("none", none); environment.add_filter("none", none);
environment.add_filter("markdown", markdown); environment.add_filter("markdown", markdown);
environment.add_filter("yesno", filter_yesno); environment.add_filter("yesno", filter_yesno);

View File

@ -1,14 +1,13 @@
use async_trait::async_trait; use async_trait::async_trait;
use log::debug; use log::debug;
use rear::admin::domain::*; use rear::depot::prelude::*;
use rear::admin::state::AdminRegistry;
use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set}; use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait, ModelTrait, Set};
use serde_json::Value; use serde_json::Value;
use crate::models::UserRepository; use crate::models::UserRepository;
#[async_trait] #[async_trait]
impl AdminRepository for UserRepository { impl DepotRepository for UserRepository {
type Key = i64; type Key = i64;
fn key_from_string(&self, s: String) -> Option<Self::Key> { fn key_from_string(&self, s: String) -> Option<Self::Key> {
@ -202,11 +201,11 @@ impl AdminRepository for UserRepository {
} }
} }
pub fn register(registry: &mut AdminRegistry, db: DatabaseConnection) -> UserRepository { pub fn register(registry: &mut DepotRegistry, db: DatabaseConnection) -> UserRepository {
let app_key = registry.register_app("Auth"); let section_key = registry.register_section("Auth");
let repo = UserRepository::new(db); let repo = UserRepository::new(db);
let model_config = AdminModelConfig { let model_config = DepotModelConfig {
app_key: app_key, section_key: section_key,
name: "User".to_owned(), name: "User".to_owned(),
}; };
let model_result = registry.register_model(model_config, repo.clone()); let model_result = registry.register_model(model_config, repo.clone());

View File

@ -23,7 +23,7 @@ pub struct NextUrl {
next: Option<String>, next: Option<String>,
} }
pub fn routes<S: rear::admin::state::AdminState + Clone + Send + Sync + 'static>() -> Router<S> pub fn routes<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>() -> Router<S>
where { where {
Router::new() Router::new()
.route("/login", post(self::post::login)) .route("/login", post(self::post::login))
@ -44,7 +44,7 @@ mod post {
Ok(None) => { Ok(None) => {
messages.error("Invalid credentials"); messages.error("Invalid credentials");
let mut login_url = "/admin/login".to_string(); let mut login_url = "/depot/login".to_string();
if let Some(next) = creds.next { if let Some(next) = creds.next {
login_url = format!("{}?next={}", login_url, next); login_url = format!("{}?next={}", login_url, next);
}; };
@ -70,7 +70,7 @@ mod post {
pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse { pub async fn logout(mut auth_session: AuthSession) -> impl IntoResponse {
match auth_session.logout().await { match auth_session.logout().await {
Ok(_) => Redirect::to("/admin/login").into_response(), Ok(_) => Redirect::to("/depot/login").into_response(),
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
} }
} }
@ -80,7 +80,7 @@ mod get {
use super::*; use super::*;
use axum::extract::State; use axum::extract::State;
pub async fn login<S: rear::admin::state::AdminState + Clone + Send + Sync + 'static>( pub async fn login<S: rear::depot::state::DepotState + Clone + Send + Sync + 'static>(
messages: Messages, messages: Messages,
admin: State<S>, admin: State<S>,
Query(NextUrl { next }): Query<NextUrl>, Query(NextUrl { next }): Query<NextUrl>,
@ -90,7 +90,7 @@ mod get {
messages: messages.into_iter().collect(), messages: messages.into_iter().collect(),
next, next,
}; };
templates.render_html("admin/login.html", context) templates.render_html("depot/login.html", context)
} }
} }

View File

@ -1,5 +1,5 @@
use crate::admin::domain::*;
use async_trait::async_trait; use async_trait::async_trait;
use rear::depot::prelude::*;
use serde_json::Value; use serde_json::Value;
struct Repository {} struct Repository {}
@ -7,7 +7,7 @@ struct Repository {}
impl Repository {} impl Repository {}
#[async_trait] #[async_trait]
impl AdminRepository for Repository { impl DepotRepository for Repository {
type Key = i64; type Key = i64;
fn key_from_string(&self, s: String) -> Option<Self::Key> { fn key_from_string(&self, s: String) -> Option<Self::Key> {

View File

@ -1,6 +1,5 @@
use crate::admin::domain::*;
use async_trait::async_trait; use async_trait::async_trait;
use rear::admin::state::AdminRegistry; use rear::depot::prelude::*;
use serde_json::Value; use serde_json::Value;
use std::path::PathBuf; use std::path::PathBuf;
@ -18,7 +17,7 @@ impl FileRepository {
} }
#[async_trait] #[async_trait]
impl AdminRepository for FileRepository { impl DepotRepository for FileRepository {
type Key = String; type Key = String;
fn key_from_string(&self, s: String) -> Option<Self::Key> { fn key_from_string(&self, s: String) -> Option<Self::Key> {
@ -101,11 +100,11 @@ impl AdminRepository for FileRepository {
} }
} }
pub fn register(registry: &mut AdminRegistry, path: &str) { pub fn register(registry: &mut DepotRegistry, path: &str) {
let app_key = registry.register_app("Files"); let section_key = registry.register_section("Files");
let repo = FileRepository::new(path); let repo = FileRepository::new(path);
let model_config = AdminModelConfig { let model_config = DepotModelConfig {
app_key: app_key, section_key: section_key,
name: "Files".to_owned(), name: "Files".to_owned(),
}; };
let model_result = registry.register_model(model_config, repo); let model_result = registry.register_model(model_config, repo);

View File

@ -1,7 +1,6 @@
use crate::admin::domain::*; use crate::depot::prelude::*;
use crate::admin::state::AdminRegistry;
use async_trait::async_trait; use async_trait::async_trait;
use log::{debug, warn}; use log::debug;
use serde_json::{json, Value}; use serde_json::{json, Value};
/// This is a showcase implementation with a static repository /// This is a showcase implementation with a static repository
@ -35,7 +34,7 @@ impl MyStaticRepository {
} }
#[async_trait] #[async_trait]
impl AdminRepository for MyStaticRepository { impl DepotRepository for MyStaticRepository {
type Key = i64; type Key = i64;
fn key_from_string(&self, s: String) -> Option<Self::Key> { fn key_from_string(&self, s: String) -> Option<Self::Key> {
@ -156,11 +155,11 @@ impl AdminRepository for MyStaticRepository {
} }
} }
pub fn register(registry: &mut AdminRegistry) { pub fn register(registry: &mut DepotRegistry) {
let app_key = registry.register_app("Example App"); let section_key = registry.register_section("Example App");
let repo = MyStaticRepository::new(); let repo = MyStaticRepository::new();
let model_config = AdminModelConfig { let model_config = DepotModelConfig {
app_key: app_key, section_key: section_key,
name: "ExampleModel".to_owned(), name: "ExampleModel".to_owned(),
}; };
let model_result = registry.register_model(model_config, repo); let model_result = registry.register_model(model_config, repo);

View File

@ -23,7 +23,7 @@ use axum_login::AuthManagerLayerBuilder;
use axum_messages::MessagesManagerLayer; use axum_messages::MessagesManagerLayer;
use dotenvy::dotenv; use dotenvy::dotenv;
use log::info; use log::info;
use rear::admin; use rear::depot;
use rear::service::{handlers, templates}; use rear::service::{handlers, templates};
use std::env; use std::env;
use std::net::SocketAddr; use std::net::SocketAddr;
@ -53,7 +53,7 @@ async fn main() {
// Prepare Application State Members // Prepare Application State Members
let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded."); let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded.");
let mut admin = admin::state::AdminRegistry::new("admin"); let mut admin = depot::prelude::DepotRegistry::new("admin");
// Register Admin Apps // Register Admin Apps
static_repository::register(&mut admin); static_repository::register(&mut admin);
@ -79,7 +79,7 @@ async fn main() {
//.merge(admin_router) //.merge(admin_router)
.nest( .nest(
"/admin", "/admin",
admin::routes() depot::routes()
.route_layer(login_required!( .route_layer(login_required!(
rear_auth::models::UserRepository, rear_auth::models::UserRepository,
login_url = "/admin/login" login_url = "/admin/login"

View File

@ -1,12 +1,12 @@
use axum::extract::FromRef; use axum::extract::FromRef;
use rear::admin::state::{AdminState, SharedAdminRegistry}; use rear::depot::state::{DepotState, SharedDepotRegistry};
use rear::service::templates; use rear::service::templates;
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub templates: templates::Templates, pub templates: templates::Templates,
pub admin: SharedAdminRegistry, pub admin: SharedDepotRegistry,
} }
impl FromRef<AppState> for templates::Templates { impl FromRef<AppState> for templates::Templates {
@ -15,12 +15,12 @@ impl FromRef<AppState> for templates::Templates {
} }
} }
impl AdminState for AppState { impl DepotState for AppState {
fn get_templates(&self) -> &templates::Templates { fn get_templates(&self) -> &templates::Templates {
&self.templates &self.templates
} }
fn get_registry(&self) -> SharedAdminRegistry { fn get_registry(&self) -> SharedDepotRegistry {
self.admin.clone() self.admin.clone()
} }
} }

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,10 +0,0 @@
{% extends "admin/base.jinja" %}
{% block content %}
{% include "admin/dashboard.jinja" %}
<div>
Some text
another text
Third text
</div>
{% endblock %}

View File

@ -6,7 +6,7 @@
{% include "fomantic.html" %} {% include "fomantic.html" %}
<link rel="stylesheet" type="text/css" href="/static/admin/admin.css"> <link rel="stylesheet" type="text/css" href="/static/rear/depot.css">
{% block extrastyle %}{% endblock %} {% block extrastyle %}{% endblock %}
{% block extrahead %}{% endblock %} {% block extrahead %}{% endblock %}
@ -42,14 +42,14 @@
{% endblock usertools %} {% endblock usertools %}
<div class="item"> <div class="item">
<a class="ui blue image label"> <a class="ui blue image label">
<img src="/static/admin/teddy_bear.png"> <img src="/static/rear/teddy_bear.png">
Person Person
<div class="detail">Admin</div> <div class="detail">Admiral</div>
</a> </a>
</div> </div>
<div class="icon item"> <div class="icon item">
<form id="logout-form" method="post" action="{{ url('admin:logout') }}"> <form id="logout-form" method="post" action="{{ url('depot:logout') }}">
<i class="power link icon" style="float: none;"></i> <i class="power link icon" style="float: none;"></i>
</form> </form>
@ -62,7 +62,7 @@
{% block sidebar %} {% block sidebar %}
<div class="ui vertical sidebar left visible overlay" id="main_sidemenu"> <div class="ui vertical sidebar left visible overlay" id="main_sidemenu">
<div class="ui vertical inverted fluid menu"> <div class="ui vertical inverted fluid menu">
<div class="item"><a href="{{admin_url}}"> <div class="item"><a href="{{depot_url}}">
<i class="big d20 dice icon" style="float: none;"></i> <i class="big d20 dice icon" style="float: none;"></i>
<b>Administration</b> <b>Administration</b>
</a> </a>
@ -97,17 +97,17 @@
</div> </div>
</div> </div>
{% if available_apps %} {% if sections %}
{% for app in available_apps %} {% for section in sections %}
<div class="item"> <div class="item">
<i class="cog link icon" href="{{ app.admin_url }}"></i> <i class="cog link icon" href="{{ section.section_url }}"></i>
<div class="header">{{ app.name }} </div> <div class="header">{{ section.name }} </div>
<div class="menu"> <div class="menu">
{% for model in app.models %} {% for model in section.models %}
<div <div
class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}"> class="item model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
{% if model.admin_url %} {% if model.model_url %}
<a href="{{ model.admin_url }}" hx-get="{{ model.admin_url }}" hx-target="#main" hx-push-url="true" {% if <a href="{{ model.model_url }}" hx-get="{{ model.model_url }}" hx-target="#main" hx-push-url="true" {% if
model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a> model.admin_url in request.path|urlencode %} aria-current="page" {% endif %}>{{ model.name }}</a>
{% else %} {% else %}
<span>{{ model.name }}</span> <span>{{ model.name }}</span>

View File

@ -1,15 +1,16 @@
{% if app_list %} {% if sections %}
{% for app in app_list %} {% for section in sections %}
<div <div
class="app-{{ app.key }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %} ui short scrolling container"> class="section-{{ section.key }} module{% if section.section_url in request.path|urlencode %} current-section{% endif %} ui short scrolling container">
<table class="ui very compact celled table head stuck unstackable"> <table class="ui very compact celled table head stuck unstackable">
<caption> <caption>
<a href="{{ app.app_url }}" class="section" title="Models in the {{ name }} application">{{ app.name }}</a> <a href="{{ section.section_url }}" class="section"
title="Models in the {{ name }} Section">{{ section.name }}</a>
</caption> </caption>
{% for model in app.models %} {% for model in section.models %}
<tr class="model-{{ model.key }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}"> <tr class="model-{{ model.key }}{% if model.model_url in request.path|urlencode %} current-model{% endif %}">
{% if model.admin_url %} {% if model.model_url %}
<th scope="row"><a href="{{ model.admin_url }}" {% if model.admin_url in request.path|urlencode %} <th scope="row"><a href="{{ model.model_url }}" {% if model.model_url in request.path|urlencode %}
aria-current="page" {% endif %}>{{ model.name }}</a></th> aria-current="page" {% endif %}>{{ model.name }}</a></th>
{% else %} {% else %}
<th scope="row">{{ model.name }}</th> <th scope="row">{{ model.name }}</th>
@ -21,11 +22,11 @@
<td></td> <td></td>
{% endif %} {% endif %}
{% if model.admin_url and show_changelinks %} {% if model.model_url and show_changelinks %}
{% if model.view_only %} {% if model.view_only %}
<td><a href="{{ model.admin_url }}" class="viewlink">{{ translate( 'View') }}</a></td> <td><a href="{{ model.model_url }}" class="viewlink">{{ translate( 'View') }}</a></td>
{% else %} {% else %}
<td><a href="{{ model.admin_url }}" class="changelink">{{ translate( 'Change') }}</a></td> <td><a href="{{ model.model_url }}" class="changelink">{{ translate( 'Change') }}</a></td>
{% endif %} {% endif %}
{% elif show_changelinks %} {% elif show_changelinks %}
<td></td> <td></td>

View File

@ -0,0 +1,10 @@
{% extends "depot/base.jinja" %}
{% block content %}
{% include "depot/dashboard.jinja" %}
<div>
Some text
another text
Third text
</div>
{% endblock %}