diff --git a/app/client/workspace/workspace_service.go b/app/client/workspace/workspace_service.go
index 25df3a43..1b5a432a 100644
--- a/app/client/workspace/workspace_service.go
+++ b/app/client/workspace/workspace_service.go
@@ -21,6 +21,55 @@ type WorkspaceServiceClient struct {
slackAlert *monitoring.SlackAlert
}
+func (ws *WorkspaceServiceClient) ImportGitRepository(importRepository *request.ImportGitRepository) (createWorkspaceResponse *response.CreateWorkspaceResponse, err error) {
+ payload, err := json.Marshal(importRepository)
+ if err != nil {
+ log.Printf("failed to marshal import git repository request: %v", err)
+ return
+ }
+
+ req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/workspaces/import", ws.endpoint), bytes.NewBuffer(payload))
+ if err != nil {
+ log.Printf("failed to create import git repository request: %v", err)
+ return
+ }
+
+ res, err := ws.client.Do(req)
+ if err != nil {
+ log.Printf("failed to send import git repository request: %v", err)
+ return
+ }
+
+ if res.StatusCode < 200 || res.StatusCode > 299 {
+ err := ws.slackAlert.SendAlert(fmt.Sprintf("failed to import git repository: %s", res.Status), map[string]string{
+ "workspace_id": importRepository.WorkspaceId,
+ "repository": importRepository.Repository,
+ })
+ if err != nil {
+ log.Printf("failed to send slack alert: %v", err)
+ return nil, err
+ }
+ return nil, errors.New(fmt.Sprintf("invalid res from workspace service for import git repository request"))
+ }
+
+ defer func(Body io.ReadCloser) {
+ _ = Body.Close()
+ }(res.Body)
+
+ responseBody, err := io.ReadAll(res.Body)
+ if err != nil {
+ log.Printf("failed to read res payload: %v", err)
+ return
+ }
+
+ createWorkspaceResponse = &response.CreateWorkspaceResponse{}
+ if err = json.Unmarshal(responseBody, &createWorkspaceResponse); err != nil {
+ log.Printf("failed to unmarshal create workspace res: %v", err)
+ return
+ }
+ return
+}
+
func (ws *WorkspaceServiceClient) CreateWorkspace(createWorkspaceRequest *request.CreateWorkspaceRequest) (createWorkspaceResponse *response.CreateWorkspaceResponse, err error) {
payload, err := json.Marshal(createWorkspaceRequest)
if err != nil {
diff --git a/app/config/config.go b/app/config/config.go
index 87c5e809..9bad4c45 100644
--- a/app/config/config.go
+++ b/app/config/config.go
@@ -48,7 +48,7 @@ func LoadConfig() (*koanf.Koanf, error) {
return config, err
}
-// Get returns the value for a given key.
+// Deprecated: This is a misuse of the config package.
func Get(key string) interface{} {
return config.Get(key)
}
diff --git a/app/config/github_integration_config.go b/app/config/github_integration_config.go
new file mode 100644
index 00000000..609412a5
--- /dev/null
+++ b/app/config/github_integration_config.go
@@ -0,0 +1,23 @@
+package config
+
+import "github.com/knadh/koanf/v2"
+
+type GithubIntegrationConfig struct {
+ config *koanf.Koanf
+}
+
+func (gic *GithubIntegrationConfig) GetClientID() string {
+ return config.String("github.integration.client.id")
+}
+
+func (gic *GithubIntegrationConfig) GetClientSecret() string {
+ return config.String("github.integration.client.secret")
+}
+
+func (gic *GithubIntegrationConfig) GetRedirectURL() string {
+ return config.String("github.integration.client.redirecturl")
+}
+
+func NewGithubIntegrationConfig(config *koanf.Koanf) *GithubIntegrationConfig {
+ return &GithubIntegrationConfig{config}
+}
diff --git a/app/config/logger.go b/app/config/logger.go
index 546edbb8..475af75a 100644
--- a/app/config/logger.go
+++ b/app/config/logger.go
@@ -5,9 +5,9 @@ import "go.uber.org/zap"
var Logger *zap.Logger
func InitLogger() {
- var err error
- Logger, err = zap.NewProduction()
- if err != nil {
- panic(err)
+ if AppEnv() == "development" {
+ Logger, _ = zap.NewDevelopment(zap.IncreaseLevel(zap.DebugLevel))
+ } else {
+ Logger, _ = zap.NewProduction()
}
}
diff --git a/app/controllers/github_integration_controller.go b/app/controllers/github_integration_controller.go
new file mode 100644
index 00000000..d1448229
--- /dev/null
+++ b/app/controllers/github_integration_controller.go
@@ -0,0 +1,88 @@
+package controllers
+
+import (
+ "ai-developer/app/config"
+ "ai-developer/app/services/integrations"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+ "net/http"
+)
+
+type GithubIntegrationController struct {
+ githubIntegrationService *integrations.GithubIntegrationService
+ logger *zap.Logger
+}
+
+func (gic *GithubIntegrationController) Authorize(c *gin.Context) {
+ userId, _ := c.Get("user_id")
+ gic.logger.Debug(
+ "Authorizing github integration",
+ zap.Any("user_id", userId),
+ )
+ authCodeUrl := gic.githubIntegrationService.GetRedirectUrl(uint64(userId.(int)))
+ c.Redirect(http.StatusTemporaryRedirect, authCodeUrl)
+}
+
+func (gic *GithubIntegrationController) DeleteIntegration(c *gin.Context) {
+ userId, _ := c.Get("user_id")
+ err := gic.githubIntegrationService.DeleteIntegration(uint64(userId.(int)))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"message": "Integration deleted successfully"})
+}
+
+func (gic *GithubIntegrationController) CheckIfIntegrationExists(c *gin.Context) {
+ userId, _ := c.Get("user_id")
+ hasIntegration, err := gic.githubIntegrationService.HasGithubIntegration(uint64(userId.(int)))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{"integrated": hasIntegration})
+}
+
+func (gic *GithubIntegrationController) GetRepositories(c *gin.Context) {
+ userId, _ := c.Get("user_id")
+ repositories, err := gic.githubIntegrationService.GetRepositories(uint64(userId.(int)))
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+ response := make([]map[string]interface{}, 0)
+ for _, repo := range repositories {
+ response = append(response, map[string]interface{}{
+ "id": repo.GetID(),
+ "url": repo.GetCloneURL(),
+ "name": repo.GetFullName(),
+ })
+ }
+ c.JSON(http.StatusOK, gin.H{"repositories": response})
+}
+
+func (gic *GithubIntegrationController) HandleCallback(c *gin.Context) {
+ code := c.Query("code")
+ state := c.Query("state")
+
+ gic.logger.Debug(
+ "Handling github integration callback",
+ zap.String("code", code),
+ zap.String("state", state),
+ )
+
+ _ = gic.githubIntegrationService.GenerateAndSaveAccessToken(code, state)
+ redirectUrl := fmt.Sprintf("%s/settings?page=integrations", config.GithubFrontendURL())
+ c.Redirect(http.StatusTemporaryRedirect, redirectUrl)
+}
+
+func NewGithubIntegrationController(
+ githubIntegrationService *integrations.GithubIntegrationService,
+ logger *zap.Logger,
+) *GithubIntegrationController {
+ return &GithubIntegrationController{
+ githubIntegrationService: githubIntegrationService,
+ logger: logger,
+ }
+}
diff --git a/app/controllers/project_controller.go b/app/controllers/project_controller.go
index 0405523a..8d9e46ab 100644
--- a/app/controllers/project_controller.go
+++ b/app/controllers/project_controller.go
@@ -1,6 +1,7 @@
package controllers
import (
+ "ai-developer/app/models"
"ai-developer/app/services"
"ai-developer/app/types/request"
"net/http"
@@ -72,7 +73,14 @@ func (controller *ProjectController) CreateProject(context *gin.Context) {
context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "User not found"})
return
}
- project, err := controller.projectService.CreateProject(int(user.OrganisationID), createProjectRequest)
+
+ var project *models.Project
+ if createProjectRequest.Repository == nil {
+ project, err = controller.projectService.CreateProject(int(user.OrganisationID), createProjectRequest)
+ } else {
+ project, err = controller.projectService.CreateProjectFromGit(user.ID, user.OrganisationID, createProjectRequest)
+ }
+
if err != nil {
context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
diff --git a/app/db/migrations/20240718105113_create_integrations_table.down.sql b/app/db/migrations/20240718105113_create_integrations_table.down.sql
new file mode 100644
index 00000000..f96a9bed
--- /dev/null
+++ b/app/db/migrations/20240718105113_create_integrations_table.down.sql
@@ -0,0 +1 @@
+DROP TABLE integrations;
\ No newline at end of file
diff --git a/app/db/migrations/20240718105113_create_integrations_table.up.sql b/app/db/migrations/20240718105113_create_integrations_table.up.sql
new file mode 100644
index 00000000..0166be53
--- /dev/null
+++ b/app/db/migrations/20240718105113_create_integrations_table.up.sql
@@ -0,0 +1,15 @@
+CREATE TABLE integrations
+(
+ id SERIAL PRIMARY KEY,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ deleted_at TIMESTAMP,
+ user_id BIGINT NOT NULL,
+ integration_type VARCHAR(255) NOT NULL,
+ access_token VARCHAR(255) NOT NULL,
+ refresh_token VARCHAR(255),
+ metadata JSONB
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_user_integration ON integrations (user_id, integration_type);
+CREATE INDEX IF NOT EXISTS idx_user ON integrations (user_id);
\ No newline at end of file
diff --git a/app/db/migrations/20240723060635_add_repository_column_to_projects.down.sql b/app/db/migrations/20240723060635_add_repository_column_to_projects.down.sql
new file mode 100644
index 00000000..56505832
--- /dev/null
+++ b/app/db/migrations/20240723060635_add_repository_column_to_projects.down.sql
@@ -0,0 +1,2 @@
+ALTER TABLE projects DROP repository;
+ALTER TABLE projects DROP repository_url;
\ No newline at end of file
diff --git a/app/db/migrations/20240723060635_add_repository_column_to_projects.up.sql b/app/db/migrations/20240723060635_add_repository_column_to_projects.up.sql
new file mode 100644
index 00000000..82d2b3f4
--- /dev/null
+++ b/app/db/migrations/20240723060635_add_repository_column_to_projects.up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE projects ADD repository VARCHAR(1024);
+ALTER TABLE projects ADD repository_url VARCHAR(1024);
\ No newline at end of file
diff --git a/app/models/dtos/integrations/github.go b/app/models/dtos/integrations/github.go
new file mode 100644
index 00000000..766b1eb3
--- /dev/null
+++ b/app/models/dtos/integrations/github.go
@@ -0,0 +1,8 @@
+package integrations
+
+type GithubIntegrationDetails struct {
+ UserId uint64
+ GithubUserId string
+ AccessToken string
+ RefreshToken *string
+}
diff --git a/app/models/integration.go b/app/models/integration.go
new file mode 100644
index 00000000..09dbf561
--- /dev/null
+++ b/app/models/integration.go
@@ -0,0 +1,21 @@
+package models
+
+import (
+ "ai-developer/app/models/types"
+ "gorm.io/gorm"
+)
+
+type Integration struct {
+ *gorm.Model
+ ID uint `gorm:"primaryKey, autoIncrement"`
+
+ UserId uint64 `gorm:"column:user_id;not null"`
+ User User `gorm:"foreignKey:UserId;uniqueIndex:idx_user_integration"`
+
+ IntegrationType string `gorm:"column:integration_type;type:varchar(255);not null;uniqueIndex:idx_user_integration"`
+
+ AccessToken string `gorm:"type:varchar(255);not null"`
+ RefreshToken *string `gorm:"type:varchar(255);null"`
+
+ Metadata *types.JSONMap `gorm:"type:json;null"`
+}
diff --git a/app/models/project.go b/app/models/project.go
index b4a5e88b..3fddab1f 100644
--- a/app/models/project.go
+++ b/app/models/project.go
@@ -13,6 +13,8 @@ type Project struct {
Name string `gorm:"type:varchar(100);"`
BackendFramework string `gorm:"type:varchar(100);not null"`
FrontendFramework string `gorm:"type:varchar(100);not null"`
+ Repository *string `gorm:"type:varchar(1024);"`
+ RepositoryUrl *string `gorm:"type:varchar(1024);"`
Description string `gorm:"type:text"`
OrganisationID uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
diff --git a/app/repositories/integrations.go b/app/repositories/integrations.go
new file mode 100644
index 00000000..6e82d5de
--- /dev/null
+++ b/app/repositories/integrations.go
@@ -0,0 +1,87 @@
+package repositories
+
+import (
+ "ai-developer/app/models"
+ "ai-developer/app/models/types"
+ "errors"
+ "go.uber.org/zap"
+ "gorm.io/gorm"
+)
+
+type IntegrationsRepository struct {
+ db *gorm.DB
+ logger *zap.Logger
+}
+
+func (ir *IntegrationsRepository) FindIntegrationIdByUserIdAndType(userId uint64, integrationType string) (integration *models.Integration, err error) {
+ err = ir.db.Model(models.Integration{
+ UserId: userId,
+ IntegrationType: integrationType,
+ }).First(&integration).Error
+ if err != nil {
+ return nil, err
+ }
+ return
+}
+
+func (ir *IntegrationsRepository) DeleteIntegration(userId uint64, integrationType string) (err error) {
+ ir.logger.Info(
+ "Deleting integration",
+ zap.Uint64("userId", userId),
+ zap.String("integrationType", integrationType),
+ )
+ err = ir.db.Unscoped().Where(&models.Integration{
+ UserId: userId,
+ IntegrationType: integrationType,
+ }).Delete(&models.Integration{
+ UserId: userId,
+ IntegrationType: integrationType,
+ }).Error
+ return
+}
+
+func (ir *IntegrationsRepository) AddOrUpdateIntegration(
+ userId uint64,
+ integrationType string,
+ accessToken string,
+ refreshToken *string,
+ metadata *types.JSONMap,
+) (err error) {
+ integration, err := ir.FindIntegrationIdByUserIdAndType(userId, integrationType)
+ if !errors.Is(err, gorm.ErrRecordNotFound) {
+ return err
+ }
+
+ if integration != nil {
+ ir.logger.Info(
+ "Updating integration",
+ zap.Uint64("userId", integration.UserId),
+ zap.String("integrationType", integration.IntegrationType),
+ )
+ integration.AccessToken = accessToken
+ integration.RefreshToken = refreshToken
+ integration.Metadata = metadata
+ return ir.db.Save(integration).Error
+ } else {
+ integration = &models.Integration{
+ UserId: userId,
+ IntegrationType: integrationType,
+ AccessToken: accessToken,
+ RefreshToken: refreshToken,
+ Metadata: metadata,
+ }
+ ir.logger.Info(
+ "Adding new integration",
+ zap.Uint64("userId", userId),
+ zap.String("integrationType", integrationType),
+ )
+ return ir.db.Create(integration).Error
+ }
+}
+
+func NewIntegrationsRepository(db *gorm.DB, logger *zap.Logger) *IntegrationsRepository {
+ return &IntegrationsRepository{
+ db: db,
+ logger: logger.Named("IntegrationsRepository"),
+ }
+}
diff --git a/app/services/integrations/github_integration_service.go b/app/services/integrations/github_integration_service.go
new file mode 100644
index 00000000..799f1bea
--- /dev/null
+++ b/app/services/integrations/github_integration_service.go
@@ -0,0 +1,173 @@
+package integrations
+
+import (
+ "ai-developer/app/config"
+ "ai-developer/app/models/dtos/integrations"
+ "ai-developer/app/utils"
+ "context"
+ "errors"
+ "fmt"
+ "github.com/google/go-github/github"
+ "go.uber.org/zap"
+ "golang.org/x/oauth2"
+ githubOAuth "golang.org/x/oauth2/github"
+ "gorm.io/gorm"
+ "strconv"
+)
+
+type GithubIntegrationService struct {
+ logger *zap.Logger
+
+ oauthConfig oauth2.Config
+ githubIntegrationConfig *config.GithubIntegrationConfig
+
+ integrationService *IntegrationService
+}
+
+func (gis *GithubIntegrationService) DeleteIntegration(userId uint64) (err error) {
+ err = gis.integrationService.DeleteIntegration(userId, GithubIntegrationType)
+ return
+}
+
+func (gis *GithubIntegrationService) GetRedirectUrl(userId uint64) string {
+ return gis.oauthConfig.AuthCodeURL(fmt.Sprintf("%d", userId), oauth2.AccessTypeOnline)
+}
+
+func (gis *GithubIntegrationService) HasGithubIntegration(userId uint64) (hasIntegration bool, err error) {
+ integration, err := gis.integrationService.FindIntegrationIdByUserIdAndType(userId, GithubIntegrationType)
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return false, nil
+ }
+ if err != nil {
+ return
+ }
+ hasIntegration = integration != nil
+ return
+}
+
+func (gis *GithubIntegrationService) GetRepositories(userId uint64) (repos []*github.Repository, err error) {
+ integration, err := gis.integrationService.FindIntegrationIdByUserIdAndType(userId, GithubIntegrationType)
+ if errors.Is(err, gorm.ErrRecordNotFound) {
+ return make([]*github.Repository, 0), nil
+ }
+
+ if err != nil {
+ return
+ }
+
+ client := github.NewClient(gis.oauthConfig.Client(context.Background(), &oauth2.Token{
+ AccessToken: integration.AccessToken,
+ }))
+
+ repos, _, err = client.Repositories.List(context.Background(), "", &github.RepositoryListOptions{
+ ListOptions: github.ListOptions{
+ PerPage: 500,
+ },
+ })
+
+ if err != nil {
+ gis.logger.Error("Error getting github repositories", zap.Error(err))
+ return
+ }
+ return
+}
+
+func (gis *GithubIntegrationService) GetGithubIntegrationDetails(userId uint64) (integrationsDetails *integrations.GithubIntegrationDetails, err error) {
+ integration, err := gis.integrationService.FindIntegrationIdByUserIdAndType(userId, GithubIntegrationType)
+ if err != nil || integration == nil {
+ return
+ }
+
+ client := github.NewClient(gis.oauthConfig.Client(context.Background(), &oauth2.Token{
+ AccessToken: integration.AccessToken,
+ }))
+ githubUser, _, err := client.Users.Get(context.Background(), "")
+ if err != nil {
+ gis.logger.Error("Error getting github user", zap.Error(err))
+ return
+ }
+
+ integrationsDetails = &integrations.GithubIntegrationDetails{
+ UserId: integration.UserId,
+ AccessToken: integration.AccessToken,
+ RefreshToken: integration.RefreshToken,
+ GithubUserId: githubUser.GetLogin(),
+ }
+
+ return
+}
+
+func (gis *GithubIntegrationService) GenerateAndSaveAccessToken(code string, state string) (err error) {
+ token, err := gis.oauthConfig.Exchange(context.Background(), code)
+ if err != nil {
+ gis.logger.Error("Error exchanging code for token", zap.Error(err))
+ return
+ }
+
+ userId, err := strconv.ParseUint(state, 10, 64)
+ if err != nil {
+ gis.logger.Error("Error parsing state to userId", zap.Error(err))
+ return
+ }
+
+ if userId == 0 {
+ gis.logger.Error("Invalid userId", zap.Uint64("userId", userId))
+ return
+ }
+
+ client := github.NewClient(gis.oauthConfig.Client(context.Background(), token))
+ githubUser, _, err := client.Users.Get(context.Background(), "")
+ if err != nil {
+ gis.logger.Error("Error getting github user", zap.Error(err))
+ return
+ }
+
+ metadata, err := utils.GetAsJsonMap(githubUser)
+ if err != nil {
+ gis.logger.Error("Error getting github user as json map", zap.Error(err))
+ return
+ }
+
+ gis.logger.Info(
+ "Adding or updating github integration",
+ zap.Uint64("userId", userId),
+ zap.Any("metadata", metadata),
+ )
+
+ var refreshToken *string
+ if token.RefreshToken != "" {
+ refreshToken = &token.RefreshToken
+ }
+
+ err = gis.integrationService.AddOrUpdateIntegration(
+ userId,
+ GithubIntegrationType,
+ token.AccessToken,
+ refreshToken,
+ metadata,
+ )
+
+ return
+}
+
+func NewGithubIntegrationService(
+ logger *zap.Logger,
+ githubIntegrationConfig *config.GithubIntegrationConfig,
+ integrationService *IntegrationService,
+) *GithubIntegrationService {
+
+ oauthConfig := oauth2.Config{
+ RedirectURL: githubIntegrationConfig.GetRedirectURL(),
+ ClientID: githubIntegrationConfig.GetClientID(),
+ ClientSecret: githubIntegrationConfig.GetClientSecret(),
+ Scopes: []string{"user:email", "repo"},
+ Endpoint: githubOAuth.Endpoint,
+ }
+
+ return &GithubIntegrationService{
+ logger: logger.Named("GithubIntegrationService"),
+ oauthConfig: oauthConfig,
+ integrationService: integrationService,
+ githubIntegrationConfig: githubIntegrationConfig,
+ }
+}
diff --git a/app/services/integrations/integration_service.go b/app/services/integrations/integration_service.go
new file mode 100644
index 00000000..2ead1c52
--- /dev/null
+++ b/app/services/integrations/integration_service.go
@@ -0,0 +1,47 @@
+package integrations
+
+import (
+ "ai-developer/app/models"
+ "ai-developer/app/models/types"
+ "ai-developer/app/repositories"
+ "go.uber.org/zap"
+)
+
+type IntegrationService struct {
+ integrationsRepository *repositories.IntegrationsRepository
+ logger *zap.Logger
+}
+
+func (is *IntegrationService) FindIntegrationIdByUserIdAndType(userId uint64, integrationType string) (integration *models.Integration, err error) {
+ return is.integrationsRepository.FindIntegrationIdByUserIdAndType(userId, integrationType)
+}
+
+func (is *IntegrationService) DeleteIntegration(userId uint64, integrationType string) (err error) {
+ return is.integrationsRepository.DeleteIntegration(userId, integrationType)
+}
+
+func (is *IntegrationService) AddOrUpdateIntegration(
+ userId uint64,
+ integrationType string,
+ accessToken string,
+ refreshToken *string,
+ metadata *types.JSONMap,
+) (err error) {
+ return is.integrationsRepository.AddOrUpdateIntegration(
+ userId,
+ integrationType,
+ accessToken,
+ refreshToken,
+ metadata,
+ )
+}
+
+func NewIntegrationService(
+ integrationsRepository *repositories.IntegrationsRepository,
+ logger *zap.Logger,
+) *IntegrationService {
+ return &IntegrationService{
+ integrationsRepository: integrationsRepository,
+ logger: logger.Named("IntegrationService"),
+ }
+}
diff --git a/app/services/integrations/integrations.go b/app/services/integrations/integrations.go
new file mode 100644
index 00000000..c51c4028
--- /dev/null
+++ b/app/services/integrations/integrations.go
@@ -0,0 +1,5 @@
+package integrations
+
+const (
+ GithubIntegrationType = "GITHUB"
+)
diff --git a/app/services/project_service.go b/app/services/project_service.go
index d8d77f13..a3cc489e 100644
--- a/app/services/project_service.go
+++ b/app/services/project_service.go
@@ -8,6 +8,7 @@ import (
"ai-developer/app/models/dtos/asynq_task"
"ai-developer/app/repositories"
"ai-developer/app/services/git_providers"
+ "ai-developer/app/services/integrations"
"ai-developer/app/types/request"
"ai-developer/app/types/response"
"ai-developer/app/utils"
@@ -22,16 +23,17 @@ import (
)
type ProjectService struct {
- redisRepo *repositories.ProjectConnectionsRepository
- projectRepo *repositories.ProjectRepository
- organisationRepository *repositories.OrganisationRepository
- storyRepository *repositories.StoryRepository
- pullRequestRepository *repositories.PullRequestRepository
- gitnessService *git_providers.GitnessService
- hashIdGenerator *utils.HashIDGenerator
- workspaceServiceClient *workspace.WorkspaceServiceClient
- asynqClient *asynq.Client
- logger *zap.Logger
+ redisRepo *repositories.ProjectConnectionsRepository
+ projectRepo *repositories.ProjectRepository
+ organisationRepository *repositories.OrganisationRepository
+ storyRepository *repositories.StoryRepository
+ pullRequestRepository *repositories.PullRequestRepository
+ gitnessService *git_providers.GitnessService
+ hashIdGenerator *utils.HashIDGenerator
+ workspaceServiceClient *workspace.WorkspaceServiceClient
+ asynqClient *asynq.Client
+ githubIntegrationService *integrations.GithubIntegrationService
+ logger *zap.Logger
}
func (s *ProjectService) GetAllProjectsOfOrganisation(organisationId int) ([]response.GetAllProjectsResponse, error) {
@@ -64,6 +66,7 @@ func (s *ProjectService) GetAllProjectsOfOrganisation(organisationId int) ([]res
ProjectBackendURL: project.BackendURL,
ProjectFrontendURL: project.FrontendURL,
PullRequestCount: len(projectPullRequestMap[int(project.ID)]),
+ ProjectRepository: project.Repository,
})
}
@@ -82,6 +85,89 @@ func (s *ProjectService) GetProjectDetailsById(projectId int) (*models.Project,
return project, nil
}
+func (s *ProjectService) CreateProjectFromGit(userId uint, orgId uint, requestData request.CreateProjectRequest) (project *models.Project, err error) {
+ integrationDetails, err := s.githubIntegrationService.GetGithubIntegrationDetails(uint64(userId))
+ if err != nil {
+ s.logger.Error("Error getting github integration details", zap.Error(err))
+ return nil, err
+ }
+
+ hashID := s.hashIdGenerator.Generate() + "-" + uuid.New().String()
+ url := "http://localhost:8081/?folder=/workspaces/" + hashID
+ backend_url := "http://localhost:5000"
+ frontend_url := "http://localhost:3000"
+ env := config.Get("app.env")
+ host := config.Get("workspace.host")
+ if env == "production" {
+ url = fmt.Sprintf("https://%s.%s/?folder=/workspaces/%s", hashID, host, hashID)
+ backend_url = fmt.Sprintf("https://be-%s.%s", hashID, host)
+ frontend_url = fmt.Sprintf("https://fe-%s.%s", hashID, host)
+ }
+ project = &models.Project{
+ OrganisationID: orgId,
+ Name: requestData.Name,
+ BackendFramework: requestData.Framework,
+ FrontendFramework: requestData.FrontendFramework,
+ Description: requestData.Description,
+ HashID: hashID,
+ Url: url,
+ BackendURL: backend_url,
+ FrontendURL: frontend_url,
+ Repository: requestData.Repository,
+ RepositoryUrl: requestData.RepositoryUrl,
+ }
+
+ organisation, err := s.organisationRepository.GetOrganisationByID(uint(int(project.OrganisationID)))
+ spaceOrProjectName := s.gitnessService.GetSpaceOrProjectName(organisation)
+ repository, err := s.gitnessService.CreateRepository(spaceOrProjectName, project.Name, project.Description)
+ if err != nil {
+ s.logger.Error("Error creating repository", zap.Error(err))
+ return nil, err
+ }
+ httpPrefix := "https"
+
+ if config.AppEnv() == constants.Development {
+ httpPrefix = "http"
+ }
+
+ remoteGitURL := fmt.Sprintf("%s://%s:%s@%s/git/%s/%s.git", httpPrefix, config.GitnessUser(), config.GitnessToken(), config.GitnessHost(), spaceOrProjectName, project.Name)
+ //Making Call to Workspace Service to create workspace on project level
+ _, err = s.workspaceServiceClient.ImportGitRepository(
+ &request.ImportGitRepository{
+ WorkspaceId: hashID,
+ Repository: *requestData.RepositoryUrl,
+ Username: integrationDetails.GithubUserId,
+ Password: integrationDetails.AccessToken,
+ RemoteURL: remoteGitURL,
+ GitnessUser: config.GitnessUser(),
+ GitnessToken: config.GitnessToken(),
+ },
+ )
+
+ if err != nil {
+ s.logger.Error("Error creating workspace", zap.Error(err))
+ return nil, err
+ }
+
+ //Enqueue job to delete workspace with updated delay
+ payloadBytes, err := json.Marshal(asynq_task.CreateDeleteWorkspaceTaskPayload{
+ WorkspaceID: project.HashID,
+ })
+ if err != nil {
+ s.logger.Error("Failed to marshal payload", zap.Error(err))
+ return nil, err
+ }
+ _, err = s.asynqClient.Enqueue(
+ asynq.NewTask(constants.DeleteWorkspaceTaskType, payloadBytes),
+ asynq.ProcessIn(constants.ProjectConnectionTTL+10*time.Minute),
+ asynq.MaxRetry(3),
+ asynq.TaskID("delete:fallback:"+project.HashID),
+ )
+
+ s.logger.Info("Project created successfully with repository", zap.Any("project", project), zap.Any("repository", repository))
+ return s.projectRepo.CreateProject(project)
+}
+
func (s *ProjectService) CreateProject(organisationID int, requestData request.CreateProjectRequest) (*models.Project, error) {
hashID := s.hashIdGenerator.Generate() + "-" + uuid.New().String()
url := "http://localhost:8081/?folder=/workspaces/" + hashID
@@ -314,21 +400,23 @@ func NewProjectService(projectRepo *repositories.ProjectRepository,
storyRepository *repositories.StoryRepository,
pullRequestRepository *repositories.PullRequestRepository,
workspaceServiceClient *workspace.WorkspaceServiceClient,
+ githubIntegrationService *integrations.GithubIntegrationService,
repo *repositories.ProjectConnectionsRepository,
asynqClient *asynq.Client,
logger *zap.Logger,
) *ProjectService {
return &ProjectService{
- projectRepo: projectRepo,
- gitnessService: gitnessService,
- organisationRepository: organisationRepository,
- storyRepository: storyRepository,
- pullRequestRepository: pullRequestRepository,
- workspaceServiceClient: workspaceServiceClient,
- redisRepo: repo,
- hashIdGenerator: utils.NewHashIDGenerator(5),
- logger: logger.Named("ProjectService"),
- asynqClient: asynqClient,
+ projectRepo: projectRepo,
+ gitnessService: gitnessService,
+ organisationRepository: organisationRepository,
+ storyRepository: storyRepository,
+ pullRequestRepository: pullRequestRepository,
+ workspaceServiceClient: workspaceServiceClient,
+ redisRepo: repo,
+ hashIdGenerator: utils.NewHashIDGenerator(5),
+ logger: logger.Named("ProjectService"),
+ asynqClient: asynqClient,
+ githubIntegrationService: githubIntegrationService,
}
}
diff --git a/app/types/request/create_project_request.go b/app/types/request/create_project_request.go
index 1024434b..f725d228 100644
--- a/app/types/request/create_project_request.go
+++ b/app/types/request/create_project_request.go
@@ -1,8 +1,10 @@
package request
type CreateProjectRequest struct {
- Name string `json:"name"`
- Framework string `json:"framework"`
- FrontendFramework string `json:"frontend_framework"`
- Description string `json:"description"`
+ Name string `json:"name"`
+ Framework string `json:"framework"`
+ FrontendFramework string `json:"frontend_framework"`
+ Description string `json:"description"`
+ Repository *string `json:"repository"`
+ RepositoryUrl *string `json:"repository_url"`
}
diff --git a/app/types/request/import_github_repository.go b/app/types/request/import_github_repository.go
new file mode 100644
index 00000000..643d8184
--- /dev/null
+++ b/app/types/request/import_github_repository.go
@@ -0,0 +1,13 @@
+package request
+
+type ImportGitRepository struct {
+ WorkspaceId string `json:"workspaceId"`
+
+ Repository string `json:"repository"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+
+ RemoteURL string `json:"remoteURL"`
+ GitnessUser string `json:"gitnessUser"`
+ GitnessToken string `json:"gitnessToken"`
+}
diff --git a/app/types/response/get_all_projects_response.go b/app/types/response/get_all_projects_response.go
index e8036270..39124b64 100644
--- a/app/types/response/get_all_projects_response.go
+++ b/app/types/response/get_all_projects_response.go
@@ -1,14 +1,15 @@
package response
type GetAllProjectsResponse struct {
- ProjectId uint `json:"project_id"`
- ProjectName string `json:"project_name"`
- ProjectDescription string `json:"project_description"`
- ProjectFramework string `json:"project_framework"`
- ProjectFrontendFramework string `json:"project_frontend_framework"`
- ProjectHashID string `json:"project_hash_id"`
- ProjectUrl string `json:"project_url"`
- ProjectBackendURL string `json:"project_backend_url"`
- ProjectFrontendURL string `json:"project_frontend_url"`
- PullRequestCount int `json:"pull_request_count"`
+ ProjectId uint `json:"project_id"`
+ ProjectName string `json:"project_name"`
+ ProjectDescription string `json:"project_description"`
+ ProjectFramework string `json:"project_framework"`
+ ProjectFrontendFramework string `json:"project_frontend_framework"`
+ ProjectHashID string `json:"project_hash_id"`
+ ProjectUrl string `json:"project_url"`
+ ProjectBackendURL string `json:"project_backend_url"`
+ ProjectFrontendURL string `json:"project_frontend_url"`
+ PullRequestCount int `json:"pull_request_count"`
+ ProjectRepository *string `json:"project_repository"`
}
diff --git a/app/utils/json_utils.go b/app/utils/json_utils.go
new file mode 100644
index 00000000..d1a6d42a
--- /dev/null
+++ b/app/utils/json_utils.go
@@ -0,0 +1,22 @@
+package utils
+
+import (
+ "ai-developer/app/config"
+ "ai-developer/app/models/types"
+ "encoding/json"
+ "go.uber.org/zap"
+)
+
+func GetAsJsonMap(item interface{}) (jsonMap *types.JSONMap, err error) {
+ jsonBytes, err := json.Marshal(item)
+ if err != nil {
+ config.Logger.Error("Error marshalling item", zap.Error(err))
+ return
+ }
+ err = json.Unmarshal(jsonBytes, &jsonMap)
+ if err != nil {
+ config.Logger.Error("Error unmarshalling item", zap.Error(err))
+ return
+ }
+ return
+}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index c3ab13c4..70adbe9b 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -30,6 +30,9 @@ services:
AI_DEVELOPER_AWS_BUCKET_NAME: ${AI_DEVELOPER_AWS_BUCKET_NAME}
AI_DEVELOPER_AWS_REGION: ${AI_DEVELOPER_AWS_REGION}
AI_DEVELOPER_WORKSPACE_STATIC_FRONTEND_URL: "http://localhost:8083"
+ AI_DEVELOPER_GITHUB_INTEGRATION_CLIENT_ID: ${AI_DEVELOPER_GITHUB_INTEGRATION_CLIENT_ID:-}
+ AI_DEVELOPER_GITHUB_INTEGRATION_CLIENT_SECRET: ${AI_DEVELOPER_GITHUB_INTEGRATION_CLIENT_SECRET:-}
+ AI_DEVELOPER_GITHUB_INTEGRATION_CLIENT_REDIRECT_URL: ${AI_DEVELOPER_GITHUB_INTEGRATION_CLIENT_REDIRECT_URL:-}
volumes:
- workspaces:/workspaces
- './startup.sh:/startup.sh'
diff --git a/gui/package.json b/gui/package.json
index 2aa1006d..b6ad796b 100644
--- a/gui/package.json
+++ b/gui/package.json
@@ -14,6 +14,7 @@
"@nextui-org/react": "^2.3.6",
"axios": "^1.7.2",
"babel-plugin-styled-components": "^2.1.4",
+ "chroma-js": "^2.4.2",
"cookie": "^0.6.0",
"diff2html": "^3.4.48",
"framer-motion": "^11.2.4",
@@ -21,6 +22,7 @@
"js-cookie": "^3.0.5",
"monaco-editor": "^0.50.0",
"next": "14.2.2",
+ "next-themes": "^0.3.0",
"react": "^18",
"react-beautiful-dnd": "^13.1.1",
"react-diff-view": "^3.2.1",
@@ -29,6 +31,7 @@
"react-dom": "^18",
"react-hot-toast": "^2.4.1",
"react-loading-skeleton": "^3.4.0",
+ "react-select": "^5.8.0",
"react-syntax-highlighter": "^15.5.0",
"react-transition-group": "^4.4.5",
"sharp": "^0.33.4",
@@ -36,6 +39,7 @@
"styled-components": "^6.1.11"
},
"devDependencies": {
+ "@types/chroma-js": "^2.4.4",
"@types/cookie": "^0.6.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20",
diff --git a/gui/public/logos/github_light_logo.svg b/gui/public/logos/github_light_logo.svg
new file mode 100644
index 00000000..a3968743
--- /dev/null
+++ b/gui/public/logos/github_light_logo.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/gui/src/api/DashboardService.tsx b/gui/src/api/DashboardService.tsx
index 73e62071..77b5590f 100644
--- a/gui/src/api/DashboardService.tsx
+++ b/gui/src/api/DashboardService.tsx
@@ -5,8 +5,8 @@ import {
} from '../../types/projectsTypes';
import { FormStoryPayload } from '../../types/storyTypes';
import {
- CommentReBuildPayload,
CommentReBuildDesignStoryPayload,
+ CommentReBuildPayload,
CreatePullRequestPayload,
} from '../../types/pullRequestsTypes';
import { CreateOrUpdateLLMAPIKeyPayload } from '../../types/modelsTypes';
@@ -180,3 +180,19 @@ export const rebuildDesignStory = (
export const updateReviewViewedStatus = (story_id: number) => {
return api.put(`/stories/design/review_viewed/${story_id}`, {});
};
+
+export const isGithubConnected = async () => {
+ const response = await api.get(`/integrations/github`);
+ const { integrated }: { integrated: boolean } = response.data;
+ return integrated;
+};
+export const getGithubRepos = async () => {
+ const response = await api.get(`/integrations/github/repos`);
+ const { repositories } = response.data;
+ return repositories;
+};
+
+export const deleteGithubIntegration = async () => {
+ const response = await api.delete(`/integrations/github`);
+ return response.data;
+};
diff --git a/gui/src/app/_app.css b/gui/src/app/_app.css
index d4c35989..5767f7d9 100644
--- a/gui/src/app/_app.css
+++ b/gui/src/app/_app.css
@@ -28,6 +28,8 @@
:root {
--foreground-rgb: #fff;
--background-color: #000;
+ --border-color: #313134;
+ --container-background: #262626;
--secondary-color: #888;
--text-color: #fff;
--error-color: #FF3E2D;
diff --git a/gui/src/app/imagePath.tsx b/gui/src/app/imagePath.tsx
index 52dbb489..76e1b16c 100644
--- a/gui/src/app/imagePath.tsx
+++ b/gui/src/app/imagePath.tsx
@@ -4,6 +4,7 @@ export default {
superagiLogo: '/logos/superagi_logo.svg',
superagiLogoRound: '/logos/superagi_logo_round.svg',
supercoderImage: '/images/supercoder_image.svg',
+ githubLightLogo: '/logos/github_light_logo.svg',
githubLogo: '/logos/github_logo.svg',
frontendWorkbenchIconSelected: '/icons/selected/frontend_workbench.svg',
backendWorkbenchIconSelected: '/icons/selected/backend_workbench.svg',
diff --git a/gui/src/app/projects/layout.tsx b/gui/src/app/projects/layout.tsx
index 9a322b9f..dc453550 100644
--- a/gui/src/app/projects/layout.tsx
+++ b/gui/src/app/projects/layout.tsx
@@ -1,7 +1,6 @@
import React, { ReactNode } from 'react';
import NavBar from '@/components/LayoutComponents/NavBar';
import styles from './projects.module.css';
-import { SocketProvider } from '@/context/SocketContext';
export default function ProjectsLayout({
children,
diff --git a/gui/src/app/projects/page.tsx b/gui/src/app/projects/page.tsx
index 07b27635..9e26bc40 100644
--- a/gui/src/app/projects/page.tsx
+++ b/gui/src/app/projects/page.tsx
@@ -11,6 +11,7 @@ import CreateOrEditProjectBody from '@/components/HomeComponents/CreateOrEditPro
import CustomLoaders from '@/components/CustomLoaders/CustomLoaders';
import { SkeletonTypes } from '@/app/constants/SkeletonConstants';
import CustomImage from '@/components/ImageComponents/CustomImage';
+import Image from 'next/image';
export default function Projects() {
const [openNewProjectModal, setOpenNewProjectModal] = useState<
@@ -79,6 +80,20 @@ export default function Projects() {
+ {project.project_repository && (
+
+
+
+
+
+ {project.project_repository.split('/').join(' / ')}
+
+
+ )}
{children};
+ return (
+
+
+ {children}
+
+
+ );
}
diff --git a/gui/src/app/settings/SettingsOptions/Integrations.tsx b/gui/src/app/settings/SettingsOptions/Integrations.tsx
new file mode 100644
index 00000000..bc5c00e4
--- /dev/null
+++ b/gui/src/app/settings/SettingsOptions/Integrations.tsx
@@ -0,0 +1,80 @@
+'use client';
+
+import React, { useEffect, useState } from 'react';
+
+import styles from '../settings.module.css';
+import {
+ deleteGithubIntegration,
+ isGithubConnected,
+} from '@/api/DashboardService';
+import { API_BASE_URL } from '@/api/apiConfig';
+import GithubIntegrationSymbol from '@/components/IntegrationComponents/GithubIntegrationSymbol';
+import { Button } from '@nextui-org/react';
+
+async function redirectToGithubIntegration() {
+ try {
+ window.location.href = `${API_BASE_URL}/integrations/github/authorize`;
+ } catch (error) {
+ console.error('Error: ', error);
+ }
+}
+
+const Integrations = () => {
+ const [isExternalGitIntegration, setIsExternalGitIntegration] = useState<
+ boolean | undefined
+ >(null);
+ useEffect(() => {
+ (async function () {
+ const gitIntegrated = await isGithubConnected();
+ setIsExternalGitIntegration(gitIntegrated);
+ })();
+ }, []);
+
+ const deleteIntegration = async () => {
+ await deleteGithubIntegration();
+ const gitIntegrated = await isGithubConnected();
+ setIsExternalGitIntegration(gitIntegrated);
+ };
+
+ return (
+
+
+
Integrations
+
+
+
+
+
+
GitHub
+
+ Integrate with GitHub to start working on your projects
+
+
+
+ {isExternalGitIntegration === undefined ? (
+ <>>
+ ) : isExternalGitIntegration === false ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ );
+};
+
+export default Integrations;
diff --git a/gui/src/app/settings/page.tsx b/gui/src/app/settings/page.tsx
index f9d1c979..0816a2cd 100644
--- a/gui/src/app/settings/page.tsx
+++ b/gui/src/app/settings/page.tsx
@@ -4,6 +4,7 @@ import Models from '@/app/settings/SettingsOptions/Models';
import CustomSidebar from '@/components/CustomSidebar/CustomSidebar';
import imagePath from '@/app/imagePath';
import BackButton from '@/components/BackButton/BackButton';
+import Integrations from '@/app/settings/SettingsOptions/Integrations';
export default function Settings() {
const options = [
@@ -15,6 +16,14 @@ export default function Settings() {
icon_css: 'size-4',
component: ,
},
+ {
+ key: 'integrations',
+ text: 'Integrations',
+ selected: imagePath.modelsIconSelected,
+ unselected: imagePath.modelsIconUnselected,
+ icon_css: 'size-4',
+ component: ,
+ },
];
const handleOptionSelect = (key: string) => {
diff --git a/gui/src/app/settings/settings.module.css b/gui/src/app/settings/settings.module.css
new file mode 100644
index 00000000..ec69373a
--- /dev/null
+++ b/gui/src/app/settings/settings.module.css
@@ -0,0 +1,3 @@
+.integration_container {
+ background-color: var(--container-background);
+}
\ No newline at end of file
diff --git a/gui/src/components/CustomSidebar/CustomSidebar.tsx b/gui/src/components/CustomSidebar/CustomSidebar.tsx
index 3fd8dc00..a9f847ed 100644
--- a/gui/src/components/CustomSidebar/CustomSidebar.tsx
+++ b/gui/src/components/CustomSidebar/CustomSidebar.tsx
@@ -1,8 +1,9 @@
'use client';
-import React, { useState, useCallback } from 'react';
+import React, { useCallback, useState } from 'react';
import styles from './sidebar.module.css';
import CustomImage from '@/components/ImageComponents/CustomImage';
import { CustomSidebarProps } from '../../../types/customComponentTypes';
+import { useSearchParams } from 'next/navigation';
const CustomSidebar: React.FC = ({
id,
@@ -10,7 +11,14 @@ const CustomSidebar: React.FC = ({
options,
onOptionSelect,
}) => {
- const [selectedKey, setSelectedKey] = useState(options[0].key);
+ const searchParams = useSearchParams();
+ const page = searchParams.get('page');
+ const selectedPage =
+ options?.find((option) => option.key === page) || options[0];
+
+ const [selectedKey, setSelectedKey] = useState(
+ selectedPage.key,
+ );
const handleOptionClick = useCallback(
(key: string) => {
diff --git a/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx b/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx
index 923ce321..c57c050f 100644
--- a/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx
+++ b/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx
@@ -13,13 +13,19 @@ import {
} from '../../../types/projectsTypes';
import {
createProject,
+ getGithubRepos,
getProjectById,
+ isGithubConnected,
updateProject,
} from '@/api/DashboardService';
import { useRouter } from 'next/navigation';
import { setProjectDetails } from '@/app/utils';
import CustomImage from '@/components/ImageComponents/CustomImage';
import CustomInput from '@/components/CustomInput/CustomInput';
+import styles from './create-project.module.css';
+import imagePath from '@/app/imagePath';
+import { API_BASE_URL } from '@/api/apiConfig';
+import Select from 'react-select';
interface CreateOrEditProjectBodyProps {
id: string;
@@ -29,6 +35,44 @@ interface CreateOrEditProjectBodyProps {
edit?: boolean;
}
+const customStyles = {
+ control: (provided) => ({
+ ...provided,
+ borderRadius: '0.5rem',
+ backgroundColor: '#1e1e1e',
+ color: '#ffffff',
+ borderColor: '#333333',
+ boxShadow: 'none',
+ '&:hover': {
+ color: '#ffffff',
+ borderColor: '#333333',
+ boxShadow: 'none',
+ },
+ }),
+ indicatorSeparator: (provided) => ({}),
+ menu: (provided) => ({
+ ...provided,
+ backgroundColor: '#1e1e1e',
+ }),
+ input: (styles) => ({
+ ...styles,
+ color: '#ffffff',
+ }),
+ option: (provided, state) => ({
+ ...provided,
+ backgroundColor: state.isFocused ? '#333333' : '#1e1e1e',
+ color: '#ffffff',
+ '&:hover': {
+ backgroundColor: '#2a2a2a',
+ color: '#ffffff',
+ },
+ }),
+ singleValue: (provided) => ({
+ ...provided,
+ color: '#ffffff',
+ }),
+};
+
export default function CreateOrEditProjectBody({
id,
openProjectModal,
@@ -48,6 +92,33 @@ export default function CreateOrEditProjectBody({
const projectIdRef = useRef(null);
const router = useRouter();
+ const [integrationLoading, setIntegrationLoading] = useState(false);
+ const [isExternalGitIntegration, setIsExternalGitIntegration] =
+ useState(false);
+ const [useExternalGit, setUseExternalGit] = useState(false);
+ const [repositories, setRepositories] = useState([]);
+ const [selectedRepository, setSelectedRepository] = useState<{
+ label: string;
+ value: string;
+ } | null>(null);
+
+ async function redirectToGithubIntegration() {
+ setIntegrationLoading(true);
+ try {
+ const interval = setInterval(async () => {
+ const gitIntegrated = await isGithubConnected();
+ if (gitIntegrated) {
+ setIsExternalGitIntegration(true);
+ setIntegrationLoading(false);
+ clearInterval(interval);
+ }
+ }, 1000);
+ window.open(`${API_BASE_URL}/integrations/github/authorize`, '_blank');
+ } catch (error) {
+ console.error('Error: ', error);
+ }
+ }
+
const handleProjectDuplicationCheck = () => {
if (!projectsList) {
return false; // or handle the case where projectsList is null
@@ -66,6 +137,10 @@ export default function CreateOrEditProjectBody({
const handleCreateNewProject = async () => {
setIsLoading(true);
const projectErrors = [
+ {
+ validation: selectedRepository === null && useExternalGit,
+ message: 'Please select a github repository to import.',
+ },
{
validation: handleProjectDuplicationCheck(),
message: 'A project with the name entered already exists.',
@@ -101,11 +176,36 @@ export default function CreateOrEditProjectBody({
framework: selectedBackendFramework,
frontend_framework: selectedFrontendFramework,
description: projectDescription,
+ repository: useExternalGit ? selectedRepository.label : undefined,
+ repository_url: useExternalGit ? selectedRepository.value : undefined,
};
await toCreateNewProject(newProjectPayload);
}
};
+ useEffect(() => {
+ (async function () {
+ const repositories = await getGithubRepos();
+ const options = repositories.map(
+ (repository: { name: string; url: string }) => {
+ const { name, url } = repository;
+ return {
+ value: url,
+ label: name,
+ };
+ },
+ );
+ setRepositories(options);
+ })();
+ }, [isExternalGitIntegration]);
+
+ useEffect(() => {
+ (async function () {
+ const gitIntegrated = await isGithubConnected();
+ setIsExternalGitIntegration(gitIntegrated);
+ })();
+ }, []);
+
useEffect(() => {
if (typeof window !== 'undefined') {
projectIdRef.current = localStorage.getItem('projectId');
@@ -261,6 +361,77 @@ export default function CreateOrEditProjectBody({
/>
)}
+
+
+
+
+
+
+
+
+ *Repository name will be named after the project name itself.
+
+
+
+ {useExternalGit && isExternalGitIntegration && (
+ <>
+