Browse Source

Merge branch 'master' of github.com:porter-dev/porter into stacks-improved-debug-flow-pre-deploy

Feroze Mohideen 2 years ago
parent
commit
c6805b8dbb
28 changed files with 854 additions and 539 deletions
  1. 7 0
      Tiltfile
  2. 14 12
      api/server/handlers/api_contract/update.go
  3. 33 14
      api/server/handlers/environment/list_deployments_by_cluster.go
  4. 7 0
      api/server/handlers/environment/update_environment_settings.go
  5. 21 4
      api/server/handlers/porter_app/create.go
  6. 49 8
      api/server/handlers/porter_app/parse.go
  7. 7 7
      dashboard/package-lock.json
  8. 1 1
      dashboard/package.json
  9. 77 1
      dashboard/src/components/ProvisionerSettings.tsx
  10. 6 6
      dashboard/src/main/home/app-dashboard/build-settings/AdvancedBuildSettings.tsx
  11. 7 1
      dashboard/src/main/home/app-dashboard/build-settings/BuildSettingsTab.tsx
  12. 3 0
      dashboard/src/main/home/app-dashboard/build-settings/DetectDockerfileAndPorterYaml.tsx
  13. 8 1
      dashboard/src/main/home/app-dashboard/build-settings/SharedBuildSettings.tsx
  14. 145 0
      dashboard/src/main/home/app-dashboard/build-settings/buildpacks/BuildpackConfigurationModal.tsx
  15. 26 29
      dashboard/src/main/home/app-dashboard/build-settings/buildpacks/BuildpackList.tsx
  16. 49 156
      dashboard/src/main/home/app-dashboard/build-settings/buildpacks/BuildpackSettings.tsx
  17. 39 152
      dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx
  18. 28 17
      dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx
  19. 7 1
      dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx
  20. 89 39
      dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx
  21. 34 19
      dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts
  22. 3 1
      dashboard/src/main/home/app-dashboard/types/porterApp.ts
  23. 102 3
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx
  24. 57 57
      dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx
  25. 32 6
      dashboard/src/main/home/sidebar/Clusters.tsx
  26. 0 1
      dashboard/src/main/home/sidebar/Sidebar.tsx
  27. 1 1
      go.mod
  28. 2 2
      go.sum

+ 7 - 0
Tiltfile

@@ -57,6 +57,7 @@ local_resource(
     "ee",
     "internal",
     "pkg",
+    "vendor",
   ],
   resource_deps=["postgresql"],
   labels=["z_binaries"]
@@ -69,6 +70,12 @@ local_resource(
     labels=["z_binaries"],
 )
 
+local_resource(
+    name="disable-porter-helm-test",
+    cmd='tilt disable porter-server-web-test-connection',
+    resource_deps=["porter-server-web"]
+)
+
 docker_build_with_restart(
     ref="porter1/porter-server",
     context=".",

+ 14 - 12
api/server/handlers/api_contract/update.go

@@ -2,7 +2,6 @@ package api_contract
 
 import (
 	"encoding/base64"
-	"fmt"
 	"net/http"
 
 	"github.com/bufbuild/connect-go"
@@ -15,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 
 type APIContractUpdateHandler struct {
@@ -34,7 +34,9 @@ func NewAPIContractUpdateHandler(
 // ServeHTTP parses the Porter API contract for validity, and forwards the requests for handling on to another service
 // For now, this handling cluster creation only, by inserting a row into the cluster table in order to create an ID for this cluster, as well as stores the raw request JSON for updating later
 func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	ctx := r.Context()
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-update-api-contract")
+	defer span.End()
+
 	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
 	user, _ := ctx.Value(types.UserScope).(*models.User)
 
@@ -42,8 +44,8 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 	err := helpers.UnmarshalContractObjectFromReader(r.Body, &apiContract)
 	if err != nil {
-		e := fmt.Errorf("error parsing api contract: %w", err)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+		e := telemetry.Error(ctx, span, err, "error parsing api contract")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusBadRequest))
 		return
 	}
 
@@ -63,8 +65,8 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 			}
 			dbcl, err := c.Config().Repo.Cluster().CreateCluster(&dbcli)
 			if err != nil {
-				e := fmt.Errorf("error updating mock contract: %w", err)
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+				e := telemetry.Error(ctx, span, err, "error updating mocking contract")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
 				return
 			}
 			clusterID = int32(dbcl.ID)
@@ -72,8 +74,8 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 
 		by, err := helpers.MarshalContractObject(ctx, &apiContract)
 		if err != nil {
-			e := fmt.Errorf("error marshalling mock api contract: %w", err)
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			e := telemetry.Error(ctx, span, err, "error marshalling mock api contract")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
 			return
 		}
 		b64Contract := base64.StdEncoding.EncodeToString([]byte(by))
@@ -86,8 +88,8 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 		}
 		revision, err := c.Config().Repo.APIContractRevisioner().Insert(ctx, revisionInput)
 		if err != nil {
-			e := fmt.Errorf("error updating mock contract: %w", err)
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+			e := telemetry.Error(ctx, span, err, "error updating mock api contract")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
 			return
 		}
 		resp := &porterv1.ContractRevision{
@@ -108,8 +110,8 @@ func (c *APIContractUpdateHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ
 	})
 	revision, err := c.Config().ClusterControlPlaneClient.UpdateContract(ctx, updateRequest)
 	if err != nil {
-		e := fmt.Errorf("error sending contract for update: %w", err)
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(e))
+		e := telemetry.Error(ctx, span, err, "error sending contract for update")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(e, http.StatusInternalServerError))
 		return
 	}
 

+ 33 - 14
api/server/handlers/environment/list_deployments_by_cluster.go

