diff --git a/Cargo.lock b/Cargo.lock index 55273ff8..cee83be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2275,6 +2275,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "strum 0.26.2", "time", "tokio", "toml", diff --git a/Cargo.toml b/Cargo.toml index 3b9127d1..ef266b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ rust-embed = { version = "8.4.0", features = [ serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" serde_yaml = "0.9.33" +strum = { version = "0.26.2", features = ["derive"] } time = { version = "0.3.36", features = ["local-offset"] } tokio = { version = "1.38.0", features = ["full"] } tower-http = { version = "0.5.2", features = [ diff --git a/crates/core-service/Cargo.toml b/crates/core-service/Cargo.toml index 8d3b5ffb..fe0a1e98 100644 --- a/crates/core-service/Cargo.toml +++ b/crates/core-service/Cargo.toml @@ -24,3 +24,4 @@ tokio = { workspace = true } toml = "0.8.13" tracing = { workspace = true } walkdir = { workspace = true } +strum = { workspace = true } diff --git a/crates/core-service/src/vars/admin.rs b/crates/core-service/src/vars/admin.rs index bac6ea58..9e7b6697 100644 --- a/crates/core-service/src/vars/admin.rs +++ b/crates/core-service/src/vars/admin.rs @@ -1,7 +1,9 @@ use anyhow::Result; -use land_dao::models::deployment; -use land_dao::projects; +use land_dao::deployment::{DeployStatus, DeploymentStatus}; +use land_dao::models::{deployment, deployment_task}; +use land_dao::{projects, user, worker}; use serde::Serialize; +use strum::IntoEnumIterator; /// DashboardVars is the vars for admin dashboard page #[derive(Serialize)] @@ -38,6 +40,9 @@ pub struct DeployVars { pub status: String, pub created_at: String, pub updated_at: String, + pub user_name: String, + pub user_email: String, + pub user_oauth: String, } impl DeployVars { @@ -48,6 +53,16 @@ impl DeployVars { project_ids.push(dp.project_id); } let projects = projects::list_by_ids(project_ids).await?; + + let mut user_ids = vec![]; + for dp in &dps { + user_ids.push(dp.user_id); + } + // unique user_ids + user_ids.sort(); + user_ids.dedup(); + let users = land_dao::user::list_infos(user_ids).await?; + for dp in dps { let mut v = DeployVars { id: dp.id, @@ -60,13 +75,146 @@ impl DeployVars { status: dp.status, created_at: dp.created_at.to_string(), updated_at: dp.updated_at.to_string(), + user_name: "".to_string(), + user_email: "".to_string(), + user_oauth: "".to_string(), }; if let Some(project) = projects.get(&dp.project_id) { v.language.clone_from(&project.language); v.created_by.clone_from(&project.created_by); } + if let Some(user) = users.get(&dp.user_id) { + v.user_name.clone_from(&user.nick_name); + v.user_email.clone_from(&user.email); + v.user_oauth.clone_from(&user.origin_provider); + } vars.push(v); } Ok(vars) } + + pub async fn from_model(dp: deployment::Model) -> Result { + let project = projects::get_by_id(dp.project_id, None).await?; + let user = user::get_info_by_id(dp.user_id, None).await?; + let mut v = DeployVars { + id: dp.id, + domain: dp.domain, + language: "".to_string(), + created_by: "".to_string(), + size: dp.storage_size, + deploy_status: dp.deploy_status, + deploy_message: dp.deploy_message, + status: dp.status, + created_at: dp.created_at.to_string(), + updated_at: dp.updated_at.to_string(), + user_name: "".to_string(), + user_email: "".to_string(), + user_oauth: "".to_string(), + }; + if let Some(project) = project { + v.language.clone_from(&project.language); + v.created_by.clone_from(&project.created_by); + } + if let Some(user) = user { + v.user_name.clone_from(&user.nick_name); + v.user_email.clone_from(&user.email); + v.user_oauth.clone_from(&user.origin_provider); + } + Ok(v) + } +} + +#[derive(Serialize)] +pub struct DeployDetailVars { + pub id: i32, + pub deploy_id: i32, + pub worker_id: i32, + pub worker_ip: String, + pub deploy_status: String, + pub deploy_message: String, + pub created_at: String, + pub updated_at: String, +} + +impl DeployDetailVars { + pub async fn from_models(tasks: Vec) -> Result> { + let mut vars = vec![]; + let mut worker_ids = vec![]; + for task in &tasks { + worker_ids.push(task.worker_id); + } + worker_ids.sort(); + worker_ids.dedup(); + + let workers = worker::list_by_ids(worker_ids).await?; + for task in tasks { + vars.push(DeployDetailVars { + id: task.id, + deploy_id: task.deployment_id, + worker_id: task.worker_id, + worker_ip: "".to_string(), + deploy_status: task.deploy_status, + deploy_message: task.deploy_message, + created_at: task.created_at.to_string(), + updated_at: task.updated_at.to_string(), + }); + } + if !workers.is_empty() { + for v in vars.iter_mut() { + if let Some(worker) = workers.get(&v.worker_id) { + v.worker_ip.clone_from(&worker.ip); + } + } + } + Ok(vars) + } +} + +#[derive(Serialize, Debug)] +pub struct DeployStatusVars { + pub value: String, + pub is_selected: bool, +} + +impl DeployStatusVars { + pub fn new_list(selected: &str) -> Vec { + let mut vars = vec![]; + vars.push(DeployStatusVars { + value: "all".to_string(), + is_selected: selected.is_empty(), + }); + for status in DeployStatus::iter() { + vars.push(DeployStatusVars { + value: status.to_string(), + is_selected: status.to_string() == selected, + }); + } + vars + } +} + +#[derive(Serialize, Debug)] +pub struct DeployCommonStatusVars { + pub value: String, + pub label: String, + pub is_selected: bool, +} + +impl DeployCommonStatusVars { + pub fn new_list(selected: &str) -> Vec { + let mut vars = vec![]; + vars.push(DeployCommonStatusVars { + value: "".to_string(), + label: "Available".to_string(), + is_selected: selected.is_empty(), + }); + for status in DeploymentStatus::iter() { + vars.push(DeployCommonStatusVars { + value: status.to_string(), + label: status.to_string(), + is_selected: status.to_string() == selected, + }); + } + vars + } } diff --git a/crates/dao/Cargo.toml b/crates/dao/Cargo.toml index 95933bc7..849e6eb9 100644 --- a/crates/dao/Cargo.toml +++ b/crates/dao/Cargo.toml @@ -29,7 +29,7 @@ serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } sha2 = "0.10.8" -strum = { version = "0.26.2", features = ["derive"] } +strum = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } diff --git a/crates/dao/src/deployment.rs b/crates/dao/src/deployment.rs index d57fabab..f6df3ad2 100644 --- a/crates/dao/src/deployment.rs +++ b/crates/dao/src/deployment.rs @@ -30,7 +30,7 @@ impl Default for Spec { } } -#[derive(strum::Display)] +#[derive(strum::Display, PartialEq, strum::EnumString, strum::EnumIter)] #[strum(serialize_all = "lowercase")] pub enum DeployStatus { Waiting, @@ -41,7 +41,7 @@ pub enum DeployStatus { Failed, } -#[derive(strum::Display)] +#[derive(strum::Display, PartialEq, strum::EnumString, strum::EnumIter)] #[strum(serialize_all = "lowercase")] pub enum DeploymentStatus { Active, @@ -62,6 +62,13 @@ pub async fn get_last_by_project(project_id: i32) -> Result Result> { + let db = DB.get().unwrap(); + let dp = deployment::Entity::find_by_id(id).one(db).await?; + Ok(dp) +} + /// create a deployment pub async fn create( user_id: i32, @@ -147,17 +154,36 @@ pub async fn list_by_deploy_status(status: DeployStatus) -> Result, + deploy_status: Vec, current: u64, page_size: u64, + domain: Option, ) -> Result<(Vec, ItemsAndPagesNumber)> { - let mut args = vec![]; - for s in status { - args.push(s.to_string()); - } let db = DB.get().unwrap(); - let pager = deployment::Entity::find() - .filter(deployment::Column::Status.is_in(args)) - .order_by_desc(deployment::Column::Id) + let mut select = deployment::Entity::find(); + + let mut args1 = vec![]; + if !status.is_empty() { + for s in status { + args1.push(s.to_string()); + } + select = select.filter(deployment::Column::Status.is_in(args1)); + } + + let mut args2 = vec![]; + if !deploy_status.is_empty() { + for s in deploy_status { + args2.push(s.to_string()); + } + select = select.filter(deployment::Column::DeployStatus.is_in(args2)); + } + + if let Some(d) = domain { + select = select.filter(deployment::Column::Domain.contains(d)); + } + + let pager = select + .order_by_desc(deployment::Column::UpdatedAt) .paginate(db, page_size); let dps = pager.fetch_page(current - 1).await?; let pages = pager.num_items_and_pages().await?; @@ -287,6 +313,16 @@ pub async fn list_tasks_by_taskid(task_id: String) -> Result Result> { + let db = DB.get().unwrap(); + let tasks = deployment_task::Entity::find() + .filter(deployment_task::Column::DeploymentId.eq(deploy_id)) + .all(db) + .await?; + Ok(tasks) +} + /// create_task creates a task pub async fn create_task( worker_id: i32, diff --git a/crates/dao/src/worker.rs b/crates/dao/src/worker.rs index c48cb009..3125d270 100644 --- a/crates/dao/src/worker.rs +++ b/crates/dao/src/worker.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::{db::DB, models::worker, now_time}; use anyhow::Result; use sea_orm::{ @@ -32,6 +34,20 @@ pub async fn list_all() -> Result> { Ok(workers) } +/// list_by_ids returns workers by ids +pub async fn list_by_ids(ids: Vec) -> Result> { + let db = DB.get().unwrap(); + let workers = worker::Entity::find() + .filter(worker::Column::Id.is_in(ids)) + .all(db) + .await?; + let mut map = HashMap::new(); + for w in workers { + map.insert(w.id, w); + } + Ok(map) +} + /// update_online updates worker status pub async fn update_online( ip: String, diff --git a/land-controller/src/server/deploys.rs b/land-controller/src/server/deploys.rs index d9b6078e..007497d5 100644 --- a/land-controller/src/server/deploys.rs +++ b/land-controller/src/server/deploys.rs @@ -1,24 +1,71 @@ -use axum::extract::Query; +use axum::extract::{Path, Query}; +use axum::http::StatusCode; use axum::response::IntoResponse; use axum::Extension; -use axum_csrf::CsrfToken; use land_core_service::clerkauth::SessionUser; use land_core_service::httputil::ServerError; use land_core_service::template::{self, RenderHtmlMinified}; +use land_core_service::vars::admin::{DeployDetailVars, DeployVars}; use land_core_service::vars::{admin, PageVars, PaginationVar}; -use land_dao::deployment; +use land_dao::deployment::{self, DeployStatus, DeploymentStatus}; use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct DeploysQuery { + domain: Option, + #[serde(rename = "common-status")] + common_status: Option, + #[serde(rename = "deploy-status")] + deploy_status: Option, page: Option, size: Option, } +impl DeploysQuery { + pub fn to_deploy_status_filter(&self) -> Vec { + let mut filters = vec![]; + if let Some(status) = &self.deploy_status { + if status == "all" { + return filters; + } + if let Ok(s) = status.parse::() { + filters.push(s); + } + } + filters + } + pub fn to_common_status_filter(&self) -> Vec { + let mut filters = vec![]; + if let Some(status) = &self.common_status { + if status == "available" || status.is_empty() { + filters.push(DeploymentStatus::Active); + filters.push(DeploymentStatus::Disabled); + return filters; + } + if let Ok(s) = status.parse::() { + filters.push(s); + } + } + filters + } + pub fn to_query_string(&self) -> String { + let mut query = String::new(); + if let Some(domain) = &self.domain { + query.push_str(&format!("domain={}&", domain)); + } + if let Some(common_status) = &self.common_status { + query.push_str(&format!("common-status={}&", common_status)); + } + if let Some(deploy_status) = &self.deploy_status { + query.push_str(&format!("deploy-status={}&", deploy_status)); + } + query + } +} + /// index is a handler for GET /admin/deploys pub async fn index( Extension(user): Extension, - csrf_layer: CsrfToken, engine: template::Engine, Query(q): Query, ) -> Result { @@ -26,20 +73,19 @@ pub async fn index( struct Vars { page: PageVars, user: SessionUser, - csrf: String, deploys: Vec, pagination: PaginationVar, + deploy_status_list: Vec, + common_status_list: Vec, } - let csrf = csrf_layer.authenticity_token()?; let page = q.page.unwrap_or(1); let page_size = q.size.unwrap_or(10); let (dps, pages) = deployment::list_by_status_paginate( - vec![ - deployment::DeploymentStatus::Active, - deployment::DeploymentStatus::Disabled, - ], + q.to_common_status_filter(), + q.to_deploy_status_filter(), page, page_size, + q.domain.clone(), ) .await?; let deploys = admin::DeployVars::from_models(dps).await?; @@ -48,21 +94,57 @@ pub async fn index( page_size, pages.number_of_items, pages.number_of_pages, - "/deploys", + format!("/deploys?{}", q.to_query_string()).as_str(), ); - Ok(( - csrf_layer, - RenderHtmlMinified( - "deploys.hbs", - engine, - Vars { - page: PageVars::new_admin("Deploys", "admin-deploys"), - user, - csrf, - deploys, - pagination, - }, - ), - ) - .into_response()) + let deploy_status_list = + admin::DeployStatusVars::new_list(&q.deploy_status.unwrap_or_default()); + let common_status_list = + admin::DeployCommonStatusVars::new_list(&q.common_status.unwrap_or_default()); + Ok(RenderHtmlMinified( + "deploys.hbs", + engine, + Vars { + page: PageVars::new_admin("Deploys", "admin-deploys"), + user, + deploys, + pagination, + deploy_status_list, + common_status_list, + }, + )) +} + +/// details is a handler for GET /admin/deploys/details/:id +pub async fn details( + Extension(user): Extension, + engine: template::Engine, + Path(deploy_id): Path, +) -> Result { + #[derive(serde::Serialize)] + struct Vars { + page: PageVars, + user: SessionUser, + deploy: DeployVars, + details: Vec, + } + let dp = deployment::get_by_id(deploy_id).await?; + if dp.is_none() { + return Err(ServerError::status_code( + StatusCode::NOT_FOUND, + "Deployment not found", + )); + } + let deploy = DeployVars::from_model(dp.unwrap()).await?; + let tasks = deployment::list_tasks_by_deploy_id(deploy_id).await?; + let details = DeployDetailVars::from_models(tasks).await?; + Ok(RenderHtmlMinified( + "deploys-details.hbs", + engine, + Vars { + page: PageVars::new_admin("Deploys", "admin-deploys"), + user, + deploy, + details, + }, + )) } diff --git a/land-controller/src/server/mod.rs b/land-controller/src/server/mod.rs index 5aa2facb..fa80093b 100644 --- a/land-controller/src/server/mod.rs +++ b/land-controller/src/server/mod.rs @@ -33,6 +33,7 @@ pub async fn start(addr: SocketAddr, assets_dir: &str) -> anyhow::Result<()> { .route("/projects/disable", post(projects::disable)) .route("/projects/enable", post(projects::enable)) .route("/deploys", get(deploys::index)) + .route("/deploys/details/:deploy_id", get(deploys::details)) .route("/workers", get(workers::index)) .route("/create-worker-token", post(workers::create_token)) .route("/delete-token", post(settings::delete_token)) diff --git a/land-controller/templates/deploys-details.hbs b/land-controller/templates/deploys-details.hbs new file mode 100644 index 00000000..700349d3 --- /dev/null +++ b/land-controller/templates/deploys-details.hbs @@ -0,0 +1,73 @@ + + + + + {{> partials/head.hbs}} + + + +
+ {{> partials/admin-sidebar.hbs}} +
+ {{> partials/nav-top.hbs}} +
+
+
+
Deployment {{deploy.domain}} (#{{deploy.id}})
+

List deployment details for all workers.

+
+
+ + + + + + + + + + + + + {{#each details}} + + + + + + + + + {{/each}} + +
#DomainIPDeploy_StatusUpdatedOps
{{id}}{{../deploy.domain}} (#{{deploy_id}}){{worker_ip}} (#{{worker_id}}) + {{deploy_status}} + {{#if (eq_str deploy_status 'failed')}} + ({{deploy_message}}) + {{/if}} + + + + + + + + + +
+
+
+ {{> partials/nav-footer.hbs}} +
+ +
+ {{> partials/footer.hbs}} + + + + \ No newline at end of file diff --git a/land-controller/templates/deploys.hbs b/land-controller/templates/deploys.hbs index 73fa0c12..c300c4a7 100644 --- a/land-controller/templates/deploys.hbs +++ b/land-controller/templates/deploys.hbs @@ -16,12 +16,55 @@
Projects Deployments

Manage all projects deployments here.

+
+
+ +
+
Domain
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+
+ @@ -36,6 +79,10 @@ + @@ -50,7 +97,8 @@
# DomainUser Lang Created_By Size
{{id}} {{domain}} + {{user_name}} + {{language}} {{created_by}} {{size}} - + @@ -74,8 +122,10 @@ {{/each}} + {{> partials/notification.hbs}} {{> partials/nav-footer.hbs}} + {{> partials/footer.hbs}}