cleanup examples

This commit is contained in:
Manuel Gugger 2022-11-08 17:10:27 +01:00
parent 48e57a2e10
commit ce964220b7
26 changed files with 365 additions and 77 deletions

View File

@ -15,17 +15,17 @@ exclude = [
]
[dependencies]
actix-web = "4.0.1"
actix-session = { version = "0.7.1", features = [] }
actix-multipart = "0.4.0"
actix-web = "^4.0.1"
actix-session = { version = "^0.7.1", features = [] }
actix-multipart = "^0.4.0"
futures-util = "0.3.21"
chrono = "0.4.20"
tera = "1.16.0"
async-trait = "0.1.53"
lazy_static = "1.4.0"
itertools = "0.10.3"
serde = "1.0.136"
serde_derive = "1.0.136"
tera = "^1.16.0"
async-trait = "^0.1.53"
lazy_static = "^1.4.0"
itertools = "^0.10.3"
serde = "^1.0.136"
serde_derive = "^1.0.136"
sea-orm = { version = "^0.9.1", features = [], default-features = false }
actix-admin-macros = { version = "0.2.0", path = "actix_admin_macros" }
derive_more = "0.99.17"

View File

@ -38,7 +38,6 @@ pub fn derive_actix_admin(_input: proc_macro::TokenStream) -> proc_macro::TokenS
EntityTrait
};
use std::collections::HashMap;
use actix_session::{Session};
};
proc_macro::TokenStream::from(expanded)
}

View File