@@ -14,6 +14,7 @@ import (
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/telemetry"
 )
 
 type ListDeploymentsByClusterHandler struct {
@@ -31,10 +32,18 @@ func NewListDeploymentsByClusterHandler(
 }
 
 func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-list-cluster-deployments")
+	defer span.End()
+
+	project, _ := ctx.Value(types.ProjectScope).(*models.Project)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
 	req := &types.ListDeploymentRequest{}
+	telemetry.WithAttributes(span,
+		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
+		telemetry.AttributeKV{Key: "project-id", Value: project.ID},
+		telemetry.AttributeKV{Key: "environment-id", Value: req.EnvironmentID},
+	)
 
 	if ok := c.DecodeAndValidate(w, r, req); !ok {
 		return
@@ -46,7 +55,8 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 	if req.EnvironmentID == 0 {
 		depls, err := c.Repo().Environment().ListDeploymentsByCluster(project.ID, cluster.ID)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			err = telemetry.Error(ctx, span, err, "failed to list deployments from cluster")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
 
@@ -60,7 +70,8 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 
 			env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, deployment.EnvironmentID)
 			if err != nil {
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				err = telemetry.Error(ctx, span, err, "failed to get environment from deployment")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 				return
 			}
 
@@ -77,14 +88,16 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 		for _, deployment := range deployments {
 			env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, deployment.EnvironmentID)
 			if err != nil {
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				err = telemetry.Error(ctx, span, err, "failed to get environment from deployment")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 				return
 			}
 
 			if _, ok := envToGithubClientMap[env.ID]; !ok {
 				client, err := getGithubClientFromEnvironment(c.Config(), env)
 				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+					err = telemetry.Error(ctx, span, err, "error getting github client from environment")
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 					return
 				}
 
@@ -110,16 +123,18 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 			if _, ok := envToGithubClientMap[env.ID]; !ok {
 				client, err := getGithubClientFromEnvironment(c.Config(), env)
 				if err != nil {
-					c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+					err = telemetry.Error(ctx, span, err, "error getting github client from environment")
+					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 					return
 				}
 
 				envToGithubClientMap[env.ID] = client
 			}
 
-			prs, err := fetchOpenPullRequests(r.Context(), c.Config(), envToGithubClientMap[env.ID], env, deplInfoMap)
+			prs, err := fetchOpenPullRequests(ctx, c.Config(), envToGithubClientMap[env.ID], env, deplInfoMap)
 			if err != nil {
-				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+				err = telemetry.Error(ctx, span, err, "error fetching pull requests")
+				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 				return
 			}
 
@@ -128,13 +143,15 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 	} else {
 		env, err := c.Repo().Environment().ReadEnvironmentByID(project.ID, cluster.ID, req.EnvironmentID)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			err = telemetry.Error(ctx, span, err, "error fetching environment")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
 
 		depls, err := c.Repo().Environment().ListDeployments(env.ID)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			err = telemetry.Error(ctx, span, err, "error listing deployments")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
 
@@ -142,7 +159,8 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 
 		client, err := getGithubClientFromEnvironment(c.Config(), env)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			err = telemetry.Error(ctx, span, err, "error getting github client from environment")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
 
@@ -170,9 +188,10 @@ func (c *ListDeploymentsByClusterHandler) ServeHTTP(w http.ResponseWriter, r *ht
 
 		wg.Wait()
 
-		prs, err := fetchOpenPullRequests(r.Context(), c.Config(), client, env, deplInfoMap)
+		prs, err := fetchOpenPullRequests(ctx, c.Config(), client, env, deplInfoMap)
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			err = telemetry.Error(ctx, span, err, "error fetching pull requests")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 			return
 		}
 

+ 7 - 0
api/server/handlers/environment/update_environment_settings.go

@@ -145,7 +145,14 @@ func (c *UpdateEnvironmentSettingsHandler) ServeHTTP(w http.ResponseWriter, r *h
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "found", Value: found})
 
 		if !found {
+			webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(env.WebhookID))
+
 			hook.Events = append(hook.Events, "push")
+			hook.Config = map[string]interface{}{
+				"url":          webhookURL,
+				"content_type": "json",
+				"secret":       c.Config().ServerConf.GithubIncomingWebhookSecret,
+			}
 
 			_, _, err := client.Repositories.EditHook(
 				context.Background(), env.GitRepoOwner, env.GitRepoName, env.GithubWebhookID, hook,

+ 21 - 4
api/server/handlers/porter_app/create.go

@@ -140,6 +140,7 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 			stackName:      stackName,
 		},
 		injectLauncher,
+		shouldCreate,
 	)
 	if err != nil {
 		err = telemetry.Error(ctx, span, err, "error parsing porter yaml into chart and values")
@@ -376,11 +377,27 @@ func (c *CreatePorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques
 		if request.BuildContext != "" {
 			app.BuildContext = request.BuildContext
 		}
-		// handles deletion of builder and buildpacks
-		app.Builder = request.Builder
-		app.Buildpacks = request.Buildpacks
+		// handles deletion of builder,buildpacks, and dockerfile path
+		if request.Builder != "" {
+			if request.Builder == "null" {
+				app.Builder = ""
+			} else {
+				app.Builder = request.Builder
+			}
+		}
+		if request.Buildpacks != "" {
+			if request.Buildpacks == "null" {
+				app.Buildpacks = ""
+			} else {
+				app.Buildpacks = request.Buildpacks
+			}
+		}
 		if request.Dockerfile != "" {
-			app.Dockerfile = request.Dockerfile
+			if request.Dockerfile == "null" {
+				app.Dockerfile = ""
+			} else {
+				app.Dockerfile = request.Dockerfile
+			}
 		}
 		if request.ImageRepoURI != "" {
 			app.ImageRepoURI = request.ImageRepoURI

+ 49 - 8
api/server/handlers/porter_app/parse.go

@@ -2,6 +2,7 @@ package porter_app
 
 import (
 	"fmt"
+	"strconv"
 	"strings"
 
 	"github.com/porter-dev/porter/api/server/shared/config"
@@ -57,6 +58,7 @@ func parse(
 	existingDependencies []*chart.Dependency,
 	opts SubdomainCreateOpts,
 	injectLauncher bool,
+	shouldCreate bool,
 ) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
 	parsed := &PorterStackYAML{}
 
@@ -65,27 +67,34 @@ func parse(
 		return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
 	}
 
-	values, err := buildStackValues(parsed, imageInfo, existingValues, opts, injectLauncher)
+	values, err := buildUmbrellaChartValues(parsed, imageInfo, existingValues, opts, injectLauncher, shouldCreate)
 	if err != nil {
 		return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
 	}
 	convertedValues := convertMap(values).(map[string]interface{})
 
-	chart, err := buildStackChart(parsed, config, projectID, existingDependencies)
+	chart, err := buildUmbrellaChart(parsed, config, projectID, existingDependencies)
 	if err != nil {
 		return nil, nil, nil, fmt.Errorf("%s: %w", "error building chart from porter.yaml", err)
 	}
 
 	// return the parsed release values for the release job chart, if they exist
-	var releaseJobValues map[string]interface{}
+	var preDeployJobValues map[string]interface{}
 	if parsed.Release != nil && parsed.Release.Run != nil {
-		releaseJobValues = buildReleaseValues(parsed.Release, parsed.Env, imageInfo, injectLauncher)
+		preDeployJobValues = buildPreDeployJobChartValues(parsed.Release, parsed.Env, imageInfo, injectLauncher)
 	}
 
-	return chart, convertedValues, releaseJobValues, nil
+	return chart, convertedValues, preDeployJobValues, nil
 }
 
-func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existingValues map[string]interface{}, opts SubdomainCreateOpts, injectLauncher bool) (map[string]interface{}, error) {
+func buildUmbrellaChartValues(
+	parsed *PorterStackYAML,
+	imageInfo types.ImageInfo,
+	existingValues map[string]interface{},
+	opts SubdomainCreateOpts,
+	injectLauncher bool,
+	shouldCreate bool,
+) (map[string]interface{}, error) {
 	values := make(map[string]interface{})
 
 	if parsed.Apps == nil {
@@ -109,6 +118,11 @@ func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existi
 			}
 		}
 
+		validateErr := validateHelmValues(helm_values, shouldCreate)
+		if validateErr != "" {
+			return nil, fmt.Errorf("error validating service \"%s\": %s", name, validateErr)
+		}
+
 		err := createSubdomainIfRequired(helm_values, opts) // modifies helm_values to add subdomains if necessary
 		if err != nil {
 			return nil, err
@@ -160,7 +174,34 @@ func buildStackValues(parsed *PorterStackYAML, imageInfo types.ImageInfo, existi
 	return values, nil
 }
 
-func buildReleaseValues(release *App, env map[string]string, imageInfo types.ImageInfo, injectLauncher bool) map[string]interface{} {
+// we can add to this function up later or use an alternative
+func validateHelmValues(values map[string]interface{}, shouldCreate bool) string {
+	// currently, we only validate port on initial app create, because this will break any updates to existing apps with lower port numbers
+	if shouldCreate {
+		containerMap, err := getNestedMap(values, "container")
+		if err != nil {
+			return "error checking port: misformatted values"
+		} else {
+			portVal, portExists := containerMap["port"]
+			if portExists {
+				portStr, pOK := portVal.(string)
+				if !pOK {
+					return "error checking port: no port in container"
+				}
+
+				port, err := strconv.Atoi(portStr)
+				if err != nil || port < 1024 || port > 65535 {
+					return "port must be a number between 1024 and 65535"
+				}
+			} else {
+				return "must specify port if choosing to expose service externally"
+			}
+		}
+	}
+	return ""
+}
+
+func buildPreDeployJobChartValues(release *App, env map[string]string, imageInfo types.ImageInfo, injectLauncher bool) map[string]interface{} {
 	defaultValues := getDefaultValues(release, env, "job")
 	convertedConfig := convertMap(release.Config).(map[string]interface{})
 	helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
@@ -214,7 +255,7 @@ func getDefaultValues(app *App, env map[string]string, appType string) map[strin
 	return defaultValues
 }
 
-func buildStackChart(parsed *PorterStackYAML, config *config.Config, projectID uint, existingDependencies []*chart.Dependency) (*chart.Chart, error) {
+func buildUmbrellaChart(parsed *PorterStackYAML, config *config.Config, projectID uint, existingDependencies []*chart.Dependency) (*chart.Chart, error) {
 	deps := make([]*chart.Dependency, 0)
 	for alias, app := range parsed.Apps {
 		var appType string

+ 7 - 7
dashboard/package-lock.json

@@ -12,7 +12,7 @@
         "@loadable/component": "^5.15.2",
         "@material-ui/core": "^4.11.3",
         "@material-ui/lab": "^4.0.0-alpha.61",
-        "@porter-dev/api-contracts": "^0.0.68",
+        "@porter-dev/api-contracts": "^0.0.70",
         "@sentry/react": "^6.13.2",
         "@sentry/tracing": "^6.13.2",
         "@tanstack/react-query": "^4.13.0",
@@ -2434,9 +2434,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.0.68",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.68.tgz",
-      "integrity": "sha512-T4lsYOn8ympv5OrY6CkXDR3W9/J4UaOTrDjdnaVkMmQK8P0V71CvTRP6S66BCeYqxV7yDQ+3IMRTeDv5Ov/OtQ==",
+      "version": "0.0.70",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.70.tgz",
+      "integrity": "sha512-JOaxn7ihyAQgikbq/4AIQmPoJqR6y+NxBlOtTGyFN/80hUegkWwIy4zCTVj8fqcvUoE0yWaROVFl9rZ7ofRIgg==",
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
       }
@@ -16671,9 +16671,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.0.68",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.68.tgz",
-      "integrity": "sha512-T4lsYOn8ympv5OrY6CkXDR3W9/J4UaOTrDjdnaVkMmQK8P0V71CvTRP6S66BCeYqxV7yDQ+3IMRTeDv5Ov/OtQ==",
+      "version": "0.0.70",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.0.70.tgz",
+      "integrity": "sha512-JOaxn7ihyAQgikbq/4AIQmPoJqR6y+NxBlOtTGyFN/80hUegkWwIy4zCTVj8fqcvUoE0yWaROVFl9rZ7ofRIgg==",
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"
       }

+ 1 - 1
dashboard/package.json

@@ -7,7 +7,7 @@
     "@loadable/component": "^5.15.2",
     "@material-ui/core": "^4.11.3",
     "@material-ui/lab": "^4.0.0-alpha.61",
-    "@porter-dev/api-contracts": "^0.0.68",
+    "@porter-dev/api-contracts": "^0.0.70",
     "@sentry/react": "^6.13.2",
     "@sentry/tracing": "^6.13.2",
     "@tanstack/react-query": "^4.13.0",

+ 77 - 1
dashboard/src/components/ProvisionerSettings.tsx

@@ -22,6 +22,7 @@ import {
   Cluster,
   LoadBalancer,
   LoadBalancerType,
+  EKSLogging
 } from "@porter-dev/api-contracts";
 import { ClusterType } from "shared/types";
 import Button from "./porter/Button";
@@ -94,9 +95,11 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
   const [clusterName, setClusterName] = useState("");
   const [awsRegion, setAwsRegion] = useState("us-east-1");
   const [machineType, setMachineType] = useState("t3.xlarge");
+  const [guardDutyEnabled, setGuardDutyEnabled] = useState<boolean>(false)
   const [loadBalancerType, setLoadBalancerType] = useState(false);
   const [wildCardDomain, setWildCardDomain] = useState("")
   const [IPAllowList, setIPAllowList] = useState<string>("")
+  const [controlPlaneLogs, setControlPlaneLogs] = useState<EKSLogging>(new EKSLogging())
   //const [accessS3Logs, setAccessS3Logs] = useState<boolean>(false)
   const [wafV2Enabled, setWaf2Enabled] = useState<boolean>(false)
   const [awsTags, setAwsTags] = useState<string>("")
@@ -108,7 +111,6 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
   const [additionalNodePolicies, setAdditionalNodePolicies] = useState<string[]>([]);
   const [cidrRange, setCidrRange] = useState("10.78.0.0/16");
   const [clusterVersion, setClusterVersion] = useState("v1.24.0");
-  const [loadBalancer, setLoadBalancer] = useState<LoadBalancer | undefined>();
   const [isReadOnly, setIsReadOnly] = useState(false);
   const [errorMessage, setErrorMessage] = useState<string>(undefined);
   const [isClicked, setIsClicked] = useState(false);
@@ -258,6 +260,8 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
             cidrRange: cidrRange || "10.78.0.0/16",
             region: awsRegion,
             loadBalancer: loadBalancerObj,
+            logging: controlPlaneLogs,
+            enableGuardDuty: guardDutyEnabled,
             nodeGroups: [
               new EKSNodeGroup({
                 instanceType: "t3.medium",
@@ -416,6 +420,17 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
         setwafV2ARN(eksValues.loadBalancer.wafv2Arn)
         setWaf2Enabled(eksValues.loadBalancer.enableWafv2)
       }
+
+      if (eksValues.logging != null) {
+        const l = new EKSLogging();
+        l.enableApiServerLogs = eksValues.logging.enableApiServerLogs;
+        l.enableAuditLogs = eksValues.logging.enableAuditLogs;
+        l.enableAuthenticatorLogs = eksValues.logging.enableAuthenticatorLogs;
+        l.enableControllerManagerLogs = eksValues.logging.enableControllerManagerLogs;
+        l.enableSchedulerLogs = eksValues.logging.enableSchedulerLogs;
+        setControlPlaneLogs(l);
+      }
+      setGuardDutyEnabled(eksValues.enableGuardDuty)
     }
 
   }, [isExpanded, props.selectedClusterVersion]);
@@ -514,6 +529,67 @@ const ProvisionerSettings: React.FC<Props> = (props) => {
 
             {!currentProject.simplified_view_enabled &&
               <>
+
+                <Spacer y={1} />
+                <Checkbox
+                  checked={controlPlaneLogs.enableApiServerLogs}
+                  disabled={isReadOnly}
+                  toggleChecked={() => {
+                    setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableApiServerLogs: !controlPlaneLogs.enableApiServerLogs }))
+                  }}
+                  disabledTooltip={"Wait for provisioning to complete before editing this field."}
+                >
+                  <Text color="helper">Enable API Server logs in CloudWatch for this cluster</Text>
+                </Checkbox>
+
+                <Spacer y={1} />
+                <Checkbox
+                  checked={controlPlaneLogs.enableAuditLogs}
+                  disabled={isReadOnly}
+                  toggleChecked={() => {
+                    setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableAuditLogs: !controlPlaneLogs.enableAuditLogs }))
+                  }}
+                  disabledTooltip={"Wait for provisioning to complete before editing this field."}
+                >
+                  <Text color="helper">Enable Audit logs in CloudWatch for this cluster</Text>
+                </Checkbox>
+
+                <Spacer y={1} />
+                <Checkbox
+                  checked={controlPlaneLogs.enableAuthenticatorLogs}
+                  disabled={isReadOnly}
+                  toggleChecked={() => {
+                    setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableAuthenticatorLogs: !controlPlaneLogs.enableAuthenticatorLogs }))
+                  }}
+                  disabledTooltip={"Wait for provisioning to complete before editing this field."}
+                >
+                  <Text color="helper">Enable Authenticator logs in CloudWatch for this cluster</Text>
+                </Checkbox>
+
+                <Spacer y={1} />
+                <Checkbox
+                  checked={controlPlaneLogs.enableControllerManagerLogs}
+                  disabled={isReadOnly}
+                  toggleChecked={() => {
+                    setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableControllerManagerLogs: !controlPlaneLogs.enableControllerManagerLogs }))
+                  }}
+                  disabledTooltip={"Wait for provisioning to complete before editing this field."}
+                >
+                  <Text color="helper">Enable Controller Manager logs in CloudWatch for this cluster</Text>
+                </Checkbox>
+
+                <Spacer y={1} />
+                <Checkbox
+                  checked={controlPlaneLogs.enableSchedulerLogs}
+                  disabled={isReadOnly}
+                  toggleChecked={() => {
+                    setControlPlaneLogs(new EKSLogging({ ...controlPlaneLogs, enableSchedulerLogs: !controlPlaneLogs.enableSchedulerLogs }))
+                  }}
+                  disabledTooltip={"Wait for provisioning to complete before editing this field."}
+                >
+                  <Text color="helper">Enable Scheduler logs in CloudWatch for this cluster</Text>
+                </Checkbox>
+
                 <Spacer y={1} />
                 <Checkbox
                   checked={loadBalancerType}

