jusrhee 4 лет назад
Родитель
Сommit
77d8b565e0
46 измененных файлов с 1104 добавлено и 317 удалено
  1. 19 4
      api/server/handlers/gitinstallation/oauth_callback.go
  2. 1 1
      api/server/handlers/handler.go
  3. 10 2
      api/server/handlers/oauth_callback/digitalocean.go
  4. 10 2
      api/server/handlers/oauth_callback/slack.go
  5. 3 0
      api/server/handlers/project/update_onboarding.go
  6. 6 1
      api/server/handlers/project_integration/create_aws.go
  7. 5 1
      api/server/handlers/project_integration/create_gcp.go
  8. 44 0
      api/server/handlers/project_integration/list_aws.go
  9. 46 0
      api/server/handlers/project_integration/list_do.go
  10. 44 0
      api/server/handlers/project_integration/list_gcp.go
  11. 9 2
      api/server/handlers/user/github_callback.go
  12. 9 2
      api/server/handlers/user/google_callback.go
  13. 81 0
      api/server/router/project_integration.go
  14. 15 0
      api/types/infra.go
  15. 10 7
      api/types/project.go
  16. 16 4
      api/types/project_integration.go
  17. 13 0
      api/types/registry.go
  18. 65 14
      dashboard/src/components/Selector.tsx
  19. 1 0
      dashboard/src/components/form-components/SelectRow.tsx
  20. 33 20
      dashboard/src/main/home/Home.tsx
  21. 83 4
      dashboard/src/main/home/onboarding/Onboarding.tsx
  22. 50 44
      dashboard/src/main/home/onboarding/components/ProviderSelector.tsx
  23. 6 1
      dashboard/src/main/home/onboarding/state/StateHandler.ts
  24. 30 28
      dashboard/src/main/home/onboarding/state/StepHandler.ts
  25. 20 19
      dashboard/src/main/home/onboarding/state/index.ts
  26. 100 27
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistry.tsx
  27. 2 0
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistryWrapper.tsx
  28. 11 4
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx
  29. 47 31
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_AWSRegistryForm.tsx
  30. 6 1
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx
  31. 45 34
      dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx
  32. 7 1
      dashboard/src/main/home/onboarding/steps/ConnectSource.tsx
  33. 1 1
      dashboard/src/main/home/onboarding/steps/NewProject.tsx
  34. 43 3
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx
  35. 2 0
      dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResourcesWrapper.tsx
  36. 20 17
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvsionerForm.tsx
  37. 20 16
      dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx
  38. 32 0
      dashboard/src/shared/api.tsx
  39. 1 0
      internal/kubernetes/provisioner/input/eks.go
  40. 1 0
      internal/kubernetes/provisioner/provisioner.go
  41. 87 4
      internal/models/infra.go
  42. 1 0
      internal/models/integrations/aws.go
  43. 17 1
      internal/models/integrations/gcp.go
  44. 1 0
      internal/models/integrations/oauth.go
  45. 21 15
      internal/models/onboarding.go
  46. 10 6
      internal/models/registry.go

+ 19 - 4
api/server/handlers/gitinstallation/oauth_callback.go

@@ -3,6 +3,7 @@ package gitinstallation
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"net/url"
 
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -42,8 +43,15 @@ func (c *GithubAppOAuthCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http
 	token, err := c.Config().GithubAppConf.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
 	token, err := c.Config().GithubAppConf.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
 
 
 	if err != nil || !token.Valid() {
 	if err != nil || !token.Valid() {
-		if session.Values["query_params"] != "" {
-			http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+		if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+			// attempt to parse the redirect uri, if it fails just redirect to dashboard
+			redirectURI, err := url.Parse(redirectStr)
+
+			if err != nil {
+				http.Redirect(w, r, "/dashboard", 302)
+			}
+
+			http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), 302)
 		} else {
 		} else {
 			http.Redirect(w, r, "/dashboard", 302)
 			http.Redirect(w, r, "/dashboard", 302)
 		}
 		}
@@ -82,8 +90,15 @@ func (c *GithubAppOAuthCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http
 		},
 		},
 	))
 	))
 
 
-	if session.Values["query_params"] != "" {
-		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", 302)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), 302)
 	} else {
 	} else {
 		http.Redirect(w, r, "/dashboard", 302)
 		http.Redirect(w, r, "/dashboard", 302)
 	}
 	}

+ 1 - 1
api/server/handlers/handler.go

