From 5ba330b7e16610a6ebdc10c57aa67996e9bff6b6 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Tue, 29 Oct 2024 17:36:00 +0800 Subject: [PATCH 1/2] add issue api and page --- .github/workflows/base.yml | 2 +- Cargo.toml | 8 +- ceres/src/api_service/mono_api_service.rs | 68 ++------- ceres/src/model/mod.rs | 1 - ceres/src/pack/monorepo.rs | 21 +-- ceres/src/protocol/mr.rs | 8 +- common/Cargo.toml | 1 + common/src/model.rs | 10 +- common/src/utils.rs | 10 ++ docker/mono-pg-dockerfile | 2 +- jupiter/callisto/src/lib.rs | 2 +- .../{mega_mr_conv.rs => mega_conversation.rs} | 6 +- jupiter/callisto/src/mega_issue.rs | 9 +- jupiter/callisto/src/mega_mr.rs | 2 +- jupiter/callisto/src/prelude.rs | 2 +- jupiter/src/context.rs | 42 +++++- jupiter/src/storage/init.rs | 2 +- jupiter/src/storage/issue_storage.rs | 130 ++++++++++++++++ jupiter/src/storage/mod.rs | 2 + jupiter/src/storage/mono_storage.rs | 112 +------------- jupiter/src/storage/mr_storage.rs | 139 ++++++++++++++++++ mono/src/api/api_router.rs | 4 +- mono/src/api/issue/issue_router.rs | 127 ++++++++++++++++ mono/src/api/issue/mod.rs | 60 ++++++++ mono/src/api/mod.rs | 30 ++-- .../src/model/mr.rs => mono/src/api/mr/mod.rs | 43 +++--- mono/src/api/{ => mr}/model.rs | 0 mono/src/api/{ => mr}/mr_router.rs | 85 +++++++---- mono/src/api/oauth/mod.rs | 7 +- mono/src/api/user/user_router.rs | 45 +----- mono/src/git_protocol/http.rs | 5 +- mono/src/git_protocol/ssh.rs | 3 +- moon/package.json | 10 +- moon/src/app/(dashboard)/issue/[id]/page.tsx | 132 +++++++++++++++++ moon/src/app/(dashboard)/issue/new/page.tsx | 67 +++++++++ moon/src/app/(dashboard)/issue/page.tsx | 122 ++++++++++++++- moon/src/app/(dashboard)/mr/page.tsx | 6 +- moon/src/app/api/issue/[id]/close/route.ts | 9 ++ moon/src/app/api/issue/[id]/comment/route.ts | 21 +++ moon/src/app/api/issue/[id]/detail/route.ts | 9 ++ moon/src/app/api/issue/list/route.ts | 19 +++ moon/src/app/api/issue/new/route.ts | 27 ++++ moon/src/app/lib/dal.ts | 9 +- moon/src/middleware.ts | 4 +- moon/tsconfig.json | 8 +- ...241023__init.sql => pg_20241029__init.sql} | 17 ++- ...1023_init.sql => sqlite_20241029_init.sql} | 8 +- 47 files changed, 1106 insertions(+), 350 deletions(-) rename jupiter/callisto/src/{mega_mr_conv.rs => mega_conversation.rs} (81%) create mode 100644 jupiter/src/storage/issue_storage.rs create mode 100644 jupiter/src/storage/mr_storage.rs create mode 100644 mono/src/api/issue/issue_router.rs create mode 100644 mono/src/api/issue/mod.rs rename ceres/src/model/mr.rs => mono/src/api/mr/mod.rs (80%) rename mono/src/api/{ => mr}/model.rs (100%) rename mono/src/api/{ => mr}/mr_router.rs (56%) create mode 100644 moon/src/app/(dashboard)/issue/[id]/page.tsx create mode 100644 moon/src/app/(dashboard)/issue/new/page.tsx create mode 100644 moon/src/app/api/issue/[id]/close/route.ts create mode 100644 moon/src/app/api/issue/[id]/comment/route.ts create mode 100644 moon/src/app/api/issue/[id]/detail/route.ts create mode 100644 moon/src/app/api/issue/list/route.ts create mode 100644 moon/src/app/api/issue/new/route.ts rename sql/postgres/{pg_20241023__init.sql => pg_20241029__init.sql} (96%) rename sql/sqlite/{sqlite_20241023_init.sql => sqlite_20241029_init.sql} (98%) diff --git a/.github/workflows/base.yml b/.github/workflows/base.yml index 65134df39..34af609b2 100644 --- a/.github/workflows/base.yml +++ b/.github/workflows/base.yml @@ -132,7 +132,7 @@ jobs: - name: Install dependencies working-directory: ./moon - run: npm install + run: npm install --force - name: Run ESLint working-directory: ./moon diff --git a/Cargo.toml b/Cargo.toml index 58d3c9826..e0c03202b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ mono = { path = "mono" } libra = { path = "libra" } anyhow = "1.0.90" -serde = "1.0.210" +serde = "1.0.214" serde_json = "1.0.132" tracing = "0.1.40" tracing-subscriber = "0.3.18" @@ -72,11 +72,11 @@ colored = "2.1.0" idgenerator = "2.0.0" num_cpus = "1.16.0" config = "0.14.1" -shadow-rs = "0.35.1" -reqwest = "0.12.8" +shadow-rs = "0.35.2" +reqwest = "0.12.9" lazy_static = "1.5.0" uuid = "1.11.0" -regex = "1.11.0" +regex = "1.11.1" ed25519-dalek = "2.1.1" ctrlc = "3.4.4" git2 = "0.19.0" diff --git a/ceres/src/api_service/mono_api_service.rs b/ceres/src/api_service/mono_api_service.rs index 027641804..dcbc7c9e1 100644 --- a/ceres/src/api_service/mono_api_service.rs +++ b/ceres/src/api_service/mono_api_service.rs @@ -4,10 +4,9 @@ use std::str::FromStr; use axum::async_trait; -use callisto::db_enums::{ConvType, MergeStatus}; +use callisto::db_enums::ConvType; use callisto::{mega_blob, mega_tree, raw_blob}; use common::errors::MegaError; -use common::model::PageParams; use jupiter::context::Context; use jupiter::storage::batch_save_model; use jupiter::utils::converter::generate_git_keep_with_timestamp; @@ -19,7 +18,6 @@ use mercury::internal::object::tree::{Tree, TreeItem, TreeItemMode}; use crate::api_service::ApiHandler; use crate::model::create_file::CreateFileInfo; -use crate::model::mr::{MRDetail, MrInfoItem}; use crate::protocol::mr::MergeRequest; #[derive(Clone)] @@ -196,41 +194,9 @@ impl ApiHandler for MonoApiService { } impl MonoApiService { - pub async fn mr_list( - &self, - status: &str, - pagination: PageParams, - ) -> Result<(Vec, u64), MegaError> { - let status = if status == "open" { - vec![MergeStatus::Open] - } else if status == "closed" { - vec![MergeStatus::Closed, MergeStatus::Merged] - } else { - vec![MergeStatus::Open, MergeStatus::Closed, MergeStatus::Merged] - }; - let storage = self.context.services.mono_storage.clone(); - let (mr_list, total) = storage - .get_mr_by_status(status, pagination.page, pagination.per_page) - .await - .unwrap(); - Ok((mr_list.into_iter().map(|m| m.into()).collect(), total)) - } - - pub async fn mr_detail(&self, mr_link: &str) -> Result, MegaError> { - let storage = self.context.services.mono_storage.clone(); - let model = storage.get_mr(mr_link).await.unwrap(); - if let Some(model) = model { - let mut detail: MRDetail = model.into(); - let conversions = storage.get_mr_conversations(mr_link).await.unwrap(); - detail.conversions = conversions.into_iter().map(|x| x.into()).collect(); - return Ok(Some(detail)); - } - Ok(None) - } - - pub async fn mr_tree_files(&self, mr_link: &str) -> Result, MegaError> { + pub async fn mr_tree_files(&self, link: &str) -> Result, MegaError> { let storage = self.context.services.mono_storage.clone(); - let model = storage.get_mr(mr_link).await.unwrap(); + let model = self.context.mr_stg().get_mr(link).await.unwrap(); if let Some(model) = model { let to_tree_id = storage .get_commit_by_hash(&model.to_hash) @@ -312,35 +278,23 @@ impl MonoApiService { // update mr mr.merge(); // add conversation - storage - .add_mr_conversation(&mr.mr_link, 0, ConvType::Merged, None) + self.context + .mr_stg() + .add_mr_conversation(&mr.link, 0, ConvType::Merged, None) .await .unwrap(); // update mr status last - storage.update_mr(mr.clone().into()).await.unwrap(); - } else { - return Err(MegaError::with_message("ref hash conflict")); - } - Ok(()) - } - - pub async fn comment(&self, mr_link: &str, comment: String) -> Result<(), MegaError> { - let storage = self.context.services.mono_storage.clone(); - if let Some(model) = storage.get_mr(mr_link).await.unwrap() { - storage - .add_mr_conversation(&model.mr_link, 0, ConvType::Comment, Some(comment)) + self.context + .mr_stg() + .update_mr(mr.clone().into()) .await .unwrap(); + } else { + return Err(MegaError::with_message("ref hash conflict")); } Ok(()) } - pub async fn delete_comment(&self, id: i64) -> Result<(), MegaError> { - let storage = self.context.services.mono_storage.clone(); - storage.remove_mr_conversation(id).await.unwrap(); - Ok(()) - } - async fn update_parent_tree( &self, mut path: PathBuf, diff --git a/ceres/src/model/mod.rs b/ceres/src/model/mod.rs index e8e90291e..58f0fb205 100644 --- a/ceres/src/model/mod.rs +++ b/ceres/src/model/mod.rs @@ -1,4 +1,3 @@ pub mod create_file; -pub mod mr; pub mod query; pub mod tree; diff --git a/ceres/src/pack/monorepo.rs b/ceres/src/pack/monorepo.rs index df3c9a96a..64acc3d93 100644 --- a/ceres/src/pack/monorepo.rs +++ b/ceres/src/pack/monorepo.rs @@ -11,13 +11,12 @@ use std::{ use async_trait::async_trait; use futures::future::join_all; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; use callisto::{db_enums::ConvType, raw_blob}; -use common::{errors::MegaError, utils::MEGA_BRANCH_NAME}; -use jupiter::{context::Context, storage::mono_storage::MonoStorage}; +use common::{errors::MegaError, utils::{self, MEGA_BRANCH_NAME}}; +use jupiter::{context::Context, storage::mr_storage::MrStorage}; use mercury::internal::{object::ObjectTrait, pack::encode::PackEncoder}; use mercury::{ errors::GitError, @@ -121,7 +120,7 @@ impl PackHandler for MonoRepo { } async fn handle_receiver(&self, receiver: Receiver) -> Result<(), GitError> { - let storage = self.context.services.mono_storage.clone(); + let storage = self.context.mr_stg(); let path_str = self.path.to_str().unwrap(); let unpack_res = self.save_entry(receiver).await; @@ -132,16 +131,12 @@ impl PackHandler for MonoRepo { self.handle_existing_mr(&mut mr, &storage).await } None => { - let mr_link: String = thread_rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect(); + let link: String = utils::generate_link(); let mr = MergeRequest { path: path_str.to_owned(), from_hash: self.from_hash.clone(), to_hash: self.to_hash.clone(), - mr_link: mr_link.to_uppercase(), + link, title, ..Default::default() }; @@ -334,14 +329,14 @@ impl MonoRepo { async fn handle_existing_mr( &self, mr: &mut MergeRequest, - storage: &MonoStorage, + storage: &MrStorage, ) -> Result<(), GitError> { if mr.from_hash == self.from_hash { if mr.to_hash != self.to_hash { let comment = self.comment_for_force_update(&mr.to_hash, &self.to_hash); mr.to_hash = self.to_hash.clone(); storage - .add_mr_conversation(&mr.mr_link, 0, ConvType::ForcePush, Some(comment)) + .add_mr_conversation(&mr.link, 0, ConvType::ForcePush, Some(comment)) .await .unwrap(); } else { @@ -351,7 +346,7 @@ impl MonoRepo { mr.close(); storage .add_mr_conversation( - &mr.mr_link, + &mr.link, 0, ConvType::Closed, Some("Mega closed MR due to conflict".to_string()), diff --git a/ceres/src/protocol/mr.rs b/ceres/src/protocol/mr.rs index cd8b611e1..4edefa333 100644 --- a/ceres/src/protocol/mr.rs +++ b/ceres/src/protocol/mr.rs @@ -6,7 +6,7 @@ use common::utils::generate_id; #[derive(Clone)] pub struct MergeRequest { pub id: i64, - pub mr_link: String, + pub link: String, pub title: String, pub status: MergeStatus, pub merge_date: Option, @@ -19,7 +19,7 @@ impl Default for MergeRequest { fn default() -> Self { Self { id: generate_id(), - mr_link: String::new(), + link: String::new(), title: String::new(), status: MergeStatus::Open, merge_date: None, @@ -45,7 +45,7 @@ impl From for mega_mr::Model { fn from(value: MergeRequest) -> Self { Self { id: value.id, - mr_link: value.mr_link, + link: value.link, title: value.title, status: value.status, merge_date: value.merge_date, @@ -62,7 +62,7 @@ impl From for MergeRequest { fn from(value: mega_mr::Model) -> Self { Self { id: value.id, - mr_link: value.mr_link, + link: value.link, title: value.title, status: value.status, merge_date: value.merge_date, diff --git a/common/Cargo.toml b/common/Cargo.toml index a35933105..fc52503e4 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -19,3 +19,4 @@ idgenerator = { workspace = true } serde = { workspace = true } config = { workspace = true } envsubst = "0.2.1" +rand = { workspace = true } diff --git a/common/src/model.rs b/common/src/model.rs index b4107082b..ece39aa15 100644 --- a/common/src/model.rs +++ b/common/src/model.rs @@ -50,14 +50,14 @@ impl CommonResult { } #[derive(Deserialize)] -pub struct PageParams { +pub struct Pagination { pub page: u64, pub per_page: u64, } -impl Default for PageParams { +impl Default for Pagination { fn default() -> Self { - PageParams { + Pagination { page: 1, per_page: 20, } @@ -65,8 +65,8 @@ impl Default for PageParams { } #[derive(Deserialize)] -pub struct RequestParams { - pub pagination: PageParams, +pub struct PageParams { + pub pagination: Pagination, pub additional: T, } diff --git a/common/src/utils.rs b/common/src/utils.rs index ca7b27c85..1c9cd5a63 100644 --- a/common/src/utils.rs +++ b/common/src/utils.rs @@ -1,4 +1,5 @@ use idgenerator::IdInstance; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; pub const ZERO_ID: &str = match std::str::from_utf8(&[b'0'; 40]) { Ok(s) => s, @@ -10,4 +11,13 @@ pub fn generate_id() -> i64 { IdInstance::next_id() } +pub fn generate_link() -> String { + let str: String = thread_rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect(); + str.to_uppercase() +} + pub const MEGA_BRANCH_NAME: &str = "refs/heads/main"; diff --git a/docker/mono-pg-dockerfile b/docker/mono-pg-dockerfile index 023a253c7..fba17cc5b 100644 --- a/docker/mono-pg-dockerfile +++ b/docker/mono-pg-dockerfile @@ -17,6 +17,6 @@ EXPOSE 5432 # Add the database initialization script to the container # When the container starts, PostgreSQL will automatically execute all .sql files in the docker-entrypoint-initdb.d/ directory -COPY ./sql/postgres/pg_20241023__init.sql /docker-entrypoint-initdb.d/ +COPY ./sql/postgres/pg_20241029__init.sql /docker-entrypoint-initdb.d/ CMD ["postgres"] diff --git a/jupiter/callisto/src/lib.rs b/jupiter/callisto/src/lib.rs index 1154dd0ca..d1aa8a4ab 100644 --- a/jupiter/callisto/src/lib.rs +++ b/jupiter/callisto/src/lib.rs @@ -19,7 +19,7 @@ pub mod mega_blob; pub mod mega_commit; pub mod mega_issue; pub mod mega_mr; -pub mod mega_mr_conv; +pub mod mega_conversation; pub mod mega_refs; pub mod mega_tag; pub mod mega_tree; diff --git a/jupiter/callisto/src/mega_mr_conv.rs b/jupiter/callisto/src/mega_conversation.rs similarity index 81% rename from jupiter/callisto/src/mega_mr_conv.rs rename to jupiter/callisto/src/mega_conversation.rs index 1f564804c..c71ba10c4 100644 --- a/jupiter/callisto/src/mega_mr_conv.rs +++ b/jupiter/callisto/src/mega_conversation.rs @@ -1,15 +1,15 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 use sea_orm::entity::prelude::*; use crate::db_enums::ConvType; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] -#[sea_orm(table_name = "mega_mr_conv")] +#[sea_orm(table_name = "mega_conversation")] pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: i64, - pub mr_link: String, + pub link: String, pub user_id: i64, pub conv_type: ConvType, #[sea_orm(column_type = "Text", nullable)] diff --git a/jupiter/callisto/src/mega_issue.rs b/jupiter/callisto/src/mega_issue.rs index fd59ac0a9..bbd60d545 100644 --- a/jupiter/callisto/src/mega_issue.rs +++ b/jupiter/callisto/src/mega_issue.rs @@ -1,4 +1,4 @@ -//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0 +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0 use sea_orm::entity::prelude::*; @@ -7,11 +7,10 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: i64, - pub number: i64, + pub link: String, pub title: String, - pub sender_name: String, - pub sender_id: i64, - pub state: String, + pub owner: i64, + pub status: String, pub created_at: DateTime, pub updated_at: DateTime, pub closed_at: Option, diff --git a/jupiter/callisto/src/mega_mr.rs b/jupiter/callisto/src/mega_mr.rs index 570b053ec..692dd2827 100644 --- a/jupiter/callisto/src/mega_mr.rs +++ b/jupiter/callisto/src/mega_mr.rs @@ -9,7 +9,7 @@ use crate::db_enums::MergeStatus; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: i64, - pub mr_link: String, + pub link: String, pub title: String, pub merge_date: Option, pub status: MergeStatus, diff --git a/jupiter/callisto/src/prelude.rs b/jupiter/callisto/src/prelude.rs index 575975f4c..88065886e 100644 --- a/jupiter/callisto/src/prelude.rs +++ b/jupiter/callisto/src/prelude.rs @@ -16,7 +16,7 @@ pub use crate::mega_blob::Entity as MegaBlob; pub use crate::mega_commit::Entity as MegaCommit; pub use crate::mega_issue::Entity as MegaIssue; pub use crate::mega_mr::Entity as MegaMr; -pub use crate::mega_mr_conv::Entity as MegaMrConv; +pub use crate::mega_conversation::Entity as MegaMrConv; pub use crate::mega_refs::Entity as MegaRefs; pub use crate::mega_tag::Entity as MegaTag; pub use crate::mega_tree::Entity as MegaTree; diff --git a/jupiter/src/context.rs b/jupiter/src/context.rs index 7d68e3432..d3aee9c51 100644 --- a/jupiter/src/context.rs +++ b/jupiter/src/context.rs @@ -5,9 +5,10 @@ use common::config::Config; use crate::{ lfs_storage::{local_storage::LocalStorage, LfsStorage}, storage::{ - git_db_storage::GitDbStorage, init::database_connection, lfs_db_storage::LfsDbStorage, - mono_storage::MonoStorage, mq_storage::MQStorage, raw_db_storage::RawDbStorage, - user_storage::UserStorage, ztm_storage::ZTMStorage, + git_db_storage::GitDbStorage, init::database_connection, issue_storage::IssueStorage, + lfs_db_storage::LfsDbStorage, mono_storage::MonoStorage, mq_storage::MQStorage, + mr_storage::MrStorage, raw_db_storage::RawDbStorage, user_storage::UserStorage, + ztm_storage::ZTMStorage, }, }; @@ -24,6 +25,19 @@ impl Context { config, } } + + pub fn user_stg(&self) -> UserStorage { + self.services.user_storage() + } + + pub fn issue_stg(&self) -> IssueStorage { + self.services.issue_storage() + } + + pub fn mr_stg(&self) -> MrStorage { + self.services.mr_storage() + } + pub fn mock() -> Self { Context { services: Service::mock(), @@ -40,7 +54,9 @@ pub struct Service { pub lfs_db_storage: LfsDbStorage, pub ztm_storage: ZTMStorage, pub mq_storage: MQStorage, - pub user_storage: UserStorage, + user_storage: UserStorage, + mr_storage: MrStorage, + issue_storage: IssueStorage, pub lfs_storage: Arc, } @@ -49,12 +65,14 @@ impl Service { let connection = Arc::new(database_connection(&config.database).await); Service { mono_storage: MonoStorage::new(connection.clone()).await, - git_db_storage:GitDbStorage::new(connection.clone()).await, + git_db_storage: GitDbStorage::new(connection.clone()).await, raw_db_storage: RawDbStorage::new(connection.clone()).await, lfs_db_storage: LfsDbStorage::new(connection.clone()).await, ztm_storage: ZTMStorage::new(connection.clone()).await, mq_storage: MQStorage::new(connection.clone()).await, user_storage: UserStorage::new(connection.clone()).await, + mr_storage: MrStorage::new(connection.clone()).await, + issue_storage: IssueStorage::new(connection.clone()).await, lfs_storage: Arc::new(LocalStorage::init(config.lfs.lfs_obj_local_path.clone())), } } @@ -63,6 +81,18 @@ impl Service { Arc::new(Self::new(config).await) } + pub fn issue_storage(&self) -> IssueStorage { + self.issue_storage.clone() + } + + pub fn mr_storage(&self) -> MrStorage { + self.mr_storage.clone() + } + + pub fn user_storage(&self) -> UserStorage { + self.user_storage.clone() + } + fn mock() -> Arc { Arc::new(Self { mono_storage: MonoStorage::mock(), @@ -75,6 +105,8 @@ impl Service { lfs_storage: Arc::new(LocalStorage::init( PathBuf::from(env::current_dir().unwrap().parent().unwrap()).join("tests"), )), + mr_storage: MrStorage::mock(), + issue_storage: IssueStorage::mock(), }) } } diff --git a/jupiter/src/storage/init.rs b/jupiter/src/storage/init.rs index c565c8293..48b1bae4d 100644 --- a/jupiter/src/storage/init.rs +++ b/jupiter/src/storage/init.rs @@ -51,7 +51,7 @@ async fn setup_sql(conn: &DatabaseConnection) -> Result<(), TransactionError, +} + +impl IssueStorage { + pub fn get_connection(&self) -> &DatabaseConnection { + &self.connection + } + + pub async fn new(connection: Arc) -> Self { + IssueStorage { connection } + } + + pub fn mock() -> Self { + IssueStorage { + connection: Arc::new(DatabaseConnection::default()), + } + } + + pub async fn get_issue_by_status( + &self, + status: &str, + page: Pagination, + ) -> Result<(Vec, u64), MegaError> { + let paginator = mega_issue::Entity::find() + .filter(mega_issue::Column::Status.eq(status)) + .order_by_desc(mega_issue::Column::CreatedAt) + .paginate(self.get_connection(), page.per_page); + let num_pages = paginator.num_items().await?; + Ok(paginator + .fetch_page(page.page - 1) + .await + .map(|m| (m, num_pages))?) + } + + pub async fn get_issue(&self, link: &str) -> Result, MegaError> { + let model = mega_issue::Entity::find() + .filter(mega_issue::Column::Link.eq(link)) + .one(self.get_connection()) + .await + .unwrap(); + Ok(model) + } + + pub async fn save_issue( + &self, + user_id: i64, + title: &str, + ) -> Result { + let model = mega_issue::Model { + id: generate_id(), + link: generate_link(), + title: title.to_owned(), + owner: user_id, + status: "open".to_owned(), + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + closed_at: None, + }; + model + .clone() + .into_active_model() + .insert(self.get_connection()) + .await + .unwrap(); + Ok(model) + } + + pub async fn close_issue(&self, link: &str) -> Result<(), MegaError> { + if let Some(model) = self.get_issue(link).await.unwrap() { + let mut issue = model.into_active_model(); + issue.status = Set("closed".to_owned()); + issue.update(self.get_connection()).await.unwrap(); + }; + Ok(()) + } + + pub async fn get_issue_conversations( + &self, + link: &str, + ) -> Result, MegaError> { + let model = mega_conversation::Entity::find() + .filter(mega_conversation::Column::Link.eq(link)) + .all(self.get_connection()) + .await; + Ok(model?) + } + + pub async fn remove_issue_conversation(&self, id: i64) -> Result<(), MegaError> { + mega_conversation::Entity::delete_by_id(id) + .exec(self.get_connection()) + .await + .unwrap(); + Ok(()) + } + + pub async fn add_issue_conversation( + &self, + link: &str, + user_id: i64, + comment: Option, + ) -> Result { + let conversation = mega_conversation::Model { + id: generate_id(), + link: link.to_owned(), + user_id, + conv_type: ConvType::Comment, + comment, + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + }; + let conversation = conversation.into_active_model(); + let res = conversation.insert(self.get_connection()).await.unwrap(); + Ok(res.id) + } +} diff --git a/jupiter/src/storage/mod.rs b/jupiter/src/storage/mod.rs index 377ce5195..e4c24989b 100644 --- a/jupiter/src/storage/mod.rs +++ b/jupiter/src/storage/mod.rs @@ -1,8 +1,10 @@ pub mod git_db_storage; pub mod init; +pub mod issue_storage; pub mod lfs_db_storage; pub mod mono_storage; pub mod mq_storage; +pub mod mr_storage; pub mod raw_db_storage; pub mod user_storage; pub mod ztm_storage; diff --git a/jupiter/src/storage/mono_storage.rs b/jupiter/src/storage/mono_storage.rs index 9d11a45ef..97a387cd6 100644 --- a/jupiter/src/storage/mono_storage.rs +++ b/jupiter/src/storage/mono_storage.rs @@ -1,15 +1,12 @@ use std::sync::{Arc, Mutex}; use futures::{stream, StreamExt}; -use sea_orm::ActiveValue::NotSet; use sea_orm::{ - ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, QueryFilter, + QuerySelect, }; -use callisto::db_enums::{ConvType, MergeStatus}; -use callisto::{ - mega_blob, mega_commit, mega_mr, mega_mr_conv, mega_refs, mega_tag, mega_tree, raw_blob, -}; +use callisto::{mega_blob, mega_commit, mega_refs, mega_tag, mega_tree, raw_blob}; use common::config::MonoConfig; use common::errors::MegaError; use common::utils::generate_id; @@ -102,109 +99,6 @@ impl MonoStorage { Ok(()) } - pub async fn get_open_mr_by_path( - &self, - path: &str, - ) -> Result, MegaError> { - let model = mega_mr::Entity::find() - .filter(mega_mr::Column::Path.eq(path)) - .filter(mega_mr::Column::Status.eq(MergeStatus::Open)) - .one(self.get_connection()) - .await - .unwrap(); - Ok(model) - } - - pub async fn get_mr_by_status( - &self, - status: Vec, - page: u64, - per_page: u64, - ) -> Result<(Vec, u64), MegaError> { - let paginator = mega_mr::Entity::find() - .filter(mega_mr::Column::Status.is_in(status)) - .order_by_desc(mega_mr::Column::CreatedAt) - .paginate(self.get_connection(),per_page); - let num_pages = paginator.num_items().await?; - Ok(paginator.fetch_page(page -1).await.map(|m| (m, num_pages))?) - } - - pub async fn get_mr(&self, mr_link: &str) -> Result, MegaError> { - let model = mega_mr::Entity::find() - .filter(mega_mr::Column::MrLink.eq(mr_link)) - .one(self.get_connection()) - .await - .unwrap(); - Ok(model) - } - - pub async fn get_open_mr_by_link( - &self, - mr_link: &str, - ) -> Result, MegaError> { - let model = mega_mr::Entity::find() - .filter(mega_mr::Column::MrLink.eq(mr_link)) - .filter(mega_mr::Column::Status.eq(MergeStatus::Open)) - .one(self.get_connection()) - .await - .unwrap(); - Ok(model) - } - - pub async fn save_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { - let a_model = mr.into_active_model(); - a_model.insert(self.get_connection()).await.unwrap(); - Ok(()) - } - - pub async fn update_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { - let mut a_model = mr.into_active_model(); - a_model = a_model.reset_all(); - a_model.created_at = NotSet; - a_model.update(self.get_connection()).await.unwrap(); - Ok(()) - } - - pub async fn get_mr_conversations( - &self, - mr_link: &str, - ) -> Result, MegaError> { - let model = mega_mr_conv::Entity::find() - .filter(mega_mr_conv::Column::MrLink.eq(mr_link)) - .all(self.get_connection()) - .await; - Ok(model?) - } - - pub async fn remove_mr_conversation(&self, id: i64) -> Result<(), MegaError> { - mega_mr_conv::Entity::delete_by_id(id) - .exec(self.get_connection()) - .await - .unwrap(); - Ok(()) - } - - pub async fn add_mr_conversation( - &self, - mr_link: &str, - user_id: i64, - conv_type: ConvType, - comment: Option, - ) -> Result { - let conversation = mega_mr_conv::Model { - id: generate_id(), - mr_link: mr_link.to_owned(), - user_id, - conv_type, - comment, - created_at: chrono::Utc::now().naive_utc(), - updated_at: chrono::Utc::now().naive_utc(), - }; - let conversation = conversation.into_active_model(); - let res = conversation.insert(self.get_connection()).await.unwrap(); - Ok(res.id) - } - pub async fn save_entry( &self, commit_id: &str, diff --git a/jupiter/src/storage/mr_storage.rs b/jupiter/src/storage/mr_storage.rs new file mode 100644 index 000000000..dd615e734 --- /dev/null +++ b/jupiter/src/storage/mr_storage.rs @@ -0,0 +1,139 @@ +use std::sync::Arc; + +use sea_orm::ActiveValue::NotSet; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, IntoActiveModel, + PaginatorTrait, QueryFilter, QueryOrder, +}; + +use callisto::db_enums::{ConvType, MergeStatus}; +use callisto::{mega_conversation, mega_mr}; +use common::errors::MegaError; +use common::utils::generate_id; + +#[derive(Clone)] +pub struct MrStorage { + pub connection: Arc, +} + +impl MrStorage { + pub fn get_connection(&self) -> &DatabaseConnection { + &self.connection + } + + pub async fn new(connection: Arc) -> Self { + MrStorage { connection } + } + + pub fn mock() -> Self { + MrStorage { + connection: Arc::new(DatabaseConnection::default()), + } + } + + pub async fn get_open_mr_by_path( + &self, + path: &str, + ) -> Result, MegaError> { + let model = mega_mr::Entity::find() + .filter(mega_mr::Column::Path.eq(path)) + .filter(mega_mr::Column::Status.eq(MergeStatus::Open)) + .one(self.get_connection()) + .await + .unwrap(); + Ok(model) + } + + pub async fn get_mr_by_status( + &self, + status: Vec, + page: u64, + per_page: u64, + ) -> Result<(Vec, u64), MegaError> { + let paginator = mega_mr::Entity::find() + .filter(mega_mr::Column::Status.is_in(status)) + .order_by_desc(mega_mr::Column::CreatedAt) + .paginate(self.get_connection(), per_page); + let num_pages = paginator.num_items().await?; + Ok(paginator + .fetch_page(page - 1) + .await + .map(|m| (m, num_pages))?) + } + + pub async fn get_mr(&self, link: &str) -> Result, MegaError> { + let model = mega_mr::Entity::find() + .filter(mega_mr::Column::Link.eq(link)) + .one(self.get_connection()) + .await + .unwrap(); + Ok(model) + } + + pub async fn get_open_mr_by_link( + &self, + link: &str, + ) -> Result, MegaError> { + let model = mega_mr::Entity::find() + .filter(mega_mr::Column::Link.eq(link)) + .filter(mega_mr::Column::Status.eq(MergeStatus::Open)) + .one(self.get_connection()) + .await + .unwrap(); + Ok(model) + } + + pub async fn save_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { + let a_model = mr.into_active_model(); + a_model.insert(self.get_connection()).await.unwrap(); + Ok(()) + } + + pub async fn update_mr(&self, mr: mega_mr::Model) -> Result<(), MegaError> { + let mut a_model = mr.into_active_model(); + a_model = a_model.reset_all(); + a_model.created_at = NotSet; + a_model.update(self.get_connection()).await.unwrap(); + Ok(()) + } + + pub async fn get_mr_conversations( + &self, + link: &str, + ) -> Result, MegaError> { + let model = mega_conversation::Entity::find() + .filter(mega_conversation::Column::Link.eq(link)) + .all(self.get_connection()) + .await; + Ok(model?) + } + + pub async fn remove_mr_conversation(&self, id: i64) -> Result<(), MegaError> { + mega_conversation::Entity::delete_by_id(id) + .exec(self.get_connection()) + .await + .unwrap(); + Ok(()) + } + + pub async fn add_mr_conversation( + &self, + link: &str, + user_id: i64, + conv_type: ConvType, + comment: Option, + ) -> Result { + let conversation = mega_conversation::Model { + id: generate_id(), + link: link.to_owned(), + user_id, + conv_type, + comment, + created_at: chrono::Utc::now().naive_utc(), + updated_at: chrono::Utc::now().naive_utc(), + }; + let conversation = conversation.into_active_model(); + let res = conversation.insert(self.get_connection()).await.unwrap(); + Ok(res.id) + } +} diff --git a/mono/src/api/api_router.rs b/mono/src/api/api_router.rs index 51d94534a..8352b6a12 100644 --- a/mono/src/api/api_router.rs +++ b/mono/src/api/api_router.rs @@ -21,7 +21,8 @@ use common::{errors::ProtocolError, model::CommonResult}; use taurus::event::api_request::{ApiRequestEvent, ApiType}; use crate::api::error::ApiError; -use crate::api::mr_router; +use crate::api::issue::issue_router; +use crate::api::mr::mr_router; use crate::api::user::user_router; use crate::api::MonoApiServiceState; @@ -40,6 +41,7 @@ pub fn routers() -> Router { .merge(router) .merge(mr_router::routers()) .merge(user_router::routers()) + .merge(issue_router::routers()) } async fn get_blob_string( diff --git a/mono/src/api/issue/issue_router.rs b/mono/src/api/issue/issue_router.rs new file mode 100644 index 000000000..705a02ab9 --- /dev/null +++ b/mono/src/api/issue/issue_router.rs @@ -0,0 +1,127 @@ +use axum::{ + extract::{Path, State}, + routing::{get, post}, + Json, Router, +}; +use bytes::Bytes; +use serde::Deserialize; + +use common::model::{CommonPage, CommonResult, PageParams}; + +use crate::api::error::ApiError; +use crate::api::issue::{IssueDetail, IssueItem, NewIssue}; +use crate::api::oauth::model::LoginUser; +use crate::api::MonoApiServiceState; + +pub fn routers() -> Router { + Router::new() + .route("/issue/list", post(fetch_issue_list)) + .route("/issue/new", post(new_issue)) + .route("/issue/:link/close", post(close_issue)) + .route("/issue/:link/detail", get(issue_detail)) + .route("/issue/:link/comment", post(save_comment)) + .route("/issue/comment/:id/delete", post(delete_comment)) +} + +#[derive(Deserialize)] +pub struct StatusParams { + pub status: String, +} + +async fn fetch_issue_list( + state: State, + Json(json): Json>, +) -> Result>>, ApiError> { + let res = state + .issue_stg() + .get_issue_by_status(&json.additional.status, json.pagination) + .await; + let res = match res { + Ok((items, total)) => CommonResult::success(Some(CommonPage { + items: items.into_iter().map(|m| m.into()).collect(), + total, + })), + + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} + +async fn issue_detail( + Path(link): Path, + state: State, +) -> Result>, ApiError> { + let res = match state.issue_stg().get_issue(&link).await { + Ok(data) => { + if let Some(model) = data { + let mut detail: IssueDetail = model.into(); + let conversions = state.mr_stg().get_mr_conversations(&link).await.unwrap(); + detail.conversions = conversions.into_iter().map(|x| x.into()).collect(); + CommonResult::success(Some(detail)) + } else { + CommonResult::success(None) + } + } + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} + +async fn new_issue( + user: LoginUser, + state: State, + Json(json): Json, +) -> Result>, ApiError> { + let stg = state.issue_stg().clone(); + let res = stg.save_issue(user.user_id, &json.title).await.unwrap(); + let res = stg + .add_issue_conversation(&res.link, user.user_id, Some(json.description)) + .await; + let res = match res { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} + +async fn close_issue( + Path(link): Path, + state: State, +) -> Result>, ApiError> { + let res = match state.issue_stg().close_issue(&link).await { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} + +async fn save_comment( + user: LoginUser, + Path(link): Path, + state: State, + body: Bytes, +) -> Result>, ApiError> { + let json_string = + String::from_utf8(body.to_vec()).unwrap_or_else(|_| "Invalid UTF-8".to_string()); + let res = match state + .issue_stg() + .add_issue_conversation(&link, user.user_id, Some(json_string)) + .await + { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} + +async fn delete_comment( + Path(id): Path, + state: State, +) -> Result>, ApiError> { + let res = state.issue_stg().remove_issue_conversation(id).await; + let res = match res { + Ok(_) => CommonResult::success(None), + Err(err) => CommonResult::failed(&err.to_string()), + }; + Ok(Json(res)) +} diff --git a/mono/src/api/issue/mod.rs b/mono/src/api/issue/mod.rs new file mode 100644 index 000000000..451222146 --- /dev/null +++ b/mono/src/api/issue/mod.rs @@ -0,0 +1,60 @@ +use callisto::mega_issue; +use serde::{Deserialize, Serialize}; + +use crate::api::mr::MegaConversion; + +pub mod issue_router; + +#[derive(Serialize, Deserialize)] +pub struct IssueItem { + pub link: String, + pub title: String, + pub status: String, + pub owner: i64, + pub open_timestamp: i64, + pub closed_at: Option, + pub updated_at: i64, +} + +impl From for IssueItem { + fn from(value: mega_issue::Model) -> Self { + Self { + link: value.link, + title: value.title, + status: value.status.to_string(), + owner: value.owner, + open_timestamp: value.created_at.and_utc().timestamp(), + closed_at: value.closed_at.map(|dt| dt.and_utc().timestamp()), + updated_at: value.updated_at.and_utc().timestamp(), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct NewIssue { + pub title: String, + pub description: String, +} + +#[derive(Serialize, Deserialize)] +pub struct IssueDetail { + pub id: i64, + pub link: String, + pub title: String, + pub status: String, + pub open_timestamp: i64, + pub conversions: Vec, +} + +impl From for IssueDetail { + fn from(value: mega_issue::Model) -> Self { + Self { + id: value.id, + link: value.link, + title: value.title, + status: value.status.to_string(), + open_timestamp: value.created_at.and_utc().timestamp(), + conversions: vec![], + } + } +} diff --git a/mono/src/api/mod.rs b/mono/src/api/mod.rs index eb8229963..0e89e9ed9 100644 --- a/mono/src/api/mod.rs +++ b/mono/src/api/mod.rs @@ -11,13 +11,16 @@ use ceres::{ protocol::repo::Repo, }; use common::{errors::ProtocolError, model::CommonOptions}; -use jupiter::{context::Context, storage::user_storage::UserStorage}; +use jupiter::{ + context::Context, + storage::{issue_storage::IssueStorage, mr_storage::MrStorage, user_storage::UserStorage}, +}; pub mod api_router; pub mod error; +pub mod issue; pub mod lfs; -pub mod model; -pub mod mr_router; +pub mod mr; pub mod oauth; pub mod user; @@ -26,7 +29,7 @@ pub struct MonoApiServiceState { pub context: Context, pub common: CommonOptions, pub oauth_client: Option, - // TODO: Remove MemoryStore + // TODO: Replace MemoryStore pub store: Option, } @@ -44,7 +47,7 @@ impl FromRef for BasicClient { impl FromRef for UserStorage { fn from_ref(state: &MonoApiServiceState) -> Self { - state.context.services.user_storage.clone() + state.context.user_stg() } } @@ -55,6 +58,18 @@ impl MonoApiServiceState { } } + fn issue_stg(&self) -> IssueStorage { + self.context.services.issue_storage() + } + + fn mr_stg(&self) -> MrStorage { + self.context.services.mr_storage() + } + + fn user_stg(&self) -> UserStorage { + self.context.services.user_storage() + } + async fn api_handler(&self, path: PathBuf) -> Result, ProtocolError> { let import_dir = self.context.config.monorepo.import_dir.clone(); if path.starts_with(&import_dir) && path != import_dir { @@ -114,10 +129,7 @@ pub mod util { state: State, ) -> Result<(), saturn::context::Error> { let entities = get_entitystore(path.into(), state).await; - let cedar_context = CedarContext::new( - entities, - ) - .unwrap(); + let cedar_context = CedarContext::new(entities).unwrap(); cedar_context.is_authorized( format!(r#"User::"{}""#, username) .to_owned() diff --git a/ceres/src/model/mr.rs b/mono/src/api/mr/mod.rs similarity index 80% rename from ceres/src/model/mr.rs rename to mono/src/api/mr/mod.rs index 74b240562..bfe28e2a3 100644 --- a/ceres/src/model/mr.rs +++ b/mono/src/api/mr/mod.rs @@ -1,10 +1,17 @@ use serde::{Deserialize, Serialize}; -use callisto::{mega_mr, mega_mr_conv}; +use callisto::{mega_conversation, mega_mr}; + +pub mod mr_router; + +#[derive(Deserialize)] +pub struct MRStatusParams { + pub status: String, +} #[derive(Serialize, Deserialize)] pub struct MrInfoItem { - pub mr_link: String, + pub link: String, pub title: String, pub status: String, pub open_timestamp: i64, @@ -15,7 +22,7 @@ pub struct MrInfoItem { impl From for MrInfoItem { fn from(value: mega_mr::Model) -> Self { Self { - mr_link: value.mr_link, + link: value.link, title: String::new(), status: value.status.to_string(), open_timestamp: value.created_at.and_utc().timestamp(), @@ -28,29 +35,19 @@ impl From for MrInfoItem { #[derive(Serialize, Deserialize)] pub struct MRDetail { pub id: i64, - pub mr_link: String, + pub link: String, pub title: String, pub status: String, pub open_timestamp: i64, pub merge_timestamp: Option, - pub conversions: Vec, -} - -#[derive(Serialize, Deserialize)] -pub struct MRConversion { - pub id: i64, - pub user_id: i64, - pub conv_type: String, - pub comment: Option, - pub created_at: i64, - pub updated_at: i64, + pub conversions: Vec, } impl From for MRDetail { fn from(value: mega_mr::Model) -> Self { Self { id: value.id, - mr_link: value.mr_link, + link: value.link, title: value.title, status: value.status.to_string(), open_timestamp: value.created_at.and_utc().timestamp(), @@ -60,8 +57,18 @@ impl From for MRDetail { } } -impl From for MRConversion { - fn from(value: mega_mr_conv::Model) -> Self { +#[derive(Serialize, Deserialize)] +pub struct MegaConversion { + pub id: i64, + pub user_id: i64, + pub conv_type: String, + pub comment: Option, + pub created_at: i64, + pub updated_at: i64, +} + +impl From for MegaConversion { + fn from(value: mega_conversation::Model) -> Self { Self { id: value.id, user_id: value.user_id, diff --git a/mono/src/api/model.rs b/mono/src/api/mr/model.rs similarity index 100% rename from mono/src/api/model.rs rename to mono/src/api/mr/model.rs diff --git a/mono/src/api/mr_router.rs b/mono/src/api/mr/mr_router.rs similarity index 56% rename from mono/src/api/mr_router.rs rename to mono/src/api/mr/mr_router.rs index 1f1b3c95d..9d77bdca5 100644 --- a/mono/src/api/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -8,13 +8,13 @@ use axum::{ use bytes::Bytes; -use ceres::model::mr::{MRDetail, MrInfoItem}; -use common::model::{CommonPage, CommonResult, RequestParams}; +use callisto::db_enums::{ConvType, MergeStatus}; +use common::model::{CommonPage, CommonResult, PageParams}; use saturn::ActionEnum; use taurus::event::api_request::{ApiRequestEvent, ApiType}; use crate::api::error::ApiError; -use crate::api::model::MRStatusParams; +use crate::api::mr::{MRDetail, MRStatusParams, MrInfoItem}; use crate::api::oauth::model::LoginUser; use crate::api::util; use crate::api::MonoApiServiceState; @@ -22,20 +22,19 @@ use crate::api::MonoApiServiceState; pub fn routers() -> Router { Router::new() .route("/mr/list", post(fetch_mr_list)) - .route("/mr/:mr_link/detail", get(mr_detail)) - .route("/mr/:mr_link/merge", post(merge)) - .route("/mr/:mr_link/files", get(get_mr_files)) - .route("/mr/:mr_link/comment", post(save_comment)) + .route("/mr/:link/detail", get(mr_detail)) + .route("/mr/:link/merge", post(merge)) + .route("/mr/:link/files", get(get_mr_files)) + .route("/mr/:link/comment", post(save_comment)) .route("/mr/comment/:conv_id/delete", post(delete_comment)) } async fn merge( user: LoginUser, - Path(mr_link): Path, + Path(link): Path, state: State, ) -> Result>, ApiError> { - let storage = state.context.services.mono_storage.clone(); - if let Some(model) = storage.get_open_mr_by_link(&mr_link).await.unwrap() { + if let Some(model) = state.mr_stg().get_open_mr_by_link(&link).await.unwrap() { let path = model.path.clone(); let _ = util::check_permissions( &user.name, @@ -58,39 +57,58 @@ async fn merge( async fn fetch_mr_list( state: State, - Json(json): Json>, + Json(json): Json>, ) -> Result>>, ApiError> { ApiRequestEvent::notify(ApiType::MergeList, &state.0.context.config); - let res = state - .monorepo() - .mr_list(&json.additional.status, json.pagination) - .await; - let res = match res { - Ok((items, total)) => CommonResult::success(Some(CommonPage { items, total })), + let status = json.additional.status; + let status = if status == "open" { + vec![MergeStatus::Open] + } else if status == "closed" { + vec![MergeStatus::Closed, MergeStatus::Merged] + } else { + vec![MergeStatus::Open, MergeStatus::Closed, MergeStatus::Merged] + }; + let res = match state + .mr_stg() + .get_mr_by_status(status, json.pagination.page, json.pagination.per_page) + .await + { + Ok((items, total)) => CommonResult::success(Some(CommonPage { + items: items.into_iter().map(|m| m.into()).collect(), + total, + })), Err(err) => CommonResult::failed(&err.to_string()), }; Ok(Json(res)) } async fn mr_detail( - Path(mr_link): Path, + Path(link): Path, state: State, -) -> Result>>, ApiError> { +) -> Result>, ApiError> { ApiRequestEvent::notify(ApiType::MergeDetail, &state.0.context.config); - let res = state.monorepo().mr_detail(&mr_link).await; - let res = match res { - Ok(data) => CommonResult::success(Some(data)), + let res = match state.mr_stg().get_mr(&link).await { + Ok(data) => { + if let Some(model) = data { + let mut detail: MRDetail = model.into(); + let conversions = state.mr_stg().get_mr_conversations(&link).await.unwrap(); + detail.conversions = conversions.into_iter().map(|x| x.into()).collect(); + CommonResult::success(Some(detail)) + } else { + CommonResult::success(None) + } + } Err(err) => CommonResult::failed(&err.to_string()), }; Ok(Json(res)) } async fn get_mr_files( - Path(mr_link): Path, + Path(link): Path, state: State, ) -> Result>>, ApiError> { ApiRequestEvent::notify(ApiType::MergeFiles, &state.0.context.config); - let res = state.monorepo().mr_tree_files(&mr_link).await; + let res = state.monorepo().mr_tree_files(&link).await; let res = match res { Ok(data) => CommonResult::success(Some(data)), Err(err) => CommonResult::failed(&err.to_string()), @@ -99,16 +117,22 @@ async fn get_mr_files( } async fn save_comment( - Path(mr_link): Path, + Path(link): Path, state: State, body: Bytes, ) -> Result>, ApiError> { let json_string = String::from_utf8(body.to_vec()).unwrap_or_else(|_| "Invalid UTF-8".to_string()); - let res = state.monorepo().comment(&mr_link, json_string).await; - let res = match res { - Ok(_) => CommonResult::success(None), - Err(err) => CommonResult::failed(&err.to_string()), + + let res = if let Some(model) = state.mr_stg().get_mr(&link).await.unwrap() { + state + .mr_stg() + .add_mr_conversation(&model.link, 0, ConvType::Comment, Some(json_string)) + .await + .unwrap(); + CommonResult::success(None) + } else { + CommonResult::failed("Invalid link") }; Ok(Json(res)) } @@ -117,8 +141,7 @@ async fn delete_comment( Path(conv_id): Path, state: State, ) -> Result>, ApiError> { - let res = state.monorepo().delete_comment(conv_id).await; - let res = match res { + let res = match state.mr_stg().remove_mr_conversation(conv_id).await { Ok(_) => CommonResult::success(None), Err(err) => CommonResult::failed(&err.to_string()), }; diff --git a/mono/src/api/oauth/mod.rs b/mono/src/api/oauth/mod.rs index 52da89227..f2099c5e2 100644 --- a/mono/src/api/oauth/mod.rs +++ b/mono/src/api/oauth/mod.rs @@ -50,7 +50,7 @@ async fn login_authorized( State(oauth_client): State, ) -> Result { let store: MemoryStore = MemoryStore::from_ref(&state); - let config = state.context.config.oauth.unwrap(); + let config = state.context.config.oauth.as_ref().unwrap(); // Get an auth token let token = oauth_client .exchange_code(AuthorizationCode::new(query.code.clone())) @@ -79,8 +79,7 @@ async fn login_authorized( } let new_user: user::Model = github_user.into(); - let user_storage = state.context.services.user_storage.clone(); - let user = user_storage + let user = state.user_stg() .find_user_by_email(&new_user.email) .await .unwrap(); @@ -90,7 +89,7 @@ async fn login_authorized( // Create a new session filled with user data login_user = user.into(); } else { - user_storage.save_user(new_user.clone()).await.unwrap(); + state.user_stg().save_user(new_user.clone()).await.unwrap(); login_user = new_user.into(); } diff --git a/mono/src/api/user/user_router.rs b/mono/src/api/user/user_router.rs index ebb610a4b..9e0418115 100644 --- a/mono/src/api/user/user_router.rs +++ b/mono/src/api/user/user_router.rs @@ -11,9 +11,9 @@ use common::model::CommonResult; use crate::api::user::model::AddSSHKey; use crate::api::user::model::ListSSHKey; +use crate::api::user::model::ListToken; use crate::api::MonoApiServiceState; use crate::api::{error::ApiError, oauth::model::LoginUser, util}; -use crate::api::user::model::ListToken; pub fn routers() -> Router { Router::new() @@ -53,9 +53,7 @@ async fn add_key( }; let res = state - .context - .services - .user_storage + .user_stg() .save_ssh_key(user.user_id, &title, &json.ssh_key, &key.fingerprint()) .await; let res = match res { @@ -70,12 +68,7 @@ async fn remove_key( state: State, Path(key_id): Path, ) -> Result>, ApiError> { - let res = state - .context - .services - .user_storage - .delete_ssh_key(user.user_id, key_id) - .await; + let res = state.user_stg().delete_ssh_key(user.user_id, key_id).await; let res = match res { Ok(_) => CommonResult::success(None), Err(err) => CommonResult::failed(&err.to_string()), @@ -87,12 +80,7 @@ async fn list_key( user: LoginUser, state: State, ) -> Result>>, ApiError> { - let res = state - .context - .services - .user_storage - .list_user_ssh(user.user_id) - .await; + let res = state.user_stg().list_user_ssh(user.user_id).await; let res = match res { Ok(data) => CommonResult::success(Some(data.into_iter().map(|x| x.into()).collect())), Err(err) => CommonResult::failed(&err.to_string()), @@ -104,12 +92,7 @@ async fn generate_token( user: LoginUser, state: State, ) -> Result>, ApiError> { - let res = state - .context - .services - .user_storage - .generate_token(user.user_id) - .await; + let res = state.user_stg().generate_token(user.user_id).await; let res = match res { Ok(data) => CommonResult::success(Some(data)), Err(err) => CommonResult::failed(&err.to_string()), @@ -117,18 +100,12 @@ async fn generate_token( Ok(Json(res)) } - async fn remove_token( user: LoginUser, state: State, Path(key_id): Path, ) -> Result>, ApiError> { - let res = state - .context - .services - .user_storage - .delete_token(user.user_id, key_id) - .await; + let res = state.user_stg().delete_token(user.user_id, key_id).await; let res = match res { Ok(_) => CommonResult::success(None), Err(err) => CommonResult::failed(&err.to_string()), @@ -136,22 +113,16 @@ async fn remove_token( Ok(Json(res)) } - async fn list_token( user: LoginUser, state: State, ) -> Result>>, ApiError> { - let res = state - .context - .services - .user_storage - .list_token(user.user_id) - .await; + let res = state.user_stg().list_token(user.user_id).await; let res = match res { Ok(data) => { let res = data.into_iter().map(|x| x.into()).collect(); CommonResult::success(Some(res)) - }, + } Err(err) => CommonResult::failed(&err.to_string()), }; Ok(Json(res)) diff --git a/mono/src/git_protocol/http.rs b/mono/src/git_protocol/http.rs index 36b4f3654..735c8aa0c 100644 --- a/mono/src/git_protocol/http.rs +++ b/mono/src/git_protocol/http.rs @@ -48,7 +48,6 @@ pub async fn git_info_refs( } async fn http_auth(header: &HeaderMap, context: &Context) -> bool { - let stg = context.services.user_storage.clone(); for (k, v) in header { if k == http::header::AUTHORIZATION { let decoded = general_purpose::STANDARD @@ -65,9 +64,9 @@ async fn http_auth(header: &HeaderMap, context: &Context) -> bool { let username = parts.next().unwrap_or(""); let password = parts.next().unwrap_or(""); tracing::debug!("{}, {}", username, password); - match stg.find_user_by_name(username).await.unwrap() { + match context.user_stg().find_user_by_name(username).await.unwrap() { Some(user) => { - return stg.check_token(user.id, password).await.unwrap(); + return context.user_stg().check_token(user.id, password).await.unwrap(); } None => return false, } diff --git a/mono/src/git_protocol/ssh.rs b/mono/src/git_protocol/ssh.rs index d6de66baf..d4be7560b 100644 --- a/mono/src/git_protocol/ssh.rs +++ b/mono/src/git_protocol/ssh.rs @@ -138,8 +138,7 @@ impl server::Handler for SshServer { public_key.fingerprint() ); let fingerprint = public_key.fingerprint(); - let stg = self.context.services.user_storage.clone(); - let res = stg.search_ssh_key_finger(&fingerprint).await.unwrap(); + let res = self.context.user_stg().search_ssh_key_finger(&fingerprint).await.unwrap(); if !res.is_empty() { tracing::info!("Client public key verified successfully!"); Ok(Auth::Accept) diff --git a/moon/package.json b/moon/package.json index c1d3140e2..63421a39d 100644 --- a/moon/package.json +++ b/moon/package.json @@ -14,16 +14,16 @@ "@headlessui/react": "^2.1.8", "@headlessui/tailwindcss": "^0.2.1", "@heroicons/react": "^2.1.5", - "@lexical/react": "^0.17.1", - "lexical": "^0.17.1", + "@lexical/react": "^0.19.0", "@tailwindcss/forms": "^0.5.9", "clsx": "^2.1.1", "copy-to-clipboard": "^3.3.3", "date-fns": "^4.1.0", - "framer-motion": "^11.7.0", + "framer-motion": "^11.11.10", "github-markdown-css": "^5.7.0", "highlight.js": "^11.10.0", - "next": "^14.2.13", + "lexical": "^0.19.0", + "next": "^15.0.1", "prism-react-renderer": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -35,7 +35,7 @@ "@types/react-dom": "^18.3.0", "autoprefixer": "^10.4.19", "eslint": "^8.57.0", - "eslint-config-next": "^14.2.5", + "eslint-config-next": "^15.0.1", "postcss": "^8.4.40", "typescript": "^5.5.0" } diff --git a/moon/src/app/(dashboard)/issue/[id]/page.tsx b/moon/src/app/(dashboard)/issue/[id]/page.tsx new file mode 100644 index 000000000..de1c1e62c --- /dev/null +++ b/moon/src/app/(dashboard)/issue/[id]/page.tsx @@ -0,0 +1,132 @@ +'use client' +import { useEffect, useState } from "react"; +import { Card, Button, List, Tabs, TabsProps, Space, Timeline, Flex } from 'antd/lib'; +import { CommentOutlined, MergeOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import RichEditor from "@/components/rich-editor/RichEditor"; +import MRComment from "@/components/MRComment"; +import { useRouter } from "next/navigation"; + +interface IssueDetail { + status: string, + conversions: Conversation[], + title: string, +} +interface Conversation { + id: number, + user_id: number, + conv_type: string, + comment: string, + created_at: number, +} + +export default function IssueDetailPage({ params }: { params: { id: string } }) { + const [editorState, setEditorState] = useState(""); + const [login, setLogin] = useState(false); + const [info, setInfo] = useState( + { + status: "", + conversions: [], + title: "", + } + ); + const [loadings, setLoadings] = useState([]); + const router = useRouter(); + + const fetchDetail = async () => { + const detail = await fetch(`/api/issue/${params.id}/detail`); + const detail_json = await detail.json(); + setInfo(detail_json.data.data); + }; + + const checkLogin = async () => { + const res = await fetch(`/api/auth`); + setLogin(res.ok); + }; + + useEffect(() => { + checkLogin() + fetchDetail() + }, [params.id]); + + const set_to_loading = (index: number) => { + setLoadings((prevLoadings) => { + const newLoadings = [...prevLoadings]; + newLoadings[index] = true; + return newLoadings; + }); + } + + const cancel_loading = (index: number) => { + setLoadings((prevLoadings) => { + const newLoadings = [...prevLoadings]; + newLoadings[index] = false; + return newLoadings; + }); + } + + async function close_issue() { + const res = await fetch(`/api/issue/${params.id}/close`, { + method: 'POST', + }); + if (res) { + router.push( + "/issue" + ); + } + }; + + async function save_comment(comment) { + set_to_loading(3); + const res = await fetch(`/api/issue/${params.id}/comment`, { + method: 'POST', + body: comment, + }); + if (res) { + setEditorState(""); + fetchDetail(); + cancel_loading(3); + } + } + + let conv_items = info?.conversions.map(conv => { + let icon; + let children; + switch (conv.conv_type) { + case "Comment": icon = ; children = ; break + case "Closed": icon = ; children = conv.comment; + }; + + const element = { + dot: icon, + children: children + } + return element + }); + + const tab_items: TabsProps['items'] = [ + { + key: '1', + label: 'Conversation', + children: + + + {info && info.status === "open" && + <> +

