Pārlūkot izejas kodu

improving gitlab support (#3050)

* initial gitlab support commit

* improving ci flow

* delete integration

* delete integration fixes

* reset gitlab envs

* display connected username in integration list

* removing print statement

* removing console log

* renaming GitLab

---------

Co-authored-by: David Townley <davidtownley@Davids-MacBook-Air.local>
dt3-5 3 gadi atpakaļ
vecāks
revīzija
cd981a3c3e
34 mainītis faili ar 730 papildinājumiem un 290 dzēšanām
  1. 47 0
      api/server/handlers/project_integration/delete_gitlab.go
  2. 89 0
      api/server/handlers/project_integration/get_gitlab_porter_yaml.go
  3. 4 18
      api/server/handlers/project_integration/get_gitlab_repo_buildpack.go
  4. 3 17
      api/server/handlers/project_integration/get_gitlab_repo_contents.go
  5. 3 16
      api/server/handlers/project_integration/get_gitlab_repo_procfile.go
  6. 29 2
      api/server/handlers/project_integration/list_gitlab.go
  7. 14 5
      api/server/handlers/project_integration/list_gitlab_repo_branches.go
  8. 13 4
      api/server/handlers/project_integration/list_gitlab_repos.go
  9. 7 8
      api/server/handlers/release/create.go
  10. 1 10
      api/server/handlers/release/delete.go
  11. 69 15
      api/server/router/project_integration.go
  12. 28 0
      api/types/git_installation.go
  13. 6 1
      api/types/project_integration.go
  14. 22 18
      dashboard/src/components/repo-selector/BranchList.tsx
  15. 5 5
      dashboard/src/components/repo-selector/BuildpackSelection.tsx
  16. 5 5
      dashboard/src/components/repo-selector/BuildpackStack.tsx
  17. 15 13
      dashboard/src/components/repo-selector/ContentsList.tsx
  18. 57 23
      dashboard/src/components/repo-selector/DetectContentsList.tsx
  19. 35 27
      dashboard/src/components/repo-selector/RepoList.tsx
  20. 24 11
      dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx
  21. 5 5
      dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx
  22. 84 5
      dashboard/src/main/home/integrations/GitlabIntegrationList.tsx
  23. 7 1
      dashboard/src/main/home/integrations/IntegrationCategories.tsx
  24. 1 1
      dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx
  25. 76 40
      dashboard/src/shared/api.tsx
  26. 1 1
      internal/integrations/buildpacks/go.go
  27. 1 1
      internal/integrations/buildpacks/nodejs.go
  28. 1 1
      internal/integrations/buildpacks/python.go
  29. 6 6
      internal/integrations/buildpacks/ruby.go
  30. 1 2
      internal/integrations/buildpacks/shared.go
  31. 47 29
      internal/integrations/ci/gitlab/ci.go
  32. 8 0
      internal/repository/gorm/auth.go
  33. 1 0
      internal/repository/integrations.go
  34. 15 0
      internal/repository/test/auth.go

+ 47 - 0
api/server/handlers/project_integration/delete_gitlab.go

@@ -0,0 +1,47 @@
+package project_integration
+
+import (
+	"fmt"
+	"net/http"
+
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+type DeleteGitlabIntegration struct {
+	handlers.PorterHandlerReadWriter
+}
+
+func NewDeleteGitlabIntegrationHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *DeleteGitlabIntegration {
+	return &DeleteGitlabIntegration{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *DeleteGitlabIntegration) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
+
+	metadata := p.Config().Metadata
+
+	if !metadata.Gitlab {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("gitlab integration endpoints are not enabled")))
+		return
+	}
+
+	err := p.Repo().GitlabIntegration().DeleteGitlabIntegrationByID(gi.ProjectID, gi.ID)
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting gitlab integration: %w", err)))
+		return
+	}
+
+	return
+}

+ 89 - 0
api/server/handlers/project_integration/get_gitlab_porter_yaml.go

@@ -0,0 +1,89 @@
+package project_integration
+
+import (
+	b64 "encoding/base64"
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/porter-dev/porter/internal/models"
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+	"github.com/xanzy/go-gitlab"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+)
+
+type GitlabRepoPorterYamlContentsHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewGetGitlabRepoPorterYamlContentsHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GitlabRepoPorterYamlContentsHandler {
+	return &GitlabRepoPorterYamlContentsHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+	}
+}
+
+func (p *GitlabRepoPorterYamlContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
+
+	request := &types.GetGitlabProcfileRequest{}
+
+	ok := p.DecodeAndValidate(w, r, request)
+	if !ok {
+		return
+	}
+
+	path, err := url.QueryUnescape(request.Path)
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("malformed query param path")))
+		return
+	}
+
+	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
+	if err != nil {
+		if errors.Is(err, errUnauthorizedGitlabUser) {
+			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(errUnauthorizedGitlabUser, http.StatusUnauthorized))
+		}
+
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	file, resp, err := client.RepositoryFiles.GetRawFile(request.RepoPath,
+		strings.TrimPrefix(path, "./"), &gitlab.GetRawFileOptions{
+			Ref: gitlab.String(request.Branch),
+		},
+	)
+
+	if resp.StatusCode == http.StatusUnauthorized {
+		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
+		return
+	} else if resp.StatusCode == http.StatusNotFound {
+		w.WriteHeader(http.StatusNotFound)
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("no such procfile exists")))
+		return
+	}
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	data := b64.StdEncoding.EncodeToString(file)
+
+	p.WriteResult(w, r, data)
+}

+ 4 - 18
api/server/handlers/project_integration/get_gitlab_repo_buildpack.go