@ -4,8 +4,7 @@ layout: default
## Getting Started
* See the [example](https://github.com/mgugger/actix-admin/tree/main/example) and run with ```cargo run```.
* See the step by [step tutorial](https://github.com/mgugger/actix-admin/tree/main/example/StepbyStep.md)
* See the [basic example](https://github.com/mgugger/actix-admin/tree/main/examples/basic) and run with ```cargo run```.
## Quick overview
@ -15,21 +14,14 @@ sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-nativ
actix_admin = { version = "^0.2.0" }
```
### See inlined steps
### Steps
1. Import ActixAdmin in the main.rs and your database models:
```rust
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
use actix_web::web;
use actix_web::App;
use actix_web::HttpServer;
use sea_orm::entity::prelude::*;
use sea_orm::entity::prelude::*;
use actix_admin::prelude::*;
// 1. Import ActixAdmin
use actix_admin::prelude::*;
```
// 2. Use DeriveActixAmin* Macros to implement the traits for the model
2. Use the DeriveActixAdminMacros on the Database models to implement required traits:
```rust
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize,
DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel
)]
@ -41,20 +33,19 @@ pub struct Model {
pub id: i32,
pub comment: String
}
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActiveModelBehavior for ActiveModel {}
```
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation { }
// 3. Add actix-admin to the AppState
3. Add ActixAdmin to the actix admin app state
```rust
#[derive(Clone)]
pub struct AppState {
pub db: DatabaseConnection,
pub actix_admin: ActixAdmin,
}
```
// 4. Implement the ActixAdminAppDataTrait for the AppState
4. Implement the ActixAdminAppDataTrait for the AppState
```rust
impl ActixAdminAppDataTrait for AppState {
fn get_db(&self) -> &DatabaseConnection {
&self.db
@ -64,8 +55,10 @@ impl ActixAdminAppDataTrait for AppState {
&self.actix_admin
}
}
```
// 5. Setup the actix admin configuration
5. Setup the actix admin configuration and add database models to it in main.rs
```rust
pub fn create_actix_admin_builder() -> ActixAdminBuilder {
let comment_view_model = ActixAdminViewModel::from(Entity);
@ -81,21 +74,25 @@ pub fn create_actix_admin_builder() -> ActixAdminBuilder {
admin_builder
}
```
// 6. Add to the actix app
let actix_admin = create_actix_admin_builder().get_actix_admin();
6. Add to the actix app in main.rs
```rust
let opt = ConnectOptions::new("sqlite::memory:".to_owned());
let conn = sea_orm::Database::connect(opt).unwrap();
let app_state = AppState {
db: conn,
actix_admin: actix_admin,
};
HttpServer::new(move || {
let actix_admin_builder = create_actix_admin_builder();
let app_state = AppState {
db: conn.clone(),
actix_admin: actix_admin_builder.get_actix_admin(),
};
App::new()
//.app_data(web::Data::new(app_state.clone()))
.app_data(web::Data::new(app_state.clone()))
.service(
create_actix_admin_builder().get_scope::<AppState>()
actix_admin_builder.get_scope::<AppState>()
)
});
```

View File

@ -6,16 +6,16 @@ list_title: ' '
The actix-admin crate aims at creating a web admin interface similar to other admin interfaces (such as [flask-admin](https://github.com/flask-admin/flask-admin) in python).
## Features
1. Async: Builds on [sea-orm](https://crates.io/crates/sea-orm) for the database backend
2. Macros: Generate the required implementations for models automatically
1. Async: Builds on [sea-orm](https://crates.io/crates/sea-orm) as the database backend
2. Macros generate the required implementations for models
3. Authentication: optionally pass authentication handler to implement authentication for views
4. Supports custom validation rules
5. Searchable attributes can be specified
6. Supports custom views which are added to the Navbar
6. Supports custom views, handlers and groups in the Navbar
## Example
Check the [example](https://github.com/mgugger/actix-admin/tree/main/example) and run with ```cargo run```. The admin interface is accessible under ```localhost:5000/admin/```.
Check the [examples](https://github.com/mgugger/actix-admin/tree/main/examples) and run with ```cargo run```. The admin interface is accessible under ```localhost:5000/admin/```.
## Screenshot

View File

@ -1,4 +0,0 @@
DATABASE_URL=sqlite://database.db
OAUTH2_CLIENT_SECRET= "TODO"
OAUTH2_CLIENT_ID= "TODO"
OAUTH2_SERVER= URL + TenantId like "login.microsoftonline.com/a5f5xxxx-xxxx-414a-8463-xxxxxxxxxxxxx"

View File

@ -1 +0,0 @@
# TODO

12
examples/basic/Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "actix-admin-example"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0.1"
actix-rt = "2.7.0"
serde = "1.0.136"
serde_derive = "1.0.136"
sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
actix-admin = { path = "../../" }

View File

@ -0,0 +1,86 @@
extern crate serde_derive;
use actix_admin::prelude::*;
use actix_web::{web, App, HttpServer, middleware};
use sea_orm::{ConnectOptions, DatabaseConnection};
use std::time::Duration;
mod entity;
use entity::{Post, Comment};
#[derive(Clone)]
pub struct AppState {
pub db: DatabaseConnection,
pub actix_admin: ActixAdmin,
}
impl ActixAdminAppDataTrait for AppState {
fn get_db(&self) -> &DatabaseConnection {
&self.db
}
fn get_actix_admin(&self) -> &ActixAdmin {
&self.actix_admin
}
}
fn create_actix_admin_builder() -> ActixAdminBuilder {
let post_view_model = ActixAdminViewModel::from(Post);
let comment_view_model = ActixAdminViewModel::from(Comment);
let configuration = ActixAdminConfiguration {
enable_auth: false,
user_is_logged_in: None,
login_link: None,
logout_link: None
};
let mut admin_builder = ActixAdminBuilder::new(configuration);
let some_category = "Groupings";
admin_builder.add_entity::<AppState, Post>(&post_view_model);
admin_builder.add_entity_to_category::<AppState, Comment>(&comment_view_model, some_category);
admin_builder
}
fn get_db_options() -> ConnectOptions {
let db_url = "sqlite::memory:".to_string();
let mut opt = ConnectOptions::new(db_url);
opt.max_connections(100)
.min_connections(5)
.connect_timeout(Duration::from_secs(8))
.idle_timeout(Duration::from_secs(8))
.sqlx_logging(true);
opt
}
#[actix_rt::main]
async fn main() {
let opt = get_db_options();
let conn = sea_orm::Database::connect(opt).await.unwrap();
let _ = entity::create_post_table(&conn).await;
println!("The admin interface will be available at http://localhost:5000/admin/");
HttpServer::new(move || {
let actix_admin_builder = create_actix_admin_builder();
let app_state = AppState {
db: conn.clone(),
actix_admin: actix_admin_builder.get_actix_admin(),
};
App::new()
.app_data(web::Data::new(app_state))
.service(
actix_admin_builder.get_scope::<AppState>()
)
.wrap(middleware::Logger::default())
})
.bind("127.0.0.1:5000")
.expect("Can not bind to port 5000")
.run()
.await
.unwrap();
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Actix Admin Example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
<ul>
<li><a href="/admin/">Go to Actix-Admin</a></li>
</ul>
</body>
</html>

View File

@ -0,0 +1,3 @@
OAUTH2_CLIENT_SECRET= "TODO"
OAUTH2_CLIENT_ID= "TODO"
OAUTH2_SERVER= "login.microsoftonline.com/a5f5xxxx-xxxx-414a-8463-xxxxxxxxxxxxx(tenantId)"

View File

@ -13,5 +13,5 @@ dotenv = "0.15"
serde = "1.0.136"
serde_derive = "1.0.136"
sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
actix-admin = { path = "../" }
actix-admin = { path = "../../" }
azure_auth = { path = "./azure_auth" }

View File

@ -0,0 +1 @@
Rename .env.example to .env and update the oauth client credentials

View File

@ -0,0 +1,51 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
use super::Post;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel)]
#[sea_orm(table_name = "comment")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
#[actix_admin(primary_key)]
pub id: i32,
pub comment: String,
#[sea_orm(column_type = "Text")]
#[actix_admin(html_input_type = "email")]
pub user: String,
#[sea_orm(column_type = "DateTime")]
pub insert_date: DateTime,
pub is_visible: bool,
#[actix_admin(select_list="Post")]
pub post_id: Option<i32>,
pub my_decimal: Decimal
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::post::Entity",
from = "Column::PostId",
to = "super::post::Column::Id"
)]
Post,
}
impl Related<super::post::Entity> for Entity {
fn to() -> RelationDef {
Relation::Post.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {
fn validate(model: &ActiveModel) -> HashMap<String, String> {
let mut errors = HashMap::new();
if model.my_decimal.clone().unwrap() < Decimal::from(100 as i16) {
errors.insert("my_decimal".to_string(), "Must be larger than 100".to_string());
}
errors
}
}

View File

@ -0,0 +1,62 @@
// setup
use sea_orm::sea_query::{ForeignKeyCreateStatement, ColumnDef, TableCreateStatement};
use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult};
pub mod comment;
pub mod post;
pub use comment::Entity as Comment;
pub use post::Entity as Post;
// setup
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
let builder = db.get_database_backend();
db.execute(builder.build(stmt)).await
}
pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let stmt = sea_query::Table::create()
.table(post::Entity)
.if_not_exists()
.col(
ColumnDef::new(post::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(post::Column::Title).string().not_null())
.col(ColumnDef::new(post::Column::Text).string().not_null())
.col(ColumnDef::new(post::Column::TeaMandatory).string().not_null())
.col(ColumnDef::new(post::Column::TeaOptional).string())
.col(ColumnDef::new(post::Column::InsertDate).date())
.to_owned();
let _result = create_table(db, &stmt).await;
let stmt = sea_query::Table::create()
.table(comment::Entity)
.if_not_exists()
.col(
ColumnDef::new(post::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(comment::Column::Comment).string().not_null())
.col(ColumnDef::new(comment::Column::User).string().not_null())
.col(ColumnDef::new(comment::Column::InsertDate).date_time().not_null())
.col(ColumnDef::new(comment::Column::IsVisible).boolean().not_null())
.col(ColumnDef::new(comment::Column::MyDecimal).decimal().not_null())
.col(ColumnDef::new(comment::Column::PostId).integer())
.foreign_key(
ForeignKeyCreateStatement::new()
.name("fk-comment-post")
.from_tbl(Comment)
.from_col(comment::Column::PostId)
.to_tbl(Post)
.to_col(post::Column::Id),
)
.to_owned();
create_table(db, &stmt).await
}

View File

@ -0,0 +1,79 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
use std::fmt;
use std::fmt::Display;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminViewModel, DeriveActixAdminModel, DeriveActixAdminModelSelectList)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
#[actix_admin(primary_key)]
pub id: i32,
#[actix_admin(searchable)]
pub title: String,
#[sea_orm(column_type = "Text")]
#[actix_admin(searchable, textarea)]
pub text: String,
#[actix_admin(select_list="Tea")]
pub tea_mandatory: Tea,
#[actix_admin(select_list="Tea")]
pub tea_optional: Option<Tea>,
pub insert_date: Date,
}
impl Display for Model {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &*self {
_ => write!(formatter, "{} {}", &self.title, &self.insert_date),
}
}
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::comment::Entity")]
Comment,
}
impl Related<super::comment::Entity> for Entity {
fn to() -> RelationDef {
Relation::Comment.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea {
#[sea_orm(string_value = "EverydayTea")]
EverydayTea,
#[sea_orm(string_value = "BreakfastTea")]
BreakfastTea,
}
impl FromStr for Tea {
type Err = ();
fn from_str(input: &str) -> Result<Tea, Self::Err> {
match input {
"EverydayTea" => Ok(Tea::EverydayTea),
"BreakfastTea" => Ok(Tea::BreakfastTea),
_ => Err(()),
}
}
}
impl Display for Tea {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &*self {
Tea::EverydayTea => write!(formatter, "{}", String::from("EverydayTea")),
Tea::BreakfastTea => write!(formatter, "{}", String::from("BreakfastTea")),
}
}
}
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}

View File

@ -103,7 +103,7 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
let comment_view_model = ActixAdminViewModel::from(Comment);
let configuration = ActixAdminConfiguration {
enable_auth: false,
enable_auth: true,
user_is_logged_in: Some(|session: &Session| -> bool {
let user_info = session.get::<UserInfo>("user_info").unwrap();
user_info.is_some()
@ -137,24 +137,9 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
async fn main() {
dotenv::dotenv().ok();
let actix_admin = create_actix_admin_builder().get_actix_admin();
let oauth2_client_id;
let oauth2_client_secret;
let oauth2_server;
match actix_admin.configuration.enable_auth {
true => {
oauth2_client_id = env::var("OAUTH2_CLIENT_ID").expect("Missing the OAUTH2_CLIENT_ID environment variable.");
oauth2_client_secret = env::var("OAUTH2_CLIENT_SECRET").expect("Missing the OAUTH2_CLIENT_SECRET environment variable.");
oauth2_server= env::var("OAUTH2_SERVER").expect("Missing the OAUTH2_SERVER environment variable.");
},
false => {
oauth2_client_id = String::new();
oauth2_client_secret = String::new();
oauth2_server = String::new();
}
}
let oauth2_client_id = env::var("OAUTH2_CLIENT_ID").expect("Missing the OAUTH2_CLIENT_ID environment variable.");
let oauth2_client_secret = env::var("OAUTH2_CLIENT_SECRET").expect("Missing the OAUTH2_CLIENT_SECRET environment variable.");
let oauth2_server= env::var("OAUTH2_SERVER").expect("Missing the OAUTH2_SERVER environment variable.");
let azure_auth = AzureAuth::new(&oauth2_server, &oauth2_client_id, &oauth2_client_secret);
@ -183,22 +168,24 @@ async fn main() {
let conn = sea_orm::Database::connect(opt).await.unwrap();
let _ = entity::create_post_table(&conn).await;
let app_state = AppState {
oauth: client,
tmpl: tera,
db: conn,
actix_admin: actix_admin,
};
let cookie_secret_key = Key::generate();
HttpServer::new(move || {
let actix_admin_builder = create_actix_admin_builder();
let app_state = AppState {
oauth: client.clone(),
tmpl: tera.clone(),
db: conn.clone(),
actix_admin: actix_admin_builder.get_actix_admin(),
};
App::new()
.app_data(web::Data::new(app_state.clone()))
.wrap(SessionMiddleware::new(CookieSessionStore::default(), cookie_secret_key.clone()))
.route("/", web::get().to(index))
.service(azure_auth.clone().create_scope::<AppState>())
.service(
create_actix_admin_builder().get_scope::<AppState>()
actix_admin_builder.get_scope::<AppState>()
)
.wrap(middleware::Logger::default())
})

View File

@ -36,6 +36,7 @@ pub mod prelude {
pub use itertools::izip;
pub use lazy_static::lazy_static;
pub use async_trait::async_trait;
pub use actix_session::{Session};
}
use crate::prelude::*;