Add a comment

+ + + + + + + } +
+ } + ]; + + return ( + + + + ) +} diff --git a/moon/src/app/(dashboard)/issue/new/page.tsx b/moon/src/app/(dashboard)/issue/new/page.tsx new file mode 100644 index 000000000..3ba8cba96 --- /dev/null +++ b/moon/src/app/(dashboard)/issue/new/page.tsx @@ -0,0 +1,67 @@ +'use client' +import { useState } from "react"; +import { Button, Input, Space, Flex } from 'antd/lib'; +import RichEditor from "@/components/rich-editor/RichEditor"; +import { useRouter } from "next/navigation"; + +export default function MRDetailPage({ params }: { params: { id: string } }) { + const [editorState, setEditorState] = useState(""); + const [title, setTitle] = useState(""); + const [loadings, setLoadings] = useState([]); + const router = useRouter(); + + const set_to_loading = (index: number) => { + setLoadings((prevLoadings) => { + const newLoadings = [...prevLoadings]; + newLoadings[index] = true; + return newLoadings; + }); + } + + const cancel_loading = (index: number) => { + setLoadings((prevLoadings) => { + const newLoadings = [...prevLoadings]; + newLoadings[index] = false; + return newLoadings; + }); + } + + async function submit(description) { + set_to_loading(3); + const res = await fetch(`/api/issue/new`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: title, + description: description + }), + }); + if (res) { + setEditorState(""); + cancel_loading(3); + router.push( + "/issue" + ); + } + } + + + return ( + <> + +

Add a title + setTitle(e.target.value)}> +

