From 99ad4d03be3983c93a516f07655a14b80891fb0e Mon Sep 17 00:00:00 2001 From: Sasikanth L Date: Thu, 18 Jul 2024 20:24:29 +0530 Subject: [PATCH 1/7] Add github integration --- app/config/config.go | 2 +- app/config/github_integration_config.go | 23 +++ app/config/logger.go | 8 +- .../github_integration_controller.go | 82 +++++++++ ...8105113_create_integrations_table.down.sql | 1 + ...718105113_create_integrations_table.up.sql | 15 ++ app/models/dtos/integrations/github.go | 8 + app/models/integration.go | 21 +++ app/repositories/integrations.go | 71 ++++++++ .../github_integration_service.go | 161 ++++++++++++++++++ .../integrations/integration_service.go | 43 +++++ app/services/integrations/integrations.go | 5 + app/utils/json_utils.go | 22 +++ docker-compose.yaml | 3 + server.go | 40 +++++ 15 files changed, 500 insertions(+), 5 deletions(-) create mode 100644 app/config/github_integration_config.go create mode 100644 app/controllers/github_integration_controller.go create mode 100644 app/db/migrations/20240718105113_create_integrations_table.down.sql create mode 100644 app/db/migrations/20240718105113_create_integrations_table.up.sql create mode 100644 app/models/dtos/integrations/github.go create mode 100644 app/models/integration.go create mode 100644 app/repositories/integrations.go create mode 100644 app/services/integrations/github_integration_service.go create mode 100644 app/services/integrations/integration_service.go create mode 100644 app/services/integrations/integrations.go create mode 100644 app/utils/json_utils.go 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..aab16791 --- /dev/null +++ b/app/controllers/github_integration_controller.go @@ -0,0 +1,82 @@ +package controllers + +import ( + "ai-developer/app/services/integrations" + "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) 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(), + "name": repo.GetName(), + "url": repo.GetURL(), + "full_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), + ) + + err := gic.githubIntegrationService.GenerateAndSaveAccessToken(code, state) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } else { + c.JSON(http.StatusOK, gin.H{"message": "Integration successful"}) + return + } +} + +func NewGithubIntegrationController( + githubIntegrationService *integrations.GithubIntegrationService, + logger *zap.Logger, +) *GithubIntegrationController { + return &GithubIntegrationController{ + githubIntegrationService: githubIntegrationService, + logger: logger, + } +} 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..f6090857 --- /dev/null +++ b/app/db/migrations/20240718105113_create_integrations_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS 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..6b07fe2a --- /dev/null +++ b/app/db/migrations/20240718105113_create_integrations_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS 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/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/repositories/integrations.go b/app/repositories/integrations.go new file mode 100644 index 00000000..d0d0dd1d --- /dev/null +++ b/app/repositories/integrations.go @@ -0,0 +1,71 @@ +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) 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..91cfd41e --- /dev/null +++ b/app/services/integrations/github_integration_service.go @@ -0,0 +1,161 @@ +package integrations + +import ( + "ai-developer/app/config" + "ai-developer/app/models/dtos/integrations" + "ai-developer/app/services" + "ai-developer/app/utils" + "context" + "fmt" + "github.com/google/go-github/github" + "go.uber.org/zap" + "golang.org/x/oauth2" + githubOAuth "golang.org/x/oauth2/github" + "strconv" +) + +type GithubIntegrationService struct { + logger *zap.Logger + + oauthConfig oauth2.Config + githubIntegrationConfig *config.GithubIntegrationConfig + + userService *services.UserService + integrationService *IntegrationService +} + +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 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 err != nil || integration == 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..52ca536c --- /dev/null +++ b/app/services/integrations/integration_service.go @@ -0,0 +1,43 @@ +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) 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/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/server.go b/server.go index 2ce00a9e..71c3a7aa 100644 --- a/server.go +++ b/server.go @@ -14,6 +14,7 @@ import ( "ai-developer/app/repositories" "ai-developer/app/services" "ai-developer/app/services/git_providers" + "ai-developer/app/services/integrations" "ai-developer/app/services/s3_providers" "context" "errors" @@ -376,6 +377,36 @@ func main() { } fmt.Println("WorkspaceGateway provided") + // Integration + { + if err = c.Provide(repositories.NewIntegrationsRepository); err != nil { + config.Logger.Error("Error providing IntegrationsRepository", zap.Error(err)) + panic(err) + } + if err = c.Provide(integrations.NewIntegrationService); err != nil { + config.Logger.Error("Error providing IntegrationService", zap.Error(err)) + panic(err) + } + } + + // Github Integration + { + if err = c.Provide(config.NewGithubIntegrationConfig); err != nil { + config.Logger.Error("Error providing GithubIntegrationConfig", zap.Error(err)) + panic(err) + } + + if err = c.Provide(integrations.NewGithubIntegrationService); err != nil { + config.Logger.Error("Error providing GithubIntegrationService", zap.Error(err)) + panic(err) + } + + if err = c.Provide(controllers.NewGithubIntegrationController); err != nil { + config.Logger.Error("Error providing GithubIntegrationController", zap.Error(err)) + panic(err) + } + } + // Setup routes and start the server err = c.Invoke(func( health *controllers.HealthController, @@ -400,6 +431,7 @@ func main() { ioServer *socketio.Server, nrApp *newrelic.Application, designStoryCtrl *controllers.DesignStoryReviewController, + githubIntegrationController *controllers.GithubIntegrationController, logger *zap.Logger, ) error { @@ -513,6 +545,14 @@ func main() { authentication.POST("/sign_in", auth.SignIn) authentication.POST("/sign_up", auth.SignUp) + integrations := api.Group("/integrations", middleware.AuthenticateJWT()) + + githubIntegration := integrations.Group("/github") + githubIntegration.GET("", githubIntegrationController.CheckIfIntegrationExists) + githubIntegration.GET("/repos", githubIntegrationController.GetRepositories) + githubIntegration.GET("/authorize", githubIntegrationController.Authorize) + githubIntegration.GET("/callback", githubIntegrationController.HandleCallback) + // Wrap the socket.io server as Gin handlers for specific routes r.GET("/api/socket.io/*any", middleware.AuthenticateJWT(), gin.WrapH(ioServer)) r.POST("/api/socket.io/*any", middleware.AuthenticateJWT(), gin.WrapH(ioServer)) From e9dbd4cd7a671d1346e86b763a40260f00c76167 Mon Sep 17 00:00:00 2001 From: Sasikanth L Date: Fri, 19 Jul 2024 09:56:18 +0530 Subject: [PATCH 2/7] Create workspace from git --- app/client/workspace/workspace_service.go | 49 +++++++ .../github_integration_controller.go | 7 +- app/controllers/project_controller.go | 38 ++++++ .../github_integration_service.go | 2 - app/services/project_service.go | 125 +++++++++++++++--- app/types/request/create_project_request.go | 8 ++ app/types/request/import_github_repository.go | 13 ++ server.go | 2 + .../app/controllers/workspace_controller.go | 33 +++++ .../models/dto/import_github_repository.go | 13 ++ .../services/impl/docker_workspace_service.go | 93 ++++++++++++- .../services/impl/k8s_workspace_service.go | 95 ++++++++++++- .../app/services/workspace_service.go | 9 ++ workspace-service/server.go | 18 ++- 14 files changed, 466 insertions(+), 39 deletions(-) create mode 100644 app/types/request/import_github_repository.go create mode 100644 workspace-service/app/models/dto/import_github_repository.go 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/controllers/github_integration_controller.go b/app/controllers/github_integration_controller.go index aab16791..ec1125de 100644 --- a/app/controllers/github_integration_controller.go +++ b/app/controllers/github_integration_controller.go @@ -42,10 +42,9 @@ func (gic *GithubIntegrationController) GetRepositories(c *gin.Context) { response := make([]map[string]interface{}, 0) for _, repo := range repositories { response = append(response, map[string]interface{}{ - "id": repo.GetID(), - "name": repo.GetName(), - "url": repo.GetURL(), - "full_name": repo.GetFullName(), + "id": repo.GetID(), + "url": repo.GetCloneURL(), + "name": repo.GetFullName(), }) } c.JSON(http.StatusOK, gin.H{"repositories": response}) diff --git a/app/controllers/project_controller.go b/app/controllers/project_controller.go index 0405523a..ff06add8 100644 --- a/app/controllers/project_controller.go +++ b/app/controllers/project_controller.go @@ -56,6 +56,44 @@ func (controller *ProjectController) GetProjectById(context *gin.Context) { context.JSON(http.StatusOK, project) } +func (controller *ProjectController) CreateProjectFromGit(context *gin.Context) { + var createProjectRequest request.CreateProjectFromGitRequest + if err := context.ShouldBindJSON(&createProjectRequest); err != nil { + context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + email, _ := context.Get("email") + user, err := controller.userService.GetUserByEmail(email.(string)) + if err != nil { + context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if user == nil { + context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "User not found"}) + return + } + + project, err := controller.projectService.CreateProjectFromGit(user.ID, user.OrganisationID, createProjectRequest) + if err != nil { + context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + context.JSON( + http.StatusOK, + gin.H{ + "project_id": project.ID, + "project_url": project.Url, + "project_name": project.Name, + "project_frontend_url": project.FrontendURL, + "project_backend_url": project.BackendURL, + "project_framework": project.BackendFramework, + "project_frontend_framework": project.FrontendFramework, + }, + ) + return +} + func (controller *ProjectController) CreateProject(context *gin.Context) { var createProjectRequest request.CreateProjectRequest if err := context.ShouldBindJSON(&createProjectRequest); err != nil { diff --git a/app/services/integrations/github_integration_service.go b/app/services/integrations/github_integration_service.go index 91cfd41e..fc8645e6 100644 --- a/app/services/integrations/github_integration_service.go +++ b/app/services/integrations/github_integration_service.go @@ -3,7 +3,6 @@ package integrations import ( "ai-developer/app/config" "ai-developer/app/models/dtos/integrations" - "ai-developer/app/services" "ai-developer/app/utils" "context" "fmt" @@ -20,7 +19,6 @@ type GithubIntegrationService struct { oauthConfig oauth2.Config githubIntegrationConfig *config.GithubIntegrationConfig - userService *services.UserService integrationService *IntegrationService } diff --git a/app/services/project_service.go b/app/services/project_service.go index d8d77f13..cd6cfed0 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) { @@ -82,6 +84,87 @@ func (s *ProjectService) GetProjectDetailsById(projectId int) (*models.Project, return project, nil } +func (s *ProjectService) CreateProjectFromGit(userId uint, orgId uint, requestData request.CreateProjectFromGitRequest) (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, + } + + 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.Repository, + 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 +397,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..41c6bc55 100644 --- a/app/types/request/create_project_request.go +++ b/app/types/request/create_project_request.go @@ -6,3 +6,11 @@ type CreateProjectRequest struct { FrontendFramework string `json:"frontend_framework"` Description string `json:"description"` } + +type CreateProjectFromGitRequest struct { + Name string `json:"name"` + Framework string `json:"framework"` + FrontendFramework string `json:"frontend_framework"` + Description string `json:"description"` + Repository string `json:"repository"` +} 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/server.go b/server.go index 71c3a7aa..afeec366 100644 --- a/server.go +++ b/server.go @@ -479,6 +479,8 @@ func main() { projects.POST("", projectsController.CreateProject) projects.PUT("", projectsController.UpdateProject) + projects.POST("/import", projectsController.CreateProjectFromGit) + projects.GET("/", projectsController.GetAllProjects) projects.POST("/", projectsController.CreateProject) projects.PUT("/", projectsController.UpdateProject) diff --git a/workspace-service/app/controllers/workspace_controller.go b/workspace-service/app/controllers/workspace_controller.go index ef816660..12238dca 100644 --- a/workspace-service/app/controllers/workspace_controller.go +++ b/workspace-service/app/controllers/workspace_controller.go @@ -12,6 +12,39 @@ type WorkspaceController struct { logger *zap.Logger } +func (wc *WorkspaceController) ImportExistingWorkspace(c *gin.Context) { + body := dto.ImportGitRepository{} + if err := c.BindJSON(&body); err != nil { + wc.logger.Error("Failed to bind json", zap.Error(err)) + c.AbortWithStatusJSON(400, gin.H{ + "error": "Bad Request", + }) + return + } + + wsDetails, err := wc.wsService.ImportGitRepository( + body.WorkspaceId, + body.Repository, + body.Username, + body.Password, + body.RemoteURL, + body.GitnessUser, + body.GitnessToken, + ) + + if err != nil { + c.AbortWithStatusJSON( + 500, + gin.H{"error": "Internal Server Error"}, + ) + return + } + c.JSON( + 200, + gin.H{"message": "success", "workspace": wsDetails}, + ) +} + func (wc *WorkspaceController) CreateWorkspace(c *gin.Context) { body := dto.CreateWorkspace{} if err := c.BindJSON(&body); err != nil { diff --git a/workspace-service/app/models/dto/import_github_repository.go b/workspace-service/app/models/dto/import_github_repository.go new file mode 100644 index 00000000..c65b060a --- /dev/null +++ b/workspace-service/app/models/dto/import_github_repository.go @@ -0,0 +1,13 @@ +package dto + +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/workspace-service/app/services/impl/docker_workspace_service.go b/workspace-service/app/services/impl/docker_workspace_service.go index 49703b7c..b2ee2be3 100644 --- a/workspace-service/app/services/impl/docker_workspace_service.go +++ b/workspace-service/app/services/impl/docker_workspace_service.go @@ -20,9 +20,94 @@ import ( type DockerWorkspaceService struct { services.WorkspaceService - workspaceServiceConfig *workspaceconfig.WorkspaceServiceConfig - frontendWorkspaceConfig *workspaceconfig.FrontendWorkspaceConfig - logger *zap.Logger + workspaceServiceConfig *workspaceconfig.WorkspaceServiceConfig + frontendWorkspaceConfig *workspaceconfig.FrontendWorkspaceConfig + logger *zap.Logger +} + +func (ws DockerWorkspaceService) ImportGitRepository( + workspaceId string, + repository string, + username string, + password string, + remoteURL string, + gitnessUser string, + gitnessToken string, +) (details *dto.WorkspaceDetails, err error) { + exists, err := utils.CheckIfWorkspaceExists(workspaceId) + if err != nil { + ws.logger.Error("Failed to check if workspace exists", zap.Error(err)) + return + } + + if exists { + ws.logger.Info("Workspace already exists", zap.String("workspaceId", workspaceId)) + return + } + + workspaceDir := "/workspaces/" + workspaceId + + repo, err := git.PlainClone(workspaceDir, false, &git.CloneOptions{ + URL: repository, + Auth: &http.BasicAuth{ + Username: username, + Password: password, + }, + }) + + if err != nil { + ws.logger.Error("Failed to clone Git repository", zap.Error(err)) + return + } + + ws.logger.Info("Pushing changes to remote repository", zap.String("remoteURL", remoteURL)) + + _, err = repo.CreateRemote(&config.RemoteConfig{ + Name: "gitness", + URLs: []string{remoteURL}, + }) + if err != nil { + ws.logger.Error("Failed to create remote", zap.Error(err)) + return + } + + auth := &http.BasicAuth{ + Username: gitnessUser, + Password: gitnessToken, + } + + err = repo.Push(&git.PushOptions{ + RemoteName: "gitness", + Auth: auth, + RefSpecs: []config.RefSpec{"refs/heads/main:refs/heads/main"}, + }) + + err = repo.DeleteRemote("gitness") + if err != nil { + ws.logger.Error("Failed to delete remote", zap.Error(err)) + return + } + + err = repo.DeleteRemote("origin") + if err != nil { + ws.logger.Error("Failed to delete remote", zap.Error(err)) + return + } + + workspaceUrl := "http://localhost:8081/?folder=/workspaces/" + workspaceId + frontendUrl := "http://localhost:3000" + backendUrl := "http://localhost:5000" + + details = &dto.WorkspaceDetails{ + WorkspaceId: workspaceId, + BackendTemplate: nil, + FrontendTemplate: nil, + WorkspaceUrl: &workspaceUrl, + FrontendUrl: &frontendUrl, + BackendUrl: &backendUrl, + } + + return } func (ws DockerWorkspaceService) CreateWorkspace(workspaceId string, backendTemplate string, frontendTemplate *string, remoteURL string, gitnessUser string, gitnessToken string) (*dto.WorkspaceDetails, error) { @@ -225,7 +310,7 @@ func (ws DockerWorkspaceService) checkAndCreateFrontendWorkspaceFromTemplate(sto ws.logger.Info("Workspace already exists", zap.String("workspaceId", workspaceId), zap.String("storyHashId", storyHashId)) return nil } - if !exists{ + if !exists { ws.logger.Info("Creating workspace from template", zap.String("workspaceId", workspaceId), zap.String("frontendTemplate", frontendTemplate), zap.String("storyHashId", storyHashId)) err = os.MkdirAll(frontendPath, os.ModePerm) if err != nil { diff --git a/workspace-service/app/services/impl/k8s_workspace_service.go b/workspace-service/app/services/impl/k8s_workspace_service.go index 12c0e38d..f045dbcb 100644 --- a/workspace-service/app/services/impl/k8s_workspace_service.go +++ b/workspace-service/app/services/impl/k8s_workspace_service.go @@ -28,11 +28,98 @@ import ( type K8sWorkspaceService struct { services.WorkspaceService - clientset *kubernetes.Clientset - workspaceServiceConfig *workspaceconfig.WorkspaceServiceConfig + clientset *kubernetes.Clientset + workspaceServiceConfig *workspaceconfig.WorkspaceServiceConfig frontendWorkspaceConfig *workspaceconfig.FrontendWorkspaceConfig - k8sControllerClient client.Client - logger *zap.Logger + k8sControllerClient client.Client + logger *zap.Logger +} + +func (ws K8sWorkspaceService) ImportGitRepository( + workspaceId string, + repository string, + username string, + password string, + remoteURL string, + gitnessUser string, + gitnessToken string, +) (details *dto.WorkspaceDetails, err error) { + exists, err := utils.CheckIfWorkspaceExists(workspaceId) + if err != nil { + ws.logger.Error("Failed to check if workspace exists", zap.Error(err)) + return + } + + if exists { + ws.logger.Info("Workspace already exists", zap.String("workspaceId", workspaceId)) + return + } + + workspaceDir := "/workspaces/" + workspaceId + + repo, err := git.PlainClone(workspaceDir, false, &git.CloneOptions{ + URL: repository, + Auth: &http.BasicAuth{ + Username: username, + Password: password, + }, + }) + + if err != nil { + ws.logger.Error("Failed to clone repository", zap.Error(err)) + return + } + + // Push to given URL of remote, in main branch + ws.logger.Info("Pushing changes to remote repository", zap.String("remoteURL", remoteURL)) + + // Add the remote + _, err = repo.CreateRemote(&config.RemoteConfig{ + Name: "gitness", + URLs: []string{remoteURL}, + }) + if err != nil { + ws.logger.Error("Failed to create remote", zap.Error(err)) + return + } + + auth := &http.BasicAuth{ + Username: gitnessUser, + Password: gitnessToken, + } + + err = repo.Push(&git.PushOptions{ + RemoteName: "gitness", + Auth: auth, + RefSpecs: []config.RefSpec{"refs/heads/main:refs/heads/main"}, + }) + + err = repo.DeleteRemote("gitness") + if err != nil { + ws.logger.Error("Failed to delete remote", zap.Error(err)) + return + } + + err = repo.DeleteRemote("origin") + if err != nil { + ws.logger.Error("Failed to delete remote", zap.Error(err)) + return + } + + workspaceUrl := fmt.Sprintf("https://%s.%s/?folder=/workspaces/%s", workspaceId, ws.workspaceServiceConfig.WorkspaceHostName(), workspaceId) + frontendUrl := fmt.Sprintf("https://fe-%s.%s", workspaceId, ws.workspaceServiceConfig.WorkspaceHostName()) + backendUrl := fmt.Sprintf("https://be-%s.%s", workspaceId, ws.workspaceServiceConfig.WorkspaceHostName()) + + details = &dto.WorkspaceDetails{ + WorkspaceId: workspaceId, + BackendTemplate: nil, + FrontendTemplate: nil, + WorkspaceUrl: &workspaceUrl, + FrontendUrl: &frontendUrl, + BackendUrl: &backendUrl, + } + + return } func (ws K8sWorkspaceService) CreateWorkspace(workspaceId string, backendTemplate string, frontendTemplate *string, remoteURL string, gitnessUser string, gitnessToken string) (*dto.WorkspaceDetails, error) { diff --git a/workspace-service/app/services/workspace_service.go b/workspace-service/app/services/workspace_service.go index f09df04a..6c506ae7 100644 --- a/workspace-service/app/services/workspace_service.go +++ b/workspace-service/app/services/workspace_service.go @@ -3,6 +3,15 @@ package services import "workspace-service/app/models/dto" type WorkspaceService interface { + ImportGitRepository( + workspaceId string, + repository string, + username string, + password string, + remoteURL string, + gitnessUser string, + gitnessToken string, + ) (*dto.WorkspaceDetails, error) CreateWorkspace(workspaceId string, backendTemplate string, frontendTemplate *string, remoteURL string, gitnessUser string, gitnessToken string) (*dto.WorkspaceDetails, error) CreateFrontendWorkspace(storyHashId, workspaceId string, frontendTemplate string) (*dto.WorkspaceDetails, error) DeleteWorkspace(workspaceId string) error diff --git a/workspace-service/server.go b/workspace-service/server.go index 88017158..66f26c40 100644 --- a/workspace-service/server.go +++ b/workspace-service/server.go @@ -173,12 +173,20 @@ func main() { c.Set("newRelicTransaction", txn) c.Next() }) - r.Handle("GET", "/api/health", healthController.Health) - r.Handle("POST", "/api/v1/workspaces", wsController.CreateWorkspace) - r.Handle("POST", "/api/v1/frontend/workspaces", wsController.CreateFrontendWorkspace) - r.Handle("DELETE", "/api/v1/workspaces/:workspaceId", wsController.DeleteWorkspace) - r.Handle("POST", "/api/v1/jobs", jobsController.CreateWorkspace) + api := r.Group("/api") + api.Handle("GET", "/health", healthController.Health) + + apiV1 := api.Group("/v1") + + apiV1.Handle("POST", "/workspaces", wsController.CreateWorkspace) + apiV1.Handle("DELETE", "/workspaces/:workspaceId", wsController.DeleteWorkspace) + + apiV1.Handle("POST", "/workspaces/import", wsController.ImportExistingWorkspace) + + apiV1.Handle("POST", "/frontend/workspaces", wsController.CreateFrontendWorkspace) + + apiV1.Handle("POST", "/jobs", jobsController.CreateWorkspace) return r.Run() }) From f5c3b40d0fc4a16dd8ca07c493b42e356b736642 Mon Sep 17 00:00:00 2001 From: Sasikanth L Date: Tue, 23 Jul 2024 11:27:50 +0530 Subject: [PATCH 3/7] Add delete github integration api --- .../github_integration_controller.go | 23 ++++++---- app/controllers/project_controller.go | 46 ++++--------------- app/repositories/integrations.go | 16 +++++++ .../github_integration_service.go | 16 ++++++- .../integrations/integration_service.go | 4 ++ app/services/project_service.go | 4 +- app/types/request/create_project_request.go | 17 ++----- server.go | 5 +- worker.go | 32 +++++++++++++ 9 files changed, 100 insertions(+), 63 deletions(-) diff --git a/app/controllers/github_integration_controller.go b/app/controllers/github_integration_controller.go index ec1125de..d1448229 100644 --- a/app/controllers/github_integration_controller.go +++ b/app/controllers/github_integration_controller.go @@ -1,7 +1,9 @@ package controllers import ( + "ai-developer/app/config" "ai-developer/app/services/integrations" + "fmt" "github.com/gin-gonic/gin" "go.uber.org/zap" "net/http" @@ -22,6 +24,16 @@ func (gic *GithubIntegrationController) Authorize(c *gin.Context) { 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))) @@ -60,14 +72,9 @@ func (gic *GithubIntegrationController) HandleCallback(c *gin.Context) { zap.String("state", state), ) - err := gic.githubIntegrationService.GenerateAndSaveAccessToken(code, state) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } else { - c.JSON(http.StatusOK, gin.H{"message": "Integration successful"}) - return - } + _ = gic.githubIntegrationService.GenerateAndSaveAccessToken(code, state) + redirectUrl := fmt.Sprintf("%s/settings?page=integrations", config.GithubFrontendURL()) + c.Redirect(http.StatusTemporaryRedirect, redirectUrl) } func NewGithubIntegrationController( diff --git a/app/controllers/project_controller.go b/app/controllers/project_controller.go index ff06add8..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" @@ -56,8 +57,8 @@ func (controller *ProjectController) GetProjectById(context *gin.Context) { context.JSON(http.StatusOK, project) } -func (controller *ProjectController) CreateProjectFromGit(context *gin.Context) { - var createProjectRequest request.CreateProjectFromGitRequest +func (controller *ProjectController) CreateProject(context *gin.Context) { + var createProjectRequest request.CreateProjectRequest if err := context.ShouldBindJSON(&createProjectRequest); err != nil { context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -73,44 +74,13 @@ func (controller *ProjectController) CreateProjectFromGit(context *gin.Context) return } - project, err := controller.projectService.CreateProjectFromGit(user.ID, user.OrganisationID, createProjectRequest) - if err != nil { - context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return + 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) } - context.JSON( - http.StatusOK, - gin.H{ - "project_id": project.ID, - "project_url": project.Url, - "project_name": project.Name, - "project_frontend_url": project.FrontendURL, - "project_backend_url": project.BackendURL, - "project_framework": project.BackendFramework, - "project_frontend_framework": project.FrontendFramework, - }, - ) - return -} - -func (controller *ProjectController) CreateProject(context *gin.Context) { - var createProjectRequest request.CreateProjectRequest - if err := context.ShouldBindJSON(&createProjectRequest); err != nil { - context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - email, _ := context.Get("email") - user, err := controller.userService.GetUserByEmail(email.(string)) - if err != nil { - context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - if user == nil { - context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "User not found"}) - return - } - project, err := controller.projectService.CreateProject(int(user.OrganisationID), createProjectRequest) if err != nil { context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/app/repositories/integrations.go b/app/repositories/integrations.go index d0d0dd1d..6e82d5de 100644 --- a/app/repositories/integrations.go +++ b/app/repositories/integrations.go @@ -24,6 +24,22 @@ func (ir *IntegrationsRepository) FindIntegrationIdByUserIdAndType(userId uint64 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, diff --git a/app/services/integrations/github_integration_service.go b/app/services/integrations/github_integration_service.go index fc8645e6..799f1bea 100644 --- a/app/services/integrations/github_integration_service.go +++ b/app/services/integrations/github_integration_service.go @@ -5,11 +5,13 @@ import ( "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" ) @@ -22,12 +24,20 @@ type GithubIntegrationService struct { 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 } @@ -37,7 +47,11 @@ func (gis *GithubIntegrationService) HasGithubIntegration(userId uint64) (hasInt func (gis *GithubIntegrationService) GetRepositories(userId uint64) (repos []*github.Repository, err error) { integration, err := gis.integrationService.FindIntegrationIdByUserIdAndType(userId, GithubIntegrationType) - if err != nil || integration == nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return make([]*github.Repository, 0), nil + } + + if err != nil { return } diff --git a/app/services/integrations/integration_service.go b/app/services/integrations/integration_service.go index 52ca536c..2ead1c52 100644 --- a/app/services/integrations/integration_service.go +++ b/app/services/integrations/integration_service.go @@ -16,6 +16,10 @@ func (is *IntegrationService) FindIntegrationIdByUserIdAndType(userId uint64, in 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, diff --git a/app/services/project_service.go b/app/services/project_service.go index cd6cfed0..9ac0546d 100644 --- a/app/services/project_service.go +++ b/app/services/project_service.go @@ -84,7 +84,7 @@ func (s *ProjectService) GetProjectDetailsById(projectId int) (*models.Project, return project, nil } -func (s *ProjectService) CreateProjectFromGit(userId uint, orgId uint, requestData request.CreateProjectFromGitRequest) (project *models.Project, err error) { +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)) @@ -132,7 +132,7 @@ func (s *ProjectService) CreateProjectFromGit(userId uint, orgId uint, requestDa _, err = s.workspaceServiceClient.ImportGitRepository( &request.ImportGitRepository{ WorkspaceId: hashID, - Repository: requestData.Repository, + Repository: *requestData.Repository, Username: integrationDetails.GithubUserId, Password: integrationDetails.AccessToken, RemoteURL: remoteGitURL, diff --git a/app/types/request/create_project_request.go b/app/types/request/create_project_request.go index 41c6bc55..be6e3b11 100644 --- a/app/types/request/create_project_request.go +++ b/app/types/request/create_project_request.go @@ -1,16 +1,9 @@ package request type CreateProjectRequest struct { - Name string `json:"name"` - Framework string `json:"framework"` - FrontendFramework string `json:"frontend_framework"` - Description string `json:"description"` -} - -type CreateProjectFromGitRequest struct { - Name string `json:"name"` - Framework string `json:"framework"` - FrontendFramework string `json:"frontend_framework"` - Description string `json:"description"` - Repository string `json:"repository"` + Name string `json:"name"` + Framework string `json:"framework"` + FrontendFramework string `json:"frontend_framework"` + Description string `json:"description"` + Repository *string `json:"repository"` } diff --git a/server.go b/server.go index afeec366..4905af5c 100644 --- a/server.go +++ b/server.go @@ -479,8 +479,6 @@ func main() { projects.POST("", projectsController.CreateProject) projects.PUT("", projectsController.UpdateProject) - projects.POST("/import", projectsController.CreateProjectFromGit) - projects.GET("/", projectsController.GetAllProjects) projects.POST("/", projectsController.CreateProject) projects.PUT("/", projectsController.UpdateProject) @@ -550,7 +548,10 @@ func main() { integrations := api.Group("/integrations", middleware.AuthenticateJWT()) githubIntegration := integrations.Group("/github") + githubIntegration.GET("", githubIntegrationController.CheckIfIntegrationExists) + githubIntegration.DELETE("", githubIntegrationController.DeleteIntegration) + githubIntegration.GET("/repos", githubIntegrationController.GetRepositories) githubIntegration.GET("/authorize", githubIntegrationController.Authorize) githubIntegration.GET("/callback", githubIntegrationController.HandleCallback) diff --git a/worker.go b/worker.go index 40df7ed9..6e758ad1 100644 --- a/worker.go +++ b/worker.go @@ -6,10 +6,12 @@ import ( "ai-developer/app/client/workspace" "ai-developer/app/config" "ai-developer/app/constants" + "ai-developer/app/controllers" "ai-developer/app/monitoring" "ai-developer/app/repositories" "ai-developer/app/services" "ai-developer/app/services/git_providers" + "ai-developer/app/services/integrations" "ai-developer/app/services/s3_providers" "ai-developer/app/tasks" "context" @@ -266,6 +268,36 @@ func main() { }, nil) }) + // Integration + { + if err = c.Provide(repositories.NewIntegrationsRepository); err != nil { + config.Logger.Error("Error providing IntegrationsRepository", zap.Error(err)) + panic(err) + } + if err = c.Provide(integrations.NewIntegrationService); err != nil { + config.Logger.Error("Error providing IntegrationService", zap.Error(err)) + panic(err) + } + } + + // Github Integration + { + if err = c.Provide(config.NewGithubIntegrationConfig); err != nil { + config.Logger.Error("Error providing GithubIntegrationConfig", zap.Error(err)) + panic(err) + } + + if err = c.Provide(integrations.NewGithubIntegrationService); err != nil { + config.Logger.Error("Error providing GithubIntegrationService", zap.Error(err)) + panic(err) + } + + if err = c.Provide(controllers.NewGithubIntegrationController); err != nil { + config.Logger.Error("Error providing GithubIntegrationController", zap.Error(err)) + panic(err) + } + } + err = c.Provide(func( deleteWorkspaceTaskHandler *tasks.DeleteWorkspaceTaskHandler, createExecutionJobTaskHandler *tasks.CreateExecutionJobTaskHandler, From ec3c2d272b74107db8db2179b4eafe88b09321a0 Mon Sep 17 00:00:00 2001 From: Sasikanth L Date: Tue, 23 Jul 2024 11:29:00 +0530 Subject: [PATCH 4/7] Add github integration ui changes --- gui/package.json | 3 + gui/src/api/DashboardService.tsx | 35 ++-- .../settings/SettingsOptions/Integrations.tsx | 74 +++++++ gui/src/app/settings/page.tsx | 12 +- gui/src/app/settings/settings.module.css | 3 + .../CustomSidebar/CustomSidebar.tsx | 21 +- .../CreateOrEditProjectBody.tsx | 173 +++++++++++++--- .../HomeComponents/create-project.module.css | 5 + gui/types/projectsTypes.ts | 1 + gui/yarn.lock | 191 +++++++++++++++++- 10 files changed, 462 insertions(+), 56 deletions(-) create mode 100644 gui/src/app/settings/SettingsOptions/Integrations.tsx create mode 100644 gui/src/app/settings/settings.module.css create mode 100644 gui/src/components/HomeComponents/create-project.module.css diff --git a/gui/package.json b/gui/package.json index 2aa1006d..4f299c45 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", @@ -29,6 +30,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 +38,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/src/api/DashboardService.tsx b/gui/src/api/DashboardService.tsx index 73e62071..970f3094 100644 --- a/gui/src/api/DashboardService.tsx +++ b/gui/src/api/DashboardService.tsx @@ -1,20 +1,14 @@ import api from './apiConfig'; +import {CreateProjectPayload, UpdateProjectPayload,} from '../../types/projectsTypes'; +import {FormStoryPayload} from '../../types/storyTypes'; import { - CreateProjectPayload, - UpdateProjectPayload, -} from '../../types/projectsTypes'; -import { FormStoryPayload } from '../../types/storyTypes'; -import { - CommentReBuildPayload, CommentReBuildDesignStoryPayload, + CommentReBuildPayload, CreatePullRequestPayload, } from '../../types/pullRequestsTypes'; -import { CreateOrUpdateLLMAPIKeyPayload } from '../../types/modelsTypes'; -import { - CreateDesignStoryPayload, - EditDesignStoryPayload, -} from '../../types/designStoryTypes'; -import { authPayload } from '../../types/authTypes'; +import {CreateOrUpdateLLMAPIKeyPayload} from '../../types/modelsTypes'; +import {CreateDesignStoryPayload, EditDesignStoryPayload,} from '../../types/designStoryTypes'; +import {authPayload} from '../../types/authTypes'; export const checkHealth = () => { return api.get(`/health`); @@ -180,3 +174,20 @@ 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; +} \ No newline at end of file diff --git a/gui/src/app/settings/SettingsOptions/Integrations.tsx b/gui/src/app/settings/SettingsOptions/Integrations.tsx new file mode 100644 index 00000000..6b0edf4f --- /dev/null +++ b/gui/src/app/settings/SettingsOptions/Integrations.tsx @@ -0,0 +1,74 @@ +'use client'; + +import React, {useCallback, useEffect, useState} from 'react'; + +import styles from '../settings.module.css'; +import {deleteGithubIntegration, isGithubConnected} from "@/api/DashboardService"; +import {API_BASE_URL} from "@/api/apiConfig"; + +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(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; \ No newline at end of file diff --git a/gui/src/app/settings/page.tsx b/gui/src/app/settings/page.tsx index f9d1c979..ba621619 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 = [ @@ -13,7 +14,14 @@ export default function Settings() { selected: imagePath.modelsIconSelected, unselected: imagePath.modelsIconUnselected, icon_css: 'size-4', - component: , + component: , + }, { + key: 'integrations', + text: 'Integrations', + selected: imagePath.modelsIconSelected, + unselected: imagePath.modelsIconUnselected, + icon_css: 'size-4', + component: , }, ]; @@ -23,7 +31,7 @@ export default function Settings() { return (
- + = ({ - id, - title, - options, - onOptionSelect, -}) => { - const [selectedKey, setSelectedKey] = useState(options[0].key); + id, + title, + options, + onOptionSelect, + }) => { + 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..180e0065 100644 --- a/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx +++ b/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx @@ -1,25 +1,18 @@ import CustomModal from '@/components/CustomModal/CustomModal'; import CustomImageSelector from '@/components/ImageComponents/CustomImageSelector'; -import { - backendFrameworkOptions, - frontendFrameworkOptions, -} from '@/app/constants/ProjectConstants'; -import { Button } from '@nextui-org/react'; -import { useEffect, useRef, useState } from 'react'; -import { - CreateProjectPayload, - ProjectTypes, - UpdateProjectPayload, -} from '../../../types/projectsTypes'; -import { - createProject, - getProjectById, - updateProject, -} from '@/api/DashboardService'; -import { useRouter } from 'next/navigation'; -import { setProjectDetails } from '@/app/utils'; +import {backendFrameworkOptions, frontendFrameworkOptions,} from '@/app/constants/ProjectConstants'; +import {Button} from '@nextui-org/react'; +import {useEffect, useRef, useState} from 'react'; +import {CreateProjectPayload, ProjectTypes, UpdateProjectPayload,} 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,13 +22,44 @@ interface CreateOrEditProjectBodyProps { edit?: boolean; } +const customStyles = { + control: (provided) => ({ + ...provided, + backgroundColor: '#1e1e1e', + color: '#ffffff', + borderColor: '#333333', + '&:hover': { + color: '#ffffff', + borderColor: '#4a4a4a' + } + }), + 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, - setOpenProjectModal, - projectsList, - edit = false, -}: CreateOrEditProjectBodyProps) { + id, + openProjectModal, + setOpenProjectModal, + projectsList, + edit = false, + }: CreateOrEditProjectBodyProps) { const [selectedBackendFramework, setSelectedBackendFramework] = useState(backendFrameworkOptions[0].id); const [selectedFrontendFramework, setSelectedFrontendFramework] = @@ -48,6 +72,29 @@ 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(null); + + async function redirectToGithubIntegration() { + setIntegrationLoading(true); + try { + const interval = setInterval(async () => { + const gitIntegrated = await isGithubConnected(); + if (gitIntegrated) { + setIsExternalGitIntegration(true); + setIntegrationLoading(false); + clearInterval(interval); + } + }, 1000, 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 @@ -101,11 +148,33 @@ export default function CreateOrEditProjectBody({ framework: selectedBackendFramework, frontend_framework: selectedFrontendFramework, description: projectDescription, + repository: useExternalGit ? selectedRepository : 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'); @@ -175,7 +244,7 @@ export default function CreateOrEditProjectBody({ width={'30vw'} onClose={() => setOpenProjectModal(false)} > - +
@@ -261,6 +330,60 @@ export default function CreateOrEditProjectBody({ /> )}
+ +
+ +
+ + +
+

*Repository name will be named after the project name itself.

+
+ + {(useExternalGit && isExternalGitIntegration) && ( + <> + setSelectedRepository(e.value)} + onChange={(e) => setSelectedRepository(e)} className="text-white" styles={customStyles} options={repositories} /> diff --git a/gui/types/projectsTypes.ts b/gui/types/projectsTypes.ts index 37bb60aa..96e4c1ad 100644 --- a/gui/types/projectsTypes.ts +++ b/gui/types/projectsTypes.ts @@ -18,6 +18,7 @@ export interface ProjectTypes { project_description: string; project_hash_id: string; project_url: string; + project_repository: string | undefined; project_backend_url: string; project_frontend_url: string; pull_request_count: number; diff --git a/server.go b/server.go index 4905af5c..43175103 100644 --- a/server.go +++ b/server.go @@ -545,9 +545,8 @@ func main() { authentication.POST("/sign_in", auth.SignIn) authentication.POST("/sign_up", auth.SignUp) - integrations := api.Group("/integrations", middleware.AuthenticateJWT()) - - githubIntegration := integrations.Group("/github") + integrationsGroup := api.Group("/integrations", middleware.AuthenticateJWT()) + githubIntegration := integrationsGroup.Group("/github") githubIntegration.GET("", githubIntegrationController.CheckIfIntegrationExists) githubIntegration.DELETE("", githubIntegrationController.DeleteIntegration) From 33ed5f54f2831ba51f4f026aac084be77af599b5 Mon Sep 17 00:00:00 2001 From: Sasikanth L Date: Tue, 23 Jul 2024 12:48:03 +0530 Subject: [PATCH 6/7] Fix linting and indentation --- gui/src/api/DashboardService.tsx | 23 +-- gui/src/app/projects/layout.tsx | 1 - gui/src/app/projects/page.tsx | 10 +- .../settings/SettingsOptions/Integrations.tsx | 40 +++-- gui/src/app/settings/page.tsx | 11 +- .../CustomSidebar/CustomSidebar.tsx | 25 +-- .../CreateOrEditProjectBody.tsx | 142 +++++++++++------- 7 files changed, 154 insertions(+), 98 deletions(-) diff --git a/gui/src/api/DashboardService.tsx b/gui/src/api/DashboardService.tsx index 970f3094..77b5590f 100644 --- a/gui/src/api/DashboardService.tsx +++ b/gui/src/api/DashboardService.tsx @@ -1,14 +1,20 @@ import api from './apiConfig'; -import {CreateProjectPayload, UpdateProjectPayload,} from '../../types/projectsTypes'; -import {FormStoryPayload} from '../../types/storyTypes'; +import { + CreateProjectPayload, + UpdateProjectPayload, +} from '../../types/projectsTypes'; +import { FormStoryPayload } from '../../types/storyTypes'; import { CommentReBuildDesignStoryPayload, CommentReBuildPayload, CreatePullRequestPayload, } from '../../types/pullRequestsTypes'; -import {CreateOrUpdateLLMAPIKeyPayload} from '../../types/modelsTypes'; -import {CreateDesignStoryPayload, EditDesignStoryPayload,} from '../../types/designStoryTypes'; -import {authPayload} from '../../types/authTypes'; +import { CreateOrUpdateLLMAPIKeyPayload } from '../../types/modelsTypes'; +import { + CreateDesignStoryPayload, + EditDesignStoryPayload, +} from '../../types/designStoryTypes'; +import { authPayload } from '../../types/authTypes'; export const checkHealth = () => { return api.get(`/health`); @@ -179,15 +185,14 @@ 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; -} \ No newline at end of file +}; 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 cea7d56c..c3b5097e 100644 --- a/gui/src/app/projects/page.tsx +++ b/gui/src/app/projects/page.tsx @@ -78,17 +78,17 @@ export default function Projects() {
-
{project.project_repository && (
-
+
- +
- {project.project_repository.split("/").join(" / ")} + + {project.project_repository.split('/').join(' / ')} +
)} { - const [isExternalGitIntegration, setIsExternalGitIntegration] = useState(null); + const [isExternalGitIntegration, setIsExternalGitIntegration] = useState< + boolean | undefined + >(null); useEffect(() => { (async function () { const gitIntegrated = await isGithubConnected(); @@ -31,35 +36,38 @@ const Integrations = () => { return (
-
-

Integrations

+
+

Integrations

-
+
-
+
- +
-

GitHub

-

+

GitHub

+

Integrate with GitHub to start working on your projects

- {isExternalGitIntegration === undefined ? (<>) : isExternalGitIntegration === false ? ( + {isExternalGitIntegration === undefined ? ( + <> + ) : isExternalGitIntegration === false ? ( ) : ( @@ -71,4 +79,4 @@ const Integrations = () => { ); }; -export default Integrations; \ No newline at end of file +export default Integrations; diff --git a/gui/src/app/settings/page.tsx b/gui/src/app/settings/page.tsx index ba621619..0816a2cd 100644 --- a/gui/src/app/settings/page.tsx +++ b/gui/src/app/settings/page.tsx @@ -4,7 +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"; +import Integrations from '@/app/settings/SettingsOptions/Integrations'; export default function Settings() { const options = [ @@ -14,14 +14,15 @@ export default function Settings() { selected: imagePath.modelsIconSelected, unselected: imagePath.modelsIconUnselected, icon_css: 'size-4', - component: , - }, { + component: , + }, + { key: 'integrations', text: 'Integrations', selected: imagePath.modelsIconSelected, unselected: imagePath.modelsIconUnselected, icon_css: 'size-4', - component: , + component: , }, ]; @@ -31,7 +32,7 @@ export default function Settings() { return (
- + = ({ - id, - title, - options, - onOptionSelect, - }) => { + id, + title, + options, + onOptionSelect, +}) => { const searchParams = useSearchParams(); - const page = searchParams.get("page"); - const selectedPage = options?.find((option) => option.key === page) || options[0]; + const page = searchParams.get('page'); + const selectedPage = + options?.find((option) => option.key === page) || options[0]; - const [selectedKey, setSelectedKey] = useState(selectedPage.key); + 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 f00ab348..8428e1f4 100644 --- a/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx +++ b/gui/src/components/HomeComponents/CreateOrEditProjectBody.tsx @@ -1,18 +1,31 @@ import CustomModal from '@/components/CustomModal/CustomModal'; import CustomImageSelector from '@/components/ImageComponents/CustomImageSelector'; -import {backendFrameworkOptions, frontendFrameworkOptions,} from '@/app/constants/ProjectConstants'; -import {Button} from '@nextui-org/react'; -import {useEffect, useRef, useState} from 'react'; -import {CreateProjectPayload, ProjectTypes, UpdateProjectPayload,} from '../../../types/projectsTypes'; -import {createProject, getGithubRepos, getProjectById, isGithubConnected, updateProject,} from '@/api/DashboardService'; -import {useRouter} from 'next/navigation'; -import {setProjectDetails} from '@/app/utils'; +import { + backendFrameworkOptions, + frontendFrameworkOptions, +} from '@/app/constants/ProjectConstants'; +import { Button } from '@nextui-org/react'; +import { useEffect, useRef, useState } from 'react'; +import { + CreateProjectPayload, + ProjectTypes, + UpdateProjectPayload, +} 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"; +import imagePath from '@/app/imagePath'; +import { API_BASE_URL } from '@/api/apiConfig'; +import Select from 'react-select'; interface CreateOrEditProjectBodyProps { id: string; @@ -30,8 +43,8 @@ const customStyles = { borderColor: '#333333', '&:hover': { color: '#ffffff', - borderColor: '#4a4a4a' - } + borderColor: '#4a4a4a', + }, }), menu: (provided) => ({ ...provided, @@ -49,17 +62,17 @@ const customStyles = { }), singleValue: (provided) => ({ ...provided, - color: '#ffffff' + color: '#ffffff', }), }; export default function CreateOrEditProjectBody({ - id, - openProjectModal, - setOpenProjectModal, - projectsList, - edit = false, - }: CreateOrEditProjectBodyProps) { + id, + openProjectModal, + setOpenProjectModal, + projectsList, + edit = false, +}: CreateOrEditProjectBodyProps) { const [selectedBackendFramework, setSelectedBackendFramework] = useState(backendFrameworkOptions[0].id); const [selectedFrontendFramework, setSelectedFrontendFramework] = @@ -73,22 +86,30 @@ export default function CreateOrEditProjectBody({ const router = useRouter(); const [integrationLoading, setIntegrationLoading] = useState(false); - const [isExternalGitIntegration, setIsExternalGitIntegration] = 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); + 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, 1000); + const interval = setInterval( + async () => { + const gitIntegrated = await isGithubConnected(); + if (gitIntegrated) { + setIsExternalGitIntegration(true); + setIntegrationLoading(false); + clearInterval(interval); + } + }, + 1000, + 1000, + ); window.open(`${API_BASE_URL}/integrations/github/authorize`, '_blank'); } catch (error) { console.error('Error: ', error); @@ -162,13 +183,15 @@ export default function CreateOrEditProjectBody({ 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, - }; - }); + const options = repositories.map( + (repository: { name: string; url: string }) => { + const { name, url } = repository; + return { + value: url, + label: name, + }; + }, + ); setRepositories(options); })(); }, [isExternalGitIntegration]); @@ -249,7 +272,7 @@ export default function CreateOrEditProjectBody({ width={'30vw'} onClose={() => setOpenProjectModal(false)} > - +
@@ -337,40 +360,57 @@ export default function CreateOrEditProjectBody({
- +
-

*Repository name will be named after the project name itself.

+

+ *Repository name will be named after the project name itself. +

- {(useExternalGit && isExternalGitIntegration) && ( + {useExternalGit && isExternalGitIntegration && ( <>