@@ -90,7 +90,7 @@ func (d *DefaultPorterHandler) PopulateOAuthSession(w http.ResponseWriter, r *ht
 
 
 	// need state parameter to validate when redirected
 	// need state parameter to validate when redirected
 	session.Values["state"] = state
 	session.Values["state"] = state
-	session.Values["query_params"] = r.URL.RawQuery
+	session.Values["redirect_uri"] = r.URL.Query().Get("redirect_uri")
 
 
 	if isProject {
 	if isProject {
 		project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
 		project, _ := r.Context().Value(types.ProjectScope).(*models.Project)

+ 10 - 2
api/server/handlers/oauth_callback/digitalocean.go

@@ -3,6 +3,7 @@ package oauth_callback
 import (
 import (
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"net/url"
 
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -78,8 +79,15 @@ func (p *OAuthCallbackDOHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		return
 		return
 	}
 	}
 
 
-	if session.Values["query_params"] != "" {
-		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", 302)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), 302)
 	} else {
 	} else {
 		http.Redirect(w, r, "/dashboard", 302)
 		http.Redirect(w, r, "/dashboard", 302)
 	}
 	}

+ 10 - 2
api/server/handlers/oauth_callback/slack.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"net/url"
 
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -69,8 +70,15 @@ func (p *OAuthCallbackSlackHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
 		return
 		return
 	}
 	}
 
 
-	if session.Values["query_params"] != "" {
-		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", 302)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), 302)
 	} else {
 	} else {
 		http.Redirect(w, r, "/dashboard", 302)
 		http.Redirect(w, r, "/dashboard", 302)
 	}
 	}

+ 3 - 0
api/server/handlers/project/update_onboarding.go

@@ -56,8 +56,11 @@ func (p *OnboardingUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
 	onboarding.SkipRegistryConnection = request.SkipRegistryConnection
 	onboarding.SkipRegistryConnection = request.SkipRegistryConnection
 	onboarding.SkipResourceProvision = request.SkipResourceProvision
 	onboarding.SkipResourceProvision = request.SkipResourceProvision
 	onboarding.RegistryConnectionID = request.RegistryConnectionID
 	onboarding.RegistryConnectionID = request.RegistryConnectionID
+	onboarding.RegistryConnectionCredentialID = request.RegistryConnectionCredentialID
 	onboarding.RegistryInfraID = request.RegistryInfraID
 	onboarding.RegistryInfraID = request.RegistryInfraID
+	onboarding.RegistryInfraCredentialID = request.RegistryInfraCredentialID
 	onboarding.ClusterInfraID = request.ClusterInfraID
 	onboarding.ClusterInfraID = request.ClusterInfraID
+	onboarding.ClusterInfraCredentialID = request.ClusterInfraCredentialID
 
 
 	if isNotFound {
 	if isNotFound {
 		// if not found, create onboarding struct
 		// if not found, create onboarding struct

+ 6 - 1
api/server/handlers/project_integration/create_aws.go

@@ -53,7 +53,7 @@ func (p *CreateAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func CreateAWSIntegration(request *types.CreateAWSRequest, projectID, userID uint) *ints.AWSIntegration {
 func CreateAWSIntegration(request *types.CreateAWSRequest, projectID, userID uint) *ints.AWSIntegration {
-	return &ints.AWSIntegration{
+	resp := &ints.AWSIntegration{
 		UserID:             userID,
 		UserID:             userID,
 		ProjectID:          projectID,
 		ProjectID:          projectID,
 		AWSRegion:          request.AWSRegion,
 		AWSRegion:          request.AWSRegion,
@@ -61,4 +61,9 @@ func CreateAWSIntegration(request *types.CreateAWSRequest, projectID, userID uin
 		AWSAccessKeyID:     []byte(request.AWSAccessKeyID),
 		AWSAccessKeyID:     []byte(request.AWSAccessKeyID),
 		AWSSecretAccessKey: []byte(request.AWSSecretAccessKey),
 		AWSSecretAccessKey: []byte(request.AWSSecretAccessKey),
 	}
 	}
+
+	// attempt to populate the ARN
+	resp.PopulateAWSArn()
+
+	return resp
 }
 }

+ 5 - 1
api/server/handlers/project_integration/create_gcp.go

@@ -53,11 +53,15 @@ func (p *CreateGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 }
 }
 
 
 func CreateGCPIntegration(request *types.CreateGCPRequest, projectID, userID uint) *ints.GCPIntegration {
 func CreateGCPIntegration(request *types.CreateGCPRequest, projectID, userID uint) *ints.GCPIntegration {
-	return &ints.GCPIntegration{
+	resp := &ints.GCPIntegration{
 		UserID:       userID,
 		UserID:       userID,
 		ProjectID:    projectID,
 		ProjectID:    projectID,
 		GCPKeyData:   []byte(request.GCPKeyData),
 		GCPKeyData:   []byte(request.GCPKeyData),
 		GCPProjectID: request.GCPProjectID,
 		GCPProjectID: request.GCPProjectID,
 		GCPRegion:    request.GCPRegion,
 		GCPRegion:    request.GCPRegion,
 	}
 	}
+
+	resp.PopulateGCPMetadata()
+
+	return resp
 }
 }

+ 44 - 0
api/server/handlers/project_integration/list_aws.go

@@ -0,0 +1,44 @@
+package project_integration
+
+import (
+	"net/http"
+
+	"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"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ListAWSHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListAWSHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListAWSHandler {
+	return &ListAWSHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ListAWSHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	awsInts, err := p.Repo().AWSIntegration().ListAWSIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListAWSResponse = make([]*types.AWSIntegration, 0)
+
+	for _, awsInt := range awsInts {
+		res = append(res, awsInt.ToAWSIntegrationType())
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 46 - 0
api/server/handlers/project_integration/list_do.go

@@ -0,0 +1,46 @@
+package project_integration
+
+import (
+	"net/http"
+
+	"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"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ListDOHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListDOHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListDOHandler {
+	return &ListDOHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ListDOHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	oauthInts, err := p.Repo().OAuthIntegration().ListOAuthIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListOAuthResponse = make([]*types.OAuthIntegration, 0)
+
+	for _, oauthInt := range oauthInts {
+		if oauthInt.Client == types.OAuthDigitalOcean {
+			res = append(res, oauthInt.ToOAuthIntegrationType())
+		}
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 44 - 0
api/server/handlers/project_integration/list_gcp.go

@@ -0,0 +1,44 @@
+package project_integration
+
+import (
+	"net/http"
+
+	"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"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type ListGCPHandler struct {
+	handlers.PorterHandlerWriter
+}
+
+func NewListGCPHandler(
+	config *config.Config,
+	writer shared.ResultWriter,
+) *ListGCPHandler {
+	return &ListGCPHandler{
+		PorterHandlerWriter: handlers.NewDefaultPorterHandler(config, nil, writer),
+	}
+}
+
+func (p *ListGCPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
+
+	gcpInts, err := p.Repo().GCPIntegration().ListGCPIntegrationsByProjectID(project.ID)
+
+	if err != nil {
+		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	var res types.ListGCPResponse = make([]*types.GCPIntegration, 0)
+
+	for _, gcpInt := range gcpInts {
+		res = append(res, gcpInt.ToGCPIntegrationType())
+	}
+
+	p.WriteResult(w, r, res)
+}

+ 9 - 2
api/server/handlers/user/github_callback.go

@@ -88,8 +88,15 @@ func (p *UserOAuthGithubCallbackHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		startEmailVerification(p.Config(), w, r, user)
 		startEmailVerification(p.Config(), w, r, user)
 	}
 	}
 
 
-	if session.Values["query_params"] != "" {
-		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", 302)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), 302)
 	} else {
 	} else {
 		http.Redirect(w, r, "/dashboard", 302)
 		http.Redirect(w, r, "/dashboard", 302)
 	}
 	}

+ 9 - 2
api/server/handlers/user/google_callback.go

@@ -91,8 +91,15 @@ func (p *UserOAuthGoogleCallbackHandler) ServeHTTP(w http.ResponseWriter, r *htt
 		startEmailVerification(p.Config(), w, r, user)
 		startEmailVerification(p.Config(), w, r, user)
 	}
 	}
 
 
-	if session.Values["query_params"] != "" {
-		http.Redirect(w, r, fmt.Sprintf("/dashboard?%s", session.Values["query_params"]), 302)
+	if redirectStr, ok := session.Values["redirect_uri"].(string); ok && redirectStr != "" {
+		// attempt to parse the redirect uri, if it fails just redirect to dashboard
+		redirectURI, err := url.Parse(redirectStr)
+
+		if err != nil {
+			http.Redirect(w, r, "/dashboard", 302)
+		}
+
+		http.Redirect(w, r, fmt.Sprintf("%s?%s", redirectURI.Path, redirectURI.RawQuery), 302)
 	} else {
 	} else {
 		http.Redirect(w, r, "/dashboard", 302)
 		http.Redirect(w, r, "/dashboard", 302)
 	}
 	}

+ 81 - 0
api/server/router/project_integration.go

@@ -79,6 +79,33 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/integrations/do -> project_integration.NewListDOHandler
+	listDOEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/do",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listDOHandler := project_integration.NewListDOHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listDOEndpoint,
+		Handler:  listDOHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/integrations/basic -> project_integration.NewCreateBasicHandler
 	// POST /api/projects/{project_id}/integrations/basic -> project_integration.NewCreateBasicHandler
 	createBasicEndpoint := factory.NewAPIEndpoint(
 	createBasicEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{
@@ -135,6 +162,33 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/integrations/aws -> project_integration.NewListAWSHandler
+	listAWSEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/aws",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listAWSHandler := project_integration.NewListAWSHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listAWSEndpoint,
+		Handler:  listAWSHandler,
+		Router:   r,
+	})
+
 	// POST /api/projects/{project_id}/integrations/aws/overwrite -> project_integration.NewOverwriteAWSHandler
 	// POST /api/projects/{project_id}/integrations/aws/overwrite -> project_integration.NewOverwriteAWSHandler
 	overwriteAWSEndpoint := factory.NewAPIEndpoint(
 	overwriteAWSEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{
@@ -191,5 +245,32 @@ func getProjectIntegrationRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/integrations/gcp -> project_integration.NewListGCPHandler
+	listGCPEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: relPath + "/gcp",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+			},
+		},
+	)
+
+	listGCPHandler := project_integration.NewListGCPHandler(
+		config,
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: listGCPEndpoint,
+		Handler:  listGCPHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 	return routes, newPath
 }
 }

+ 15 - 0
api/types/infra.go

@@ -37,4 +37,19 @@ type Infra struct {
 
 
 	// Status is the status of the infra
 	// Status is the status of the infra
 	Status InfraStatus `json:"status"`
 	Status InfraStatus `json:"status"`
+
+	// The AWS integration that was used to create the infra
+	AWSIntegrationID uint `json:"aws_integration_id,omitempty"`
+
+	// The GCP integration that was used to create the infra
+	GCPIntegrationID uint `json:"gcp_integration_id,omitempty"`
+
+	// The DO integration that was used to create the infra:
+	// this points to an OAuthIntegrationID
+	DOIntegrationID uint `json:"do_integration_id,omitempty"`
+
+	// The last-applied, non-sensitive input variables to the provisioner. For now,
+	// this is a map[string]string since we marshal into env vars anyway, but
+	// eventually this config will be more complex.
+	LastApplied map[string]string `json:"last_applied"`
 }
 }

+ 10 - 7
api/types/project.go

@@ -85,13 +85,16 @@ const (
 )
 )
 
 
 type OnboardingData struct {
 type OnboardingData struct {
-	CurrentStep            StepEnum            `json:"current_step"`
-	ConnectedSource        ConnectedSourceType `json:"connected_source"`
-	SkipRegistryConnection bool                `json:"skip_registry_connection"`
-	SkipResourceProvision  bool                `json:"skip_resource_provision"`
-	RegistryConnectionID   uint                `json:"registry_connection_id"`
-	RegistryInfraID        uint                `json:"registry_infra_id"`
-	ClusterInfraID         uint                `json:"cluster_infra_id"`
+	CurrentStep                    StepEnum            `json:"current_step"`
+	ConnectedSource                ConnectedSourceType `json:"connected_source"`
+	SkipRegistryConnection         bool                `json:"skip_registry_connection"`
+	SkipResourceProvision          bool                `json:"skip_resource_provision"`
+	RegistryConnectionID           uint                `json:"registry_connection_id"`
+	RegistryConnectionCredentialID uint                `json:"registry_connection_credential_id"`
+	RegistryInfraID                uint                `json:"registry_infra_id"`
+	RegistryInfraCredentialID      uint                `json:"registry_infra_credential_id"`
+	ClusterInfraID                 uint                `json:"cluster_infra_id"`
+	ClusterInfraCredentialID       uint                `json:"cluster_infra_credential_id"`
 }
 }
 
 
 type UpdateOnboardingRequest OnboardingData
 type UpdateOnboardingRequest OnboardingData

+ 16 - 4
api/types/project_integration.go

@@ -1,5 +1,7 @@
 package types
 package types
 
 
+import "time"
+
 // The supported oauth mechanism clients
 // The supported oauth mechanism clients
 const (
 const (
 	OAuthGithub       OAuthIntegrationClient = "github"
 	OAuthGithub       OAuthIntegrationClient = "github"
@@ -12,6 +14,8 @@ type OAuthIntegrationClient string
 
 
 // OAuthIntegration is an OAuthIntegration to be shared over REST
 // OAuthIntegration is an OAuthIntegration to be shared over REST
 type OAuthIntegration struct {
 type OAuthIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+
 	ID uint `json:"id"`
 	ID uint `json:"id"`
 
 
 	// The name of the auth mechanism
 	// The name of the auth mechanism
@@ -46,6 +50,8 @@ type CreateBasicResponse struct {
 }
 }
 
 
 type AWSIntegration struct {
 type AWSIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+
 	ID uint `json:"id"`
 	ID uint `json:"id"`
 
 
 	// The id of the user that linked this auth mechanism
 	// The id of the user that linked this auth mechanism
@@ -58,6 +64,8 @@ type AWSIntegration struct {
 	AWSArn string `json:"aws_arn"`
 	AWSArn string `json:"aws_arn"`
 }
 }
 
 
+type ListAWSResponse []*AWSIntegration
+
 type CreateAWSRequest struct {
 type CreateAWSRequest struct {
 	AWSRegion          string `json:"aws_region"`
 	AWSRegion          string `json:"aws_region"`
 	AWSClusterID       string `json:"aws_cluster_id"`
 	AWSClusterID       string `json:"aws_cluster_id"`
@@ -81,6 +89,8 @@ type OverwriteAWSResponse struct {
 }
 }
 
 
 type GCPIntegration struct {
 type GCPIntegration struct {
+	CreatedAt time.Time `json:"created_at"`
+
 	ID uint `json:"id"`
 	ID uint `json:"id"`
 
 
 	// The id of the user that linked this auth mechanism
 	// The id of the user that linked this auth mechanism
@@ -89,13 +99,15 @@ type GCPIntegration struct {
 	// The project that this integration belongs to
 	// The project that this integration belongs to
 	ProjectID uint `json:"project_id"`
 	ProjectID uint `json:"project_id"`
 
 
-	// The GCP project id where the service account for this auth mechanism persists
-	GCPProjectID string `json:"gcp-project-id"`
+	// The GCP service account email for this credential
+	GCPSAEmail string `json:"gcp_sa_email"`
 
 
-	// The GCP user email that linked this service account
-	GCPUserEmail string `json:"gcp-user-email"`
+	// The GCP project id where the service account for this auth mechanism persists
+	GCPProjectID string `json:"gcp_project_id"`
 }
 }
 
 
+type ListGCPResponse []*GCPIntegration
+
 type CreateGCPRequest struct {
 type CreateGCPRequest struct {
 	GCPKeyData   string `json:"gcp_key_data" form:"required"`
 	GCPKeyData   string `json:"gcp_key_data" form:"required"`
 	GCPProjectID string `json:"gcp_project_id"`
 	GCPProjectID string `json:"gcp_project_id"`

+ 13 - 0
api/types/registry.go

@@ -23,6 +23,19 @@ type Registry struct {
 
 
 	// The infra id, if registry was provisioned with Porter
 	// The infra id, if registry was provisioned with Porter
 	InfraID uint `json:"infra_id"`
 	InfraID uint `json:"infra_id"`
+
+	// The AWS integration that was used to create or connect the registry
+	AWSIntegrationID uint `json:"aws_integration_id,omitempty"`
+
+	// The GCP integration that was used to create or connect the registry
+	GCPIntegrationID uint `json:"gcp_integration_id,omitempty"`
+
+	// The DO integration that was used to create or connect the registry:
+	// this points to an OAuthIntegrationID
+	DOIntegrationID uint `json:"do_integration_id,omitempty"`
+
+	// The basic integration that was used to connect the registry:
+	BasicIntegrationID uint `json:"basic_integration_id,omitempty"`
 }
 }
 
 
 // Repository is a collection of images
 // Repository is a collection of images

+ 65 - 14
dashboard/src/components/Selector.tsx

@@ -5,7 +5,7 @@ import { Context } from "shared/Context";
 type PropsType = {
 type PropsType = {
   activeValue: string;
   activeValue: string;
   refreshOptions?: () => void;
   refreshOptions?: () => void;
-  options: { value: string; label: string }[];
+  options: { value: string; label: string, icon?: any }[];
   addButton?: boolean;
   addButton?: boolean;
   setActiveValue: (x: string) => void;
   setActiveValue: (x: string) => void;
   width: string;
   width: string;
@@ -58,14 +58,20 @@ export default class Selector extends Component<PropsType, StateType> {
   renderOptionList = () => {
   renderOptionList = () => {
     let { options, activeValue } = this.props;
     let { options, activeValue } = this.props;
     return options.map(
     return options.map(
-      (option: { value: string; label: string }, i: number) => {
+      (option: { value: string; label: string, icon?: any }, i: number) => {
         return (
         return (
           <Option
           <Option
             key={i}
             key={i}
+            height={this.props.height}
             selected={option.value === activeValue}
             selected={option.value === activeValue}
             onClick={() => this.handleOptionClick(option)}
             onClick={() => this.handleOptionClick(option)}
             lastItem={i === options.length - 1}
             lastItem={i === options.length - 1}
           >
           >
+            {
+              option.icon && (
+                <Icon><img src={option.icon} /></Icon>
+              )
+            }
             {option.label}
             {option.label}
           </Option>
           </Option>
         );
         );
@@ -124,6 +130,24 @@ export default class Selector extends Component<PropsType, StateType> {
     }
     }
   };
   };
 
 
+  renderIcon = () => {
+    var icon;
+    this.props.options.forEach((option: any) => {
+      if (option.icon && option.value === this.props.activeValue) {
+        icon = option.icon;
+      }
+    });
+    return (
+      <>
+        {
+          icon && (
+            <Icon><img src={icon} /></Icon>
+          )
+        }
+      </>
+    )
+  }
+
   render() {
   render() {
     let { activeValue } = this.props;
     let { activeValue } = this.props;
 
 
@@ -141,9 +165,12 @@ export default class Selector extends Component<PropsType, StateType> {
           width={this.props.width}
           width={this.props.width}
           height={this.props.height}
           height={this.props.height}
         >
         >
-          <TextWrap>
-            {activeValue === "" ? "All" : this.getLabel(activeValue)}
-          </TextWrap>
+          <Flex>
+            {this.renderIcon()}
+            <TextWrap>
+              {activeValue === "" ? "All" : this.getLabel(activeValue)}
+            </TextWrap>
+          </Flex>
           <i className="material-icons">arrow_drop_down</i>
           <i className="material-icons">arrow_drop_down</i>
         </MainSelector>
         </MainSelector>
         {this.renderDropdown()}
         {this.renderDropdown()}
@@ -154,6 +181,27 @@ export default class Selector extends Component<PropsType, StateType> {
 
 
 Selector.contextType = Context;
 Selector.contextType = Context;
 
 
+const Flex = styled.div`
+  display: flex;
+  align-items: center;
+`;
+
+const Icon = styled.div`
+  height: 20px;
+  width: 30px;
+  margin-left: -5px;
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: visible;
+  
+  > img {
+    height: 18px;
+    width: auto;
+  }
+`;
+
 const Plus = styled.div`
 const Plus = styled.div`
   margin-right: 10px;
   margin-right: 10px;
   font-size: 15px;
   font-size: 15px;
@@ -193,15 +241,19 @@ const NewOption = styled.div`
   }
   }
 `;
 `;
 
 
-const Option = styled.div`
+const Option = styled.div<{ 
+  selected: boolean; 
+  lastItem: boolean;
+  height: string;
+}>`
   width: 100%;
   width: 100%;
   border-top: 1px solid #00000000;
   border-top: 1px solid #00000000;
   border-bottom: 1px solid
   border-bottom: 1px solid
-    ${(props: { selected: boolean; lastItem: boolean }) =>
-      props.lastItem ? "#ffffff00" : "#ffffff15"};
-  height: 37px;
+    ${props => props.lastItem ? "#ffffff00" : "#ffffff15"};
+  height: ${props => props.height || "37px"};
   font-size: 13px;
   font-size: 13px;
-  padding-top: 9px;
+  align-items: center;
+  display: flex;
   align-items: center;
   align-items: center;
   padding-left: 15px;
   padding-left: 15px;
   cursor: pointer;
   cursor: pointer;
@@ -209,8 +261,7 @@ const Option = styled.div`
   white-space: nowrap;
   white-space: nowrap;
   overflow: hidden;
   overflow: hidden;
   text-overflow: ellipsis;
   text-overflow: ellipsis;
-  background: ${(props: { selected: boolean; lastItem: boolean }) =>
-    props.selected ? "#ffffff11" : ""};
+  background: ${props => props.selected ? "#ffffff11" : ""};
 
 
   :hover {
   :hover {
     background: #ffffff22;
     background: #ffffff22;
@@ -255,11 +306,11 @@ const MainSelector = styled.div`
   border: 1px solid #ffffff55;
   border: 1px solid #ffffff55;
   font-size: 13px;
   font-size: 13px;
   padding: 5px 10px;
   padding: 5px 10px;
-  padding-left: 12px;
+  padding-left: 15px;
   border-radius: 3px;
   border-radius: 3px;
   display: flex;
   display: flex;
-  align-items: center;
   justify-content: space-between;
   justify-content: space-between;
+  align-items: center;
   cursor: pointer;
   cursor: pointer;
   background: ${(props: {
   background: ${(props: {
     expanded: boolean;
     expanded: boolean;

+ 1 - 0
dashboard/src/components/form-components/SelectRow.tsx

@@ -41,6 +41,7 @@ const SelectWrapper = styled.div``;
 const Label = styled.div`
 const Label = styled.div`
   color: #ffffff;
   color: #ffffff;
   margin-bottom: 10px;
   margin-bottom: 10px;
+  font-size: 13px;
 `;
 `;
 
 
 const StyledSelectRow = styled.div`
 const StyledSelectRow = styled.div`

+ 33 - 20
dashboard/src/main/home/Home.tsx

@@ -288,12 +288,20 @@ class Home extends Component<PropsType, StateType> {
   }
   }
 
 
   async checkIfProjectHasBilling(projectId: number) {
   async checkIfProjectHasBilling(projectId: number) {
-    const res = await api.getHasBilling(
-      "<token>",
-      {},
-      { project_id: projectId }
-    );
-    this.context.setHasBillingEnabled(res.data?.has_billing);
+    if (!projectId) {
+      return false;
+    }
+    try {
+      const res = await api.getHasBilling(
+        "<token>",
+        {},
+        { project_id: projectId }
+      );
+      this.context.setHasBillingEnabled(res.data?.has_billing);
+      return res?.data?.has_billing;
+    } catch (error) {
+      console.log(error);
+    }
   }
   }
 
 
   // TODO: Need to handle the following cases. Do a deep rearchitecture (Prov -> Dashboard?) if need be:
   // TODO: Need to handle the following cases. Do a deep rearchitecture (Prov -> Dashboard?) if need be:
@@ -302,20 +310,25 @@ class Home extends Component<PropsType, StateType> {
   // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
   // 3. Make sure initializing from URL (DO oauth) displays the appropriate initial view
   componentDidUpdate(prevProps: PropsType) {
   componentDidUpdate(prevProps: PropsType) {
     if (prevProps.currentProject?.id !== this.props.currentProject?.id) {
     if (prevProps.currentProject?.id !== this.props.currentProject?.id) {
-      this.checkIfProjectHasBilling(this?.context?.currentProject?.id);
-      api
-        .getUsage(
-          "<token>",
-          {},
-          { project_id: this.context?.currentProject?.id }
-        )
-        .then((res) => {
-          const usage = res.data;
-          this.context.setUsage(usage);
-          if (usage.exceeded) {
-            this.context.setCurrentModal("UsageWarningModal", {
-              usage,
-            });
+      this.checkIfProjectHasBilling(this?.context?.currentProject?.id)
+        .then((isBillingEnabled) => {
+          if (isBillingEnabled) {
+            api
+              .getUsage(
+                "<token>",
+                {},
+                { project_id: this.context?.currentProject?.id }
+              )
+              .then((res) => {
+                const usage = res.data;
+                this.context.setUsage(usage);
+                if (usage.exceeded) {
+                  this.context.setCurrentModal("UsageWarningModal", {
+                    usage,
+                  });
+                }
+              })
+              .catch(console.log);
           }
           }
         })
         })
         .catch(console.log);
         .catch(console.log);

+ 83 - 4
dashboard/src/main/home/onboarding/Onboarding.tsx

@@ -1,13 +1,12 @@
 import React, { useContext, useEffect } from "react";
 import React, { useContext, useEffect } from "react";
-import { useLocation } from "react-router";
+import api from "shared/api";
 import { Context } from "shared/Context";
 import { Context } from "shared/Context";
-import { useRouting } from "shared/routing";
 import styled from "styled-components";
 import styled from "styled-components";
-import { useSnapshot } from "valtio";
 import { devtools } from "valtio/utils";
 import { devtools } from "valtio/utils";
 import Routes from "./Routes";
 import Routes from "./Routes";
 import { OFState } from "./state";
 import { OFState } from "./state";
 import { useSteps } from "./state/StepHandler";
 import { useSteps } from "./state/StepHandler";
+import { Onboarding as OnboardingSaveType } from "./types";
 
 
 const Onboarding = () => {
 const Onboarding = () => {
   const context = useContext(Context);
   const context = useContext(Context);
@@ -20,8 +19,88 @@ const Onboarding = () => {
     };
     };
   }, []);
   }, []);
 
 
+  const getData = async ({
+    id: project_id,
+    name: project_name,
+  }: {
+    id: number;
+    name: string;
+  }): Promise<OnboardingSaveType> => {
+    let odata = null;
+
+    // Get general onboarding data
+    try {
+      const response = await api.getOnboardingState(
+        "<token>",
+        {},
+        { project_id: project_id }
+      );
+
+      if (response.data) {
+        odata = response.data;
+      }
+    } catch (error) {
+      return null;
+    }
+
+    let registry_connection_data = null;
+    if (odata?.registry_connection_id) {
+      // Get subflows data
+      try {
+        const response = await api.getOnboardingRegistry(
+          "<token>",
+          {},
+          {
+            project_id: project_id,
+            registry_connection_id: odata.registry_connection_id,
+          }
+        );
+        console.log(response);
+        if (response.data) {
+          registry_connection_data = response.data;
+        }
+      } catch (error) {
+        console.error("Couldn't get registry connection data");
+      }
+    }
+
+    let provision_connection_data = null;
+    if (odata?.registry_infra_id) {
+      try {
+        const response = await api.getOnboardingInfra(
+          "<token>",
+          {},
+          {
+            project_id: project_id,
+            registry_infra_id: odata.registry_infra_id,
+          }
+        );
+
+        if (response.data) {
+          provision_connection_data = response.data;
+        }
+      } catch (error) {
+        console.error("Couldn't get infra data");
+      }
+    }
+
+    return {
+      project_id,
+      project_name,
+      ...(odata || {}),
+      ...(registry_connection_data || {}),
+      ...(provision_connection_data || {}),
+    };
+  };
+
   useEffect(() => {
   useEffect(() => {
-    OFState.actions.initializeState(context.currentProject?.id);
+    if (context?.currentProject?.id) {
+      getData(context.currentProject).then((data) => {
+        if (data) {
+          OFState.actions.initializeState(data);
+        }
+      });
+    }
     return () => {
     return () => {
       OFState.actions.clearState();
       OFState.actions.clearState();
     };
     };

+ 50 - 44
dashboard/src/main/home/onboarding/components/ProviderSelector.tsx

@@ -1,7 +1,8 @@
-import React from "react";
+import React, { useState } from "react";
 import { integrationList } from "shared/common";
 import { integrationList } from "shared/common";
 import styled from "styled-components";
 import styled from "styled-components";
 import { SupportedProviders } from "../types";
 import { SupportedProviders } from "../types";
+import Selector from "components/Selector";
 
 
 export type ProviderSelectorProps = {
 export type ProviderSelectorProps = {
   selectProvider: (
   selectProvider: (
@@ -12,61 +13,66 @@ export type ProviderSelectorProps = {
 
 
 const providers: SupportedProviders[] = ["aws", "gcp", "do"];
 const providers: SupportedProviders[] = ["aws", "gcp", "do"];
 
 
+const providerOptions = [
+  { 
+    value: "skip",
+    label: "Skip / I don't know what this is" 
+  },
+  { 
+    value: "aws",
+    icon: integrationList["aws"].icon,
+    label: "Amazon Elastic Container Registry (ECR)" 
+  },
+  { 
+    value: "gcp",
+    icon: integrationList["gcp"].icon,
+    label: "Google Cloud Registry (GCR)" 
+  },
+  { 
+    value: "do",
+    icon: integrationList["do"].icon,
+    label: "DigitalOcean Container Registry (DOCR)" 
+  },
+  { 
+    value: "external",
+    icon: integrationList["kubernetes"].icon,
+    label: "Link to an existing cluster"
+  }
+]
+
 const ProviderSelector: React.FC<ProviderSelectorProps> = ({
 const ProviderSelector: React.FC<ProviderSelectorProps> = ({
   selectProvider,
   selectProvider,
   enableExternal,
   enableExternal,
 }) => {
 }) => {
+  const [provider, setProvider] = useState("skip");
+
   return (
   return (
     <>
     <>
-      <BlockList>
-        {providers.map((provider, i: number) => {
-          let providerInfo = integrationList[provider];
-          return (
-            <Block
-              key={i}
-              onClick={() => {
-                selectProvider(provider);
-              }}
-            >
-              <Icon src={providerInfo.icon} />
-              <BlockTitle>{providerInfo.label}</BlockTitle>
-              <CostSection
-                onClick={(e) => {
-                  e.stopPropagation();
-                  selectProvider(provider);
-                }}
-              ></CostSection>
-              <BlockDescription>Hosted in your own cloud.</BlockDescription>
-            </Block>
-          );
-        })}
-        {enableExternal && (
-          <Block
-            key={"external"}
-            onClick={() => {
-              selectProvider("external");
-            }}
-          >
-            <Icon src={""} />
-            <BlockTitle>External Cluster</BlockTitle>
-            <CostSection
-              onClick={(e) => {
-                e.stopPropagation();
-                selectProvider("external");
-              }}
-            ></CostSection>
-            <BlockDescription>
-              Connect your own cluster via CLI.
-            </BlockDescription>
-          </Block>
-        )}
-      </BlockList>
+      <Br />
+      <Selector
+        activeValue={provider}
+        options={providerOptions}
+        setActiveValue={provider => {
+          setProvider(provider);
+          if (provider !== "skip" && provider !== "external") {
+            selectProvider(provider as SupportedProviders);
+          }
+        }}
+        width="100%"
+        height="45px"
+      />
+      <Br />
     </>
     </>
   );
   );
 };
 };
 
 
 export default ProviderSelector;
 export default ProviderSelector;
 
 
+const Br = styled.div`
+  width: 100%;
+  height: 10px;
+`;
+
 const CostSection = styled.p`
 const CostSection = styled.p`
   position: absolute;
   position: absolute;
   left: 0;
   left: 0;

+ 6 - 1
dashboard/src/main/home/onboarding/state/StateHandler.ts

@@ -7,7 +7,6 @@ import type {
   GCPRegistryConfig,
   GCPRegistryConfig,
   SkipProvisionConfig,
   SkipProvisionConfig,
   SkipRegistryConnection,
   SkipRegistryConnection,
-  SupportedProviders,
 } from "../types";
 } from "../types";
 
 
 export type ConnectedRegistryConfig =
 export type ConnectedRegistryConfig =
@@ -116,5 +115,11 @@ export const StateHandler = proxy({
         ...settings,
         ...settings,
       };
       };
     },
     },
+    clearRegistryProvider: () => {
+      StateHandler.connected_registry.provider = "";
+    },
+    clearResourceProvisioningProvider: () => {
+      StateHandler.provision_resources.provider = "";
+    },
   },
   },
 });
 });

+ 30 - 28
dashboard/src/main/home/onboarding/state/StepHandler.ts

@@ -2,35 +2,29 @@ import { useEffect } from "react";
 import { useLocation } from "react-router";
 import { useLocation } from "react-router";
 import { useRouting } from "shared/routing";
 import { useRouting } from "shared/routing";
 import { proxy, useSnapshot } from "valtio";
 import { proxy, useSnapshot } from "valtio";
-import { devtools } from "valtio/utils";
 import { StepKey, Steps } from "../types";
 import { StepKey, Steps } from "../types";
-import { StateKeys } from "./StateHandler";
 
 
 type Step = {
 type Step = {
-  previous?: StepKey;
   url: string;
   url: string;
   final?: true;
   final?: true;
   substeps?: {
   substeps?: {
-    [key in string]: SubStep;
+    [key in string]: Step;
   };
   };
   on?: ActionHandler;
   on?: ActionHandler;
   execute?: {
   execute?: {
     on: {
     on: {
       skip?: string;
       skip?: string;
       continue?: string;
       continue?: string;
+      go_back?: string;
     };
     };
   };
   };
 };
 };
 
 
-type SubStep = Omit<Step, "previous"> & {
-  parent: StepKey;
-  previous?: string;
-};
-
-export type Action = "skip" | "continue";
+export type Action = "skip" | "continue" | "go_back";
 type ActionHandler = {
 type ActionHandler = {
   skip?: string;
   skip?: string;
   continue: string;
   continue: string;
+  go_back?: string;
 };
 };
 
 
 export type FlowType = {
 export type FlowType = {
@@ -55,7 +49,6 @@ const flow: FlowType = {
       },
       },
     },
     },
     connect_source: {
     connect_source: {
-      previous: "new_project",
       url: "/onboarding/source",
       url: "/onboarding/source",
       on: {
       on: {
         continue: "connect_registry",
         continue: "connect_registry",
@@ -67,11 +60,11 @@ const flow: FlowType = {
       },
       },
     },
     },
     connect_registry: {
     connect_registry: {
-      previous: "connect_source",
       url: "/onboarding/registry",
       url: "/onboarding/registry",
       on: {
       on: {
         skip: "provision_resources",
         skip: "provision_resources",
         continue: "connect_registry.credentials",
         continue: "connect_registry.credentials",
+        go_back: "connect_source",
       },
       },
       execute: {
       execute: {
         on: {
         on: {
@@ -84,21 +77,23 @@ const flow: FlowType = {
           url: "/onboarding/registry/credentials",
           url: "/onboarding/registry/credentials",
           on: {
           on: {
             continue: "connect_registry.settings",
             continue: "connect_registry.settings",
+            go_back: "connect_registry",
           },
           },
-          parent: "connect_registry",
+
           execute: {
           execute: {
             on: {
             on: {
               continue: "saveRegistryCredentials",
               continue: "saveRegistryCredentials",
+              go_back: "clearRegistryProvider",
             },
             },
           },
           },
         },
         },
         settings: {
         settings: {
-          previous: "credentials",
           url: "/onboarding/registry/settings",
           url: "/onboarding/registry/settings",
           on: {
           on: {
             continue: "connect_registry.test_connection",
             continue: "connect_registry.test_connection",
+            go_back: "connect_registry.credentials",
           },
           },
-          parent: "connect_registry",
+
           execute: {
           execute: {
             on: {
             on: {
               continue: "saveRegistrySettings",
               continue: "saveRegistrySettings",
@@ -106,21 +101,19 @@ const flow: FlowType = {
           },
           },
         },
         },
         test_connection: {
         test_connection: {
-          previous: "settings",
           url: "/onboarding/registry/test_connection",
           url: "/onboarding/registry/test_connection",
           on: {
           on: {
             continue: "provision_resources",
             continue: "provision_resources",
           },
           },
-          parent: "connect_registry",
         },
         },
       },
       },
     },
     },
     provision_resources: {
     provision_resources: {
-      previous: "connect_registry",
       url: "/onboarding/provision",
       url: "/onboarding/provision",
       on: {
       on: {
         skip: "provision_resources.connect_own_cluster",
         skip: "provision_resources.connect_own_cluster",
         continue: "provision_resources.credentials",
         continue: "provision_resources.credentials",
+        go_back: "connect_registry",
       },
       },
       execute: {
       execute: {
         on: {
         on: {
@@ -133,26 +126,33 @@ const flow: FlowType = {
           url: "/onboarding/provision/connect_own_cluster",
           url: "/onboarding/provision/connect_own_cluster",
           on: {
           on: {
             continue: "clean_up",
             continue: "clean_up",
+            go_back: "provision_resources",
+          },
+          execute: {
+            on: {
+              go_back: "clearResourceProvisioningProvider",
+            },
           },
           },
-          parent: "provision_resources",
         },
         },
         credentials: {
         credentials: {
           url: "/onboarding/provision/credentials",
           url: "/onboarding/provision/credentials",
-          on: { continue: "provision_resources.settings" },
-          parent: "provision_resources",
+          on: {
+            continue: "provision_resources.settings",
+            go_back: "provision_resources",
+          },
           execute: {
           execute: {
             on: {
             on: {
               continue: "saveResourceProvisioningCredentials",
               continue: "saveResourceProvisioningCredentials",
+              go_back: "clearResourceProvisioningProvider",
             },
             },
           },
           },
         },
         },
         settings: {
         settings: {
-          previous: "credentials",
           url: "/onboarding/provision/settings",
           url: "/onboarding/provision/settings",
           on: {
           on: {
             continue: "clean_up",
             continue: "clean_up",
+            go_back: "provision_resources.credentials",
           },
           },
-          parent: "provision_resources",
           execute: {
           execute: {
             on: {
             on: {
               continue: "saveResourceProvisioningSettings",
               continue: "saveResourceProvisioningSettings",
@@ -171,12 +171,13 @@ const flow: FlowType = {
 type StepHandlerType = {
 type StepHandlerType = {
   flow: FlowType;
   flow: FlowType;
   currentStepName: string;
   currentStepName: string;
-  currentStep: Step | SubStep;
+  currentStep: Step;
+  canGoBack?: boolean;
   actions: {
   actions: {
     nextStep: (action?: Action) => void;
     nextStep: (action?: Action) => void;
     clearState: () => void;
     clearState: () => void;
     restoreState: (prevState: Partial<StepHandlerType>) => void;
     restoreState: (prevState: Partial<StepHandlerType>) => void;
-    getStep: (nextStepName: string) => Step | SubStep;
+    getStep: (nextStepName: string) => Step;
   };
   };
 };
 };
 
 
@@ -199,9 +200,10 @@ export const StepHandler: StepHandlerType = proxy({
           "No next step name found, fix the action triggering nextStep"
           "No next step name found, fix the action triggering nextStep"
         );
         );
       }
       }
-
+      const newStep = StepHandler.actions.getStep(nextStepName);
       StepHandler.currentStepName = nextStepName;
       StepHandler.currentStepName = nextStepName;
-      StepHandler.currentStep = StepHandler.actions.getStep(nextStepName);
+      StepHandler.currentStep = newStep;
+      StepHandler.canGoBack = !!newStep?.on?.go_back;
       return;
       return;
     },
     },
     getStep: (nextStepName: string) => {
     getStep: (nextStepName: string) => {
@@ -209,7 +211,7 @@ export const StepHandler: StepHandlerType = proxy({
 
 
       const step = flow.steps[stepName as Steps];
       const step = flow.steps[stepName as Steps];
 
 
-      let nextStep: Step | SubStep = step;
+      let nextStep: Step = step;
 
 
       if (substep) {
       if (substep) {
         nextStep = step.substeps[substep];
         nextStep = step.substeps[substep];

+ 20 - 19
dashboard/src/main/home/onboarding/state/index.ts

@@ -1,5 +1,5 @@
-import { proxy, subscribe } from "valtio";
-import { devtools, subscribeKey } from "valtio/utils";
+import api from "shared/api";
+import { proxy } from "valtio";
 import { Onboarding } from "../types";
 import { Onboarding } from "../types";
 import { StateHandler } from "./StateHandler";
 import { StateHandler } from "./StateHandler";
 import { Action, StepHandler } from "./StepHandler";
 import { Action, StepHandler } from "./StepHandler";
@@ -9,8 +9,8 @@ export const OFState = proxy({
   StepHandler,
   StepHandler,
   subscriptions: [],
   subscriptions: [],
   actions: {
   actions: {
-    initializeState: (projectId: number) => {
-      OFState.actions.restoreState(projectId);
+    initializeState: (state: Onboarding) => {
+      OFState.actions.restoreState(state);
     },
     },
     nextStep: (action?: Action, data?: any) => {
     nextStep: (action?: Action, data?: any) => {
       const functionToExecute = StepHandler?.currentStep?.execute?.on[action];
       const functionToExecute = StepHandler?.currentStep?.execute?.on[action];
@@ -30,19 +30,20 @@ export const OFState = proxy({
     },
     },
     saveState: () => {
     saveState: () => {
       const state = compressState(OFState);
       const state = compressState(OFState);
-      localStorage.setItem(
-        `onboarding-${OFState.StateHandler.project?.id}`,
-        state
-      );
+
+      api
+        .saveOnboardingState(
+          "<token>",
+          {
+            ...state,
+          },
+          { project_id: state.project_id }
+        )
+        .then((res) => console.log(res))
+        .catch((err) => console.log(err));
     },
     },
-    restoreState: (projectId: number) => {
-      const notParsedPrevState = localStorage.getItem(
-        `onboarding-${projectId}`
-      );
-      if (!notParsedPrevState) {
-        return;
-      }
-      const prevState = decompressState(notParsedPrevState);
+    restoreState: (state: Onboarding) => {
+      const prevState = decompressState(state);
 
 
       if (prevState.StepHandler.currentStepName === "clean_up") {
       if (prevState.StepHandler.currentStepName === "clean_up") {
         return;
         return;
@@ -93,11 +94,11 @@ const compressState = (state: typeof OFState) => {
       provision?.settings?.aws_machine_type,
       provision?.settings?.aws_machine_type,
   };
   };
 
 
-  return JSON.stringify(onboarding_state);
+  return onboarding_state;
 };
 };
 
 
-const decompressState = (prev_state: string) => {
-  const state: Onboarding = JSON.parse(prev_state);
+const decompressState = (prev_state: Onboarding) => {
+  const state: Onboarding = prev_state;
 
 
   const step = state.current_step;
   const step = state.current_step;
   const project = {
   const project = {

+ 100 - 27
dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistry.tsx

@@ -1,18 +1,19 @@
 import Helper from "components/form-components/Helper";
 import Helper from "components/form-components/Helper";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import TitleSection from "components/TitleSection";
 import TitleSection from "components/TitleSection";
-import React, { useState } from "react";
+import React from "react";
 import { useParams } from "react-router";
 import { useParams } from "react-router";
 
 
 import styled from "styled-components";
 import styled from "styled-components";
 import ProviderSelector from "../../components/ProviderSelector";
 import ProviderSelector from "../../components/ProviderSelector";
-import { ConnectedRegistryConfig } from "../../state/StateHandler";
 import { SupportedProviders } from "../../types";
 import { SupportedProviders } from "../../types";
+import backArrow from "assets/back_arrow.png";
 
 
 import FormFlowWrapper from "./forms/FormFlow";
 import FormFlowWrapper from "./forms/FormFlow";
 
 
 const ConnectRegistry: React.FC<{
 const ConnectRegistry: React.FC<{
   provider: SupportedProviders;
   provider: SupportedProviders;
+  enable_go_back: boolean;
   project: {
   project: {
     id: number;
     id: number;
     name: string;
     name: string;
@@ -22,6 +23,7 @@ const ConnectRegistry: React.FC<{
   onSaveSettings: (settings: any) => void;
   onSaveSettings: (settings: any) => void;
   onSuccess: () => void;
   onSuccess: () => void;
   onSkip: () => void;
   onSkip: () => void;
+  goBack: () => void;
 }> = ({
 }> = ({
   onSelectProvider,
   onSelectProvider,
   onSaveCredentials,
   onSaveCredentials,
@@ -30,18 +32,36 @@ const ConnectRegistry: React.FC<{
   onSkip,
   onSkip,
   project,
   project,
   provider,
   provider,
+  enable_go_back,
+  goBack,
 }) => {
 }) => {
   const { step } = useParams<any>();
   const { step } = useParams<any>();
 
 
   return (
   return (
-    <div>
+    <Div>
+      {enable_go_back && (
+        <BackButton
+          onClick={() => {
+            goBack();
+          }}
+        >
+          <BackButtonImg src={backArrow} />
+        </BackButton>
+      )}
       <TitleSection>Getting Started</TitleSection>
       <TitleSection>Getting Started</TitleSection>
-      <Subtitle>Step 2 of 3</Subtitle>
+      <Subtitle>Step 2 of 3 - Connect an existing registry (Optional)</Subtitle>
       <Helper>
       <Helper>
         {provider
         {provider
-          ? "Link to an existing Docker registry. Don't worry if you don't know what this is"
-          : "Link to an existing docker registry or continue"}
+          ? "Link to an existing Docker registry. Don't worry if you don't know what this is."
+          : "Link to an existing Docker registry or continue."}
       </Helper>
       </Helper>
+      <ProviderSelector
+          selectProvider={(provider) => {
+            if (provider !== "external") {
+              onSelectProvider(provider);
+            }
+          }}
+        />
       {provider ? (
       {provider ? (
         <FormFlowWrapper
         <FormFlowWrapper
           provider={provider}
           provider={provider}
@@ -52,37 +72,90 @@ const ConnectRegistry: React.FC<{
           currentStep={step}
           currentStep={step}
         />
         />
       ) : (
       ) : (
-        <>
-          <ProviderSelector
-            selectProvider={(provider) => {
-              if (provider !== "external") {
-                onSelectProvider(provider);
-              }
-            }}
-          />
-          <NextStep
-            text="Skip step"
-            disabled={false}
-            onClick={() => onSkip()}
-            status={""}
-            makeFlush={true}
-            clearPosition={true}
-            statusPosition="right"
-            saveText=""
-          />
-        </>
+        <NextStep
+          text="Skip step"
+          disabled={false}
+          onClick={() => onSkip()}
+          status={""}
+          makeFlush={true}
+          clearPosition={true}
+          statusPosition="right"
+          saveText=""
+        />
       )}
       )}
-    </div>
+    </Div>
   );
   );
 };
 };
 
 
 export default ConnectRegistry;
 export default ConnectRegistry;
 
 
-const Subtitle = styled(TitleSection)`
+const Div = styled.div`
+  width: 100%;
+`;
+
+const FadeWrapper = styled.div<{ delay?: string }>`
+  opacity: 0;
+  animation: fadeIn 0.5s ${(props) => props.delay || "0.2s"};
+  animation-fill-mode: forwards;
+
+  @keyframes fadeIn {
+    from {
+      opacity: 0;
+    }
+    to {
+      opacity: 1;
+    }
+  }
+`;
+
+const SlideWrapper = styled.div<{ delay?: string }>`
+  opacity: 0;
+  animation: slideIn 0.7s ${props => props.delay || "1.3s"};
+  animation-fill-mode: forwards;
+
+  @keyframes slideIn {
+    from {
+      opacity: 0;
+      transform: translateX(30px);
+    }
+    to {
+      opacity: 1;
+      transform: translateX(0);
+    }
+  }
+`;
+
+const Subtitle = styled.div`
   font-size: 16px;
   font-size: 16px;
+  font-weight: 500;
   margin-top: 16px;
   margin-top: 16px;
 `;
 `;
 
 
 const NextStep = styled(SaveButton)`
 const NextStep = styled(SaveButton)`
   margin-top: 24px;
   margin-top: 24px;
 `;
 `;
+
+const BackButton = styled.div`
+  margin-bottom: 24px;
+  display: flex;
+  width: 36px;
+  cursor: pointer;
+  height: 36px;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const BackButtonImg = styled.img`
+  width: 16px;
+  opacity: 0.75;
+`;

+ 2 - 0
dashboard/src/main/home/onboarding/steps/ConnectRegistry/ConnectRegistryWrapper.tsx

@@ -16,6 +16,8 @@ const ConnectRegistryWrapper = () => {
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSkip={() => OFState.actions.nextStep("skip")}
       onSkip={() => OFState.actions.nextStep("skip")}
+      enable_go_back={snap.StepHandler.canGoBack}
+      goBack={() => OFState.actions.nextStep("go_back")}
     />
     />
   );
   );
 };
 };

+ 11 - 4
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/FormFlow.tsx

@@ -94,8 +94,7 @@ const FormFlowWrapper: React.FC<Props> = ({
   }, [provider, currentStep]);
   }, [provider, currentStep]);
 
 
   return (
   return (
-    <>
-      {FormTitle[provider]}
+    <FormWrapper>
       <Breadcrumb>
       <Breadcrumb>
         <Text bold={currentStep === "credentials"}>Credentials</Text>
         <Text bold={currentStep === "credentials"}>Credentials</Text>
         {" > "}
         {" > "}
@@ -104,16 +103,24 @@ const FormFlowWrapper: React.FC<Props> = ({
         <Text bold={currentStep === "test_connection"}>Test Connection</Text>
         <Text bold={currentStep === "test_connection"}>Test Connection</Text>
       </Breadcrumb>
       </Breadcrumb>
       {CurrentForm}
       {CurrentForm}
-    </>
+    </FormWrapper>
   );
   );
 };
 };
 
 
 export default FormFlowWrapper;
 export default FormFlowWrapper;
 
 
+const FormWrapper = styled.div`
+  background: #ffffff11;
+  margin-top: -10px;
+  padding: 20px;
+  border-bottom-left-radius: 5px;
+  border-bottom-right-radius: 5px;
+`;
+
 const Text = styled.span<{ bold: boolean }>`
 const Text = styled.span<{ bold: boolean }>`
   font-weight: ${(props) => (props.bold ? "600" : "normal")};
   font-weight: ${(props) => (props.bold ? "600" : "normal")};
 `;
 `;
 
 
 const Breadcrumb = styled.div`
 const Breadcrumb = styled.div`
-  margin: 0 10px;
+  font-size: 13px;
 `;
 `;

+ 47 - 31
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_AWSRegistryForm.tsx

@@ -3,9 +3,11 @@ import SelectRow from "components/form-components/SelectRow";
 import SaveButton from "components/SaveButton";
 import SaveButton from "components/SaveButton";
 import { AWSRegistryConfig } from "main/home/onboarding/types";
 import { AWSRegistryConfig } from "main/home/onboarding/types";
 import React, { useState } from "react";
 import React, { useState } from "react";
+import styled from "styled-components";
 import api from "shared/api";
 import api from "shared/api";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
 import { OFState } from "../../../state/index";
 import { OFState } from "../../../state/index";
+import IntegrationCategories from "main/home/integrations/IntegrationCategories";
 
 
 const regionOptions = [
 const regionOptions = [
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
   { value: "us-east-1", label: "US East (N. Virginia) us-east-1" },
@@ -64,24 +66,27 @@ export const CredentialsForm: React.FC<{
       setButtonStatus(validation.error);
       setButtonStatus(validation.error);
       return;
       return;
     }
     }
+    try {
+      const res = await api.createAWSIntegration(
+        "token",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: accessId,
+          aws_secret_access_key: secretKey,
+        },
+        {
+          id: project.id,
+        }
+      );
 
 
-    // const res = await api.createAWSIntegration(
-    //   "token",
-    //   {
-    //     aws_region: awsRegion,
-    //     aws_access_key_id: accessId,
-    //     aws_secret_access_key: secretKey,
-    //   },
-    //   {
-    //     id: project.id,
-    //   }
-    // );
-
-    nextFormStep({
-      credentials: {
-        id: "some_id",
-      },
-    });
+      nextFormStep({
+        credentials: {
+          id: res.data?.id,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Something went wrong, please try again");
+    }
   };
   };
 
 
   return (
   return (
@@ -118,6 +123,7 @@ export const CredentialsForm: React.FC<{
         }}
         }}
         label="📍 AWS Region"
         label="📍 AWS Region"
       />
       />
+      <Br />
       <SaveButton
       <SaveButton
         text="Continue"
         text="Continue"
         disabled={false}
         disabled={false}
@@ -158,21 +164,25 @@ export const SettingsForm: React.FC<{
       setButtonStatus(validation.error);
       setButtonStatus(validation.error);
       return;
       return;
     }
     }
+    try {
+      await api.connectECRRegistry(
+        "<token>",
+        {
+          name: registryName,
+          aws_integration_id:
+            snap.StateHandler.connected_registry.credentials.id,
+        },
+        { id: project.id }
+      );
 
 
-    // await api.connectECRRegistry(
-    //   "<token>",
-    //   {
-    //     name: registryName,
-    //     aws_integration_id: snap.StateHandler.connected_registry.credentials.id,
-    //   },
-    //   { id: project.id }
-    // );
-
-    nextFormStep({
-      settings: {
-        registry_name: registryName,
-      },
-    });
+      nextFormStep({
+        settings: {
+          registry_name: registryName,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Couldn't connect registry.");
+    }
   };
   };
 
 
   return (
   return (
@@ -206,6 +216,7 @@ export const TestRegistryConnection: React.FC<{ nextFormStep: () => void }> = ({
 }) => {
 }) => {
   return (
   return (
     <>
     <>
+      <IntegrationCategories category={"registry"} />
       <SaveButton
       <SaveButton
         text="Continue"
         text="Continue"
         disabled={false}
         disabled={false}
@@ -218,3 +229,8 @@ export const TestRegistryConnection: React.FC<{ nextFormStep: () => void }> = ({
     </>
     </>
   );
   );
 };
 };
+
+const Br = styled.div`
+  width: 100%;
+  height: 10px;
+`;

+ 6 - 1
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_DORegistryForm.tsx

@@ -4,6 +4,7 @@ import SaveButton from "components/SaveButton";
 import { OFState } from "main/home/onboarding/state";
 import { OFState } from "main/home/onboarding/state";
 import { DORegistryConfig } from "main/home/onboarding/types";
 import { DORegistryConfig } from "main/home/onboarding/types";
 import React, { useEffect, useState } from "react";
 import React, { useEffect, useState } from "react";
+import { useLocation } from "react-router";
 import api from "shared/api";
 import api from "shared/api";
 import styled from "styled-components";
 import styled from "styled-components";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
@@ -20,6 +21,7 @@ export const CredentialsForm: React.FC<{
   nextFormStep: (data: Partial<DORegistryConfig>) => void;
   nextFormStep: (data: Partial<DORegistryConfig>) => void;
   project: any;
   project: any;
 }> = ({ nextFormStep, project }) => {
 }> = ({ nextFormStep, project }) => {
+  const location = useLocation();
   useEffect(() => {
   useEffect(() => {
     api.getOAuthIds("<token>", {}, { project_id: project?.id }).then((res) => {
     api.getOAuthIds("<token>", {}, { project_id: project?.id }).then((res) => {
       let tgtIntegration = res.data.find((integration: any) => {
       let tgtIntegration = res.data.find((integration: any) => {
@@ -36,11 +38,14 @@ export const CredentialsForm: React.FC<{
     });
     });
   }, []);
   }, []);
 
 
+  const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
+
+  const encoded_redirect_uri = encodeURIComponent(url);
   return (
   return (
     <>
     <>
       <ConnectDigitalOceanButton
       <ConnectDigitalOceanButton
         target={"_blank"}
         target={"_blank"}
-        href={`/api/projects/${project?.id}/oauth/digitalocean`}
+        href={`/api/projects/${project?.id}/oauth/digitalocean?redirect_uri=${encoded_redirect_uri}`}
       >
       >
         Connect Digital Ocean
         Connect Digital Ocean
       </ConnectDigitalOceanButton>
       </ConnectDigitalOceanButton>

+ 45 - 34
dashboard/src/main/home/onboarding/steps/ConnectRegistry/forms/_GCPRegistryForm.tsx

@@ -39,24 +39,29 @@ export const CredentialsForm: React.FC<{
       return;
       return;
     }
     }
     setButtonStatus("loading");
     setButtonStatus("loading");
-    // const gcpIntegration = await api
-    //   .createGCPIntegration(
-    //     "<token>",
-    //     {
-    //       gcp_region: "",
-    //       gcp_key_data: serviceAccountKey,
-    //       gcp_project_id: projectId,
-    //     },
-    //     { project_id: project.id }
-    //   )
-    //   .then((res) => res.data);
-
-    nextFormStep({
-      credentials: {
-        id: "some_Id",
-      },
-    });
+    try {
+      const gcpIntegration = await api
+        .createGCPIntegration(
+          "<token>",
+          {
+            gcp_region: "",
+            gcp_key_data: serviceAccountKey,
+            gcp_project_id: projectId,
+          },
+          { project_id: project.id }
+        )
+        .then((res) => res.data);
+
+      nextFormStep({
+        credentials: {
+          id: gcpIntegration?.id,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Something went wrong, please try again");
+    }
   };
   };
+
   return (
   return (
     <>
     <>
       <InputRow
       <InputRow
@@ -128,23 +133,29 @@ export const SettingsForm: React.FC<{
 
 
     setButtonStatus("loading");
     setButtonStatus("loading");
 
 
-    // await api.connectGCRRegistry(
-    //   "<token>",
-    //   {
-    //     name: registryName,
-    //     gcp_integration_id: snap.StateHandler.connected_registry.credentials.id,
-    //     url: registryUrl,
-    //   },
-    //   {
-    //     id: project.id,
-    //   }
-    // );
-    nextFormStep({
-      settings: {
-        gcr_url: registryUrl,
-        registry_name: registryName,
-      },
-    });
+    try {
+      await api.connectGCRRegistry(
+        "<token>",
+        {
+          name: registryName,
+          gcp_integration_id:
+            snap.StateHandler.connected_registry.credentials.id,
+          url: registryUrl,
+        },
+        {
+          id: project.id,
+        }
+      );
+
+      nextFormStep({
+        settings: {
+          gcr_url: registryUrl,
+          registry_name: registryName,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Couldn't connect registry.");
+    }
   };
   };
   return (
   return (
     <>
     <>

+ 7 - 1
dashboard/src/main/home/onboarding/steps/ConnectSource.tsx

@@ -63,6 +63,10 @@ const ConnectSource: React.FC<{
     onSuccess(selectedSource);
     onSuccess(selectedSource);
   };
   };
 
 
+  const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
+
+  const encoded_redirect_uri = encodeURIComponent(url);
+
   return (
   return (
     <div>
     <div>
       <FadeWrapper>
       <FadeWrapper>
@@ -78,7 +82,9 @@ const ConnectSource: React.FC<{
         </Helper>
         </Helper>
         {!isLoading && (!accountData || !accountData?.accounts?.length) && (
         {!isLoading && (!accountData || !accountData?.accounts?.length) && (
           <>
           <>
-            <ConnectToGithubButton href="/api/integrations/github-app/oauth">
+            <ConnectToGithubButton
+              href={`/api/integrations/github-app/install?redirect_uri=${encoded_redirect_uri}`}
+            >
               <GitHubIcon src={github} /> Connect to GitHub
               <GitHubIcon src={github} /> Connect to GitHub
             </ConnectToGithubButton>
             </ConnectToGithubButton>
             <Helper>
             <Helper>

+ 1 - 1
dashboard/src/main/home/onboarding/steps/NewProject.tsx

@@ -90,7 +90,7 @@ export const NewProjectFC: React.FC<{
         id: project.id,
         id: project.id,
         name: project.name,
         name: project.name,
       });
       });
-      setButtonStatus("success");
+      setButtonStatus("successful");
     } catch (error) {
     } catch (error) {
       setButtonStatus("Couldn't create project, try again.");
       setButtonStatus("Couldn't create project, try again.");
       console.log(error);
       console.log(error);

+ 43 - 3
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResources.tsx

@@ -9,9 +9,11 @@ import ProviderSelector from "../../components/ProviderSelector";
 import FormFlowWrapper from "./forms/FormFlow";
 import FormFlowWrapper from "./forms/FormFlow";
 import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
 import ConnectExternalCluster from "./forms/_ConnectExternalCluster";
 import { SupportedProviders } from "../../types";
 import { SupportedProviders } from "../../types";
+import backArrow from "assets/back_arrow.png";
 
 
 type Props = {
 type Props = {
   provider: SupportedProviders | "external";
   provider: SupportedProviders | "external";
+  enable_go_back: boolean;
   project: {
   project: {
     id: number;
     id: number;
     name: string;
     name: string;
@@ -22,6 +24,7 @@ type Props = {
   onSaveSettings: (settings: any) => void;
   onSaveSettings: (settings: any) => void;
   onSuccess: () => void;
   onSuccess: () => void;
   onSkip: () => void;
   onSkip: () => void;
+  goBack: () => void;
 };
 };
 
 
 const ProvisionResources: React.FC<Props> = ({
 const ProvisionResources: React.FC<Props> = ({
@@ -32,13 +35,25 @@ const ProvisionResources: React.FC<Props> = ({
   onSaveCredentials,
   onSaveCredentials,
   onSaveSettings,
   onSaveSettings,
   onSuccess,
   onSuccess,
+
+  enable_go_back,
+  goBack,
 }) => {
 }) => {
   const { step } = useParams<{ step: any }>();
   const { step } = useParams<{ step: any }>();
 
 
   return (
   return (
-    <>
+    <div>
+      {enable_go_back && (
+        <BackButton
+          onClick={() => {
+            goBack();
+          }}
+        >
+          <BackButtonImg src={backArrow} />
+        </BackButton>
+      )}
       <TitleSection>Getting Started</TitleSection>
       <TitleSection>Getting Started</TitleSection>
-      <Subtitle>Step 3 of 3</Subtitle>
+      <Subtitle>Step 3 of 3 - Provision resources</Subtitle>
       <Helper>
       <Helper>
         Porter automatically creates a cluster and registry in your cloud to run
         Porter automatically creates a cluster and registry in your cloud to run
         applications.
         applications.
@@ -65,7 +80,7 @@ const ProvisionResources: React.FC<Props> = ({
           />
           />
         </>
         </>
       )}
       )}
-    </>
+    </div>
   );
   );
 };
 };
 
 
@@ -79,3 +94,28 @@ const Subtitle = styled(TitleSection)`
 const NextStep = styled(SaveButton)`
 const NextStep = styled(SaveButton)`
   margin-top: 24px;
   margin-top: 24px;
 `;
 `;
+
+const BackButton = styled.div`
+  margin-bottom: 24px;
+  display: flex;
+  width: 36px;
+  cursor: pointer;
+  height: 36px;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid #ffffff55;
+  border-radius: 100px;
+  background: #ffffff11;
+
+  :hover {
+    background: #ffffff22;
+    > img {
+      opacity: 1;
+    }
+  }
+`;
+
+const BackButtonImg = styled.img`
+  width: 16px;
+  opacity: 0.75;
+`;

+ 2 - 0
dashboard/src/main/home/onboarding/steps/ProvisionResources/ProvisionResourcesWrapper.tsx

@@ -17,6 +17,8 @@ const ProvisionResourcesWrapper = () => {
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSaveSettings={(data) => OFState.actions.nextStep("continue", data)}
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSuccess={() => OFState.actions.nextStep("continue")}
       onSkip={() => OFState.actions.nextStep("skip")}
       onSkip={() => OFState.actions.nextStep("skip")}
+      enable_go_back={snap.StepHandler.canGoBack}
+      goBack={() => OFState.actions.nextStep("go_back")}
     />
     />
   );
   );
 };
 };

+ 20 - 17
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_AWSProvsionerForm.tsx

@@ -8,7 +8,6 @@ import {
 } from "main/home/onboarding/types";
 } from "main/home/onboarding/types";
 import React, { useState } from "react";
 import React, { useState } from "react";
 import api from "shared/api";
 import api from "shared/api";
-import { Context } from "shared/Context";
 import { useSnapshot } from "valtio";
 import { useSnapshot } from "valtio";
 
 
 const regionOptions = [
 const regionOptions = [
@@ -69,23 +68,27 @@ export const CredentialsForm: React.FC<{
       return;
       return;
     }
     }
 
 
-    // const res = await api.createAWSIntegration(
-    //   "token",
-    //   {
-    //     aws_region: awsRegion,
-    //     aws_access_key_id: accessId,
-    //     aws_secret_access_key: secretKey,
-    //   },
-    //   {
-    //     id: project.id,
-    //   }
-    // );
+    try {
+      const res = await api.createAWSIntegration(
+        "token",
+        {
+          aws_region: awsRegion,
+          aws_access_key_id: accessId,
+          aws_secret_access_key: secretKey,
+        },
+        {
+          id: project.id,
+        }
+      );
 
 
-    nextFormStep({
-      credentials: {
-        id: "res.data.id",
-      },
-    });
+      nextFormStep({
+        credentials: {
+          id: res.data?.id,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Something went wrong, please try again");
+    }
   };
   };
 
 
   return (
   return (

+ 20 - 16
dashboard/src/main/home/onboarding/steps/ProvisionResources/forms/_GCPProvisionerForm.tsx

@@ -71,23 +71,27 @@ export const CredentialsForm: React.FC<{
       return;
       return;
     }
     }
     setButtonStatus("loading");
     setButtonStatus("loading");
-    // const gcpIntegration = await api
-    //   .createGCPIntegration(
-    //     "<token>",
-    //     {
-    //       gcp_region: "",
-    //       gcp_key_data: serviceAccountKey,
-    //       gcp_project_id: projectId,
-    //     },
-    //     { project_id: project.id }
-    //   )
-    //   .then((res) => res.data);
+    try {
+      const gcpIntegration = await api
+        .createGCPIntegration(
+          "<token>",
+          {
+            gcp_region: region,
+            gcp_key_data: serviceAccountKey,
+            gcp_project_id: projectId,
+          },
+          { project_id: project.id }
+        )
+        .then((res) => res.data);
 
 
-    nextFormStep({
-      credentials: {
-        id: "gcpIntegration.id",
-      },
-    });
+      nextFormStep({
+        credentials: {
+          id: gcpIntegration?.id,
+        },
+      });
+    } catch (error) {
+      setButtonStatus("Something went wrong, please try again");
+    }
   };
   };
   return (
   return (
     <>
     <>

+ 32 - 0
dashboard/src/shared/api.tsx

@@ -1071,6 +1071,34 @@ const getHasBilling = baseApi<{}, { project_id: number }>(
   ({ project_id }) => `/api/projects/${project_id}/billing`
   ({ project_id }) => `/api/projects/${project_id}/billing`
 );
 );
 
 
+const getOnboardingState = baseApi<{}, { project_id: number }>(
+  "GET",
+  ({ project_id }) => `/api/projects/${project_id}/onboarding`
+);
+
+const saveOnboardingState = baseApi<{}, { project_id: number }>(
+  "POST",
+  ({ project_id }) => `/api/projects/${project_id}/onboarding`
+);
+
+const getOnboardingInfra = baseApi<
+  {},
+  { project_id: number; registry_infra_id: number }
+>(
+  "GET",
+  ({ project_id, registry_infra_id }) =>
+    `/api/projects/${project_id}/infras/${registry_infra_id}`
+);
+
+const getOnboardingRegistry = baseApi<
+  {},
+  { project_id: number; registry_connection_id: number }
+>(
+  "GET",
+  ({ project_id, registry_connection_id }) =>
+    `/api/projects/${project_id}/registries/${registry_connection_id}`
+);
+
 // Bundle export to allow default api import (api.<method> is more readable)
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
 export default {
   checkAuth,
   checkAuth,
@@ -1180,4 +1208,8 @@ export default {
   getUsage,
   getUsage,
   getCustomerToken,
   getCustomerToken,
   getHasBilling,
   getHasBilling,
+  getOnboardingState,
+  saveOnboardingState,
+  getOnboardingInfra,
+  getOnboardingRegistry,
 };
 };

+ 1 - 0
internal/kubernetes/provisioner/input/eks.go

@@ -9,6 +9,7 @@ type EKS struct {
 	AWSAccessKey string `json:"aws_access_key"`
 	AWSAccessKey string `json:"aws_access_key"`
 	AWSSecretKey string `json:"aws_secret_key"`
 	AWSSecretKey string `json:"aws_secret_key"`
 	ClusterName  string `json:"cluster_name"`
 	ClusterName  string `json:"cluster_name"`
+	MachineType  string `json:"machine_type"`
 }
 }
 
 
 func (eks *EKS) GetInput() ([]byte, error) {
 func (eks *EKS) GetInput() ([]byte, error) {

+ 1 - 0
internal/kubernetes/provisioner/provisioner.go

@@ -150,6 +150,7 @@ func (conf *Conf) GetProvisionerJobTemplate() (*batchv1.Job, error) {
 				AWSAccessKey: conf.AWS.AWSAccessKeyID,
 				AWSAccessKey: conf.AWS.AWSAccessKeyID,
 				AWSSecretKey: conf.AWS.AWSSecretAccessKey,
 				AWSSecretKey: conf.AWS.AWSSecretAccessKey,
 				ClusterName:  conf.EKS.ClusterName,
 				ClusterName:  conf.EKS.ClusterName,
+				MachineType:  conf.EKS.MachineType,
 			}
 			}
 
 
 			lastApplied, err := inputConf.GetInput()
 			lastApplied, err := inputConf.GetInput()

+ 87 - 4
internal/models/infra.go

@@ -1,6 +1,7 @@
 package models
 package models
 
 
 import (
 import (
+	"encoding/json"
 	"fmt"
 	"fmt"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
@@ -8,6 +9,7 @@ import (
 	"gorm.io/gorm"
 	"gorm.io/gorm"
 
 
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/kubernetes/provisioner/input"
 )
 )
 
 
 // Infra represents the metadata for an infrastructure type provisioned on
 // Infra represents the metadata for an infrastructure type provisioned on
@@ -51,13 +53,94 @@ type Infra struct {
 // ToInfraType generates an external Infra to be shared over REST
 // ToInfraType generates an external Infra to be shared over REST
 func (i *Infra) ToInfraType() *types.Infra {
 func (i *Infra) ToInfraType() *types.Infra {
 	return &types.Infra{
 	return &types.Infra{
-		ID:        i.ID,
-		ProjectID: i.ProjectID,
-		Kind:      i.Kind,
-		Status:    i.Status,
+		ID:               i.ID,
+		ProjectID:        i.ProjectID,
+		Kind:             i.Kind,
+		Status:           i.Status,
+		AWSIntegrationID: i.AWSIntegrationID,
+		DOIntegrationID:  i.DOIntegrationID,
+		GCPIntegrationID: i.GCPIntegrationID,
+		LastApplied:      i.SafelyGetLastApplied(),
 	}
 	}
 }
 }
 
 
+// SafeGetLastApplied gets non-sensitive values for the last applied configuration
+func (i *Infra) SafelyGetLastApplied() map[string]string {
+	resp := make(map[string]string)
+
+	switch i.Kind {
+	case types.InfraECR:
+		lastApplied := &input.ECR{}
+
+		if err := json.Unmarshal(i.LastApplied, lastApplied); err != nil {
+			return resp
+		}
+
+		resp["region"] = lastApplied.AWSRegion
+
+		return resp
+	case types.InfraEKS:
+		lastApplied := &input.EKS{}
+
+		if err := json.Unmarshal(i.LastApplied, lastApplied); err != nil {
+			return resp
+		}
+
+		resp["region"] = lastApplied.AWSRegion
+		resp["cluster_name"] = lastApplied.ClusterName
+		resp["machine_type"] = lastApplied.MachineType
+
+		return resp
+	case types.InfraGCR:
+		lastApplied := &input.GCR{}
+
+		if err := json.Unmarshal(i.LastApplied, lastApplied); err != nil {
+			return resp
+		}
+
+		resp["project_id"] = lastApplied.GCPProjectID
+		resp["region"] = lastApplied.GCPRegion
+
+		return resp
+	case types.InfraGKE:
+		lastApplied := &input.GKE{}
+
+		if err := json.Unmarshal(i.LastApplied, lastApplied); err != nil {
+			return resp
+		}
+
+		resp["project_id"] = lastApplied.GCPProjectID
+		resp["region"] = lastApplied.GCPRegion
+		resp["cluster_name"] = lastApplied.ClusterName
+
+		return resp
+	case types.InfraDOCR:
+		lastApplied := &input.DOCR{}
+
+		if err := json.Unmarshal(i.LastApplied, lastApplied); err != nil {
+			return resp
+		}
+
+		resp["name"] = lastApplied.DOCRName
+		resp["subscription_tier"] = lastApplied.DOCRSubscriptionTier
+
+		return resp
+	case types.InfraDOKS:
+		lastApplied := &input.DOKS{}
+
+		if err := json.Unmarshal(i.LastApplied, lastApplied); err != nil {
+			return resp
+		}
+
+		resp["cluster_name"] = lastApplied.ClusterName
+		resp["region"] = lastApplied.DORegion
+
+		return resp
+	}
+
+	return resp
+}
+
 // GetID returns the unique id for this infra
 // GetID returns the unique id for this infra
 func (i *Infra) GetUniqueName() string {
 func (i *Infra) GetUniqueName() string {
 	return fmt.Sprintf("%s-%d-%d-%s", i.Kind, i.ProjectID, i.ID, i.Suffix)
 	return fmt.Sprintf("%s-%d-%d-%s", i.Kind, i.ProjectID, i.ID, i.Suffix)

+ 1 - 0
internal/models/integrations/aws.go

@@ -49,6 +49,7 @@ type AWSIntegration struct {
 
 
 func (a *AWSIntegration) ToAWSIntegrationType() *types.AWSIntegration {
 func (a *AWSIntegration) ToAWSIntegrationType() *types.AWSIntegration {
 	return &types.AWSIntegration{
 	return &types.AWSIntegration{
+		CreatedAt: a.CreatedAt,
 		ID:        a.ID,
 		ID:        a.ID,
 		UserID:    a.UserID,
 		UserID:    a.UserID,
 		ProjectID: a.ProjectID,
 		ProjectID: a.ProjectID,

+ 17 - 1
internal/models/integrations/gcp.go

@@ -25,6 +25,9 @@ type GCPIntegration struct {
 	// The GCP project id where the service account for this auth mechanism persists
 	// The GCP project id where the service account for this auth mechanism persists
 	GCPProjectID string `json:"gcp_project_id"`
 	GCPProjectID string `json:"gcp_project_id"`
 
 
+	// The GCP service account email for this credential
+	GCPSAEmail string `json:"gcp_sa_email"`
+
 	// The GCP user email that linked this service account
 	// The GCP user email that linked this service account
 	GCPUserEmail string `json:"gcp-user-email"`
 	GCPUserEmail string `json:"gcp-user-email"`
 
 
@@ -45,7 +48,7 @@ func (g *GCPIntegration) ToGCPIntegrationType() *types.GCPIntegration {
 		UserID:       g.UserID,
 		UserID:       g.UserID,
 		ProjectID:    g.ProjectID,
 		ProjectID:    g.ProjectID,
 		GCPProjectID: g.GCPProjectID,
 		GCPProjectID: g.GCPProjectID,
-		GCPUserEmail: g.GCPUserEmail,
+		GCPSAEmail:   g.GCPSAEmail,
 	}
 	}
 }
 }
 
 
@@ -125,3 +128,16 @@ func GCPProjectIDFromJSON(jsonData []byte) (string, error) {
 
 
 	return f.ProjectID, nil
 	return f.ProjectID, nil
 }
 }
+
+// PopulateGCPMetadata uses the credentials file to get the GCP SA name and
+// project ID, and attaches it to the GCP credential
+func (g *GCPIntegration) PopulateGCPMetadata() {
+	var f credentialsFile
+
+	if err := json.Unmarshal(g.GCPKeyData, &f); err != nil {
+		return
+	}
+
+	g.GCPProjectID = f.ProjectID
+	g.GCPSAEmail = f.ClientEmail
+}

+ 1 - 0
internal/models/integrations/oauth.go

@@ -56,6 +56,7 @@ type GithubAppOAuthIntegration struct {
 // ToOAuthIntegrationType generates an external OAuthIntegration to be shared over REST
 // ToOAuthIntegrationType generates an external OAuthIntegration to be shared over REST
 func (o *OAuthIntegration) ToOAuthIntegrationType() *types.OAuthIntegration {
 func (o *OAuthIntegration) ToOAuthIntegrationType() *types.OAuthIntegration {
 	return &types.OAuthIntegration{
 	return &types.OAuthIntegration{
+		CreatedAt: o.CreatedAt,
 		ID:        o.ID,
 		ID:        o.ID,
 		Client:    o.Client,
 		Client:    o.Client,
 		UserID:    o.UserID,
 		UserID:    o.UserID,

+ 21 - 15
internal/models/onboarding.go

@@ -9,25 +9,31 @@ import (
 type Onboarding struct {
 type Onboarding struct {
 	gorm.Model
 	gorm.Model
 
 
-	ProjectID              uint
-	CurrentStep            types.StepEnum
-	ConnectedSource        types.ConnectedSourceType
-	SkipRegistryConnection bool
-	SkipResourceProvision  bool
-	RegistryConnectionID   uint
-	RegistryInfraID        uint
-	ClusterInfraID         uint
+	ProjectID                      uint
+	CurrentStep                    types.StepEnum
+	ConnectedSource                types.ConnectedSourceType
+	SkipRegistryConnection         bool
+	SkipResourceProvision          bool
+	RegistryConnectionID           uint
+	RegistryConnectionCredentialID uint
+	RegistryInfraID                uint
+	RegistryInfraCredentialID      uint
+	ClusterInfraID                 uint
+	ClusterInfraCredentialID       uint
 }
 }
 
 
 // ToOnboardingType generates an external types.OnboardingData to be shared over REST
 // ToOnboardingType generates an external types.OnboardingData to be shared over REST
 func (o *Onboarding) ToOnboardingType() *types.OnboardingData {
 func (o *Onboarding) ToOnboardingType() *types.OnboardingData {
 	return &types.OnboardingData{
 	return &types.OnboardingData{
-		CurrentStep:            o.CurrentStep,
-		ConnectedSource:        o.ConnectedSource,
-		SkipRegistryConnection: o.SkipRegistryConnection,
-		SkipResourceProvision:  o.SkipResourceProvision,
-		RegistryConnectionID:   o.RegistryConnectionID,
-		RegistryInfraID:        o.RegistryInfraID,
-		ClusterInfraID:         o.ClusterInfraID,
+		CurrentStep:                    o.CurrentStep,
+		ConnectedSource:                o.ConnectedSource,
+		SkipRegistryConnection:         o.SkipRegistryConnection,
+		SkipResourceProvision:          o.SkipResourceProvision,
+		RegistryConnectionID:           o.RegistryConnectionID,
+		RegistryConnectionCredentialID: o.RegistryConnectionCredentialID,
+		RegistryInfraID:                o.RegistryInfraID,
+		RegistryInfraCredentialID:      o.RegistryInfraCredentialID,
+		ClusterInfraID:                 o.ClusterInfraID,
+		ClusterInfraCredentialID:       o.ClusterInfraCredentialID,
 	}
 	}
 }
 }

+ 10 - 6
internal/models/registry.go

@@ -59,11 +59,15 @@ func (r *Registry) ToRegistryType() *types.Registry {
 	}
 	}
 
 
 	return &types.Registry{
 	return &types.Registry{
-		ID:        r.ID,
-		ProjectID: r.ProjectID,
-		Name:      r.Name,
-		URL:       uri,
-		Service:   serv,
-		InfraID:   r.InfraID,
+		ID:                 r.ID,
+		ProjectID:          r.ProjectID,
+		Name:               r.Name,
+		URL:                uri,
+		Service:            serv,
+		InfraID:            r.InfraID,
+		GCPIntegrationID:   r.GCPIntegrationID,
+		AWSIntegrationID:   r.AWSIntegrationID,
+		DOIntegrationID:    r.DOIntegrationID,
+		BasicIntegrationID: r.BasicIntegrationID,
 	}
 	}
 }
 }