+ 6 - 6
dashboard/src/main/home/app-dashboard/build-settings/AdvancedBuildSettings.tsx

@@ -5,7 +5,7 @@ import Spacer from "components/porter/Spacer";
 import Input from "components/porter/Input";
 import AnimateHeight from "react-animate-height";
 import Select from "components/porter/Select";
-import { PorterApp } from "../types/porterApp";
+import { BuildMethod, PorterApp } from "../types/porterApp";
 import BuildpackSettings from "./buildpacks/BuildpackSettings";
 import _ from "lodash";
 
@@ -13,18 +13,18 @@ interface AdvancedBuildSettingsProps {
   porterApp: PorterApp;
   updatePorterApp: (attrs: Partial<PorterApp>) => void;
   autoDetectBuildpacks: boolean;
+  buildView: BuildMethod;
+  setBuildView: (buildView: BuildMethod) => void;
 }
 
 const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = ({
   porterApp,
   updatePorterApp,
   autoDetectBuildpacks,
+  buildView,
+  setBuildView,
 }) => {
   const [showSettings, setShowSettings] = useState<boolean>(false);
-  const [buildView, setBuildView] = useState<string>(
-    !_.isEmpty(porterApp.dockerfile)
-      ? "docker" : "buildpacks"
-  );
 
   return (
     <>
@@ -57,7 +57,7 @@ const AdvancedBuildSettings: React.FC<AdvancedBuildSettingsProps> = ({
               { value: "docker", label: "Docker" },
               { value: "buildpacks", label: "Buildpacks" },
             ]}
-            setValue={(option) => setBuildView(option)}
+            setValue={(option: string) => setBuildView(option as BuildMethod)}
             label="Build method"
           />
           {buildView === "docker"

+ 7 - 1
dashboard/src/main/home/app-dashboard/build-settings/BuildSettingsTab.tsx

@@ -12,7 +12,7 @@ import { AxiosError } from "axios";
 import Button from "components/porter/Button";
 import Checkbox from "components/porter/Checkbox";
 import SharedBuildSettings from "./SharedBuildSettings";
-import { PorterApp } from "../types/porterApp";
+import { BuildMethod, PorterApp } from "../types/porterApp";
 import _ from "lodash";
 
 type Props = {
@@ -20,6 +20,8 @@ type Props = {
   setTempPorterApp: (app: PorterApp) => void;
   updatePorterApp: (options: Partial<PorterAppOptions>) => Promise<void>;
   clearStatus: () => void;
+  buildView: BuildMethod;
+  setBuildView: (buildView: BuildMethod) => void;
 };
 
 const BuildSettingsTab: React.FC<Props> = ({
@@ -27,6 +29,8 @@ const BuildSettingsTab: React.FC<Props> = ({
   setTempPorterApp,
   clearStatus,
   updatePorterApp,
+  buildView,
+  setBuildView,
 }) => {
   const { setCurrentError, currentCluster, currentProject } = useContext(Context);
   const [redeployOnSave, setRedeployOnSave] = useState(true);
@@ -161,6 +165,8 @@ const BuildSettingsTab: React.FC<Props> = ({
         setPorterYaml={() => { }}
         autoDetectionOn={false}
         canChangeRepo={false}
+        buildView={buildView}
+        setBuildView={setBuildView}
       />
       <Spacer y={1} />
       <Checkbox

+ 3 - 0
dashboard/src/main/home/app-dashboard/build-settings/DetectDockerfileAndPorterYaml.tsx

@@ -18,12 +18,14 @@ type PropsType = {
   setPorterYaml: (yaml: string, filename: string) => void;
   porterApp: PorterApp;
   updatePorterApp: (attrs: Partial<PorterApp>) => void;
+  updateDockerfileFound: () => void;
 };
 
 const DetectDockerfileAndPorterYaml: React.FC<PropsType> = ({
   setPorterYaml,
   porterApp,
   updatePorterApp,
+  updateDockerfileFound,
 }) => {
   const [showModal, setShowModal] = useState(false);
   const [loading, setLoading] = useState(true);
@@ -68,6 +70,7 @@ const DetectDockerfileAndPorterYaml: React.FC<PropsType> = ({
 
     if (dockerFileItem) {
       updatePorterApp({ dockerfile: dockerFileItem.path });
+      updateDockerfileFound();
     }
   }, [contents]);
 

+ 8 - 1
dashboard/src/main/home/app-dashboard/build-settings/SharedBuildSettings.tsx

@@ -3,7 +3,7 @@ import Spacer from "components/porter/Spacer";
 import Text from "components/porter/Text";
 import React from "react";
 import styled from "styled-components";
-import { PorterApp } from "../types/porterApp";
+import { BuildMethod, PorterApp } from "../types/porterApp";
 import DetectDockerfileAndPorterYaml from "./DetectDockerfileAndPorterYaml";
 import RepositorySelector from "./RepositorySelector";
 import BranchSelector from "./BranchSelector";
@@ -15,6 +15,8 @@ type Props = {
   porterApp: PorterApp;
   autoDetectionOn: boolean;
   canChangeRepo: boolean;
+  buildView: BuildMethod;
+  setBuildView: (buildView: BuildMethod) => void;
 };
 
 const SharedBuildSettings: React.FC<Props> = ({
@@ -23,6 +25,8 @@ const SharedBuildSettings: React.FC<Props> = ({
   porterApp,
   autoDetectionOn,
   canChangeRepo,
+  buildView,
+  setBuildView,
 }) => {
   return (
     <>
@@ -128,12 +132,15 @@ const SharedBuildSettings: React.FC<Props> = ({
                   setPorterYaml={setPorterYaml}
                   porterApp={porterApp}
                   updatePorterApp={updatePorterApp}
+                  updateDockerfileFound={() => setBuildView("docker")}
                 />
               )}
               <AdvancedBuildSettings
                 porterApp={porterApp}
                 updatePorterApp={updatePorterApp}
                 autoDetectBuildpacks={autoDetectionOn}
+                buildView={buildView}
+                setBuildView={setBuildView}
               />
             </>
           )}

+ 145 - 0
dashboard/src/main/home/app-dashboard/build-settings/buildpacks/BuildpackConfigurationModal.tsx

@@ -0,0 +1,145 @@
+import Spacer from 'components/porter/Spacer';
+import Text from 'components/porter/Text';
+import React from 'react';
+import BuildpackList from './BuildpackList';
+import AddCustomBuildpackComponent from './AddCustomBuildpackComponent';
+import Icon from 'components/porter/Icon';
+import Button from 'components/porter/Button';
+import Modal from 'components/porter/Modal';
+import styled from 'styled-components';
+import Select from 'components/porter/Select';
+import stars from "assets/stars-white.svg";
+import { Buildpack } from '../../types/buildpack';
+import { PorterApp } from '../../types/porterApp';
+
+interface Props {
+    closeModal: () => void;
+    selectedStack: string;
+    sortedStackOptions: { value: string; label: string }[];
+    setStackValue: (value: string) => void;
+    selectedBuildpacks: Buildpack[];
+    setSelectedBuildpacks: (buildpacks: Buildpack[]) => void;
+    availableBuildpacks: Buildpack[];
+    setAvailableBuildpacks: (buildpacks: Buildpack[]) => void;
+    porterApp: PorterApp;
+    updatePorterApp: (attrs: Partial<PorterApp>) => void;
+    isDetectingBuildpacks: boolean;
+    detectBuildpacksError: string;
+    handleAddCustomBuildpack: (buildpack: Buildpack) => void;
+    detectAndSetBuildPacks: (detect: boolean) => void;
+}
+const BuildpackConfigurationModal: React.FC<Props> = ({
+    closeModal,
+    selectedStack,
+    sortedStackOptions,
+    setStackValue,
+    selectedBuildpacks,
+    setSelectedBuildpacks,
+    availableBuildpacks,
+    setAvailableBuildpacks,
+    porterApp,
+    updatePorterApp,
+    isDetectingBuildpacks,
+    detectBuildpacksError,
+    handleAddCustomBuildpack,
+    detectAndSetBuildPacks,
+}) => {
+    return (
+        <Modal closeModal={closeModal}>
+            <Text size={16}>Buildpack Configuration</Text>
+            <Spacer y={1} />
+            <Scrollable>
+                <Text>Builder:</Text>
+                {selectedStack === "" &&
+                    <>
+                        <Spacer y={0.5} />
+                        <Text color="helper">
+                            No builder detected. Click 'Detect buildpacks' below to scan your repository for available builders and buildpacks.
+                        </Text>
+                    </>
+                }
+                {selectedStack !== "" &&
+                    <>
+                        <Spacer y={0.5} />
+                        <Select
+                            value={selectedStack}
+                            width="300px"
+                            options={sortedStackOptions}
+                            setValue={setStackValue}
+                        />
+                    </>
+                }
+                <BuildpackList
+                    selectedBuildpacks={selectedBuildpacks}
+                    setSelectedBuildpacks={setSelectedBuildpacks}
+                    availableBuildpacks={availableBuildpacks}
+                    setAvailableBuildpacks={setAvailableBuildpacks}
+                    porterApp={porterApp}
+                    updatePorterApp={updatePorterApp}
+                    showAvailableBuildpacks={true}
+                    isDetectingBuildpacks={isDetectingBuildpacks}
+                    detectBuildpacksError={detectBuildpacksError}
+                    droppableId={"modal"}
+                />
+                <Spacer y={0.5} />
+                <Text>
+                    Custom buildpacks
+                </Text>
+                <Spacer y={0.5} />
+                <Text color="helper">
+                    You may also add buildpacks by directly providing their GitHub links
+                    or links to ZIP files that contain the buildpack source code.
+                </Text>
+                <Spacer y={1} />
+                <AddCustomBuildpackComponent onAdd={handleAddCustomBuildpack} />
+                <Spacer y={2} />
+            </Scrollable>
+            <Footer>
+                <Shade />
+                <FooterButtons>
+                    <Button onClick={() => detectAndSetBuildPacks(true)}>
+                        <Icon src={stars} height="15px" />
+                        <Spacer inline x={0.5} />
+                        Detect buildpacks
+                    </Button>
+                    <Button onClick={closeModal} width={"75px"}>Close</Button>
+                </FooterButtons>
+            </Footer>
+        </Modal>
+    );
+}
+export default BuildpackConfigurationModal;
+
+const Scrollable = styled.div`
+  overflow-y: auto;
+  padding: 0 25px;
+  width: calc(100% + 50px);
+  margin-left: -25px;
+  max-height: calc(100vh - 300px);
+`;
+
+const FooterButtons = styled.div`
+  display: flex;
+  justify-content: space-between;
+`;
+
+const Footer = styled.div`
+  position: relative;
+  width: calc(100% + 50px);
+  margin-left: -25px;
+  padding: 0 25px;
+  border-bottom-left-radius: 10px;
+  border-bottom-right-radius: 10px;
+  background: ${({ theme }) => theme.fg};
+  margin-bottom: -30px;
+  padding-bottom: 30px;
+`;
+
+const Shade = styled.div`
+  position: absolute;
+  top: -50px;
+  left: 0;
+  height: 50px;
+  width: 100%;
+  background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
+`;

+ 26 - 29
dashboard/src/main/home/app-dashboard/build-settings/buildpacks/BuildpackList.tsx

@@ -79,33 +79,20 @@ const BuildpackList: React.FC<Props> = ({
         }
 
         if (availableBuildpacks.length > 0) {
-            return (
-                <>
-                    <Spacer y={0.5} />
-                    <Text>Available buildpacks:</Text>
-                    <Spacer y={0.5} />
-                    {availableBuildpacks.map((buildpack, index) => {
-                        return (
-                            <BuildpackCard
-                                buildpack={buildpack}
-                                action={"add"}
-                                onClickFn={handleAddBuildpack}
-                                index={index}
-                                draggable={false}
-                            />
-                        )
-                    })
-                    }
-                </>
-            )
+            return availableBuildpacks.map((buildpack, index) => {
+                return (
+                    <BuildpackCard
+                        buildpack={buildpack}
+                        action={"add"}
+                        onClickFn={handleAddBuildpack}
+                        index={index}
+                        draggable={false}
+                    />
+                )
+            })
         }
 
-        return (
-            <>
-                <Spacer y={0.5} />
-                <Text color="helper">No buildpacks detected. Click 'Detect buildpacks' below to scan your repository for available buildpacks.</Text>
-            </>
-        )
+        return <Text color="helper">No available buildpacks detected.</Text>
     }
 
     return (
@@ -117,13 +104,13 @@ const BuildpackList: React.FC<Props> = ({
                     <Spacer y={0.5} />
                 </>
             }
-            <Droppable droppableId={droppableId}>
+            {selectedBuildpacks.length !== 0 && <Droppable droppableId={droppableId}>
                 {provided => (
                     <div
                         {...provided.droppableProps}
                         ref={provided.innerRef}
                     >
-                        {selectedBuildpacks?.map((buildpack, index) => (
+                        {selectedBuildpacks.map((buildpack, index) => (
                             <BuildpackCard
                                 buildpack={buildpack}
                                 action={"remove"}
@@ -136,8 +123,18 @@ const BuildpackList: React.FC<Props> = ({
                         {provided.placeholder}
                     </div>
                 )}
-            </Droppable>
-            {showAvailableBuildpacks && renderAvailableBuildpacks()}
+            </Droppable>}
+            {selectedBuildpacks.length === 0 &&
+                <Text color="helper">No buildpacks selected.</Text>
+            }
+            {showAvailableBuildpacks &&
+                <>
+                    <Spacer y={0.5} />
+                    <Text>Available buildpacks:</Text>
+                    <Spacer y={0.5} />
+                    {renderAvailableBuildpacks()}
+                </>
+            }
         </DragDropContext>
     );
 };

+ 49 - 156
dashboard/src/main/home/app-dashboard/build-settings/buildpacks/BuildpackSettings.tsx

@@ -1,20 +1,13 @@
 import Helper from "components/form-components/Helper";
-import Select from "components/porter/Select";
-import Loading from "components/Loading";
 import React, { useContext, useEffect, useMemo, useState } from "react";
 import api from "shared/api";
 import { Context } from "shared/Context";
 import styled, { keyframes } from "styled-components";
 import Button from "components/porter/Button";
-import Modal from "components/porter/Modal";
 import Spacer from "components/porter/Spacer";
-import Text from "components/porter/Text";
 import Error from "components/porter/Error";
 import { PorterApp } from "../../types/porterApp";
-import AddCustomBuildpackComponent from "./AddCustomBuildpackComponent";
 import BuildpackList from "./BuildpackList";
-import Icon from "components/porter/Icon";
-import stars from "assets/stars-white.svg";
 import {
   BUILDPACK_TO_NAME,
   BuildConfig,
@@ -23,6 +16,7 @@ import {
   DEFAULT_HEROKU_STACK,
   DetectedBuildpack
 } from "../../types/buildpack";
+import BuildpackConfigurationModal from "./BuildpackConfigurationModal";
 
 const BuildpackSettings: React.FC<{
   porterApp: PorterApp;
@@ -35,66 +29,15 @@ const BuildpackSettings: React.FC<{
 }) => {
     const { currentProject } = useContext(Context);
 
-    const [builders, setBuilders] = useState<DetectedBuildpack[]>([]);
     const [selectedStack, setSelectedStack] = useState<string>("");
+    const [stackOptions, setStackOptions] = useState<{ label: string; value: string }[]>([]);
     const [isModalOpen, setIsModalOpen] = useState(false);
     const [isDetectingBuildpacks, setIsDetectingBuildpacks] = useState(false);
     const [error, setError] = useState<string>("");
 
     const [selectedBuildpacks, setSelectedBuildpacks] = useState<Buildpack[]>([]);
     const [availableBuildpacks, setAvailableBuildpacks] = useState<Buildpack[]>([]);
-    const renderModalContent = () => {
-      return (
-        <>
-          <Text size={16}>Buildpack Configuration</Text>
-          <Spacer y={1} />
-          <Scrollable>
-            <Select
-              value={selectedStack}
-              width="300px"
-              options={sortedStackOptions}
-              setValue={(option) => {
-                setSelectedStack(option);
-                updatePorterApp({ builder: option });
-              }}
-              label="Builder and stack"
-            />
-            <Spacer y={0.5} />
-            <BuildpackList
-              selectedBuildpacks={selectedBuildpacks}
-              setSelectedBuildpacks={setSelectedBuildpacks}
-              availableBuildpacks={availableBuildpacks}
-              setAvailableBuildpacks={setAvailableBuildpacks}
-              porterApp={porterApp}
-              updatePorterApp={updatePorterApp}
-              showAvailableBuildpacks={true}
-              isDetectingBuildpacks={isDetectingBuildpacks}
-              detectBuildpacksError={error}
-              droppableId={"modal"}
-            />
-            <Spacer y={0.5} />
-            <Text color="helper">
-              You may also add buildpacks by directly providing their GitHub links
-              or links to ZIP files that contain the buildpack source code.
-            </Text>
-            <Spacer y={1} />
-            <AddCustomBuildpackComponent onAdd={handleAddCustomBuildpack} />
-            <Spacer y={2} />
-          </Scrollable>
-          <Footer>
-            <Shade />
-            <FooterButtons>
-              <Button onClick={() => detectAndSetBuildPacks(true)}>
-                <Icon src={stars} height="15px" />
-                <Spacer inline x={0.5} />
-                Detect buildpacks
-              </Button>
-              <Button onClick={() => setIsModalOpen(false)} width={"75px"}>Save</Button>
-            </FooterButtons>
-          </Footer>
-        </>
-      );
-    };
+
     const detectAndSetBuildPacks = async (detect: boolean) => {
       try {
         if (currentProject == null) {
@@ -103,20 +46,17 @@ const BuildpackSettings: React.FC<{
 
         if (!detect) {
           // in this case, we are not detecting buildpacks, so we just populate based on the DB
-          setBuilders([{
-            name: porterApp.builder.split("/")[0],
-            builders: [porterApp.builder],
-            detected: [],
-            others: [],
-            buildConfig: {} as BuildConfig,
-          }])
-          setSelectedStack(porterApp.builder);
-          setSelectedBuildpacks(porterApp.buildpacks?.map(bp => ({
-            name: BUILDPACK_TO_NAME[bp] ?? bp,
-            buildpack: bp,
-            config: {},
-          })) ?? []);
-          setAvailableBuildpacks([]);
+          if (porterApp.builder != null) {
+            setSelectedStack(porterApp.builder);
+            setStackOptions([{ label: porterApp.builder, value: porterApp.builder }]);
+          }
+          if (porterApp.buildpacks != null) {
+            setSelectedBuildpacks(porterApp.buildpacks.map(bp => ({
+              name: BUILDPACK_TO_NAME[bp] ?? bp,
+              buildpack: bp,
+              config: {},
+            })));
+          }
         } else {
           if (isDetectingBuildpacks) {
             return;
@@ -141,7 +81,20 @@ const BuildpackSettings: React.FC<{
           if (builders.length === 0) {
             return;
           }
-          setBuilders(builders);
+          setStackOptions(builders.flatMap((builder) => {
+            return builder.builders.map((stack) => ({
+              label: `${builder.name} - ${stack}`,
+              value: stack.toLowerCase(),
+            }));
+          }).sort((a, b) => {
+            if (a.label < b.label) {
+              return -1;
+            }
+            if (a.label > b.label) {
+              return 1;
+            }
+            return 0;
+          }));
 
           const defaultBuilder = builders.find(
             (builder) => builder.name.toLowerCase() === DEFAULT_BUILDER_NAME
@@ -165,16 +118,12 @@ const BuildpackSettings: React.FC<{
             setAvailableBuildpacks(defaultBuilder.others);
             setError("");
           } else {
+            updatePorterApp({ builder: detectedBuilder });
             setAvailableBuildpacks(allBuildpacks.filter(bp => !porterApp.buildpacks?.includes(bp.buildpack)));
           }
         }
       } catch (err) {
-        if (autoDetectBuildpacks) {
-          updatePorterApp({ buildpacks: [] });
-          setSelectedBuildpacks([]);
-          setAvailableBuildpacks([]);
-          setError(`Unable to detect buildpacks at path: ${porterApp.build_context}. Please make sure your repo, branch, and application root path are all set correctly and attempt to detect again.`);
-        }
+        setError(`Unable to detect buildpacks at path: ${porterApp.build_context}. Please make sure your repo, branch, and application root path are all set correctly and attempt to detect again.`);
       } finally {
         setIsDetectingBuildpacks(false);
       }
@@ -184,30 +133,6 @@ const BuildpackSettings: React.FC<{
       detectAndSetBuildPacks(autoDetectBuildpacks);
     }, [currentProject]);
 
-    const builderOptions = useMemo(() => {
-      if (!Array.isArray(builders)) {
-        return;
-      }
-
-      return builders.map((builder) => ({
-        label: builder.name,
-        value: builder.name.toLowerCase(),
-      }));
-    }, [builders]);
-
-    const stackOptions = useMemo(() => {
-      if (!Array.isArray(builders)) {
-        return;
-      }
-
-      return builders.flatMap((builder) => {
-        return builder.builders.map((stack) => ({
-          label: `${builder.name} - ${stack}`,
-          value: stack.toLowerCase(),
-        }));
-      });
-    }, [builders]);
-
     const handleAddCustomBuildpack = (buildpack: Buildpack) => {
       if (porterApp.buildpacks.find((bp) => bp === buildpack.buildpack) == null) {
         updatePorterApp({ buildpacks: [...porterApp.buildpacks, buildpack.buildpack] });
@@ -215,20 +140,6 @@ const BuildpackSettings: React.FC<{
       }
     };
 
-    if (!stackOptions?.length || !builderOptions?.length) {
-      return <Loading />;
-    }
-
-    const sortedStackOptions = stackOptions.sort((a, b) => {
-      if (a.label < b.label) {
-        return -1;
-      }
-      if (a.label > b.label) {
-        return 1;
-      }
-      return 0;
-    });
-
     return (
       <BuildpackConfigurationContainer>
         {selectedBuildpacks.length > 0 && (
@@ -265,9 +176,25 @@ const BuildpackSettings: React.FC<{
           <I className="material-icons">add</I> Add / detect buildpacks
         </Button>
         {isModalOpen && (
-          <Modal closeModal={() => setIsModalOpen(false)}>
-            {renderModalContent()}
-          </Modal>
+          <BuildpackConfigurationModal
+            closeModal={() => setIsModalOpen(false)}
+            selectedStack={selectedStack}
+            sortedStackOptions={stackOptions}
+            setStackValue={(option) => {
+              setSelectedStack(option);
+              updatePorterApp({ builder: option });
+            }}
+            selectedBuildpacks={selectedBuildpacks}
+            setSelectedBuildpacks={setSelectedBuildpacks}
+            availableBuildpacks={availableBuildpacks}
+            setAvailableBuildpacks={setAvailableBuildpacks}
+            porterApp={porterApp}
+            updatePorterApp={updatePorterApp}
+            isDetectingBuildpacks={isDetectingBuildpacks}
+            detectBuildpacksError={error}
+            handleAddCustomBuildpack={handleAddCustomBuildpack}
+            detectAndSetBuildPacks={detectAndSetBuildPacks}
+          />
         )}
       </BuildpackConfigurationContainer>
     );
@@ -276,32 +203,6 @@ const BuildpackSettings: React.FC<{
 export default BuildpackSettings;
 
 
-const Shade = styled.div`
-  position: absolute;
-  top: -50px;
-  left: 0;
-  height: 50px;
-  width: 100%;
-  background: linear-gradient(to bottom, #00000000, ${({ theme }) => theme.fg});
-`;
-
-const FooterButtons = styled.div`
-  display: flex;
-  justify-content: space-between;
-`;
-
-const Footer = styled.div`
-  position: relative;
-  width: calc(100% + 50px);
-  margin-left: -25px;
-  padding: 0 25px;
-  border-bottom-left-radius: 10px;
-  border-bottom-right-radius: 10px;
-  background: ${({ theme }) => theme.fg};
-  margin-bottom: -30px;
-  padding-bottom: 30px;
-`;
-
 const I = styled.i`
   color: white;
   font-size: 14px;
@@ -311,14 +212,6 @@ const I = styled.i`
   justify-content: center;
 `;
 
-const Scrollable = styled.div`
-  overflow-y: auto;
-  padding: 0 25px;
-  width: calc(100% + 50px);
-  margin-left: -25px;
-  max-height: calc(100vh - 300px);
-`;
-
 const fadeIn = keyframes`
   from {
     opacity: 0;

+ 39 - 152
dashboard/src/main/home/app-dashboard/expanded-app/ExpandedApp.tsx

@@ -48,7 +48,7 @@ import { Log } from "main/home/cluster-dashboard/expanded-chart/logs-section/use
 import Anser, { AnserJsonEntry } from "anser";
 import _ from "lodash";
 import AnimateHeight from "react-animate-height";
-import { PorterApp } from "../types/porterApp";
+import { BuildMethod, PorterApp } from "../types/porterApp";
 
 type Props = RouteComponentProps & {};
 
@@ -97,7 +97,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     false
   );
 
-  const [saveValuesStatus, setSaveValueStatus] = useState<string>("");
   const [bannerLoading, setBannerLoading] = useState<boolean>(false);
 
   const [showRevisions, setShowRevisions] = useState<boolean>(false);
@@ -120,6 +119,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   const [porterApp, setPorterApp] = useState<PorterApp>();
   // this is the version of the porterApp that is being edited. on save, we set the real porter app to be this version
   const [tempPorterApp, setTempPorterApp] = useState<PorterApp>();
+  const [buildView, setBuildView] = useState<BuildMethod>("docker");
 
   const { eventId, tab } = useParams<Params>();
   const selectedTab: ValidTab = tab != null && validTabs.includes(tab) ? tab : DEFAULT_TAB;
@@ -143,12 +143,12 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
   useEffect(() => {
     const { appName } = props.match.params as any;
     if (currentCluster && appName && currentProject) {
-      getPorterApp();
+      getPorterApp({ revision: 0 });
     }
   }, [currentCluster]);
 
   // this method fetches and reconstructs the porter yaml as well as the DB info (stored in PorterApp)
-  const getPorterApp = async () => {
+  const getPorterApp = async ({ revision }: { revision: number }) => {
     setBannerLoading(true);
     setIsLoading(true);
     const { appName } = props.match.params as any;
@@ -173,14 +173,14 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
           namespace: `porter-stack-${appName}`,
           cluster_id: currentCluster.id,
           name: appName,
-          revision: 0,
+          revision: revision,
         }
       );
 
-      let releaseChartData;
-      // get the release chart
+      let preDeployChartData;
+      // get the pre-deploy chart
       try {
-        releaseChartData = await api.getChart(
+        preDeployChartData = await api.getChart(
           "<token>",
           {},
           {
@@ -188,6 +188,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             namespace: `porter-stack-${appName}`,
             cluster_id: currentCluster.id,
             name: `${appName}-r`,
+            // this is always latest because we do not tie the pre-deploy chart to the umbrella chart
             revision: 0,
           }
         );
@@ -200,7 +201,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       const newAppData = {
         app: resPorterApp?.data,
         chart: resChartData?.data,
-        releaseChart: releaseChartData?.data,
+        releaseChart: preDeployChartData?.data,
       };
       const porterJson = await fetchPorterYamlContent(
         resPorterApp?.data?.porter_yaml_path ?? "porter.yaml",
@@ -213,10 +214,11 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
       const parsedPorterApp = { ...resPorterApp?.data, buildpacks: newAppData.app.buildpacks?.split(",") ?? [] };
       setPorterApp(parsedPorterApp);
       setTempPorterApp(parsedPorterApp);
+      setBuildView(!_.isEmpty(parsedPorterApp.dockerfile) ? "docker" : "buildpacks")
 
       const [newServices, newEnvVars] = updateServicesAndEnvVariables(
         resChartData?.data,
-        releaseChartData?.data,
+        preDeployChartData?.data,
         porterJson,
       );
       const finalPorterYaml = createFinalPorterYaml(
@@ -333,19 +335,30 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
         );
         const yamlString = yaml.dump(finalPorterYaml);
         const base64Encoded = btoa(yamlString);
+
+        const updatedPorterApp = {
+          porter_yaml: base64Encoded,
+          override_release: true,
+          ...PorterApp.empty(),
+          build_context: tempPorterApp.build_context,
+          repo_name: tempPorterApp.repo_name,
+          git_branch: tempPorterApp.git_branch,
+          buildpacks: "",
+          ...options,
+        }
+        if (buildView === "docker") {
+          updatedPorterApp.dockerfile = tempPorterApp.dockerfile;
+          updatedPorterApp.builder = "null";
+          updatedPorterApp.buildpacks = "null";
+        } else {
+          updatedPorterApp.builder = tempPorterApp.builder;
+          updatedPorterApp.buildpacks = tempPorterApp.buildpacks.join(",");
+          updatedPorterApp.dockerfile = "null";
+        }
+
         await api.createPorterApp(
           "<token>",
-          {
-            porter_yaml: base64Encoded,
-            repo_name: tempPorterApp.repo_name,
-            git_branch: tempPorterApp.git_branch,
-            build_context: tempPorterApp.build_context,
-            builder: tempPorterApp.builder,
-            buildpacks: tempPorterApp.buildpacks.join(","),
-            dockerfile: tempPorterApp.dockerfile,
-            ...options,
-            override_release: true,
-          },
+          updatedPorterApp,
           {
             cluster_id: currentCluster.id,
             project_id: currentProject.id,
@@ -528,137 +541,10 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
     return [newServices, envVars];
   };
 
-  const getChartData = async (chart: ChartType, isCurrent?: boolean) => {
-    setButtonStatus("");
-    setIsLoading(true);
-    try {
-      const res = await api.getChart(
-        "<token>",
-        {},
-        {
-          name: chart.name,
-          namespace: chart.namespace,
-          cluster_id: currentCluster.id,
-          revision: chart.version,
-          id: currentProject.id,
-        }
-      );
-
-      const updatedChart = res.data;
-
-      if (appData != null && updatedChart != null) {
-        setAppData({ ...appData, chart: updatedChart });
-      }
-
-      // let releaseChartData;
-      // // get the release chart
-      // try {
-      //   releaseChartData = await api.getChart(
-      //     "<token>",
-      //     {},
-      //     {
-      //       id: currentProject.id,
-      //       namespace: `porter-stack-${chart.name}`,
-      //       cluster_id: currentCluster.id,
-      //       name: `${chart.name}-r`,
-      //       revision: 0,
-      //     }
-      //   );
-      // } catch (err) {
-      //   // do nothing, unable to find release chart
-      //   // console.log(err);
-      // }
-
-      // const releaseChart = releaseChartData?.data;
-
-      // if (appData != null && updatedChart != null) {
-      //   if (releaseChart != null) {
-      //     setAppData({ ...appData, chart: updatedChart, releaseChart });
-      //   } else {
-      //     setAppData({ ...appData, chart: updatedChart });
-      //   }
-      // }
-
-      const [newServices, newEnvVars] = updateServicesAndEnvVariables(
-        updatedChart,
-        appData.releaseChart,
-        porterJson,
-        appData.app.builder != null && appData.app.builder.includes("heroku")
-      );
-
-      if (isCurrent) {
-        setShowUnsavedChangesBanner(false);
-      } else {
-        onAppUpdate(newServices, newEnvVars);
-      }
-    } catch (err) {
-      console.log(err);
-    } finally {
-      setIsLoading(false);
-    }
-
-  };
-
   const setRevision = (chart: ChartType, isCurrent?: boolean) => {
-    getChartData(chart, isCurrent);
+    getPorterApp({ revision: chart.version });
   };
 
-  const appUpgradeVersion = useCallback(
-    async (version: string, cb: () => void) => {
-      // convert current values to yaml
-      const values = appData.chart.config;
-
-      const valuesYaml = yaml.dump({
-        ...values,
-      });
-
-      setSaveValueStatus("loading");
-      getChartData(appData.chart);
-
-      try {
-        await api.upgradeChartValues(
-          "<token>",
-          {
-            values: valuesYaml,
-            version: version,
-            latest_revision: appData.chart.version,
-          },
-          {
-            id: currentProject.id,
-            namespace: appData.chart.namespace,
-            name: appData.chart.name,
-            cluster_id: currentCluster.id,
-          }
-        );
-        setSaveValueStatus("successful");
-        setForceRefreshRevisions(true);
-
-        window.analytics?.track("Chart Upgraded", {
-          chart: appData.chart.name,
-          values: valuesYaml,
-        });
-
-        cb && cb();
-      } catch (err) {
-        const parsedErr = err?.response?.data?.error;
-
-        if (parsedErr) {
-          err = parsedErr;
-        }
-
-        setSaveValueStatus(err);
-        setCurrentError(parsedErr);
-
-        window.analytics?.track("Failed to Upgrade Chart", {
-          chart: appData.chart.name,
-          values: valuesYaml,
-          error: err,
-        });
-      }
-    },
-    [appData?.chart]
-  );
-
   const getReadableDate = (s: string) => {
     const ts = new Date(s);
     const date = ts.toLocaleDateString();
@@ -762,6 +648,8 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             clearStatus={() => setButtonStatus("")}
             updatePorterApp={updatePorterApp}
             setShowUnsavedChangesBanner={setShowUnsavedChangesBanner}
+            buildView={buildView}
+            setBuildView={setBuildView}
           />
         );
       case "settings":
@@ -824,7 +712,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
 
   return (
     <>
-      {isLoading && appData == null && <Loading />}
+      {isLoading && <Loading />}
       {!isLoading && appData == null && (
         <Placeholder>
           <Container row>
@@ -891,7 +779,7 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
             <>
               <Container>
                 <Text>
-                  <a href={subdomain} target="_blank">
+                  <a href={Service.prefixSubdomain(subdomain)} target="_blank">
                     {subdomain}
                   </a>
                 </Text>
@@ -977,7 +865,6 @@ const ExpandedApp: React.FC<Props> = ({ ...props }) => {
                       appData.chart.chart.metadata.version
                     }
                     latestVersion={appData.chart.latest_version}
-                    upgradeVersion={appUpgradeVersion}
                   />
                   <DarkMatter antiHeight="-18px" />
                 </>

+ 28 - 17
dashboard/src/main/home/app-dashboard/new-app-flow/NewAppFlow.tsx

@@ -27,7 +27,7 @@ import { PorterJson, PorterYamlSchema, createFinalPorterYaml } from "./schema";
 import { Service } from "./serviceTypes";
 import GithubConnectModal from "./GithubConnectModal";
 import Link from "components/porter/Link";
-import { PorterApp } from "../types/porterApp";
+import { BuildMethod, PorterApp } from "../types/porterApp";
 
 type Props = RouteComponentProps & {};
 
@@ -95,6 +95,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
 
   const [porterJsonWithPath, setPorterJsonWithPath] = useState<PorterJsonWithPath | undefined>(undefined);
   const [detected, setDetected] = useState<Detected | undefined>(undefined);
+  const [buildView, setBuildView] = useState<BuildMethod>("buildpacks");
+
   const handleSetAccessData = (data: GithubAppAccessData) => {
     setAccessData(data);
     setShowGithubConnectModal(
@@ -225,6 +227,7 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
     const regex = /^[a-z0-9-]{1,61}$/;
     return regex.test(name);
   };
+
   const handleAppNameChange = (name: string) => {
     setPorterApp(PorterApp.setAttribute(porterApp, "name", name));
     if (isAppNameValid(name) && Validators.applicationName(name)) {
@@ -282,23 +285,30 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
         };
       }
 
+      const porterAppRequest = {
+        porter_yaml: base64Encoded,
+        override_release: true,
+        image_info: imageInfo,
+        ...PorterApp.empty(),
+        buildpacks: "",
+        // for some reason I couldn't get the path to update the porterApp object correctly here so I just grouped it with the porter json :/
+        porter_yaml_path: porterJsonWithPath?.porterYamlPath,
+        repo_name: porterApp.repo_name,
+        git_branch: porterApp.git_branch,
+        git_repo_id: porterApp.git_repo_id,
+        build_context: porterApp.build_context,
+        image_repo_uri: porterApp.image_repo_uri,
+      }
+      if (buildView === "docker") {
+        porterAppRequest.dockerfile = porterApp.dockerfile;
+      } else {
+        porterAppRequest.builder = porterApp.builder;
+        porterAppRequest.buildpacks = porterApp.buildpacks.join(",");
+      }
+
       await api.createPorterApp(
         "<token>",
-        {
-          repo_name: porterApp.repo_name,
-          git_branch: porterApp.git_branch,
-          git_repo_id: porterApp.git_repo_id,
-          build_context: porterApp.build_context,
-          builder: !_.isEmpty(porterApp.dockerfile) ? "" : porterApp.builder,
-          buildpacks: !_.isEmpty(porterApp.dockerfile) ? "" : porterApp.buildpacks.join(","),
-          dockerfile: porterApp.dockerfile,
-          image_repo_uri: porterApp.image_repo_uri,
-          porter_yaml: base64Encoded,
-          override_release: true,
-          image_info: imageInfo,
-          // for some reason I couldn't get the path to update the porterApp object correctly here so I just grouped it with the porter json :/
-          porter_yaml_path: porterJsonWithPath?.porterYamlPath,
-        },
+        porterAppRequest,
         {
           cluster_id: currentCluster.id,
           project_id: currentProject.id,
@@ -316,7 +326,6 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
       return true;
     } catch (err: any) {
       // TODO: better error handling
-      console.log(err);
       const errMessage =
         err?.response?.data?.error ??
         err?.toString() ??
@@ -419,6 +428,8 @@ const NewAppFlow: React.FC<Props> = ({ ...props }) => {
                   }}
                   imageTag={imageTag}
                   setImageTag={setImageTag}
+                  buildView={buildView}
+                  setBuildView={setBuildView}
                 />
               </>,
               <>

+ 7 - 1
dashboard/src/main/home/app-dashboard/new-app-flow/SourceSettings.tsx

@@ -8,7 +8,7 @@ import { pushFiltered } from "shared/routing";
 import ImageSelector from "components/image-selector/ImageSelector";
 import SharedBuildSettings from "../build-settings/SharedBuildSettings";
 import Link from "components/porter/Link";
-import { PorterApp } from "../types/porterApp";
+import { BuildMethod, PorterApp } from "../types/porterApp";
 
 type Props = RouteComponentProps & {
   source: SourceType | undefined;
@@ -19,6 +19,8 @@ type Props = RouteComponentProps & {
   setPorterYaml: (yaml: string, filename: string) => void;
   porterApp: PorterApp;
   setPorterApp: (x: PorterApp) => void;
+  buildView: BuildMethod;
+  setBuildView: (buildView: BuildMethod) => void;
 };
 
 const SourceSettings: React.FC<Props> = ({
@@ -30,6 +32,8 @@ const SourceSettings: React.FC<Props> = ({
   setPorterYaml,
   porterApp,
   setPorterApp,
+  buildView,
+  setBuildView,
   location,
   history,
 }) => {
@@ -44,6 +48,8 @@ const SourceSettings: React.FC<Props> = ({
             updatePorterApp={(attrs: Partial<PorterApp>) => setPorterApp(PorterApp.setAttributes(porterApp, attrs))}
             autoDetectionOn={true}
             canChangeRepo={true}
+            buildView={buildView}
+            setBuildView={setBuildView}
           />
         ) : (
           <StyledSourceBox>

+ 89 - 39
dashboard/src/main/home/app-dashboard/new-app-flow/WebTabs.tsx

@@ -4,7 +4,7 @@ import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import TabSelector from "components/TabSelector";
 import Checkbox from "components/porter/Checkbox";
-import { WebService } from "./serviceTypes";
+import { Service, WebService } from "./serviceTypes";
 import AnimateHeight, { Height } from "react-animate-height";
 
 interface Props {
@@ -15,7 +15,9 @@ interface Props {
 
 const RESOURCE_HEIGHT_WITHOUT_AUTOSCALING = 373;
 const RESOURCE_HEIGHT_WITH_AUTOSCALING = 713;
-const ADVANCED_BASE_HEIGHT = 300;
+const NETWORKING_HEIGHT_WITHOUT_INGRESS = 204;
+const NETWORKING_HEIGHT_WITH_INGRESS = 333;
+const ADVANCED_BASE_HEIGHT = 215;
 const PROBE_INPUTS_HEIGHT = 230;
 
 const WebTabs: React.FC<Props> = ({
@@ -26,7 +28,7 @@ const WebTabs: React.FC<Props> = ({
   const [currentTab, setCurrentTab] = React.useState<string>("main");
 
   const renderMain = () => {
-    setHeight(288);
+    setHeight(159);
     return (
       <>
         <Spacer y={1} />
@@ -57,6 +59,14 @@ const WebTabs: React.FC<Props> = ({
           }}
           disabledTooltip={"You may only edit this field in your porter.yaml."}
         />
+      </>
+    );
+  };
+
+  const renderNetworking = () => {
+    setHeight(service.ingress.enabled.value ? NETWORKING_HEIGHT_WITH_INGRESS : NETWORKING_HEIGHT_WITHOUT_INGRESS)
+    return (
+      <>
         <Spacer y={1} />
         <Input
           label="Container port"
@@ -87,14 +97,48 @@ const WebTabs: React.FC<Props> = ({
           }}
           disabledTooltip={"You may only edit this field in your porter.yaml."}
         >
-          <Text color="helper">Generate a Porter URL for external traffic</Text>
+          <Text color="helper">Expose to external traffic</Text>
         </Checkbox>
+        <AnimateHeight height={service.ingress.enabled.value ? 'auto' : 0}>
+          <Spacer y={1} />
+          <Input
+            label={
+              <>
+                <span>Custom domain</span>
+                <a
+                  href="https://docs.porter.run/standard/deploying-applications/https-and-domains/custom-domains"
+                  target="_blank"
+                >
+                  &nbsp;(?)
+                </a>
+              </>
+            }
+            placeholder="ex: my-app.my-domain.com"
+            value={service.ingress.customDomain.value}
+            disabled={service.ingress.customDomain.readOnly}
+            width="300px"
+            setValue={(e) => {
+              editService({
+                ...service,
+                ingress: {
+                  ...service.ingress,
+                  customDomain: { readOnly: false, value: e },
+                },
+              });
+            }}
+            disabledTooltip={
+              "You may only edit this field in your porter.yaml."
+            }
+          />
+          <Spacer y={1} />
+          {getApplicationURLText()}
+        </AnimateHeight>
       </>
     );
-  };
+  }
 
   const renderResources = () => {
-    service.autoscaling.enabled.value ? setHeight(RESOURCE_HEIGHT_WITH_AUTOSCALING) : setHeight(RESOURCE_HEIGHT_WITHOUT_AUTOSCALING);
+    setHeight(service.autoscaling.enabled.value ? RESOURCE_HEIGHT_WITH_AUTOSCALING : RESOURCE_HEIGHT_WITHOUT_AUTOSCALING)
     return (
       <>
         <Spacer y={1} />
@@ -285,6 +329,7 @@ const WebTabs: React.FC<Props> = ({
     }
     return height;
   };
+
   const renderHealth = () => {
     setHeight(calculateHealthHeight());
     return (
@@ -629,42 +674,45 @@ const WebTabs: React.FC<Props> = ({
   const renderAdvanced = () => {
     return (
       <>
-        <>
-          <Spacer y={1} />
-          <Input
-            label={
-              <>
-                <span>Custom domain</span>
-                <a
-                  href="https://docs.porter.run/standard/deploying-applications/https-and-domains/custom-domains"
-                  target="_blank"
-                >
-                  &nbsp;(?)
-                </a>
-              </>
-            }
-            placeholder="ex: my-app.my-domain.com"
-            value={service.ingress.hosts.value}
-            disabled={service.ingress.hosts.readOnly}
-            width="300px"
-            setValue={(e) => {
-              editService({
-                ...service,
-                ingress: {
-                  ...service.ingress,
-                  hosts: { readOnly: false, value: e },
-                },
-              });
-            }}
-            disabledTooltip={
-              "You may only edit this field in your porter.yaml."
-            }
-          />
-          {renderHealth()}
-        </>
+        {renderHealth()}
       </>
     );
   };
+
+  const getApplicationURLText = () => {
+    if (service.ingress.hosts.value !== "") {
+      return (
+        <Text>Application URL:{" "}
+          <a href={Service.prefixSubdomain(service.ingress.hosts.value)} target="_blank">
+            {service.ingress.hosts.value}
+          </a>
+        </Text>
+      )
+    } else if (service.ingress.porterHosts.value !== "") {
+      return (
+        <Text>Application URL:{" "}
+          <a href={Service.prefixSubdomain(service.ingress.porterHosts.value)} target="_blank">
+            {service.ingress.porterHosts.value}
+          </a>
+        </Text>
+      )
+    } else if (service.ingress.customDomain.value !== "") {
+      return (
+        <Text color="helper">Application URL: Your application will be available at{" "}
+          <a href={Service.prefixSubdomain(service.ingress.customDomain.value)} target="_blank">
+            {service.ingress.customDomain.value}
+          </a>
+          {" "}on next deploy.
+        </Text>
+      )
+    } else {
+      return (
+        <Text color="helper">
+          Application URL: Not generated yet. Porter will generate a URL for you on next deploy.
+        </Text>
+      )
+    }
+  }
   return (
     <>
       <>
@@ -672,6 +720,7 @@ const WebTabs: React.FC<Props> = ({
           options={[
             { label: "Main", value: "main" },
             { label: "Resources", value: "resources" },
+            { label: "Networking", value: "networking" },
             { label: "Advanced", value: "advanced" },
           ]}
           currentTab={currentTab}
@@ -679,6 +728,7 @@ const WebTabs: React.FC<Props> = ({
         />
         {currentTab === "main" && renderMain()}
         {currentTab === "resources" && renderResources()}
+        {currentTab === "networking" && renderNetworking()}
         {currentTab === "advanced" && renderAdvanced()}
       </>
     </>

+ 34 - 19
dashboard/src/main/home/app-dashboard/new-app-flow/serviceTypes.ts

@@ -16,6 +16,7 @@ type ServiceBoolean = {
 }
 type Ingress = {
     enabled: ServiceBoolean;
+    customDomain: ServiceString;
     hosts: ServiceString;
     porterHosts: ServiceString;
 }
@@ -135,7 +136,7 @@ const WorkerService = {
             },
             canDelete: porterJson?.apps?.[name] == null,
         }
-    }
+    },
 }
 
 export type WebService = SharedServiceParams & Omit<WorkerService, 'type'> & {
@@ -161,6 +162,7 @@ const WebService = {
         },
         ingress: {
             enabled: ServiceField.boolean(true, porterJson?.apps?.[name]?.config?.ingress?.enabled),
+            customDomain: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
             hosts: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
             porterHosts: ServiceField.string('', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined),
         },
@@ -212,8 +214,8 @@ const WebService = {
             },
             ingress: {
                 enabled: service.ingress.enabled.value,
-                hosts: service.ingress.hosts.value ? [service.ingress.hosts.value] : [],
-                custom_domain: service.ingress.hosts.value ? true : false,
+                custom_domain: service.ingress.customDomain.value ? true : false,
+                hosts: service.ingress.customDomain.value ? [service.ingress.customDomain.value] : [],
                 porter_hosts: service.ingress.porterHosts.value ? [service.ingress.porterHosts.value] : [],
             },
             health: {
@@ -255,6 +257,7 @@ const WebService = {
             },
             ingress: {
                 enabled: ServiceField.boolean(values.ingress?.enabled ?? false, porterJson?.apps?.[name]?.config?.ingress?.enabled),
+                customDomain: ServiceField.string(values.ingress?.hosts?.length ? values.ingress.hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
                 hosts: ServiceField.string(values.ingress?.hosts?.length ? values.ingress.hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.hosts[0] : undefined),
                 porterHosts: ServiceField.string(values.ingress?.porter_hosts?.length ? values.ingress.porter_hosts[0] : '', porterJson?.apps?.[name]?.config?.ingress?.porter_hosts?.length ? porterJson?.apps?.[name]?.config?.ingress?.porter_hosts[0] : undefined),
             },
@@ -281,7 +284,7 @@ const WebService = {
                 },
             }
         }
-    }
+    },
 }
 
 export type JobService = SharedServiceParams & {
@@ -332,7 +335,7 @@ const JobService = {
             cronSchedule: ServiceField.string(values.schedule?.value ?? '', porterJson?.apps?.[name]?.config?.schedule?.value),
             canDelete: porterJson?.apps?.[name] == null,
         }
-    }
+    },
 }
 
 export type ReleaseService = SharedServiceParams & {
@@ -372,7 +375,7 @@ const ReleaseService = {
             type: 'release',
             canDelete: porterJson?.release == null,
         }
-    }
+    },
 }
 
 
@@ -485,27 +488,39 @@ export const Service = {
             return "";
         }
 
-        const prefixSubdomain = (subdomain: string) => {
-            if (subdomain.startsWith('https://') || subdomain.startsWith('http://')) {
-                return subdomain;
-            }
-            return 'https://' + subdomain;
-        }
+        let matchedWebCount = 0;
+        let matchedWebHost = "";
 
         for (const web of webServices) {
             const values = helmValues[Service.toHelmName(web)];
             if (values == null || values.ingress == null || !values.ingress.enabled) {
                 continue;
             }
-            if (values.ingress.custom_domain && values.ingress.hosts?.length > 0) {
-                return prefixSubdomain(values.ingress.hosts[0]);
-            }
-            if (values.ingress.porter_hosts?.length > 0) {
-                return prefixSubdomain(values.ingress.porter_hosts[0]);
+            if (values.ingress.porter_hosts?.length > 0 || (values.ingress.custom_domain && values.ingress.hosts?.length > 0)) {
+                if (values.ingress.custom_domain && values.ingress.hosts?.length > 0) {
+                    // if they have a custom domain, use that
+                    matchedWebHost = values.ingress.hosts[0];
+                } else {
+                    // otherwise, use their porter domain
+                    matchedWebHost = values.ingress.porter_hosts[0];
+                }
+                matchedWebCount++;
             }
         }
 
-        return "";
-    }
+        // if multiple web services have a subdomain, return nothing
+        if (matchedWebCount > 1) {
+            return "";
+        }
+
+        return matchedWebHost;
+    },
+
+    prefixSubdomain: (subdomain: string) => {
+        if (subdomain.startsWith('https://') || subdomain.startsWith('http://')) {
+            return subdomain;
+        }
+        return 'https://' + subdomain;
+    },
 }
 

+ 3 - 1
dashboard/src/main/home/app-dashboard/types/porterApp.ts

@@ -38,4 +38,6 @@ export const PorterApp = {
         ...app,
         ...values,
     }),
-}
+}
+
+export type BuildMethod = "docker" | "buildpacks";

+ 102 - 3
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepo.tsx

@@ -20,6 +20,8 @@ import AnimateHeight from "react-animate-height";
 import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import ConnectNewRepoActionConfEditor from "./ConnectNewRepoActionConfEditor";
+import VerticalSteps from "components/porter/VerticalSteps";
+import Back from "components/porter/Back";
 
 const ConnectNewRepo: React.FC = () => {
   const { currentProject, currentCluster, setCurrentError } = useContext(
@@ -34,6 +36,7 @@ const ConnectNewRepo: React.FC = () => {
   const [status, setStatus] = useState(null);
   const { pushFiltered } = useRouting();
   const [showSettings, setShowSettings] = useState<boolean>(false);
+  const [currentStep, setCurrentStep] = useState<number>(0);
 
   // NOTE: git_repo_id is a misnomer as this actually refers to the github app's installation id.
   const [actionConfig, setActionConfig] = useState<ActionConfigType>({
@@ -77,7 +80,7 @@ const ConnectNewRepo: React.FC = () => {
         });
         setFilteredRepos(newFilteredRepos || []);
       })
-      .catch(() => { });
+      .catch(() => {});
   }, []);
 
   useEffect(() => {
@@ -175,6 +178,94 @@ const ConnectNewRepo: React.FC = () => {
       });
   };
 
+  if (currentProject?.simplified_view_enabled) {
+    return (
+      <CenterWrapper>
+        <Div>
+          <Back to="/preview-environments" />
+          <DashboardHeader
+            image={PullRequestIcon}
+            title="Preview environments"
+            capitalize={false}
+            description="Create full-stack preview environments for your pull requests."
+          />
+          <VerticalSteps
+            currentStep={currentStep}
+            steps={[
+              <>
+                <Text size={16}>Choose a repository</Text>
+                <ConnectNewRepoActionConfEditor
+                  actionConfig={actionConfig}
+                  setActionConfig={(actionConfig: ActionConfigType) => {
+                    setActionConfig(
+                      (currentActionConfig: ActionConfigType) => ({
+                        ...currentActionConfig,
+                        ...actionConfig,
+                      })
+                    );
+
+                    if (!!actionConfig.git_repo) {
+                      setCurrentStep((prev) => {
+                        if (prev > 0) {
+                          return prev;
+                        }
+
+                        return prev + 1;
+                      });
+                    }
+                  }}
+                />
+                <HelperContainer>
+                  Note: you will need to add a{" "}
+                  <CodeBlock>porter.yaml</CodeBlock> file to create a preview
+                  environment.
+                  <DocsHelper
+                    disableMargin
+                    tooltipText="A Porter YAML file is a declarative set of resources that Porter uses to build and update your preview environment deployments."
+                    link="https://docs.porter.run/preview-environments/porter-yaml-reference"
+                  />
+                </HelperContainer>
+              </>,
+
+              <>
+                <Text size={16}>Automatic pull request deployments</Text>
+                <Helper style={{ marginTop: "10px", marginBottom: "10px" }}>
+                  If you enable this option, the new pull requests will be
+                  automatically deployed.
+                </Helper>
+                <CheckboxWrapper>
+                  <CheckboxRow
+                    label="Enable automatic deploys"
+                    checked={enableAutomaticDeployments}
+                    toggle={() => {
+                      setEnableAutomaticDeployments(
+                        !enableAutomaticDeployments
+                      );
+                    }}
+                    wrapperStyles={{
+                      disableMargin: true,
+                    }}
+                  />
+                </CheckboxWrapper>
+              </>,
+            ]}
+          />
+          <ActionContainer>
+            <SaveButton
+              text="Add repository"
+              disabled={actionConfig.git_repo_id ? false : true}
+              onClick={addRepo}
+              makeFlush={true}
+              clearPosition={true}
+              status={status}
+              statusPosition={"left"}
+            />
+          </ActionContainer>
+        </Div>
+      </CenterWrapper>
+    );
+  }
+
   return (
     <>
       <DashboardHeader
@@ -353,8 +444,16 @@ const ConnectNewRepo: React.FC = () => {
 
 export default ConnectNewRepo;
 
+const CenterWrapper = styled.div`
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+`;
+
 const Div = styled.div`
-  margin-bottom: -7px;
+  width: 100%;
+  max-width: 900px;
 `;
 
 const FlexWrap = styled.div`
@@ -509,7 +608,7 @@ const StyledAdvancedBuildSettings = styled.div`
     cursor: pointer;
     border-radius: 20px;
     transform: ${(props: { showSettings: boolean; isCurrent: boolean }) =>
-    props.showSettings ? "" : "rotate(-90deg)"};
+      props.showSettings ? "" : "rotate(-90deg)"};
   }
 `;
 const AdvancedBuildTitle = styled.div`

+ 57 - 57
dashboard/src/main/home/cluster-dashboard/preview-environments/ConnectNewRepoActionConfEditor.tsx

@@ -7,70 +7,70 @@ import Input from "components/porter/Input";
 import RepoList from "components/repo-selector/RepoList";
 
 type Props = {
-    actionConfig: ActionConfigType | null;
-    setActionConfig: (x: ActionConfigType) => void;
-    setBranch?: (x: string) => void;
-    setDockerfilePath?: (x: string) => void;
-    setFolderPath?: (x: string) => void;
-    setBuildView?: (x: string) => void;
-    setPorterYamlPath?: (x: string) => void;
+  actionConfig: ActionConfigType | null;
+  setActionConfig: (x: ActionConfigType) => void;
+  setBranch?: (x: string) => void;
+  setDockerfilePath?: (x: string) => void;
+  setFolderPath?: (x: string) => void;
+  setBuildView?: (x: string) => void;
+  setPorterYamlPath?: (x: string) => void;
 };
 
 const defaultActionConfig: ActionConfigType = {
-    git_repo: null,
-    image_repo_uri: null,
-    git_branch: null,
-    git_repo_id: 0,
-    kind: "github",
+  git_repo: null,
+  image_repo_uri: null,
+  git_branch: null,
+  git_repo_id: 0,
+  kind: "github",
 };
 
 const ConnectNewRepoActionConfEditor: React.FC<Props> = ({
-    actionConfig,
-    setBranch,
-    setActionConfig,
-    setFolderPath,
-    setDockerfilePath,
-    setBuildView,
-    setPorterYamlPath,
+  actionConfig,
+  setBranch,
+  setActionConfig,
+  setFolderPath,
+  setDockerfilePath,
+  setBuildView,
+  setPorterYamlPath,
 }) => {
-    if (!actionConfig.git_repo) {
-        return (
-            <ExpandedWrapperAlt>
-                <RepoList
-                    actionConfig={actionConfig}
-                    setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
-                    readOnly={false}
-                />
-            </ExpandedWrapperAlt>
-        );
-    } else {
-        return (
-            <>
-                <Input
-                    disabled={true}
-                    label="GitHub repository:"
-                    width="100%"
-                    value={actionConfig?.git_repo}
-                    setValue={() => { }}
-                    placeholder=""
-                />
-                <BackButton
-                    width="135px"
-                    onClick={() => {
-                        setActionConfig({ ...defaultActionConfig });
-                        setBranch ? setBranch("") : null;
-                        setFolderPath ? setFolderPath("") : null;
-                        setDockerfilePath ? setDockerfilePath("") : null;
-                        setBuildView ? setBuildView("buildpacks") : null;
-                        setPorterYamlPath("");
-                    }}
-                >
-                    <i className="material-icons">keyboard_backspace</i>
-                    Select repo
-                </BackButton>
-            </>
-        );
-    }
+  if (!actionConfig.git_repo) {
+    return (
+      <ExpandedWrapperAlt>
+        <RepoList
+          actionConfig={actionConfig}
+          setActionConfig={(x: ActionConfigType) => setActionConfig(x)}
+          readOnly={false}
+        />
+      </ExpandedWrapperAlt>
+    );
+  } else {
+    return (
+      <>
+        <Input
+          disabled={true}
+          label="GitHub repository:"
+          width="100%"
+          value={actionConfig?.git_repo}
+          setValue={() => {}}
+          placeholder=""
+        />
+        <BackButton
+          width="135px"
+          onClick={() => {
+            setActionConfig({ ...defaultActionConfig });
+            setBranch ? setBranch("") : null;
+            setFolderPath ? setFolderPath("") : null;
+            setDockerfilePath ? setDockerfilePath("") : null;
+            setBuildView ? setBuildView("buildpacks") : null;
+            setPorterYamlPath && setPorterYamlPath("");
+          }}
+        >
+          <i className="material-icons">keyboard_backspace</i>
+          Select repo
+        </BackButton>
+      </>
+    );
+  }
 };
 
 export default ConnectNewRepoActionConfEditor;

+ 32 - 6
dashboard/src/main/home/sidebar/Clusters.tsx

@@ -40,7 +40,7 @@ class Clusters extends Component<PropsType, StateType> {
 
   updateClusters = () => {
     if (!this.context.currentProject) {
-      return
+      return;
     }
     let {
       user,
@@ -143,10 +143,34 @@ class Clusters extends Component<PropsType, StateType> {
     });
   };
 
-  renderContents = (): JSX.Element[] | JSX.Element => {
+  renderContents = (): React.ReactNode => {
     let { clusters } = this.state;
     let { currentCluster, setCurrentCluster, currentProject } = this.context;
 
+    if (currentProject?.simplified_view_enabled ) {
+      const cluster = clusters[0];
+      return currentProject?.preview_envs_enabled && currentCluster?.preview_envs_enabled ? (
+        <NavButton
+          path="/preview-environments"
+          targetClusterName={cluster?.name}
+          active={
+            currentCluster?.id === cluster?.id &&
+            window.location.pathname.startsWith("/preview-environments")
+          }
+        >
+          <InlineSVGWrapper
+            id="Flat"
+            fill="#FFFFFF"
+            xmlns="http://www.w3.org/2000/svg"
+            viewBox="0 0 256 256"
+          >
+            <path d="M103.99951,68a36,36,0,1,0-44,35.0929v49.8142a36,36,0,1,0,16,0V103.0929A36.05516,36.05516,0,0,0,103.99951,68Zm-56,0a20,20,0,1,1,20,20A20.0226,20.0226,0,0,1,47.99951,68Zm40,120a20,20,0,1,1-20-20A20.0226,20.0226,0,0,1,87.99951,188ZM196.002,152.907l-.00146-33.02563a55.63508,55.63508,0,0,0-16.40137-39.59619L155.31348,56h20.686a8,8,0,0,0,0-16h-40c-.02978,0-.05859.00415-.08838.00446-.2334.00256-.46631.01245-.69824.03527-.12891.01258-.25391.03632-.38086.05494-.13135.01928-.26318.03424-.39355.06-.14014.02778-.27686.06611-.41455.10114-.11475.02924-.23047.05426-.34424.08862-.13428.04059-.26367.0907-.395.13806-.11524.04151-.231.07929-.34473.12629-.12109.05011-.23681.10876-.35449.16455-.11914.05621-.23926.10907-.356.17144-.11133.0597-.21728.12757-.32519.1922-.11621.06928-.23389.13483-.34668.21051-.11719.07831-.227.16553-.33985.24976-.09668.07227-.1958.1394-.28955.21655-.18652.1529-.36426.31531-.53564.48413-.01612.01593-.03418.02918-.05029.04529-.02051.02051-.0376.04321-.05762.06391-.16358.16711-.32178.33941-.47022.52032-.083.10059-.15527.20648-.23193.31006-.07861.10571-.16064.20862-.23438.3183-.08056.12072-.15087.24591-.2246.36993-.05958.1-.12208.19757-.17725.30036-.06787.12591-.125.25531-.18506.384-.05078.1084-.10547.21466-.15137.32568-.05127.12463-.09326.25189-.13867.37848-.04248.11987-.08887.238-.126.36047-.03857.12775-.06738.25757-.09912.38678-.03125.124-.06591.24622-.0913.37244-.02979.15088-.04786.30328-.06934.45544-.01465.10645-.03516.21094-.0459.31867q-.03955.39752-.04.79706V88a8,8,0,0,0,16,0V67.31378l24.28516,24.28485a39.73874,39.73874,0,0,1,11.71582,28.28321l.00146,33.02533a36.00007,36.00007,0,1,0,16-.00019ZM188.00244,208a20,20,0,1,1,20-20A20.0226,20.0226,0,0,1,188.00244,208Z" />
+          </InlineSVGWrapper>
+          Preview envs
+        </NavButton>
+      ) : null;
+    }
+
     if (
       clusters.length > 0 &&
       currentCluster &&
@@ -256,7 +280,9 @@ class Clusters extends Component<PropsType, StateType> {
   };
 
   render() {
-    return <Wrapper display={this.props.display}>{this.renderContents()}</Wrapper>;
+    return (
+      <Wrapper display={this.props.display}>{this.renderContents()}</Wrapper>
+    );
   }
 }
 
@@ -265,7 +291,7 @@ Clusters.contextType = Context;
 export default withRouter(Clusters);
 
 const Wrapper = styled.div<{ display: string }>`
-  display: ${props => props.display || ""};
+  display: ${(props) => props.display || ""};
 `;
 
 const InlineSVGWrapper = styled.svg`
@@ -324,7 +350,7 @@ const NavButton = styled(SidebarLink)`
   margin: 5px 15px;
   padding: 0 30px 2px 8px;
   font-size: 13px;
-  color: ${props => props.theme.text.primary};
+  color: ${(props) => props.theme.text.primary};
   cursor: ${(props: { disabled?: boolean }) =>
     props.disabled ? "not-allowed" : "pointer"};
 
@@ -340,4 +366,4 @@ const NavButton = styled(SidebarLink)`
     border-radius: 3px;
     margin-right: 10px;
   }
-`;
+`;

+ 0 - 1
dashboard/src/main/home/sidebar/Sidebar.tsx

@@ -228,7 +228,6 @@ class Sidebar extends Component<PropsType, StateType> {
 
           {/* Hacky workaround for setting currentCluster with legacy method */}
           <Clusters
-            display="none"
             setWelcome={this.props.setWelcome}
             currentView={currentView}
             isSelected={false}

+ 1 - 1
go.mod

@@ -76,7 +76,7 @@ require (
 	github.com/honeycombio/otel-launcher-go v0.2.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.0.68
+	github.com/porter-dev/api-contracts v0.0.70
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 2
go.sum

@@ -1490,8 +1490,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.0.68 h1:8RZBu7ZImjf0KTao7NPAfB0FQ2cjwGE6sz5SqbutLDM=
-github.com/porter-dev/api-contracts v0.0.68/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
+github.com/porter-dev/api-contracts v0.0.70 h1:7Qj+wCehQDhAQyqoTNDeJXlKXYPzPenceR98ygTz2oo=
+github.com/porter-dev/api-contracts v0.0.70/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935 h1:hfb3nt3AJXIBbevu6ARTg9SdOkMP6WLbKBiG5hT5rcc=
 github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=