diff --git a/Cargo.toml b/Cargo.toml index 43aee1e..ca7030f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ [workspace] name = "orca" -members=[ +members = [ "crates/libs/cerium", "crates/libs/entity", "crates/libs/migration", @@ -12,7 +12,7 @@ members=[ ] [workspace.package] -authors = [ "Vasanth Kumar " ] +authors = ["Vasanth Kumar "] edition = "2021" version = "0.1.0" license = "MIT OR Apache-2.0" @@ -28,14 +28,14 @@ entity = { path = "crates/libs/entity", default-features = true } migration = { path = "crates/libs/migration", default-features = true } engine = { path = "crates/services/engine", default-features = true } api = { path = "crates/services/api", default-features = true } -chrome = { path = "crates/workshop/chrome", default-features = true } +#chrome = { path = "crates/workshop/chrome", default-features = true } entity_macro = { path = "crates/macro/entity-macro", default-features = true } thiserror = "1.0.31" jsonwebtoken = "9.2.0" -serde = { version = "1.0.147"} +serde = { version = "1.0.147" } serde_json = "1.0.87" -chrono = { version = "0.4.31"} +chrono = { version = "0.4.31" } tracing = "0.1.37" tracing-subscriber = "0.3.16" uuid = { version = "1.6.1", features = ["serde", "v4"] } @@ -47,28 +47,30 @@ rust_decimal = "1.14.3" cross-test = "0.1.6" sea-query = "0.30.5" -sea-orm = { version = "0.12.3", features = [ - "macros", - "debug-print", - "runtime-async-std-native-tls", - "sqlx-postgres", +sea-orm = { version = "0.12.3", features = [ + "macros", + "debug-print", + "runtime-async-std-native-tls", + "sqlx-postgres", ] } -sea-orm-migration = {version = "0.12.3", features = ["sqlx-postgres"]} +sea-orm-migration = { version = "0.12.3", features = ["sqlx-postgres"] } + axum = "0.7.1" axum-extra = "0.9.1" tokio = { version = "1.34.0", features = ["full"] } tower = "0.4.13" tower-http = { version = "0.5.0", default-features = true, features = ["uuid", "cors", "trace", "compression-br", "catch-panic", "request-id"] } +#tower_governor = "0.3.2" rust-s3 = "0.33.0" thirtyfour = "0.31.0" -geckodriver="0.34.0" +geckodriver = "0.34.0" [patch.crates-io] -sea-orm = { git="https://github.com/itsparser/sea-orm", branch = "master" } -sea-orm-migration = { git="https://github.com/itsparser/sea-orm", branch = "master" } +sea-orm = { git = "https://github.com/itsparser/sea-orm", branch = "master" } +sea-orm-migration = { git = "https://github.com/itsparser/sea-orm", branch = "master" } diff --git a/crates/libs/cerium/Cargo.toml b/crates/libs/cerium/Cargo.toml index d39643d..5e41f4d 100644 --- a/crates/libs/cerium/Cargo.toml +++ b/crates/libs/cerium/Cargo.toml @@ -25,29 +25,30 @@ default = [] #axum = ["dep:axum"] [dependencies] -entity.workspace=true - -tracing.workspace=true -tracing-subscriber.workspace=true -tokio.workspace=true -axum.workspace=true -axum-extra.workspace=true -tower.workspace=true -tower-http.workspace=true -config.workspace=true -sea-orm.workspace=true -sea-query.workspace=true -thirtyfour.workspace=true -chrono.workspace=true -jsonwebtoken.workspace=true -thiserror.workspace=true -serde.workspace=true -serde_json.workspace=true -rust-s3.workspace=true +entity.workspace = true + +tracing.workspace = true +tracing-subscriber.workspace = true +tokio.workspace = true +axum.workspace = true +axum-extra.workspace = true +tower.workspace = true +tower-http.workspace = true +config.workspace = true +sea-orm.workspace = true +sea-query.workspace = true +thirtyfour.workspace = true +chrono.workspace = true +jsonwebtoken.workspace = true +thiserror.workspace = true +serde.workspace = true +serde_json.workspace = true +rust-s3.workspace = true lazy_static = "1.4.0" async_once = "0.2.6" rand = { version = "0.8.5", default-features = false, features = ["std"] } log = "0.4.20" +backtrace = "0.3.71" diff --git a/crates/libs/cerium/src/client/driver/web.rs b/crates/libs/cerium/src/client/driver/web.rs index 8e203d5..31a8743 100644 --- a/crates/libs/cerium/src/client/driver/web.rs +++ b/crates/libs/cerium/src/client/driver/web.rs @@ -1,7 +1,7 @@ -use thirtyfour::{CapabilitiesHelper, WebDriver as TFWebDriver}; +use thirtyfour::{By, CapabilitiesHelper, DesiredCapabilities, WebElement}; +use thirtyfour::WebDriver as TFWebDriver; use crate::error::CeriumResult; -use thirtyfour::{By, DesiredCapabilities, WebElement}; #[derive(Clone)] pub struct WebDriver { @@ -30,13 +30,14 @@ impl WebDriver { let helper = WebDriver { driver }; Ok(helper) } - + pub async fn session_id(&self) -> CeriumResult { Ok(self.driver.session_id().await?.clone().to_string()) } pub async fn default() -> CeriumResult { let mut caps = DesiredCapabilities::firefox(); + caps.set_headless()?; caps.add("se:recordVideo", true)?; let driver = TFWebDriver::new("http://localhost:4444/wd/hub/session", caps).await?; Self::new(driver) diff --git a/crates/libs/cerium/src/error/mod.rs b/crates/libs/cerium/src/error/mod.rs index 963fa3f..255cc44 100644 --- a/crates/libs/cerium/src/error/mod.rs +++ b/crates/libs/cerium/src/error/mod.rs @@ -1,12 +1,15 @@ +use std::backtrace::Backtrace; + use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; use axum::Json; +use axum::response::{IntoResponse, Response}; use s3::creds::error::CredentialsError; use s3::error::S3Error; use sea_orm::DbErr; -use serde_json::{json, Error as SerdeJsonError}; +use serde_json::{Error as SerdeJsonError, json}; use thirtyfour::error::WebDriverError; use thiserror::Error; +use tracing::error; // pub use cerium::{CeriumError as OtherCeriumError, CeriumResult, ErrorResponse}; pub type CeriumResult = Result; @@ -29,7 +32,6 @@ pub enum CeriumError { #[error("CredentialsError error: {0}")] S3Error(#[from] S3Error), - } impl IntoResponse for CeriumError { @@ -45,6 +47,8 @@ impl IntoResponse for CeriumError { ), }; + error!("Error: {}", Backtrace::force_capture()); + let body = Json(json!({ "error": error_message, })); diff --git a/crates/libs/cerium/src/server/mod.rs b/crates/libs/cerium/src/server/mod.rs index 9d2cf9f..5e96eb3 100644 --- a/crates/libs/cerium/src/server/mod.rs +++ b/crates/libs/cerium/src/server/mod.rs @@ -1,4 +1,5 @@ use std::sync::{Arc, Mutex}; +use std::time::Duration; use axum::{Router, serve}; use axum::http::{HeaderName, Method}; @@ -9,11 +10,13 @@ use tower_http::{ compression::CompressionLayer, cors::{Any, CorsLayer}, }; +use tower_http::classify::ServerErrorsFailureClass; use tower_http::request_id::{PropagateRequestIdLayer, SetRequestIdLayer}; use tower_http::trace::TraceLayer; -use tracing::{info, Level}; -use tracing_subscriber::fmt; +use tracing::{error, info, Level, Span}; +use tracing_subscriber::{filter, fmt}; use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; use crate::client::Client; use crate::server::request_id::OrcaRequestId; @@ -69,8 +72,19 @@ impl App { /// let app = App::new("my_app", client); /// app.set_logger(Level::INFO); /// - pub fn set_logger(&self, filter: Level) { - fmt().with_max_level(filter).init() + pub fn set_logger(&self, level: Level) { + let filter = filter::Targets::new() + .with_target("tower_http::trace::on_response", Level::TRACE) + .with_target("tower_http::trace::on_request", Level::TRACE) + .with_target("tower_http::trace::on_failure", Level::TRACE) + .with_target("tower_http::trace::make_span", Level::DEBUG) + .with_default(level); + let tracing_layer = tracing_subscriber::fmt::layer(); + + tracing_subscriber::registry() + .with(tracing_layer) + .with(filter) + .init(); // .with(tracing_subscriber::fmt::layer()) // .with_target(true) // .with_timer(tracing_subscriber::fmt::time::uptime()) @@ -102,7 +116,16 @@ impl App { .layer(cors) .layer(CompressionLayer::new()) .layer(CatchPanicLayer::new()) - .layer(TraceLayer::new_for_http()); + .layer( + TraceLayer::new_for_http() + // .on_failure(|error: ServerErrorsFailureClass, latency: Duration, _span: &Span| { + // let mut error_msg = format!("something went wrong: {:?}", error); + // let backtrace = backtrace::Backtrace::new(); + // error_msg.push_str("\nBacktrace:\n"); + // // error_msg.push_str(&backtrace); + // error!("{:?} - {:?} - {:#?}", error_msg, latency, backtrace); + // }) + ); self.router = router } diff --git a/crates/libs/entity/Cargo.toml b/crates/libs/entity/Cargo.toml index cedd78d..d47c970 100644 --- a/crates/libs/entity/Cargo.toml +++ b/crates/libs/entity/Cargo.toml @@ -14,7 +14,10 @@ rust-version.workspace = true [dependencies] serde.workspace = true serde_json.workspace = true -sea-orm.workspace=true -sea-query.workspace=true -chrono.workspace=true -entity_macro.workspace = true \ No newline at end of file +sea-orm.workspace = true +sea-query.workspace = true +chrono.workspace = true +entity_macro.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +thiserror.workspace = true \ No newline at end of file diff --git a/crates/libs/entity/src/admin/user.rs b/crates/libs/entity/src/admin/user.rs index acf168f..7ba927f 100644 --- a/crates/libs/entity/src/admin/user.rs +++ b/crates/libs/entity/src/admin/user.rs @@ -9,15 +9,52 @@ use serde::{Deserialize, Serialize}; pub struct Model { #[serde(skip_deserializing)] #[sea_orm(primary_key)] - pub id: i32, + pub id: String, pub name: String, pub first_name: String, pub last_name: Option, + #[sea_orm(unique_key)] pub email: String, - pub profile_url: String, + pub profile_url: Option, + + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, + pub deleted_at: Option, + pub created_by: Option, + pub updated_by: Option, + } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(belongs_to = "Entity", from = "Column::CreatedBy", to = "Column::Id")] + CSelfReferencing, + #[sea_orm(belongs_to = "Entity", from = "Column::UpdatedBy", to = "Column::Id")] + USelfReferencing, +} + +pub struct SelfReferencingLink; + +impl Linked for SelfReferencingLink { + type FromEntity = Entity; + + type ToEntity = Entity; + + fn link(&self) -> Vec { + vec![Relation::CSelfReferencing.def()] + } +} + +pub struct USelfReferencingLink; + +impl Linked for USelfReferencingLink { + type FromEntity = Entity; + + type ToEntity = Entity; + + fn link(&self) -> Vec { + vec![Relation::USelfReferencing.def()] + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/libs/entity/src/app/app.rs b/crates/libs/entity/src/app/app.rs index e07b3b7..a00c65e 100644 --- a/crates/libs/entity/src/app/app.rs +++ b/crates/libs/entity/src/app/app.rs @@ -12,23 +12,13 @@ pub struct Model { pub id: Uuid, pub name: String, pub description: Option, - // pub created_by: Uuid, - // pub updated_by: Uuid, + + // pub created_by: String, + // pub updated_by: String, // pub created_at: DateTimeWithTimeZone, // pub updated_at: DateTimeWithTimeZone, } -// impl ActiveModelBehavior for ActiveModel { -// async fn before_save(mut self, _db: &C, _insert: bool) -> Result -// where -// C: ConnectionTrait, -// { -// self.updated_at = Set(Utc::now().into()); -// Ok(self) -// } -// -// } - #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} diff --git a/crates/libs/entity/src/ctx/mod.rs b/crates/libs/entity/src/ctx/mod.rs new file mode 100644 index 0000000..5c69f39 --- /dev/null +++ b/crates/libs/entity/src/ctx/mod.rs @@ -0,0 +1,3 @@ +pub trait RequestCtx { + fn get_user_id(&self) -> i32; +} diff --git a/crates/libs/entity/src/error.rs b/crates/libs/entity/src/error.rs new file mode 100644 index 0000000..09db44a --- /dev/null +++ b/crates/libs/entity/src/error.rs @@ -0,0 +1,10 @@ +use sea_orm::DbErr; +use thiserror::Error; + +pub type EntityResult = Result; + +#[derive(Error, Debug)] +pub enum EntityError { + #[error("Failed to convert Active Model : {0}")] + FailedActiveModelConvert(DbErr), +} \ No newline at end of file diff --git a/crates/libs/entity/src/lib.rs b/crates/libs/entity/src/lib.rs index af1d5ff..dceeeaa 100644 --- a/crates/libs/entity/src/lib.rs +++ b/crates/libs/entity/src/lib.rs @@ -10,3 +10,6 @@ pub mod prelude; pub mod test; pub mod account; pub mod api; +mod session; +mod error; +mod ctx; diff --git a/crates/libs/entity/src/prelude.rs b/crates/libs/entity/src/prelude.rs index a291633..b62c215 100644 --- a/crates/libs/entity/src/prelude.rs +++ b/crates/libs/entity/src/prelude.rs @@ -1,13 +1,43 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.6.0 -pub use super::common::attachment; +pub use super::{ + app::app::{ActiveModel as ActiveApplication, Entity as ApplicationEntity, Model as Application}, + test::ui::{ + action::action::{ActiveModel as ActiveAction, + Entity as ActionEntity, + Model as Action}, + action::group::{ActiveModel as ActiveActionGroup, + Entity as ActionGroupEntity, + Model as ActionGroup}, + case::case::{ActiveModel as ActiveCase, + Entity as CaseEntity, + Model as Case}, + case::case_block::{ActiveModel as ActiveCaseBlock, + Entity as CaseBlockEntity, + Model as CaseBlock}, + case::data_binding::{ActiveModel as ActiveDataBinding, + Entity as DataBindingEntity, + Model as DataBinding}, + log::item_log::{ActiveModel as ActiveItemLog, + Column as ItemLogColumn, + Entity as ItemLogEntity, ItemLogStatus, ItemLogType, Model as ItemLog}, + request::{ActiveModel as ActiveExecutionRequest, + Entity as ExecutionRequestEntity, + ExecutionKind, ExecutionStatus, ExecutionType, Model as ExecutionRequest}, + suit::suite_block::{ActiveModel as ActiveSuiteBlock, + Column as SuiteBlockColumn, + Entity as SuiteBlockEntity, + Model as SuiteBlock, SuiteBlockType}, + }, +}; pub use super::test::ui::{ action::{group, target}, case::{case, case_block, data_binding}, }; pub use super::test::ui::{ - action::{group::Entity as ActionGroup, target::Entity as ActionTarget}, + action::{group::Entity as ActionGroupE, target::Entity as ActionTarget}, case::{ - case::Entity as Case, case_block::Entity as CaseBlock, data_binding::Entity as DataBinding, + case::Entity, case_block::Entity as CaseBlockE, data_binding::Entity as DataBindingE, }, }; + diff --git a/crates/libs/entity/src/session.rs b/crates/libs/entity/src/session.rs new file mode 100644 index 0000000..6e966c9 --- /dev/null +++ b/crates/libs/entity/src/session.rs @@ -0,0 +1,51 @@ +use sea_orm::ActiveModelTrait; +use sea_orm::DatabaseTransaction; +use sea_orm::prelude::async_trait::async_trait; + +use crate::error::EntityResult; + +#[derive(Clone)] +pub struct Session(DatabaseTransaction, String); + +impl Session { + pub fn new(trx: DatabaseTransaction, user_id: String) -> Self { + Self(trx, user_id) + } + pub fn trx(&self) -> &DatabaseTransaction { + &self.0 + } + + /// user_id - returns the user_id + pub fn user_id(&self) -> &str { + &self.1 + } +} + +#[async_trait] +pub trait SessionLayer: ActiveModelTrait { + fn update_audit(&mut self, user_id: String) -> EntityResult<()>; + + fn create_audit(&mut self, user_id: String) -> EntityResult<()>; + + fn delete_audit(&mut self, user_id: String) -> EntityResult<()> { + Ok(()) + } + + // async fn ssave<'a, 'b>(&'a mut self, session: &'b Session) -> Result + // where + // ::Model: IntoActiveModel, + // Self: ActiveModelBehavior + 'a, + // { + // debug!("Saving the session layer"); + // let c = self.update_audit(session.user_id().to_string())?; + // Ok(self.save(session.trx()).await?) + // } + + // fn force_save(&self) -> EntityResult<()> { + // debug!("!! Force Saving the session layer"); + // let model = self.try_into_model() + // .unwrap_or_else(); + // // model.save().map_err(|e| EntityError::FailedActiveModelConvert(e))?; + // Ok(()) + // } +} diff --git a/crates/libs/entity/src/test/ui/log/item_log.rs b/crates/libs/entity/src/test/ui/log/item_log.rs index fc566f5..61c11f5 100644 --- a/crates/libs/entity/src/test/ui/log/item_log.rs +++ b/crates/libs/entity/src/test/ui/log/item_log.rs @@ -26,17 +26,19 @@ pub enum ItemLogStatus { #[sea_orm(rs_type = "String", db_type = "String(Some(5))", enum_name = "item_log_type")] pub enum ItemLogType { #[sea_orm(string_value = "A")] - #[serde(rename = "Action")] Action, #[sea_orm(string_value = "AG")] - #[serde(rename = "ActionGroup")] ActionGroup, #[sea_orm(string_value = "AS")] - #[serde(rename = "Assertion")] Assertion, + #[sea_orm(string_value = "TCB")] + TestCaseBlock, #[sea_orm(string_value = "TC")] - #[serde(rename = "TestCase")] TestCase, + #[sea_orm(string_value = "TS")] + TestSuite, + #[sea_orm(string_value = "TSB")] + TestSuiteBlock, } #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] @@ -45,10 +47,11 @@ pub struct Model { #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: i32, - pub ref_id: Uuid, + pub step_id: Uuid, + pub ref_id: Option, pub ref_type: ItemLogType, - pub step_id: Uuid, + pub er_id: i32, pub has_screenshot: bool, pub has_recording: bool, pub execution_time: i32, @@ -59,23 +62,34 @@ pub struct Model { pub finished_at: DateTimeWithTimeZone, } -pub fn new(ref_id: Uuid, ref_type: ItemLogType, step_id: Uuid, log_id: Option) -> ActiveModel { - ActiveModel { - id: Default::default(), - ref_id: Set(ref_id), - ref_type: Set(ref_type), - step_id: Set(step_id), - has_screenshot: Set(false), - has_recording: Set(false), - execution_time: Set(0), - status: Set(ItemLogStatus::Running), - log_id: Set(log_id), - created_at: Set(chrono::Utc::now().into()), - created_by: Set("System".to_string()), - finished_at: Set(chrono::Utc::now().into()), +impl ActiveModel { + pub fn new(er_id: i32, ref_id: Option, ref_type: ItemLogType, step_id: Uuid, log_id: Option) -> ActiveModel { + ActiveModel { + id: Default::default(), + er_id: Set(er_id), + ref_id: Set(ref_id), + ref_type: Set(ref_type), + step_id: Set(step_id), + has_screenshot: Set(false), + has_recording: Set(false), + execution_time: Set(0), + status: Set(ItemLogStatus::Running), + log_id: Set(log_id), + created_at: Set(chrono::Utc::now().into()), + created_by: Set("System".to_string()), + finished_at: Set(chrono::Utc::now().into()), + } } -} + pub async fn save_status<'a, C>(mut self, db: &'a C, status: ItemLogStatus) -> Result + where + Self: ActiveModelBehavior + 'a, + C: ConnectionTrait, + { + self.status = Set(status); + Ok(self.save(db).await?) + } +} #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { diff --git a/crates/libs/entity/src/test/ui/mod.rs b/crates/libs/entity/src/test/ui/mod.rs index 101dab2..a16025c 100644 --- a/crates/libs/entity/src/test/ui/mod.rs +++ b/crates/libs/entity/src/test/ui/mod.rs @@ -1,4 +1,6 @@ -pub use request::Model as ExecutionRequest; +use sea_orm::{ActiveModelTrait, EntityTrait, ModelTrait, TryIntoModel}; + +pub use request::{Column as ExecutionRequestColumn, Entity as ExecutionRequestEntity, Model as ExecutionRequest}; pub mod action; pub mod case; @@ -8,5 +10,3 @@ pub mod suit; pub mod object_repository; pub mod log; pub mod request; - - diff --git a/crates/libs/entity/src/test/ui/request.rs b/crates/libs/entity/src/test/ui/request.rs index ab585da..e9113c5 100644 --- a/crates/libs/entity/src/test/ui/request.rs +++ b/crates/libs/entity/src/test/ui/request.rs @@ -27,12 +27,15 @@ db_type = "String(Some(10))", enum_name = "execution_type" )] pub enum ExecutionType { - #[sea_orm(string_value = "TestCase")] - #[serde(rename = "TestCase")] + #[sea_orm(string_value = "TC")] + #[serde(rename = "TC")] TestCase, - #[sea_orm(string_value = "TestSuite")] - #[serde(rename = "TestSuite")] + #[sea_orm(string_value = "TS")] + #[serde(rename = "TS")] TestSuite, + #[sea_orm(string_value = "AG")] + #[serde(rename = "AG")] + ActionGroup, } #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize)] @@ -78,32 +81,60 @@ pub struct Model { pub updated_at: DateTimeWithTimeZone, } -pub fn new( - ref_id: Uuid, - ref_type: ExecutionType, - kind: ExecutionKind, - status: ExecutionStatus, - log_id: i32, - is_dry_run: bool, - desc: Option, -) -> ActiveModel { - ActiveModel { - id: Default::default(), - description: Set(desc), - is_dry_run: Set(is_dry_run), - ref_id: Set(ref_id), - ref_type: Set(ref_type), - kind: Set(kind), - status: Set(status), - args: NotSet, - log_id: Set(log_id), - created_at: Set(chrono::Utc::now().into()), - created_by: Set("System".to_string()), - finished_at: Set(chrono::Utc::now().into()), - updated_at: Set(chrono::Utc::now().into()), +impl ActiveModel { + pub fn new( + ref_id: Uuid, + ref_type: ExecutionType, + kind: ExecutionKind, + status: ExecutionStatus, + log_id: i32, + is_dry_run: bool, + desc: Option, + ) -> Self { + Self { + id: Default::default(), + description: Set(desc), + is_dry_run: Set(is_dry_run), + ref_id: Set(ref_id), + ref_type: Set(ref_type), + kind: Set(kind), + status: Set(status), + args: NotSet, + log_id: Set(log_id), + created_at: Set(chrono::Utc::now().into()), + created_by: Set("System".to_string()), + finished_at: Set(chrono::Utc::now().into()), + updated_at: Set(chrono::Utc::now().into()), + } } } +// pub fn new( +// ref_id: Uuid, +// ref_type: ExecutionType, +// kind: ExecutionKind, +// status: ExecutionStatus, +// log_id: i32, +// is_dry_run: bool, +// desc: Option, +// ) -> ActiveModel { +// ActiveModel { +// id: Default::default(), +// description: Set(desc), +// is_dry_run: Set(is_dry_run), +// ref_id: Set(ref_id), +// ref_type: Set(ref_type), +// kind: Set(kind), +// status: Set(status), +// args: NotSet, +// log_id: Set(log_id), +// created_at: Set(chrono::Utc::now().into()), +// created_by: Set("System".to_string()), +// finished_at: Set(chrono::Utc::now().into()), +// updated_at: Set(chrono::Utc::now().into()), +// } +// } + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} diff --git a/crates/libs/entity/src/test/ui/suit/suite_block.rs b/crates/libs/entity/src/test/ui/suit/suite_block.rs index b5a8a9e..5098961 100644 --- a/crates/libs/entity/src/test/ui/suit/suite_block.rs +++ b/crates/libs/entity/src/test/ui/suit/suite_block.rs @@ -6,9 +6,9 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize)] #[sea_orm( - rs_type = "String", - db_type = "String(Some(15))", - enum_name = "block_kind" +rs_type = "String", +db_type = "String(Some(15))", +enum_name = "block_kind" )] pub enum BlockKind { #[sea_orm(string_value = "Loop")] @@ -23,9 +23,9 @@ pub enum BlockKind { #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize)] #[sea_orm( - rs_type = "String", - db_type = "String(Some(15))", - enum_name = "suit_block_type" +rs_type = "String", +db_type = "String(Some(15))", +enum_name = "suit_block_type" )] pub enum SuiteBlockType { #[sea_orm(string_value = "TestCase")] @@ -35,6 +35,7 @@ pub enum SuiteBlockType { #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] #[sea_orm(table_name = "suite_block")] pub struct Model { + #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: Uuid, #[serde(skip_deserializing)] @@ -43,6 +44,11 @@ pub struct Model { pub type_field: SuiteBlockType, pub reference: Option, + #[sea_orm(ignore)] + pub name: Option, + #[sea_orm(ignore)] + pub description: Option, + #[serde(skip_deserializing)] pub suite_id: Uuid, } @@ -52,9 +58,9 @@ pub enum Relation { // #[sea_orm(belongs_to = "Entity", from = "Column::Id", to = "Column::ParentId")] // SelfReferencing, #[sea_orm( - belongs_to = "super::suite::Entity", - from = "Column::SuiteId", - to = "super::suite::Column::Id" + belongs_to = "super::suite::Entity", + from = "Column::SuiteId", + to = "super::suite::Column::Id" )] Suite, } diff --git a/crates/libs/migration/src/migration001.rs b/crates/libs/migration/src/migration001.rs index fc3c2f1..cb0f8ce 100644 --- a/crates/libs/migration/src/migration001.rs +++ b/crates/libs/migration/src/migration001.rs @@ -30,15 +30,35 @@ impl MigrationTrait for Migration { .if_not_exists() .col( ColumnDef::new(user::Column::Id) - .integer() - .auto_increment() + .string() .not_null() .primary_key(), ) .col(ColumnDef::new(user::Column::Name).string().not_null()) .col(ColumnDef::new(user::Column::FirstName).string().not_null()) .col(ColumnDef::new(user::Column::LastName).string()) - .col(ColumnDef::new(user::Column::Email).string().not_null()) + .col(ColumnDef::new(user::Column::Email).string().unique_key().not_null()) + .col(ColumnDef::new(user::Column::ProfileUrl).string()) + + .col(ColumnDef::new(user::Column::CreatedAt).timestamp_with_time_zone().default(Expr::current_timestamp()).not_null()) + .col(ColumnDef::new(user::Column::UpdatedAt).timestamp_with_time_zone().default(Expr::current_timestamp()).not_null()) + .col(ColumnDef::new(user::Column::DeletedAt).timestamp_with_time_zone()) + .col(ColumnDef::new(user::Column::CreatedBy).string()) + .col(ColumnDef::new(user::Column::UpdatedBy).string()) + .foreign_key( + ForeignKey::create() + .from(user::Entity, user::Column::CreatedBy) + .to(user::Entity, user::Column::Id) + .on_delete(ForeignKeyAction::NoAction) + .on_update(ForeignKeyAction::NoAction), + ) + .foreign_key( + ForeignKey::create() + .from(user::Entity, user::Column::UpdatedBy) + .to(user::Entity, user::Column::Id) + .on_delete(ForeignKeyAction::NoAction) + .on_update(ForeignKeyAction::NoAction), + ) .to_owned(), ) .await?; @@ -449,7 +469,7 @@ impl MigrationTrait for Migration { .foreign_key( ForeignKey::create() .from(suite_block::Entity, suite_block::Column::SuiteId) - .to(case::Entity, case::Column::Id) + .to(suite::Entity, suite::Column::Id) .on_delete(ForeignKeyAction::Cascade) .on_update(ForeignKeyAction::Cascade), ) @@ -469,10 +489,14 @@ impl MigrationTrait for Migration { .primary_key().auto_increment(), ) .col( - ColumnDef::new(item_log::Column::RefId) - .uuid() + ColumnDef::new(item_log::Column::ErId) + .integer() .not_null(), ) + .col( + ColumnDef::new(item_log::Column::RefId) + .uuid(), + ) .col( ColumnDef::new(item_log::Column::RefType) .string() diff --git a/crates/libs/migration/src/migration002.rs b/crates/libs/migration/src/migration002.rs index 3436d2f..4084e52 100644 --- a/crates/libs/migration/src/migration002.rs +++ b/crates/libs/migration/src/migration002.rs @@ -3,17 +3,18 @@ use std::vec; use sea_orm_migration::prelude::*; use sea_orm_migration::sea_orm::ActiveValue::Set; +use entity::admin::user::Model; use entity::app::app; +use entity::prelude::{case, case_block}; use entity::prelude::case_block::{BlockKind, BlockType}; use entity::prelude::group::ActionGroupKind; use entity::prelude::target::ActionTargetKind; -use entity::prelude::{case, case_block}; +use entity::test::ui::action::{action, group as action_group}; use entity::test::ui::action::action::ActionKind; use entity::test::ui::action::data::ActionDataKind; -use entity::test::ui::action::{action, group as action_group}; -use crate::sea_orm::prelude::Uuid; use crate::sea_orm::{ActiveModelTrait, EntityTrait, InsertResult}; +use crate::sea_orm::prelude::Uuid; #[derive(DeriveMigrationName)] pub struct Migration; @@ -24,6 +25,33 @@ pub struct Migration; impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { let db = manager.get_connection(); + + /// create Seed User for Admin user + let user_am = entity::admin::user::ActiveModel { + id: Set("system".to_string()), + name: Set("Bot User".to_string()), + first_name: Set("Bot User".to_string()), + email: Set("system@orcaci.dev".to_string()), + created_at: Default::default(), + updated_at: Default::default(), + created_by: Set(Some("system".to_string())), + updated_by: Set(Some("system".to_string())), + ..Default::default() + }; + let _usr: Model = user_am.insert(db).await?; + + /// create Seed User for Admin user + let user_am = entity::admin::user::ActiveModel { + id: Set("user1".to_string()), + name: Set("user1".to_string()), + first_name: Set("user1".to_string()), + email: Set("user1@orcaci.dev".to_string()), + created_by: Set(Some("system".to_string())), + updated_by: Set(Some("system".to_string())), + ..Default::default() + }; + let usr: Model = user_am.insert(db).await?; + let app_am = app::ActiveModel { id: Set(Uuid::new_v4()), name: Set("Wikipedia Testing".to_string()), @@ -54,8 +82,8 @@ impl MigrationTrait for Migration { action_group_id: Set(app_g.clone().id), ..Default::default() } - .insert(db) - .await?, + .insert(db) + .await?, action::ActiveModel { id: Set(Uuid::new_v4()), description: Set(Some("Search for Ana de Armas".to_string())), @@ -68,8 +96,8 @@ impl MigrationTrait for Migration { action_group_id: Set(app_g.clone().id), ..Default::default() } - .insert(db) - .await?, + .insert(db) + .await?, action::ActiveModel { id: Set(Uuid::new_v4()), description: Set(Some("Search".to_string())), @@ -82,8 +110,8 @@ impl MigrationTrait for Migration { action_group_id: Set(app_g.clone().id), ..Default::default() } - .insert(db) - .await?, + .insert(db) + .await?, ]; // let _action_m: InsertResult = // action::Entity::insert_many(action_ms).exec(db).await?; @@ -126,8 +154,8 @@ impl MigrationTrait for Migration { }; let case_m: case::Model = case_am.insert(db).await?; let uuid1 = Uuid::new_v4(); - let uuid2 = Uuid::new_v4(); - let uuid3 = Uuid::new_v4(); + let uuid2 = Uuid::new_v4(); + let uuid3 = Uuid::new_v4(); let case_blocks = vec![ case_block::ActiveModel { @@ -140,75 +168,74 @@ impl MigrationTrait for Migration { reference: Set(Some(app_g.clone().id)), case_id: Set(case_m.clone().id), ..Default::default() - } - .insert(db) - .await?, - case_block::ActiveModel { - id: Set(Uuid::new_v4()), - execution_order: Set(2), - kind: Set(BlockKind::Reference), - type_field: Set(BlockType::Assertion), - name: Set(Some(assert_g_m.name.clone())), - desc: Set(assert_g_m.description.clone()), - reference: Set(Some(assert_g_m.clone().id)), - case_id: Set(case_m.clone().id), - ..Default::default() - } - .insert(db) - .await?, - case_block::ActiveModel { - id: Set(uuid1.clone()), - execution_order: Set(3), - kind: Set(BlockKind::SelfReference), - name: Set(Some("This is a condition block".to_string())), - desc: Set(Some("This is a condition block".to_string())), - type_field: Set(BlockType::Condition), - case_id: Set(case_m.clone().id), - ..Default::default() - } - .insert(db) - .await?, - case_block::ActiveModel { - id: Set(uuid2.clone()), - execution_order: Set(1), - kind: Set(BlockKind::SelfReference), - name: Set(Some("This is yes block".to_string())), - desc: Set(Some("This is yes condition block".to_string())), - type_field: Set(BlockType::YesCase), - parent_id: Set(Some(uuid1.clone())), - case_id: Set(case_m.clone().id), - ..Default::default() - } - .insert(db) - .await?, - case_block::ActiveModel { - id: Set(uuid3.clone()), - execution_order: Set(2), - kind: Set(BlockKind::SelfReference), - name: Set(Some("This is no block".to_string())), - desc: Set(Some("This is no condition block".to_string())), - type_field: Set(BlockType::NoCase), - parent_id: Set(Some(uuid1.clone())), - case_id: Set(case_m.clone().id), - ..Default::default() - } - .insert(db) - .await?, - - case_block::ActiveModel { - id: Set(Uuid::new_v4()), - execution_order: Set(1), - kind: Set(BlockKind::Reference), - name: Set(Some(app_g.name.clone())), - desc: Set(app_g.description.clone()), - type_field: Set(BlockType::ActionGroup), - reference: Set(Some(app_g.clone().id)), - parent_id: Set(Some(uuid2.clone())), - case_id: Set(case_m.clone().id), - ..Default::default() } .insert(db) .await?, + // case_block::ActiveModel { + // id: Set(Uuid::new_v4()), + // execution_order: Set(2), + // kind: Set(BlockKind::Reference), + // type_field: Set(BlockType::Assertion), + // name: Set(Some(assert_g_m.name.clone())), + // desc: Set(assert_g_m.description.clone()), + // reference: Set(Some(assert_g_m.clone().id)), + // case_id: Set(case_m.clone().id), + // ..Default::default() + // } + // .insert(db) + // .await?, + // case_block::ActiveModel { + // id: Set(uuid1.clone()), + // execution_order: Set(3), + // kind: Set(BlockKind::SelfReference), + // name: Set(Some("This is a condition block".to_string())), + // desc: Set(Some("This is a condition block".to_string())), + // type_field: Set(BlockType::Condition), + // case_id: Set(case_m.clone().id), + // ..Default::default() + // } + // .insert(db) + // .await?, + // case_block::ActiveModel { + // id: Set(uuid2.clone()), + // execution_order: Set(1), + // kind: Set(BlockKind::SelfReference), + // name: Set(Some("This is yes block".to_string())), + // desc: Set(Some("This is yes condition block".to_string())), + // type_field: Set(BlockType::YesCase), + // parent_id: Set(Some(uuid1.clone())), + // case_id: Set(case_m.clone().id), + // ..Default::default() + // } + // .insert(db) + // .await?, + // case_block::ActiveModel { + // id: Set(uuid3.clone()), + // execution_order: Set(2), + // kind: Set(BlockKind::SelfReference), + // name: Set(Some("This is no block".to_string())), + // desc: Set(Some("This is no condition block".to_string())), + // type_field: Set(BlockType::NoCase), + // parent_id: Set(Some(uuid1.clone())), + // case_id: Set(case_m.clone().id), + // ..Default::default() + // } + // .insert(db) + // .await?, + // case_block::ActiveModel { + // id: Set(Uuid::new_v4()), + // execution_order: Set(1), + // kind: Set(BlockKind::Reference), + // name: Set(Some(app_g.name.clone())), + // desc: Set(app_g.description.clone()), + // type_field: Set(BlockType::ActionGroup), + // reference: Set(Some(app_g.clone().id)), + // parent_id: Set(Some(uuid2.clone())), + // case_id: Set(case_m.clone().id), + // ..Default::default() + // } + // .insert(db) + // .await?, ]; Ok(()) } diff --git a/crates/services/api/Cargo.toml b/crates/services/api/Cargo.toml index cae79b0..a6b716d 100644 --- a/crates/services/api/Cargo.toml +++ b/crates/services/api/Cargo.toml @@ -13,29 +13,29 @@ exclude.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -serde.workspace=true -serde_json.workspace=true -tracing.workspace=true -tracing-subscriber.workspace=true -jsonwebtoken.workspace=true -tower.workspace=true -tower-http.workspace=true -uuid.workspace=true -futures.workspace=true -futures-util.workspace=true -config.workspace=true -chrono.workspace=true - -sea-orm.workspace =true -sea-query.workspace=true -sea-orm-migration.workspace=true -axum.workspace=true - -cerium.workspace=true -engine.workspace=true -migration.workspace=true -entity.workspace=true -thiserror.workspace=true +serde.workspace = true +serde_json.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +jsonwebtoken.workspace = true +tower.workspace = true +tower-http.workspace = true +uuid.workspace = true +futures.workspace = true +futures-util.workspace = true +config.workspace = true +chrono.workspace = true + +sea-orm.workspace = true +sea-query.workspace = true +sea-orm-migration.workspace = true +axum.workspace = true + +cerium.workspace = true +engine.workspace = true +migration.workspace = true +entity.workspace = true +thiserror.workspace = true async-recursion = "1.0.5" diff --git a/crates/services/api/src/error.rs b/crates/services/api/src/error.rs index 0e31c49..8afbc04 100644 --- a/crates/services/api/src/error.rs +++ b/crates/services/api/src/error.rs @@ -1,11 +1,15 @@ +use std::backtrace::Backtrace; + use axum::http::StatusCode; -use axum::response::{IntoResponse, Response}; use axum::Json; -use cerium::error::CeriumError; -use engine::error::EngineError; +use axum::response::{IntoResponse, Response}; use sea_orm::DbErr; -use serde_json::{json, Error as SerdeJsonError}; +use serde_json::{Error as SerdeJsonError, json}; use thiserror::Error; +use tracing::error; + +use cerium::error::CeriumError; +use engine::error::EngineError; use crate::error::OrcaError::RepoError; @@ -19,14 +23,13 @@ pub enum OrcaRepoError { #[error("{0} Not Found: {1}")] ModelNotFound(String, String), #[error("Invalid UserName: {0}")] - InvalidUsername(i32), + InvalidUsername(String), } /// Our app's top level error type. #[derive(Error, Debug)] pub enum OrcaError { /// Something went wrong when calling the user repo. - #[error("DbErr error: {0}")] DataBaseError(#[from] DbErr), @@ -43,46 +46,6 @@ pub enum OrcaError { CeriumError(#[from] CeriumError), } -// /// This makes it possible to use `?` to automatically convert a `DbErr` -// /// into an `OrcaError`. -// impl From for OrcaError { -// fn from(inner: OrcaRepoError) -> Self { -// OrcaError::RepoError(inner) -// } -// } -// -// /// This makes it possible to use `?` to automatically convert a `DbErr` -// /// into an `OrcaError`. -// impl From for OrcaError { -// fn from(inner: DbErr) -> Self { -// OrcaError::DataBaseError(inner) -// } -// } -// -// /// This makes it possible to use `?` to automatically convert a `EngineError` -// /// into an `OrcaError`. -// impl From for OrcaError { -// fn from(inner: EngineError) -> Self { -// OrcaError::EngineError(inner) -// } -// } -// -// /// This makes it possible to use `?` to automatically convert a `CeriumError` -// /// into an `OrcaError`. -// impl From for OrcaError { -// fn from(inner: CeriumError) -> Self { -// OrcaError::CeriumError(inner) -// } -// } - -// /// This makes it possible to use `?` to automatically convert a `DbErr` -// /// into an `OrcaError`. -// impl From for OrcaError { -// fn from(inner: SerdeJsonError) -> Self { -// OrcaError::SerializerError(inner) -// } -// } - impl IntoResponse for OrcaError { fn into_response(self) -> Response { let (status, error_message) = match self { @@ -96,7 +59,7 @@ impl IntoResponse for OrcaError { "Internal Error Not Specify".to_string(), ), }; - + error!("Error: {}", Backtrace::force_capture()); let body = Json(json!({ "error": error_message, })); diff --git a/crates/services/api/src/route/admin/user.rs b/crates/services/api/src/route/admin/user.rs index ce5aca0..dc66c38 100644 --- a/crates/services/api/src/route/admin/user.rs +++ b/crates/services/api/src/route/admin/user.rs @@ -1,8 +1,8 @@ +use axum::{Extension, Json, Router}; use axum::extract::{Path, Query}; use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::{get, post}; -use axum::{Extension, Json, Router}; use sea_orm::ActiveValue::Set; use sea_orm::IntoActiveModel; use serde_json::json; @@ -50,7 +50,7 @@ async fn list_user( /// get_user - this will get User by ID in Orca async fn get_user_by_id( Extension(session): Extension, - Path(user_id): Path, + Path(user_id): Path, ) -> InternalResult { let result = UserService::new(session).get_user_by_id(user_id).await?; Ok(Json(result)) @@ -59,11 +59,11 @@ async fn get_user_by_id( /// update_user_by_id - update user by user ID in Orca async fn update_user_by_id( Extension(session): Extension, - Path(user_id): Path, + Path(user_id): Path, Json(body): Json, ) -> InternalResult { let mut _user = body.clone().into_active_model(); - _user.id = Set(user_id); + _user.id = Set(user_id.clone()); let result = UserService::new(session).update_user(_user).await?; info!("User Got Updated - {:?}", user_id); Ok(Json(result)) @@ -72,7 +72,7 @@ async fn update_user_by_id( /// delete_user_by_id - delete user by User by ID in Orca async fn delete_user_by_id( Extension(session): Extension, - Path(user_id): Path, + Path(user_id): Path, ) -> InternalResult { UserService::new(session).delete_user_by_id(user_id).await?; Ok(Json(json!({"status": "success"}))) diff --git a/crates/services/api/src/route/app/history.rs b/crates/services/api/src/route/app/history.rs index 3f6ed13..c78014c 100644 --- a/crates/services/api/src/route/app/history.rs +++ b/crates/services/api/src/route/app/history.rs @@ -4,6 +4,8 @@ use axum::response::IntoResponse; use axum::routing::get; use uuid::Uuid; +use entity::prelude::ItemLogType; + use crate::error::InternalResult; use crate::server::session::OrcaSession; use crate::service::app::history::HistoryService; @@ -11,6 +13,9 @@ use crate::service::app::history::HistoryService; /// history_route - this will register all the endpoint in Execution history route pub(crate) fn history_route() -> Router { Router::new() + .nest("/:history_id", Router::new() + .route("/log", get(by_id)).route("/log/:log_type/:log_id/blocks", get(log_by_id)), + ) .route("/", get(get_history)) } @@ -22,3 +27,24 @@ async fn get_history( let result = HistoryService::new(session).list_history().await?; Ok(Json(result)) } + + +/// by_id - list all the Log list in Specific Application in the Orca Application +async fn by_id( + Extension(session): Extension, + Path((app_id, history_id)): Path<(Uuid, i32)>, +) -> InternalResult { + let result = HistoryService::new(session).by_id(history_id).await?; + Ok(Json(result)) +} + + +/// log_by_id - list all the Log list in Specific Application in the Orca Application +async fn log_by_id( + Extension(session): Extension, + Path((app_id, history_id, log_type, log_id)): Path<(Uuid, i32, ItemLogType, Uuid)>, +) -> InternalResult { + let result = HistoryService::new(session).log_by_id(history_id, log_type, log_id).await?; + Ok(Json(result)) +} + diff --git a/crates/services/api/src/route/app/suit.rs b/crates/services/api/src/route/app/suit.rs index 312cedd..0a7a683 100644 --- a/crates/services/api/src/route/app/suit.rs +++ b/crates/services/api/src/route/app/suit.rs @@ -2,9 +2,10 @@ use axum::{Extension, Json, Router}; use axum::extract::Path; use axum::http::StatusCode; use axum::response::IntoResponse; -use axum::routing::{get, post}; +use axum::routing::{delete, get, post}; use uuid::Uuid; +use cerium::client::Client; use entity::test::ui::suit::suite::Model; use entity::test::ui::suit::suite_block::Model as BlockModel; @@ -19,14 +20,24 @@ pub(crate) fn suite_route() -> Router { .nest( "/:suite_id", Router::new() - .route("/batch/update", post(update_block)) + .route("/batch", post(update_block)) + .route("/", delete(delete_suite)) + .route("/dryrun", post(dry_run)) .nest( "/block", - Router::new().route("/", get(get_suite_info).post(insert_block)), + Router::new() + .route("/", get(get_suite_info).post(insert_block)) + .route("/:block_id", delete(delete_block)) + .route("/:block_id/reorder", post(reorder_block)), ), ) } +#[derive(Debug, serde::Deserialize)] +struct ReorderBlock { + location: i32, +} + /// list_suites - list all the Suites that is Bind with Current Application async fn list_suites( Extension(session): Extension, @@ -46,6 +57,15 @@ async fn create_suite( Ok((StatusCode::CREATED, Json(result))) } +/// delete_suite - This will delete the Suite for the specific Application in Orca +async fn delete_suite( + Extension(session): Extension, + Path((app_id, suite_id)): Path<(Uuid, Uuid)>, +) -> InternalResult { + let result = SuitService::new(session, app_id).delete(suite_id).await?; + Ok((StatusCode::OK, Json(result))) +} + /// get_suits_info - Get Suite Info and the batch information with the list of block async fn get_suite_info( Extension(session): Extension, @@ -69,6 +89,30 @@ async fn insert_block( Ok(Json(result)) } +/// delete_block - This will Append New Block to the code for spe +async fn delete_block( + Extension(session): Extension, + Path((app_id, suite_id, block_id)): Path<(Uuid, Uuid, Uuid)>, +) -> InternalResult { + let result = SuitService::new(session, app_id) + .delete_block(block_id) + .await?; + Ok(Json(result)) +} + + +/// reorder_block - this will reorder the block to new location +async fn reorder_block( + Extension(session): Extension, + Path((app_id, suite_id, block_id)): Path<(Uuid, Uuid, Uuid)>, + Json(body): Json, +) -> InternalResult { + let result = SuitService::new(session, app_id) + .reorder_block(block_id, body.location) + .await?; + Ok(Json(result)) +} + /// update_block - update suite Block async fn update_block( Extension(session): Extension, @@ -80,3 +124,14 @@ async fn update_block( .await?; Ok(Json(result)) } + + +/// dry_run - Dry run the TestSuite +async fn dry_run( + Extension(session): Extension, + Extension(cli): Extension, + Path((app_id, suite_id)): Path<(Uuid, Uuid)>, +) -> InternalResult { + let result = SuitService::new(session, app_id).run(cli, suite_id).await?; + Ok(Json(result)) +} diff --git a/crates/services/api/src/server/middleware/mod.rs b/crates/services/api/src/server/middleware/mod.rs index 47964fe..45da330 100644 --- a/crates/services/api/src/server/middleware/mod.rs +++ b/crates/services/api/src/server/middleware/mod.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use std::task::{Context, Poll}; -use crate::server::session::OrcaSession; use axum::{async_trait, extract::Request, response::Response}; use futures::executor::block_on; use futures_util::future::BoxFuture; use sea_orm::{DatabaseConnection, TransactionTrait}; use tower::{Layer, Service}; -use tracing::info; +use tracing::{debug, info}; + +use crate::server::session::OrcaSession; pub mod orca; @@ -35,9 +36,9 @@ pub struct OrcaMiddleware { #[async_trait] impl Service for OrcaMiddleware -where - S: Service + Send + 'static, - S::Future: Send + 'static, + where + S: Service + Send + 'static, + S::Future: Send + 'static, { type Response = S::Response; type Error = S::Error; @@ -50,16 +51,16 @@ where fn call(&mut self, mut request: Request) -> Self::Future { let ext = request.extensions_mut(); - info!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>--------------BEFORE REQUEST------------------>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + info!(">>>>>>>>>--------------BEFORE REQUEST------------------>>>>>>>>>"); let trx = block_on(self.db.begin()).expect("got error on trx"); - ext.insert(OrcaSession::new(trx.clone())); + ext.insert(OrcaSession::new(trx.clone(), "system".to_string())); let future = self.inner.call(request); Box::pin(async move { let mut response: Response = future.await?; let headers = response.headers_mut(); trx.commit().await.expect("TODO: panic message"); - info!("headers - {:?}", headers); - info!(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>--------------AFTER REQUEST------------------>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); + debug!("headers - {:?}", headers); + info!(">>>>>>>>>-------------AFTER REQUEST------------->>>>>>>>>"); Ok(response) }) } diff --git a/crates/services/api/src/server/session.rs b/crates/services/api/src/server/session.rs index 31d374d..6327e42 100644 --- a/crates/services/api/src/server/session.rs +++ b/crates/services/api/src/server/session.rs @@ -1,14 +1,18 @@ -use std::sync::Arc; use sea_orm::DatabaseTransaction; #[derive(Clone)] -pub struct OrcaSession(DatabaseTransaction); +pub struct OrcaSession(DatabaseTransaction, String); impl OrcaSession { - pub fn new(trx: DatabaseTransaction) -> Self { - OrcaSession(trx) + pub fn new(trx: DatabaseTransaction, user_id: String) -> Self { + OrcaSession(trx, user_id) } pub fn trx(&self) -> &DatabaseTransaction { &self.0 } + + /// user_id - returns the user_id + pub fn user_id(&self) -> &str { + &self.1 + } } diff --git a/crates/services/api/src/service/admin/auth.rs b/crates/services/api/src/service/admin/auth.rs index 77613e3..23f7bba 100644 --- a/crates/services/api/src/service/admin/auth.rs +++ b/crates/services/api/src/service/admin/auth.rs @@ -1,11 +1,11 @@ +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter}; +use sea_query::Condition; + +use entity::admin::user; +use entity::admin::user::{Column as UserColumn, Model}; + use crate::error::{InternalResult, OrcaRepoError}; -use crate::route::Pagination; use crate::server::session::OrcaSession; -use entity::admin::user; -use entity::admin::user::{ActiveModel, Column as UserColumn, Model}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, NotSet, QueryFilter, QuerySelect, TryIntoModel}; -use sea_query::Condition; -use tracing::info; pub(crate) struct AuthService(OrcaSession); @@ -19,15 +19,9 @@ impl AuthService { } pub async fn auth_user(&self, email: String, password: String) -> InternalResult { - let condition = Condition::all().add(UserColumn::Email.eq(email)); - let user = user::Entity::find().filter(condition).one(self.trx()).await?; - if user.is_none() { - return Err(OrcaRepoError::ModelNotFound( - "User".to_string(), - "email".to_string(), - ))?; - } - let user = user.unwrap(); + let condition = Condition::all().add(UserColumn::Email.eq(&email)); + let user = user::Entity::find().filter(condition).one(self.trx()).await? + .ok_or_else(|| OrcaRepoError::ModelNotFound("User".to_string(), email))?; if "password".to_string() != password { return Err(OrcaRepoError::InvalidUsername(user.id))?; } diff --git a/crates/services/api/src/service/admin/user.rs b/crates/services/api/src/service/admin/user.rs index 3333e6f..b1cb8bb 100644 --- a/crates/services/api/src/service/admin/user.rs +++ b/crates/services/api/src/service/admin/user.rs @@ -1,13 +1,15 @@ -use crate::error::{InternalResult, OrcaRepoError}; -use crate::route::Pagination; -use crate::server::session::OrcaSession; -use entity::admin::user; -use entity::admin::user::{ActiveModel, Model}; use sea_orm::{ ActiveModelTrait, DatabaseTransaction, EntityTrait, NotSet, QuerySelect, TryIntoModel, }; use tracing::info; +use entity::admin::user; +use entity::admin::user::{ActiveModel, Model}; + +use crate::error::{InternalResult, OrcaRepoError}; +use crate::route::Pagination; +use crate::server::session::OrcaSession; + pub(crate) struct UserService(OrcaSession); impl UserService { @@ -34,15 +36,11 @@ impl UserService { Ok(users) } - pub async fn get_user_by_id(&self, id: i32) -> InternalResult { - let user = user::Entity::find_by_id(id).one(self.trx()).await?; - if user.is_none() { - return Err(OrcaRepoError::ModelNotFound( - "User".to_string(), - id.to_string(), - ))?; - } - return Ok(user.unwrap()); + pub async fn get_user_by_id(&self, id: String) -> InternalResult<()> { + let user = user::Entity::find_by_id(&id).one(self.trx()).await?.ok_or_else(|| { + OrcaRepoError::ModelNotFound("User".to_string(), id.clone()) + })?; + return Ok(()); } pub async fn update_user(&self, mut user: ActiveModel) -> InternalResult { @@ -50,12 +48,12 @@ impl UserService { return Ok(result); } - pub async fn delete_user_by_id(&self, id: i32) -> InternalResult<()> { - let result = user::Entity::delete_by_id(id).exec(self.trx()).await?; + pub async fn delete_user_by_id(&self, id: String) -> InternalResult<()> { + let result = user::Entity::delete_by_id(id.clone()).exec(self.trx()).await?; if result.rows_affected == 0 { return Err(OrcaRepoError::ModelNotFound( "User".to_string(), - id.to_string(), + id.clone(), ))?; } info!( diff --git a/crates/services/api/src/service/app/action.rs b/crates/services/api/src/service/app/action.rs index d287e5b..f25f351 100644 --- a/crates/services/api/src/service/app/action.rs +++ b/crates/services/api/src/service/app/action.rs @@ -1,9 +1,9 @@ -use sea_orm::prelude::Uuid; -use sea_orm::ActiveValue::Set; use sea_orm::{ ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, NotSet, QueryFilter, QueryOrder, TryIntoModel, }; +use sea_orm::ActiveValue::Set; +use sea_orm::prelude::Uuid; use sea_query::Expr; use tracing::info; @@ -44,7 +44,7 @@ impl ActionService { } /// update_action - this will update existing Action in Application in Orca - pub async fn update_action(&self, action_id: Uuid, mut action: Model) -> InternalResult { + pub async fn update_action(&self, action_id: Uuid, action: Model) -> InternalResult { let mut _action = action::Entity::find_by_id(action_id) .one(self.trx()) .await?; diff --git a/crates/services/api/src/service/app/case.rs b/crates/services/api/src/service/app/case.rs index d613e79..84a0646 100644 --- a/crates/services/api/src/service/app/case.rs +++ b/crates/services/api/src/service/app/case.rs @@ -11,6 +11,7 @@ use uuid::Uuid; use cerium::client::Client; use cerium::client::driver::web::WebDriver; use engine::controller::case::CaseController; +use entity::prelude::ActiveExecutionRequest; use entity::prelude::case::{Column, Entity, Model}; use entity::prelude::case_block::{ ActiveModel as BlockActiveModel, Column as BlockColumn, Entity as BlockEntity, @@ -18,7 +19,7 @@ use entity::prelude::case_block::{ }; use entity::test::history; use entity::test::ui::{ExecutionRequest, request}; -use entity::test::ui::request::{ExecutionKind, ExecutionStatus, ExecutionType, new}; +use entity::test::ui::request::{ExecutionKind, ExecutionStatus, ExecutionType}; use crate::error::{InternalResult, OrcaRepoError}; use crate::server::session::OrcaSession; @@ -191,44 +192,11 @@ impl CaseService { /// run - this will run the single tes case pub async fn run(&self, case_id: Uuid) -> InternalResult<()> { - let case = Entity::find_by_id(case_id).one(self.trx()).await?; - debug!("run {:?}", case); - if case.is_none() { - return Err(OrcaRepoError::ModelNotFound( - "Test Case".to_string(), - case_id.to_string(), - ))?; - } - let _case = case.unwrap(); - let er_am = new(case_id, ExecutionType::TestCase, ExecutionKind::Trigger, ExecutionStatus::Started, 0, false, None); - // let er = ExecutionRequest { - // description: Some(format!("Executing - {case_name}", case_name = _case.name)), - // is_dry_run: true, - // ref_id: case_id, - // ref_type: ExecutionType::TestCase, - // kind: ExecutionKind::Trigger, - // status: ExecutionStatus::Started, - // args: None, - // log_id: 0, - // created_at: chrono::Utc::now().into(), - // created_by: Some("system".to_string()), - // finished_at: None, - // updated_at: None, - // }; - - let mut er_am = er_am.save(self.trx()).await?; - debug!("run 2 {:?}", er_am); let ui_driver = WebDriver::default().await?; let controller = CaseController::new(self.trx(), ui_driver.clone(), self.1.clone()); - let er = er_am.clone().try_into_model()?; - controller.process(&_case, &er, None).await?; + controller.run(case_id, true).await?; ui_driver.quit().await?; - er_am.status = Set(ExecutionStatus::Completed); - er_am.finished_at = Set(chrono::Utc::now().into()); - er_am.updated_at = Set(chrono::Utc::now().into()); - er_am.save(self.trx()).await?; - Ok(()) } diff --git a/crates/services/api/src/service/app/history.rs b/crates/services/api/src/service/app/history.rs index 8cc7bd7..4ada9f0 100644 --- a/crates/services/api/src/service/app/history.rs +++ b/crates/services/api/src/service/app/history.rs @@ -1,9 +1,13 @@ -use sea_orm::{ActiveModelTrait, DatabaseTransaction, EntityTrait, QueryOrder}; +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, QueryFilter}; use sea_orm::ActiveValue::Set; +use sea_query::Condition; use uuid::Uuid; + +use entity::prelude::{ExecutionRequest, ExecutionRequestEntity, ItemLog, ItemLogColumn, ItemLogEntity, ItemLogType}; use entity::test::history; -use entity::test::history::{Entity, Column, Model, ExecutionKind, ExecutionType, ExecutionStatus}; -use crate::error::InternalResult; +use entity::test::history::{ExecutionKind, ExecutionStatus, ExecutionType, Model}; + +use crate::error::{InternalResult, OrcaRepoError}; use crate::server::session::OrcaSession; pub(crate) struct HistoryService(OrcaSession); @@ -17,12 +21,37 @@ impl HistoryService { self.0.trx() } + /// return Log By Request ID in the Orca Application + pub async fn log_by_id(&self, history_id: i32, log_type: ItemLogType, log_id: Uuid) + -> InternalResult> { + let _filter = Condition::all() + .add(ItemLogColumn::ErId.eq(history_id)) + .add(ItemLogColumn::RefType.eq(log_type)) + .add(ItemLogColumn::StepId.eq(log_id)); + let log = ItemLogEntity::find().filter(_filter).one(self.trx()).await? + .ok_or_else(|| OrcaRepoError::ModelNotFound("Log".to_string(), log_id.to_string()))?; + let _filter_2 = Condition::all() + .add(ItemLogColumn::ErId.eq(history_id)) + .add(ItemLogColumn::LogId.eq(log.id)) + .add( + Condition::any().add(ItemLogColumn::RefType.eq(ItemLogType::TestCase)) + .add(ItemLogColumn::RefType.eq(ItemLogType::ActionGroup)) + .add(ItemLogColumn::RefType.eq(ItemLogType::Action)) + ); + let logs = ItemLogEntity::find().filter(_filter_2).all(self.trx()).await?; + Ok(logs) + } + + /// return History By ID in the Orca Application + pub async fn by_id(&self, id: i32) -> InternalResult { + let histories = ExecutionRequestEntity::find_by_id(id).one(self.trx()).await? + .ok_or_else(|| OrcaRepoError::ModelNotFound("Request History".to_string(), id.to_string()))?; + Ok(histories) + } + /// list all the History Data in the Orca Application - pub async fn list_history(&self) -> InternalResult> { - let histories = Entity::find() - .order_by_desc(Column::Id) - .all(self.trx()) - .await?; + pub async fn list_history(&self) -> InternalResult> { + let histories = ExecutionRequestEntity::find().all(self.trx()).await?; Ok(histories) } @@ -41,6 +70,4 @@ impl HistoryService { }; Ok(history.insert(self.trx()).await?) } - - } \ No newline at end of file diff --git a/crates/services/api/src/service/app/suit.rs b/crates/services/api/src/service/app/suit.rs index 575a493..8d7a17b 100644 --- a/crates/services/api/src/service/app/suit.rs +++ b/crates/services/api/src/service/app/suit.rs @@ -1,11 +1,17 @@ -use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, QueryFilter, - QueryOrder, QuerySelect, -}; -use sea_query::{Condition, Expr}; -use tracing::info; +use std::cmp::{max, min}; + +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, ModelTrait, QueryFilter, QueryOrder, QuerySelect, TryIntoModel}; +use sea_orm::ActiveValue::Set; +use sea_orm_migration::SchemaManager; +use sea_query::{Alias, Condition, Expr, Table}; +use tracing::{debug, info}; use uuid::Uuid; +use cerium::client::Client; +use cerium::client::driver::web::WebDriver; +use engine::controller::case::CaseController; +use engine::controller::suite::SuiteController; +use entity::test::ui::case::case::{Column as CaseColumn, Entity as CaseEntity, Model as CaseModel}; use entity::test::ui::suit::suite::{Column, Entity, Model}; use entity::test::ui::suit::suite_block::{ ActiveModel, Column as BlockColumn, Entity as BlockEntity, Model as BlockModel, @@ -43,6 +49,20 @@ impl SuitService { return Ok(result); } + /// delete - this will delete Suite in Application in Orca + pub async fn delete(&self, suite_id: Uuid) -> InternalResult<()> { + let suite = Entity::find_by_id(suite_id).one(self.trx()).await?; + if suite.is_none() { + return Err(OrcaRepoError::ModelNotFound( + "Suite".to_string(), + suite_id.to_string(), + ))?; + } + let suite = suite.unwrap(); + suite.delete(self.trx()).await?; + Ok(()) + } + /// batch_update_suite_block - update suite Block pub(crate) async fn batch_update_suite_block( &self, @@ -52,6 +72,9 @@ impl SuitService { let suit_blocks: Vec = body .into_iter() .map(|mut block| { + if block.id.is_nil() { + block.id = Uuid::new_v4(); + } block.suite_id = suite_id.clone(); block.into_active_model() }) @@ -77,7 +100,22 @@ impl SuitService { .order_by_asc(BlockColumn::ExecutionOrder) .all(self.trx()) .await?; - suite.suite_execution = Some(serde_json::to_value(suite_blocks)?); + let mut result = vec![]; + for mut item in suite_blocks { + if item.reference.is_some() { + let _ref = CaseEntity::find_by_id(item.reference.unwrap()) + .one(self.trx()) + .await?; + if _ref.is_some() { + let r = _ref.clone().unwrap(); + info!("{:#?}", r); + item.name = Some(r.clone().name.clone()); + item.description = r.description; + } + } + result.push(item); + } + suite.suite_execution = Some(serde_json::to_value(result)?); Ok(suite) } @@ -124,7 +162,76 @@ impl SuitService { body.execution_order = _index; let _suite = body.clone().into_active_model(); info!("{:?}", _suite); - let result = _suite.insert(self.trx()).await?; + let mut result = _suite.insert(self.trx()).await?; + if result.reference.is_some() { + let _ref = CaseEntity::find_by_id(result.reference.unwrap()) + .one(self.trx()) + .await?; + if _ref.is_some() { + let r = _ref.clone().unwrap(); + info!("{:#?}", r); + result.name = Some(r.clone().name.clone()); + result.description = r.description; + } + } Ok(result) } + + /// delete_block - This will delete Block in the Suite + pub(crate) async fn delete_block( + &self, + block_id: Uuid, + ) -> InternalResult<()> { + BlockEntity::delete_by_id(block_id).exec(self.trx()).await?; + return Ok(()); + } + + + /// reorder_block - this function will reorder the block to new location + pub(crate) async fn reorder_block( + &self, + block_id: Uuid, + location: i32, + ) -> InternalResult { + let block = BlockEntity::find_by_id(block_id).one(self.trx()).await?; + if block.is_none() { + return Err(OrcaRepoError::ModelNotFound( + "Block".to_string(), + block_id.to_string(), + ))?; + } + let mut block = block.unwrap(); + let old_location = block.execution_order.clone(); + let mut _filter = Condition::all() + .add(BlockColumn::SuiteId.eq(block.suite_id.clone())); + let mut expr = Expr::expr(Expr::col(BlockColumn::ExecutionOrder).if_null(0)).sub(0); + if location > old_location { + _filter = _filter.add(BlockColumn::ExecutionOrder.gt(old_location)) + .add(BlockColumn::ExecutionOrder.lte(location)); + expr = Expr::expr(Expr::col(BlockColumn::ExecutionOrder).if_null(0)).sub(1); + } else { + _filter = _filter.add(BlockColumn::ExecutionOrder.lt(old_location)) + .add(BlockColumn::ExecutionOrder.gte(location)); + expr = Expr::expr(Expr::col(BlockColumn::ExecutionOrder).if_null(0)).add(1); + } + let _result = BlockEntity::update_many().col_expr( + BlockColumn::ExecutionOrder, + expr, + ).filter(_filter).exec(self.trx()).await?; + debug!("updated result {:?}", _result); + let mut am_block = block.into_active_model(); + am_block.execution_order = Set(location); + let result = am_block.save(self.trx()).await?.try_into_model()?; + return Ok(result); + } + + + /// run - this will run the single testsuite + pub async fn run(&self, client: Client, suite_id: Uuid) -> InternalResult<()> { + let ui_driver = WebDriver::default().await?; + let controller = SuiteController::new(self.trx(), ui_driver.clone(), client.clone()); + controller.run(suite_id, true).await?; + ui_driver.quit().await?; + Ok(()) + } } diff --git a/crates/services/engine/src/controller/action.rs b/crates/services/engine/src/controller/action.rs index d25492e..6c804d4 100644 --- a/crates/services/engine/src/controller/action.rs +++ b/crates/services/engine/src/controller/action.rs @@ -3,17 +3,18 @@ use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, I use sea_orm::ActiveValue::Set; use sea_orm::prelude::{DateTimeWithTimeZone, Uuid}; use thirtyfour::By; -use tracing::info; +use tracing::{error, info}; use cerium::client::Client; use cerium::client::driver::web::WebDriver; use cerium::client::storage::s3::S3Client; +use entity::prelude::ActiveItemLog; use entity::prelude::target::ActionTargetKind; use entity::test::ui::action::action; use entity::test::ui::action::action::ActionKind; use entity::test::ui::action::group::{Entity as ActionGroupEntity, Model as ActionGroupModel}; use entity::test::ui::ExecutionRequest; -use entity::test::ui::log::item_log::{ItemLogStatus, ItemLogType, new}; +use entity::test::ui::log::item_log::{ItemLogStatus, ItemLogType}; use entity::test::ui::log::ItemLog; use crate::error::{EngineError, EngineResult}; @@ -78,13 +79,19 @@ impl<'ccl> ActionController<'ccl> { /// /// * `Result<(), EngineError>` - If the `data_value` field is `None`, it returns an `Err` with an `EngineError::Forbidden` variant. If the `data_value` field is not `None`, it opens the URL using the `drive` object and returns `Ok(())`. pub async fn command_open(&self, action: &action::Model) -> EngineResult<()> { - match action.data_value.clone() { - Some(value) => Ok(self.driver.open(value.as_str()).await?), - None => Err(EngineError::MissingParameter( - "url".to_string(), - "".to_string(), - )), - } + let url = action.data_value.clone().ok_or_else(|| { + EngineError::MissingParameter("url".to_string(), "".to_string()) + })?; + info!("Opening URL: {}", url); + self.driver.open(url.as_str()).await?; + Ok(()) + // match action.data_value.clone() { + // Some(value) => Ok(self.driver.open(value.as_str()).await?), + // None => Err(EngineError::MissingParameter( + // "url".to_string(), + // "".to_string(), + // )), + // } } /// Asynchronously enters data into a target element on a web page using a WebDriver. @@ -245,25 +252,23 @@ impl<'ccl> ActionController<'ccl> { } pub async fn execute_action(&self, action: &action::Model, er: &ExecutionRequest, - log: Option<&ItemLog>) -> EngineResult<()> { - let log_id = log.map(|l| l.id); - let mut log_am = new(er.ref_id, ItemLogType::Action, action.id, log_id).save(self.db).await?; + log: &ItemLog) -> EngineResult<()> { + // let log_id = log.map(|l| l.id); + // let mut log_am = ActiveItemLog::new(er.id, Some(action.id), ItemLogType::Action, + // action.id, log_id).save(self.db).await?; info!("[{er}] Trigger Action {action_id}", er=er.ref_id, action_id = action.id); - let start = chrono::Utc::now(); - info!( - "Executing step == [id] {:?}, [desc] {:?}", - action.id, action.description - ); + info!("Action {:#?}", action); + // let start = chrono::Utc::now(); self.step_executor(&action).await?; self.take_screenshot(action.id.to_string()).await?; info!( "Done step == [id] {:?}, [desc] {:?}", action.id, action.description ); - log_am.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); - log_am.status = Set(ItemLogStatus::Success); - log_am.finished_at = Set(chrono::Utc::now().into()); - log_am.save(self.db).await?; + // log_am.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); + // log_am.status = Set(ItemLogStatus::Success); + // log_am.finished_at = Set(chrono::Utc::now().into()); + // log_am.save(self.db).await?; Ok(()) } @@ -275,7 +280,27 @@ impl<'ccl> ActionController<'ccl> { .paginate(self.db, 50); while let Some(actions) = action_page.fetch_and_next().await? { for action in actions.into_iter() { - self.execute_action(&action, er, log).await?; + let start = chrono::Utc::now(); + let log_id = log.map(|l| l.id); + let mut action_log = ActiveItemLog::new(er.id, Some(action.id.clone()), + ItemLogType::Action, action.id.clone(), + log_id).save(self.db).await?; + let _log = action_log.clone().try_into_model()?; + let result = self.execute_action(&action, er, &_log).await; + action_log.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); + action_log.finished_at = Set(chrono::Utc::now().into()); + match result { + Ok(_) => { + action_log.status = Set(ItemLogStatus::Success); + action_log.save(self.db).await?; + } + Err(e) => { + error!("Error in Action: {:?}", e); + action_log.status = Set(ItemLogStatus::Failed); + action_log.save(self.db).await?; + return Err(e); + } + } } } Ok(()) @@ -283,9 +308,9 @@ impl<'ccl> ActionController<'ccl> { /// run_case - will execute the test case by the case ID pub async fn execute(&self, id: Uuid, er: &ExecutionRequest, - log: Option<&ItemLog>) -> EngineResult<()> { + log: Option<&ItemLog>, ref_id: Option) -> EngineResult<()> { let start = chrono::Utc::now(); - let mut log_am = new(er.ref_id, ItemLogType::Action, id, None).save(self.db).await?; + let mut log_am = ActiveItemLog::new(er.id, ref_id, ItemLogType::ActionGroup, id, None).save(self.db).await?; info!("[{er}] Trigger Action {action_id}", er=er.ref_id, action_id = id); let action_group = ActionGroupEntity::find_by_id(id).one(self.db) .await? diff --git a/crates/services/engine/src/controller/case.rs b/crates/services/engine/src/controller/case.rs index 887fef9..6711dd0 100644 --- a/crates/services/engine/src/controller/case.rs +++ b/crates/services/engine/src/controller/case.rs @@ -1,17 +1,17 @@ -use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, TryIntoModel}; +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, + PaginatorTrait, QueryFilter, QueryOrder, TryIntoModel}; use sea_orm::ActiveValue::Set; use sea_orm::prelude::Uuid; use tracing::{debug, info}; use cerium::client::Client; use cerium::client::driver::web::WebDriver; -use entity::prelude::case::Entity; -use entity::prelude::case_block; +use entity::prelude::{ActiveExecutionRequest, ActiveItemLog, case_block, CaseEntity, ExecutionKind, ExecutionStatus, ExecutionType}; use entity::prelude::case_block::{BlockKind, BlockType}; -use entity::test::ui::{ExecutionRequest, request}; use entity::test::ui::case::case; -use entity::test::ui::log::{item_log, ItemLog}; -use entity::test::ui::log::item_log::{ItemLogStatus, ItemLogType, new}; +use entity::test::ui::ExecutionRequest; +use entity::test::ui::log::item_log::{ItemLogStatus, ItemLogType}; +use entity::test::ui::log::ItemLog; use crate::controller::action::ActionController; use crate::error::{EngineError, EngineResult}; @@ -32,59 +32,48 @@ impl<'ccl> CaseController<'ccl> { } - /// run - will execute the test cases based on the execution request - pub async fn run(&self, id: Uuid, er: &ExecutionRequest, log: Option<&ItemLog>) -> EngineResult<()> { - info!("[{er}] Trigger Test Case {action_id}", er=er.ref_id, action_id = id); + /// execute - will execute the test cases based on the execution request + pub async fn execute(&self, id: Uuid, er: &ExecutionRequest, log: Option<&ItemLog>, + ref_id: Option) -> EngineResult<()> { let start = chrono::Utc::now(); let log_id = log.map(|l| l.id); - let mut log_am = new(er.ref_id, ItemLogType::ActionGroup, id, log_id).save(self.db).await?; - // let mut log_item = item_log::Model { - // ref_id: er.ref_id, - // ref_type: ItemLogType::TestCase, - // step_id: id, - // has_screenshot: false, - // has_recording: false, - // execution_time: 0, - // status: ItemLogStatus::Running, - // log_id: None, - // created_at: start.into(), - // created_by: "system".to_string(), - // finished_at: chrono::Utc::now().into(), - // ..Default::default() - // }; - // if log.is_some() { - // log_item.log_id = Some(log.unwrap().id); - // } - // let mut log_item_am = log_item.into_active_model().save(self.db).await?; - let case = Entity::find_by_id(id).one(self.db).await? - .ok_or(EngineError::MissingParameter("ActionGroup".to_string(), id.into()))?; - let log = log_am.clone().try_into_model()?; - self.process(&case, er, Some(&log)).await?; + let mut case_log = ActiveItemLog::new(er.id, ref_id, ItemLogType::TestCase, + id, log_id).save(self.db).await?; + let case = CaseEntity::find_by_id(id).one(self.db).await? + .ok_or(EngineError::MissingParameter("Case".to_string(), id.into()))?; + let log = case_log.clone().try_into_model()?; + let result = self.process(&case, er, Some(&log)).await; - log_am.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); - log_am.status = Set(ItemLogStatus::Success); - log_am.finished_at = Set(chrono::Utc::now().into()); - log_am.save(self.db).await?; - Ok(()) + case_log.finished_at = Set(chrono::Utc::now().into()); + case_log.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); + match result { + Ok(_) => { + case_log.status = Set(ItemLogStatus::Success); + case_log.save(self.db).await?; + } + Err(e) => { + case_log.status = Set(ItemLogStatus::Failed); + case_log.save(self.db).await?; + return Err(e); + } + } + return Ok(()); } - /// run_case - will execute the test case by the case ID - // pub async fn run_case(&self, id: Uuid) -> EngineResult<()> { - // let case_res = Entity::find_by_id(id).one(self.db).await?; - // if case_res.is_none() { - // error!("Unable to find the Case - {:?}", id.clone()); - // return Ok(()); - // } - // let case: &case::Model = &case_res.unwrap(); - // info!( - // "Start Processing Case - [[ {name} || {id} ]]", - // name = case.name, - // id = case.id - // ); - // self.process(case).await?; - // Ok(()) - // } + pub async fn run(&self, id: Uuid, is_dry_run: bool) -> EngineResult<()> { + let mut am_er = ActiveExecutionRequest::new(id, ExecutionType::TestCase, + ExecutionKind::Trigger, + ExecutionStatus::Started, 0, + is_dry_run, Some(format!("[TC] Executing - {id}")), ).save(self.db).await?; + let model_er = am_er.clone().try_into_model()?; + info!("[{er}] Trigger Test Case {case_id}", er=model_er.id, case_id = id); + self.execute(id, &model_er, None, Some(id)).await?; + am_er.finished_at = Set(chrono::Utc::now().into()); + am_er.status = Set(ExecutionStatus::Completed); + am_er.save(self.db).await?; + Ok(()) + } /// process will get the block and execute in the batch based on the kind of the block pub async fn process(&self, case: &case::Model, er: &ExecutionRequest, log: Option<&ItemLog>) -> EngineResult<()> { @@ -94,7 +83,25 @@ impl<'ccl> CaseController<'ccl> { .paginate(self.db, 10); while let Some(blocks) = block_page.fetch_and_next().await? { for block in blocks.into_iter() { - self.switch_block(&block, er, log).await?; + let start = chrono::Utc::now(); + let log_id = log.map(|l| l.id); + let mut item_log = ActiveItemLog::new(er.id, block.reference.clone(), ItemLogType::TestCaseBlock, + block.id.clone(), log_id).save(self.db).await?; + let _log = item_log.clone().try_into_model()?; + let result = self.switch_block(&block, er, log).await; + item_log.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); + item_log.finished_at = Set(chrono::Utc::now().into()); + match result { + Ok(_) => { + item_log.status = Set(ItemLogStatus::Success); + item_log.save(self.db).await?; + } + Err(e) => { + item_log.status = Set(ItemLogStatus::Failed); + item_log.save(self.db).await?; + return Err(e); + } + } } } Ok(()) @@ -145,7 +152,7 @@ impl<'ccl> CaseController<'ccl> { info!("Starting processing {block_id} ", block_id = block.id); let controller = ActionController::new(self.db, self.drive.clone(), self.cli.clone()); let result = controller - .execute(block.reference.unwrap(), er, log) + .execute(block.reference.unwrap(), er, log, Some(block.id)) .await?; Ok(result) } diff --git a/crates/services/engine/src/controller/suite.rs b/crates/services/engine/src/controller/suite.rs index 8b13789..0052305 100644 --- a/crates/services/engine/src/controller/suite.rs +++ b/crates/services/engine/src/controller/suite.rs @@ -1 +1,136 @@ +use sea_orm::{ActiveModelTrait, DatabaseTransaction, EntityTrait, ModelTrait, PaginatorTrait, QueryOrder, Related, TryIntoModel}; +use sea_orm::ActiveValue::Set; +use sea_orm::ColumnTrait; +use sea_orm::prelude::Uuid; +use sea_orm::QueryFilter; +use tracing::info; +use cerium::client::Client; +use cerium::client::driver::web::WebDriver; +use cerium::client::storage::s3::S3Client; +use entity::prelude::{ActiveExecutionRequest, ActiveItemLog, case_block, CaseEntity, ExecutionKind, ExecutionRequest, ExecutionStatus, ExecutionType, ItemLog, ItemLogStatus, ItemLogType, SuiteBlockColumn, SuiteBlockEntity}; +use entity::test::ui::suit::suite::{Column as SuiteColumn, Entity as SuiteEntity, Model as SuiteModel}; +use entity::test::ui::suit::suite_block::{Column as BlockColumn, Entity as BlockEntity, Model as BlockModel, Model, SuiteBlockType}; + +use crate::controller::case::CaseController; +use crate::error::{EngineError, EngineResult}; + +pub struct SuiteController<'scl> { + db: &'scl DatabaseTransaction, + driver: WebDriver, + client: Client, + storage_cli: S3Client, +} + +impl<'scl> SuiteController<'scl> { + /// Constructs a new `ActionController` instance. + /// + /// # Arguments + /// + /// * `db` - A reference to a `DatabaseTransaction` instance. + /// * `driver` - A `WebDriver` instance. + /// * `client` - A `Client` instance. + /// + /// # Returns + /// + /// Returns a new `ActionController` instance. + pub fn new( + db: &'scl DatabaseTransaction, + driver: WebDriver, + client: Client, + ) -> SuiteController<'scl> { + let storage_cli = client.storage_cli.clone(); + Self { + db, + driver, + client, + storage_cli, + } + } + + /// execute - will execute the Suit based on the execution request + pub async fn execute(&self, id: Uuid, er: &ExecutionRequest, log: Option<&ItemLog>) -> EngineResult<()> { + let start = chrono::Utc::now(); + let log_id = log.map(|l| l.id); + let mut action_log = ActiveItemLog::new(er.id, None, ItemLogType::TestSuite, + id, log_id).save(self.db).await?; + let suite = SuiteEntity::find_by_id(id).one(self.db).await? + .ok_or(EngineError::MissingParameter("Suite".to_string(), id.into()))?; + let log = action_log.clone().try_into_model()?; + let result = self.execute_suite(&suite, er, Some(&log)).await; + + action_log.finished_at = Set(chrono::Utc::now().into()); + action_log.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); + match result { + Ok(_) => { + action_log.status = Set(ItemLogStatus::Success); + action_log.save(self.db).await?; + } + Err(e) => { + action_log.status = Set(ItemLogStatus::Failed); + action_log.save(self.db).await?; + return Err(e); + } + } + return Ok(()); + } + + + pub async fn run(&self, id: Uuid, is_dry_run: bool) -> EngineResult<()> { + let mut am_er = ActiveExecutionRequest::new(id, ExecutionType::TestSuite, + ExecutionKind::Trigger, + ExecutionStatus::Started, 0, + is_dry_run, Some(format!("[TS] Executing - {id}")), ).save(self.db).await?; + let model_er = am_er.clone().try_into_model()?; + info!("[{er}] Trigger Test Suite {suite_id}", er=model_er.id, suite_id = id); + self.execute(id, &model_er, None).await?; + am_er.finished_at = Set(chrono::Utc::now().into()); + am_er.status = Set(ExecutionStatus::Completed); + am_er.save(self.db).await?; + Ok(()) + } + + async fn switch_block<'a>(&self, ctrl: &'a CaseController<'a>, block: Model, er: &ExecutionRequest, + log: Option<&ItemLog>) -> EngineResult<()> { + match block.type_field { + SuiteBlockType::TestCase => { + let case_id = block.reference.ok_or(EngineError::MissingParameter("Suite Reference".to_string(), block.id.into()))?; + info!("Switching to Test Case {block_id} - reference {case_id}", + block_id = block.id, case_id=case_id); + ctrl.execute(case_id, er, log, Some(block.id)).await?; + Ok(()) + } + } + } + + async fn execute_suite(&self, suite: &SuiteModel, er: &ExecutionRequest, + log: Option<&ItemLog>) -> EngineResult<()> { + info!("Executing Suite {suite_id}", suite_id = suite.id); + let mut block_page = SuiteBlockEntity::find() + .filter(SuiteBlockColumn::SuiteId.eq(suite.id)) + .order_by_asc(SuiteBlockColumn::ExecutionOrder) + .paginate(self.db, 10); + let ctrl = CaseController::new(self.db, self.driver.clone(), self.client.clone()); + while let Some(blocks) = block_page.fetch_and_next().await? { + for block in blocks.into_iter() { + let start = chrono::Utc::now(); + let log_id = log.map(|l| l.id); + let mut action_log = ActiveItemLog::new(er.id, block.reference.clone(), + ItemLogType::TestSuiteBlock, + block.id.clone(), log_id).save(self.db).await?; + let result = self.switch_block(&ctrl, block, er, log).await; + action_log.finished_at = Set(chrono::Utc::now().into()); + action_log.execution_time = Set((chrono::Utc::now() - start).num_milliseconds() as i32); + match result { + Ok(_) => { + action_log.save_status(self.db, ItemLogStatus::Success).await?; + } + Err(e) => { + action_log.save_status(self.db, ItemLogStatus::Failed).await?; + } + } + } + } + Ok(()) + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 12eec75..38def02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,7 +21,7 @@ services: chrome: platform: linux/amd64 - image: selenium/node-chrome:nightly + image: selenium/node-chrome:latest shm_size: 2gb depends_on: - selenium-hub @@ -34,7 +34,7 @@ services: firefox: container_name: firefox platform: linux/amd64 - image: selenium/node-firefox:nightly + image: selenium/node-firefox:latest shm_size: 2gb depends_on: - selenium-hub @@ -47,7 +47,7 @@ services: firefox_video: platform: linux/amd64 - image: selenium/video:nightly + image: selenium/video:latest depends_on: - firefox - minio @@ -92,7 +92,7 @@ services: selenium-hub: platform: linux/amd64 - image: selenium/hub:nightly + image: selenium/hub:latest container_name: selenium-hub ports: - "4442:4442" @@ -107,10 +107,11 @@ services: - POSTGRES_USER=root - POSTGRES_PASSWORD=root - POSTGRES_DB=orca + # - PGDATA=/var/lib/postgresql/data/some_name/ ports: - "5433:5432" volumes: - - ./data:/var/lib/postgresql/data + - ./data/pg-data:/var/lib/postgresql/data container_name: postgres-ser ### Redis ################################################ diff --git a/web/package-lock.json b/web/package-lock.json index c764148..1b50c3d 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -9,6 +9,10 @@ "version": "0.1.0", "dependencies": { "@dagrejs/dagre": "^1.0.4", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.6.6", "@heroicons/react": "^2.0.18", "@hookform/resolvers": "^3.3.4", @@ -2384,6 +2388,68 @@ "node": ">17.0.0" } }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.0.tgz", + "integrity": "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.1.0.tgz", + "integrity": "sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.0", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-7.0.0.tgz", + "integrity": "sha512-BG/ETy3eBjFap7+zIti53f0PCLGDzNXyTmn6fSdrudORf+OH04MxrW4p5+mPu4mgMk9kM41iYONjc3DOUWTcfg==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz", + "integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.1.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", diff --git a/web/package.json b/web/package.json index d3457a8..5293933 100644 --- a/web/package.json +++ b/web/package.json @@ -4,6 +4,10 @@ "private": true, "dependencies": { "@dagrejs/dagre": "^1.0.4", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/modifiers": "^7.0.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.6.6", "@heroicons/react": "^2.0.18", "@hookform/resolvers": "^3.3.4", diff --git a/web/src/core/components/dropdown/dropdown.css b/web/src/core/components/dropdown/dropdown.css new file mode 100644 index 0000000..1735f7d --- /dev/null +++ b/web/src/core/components/dropdown/dropdown.css @@ -0,0 +1,100 @@ +.dropdown { + position: relative; + color: #333; + cursor: default; + } + + .dropdown .arrow { + border-color: #999 transparent transparent; + border-style: solid; + border-width: 5px 5px 0; + content: " "; + display: block; + height: 0; + margin-top: 0.3rem; + position: absolute; + right: 10px; + top: 14px; + width: 0; + } + + .dropdown .arrow.open { + border-color: transparent transparent #999; + border-width: 0 5px 5px; + } + + .input { + line-height: 1.5; + font-size: 1rem; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 2px; + box-sizing: border-box; + cursor: default; + outline: none; + padding: 8px 52px 8px 10px; + transition: all 200ms ease; + width: 100%; + } + + .dropdown .options { + display: none; + background-color: #fff; + border: 1px solid #ccc; + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); + box-sizing: border-box; + margin-top: -1px; + max-height: 200px; + overflow-y: auto; + position: absolute; + top: 100%; + width: 100%; + z-index: 1000; + -webkit-overflow-scrolling: touch; + } + + .dropdown .options.open { + display: block; + } + + .dropdown .option { + box-sizing: border-box; + color: rgba(51, 51, 51, 0.8); + cursor: pointer; + display: block; + padding: 8px 10px; + } + + .dropdown .option.selected, + .dropdown .option:hover { + background-color: #f2f9fc; + color: #333; + } + + .close { + position: absolute; + right: 40px; + top: 14px; + width: 16px; + height: 16px; + opacity: 0.3; + } + .close:hover { + opacity: 1; + } + .close:before, .close:after { + position: absolute; + left: 15px; + content: ' '; + height: 16px; + width: 2px; + background-color: #333; + } + .close:before { + transform: rotate(45deg); + } + .close:after { + transform: rotate(-45deg); + } + + \ No newline at end of file diff --git a/web/src/core/components/dropdown/index.jsx b/web/src/core/components/dropdown/index.jsx new file mode 100644 index 0000000..212c9f0 --- /dev/null +++ b/web/src/core/components/dropdown/index.jsx @@ -0,0 +1,96 @@ +import {useEffect, useRef, useState} from "react"; +import PropTypes from "prop-types"; +import "./dropdown.css"; + + +export const SearchableDropdown = ({ + options, + label, + id, + selectedValue, + handleChange + }) => { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + + const inputRef = useRef(null); + + useEffect(() => { + document.addEventListener("click", toggle); + return () => document.removeEventListener("click", toggle); + }, []); + + const selectOption = (option) => { + setQuery(() => ""); + handleChange(option); + setIsOpen((isOpen) => !isOpen); + }; + + function toggle(e) { + setIsOpen(e && e.target === inputRef.current); + } + + const getDisplayValue = () => { + if (query) return query; + if (selectedValue) return selectedValue[label] || ""; + + return ""; + }; + + const filter = (options) => { + return options.filter( + (option) => option[label] && option[label].toLowerCase().indexOf(query.toLowerCase()) > -1 + ); + }; + + return ( +
+
+
+ { + setQuery(e.target.value); + handleChange(null); + }} + onClick={toggle} + /> +
+ {selectedValue[label] &&
handleChange({})}/>} +
+
+ +
+ {filter(options).map((option, index) => { + return ( +
selectOption(option)} + className={`option ${ + option === selectedValue ? "selected" : "" + }`} + key={`${id}-${index}`} + > + {option[label]} +
+ ); + })} +
+
+ ); +}; + +SearchableDropdown.propTypes = { + options: PropTypes.arrayOf(PropTypes.object), + label: PropTypes.string, + id: PropTypes.string, + selectedValue: PropTypes.object, + handleChange: PropTypes.func +}; + + + diff --git a/web/src/core/components/flow/components/index.tsx b/web/src/core/components/flow/components/index.tsx deleted file mode 100644 index 9147356..0000000 --- a/web/src/core/components/flow/components/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { New } from "./new"; - -export { New }; diff --git a/web/src/core/components/flow/components/new.tsx b/web/src/core/components/flow/components/new.tsx deleted file mode 100644 index 7cbb62e..0000000 --- a/web/src/core/components/flow/components/new.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Listbox, Transition } from "@headlessui/react"; -import { - ArrowPathRoundedSquareIcon, - CodeBracketSquareIcon, - HashtagIcon, - PlusIcon -} from "@heroicons/react/24/outline"; -import { Fragment, useState } from "react"; -import { RFState, useFlowStore } from "stores/flow.store"; -import { shallow } from "zustand/shallow"; - -export const New: React.FC = () => { - let options = [ - { - key: "loop", - label: "Loop", - icon: - }, - { - key: "ifcondition", - label: "If Condidion", - icon: - }, - { - key: "block", - label: "Block", - icon: - }, - { - key: "action_group", - label: "Action group", - icon: - } - ]; - const [open, setOpen] = useState(false); - const { addNewNode, nodes } = useFlowStore( - (state: RFState) => ({ - addNewNode: state.addNewNode, - nodes: state.nodes - }), - shallow - ); - return ( - - setOpen(true)} - onMouseOut={() => setOpen(false)} - > - - - - - setOpen(true)} - onMouseOut={() => setOpen(false)} - className={ - "absolute z-50 mt-1 max-h-56 w-40 -left-16 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" - } - > - {options.map((item) => ( - addNewNode([])} - > -
- {item["icon"]} - {item["label"]} -
-
- ))} -
-
-
- ); -}; diff --git a/web/src/core/components/flow/edge/general.tsx b/web/src/core/components/flow/edge/general.tsx index e38cc38..2d4132a 100644 --- a/web/src/core/components/flow/edge/general.tsx +++ b/web/src/core/components/flow/edge/general.tsx @@ -1,111 +1,21 @@ -import { - ArrowPathRoundedSquareIcon, - CodeBracketSquareIcon, - HashtagIcon -} from "@heroicons/react/24/outline"; -import { useState } from "react"; + import { BaseEdge, EdgeProps, getSmoothStepPath } from "reactflow"; -// export default function CustomEdge() { export const DefaultEdge: React.FC = ({ id, sourceX, sourceY, targetX, targetY, - sourcePosition, - targetPosition }) => { - let options = [ - { - key: "loop", - label: "Loop", - icon: - }, - { - key: "ifcondition", - label: "If Condidion", - icon: - }, - { - key: "block", - label: "Block", - icon: - } - ]; - const [open, setOpen] = useState(false); - const [edgePath, labelX, labelY] = getSmoothStepPath({ + const [edgePath] = getSmoothStepPath({ sourceX, sourceY, targetX, targetY - // sourcePosition, - // targetPosition }); return ( - <> - {/* -
- - setOpen(true)} - onMouseOut={() => setOpen(false)} - > - - - - - setOpen(true)} - onMouseOut={() => setOpen(false)} - className={ - "absolute z-10000 mt-1 max-h-56 w-40 -left-16 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm" - } - > - {options.map((item) => ( - -
- {item["icon"]} - - {item["label"]} - -
-
- ))} -
-
-
-
-
*/} - ); }; diff --git a/web/src/core/components/flow/edge/no.tsx b/web/src/core/components/flow/edge/no.tsx index fda0ec5..4ac72b0 100644 --- a/web/src/core/components/flow/edge/no.tsx +++ b/web/src/core/components/flow/edge/no.tsx @@ -1,4 +1,3 @@ -import { useState } from "react"; import { BaseEdge, EdgeProps, getSmoothStepPath } from "reactflow"; export const NoEdge: React.FC = ({ @@ -10,8 +9,7 @@ export const NoEdge: React.FC = ({ sourcePosition, targetPosition }) => { - const [open, setOpen] = useState(false); - const [edgePath, labelX, labelY] = getSmoothStepPath({ + const [edgePath ] = getSmoothStepPath({ sourceX, sourceY, targetX, @@ -21,24 +19,6 @@ export const NoEdge: React.FC = ({ }); return ( - <> - {/* -
- -
-
*/} - ); }; diff --git a/web/src/core/components/flow/edge/yes.tsx b/web/src/core/components/flow/edge/yes.tsx index 70d3cac..b158a3e 100644 --- a/web/src/core/components/flow/edge/yes.tsx +++ b/web/src/core/components/flow/edge/yes.tsx @@ -11,8 +11,6 @@ export const getSpecialPath = ( { sourceX, sourceY, targetX, targetY }: GetSpecialPathParams, offset: number ) => { - const centerX = (sourceX + targetX) / 2; - const centerY = (sourceY + targetY) / 2; return `M ${sourceX} ${sourceY} L ${targetX} ${sourceY} ${targetX} ${targetY}`; }; @@ -27,7 +25,7 @@ export const YesEdge: React.FC = ({ sourcePosition, targetPosition }) => { - const [edgePath, labelX, labelY] = getSmoothStepPath({ + const [edgePath] = getSmoothStepPath({ sourceX, sourceY, targetX, @@ -50,23 +48,6 @@ export const YesEdge: React.FC = ({ return ( <> - {/* -
- -
-
*/} ); }; diff --git a/web/src/core/components/flow/flow.tsx b/web/src/core/components/flow/flow.tsx deleted file mode 100644 index 7ff223a..0000000 --- a/web/src/core/components/flow/flow.tsx +++ /dev/null @@ -1,348 +0,0 @@ -import ReactFlow, { - Background, - Node, - useEdgesState, - useNodesState -} from "reactflow"; - -import { graphlib, layout } from "@dagrejs/dagre"; -import { useEffect } from "react"; -import "reactflow/dist/style.css"; -import { DefaultEdge } from "./edge"; -import { NoEdge } from "./edge/no"; -import { YesEdge } from "./edge/yes"; -import { StartNode } from "./node"; -import { ActionNode } from "./node/action"; -import { ConditionalNode } from "./node/condition"; -import { NewNode } from "./node/new"; - -const initialNodes: Array = [ - // { - // id: "1-4640-4afe-a05b-1d0e08ee9594", - // position: { - // x: 0, - // y: -50 - // }, - // data: { - // label: "1", - // payload: { - // id: "fcb03ff7-4640-4afe-a05b-1d0e08ee9594", - // execution_order: 1, - // kind: "Reference", - // type_field: "ActionGroup", - // reference: "cb2f6a96-effe-4bc7-adae-c45578cd2a56", - // parent_id: null, - // case_id: "731453aa-95a5-4180-be0d-c211a1e92aad" - // } - // } - // }, - { - id: "actionNodefcb03ff7-4640-4afe-a05b-1d0e08ee9594", - type: "actionNode", - position: { - x: 0, - y: 0 - }, - data: { - payload: { - id: "fcb03ff7-4640-4afe-a05b-1d0e08ee9594", - execution_order: 1, - kind: "Reference", - type_field: "ActionGroup", - reference: "cb2f6a96-effe-4bc7-adae-c45578cd2a56", - parent_id: null, - case_id: "731453aa-95a5-4180-be0d-c211a1e92aad" - } - } - }, - // { - // id: "addNewfcb03ff7-4640-4afe-a05b-1d0e08ee9594", - // type: "newNode", - // position: { - // x: 178, - // y: 150 - // }, - // data: { - // payload: { - // id: "d3fa5829-a393-4398-b12f-678ffdbb0871", - // execution_order: 2, - // kind: "Reference", - // type_field: "Assertion", - // reference: "150d0ae1-af8a-4d03-bafd-299fdc99ffde", - // parent_id: null, - // case_id: "731453aa-95a5-4180-be0d-c211a1e92aad" - // } - // } - // }, - { - id: "actionNoded3fa5829-a393-4398-b12f-678ffdbb0871", - type: "actionNode", - position: { - x: 0, - y: 300 - }, - data: { - payload: { - id: "d3fa5829-a393-4398-b12f-678ffdbb0871", - execution_order: 2, - kind: "Reference", - type_field: "Assertion", - reference: "150d0ae1-af8a-4d03-bafd-299fdc99ffde", - parent_id: null, - case_id: "731453aa-95a5-4180-be0d-c211a1e92aad" - } - } - } - // { - // id: "addNewd3fa5829-a393-4398-b12f-678ffdbb0871", - // type: "newNode", - // position: { - // x: 178, - // y: 450 - // }, - // data: { - // payload: { - // id: "d3fa5829-a393-4398-b12f-678ffdbb0871", - // execution_order: 2, - // kind: "Reference", - // type_field: "Assertion", - // reference: "150d0ae1-af8a-4d03-bafd-299fdc99ffde", - // parent_id: null, - // case_id: "731453aa-95a5-4180-be0d-c211a1e92aad" - // } - // } - // } -]; -const initialEdges = [ - { - id: "actionNodefcb03ff7-4640-4afe-a05b-1d0e08ee9594_to_addNewfcb03ff7-4640-4afe-a05b-1d0e08ee9594", - type: "defaultE", - source: "actionNodefcb03ff7-4640-4afe-a05b-1d0e08ee9594", - target: "actionNoded3fa5829-a393-4398-b12f-678ffdbb0871" - } - // { - // id: "actionNodefcb03ff7-4640-4afe-a05b-1d0e08ee9594_to_addNewfcb03ff7-4640-4afe-a05b-1d0e08ee9594", - // type: "defaultE", - // source: "actionNodefcb03ff7-4640-4afe-a05b-1d0e08ee9594", - // target: "addNewfcb03ff7-4640-4afe-a05b-1d0e08ee9594" - // }, - // { - // id: "actionNodefcb03ff7-4640-4afe-a05b-1d0e08ee9594_to_actionNoded3fa5829-a393-4398-b12f-678ffdbb0871", - // type: "defaultE", - // source: "addNewfcb03ff7-4640-4afe-a05b-1d0e08ee9594", - // target: "actionNoded3fa5829-a393-4398-b12f-678ffdbb0871" - // }, - // { - // id: "actionNoded3fa5829-a393-4398-b12f-678ffdbb0871_to_addNewd3fa5829-a393-4398-b12f-678ffdbb0871", - // type: "defaultE", - // source: "actionNoded3fa5829-a393-4398-b12f-678ffdbb0871", - // target: "addNewd3fa5829-a393-4398-b12f-678ffdbb0871" - // } -]; - -let initNode = [ - { - id: "1", - type: "conditionalNode", - position: { - x: 0, - y: 0 - }, - data: { - label: "actionNode 1" - } - }, - { - id: "2", - type: "actionNode", - position: { - x: 0, - y: 0 - }, - data: { - label: "actionNode 2" - } - }, - { - id: "3", - type: "actionNode", - position: { - x: 0, - y: 0 - }, - data: { - label: "actionNode 3" - } - }, - { - id: "4", - type: "conditionalNode", - position: { - x: 0, - y: 0 - }, - data: { - label: "actionNode 4" - } - }, - { - id: "5", - type: "actionNode", - position: { - x: 0, - y: 0 - }, - data: { - label: "actionNode 5" - } - }, - { - id: "6", - type: "actionNode", - position: { - x: 0, - y: 0 - }, - data: { - label: "actionNode 6 he how are you" - } - } -]; - -let initEdge = [ - { - id: "edge1->2", - type: "yes", - target: "2", - sourceHandle: "yes", - source: "1" - }, - { - id: "edge1->3", - type: "no", - sourceHandle: "no", - - target: "3", - source: "1" - }, - { - id: "3->4", - type: "defaultE", - - target: "4", - source: "3" - }, - { - id: "4->5", - type: "yes", - target: "5", - sourceHandle: "yes", - source: "4" - }, - { - id: "4->6", - type: "no", - sourceHandle: "no", - - target: "6", - source: "4" - } -]; - -const nodeTypes = { - newNode: NewNode, - startNode: StartNode, - actionNode: ActionNode, - conditionalNode: ConditionalNode -}; - -const edgeTypes = { - defaultE: DefaultEdge, - yes: YesEdge, - no: NoEdge -}; - -export default function RFlow() { - const dagreGraph = new graphlib.Graph(); - dagreGraph.setDefaultEdgeLabel(() => ({})); - - const getLayoutedElements = (nodes: Array, edges: Array): any => { - dagreGraph.setGraph({ - rankdir: "TB", - ranker: "network-simplex", - marginx: 30, - marginy: 20 - }); - - const nodeWidth = 400; - const nodeHeight = 100; - let sizeMatrix: any = { - newNode: { width: 28, height: 28 }, - actionNode: { width: 74, height: 69 } - }; - - nodes.forEach((node) => { - let t: string = node["type"]; - let s: any = sizeMatrix[t] || { width: 172, height: 36 }; - console.log("currect value of the width and height ", t, " - ", s); - s = { width: nodeWidth, height: nodeHeight }; - dagreGraph.setNode(node.id, s); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - // dagreGraph.removeEdge - // dagreGraph.setParent - - layout(dagreGraph); - - console.log("result", "graph", dagreGraph.graph()); - - let n: Array = []; - nodes.forEach((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - console.log("result", node.id, nodeWithPosition); - node.targetPosition = "top"; - node.sourcePosition = "bottom"; - - // let t: string = node["type"]; - // let s: any = sizeMatrix[t] || { width: 172, height: 36 }; - - // We are shifting the dagre node position (anchor=center center) to the top left - // so it matches the React Flow node anchor point (top left). - node.position = { - x: nodeWithPosition.x - nodeWidth / 2, - y: nodeWithPosition.y - nodeHeight / 2 - }; - n.push(node); - return node; - }); - - return { nodes: n, edges: edges }; - }; - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - - useEffect(() => { - let result = getLayoutedElements(initNode, initEdge); - console.log("result", result); - setNodes(result["nodes"]); - setEdges(result["edges"]); - }, []); - - return ( -
- - - -
- ); -} diff --git a/web/src/core/components/flow/form.tsx b/web/src/core/components/flow/form.tsx index 0f66a84..66d2d24 100644 --- a/web/src/core/components/flow/form.tsx +++ b/web/src/core/components/flow/form.tsx @@ -1,92 +1,95 @@ -import {Service} from "service"; -import {Select as OSelect} from "core/components/select"; -import {Endpoint} from "service/endpoint"; -import React, {useEffect, useState} from "react"; -import {useFlowStore} from "stores/flow.store"; -import {shallow} from "zustand/shallow"; -import {useParams} from "react-router-dom"; +import { Service } from "service"; +import { Endpoint } from "service/endpoint"; +import { useEffect, useState } from "react"; +import { useFlowStore } from "stores/flow.store"; +import { Button } from "@radix-ui/themes"; -export interface WorkflowFormParm { - title: string; -} +import { shallow } from "zustand/shallow"; +import { useParams } from "react-router-dom"; +import { SearchableDropdown } from "core/components/dropdown/index.jsx"; -export const WorkflowForm: React.FC = ({title}) => { - const [obj, setObject] = useState({} as any); - const {appId = ""} = useParams(); - const [dataSource, setDataSource] = useState([] as any); - const {nodes, edges, currentNode} = useFlowStore( - (state: any) => ({ - nodes: state.nodes, - edges: state.edges, - currentNode: state.currentNode - }), - shallow - ); +import "./index.css" - /** - * fetchActionGroups - fetch all ActionGroup from the specify Application - */ - const fetchActionGroups = async () => { - await Service.get(`${Endpoint.v1.group.getList(appId)}`) - .then((groups) => { - setDataSource(groups); - setObject(currentNode); - }) - .finally(() => { - }); - }; - const onCurrentNode = (node: any) => { - setObject(node); - }; +export const WorkflowForm: React.FC = () => { + const { appId = "" } = useParams(); + const [dataSource, setDataSource] = useState([] as any); + const [actionGroup, setActionGroup] = useState({}); + const { currentNode, graph, setGraph } = useFlowStore( + (state: any) => ({ + currentNode: state.currentNode, + graph: state.graph, + setGraph: state.setGraph, + }), + shallow + ); - useEffect(() => { - fetchActionGroups(); - // onCurrentNode(currentNode); - // console.log(currentNode); - }, []); + /** + * fetchActionGroups - fetch all ActionGroup from the specify Application + */ + const fetchActionGroups = async () => { + await Service.get(`${Endpoint.v1.group.getList(appId)}`) + .then((groups) => { + setDataSource(groups); - return ( - <> -
-
-

- {obj.name} -

-
-
-
- { - console.log(value); - // row["kind"] = value["key"]; - }} - render={(row: any) => { - return {row["name"]}; - }} - > - {/* */} - {/* */} - {/* - - - - {(dataSource || []).map((item: any) => { - return {item.name}; - })} - - - */} -
- - ); + }) + .finally(() => { + }); + }; + + + useEffect(() => { + fetchActionGroups(); + }, []); + + + useEffect(() => { + if (dataSource.length > 0) { + const selectedActionGroup = dataSource.find((item: any) => item.id === currentNode.reference); + setActionGroup(selectedActionGroup || {}); + } + }, [currentNode, dataSource]); + + + const onUpdateActionGroup = (val: any) => { + const newGraph = graph.map((item: any) => { + if (item.id === currentNode.id) { + return { + ...item, + name: val.name, + reference: val.id, + }; + } + return item; + }) + setGraph(newGraph); + } + + return ( + <> +
useFlowStore.getState().setCurrentNode({})} /> +
+
Select action group:
+ { + setActionGroup(val) + }} + /> + {Object.keys(actionGroup).length > 0 && +
+ +
} +
+ + ); }; diff --git a/web/src/core/components/flow/handler/test.tsx b/web/src/core/components/flow/handler/test.tsx index 60ff4bd..0a3eec3 100644 --- a/web/src/core/components/flow/handler/test.tsx +++ b/web/src/core/components/flow/handler/test.tsx @@ -25,12 +25,6 @@ export const CustomHandle: React.FC = ({ const nodeId = useNodeId(); const isHandleConnectable = useMemo(() => { - if (typeof connectionSize === "function") { - const node = nodeInternals.get(nodeId); - const connectedEdges = getConnectedEdges([node], edges); - - // return connectionSize({ node, connectedEdges }); - } if (typeof connectionSize === "number") { const node = nodeInternals.get(nodeId); @@ -46,14 +40,6 @@ export const CustomHandle: React.FC = ({ ); diff --git a/web/src/core/components/flow/index.css b/web/src/core/components/flow/index.css new file mode 100644 index 0000000..f7e595d --- /dev/null +++ b/web/src/core/components/flow/index.css @@ -0,0 +1,27 @@ +.closeForm { + position: absolute; + padding: 4px; + right: 28px; + } + .closeForm:hover { + opacity: 1; + } + .closeForm:before, .closeForm:after { + position: absolute; + left: 15px; + content: ' '; + height: 16px; + width: 2px; + background-color: #333; + } + .closeForm:before { + transform: rotate(45deg); + } + .closeForm:after { + transform: rotate(-45deg); + } + + .workflow{ + height: calc(100% - 66px); + } + \ No newline at end of file diff --git a/web/src/core/components/flow/index.tsx b/web/src/core/components/flow/index.tsx index 9ab6461..4924d1b 100644 --- a/web/src/core/components/flow/index.tsx +++ b/web/src/core/components/flow/index.tsx @@ -1,7 +1,4 @@ import { Workflow } from "./workflow"; +import {classNames} from "./utils/index"; -export { Workflow }; - -export function classNames(...classes: string[]) { - return classes.filter(Boolean).join(" "); -} +export { Workflow, classNames }; \ No newline at end of file diff --git a/web/src/core/components/flow/node.tsx b/web/src/core/components/flow/node.tsx deleted file mode 100644 index 3f479c4..0000000 --- a/web/src/core/components/flow/node.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { ArrowDownCircleIcon } from "@heroicons/react/24/outline"; -import React, { useState } from "react"; -import { NodeProps } from "reactflow"; - -export type CounterData = { - initialCount?: number; -}; - -function classNames(...classes: string[]) { - return classes.filter(Boolean).join(" "); -} - -const handleStyle = { left: 10 }; - -export const StartNode: React.FC> = ({ - data, - isConnectable, - ...restProps -}) => { - const [count, setCount] = useState(data?.initialCount ?? 0); - - return ( - - ); -}; diff --git a/web/src/core/components/flow/node/action.tsx b/web/src/core/components/flow/node/action.tsx index 8a3ec96..28db413 100644 --- a/web/src/core/components/flow/node/action.tsx +++ b/web/src/core/components/flow/node/action.tsx @@ -1,13 +1,24 @@ -import { useEffect, useState } from "react"; -import { NodeProps, Position, useNodeId } from "reactflow"; +import { useEffect } from "react"; +import { NodeProps, Position } from "reactflow"; +import { shallow } from "zustand/shallow"; +import { IconButton } from "@radix-ui/themes"; +import { TrashIcon } from "@heroicons/react/24/outline"; + import { useFlowStore } from "stores/flow.store"; import { classNames } from ".."; import CustomHandle from "../handler/test"; +import "./index.css"; + export const ActionNode: React.FC = ({ data, xPos, yPos }) => { - const [selected, setValueSelected] = useState({} as any); - const [open, setOpen] = useState(false); - const nodeId = useNodeId(); + const { graph, setGraph } = useFlowStore( + (state: any) => ({ + graph: state.graph, + setGraph: state.setGraph, + }), + shallow + ); + let bgColor = data?.payload?.type_field == "Assertion" ? "bg-red-100" : "bg-indigo-100"; @@ -15,6 +26,14 @@ export const ActionNode: React.FC = ({ data, xPos, yPos }) => { bgColor = data?.payload?.type_field == "Assertion" ? "bg-red-100" : "bg-indigo-100"; }, [data]); + + const onDelete = () => { + const newGraph = graph.filter((item: any) => + item.id !== data?.payload.id + ) + setGraph(newGraph); + } + return ( <> = ({ data, xPos, yPos }) => { />
@@ -35,8 +54,16 @@ export const ActionNode: React.FC = ({ data, xPos, yPos }) => { className="self-center p-2 align-middle text-center " onClick={() => useFlowStore.getState().setCurrentNode(data?.payload)} > - [ {data?.payload?.type_field} ] - {data?.payload?.name} + {data?.payload?.name ? data?.payload?.name : `Configure [${data?.payload?.type_field}]`}
+ onDelete()} + > + +
= ({ data, xPos, yPos }) => { - const [selected, setValueSelected] = useState({} as any); - const [open, setOpen] = useState(false); - const nodeId = useNodeId(); return ( <> = ({ data, xPos, yPos }) => {

- If new more content comming into this Node[ Condition ] -{" "} {data?.payload?.name}

@@ -34,7 +29,6 @@ export const ConditionalNode: React.FC = ({ data, xPos, yPos }) => { isConnectable={true} isConnectableEnd={false} /> - ({ - nodeInternals: s.nodeInternals, - edges: s.edges -}); export const EndLoopNode: React.FC = ({ data, xPos, yPos }) => { - const [selected, setValueSelected] = useState({} as any); - const [open, setOpen] = useState(false); - const nodeId = useNodeId(); - const useedges = useEdges(); - - const { nodeInternals } = useStore(selector); - - const node = nodeInternals.get(nodeId); - return ( <> = ({ data, xPos, yPos }) => { />
setOpen(true)} - onMouseOut={() => setOpen(false)} + >
diff --git a/web/src/core/components/flow/node/for.tsx b/web/src/core/components/flow/node/for.tsx index 00e0a22..aae0bc4 100644 --- a/web/src/core/components/flow/node/for.tsx +++ b/web/src/core/components/flow/node/for.tsx @@ -1,10 +1,7 @@ -import { useState } from "react"; import { NodeProps, Position, useNodeId } from "reactflow"; import CustomHandle from "../handler/test"; export const ForNode: React.FC = ({ data, xPos, yPos }) => { - const [selected, setValueSelected] = useState({} as any); - const [open, setOpen] = useState(false); const nodeId = useNodeId(); console.log("x - ", xPos, ", y -", yPos); return ( diff --git a/web/src/core/components/flow/node/index.css b/web/src/core/components/flow/node/index.css new file mode 100644 index 0000000..27c42dd --- /dev/null +++ b/web/src/core/components/flow/node/index.css @@ -0,0 +1,6 @@ +.nodeContainer{ + display: grid; + grid-template-columns: 1fr max-content; + align-items: center; + padding-right: 8px; +} \ No newline at end of file diff --git a/web/src/core/components/flow/node/index.tsx b/web/src/core/components/flow/node/index.tsx deleted file mode 100644 index cb0ff5c..0000000 --- a/web/src/core/components/flow/node/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/web/src/core/components/flow/node/loop.tsx b/web/src/core/components/flow/node/loop.tsx index 352e968..d2289c0 100644 --- a/web/src/core/components/flow/node/loop.tsx +++ b/web/src/core/components/flow/node/loop.tsx @@ -1,11 +1,7 @@ -import { useState } from "react"; -import { NodeProps, Position, useNodeId } from "reactflow"; +import { NodeProps, Position } from "reactflow"; import CustomHandle from "../handler/test"; export const LoopNode: React.FC = ({ data, xPos, yPos }) => { - const [selected, setValueSelected] = useState({} as any); - const [open, setOpen] = useState(false); - const nodeId = useNodeId(); return ( <> ({ - nodeInternals: s.nodeInternals, - edges: s.edges -}); - interface Option { key: string; label: string; @@ -27,41 +21,31 @@ type MyObject = { [key: string]: () => any }; export const NewNode: React.FC = ({ data, xPos, yPos }) => { const options: Array