From 46e3906af68e3f4d2748dd78d181166317ae5809 Mon Sep 17 00:00:00 2001 From: boxbeam Date: Thu, 25 Apr 2024 20:00:23 -0400 Subject: [PATCH] feat(webserver): add GraphQL endpoints for gitlab integration (#1967) * feat(webserver): add GraphQL endpoints for gitlab integration * Update graphql schema * Extract input types * Fix ui code --- .../components/provider-detail-form.tsx | 4 +- .../github/new/components/new-page.tsx | 2 +- ee/tabby-webserver/graphql/schema.graphql | 87 ++++++++---- .../src/schema/github_repository_provider.rs | 26 +--- .../src/schema/gitlab_repository_provider.rs | 26 +--- ee/tabby-webserver/src/schema/mod.rs | 130 +++++++++++++++++- ee/tabby-webserver/src/schema/types.rs | 25 ++++ 7 files changed, 217 insertions(+), 83 deletions(-) create mode 100644 ee/tabby-webserver/src/schema/types.rs diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/detail/components/provider-detail-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/detail/components/provider-detail-form.tsx index b5ba2479dc01..048ad7b6f7d8 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/detail/components/provider-detail-form.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/detail/components/provider-detail-form.tsx @@ -30,14 +30,14 @@ import { } from '../../components/github-form' const deleteGithubRepositoryProviderMutation = graphql(/* GraphQL */ ` - mutation DeleteGithubRepositoryProvider($id: ID!) { + mutation DeleteRepositoryProvider($id: ID!) { deleteGithubRepositoryProvider(id: $id) } `) const updateGithubRepositoryProviderMutation = graphql(/* GraphQL */ ` mutation UpdateGithubRepositoryProvider( - $input: UpdateGithubRepositoryProviderInput! + $input: UpdateRepositoryProviderInput! ) { updateGithubRepositoryProvider(input: $input) } diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/new/components/new-page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/new/components/new-page.tsx index dc78637d0aca..83e971d7f74d 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/new/components/new-page.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/repository/github/new/components/new-page.tsx @@ -20,7 +20,7 @@ import { const createGithubRepositoryProvider = graphql(/* GraphQL */ ` mutation CreateGithubRepositoryProvider( - $input: CreateGithubRepositoryProviderInput! + $input: CreateRepositoryProviderInput! ) { createGithubRepositoryProvider(input: $input) } diff --git a/ee/tabby-webserver/graphql/schema.graphql b/ee/tabby-webserver/graphql/schema.graphql index c52e2e6a3bc0..dd35fc094ae0 100644 --- a/ee/tabby-webserver/graphql/schema.graphql +++ b/ee/tabby-webserver/graphql/schema.graphql @@ -1,3 +1,15 @@ +input UpdateRepositoryProviderInput { + id: ID! + displayName: String! + accessToken: String! +} + +type GitlabRepositoryProvider { + id: ID! + displayName: String! + connected: Boolean! +} + enum Language { RUST PYTHON @@ -36,6 +48,11 @@ type JobRun { stderr: String! } +type GitlabProvidedRepositoryEdge { + node: GitlabProvidedRepository! + cursor: String! +} + enum AuthMethod { NONE PLAIN @@ -71,6 +88,11 @@ type LicenseInfo { expiresAt: DateTimeUtc } +type GitlabRepositoryProviderEdge { + node: GitlabRepositoryProvider! + cursor: String! +} + type GithubRepositoryProviderConnection { edges: [GithubRepositoryProviderEdge!]! pageInfo: PageInfo! @@ -130,6 +152,11 @@ type Invitation { createdAt: DateTimeUtc! } +type GitlabProvidedRepositoryConnection { + edges: [GitlabProvidedRepositoryEdge!]! + pageInfo: PageInfo! +} + type EmailSetting { smtpUsername: String! smtpServer: String! @@ -139,17 +166,17 @@ type EmailSetting { authMethod: AuthMethod! } -type TokenAuthResponse { - accessToken: String! - refreshToken: String! -} - type JobStats { success: Int! failed: Int! pending: Int! } +type TokenAuthResponse { + accessToken: String! + refreshToken: String! +} + type NetworkSetting { externalUrl: String! } @@ -201,10 +228,14 @@ type Mutation { deleteEmailSetting: Boolean! uploadLicense(license: String!): Boolean! resetLicense: Boolean! - createGithubRepositoryProvider(input: CreateGithubRepositoryProviderInput!): ID! + createGithubRepositoryProvider(input: CreateRepositoryProviderInput!): ID! deleteGithubRepositoryProvider(id: ID!): Boolean! - updateGithubRepositoryProvider(input: UpdateGithubRepositoryProviderInput!): Boolean! + updateGithubRepositoryProvider(input: UpdateRepositoryProviderInput!): Boolean! updateGithubProvidedRepositoryActive(id: ID!, active: Boolean!): Boolean! + createGitlabRepositoryProvider(input: CreateRepositoryProviderInput!): ID! + deleteGitlabRepositoryProvider(id: ID!): Boolean! + updateGitlabRepositoryProvider(input: UpdateRepositoryProviderInput!): Boolean! + updateGitlabProvidedRepositoryActive(id: ID!, active: Boolean!): Boolean! } type UserEventEdge { @@ -217,12 +248,6 @@ type RepositoryEdge { cursor: String! } -input UpdateGithubRepositoryProviderInput { - id: ID! - displayName: String! - accessToken: String! -} - "DateTime" scalar DateTimeUtc @@ -250,6 +275,8 @@ type Query { invitations(after: String, before: String, first: Int, last: Int): InvitationConnection! githubRepositoryProviders(ids: [ID!], after: String, before: String, first: Int, last: Int): GithubRepositoryProviderConnection! githubRepositories(providerIds: [ID!]!, after: String, before: String, first: Int, last: Int): GithubProvidedRepositoryConnection! + gitlabRepositoryProviders(ids: [ID!], after: String, before: String, first: Int, last: Int): GitlabRepositoryProviderConnection! + gitlabRepositories(providerIds: [ID!]!, after: String, before: String, first: Int, last: Int): GitlabProvidedRepositoryConnection! jobRuns(ids: [ID!], jobs: [String!], after: String, before: String, first: Int, last: Int): JobRunConnection! jobRunStats(jobs: [String!]): JobStats! emailSetting: EmailSetting @@ -299,6 +326,11 @@ type RefreshTokenResponse { refreshExpiresAt: DateTimeUtc! } +type GitlabRepositoryProviderConnection { + edges: [GitlabRepositoryProviderEdge!]! + pageInfo: PageInfo! +} + type RepositoryConnection { edges: [RepositoryEdge!]! pageInfo: PageInfo! @@ -320,6 +352,15 @@ enum LicenseType { ENTERPRISE } +type GitlabProvidedRepository { + id: ID! + vendorId: String! + gitlabRepositoryProviderId: ID! + name: String! + gitUrl: String! + active: Boolean! +} + type OAuthCredential { provider: OAuthProvider! clientId: String! @@ -347,7 +388,7 @@ input PasswordResetInput { password2: String! } -input CreateGithubRepositoryProviderInput { +input CreateRepositoryProviderInput { displayName: String! accessToken: String! } @@ -363,10 +404,6 @@ type User { isPasswordSet: Boolean! } -input RequestInvitationInput { - email: String! -} - type Worker { kind: WorkerKind! name: String! @@ -378,16 +415,16 @@ type Worker { cudaDevices: [String!]! } -enum OAuthProvider { - GITHUB - GOOGLE -} - type InvitationEdge { node: Invitation! cursor: String! } +enum OAuthProvider { + GITHUB + GOOGLE +} + type PageInfo { hasPreviousPage: Boolean! hasNextPage: Boolean! @@ -395,6 +432,10 @@ type PageInfo { endCursor: String } +input RequestInvitationInput { + email: String! +} + type GithubProvidedRepositoryEdge { node: GithubProvidedRepository! cursor: String! diff --git a/ee/tabby-webserver/src/schema/github_repository_provider.rs b/ee/tabby-webserver/src/schema/github_repository_provider.rs index 2251126cd215..83cabf889321 100644 --- a/ee/tabby-webserver/src/schema/github_repository_provider.rs +++ b/ee/tabby-webserver/src/schema/github_repository_provider.rs @@ -1,34 +1,10 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; -use juniper::{GraphQLInputObject, GraphQLObject, ID}; -use validator::Validate; +use juniper::{GraphQLObject, ID}; use super::Context; use crate::{juniper::relay::NodeType, schema::Result}; -#[derive(GraphQLInputObject, Validate)] -pub struct CreateGithubRepositoryProviderInput { - #[validate(regex( - code = "displayName", - path = "crate::schema::constants::REPOSITORY_NAME_REGEX" - ))] - pub display_name: String, - #[validate(length(code = "access_token", min = 10))] - pub access_token: String, -} - -#[derive(GraphQLInputObject, Validate)] -pub struct UpdateGithubRepositoryProviderInput { - pub id: ID, - #[validate(regex( - code = "displayName", - path = "crate::schema::constants::REPOSITORY_NAME_REGEX" - ))] - pub display_name: String, - #[validate(length(code = "access_token", min = 10))] - pub access_token: String, -} - #[derive(GraphQLObject, Debug, PartialEq)] #[graphql(context = Context)] pub struct GithubRepositoryProvider { diff --git a/ee/tabby-webserver/src/schema/gitlab_repository_provider.rs b/ee/tabby-webserver/src/schema/gitlab_repository_provider.rs index 6c33570c3860..c6bde56115e3 100644 --- a/ee/tabby-webserver/src/schema/gitlab_repository_provider.rs +++ b/ee/tabby-webserver/src/schema/gitlab_repository_provider.rs @@ -1,34 +1,10 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; -use juniper::{GraphQLInputObject, GraphQLObject, ID}; -use validator::Validate; +use juniper::{GraphQLObject, ID}; use super::Context; use crate::{juniper::relay::NodeType, schema::Result}; -#[derive(GraphQLInputObject, Validate)] -pub struct CreateGitlabRepositoryProviderInput { - #[validate(regex( - code = "displayName", - path = "crate::schema::constants::REPOSITORY_NAME_REGEX" - ))] - pub display_name: String, - #[validate(length(code = "access_token", min = 10))] - pub access_token: String, -} - -#[derive(GraphQLInputObject, Validate)] -pub struct UpdateGitlabRepositoryProviderInput { - pub id: ID, - #[validate(regex( - code = "displayName", - path = "crate::schema::constants::REPOSITORY_NAME_REGEX" - ))] - pub display_name: String, - #[validate(length(code = "access_token", min = 10))] - pub access_token: String, -} - #[derive(GraphQLObject, Debug, PartialEq)] #[graphql(context = Context)] pub struct GitlabRepositoryProvider { diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs index a0845c8b258a..3d5ecfbd28f2 100644 --- a/ee/tabby-webserver/src/schema/mod.rs +++ b/ee/tabby-webserver/src/schema/mod.rs @@ -9,6 +9,7 @@ pub mod job; pub mod license; pub mod repository; pub mod setting; +pub mod types; pub mod user_event; pub mod worker; @@ -38,10 +39,7 @@ use self::{ }, email::{EmailService, EmailSetting, EmailSettingInput}, git_repository::GitRepository, - github_repository_provider::{ - CreateGithubRepositoryProviderInput, GithubProvidedRepository, GithubRepositoryProvider, - UpdateGithubRepositoryProviderInput, - }, + github_repository_provider::{GithubProvidedRepository, GithubRepositoryProvider}, job::JobStats, license::{IsLicenseValid, LicenseInfo, LicenseService, LicenseType}, repository::RepositoryService, @@ -53,7 +51,11 @@ use self::{ use crate::{ axum::FromAuth, juniper::relay::{self, Connection}, - schema::repository::FileEntrySearchResult, + schema::{ + gitlab_repository_provider::{GitlabProvidedRepository, GitlabRepositoryProvider}, + repository::FileEntrySearchResult, + types::{CreateRepositoryProviderInput, UpdateRepositoryProviderInput}, + }, }; pub trait ServiceLocator: Send + Sync { @@ -286,6 +288,68 @@ impl Query { .await } + async fn gitlab_repository_providers( + ctx: &Context, + ids: Option>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .repository() + .gitlab() + .list_gitlab_repository_providers( + ids.unwrap_or_default(), + after, + before, + first, + last, + ) + .await + }, + ) + .await + } + + async fn gitlab_repositories( + ctx: &Context, + provider_ids: Vec, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + check_admin(ctx).await?; + relay::query_async( + after, + before, + first, + last, + |after, before, first, last| async move { + ctx.locator + .repository() + .gitlab() + .list_gitlab_provided_repositories_by_provider( + provider_ids, + after, + before, + first, + last, + ) + .await + }, + ) + .await + } + async fn job_runs( ctx: &Context, ids: Option>, @@ -733,7 +797,7 @@ impl Mutation { async fn create_github_repository_provider( ctx: &Context, - input: CreateGithubRepositoryProviderInput, + input: CreateRepositoryProviderInput, ) -> Result { check_admin(ctx).await?; input.validate()?; @@ -758,7 +822,7 @@ impl Mutation { async fn update_github_repository_provider( ctx: &Context, - input: UpdateGithubRepositoryProviderInput, + input: UpdateRepositoryProviderInput, ) -> Result { check_admin(ctx).await?; input.validate()?; @@ -782,6 +846,58 @@ impl Mutation { .await?; Ok(true) } + + async fn create_gitlab_repository_provider( + ctx: &Context, + input: CreateRepositoryProviderInput, + ) -> Result { + check_admin(ctx).await?; + input.validate()?; + let id = ctx + .locator + .repository() + .gitlab() + .create_gitlab_repository_provider(input.display_name, input.access_token) + .await?; + Ok(id) + } + + async fn delete_gitlab_repository_provider(ctx: &Context, id: ID) -> Result { + check_admin(ctx).await?; + ctx.locator + .repository() + .gitlab() + .delete_gitlab_repository_provider(id) + .await?; + Ok(true) + } + + async fn update_gitlab_repository_provider( + ctx: &Context, + input: UpdateRepositoryProviderInput, + ) -> Result { + check_admin(ctx).await?; + input.validate()?; + ctx.locator + .repository() + .gitlab() + .update_gitlab_repository_provider(input.id, input.display_name, input.access_token) + .await?; + Ok(true) + } + + async fn update_gitlab_provided_repository_active( + ctx: &Context, + id: ID, + active: bool, + ) -> Result { + ctx.locator + .repository() + .gitlab() + .update_gitlab_provided_repository_active(id, active) + .await?; + Ok(true) + } } async fn check_analytic_access(ctx: &Context, users: &[ID]) -> Result<(), CoreError> { diff --git a/ee/tabby-webserver/src/schema/types.rs b/ee/tabby-webserver/src/schema/types.rs new file mode 100644 index 000000000000..06ff4ceed7c7 --- /dev/null +++ b/ee/tabby-webserver/src/schema/types.rs @@ -0,0 +1,25 @@ +use juniper::{GraphQLInputObject, ID}; +use validator::Validate; + +#[derive(GraphQLInputObject, Validate)] +pub struct CreateRepositoryProviderInput { + #[validate(regex( + code = "displayName", + path = "crate::schema::constants::REPOSITORY_NAME_REGEX" + ))] + pub display_name: String, + #[validate(length(code = "access_token", min = 10))] + pub access_token: String, +} + +#[derive(GraphQLInputObject, Validate)] +pub struct UpdateRepositoryProviderInput { + pub id: ID, + #[validate(regex( + code = "displayName", + path = "crate::schema::constants::REPOSITORY_NAME_REGEX" + ))] + pub display_name: String, + #[validate(length(code = "access_token", min = 10))] + pub access_token: String, +}