+
+ +

Add a description

+ + + + +
+ + ) +} diff --git a/moon/src/app/(dashboard)/issue/page.tsx b/moon/src/app/(dashboard)/issue/page.tsx index 2a97c2f9f..51a6e70d9 100644 --- a/moon/src/app/(dashboard)/issue/page.tsx +++ b/moon/src/app/(dashboard)/issue/page.tsx @@ -1,8 +1,124 @@ +'use client' +import { Heading } from '@/components/catalyst/heading' +import { useEffect, useState } from 'react'; +import React from 'react'; +import { Flex, List, PaginationProps, Tag, Button, Tabs, Space, TabsProps, Segmented } from 'antd/lib'; +import { format, formatDistance, fromUnixTime } from 'date-fns' +import { IssuesCloseOutlined, ExclamationCircleOutlined } from '@ant-design/icons'; +import Link from 'next/link'; + + +interface Item { + link: string, + title: string, + status: string, + open_timestamp: number, + merge_timestamp: number | null, + updated_at: number, +} + export default function IssuePage() { + const [itemList, setItemList] = useState([]); + const [numTotal, setNumTotal] = useState(0); + const [pageSize, setPageSize] = useState(10); + const [status, setStatus] = useState("open") + + const fetchData = async (page: number, per_page: number) => { + try { + const res = await fetch(`/api/issue/list`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + pagination: { + page: page, + per_page: per_page + }, + additional: { + status: status + } + }), + }); + const response = await res.json(); + const data = response.data.data; + setItemList(data.items); + setNumTotal(data.total) + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + useEffect(() => { + fetchData(1, pageSize); + }, [pageSize, status]); + + const getStatusTag = (status: string) => { + switch (status) { + case 'open': + return open; + case 'closed': + return closed; + } + }; + + const getDescription = (item: Item) => { + switch (item.status) { + case 'open': + return `Issue opened by Admin ${formatDistance(fromUnixTime(item.open_timestamp), new Date(), { addSuffix: true })} `; + case 'closed': + return (`Issue ${item.link} closed by Admin ${formatDistance(fromUnixTime(item.updated_at), new Date(), { addSuffix: true })}`) + } + } + + const onChange: PaginationProps['onChange'] = (current, pageSize) => { + fetchData(current, pageSize); + }; + + const tabsChange = (activeKey: string) => { + if (activeKey === '1') { + setStatus("open"); + } else { + setStatus("closed"); + } + } + + const tab_items: TabsProps['items'] = [ + { + key: '1', + label: 'Open', + }, + { + key: '2', + label: 'Closed', + } + ]; return ( -
- Issue Page -
+ <> + Issues + + + + + + + ( + + + } + title={{item.title} {getStatusTag(item.status)}} + description={getDescription(item)} + /> + + )} + /> + ) } \ No newline at end of file diff --git a/moon/src/app/(dashboard)/mr/page.tsx b/moon/src/app/(dashboard)/mr/page.tsx index 808facbc1..5a61bc6c7 100644 --- a/moon/src/app/(dashboard)/mr/page.tsx +++ b/moon/src/app/(dashboard)/mr/page.tsx @@ -8,7 +8,7 @@ import Link from 'next/link'; interface MrInfoItem { - mr_link: string, + link: string, title: string, status: string, open_timestamp: number, @@ -74,7 +74,7 @@ export default function MergeRequestPage() { return ""; } case 'closed': - return (`MR ${item.mr_link} closed by Admin ${formatDistance(fromUnixTime(item.updated_at), new Date(), { addSuffix: true })}`) + return (`MR ${item.link} closed by Admin ${formatDistance(fromUnixTime(item.updated_at), new Date(), { addSuffix: true })}`) } } @@ -93,7 +93,7 @@ export default function MergeRequestPage() { avatar={ } - title={{`MR ${item.mr_link} open by Mega automacticlly${item.title}`}{getStatusTag(item.status)}} + title={{`MR ${item.link} open by Mega automacticlly${item.title}`}{getStatusTag(item.status)}} description={getDescription(item)} /> diff --git a/moon/src/app/api/issue/[id]/close/route.ts b/moon/src/app/api/issue/[id]/close/route.ts new file mode 100644 index 000000000..666f19ada --- /dev/null +++ b/moon/src/app/api/issue/[id]/close/route.ts @@ -0,0 +1,9 @@ +export async function POST(request: Request, { params }: { params: { id: string } }) { + const endpoint = process.env.MEGA_INTERNAL_HOST; + const res = await fetch(`${endpoint}/api/v1/issue/${params.id}/close`, { + method: 'POST', + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/api/issue/[id]/comment/route.ts b/moon/src/app/api/issue/[id]/comment/route.ts new file mode 100644 index 000000000..f94959a31 --- /dev/null +++ b/moon/src/app/api/issue/[id]/comment/route.ts @@ -0,0 +1,21 @@ +import { verifySession } from "@/app/lib/dal"; +import { redirect } from "next/navigation"; +import { NextResponse } from "next/server"; + +export async function POST(request: Request, { params }: { params: { id: string } }) { + const session = await verifySession() + const jsonData = await request.json(); + + const endpoint = process.env.MEGA_INTERNAL_HOST; + const res = await fetch(`${endpoint}/api/v1/issue/${params.id}/comment`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Cookie': request.headers.get('cookie') || '', + }, + body: JSON.stringify(jsonData), + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/api/issue/[id]/detail/route.ts b/moon/src/app/api/issue/[id]/detail/route.ts new file mode 100644 index 000000000..1c02fe1dc --- /dev/null +++ b/moon/src/app/api/issue/[id]/detail/route.ts @@ -0,0 +1,9 @@ + +export async function GET(request: Request, { params }: { params: { id: string } }) { + + const endpoint = process.env.MEGA_INTERNAL_HOST; + const res = await fetch(`${endpoint}/api/v1/issue/${params.id}/detail`) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/api/issue/list/route.ts b/moon/src/app/api/issue/list/route.ts new file mode 100644 index 000000000..d8621ea99 --- /dev/null +++ b/moon/src/app/api/issue/list/route.ts @@ -0,0 +1,19 @@ +export const dynamic = 'force-dynamic' // defaults to auto +export const revalidate = 0 + +import { type NextRequest } from 'next/server' + +export async function POST(request: NextRequest) { + const endpoint = process.env.MEGA_INTERNAL_HOST; + const body = await request.json(); + const res = await fetch(`${endpoint}/api/v1/issue/list`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/api/issue/new/route.ts b/moon/src/app/api/issue/new/route.ts new file mode 100644 index 000000000..b1d8931fc --- /dev/null +++ b/moon/src/app/api/issue/new/route.ts @@ -0,0 +1,27 @@ +import { verifySession } from "@/app/lib/dal"; + +export const dynamic = 'force-dynamic' // defaults to auto + +export async function POST(request: Request, { params }: { params: { id: string } }) { + const session = await verifySession() + + // Check if the user is authenticated + if (!session) { + // User is not authenticated + return new Response(null, { status: 401 }) + } + const jsonData = await request.json(); + + const endpoint = process.env.MEGA_INTERNAL_HOST; + const res = await fetch(`${endpoint}/api/v1/issue/new`, { + method: 'POST', + headers: { + 'Cookie': request.headers.get('cookie') || '', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(jsonData) + }) + const data = await res.json() + + return Response.json({ data }) +} \ No newline at end of file diff --git a/moon/src/app/lib/dal.ts b/moon/src/app/lib/dal.ts index 6aeb7f62d..580ba5086 100644 --- a/moon/src/app/lib/dal.ts +++ b/moon/src/app/lib/dal.ts @@ -5,15 +5,14 @@ import { cache } from 'react' import { redirect } from 'next/navigation' export const verifySession = cache(async () => { - const session = cookies().get('SESSION')?.value + const session = (await cookies()).get('SESSION')?.value if (!session) { - redirect('/') + redirect('/login') } return { session } }) - -export const isLoginIn = () => { - const session = cookies().get('SESSION')?.value +export const isLoginIn = async () => { + const session = (await cookies()).get('SESSION')?.value return session != null } diff --git a/moon/src/middleware.ts b/moon/src/middleware.ts index 6b85b84d2..1396199b0 100644 --- a/moon/src/middleware.ts +++ b/moon/src/middleware.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server' import { cookies } from 'next/headers' // 1. Specify protected and public routes -const protectedRoutes = ['/user/keys/add', '/user/keys', 'user/token'] +const protectedRoutes = ['/user/keys/add', '/user/keys', '/user/token', '/issue/new',] const publicRoutes = ['/login', '/'] export default async function middleware(req: NextRequest) { @@ -12,7 +12,7 @@ export default async function middleware(req: NextRequest) { const isPublicRoute = publicRoutes.includes(path) // 3. Decrypt the session from the cookie - const session = cookies().get('SESSION')?.value + const session = (await cookies()).get('SESSION')?.value // 5. Redirect to /login if the user is not authenticated if (isProtectedRoute && !session) { diff --git a/moon/tsconfig.json b/moon/tsconfig.json index 204075550..107ed4691 100644 --- a/moon/tsconfig.json +++ b/moon/tsconfig.json @@ -26,14 +26,16 @@ "./src/*" ] }, - "strictNullChecks": true + "strictNullChecks": true, + "target": "ES2017" }, "include": [ "next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", - "**/*.tsx" -, "src/pages/_app.back" ], + "**/*.tsx", + "src/pages/_app.back" + ], "exclude": [ "node_modules" ] diff --git a/sql/postgres/pg_20241023__init.sql b/sql/postgres/pg_20241029__init.sql similarity index 96% rename from sql/postgres/pg_20241023__init.sql rename to sql/postgres/pg_20241029__init.sql index e510587b3..da11cb846 100644 --- a/sql/postgres/pg_20241023__init.sql +++ b/sql/postgres/pg_20241029__init.sql @@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS "mega_tag" ( ); CREATE TABLE IF NOT EXISTS "mega_mr" ( "id" BIGINT PRIMARY KEY, - "mr_link" VARCHAR(40) NOT NULL, + "link" VARCHAR(40) NOT NULL, "title" TEXT NOT NULL, "merge_date" TIMESTAMP, "status" VARCHAR(20) NOT NULL, @@ -53,29 +53,30 @@ CREATE TABLE IF NOT EXISTS "mega_mr" ( ); CREATE INDEX "idx_mr_path" ON "mega_mr" ("path"); -CREATE TABLE IF NOT EXISTS "mega_mr_conv" ( +CREATE TABLE IF NOT EXISTS "mega_conversion" ( "id" BIGINT PRIMARY KEY, - "mr_link" VARCHAR(20) NOT NULL, + "link" VARCHAR(20) NOT NULL, "user_id" BIGINT NOT NULL, "conv_type" VARCHAR(20) NOT NULL, "comment" TEXT, "created_at" TIMESTAMP NOT NULL, "updated_at" TIMESTAMP NOT NULL ); -CREATE INDEX "idx_conversation" ON "mega_mr_conv" ("mr_link"); +CREATE INDEX "idx_conversation" ON "mega_conversion" ("link"); CREATE TABLE IF NOT EXISTS "mega_issue" ( "id" BIGINT PRIMARY KEY, - "number" BIGINT NOT NULL, + "link" VARCHAR(20) NOT NULL, "title" VARCHAR(255) NOT NULL, - "sender_name" VARCHAR(255) NOT NULL, - "sender_id" BIGINT NOT NULL, - "state" VARCHAR(255) NOT NULL, + "owner" BIGINT NOT NULL, + "status" VARCHAR(20) NOT NULL, "created_at" TIMESTAMP NOT NULL, "updated_at" TIMESTAMP NOT NULL, "closed_at" TIMESTAMP DEFAULT NULL ); +CREATE INDEX "idx_issue" ON "mega_issue" ("link"); + CREATE TABLE IF NOT EXISTS "mega_refs" ( "id" BIGINT PRIMARY KEY, "path" TEXT NOT NULL, diff --git a/sql/sqlite/sqlite_20241023_init.sql b/sql/sqlite/sqlite_20241029_init.sql similarity index 98% rename from sql/sqlite/sqlite_20241023_init.sql rename to sql/sqlite/sqlite_20241029_init.sql index e54a13416..866c0db87 100644 --- a/sql/sqlite/sqlite_20241023_init.sql +++ b/sql/sqlite/sqlite_20241029_init.sql @@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS "mega_tag" ( ); CREATE TABLE IF NOT EXISTS "mega_mr" ( "id" INTEGER PRIMARY KEY, - "mr_link" TEXT NOT NULL, + "link" TEXT NOT NULL, "title" TEXT NOT NULL, "merge_date" TEXT, "status" TEXT NOT NULL, @@ -53,16 +53,16 @@ CREATE TABLE IF NOT EXISTS "mega_mr" ( ); CREATE INDEX "idx_mr_path" ON "mega_mr" ("path"); -CREATE TABLE IF NOT EXISTS "mega_mr_conv" ( +CREATE TABLE IF NOT EXISTS "mega_conversion" ( "id" INTEGER PRIMARY KEY, - "mr_link" INTEGER NOT NULL, + "link" INTEGER NOT NULL, "user_id" INTEGER NOT NULL, "conv_type" TEXT NOT NULL, "comment" TEXT, "created_at" TEXT NOT NULL, "updated_at" TEXT NOT NULL ); -CREATE INDEX "idx_conversation" ON "mega_mr_conv" ("mr_link"); +CREATE INDEX "idx_conversation" ON "mega_conversion" ("link"); CREATE TABLE IF NOT EXISTS "mega_issue" ( "id" INTEGER PRIMARY KEY, From f73a89112a0794cfe4f314ede4415fa1ce02d2a7 Mon Sep 17 00:00:00 2001 From: "benjamin.747" Date: Wed, 30 Oct 2024 09:59:28 +0800 Subject: [PATCH 2/2] upgrade nextjs from 14 to 15 --- moon/package.json | 2 +- .../app/(dashboard)/blob/[...path]/page.tsx | 15 ++++++++++----- moon/src/app/(dashboard)/issue/[id]/page.tsx | 17 +++++++++++------ moon/src/app/(dashboard)/issue/new/page.tsx | 2 +- moon/src/app/(dashboard)/mr/[id]/page.tsx | 19 ++++++++++++------- .../app/(dashboard)/tree/[...path]/page.tsx | 19 ++++++++++++------- moon/src/app/actions.ts | 5 +++-- moon/src/app/api/auth/route.ts | 2 +- moon/src/app/api/issue/[id]/close/route.ts | 6 +++++- moon/src/app/api/issue/[id]/comment/route.ts | 8 +++++--- moon/src/app/api/issue/[id]/detail/route.ts | 6 +++++- moon/src/app/api/issue/new/route.ts | 2 +- moon/src/app/api/mr/[id]/comment/route.ts | 6 +++++- moon/src/app/api/mr/[id]/detail/route.ts | 6 +++++- moon/src/app/api/mr/[id]/files/route.ts | 6 +++++- moon/src/app/api/mr/[id]/merge/route.ts | 5 ++++- .../app/api/mr/comment/[id]/delete/route.ts | 6 +++++- .../src/app/api/user/ssh/[id]/delete/route.ts | 6 +++++- .../app/api/user/token/[id]/delete/route.ts | 6 +++++- 19 files changed, 101 insertions(+), 43 deletions(-) diff --git a/moon/package.json b/moon/package.json index 63421a39d..c462d30b8 100644 --- a/moon/package.json +++ b/moon/package.json @@ -23,7 +23,7 @@ "github-markdown-css": "^5.7.0", "highlight.js": "^11.10.0", "lexical": "^0.19.0", - "next": "^15.0.1", + "next": "^15.0.2", "prism-react-renderer": "^2.4.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/moon/src/app/(dashboard)/blob/[...path]/page.tsx b/moon/src/app/(dashboard)/blob/[...path]/page.tsx index 814943291..6efa76f90 100644 --- a/moon/src/app/(dashboard)/blob/[...path]/page.tsx +++ b/moon/src/app/(dashboard)/blob/[...path]/page.tsx @@ -3,21 +3,26 @@ import CodeContent from '@/components/CodeContent'; import Bread from '@/components/BreadCrumb'; import { useEffect, useState } from 'react'; import { Flex, Layout } from "antd/lib"; +import * as React from 'react' -export default function BlobPage({ params }: { params: { path: string[] } }) { - let path = '/' + params.path.join('/'); +type Params = Promise<{ path: string[] }> + +export default function BlobPage({ params }: { params: Params }) { + const { path } = React.use(params); + + let new_path = '/' + path.join('/'); const [fileContent, setFileContent] = useState(""); useEffect(() => { const fetchData = async () => { try { - let fileContent = await getFileContent(path); + let fileContent = await getFileContent(new_path); setFileContent(fileContent); } catch (error) { console.error('Error fetching data:', error); } }; fetchData(); - }, [path]); + }, [new_path]); const treeStyle = { borderRadius: 8, @@ -46,7 +51,7 @@ export default function BlobPage({ params }: { params: { path: string[] } }) {
- + {/* */} diff --git a/moon/src/app/(dashboard)/issue/[id]/page.tsx b/moon/src/app/(dashboard)/issue/[id]/page.tsx index de1c1e62c..0e5ab6419 100644 --- a/moon/src/app/(dashboard)/issue/[id]/page.tsx +++ b/moon/src/app/(dashboard)/issue/[id]/page.tsx @@ -5,6 +5,7 @@ import { CommentOutlined, MergeOutlined, CloseCircleOutlined } from '@ant-design import RichEditor from "@/components/rich-editor/RichEditor"; import MRComment from "@/components/MRComment"; import { useRouter } from "next/navigation"; +import * as React from 'react' interface IssueDetail { status: string, @@ -19,7 +20,11 @@ interface Conversation { created_at: number, } -export default function IssueDetailPage({ params }: { params: { id: string } }) { +type Params = Promise<{ id: string }> + +export default function IssueDetailPage({ params }: { params: Params }) { + const { id } = React.use(params) + const [editorState, setEditorState] = useState(""); const [login, setLogin] = useState(false); const [info, setInfo] = useState( @@ -33,7 +38,7 @@ export default function IssueDetailPage({ params }: { params: { id: string } }) const router = useRouter(); const fetchDetail = async () => { - const detail = await fetch(`/api/issue/${params.id}/detail`); + const detail = await fetch(`/api/issue/${id}/detail`); const detail_json = await detail.json(); setInfo(detail_json.data.data); }; @@ -46,7 +51,7 @@ export default function IssueDetailPage({ params }: { params: { id: string } }) useEffect(() => { checkLogin() fetchDetail() - }, [params.id]); + }, [id]); const set_to_loading = (index: number) => { setLoadings((prevLoadings) => { @@ -65,7 +70,7 @@ export default function IssueDetailPage({ params }: { params: { id: string } }) } async function close_issue() { - const res = await fetch(`/api/issue/${params.id}/close`, { + const res = await fetch(`/api/issue/${id}/close`, { method: 'POST', }); if (res) { @@ -77,7 +82,7 @@ export default function IssueDetailPage({ params }: { params: { id: string } }) async function save_comment(comment) { set_to_loading(3); - const res = await fetch(`/api/issue/${params.id}/comment`, { + const res = await fetch(`/api/issue/${id}/comment`, { method: 'POST', body: comment, }); @@ -125,7 +130,7 @@ export default function IssueDetailPage({ params }: { params: { id: string } }) ]; return ( - + ) diff --git a/moon/src/app/(dashboard)/issue/new/page.tsx b/moon/src/app/(dashboard)/issue/new/page.tsx index 3ba8cba96..09c06e241 100644 --- a/moon/src/app/(dashboard)/issue/new/page.tsx +++ b/moon/src/app/(dashboard)/issue/new/page.tsx @@ -4,7 +4,7 @@ import { Button, Input, Space, Flex } from 'antd/lib'; import RichEditor from "@/components/rich-editor/RichEditor"; import { useRouter } from "next/navigation"; -export default function MRDetailPage({ params }: { params: { id: string } }) { +export default function MRDetailPage() { const [editorState, setEditorState] = useState(""); const [title, setTitle] = useState(""); const [loadings, setLoadings] = useState([]); diff --git a/moon/src/app/(dashboard)/mr/[id]/page.tsx b/moon/src/app/(dashboard)/mr/[id]/page.tsx index 6f992526e..943710a3e 100644 --- a/moon/src/app/(dashboard)/mr/[id]/page.tsx +++ b/moon/src/app/(dashboard)/mr/[id]/page.tsx @@ -5,6 +5,7 @@ import { CommentOutlined, MergeOutlined, CloseCircleOutlined } from '@ant-design import { formatDistance, fromUnixTime } from 'date-fns'; import RichEditor from "@/components/rich-editor/RichEditor"; import MRComment from "@/components/MRComment"; +import * as React from 'react' interface MRDetail { status: string, @@ -19,7 +20,11 @@ interface Conversation { created_at: number, } -export default function MRDetailPage({ params }: { params: { id: string } }) { +type Params = Promise<{ id: string }> + +export default function MRDetailPage({ params }: { params: Params }) { + const { id } = React.use(params) + const [editorState, setEditorState] = useState(""); const [login, setLogin] = useState(false); const [mrDetail, setMrDetail] = useState( @@ -39,7 +44,7 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { }; const fetchDetail = async () => { - const detail = await fetch(`/api/mr/${params.id}/detail`); + const detail = await fetch(`/api/mr/${id}/detail`); const detail_json = await detail.json(); setMrDetail(detail_json.data.data); }; @@ -47,7 +52,7 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { const fetchFileList = async () => { set_to_loading(2) try { - const res = await fetch(`/api/mr/${params.id}/files`); + const res = await fetch(`/api/mr/${id}/files`); const result = await res.json(); setFileData(result.data.data); } finally { @@ -59,7 +64,7 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { fetchDetail() fetchFileList(); checkLogin(); - }, [params.id]); + }, [id]); const set_to_loading = (index: number) => { setLoadings((prevLoadings) => { @@ -79,7 +84,7 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { async function approve_mr() { set_to_loading(1); - const res = await fetch(`/api/mr/${params.id}/merge`, { + const res = await fetch(`/api/mr/${id}/merge`, { method: 'POST', }); if (res) { @@ -89,7 +94,7 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { async function save_comment(comment) { set_to_loading(3); - const res = await fetch(`/api/mr/${params.id}/comment`, { + const res = await fetch(`/api/mr/${id}/comment`, { method: 'POST', body: comment, }); @@ -152,7 +157,7 @@ export default function MRDetailPage({ params }: { params: { id: string } }) { ]; return ( - + {mrDetail && mrDetail.status === "open" &&