diff --git a/.dockerignore b/.dockerignore index b8a7e18c8..3c9463159 100644 --- a/.dockerignore +++ b/.dockerignore @@ -32,5 +32,4 @@ buck-out # Dockderfile docker/mono-engine-dockerfile docker/mono-pg-dockerfile -docker/mono-ui-dockerfile docker/aries-engine-dockerfile diff --git a/aria/contents/docs/getting-started/installation/index.mdx b/aria/contents/docs/getting-started/installation/index.mdx index 24d58ce9d..2adafdec1 100644 --- a/aria/contents/docs/getting-started/installation/index.mdx +++ b/aria/contents/docs/getting-started/installation/index.mdx @@ -23,7 +23,7 @@ To get started, you can clone the mega repository directly from GitHub. git submodule update --init --recursive docker buildx build -t mega:mono-pg-0.1-pre-release -f ./docker/mono-pg-dockerfile . docker buildx build -t mega:mono-engine-0.1-pre-release -f ./docker/mono-engine-dockerfile . - docker buildx build -t mega:mono-ui-0.1-pre-release -f ./docker/mono-ui-dockerfile . + docker buildx build -t mega:mono-ui-0.1-pre-release -f ./moon/apps/web/Dockerfile . ``` diff --git a/common/Cargo.toml b/common/Cargo.toml index 8d4939835..a4784834d 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -19,7 +19,6 @@ idgenerator = { workspace = true } serde = { workspace = true } config = { workspace = true } envsubst = { workspace = true } -rand = { workspace = true } serde_json = { workspace = true } regex = { workspace = true } sea-orm = { workspace = true, features = ["macros"] } diff --git a/common/src/utils.rs b/common/src/utils.rs index 1487cd326..28621f8cd 100644 --- a/common/src/utils.rs +++ b/common/src/utils.rs @@ -1,5 +1,4 @@ use idgenerator::IdInstance; -use rand::prelude::*; use regex::Regex; use serde_json::{json, Value}; @@ -13,16 +12,6 @@ pub fn generate_id() -> i64 { IdInstance::next_id() } -pub fn generate_link() -> String { - let rng = rand::rng(); - let str: String = rng - .sample_iter(rand::distr::Alphanumeric) - .take(8) - .map(char::from) - .collect(); - str.to_uppercase() -} - pub const MEGA_BRANCH_NAME: &str = "refs/heads/main"; pub fn generate_rich_text(content: &str) -> String { @@ -169,14 +158,4 @@ mod test { let msg = "()(common): add new feature"; // unssupported characters in type assert!(!check_conventional_commits_message(msg)); } - - #[test] - fn test_link_generate() { - let link = generate_link(); - println!("MR Link: '{:?}'", link); - assert!( - link.chars().count() == 8 - && link.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()) - ) - } } diff --git a/docker/README.md b/docker/README.md index 6fb2b3af1..717602a21 100644 --- a/docker/README.md +++ b/docker/README.md @@ -13,7 +13,7 @@ docker buildx build -t mega:mono-engine-latest -f ./docker/mono-engine-dockerfil # docker buildx build -t mega:mono-engine-latest-debug -f ./docker/mono-engine-dockerfile --build-arg BUILD_TYPE=debug . # build frontend mono ui image -docker buildx build -t mega:mono-ui-latest-release -f ./docker/mono-ui-dockerfile . +docker buildx build -t mega:mono-ui-latest-release -f ./moon/apps/web/Dockerfile . # build backend aries engine image (default in release mode) docker buildx build -t mega:aries-engine-latest -f ./docker/aries-engine-dockerfile . diff --git a/jupiter/callisto/src/entity_ext/label.rs b/jupiter/callisto/src/entity_ext/label.rs index 6a0fd4aa5..d981a38f1 100644 --- a/jupiter/callisto/src/entity_ext/label.rs +++ b/jupiter/callisto/src/entity_ext/label.rs @@ -1,6 +1,9 @@ use sea_orm::entity::prelude::*; -use crate::label::Entity; +use crate::{ + entity_ext::generate_id, + label::{self, Entity}, +}; #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { @@ -14,3 +17,17 @@ impl RelationTrait for Relation { } } } + +impl label::Model { + pub fn new(name: &str, color: &str, description: &str) -> Self { + let now = chrono::Utc::now().naive_utc(); + Self { + id: generate_id(), + created_at: now, + updated_at: now, + name: name.to_owned(), + color: color.to_owned(), + description: description.to_owned(), + } + } +} diff --git a/jupiter/callisto/src/entity_ext/mega_issue.rs b/jupiter/callisto/src/entity_ext/mega_issue.rs index 2037e7e28..d8339aeef 100644 --- a/jupiter/callisto/src/entity_ext/mega_issue.rs +++ b/jupiter/callisto/src/entity_ext/mega_issue.rs @@ -1,6 +1,9 @@ use sea_orm::entity::prelude::*; -use crate::mega_issue::Entity; +use crate::{ + entity_ext::{generate_id, generate_link}, + mega_issue::{self, Entity}, +}; #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { @@ -50,3 +53,19 @@ impl Related for Entity { None } } + +impl mega_issue::Model { + pub fn new(title: String, author: String) -> Self { + let now = chrono::Utc::now().naive_utc(); + Self { + id: generate_id(), + link: generate_link(), + title, + author, + status: "open".to_owned(), + created_at: now, + updated_at: now, + closed_at: None, + } + } +} diff --git a/jupiter/callisto/src/entity_ext/mega_mr.rs b/jupiter/callisto/src/entity_ext/mega_mr.rs index 41ac69a9b..85fd1b866 100644 --- a/jupiter/callisto/src/entity_ext/mega_mr.rs +++ b/jupiter/callisto/src/entity_ext/mega_mr.rs @@ -1,6 +1,10 @@ use sea_orm::entity::prelude::*; -use crate::mega_mr::Entity; +use crate::{ + entity_ext::{generate_id, generate_link}, + mega_mr::{self, Entity}, + sea_orm_active_enums::MergeStatusEnum, +}; #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { @@ -46,3 +50,21 @@ impl Related for Entity { None } } + +impl mega_mr::Model { + pub fn new(path: String, title: String, from_hash: String, to_hash: String) -> Self { + let now = chrono::Utc::now().naive_utc(); + Self { + id: generate_id(), + link: generate_link(), + title: title.to_owned(), + status: MergeStatusEnum::Open, + created_at: now, + updated_at: now, + merge_date: None, + path, + from_hash, + to_hash, + } + } +} diff --git a/jupiter/callisto/src/entity_ext/mod.rs b/jupiter/callisto/src/entity_ext/mod.rs index 81173f3ac..e22234b13 100644 --- a/jupiter/callisto/src/entity_ext/mod.rs +++ b/jupiter/callisto/src/entity_ext/mod.rs @@ -14,6 +14,16 @@ pub fn generate_id() -> i64 { IdInstance::next_id() } +pub fn generate_link() -> String { + let rng = rand::rng(); + let str: String = rng + .sample_iter(rand::distr::Alphanumeric) + .take(8) + .map(char::from) + .collect(); + str.to_uppercase() +} + pub fn generate_public_id() -> String { let rng = rand::rng(); let str: String = rng @@ -26,10 +36,10 @@ pub fn generate_public_id() -> String { #[cfg(test)] mod test { - use crate::entity_ext::generate_public_id; + use crate::entity_ext::{generate_link, generate_public_id}; #[test] - fn test_link_generate() { + fn test_pub_id_generate() { let link = generate_public_id(); println!("public id: {:?}", link); assert!( @@ -39,4 +49,14 @@ mod test { .all(|c| c.is_alphanumeric() || c.is_lowercase()) ) } + + #[test] + fn test_link_generate() { + let link = generate_link(); + println!("MR Link: '{:?}'", link); + assert!( + link.chars().count() == 8 + && link.chars().all(|c| !c.is_alphabetic() || c.is_uppercase()) + ) + } } diff --git a/jupiter/src/service/issue_service.rs b/jupiter/src/service/issue_service.rs index d9cd40dab..2d8c64dd9 100644 --- a/jupiter/src/service/issue_service.rs +++ b/jupiter/src/service/issue_service.rs @@ -4,23 +4,32 @@ use crate::{ base_storage::{BaseStorage, StorageConnector}, conversation_storage::ConversationStorage, issue_storage::IssueStorage, + mr_storage::MrStorage, }, }; +use callisto::{mega_issue, mega_mr}; use common::errors::MegaError; #[derive(Clone)] pub struct IssueService { pub issue_storage: IssueStorage, + pub mr_storage: MrStorage, pub conversation_storage: ConversationStorage, } - impl IssueService { - pub fn new(issue_storage: IssueStorage, conversation_storage: ConversationStorage) -> Self { + pub fn new(base_storage: BaseStorage) -> Self { Self { - issue_storage, - conversation_storage, + issue_storage: IssueStorage { + base: base_storage.clone(), + }, + conversation_storage: ConversationStorage { + base: base_storage.clone(), + }, + mr_storage: MrStorage { + base: base_storage.clone(), + }, } } @@ -29,6 +38,7 @@ impl IssueService { Self { issue_storage: IssueStorage { base: mock.clone() }, conversation_storage: ConversationStorage { base: mock.clone() }, + mr_storage: MrStorage { base: mock.clone() }, } } @@ -63,4 +73,16 @@ impl IssueService { }; Ok(res) } + + pub async fn get_suggestions( + &self, + query: &str, + ) -> Result<(Vec, Vec), MegaError> { + let issues = self + .issue_storage + .get_issue_suggestions_by_query(query) + .await?; + let mrs = self.mr_storage.get_mr_suggestions_by_query(query).await?; + Ok((issues, mrs)) + } } diff --git a/jupiter/src/service/mr_service.rs b/jupiter/src/service/mr_service.rs index 08139b040..33d5c2141 100644 --- a/jupiter/src/service/mr_service.rs +++ b/jupiter/src/service/mr_service.rs @@ -16,10 +16,14 @@ pub struct MRService { } impl MRService { - pub fn new(mr_storage: MrStorage, conversation_storage: ConversationStorage) -> Self { + pub fn new(base_storage: BaseStorage) -> Self { Self { - mr_storage, - conversation_storage, + conversation_storage: ConversationStorage { + base: base_storage.clone(), + }, + mr_storage: MrStorage { + base: base_storage.clone(), + }, } } diff --git a/jupiter/src/storage/issue_storage.rs b/jupiter/src/storage/issue_storage.rs index ce5007e42..055c792aa 100644 --- a/jupiter/src/storage/issue_storage.rs +++ b/jupiter/src/storage/issue_storage.rs @@ -10,7 +10,6 @@ use sea_orm::{ use callisto::{item_assignees, item_labels, label, mega_conversation, mega_issue}; use common::errors::MegaError; use common::model::Pagination; -use common::utils::{generate_id, generate_link}; use crate::model::common::{ItemDetails, LabelAssigneeParams, ListParams}; use crate::storage::base_storage::{BaseStorage, StorageConnector}; @@ -100,6 +99,23 @@ impl IssueStorage { Ok((res, page)) } + pub async fn get_issue_suggestions_by_query( + &self, + query: &str, + ) -> Result, MegaError> { + let keyword = format!("%{query}%"); + let res = mega_issue::Entity::find() + .filter( + Condition::any() + .add(mega_issue::Column::Link.like(&keyword)) + .add(mega_issue::Column::Title.like(&keyword)), + ) + .limit(5) + .all(self.get_connection()) + .await?; + Ok(res) + } + pub async fn get_issue(&self, link: &str) -> Result, MegaError> { let model = mega_issue::Entity::find() .filter(mega_issue::Column::Link.eq(link)) @@ -147,16 +163,7 @@ impl IssueStorage { username: &str, title: &str, ) -> Result { - let model = mega_issue::Model { - id: generate_id(), - link: generate_link(), - title: title.to_owned(), - author: username.to_owned(), - status: "open".to_owned(), - created_at: chrono::Utc::now().naive_utc(), - updated_at: chrono::Utc::now().naive_utc(), - closed_at: None, - }; + let model = mega_issue::Model::new(title.to_owned(), username.to_owned()); let res = model .into_active_model() .insert(self.get_connection()) @@ -201,14 +208,7 @@ impl IssueStorage { color: &str, description: &str, ) -> Result { - let model = label::Model { - id: generate_id(), - name: name.to_owned(), - color: color.to_owned(), - description: description.to_owned(), - created_at: chrono::Utc::now().naive_utc(), - updated_at: chrono::Utc::now().naive_utc(), - }; + let model = label::Model::new(name, color, description); let res = model .into_active_model() .insert(self.get_connection()) diff --git a/jupiter/src/storage/mod.rs b/jupiter/src/storage/mod.rs index a6337eeef..7925cc226 100644 --- a/jupiter/src/storage/mod.rs +++ b/jupiter/src/storage/mod.rs @@ -96,16 +96,16 @@ impl Storage { relay_storage, user_storage, vault_storage, - mr_storage: mr_storage.clone(), - issue_storage: issue_storage.clone(), - conversation_storage: conversation_storage.clone(), + mr_storage, + issue_storage, + conversation_storage, lfs_file_storage, }; Storage { app_service: app_service.into(), config: Arc::downgrade(&config), - issue_service: IssueService::new(issue_storage, conversation_storage.clone()), - mr_service: MRService::new(mr_storage, conversation_storage), + issue_service: IssueService::new(base.clone()), + mr_service: MRService::new(base.clone()), } } diff --git a/jupiter/src/storage/mr_storage.rs b/jupiter/src/storage/mr_storage.rs index fe7ce7ed9..8ee114e36 100644 --- a/jupiter/src/storage/mr_storage.rs +++ b/jupiter/src/storage/mr_storage.rs @@ -11,7 +11,6 @@ use sea_orm::{ use callisto::sea_orm_active_enums::MergeStatusEnum; use callisto::{item_assignees, label, mega_conversation, mega_mr}; use common::errors::MegaError; -use common::utils::generate_id; use crate::model::common::{ItemDetails, ListParams}; use crate::storage::base_storage::{BaseStorage, StorageConnector}; @@ -123,6 +122,23 @@ impl MrStorage { Ok((res, page)) } + pub async fn get_mr_suggestions_by_query( + &self, + query: &str, + ) -> Result, MegaError> { + let keyword = format!("%{query}%"); + let res = mega_mr::Entity::find() + .filter( + Condition::any() + .add(mega_mr::Column::Link.like(&keyword)) + .add(mega_mr::Column::Title.like(&keyword)), + ) + .limit(5) + .all(self.get_connection()) + .await?; + Ok(res) + } + pub async fn get_mr(&self, link: &str) -> Result, MegaError> { let model = mega_mr::Entity::find() .filter(mega_mr::Column::Link.eq(link)) @@ -162,23 +178,17 @@ impl MrStorage { from_hash: &str, to_hash: &str, ) -> Result { - let link = common::utils::generate_link(); - - let mr = mega_mr::ActiveModel { - id: Set(generate_id()), - link: Set(link.clone()), - title: Set(title.to_owned()), - merge_date: Set(None), - status: Set(MergeStatusEnum::Open), - path: Set(path.to_owned()), - from_hash: Set(from_hash.to_owned()), - to_hash: Set(to_hash.to_owned()), - created_at: Set(chrono::Utc::now().naive_utc()), - updated_at: Set(chrono::Utc::now().naive_utc()), - }; - - mr.insert(self.get_connection()).await.unwrap(); - Ok(link) + let model = mega_mr::Model::new( + path.to_owned(), + title.to_owned(), + from_hash.to_owned(), + to_hash.to_owned(), + ); + let res = model + .into_active_model() + .insert(self.get_connection()) + .await?; + Ok(res.link) } pub async fn edit_title(&self, link: &str, title: &str) -> Result<(), MegaError> { diff --git a/mono/src/api/issue/issue_router.rs b/mono/src/api/issue/issue_router.rs index 22bf15d65..693b997bb 100644 --- a/mono/src/api/issue/issue_router.rs +++ b/mono/src/api/issue/issue_router.rs @@ -1,5 +1,5 @@ use axum::{ - extract::{Path, State}, + extract::{Path, Query, State}, Json, }; use utoipa_axum::{router::OpenApiRouter, routes}; @@ -9,7 +9,10 @@ use common::model::{CommonPage, CommonResult, PageParams}; use jupiter::service::issue_service::IssueService; use crate::api::{ - api_common::model::ListPayload, conversation::ContentPayload, label::LabelUpdatePayload, + api_common::model::ListPayload, + conversation::ContentPayload, + issue::{IssueSuggestions, QueryPayload}, + label::LabelUpdatePayload, }; use crate::api::{ api_common::{self, model::AssigneeUpdatePayload}, @@ -33,7 +36,8 @@ pub fn routers() -> OpenApiRouter { .routes(routes!(save_comment)) .routes(routes!(labels)) .routes(routes!(assignees)) - .routes(routes!(edit_title)), + .routes(routes!(edit_title)) + .routes(routes!(issue_suggester)), ) } @@ -270,3 +274,29 @@ async fn edit_title( .await?; Ok(Json(CommonResult::success(None))) } + +/// Get issue suggester in comment +#[utoipa::path( + get, + params(QueryPayload), + path = "/issue_suggester", + responses( + (status = 200, body = CommonResult>, content_type = "application/json") + ), + tag = ISSUE_TAG +)] +async fn issue_suggester( + Query(payload): Query, + state: State, +) -> Result>>, ApiError> { + let (issues, mrs) = state + .storage + .issue_service + .get_suggestions(&payload.query) + .await?; + let mut res: Vec = issues.into_iter().map(|m| m.into()).collect(); + let mut mr_list: Vec = mrs.into_iter().map(|m| m.into()).collect(); + res.append(&mut mr_list); + res.sort(); + Ok(Json(CommonResult::success(Some(res)))) +} diff --git a/mono/src/api/issue/mod.rs b/mono/src/api/issue/mod.rs index ba99bb936..0d3515fe5 100644 --- a/mono/src/api/issue/mod.rs +++ b/mono/src/api/issue/mod.rs @@ -1,9 +1,11 @@ +use callisto::{mega_issue, mega_mr, sea_orm_active_enums::MergeStatusEnum}; +use chrono::NaiveDateTime; use jupiter::{ model::common::{ItemDetails, ItemKind}, model::issue_dto::IssueDetails, }; use serde::{Deserialize, Serialize}; -use utoipa::ToSchema; +use utoipa::{IntoParams, ToSchema}; use crate::api::{conversation::ConversationItem, label::LabelItem}; @@ -100,3 +102,63 @@ impl From for IssueDetailRes { } } } + +#[derive(Serialize, ToSchema, PartialEq, Eq)] +pub struct IssueSuggestions { + pub id: i64, + pub link: String, + pub title: String, + #[serde(rename = "type")] + pub suggest_type: String, + #[serde(skip)] + pub created_at: NaiveDateTime, +} + +impl PartialOrd for IssueSuggestions { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for IssueSuggestions { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.created_at.cmp(&other.created_at) + } +} + +impl From for IssueSuggestions { + fn from(value: mega_issue::Model) -> Self { + Self { + id: value.id, + link: value.link, + title: value.title, + suggest_type: if value.status == "open" { + String::from("issue_open") + } else { + String::from("issue_closed") + }, + created_at: value.created_at, + } + } +} + +impl From for IssueSuggestions { + fn from(value: mega_mr::Model) -> Self { + Self { + id: value.id, + link: value.link, + title: value.title, + suggest_type: if value.status == MergeStatusEnum::Open { + String::from("merge_request") + } else { + String::from("merge_request_closed") + }, + created_at: value.created_at, + } + } +} + +#[derive(Deserialize, ToSchema, IntoParams)] +pub struct QueryPayload { + pub query: String, +} diff --git a/mono/src/api/mr/mr_router.rs b/mono/src/api/mr/mr_router.rs index c58b320c7..aece15376 100644 --- a/mono/src/api/mr/mr_router.rs +++ b/mono/src/api/mr/mr_router.rs @@ -344,10 +344,7 @@ async fn edit_title( state: State, Json(payload): Json, ) -> Result>, ApiError> { - state - .mr_stg() - .edit_title(&link, &payload.content) - .await?; + state.mr_stg().edit_title(&link, &payload.content).await?; Ok(Json(CommonResult::success(None))) } diff --git a/moon/packages/types/generated.ts b/moon/packages/types/generated.ts index 374176cd2..b8a446df8 100644 --- a/moon/packages/types/generated.ts +++ b/moon/packages/types/generated.ts @@ -3151,6 +3151,18 @@ export type CommonResultTreeResponse = { req_result: boolean } +export type CommonResultVecIssueSuggestions = { + data?: { + /** @format int64 */ + id: number + link: string + title: string + type: string + }[] + err_message: string + req_result: boolean +} + export type CommonResultVecMrFilesRes = { data?: { action: string @@ -3255,6 +3267,14 @@ export type IssueDetailRes = { title: string } +export type IssueSuggestions = { + /** @format int64 */ + id: number + link: string + title: string + type: string +} + export type ItemRes = { assignees: string[] author: string @@ -4578,6 +4598,12 @@ export type PostApiCreateFileData = CommonResultString export type PostApiIssueAssigneesData = CommonResultString +export type GetApiIssueIssueSuggesterParams = { + query: string +} + +export type GetApiIssueIssueSuggesterData = CommonResultVecIssueSuggestions + export type PostApiIssueLabelsData = CommonResultString export type PostApiIssueListData = CommonResultCommonPageItemRes @@ -13092,7 +13118,7 @@ export class Api extends HttpClient { @@ -13112,12 +13138,37 @@ export class Api extends HttpClient { + const base = 'GET:/api/v1/issue/issue_suggester' as const + + return { + baseKey: dataTaggedQueryKey([base]), + requestKey: (params: GetApiIssueIssueSuggesterParams) => + dataTaggedQueryKey([base, params]), + request: (query: GetApiIssueIssueSuggesterParams, params: RequestParams = {}) => + this.request({ + path: `/api/v1/issue/issue_suggester`, + method: 'GET', + query: query, + ...params + }) + } + }, + /** * No description * * @tags issue * @name PostApiIssueLabels - * @summary update issue related labels + * @summary Update issue related labels * @request POST:/api/v1/issue/labels */ postApiIssueLabels: () => { @@ -13385,7 +13436,7 @@ export class Api extends HttpClient { @@ -13410,7 +13461,7 @@ export class Api extends HttpClient { @@ -13623,7 +13674,7 @@ export class Api extends HttpClient {