show model overview in index page
This commit is contained in:
+5
-1
@@ -8,6 +8,10 @@ build = "build.rs"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
yopa = { path = "../yopa" }
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
|
||||
log = "0.4.14"
|
||||
simple-logging = "2.0.2"
|
||||
actix-web = "3"
|
||||
@@ -15,7 +19,7 @@ parking_lot = "0.11.1"
|
||||
include_dir = "0.6.0"
|
||||
tera = "1.6.1"
|
||||
actix-web-static-files = "3.0"
|
||||
lazy_static = "1.4.0"
|
||||
once_cell = "1.5.2"
|
||||
|
||||
tokio = { version="0.2.6", features=["full"] }
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends "_layout" %}
|
||||
|
||||
{% block title -%}
|
||||
Index
|
||||
{%- endblock %}
|
||||
|
||||
{% block nav -%}
|
||||
<a href="/">Home</a>
|
||||
{%- endblock %}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<h1>Welcome to tera on actix</h1>
|
||||
|
||||
<a href="/model/object/create">New model</a>
|
||||
|
||||
<h2>Defined models:</h2>
|
||||
|
||||
<ul>
|
||||
{% for model in models %}
|
||||
<li>
|
||||
<b>{{model.name}}</b><br>
|
||||
|
||||
{#
|
||||
|
||||
{% if !model.properties.is_empty() %}
|
||||
Properties:
|
||||
<ul>
|
||||
{% for prop in model.properties %}
|
||||
<li>{{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<a href="/model/property/create/{{model.id}}">New property</a>
|
||||
|
||||
{% if !model.relations.is_empty() %}
|
||||
Relations:
|
||||
<ul>
|
||||
{% for rel in model.relations %}
|
||||
<li>
|
||||
{{rel.name}} -> {{rel.related_name}}
|
||||
|
||||
{% if !rel.properties.is_empty() %}
|
||||
Properties:
|
||||
<ul>
|
||||
{% for prop in rel.properties %}
|
||||
<li>{{prop.name}}, {{prop.data_type}}, def: {{prop.default}}, opt: {{prop.optional}}, mult: {{prop.multiple}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<a href="/model/relation/create/{{model.id}}">New relation</a>
|
||||
|
||||
#}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{%- endblock %}
|
||||
@@ -119,6 +119,8 @@ textarea:focus,
|
||||
outline: 0 none !important;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
.cards-table {
|
||||
border-collapse: collapse;
|
||||
margin: 0 auto;
|
||||
@@ -198,3 +200,8 @@ textarea:focus,
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
li {
|
||||
padding-bottom: .5rem;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,72 @@
|
||||
{%- endblock %}
|
||||
|
||||
{% block nav -%}
|
||||
<a href="/">Home</a>
|
||||
<a href="/">Home</a>
|
||||
{%- endblock %}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<h1>Welcome to tera on actix</h1>
|
||||
<h1>Welcome to YOPA</h1>
|
||||
|
||||
<a href="/model/object/create">New model</a>
|
||||
|
||||
<h2>Defined models:</h2>
|
||||
|
||||
<ul>
|
||||
{% for model in models %}
|
||||
<li>
|
||||
<b title="{{model.id}}">{{model.name}}</b><br>
|
||||
|
||||
{% if model.properties %}
|
||||
Properties:
|
||||
<ul>
|
||||
{% for prop in model.properties %}
|
||||
<li>
|
||||
{{prop.name}}, {{prop.data_type}}
|
||||
{%- if prop.default -%}
|
||||
, default: "{{prop.default | print_typed_value}}"
|
||||
{%- endif -%}
|
||||
{%- if prop.optional %}, OPTIONAL{% endif %}
|
||||
{%- if prop.multiple %}, MULTIPLE{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<a href="/model/property/create/{{model.id}}">Add a property</a><br>
|
||||
|
||||
{% if model.relations %}
|
||||
Relations:
|
||||
<ul>
|
||||
{% for rel in model.relations %}
|
||||
<li>
|
||||
<span title="{{rel.model.id}}">"{{rel.model.name}}", pointing to: <i>{{rel.related_name}}</i></span><br>
|
||||
|
||||
{% if rel.properties %}
|
||||
Properties:
|
||||
<ul>
|
||||
{% for prop in rel.properties %}
|
||||
<li title="{{prop.id}}">
|
||||
{{prop.name}}, {{prop.data_type}}
|
||||
{%- if prop.default -%}
|
||||
, default: "{{prop.default | print_typed_value}}"
|
||||
{%- endif -%}
|
||||
{%- if prop.optional %}, OPTIONAL{% endif %}
|
||||
{%- if prop.multiple %}, MULTIPLE{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<a href="/model/property/create/{{rel.model.id}}">Add a property</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<a href="/model/relation/create/{{model.id}}">Add a relation</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{%- endblock %}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "_layout" %}
|
||||
|
||||
{% block title -%}
|
||||
Define object
|
||||
{%- endblock %}
|
||||
|
||||
{% block nav -%}
|
||||
<a href="/">Home</a>
|
||||
{%- endblock %}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<h1>Define new object model</h1>
|
||||
|
||||
<form action="/model/object/create" method="POST">
|
||||
<label for="parent">Parent:</label>
|
||||
<select name="parent" id="parent">
|
||||
<option value="">No parent</option>
|
||||
{%- for model in all_models %}
|
||||
<option value="{{model.id}}">{{model.name}}</option>
|
||||
{%- endfor %}
|
||||
</select><br>
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name"><br>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
{%- endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "_layout" %}
|
||||
|
||||
{% block title -%}
|
||||
Define property
|
||||
{%- endblock %}
|
||||
|
||||
{% block nav -%}
|
||||
<a href="/">Home</a>
|
||||
{%- endblock %}
|
||||
|
||||
{% block content -%}
|
||||
|
||||
<h1>Add new property to model "{{model.name}}"</h1>
|
||||
|
||||
<form action="/model/property/create" method="POST">
|
||||
<input type="hidden" name="object" value="{{object}}">
|
||||
|
||||
<label for="name">Name:</label>
|
||||
<input type="text" id="name" name="name"><br>
|
||||
|
||||
<label for="optional">Optional:</label>
|
||||
<input type="checkbox" name="optional" id="optional">
|
||||
<br>
|
||||
|
||||
<label for="multiple">Multiple:</label>
|
||||
<input type="checkbox" name="multiple" id="multiple"><br>
|
||||
|
||||
<label for="data_type">Type:</label>
|
||||
<select name="data_type" id="data_type">
|
||||
<option value="String" selected>String</option>
|
||||
<option value="Integer">Integer</option>
|
||||
<option value="Decimal">Decimal</option>
|
||||
<option value="Boolean">Boolean</option>
|
||||
</select><br>
|
||||
|
||||
<label for="default">Default:</label>
|
||||
<input type="text" id="default" name="default"><br>
|
||||
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
|
||||
{%- endblock %}
|
||||
+115
-17
@@ -1,5 +1,5 @@
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
#[macro_use] extern crate actix_web;
|
||||
|
||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest};
|
||||
use parking_lot::Mutex;
|
||||
@@ -13,8 +13,14 @@ use include_dir::Dir;
|
||||
use std::sync::Arc;
|
||||
use log::LevelFilter;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::borrow::Borrow;
|
||||
use std::ops::Deref;
|
||||
use yopa::{Storage, TypedValue};
|
||||
use std::collections::HashMap;
|
||||
|
||||
mod tera_ext;
|
||||
mod routes;
|
||||
|
||||
// Embed static files
|
||||
include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
|
||||
@@ -22,35 +28,127 @@ include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
|
||||
// Embed templates
|
||||
static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates");
|
||||
|
||||
lazy_static! {
|
||||
static ref TERA : Tera = {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_include_dir_templates(&TEMPLATES);
|
||||
tera
|
||||
};
|
||||
}
|
||||
pub(crate) static TERA : Lazy<Tera> = Lazy::new(|| {
|
||||
let mut tera = Tera::default();
|
||||
tera.add_include_dir_templates(&TEMPLATES);
|
||||
|
||||
#[get("/")]
|
||||
async fn service_index(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
||||
let mut context = tera::Context::new();
|
||||
// Special filter for the TypedValue map
|
||||
use serde_json::Value;
|
||||
tera.register_filter("print_typed_value", |v : &Value, _ : &HashMap<String, Value>| -> tera::Result<Value> {
|
||||
if v.is_null() {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
if let Value::Object(map) = v {
|
||||
if let Some((_, v)) = map.iter().next() {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
}
|
||||
Err(tera::Error::msg("Expected nonenmpty object"))
|
||||
});
|
||||
|
||||
let html = TERA.render("index", &context).map_err(|e| {
|
||||
actix_web::error::ErrorInternalServerError(e)
|
||||
})?;
|
||||
tera
|
||||
});
|
||||
|
||||
Ok(HttpResponse::Ok().body(html))
|
||||
}
|
||||
type YopaStoreWrapper = web::Data<tokio::sync::RwLock<yopa::Storage>>;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
simple_logging::log_to_stderr(LevelFilter::Debug);
|
||||
|
||||
// Ensure the lazy ref is initialized early (to catch template bugs ASAP)
|
||||
let _ = TERA.deref();
|
||||
|
||||
let database : YopaStoreWrapper = {
|
||||
let mut store = Storage::new();
|
||||
|
||||
// Seed the store with some dummy data for view development
|
||||
use yopa::model;
|
||||
use yopa::DataType;
|
||||
|
||||
let id_recipe = store.define_object(model::ObjectModel {
|
||||
id: Default::default(),
|
||||
name: "Recipe".to_string(),
|
||||
parent: None
|
||||
}).unwrap();
|
||||
let id_book = store.define_object(model::ObjectModel {
|
||||
id: Default::default(),
|
||||
name: "Book".to_string(),
|
||||
parent: None
|
||||
}).unwrap();
|
||||
let id_ing = store.define_object(model::ObjectModel {
|
||||
id: Default::default(),
|
||||
name: "Ingredient".to_string(),
|
||||
parent: None
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
id: Default::default(),
|
||||
object: id_recipe,
|
||||
name: "name".to_string(),
|
||||
optional: false,
|
||||
multiple: true,
|
||||
data_type: DataType::String,
|
||||
default: None
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
id: Default::default(),
|
||||
object: id_book,
|
||||
name: "title".to_string(),
|
||||
optional: false,
|
||||
multiple: false,
|
||||
data_type: DataType::String,
|
||||
default: None
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
id: Default::default(),
|
||||
object: id_book,
|
||||
name: "author".to_string(),
|
||||
optional: true,
|
||||
multiple: true,
|
||||
data_type: DataType::String,
|
||||
default: Some(TypedValue::String("Pepa Novák".into()))
|
||||
}).unwrap();
|
||||
|
||||
let rel_book_id = store.define_relation(model::RelationModel {
|
||||
id: Default::default(),
|
||||
object: id_recipe,
|
||||
name: "book reference".to_string(),
|
||||
optional: true,
|
||||
multiple: true,
|
||||
related: id_book
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
id: Default::default(),
|
||||
object: rel_book_id,
|
||||
name: "page".to_string(),
|
||||
optional: true,
|
||||
multiple: false,
|
||||
data_type: DataType::Integer,
|
||||
default: None
|
||||
}).unwrap();
|
||||
|
||||
store.define_relation(model::RelationModel {
|
||||
id: Default::default(),
|
||||
object: id_recipe,
|
||||
name: "related recipe".to_string(),
|
||||
optional: true,
|
||||
multiple: true,
|
||||
related: id_recipe
|
||||
}).unwrap();
|
||||
|
||||
web::Data::new(tokio::sync::RwLock::new(store))
|
||||
};
|
||||
|
||||
HttpServer::new(move || {
|
||||
let static_files = actix_web_static_files::ResourceFiles::new("/static", included_static_files())
|
||||
.do_not_resolve_defaults();
|
||||
|
||||
App::new()
|
||||
.service(service_index)
|
||||
.app_data(database.clone())
|
||||
.service(routes::index)
|
||||
.service(static_files)
|
||||
})
|
||||
.bind("127.0.0.1:8080")?.run().await
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
use actix_web::{web, HttpRequest, Responder};
|
||||
use crate::TERA;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use yopa::Storage;
|
||||
use serde::Serialize;
|
||||
use yopa::model::{PropertyModel, RelationModel};
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ObjectModelDisplay<'a> {
|
||||
id : yopa::ID,
|
||||
name : &'a str,
|
||||
properties: Vec<&'a PropertyModel>,
|
||||
relations: Vec<RelationModelDisplay<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct RelationModelDisplay<'a> {
|
||||
model : &'a RelationModel,
|
||||
related_name : &'a str,
|
||||
properties: Vec<&'a PropertyModel>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub(crate) async fn index(req: HttpRequest, store : crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> {
|
||||
|
||||
let rg = store.read().await;
|
||||
|
||||
let models_iter = rg.get_object_models();
|
||||
|
||||
// object and relation props
|
||||
let mut model_props = rg.get_grouped_prop_models();
|
||||
|
||||
let mut model_relations = rg.get_grouped_relation_models();
|
||||
|
||||
let mut models = vec![];
|
||||
for om in models_iter {
|
||||
let mut oprops = model_props.remove(&om.id).unwrap_or_default();
|
||||
let mut relations = model_relations.remove(&om.id).unwrap_or_default();
|
||||
|
||||
let rel_displays = relations.into_iter().map(|rm| {
|
||||
let mut rprops = model_props.remove(&rm.id).unwrap_or_default();
|
||||
rprops.sort_by_key(|m| &m.name);
|
||||
|
||||
RelationModelDisplay {
|
||||
model: rm,
|
||||
related_name: rg.get_model_name(rm.related),
|
||||
properties: rprops
|
||||
}
|
||||
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
oprops.sort_by_key(|m| &m.name);
|
||||
|
||||
models.push(ObjectModelDisplay {
|
||||
id: om.id,
|
||||
name: &om.name,
|
||||
properties: oprops,
|
||||
relations: rel_displays,
|
||||
})
|
||||
}
|
||||
|
||||
models.sort_by_key(|m| m.name);
|
||||
|
||||
let mut ctx = tera::Context::new();
|
||||
ctx.insert("models", &models);
|
||||
|
||||
TERA.build_response("index", &ctx)
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use tera::Tera;
|
||||
use include_dir::Dir;
|
||||
use actix_web::HttpResponse;
|
||||
use actix_web::http::StatusCode;
|
||||
|
||||
fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, tera : &mut Tera, dir: &Dir) {
|
||||
for f in dir.files() {
|
||||
@@ -27,13 +29,27 @@ fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, ter
|
||||
|
||||
pub(crate) trait TeraExt {
|
||||
fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()>;
|
||||
|
||||
fn build_response(&self, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse> {
|
||||
self.build_err_response(StatusCode::OK, template, context)
|
||||
}
|
||||
|
||||
fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse>;
|
||||
}
|
||||
|
||||
impl TeraExt for Tera {
|
||||
fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()> {
|
||||
debug!("Walk dir: {:?}", dir);
|
||||
debug!("Walk dir: {:?}", dir.path());
|
||||
let mut templates = vec![];
|
||||
tera_includedir_walk_folder_inner(&mut templates, self, dir);
|
||||
self.add_raw_templates(templates)
|
||||
}
|
||||
|
||||
fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse> {
|
||||
let html = self.render(template, context).map_err(|e| {
|
||||
actix_web::error::ErrorInternalServerError(e)
|
||||
})?;
|
||||
|
||||
Ok(HttpResponse::build(code).body(html))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user