@@ -11,7 +11,6 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/integrations/buildpacks"
@@ -39,22 +38,9 @@ func (p *GetGitlabRepoBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
-	request := &types.GetBuildpackRequest{}
+	request := &types.GetGitlabBuildpackRequest{}
 
 	ok := p.DecodeAndValidate(w, r, request)
-
-	if !ok {
-		return
-	}
-
-	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
-
-	if !ok {
-		return
-	}
-
-	branch, ok := commonutils.GetBranchParam(p, w, r)
-
 	if !ok {
 		return
 	}
@@ -81,9 +67,9 @@ func (p *GetGitlabRepoBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http
 		dir = "."
 	}
 
-	tree, resp, err := client.Repositories.ListTree(fmt.Sprintf("%s/%s", owner, name), &gitlab.ListTreeOptions{
+	tree, resp, err := client.Repositories.ListTree(request.RepoPath, &gitlab.ListTreeOptions{
 		Path: gitlab.String(dir),
-		Ref:  gitlab.String(branch),
+		Ref:  gitlab.String(request.Branch),
 	})
 
 	if resp.StatusCode == http.StatusUnauthorized {
@@ -111,7 +97,7 @@ func (p *GetGitlabRepoBuildpackHandler) ServeHTTP(w http.ResponseWriter, r *http
 				}
 			}()
 			buildpacks.Runtimes[idx].DetectGitlab(
-				client, tree, owner, name, dir, branch,
+				client, tree, request.RepoPath, dir, request.Branch,
 				builderInfoMap[buildpacks.PaketoBuilder], builderInfoMap[buildpacks.HerokuBuilder],
 			)
 			wg.Done()

+ 3 - 17
api/server/handlers/project_integration/get_gitlab_repo_contents.go

@@ -10,7 +10,6 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -37,22 +36,9 @@ func (p *GetGitlabRepoContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
-	request := &types.GetContentsRequest{}
+	request := &types.GetGitlabContentsRequest{}
 
 	ok := p.DecodeAndValidate(w, r, request)
-
-	if !ok {
-		return
-	}
-
-	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
-
-	if !ok {
-		return
-	}
-
-	branch, ok := commonutils.GetBranchParam(p, w, r)
-
 	if !ok {
 		return
 	}
@@ -79,9 +65,9 @@ func (p *GetGitlabRepoContentsHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		return
 	}
 
-	tree, resp, err := client.Repositories.ListTree(fmt.Sprintf("%s/%s", owner, name), &gitlab.ListTreeOptions{
+	tree, resp, err := client.Repositories.ListTree(request.RepoPath, &gitlab.ListTreeOptions{
 		Path: gitlab.String(dir),
-		Ref:  gitlab.String(branch),
+		Ref:  gitlab.String(request.Branch),
 	})
 
 	if resp.StatusCode == http.StatusUnauthorized {

+ 3 - 16
api/server/handlers/project_integration/get_gitlab_repo_procfile.go

@@ -11,7 +11,6 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -40,7 +39,7 @@ func (p *GetGitlabRepoProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
-	request := &types.GetProcfileRequest{}
+	request := &types.GetGitlabProcfileRequest{}
 
 	ok := p.DecodeAndValidate(w, r, request)
 
@@ -48,18 +47,6 @@ func (p *GetGitlabRepoProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		return
 	}
 
-	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
-
-	if !ok {
-		return
-	}
-
-	branch, ok := commonutils.GetBranchParam(p, w, r)
-
-	if !ok {
-		return
-	}
-
 	path, err := url.QueryUnescape(request.Path)
 	if err != nil {
 		p.HandleAPIError(w, r, apierrors.NewErrForbidden(fmt.Errorf("malformed query param path")))
@@ -76,9 +63,9 @@ func (p *GetGitlabRepoProcfileHandler) ServeHTTP(w http.ResponseWriter, r *http.
 		return
 	}
 
-	file, resp, err := client.RepositoryFiles.GetRawFile(fmt.Sprintf("%s/%s", owner, name),
+	file, resp, err := client.RepositoryFiles.GetRawFile(request.RepoPath,
 		strings.TrimPrefix(path, "./"), &gitlab.GetRawFileOptions{
-			Ref: gitlab.String(branch),
+			Ref: gitlab.String(request.Branch),
 		},
 	)
 

+ 29 - 2
api/server/handlers/project_integration/list_gitlab.go

@@ -3,6 +3,8 @@ package project_integration
 import (
 	"net/http"
 
+	ints "github.com/porter-dev/porter/internal/models/integrations"
+
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
@@ -26,6 +28,7 @@ func NewListGitlabHandler(
 
 func (p *ListGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
 
 	gitlabInts, err := p.Repo().GitlabIntegration().ListGitlabIntegrationsByProjectID(project.ID)
 	if err != nil {
@@ -33,11 +36,35 @@ func (p *ListGitlabHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	var res types.ListGitlabResponse = make([]*types.GitlabIntegration, 0)
+	var res types.ListGitlabResponse = make([]*types.GitlabIntegrationWithUsername, 0)
 
 	for _, gitlabInt := range gitlabInts {
-		res = append(res, gitlabInt.ToGitlabIntegrationType())
+		username := p.getCurrentUsername(user.ID, project.ID, gitlabInt)
+		res = append(res,
+			&types.GitlabIntegrationWithUsername{
+				*gitlabInt.ToGitlabIntegrationType(),
+				username,
+			},
+		)
 	}
 
 	p.WriteResult(w, r, res)
 }
+
+func (p *ListGitlabHandler) getCurrentUsername(userID uint, projectID uint, gi *ints.GitlabIntegration) string {
+	client, err := getGitlabClient(p.Repo(), userID, projectID, gi, p.Config())
+	if err != nil {
+		return "Unable to connect"
+	}
+
+	currentUser, resp, err := client.Users.CurrentUser()
+	if resp.StatusCode == http.StatusUnauthorized {
+		return "Unable to connect"
+	}
+
+	if err != nil {
+		return "Unable to connect"
+	}
+
+	return currentUser.Username
+}

+ 14 - 5
api/server/handlers/project_integration/list_gitlab_repo_branches.go

@@ -8,7 +8,6 @@ import (
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
-	"github.com/porter-dev/porter/api/server/shared/commonutils"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
@@ -35,12 +34,14 @@ func (p *ListGitlabRepoBranchesHandler) ServeHTTP(w http.ResponseWriter, r *http
 	user, _ := r.Context().Value(types.UserScope).(*models.User)
 	gi, _ := r.Context().Value(types.GitlabIntegrationScope).(*ints.GitlabIntegration)
 
-	owner, name, ok := commonutils.GetOwnerAndNameParams(p, w, r)
-
-	if !ok {
+	request := &types.ListGitlabRepoBranchesRequest{}
+	if ok := p.DecodeAndValidate(w, r, request); !ok {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(errors.New("cannot decode and validate request")))
 		return
 	}
 
+	repoPath := request.RepoPath
+
 	client, err := getGitlabClient(p.Repo(), user.ID, project.ID, gi, p.Config())
 	if err != nil {
 		if errors.Is(err, errUnauthorizedGitlabUser) {
@@ -51,7 +52,15 @@ func (p *ListGitlabRepoBranchesHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	branches, resp, err := client.Branches.ListBranches(fmt.Sprintf("%s/%s", owner, name), &gitlab.ListBranchesOptions{})
+	branches, resp, err := client.Branches.ListBranches(repoPath,
+		&gitlab.ListBranchesOptions{
+			ListOptions: gitlab.ListOptions{
+				Page:    1,
+				PerPage: 20,
+			},
+			Search: &request.SearchTerm,
+		},
+	)
 
 	if resp.StatusCode == http.StatusUnauthorized {
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))

+ 13 - 4
api/server/handlers/project_integration/list_gitlab_repos.go

@@ -50,10 +50,21 @@ func (p *ListGitlabReposHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	giProjects, resp, err := client.Projects.ListProjects(&gitlab.ListProjectsOptions{
+	searchTerm := r.URL.Query().Get("searchTerm")
+
+	opts := &gitlab.ListProjectsOptions{
 		Simple:     gitlab.Bool(true),
 		Membership: gitlab.Bool(true),
-	})
+		ListOptions: gitlab.ListOptions{
+			PerPage: 20,
+			Page:    1,
+		},
+		Search:           gitlab.String(searchTerm),
+		SearchNamespaces: gitlab.Bool(true),
+	}
+
+	var res []string
+	giProjects, resp, err := client.Projects.ListProjects(opts)
 
 	if resp.StatusCode == http.StatusUnauthorized {
 		p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("unauthorized gitlab user"), http.StatusUnauthorized))
@@ -65,8 +76,6 @@ func (p *ListGitlabReposHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 	}
 
-	var res []string
-
 	for _, giProject := range giProjects {
 		res = append(res, giProject.PathWithNamespace)
 	}

+ 7 - 8
api/server/handlers/release/create.go

@@ -373,12 +373,6 @@ func createGitAction(
 
 	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "is-dry-run", Value: isDryRun})
 
-	repoSplit := strings.Split(request.GitRepo, "/")
-
-	if len(repoSplit) != 2 {
-		return nil, nil, fmt.Errorf("invalid formatting of repo name")
-	}
-
 	encoded := ""
 	var err error
 
@@ -397,8 +391,7 @@ func createGitAction(
 	if request.GitlabIntegrationID != 0 {
 		giRunner := &gitlab.GitlabCI{
 			ServerURL:        config.ServerConf.ServerURL,
-			GitRepoOwner:     repoSplit[0],
-			GitRepoName:      repoSplit[1],
+			GitRepoPath:      request.GitRepo,
 			GitBranch:        request.GitBranch,
 			Repo:             config.Repo,
 			ProjectID:        projectID,
@@ -414,6 +407,12 @@ func createGitAction(
 
 		gitErr = giRunner.Setup()
 	} else {
+		repoSplit := strings.Split(request.GitRepo, "/")
+
+		if len(repoSplit) != 2 {
+			return nil, nil, fmt.Errorf("invalid formatting of repo name")
+		}
+
 		// create the commit in the git repo
 		gaRunner := &actions.GithubActions{
 			InstanceName:           config.ServerConf.InstanceName,

+ 1 - 10
api/server/handlers/release/delete.go

@@ -2,9 +2,7 @@ package release
 
 import (
 	"context"
-	"fmt"
 	"net/http"
-	"strings"
 
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
@@ -60,17 +58,10 @@ func (c *DeleteReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 
 			if gitAction != nil && gitAction.ID != 0 {
 				if gitAction.GitlabIntegrationID != 0 {
-					repoSplit := strings.Split(gitAction.GitRepo, "/")
-
-					if len(repoSplit) != 2 {
-						c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("invalid formatting of repo name")))
-						return
-					}
 
 					giRunner := &gitlab.GitlabCI{
 						ServerURL:        c.Config().ServerConf.ServerURL,
-						GitRepoOwner:     repoSplit[0],
-						GitRepoName:      repoSplit[1],
+						GitRepoPath:      gitAction.GitRepo,
 						Repo:             c.Repo(),
 						ProjectID:        cluster.ProjectID,
 						ClusterID:        cluster.ID,

+ 69 - 15
api/server/router/project_integration.go

@@ -416,6 +416,33 @@ func getProjectIntegrationRoutes(
 	// PATCH /api/projects/{project_id}/integrations/gitlab/{integration_id}
 
 	// DELETE /api/projects/{project_id}/integrations/gitlab/{integration_id}
+	deleteGitlabEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbDelete,
+			Method: types.HTTPVerbDelete,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gitlab/{integration_id}",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitlabIntegrationScope,
+			},
+		},
+	)
+
+	deleteGitlabHandler := project_integration.NewDeleteGitlabIntegrationHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: deleteGitlabEndpoint,
+		Handler:  deleteGitlabHandler,
+		Router:   r,
+	})
 
 	// GET /api/projects/{project_id}/integrations/git
 	listGitIntegrationsEndpoint := factory.NewAPIEndpoint(
@@ -474,15 +501,15 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/branches
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/branches
 	listGitlabRepoBranchesEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/branches",
-					relPath, types.URLParamIntegrationID, types.URLParamGitRepoOwner, types.URLParamGitRepoName),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/branches",
+					relPath, types.URLParamIntegrationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -504,16 +531,15 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/{branch}/contents
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/contents
 	getGitlabRepoContentsEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/{%s}/contents", relPath,
-					types.URLParamIntegrationID, types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName, types.URLParamGitBranch),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/contents", relPath,
+					types.URLParamIntegrationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -535,16 +561,15 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/{branch}/buildpack/detect
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/buildpack/detect
 	getGitlabRepoBuildpackEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/{%s}/buildpack/detect", relPath,
-					types.URLParamIntegrationID, types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName, types.URLParamGitBranch),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/buildpack/detect", relPath,
+					types.URLParamIntegrationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -566,16 +591,15 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 	})
 
-	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/{owner}/{name}/{branch}/procfile
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/procfile
 	getGitlabRepoProcfileEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 			Verb:   types.APIVerbGet,
 			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 				Parent: basePath,
-				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/{%s}/{%s}/{%s}/procfile", relPath,
-					types.URLParamIntegrationID, types.URLParamGitRepoOwner,
-					types.URLParamGitRepoName, types.URLParamGitBranch),
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/procfile", relPath,
+					types.URLParamIntegrationID),
 			},
 			Scopes: []types.PermissionScope{
 				types.UserScope,
@@ -597,5 +621,35 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 	})
 
+	// GET /api/projects/{project_id}/integrations/gitlab/{integration_id}/repos/porteryaml
+	getGitlabRepoPorterYamlContentsEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent: basePath,
+				RelativePath: fmt.Sprintf("%s/gitlab/{%s}/repos/porteryaml", relPath,
+					types.URLParamIntegrationID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.GitlabIntegrationScope,
+			},
+		},
+	)
+
+	getGitlabRepoPorterYamlContentsHandler := project_integration.NewGetGitlabRepoPorterYamlContentsHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getGitlabRepoPorterYamlContentsEndpoint,
+		Handler:  getGitlabRepoPorterYamlContentsHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 28 - 0
api/types/git_installation.go

@@ -32,6 +32,34 @@ const (
 
 type ListRepoBranchesResponse []string
 
+type ListGitlabRepoBranchesRequest struct {
+	RepoPath   string `schema:"repo_path" form:"required"`
+	SearchTerm string `schema:"search_term"`
+}
+
+type GitlabRepoBranchRequest struct {
+	RepoPath string `schema:"repo_path" form:"required"`
+	Branch   string `schema:"branch" form:"required"`
+}
+
+type GetGitlabContentsRequest struct {
+	GitlabRepoBranchRequest
+	GetContentsRequest
+}
+type GetGitlabBuildpackRequest struct {
+	GitlabRepoBranchRequest
+	GetBuildpackRequest
+}
+type GetGitlabProcfileRequest struct {
+	GitlabRepoBranchRequest
+	GetProcfileRequest
+}
+
+type GetGitlabPorterYamlContentsRequest struct {
+	GitlabRepoBranchRequest
+	GetPorterYamlRequest
+}
+
 type GithubDirectoryRequest struct {
 	Dir string `schema:"dir" form:"required"`
 }

+ 6 - 1
api/types/project_integration.go

@@ -185,7 +185,12 @@ type GitlabIntegration struct {
 	InstanceURL string `json:"instance_url"`
 }
 
-type ListGitlabResponse []*GitlabIntegration
+type GitlabIntegrationWithUsername struct {
+	GitlabIntegration
+	Username string `json:"username"`
+}
+
+type ListGitlabResponse []*GitlabIntegrationWithUsername
 
 type CreateGitlabRequest struct {
 	InstanceURL     string `json:"instance_url"`

+ 22 - 18
dashboard/src/components/repo-selector/BranchList.tsx

@@ -37,7 +37,7 @@ const BranchList: React.FC<Props> = ({
   useEffect(() => {
     // Get branches
     if (!actionConfig) {
-      return () => { };
+      return () => {};
     }
 
     if (actionConfig?.kind === "github") {
@@ -67,12 +67,13 @@ const BranchList: React.FC<Props> = ({
       api
         .getGitlabBranches(
           "<token>",
-          {},
+          {
+            repo_path: actionConfig.git_repo,
+            search_term: searchFilter,
+          },
           {
             project_id: currentProject.id,
             integration_id: actionConfig.gitlab_integration_id,
-            repo_owner: actionConfig.git_repo.split("/")[0],
-            repo_name: actionConfig.git_repo.split("/")[1],
           }
         )
         .then((res) => {
@@ -86,7 +87,7 @@ const BranchList: React.FC<Props> = ({
           setError(true);
         });
     }
-  }, []);
+  }, [searchFilter]);
 
   const renderBranchList = () => {
     if (loading) {
@@ -99,19 +100,22 @@ const BranchList: React.FC<Props> = ({
       return <LoadingWrapper>Error loading branches</LoadingWrapper>;
     }
 
-    let results = searchFilter != null
-      ? branches
-        .filter((branch) => {
-          return branch.toLowerCase().includes(
-            searchFilter.toLowerCase()
-          );
-        })
-        .sort((a: string, b: string) => {
-          const aIndex = a.toLowerCase().indexOf(searchFilter.toLowerCase());
-          const bIndex = b.toLowerCase().indexOf(searchFilter.toLowerCase());
-          return aIndex - bIndex;
-        })
-      : sortBranches(branches).slice(0, 10);
+    let results =
+      searchFilter != null
+        ? branches
+            .filter((branch) => {
+              return branch.toLowerCase().includes(searchFilter.toLowerCase());
+            })
+            .sort((a: string, b: string) => {
+              const aIndex = a
+                .toLowerCase()
+                .indexOf(searchFilter.toLowerCase());
+              const bIndex = b
+                .toLowerCase()
+                .indexOf(searchFilter.toLowerCase());
+              return aIndex - bIndex;
+            })
+        : sortBranches(branches).slice(0, 10);
 
     if (results.length == 0) {
       return <LoadingWrapper>No matching Branches found.</LoadingWrapper>;

+ 5 - 5
dashboard/src/components/repo-selector/BuildpackSelection.tsx

@@ -73,14 +73,14 @@ export const BuildpackSelection: React.FC<{
     if (actionConfig.kind === "gitlab") {
       return api.detectGitlabBuildpack<DetectBuildpackResponse>(
         "<token>",
-        { dir: folderPath || "." },
+        {
+          repo_path: actionConfig.git_repo,
+          branch: branch,
+          dir: folderPath || ".",
+        },
         {
           project_id: currentProject.id,
           integration_id: actionConfig.gitlab_integration_id,
-
-          repo_owner: actionConfig.git_repo.split("/")[0],
-          repo_name: actionConfig.git_repo.split("/")[1],
-          branch: branch,
         }
       );
     }

+ 5 - 5
dashboard/src/components/repo-selector/BuildpackStack.tsx

@@ -133,14 +133,14 @@ export const BuildpackStack: React.FC<{
     if (actionConfig.kind === "gitlab") {
       return api.detectGitlabBuildpack<DetectBuildpackResponse>(
         "<token>",
-        { dir: folderPath || "." },
+        {
+          repo_path: actionConfig.git_repo,
+          branch: branch,
+          dir: folderPath || ".",
+        },
         {
           project_id: currentProject.id,
           integration_id: actionConfig.gitlab_integration_id,
-
-          repo_owner: actionConfig.git_repo.split("/")[0],
-          repo_name: actionConfig.git_repo.split("/")[1],
-          branch: branch,
         }
       );
     }

+ 15 - 13
dashboard/src/components/repo-selector/ContentsList.tsx

@@ -71,13 +71,14 @@ export default class ContentsList extends Component<PropsType, StateType> {
       return api
         .getGitlabFolderContent(
           "<token>",
-          { dir: this.state.currentDir || "./" },
+          {
+            repo_path: actionConfig.git_repo,
+            branch: branch,
+            dir: this.state.currentDir || "./",
+          },
           {
             project_id: currentProject.id,
             integration_id: actionConfig.gitlab_integration_id,
-            repo_owner: actionConfig.git_repo.split("/")[0],
-            repo_name: actionConfig.git_repo.split("/")[1],
-            branch: branch,
           }
         )
         .then((res) => {
@@ -128,14 +129,14 @@ export default class ContentsList extends Component<PropsType, StateType> {
 
     return api.detectGitlabBuildpack(
       "<token>",
-      { dir: this.state.currentDir || "." },
+      {
+        repo_path: actionConfig.git_repo,
+        branch: branch,
+        dir: this.state.currentDir || ".",
+      },
       {
         project_id: currentProject.id,
         integration_id: actionConfig.gitlab_integration_id,
-
-        repo_owner: actionConfig.git_repo.split("/")[0],
-        repo_name: actionConfig.git_repo.split("/")[1],
-        branch: branch,
       }
     );
   };
@@ -162,13 +163,14 @@ export default class ContentsList extends Component<PropsType, StateType> {
 
     return api.getGitlabProcfileContents(
       "<token>",
-      { path: procfilePath },
+      {
+        repo_path: actionConfig.git_repo,
+        branch: branch,
+        path: procfilePath,
+      },
       {
         project_id: currentProject.id,
         integration_id: actionConfig.gitlab_integration_id,
-        owner: actionConfig.git_repo.split("/")[0],
-        name: actionConfig.git_repo.split("/")[1],
-        branch: branch,
       }
     );
   };

+ 57 - 23
dashboard/src/components/repo-selector/DetectContentsList.tsx

@@ -132,13 +132,14 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
       return api
         .getGitlabFolderContent(
           "<token>",
-          { dir: currentDir || "./" },
+          {
+            repo_path: actionConfig.git_repo,
+            branch: branch,
+            dir: currentDir || "./",
+          },
           {
             project_id: currentProject.id,
             integration_id: actionConfig.gitlab_integration_id,
-            repo_owner: actionConfig.git_repo.split("/")[0],
-            repo_name: actionConfig.git_repo.split("/")[1],
-            branch: branch,
           }
         )
         .then((res) => {
@@ -169,25 +170,44 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
   const fetchPorterYamlContent = async (porterYaml: string) => {
     let { currentProject } = context;
     let { actionConfig, branch } = props;
-
-    try {
-      const res = await api.getPorterYamlContents(
-        "<token>",
-        {
-          path: porterYaml,
-        },
-        {
-          project_id: currentProject.id,
-          git_repo_id: actionConfig.git_repo_id,
-          kind: "github",
-          owner: actionConfig.git_repo.split("/")[0],
-          name: actionConfig.git_repo.split("/")[1],
-          branch: branch,
-        }
-      );
-      return res;
-    } catch (err) {
-      console.log(err);
+    if (actionConfig.kind === "github") {
+      try {
+        const res = await api.getPorterYamlContents(
+          "<token>",
+          {
+            path: porterYaml,
+          },
+          {
+            project_id: currentProject.id,
+            git_repo_id: actionConfig.git_repo_id,
+            kind: "github",
+            owner: actionConfig.git_repo.split("/")[0],
+            name: actionConfig.git_repo.split("/")[1],
+            branch: branch,
+          }
+        );
+        return res;
+      } catch (err) {
+        console.log(err);
+      }
+    } else if (actionConfig.kind === "gitlab") {
+      try {
+        const res = await api.getGitlabPorterYamlContents(
+          "<token>",
+          {
+            repo_path: actionConfig.git_repo,
+            branch: branch,
+            path: porterYaml,
+          },
+          {
+            project_id: currentProject.id,
+            integration_id: actionConfig.gitlab_integration_id,
+          }
+        );
+        return res;
+      } catch (err) {
+        console.log(err);
+      }
     }
   };
   const detectBuildpacks = () => {
@@ -209,8 +229,22 @@ const DetectContentsList: React.FC<PropsType> = (props) => {
           branch: branch,
         }
       );
+    } else if (actionConfig.kind === "gitlab") {
+      return api.detectGitlabBuildpack(
+        "<token>",
+        {
+          repo_path: actionConfig.git_repo,
+          branch: branch,
+          dir: currentDir || ".",
+        },
+        {
+          project_id: currentProject.id,
+          integration_id: actionConfig.gitlab_integration_id,
+        }
+      );
     }
   };
+
   const handleInputChange = (newValue: string) => {
     props.setPorterYamlPath(newValue);
     setChangedPorterYaml(newValue === "");

+ 35 - 27
dashboard/src/components/repo-selector/RepoList.tsx

@@ -11,6 +11,7 @@ import SearchBar from "../SearchBar";
 import DynamicLink from "components/DynamicLink";
 import { useOutsideAlerter } from "shared/hooks/useOutsideAlerter";
 import Text from "components/porter/Text";
+import { search } from "../../shared/search";
 
 type Props = {
   actionConfig: ActionConfigType | null;
@@ -22,15 +23,15 @@ type Props = {
 
 type Provider =
   | {
-    provider: "github";
-    name: string;
-    installation_id: number;
-  }
+      provider: "github";
+      name: string;
+      installation_id: number;
+    }
   | {
-    provider: "gitlab";
-    instance_url: string;
-    integration_id: number;
-  };
+      provider: "gitlab";
+      instance_url: string;
+      integration_id: number;
+    };
 
 // Sort provider by name if it's github or instance url if it's gitlab
 const sortProviders = (providers: Provider[]) => {
@@ -69,7 +70,7 @@ const RepoList: React.FC<Props> = ({
   const [repoLoading, setRepoLoading] = useState(true);
   const [selectedRepo, setSelectedRepo] = useState(null);
   const [repoError, setRepoError] = useState(false);
-  const [searchFilter, setSearchFilter] = useState(null);
+  const [searchFilter, setSearchFilter] = useState<string>("");
   const [hasProviders, setHasProviders] = useState(true);
   const { currentProject, setCurrentError } = useContext(Context);
 
@@ -110,14 +111,16 @@ const RepoList: React.FC<Props> = ({
 
       const repos = res.data.map((repo) => ({ ...repo, GHRepoID: repoId }));
       return repos;
-    } catch (error) { }
+    } catch (error) {}
   };
 
   const loadGitlabRepos = async (integrationId: number) => {
     try {
       const res = await api.getGitlabRepos<string[]>(
         "<token>",
-        {},
+        {
+          searchTerm: searchFilter,
+        },
         { project_id: currentProject.id, integration_id: integrationId }
       );
       const repos: RepoType[] = res.data.map((repo) => ({
@@ -126,7 +129,7 @@ const RepoList: React.FC<Props> = ({
         GitIntegrationId: integrationId,
       }));
       return repos;
-    } catch (error) { }
+    } catch (error) {}
   };
 
   const loadRepos = (provider: any) => {
@@ -160,7 +163,7 @@ const RepoList: React.FC<Props> = ({
       .finally(() => {
         setRepoLoading(false);
       });
-  }, [currentProvider]);
+  }, [currentProvider, searchFilter]);
 
   // clear out actionConfig and SelectedRepository if new search is performed
   useEffect(() => {
@@ -237,19 +240,24 @@ const RepoList: React.FC<Props> = ({
     }
 
     // show 10 most recently used repos if user hasn't searched anything yet
-    let results = searchFilter != null
-      ? repos
-        .filter((repo: RepoType) => {
-          return repo.FullName.toLowerCase().includes(
-            searchFilter.toLowerCase()
-          );
-        })
-        .sort((a: RepoType, b: RepoType) => {
-          const aIndex = a.FullName.toLowerCase().indexOf(searchFilter.toLowerCase());
-          const bIndex = b.FullName.toLowerCase().indexOf(searchFilter.toLowerCase());
-          return aIndex - bIndex;
-        })
-      : repos.slice(0, 10);
+    let results =
+      searchFilter != null
+        ? repos
+            .filter((repo: RepoType) => {
+              return repo.FullName.toLowerCase().includes(
+                searchFilter.toLowerCase()
+              );
+            })
+            .sort((a: RepoType, b: RepoType) => {
+              const aIndex = a.FullName.toLowerCase().indexOf(
+                searchFilter.toLowerCase()
+              );
+              const bIndex = b.FullName.toLowerCase().indexOf(
+                searchFilter.toLowerCase()
+              );
+              return aIndex - bIndex;
+            })
+        : repos.slice(0, 10);
 
     if (results.length == 0) {
       return <LoadingWrapper>No matching Github repos found.</LoadingWrapper>;
@@ -358,7 +366,7 @@ const ConnectToGithubButton = styled.a`
     props.disabled ? "#aaaabbee" : "#2E3338"};
   :hover {
     background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "" : "#353a3e"};
+      props.disabled ? "" : "#353a3e"};
   }
 
   > i {

+ 24 - 11
dashboard/src/main/home/cluster-dashboard/expanded-chart/ExpandedChart.tsx

@@ -462,17 +462,30 @@ const ExpandedChart: React.FC<Props> = (props) => {
                   <Spinner src={loadingSrc} /> This application is currently
                   being deployed
                 </Header>
-                Navigate to the{" "}
-                <A
-                  href={
-                    props.currentChart.git_action_config &&
-                    `https://github.com/${props.currentChart.git_action_config?.git_repo}/actions`
-                  }
-                  target={"_blank"}
-                >
-                  Actions
-                </A>{" "}
-                tab of your GitHub repo to view live build logs.
+                {props.currentChart.git_action_config &&
+                props.currentChart.git_action_config.gitlab_integration_id ? (
+                  <>
+                    Navigate to the{" "}
+                    <A
+                      href={`https://gitlab.com/${props.currentChart.git_action_config?.git_repo}/-/jobs`}
+                      target={"_blank"}
+                    >
+                      Jobs
+                    </A>{" "}
+                    tab of your GitLab repo to view live build logs.
+                  </>
+                ) : (
+                  <>
+                    Navigate to the{" "}
+                    <A
+                      href={`https://github.com/${props.currentChart.git_action_config?.git_repo}/actions`}
+                      target={"_blank"}
+                    >
+                      Actions
+                    </A>{" "}
+                    tab of your GitHub repo to view live build logs.
+                  </>
+                )}
               </TextWrap>
             </Placeholder>
           );

+ 5 - 5
dashboard/src/main/home/cluster-dashboard/expanded-chart/build-settings/_BuildpackConfigSection.tsx

@@ -95,14 +95,14 @@ const BuildpackConfigSection = forwardRef<
     if (actionConfig.kind === "gitlab") {
       return api.detectGitlabBuildpack<DetectBuildpackResponse>(
         "<token>",
-        { dir: actionConfig.folder_path || "." },
+        {
+          repo_path: actionConfig.git_repo,
+          branch: actionConfig.git_branch,
+          dir: actionConfig.folder_path || ".",
+        },
         {
           project_id: currentProject.id,
           integration_id: actionConfig.gitlab_integration_id,
-
-          repo_owner: actionConfig.git_repo.split("/")[0],
-          repo_name: actionConfig.git_repo.split("/")[1],
-          branch: actionConfig.git_branch,
         }
       );
     }

+ 84 - 5
dashboard/src/main/home/integrations/GitlabIntegrationList.tsx

@@ -8,26 +8,91 @@ import DynamicLink from "components/DynamicLink";
 
 interface Props {
   gitlabData: any[];
+  updateIntegrationList: () => void;
 }
 
+type StateType = {
+  isDelete: boolean;
+  deleteName: string;
+  deleteID: number;
+};
+
 const GitlabIntegrationList: React.FC<Props> = (props) => {
+  const [currentState, setCurrentState] = useState<StateType>({
+    isDelete: false,
+    deleteName: "",
+    deleteID: 0,
+  });
+
+  const { currentCluster, currentProject, setCurrentError } = useContext(
+    Context
+  );
+
+  const handleDeleteIntegration = () => {
+    api
+      .deleteGitlabIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          integration_id: currentState.deleteID,
+        }
+      )
+      .then(() => {
+        setCurrentState({
+          isDelete: false,
+          deleteName: "",
+          deleteID: 0,
+        });
+        props.updateIntegrationList();
+      })
+      .catch((err) => {
+        setCurrentError(err);
+      });
+  };
+
   return (
     <>
+      <ConfirmOverlay
+        show={currentState.isDelete}
+        message={`Are you sure you want to delete the GitLab integration for instance ${currentState.deleteName}?`}
+        onYes={handleDeleteIntegration}
+        onNo={() =>
+          setCurrentState({
+            isDelete: false,
+            deleteName: "",
+            deleteID: 0,
+          })
+        }
+      />
       <StyledIntegrationList>
         {props.gitlabData?.length > 0 ? (
           props.gitlabData.map((inst, idx) => {
             return (
-              <Integration
-                onClick={() => {}}
-                disabled={false}
-                key={`${inst.team_id}-${inst.channel}`}
-              >
+              <Integration onClick={() => {}} disabled={false} key={inst.id}>
                 <MainRow disabled={false}>
                   <Flex>
                     <Icon src={integrationList.gitlab.icon} />
                     <Label>{inst.instance_url}</Label>
+                    {inst.username.includes("Unable") ? (
+                      <ErrorLabel>[{inst.username}]</ErrorLabel>
+                    ) : (
+                      <UsernameLabel>({inst.username})</UsernameLabel>
+                    )}
                   </Flex>
                   <MaterialIconTray disabled={false}>
+                    <i
+                      className="material-icons"
+                      onClick={() => {
+                        setCurrentState({
+                          isDelete: true,
+                          deleteName: inst.instance_url,
+                          deleteID: inst.id,
+                        });
+                      }}
+                    >
+                      delete
+                    </i>
                     <i
                       className="material-icons"
                       onClick={() => {
@@ -71,6 +136,20 @@ const Label = styled.div`
   font-weight: 500;
 `;
 
+const UsernameLabel = styled.div`
+  color: #ffffff66;
+  font-size: 14px;
+  font-weight: 500;
+  padding: 10px;
+`;
+
+const ErrorLabel = styled.div`
+  color: #f6685e;
+  font-size: 14px;
+  font-weight: 500;
+  padding: 10px;
+`;
+
 const StyledIntegrationList = styled.div`
   margin-top: 20px;
   margin-bottom: 80px;

+ 7 - 1
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -94,6 +94,7 @@ const IntegrationCategories: React.FC<Props> = (props) => {
             setGitlabData(res.data);
             setLoading(false);
           });
+        break;
       default:
         console.log("Unknown integration category.");
     }
@@ -155,7 +156,12 @@ const IntegrationCategories: React.FC<Props> = (props) => {
       {loading ? (
         <Loading />
       ) : props.category === "gitlab" ? (
-        <GitlabIntegrationList gitlabData={gitlabData} />
+        <GitlabIntegrationList
+          gitlabData={gitlabData}
+          updateIntegrationList={() =>
+            getIntegrationsForCategory(props.category)
+          }
+        />
       ) : props.category == "slack" ? (
         <SlackIntegrationList slackData={slackData} />
       ) : (

+ 1 - 1
dashboard/src/main/home/integrations/create-integration/GitlabForm.tsx

@@ -124,7 +124,7 @@ const GitlabForm: React.FC<Props> = () => {
         <SaveButton
           onClick={submit}
           makeFlush={true}
-          text="Save Gitlab Settings"
+          text="Save GitLab Settings"
           status={buttonStatus || error?.message}
         />
       </StyledForm>

+ 76 - 40
dashboard/src/shared/api.tsx

@@ -480,6 +480,16 @@ const deleteRegistryIntegration = baseApi<
   return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}`;
 });
 
+const deleteGitlabIntegration = baseApi<
+  {},
+  {
+    project_id: number;
+    integration_id: number;
+  }
+>("DELETE", ({ project_id, integration_id }) => {
+  return `/api/projects/${project_id}/integrations/gitlab/${integration_id}`;
+});
+
 const deleteSlackIntegration = baseApi<
   {},
   {
@@ -633,24 +643,27 @@ const detectBuildpack = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/buildpack/detect`;
 });
 
 const detectGitlabBuildpack = baseApi<
-  { dir: string },
+  {
+    repo_path: string;
+    branch: string;
+    dir: string;
+  },
   {
     project_id: number;
     integration_id: number;
-    repo_owner: string;
-    repo_name: string;
-    branch: string;
   }
 >(
   "GET",
-  ({ project_id, integration_id, repo_name, repo_owner, branch }) =>
-    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/${repo_owner}/${repo_name}/${branch}/buildpack/detect`
+  ({ project_id, integration_id }) =>
+    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/buildpack/detect`
 );
 
 const getBranchContents = baseApi<
@@ -666,9 +679,11 @@ const getBranchContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/contents`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/contents`;
 });
 
 const getProcfileContents = baseApi<
@@ -684,9 +699,11 @@ const getProcfileContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/procfile`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/procfile`;
 });
 
 const getPorterYamlContents = baseApi<
@@ -702,28 +719,41 @@ const getPorterYamlContents = baseApi<
     branch: string;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id
-    }/repos/${pathParams.kind}/${pathParams.owner}/${pathParams.name
-    }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
+  return `/api/projects/${pathParams.project_id}/gitrepos/${
+    pathParams.git_repo_id
+  }/repos/${pathParams.kind}/${pathParams.owner}/${
+    pathParams.name
+  }/${encodeURIComponent(pathParams.branch)}/porteryaml`;
 });
 
-const getGitlabProcfileContents = baseApi<
+const getGitlabPorterYamlContents = baseApi<
   {
+    repo_path: string;
+    branch: string;
     path: string;
   },
   {
     project_id: number;
     integration_id: number;
-    owner: string;
-    name: string;
+  }
+>("GET", ({ project_id, integration_id }) => {
+  return `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/porteryaml`;
+});
+
+const getGitlabProcfileContents = baseApi<
+  {
+    repo_path: string;
     branch: string;
+    path: string;
+  },
+  {
+    project_id: number;
+    integration_id: number;
   }
 >(
   "GET",
-  ({ project_id, integration_id, owner, name, branch }) =>
-    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/${owner}/${name}/${encodeURIComponent(
-      branch
-    )}/procfile`
+  ({ project_id, integration_id }) =>
+    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/procfile`
 );
 
 const getBranches = baseApi<
@@ -1558,9 +1588,11 @@ const getEnvGroup = baseApi<
     version?: number;
   }
 >("GET", (pathParams) => {
-  return `/api/projects/${pathParams.id}/clusters/${pathParams.cluster_id
-    }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${pathParams.version ? "&version=" + pathParams.version : ""
-    }`;
+  return `/api/projects/${pathParams.id}/clusters/${
+    pathParams.cluster_id
+  }/namespaces/${pathParams.namespace}/envgroup?name=${pathParams.name}${
+    pathParams.version ? "&version=" + pathParams.version : ""
+  }`;
 });
 
 const getConfigMap = baseApi<
@@ -2134,7 +2166,9 @@ const getGitProviders = baseApi<{}, { project_id: number }>(
 );
 
 const getGitlabRepos = baseApi<
-  {},
+  {
+    search_term: string;
+  },
   { project_id: number; integration_id: number }
 >(
   "GET",
@@ -2143,34 +2177,34 @@ const getGitlabRepos = baseApi<
 );
 
 const getGitlabBranches = baseApi<
-  {},
+  {
+    repo_path: string;
+    search_term: string;
+  },
   {
     project_id: number;
     integration_id: number;
-    repo_owner: string;
-    repo_name: string;
   }
 >(
   "GET",
-  ({ project_id, integration_id, repo_owner, repo_name }) =>
-    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/${repo_owner}/${repo_name}/branches`
+  ({ project_id, integration_id }) =>
+    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/branches`
 );
 
 const getGitlabFolderContent = baseApi<
   {
+    repo_path: string;
+    branch: string;
     dir: string;
   },
   {
     project_id: number;
     integration_id: number;
-    repo_owner: string;
-    repo_name: string;
-    branch: string;
   }
 >(
   "GET",
-  ({ project_id, integration_id, repo_owner, repo_name, branch }) =>
-    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/${repo_owner}/${repo_name}/${branch}/contents`
+  ({ project_id, integration_id }) =>
+    `/api/projects/${project_id}/integrations/gitlab/${integration_id}/repos/contents`
 );
 
 const getLogPodValues = baseApi<
@@ -2487,7 +2521,7 @@ const removeStackEnvGroup = baseApi<
     `/api/v1/projects/${project_id}/clusters/${cluster_id}/namespaces/${namespace}/stacks/${stack_id}/remove_env_group/${env_group_name}`
 );
 
-const getGithubStatus = baseApi<{}, {}>("GET", ({ }) => `/api/status/github`);
+const getGithubStatus = baseApi<{}, {}>("GET", ({}) => `/api/status/github`);
 
 const createSecretAndOpenGitHubPullRequest = baseApi<
   {
@@ -2554,6 +2588,7 @@ export default {
   deletePod,
   deleteProject,
   deleteRegistryIntegration,
+  deleteGitlabIntegration,
   deleteSlackIntegration,
   updateNotificationConfig,
   getNotificationConfig,
@@ -2707,6 +2742,7 @@ export default {
   getGitlabRepos,
   getGitlabBranches,
   getGitlabFolderContent,
+  getGitlabPorterYamlContents,
   getLogPodValues,
   getLogs,
   listPorterEvents,

+ 1 - 1
internal/integrations/buildpacks/go.go

@@ -155,7 +155,7 @@ func (runtime *goRuntime) DetectGithub(
 func (runtime *goRuntime) DetectGitlab(
 	client *gitlab.Client,
 	tree []*gitlab.TreeNode,
-	owner, name, path, ref string,
+	repoPath, path, ref string,
 	paketo, heroku *BuilderInfo,
 ) error {
 	results := make(chan struct {

+ 1 - 1
internal/integrations/buildpacks/nodejs.go

@@ -414,7 +414,7 @@ func (runtime *nodejsRuntime) DetectGithub(
 func (runtime *nodejsRuntime) DetectGitlab(
 	client *gitlab.Client,
 	tree []*gitlab.TreeNode,
-	owner, name, path, ref string,
+	repoPath, path, ref string,
 	paketo, heroku *BuilderInfo,
 ) error {
 	results := make(chan struct {

+ 1 - 1
internal/integrations/buildpacks/python.go

@@ -252,7 +252,7 @@ func (runtime *pythonRuntime) DetectGithub(
 func (runtime *pythonRuntime) DetectGitlab(
 	client *gitlab.Client,
 	tree []*gitlab.TreeNode,
-	owner, name, path, ref string,
+	repoPath, path, ref string,
 	paketo, heroku *BuilderInfo,
 ) error {
 	results := make(chan struct {

+ 6 - 6
internal/integrations/buildpacks/ruby.go

@@ -161,13 +161,13 @@ func (runtime *rubyRuntime) detectRackupGithub(
 }
 
 func (runtime *rubyRuntime) detectRackupGitlab(
-	client *gitlab.Client, owner, name, ref string, results chan struct {
+	client *gitlab.Client, repoPath, ref string, results chan struct {
 		string
 		bool
 	},
 ) {
 	fileContent, _, err := client.RepositoryFiles.GetRawFile(
-		fmt.Sprintf("%s/%s", owner, name), "Gemfile.lock", &gitlab.GetRawFileOptions{
+		repoPath, "Gemfile.lock", &gitlab.GetRawFileOptions{
 			Ref: gitlab.String(ref),
 		})
 	if err != nil {
@@ -325,7 +325,7 @@ func (runtime *rubyRuntime) DetectGithub(
 func (runtime *rubyRuntime) DetectGitlab(
 	client *gitlab.Client,
 	tree []*gitlab.TreeNode,
-	owner, name, path, ref string,
+	repoPath, path, ref string,
 	paketo, heroku *BuilderInfo,
 ) error {
 	gemfileFound := false
@@ -361,13 +361,13 @@ func (runtime *rubyRuntime) DetectGitlab(
 	}
 
 	fileContent, _, err := client.RepositoryFiles.GetRawFile(
-		fmt.Sprintf("%s/%s", owner, name), "Gemfile", &gitlab.GetRawFileOptions{
+		repoPath, "Gemfile", &gitlab.GetRawFileOptions{
 			Ref: gitlab.String(ref),
 		})
 	if err != nil {
 		paketo.Others = append(paketo.Others, paketoBuildpackInfo)
 		heroku.Others = append(heroku.Others, herokuBuildpackInfo)
-		return fmt.Errorf("error fetching contents of Gemfile for %s/%s: %v", owner, name, err)
+		return fmt.Errorf("error fetching contents of Gemfile for %s: %v", repoPath, err)
 	}
 	gemfileContent := string(fileContent)
 
@@ -405,7 +405,7 @@ func (runtime *rubyRuntime) DetectGitlab(
 	}
 	go runtime.detectPassenger(gemfileContent, results)
 	if !configRuFound && gemfileLockFound {
-		go runtime.detectRackupGitlab(client, owner, name, ref, results)
+		go runtime.detectRackupGitlab(client, repoPath, ref, results)
 	}
 	if rakefileFound {
 		go runtime.detectRake(gemfileContent, results)

+ 1 - 2
internal/integrations/buildpacks/shared.go

@@ -62,8 +62,7 @@ type Runtime interface {
 	DetectGitlab(
 		*gitlab.Client, // github client to pull contents of files
 		[]*gitlab.TreeNode, // the root folder structure of the git repo
-		string, // owner
-		string, // name
+		string, // repoPath
 		string, // path
 		string, // SHA, branch or tag
 		*BuilderInfo, // paketo

+ 47 - 29
internal/integrations/ci/gitlab/ci.go

@@ -15,10 +15,9 @@ import (
 )
 
 type GitlabCI struct {
-	ServerURL    string
-	GitRepoName  string
-	GitRepoOwner string
-	GitBranch    string
+	ServerURL   string
+	GitRepoPath string
+	GitBranch   string
 
 	Repo repository.Repository
 
@@ -44,18 +43,12 @@ func (g *GitlabCI) Setup() error {
 		return err
 	}
 
-	g.pID = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
+	g.pID = g.GitRepoPath
 
-	branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
-	if err != nil {
-		return fmt.Errorf("error fetching list of branches: %w", err)
-	}
+	err = g.setGitlabDefaultBranch(client)
 
-	for _, branch := range branches {
-		if branch.Default {
-			g.defaultGitBranch = branch.Name
-			break
-		}
+	if err != nil {
+		return err
 	}
 
 	err = g.createGitlabSecret(client)
@@ -67,7 +60,7 @@ func (g *GitlabCI) Setup() error {
 	jobName := getGitlabStageJobName(g.ReleaseName)
 
 	ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
-		Ref: gitlab.String(g.defaultGitBranch),
+		Ref: gitlab.String(g.GitBranch),
 	})
 
 	if resp.StatusCode == http.StatusNotFound {
@@ -81,7 +74,7 @@ func (g *GitlabCI) Setup() error {
 		contentsYAML, _ := yaml.Marshal(contentsMap)
 
 		_, _, err = client.RepositoryFiles.CreateFile(g.pID, ".gitlab-ci.yml", &gitlab.CreateFileOptions{
-			Branch:        gitlab.String(g.defaultGitBranch),
+			Branch:        gitlab.String(g.GitBranch),
 			AuthorName:    gitlab.String("Porter Bot"),
 			AuthorEmail:   gitlab.String("contact@getporter.dev"),
 			Content:       gitlab.String(string(contentsYAML)),
@@ -173,7 +166,7 @@ func (g *GitlabCI) Setup() error {
 		}
 
 		_, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
-			Branch:        gitlab.String(g.defaultGitBranch),
+			Branch:        gitlab.String(g.GitBranch),
 			AuthorName:    gitlab.String("Porter Bot"),
 			AuthorEmail:   gitlab.String("contact@getporter.dev"),
 			Content:       gitlab.String(string(contentsYAML)),
@@ -194,18 +187,12 @@ func (g *GitlabCI) Cleanup() error {
 		return err
 	}
 
-	g.pID = fmt.Sprintf("%s/%s", g.GitRepoOwner, g.GitRepoName)
+	g.pID = g.GitRepoPath
 
-	branches, _, err := client.Branches.ListBranches(g.pID, &gitlab.ListBranchesOptions{})
-	if err != nil {
-		return fmt.Errorf("error fetching list of branches: %w", err)
-	}
+	err = g.setGitlabDefaultBranch(client)
 
-	for _, branch := range branches {
-		if branch.Default {
-			g.defaultGitBranch = branch.Name
-			break
-		}
+	if err != nil {
+		return err
 	}
 
 	err = g.deleteGitlabSecret(client)
@@ -217,7 +204,7 @@ func (g *GitlabCI) Cleanup() error {
 	jobName := getGitlabStageJobName(g.ReleaseName)
 
 	ciFile, resp, err := client.RepositoryFiles.GetRawFile(g.pID, ".gitlab-ci.yml", &gitlab.GetRawFileOptions{
-		Ref: gitlab.String(g.defaultGitBranch),
+		Ref: gitlab.String(g.GitBranch),
 	})
 
 	if resp.StatusCode == http.StatusNotFound {
@@ -293,7 +280,7 @@ func (g *GitlabCI) Cleanup() error {
 	}
 
 	_, _, err = client.RepositoryFiles.UpdateFile(g.pID, ".gitlab-ci.yml", &gitlab.UpdateFileOptions{
-		Branch:        gitlab.String(g.defaultGitBranch),
+		Branch:        gitlab.String(g.GitBranch),
 		AuthorName:    gitlab.String("Porter Bot"),
 		AuthorEmail:   gitlab.String("contact@getporter.dev"),
 		Content:       gitlab.String(string(contentsYAML)),
@@ -495,3 +482,34 @@ func (g *GitlabCI) getPorterTokenSecretName() string {
 func getGitlabStageJobName(releaseName string) string {
 	return fmt.Sprintf("porter-%s", strings.ToLower(strings.ReplaceAll(releaseName, "_", "-")))
 }
+
+func (g *GitlabCI) setGitlabDefaultBranch(client *gitlab.Client) error {
+	opt := &gitlab.ListBranchesOptions{
+		ListOptions: gitlab.ListOptions{
+			PerPage: 20,
+			Page:    1,
+		},
+	}
+
+	for {
+		branches, resp, err := client.Branches.ListBranches(g.pID, opt)
+		if err != nil {
+			return fmt.Errorf("error fetching list of branches: %w", err)
+		}
+
+		for _, branch := range branches {
+			if branch.Default {
+				g.defaultGitBranch = branch.Name
+				return nil
+			}
+		}
+		// Exit the loop when we've seen all pages.
+		if resp.NextPage == 0 {
+			break
+		}
+
+		// Update the page number to get the next page.
+		opt.Page = resp.NextPage
+	}
+	return nil
+}

+ 8 - 0
internal/repository/gorm/auth.go

@@ -1621,6 +1621,14 @@ func (repo *GitlabIntegrationRepository) ListGitlabIntegrationsByProjectID(proje
 	return gi, nil
 }
 
+func (repo *GitlabIntegrationRepository) DeleteGitlabIntegrationByID(projectID, id uint) error {
+	if err := repo.db.Where("project_id = ? AND id = ?", projectID, id).Delete(&ints.GitlabIntegration{}).Error; err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // EncryptGitlabIntegrationData will encrypt the gitlab integration data before
 // writing to the DB
 func (repo *GitlabIntegrationRepository) EncryptGitlabIntegrationData(

+ 1 - 0
internal/repository/integrations.go

@@ -93,6 +93,7 @@ type GitlabIntegrationRepository interface {
 	CreateGitlabIntegration(gi *ints.GitlabIntegration) (*ints.GitlabIntegration, error)
 	ReadGitlabIntegration(projectID, id uint) (*ints.GitlabIntegration, error)
 	ListGitlabIntegrationsByProjectID(projectID uint) ([]*ints.GitlabIntegration, error)
+	DeleteGitlabIntegrationByID(projectID, id uint) error
 }
 
 // GitlabAppOAuthIntegrationRepository represents the set of queries on the GitlabOAuthIntegration model

+ 15 - 0
internal/repository/test/auth.go

@@ -677,6 +677,21 @@ func (repo *GitlabIntegrationRepository) ListGitlabIntegrationsByProjectID(proje
 	return res, nil
 }
 
+func (repo *GitlabIntegrationRepository) DeleteGitlabIntegrationByID(projectID, id uint) error {
+	if !repo.canQuery {
+		return errors.New("Cannot write database")
+	}
+
+	if int(id-1) >= len(repo.gitlabIntegrations) || repo.gitlabIntegrations[id-1] == nil || repo.gitlabIntegrations[id-1].ProjectID != projectID {
+		return gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	repo.gitlabIntegrations[index] = nil
+
+	return nil
+}
+
 type GitlabAppOAuthIntegrationRepository struct {
 	canQuery                   bool
 	gitlabAppOAuthIntegrations []*ints.GitlabAppOAuthIntegration